Skip to content

Commit e53f19f

Browse files
authored
Issue "Cannot find name did-you-mean" errors as suggestions in plain JS (#44271)
* Always issue cannot find name did-you-mean error This PR issues "cannot find ${name}, did you mean ${name}" errors for identifiers and propery access expressions in JS files *without* `// @ts-check` and without `// @ts-nocheck`. This brings some benefits of Typescript's binder to all Javascript users, even those who haven't opted into Typescript checking. ```js export var inModule = 1 inmodule.toFixed() // errors on exports function f() { var locals = 2 locale.toFixed() // errors on locals } var object = { spaaace: 3 } object.spaaaace // error on read object.spaace = 2 // error on write object.fresh = 12 // OK, no spelling correction to offer ``` To disable the errors, add `// @ts-nocheck` to the file. To get the normal checkJs experience, add `// @ts-check`. == Why This Works == In a word: precision. This change has low recall — it misses lots of correct errors that would be nice to show — but it has high precision: almost all the errors it shows are correct. And they come with a suggested correction. Here are the ingredients: 1. For unchecked JS files, the compiler suppresses all errors except two did-you-mean name resolution errors. 2. Did-you-mean spelling correction is already tuned for high precision/low recall, and doesn't show many bogus errors even in JS. 3. For identifiers, the error is suppressed for suggestions from global files. These are often DOM feature detection, for example. 4. For property accesses, the error is suppressed for suggestions from other files, for the same reason. 5. For property accesses, the error is suppressed for `this` property accesses because the compiler doesn't understand JS constructor functions well enough. In particular, it doesn't understand any inheritance patterns. == Work Remaining == 1. Code cleanup. 2. Fix a couple of failures in existing tests. 3. Suppress errors on property access suggestions from large objects. 4. Combine (3) and (4) above to suppress errors on suggestions from other, global files. 5. A little more testing on random files to make sure that precision is good there too. 6. Have people from the regular Code editor meeting test the code and suggest ideas. * all (most?) tests pass * NOW they all pass * add tonnes of semi-colons * restore this.x check+add a test case * make ts-ignore/no-check codefix work in unchecked js * Issues errors only in the language service * add a few more tests * fix incorrect parentheses * More cleanup in program.ts * Improve readability of isExcludedJSError * make diff in program.ts smaller via closure * Switch unchecked JS did-you-mean to suggestion Instead of selectively letting errors through. * undo more missed changes * disallow ignoring suggestions * Issue different messages for plain JS than others Straw text for the messages, I just changed the modals to avoid name collisions.
1 parent 5be0d71 commit e53f19f

21 files changed

+603
-27
lines changed

Diff for: src/compiler/builder.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -683,7 +683,7 @@ namespace ts {
683683
const cachedDiagnostics = state.semanticDiagnosticsPerFile.get(path);
684684
// Report the bind and check diagnostics from the cache if we already have those diagnostics present
685685
if (cachedDiagnostics) {
686-
return filterSemanticDiagnotics(cachedDiagnostics, state.compilerOptions);
686+
return filterSemanticDiagnostics(cachedDiagnostics, state.compilerOptions);
687687
}
688688
}
689689

@@ -692,7 +692,7 @@ namespace ts {
692692
if (state.semanticDiagnosticsPerFile) {
693693
state.semanticDiagnosticsPerFile.set(path, diagnostics);
694694
}
695-
return filterSemanticDiagnotics(diagnostics, state.compilerOptions);
695+
return filterSemanticDiagnostics(diagnostics, state.compilerOptions);
696696
}
697697

698698
export type ProgramBuildInfoFileId = number & { __programBuildInfoFileIdBrand: any };

Diff for: src/compiler/checker.ts

+46-17
Original file line numberDiff line numberDiff line change
@@ -1072,15 +1072,19 @@ namespace ts {
10721072
return diagnostic;
10731073
}
10741074

1075-
function error(location: Node | undefined, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): Diagnostic {
1076-
const diagnostic = location
1075+
function createError(location: Node | undefined, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): Diagnostic {
1076+
return location
10771077
? createDiagnosticForNode(location, message, arg0, arg1, arg2, arg3)
10781078
: createCompilerDiagnostic(message, arg0, arg1, arg2, arg3);
1079+
}
1080+
1081+
function error(location: Node | undefined, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): Diagnostic {
1082+
const diagnostic = createError(location, message, arg0, arg1, arg2, arg3);
10791083
diagnostics.add(diagnostic);
10801084
return diagnostic;
10811085
}
10821086

1083-
function addErrorOrSuggestion(isError: boolean, diagnostic: DiagnosticWithLocation) {
1087+
function addErrorOrSuggestion(isError: boolean, diagnostic: Diagnostic) {
10841088
if (isError) {
10851089
diagnostics.add(diagnostic);
10861090
}
@@ -1704,8 +1708,8 @@ namespace ts {
17041708
nameArg: __String | Identifier | undefined,
17051709
isUse: boolean,
17061710
excludeGlobals = false,
1707-
suggestedNameNotFoundMessage?: DiagnosticMessage): Symbol | undefined {
1708-
return resolveNameHelper(location, name, meaning, nameNotFoundMessage, nameArg, isUse, excludeGlobals, getSymbol, suggestedNameNotFoundMessage);
1711+
issueSuggestions?: boolean): Symbol | undefined {
1712+
return resolveNameHelper(location, name, meaning, nameNotFoundMessage, nameArg, isUse, excludeGlobals, getSymbol, issueSuggestions);
17091713
}
17101714

17111715
function resolveNameHelper(
@@ -1716,8 +1720,7 @@ namespace ts {
17161720
nameArg: __String | Identifier | undefined,
17171721
isUse: boolean,
17181722
excludeGlobals: boolean,
1719-
lookup: typeof getSymbol,
1720-
suggestedNameNotFoundMessage?: DiagnosticMessage): Symbol | undefined {
1723+
lookup: typeof getSymbol, issueSuggestions?: boolean): Symbol | undefined {
17211724
const originalLocation = location; // needed for did-you-mean error reporting, which gathers candidates starting from the original location
17221725
let result: Symbol | undefined;
17231726
let lastLocation: Node | undefined;
@@ -2054,15 +2057,19 @@ namespace ts {
20542057
!checkAndReportErrorForUsingNamespaceModuleAsValue(errorLocation, name, meaning) &&
20552058
!checkAndReportErrorForUsingValueAsType(errorLocation, name, meaning)) {
20562059
let suggestion: Symbol | undefined;
2057-
if (suggestedNameNotFoundMessage && suggestionCount < maximumSuggestionCount) {
2060+
if (issueSuggestions && suggestionCount < maximumSuggestionCount) {
20582061
suggestion = getSuggestedSymbolForNonexistentSymbol(originalLocation, name, meaning);
2059-
const isGlobalScopeAugmentationDeclaration = suggestion && suggestion.valueDeclaration && isAmbientModule(suggestion.valueDeclaration) && isGlobalScopeAugmentation(suggestion.valueDeclaration);
2062+
const isGlobalScopeAugmentationDeclaration = suggestion?.valueDeclaration && isAmbientModule(suggestion.valueDeclaration) && isGlobalScopeAugmentation(suggestion.valueDeclaration);
20602063
if (isGlobalScopeAugmentationDeclaration) {
20612064
suggestion = undefined;
20622065
}
20632066
if (suggestion) {
20642067
const suggestionName = symbolToString(suggestion);
2065-
const diagnostic = error(errorLocation, suggestedNameNotFoundMessage, diagnosticName(nameArg!), suggestionName);
2068+
const isUncheckedJS = isUncheckedJSSuggestion(originalLocation, suggestion, /*excludeClasses*/ false);
2069+
const message = isUncheckedJS ? Diagnostics.Could_not_find_name_0_Did_you_mean_1 : Diagnostics.Cannot_find_name_0_Did_you_mean_1;
2070+
const diagnostic = createError(errorLocation, message, diagnosticName(nameArg!), suggestionName);
2071+
addErrorOrSuggestion(!isUncheckedJS, diagnostic);
2072+
20662073
if (suggestion.valueDeclaration) {
20672074
addRelatedInfo(
20682075
diagnostic,
@@ -21923,7 +21930,7 @@ namespace ts {
2192321930
node,
2192421931
!isWriteOnlyAccess(node),
2192521932
/*excludeGlobals*/ false,
21926-
Diagnostics.Cannot_find_name_0_Did_you_mean_1) || unknownSymbol;
21933+
/*issueSuggestions*/ true) || unknownSymbol;
2192721934
}
2192821935
return links.resolvedSymbol;
2192921936
}
@@ -27437,7 +27444,8 @@ namespace ts {
2743727444
if (!prop) {
2743827445
const indexInfo = !isPrivateIdentifier(right) && (assignmentKind === AssignmentKind.None || !isGenericObjectType(leftType) || isThisTypeParameter(leftType)) ? getIndexInfoOfType(apparentType, IndexKind.String) : undefined;
2743927446
if (!(indexInfo && indexInfo.type)) {
27440-
if (isJSLiteralType(leftType)) {
27447+
const isUncheckedJS = isUncheckedJSSuggestion(node, leftType.symbol, /*excludeClasses*/ true);
27448+
if (!isUncheckedJS && isJSLiteralType(leftType)) {
2744127449
return anyType;
2744227450
}
2744327451
if (leftType.symbol === globalThisSymbol) {
@@ -27450,7 +27458,7 @@ namespace ts {
2745027458
return anyType;
2745127459
}
2745227460
if (right.escapedText && !checkAndReportErrorForExtendingInterface(node)) {
27453-
reportNonexistentProperty(right, isThisTypeParameter(leftType) ? apparentType : leftType);
27461+
reportNonexistentProperty(right, isThisTypeParameter(leftType) ? apparentType : leftType, isUncheckedJS);
2745427462
}
2745527463
return errorType;
2745627464
}
@@ -27483,6 +27491,26 @@ namespace ts {
2748327491
return getFlowTypeOfAccessExpression(node, prop, propType, right, checkMode);
2748427492
}
2748527493

27494+
/**
27495+
* Determines whether a did-you-mean error should be a suggestion in an unchecked JS file.
27496+
* Only applies to unchecked JS files without checkJS, // @ts-check or // @ts-nocheck
27497+
* It does not suggest when the suggestion:
27498+
* - Is from a global file that is different from the reference file, or
27499+
* - (optionally) Is a class, or is a this.x property access expression
27500+
*/
27501+
function isUncheckedJSSuggestion(node: Node | undefined, suggestion: Symbol | undefined, excludeClasses: boolean): boolean {
27502+
const file = getSourceFileOfNode(node);
27503+
if (file) {
27504+
if (compilerOptions.checkJs === undefined && file.checkJsDirective === undefined && (file.scriptKind === ScriptKind.JS || file.scriptKind === ScriptKind.JSX)) {
27505+
const declarationFile = forEach(suggestion?.declarations, getSourceFileOfNode);
27506+
return !(file !== declarationFile && !!declarationFile && isGlobalSourceFile(declarationFile))
27507+
&& !(excludeClasses && suggestion && suggestion.flags & SymbolFlags.Class)
27508+
&& !(!!node && excludeClasses && isPropertyAccessExpression(node) && node.expression.kind === SyntaxKind.ThisKeyword);
27509+
}
27510+
}
27511+
return false;
27512+
}
27513+
2748627514
function getFlowTypeOfAccessExpression(node: ElementAccessExpression | PropertyAccessExpression | QualifiedName, prop: Symbol | undefined, propType: Type, errorNode: Node, checkMode: CheckMode | undefined) {
2748727515
// Only compute control flow type if this is a property access expression that isn't an
2748827516
// assignment target, and the referenced property was declared as a variable, property,
@@ -27614,7 +27642,7 @@ namespace ts {
2761427642
return getIntersectionType(x);
2761527643
}
2761627644

27617-
function reportNonexistentProperty(propNode: Identifier | PrivateIdentifier, containingType: Type) {
27645+
function reportNonexistentProperty(propNode: Identifier | PrivateIdentifier, containingType: Type, isUncheckedJS: boolean) {
2761827646
let errorInfo: DiagnosticMessageChain | undefined;
2761927647
let relatedInfo: Diagnostic | undefined;
2762027648
if (!isPrivateIdentifier(propNode) && containingType.flags & TypeFlags.Union && !(containingType.flags & TypeFlags.Primitive)) {
@@ -27647,7 +27675,8 @@ namespace ts {
2764727675
const suggestion = getSuggestedSymbolForNonexistentProperty(propNode, containingType);
2764827676
if (suggestion !== undefined) {
2764927677
const suggestedName = symbolName(suggestion);
27650-
errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_2, missingProperty, container, suggestedName);
27678+
const message = isUncheckedJS ? Diagnostics.Property_0_may_not_exist_on_type_1_Did_you_mean_2 : Diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_2;
27679+
errorInfo = chainDiagnosticMessages(errorInfo, message, missingProperty, container, suggestedName);
2765127680
relatedInfo = suggestion.valueDeclaration && createDiagnosticForNode(suggestion.valueDeclaration, Diagnostics._0_is_declared_here, suggestedName);
2765227681
}
2765327682
else {
@@ -27663,7 +27692,7 @@ namespace ts {
2766327692
if (relatedInfo) {
2766427693
addRelatedInfo(resultDiagnostic, relatedInfo);
2766527694
}
27666-
diagnostics.add(resultDiagnostic);
27695+
addErrorOrSuggestion(!isUncheckedJS, resultDiagnostic);
2766727696
}
2766827697

2766927698
function containerSeemsToBeEmptyDomElement(containingType: Type) {
@@ -34582,7 +34611,7 @@ namespace ts {
3458234611

3458334612
const rootName = getFirstIdentifier(typeName);
3458434613
const meaning = (typeName.kind === SyntaxKind.Identifier ? SymbolFlags.Type : SymbolFlags.Namespace) | SymbolFlags.Alias;
34585-
const rootSymbol = resolveName(rootName, rootName.escapedText, meaning, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isRefernce*/ true);
34614+
const rootSymbol = resolveName(rootName, rootName.escapedText, meaning, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isReference*/ true);
3458634615
if (rootSymbol
3458734616
&& rootSymbol.flags & SymbolFlags.Alias
3458834617
&& symbolIsValue(rootSymbol)

Diff for: src/compiler/diagnosticMessages.json

+8
Original file line numberDiff line numberDiff line change
@@ -2442,10 +2442,18 @@
24422442
"category": "Error",
24432443
"code": 2567
24442444
},
2445+
"Property '{0}' may not exist on type '{1}'. Did you mean '{2}'?": {
2446+
"category": "Error",
2447+
"code": 2568
2448+
},
24452449
"Type '{0}' is not an array type or a string type. Use compiler option '--downlevelIteration' to allow iterating of iterators.": {
24462450
"category": "Error",
24472451
"code": 2569
24482452
},
2453+
"Could not find name '{0}'. Did you mean '{1}'?": {
2454+
"category": "Error",
2455+
"code": 2570
2456+
},
24492457
"Object is of type 'unknown'.": {
24502458
"category": "Error",
24512459
"code": 2571

Diff for: src/compiler/program.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1896,7 +1896,7 @@ namespace ts {
18961896

18971897
function getSemanticDiagnosticsForFile(sourceFile: SourceFile, cancellationToken: CancellationToken | undefined): readonly Diagnostic[] {
18981898
return concatenate(
1899-
filterSemanticDiagnotics(getBindAndCheckDiagnosticsForFile(sourceFile, cancellationToken), options),
1899+
filterSemanticDiagnostics(getBindAndCheckDiagnosticsForFile(sourceFile, cancellationToken), options),
19001900
getProgramDiagnostics(sourceFile)
19011901
);
19021902
}
@@ -1918,8 +1918,8 @@ namespace ts {
19181918
const isCheckJs = isCheckJsEnabledForFile(sourceFile, options);
19191919
const isTsNoCheck = !!sourceFile.checkJsDirective && sourceFile.checkJsDirective.enabled === false;
19201920
// By default, only type-check .ts, .tsx, 'Deferred' and 'External' files (external files are added by plugins)
1921-
const includeBindAndCheckDiagnostics = !isTsNoCheck && (sourceFile.scriptKind === ScriptKind.TS || sourceFile.scriptKind === ScriptKind.TSX ||
1922-
sourceFile.scriptKind === ScriptKind.External || isCheckJs || sourceFile.scriptKind === ScriptKind.Deferred);
1921+
const includeBindAndCheckDiagnostics = !isTsNoCheck && (sourceFile.scriptKind === ScriptKind.TS || sourceFile.scriptKind === ScriptKind.TSX
1922+
|| sourceFile.scriptKind === ScriptKind.External || isCheckJs || sourceFile.scriptKind === ScriptKind.Deferred);
19231923
const bindDiagnostics: readonly Diagnostic[] = includeBindAndCheckDiagnostics ? sourceFile.bindDiagnostics : emptyArray;
19241924
const checkDiagnostics = includeBindAndCheckDiagnostics ? typeChecker.getDiagnostics(sourceFile, cancellationToken) : emptyArray;
19251925

@@ -3894,7 +3894,7 @@ namespace ts {
38943894
}
38953895

38963896
/*@internal*/
3897-
export function filterSemanticDiagnotics(diagnostic: readonly Diagnostic[], option: CompilerOptions): readonly Diagnostic[] {
3897+
export function filterSemanticDiagnostics(diagnostic: readonly Diagnostic[], option: CompilerOptions): readonly Diagnostic[] {
38983898
return filter(diagnostic, d => !d.skippedOn || !option[d.skippedOn]);
38993899
}
39003900

Diff for: src/services/codefixes/fixSpelling.ts

+2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ namespace ts.codefix {
33
const fixId = "fixSpelling";
44
const errorCodes = [
55
Diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_2.code,
6+
Diagnostics.Property_0_may_not_exist_on_type_1_Did_you_mean_2.code,
67
Diagnostics.Cannot_find_name_0_Did_you_mean_1.code,
8+
Diagnostics.Could_not_find_name_0_Did_you_mean_1.code,
79
Diagnostics.Cannot_find_name_0_Did_you_mean_the_instance_member_this_0.code,
810
Diagnostics.Cannot_find_name_0_Did_you_mean_the_static_member_1_0.code,
911
Diagnostics._0_has_no_exported_member_named_1_Did_you_mean_2.code,

Diff for: tests/baselines/reference/argumentsReferenceInConstructor3_Js.types

+2-2
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,11 @@ class B extends A {
4343
* @type object
4444
*/
4545
this.bar = super.arguments.foo;
46-
>this.bar = super.arguments.foo : any
46+
>this.bar = super.arguments.foo : error
4747
>this.bar : any
4848
>this : this
4949
>bar : any
50-
>super.arguments.foo : any
50+
>super.arguments.foo : error
5151
>super.arguments : { bar: {}; }
5252
>super : A
5353
>arguments : { bar: {}; }

Diff for: tests/baselines/reference/jsObjectsMarkedAsOpenEnded.types

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class C {
2727

2828
this.member.a = 0;
2929
>this.member.a = 0 : 0
30-
>this.member.a : any
30+
>this.member.a : error
3131
>this.member : {}
3232
>this : this
3333
>member : {}
@@ -48,7 +48,7 @@ var obj = {
4848

4949
obj.property.a = 0;
5050
>obj.property.a = 0 : 0
51-
>obj.property.a : any
51+
>obj.property.a : error
5252
>obj.property : {}
5353
>obj : { property: {}; }
5454
>property : {}
+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
=== tests/cases/conformance/salsa/spellingUncheckedJS.js ===
2+
export var inModule = 1
3+
>inModule : Symbol(inModule, Decl(spellingUncheckedJS.js, 0, 10))
4+
5+
inmodule.toFixed()
6+
7+
function f() {
8+
>f : Symbol(f, Decl(spellingUncheckedJS.js, 1, 18))
9+
10+
var locals = 2 + true
11+
>locals : Symbol(locals, Decl(spellingUncheckedJS.js, 4, 7))
12+
13+
locale.toFixed()
14+
// @ts-expect-error
15+
localf.toExponential()
16+
// @ts-expect-error
17+
"this is fine"
18+
}
19+
class Classe {
20+
>Classe : Symbol(Classe, Decl(spellingUncheckedJS.js, 10, 1))
21+
22+
non = 'oui'
23+
>non : Symbol(Classe.non, Decl(spellingUncheckedJS.js, 11, 14))
24+
25+
methode() {
26+
>methode : Symbol(Classe.methode, Decl(spellingUncheckedJS.js, 12, 15))
27+
28+
// no error on 'this' references
29+
return this.none
30+
>this : Symbol(Classe, Decl(spellingUncheckedJS.js, 10, 1))
31+
}
32+
}
33+
class Derivee extends Classe {
34+
>Derivee : Symbol(Derivee, Decl(spellingUncheckedJS.js, 17, 1))
35+
>Classe : Symbol(Classe, Decl(spellingUncheckedJS.js, 10, 1))
36+
37+
methode() {
38+
>methode : Symbol(Derivee.methode, Decl(spellingUncheckedJS.js, 18, 30))
39+
40+
// no error on 'super' references
41+
return super.none
42+
>super : Symbol(Classe, Decl(spellingUncheckedJS.js, 10, 1))
43+
}
44+
}
45+
46+
47+
var object = {
48+
>object : Symbol(object, Decl(spellingUncheckedJS.js, 26, 3), Decl(spellingUncheckedJS.js, 29, 15), Decl(spellingUncheckedJS.js, 30, 18))
49+
50+
spaaace: 3
51+
>spaaace : Symbol(spaaace, Decl(spellingUncheckedJS.js, 26, 14))
52+
}
53+
object.spaaaace // error on read
54+
>object : Symbol(object, Decl(spellingUncheckedJS.js, 26, 3), Decl(spellingUncheckedJS.js, 29, 15), Decl(spellingUncheckedJS.js, 30, 18))
55+
56+
object.spaace = 12 // error on write
57+
>object : Symbol(object, Decl(spellingUncheckedJS.js, 26, 3), Decl(spellingUncheckedJS.js, 29, 15), Decl(spellingUncheckedJS.js, 30, 18))
58+
59+
object.fresh = 12 // OK
60+
>object : Symbol(object, Decl(spellingUncheckedJS.js, 26, 3), Decl(spellingUncheckedJS.js, 29, 15), Decl(spellingUncheckedJS.js, 30, 18))
61+
62+
other.puuuce // OK, from another file
63+
>other : Symbol(other, Decl(other.js, 3, 3))
64+
65+
new Date().getGMTDate() // OK, from another file
66+
>Date : Symbol(Date, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.scripthost.d.ts, --, --))
67+
68+
// No suggestions for globals from other files
69+
const atoc = setIntegral(() => console.log('ok'), 500)
70+
>atoc : Symbol(atoc, Decl(spellingUncheckedJS.js, 36, 5))
71+
>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --))
72+
>console : Symbol(console, Decl(lib.dom.d.ts, --, --))
73+
>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --))
74+
75+
AudioBuffin // etc
76+
Jimmy
77+
>Jimmy : Symbol(Jimmy, Decl(other.js, 0, 3))
78+
79+
Jon
80+
81+
=== tests/cases/conformance/salsa/other.js ===
82+
var Jimmy = 1
83+
>Jimmy : Symbol(Jimmy, Decl(other.js, 0, 3))
84+
85+
var John = 2
86+
>John : Symbol(John, Decl(other.js, 1, 3))
87+
88+
Jon // error, it's from the same file
89+
var other = {
90+
>other : Symbol(other, Decl(other.js, 3, 3))
91+
92+
puuce: 4
93+
>puuce : Symbol(puuce, Decl(other.js, 3, 13))
94+
}
95+

0 commit comments

Comments
 (0)