Skip to content

Fix use-before-def errors for ESNext property declarations #36465

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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1326,12 +1326,14 @@ namespace ts {
}
else if (isPropertyDeclaration(declaration)) {
// still might be illegal if a self-referencing property initializer (eg private x = this.x)
return !isPropertyImmediatelyReferencedWithinDeclaration(declaration, usage);
return !isPropertyImmediatelyReferencedWithinDeclaration(declaration, usage, /*stopAtAnyPropertyDeclaration*/ false);
}
else if (isParameterPropertyDeclaration(declaration, declaration.parent)) {
const container = getEnclosingBlockScopeContainer(declaration.parent);
// foo = this.bar is illegal in esnext+useDefineForClassFields when bar is a parameter property
return !(compilerOptions.target === ScriptTarget.ESNext && !!compilerOptions.useDefineForClassFields
Copy link
Member

Choose a reason for hiding this comment

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

For some reason my brain wants this expression to be De Morgan’d, or maybe for whole function itself to return use-before-declared, not declared-before-used. Oh well.

Copy link
Member Author

Choose a reason for hiding this comment

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

the WHOLE time I was working on this, except every change made things harder to read.

&& isUsedInFunctionOrInstanceProperty(usage, declaration));
&& getContainingClass(declaration) === getContainingClass(usage)
&& isUsedInFunctionOrInstanceProperty(usage, declaration, container));
}
return true;
}
Expand All @@ -1357,10 +1359,19 @@ namespace ts {
}

const container = getEnclosingBlockScopeContainer(declaration);
return !!(usage.flags & NodeFlags.JSDoc)
|| isInTypeQuery(usage)
|| isUsedInFunctionOrInstanceProperty(usage, declaration, container)
&& !(compilerOptions.target === ScriptTarget.ESNext && !!compilerOptions.useDefineForClassFields);
if (!!(usage.flags & NodeFlags.JSDoc) || isInTypeQuery(usage)) {
Copy link
Member

Choose a reason for hiding this comment

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

I bet this condition actually could be the same as isValidTypeOnlyAliasUseSite. But probably not worth changing.

return true;
}
if (isUsedInFunctionOrInstanceProperty(usage, declaration, container)) {
if (compilerOptions.target === ScriptTarget.ESNext && !!compilerOptions.useDefineForClassFields) {
return (isPropertyDeclaration(declaration) || isParameterPropertyDeclaration(declaration, declaration.parent)) &&
!isPropertyImmediatelyReferencedWithinDeclaration(declaration, usage, /*stopAtAnyPropertyDeclaration*/ true);
}
else {
return true;
}
}
return false;

function isImmediatelyUsedInInitializerOfBlockScopedVariable(declaration: VariableDeclaration, usage: Node): boolean {
const container = getEnclosingBlockScopeContainer(declaration);
Expand Down Expand Up @@ -1412,7 +1423,8 @@ namespace ts {
});
}

function isPropertyImmediatelyReferencedWithinDeclaration(declaration: PropertyDeclaration, usage: Node) {
/** stopAtAnyPropertyDeclaration is used for detecting ES-standard class field use-before-def errors */
function isPropertyImmediatelyReferencedWithinDeclaration(declaration: PropertyDeclaration | ParameterPropertyDeclaration, usage: Node, stopAtAnyPropertyDeclaration: boolean) {
// always legal if usage is after declaration
if (usage.end > declaration.end) {
return false;
Expand All @@ -1427,8 +1439,13 @@ namespace ts {

switch (node.kind) {
case SyntaxKind.ArrowFunction:
case SyntaxKind.PropertyDeclaration:
return true;
case SyntaxKind.PropertyDeclaration:
Copy link
Member

Choose a reason for hiding this comment

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

I can’t quite figure out what this means without stepping through one case that needs stopAtAllPropertyDeclarations as true and one that needs false, but are there any tests with a class expression as the initializer of a property declaration?

class Foo {
  Bar = class extends Foo {
    p2 = this.p1
  }
  p1 = 0
}

(I don’t know, maybe there’s a more creative combo of these horrors)

Copy link
Member Author

Choose a reason for hiding this comment

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

There is one:

class B2<V> {
    anon = class extends A<V> { }
}

Your example works on 3.7, so needs to keep working on 3.8:

https://www.typescriptlang.org/play/index.html#code/MYGwhgzhAECC0G8BQ1XQELQLzVJGApgB4AuBAdgCYzzJr3QAOATNtCQBYCWEAdIwEYUaAL7DUgtgAYkYgG5gATtDBtyBAO5wAFAEokwAPbkIhkAV4hDAc21h+A-QuVgARms0re6PQeOnzSxs7Vwd9IA

Back to the drawing board I guess. What a test case!

Copy link
Member

Choose a reason for hiding this comment

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

Commit to fix this looks good 👍

// even when stopping at any property declaration, they need to come from the same class
return stopAtAnyPropertyDeclaration &&
(isPropertyDeclaration(declaration) && node.parent === declaration.parent
|| isParameterPropertyDeclaration(declaration, declaration.parent) && node.parent === declaration.parent.parent)
? "quit": true;
case SyntaxKind.Block:
switch (node.parent.kind) {
case SyntaxKind.GetAccessor:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ tests/cases/conformance/classes/propertyMemberDeclarations/assignParameterProper
m1() {
this.foo // ok
}
constructor(private foo: string) {}
constructor(public foo: string) {}
quim = this.baz // should error
~~~
!!! error TS2729: Property 'baz' is used before its initialization.
Expand All @@ -32,4 +32,27 @@ tests/cases/conformance/classes/propertyMemberDeclarations/assignParameterProper
this.foo // ok
}
}

class D extends C {
quill = this.foo // ok
}

class E {
bar = () => this.foo1 + this.foo2; // both ok
foo1 = '';
constructor(public foo2: string) {}
}

class F {
Inner = class extends F {
p2 = this.p1
}
p1 = 0
}
class G {
Inner = class extends G {
p2 = this.p1
}
constructor(public p1: number) {}
}

Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,37 @@ class C {
m1() {
this.foo // ok
}
constructor(private foo: string) {}
constructor(public foo: string) {}
quim = this.baz // should error
baz = this.foo; // should error
quid = this.baz // ok
m2() {
this.foo // ok
}
}

class D extends C {
quill = this.foo // ok
}

class E {
bar = () => this.foo1 + this.foo2; // both ok
foo1 = '';
constructor(public foo2: string) {}
}

class F {
Inner = class extends F {
p2 = this.p1
}
p1 = 0
}
class G {
Inner = class extends G {
p2 = this.p1
}
constructor(public p1: number) {}
}


//// [assignParameterPropertyToPropertyDeclarationESNext.js]
Expand All @@ -35,3 +58,29 @@ class C {
this.foo; // ok
}
}
class D extends C {
quill = this.foo; // ok
}
class E {
foo2;
bar = () => this.foo1 + this.foo2; // both ok
foo1 = '';
constructor(foo2) {
this.foo2 = foo2;
}
}
class F {
Inner = class extends F {
p2 = this.p1;
};
p1 = 0;
}
class G {
p1;
Inner = class extends G {
p2 = this.p1;
};
constructor(p1) {
this.p1 = p1;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ class C {
>this : Symbol(C, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 0, 0))
>foo : Symbol(C.foo, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 7, 16))
}
constructor(private foo: string) {}
constructor(public foo: string) {}
>foo : Symbol(C.foo, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 7, 16))

quim = this.baz // should error
>quim : Symbol(C.quim, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 7, 39))
>quim : Symbol(C.quim, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 7, 38))
>this.baz : Symbol(C.baz, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 8, 19))
>this : Symbol(C, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 0, 0))
>baz : Symbol(C.baz, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 8, 19))
Expand All @@ -59,3 +59,66 @@ class C {
}
}

class D extends C {
>D : Symbol(D, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 14, 1))
>C : Symbol(C, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 0, 0))

quill = this.foo // ok
>quill : Symbol(D.quill, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 16, 19))
>this.foo : Symbol(C.foo, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 7, 16))
>this : Symbol(D, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 14, 1))
>foo : Symbol(C.foo, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 7, 16))
}

