refactor(compiler-cli): introduce APIs to support directive autocompletion (#40032)
This commit adds two new APIs to the `TemplateTypeChecker`: `getPotentialDomBindings` and `getDirectiveMetadata`. Together, these will support the Language Service in performing autocompletion of directive inputs/outputs. PR Close #40032
This commit is contained in:
parent
a543e69497
commit
c0ab43f3c8
|
@ -805,7 +805,7 @@ export class NgCompiler {
|
||||||
const templateTypeChecker = new TemplateTypeCheckerImpl(
|
const templateTypeChecker = new TemplateTypeCheckerImpl(
|
||||||
this.tsProgram, this.typeCheckingProgramStrategy, traitCompiler,
|
this.tsProgram, this.typeCheckingProgramStrategy, traitCompiler,
|
||||||
this.getTypeCheckingConfig(), refEmitter, reflector, this.adapter, this.incrementalDriver,
|
this.getTypeCheckingConfig(), refEmitter, reflector, this.adapter, this.incrementalDriver,
|
||||||
scopeRegistry);
|
scopeRegistry, typeCheckScopeRegistry);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isCore,
|
isCore,
|
||||||
|
|
|
@ -6,14 +6,14 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {AST, MethodCall, ParseError, PropertyRead, SafeMethodCall, SafePropertyRead, TmplAstNode, TmplAstTemplate} from '@angular/compiler';
|
import {AST, MethodCall, ParseError, PropertyRead, SafeMethodCall, SafePropertyRead, TmplAstElement, TmplAstNode, TmplAstTemplate} from '@angular/compiler';
|
||||||
import {AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system';
|
import {AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {FullTemplateMapping} from './api';
|
import {FullTemplateMapping, TypeCheckableDirectiveMeta} from './api';
|
||||||
import {GlobalCompletion} from './completion';
|
import {GlobalCompletion} from './completion';
|
||||||
import {DirectiveInScope, PipeInScope} from './scope';
|
import {DirectiveInScope, PipeInScope} from './scope';
|
||||||
import {ShimLocation, Symbol} from './symbols';
|
import {DirectiveSymbol, ElementSymbol, ShimLocation, Symbol} from './symbols';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface to the Angular Template Type Checker to extract diagnostics and intelligence from the
|
* Interface to the Angular Template Type Checker to extract diagnostics and intelligence from the
|
||||||
|
@ -110,6 +110,7 @@ export interface TemplateTypeChecker {
|
||||||
*
|
*
|
||||||
* @see Symbol
|
* @see Symbol
|
||||||
*/
|
*/
|
||||||
|
getSymbolOfNode(node: TmplAstElement, component: ts.ClassDeclaration): ElementSymbol|null;
|
||||||
getSymbolOfNode(node: AST|TmplAstNode, component: ts.ClassDeclaration): Symbol|null;
|
getSymbolOfNode(node: AST|TmplAstNode, component: ts.ClassDeclaration): Symbol|null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -149,6 +150,20 @@ export interface TemplateTypeChecker {
|
||||||
* the DOM schema.
|
* the DOM schema.
|
||||||
*/
|
*/
|
||||||
getPotentialElementTags(component: ts.ClassDeclaration): Map<string, DirectiveInScope|null>;
|
getPotentialElementTags(component: ts.ClassDeclaration): Map<string, DirectiveInScope|null>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve any potential DOM bindings for the given element.
|
||||||
|
*
|
||||||
|
* This returns an array of objects which list both the attribute and property names of each
|
||||||
|
* binding, which are usually identical but can vary if the HTML attribute name is for example a
|
||||||
|
* reserved keyword in JS, like the `for` attribute which corresponds to the `htmlFor` property.
|
||||||
|
*/
|
||||||
|
getPotentialDomBindings(tagName: string): {attribute: string, property: string}[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the type checking engine's metadata for the given directive class, if available.
|
||||||
|
*/
|
||||||
|
getDirectiveMetadata(dir: ts.ClassDeclaration): TypeCheckableDirectiveMeta|null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -6,17 +6,17 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {AST, CssSelector, DomElementSchemaRegistry, MethodCall, ParseError, parseTemplate, PropertyRead, SafeMethodCall, SafePropertyRead, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstVariable} from '@angular/compiler';
|
import {AST, CssSelector, DomElementSchemaRegistry, MethodCall, ParseError, parseTemplate, PropertyRead, SafeMethodCall, SafePropertyRead, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstVariable} from '@angular/compiler';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath, getSourceFileOrError} from '../../file_system';
|
import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath, getSourceFileOrError} from '../../file_system';
|
||||||
import {ReferenceEmitter} from '../../imports';
|
import {Reference, ReferenceEmitter} from '../../imports';
|
||||||
import {IncrementalBuild} from '../../incremental/api';
|
import {IncrementalBuild} from '../../incremental/api';
|
||||||
import {ClassDeclaration, isNamedClassDeclaration, ReflectionHost} from '../../reflection';
|
import {ClassDeclaration, isNamedClassDeclaration, ReflectionHost} from '../../reflection';
|
||||||
import {ComponentScopeReader} from '../../scope';
|
import {ComponentScopeReader, TypeCheckScopeRegistry} from '../../scope';
|
||||||
import {isShim} from '../../shims';
|
import {isShim} from '../../shims';
|
||||||
import {getSourceFileOrNull} from '../../util/src/typescript';
|
import {getSourceFileOrNull} from '../../util/src/typescript';
|
||||||
import {DirectiveInScope, FullTemplateMapping, GlobalCompletion, OptimizeFor, PipeInScope, ProgramTypeCheckAdapter, ShimLocation, Symbol, TemplateId, TemplateTypeChecker, TypeCheckingConfig, TypeCheckingProgramStrategy, UpdateMode} from '../api';
|
import {DirectiveInScope, ElementSymbol, FullTemplateMapping, GlobalCompletion, OptimizeFor, PipeInScope, ProgramTypeCheckAdapter, ShimLocation, Symbol, TemplateId, TemplateTypeChecker, TypeCheckableDirectiveMeta, TypeCheckingConfig, TypeCheckingProgramStrategy, UpdateMode} from '../api';
|
||||||
import {TemplateDiagnostic} from '../diagnostics';
|
import {TemplateDiagnostic} from '../diagnostics';
|
||||||
|
|
||||||
import {CompletionEngine} from './completion';
|
import {CompletionEngine} from './completion';
|
||||||
|
@ -83,7 +83,8 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
|
||||||
private refEmitter: ReferenceEmitter, private reflector: ReflectionHost,
|
private refEmitter: ReferenceEmitter, private reflector: ReflectionHost,
|
||||||
private compilerHost: Pick<ts.CompilerHost, 'getCanonicalFileName'>,
|
private compilerHost: Pick<ts.CompilerHost, 'getCanonicalFileName'>,
|
||||||
private priorBuild: IncrementalBuild<unknown, FileTypeCheckingData>,
|
private priorBuild: IncrementalBuild<unknown, FileTypeCheckingData>,
|
||||||
private readonly componentScopeReader: ComponentScopeReader) {}
|
private readonly componentScopeReader: ComponentScopeReader,
|
||||||
|
private readonly typeCheckScopeRegistry: TypeCheckScopeRegistry) {}
|
||||||
|
|
||||||
resetOverrides(): void {
|
resetOverrides(): void {
|
||||||
for (const fileRecord of this.state.values()) {
|
for (const fileRecord of this.state.values()) {
|
||||||
|
@ -471,7 +472,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
|
||||||
}
|
}
|
||||||
return this.state.get(path)!;
|
return this.state.get(path)!;
|
||||||
}
|
}
|
||||||
|
getSymbolOfNode(node: TmplAstElement, component: ts.ClassDeclaration): ElementSymbol|null;
|
||||||
getSymbolOfNode(node: AST|TmplAstNode, component: ts.ClassDeclaration): Symbol|null {
|
getSymbolOfNode(node: AST|TmplAstNode, component: ts.ClassDeclaration): Symbol|null {
|
||||||
const builder = this.getOrCreateSymbolBuilder(component);
|
const builder = this.getOrCreateSymbolBuilder(component);
|
||||||
if (builder === null) {
|
if (builder === null) {
|
||||||
|
@ -513,6 +514,13 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
|
||||||
return data.pipes;
|
return data.pipes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDirectiveMetadata(dir: ts.ClassDeclaration): TypeCheckableDirectiveMeta|null {
|
||||||
|
if (!isNamedClassDeclaration(dir)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.typeCheckScopeRegistry.getTypeCheckDirectiveMetadata(new Reference(dir));
|
||||||
|
}
|
||||||
|
|
||||||
getPotentialElementTags(component: ts.ClassDeclaration): Map<string, DirectiveInScope|null> {
|
getPotentialElementTags(component: ts.ClassDeclaration): Map<string, DirectiveInScope|null> {
|
||||||
if (this.elementTagCache.has(component)) {
|
if (this.elementTagCache.has(component)) {
|
||||||
return this.elementTagCache.get(component)!;
|
return this.elementTagCache.get(component)!;
|
||||||
|
@ -543,6 +551,14 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
|
||||||
return tagMap;
|
return tagMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPotentialDomBindings(tagName: string): {attribute: string, property: string}[] {
|
||||||
|
const attributes = REGISTRY.allKnownAttributesOfElement(tagName);
|
||||||
|
return attributes.map(attribute => ({
|
||||||
|
attribute,
|
||||||
|
property: REGISTRY.getMappedPropName(attribute),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
private getScopeData(component: ts.ClassDeclaration): ScopeData|null {
|
private getScopeData(component: ts.ClassDeclaration): ScopeData|null {
|
||||||
if (this.scopeCache.has(component)) {
|
if (this.scopeCache.has(component)) {
|
||||||
return this.scopeCache.get(component)!;
|
return this.scopeCache.get(component)!;
|
||||||
|
|
|
@ -13,9 +13,9 @@ import {absoluteFrom, AbsoluteFsPath, getSourceFileOrError, LogicalFileSystem} f
|
||||||
import {TestFile} from '../../file_system/testing';
|
import {TestFile} from '../../file_system/testing';
|
||||||
import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, Reexport, Reference, ReferenceEmitter} from '../../imports';
|
import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, Reexport, Reference, ReferenceEmitter} from '../../imports';
|
||||||
import {NOOP_INCREMENTAL_BUILD} from '../../incremental';
|
import {NOOP_INCREMENTAL_BUILD} from '../../incremental';
|
||||||
import {ClassPropertyMapping} from '../../metadata';
|
import {ClassPropertyMapping, CompoundMetadataReader} from '../../metadata';
|
||||||
import {ClassDeclaration, isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection';
|
import {ClassDeclaration, isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection';
|
||||||
import {ComponentScopeReader, LocalModuleScope, ScopeData} from '../../scope';
|
import {ComponentScopeReader, LocalModuleScope, ScopeData, TypeCheckScopeRegistry} from '../../scope';
|
||||||
import {makeProgram} from '../../testing';
|
import {makeProgram} from '../../testing';
|
||||||
import {getRootDirs} from '../../util/src/typescript';
|
import {getRootDirs} from '../../util/src/typescript';
|
||||||
import {ProgramTypeCheckAdapter, TemplateTypeChecker, TypeCheckContext} from '../api';
|
import {ProgramTypeCheckAdapter, TemplateTypeChecker, TypeCheckContext} from '../api';
|
||||||
|
@ -461,9 +461,12 @@ export function setup(targets: TypeCheckingTarget[], overrides: {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const typeCheckScopeRegistry =
|
||||||
|
new TypeCheckScopeRegistry(fakeScopeReader, new CompoundMetadataReader([]));
|
||||||
|
|
||||||
const templateTypeChecker = new TemplateTypeCheckerImpl(
|
const templateTypeChecker = new TemplateTypeCheckerImpl(
|
||||||
program, programStrategy, checkAdapter, fullConfig, emitter, reflectionHost, host,
|
program, programStrategy, checkAdapter, fullConfig, emitter, reflectionHost, host,
|
||||||
NOOP_INCREMENTAL_BUILD, fakeScopeReader);
|
NOOP_INCREMENTAL_BUILD, fakeScopeReader, typeCheckScopeRegistry);
|
||||||
return {
|
return {
|
||||||
templateTypeChecker,
|
templateTypeChecker,
|
||||||
program,
|
program,
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {ASTWithSource, Binary, BindingPipe, Conditional, Interpolation, PropertyRead, TmplAstBoundAttribute, TmplAstBoundText, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate} from '@angular/compiler';
|
import {ASTWithSource, Binary, BindingPipe, Conditional, Interpolation, PropertyRead, TmplAstBoundAttribute, TmplAstBoundText, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate} from '@angular/compiler';
|
||||||
|
import {AST, LiteralArray, LiteralMap} from '@angular/compiler/src/compiler';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {absoluteFrom, AbsoluteFsPath, getSourceFileOrError} from '../../file_system';
|
import {absoluteFrom, AbsoluteFsPath, getSourceFileOrError} from '../../file_system';
|
||||||
|
@ -693,7 +694,7 @@ runInEachFileSystem(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('literal array', () => {
|
it('literal array', () => {
|
||||||
const literalArray = interpolation.expressions[0];
|
const literalArray = interpolation.expressions[0] as LiteralArray;
|
||||||
const symbol = templateTypeChecker.getSymbolOfNode(literalArray, cmp)!;
|
const symbol = templateTypeChecker.getSymbolOfNode(literalArray, cmp)!;
|
||||||
assertExpressionSymbol(symbol);
|
assertExpressionSymbol(symbol);
|
||||||
expect(program.getTypeChecker().symbolToString(symbol.tsSymbol!)).toEqual('Array');
|
expect(program.getTypeChecker().symbolToString(symbol.tsSymbol!)).toEqual('Array');
|
||||||
|
@ -701,7 +702,7 @@ runInEachFileSystem(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('literal map', () => {
|
it('literal map', () => {
|
||||||
const literalMap = interpolation.expressions[1];
|
const literalMap = interpolation.expressions[1] as LiteralMap;
|
||||||
const symbol = templateTypeChecker.getSymbolOfNode(literalMap, cmp)!;
|
const symbol = templateTypeChecker.getSymbolOfNode(literalMap, cmp)!;
|
||||||
assertExpressionSymbol(symbol);
|
assertExpressionSymbol(symbol);
|
||||||
expect(program.getTypeChecker().symbolToString(symbol.tsSymbol!)).toEqual('__object');
|
expect(program.getTypeChecker().symbolToString(symbol.tsSymbol!)).toEqual('__object');
|
||||||
|
@ -762,7 +763,7 @@ runInEachFileSystem(() => {
|
||||||
|
|
||||||
it('should get symbol for pipe, checkTypeOfPipes: false', () => {
|
it('should get symbol for pipe, checkTypeOfPipes: false', () => {
|
||||||
setupPipesTest(false);
|
setupPipesTest(false);
|
||||||
const pipeSymbol = templateTypeChecker.getSymbolOfNode(binding, cmp)!;
|
const pipeSymbol = templateTypeChecker.getSymbolOfNode(binding, cmp)! as PipeSymbol;
|
||||||
assertPipeSymbol(pipeSymbol);
|
assertPipeSymbol(pipeSymbol);
|
||||||
expect(pipeSymbol.tsSymbol).toBeNull();
|
expect(pipeSymbol.tsSymbol).toBeNull();
|
||||||
expect(program.getTypeChecker().typeToString(pipeSymbol.tsType!)).toEqual('any');
|
expect(program.getTypeChecker().typeToString(pipeSymbol.tsType!)).toEqual('any');
|
||||||
|
@ -772,6 +773,24 @@ runInEachFileSystem(() => {
|
||||||
.toEqual('TestPipe');
|
.toEqual('TestPipe');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should get symbols for pipe expression and args', () => {
|
||||||
|
setupPipesTest(false);
|
||||||
|
const aSymbol = templateTypeChecker.getSymbolOfNode(binding.exp, cmp)!;
|
||||||
|
assertExpressionSymbol(aSymbol);
|
||||||
|
expect(program.getTypeChecker().symbolToString(aSymbol.tsSymbol!)).toEqual('a');
|
||||||
|
expect(program.getTypeChecker().typeToString(aSymbol.tsType)).toEqual('string');
|
||||||
|
|
||||||
|
const bSymbol = templateTypeChecker.getSymbolOfNode(binding.args[0] as AST, cmp)!;
|
||||||
|
assertExpressionSymbol(bSymbol);
|
||||||
|
expect(program.getTypeChecker().symbolToString(bSymbol.tsSymbol!)).toEqual('b');
|
||||||
|
expect(program.getTypeChecker().typeToString(bSymbol.tsType)).toEqual('number');
|
||||||
|
|
||||||
|
const cSymbol = templateTypeChecker.getSymbolOfNode(binding.args[1] as AST, cmp)!;
|
||||||
|
assertExpressionSymbol(cSymbol);
|
||||||
|
expect(program.getTypeChecker().symbolToString(cSymbol.tsSymbol!)).toEqual('c');
|
||||||
|
expect(program.getTypeChecker().typeToString(cSymbol.tsType)).toEqual('boolean');
|
||||||
|
});
|
||||||
|
|
||||||
for (const checkTypeOfPipes of [true, false]) {
|
for (const checkTypeOfPipes of [true, false]) {
|
||||||
describe(`checkTypeOfPipes: ${checkTypeOfPipes}`, () => {
|
describe(`checkTypeOfPipes: ${checkTypeOfPipes}`, () => {
|
||||||
// Because the args are property reads, we still need information about them.
|
// Because the args are property reads, we still need information about them.
|
||||||
|
@ -782,12 +801,12 @@ runInEachFileSystem(() => {
|
||||||
expect(program.getTypeChecker().symbolToString(aSymbol.tsSymbol!)).toEqual('a');
|
expect(program.getTypeChecker().symbolToString(aSymbol.tsSymbol!)).toEqual('a');
|
||||||
expect(program.getTypeChecker().typeToString(aSymbol.tsType)).toEqual('string');
|
expect(program.getTypeChecker().typeToString(aSymbol.tsType)).toEqual('string');
|
||||||
|
|
||||||
const bSymbol = templateTypeChecker.getSymbolOfNode(binding.args[0], cmp)!;
|
const bSymbol = templateTypeChecker.getSymbolOfNode(binding.args[0] as AST, cmp)!;
|
||||||
assertExpressionSymbol(bSymbol);
|
assertExpressionSymbol(bSymbol);
|
||||||
expect(program.getTypeChecker().symbolToString(bSymbol.tsSymbol!)).toEqual('b');
|
expect(program.getTypeChecker().symbolToString(bSymbol.tsSymbol!)).toEqual('b');
|
||||||
expect(program.getTypeChecker().typeToString(bSymbol.tsType)).toEqual('number');
|
expect(program.getTypeChecker().typeToString(bSymbol.tsType)).toEqual('number');
|
||||||
|
|
||||||
const cSymbol = templateTypeChecker.getSymbolOfNode(binding.args[1], cmp)!;
|
const cSymbol = templateTypeChecker.getSymbolOfNode(binding.args[1] as AST, cmp)!;
|
||||||
assertExpressionSymbol(cSymbol);
|
assertExpressionSymbol(cSymbol);
|
||||||
expect(program.getTypeChecker().symbolToString(cSymbol.tsSymbol!)).toEqual('c');
|
expect(program.getTypeChecker().symbolToString(cSymbol.tsSymbol!)).toEqual('c');
|
||||||
expect(program.getTypeChecker().typeToString(cSymbol.tsType)).toEqual('boolean');
|
expect(program.getTypeChecker().typeToString(cSymbol.tsType)).toEqual('boolean');
|
||||||
|
|
|
@ -240,6 +240,13 @@ const _ATTR_TO_PROP: {[name: string]: string} = {
|
||||||
'tabindex': 'tabIndex',
|
'tabindex': 'tabIndex',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Invert _ATTR_TO_PROP.
|
||||||
|
const _PROP_TO_ATTR: {[name: string]: string} =
|
||||||
|
Object.keys(_ATTR_TO_PROP).reduce((inverted, attr) => {
|
||||||
|
inverted[_ATTR_TO_PROP[attr]] = attr;
|
||||||
|
return inverted;
|
||||||
|
}, {} as {[prop: string]: string});
|
||||||
|
|
||||||
export class DomElementSchemaRegistry extends ElementSchemaRegistry {
|
export class DomElementSchemaRegistry extends ElementSchemaRegistry {
|
||||||
private _schema: {[element: string]: {[property: string]: string}} = {};
|
private _schema: {[element: string]: {[property: string]: string}} = {};
|
||||||
|
|
||||||
|
@ -386,6 +393,12 @@ export class DomElementSchemaRegistry extends ElementSchemaRegistry {
|
||||||
return Object.keys(this._schema);
|
return Object.keys(this._schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
allKnownAttributesOfElement(tagName: string): string[] {
|
||||||
|
const elementProperties = this._schema[tagName.toLowerCase()] || this._schema['unknown'];
|
||||||
|
// Convert properties to attributes.
|
||||||
|
return Object.keys(elementProperties).map(prop => _PROP_TO_ATTR[prop] ?? prop);
|
||||||
|
}
|
||||||
|
|
||||||
normalizeAnimationStyleProperty(propName: string): string {
|
normalizeAnimationStyleProperty(propName: string): string {
|
||||||
return dashCaseToCamelCase(propName);
|
return dashCaseToCamelCase(propName);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue