Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(language-service): Omit typechecking for finding directives #32156

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
102 changes: 42 additions & 60 deletions packages/language-service/src/typescript_host.ts
Expand Up @@ -14,8 +14,8 @@ import {AstResult, TemplateInfo} from './common';
import {createLanguageService} from './language_service';
import {ReflectorHost} from './reflector_host';
import {ExternalTemplate, InlineTemplate, getClassDeclFromTemplateNode} from './template';
import {Declaration, DeclarationError, Declarations, Diagnostic, DiagnosticKind, DiagnosticMessageChain, LanguageService, LanguageServiceHost, Span, TemplateSource} from './types';
import {findTighestNode} from './utils';
import {Declaration, DeclarationError, Diagnostic, DiagnosticKind, DiagnosticMessageChain, LanguageService, LanguageServiceHost, Span, TemplateSource} from './types';
import {findTightestNode, getDirectiveClassLike} from './utils';

/**
* Create a `LanguageServiceHost`
Expand Down Expand Up @@ -194,24 +194,50 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
return results;
}

getDeclarations(fileName: string): Declarations {
/**
* Return metadata about all class declarations in the file that are Angular
* directives. Potential matches are `@NgModule`, `@Component`, `@Directive`,
* `@Pipes`, etc. class declarations.
*
* @param fileName TS file
*/
getDeclarations(fileName: string): Declaration[] {
if (!fileName.endsWith('.ts')) {
return [];
}
const result: Declarations = [];
const sourceFile = this.getSourceFile(fileName);
if (sourceFile) {
const visit = (child: ts.Node) => {
const declaration = this.getDeclarationFromNode(sourceFile, child);
if (declaration) {
result.push(declaration);
} else {
ts.forEachChild(child, visit);
}
};
ts.forEachChild(sourceFile, visit);
if (!sourceFile) {
return [];
}
return result;
const results: Declaration[] = [];
const visit = (child: ts.Node) => {
const candidate = getDirectiveClassLike(child);
if (candidate) {
const {decoratorId, classDecl} = candidate;
const declarationSpan = spanOf(decoratorId);
const className = classDecl.name !.text;
const classSymbol = this.reflector.getStaticSymbol(sourceFile.fileName, className);
// Ask the resolver to check if candidate is actually Angular directive
if (!this.resolver.isDirective(classSymbol)) {
return;
}
const data = this.resolver.getNonNormalizedDirectiveMetadata(classSymbol);
if (!data) {
return;
}
results.push({
type: classSymbol,
declarationSpan,
metadata: data.metadata,
errors: this.getCollectedErrors(declarationSpan, sourceFile),
});
} else {
child.forEachChild(visit);
}
};
ts.forEachChild(sourceFile, visit);

return results;
}

getSourceFile(fileName: string): ts.SourceFile|undefined {
Expand Down Expand Up @@ -277,7 +303,6 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
* class AppComponent {}
* ^---- class declaration node
*
*
* @param node Potential template node
*/
private getInternalTemplate(node: ts.Node): TemplateSource|undefined {
Expand Down Expand Up @@ -355,49 +380,6 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
});
}

private getDeclarationFromNode(sourceFile: ts.SourceFile, node: ts.Node): Declaration|undefined {
if (node.kind == ts.SyntaxKind.ClassDeclaration && node.decorators &&
(node as ts.ClassDeclaration).name) {
for (const decorator of node.decorators) {
if (decorator.expression && decorator.expression.kind == ts.SyntaxKind.CallExpression) {
const classDeclaration = node as ts.ClassDeclaration;
if (classDeclaration.name) {
const call = decorator.expression as ts.CallExpression;
const target = call.expression;
const type = this.program.getTypeChecker().getTypeAtLocation(target);
if (type) {
const staticSymbol =
this.reflector.getStaticSymbol(sourceFile.fileName, classDeclaration.name.text);
try {
if (this.resolver.isDirective(staticSymbol as any)) {
const {metadata} =
this.resolver.getNonNormalizedDirectiveMetadata(staticSymbol as any) !;
const declarationSpan = spanOf(target);
return {
type: staticSymbol,
declarationSpan,
metadata,
errors: this.getCollectedErrors(declarationSpan, sourceFile)
};
}
} catch (e) {
if (e.message) {
this.collectError(e, sourceFile.fileName);
const declarationSpan = spanOf(target);
return {
type: staticSymbol,
declarationSpan,
errors: this.getCollectedErrors(declarationSpan, sourceFile)
};
}
}
}
}
}
}
}
}

/**
* Return the parsed template for the template at the specified `position`.
* @param fileName TS or HTML file
Expand All @@ -411,7 +393,7 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
return;
}
// Find the node that most closely matches the position
const node = findTighestNode(sourceFile, position);
const node = findTightestNode(sourceFile, position);
if (!node) {
return;
}
Expand Down
47 changes: 45 additions & 2 deletions packages/language-service/src/utils.ts
Expand Up @@ -183,8 +183,51 @@ export function findTemplateAstAt(
* @param node
* @param position
*/
export function findTighestNode(node: ts.Node, position: number): ts.Node|undefined {
export function findTightestNode(node: ts.Node, position: number): ts.Node|undefined {
if (node.getStart() <= position && position < node.getEnd()) {
return node.forEachChild(c => findTighestNode(c, position)) || node;
return node.forEachChild(c => findTightestNode(c, position)) || node;
}
}

interface DirectiveClassLike {
decoratorId: ts.Identifier; // decorator identifier
classDecl: ts.ClassDeclaration;
}

/**
* Return metadata about `node` if it looks like an Angular directive class.
* In this case, potential matches are `@NgModule`, `@Component`, `@Directive`,
* `@Pipe`, etc.
* These class declarations all share some common attributes, namely their
* decorator takes exactly one parameter and the parameter must be an object
* literal.
*
* For example,
* v---------- `decoratorId`
* @NgModule({
* declarations: [],
* })
* class AppModule {}
* ^----- `classDecl`
*
* @param node Potential node that represents an Angular directive.
*/
export function getDirectiveClassLike(node: ts.Node): DirectiveClassLike|undefined {
if (!ts.isClassDeclaration(node) || !node.name || !node.decorators) {
return;
}
for (const d of node.decorators) {
const expr = d.expression;
if (!ts.isCallExpression(expr) || expr.arguments.length !== 1 ||
!ts.isIdentifier(expr.expression)) {
continue;
}
const arg = expr.arguments[0];
if (ts.isObjectLiteralExpression(arg)) {
return {
decoratorId: expr.expression,
classDecl: node,
};
}
}
}
21 changes: 20 additions & 1 deletion packages/language-service/test/template_spec.ts
Expand Up @@ -12,7 +12,26 @@ import {toh} from './test_data';
import {MockTypescriptHost} from './test_utils';

describe('getClassDeclFromTemplateNode', () => {
it('should return class declaration', () => {

it('should find class declaration in syntax-only mode', () => {
const sourceFile = ts.createSourceFile(
'foo.ts', `
@Component({
template: '<div></div>'
})
class MyComponent {}`,
ts.ScriptTarget.ES2015, true /* setParentNodes */);
function visit(node: ts.Node): ts.ClassDeclaration|undefined {
return getClassDeclFromTemplateNode(node) || node.forEachChild(visit);
}
const classDecl = sourceFile.forEachChild(visit);
expect(classDecl).toBeTruthy();
expect(classDecl !.kind).toBe(ts.SyntaxKind.ClassDeclaration);
expect((classDecl as ts.ClassDeclaration).name !.text).toBe('MyComponent');
});


it('should return class declaration for AppComponent', () => {
const host = new MockTypescriptHost(['/app/app.component.ts'], toh);
const tsLS = ts.createLanguageService(host);
const sourceFile = tsLS.getProgram() !.getSourceFile('/app/app.component.ts');
Expand Down
34 changes: 34 additions & 0 deletions packages/language-service/test/utils_spec.ts
@@ -0,0 +1,34 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import * as ts from 'typescript';
import {getDirectiveClassLike} from '../src/utils';

describe('getDirectiveClassLike()', () => {
it('should return a directive class', () => {
const sourceFile = ts.createSourceFile(
'foo.ts', `
@NgModule({
declarations: [],
})
class AppModule {}
`,
ts.ScriptTarget.ES2015);
const result = sourceFile.forEachChild(c => {
const directive = getDirectiveClassLike(c);
if (directive) {
return directive;
}
});
expect(result).toBeTruthy();
const {decoratorId, classDecl} = result !;
expect(decoratorId.kind).toBe(ts.SyntaxKind.Identifier);
expect((decoratorId as ts.Identifier).text).toBe('NgModule');
expect(classDecl.name !.text).toBe('AppModule');
});
});