fix(compiler-cli): return directives for an element on a microsyntax template (#42640)

When the template type checker try to get a symbol of a template node, it will
not return the directives intended for an element on a microsyntax template,
for example, `<div *ngFor="let user of users;" dir>`, the `dir` will be skipped,
but it's needed in language service.

Fixes https://github.com/angular/vscode-ng-language-service/issues/1420

PR Close #42640
This commit is contained in:
ivanwonder 2021-06-24 10:28:42 +08:00 committed by Jessica Janiuk
parent bfa1b5d9eb
commit 74350a5cf1
2 changed files with 47 additions and 4 deletions

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AST, ASTWithSource, BindingPipe, MethodCall, PropertyRead, PropertyWrite, SafeMethodCall, SafePropertyRead, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstTextAttribute, TmplAstVariable} from '@angular/compiler'; import {AST, ASTWithSource, BindingPipe, MethodCall, ParseSourceSpan, PropertyRead, PropertyWrite, SafeMethodCall, SafePropertyRead, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstTextAttribute, TmplAstVariable} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../file_system'; import {AbsoluteFsPath} from '../../file_system';
@ -150,7 +150,24 @@ export class SymbolBuilder {
private getDirectiveMeta( private getDirectiveMeta(
host: TmplAstTemplate|TmplAstElement, host: TmplAstTemplate|TmplAstElement,
directiveDeclaration: ts.Declaration): TypeCheckableDirectiveMeta|null { directiveDeclaration: ts.Declaration): TypeCheckableDirectiveMeta|null {
const directives = this.templateData.boundTarget.getDirectivesOfNode(host); let directives = this.templateData.boundTarget.getDirectivesOfNode(host);
// `getDirectivesOfNode` will not return the directives intended for an element
// on a microsyntax template, for example `<div *ngFor="let user of users;" dir>`,
// the `dir` will be skipped, but it's needed in language service.
const firstChild = host.children[0];
if (firstChild instanceof TmplAstElement) {
const isMicrosyntaxTemplate = host instanceof TmplAstTemplate &&
sourceSpanEqual(firstChild.sourceSpan, host.sourceSpan);
if (isMicrosyntaxTemplate) {
const firstChildDirectives = this.templateData.boundTarget.getDirectivesOfNode(firstChild);
if (firstChildDirectives !== null && directives !== null) {
directives = directives.concat(firstChildDirectives);
} else {
directives = directives ?? firstChildDirectives;
}
}
}
if (directives === null) { if (directives === null) {
return null; return null;
} }
@ -577,3 +594,7 @@ export class SymbolBuilder {
function anyNodeFilter(n: ts.Node): n is ts.Node { function anyNodeFilter(n: ts.Node): n is ts.Node {
return true; return true;
} }
function sourceSpanEqual(a: ParseSourceSpan, b: ParseSourceSpan) {
return a.start.offset === b.start.offset && a.end.offset === b.end.offset;
}

View File

@ -223,8 +223,9 @@ runInEachFileSystem(() => {
beforeEach(() => { beforeEach(() => {
const fileName = absoluteFrom('/main.ts'); const fileName = absoluteFrom('/main.ts');
const dirFile = absoluteFrom('/dir.ts');
const templateString = ` const templateString = `
<div *ngFor="let user of users; let i = index;"> <div *ngFor="let user of users; let i = index;" dir>
{{user.name}} {{user.streetNumber}} {{user.name}} {{user.streetNumber}}
<div [tabIndex]="i"></div> <div [tabIndex]="i"></div>
</div>`; </div>`;
@ -239,9 +240,23 @@ runInEachFileSystem(() => {
} }
export class Cmp { users: User[]; } export class Cmp { users: User[]; }
`, `,
declarations: [ngForDeclaration()], declarations: [
ngForDeclaration(),
{
name: 'TestDir',
selector: '[dir]',
file: dirFile,
type: 'directive',
inputs: {name: 'name'}
},
],
}, },
ngForTypeCheckTarget(), ngForTypeCheckTarget(),
{
fileName: dirFile,
source: `export class TestDir {name:string}`,
templates: {},
},
]); ]);
templateTypeChecker = testValues.templateTypeChecker; templateTypeChecker = testValues.templateTypeChecker;
program = testValues.program; program = testValues.program;
@ -250,6 +265,13 @@ runInEachFileSystem(() => {
templateNode = getAstTemplates(templateTypeChecker, cmp)[0]; templateNode = getAstTemplates(templateTypeChecker, cmp)[0];
}); });
it('should retrieve a symbol for a directive on a microsyntax template', () => {
const symbol = templateTypeChecker.getSymbolOfNode(templateNode, cmp);
const testDir = symbol?.directives.find(dir => dir.selector === '[dir]');
expect(testDir).toBeDefined();
expect(program.getTypeChecker().symbolToString(testDir!.tsSymbol)).toEqual('TestDir');
});
it('should retrieve a symbol for an expression inside structural binding', () => { it('should retrieve a symbol for an expression inside structural binding', () => {
const ngForOfBinding = const ngForOfBinding =
templateNode.templateAttrs.find(a => a.name === 'ngForOf')! as TmplAstBoundAttribute; templateNode.templateAttrs.find(a => a.name === 'ngForOf')! as TmplAstBoundAttribute;