Skip to content

[CodeCompletion] Update member completion to handle ambiguous and invalid base expressions #33749

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 35 commits into from
Sep 10, 2020

Conversation

nathawes
Copy link
Contributor

@nathawes nathawes commented Sep 1, 2020

This hooks up member completion to the new typeCheckForCodeCompletion API to generate completions from all solutions the constraint solver produces – including ones requiring fixes – rather than relying on a single solution being applied to the AST (if any). This lets us still produce member completion results when the base expression is ambiguous or invalid.

Whenever typeCheckExpression is called on an expression containing a code completion expression and a CompletionCallback has been set, each solution formed is passed to the callback so the type and decl of the base expression can be extracted along with any other info needed to generate member completions.

Nathan Hawes and others added 28 commits August 28, 2020 22:24
…e expression to the DotExpr completion callback.
…mpletion.

This hooks up member completion to the new typeCheckForCodeCompletion API to
generate completions from all solutions the constraint solver produces (include
ones requiring fixes) rather than relying purely the single solution being
applied to the AST (if any). This lets us still give completion results in
ambiguous and invalid code.
…expression and use it

If there was an invalid reference which was caught by pre-check,
let's remove all context besides code completion itself and use
it to produce code completion results.
…or code completion

To aid code completion, we need to attempt to convert type holes
back into underlying generic parameters if possible, since type
of the code completion expression is used as "expected" (or contextual)
type so it's helpful to know what requirements it has to filter
the list of possible member candidates e.g.

```swift
func test<T: P>(_: [T]) {}

test(42.#^MEMBERS^#)
```

It's impossible to resolve `T` in this case but code completion
expression should still have a type of `[T]` instead of `[<<hole>>]`
because it helps to produce correct contextual member list based on
a conformance requirement associated with generic parameter `T`.
…ion in cases where typeCheckExpression is never called.

This happens when, e.g. an expression being switched on is invalid so
expression patterns in the switch cases (which may contain the completion
expression) are not checked.

Also setup the Lookup object to handle member completion in ObjC selector
expressions correctly, and fix passing the wrong expression when computing
isStaticallyDerivedMetatype().
…etion`

If an expression fails pre-check or constraint generation,
code completion should be performed directly on the
`CodeCompletionExpr` as a fallback. Let's factor that logic
out from `solveForCodeCompletion` and put it directly into
`typeCheckForCodeCompletion` because it's easier to establish
fault conditions there.
…on` to operate on target instead of expression

Using `SolutionApplicationTarget` make it easier to propage
contextual information and avoid mistakes of using incorrect
accessors for constraint generation.
…f `typeCheckForCodeCompletion`

There is no way to separate viability/applicability checking from
`TypeChecker::typeCheckForCodeCompletion` because multi-statement
closures could be either type-checked together with enclosing
context (e.g. when closure represents a function builder body), or
separately - when it's just a regular closure.

Due to this "duality" we need to attempt to run code completion
code to determine whether body of the closure participated in
type-check and if it didn't, fallback to a `typeCheckExpression`.
…n to `typeCheckExpression`

This fallback to `typeCheckExpression` is triggered when it's
determined that code completion expression is located inside
of a multi-statement closure and its body is going going to
participate in type-check.
…ts test.

We were reporting methods that return function types that return void (rather
than returning void directly) as being invalid in contexts that expect non-void
expressions and testing for that incorrect behavior.
…sion

Having all information possible makes it much easier to determine
when it's appropriate to fallback to type-check code completion
expression and whether it's possible to add some more context to it.
…ayed parsing as well

We were previously only doing it when parsing up front.
…dds to single expression bodies for void-returning functions

This makes TypeCheckNodeAtLoc match the regular function body typechecking
behavior and fixes a crash in test/IDE/complete_value_expr.swift
…into `sawSolution`

Instead of exposing archetypes during solution generation, let's
do that based on solution data when a type of code completion
expression is requested by code completion callback.
Code completion expression type could be a hole if and only
if there is obosolutely no contextual information available
e.g. `let _ = .#^MEMBER^#`
…ined

If base type couldn't be determined (e.g. because base expression
is an invalid reference), let's not attempt to do a lookup since
it wouldn't produce any useful results anyway.
…tion when there's no expected type.

The tests were matching the previous implementation's output, which sometimes produced 'unknown' and sometimes 'unrelated' in cases where there was no expected type from the context.
… in the new member completion implementation.
@nathawes
Copy link
Contributor Author

nathawes commented Sep 1, 2020

@swift-ci please test

@nathawes
Copy link
Contributor Author

nathawes commented Sep 1, 2020

@swift-ci Please Build Toolchain macOS Platform

@swift-ci
Copy link
Contributor

swift-ci commented Sep 3, 2020

Build failed
Swift Test Linux Platform
Git Sha - 491b691

@nathawes nathawes marked this pull request as ready for review September 3, 2020 23:22
@nathawes nathawes requested a review from rintaro September 3, 2020 23:22
@swift-ci
Copy link
Contributor

