Skip to content

Commit 3fc2291

Browse files
committed
Add basic typo correction for unqualified lookup.
There's a lot of room for better QoI / performance here.
1 parent dba16ee commit 3fc2291

21 files changed

+467
-26
lines changed

include/swift/AST/DiagnosticsSema.def

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,8 @@ ERROR(use_undeclared_type,none,
549549
"use of undeclared type %0", (Identifier))
550550
ERROR(use_undeclared_type_did_you_mean,none,
551551
"use of undeclared type %0; did you mean to use '%1'?", (Identifier, StringRef))
552+
NOTE(note_typo_candidate,none,
553+
"did you mean '%0'?", (StringRef))
552554
NOTE(note_remapped_type,none,
553555
"did you mean to use '%0'?", (StringRef))
554556
ERROR(identifier_init_failure,none,

include/swift/AST/NameLookup.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,22 @@ class VisibleDeclConsumer {
158158
virtual void foundDecl(ValueDecl *VD, DeclVisibilityKind Reason) = 0;
159159
};
160160

161+
/// An implementation of VisibleDeclConsumer that's built from a lambda.
162+
template <class Fn>
163+
class LambdaDeclConsumer : public VisibleDeclConsumer {
164+
Fn Callback;
165+
public:
166+
LambdaDeclConsumer(Fn &&callback) : Callback(std::move(callback)) {}
167+
168+
void foundDecl(ValueDecl *VD, DeclVisibilityKind reason) {
169+
Callback(VD, reason);
170+
}
171+
};
172+
template <class Fn>
173+
LambdaDeclConsumer<Fn> makeDeclConsumer(Fn &&callback) {
174+
return LambdaDeclConsumer<Fn>(std::move(callback));
175+
}
176+
161177
/// A consumer that inserts found decls into an externally-owned SmallVector.
162178
class VectorDeclConsumer : public VisibleDeclConsumer {
163179
virtual void anchor() override;

include/swift/Basic/TopCollection.h

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
//===--- TopCollection.h - A size-limiting top-N collection -----*- C++ -*-===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
//
13+
// This file defines the TopCollection class, a data structure which,
14+
// given a size limit, keeps the best-scoring (i.e. lowest) N values
15+
// added to it.
16+
//
17+
// The current implementation of this is only suited for small values
18+
// of maxSize.
19+
//
20+
//===----------------------------------------------------------------------===//
21+
22+
#ifndef SWIFT_BASIC_TOPCOLLECTION_H
23+
#define SWIFT_BASIC_TOPCOLLECTION_H
24+
25+
#include "swift/Basic/LLVM.h"
26+
#include "llvm/ADT/SmallVector.h"
27+
28+
namespace swift {
29+
30+
template <class ScoreType, class T, unsigned InlineCapacity = 16>
31+
class TopCollection {
32+
public:
33+
struct Entry {
34+
ScoreType Score;
35+
T Value;
36+
};
37+
38+
private:
39+
SmallVector<Entry, InlineCapacity> Data;
40+
41+
unsigned MaxSize;
42+
unsigned EndOfAccepted = 0;
43+
public:
44+
explicit TopCollection(unsigned maxSize) : MaxSize(maxSize) {
45+
assert(maxSize > 0 && "creating collection with a maximum size of 0?");
46+
Data.reserve(maxSize);
47+
}
48+
49+
// The invariants work fine with these.
50+
TopCollection(const TopCollection &other) = default;
51+
TopCollection &operator=(const TopCollection &other) = default;
52+
53+
// We need to clear EndOfAccepted to preserve the invariants here.
54+
TopCollection(TopCollection &&other)
55+
: Data(std::move(other.Data)),
56+
MaxSize(other.MaxSize),
57+
EndOfAccepted(other.EndOfAccepted) {
58+
other.EndOfAccepted = 0;
59+
}
60+
TopCollection &operator=(TopCollection &&other) {
61+
Data = std::move(other.Data);
62+
MaxSize = other.MaxSize;
63+
EndOfAccepted = other.EndOfAccepted;
64+
other.EndOfAccepted = 0;
65+
return *this;
66+
}
67+
68+
~TopCollection() {}
69+
70+
bool empty() const {
71+
return EndOfAccepted == 0;
72+
}
73+
74+
size_t size() const {
75+
return EndOfAccepted;
76+
}
77+
78+
using iterator = const Entry *;
79+
iterator begin() const { return Data.begin(); }
80+
iterator end() const { return Data.begin() + EndOfAccepted; }
81+
82+
/// Return a score beyond which scores are uninteresting. Inserting
83+
/// a value with this score will never change the collection.
84+
ScoreType getMinUninterestingScore(ScoreType defaultBound) const {
85+
assert(EndOfAccepted <= MaxSize);
86+
assert(EndOfAccepted <= Data.size());
87+
88+
// If we've accepted as many values as we can, then all scores up (and
89+
// including) that value are interesting.
90+
if (EndOfAccepted == MaxSize)
91+
return Data[EndOfAccepted - 1].Score + 1;
92+
93+
// Otherwise, if there are values in the collection that we've rejected,
94+
// any score up to that is still interesting.
95+
if (EndOfAccepted != Data.size())
96+
return Data[EndOfAccepted].Score;
97+
98+
// Otherwise, use the default bound.
99+
return defaultBound;
100+
}
101+
102+
/// Try to add a scored value to the collection.
103+
///
104+
/// \return true if the insertion was successful
105+
bool insert(ScoreType score, T &&value) {
106+
assert(EndOfAccepted <= MaxSize);
107+
assert(EndOfAccepted <= Data.size());
108+
109+
// Find the index of the last entry whose score is larger than 'score'.
110+
auto i = EndOfAccepted;
111+
while (i != 0 && score < Data[i - 1].Score)
112+
--i;
113+
114+
assert(0 <= i && i <= EndOfAccepted);
115+
assert(i == 0 || score >= Data[i - 1].Score);
116+
117+
// If we skipped anything, it's definitely safe to insert.
118+
if (i != EndOfAccepted) {
119+
// fall through
120+
121+
// If there's a tie with the previous thing, we might have to declare
122+
// an existing tier off-bounds.
123+
} else if (i != 0 && score == Data[i - 1].Score) {
124+
// Only if there isn't space to expand the tier.
125+
if (i == MaxSize) {
126+
for (--i; i != 0; --i) {
127+
if (Data[i - 1].Score != Data[i].Score)
128+
break;
129+
}
130+
EndOfAccepted = i;
131+
return false;
132+
}
133+
134+
// Otherwise, there's a non-trivial prefix that the new element has a
135+
// strictly higher score than. We can still insert if there's space.
136+
} else {
137+
// Don't insert if there's no room.
138+
if (i == MaxSize)
139+
return false;
140+
141+
// Don't insert if we're at least as high as things we've previously
142+
// rejected.
143+
if (i != Data.size() && score >= Data[i].Score)
144+
return false;
145+
}
146+
147+
// We don't care about any of the actual values after EndOfAccepted
148+
// *except* that we need to remember the minimum value following
149+
// EndOfAccepted if that's less than MaxSize so that we continue to
150+
// drop values with that score.
151+
//
152+
// Note that all of the values between EndOfAccepted and MaxSize
153+
// should have the same score, beause otherwise there's a tier we
154+
// shouldn't have marked dead.
155+
156+
// Just overwrite the next element instead of inserting if possible.
157+
if (i == EndOfAccepted && i != Data.size()) {
158+
Data[i].Score = score;
159+
Data[i].Value = std::move(value);
160+
} else {
161+
if (Data.size() == MaxSize)
162+
Data.pop_back();
163+
Data.insert(Data.begin() + i, { score, std::move(value) });
164+
}
165+
166+
EndOfAccepted++;
167+
assert(EndOfAccepted <= Data.size());
168+
assert(EndOfAccepted <= MaxSize);
169+
return true;
170+
}
171+
172+
/// Drop any values which a score more than the given value from the
173+
/// minimum score.
174+
template <class Range>
175+
void filterMaxScoreRange(Range difference) {
176+
if (EndOfAccepted < 2) return;
177+
for (unsigned i = 1; i != EndOfAccepted; ++i) {
178+
if (Data[i].Score > Data[0].Score + difference) {
179+
EndOfAccepted = i;
180+
return;
181+
}
182+
}
183+
184+
}
185+
};
186+
187+
} // end namespace swift
188+
189+
#endif // SWIFT_BASIC_CLUSTEREDBITVECTOR_H

lib/Sema/TypeCheckConstraints.cpp

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -450,11 +450,28 @@ resolveDeclRefExpr(UnresolvedDeclRefExpr *UDRE, DeclContext *DC) {
450450
// If we failed lookup of an operator, check to see it to see if it is
451451
// because two operators are juxtaposed e.g. (x*-4) that needs whitespace.
452452
// If so, emit specific diagnostics for it.
453-
if (!diagnoseOperatorJuxtaposition(UDRE, DC, *this)) {
454-
diagnose(Loc, diag::use_unresolved_identifier, Name,
455-
UDRE->getName().isOperator())
456-
.highlight(UDRE->getSourceRange());
453+
if (diagnoseOperatorJuxtaposition(UDRE, DC, *this)) {
454+
return new (Context) ErrorExpr(UDRE->getSourceRange());
455+
}
456+
457+
// TODO: Name will be a compound name if it was written explicitly as
458+
// one, but we should also try to propagate labels into this.
459+
DeclNameLoc nameLoc = UDRE->getNameLoc();
460+
461+
performTypoCorrection(DC, UDRE->getRefKind(), Name, Loc, LookupOptions,
462+
Lookup);
463+
464+
diagnose(Loc, diag::use_unresolved_identifier, Name, Name.isOperator())
465+
.highlight(UDRE->getSourceRange());
466+
467+
// Note all the correction candidates.
468+
for (auto &result : Lookup) {
469+
noteTypoCorrection(Name, nameLoc, result);
457470
}
471+
472+
// TODO: consider recovering from here. We may want some way to suppress
473+
// downstream diagnostics, though.
474+
458475
return new (Context) ErrorExpr(UDRE->getSourceRange());
459476
}
460477

lib/Sema/TypeCheckNameLookup.cpp

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
//===----------------------------------------------------------------------===//
1818
#include "TypeChecker.h"
1919
#include "swift/AST/NameLookup.h"
20+
#include "swift/Basic/TopCollection.h"
2021
#include <algorithm>
2122

2223
using namespace swift;
@@ -415,3 +416,126 @@ LookupResult TypeChecker::lookupConstructors(DeclContext *dc, Type type,
415416
NameLookupOptions options) {
416417
return lookupMember(dc, type, Context.Id_init, options);
417418
}
419+
420+
enum : unsigned {
421+
/// Never consider a candidate that's this distance away or worse.
422+
UnreasonableCallEditDistance = 8,
423+
424+
/// Don't consider candidates that score worse than the given distance
425+
/// from the best candidate.
426+
MaxCallEditDistanceFromBestCandidate = 1
427+
};
428+
429+
static unsigned getCallEditDistance(DeclName argName, DeclName paramName,
430+
unsigned maxEditDistance) {
431+
// TODO: consider arguments.
432+
// TODO: maybe ignore certain kinds of missing / present labels for the
433+
// first argument label?
434+
// TODO: word-based rather than character-based?
435+
StringRef argBase = argName.getBaseName().str();
436+
StringRef paramBase = paramName.getBaseName().str();
437+
438+
unsigned distance = argBase.edit_distance(paramBase, maxEditDistance);
439+
440+
// Bound the distance to UnreasonableCallEditDistance.
441+
if (distance >= maxEditDistance ||
442+
distance > (paramBase.size() + 2) / 3) {
443+
return UnreasonableCallEditDistance;
444+
}
445+
446+
return distance;
447+
}
448+
449+
static bool isPlausibleTypo(DeclRefKind refKind, DeclName typedName,
450+
ValueDecl *candidate) {
451+
// Ignore anonymous declarations.
452+
if (!candidate->hasName())
453+
return false;
454+
455+
// An operator / identifier mismatch is never a plausible typo.
456+
auto fn = dyn_cast<FuncDecl>(candidate);
457+
if (typedName.isOperator() != (fn && fn->isOperator()))
458+
return false;
459+
if (!typedName.isOperator())
460+
return true;
461+
462+
// TODO: honor ref kind? This is trickier than it sounds because we
463+
// may not have processed attributes and types on the candidate yet.
464+
return true;
465+
}
466+
467+
void TypeChecker::performTypoCorrection(DeclContext *DC, DeclRefKind refKind,
468+
DeclName targetDeclName,
469+
SourceLoc nameLoc,
470+
NameLookupOptions lookupOptions,
471+
LookupResult &result,
472+
unsigned maxResults) {
473+
// Fill in a collection of the most reasonable entries.
474+
TopCollection<unsigned, ValueDecl*> entries(maxResults);
475+
auto consumer = makeDeclConsumer([&](ValueDecl *decl,
476+
DeclVisibilityKind reason) {
477+
// Never match an operator with an identifier or vice-versa; this is
478+
// not a plausible typo.
479+
if (!isPlausibleTypo(refKind, targetDeclName, decl))
480+
return;
481+
482+
// Don't suggest a variable within its own initializer.
483+
if (auto var = dyn_cast<VarDecl>(decl)) {
484+
if (auto binding = var->getParentPatternBinding()) {
485+
if (!binding->isImplicit() &&
486+
Context.SourceMgr.rangeContainsTokenLoc(binding->getSourceRange(),
487+
nameLoc))
488+
return;
489+
}
490+
}
491+
492+
// Don't waste time computing edit distances that are more than
493+
// the worst in our collection.
494+
unsigned maxDistance =
495+
entries.getMinUninterestingScore(UnreasonableCallEditDistance);
496+
497+
unsigned distance =
498+
getCallEditDistance(targetDeclName, decl->getFullName(), maxDistance);
499+
500+
// Ignore values that are further than a reasonable distance.
501+
if (distance >= UnreasonableCallEditDistance)
502+
return;
503+
504+
entries.insert(distance, std::move(decl));
505+
});
506+
507+
lookupVisibleDecls(consumer, DC, nullptr, /*top level*/ true, nameLoc);
508+
509+
// Impose a maximum distance from the best score.
510+
entries.filterMaxScoreRange(MaxCallEditDistanceFromBestCandidate);
511+
512+
for (auto &entry : entries)
513+
result.add({ entry.Value, nullptr });
514+
}
515+
516+
static InFlightDiagnostic
517+
diagnoseTypoCorrection(TypeChecker &tc, DeclNameLoc loc, ValueDecl *decl) {
518+
if (auto var = dyn_cast<VarDecl>(decl)) {
519+
// Suggest 'self' at the use point instead of pointing at the start
520+
// of the function.
521+
if (var->isSelfParameter())
522+
return tc.diagnose(loc.getBaseNameLoc(), diag::note_typo_candidate,
523+
decl->getName().str());
524+
}
525+
526+
return tc.diagnose(decl, diag::note_typo_candidate, decl->getName().str());
527+
}
528+
529+
void TypeChecker::noteTypoCorrection(DeclName writtenName, DeclNameLoc loc,
530+
const LookupResult::Result &suggestion) {
531+
auto decl = suggestion.Decl;
532+
auto &&diagnostic = diagnoseTypoCorrection(*this, loc, decl);
533+
534+
DeclName declName = decl->getFullName();
535+
536+
if (writtenName.getBaseName() != declName.getBaseName())
537+
diagnostic.fixItReplace(loc.getBaseNameLoc(), declName.getBaseName().str());
538+
539+
// TODO: add fix-its for typo'ed argument labels. This is trickier
540+
// because of the reordering rules.
541+
}

0 commit comments

Comments
 (0)