Skip to content

Commit 07ec04d

Browse files
devversionthePunderWoman
authored andcommitted
refactor(migrations): improve temporary variable generation for input migration (angular#57292)
This changes the migration so that we don't generate `_1` suffixes when a temporary variable wouldn't conflict with any variables in the lexical scope. In addition, if we discover conflicts, we try alternative suffixes that seem more natural and follow style guides. E.g. `Value`, `Val` or `Input`. PR Close angular#57292
1 parent e1240c6 commit 07ec04d

File tree

7 files changed

+352
-62
lines changed

7 files changed

+352
-62
lines changed

packages/core/schematics/migrations/signal-migration/src/flow_analysis/flow_containers.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export function isControlFlowBoundary(node: ts.Node): boolean {
2323

2424
/** Determines the current flow container of a given node. */
2525
export function getControlFlowContainer(node: ts.Node): ts.Node {
26-
return findAncestor(node.parent, (node) => isControlFlowBoundary(node))!;
26+
return ts.findAncestor(node.parent, (node) => isControlFlowBoundary(node))!;
2727
}
2828

2929
/** Checks whether the given node refers to an IIFE declaration. */
@@ -44,18 +44,3 @@ function getImmediatelyInvokedFunctionExpression(func: ts.Node): ts.CallExpressi
4444
}
4545
return undefined;
4646
}
47-
48-
/** Traverses up the node chain until the callback is true, returning the node. */
49-
function findAncestor(
50-
node: ts.Node | undefined,
51-
callback: (element: ts.Node) => boolean,
52-
): ts.Node | undefined {
53-
while (node) {
54-
const result = callback(node);
55-
if (result) {
56-
return node;
57-
}
58-
node = node.parent;
59-
}
60-
return undefined;
61-
}

packages/core/schematics/migrations/signal-migration/src/passes/5_migrate_ts_references.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {InputUniqueKey} from '../utils/input_id';
1414
import {isTsInputReference} from '../utils/input_reference';
1515
import {traverseAccess} from '../utils/traverse_access';
1616
import {KnownInputs} from '../input_detection/known_inputs';
17-
import {createGenerateUniqueIdentifierHelper} from '../../../../../../compiler-cli/src/ngtsc/translator/src/import_manager/check_unique_identifier_name';
17+
import {UniqueNamesGenerator} from '../utils/unique_names';
1818

