Skip to content

Conversation

z0gSh1u
Copy link
Contributor

@z0gSh1u z0gSh1u commented Apr 13, 2021

Fixes #43650
and more.

This is my first pull-request here. Help and suggestions are appreciated.

All these various situations are fixed, expanding the case in original issue:

  • implicit any property, property with type, property with initializer...
  • literal initializer, object initializer, array initializer, identifier initializer, function execution initializer...
  • fully present constructor, partly present constructor, no presence...
  • different semicolon styles...
  • w/ or w/o semicolon or newline...
  • and so on.

Please refer to the test case for detail:

tests/cases/fourslash/completionConstructorKeywordAfterPropertyDeclaration.ts

@typescript-bot typescript-bot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Apr 13, 2021
@typescript-bot
Copy link
Collaborator

This PR doesn't have any linked issues. Please open an issue that references this PR. From there we can discuss and prioritise.

@ghost
Copy link

ghost commented Apr 13, 2021

CLA assistant check
All CLA requirements met.

@typescript-bot typescript-bot added For Backlog Bug PRs that fix a backlog bug and removed For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels Apr 13, 2021
@z0gSh1u z0gSh1u changed the title Complete constructor keyword after property declaration [WIP] Complete constructor keyword after property declaration Apr 13, 2021
@z0gSh1u z0gSh1u changed the title [WIP] Complete constructor keyword after property declaration Complete constructor keyword after property declaration Apr 13, 2021
@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Apr 13, 2021

This seems like an oddly-specific fix, and I'm not sure if it really identifies the root problem. I think we need to understand why we weren't offering up completions before.

@z0gSh1u
Copy link
Contributor Author

z0gSh1u commented Apr 14, 2021

In my opinion, this is a missing case of getting completions during class declaration.

The function tryGetObjectTypeDeclarationCompletionContainer checks the contextToken to give a completion.

switch (contextToken.kind) {

However, it doesn't consider the implicit any case like:

class Foo {
   bar // <- context token is identifier when w/o semicolon
   // bar; // <- context token is semicolon when with semicolon
   constructor/**/()
}

since there is no switch-case for contextToken as an Identifier. And the switch-case for SemicolonToken doesn't cover this situation either.

So it falls back to default, and checks the contextToken using

function isClassMemberCompletionKeyword(kind: SyntaxKind) {

This function doesn't consider the situation that contextToken is an Identifier either (but does for like AsyncKeyword), resulting no suggestion return for completions.

What's more, for case with a TypeReference like:

class Foo {
  bar: number // <- context token w/o semicolon
  consturctor/**/()
}

The completion is blocked beforehand by

isSolelyIdentifierDefinitionLocation(contextToken) ||

which is in fact a different problem from the previous one.

The fix for this is made referring to a similar issue:

// Don't block completions if we're in `class C /**/`, because we're *past* the end of the identifier and might want to complete `extends`.

@andrewbranch
Copy link
Member

You need to actually check for an ASI-triggering newline if you want to prevent a completion at

class C {
  blah /*| constructor*/
}

I don’t know how important that is, though.

@z0gSh1u
Copy link
Contributor Author

z0gSh1u commented Apr 21, 2021

Hi, I'm not very sure of what you mean. I'm basically allowing instead of preventing completions. And the behavior for cursor at that position should be unchanged. Do these changes affect ASI in some way?


////class A {
//// bla
//// constructor/*1*/(props) {}
Copy link
Member

Choose a reason for hiding this comment

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

The presence of the full constructor keyword is changing the behavior of the completions. It seems like what you really want to test is

class A {
  bla
  /**/
}

and

class A {
  bla
  co/**/
}

right? Those currently don’t work.

Copy link
Member

Choose a reason for hiding this comment

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

We’ll have to revisit my comment about newlines after this issue is fixed—you’re right that the behavior is currently unchanged, but I think it’s because of this issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Got it. This is a more universal situation. And I've found that completions are not working either for snippet like this:

class C {
  blah = 123 // property with an immediate assignment
  constructor/**/
}

I'll take a deeper dive into these problems.

@z0gSh1u
Copy link
Contributor Author

z0gSh1u commented Apr 22, 2021

Updated. New fix available now.

@andrewbranch
Copy link
Member

@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Apr 22, 2021

Heya @andrewbranch, I've started to run the tarball bundle task on this PR at 59756d3. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Apr 22, 2021

Hey @andrewbranch, I've packed this into an installable tgz. You can install it for testing by referencing it in your package.json like so:

{
    "devDependencies": {
        "typescript": "https://typescript.visualstudio.com/cf7ac146-d525-443c-b23c-0d58337efebc/_apis/build/builds/101548/artifacts?artifactName=tgz&fileId=0893DA32FEBAAB115A1A95CFDCF527ABB9460D922231E4A45C3D77411A8B80EE02&fileName=/typescript-4.3.0-insiders.20210422.tgz"
    }
}

and then running npm install.


There is also a playground for this build and an npm module you can use via "typescript": "npm:@typescript-deploys/[email protected]".;

@andrewbranch
Copy link
Member

Ok, so this is what I meant about checking newlines: https://www.staging-typescript.org/play?ts=4.3.0-pr-43654-9#code/MYGwhgzhAEDC0G8BQ1oCNwAtrAPQCp9ckBfIA

If you trigger completions at the comment, you now get a completion for constructor which is invalid due to the lack of a semicolon or newline. Because of ASI, either a semicolon or a newline makes constructor a valid next token, but it’s not valid here.

@z0gSh1u
Copy link
Contributor Author

z0gSh1u commented Apr 23, 2021

I've adopted a validation using newline or semicolon. The completions for these two cases are blocked now as the original behavior in 4.3.0-beta:

class A {
  blah /*|con*/
}
class B {
  blah: number /*|con*/
}

And constructor will not be suggested for this:

class C {
  blah
  /**/
}

since

class C {
  blah
   = 123
}

is legal. Unless user types a c at that position.

During the debugging, I've found another issue of property with an initializer that might be controversial. In 4.3.0-beta, completions of private / protected / public / constructor can be triggered like this:

class C {
  blah = blah /*|p*/
}
// PLAYGROUND: https://www.staging-typescript.org/play?jsx=0&ts=4.3.0-beta#code/MYGwhgzhAEDC0G8BQ1oCNwAtoF51egAcB6AKlOKQF8g

It looks unlike an intended one. But I'm not sure and haven't fix this since it is guaranteed by some test cases like tests\cases\fourslash\completionsClassPropertiesAssignment.ts.

Comment on lines 2374 to 2375
return getLinesBetweenPositions(sourceFile, contextToken.end, previousToken.end) > 0 ||
contextToken.kind === SyntaxKind.SemicolonToken;
Copy link
Member

Choose a reason for hiding this comment

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

Slightly cheaper:

Suggested change
return getLinesBetweenPositions(sourceFile, contextToken.end, previousToken.end) > 0 ||
contextToken.kind === SyntaxKind.SemicolonToken;
return contextToken.kind === SyntaxKind.SemicolonToken ||
positionsAreOnSameLine(contextToken.end, previousToken.end, sourceFile);

Copy link
Member

Choose a reason for hiding this comment

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

Also, can you move this function declaration outside of the flow of its parent function? Our loosely-enforced convention is to put helper functions at the bottom of their parent, but it looks like this one could move outside of isSolelyIdentifierDefinitionLocation entirely.

Copy link
Contributor Author

@z0gSh1u z0gSh1u Apr 23, 2021

Choose a reason for hiding this comment

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

Got it.

+ return contextToken.kind === SyntaxKind.SemicolonToken ||
+     !positionsAreOnSameLine(contextToken.end, previousToken.end, sourceFile);
      ^~~~~~

exactly.

Comment on lines 2867 to 2870
isPropertyBeforeConstructorInitalized(getAncestor(location, SyntaxKind.ClassDeclaration) as ClassDeclaration, location.parent as ClassElement) ||
contextToken.kind === SyntaxKind.SemicolonToken
)) {
return getAncestor(contextToken, SyntaxKind.ClassDeclaration) as ObjectTypeDeclaration;
Copy link
Member

Choose a reason for hiding this comment

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

I believe we will not produce a ConstructorKeyword outside a class (it will just be an Identifier if it appears), but the class node could be a ClassExpression, not only a ClassDeclaration.

Copy link
Contributor Author

@z0gSh1u z0gSh1u Apr 23, 2021

Choose a reason for hiding this comment

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

Got it. Replaced using isClassLike to predicate.

// class c { prop: number \n constructor| }
// class c { prop = somewhat \n constructor| }
// and also for `;` replacing `\n`
if (location.kind === SyntaxKind.ConstructorKeyword && (
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 this whole condition applies only once you’ve typed the entire constructor keyword, and stops applying as soon as you’ve typed the open parenthesis afterwards, which is not really a time when completions are likely to be triggered, or when they will be valuable. This might be technically correct, but I’m confused about what purpose it’s serving.

Copy link
Contributor Author

@z0gSh1u z0gSh1u Apr 23, 2021

Choose a reason for hiding this comment

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

This is the fix for the most original issue. #43650

Manually triggered completion (Ctrl+Space in Windows) doesn't offer constructor for this:

class C {
  blah // w/ w/o semicolon, type, initializer
  constructor/**/ // must be fully present
}

It looks like that the full presence of constructor is changing the behavior of completions in some way.

The cause seems to be a missing case in tryGetObjectTypeDeclarationCompletionContainer. Removing this will result in no suggestion. But the conditions here are unnecessarily complex. I'm trying to simplify it.

Copy link
Member

Choose a reason for hiding this comment

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

I think I disagree with the premise that #43650 is worth more than a line of code to fix. From the original issue opened against VS Code, it looks like someone was just hunting for inconsistencies, not experiencing a legitimate UX issue. I’m not against fixing it, but I am against fixing it if no one can explain why it matters and it takes a multiline if statement, a bunch of explanatory comments, and a dozen fourslash tests.

Copy link
Contributor Author

@z0gSh1u z0gSh1u Apr 23, 2021

Choose a reason for hiding this comment

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

😆 Yep. The latest fix is of 2 LOC.

&& isClassLike(previousToken.parent.parent)
&& isPropertyDeclaration(contextToken.parent)
// After considering different contextToken...
&& (isIdentifier(contextToken) || isTypeKeyword(contextToken.kind))
Copy link
Member

Choose a reason for hiding this comment

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

Is this condition necessary/correct? If the previous property declaration has a type annotation or an initializer, there are lots of ways it might be something other than an identifier or type keyword. But those cases seem to work already... are we able to just drop this line?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Correct. This is already supported by some other code. Cases like

type SomeType = number
class C {
  blah: SomeType; /**/
}

are already working in 4.3.0-beta.

//// }
//// class R {
//// blah
//// /*18*/ // dont complete since `blah \n = 123` is legal
Copy link
Member

Choose a reason for hiding this comment

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

But constructor is also legal here, so why not offer it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's right.

//// class G {
//// blah = 123; con/*7*/
//// }
//// // situations that `constructor` is fully present
Copy link
Member

Choose a reason for hiding this comment

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

All of these are fine to have, and correct, but as far as I understand, essentially pointless. I don’t think they’re particularly important to test, and I definitely think that little to no implementation code should be written to make these cases pass.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Kindly refer to #43654 (comment) maybe.

@z0gSh1u
Copy link
Contributor Author

z0gSh1u commented Apr 23, 2021

constructor should not but is now still suggested at:

class T {
  blah: number con/**/
}
const SomeConst = 123
class U {
  blah = SomeConst con/**/
}

I'm still working on this.

@andrewbranch
Copy link
Member

The simplifications are looking great. Let me know when you’re ready for another review. Thanks!

@z0gSh1u
Copy link
Contributor Author

z0gSh1u commented Apr 24, 2021

The fix should be well-rounded and reliable enough now. Please refer to the test case:

https://github.com/microsoft/TypeScript/blob/70adf96ed0b9445ec0468ff038588247f87a7c81/tests/cases/fourslash/completionConstructorKeywordAfterPropertyDeclaration.ts

for detail.

What's more, we've got a byproduct that, for 15-21 in the test case of "situations that constructor should not be suggested", completions for private / public / protected are also blocked, which should be more reasonable comparing to 4.3.0-beta. (See #43654 (comment))

At last, kindly ping @andrewbranch .

@@ -2370,6 +2370,31 @@ namespace ts.Completions {
return isPropertyDeclaration(contextToken.parent);
}

// If `constructor` is totally not present, but we request a completion manually at a space...
if (contextToken === previousToken && isPreviousPropertyDeclarationTerminated(contextToken, position)) {
return false; // Don't block completions.
Copy link
Member

Choose a reason for hiding this comment

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

Do we even know that the current location is in a class right here?

Copy link
Contributor Author

@z0gSh1u z0gSh1u Apr 27, 2021

Choose a reason for hiding this comment

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

Agree. Narrowing this fix to inside a ClassLike should be more reliable. I've edited the code.

Copy link
Member

@andrewbranch andrewbranch left a comment

Choose a reason for hiding this comment

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

Thanks!

@andrewbranch andrewbranch merged commit 4ecb563 into microsoft:master Apr 29, 2021
@z0gSh1u
Copy link
Contributor Author

z0gSh1u commented Apr 30, 2021

Thanks for your help. Have a nice day!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
For Backlog Bug PRs that fix a backlog bug
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

Constructor keyword not suggested
4 participants