fix(language-service): Diagnostic span should point to class name (#34932)

Right now, if an Angular diagnostic is generated for a TypeScript node,
the span points to the decorator Identifier, i.e. the Identifier node
like `@NgModule`, `@Component`, etc.
This is weird. It should point to the class name instead.
Note, we do not have a more fine-grained breakdown of the span when
diagnostics are emitted, this work remains to be done.

PR Close #34932
This commit is contained in:
Keen Yee Liau 2020-01-23 12:02:47 -08:00 committed by Andrew Kushnir
parent b50ed5c22c
commit 0223aa6121
4 changed files with 18 additions and 24 deletions

View File

@ -288,9 +288,9 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
const visit = (child: tss.Node) => { const visit = (child: tss.Node) => {
const candidate = getDirectiveClassLike(child); const candidate = getDirectiveClassLike(child);
if (candidate) { if (candidate) {
const {decoratorId, classDecl} = candidate; const {classId} = candidate;
const declarationSpan = spanOf(decoratorId); const declarationSpan = spanOf(classId);
const className = classDecl.name !.text; const className = classId.getText();
const classSymbol = this.reflector.getStaticSymbol(sourceFile.fileName, className); const classSymbol = this.reflector.getStaticSymbol(sourceFile.fileName, className);
// Ask the resolver to check if candidate is actually Angular directive // Ask the resolver to check if candidate is actually Angular directive
if (!this.resolver.isDirective(classSymbol)) { if (!this.resolver.isDirective(classSymbol)) {

View File

@ -164,8 +164,8 @@ export function findTightestNode(node: ts.Node, position: number): ts.Node|undef
} }
interface DirectiveClassLike { interface DirectiveClassLike {
decoratorId: ts.Identifier; // decorator identifier decoratorId: ts.Identifier; // decorator identifier, like @Component
classDecl: ts.ClassDeclaration; classId: ts.Identifier;
} }
/** /**
@ -178,11 +178,11 @@ interface DirectiveClassLike {
* *
* For example, * For example,
* v---------- `decoratorId` * v---------- `decoratorId`
* @NgModule({ * @NgModule({ <
* declarations: [], * declarations: [], < classDecl
* }) * }) <
* class AppModule {} * class AppModule {} <
* ^----- `classDecl` * ^----- `classId`
* *
* @param node Potential node that represents an Angular directive. * @param node Potential node that represents an Angular directive.
*/ */
@ -200,7 +200,7 @@ export function getDirectiveClassLike(node: ts.Node): DirectiveClassLike|undefin
if (ts.isObjectLiteralExpression(arg)) { if (ts.isObjectLiteralExpression(arg)) {
return { return {
decoratorId: expr.expression, decoratorId: expr.expression,
classDecl: node, classId: node.name,
}; };
} }
} }

View File

@ -354,9 +354,7 @@ describe('diagnostics', () => {
.toBe( .toBe(
`Component 'MyComponent' is not included in a module and will not be available inside a template. Consider adding it to a NgModule declaration.`); `Component 'MyComponent' is not included in a module and will not be available inside a template. Consider adding it to a NgModule declaration.`);
const content = mockHost.readFile(fileName) !; const content = mockHost.readFile(fileName) !;
const keyword = '@Component'; expect(content.substring(start !, start ! + length !)).toBe('MyComponent');
expect(start).toBe(content.lastIndexOf(keyword) + 1); // exclude leading '@'
expect(length).toBe(keyword.length - 1); // exclude leading '@'
}); });
@ -596,9 +594,7 @@ describe('diagnostics', () => {
.toBe( .toBe(
'Invalid providers for "AppComponent in /app/app.component.ts" - only instances of Provider and Type are allowed, got: [?null?]'); 'Invalid providers for "AppComponent in /app/app.component.ts" - only instances of Provider and Type are allowed, got: [?null?]');
// TODO: Looks like this is the wrong span. Should point to 'null' instead. // TODO: Looks like this is the wrong span. Should point to 'null' instead.
const keyword = '@Component'; expect(content.substring(start !, start ! + length !)).toBe('AppComponent');
expect(start).toBe(content.lastIndexOf(keyword) + 1); // exclude leading '@'
expect(length).toBe(keyword.length - 1); // exclude leading '@
}); });
// Issue #15768 // Issue #15768
@ -767,8 +763,7 @@ describe('diagnostics', () => {
const {file, messageText, start, length} = diags[0]; const {file, messageText, start, length} = diags[0];
expect(file !.fileName).toBe(APP_COMPONENT); expect(file !.fileName).toBe(APP_COMPONENT);
expect(messageText).toBe(`Component 'AppComponent' must have a template or templateUrl`); expect(messageText).toBe(`Component 'AppComponent' must have a template or templateUrl`);
expect(start).toBe(content.indexOf(`@Component`) + 1); expect(content.substring(start !, start ! + length !)).toBe('AppComponent');
expect(length).toBe('Component'.length);
}); });
it('should report diagnostic for both template and templateUrl', () => { it('should report diagnostic for both template and templateUrl', () => {
@ -787,8 +782,7 @@ describe('diagnostics', () => {
expect(file !.fileName).toBe(APP_COMPONENT); expect(file !.fileName).toBe(APP_COMPONENT);
expect(messageText) expect(messageText)
.toBe(`Component 'AppComponent' must not have both template and templateUrl`); .toBe(`Component 'AppComponent' must not have both template and templateUrl`);
expect(start).toBe(content.indexOf(`@Component`) + 1); expect(content.substring(start !, start ! + length !)).toBe('AppComponent');
expect(length).toBe('Component'.length);
}); });
it('should report errors for invalid styleUrls', () => { it('should report errors for invalid styleUrls', () => {

View File

@ -28,10 +28,10 @@ describe('getDirectiveClassLike()', () => {
} }
}); });
expect(result).toBeTruthy(); expect(result).toBeTruthy();
const {decoratorId, classDecl} = result !; const {decoratorId, classId} = result !;
expect(decoratorId.kind).toBe(ts.SyntaxKind.Identifier); expect(decoratorId.kind).toBe(ts.SyntaxKind.Identifier);
expect((decoratorId as ts.Identifier).text).toBe('NgModule'); expect(decoratorId.text).toBe('NgModule');
expect(classDecl.name !.text).toBe('AppModule'); expect(classId.text).toBe('AppModule');
}); });
}); });