Skip to content

Commit 33ef9cc

Browse files
authored
Merge pull request #53 from typed-ember/handle-errors
2 parents e06775b + b5b6734 commit 33ef9cc

File tree

11 files changed

+233
-89
lines changed

11 files changed

+233
-89
lines changed

.eslintrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"@typescript-eslint/no-non-null-assertion": "off",
3434
"@typescript-eslint/ban-types": "off",
3535
"@typescript-eslint/no-var-requires": "off",
36+
"@typescript-eslint/ban-ts-comment": ["error", { "ts-ignore": "allow-with-description" }],
3637
"@typescript-eslint/explicit-function-return-type": [
3738
"error",
3839
{

packages/core/__tests__/cli/declaration.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Project from '../utils/project';
44
describe('CLI: emitting declarations', () => {
55
let project!: Project;
66
beforeEach(async () => {
7+
jest.setTimeout(20_000);
78
project = await Project.create();
89
});
910

packages/core/__tests__/language-server/definitions.test.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,12 +155,16 @@ describe('Language Server: Definitions', () => {
155155
character: 27,
156156
});
157157

158-
expect(definitions).toEqual([
158+
expect(definitions).toMatchObject([
159159
{
160160
uri: project.fileURI('greeting.ts'),
161+
162+
// Versions of TS vary on whether they consider the source to be
163+
// the entire module or just the first character, so we'll consider
164+
// the test passing as long as the loose shape is right.
161165
range: {
162166
start: { line: 0, character: 0 },
163-
end: { line: 8, character: 1 },
167+
end: {},
164168
},
165169
},
166170
]);
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import {
2+
Connection,
3+
ServerCapabilities,
4+
TextDocuments,
5+
TextDocumentSyncKind,
6+
} from 'vscode-languageserver';
7+
import { TextDocument } from 'vscode-languageserver-textdocument';
8+
import GlintLanguageServer from './glint-language-server';
9+
import { debounce } from '../common/scheduling';
10+
11+
export const capabilities: ServerCapabilities = {
12+
textDocumentSync: TextDocumentSyncKind.Full,
13+
completionProvider: {
14+
resolveProvider: true,
15+
},
16+
referencesProvider: true,
17+
hoverProvider: true,
18+
definitionProvider: true,
19+
workspaceSymbolProvider: true,
20+
renameProvider: {
21+
prepareProvider: true,
22+
},
23+
};
24+
25+
export type BindingArgs = {
26+
languageServer: GlintLanguageServer;
27+
documents: TextDocuments<TextDocument>;
28+
connection: Connection;
29+
};
30+
31+
export function bindLanguageServer(args: BindingArgs): void {
32+
let { connection, languageServer, documents } = args;
33+
let { scheduleDiagnostics, captureErrors } = buildHelpers(args);
34+
35+
connection.onInitialize(() => ({ capabilities }));
36+
37+
documents.onDidOpen(({ document }) => {
38+
languageServer.openFile(document.uri, document.getText());
39+
scheduleDiagnostics();
40+
});
41+
42+
documents.onDidClose(({ document }) => {
43+
languageServer.closeFile(document.uri);
44+
});
45+
46+
documents.onDidChangeContent(({ document }) => {
47+
languageServer.updateFile(document.uri, document.getText());
48+
scheduleDiagnostics();
49+
});
50+
51+
connection.onPrepareRename(({ textDocument, position }) => {
52+
return captureErrors(() => languageServer.prepareRename(textDocument.uri, position));
53+
});
54+
55+
connection.onRenameRequest(({ textDocument, position, newName }) => {
56+
return captureErrors(() =>
57+
languageServer.getEditsForRename(textDocument.uri, position, newName)
58+
);
59+
});
60+
61+
connection.onCompletion(({ textDocument, position }) => {
62+
return captureErrors(() => languageServer.getCompletions(textDocument.uri, position));
63+
});
64+
65+
connection.onCompletionResolve((item) => {
66+
return captureErrors(() => languageServer.getCompletionDetails(item)) ?? item;
67+
});
68+
69+
connection.onHover(({ textDocument, position }) => {
70+
return captureErrors(() => languageServer.getHover(textDocument.uri, position));
71+
});
72+
73+
connection.onDefinition(({ textDocument, position }) => {
74+
return captureErrors(() => languageServer.getDefinition(textDocument.uri, position));
75+
});
76+
77+
connection.onReferences(({ textDocument, position }) => {
78+
return captureErrors(() => languageServer.getReferences(textDocument.uri, position));
79+
});
80+
81+
connection.onWorkspaceSymbol(({ query }) => {
82+
return captureErrors(() => languageServer.findSymbols(query));
83+
});
84+
85+
connection.onDidChangeWatchedFiles(() => {
86+
// TODO: use this to synchronize files that aren't open so we don't assume changes only
87+
// happen in the editor.
88+
});
89+
}
90+
91+
type BindingHelpers = {
92+
scheduleDiagnostics: () => void;
93+
captureErrors: <T>(callback: () => T) => T | undefined;
94+
};
95+
96+
function buildHelpers({ languageServer, documents, connection }: BindingArgs): BindingHelpers {
97+
return {
98+
scheduleDiagnostics: debounce(250, () => {
99+
for (let { uri } of documents.all()) {
100+
const diagnostics = languageServer.getDiagnostics(uri);
101+
connection.sendDiagnostics({ uri, diagnostics });
102+
}
103+
}),
104+
105+
captureErrors(callback) {
106+
try {
107+
return callback();
108+
} catch (error) {
109+
connection.console.error(error.stack ?? error);
110+
}
111+
},
112+
};
113+
}

packages/core/src/language-server/glint-language-server.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,9 @@ export default class GlintLanguageServer {
151151
label,
152152
{},
153153
source,
154-
{}
154+
{},
155+
// @ts-ignore: 4.3 adds a new not-optional-but-can-be-undefined `data` arg
156+
undefined
155157
);
156158

157159
if (!details) {
@@ -284,9 +286,10 @@ export default class GlintLanguageServer {
284286
textSpan.start + textSpan.length
285287
);
286288

287-
// Zero-length spans correspond to a synthetic use of the symbol, so we don't
288-
// want to actually show them to the user.
289-
if (originalStart === originalEnd) return;
289+
// If our calculated original span is zero-length but the transformed span
290+
// does take up space, this corresponds to a synthetic usage we generated
291+
// in the transformed output, and we don't want to show it to the user.
292+
if (originalStart === originalEnd && textSpan.length > 0) return;
290293

291294
let originalContents = this.documents.getDocumentContents(originalFileName);
292295
let start = offsetToPosition(originalContents, originalStart);

packages/core/src/language-server/index.ts

Lines changed: 4 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { TextDocuments, TextDocumentSyncKind, createConnection } from 'vscode-languageserver/node';
1+
import { TextDocuments, createConnection } from 'vscode-languageserver/node';
22
import { TextDocument } from 'vscode-languageserver-textdocument';
33
import { findConfig } from '@glint/config';
44
import { loadTypeScript } from '../common/load-typescript';
55
import GlintLanguageServer from './glint-language-server';
66
import { parseConfigFile, uriToFilePath } from './util';
7-
import { debounce } from '../common/scheduling';
7+
import { bindLanguageServer } from './binding';
88

99
const connection = createConnection(process.stdin, process.stdout);
1010
const documents = new TextDocuments(TextDocument);
@@ -19,85 +19,9 @@ const getRootFileNames = (): Array<string> => {
1919
};
2020

2121
if (glintConfig) {
22-
const gls = new GlintLanguageServer(ts, glintConfig, getRootFileNames, options);
22+
const languageServer = new GlintLanguageServer(ts, glintConfig, getRootFileNames, options);
2323

24-
connection.onInitialize(() => ({
25-
capabilities: {
26-
textDocumentSync: TextDocumentSyncKind.Full,
27-
completionProvider: {
28-
resolveProvider: true,
29-
},
30-
referencesProvider: true,
31-
hoverProvider: true,
32-
definitionProvider: true,
33-
workspaceSymbolProvider: true,
34-
renameProvider: {
35-
prepareProvider: true,
36-
},
37-
},
38-
}));
39-
40-
const scheduleDiagnostics = debounce(250, () => {
41-
for (let { uri } of documents.all()) {
42-
connection.sendDiagnostics({
43-
uri,
44-
diagnostics: gls.getDiagnostics(uri),
45-
});
46-
}
47-
});
48-
49-
connection.onDidChangeWatchedFiles(() => {
50-
// TODO: use this to synchronize files that aren't open so we don't assume changes only
51-
// happen in the editor.
52-
});
53-
54-
documents.onDidOpen(({ document }) => {
55-
gls.openFile(document.uri, document.getText());
56-
57-
scheduleDiagnostics();
58-
});
59-
60-
documents.onDidClose(({ document }) => {
61-
gls.closeFile(document.uri);
62-
});
63-
64-
documents.onDidChangeContent(({ document }) => {
65-
gls.updateFile(document.uri, document.getText());
66-
67-
scheduleDiagnostics();
68-
});
69-
70-
connection.onPrepareRename(({ textDocument, position }) => {
71-
return gls.prepareRename(textDocument.uri, position);
72-
});
73-
74-
connection.onRenameRequest(({ textDocument, position, newName }) => {
75-
return gls.getEditsForRename(textDocument.uri, position, newName);
76-
});
77-
78-
connection.onCompletion(({ textDocument, position }) => {
79-
return gls.getCompletions(textDocument.uri, position);
80-
});
81-
82-
connection.onCompletionResolve((item) => {
83-
return gls.getCompletionDetails(item);
84-
});
85-
86-
connection.onHover(({ textDocument, position }) => {
87-
return gls.getHover(textDocument.uri, position);
88-
});
89-
90-
connection.onDefinition(({ textDocument, position }) => {
91-
return gls.getDefinition(textDocument.uri, position);
92-
});
93-
94-
connection.onReferences(({ textDocument, position }) => {
95-
return gls.getReferences(textDocument.uri, position);
96-
});
97-
98-
connection.onWorkspaceSymbol(({ query }) => {
99-
return gls.findSymbols(query);
100-
});
24+
bindLanguageServer({ languageServer, documents, connection });
10125

10226
documents.listen(connection);
10327
connection.listen();

packages/transform/__tests__/rewrite.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,28 @@ describe('rewriteModule', () => {
9999
}"
100100
`);
101101
});
102+
103+
test('with a syntax error', () => {
104+
let script = {
105+
filename: 'test.ts',
106+
contents: stripIndent`
107+
import Component, { hbs } from '@glimmerx/component';
108+
export default class MyComponent extends Component {
109+
static template = hbs\`
110+
{{hello
111+
\`;
112+
}
113+
`,
114+
};
115+
116+
let transformedModule = rewriteModule({ script }, glimmerxEnvironment);
117+
118+
expect(transformedModule?.errors.length).toBe(1);
119+
expect(transformedModule?.transformedContents).toBe(script.contents);
120+
121+
expect(transformedModule?.getOriginalOffset(100)).toEqual({ offset: 100, source: script });
122+
expect(transformedModule?.getTransformedOffset(script.filename, 100)).toEqual(100);
123+
});
102124
});
103125

104126
describe('standalone companion template', () => {
@@ -284,5 +306,34 @@ describe('rewriteModule', () => {
284306
}"
285307
`);
286308
});
309+
310+
test('with a syntax error', () => {
311+
let script = {
312+
filename: 'test.ts',
313+
contents: stripIndent`
314+
import Component from '@glimmer/component';
315+
export default class MyComponent extends Component {
316+
}
317+
`,
318+
};
319+
320+
let template = {
321+
filename: 'test.hbs',
322+
contents: stripIndent`
323+
{{hello
324+
`,
325+
};
326+
327+
let transformedModule = rewriteModule({ script, template }, emberLooseEnvironment);
328+
329+
expect(transformedModule?.errors.length).toBe(1);
330+
expect(transformedModule?.transformedContents).toBe(script.contents);
331+
332+
expect(transformedModule?.getOriginalOffset(50)).toEqual({ offset: 50, source: script });
333+
expect(transformedModule?.getTransformedOffset(script.filename, 50)).toEqual(50);
334+
expect(transformedModule?.getTransformedOffset(template.filename, 5)).toEqual(
335+
script.contents.lastIndexOf('}')
336+
);
337+
});
287338
});
288339
});

packages/transform/__tests__/template-to-typescript.test.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -660,7 +660,7 @@ describe('rewriteTemplate', () => {
660660
});
661661

662662
describe('error conditions', () => {
663-
test('Syntax error', () => {
663+
test('Handlebars syntax error', () => {
664664
let { errors } = templateToTypescript('<Foo @attr={{"123}} />', {
665665
typesPath: '@glint/template',
666666
});
@@ -678,6 +678,20 @@ describe('rewriteTemplate', () => {
678678
]);
679679
});
680680

681+
test('HTML syntax error', () => {
682+
let { errors } = templateToTypescript('<Foo </Foo>', {
683+
typesPath: '@glint/template',
684+
});
685+
686+
expect(errors).toEqual([
687+
{
688+
location: { start: 5, end: 5 },
689+
message:
690+
"< is not a valid character within attribute names: (error occurred in 'an unknown module' @ line 1 : column 5)",
691+
},
692+
]);
693+
});
694+
681695
test('{{yield}} in expression position', () => {
682696
let { errors } = templateToTypescript('<Foo @attr={{yield}} />', {
683697
typesPath: '@glint/template',

packages/transform/src/inlining/companion-file.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { NodePath, types as t } from '@babel/core';
22
import { GlintEnvironment } from '@glint/config';
33
import { CorrelatedSpansResult, getContainingTypeInfo, PartialCorrelatedSpan } from '.';
4+
import MappingTree, { ParseError } from '../mapping-tree';
45
import { templateToTypescript } from '../template-to-typescript';
56
import { SourceFile, TransformError } from '../transformed-module';
67
import { assert } from '../util';
@@ -96,6 +97,22 @@ export function calculateCompanionTemplateSpans(
9697
transformedSource: `;\n`,
9798
}
9899
);
100+
} else {
101+
let mapping = new MappingTree(
102+
{ start: 0, end: 0 },
103+
{ start: 0, end: template.contents.length },
104+
[],
105+
new ParseError()
106+
);
107+
108+
partialSpans.push({
109+
originalFile: template,
110+
originalStart: 0,
111+
originalLength: template.contents.length,
112+
insertionPoint: target.end - 1,
113+
transformedSource: '',
114+
mapping,
115+
});
99116
}
100117
} else {
101118
// TODO: handle opaque expression like an imported identifier or `templateOnlyComponent()`

0 commit comments

Comments
 (0)