swift-ci commented Sep 3, 2020

Build failed
Swift Test OS X Platform
Git Sha - 491b691

@rintaro
Copy link
Member

rintaro commented Sep 4, 2020

Copy link
Member

@rintaro rintaro left a comment

Choose a reason for hiding this comment

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

Looks great in general 👍

ReferencedDecl = SelectedOverload->choice.getDeclOrNull();

auto Key = std::make_pair(BaseTy, ReferencedDecl);
auto Ret = BaseToSolutionIdx.insert({Key, Solutions.size()});
Copy link
Member

Choose a reason for hiding this comment

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

Could you explain why Solutions.size() is a part of the key?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's the value here, rather than the key. If the key is not already in the BaseToSolutionIdx map we will insert a new entry into the Solutions vector, and Solutions.size() will be its index.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, sorry. I misread the code 😓

/// Called for each solution produced while type-checking an expression containing a code
/// completion expression.
void sawSolution(const constraints::Solution &solution) override;
};
Copy link
Member

Choose a reason for hiding this comment

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

I feel like we should have dedicated header for code completion specific type checking. e.g. CompletionTypeChecking.h

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense to me, particularly as we'll hopefully be adding a bunch more of these.

@@ -1334,9 +1335,12 @@ CodeCompletionResult *CodeCompletionResultBuilder::takeResult() {
}

auto typeRelation = ExpectedTypeRelation;
// FIXME: we don't actually have enough info to compute
// IsImplicitlyCurriedInstanceMethod here.
Copy link
Member

Choose a reason for hiding this comment

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

(Not in this PR) I think we should calculate it in each result building logic which knows the actual result type.

E.g.

Builder.addTypeAnnotation(resultTy, PrintOptions());
Builder.setExpectedTypeRelation(calculateMaxTypeRelation(resultTy, expectedTypes, DC));

SourceLoc DotLoc;
DeclContext *DC;
CodeCompletionExpr *CompletionExpr;
SmallVector<SolutionInfo, 4> Solutions;
Copy link
Member

Choose a reason for hiding this comment

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

Naming Solution can be confusable with constraints::Solution. But I'm not able to come up with better name 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I'm not super happy with most of the names. I was hoping to come up with something better for "CompletionCollector" and "DotExprLookup" too.

Copy link
Member

Choose a reason for hiding this comment

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

How about
CompletionCollector -> TypeCheckCompletionCallback
DotExprLookup -> DotExprTypeCheckCompletionCallback
Solution -> Result

@@ -1526,6 +1530,8 @@ void CodeCompletionContext::sortCompletionResults(
}

namespace {
class CompletionLookup;

class CodeCompletionCallbacksImpl : public CodeCompletionCallbacks {
CodeCompletionContext &CompletionContext;
std::vector<RequestedCachedModule> RequestedModules;
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 still needed in CodeCompletionCallbacksImpl?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh maybe not - thanks!

DotExprLookup Lookup(DotLoc, CurDeclContext, CodeCompleteTokenExpr);
llvm::SaveAndRestore<CompletionCollector*>
CompletionCollector(Context.CompletionCallback, &Lookup);
typeCheckContextAt(CurDeclContext, CompletionLoc);
Copy link
Member

Choose a reason for hiding this comment

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

(Not in this PR) When we migrate everything, we should rename this to something else :)
e.g.

performCompletionTypeChecking(CurDeclContext, ComletionLoc, Lookup);


// This (hopefully) only happens in cases where the expression isn't
// typechecked during normal compilation either (e.g. member completion in a
// switch case where there switched value is invalid). Having normal
Copy link
Member

Choose a reason for hiding this comment

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

According the the language reference, it's called "control expression"
https://docs.swift.org/swift-book/ReferenceManual/Statements.html#ID436

Comment on lines 741 to 745
// If there was no completion expr (e.g. if the code completion location is
// within an ErrorExpr without an valid subexpr due to parser error recovery)
// bail.
if (!contextAnalyzer.hasCompletionExpr())
return false;
Copy link
Member

Choose a reason for hiding this comment

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

The original expression in ErrorExpr is now walked into. If you know other cases, please update the comment.

Copy link
Contributor Author

@nathawes nathawes Sep 4, 2020

Choose a reason for hiding this comment

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

Right, I think this branch is impossible to get into at the moment since we're only doing this for member completion. Do you know if once we change the other completion kinds to use this approach there will ever be a case where no CodeCompletionExpr is formed within an ErrorExpr, or any other kind of expression containing the completion loc? I assumed this was possible, e.g. if the completion location is in the middle of some nonsensical tokens between statement starting tokens that are skipped over during parser recovery, but if we're always guaranteed to have a CodeCompletionExpr regardless of the completion kind this should probably be an assertion.

Copy link
Member

Choose a reason for hiding this comment

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

I don’t know off the top of my head. But I think we should keep this return false path, just to be on the safe side.

Comment on lines +770 to +779
// First, pre-check the expression, validating any types that occur in the
// expression and folding sequence expressions.
auto failedPreCheck = ConstraintSystem::preCheckExpression(
expr, DC,
/*replaceInvalidRefsWithErrors=*/true);

target.setExpr(expr);

if (failedPreCheck)
return false;
Copy link
Member

Choose a reason for hiding this comment

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

If preCheckExpression(), it falls back to normal type checking which calls preCheckExpression() again. @xedin Is it safe to call preCheckExpression() on already pre-checked expressions?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, it's effectively a noop.

/*contextualType=*/Type(),
/*isDiscarded=*/true);

(void)solveForCodeCompletion(completionTarget);
Copy link
Member

Choose a reason for hiding this comment

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

Assert the result is not CompletionResult::NotApplicable?

@@ -133,7 +133,7 @@ enum FooEnum: CaseIterable {
// FOO_ENUM_DOT_INVALID-NEXT: Decl[EnumElement]/CurrNominal: Foo1[#FooEnum#]{{; name=.+$}}
// FOO_ENUM_DOT_INVALID-NEXT: Decl[EnumElement]/CurrNominal: Foo2[#FooEnum#]{{; name=.+$}}
// FOO_ENUM_DOT_INVALID-NEXT: Decl[StaticVar]/CurrNominal: alias1[#FooEnum#]{{; name=.+$}}
// FOO_ENUM_DOT_INVALID-NEXT: Decl[InstanceMethod]/CurrNominal/TypeRelation[Invalid]: hash({#(self): FooEnum#})[#(into: inout Hasher) -> Void#]{{; name=.+$}}
// FOO_ENUM_DOT_INVALID-NEXT: Decl[InstanceMethod]/CurrNominal: hash({#(self): FooEnum#})[#(into: inout Hasher) -> Void#]{{; name=.+$}}
Copy link
Member

Choose a reason for hiding this comment

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

This makes FOO_ENUM_DOT_INVALID the same as FOO_ENUM_DOT. Just remove FOO_ENUM_DOT_INVALID and modify the FileCheck argument to use FOO_ENUM_DOT

Comment on lines 5914 to 5916
void DotExprLookup::performLookup(ide::CodeCompletionContext &CompletionCtx,
CodeCompletionConsumer &Consumer,
bool IsInSelector) const {
Copy link
Member

Choose a reason for hiding this comment

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

It's not clear to me why the logic in this function should be in DotExprLookup especially considering this is declared in Sema. How about DotExprLookup just have an accessor for Solutions and inline this logic in trySolverCompletion?

Nathan Hawes added 3 commits September 8, 2020 11:43
…all refactorings (NFC)

Also put subclasses of TypeCheckCompletionCallback into their own header.
@nathawes
Copy link
Contributor Author

nathawes commented Sep 9, 2020

@swift-ci clean test

@nathawes nathawes requested a review from rintaro September 9, 2020 00:09
@nathawes
Copy link
Contributor Author

nathawes commented Sep 9, 2020

@swift-ci please test

@swift-ci
Copy link
Contributor

swift-ci commented Sep 9, 2020

Build failed
Swift Test Linux Platform
Git Sha - d60116d

@swift-ci
Copy link
Contributor

swift-ci commented Sep 9, 2020

Build failed
Swift Test OS X Platform
Git Sha - d60116d

Comment on lines 2756 to 2770
static bool isMemberCompletion(ASTNode Node) {
struct HasMemberCompletion: public ASTWalker {
bool Value = false;
std::pair<bool, Expr *> walkToExprPre(Expr *E) override {
if (auto *CCE = dyn_cast<CodeCompletionExpr>(E)) {
Value = CCE->getBase();
return {false, nullptr};
}
return {true, E};
}
};
HasMemberCompletion Check;
Node.walk(Check);
return Check.Value;
}
Copy link
Member

Choose a reason for hiding this comment

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

Could you consolidate this with the one in ParseDecl?

@@ -0,0 +1,82 @@
//
Copy link
Member

Choose a reason for hiding this comment

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

Missing the first line?

//===--- CodeCompletionTypeChecking.h  --------------------------*- C++ -*-===//

…ParseExpr.cpp and ParseDecl.cpp

Also:
- propagate the Solution -> Result rename to Solution parameter of deliverDotExprResults
- fixup header comment in CodeCompletionTypeChecking.h
@nathawes
Copy link
Contributor Author

nathawes commented Sep 9, 2020

@swift-ci please test

@nathawes nathawes requested a review from rintaro September 9, 2020 19:22
@swift-ci
Copy link
Contributor

swift-ci commented Sep 9, 2020

Build failed
Swift Test OS X Platform
Git Sha - 7a06792

@swift-ci
Copy link
Contributor

swift-ci commented Sep 9, 2020

Build failed
Swift Test Linux Platform
Git Sha - 7a06792

@nathawes
Copy link
Contributor Author

nathawes commented Sep 9, 2020

@swift-ci please test Linux

Copy link
Member

@rintaro rintaro left a comment

Choose a reason for hiding this comment

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

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants