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

Already on GitHub? Sign in to your account

Reuse checker's property accessibility check for completions #45388

Merged
merged 11 commits into from Aug 19, 2021

Conversation

gabritto
Copy link
Member

@gabritto gabritto commented Aug 9, 2021

This is a very rough draft PR attempting to use the checker's logic for checking property accessibility when filtering visible/accessible properties that should be offered for completion.

This PR was inspired by bug #45045, caused by trying to access a type's undefined symbol when checking if we should offer a property for completions in an object binding pattern (in tryGetObjectLikeCompletions from completions).
Looking at the code in completions, I thought the type checker must already do this sort of property accessibility check and set out to see if we could reuse the checker's code for this purpose.

The checker function that does this is checkPropertyAccessibility.
This function returns a boolean saying if a property access is valid, given a property access-like node, the property's symbol, the type where the property comes from, and some options (writing if it's a write access, isSuper if we're accessing a super property e.g. super.foo).
But, it also reports an invalid property error if the reportError flag is set to true.

Based on checkPropertyAccessibility, I created the function checkPropertyAccessibilityAtNode that has the same implementation, but with two differences:

  • checkPropertyAccessibility takes as argument nodes that are property access-like only. This is enough for type checking purposes, and also if we are in a completions scenario where we have an actual property access (e.g. foo.|), because then we have such a property access-like node. But, in the case of offering completions for an object binding pattern, we are trying to figure out if a property is accessible when we don't yet have a property access-like (i.e. a binding element) node. To that end, the new function checkPropertyAccessibilityAtNode accepts any Node, indicating the location in the AST where we'd want to access the property.
  • unlike checkPropertyAccessibility, checkPropertyAccessibilityAtNode does not report errors, but instead, if the property access is invalid, it returns diagnostics info (diagnostic message + arguments).

I changed checkPropertyAccessibility to call the new checkPropertyAccessibilityAtNode and report an error (i.e. call error(...)) depending on the returned value.
I also added a boolean-returning wrapper over the checkPropertyAccessibilityAtNode function (isPropertyAccessible), exposed by the checker and used by completions.

I only replaced the existing property accessibility completions-specific code to use the new isPropertyAccessible function in tryGetObjectLikeCompletions, but there might be other places that could use this new checker function.

The variable/function/type names and overall code structure is in no way ideal, but I wanted to know if the idea makes any sense before investing more time on making things pretty. I'm also very open to suggestions on alternative ways to achieve the same thing, or just explanations on why that sort of reuse shouldn't be done.

@typescript-bot typescript-bot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Aug 9, 2021
@@ -4354,6 +4354,7 @@ namespace ts {

/* @internal */ getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol: Symbol): readonly TypeParameter[] | undefined;
/* @internal */ isDeclarationVisible(node: Declaration | AnyImportSyntax): boolean;
/* @internal */ isPropertyAccessible(node: Node, isSuper: boolean, writing: boolean, type: Type, prop: Symbol): boolean;
Copy link
Member Author

Choose a reason for hiding this comment

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

I'm guessing this should be internal, unless we add some code here that determines what value isSuper and writing should be, and possibly get the type or the property symbol ourselves instead of as arguments to avoid incorrect usages of the function.

Copy link
Member

Choose a reason for hiding this comment

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

I would definitely start this as internal. When are isSuper and writing true? I think calculating them for the caller is a good idea.

I had another idea in a comment in completions.ts

Copy link
Member Author

Choose a reason for hiding this comment

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

Decided this function shouldn't (and actually can't) calculate isSuper and isWrite. This function does not assume we have an existing property access-y node, which we would need to calculate those arguments, so the caller should specify what are the appropriate arguments.

Copy link
Member

@sandersn sandersn left a comment

Choose a reason for hiding this comment

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

I think making completions use the same algorithm as the checker is a good idea.

src/compiler/checker.ts Outdated Show resolved Hide resolved
src/compiler/checker.ts Outdated Show resolved Hide resolved
src/compiler/checker.ts Show resolved Hide resolved
src/services/completions.ts Show resolved Hide resolved
@@ -4354,6 +4354,7 @@ namespace ts {

/* @internal */ getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol: Symbol): readonly TypeParameter[] | undefined;
/* @internal */ isDeclarationVisible(node: Declaration | AnyImportSyntax): boolean;
/* @internal */ isPropertyAccessible(node: Node, isSuper: boolean, writing: boolean, type: Type, prop: Symbol): boolean;
Copy link
Member

Choose a reason for hiding this comment

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

I would definitely start this as internal. When are isSuper and writing true? I think calculating them for the caller is a good idea.

I had another idea in a comment in completions.ts

src/compiler/checker.ts Show resolved Hide resolved
@DanielRosenwasser
Copy link
Member

I agree with Nathan - this functionality shouldn't move too much around in the checker, and should instead keep consistent with the existing styles.

}
// In js files properties of unions are allowed in completion
Copy link
Member Author

Choose a reason for hiding this comment

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

Deleted this special casing of JS files because it's already dealt with in completions by addTypeProperties.

@@ -4216,7 +4216,7 @@ namespace ts {

getConstantValue(node: EnumMember | PropertyAccessExpression | ElementAccessExpression): string | number | undefined;
isValidPropertyAccess(node: PropertyAccessExpression | QualifiedName | ImportTypeNode, propertyName: string): boolean;
/** Exclude accesses to private properties or methods with a `this` parameter that `type` doesn't satisfy. */
Copy link
Member Author

Choose a reason for hiding this comment

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

Updating comment since the implementation of this function no longer verifies this type for performance reasons (see #31377).

@@ -28128,19 +28175,45 @@ namespace ts {
propertyName: __String,
type: Type): boolean {

// Short-circuiting for improved performance.
Copy link
Member Author

Choose a reason for hiding this comment

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

Copied this from the existing isValidPropertyAccessWithType function.
It seemed to be there for performance reasons, doesn't look like it would change behavior, so I added it assuming someone had a good reason to write it in the first place.

@gabritto
Copy link
Member Author

On the second pass here, I reviewed the checker functions used by completions related to checking property accessibility.
The existing functions, isValidPropertyAccess (public), isValidPropertyAccessForCompletions (marked as internal), and isValidPropertyAccessWithType (helper called by isValidPropertyAccess), are all called by completions when we have something like foo.|, to try to validate an existing property access-like node (e.g. in foo.|, we'd have a PropertyAccess node with empty string as property name). This differs from isPropertyAccessible (newly added), in that isPropertyAccessible does not assume we already have a property access-like node, and uses the node exclusively as a location.
The existing isValidPropertyAccess... functions also have slightly different parameter (e.g. one takes in a property symbol and the other a property name), so I decided to just change their implementations to call the new isPropertyAccessible function where possible. Further refactoring would probably require also refactoring completions code.

@gabritto gabritto marked this pull request as ready for review August 16, 2021 22:42
@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.

@gabritto gabritto linked an issue Aug 16, 2021 that may be closed by this pull request
@gabritto gabritto requested a review from sandersn August 17, 2021 16:52
* @param location The location node where we want to check if the property is accessible.
* @param isSuper True if the access is from `super.`.
* @param writing True if this is a write property access, false if it is a read property access.
* @param type The type of the object whose property is being accessed. (Not the type of the property.)
Copy link
Member

Choose a reason for hiding this comment

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

minor: I've seen containingType as a pretty good name for this.

@@ -27291,11 +27292,29 @@ namespace ts {
node: PropertyAccessExpression | QualifiedName | PropertyAccessExpression | VariableDeclaration | ParameterDeclaration | ImportTypeNode | PropertyAssignment | ShorthandPropertyAssignment | BindingElement,
isSuper: boolean, writing: boolean, type: Type, prop: Symbol, reportError = true): boolean {

const flags = getDeclarationModifierFlagsFromSymbol(prop, writing);
const errorNode = node.kind === SyntaxKind.QualifiedName ? node.right :
const errorNode = !reportError ? undefined : node.kind === SyntaxKind.QualifiedName ? node.right :
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
const errorNode = !reportError ? undefined : node.kind === SyntaxKind.QualifiedName ? node.right :
const errorNode = !reportError ? undefined :
node.kind === SyntaxKind.QualifiedName ? node.right :

@gabritto gabritto changed the title Draft: reuse checker's property accessibility check for completions Reuse checker's property accessibility check for completions Aug 19, 2021
@gabritto gabritto merged commit 945179f into main Aug 19, 2021
@gabritto gabritto deleted the gabritto/issue45045 branch August 19, 2021 20:02
BobobUnicorn pushed a commit to BobobUnicorn/TypeScript that referenced this pull request Oct 24, 2021
…ft#45388)

* add test for completions crash

* WIP: experiment

* remove comments

* add call to getParseTreeNode

* Revert "add call to getParseTreeNode"

This reverts commit 8dd1cf6.

* Fix comments

* minor fixes

* fix formatting

* rename type to containingType
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
For Uncommitted Bug PR for untriaged, rejected, closed or missing bug
Projects
None yet
Development

Successfully merging this pull request may close these issues.

class method destructuring assignment autocomplete not work
4 participants