Skip to content

Commit cc8b76e

Browse files
crisbetodylhunn
authored andcommitted
fix(language-service): correctly handle host directive inputs/outputs (#48147)
Adds some logic to correctly handle hidden or aliased inputs/outputs in the language service. Fixes #48102. PR Close #48147
1 parent fa5528f commit cc8b76e

File tree

6 files changed

+328
-42
lines changed

6 files changed

+328
-42
lines changed

packages/compiler-cli/src/ngtsc/typecheck/api/api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@ import {AbsoluteSourceSpan, BoundTarget, DirectiveMeta, ParseSourceSpan, SchemaM
1010
import ts from 'typescript';
1111

1212
import {ErrorCode} from '../../diagnostics';
13-
import {AbsoluteFsPath} from '../../file_system';
1413
import {Reference} from '../../imports';
15-
import {ClassPropertyMapping, DirectiveTypeCheckMeta} from '../../metadata';
14+
import {ClassPropertyMapping, DirectiveTypeCheckMeta, HostDirectiveMeta} from '../../metadata';
1615
import {ClassDeclaration} from '../../reflection';
1716

1817

@@ -26,6 +25,7 @@ export interface TypeCheckableDirectiveMeta extends DirectiveMeta, DirectiveType
2625
inputs: ClassPropertyMapping;
2726
outputs: ClassPropertyMapping;
2827
isStandalone: boolean;
28+
hostDirectives: HostDirectiveMeta[]|null;
2929
}
3030

3131
export type TemplateId = string&{__brand: 'TemplateId'};

packages/compiler-cli/src/ngtsc/typecheck/api/symbols.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -258,11 +258,8 @@ export interface TemplateSymbol {
258258
templateNode: TmplAstTemplate;
259259
}
260260

261-
/**
262-
* A representation of a directive/component whose selector matches a node in a component
263-
* template.
264-
*/
265-
export interface DirectiveSymbol extends PotentialDirective {
261+
/** Interface shared between host and non-host directives. */
262+
interface DirectiveSymbolBase extends PotentialDirective {
266263
kind: SymbolKind.Directive;
267264

268265
/** The `ts.Type` for the class declaration. */
@@ -272,6 +269,16 @@ export interface DirectiveSymbol extends PotentialDirective {
272269
tcbLocation: TcbLocation;
273270
}
274271

272+
/**
273+
* A representation of a directive/component whose selector matches a node in a component
274+
* template.
275+
*/
276+
export type DirectiveSymbol = (DirectiveSymbolBase&{isHostDirective: false})|(DirectiveSymbolBase&{
277+
isHostDirective: true;
278+
exposedInputs: Record<string, string>|null;
279+
exposedOutputs: Record<string, string>|null;
280+
});
281+
275282
/**
276283
* A representation of an attribute on an element or template. These bindings aren't currently
277284
* type-checked (see `checkTypeOfDomBindings`) so they won't have a `ts.Type`, `ts.Symbol`, or shim

packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts

Lines changed: 76 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import ts from 'typescript';
1111

1212
import {AbsoluteFsPath} from '../../file_system';
1313
import {Reference} from '../../imports';
14+
import {HostDirectiveMeta} from '../../metadata';
1415
import {ClassDeclaration} from '../../reflection';
1516
import {ComponentScopeKind, ComponentScopeReader} from '../../scope';
1617
import {isAssignment, isSymbolWithValueDeclaration} from '../../util/src/typescript';
@@ -118,38 +119,80 @@ export class SymbolBuilder {
118119

119120
const nodes = findAllMatchingNodes(
120121
this.typeCheckBlock, {withSpan: elementSourceSpan, filter: isDirectiveDeclaration});
121-
return nodes
122-
.map(node => {
123-
const symbol = this.getSymbolOfTsNode(node.parent);
124-
if (symbol === null || !isSymbolWithValueDeclaration(symbol.tsSymbol) ||
125-
!ts.isClassDeclaration(symbol.tsSymbol.valueDeclaration)) {
126-
return null;
127-
}
128-
const meta = this.getDirectiveMeta(element, symbol.tsSymbol.valueDeclaration);
129-
if (meta === null) {
130-
return null;
131-
}
132-
133-
const ngModule = this.getDirectiveModule(symbol.tsSymbol.valueDeclaration);
134-
if (meta.selector === null) {
135-
return null;
136-
}
137-
const isComponent = meta.isComponent ?? null;
138-
const ref = new Reference<ClassDeclaration>(symbol.tsSymbol.valueDeclaration as any);
139-
const directiveSymbol: DirectiveSymbol = {
140-
...symbol,
141-
ref,
142-
tsSymbol: symbol.tsSymbol,
143-
selector: meta.selector,
144-
isComponent,
145-
ngModule,
146-
kind: SymbolKind.Directive,
147-
isStructural: meta.isStructural,
148-
isInScope: true,
149-
};
150-
return directiveSymbol;
151-
})
152-
.filter((d): d is DirectiveSymbol => d !== null);
122+
const symbols: DirectiveSymbol[] = [];
123+
124+
for (const node of nodes) {
125+
const symbol = this.getSymbolOfTsNode(node.parent);
126+
if (symbol === null || !isSymbolWithValueDeclaration(symbol.tsSymbol) ||
127+
!ts.isClassDeclaration(symbol.tsSymbol.valueDeclaration)) {
128+
continue;
129+
}
130+
131+
const meta = this.getDirectiveMeta(element, symbol.tsSymbol.valueDeclaration);
132+
133+
if (meta !== null && meta.selector !== null) {
134+
const ref = new Reference<ClassDeclaration>(symbol.tsSymbol.valueDeclaration as any);
135+
136+
if (meta.hostDirectives !== null) {
137+
this.addHostDirectiveSymbols(element, meta.hostDirectives, symbols);
138+
}
139+
140+
const directiveSymbol: DirectiveSymbol = {
141+
...symbol,
142+
ref,
143+
tsSymbol: symbol.tsSymbol,
144+
selector: meta.selector,
145+
isComponent: meta.isComponent,
146+
ngModule: this.getDirectiveModule(symbol.tsSymbol.valueDeclaration),
147+
kind: SymbolKind.Directive,
148+
isStructural: meta.isStructural,
149+
isInScope: true,
150+
isHostDirective: false,
151+
};
152+
153+
symbols.push(directiveSymbol);
154+
}
155+
}
156+
157+
return symbols;
158+
}
159+
160+
private addHostDirectiveSymbols(
161+
host: TmplAstTemplate|TmplAstElement, hostDirectives: HostDirectiveMeta[],
162+
symbols: DirectiveSymbol[]): void {
163+
for (const current of hostDirectives) {
164+
if (!ts.isClassDeclaration(current.directive.node)) {
165+
continue;
166+
}
167+
168+
const symbol = this.getSymbolOfTsNode(current.directive.node);
169+
const meta = this.getDirectiveMeta(host, current.directive.node);
170+
171+
if (meta !== null && symbol !== null && isSymbolWithValueDeclaration(symbol.tsSymbol)) {
172+
if (meta.hostDirectives !== null) {
173+
this.addHostDirectiveSymbols(host, meta.hostDirectives, symbols);
174+
}
175+
176+
const directiveSymbol: DirectiveSymbol = {
177+
...symbol,
178+
isHostDirective: true,
179+
ref: current.directive,
180+
tsSymbol: symbol.tsSymbol,
181+
exposedInputs: current.inputs,
182+
exposedOutputs: current.outputs,
183+
// TODO(crisbeto): rework `DirectiveSymbol` to make
184+
// `selector` nullable and remove the `|| ''` here.
185+
selector: meta.selector || '',
186+
isComponent: meta.isComponent,
187+
ngModule: this.getDirectiveModule(current.directive.node),
188+
kind: SymbolKind.Directive,
189+
isStructural: meta.isStructural,
190+
isInScope: true,
191+
};
192+
193+
symbols.push(directiveSymbol);
194+
}
195+
}
153196
}
154197

155198
private getDirectiveMeta(
@@ -376,6 +419,7 @@ export class SymbolBuilder {
376419
isStructural,
377420
selector,
378421
ngModule,
422+
isHostDirective: false,
379423
isInScope: true, // TODO: this should always be in scope in this context, right?
380424
};
381425
}

packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ export interface TestDirective extends Partial<Pick<
228228
Exclude<
229229
keyof TypeCheckableDirectiveMeta,
230230
'ref'|'coercedInputFields'|'restrictedInputFields'|'stringLiteralInputFields'|
231-
'undeclaredInputFields'|'inputs'|'outputs'>>> {
231+
'undeclaredInputFields'|'inputs'|'outputs'|'hostDirectives'>>> {
232232
selector: string;
233233
name: string;
234234
file?: AbsoluteFsPath;

packages/language-service/src/attribute_completions.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,18 @@ export function buildAttributeCompletionTable(
220220
continue;
221221
}
222222

223-
for (const [classPropertyName, propertyName] of meta.inputs) {
223+
for (const [classPropertyName, rawProperyName] of meta.inputs) {
224+
let propertyName: string;
225+
226+
if (dirSymbol.isHostDirective) {
227+
if (!dirSymbol.exposedInputs?.hasOwnProperty(rawProperyName)) {
228+
continue;
229+
}
230+
propertyName = dirSymbol.exposedInputs[rawProperyName];
231+
} else {
232+
propertyName = rawProperyName;
233+
}
234+
224235
if (table.has(propertyName)) {
225236
continue;
226237
}
@@ -234,7 +245,18 @@ export function buildAttributeCompletionTable(
234245
});
235246
}
236247

237-
for (const [classPropertyName, propertyName] of meta.outputs) {
248+
for (const [classPropertyName, rawProperyName] of meta.outputs) {
249+
let propertyName: string;
250+
251+
if (dirSymbol.isHostDirective) {
252+
if (!dirSymbol.exposedOutputs?.hasOwnProperty(rawProperyName)) {
253+
continue;
254+
}
255+
propertyName = dirSymbol.exposedOutputs[rawProperyName];
256+
} else {
257+
propertyName = rawProperyName;
258+
}
259+
238260
if (table.has(propertyName)) {
239261
continue;
240262
}

0 commit comments

Comments
 (0)