Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Better errors for indexing gettable/settable values (#26446)
* give suggestions when index signature given

* add tests for noImplicitAny indexing on Object

* remove comments regarding error messages

* recommend set if el is on RHS of assignment else get

* add new baseline tests
  • Loading branch information
collin5 authored and RyanCavanaugh committed Apr 30, 2019
1 parent 3ce3cde commit 7016d45
Show file tree
Hide file tree
Showing 8 changed files with 289 additions and 19 deletions.
37 changes: 36 additions & 1 deletion src/compiler/checker.ts
Expand Up @@ -10117,7 +10117,13 @@ namespace ts {
}
}
else {
error(accessExpression, Diagnostics.Element_implicitly_has_an_any_type_because_type_0_has_no_index_signature, typeToString(objectType));
const suggestion = getSuggestionForNonexistentIndexSignature(objectType, accessExpression);
if (suggestion !== undefined) {
error(accessExpression, Diagnostics.Element_implicitly_has_an_any_type_because_type_0_has_no_index_signature_Did_you_mean_to_call_1, typeToString(objectType), suggestion);
}
else {
error(accessExpression, Diagnostics.Element_implicitly_has_an_any_type_because_type_0_has_no_index_signature, typeToString(objectType));
}
}
}
}
Expand Down Expand Up @@ -20186,6 +20192,35 @@ namespace ts {
return suggestion && symbolName(suggestion);
}

function getSuggestionForNonexistentIndexSignature(objectType: Type, expr: ElementAccessExpression): string | undefined {
// check if object type has setter or getter
const hasProp = (name: "set" | "get", argCount = 1) => {
const prop = getPropertyOfObjectType(objectType, <__String>name);
if (prop) {
const s = getSingleCallSignature(getTypeOfSymbol(prop));
if (s && getMinArgumentCount(s) === argCount && typeToString(getTypeAtPosition(s, 0)) === "string") {
return true;
}
}
return false;
};

const suggestedMethod = isAssignmentTarget(expr) ? "set" : "get";
if (!hasProp(suggestedMethod)) {
return undefined;
}

let suggestion = tryGetPropertyAccessOrIdentifierToString(expr);
if (suggestion === undefined) {
suggestion = suggestedMethod;
}
else {
suggestion += "." + suggestedMethod;
}

return suggestion;
}

