Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Forbid unused property renaming in destructuring binding in function types #41044

Merged
merged 31 commits into from Jun 14, 2022
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
4aec8ef
Forbid renaming a propertyin function type parameters
uhyo Oct 11, 2020
a37d6d4
add tests
uhyo Oct 11, 2020
67732d3
Remove renaming from declaration output
uhyo Oct 11, 2020
1d114e6
accept baseline
uhyo Oct 11, 2020
4abcfa1
accept baseline
uhyo Jun 8, 2022
39e90ce
renew tests (not very right now)
uhyo Jun 8, 2022
79ef4fd
get correct result
uhyo Jun 8, 2022
69b8a58
update diagnostic text
uhyo Jun 8, 2022
4ddd9ae
accept baseline
uhyo Jun 8, 2022
e210484
add declaration emit test
uhyo Jun 8, 2022
2797958
fix declaration emit
uhyo Jun 8, 2022
cb326d2
fix formatting
uhyo Jun 8, 2022
17a29ff
revert unnecessary change
uhyo Jun 8, 2022
c4c80db
accept baseline
uhyo Jun 8, 2022
1e3dc2e
extend tests
uhyo Jun 8, 2022
32c340e
Revert "revert unnecessary change"
uhyo Jun 8, 2022
9c9fe5e
accept baseline
uhyo Jun 8, 2022
f0e5112
Rename and refactor potentialAlways... stuff
uhyo Jun 9, 2022
7aa2b6e
add non-identifier names
uhyo Jun 9, 2022
755c7c4
extend check to non-identifier original property names
uhyo Jun 9, 2022
c7768cf
update diagnostic message
uhyo Jun 9, 2022
bdb42e1
add related span
uhyo Jun 9, 2022
6d4d147
accept baseline
uhyo Jun 9, 2022
bc92945
add symbol-keyed test case
uhyo Jun 9, 2022
e2710b7
oops?
uhyo Jun 9, 2022
1e4c14b
workaround for unstable test
uhyo Jun 9, 2022
4e519ff
fix suggested name
uhyo Jun 10, 2022
07f162f
add comment about non-identifier property names
uhyo Jun 10, 2022
6c8a95b
simplify isReferenced check
uhyo Jun 10, 2022
bd0da30
accept baseline
uhyo Jun 10, 2022
600b614
move it one step further
uhyo Jun 10, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
64 changes: 55 additions & 9 deletions src/compiler/checker.ts
Expand Up @@ -1026,6 +1026,7 @@ namespace ts {
const potentialNewTargetCollisions: Node[] = [];
const potentialWeakMapSetCollisions: Node[] = [];
const potentialReflectCollisions: Node[] = [];
const potentialUnusedRenamedBindingElementsInTypes: BindingElement[] = [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like name and diagnostic are always predictable / derivable from node, so this can just be Node[] like the others. (I don鈥檛 think there鈥檚 a need to try to make it general-purpose for future use.) I also don鈥檛 like the name but it鈥檚 extremely hard to come up with anything better... potentialUnusedRenamedBindingElementsInTypes? 馃槄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for feedback, fixed 馃檪

const awaitedTypeStack: number[] = [];

const diagnostics = createDiagnosticCollection();
Expand Down Expand Up @@ -5964,19 +5965,29 @@ namespace ts {
return parameterNode;

function cloneBindingName(node: BindingName): BindingName {
return elideInitializerAndSetEmitFlags(node) as BindingName;
function elideInitializerAndSetEmitFlags(node: Node): Node {
return elideInitializerAndPropertyRenamingAndSetEmitFlags(node) as BindingName;
function elideInitializerAndPropertyRenamingAndSetEmitFlags(node: Node): Node {
if (context.tracker.trackSymbol && isComputedPropertyName(node) && isLateBindableName(node)) {
trackComputedName(node.expression, context.enclosingDeclaration, context);
}
let visited = visitEachChild(node, elideInitializerAndSetEmitFlags, nullTransformationContext, /*nodesVisitor*/ undefined, elideInitializerAndSetEmitFlags)!;
let visited = visitEachChild(node, elideInitializerAndPropertyRenamingAndSetEmitFlags, nullTransformationContext, /*nodesVisitor*/ undefined, elideInitializerAndPropertyRenamingAndSetEmitFlags)!;
if (isBindingElement(visited)) {
visited = factory.updateBindingElement(
visited,
visited.dotDotDotToken,
visited.propertyName,
visited.name,
/*initializer*/ undefined);
if (visited.propertyName && isIdentifier(visited.propertyName) && isIdentifier(visited.name)) {
visited = factory.updateBindingElement(
visited,
visited.dotDotDotToken,
/* propertyName*/ undefined,
visited.propertyName,
/*initializer*/ undefined);
}
else {
visited = factory.updateBindingElement(
visited,
visited.dotDotDotToken,
visited.propertyName,
visited.name,
/*initializer*/ undefined);
}
}
if (!nodeIsSynthesized(visited)) {
visited = factory.cloneNode(visited);
Expand Down Expand Up @@ -37564,6 +37575,24 @@ namespace ts {
});
}

function checkPotentialUncheckedRenamedBindingElementsInTypes() {
for (const node of potentialUnusedRenamedBindingElementsInTypes) {
if (!(getSymbolOfNode(node)?.isReferenced! & SymbolFlags.Type)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, isReferenced is marked with Type in the case of typeof renamedThing? I would have thought that counts as a value usage. The tests look right so I assume this works, I鈥檓 just surprised. Do we need to check against Type at all or can we just say if (!(getSymbolOfNode(node)?.isReferenced)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently just checking truthiness of isReferenced is enough. Thank you 馃檪

const wrappingDeclaration = walkUpBindingElementsAndPatterns(node);
Debug.assert(isParameterDeclaration(wrappingDeclaration), "Only parameter declaration should be checked here");
const diagnostic = createDiagnosticForNode(node.name, Diagnostics._0_is_an_unused_renaming_of_1_Did_you_intend_to_use_it_as_a_type_annotation, declarationNameToString(node.name), declarationNameToString(node.propertyName));
if (!wrappingDeclaration.type) {
// entire parameter does not have type annotation, suggest adding an annotation
addRelatedInfo(
diagnostic,
createFileDiagnostic(getSourceFileOfNode(wrappingDeclaration), wrappingDeclaration.end - 1, 1,Diagnostics.We_can_only_write_a_type_for_0_by_adding_a_type_for_the_entire_parameter_here, declarationNameToString(wrappingDeclaration.name))
);
}
diagnostics.add(diagnostic);
}
}
}

function bindingNameText(name: BindingName): string {
switch (name.kind) {
case SyntaxKind.Identifier:
Expand Down Expand Up @@ -37905,6 +37934,19 @@ namespace ts {
}

if (isBindingElement(node)) {
if (
node.propertyName &&
isIdentifier(node.name) &&
isParameterDeclaration(node) &&
nodeIsMissing((getContainingFunction(node) as FunctionLikeDeclaration).body)) {
// type F = ({a: string}) => void;
// ^^^^^^
// variable renaming in function type notation is confusing,
// so we forbid it even if noUnusedLocals is not enabled
potentialUnusedRenamedBindingElementsInTypes.push(node);
return;
}

if (isObjectBindingPattern(node.parent) && node.dotDotDotToken && languageVersion < ScriptTarget.ES2018) {
checkExternalEmitHelpers(node, ExternalEmitHelpers.Rest);
}
Expand Down Expand Up @@ -41748,6 +41790,7 @@ namespace ts {
clear(potentialNewTargetCollisions);
clear(potentialWeakMapSetCollisions);
clear(potentialReflectCollisions);
clear(potentialUnusedRenamedBindingElementsInTypes);

forEach(node.statements, checkSourceElement);
checkSourceElement(node.endOfFileToken);
Expand All @@ -41767,6 +41810,9 @@ namespace ts {
}
});
}
if (!node.isDeclarationFile) {
checkPotentialUncheckedRenamedBindingElementsInTypes();
}
});

if (compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Error &&
Expand Down
8 changes: 8 additions & 0 deletions src/compiler/diagnosticMessages.json
Expand Up @@ -3487,6 +3487,14 @@
"category": "Error",
"code": 2841
},
"'{0}' is an unused renaming of '{1}'. Did you intend to use it as a type annotation?": {
"category": "Error",
"code": 2842
},
"We can only write a type for '{0}' by adding a type for the entire parameter here.": {
"category": "Error",
"code": 2843
},

"Import declaration '{0}' is using private name '{1}'.": {
"category": "Error",
Expand Down
22 changes: 19 additions & 3 deletions src/compiler/transformers/declarations.ts
Expand Up @@ -454,7 +454,7 @@ namespace ts {
return ret;
}

function filterBindingPatternInitializers(name: BindingName) {
function filterBindingPatternInitializersAndRenamings(name: BindingName) {
if (name.kind === SyntaxKind.Identifier) {
return name;
}
Expand All @@ -472,7 +472,23 @@ namespace ts {
if (elem.kind === SyntaxKind.OmittedExpression) {
return elem;
}
return factory.updateBindingElement(elem, elem.dotDotDotToken, elem.propertyName, filterBindingPatternInitializers(elem.name), shouldPrintWithInitializer(elem) ? elem.initializer : undefined);
if (elem.propertyName && isIdentifier(elem.propertyName) && isIdentifier(elem.name) && !elem.symbol.isReferenced) {
// Unnecessary property renaming is forbidden in types, so remove renaming
return factory.updateBindingElement(
elem,
elem.dotDotDotToken,
/* propertyName */ undefined,
elem.propertyName,
shouldPrintWithInitializer(elem) ? elem.initializer : undefined
);
}
return factory.updateBindingElement(
elem,
elem.dotDotDotToken,
elem.propertyName,
filterBindingPatternInitializersAndRenamings(elem.name),
shouldPrintWithInitializer(elem) ? elem.initializer : undefined
);
}
}

Expand All @@ -487,7 +503,7 @@ namespace ts {
/*decorators*/ undefined,
maskModifiers(p, modifierMask),
p.dotDotDotToken,
filterBindingPatternInitializers(p.name),
filterBindingPatternInitializersAndRenamings(p.name),
resolver.isOptionalParameter(p) ? (p.questionToken || factory.createToken(SyntaxKind.QuestionToken)) : undefined,
ensureType(p, type || p.type, /*ignorePrivate*/ true), // Ignore private param props, since this type is going straight back into a param
ensureNoInitializer(p)
Expand Down
Expand Up @@ -5,7 +5,7 @@ interface Show {
>x : number
}
function f({ show: showRename = v => v }: Show) {}
>f : ({ show: showRename }: Show) => void
>f : ({ show }: Show) => void
>show : any
>showRename : (x: number) => string
>v => v : (v: number) => number
Expand All @@ -32,7 +32,7 @@ interface Nested {
>nested : Show
}
function ff({ nested: nestedRename = { show: v => v } }: Nested) {}
>ff : ({ nested: nestedRename }: Nested) => void
>ff : ({ nested }: Nested) => void
>nested : any
>nestedRename : Show
>{ show: v => v } : { show: (v: number) => number; }
Expand Down
Expand Up @@ -18,7 +18,7 @@ function f(_a, _b, _c) {


//// [declarationEmitBindingPatterns.d.ts]
declare const k: ({ x: z }: {
declare const k: ({ x }: {
x?: string;
}) => void;
declare var a: any;
Expand Down
@@ -1,7 +1,7 @@
=== tests/cases/compiler/declarationEmitBindingPatterns.ts ===
const k = ({x: z = 'y'}) => { }
>k : ({ x: z }: { x?: string; }) => void
>({x: z = 'y'}) => { } : ({ x: z }: { x?: string; }) => void
>k : ({ x }: { x?: string; }) => void
>({x: z = 'y'}) => { } : ({ x }: { x?: string; }) => void
>x : any
>z : string
>'y' : "y"
Expand Down
8 changes: 4 additions & 4 deletions tests/baselines/reference/declarationsAndAssignments.types
Expand Up @@ -417,7 +417,7 @@ function f13() {
}

function f14([a = 1, [b = "hello", { x, y: c = false }]]) {
>f14 : ([a, [b, { x, y: c }]]: [number, [string, { x: any; y?: boolean; }]]) => void
>f14 : ([a, [b, { x, y }]]: [number, [string, { x: any; y?: boolean; }]]) => void
>a : number
>1 : 1
>b : string
Expand All @@ -438,7 +438,7 @@ function f14([a = 1, [b = "hello", { x, y: c = false }]]) {
}
f14([2, ["abc", { x: 0, y: true }]]);
>f14([2, ["abc", { x: 0, y: true }]]) : void
>f14 : ([a, [b, { x, y: c }]]: [number, [string, { x: any; y?: boolean; }]]) => void
>f14 : ([a, [b, { x, y }]]: [number, [string, { x: any; y?: boolean; }]]) => void
>[2, ["abc", { x: 0, y: true }]] : [number, [string, { x: number; y: true; }]]
>2 : 2
>["abc", { x: 0, y: true }] : [string, { x: number; y: true; }]
Expand All @@ -451,7 +451,7 @@ f14([2, ["abc", { x: 0, y: true }]]);

f14([2, ["abc", { x: 0 }]]);
>f14([2, ["abc", { x: 0 }]]) : void
>f14 : ([a, [b, { x, y: c }]]: [number, [string, { x: any; y?: boolean; }]]) => void
>f14 : ([a, [b, { x, y }]]: [number, [string, { x: any; y?: boolean; }]]) => void
>[2, ["abc", { x: 0 }]] : [number, [string, { x: number; }]]
>2 : 2
>["abc", { x: 0 }] : [string, { x: number; }]
Expand All @@ -462,7 +462,7 @@ f14([2, ["abc", { x: 0 }]]);

f14([2, ["abc", { y: false }]]); // Error, no x
>f14([2, ["abc", { y: false }]]) : void
>f14 : ([a, [b, { x, y: c }]]: [number, [string, { x: any; y?: boolean; }]]) => void
>f14 : ([a, [b, { x, y }]]: [number, [string, { x: any; y?: boolean; }]]) => void
>[2, ["abc", { y: false }]] : [number, [string, { y: false; }]]
>2 : 2
>["abc", { y: false }] : [string, { y: false; }]
Expand Down
32 changes: 32 additions & 0 deletions tests/baselines/reference/destructuringInFunctionType.errors.txt
@@ -0,0 +1,32 @@
tests/cases/conformance/es6/destructuring/destructuringInFunctionType.ts(12,18): error TS2842: 'b' is an unused renaming of 'a'. Did you intend to use it as a type annotation?
tests/cases/conformance/es6/destructuring/destructuringInFunctionType.ts(12,28): error TS2842: 'a' is an unused renaming of 'b'. Did you intend to use it as a type annotation?


==== tests/cases/conformance/es6/destructuring/destructuringInFunctionType.ts (2 errors) ====
interface a { a }
interface b { b }
interface c { c }

type T1 = ([a, b, c]);
type F1 = ([a, b, c]) => void;

type T2 = ({ a });
type F2 = ({ a }) => void;

type T3 = ([{ a: b }, { b: a }]);
type F3 = ([{ a: b }, { b: a }]) => void;
~
!!! error TS2842: 'b' is an unused renaming of 'a'. Did you intend to use it as a type annotation?
!!! related TS2843 tests/cases/conformance/es6/destructuring/destructuringInFunctionType.ts:12:31: We can only write a type for '[{ a: b }, { b: a }]' by adding a type for the entire parameter here.
~
!!! error TS2842: 'a' is an unused renaming of 'b'. Did you intend to use it as a type annotation?
!!! related TS2843 tests/cases/conformance/es6/destructuring/destructuringInFunctionType.ts:12:31: We can only write a type for '[{ a: b }, { b: a }]' by adding a type for the entire parameter here.

type T4 = ([{ a: [b, c] }]);
type F4 = ([{ a: [b, c] }]) => void;

type C1 = new ([{ a: [b, c] }]) => void;

var v1 = ([a, b, c]) => "hello";
var v2: ([a, b, c]) => string;

2 changes: 1 addition & 1 deletion tests/baselines/reference/destructuringInFunctionType.js
Expand Up @@ -52,7 +52,7 @@ declare type T3 = ([{
}, {
b: a;
}]);
declare type F3 = ([{ a: b }, { b: a }]: [{
declare type F3 = ([{ a }, { b }]: [{
a: any;
}, {
b: any;
Expand Down
Expand Up @@ -31,7 +31,7 @@ type T3 = ([{ a: b }, { b: a }]);
>b : a

type F3 = ([{ a: b }, { b: a }]) => void;
>F3 : ([{ a: b }, { b: a }]: [{ a: any; }, { b: any; }]) => void
>F3 : ([{ a }, { b }]: [{ a: any; }, { b: any; }]) => void
>a : any
>b : any
>b : any
Expand Down
Expand Up @@ -402,7 +402,7 @@ d5(); // Parameter is optional as its declaration included an initializer
// Type annotations must instead be written on the top- level parameter declaration

function e1({x: number}) { } // x has type any NOT number
>e1 : ({ x: number }: { x: any; }) => void
>e1 : ({ x }: { x: any; }) => void
>x : any
>number : any

Expand Down
Expand Up @@ -402,7 +402,7 @@ d5(); // Parameter is optional as its declaration included an initializer
// Type annotations must instead be written on the top- level parameter declaration

function e1({x: number}) { } // x has type any NOT number
>e1 : ({ x: number }: { x: any; }) => void
>e1 : ({ x }: { x: any; }) => void
>x : any
>number : any

Expand Down
Expand Up @@ -376,7 +376,7 @@ d5(); // Parameter is optional as its declaration included an initializer
// Type annotations must instead be written on the top- level parameter declaration

function e1({x: number}) { } // x has type any NOT number
>e1 : ({ x: number }: { x: any; }) => void
>e1 : ({ x }: { x: any; }) => void
>x : any
>number : any

Expand Down
Expand Up @@ -7,7 +7,7 @@

// Error
function a({while}) { }
>a : ({ while: }: { while: any; }) => void
>a : ({ while }: { while: any; }) => void
>while : any
> : any

Expand Down Expand Up @@ -42,32 +42,32 @@ function a7(...a: string) { }

a({ while: 1 });
>a({ while: 1 }) : void
>a : ({ while: }: { while: any; }) => void
>a : ({ while }: { while: any; }) => void
>{ while: 1 } : { while: number; }
>while : number
>1 : 1

// No Error
function b1({public: x}) { }
>b1 : ({ public: x }: { public: any; }) => void
>b1 : ({ public }: { public: any; }) => void
>public : any
>x : any

function b2({while: y}) { }
>b2 : ({ while: y }: { while: any; }) => void
>b2 : ({ while }: { while: any; }) => void
>while : any
>y : any

b1({ public: 1 });
>b1({ public: 1 }) : void
>b1 : ({ public: x }: { public: any; }) => void
>b1 : ({ public }: { public: any; }) => void
>{ public: 1 } : { public: number; }
>public : number
>1 : 1

b2({ while: 1 });
>b2({ while: 1 }) : void
>b2 : ({ while: y }: { while: any; }) => void
>b2 : ({ while }: { while: any; }) => void
>{ while: 1 } : { while: number; }
>while : number
>1 : 1
Expand Down
24 changes: 24 additions & 0 deletions tests/baselines/reference/excessPropertyCheckWithSpread.errors.txt
@@ -0,0 +1,24 @@
tests/cases/compiler/excessPropertyCheckWithSpread.ts(1,25): error TS2842: 'number' is an unused renaming of 'a'. Did you intend to use it as a type annotation?


==== tests/cases/compiler/excessPropertyCheckWithSpread.ts (1 errors) ====
declare function f({ a: number }): void
~~~~~~
!!! error TS2842: 'number' is an unused renaming of 'a'. Did you intend to use it as a type annotation?
!!! related TS2843 tests/cases/compiler/excessPropertyCheckWithSpread.ts:1:32: We can only write a type for '{ a: number }' by adding a type for the entire parameter here.
interface I {
readonly n: number;
}
declare let i: I;
f({ a: 1, ...i });

interface R {
opt?: number
}
interface L {
opt: string
}
declare let l: L;
declare let r: R;
f({ a: 1, ...l, ...r });