Skip to content

Commit 3f5e300

Browse files
committed
WIP: fixing tests and completion with signal based type checking
NOTE: This is very experimental and manual type check block code is not updated. All of this complexity arises from the fact of attempting to support bindings where they go to the same directive with multiple inputs of the same name. Potentially we can deprecate this behavior for input signal based components and avoid this complexity. Alternative solution is to simply repeat expression diagnostics if a binding goes to two same fields.
1 parent e5e405e commit 3f5e300

File tree

8 files changed

+95
-43
lines changed

8 files changed

+95
-43
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ export interface TemplateTypeChecker {
123123
expr: PropertyRead|SafePropertyRead, component: ts.ClassDeclaration): TcbLocation|null;
124124

125125
/**
126-
* For the given node represents a `LiteralPrimitive`(the `TextAttribute` represents a string
126+
* For the given node represents a `LiteralPrimitive` (the `TextAttribute` represents a string
127127
* literal), retrieve a `TcbLocation` that can be used to perform autocompletion at that point in
128128
* the node, if such a location exists.
129129
*/

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

Lines changed: 54 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -78,25 +78,22 @@ export class CompletionEngine {
7878
}
7979

8080
let nodeContext: TcbLocation|null = null;
81-
if (node instanceof EmptyExpr) {
82-
const nodeLocation = findFirstMatchingNode(this.tcb, {
83-
filter: ts.isIdentifier,
84-
withSpan: node.sourceSpan,
85-
});
86-
if (nodeLocation !== null) {
87-
nodeContext = {
88-
tcbPath: this.tcbPath,
89-
isShimFile: this.tcbIsShim,
90-
positionInFile: nodeLocation.getStart(),
91-
};
92-
}
93-
}
9481