/**
* Given a name and a list of symbols whose names are *not* equal to the name, return a spelling suggestion if there is one that is close enough.
* Names less than length 3 only check for case-insensitive equality, not levenshtein distance.
Expand Down
5 changes: 4 additions & 1 deletion src/compiler/diagnosticMessages.json
Expand Up @@ -4272,7 +4272,10 @@
"category": "Error",
"code": 7051
},

"Element implicitly has an 'any' type because type '{0}' has no index signature. Did you mean to call '{1}' ?": {
"category": "Error",
"code": 7052
},
"You cannot rename this element.": {
"category": "Error",
"code": 8000
Expand Down
10 changes: 10 additions & 0 deletions src/compiler/utilities.ts
Expand Up @@ -3960,6 +3960,16 @@ namespace ts {
return isPropertyAccessExpression(node) && isEntityNameExpression(node.expression);
}

export function tryGetPropertyAccessOrIdentifierToString(expr: Expression): string | undefined {
if (isPropertyAccessExpression(expr)) {
return tryGetPropertyAccessOrIdentifierToString(expr.expression) + "." + expr.name;
}
if (isIdentifier(expr)) {
return unescapeLeadingUnderscores(expr.escapedText);
}
return undefined;
}

export function isPrototypeAccess(node: Node): node is PropertyAccessExpression {
return isPropertyAccessExpression(node) && node.name.escapedText === "prototype";
}
Expand Down
@@ -1,8 +1,47 @@
tests/cases/compiler/noImplicitAnyStringIndexerOnObject.ts(1,9): error TS7017: Element implicitly has an 'any' type because type '{}' has no index signature.
tests/cases/compiler/noImplicitAnyStringIndexerOnObject.ts(7,1): error TS7052: Element implicitly has an 'any' type because type '{ get: (key: string) => string; }' has no index signature. Did you mean to call 'get' ?
tests/cases/compiler/noImplicitAnyStringIndexerOnObject.ts(8,13): error TS7052: Element implicitly has an 'any' type because type '{ get: (key: string) => string; }' has no index signature. Did you mean to call 'get' ?
tests/cases/compiler/noImplicitAnyStringIndexerOnObject.ts(13,13): error TS7017: Element implicitly has an 'any' type because type '{ set: (key: string) => string; }' has no index signature.
tests/cases/compiler/noImplicitAnyStringIndexerOnObject.ts(19,1): error TS7052: Element implicitly has an 'any' type because type '{ set: (key: string) => string; get: (key: string) => string; }' has no index signature. Did you mean to call 'set' ?
tests/cases/compiler/noImplicitAnyStringIndexerOnObject.ts(20,1): error TS7052: Element implicitly has an 'any' type because type '{ set: (key: string) => string; get: (key: string) => string; }' has no index signature. Did you mean to call 'set' ?
tests/cases/compiler/noImplicitAnyStringIndexerOnObject.ts(21,1): error TS7052: Element implicitly has an 'any' type because type '{ set: (key: string) => string; get: (key: string) => string; }' has no index signature. Did you mean to call 'set' ?


==== tests/cases/compiler/noImplicitAnyStringIndexerOnObject.ts (1 errors) ====
var x = {}["hello"];
==== tests/cases/compiler/noImplicitAnyStringIndexerOnObject.ts (7 errors) ====
var a = {}["hello"];
~~~~~~~~~~~
!!! error TS7017: Element implicitly has an 'any' type because type '{}' has no index signature.
var y: string = { '': 'foo' }[''];
var b: string = { '': 'foo' }[''];

var c = {
get: (key: string) => 'foobar'
};
c['hello'];
~~~~~~~~~~
!!! error TS7052: Element implicitly has an 'any' type because type '{ get: (key: string) => string; }' has no index signature. Did you mean to call 'get' ?
const foo = c['hello'];
~~~~~~~~~~
!!! error TS7052: Element implicitly has an 'any' type because type '{ get: (key: string) => string; }' has no index signature. Did you mean to call 'get' ?

var d = {
set: (key: string) => 'foobar'
};
const bar = d['hello'];
~~~~~~~~~~
!!! error TS7017: Element implicitly has an 'any' type because type '{ set: (key: string) => string; }' has no index signature.

var e = {
set: (key: string) => 'foobar',
get: (key: string) => 'foobar'
};
e['hello'] = 'modified';
~~~~~~~~~~
!!! error TS7052: Element implicitly has an 'any' type because type '{ set: (key: string) => string; get: (key: string) => string; }' has no index signature. Did you mean to call 'set' ?
e['hello'] += 1;
~~~~~~~~~~
!!! error TS7052: Element implicitly has an 'any' type because type '{ set: (key: string) => string; get: (key: string) => string; }' has no index signature. Did you mean to call 'set' ?
e['hello'] ++;
~~~~~~~~~~
!!! error TS7052: Element implicitly has an 'any' type because type '{ set: (key: string) => string; get: (key: string) => string; }' has no index signature. Did you mean to call 'set' ?


45 changes: 41 additions & 4 deletions tests/baselines/reference/noImplicitAnyStringIndexerOnObject.js
@@ -1,7 +1,44 @@
//// [noImplicitAnyStringIndexerOnObject.ts]
var x = {}["hello"];
var y: string = { '': 'foo' }[''];
var a = {}["hello"];
var b: string = { '': 'foo' }[''];

var c = {
get: (key: string) => 'foobar'
};
c['hello'];
const foo = c['hello'];

var d = {
set: (key: string) => 'foobar'
};
const bar = d['hello'];

var e = {
set: (key: string) => 'foobar',
get: (key: string) => 'foobar'
};
e['hello'] = 'modified';
e['hello'] += 1;
e['hello'] ++;



//// [noImplicitAnyStringIndexerOnObject.js]
var x = {}["hello"];
var y = { '': 'foo' }[''];
var a = {}["hello"];
var b = { '': 'foo' }[''];
var c = {
get: function (key) { return 'foobar'; }
};
c['hello'];
var foo = c['hello'];
var d = {
set: function (key) { return 'foobar'; }
};
var bar = d['hello'];
var e = {
set: function (key) { return 'foobar'; },
get: function (key) { return 'foobar'; }
};
e['hello'] = 'modified';
e['hello'] += 1;
e['hello']++;
@@ -1,9 +1,58 @@
=== tests/cases/compiler/noImplicitAnyStringIndexerOnObject.ts ===
var x = {}["hello"];
>x : Symbol(x, Decl(noImplicitAnyStringIndexerOnObject.ts, 0, 3))
var a = {}["hello"];
>a : Symbol(a, Decl(noImplicitAnyStringIndexerOnObject.ts, 0, 3))

var y: string = { '': 'foo' }[''];
>y : Symbol(y, Decl(noImplicitAnyStringIndexerOnObject.ts, 1, 3))
var b: string = { '': 'foo' }[''];
>b : Symbol(b, Decl(noImplicitAnyStringIndexerOnObject.ts, 1, 3))
>'' : Symbol('', Decl(noImplicitAnyStringIndexerOnObject.ts, 1, 17))
>'' : Symbol('', Decl(noImplicitAnyStringIndexerOnObject.ts, 1, 17))

var c = {
>c : Symbol(c, Decl(noImplicitAnyStringIndexerOnObject.ts, 3, 3))

get: (key: string) => 'foobar'
>get : Symbol(get, Decl(noImplicitAnyStringIndexerOnObject.ts, 3, 9))
>key : Symbol(key, Decl(noImplicitAnyStringIndexerOnObject.ts, 4, 8))

};
c['hello'];
>c : Symbol(c, Decl(noImplicitAnyStringIndexerOnObject.ts, 3, 3))

const foo = c['hello'];
>foo : Symbol(foo, Decl(noImplicitAnyStringIndexerOnObject.ts, 7, 5))
>c : Symbol(c, Decl(noImplicitAnyStringIndexerOnObject.ts, 3, 3))

var d = {
>d : Symbol(d, Decl(noImplicitAnyStringIndexerOnObject.ts, 9, 3))

set: (key: string) => 'foobar'
>set : Symbol(set, Decl(noImplicitAnyStringIndexerOnObject.ts, 9, 9))
>key : Symbol(key, Decl(noImplicitAnyStringIndexerOnObject.ts, 10, 8))

};
const bar = d['hello'];
>bar : Symbol(bar, Decl(noImplicitAnyStringIndexerOnObject.ts, 12, 5))
>d : Symbol(d, Decl(noImplicitAnyStringIndexerOnObject.ts, 9, 3))

var e = {
>e : Symbol(e, Decl(noImplicitAnyStringIndexerOnObject.ts, 14, 3))

set: (key: string) => 'foobar',
>set : Symbol(set, Decl(noImplicitAnyStringIndexerOnObject.ts, 14, 9))
>key : Symbol(key, Decl(noImplicitAnyStringIndexerOnObject.ts, 15, 8))

get: (key: string) => 'foobar'
>get : Symbol(get, Decl(noImplicitAnyStringIndexerOnObject.ts, 15, 33))
>key : Symbol(key, Decl(noImplicitAnyStringIndexerOnObject.ts, 16, 8))

};
e['hello'] = 'modified';
>e : Symbol(e, Decl(noImplicitAnyStringIndexerOnObject.ts, 14, 3))

e['hello'] += 1;
>e : Symbol(e, Decl(noImplicitAnyStringIndexerOnObject.ts, 14, 3))

e['hello'] ++;
>e : Symbol(e, Decl(noImplicitAnyStringIndexerOnObject.ts, 14, 3))


85 changes: 81 additions & 4 deletions tests/baselines/reference/noImplicitAnyStringIndexerOnObject.types
@@ -1,15 +1,92 @@
=== tests/cases/compiler/noImplicitAnyStringIndexerOnObject.ts ===
var x = {}["hello"];
>x : any
var a = {}["hello"];
>a : any
>{}["hello"] : any
>{} : {}
>"hello" : "hello"

var y: string = { '': 'foo' }[''];
>y : string
var b: string = { '': 'foo' }[''];
>b : string
>{ '': 'foo' }[''] : string
>{ '': 'foo' } : { '': string; }
>'' : string
>'foo' : "foo"
>'' : ""

var c = {
>c : { get: (key: string) => string; }
>{ get: (key: string) => 'foobar'} : { get: (key: string) => string; }

get: (key: string) => 'foobar'
>get : (key: string) => string
>(key: string) => 'foobar' : (key: string) => string
>key : string
>'foobar' : "foobar"

};
c['hello'];
>c['hello'] : any
>c : { get: (key: string) => string; }
>'hello' : "hello"

const foo = c['hello'];
>foo : any
>c['hello'] : any
>c : { get: (key: string) => string; }
>'hello' : "hello"

var d = {
>d : { set: (key: string) => string; }
>{ set: (key: string) => 'foobar'} : { set: (key: string) => string; }

set: (key: string) => 'foobar'
>set : (key: string) => string
>(key: string) => 'foobar' : (key: string) => string
>key : string
>'foobar' : "foobar"

};
const bar = d['hello'];
>bar : any
>d['hello'] : any
>d : { set: (key: string) => string; }
>'hello' : "hello"

var e = {
>e : { set: (key: string) => string; get: (key: string) => string; }
>{ set: (key: string) => 'foobar', get: (key: string) => 'foobar'} : { set: (key: string) => string; get: (key: string) => string; }

set: (key: string) => 'foobar',
>set : (key: string) => string
>(key: string) => 'foobar' : (key: string) => string
>key : string
>'foobar' : "foobar"

get: (key: string) => 'foobar'
>get : (key: string) => string
>(key: string) => 'foobar' : (key: string) => string
>key : string
>'foobar' : "foobar"

};
e['hello'] = 'modified';
>e['hello'] = 'modified' : "modified"
>e['hello'] : any
>e : { set: (key: string) => string; get: (key: string) => string; }
>'hello' : "hello"
>'modified' : "modified"

e['hello'] += 1;
>e['hello'] += 1 : any
>e['hello'] : any
>e : { set: (key: string) => string; get: (key: string) => string; }
>'hello' : "hello"
>1 : 1

e['hello'] ++;
>e['hello'] ++ : number
>e['hello'] : any
>e : { set: (key: string) => string; get: (key: string) => string; }
>'hello' : "hello"


24 changes: 22 additions & 2 deletions tests/cases/compiler/noImplicitAnyStringIndexerOnObject.ts
@@ -1,4 +1,24 @@
// @noimplicitany: true

var x = {}["hello"];
var y: string = { '': 'foo' }[''];
var a = {}["hello"];
var b: string = { '': 'foo' }[''];

var c = {
get: (key: string) => 'foobar'
};
c['hello'];
const foo = c['hello'];

var d = {
set: (key: string) => 'foobar'
};
const bar = d['hello'];

var e = {
set: (key: string) => 'foobar',
get: (key: string) => 'foobar'
};
e['hello'] = 'modified';
e['hello'] += 1;
e['hello'] ++;

0 comments on commit 7016d45

Please sign in to comment.