1919
/**
2020
* Phase that migrates TypeScript input references to be signal compatible.
@@ -46,9 +46,9 @@ export function pass5__migrateTypeScriptReferences(
4646
checker: ts.TypeChecker,
4747
knownInputs: KnownInputs,
4848
) {
49-
const generateUniqueIdentifier = createGenerateUniqueIdentifierHelper();
5049
const tsReferences = new Map<InputUniqueKey, {accesses: ts.Identifier[]}>();
5150
const seenIdentifiers = new WeakSet<ts.Identifier>();
51+
const nameGenerator = new UniqueNamesGenerator();
5252

5353
for (const reference of result.references) {
5454
// This pass only deals with TS references.
@@ -130,8 +130,7 @@ export function pass5__migrateTypeScriptReferences(
130130
const leadingSpace = ts.getLineAndCharacterOfPosition(sf, previous.getStart());
131131

132132
const replaceNode = traverseAccess(originalNode);
133-
const uniqueFieldName = generateUniqueIdentifier(sf, originalNode.text);
134-
const fieldName = uniqueFieldName === null ? originalNode.text : uniqueFieldName.text;
133+
const fieldName = nameGenerator.generate(originalNode.text, previous);
135134

136135
idToSharedField.set(id, fieldName);
137136

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import ts from 'typescript';
10+
11+
/** Whether the given node is a descendant of the given ancestor. */
12+
export function isNodeDescendantOf(node: ts.Node, ancestor: ts.Node | undefined): boolean {
13+
while (node) {
14+
if (node === ancestor) return true;
15+
node = node.parent;
16+
}
17+
return false;
18+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import assert from 'assert';
10+
import ts from 'typescript';
11+
import {isNodeDescendantOf} from './is_descendant_of';
12+
13+
// typescript/stable/src/compiler/types.ts;l=967;rcl=651008033
14+
export interface LocalsContainer extends ts.Node {
15+
locals?: Map<string, ts.Symbol>;
16+
nextContainer?: LocalsContainer;
17+
}
18+
19+
/**
20+
* Gets whether the given identifier name is free for use in the
21+
* given location, avoiding shadowed variable names.
22+
*
23+
*/
24+
export function isIdentifierFreeInScope(
25+
name: string,
26+
location: ts.Node,
27+
): null | {container: LocalsContainer} {
28+
const startContainer = findClosestParentLocalsContainer(location);
29+
assert(startContainer !== undefined, 'Expecting a locals container.');
30+
31+
// Traverse up and check for potential collisions.
32+
let container: LocalsContainer | undefined = startContainer;
33+
let firstNextContainer: LocalsContainer | undefined = undefined;
34+
35+
while (container !== undefined) {
36+
if (!isIdentifierFreeInContainer(name, container)) {
37+
return null;
38+
}
39+
if (firstNextContainer === undefined && container.nextContainer !== undefined) {
40+
firstNextContainer = container.nextContainer;
41+
}
42+
container = findClosestParentLocalsContainer(container.parent);
43+
}
44+
45+
// Check descendent local containers to avoid shadowing variables.
46+
// Note that this is not strictly needed, but it's helping avoid
47+
// some lint errors, like TSLint's no shadowed variables.
48+
container = firstNextContainer;
49+
while (container && isNodeDescendantOf(container, startContainer)) {
50+
if (!isIdentifierFreeInContainer(name, container)) {
51+
return null;
52+
}
53+
container = container.nextContainer;
54+
}
55+
56+
return {container: startContainer};
57+
}
58+
59+
/** Finds the closest parent locals container. */
60+
function findClosestParentLocalsContainer(node: ts.Node): LocalsContainer | undefined {
61+
return ts.findAncestor(node, isLocalsContainer);
62+
}
63+
64+
/** Whether the given identifier is free in the given locals container. */
65+
function isIdentifierFreeInContainer(name: string, container: LocalsContainer): boolean {
66+
if (container.locals === undefined || !container.locals.has(name)) {
67+
return true;
68+
}
69+
70+
// We consider alias symbols as locals conservatively.
71+
// Note: This check is similar to the check by the TypeScript emitter.
72+
// typescript/stable/src/compiler/emitter.ts;l=5436;rcl=651008033
73+
const local = container.locals.get(name)!;
74+
return !(
75+
local.flags &
76+
(ts.SymbolFlags.Value | ts.SymbolFlags.ExportValue | ts.SymbolFlags.Alias)
77+
);
78+
}
79+
80+
/**
81+
* Whether the given node can contain local variables.
82+
*
83+
* Note: This is similar to TypeScript's `canHaveLocals` internal helper.
84+
* typescript/stable/src/compiler/utilitiesPublic.ts;l=2265;rcl=651008033
85+
*/
86+
function isLocalsContainer(node: ts.Node): node is LocalsContainer {
87+
switch (node.kind) {
88+
case ts.SyntaxKind.ArrowFunction:
89+
case ts.SyntaxKind.Block:
90+
case ts.SyntaxKind.CallSignature:
91+
case ts.SyntaxKind.CaseBlock:
92+
case ts.SyntaxKind.CatchClause:
93+
case ts.SyntaxKind.ClassStaticBlockDeclaration:
94+
case ts.SyntaxKind.ConditionalType:
95+
case ts.SyntaxKind.Constructor:
96+
case ts.SyntaxKind.ConstructorType:
97+
case ts.SyntaxKind.ConstructSignature:
98+
case ts.SyntaxKind.ForStatement:
99+
case ts.SyntaxKind.ForInStatement:
100+
case ts.SyntaxKind.ForOfStatement:
101+
case ts.SyntaxKind.FunctionDeclaration:
102+
case ts.SyntaxKind.FunctionExpression:
103+
case ts.SyntaxKind.FunctionType:
104+
case ts.SyntaxKind.GetAccessor:
105+
case ts.SyntaxKind.IndexSignature:
106+
case ts.SyntaxKind.JSDocCallbackTag:
107+
case ts.SyntaxKind.JSDocEnumTag:
108+
case ts.SyntaxKind.JSDocFunctionType:
109+
case ts.SyntaxKind.JSDocSignature:
110+
case ts.SyntaxKind.JSDocTypedefTag:
111+
case ts.SyntaxKind.MappedType:
112+
case ts.SyntaxKind.MethodDeclaration:
113+
case ts.SyntaxKind.MethodSignature:
114+
case ts.SyntaxKind.ModuleDeclaration:
115+
case ts.SyntaxKind.SetAccessor:
116+
case ts.SyntaxKind.SourceFile:
117+
case ts.SyntaxKind.TypeAliasDeclaration:
118+
return true;
119+
default:
120+
return false;
121+
}
122+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import ts from 'typescript';
10+
import {isIdentifierFreeInScope} from './is_identifier_free_in_scope';
11+
12+
/** List of potential suffixes to avoid conflicts. */
13+
const fallbackSuffixes = ['Value', 'Val', 'Input'];
14+
15+
/**
16+
* Helper that can generate unique identifier names at a
17+
* given location.
18+
*
19+
* Used for generating unique names to extract input reads
20+
* to support narrowing.
21+
*/
22+
export class UniqueNamesGenerator {
23+
generate(base: string, location: ts.Node): string {
24+
const checkNameAndClaimIfAvailable = (name: string): boolean => {
25+
const freeInfo = isIdentifierFreeInScope(name, location);
26+
if (freeInfo === null) {
27+
return false;
28+
}
29+
30+
// Claim the locals to avoid conflicts with future generations.
31+
freeInfo.container.locals?.set(name, null! as ts.Symbol);
32+
return true;
33+
};
34+
35+
// Check the base name. Ideally, we'd use this one.
36+
if (checkNameAndClaimIfAvailable(base)) {
37+
return base;
38+
}
39+
40+
// Try any of the possible suffixes.
41+
for (const suffix of fallbackSuffixes) {
42+
const name = `${base}${suffix}`;
43+
if (checkNameAndClaimIfAvailable(name)) {
44+
return name;
45+
}
46+
}
47+
48+
// Worst case, suffix the base name with a unique number until
49+
// we find an available name.
50+
let name = null;
51+
let counter = 1;
52+
do {
53+
name = `${base}_${counter++}`;
54+
} while (!checkNameAndClaimIfAvailable(name));
55+
56+
return name;
57+
}
58+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
// tslint:disable
10+
11+
import {Component, Input} from '@angular/core';
12+
13+
const complex = 'some global variable';
14+
15+
@Component({template: ''})
16+
class MyComp {
17+
@Input() name: string | null = null;
18+
@Input() complex: string | null = null;
19+
20+
valid() {
21+
if (this.name) {
22+
this.name.charAt(0);
23+
}
24+
}
25+
26+
// Input read cannot be stored in a variable: `name`.
27+
simpleLocalCollision() {
28+
const name = 'some other name';
29+
if (this.name) {
30+
this.name.charAt(0);
31+
}
32+
}
33+
34+
// `this.complex` should conflict with the file-level `complex` variable,
35+
// and result in a suffix variable.
36+
complexParentCollision() {
37+
if (this.complex) {
38+
this.complex.charAt(0);
39+
}
40+
}
41+
42+
nestedShadowing() {
43+
if (this.name) {
44+
this.name.charAt(0);
45+
}
46+
47+
function nested() {
48+
const name = '';
49+
}
50+
}
51+
}

0 commit comments

Comments
 (0)