class E {
>E : Symbol(E, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 18, 1))

bar = () => this.foo1 + this.foo2; // both ok
>bar : Symbol(E.bar, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 20, 9))
>this.foo1 : Symbol(E.foo1, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 21, 38))
>this : Symbol(E, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 18, 1))
>foo1 : Symbol(E.foo1, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 21, 38))
>this.foo2 : Symbol(E.foo2, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 23, 16))
>this : Symbol(E, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 18, 1))
>foo2 : Symbol(E.foo2, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 23, 16))

foo1 = '';
>foo1 : Symbol(E.foo1, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 21, 38))

constructor(public foo2: string) {}
>foo2 : Symbol(E.foo2, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 23, 16))
}

class F {
>F : Symbol(F, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 24, 1))

Inner = class extends F {
>Inner : Symbol(F.Inner, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 26, 9))
>F : Symbol(F, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 24, 1))

p2 = this.p1
>p2 : Symbol((Anonymous class).p2, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 27, 29))
>this.p1 : Symbol(F.p1, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 29, 5))
>this : Symbol((Anonymous class), Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 27, 11))
>p1 : Symbol(F.p1, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 29, 5))
}
p1 = 0
>p1 : Symbol(F.p1, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 29, 5))
}
class G {
>G : Symbol(G, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 31, 1))

Inner = class extends G {
>Inner : Symbol(G.Inner, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 32, 9))
>G : Symbol(G, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 31, 1))

p2 = this.p1
>p2 : Symbol((Anonymous class).p2, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 33, 29))
>this.p1 : Symbol(G.p1, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 36, 16))
>this : Symbol((Anonymous class), Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 33, 11))
>p1 : Symbol(G.p1, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 36, 16))
}
constructor(public p1: number) {}
>p1 : Symbol(G.p1, Decl(assignParameterPropertyToPropertyDeclarationESNext.ts, 36, 16))
}

Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class C {
>this : this
>foo : string
}
constructor(private foo: string) {}
constructor(public foo: string) {}
>foo : string

quim = this.baz // should error
Expand Down Expand Up @@ -59,3 +59,72 @@ class C {
}
}

class D extends C {
>D : D
>C : C

quill = this.foo // ok
>quill : string
>this.foo : string
>this : this
>foo : string
}

class E {
>E : E

bar = () => this.foo1 + this.foo2; // both ok
>bar : () => string
>() => this.foo1 + this.foo2 : () => string
>this.foo1 + this.foo2 : string
>this.foo1 : string
>this : this
>foo1 : string
>this.foo2 : string
>this : this
>foo2 : string

foo1 = '';
>foo1 : string
>'' : ""

constructor(public foo2: string) {}
>foo2 : string
}

class F {
>F : F

Inner = class extends F {
>Inner : typeof (Anonymous class)
>class extends F { p2 = this.p1 } : typeof (Anonymous class)
>F : F

p2 = this.p1
>p2 : number
>this.p1 : number
>this : this
>p1 : number
}
p1 = 0
>p1 : number
>0 : 0
}
class G {
>G : G

Inner = class extends G {
>Inner : typeof (Anonymous class)
>class extends G { p2 = this.p1 } : typeof (Anonymous class)
>G : G

p2 = this.p1
>p2 : number
>this.p1 : number
>this : this
>p1 : number
}
constructor(public p1: number) {}
>p1 : number
}

Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,34 @@ class C {
m1() {
this.foo // ok
}
constructor(private foo: string) {}
constructor(public foo: string) {}
quim = this.baz // should error
baz = this.foo; // should error
quid = this.baz // ok
m2() {
this.foo // ok
}
}

class D extends C {
quill = this.foo // ok
}

class E {
bar = () => this.foo1 + this.foo2; // both ok
foo1 = '';
constructor(public foo2: string) {}
}

class F {
Inner = class extends F {
p2 = this.p1
}
p1 = 0
}
class G {
Inner = class extends G {
p2 = this.p1
}
constructor(public p1: number) {}
}