95-
if (node instanceof PropertyRead && node.receiver instanceof ImplicitReceiver) {
82+
// Two types of expressions:
83+
// - `[bla]=""` — Empty expression
84+
// - `[bla]="smth"` - Property Read with implicit receiver.
85+
if (node instanceof EmptyExpr ||
86+
(node instanceof PropertyRead && node.receiver instanceof ImplicitReceiver)) {
9687
const nodeLocation = findFirstMatchingNode(this.tcb, {
97-
filter: ts.isPropertyAccessExpression,
88+
// We look for input binding TCBs of the following formats:
89+
// `const _t1 = <value>; myDir.inputField = *_t1`.
90+
// `const _t1 = <value>; myDir.inputField.set(*_t1)`;
91+
// Note: We would like to autocomplete at the `*` character,
92+
// so that we get suggestions in the context of input target.
93+
filter: isInputTcbValueExpression,
9894
withSpan: node.sourceSpan,
9995
});
96+
10097
if (nodeLocation) {
10198
nodeContext = {
10299
tcbPath: this.tcbPath,
@@ -165,37 +162,48 @@ export class CompletionEngine {
165162
return this.expressionCompletionCache.get(expr)!;
166163
}
167164

168-
let tsExpr: ts.StringLiteral|ts.NumericLiteral|null = null;
169-
170-
if (expr instanceof TmplAstTextAttribute) {
171-
const strNode = findFirstMatchingNode(this.tcb, {
172-
filter: ts.isParenthesizedExpression,
165+
let completionIndex: number|undefined = undefined;
166+
167+
completionIndex =
168+
findFirstMatchingNode(this.tcb, {
169+
// We look for input binding TCBs of the following formats:
170+
// `const _t1 = <value>; myDir.inputField = *_t1`.
171+
// `const _t1 = <value>; myDir.inputField.set(*_t1)`;
172+
// Note: We would like to autocomplete at the `*` character,
173+
// so that we get suggestions in the context of input target.
174+
filter: isInputTcbValueExpression,
175+
withSpan: expr instanceof TmplAstTextAttribute && expr.valueSpan !== undefined ?
176+
expr.valueSpan :
177+
expr.sourceSpan,
178+
})?.getStart();
179+
180+
// If we are completing in the middle of an expression, in a literal,
181+
// we look for the actual string literal or numeric literal and complete there.
182+
if (completionIndex === undefined && expr instanceof LiteralPrimitive) {
183+
const literalNode = findFirstMatchingNode(this.tcb, {
184+
filter: (n: ts.Node): n is ts.StringLiteral | ts.NumericLiteral =>
185+
ts.isStringLiteralLike(n) || ts.isNumericLiteral(n),
173186
withSpan: expr.sourceSpan,
174187
});
175-
if (strNode !== null && ts.isStringLiteral(strNode.expression)) {
176-
tsExpr = strNode.expression;
188+
189+
completionIndex = literalNode?.getStart();
190+
191+
if (literalNode !== null && ts.isStringLiteralLike(literalNode)) {
192+
// Completion needs to be inside the string literal, so we account
193+
// for the opening quotes.
194+
completionIndex = literalNode.getStart() + 1;
177195
}
178-
} else {
179-
tsExpr = findFirstMatchingNode(this.tcb, {
180-
filter: (n: ts.Node): n is ts.NumericLiteral | ts.StringLiteral =>
181-
ts.isStringLiteral(n) || ts.isNumericLiteral(n),
182-
withSpan: expr.sourceSpan,
183-
});
184196
}
185197

186-
if (tsExpr === null) {
198+
199+
if (completionIndex === undefined) {
187200
return null;
188201
}
189202

190-
let positionInShimFile = tsExpr.getEnd();
191-
if (ts.isStringLiteral(tsExpr)) {
192-
// In the shimFile, if `tsExpr` is a string, the position should be in the quotes.
193-
positionInShimFile -= 1;
194-
}
195203
const res: TcbLocation = {
196204
tcbPath: this.tcbPath,
197205
isShimFile: this.tcbIsShim,
198-
positionInFile: positionInShimFile,
206+
positionInFile: completionIndex,
199207
};
200208
this.expressionCompletionCache.set(expr, res);
201209
return res;
@@ -233,3 +241,14 @@ export class CompletionEngine {
233241
return templateContext;
234242
}
235243
}
244+
245+
/**
246+
* Whether the given node refers to an TCB input value binding. e.g.
247+
* with the formats:
248+
*
249+
* `const _t1 = <value>; myDir.inputField = *_t1*`.
250+
* `const _t1 = <value>; myDir.inputField.set(*_t1*)`;
251+
*/
252+
function isInputTcbValueExpression(n: ts.Node): n is ts.Identifier {
253+
return (ts.isBinaryExpression(n.parent) || ts.isCallExpression(n.parent)) && ts.isIdentifier(n);
254+
}

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

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -347,10 +347,12 @@ export class SymbolBuilder {
347347
return host !== null ? {kind: SymbolKind.DomBinding, host} : null;
348348
}
349349

350-
const nodes = findAllMatchingNodes(
351-
this.typeCheckBlock, {withSpan: binding.sourceSpan, filter: isAssignment});
350+
const inputStatementNodes = findAllMatchingNodes(
351+
this.typeCheckBlock, {withSpan: binding.sourceSpan, filter: isInputStatement});
352352
const bindings: BindingSymbol[] = [];
353-
for (const node of nodes) {
353+
for (const statementNode of inputStatementNodes) {
354+
const node = statementNode.expression;
355+
354356
if (!isAccessExpression(node.left)) {
355357
continue;
356358
}
@@ -652,6 +654,19 @@ export class SymbolBuilder {
652654
}
653655
}
654656

657+
function isInputStatement(n: ts.Node): n is ts.ExpressionStatement&{
658+
expression: ts.BinaryExpression
659+
}
660+
{
661+
if (ts.isExpressionStatement(n) && isAssignment(n.expression)) {
662+
return true;
663+
}
664+
665+
// TODO(signals): Handle signal statements.
666+
667+
return false;
668+
}
669+
655670
/** Filter predicate function that matches any AST node. */
656671
function anyNodeFilter(n: ts.Node): n is ts.Node {
657672
return true;

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

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {AST, BindingPipe, BindingType, BoundTarget, Call, DYNAMIC_TYPE, ImplicitReceiver, ParsedEventType, ParseSourceSpan, PropertyRead, PropertyWrite, SafeCall, SafePropertyRead, SchemaMetadata, ThisReceiver, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstBoundText, TmplAstDeferredBlock, TmplAstDeferredBlockTriggers, TmplAstElement, TmplAstForLoopBlock, TmplAstHoverDeferredTrigger, TmplAstIcu, TmplAstIfBlock, TmplAstIfBlockBranch, TmplAstInteractionDeferredTrigger, TmplAstNode, TmplAstReference, TmplAstSwitchBlock, TmplAstSwitchBlockCase, TmplAstTemplate, TmplAstTextAttribute, TmplAstVariable, TmplAstViewportDeferredTrigger, TransplantedType} from '@angular/compiler';
9+
import {AST, BindingPipe, BindingType, BoundTarget, Call, DYNAMIC_TYPE, ImplicitReceiver, ParsedEventType, ParseSourceSpan, PropertyRead, PropertyWrite, R3Identifiers, SafeCall, SafePropertyRead, SchemaMetadata, ThisReceiver, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstBoundText, TmplAstDeferredBlock, TmplAstDeferredBlockTriggers, TmplAstElement, TmplAstForLoopBlock, TmplAstHoverDeferredTrigger, TmplAstIcu, TmplAstIfBlock, TmplAstIfBlockBranch, TmplAstInteractionDeferredTrigger, TmplAstNode, TmplAstReference, TmplAstSwitchBlock, TmplAstSwitchBlockCase, TmplAstTemplate, TmplAstTextAttribute, TmplAstVariable, TmplAstViewportDeferredTrigger, TransplantedType} from '@angular/compiler';
1010
import ts from 'typescript';
1111

1212
import {Reference} from '../../imports';
@@ -707,6 +707,13 @@ class TcbDirectiveInputsOp extends TcbOp {
707707
widenBinding(translateInput(attr.attribute, this.tcb, this.scope), this.tcb);
708708
const sharedExprId = this.tcb.allocateId();
709709

710+
// Add the value span to the shared expression. This will be useful for completion
711+
// purposes so that we can find a position for global completion in the context
712+
// of assignment to a specific directive/component input. See `completion.ts`.
713+
if (attr.attribute.valueSpan !== undefined) {
714+
addParseSpanInfo(sharedExprId, attr.attribute.valueSpan);
715+
}
716+
710717
const inputTestStatements: ts.Statement[] = [
711718
// The expression itself is always added as test statement. In case no target
712719
// can be found (e.g. there is no class member for the input)- we'd still want to
@@ -722,7 +729,7 @@ class TcbDirectiveInputsOp extends TcbOp {
722729
undefined,
723730
exprForDiag,
724731
)],
725-
ts.NodeFlags.Const))
732+
ts.NodeFlags.Const)),
726733
];
727734

728735
for (const {fieldName, required, transformType} of attr.inputs) {
@@ -805,8 +812,8 @@ class TcbDirectiveInputsOp extends TcbOp {
805812

806813
// TODO(signals)
807814
if (this.dir.isSignal) {
808-
const toWritableSignalExpr =
809-
this.tcb.env.referenceExternalSymbol('@angular/core', 'ɵɵtoWritableSignal')
815+
const toWritableSignalExpr = this.tcb.env.referenceExternalSymbol(
816+
R3Identifiers.toWriteableSignal.moduleName, R3Identifiers.toWriteableSignal.name);
810817

811818
inputTestStatements.push(
812819
ts.factory.createExpressionStatement(ts.factory.createCallExpression(

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ export class TypeCheckFile extends Environment {
7474
// is somehow more expensive than the initial parse.
7575
source += '\nexport const IS_A_MODULE = true;\n';
7676

77+
console.error(source);
78+
7779
return source;
7880
}
7981

packages/compiler/src/render3/r3_identifiers.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,4 +391,10 @@ export class Identifiers {
391391
o.ExternalReference = {name: 'ɵɵtrustConstantResourceUrl', moduleName: CORE};
392392
static validateIframeAttribute:
393393
o.ExternalReference = {name: 'ɵɵvalidateIframeAttribute', moduleName: CORE};
394+
395+
// type-checking helpers
396+
static toWriteableSignal = {
397+
name: 'ɵɵtoWritableSignal',
398+
moduleName: CORE,
399+
};
394400
}

packages/language-service/src/completions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,8 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {
203203
if (this.isValidNodeContextCompletion(result)) {
204204
ngResults.push({
205205
...result,
206+
// Remove the leading/trailing quotes from the TS completion entries.
207+
name: result.name.replace(/(^['"`]|['"~]$)/g, ''),
206208
replacementSpan,
207209
});
208210
}

packages/language-service/test/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ jasmine_node_test(
2424
"//packages/compiler-cli/src/ngtsc/testing/fake_common:npm_package",
2525
"//packages/compiler-cli/src/ngtsc/testing/fake_core:npm_package",
2626
],
27+
shard_count = 4,
2728
deps = [
2829
":test_lib",
2930
],

0 commit comments

Comments
 (0)