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:
parent
b50ed5c22c
commit
0223aa6121
|
@ -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)) {
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue