diff --git a/src/analysis/typepal/Collector.rsc b/src/analysis/typepal/Collector.rsc index 7b50d40..e3635f0 100644 --- a/src/analysis/typepal/Collector.rsc +++ b/src/analysis/typepal/Collector.rsc @@ -1016,7 +1016,7 @@ Collector newCollector(str modelName, map[str,Tree] namedTrees, TypePalConfig co throw TypePalUsage("Missing `leaveScope`(s): unclosed scopes "); } - tm = tmodel(); + tm = tmodel()[usesPhysicalLocs=true]; tm.modelName = modelName; tm.moduleLocs = (nm : getLoc(namedTrees[nm]) | nm <- namedTrees); diff --git a/src/analysis/typepal/ConfigurableScopeGraph.rsc b/src/analysis/typepal/ConfigurableScopeGraph.rsc index 02f6636..3204020 100644 --- a/src/analysis/typepal/ConfigurableScopeGraph.rsc +++ b/src/analysis/typepal/ConfigurableScopeGraph.rsc @@ -15,6 +15,7 @@ import Map; import util::Reflective; import String; extend ParseTree; +import analysis::typepal::StringSimilarity; public loc anonymousOccurrence = |rascal-typepal:///anonymous_occurrence|(0,1,<2,3>,<2,4>); @@ -79,6 +80,8 @@ loc defaultLogicalLoc(Define def, str _modelName, PathConfig _pcfg){ return def.defined; // return original and don't create logical location } +list[str] defaultSimilarNames(Use u, TModel tm) = similarNames(u, tm); + // Extends TypePalConfig defined in analysis::typepal::ScopeGraph data TypePalConfig( @@ -137,7 +140,13 @@ data TypePalConfig( bool(loc def, TModel tm) reportUnused = defaultReportUnused, - loc (Define def, str modelName, PathConfig pcfg) createLogicalLoc = defaultLogicalLoc + loc (Define def, str modelName, PathConfig pcfg) createLogicalLoc = defaultLogicalLoc, + + list[str] (Use u, TModel tm) similarNames = defaultSimilarNames, + + bool enableErrorFixes = true, + + int cutoffForNameSimilarity = 3 ); diff --git a/src/analysis/typepal/Solver.rsc b/src/analysis/typepal/Solver.rsc index e5bfe24..813d220 100644 --- a/src/analysis/typepal/Solver.rsc +++ b/src/analysis/typepal/Solver.rsc @@ -3,21 +3,22 @@ module analysis::typepal::Solver /* Implementation of the ISolver interface; this is the API of TypePal's constraint solver */ -import Set; -import Node; -import Map; +extend analysis::typepal::Collector; +extend analysis::typepal::Messenger; + +import Exception; import IO; import List; import Location; +import Map; +import Message; +import Node; import ParseTree; -import Type; +import Set; import String; -import Message; -import Exception; -//import util::Benchmark; - -extend analysis::typepal::Collector; -extend analysis::typepal::Messenger; +import Type; +import analysis::typepal::StringSimilarity; +import util::IDEServices; void checkAllTypesAvailable(TModel tm){ for(tup: <- tm.defines){ @@ -1517,13 +1518,21 @@ Solver newSolver(map[str,Tree] namedTrees, TModel tm){ foundDefs = scopeGraph.lookup(u); } catch NoBinding(): { roles = size(u.idRoles) > 5 ? "" : intercalateOr([prettyRole(idRole) | idRole <- u.idRoles]); - messages += error("Undefined ``", u.occ); + msg = error("Undefined ``", u.occ); + if(tm.config.enableErrorFixes){ + msg.fixes = undefinedNameProposals(u, tm); + } + messages += msg; } } for(u <- notYetDefinedUses){ roles = size(u.idRoles) > 5 ? "" : intercalateOr([prettyRole(idRole) | idRole <- u.idRoles]); - messages += error("Undefined ``", u.occ); + msg = error("Undefined ``", u.occ); + if(tm.config.enableErrorFixes){ + msg.fixes = undefinedNameProposals(u, tm); + } + messages += msg; } error_locations = { src | error(_,loc src) <- messages }; @@ -1704,3 +1713,14 @@ Solver newSolver(map[str,Tree] namedTrees, TModel tm){ return thisSolver; } + +// CodeActions for errors generated by Solver + +list[CodeAction] undefinedNameProposals(Use u, TModel tm) + = + [ action( + title="Replace undefined ``", + edits=[changed([replace(u.occ, prop)])] + ) + | str prop <- tm.config.similarNames(u, tm) + ]; \ No newline at end of file diff --git a/src/analysis/typepal/StringSimilarity.rsc b/src/analysis/typepal/StringSimilarity.rsc new file mode 100644 index 0000000..af44681 --- /dev/null +++ b/src/analysis/typepal/StringSimilarity.rsc @@ -0,0 +1,66 @@ +module analysis::typepal::StringSimilarity + +import List; +import IO; +import Location; +import Set; +import String; +import analysis::typepal::TModel; +import analysis::typepal::ConfigurableScopeGraph; + +@synopsis{Tryadic minimum function on integers} +int min(int a, int b, int c) += a < b ? (a < c ? a : c) : (b < c ? b : c); + +@synopsis{Calculate the Levenshtein distance of 2 strings} +int lev(str a, str b){ + int sizea = size(a); + int sizeb = size(b); + + @memo{expireAfter(minutes=1),maximumSize(50)} + int lev(int ia, int ib){ + if(ib == sizeb) return sizea - ia; + if(ia == sizea) return sizeb - ib; + if(a[ia] == b[ib]) return lev(ia+1, ib+1); + + return 1 + min(lev(ia+1, ib), + lev(ia, ib+1), + lev(ia+1, ib+1)); + } + + return lev(0, 0); +} + +// Tests for `lev` + +test bool levCommutative(str a, str b) = lev(a, b) == lev(b, a); + +test bool levLeftAdditive(str a, str b, str c) = lev(a, b) == lev(c + a, c + b); + +test bool lev1() = lev("kitten", "sitting") == 3; +test bool lev2() = lev("kelm", "hello") == 3; +test bool lev3() = lev("hello", "hella") == 1; +test bool lev4() = lev("hello", "") == 5; +test bool lev5() = lev("", "hello") == 5; +test bool lev6() = lev("aap", "noot") == 4; +test bool lev7() = lev("page", "pope") == 2; +test bool lev8() = lev("december", "january") == 8; +test bool lev9() = lev("march", "may") == 3; + +// Similarity functions to be used by TypePal + +@synopsis{WordSim represents one word from the vocabulary and its similariy to the original word} +alias WordSim = tuple[str word, int sim]; + +@synopsis{Compute list of words from vocabulary, that are similar to give word w with at most maxDistance edits} +list[str] similarWords(str w, list[str] vocabulary, int maxDistance) += sort({ | str v <- vocabulary, d := lev(w, v), d <= maxDistance }, + bool (WordSim x, WordSim y){ return x.sim < y.sim;}).word; + +@synopsis{Find in TModel tm, names similar to Use u. Max edit distance comes from TypePal Configuration.} +list[str] similarNames(Use u, TModel tm){ + w = getOrgId(u); + idRoles = u.idRoles; + vocabulary = [ d.orgId | d <- tm.defines, d.idRole in idRoles, isContainedIn(u.occ, d.scope) ]; + return similarWords(w, vocabulary, tm.config.cutoffForNameSimilarity); +}