Skip to content

Commit 78cd273

Browse files
nbeloglazovblickly
authored andcommitted
Add a check to jscompiler that verifies that all types used as keys in Object are stringifiable (string, numbers, classes with custom toString() methods).
Proposal doc: https://docs.google.com/document/d/1MJV0YYKvDf_5-CsRm8paYwb8bIv4L-oPP8rBH_8Ws6Q/edit# ------------- Created by MOE: http://code.google.com/p/moe-java MOE_MIGRATED_REVID=92027498
1 parent 0f7da6b commit 78cd273

File tree

4 files changed

+341
-2
lines changed

4 files changed

+341
-2
lines changed

Diff for: src/com/google/javascript/jscomp/DiagnosticGroups.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,8 @@ public DiagnosticGroup forName(String name) {
428428
CheckNullableReturn.NULLABLE_RETURN,
429429
CheckNullableReturn.NULLABLE_RETURN_WITH_NAME,
430430
CheckPrototypeProperties.ILLEGAL_PROTOTYPE_MEMBER,
431-
ImplicitNullabilityCheck.IMPLICITLY_NULLABLE_JSDOC);
431+
ImplicitNullabilityCheck.IMPLICITLY_NULLABLE_JSDOC,
432+
TypeCheck.NON_STRINGIFIABLE_OBJECT_KEY);
432433

433434
public static final DiagnosticGroup USE_OF_GOOG_BASE =
434435
DiagnosticGroups.registerGroup("useOfGoogBase",

Diff for: src/com/google/javascript/jscomp/TypeCheck.java

+159
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import com.google.javascript.jscomp.CodingConvention.SubclassType;
3434
import com.google.javascript.jscomp.type.ReverseAbstractInterpreter;
3535
import com.google.javascript.rhino.JSDocInfo;
36+
import com.google.javascript.rhino.JSTypeExpression;
3637
import com.google.javascript.rhino.Node;
3738
import com.google.javascript.rhino.Token;
3839
import com.google.javascript.rhino.jstype.EnumType;
@@ -41,8 +42,10 @@
4142
import com.google.javascript.rhino.jstype.JSTypeNative;
4243
import com.google.javascript.rhino.jstype.JSTypeRegistry;
4344
import com.google.javascript.rhino.jstype.ObjectType;
45+
import com.google.javascript.rhino.jstype.Property;
4446
import com.google.javascript.rhino.jstype.TemplateTypeMap;
4547
import com.google.javascript.rhino.jstype.TemplateTypeMapReplacer;
48+
import com.google.javascript.rhino.jstype.TemplatizedType;
4649
import com.google.javascript.rhino.jstype.TernaryValue;
4750
import com.google.javascript.rhino.jstype.UnionType;
4851

@@ -242,6 +245,12 @@ public final class TypeCheck implements NodeTraversal.Callback, CompilerPass {
242245
"ILLEGAL_OBJLIT_KEY",
243246
"Illegal key, the object literal is a {0}");
244247

248+
static final DiagnosticType NON_STRINGIFIABLE_OBJECT_KEY =
249+
DiagnosticType.disabled(
250+
"JSC_NON_STRINGIFIABLE_OBJECT_KEY",
251+
"Object type \"{0}\" contains non-stringifiable key and it may lead to an "
252+
+ "error. Please use ES6 Map instead or implement your own Map structure.");
253+
245254
// If a diagnostic is disabled by default, do not add it in this list
246255
// TODO(dimvar): Either INEXISTENT_PROPERTY shouldn't be here, or we should
247256
// change DiagnosticGroups.setWarningLevel to not accidentally enable it.
@@ -848,6 +857,8 @@ public void visit(NodeTraversal t, Node n, Node parent) {
848857
if (typeable) {
849858
doPercentTypedAccounting(t, n);
850859
}
860+
861+
checkJsdocInfoContainsObjectWithBadKey(t, n);
851862
}
852863

853864
private void checkTypeofString(NodeTraversal t, Node n, String s) {
@@ -2089,4 +2100,152 @@ private void ensureTyped(NodeTraversal t, Node n, JSType type) {
20892100
private JSType getNativeType(JSTypeNative typeId) {
20902101
return typeRegistry.getNativeType(typeId);
20912102
}
2103+
2104+
/**
2105+
* Checks if current node contains js docs and checks all types specified in the js doc whether
2106+
* they have Objects with potentially invalid keys. For example: {@code
2107+
* Object<!Object, number>}. If such type is found, a warning is reported for the current node.
2108+
*/
2109+
private void checkJsdocInfoContainsObjectWithBadKey(NodeTraversal t, Node n) {
2110+
if (n.getJSDocInfo() != null) {
2111+
JSDocInfo info = n.getJSDocInfo();
2112+
checkTypeContainsObjectWithBadKey(t, n, info.getType());
2113+
checkTypeContainsObjectWithBadKey(t, n, info.getReturnType());
2114+
checkTypeContainsObjectWithBadKey(t, n, info.getTypedefType());
2115+
for (String param : info.getParameterNames()) {
2116+
checkTypeContainsObjectWithBadKey(t, n, info.getParameterType(param));
2117+
}
2118+
}
2119+
}
2120+
2121+
private void checkTypeContainsObjectWithBadKey(NodeTraversal t, Node n, JSTypeExpression type) {
2122+
if (type != null && type.getRoot().getJSType() != null) {
2123+
JSType realType = type.getRoot().getJSType();
2124+
JSType objectWithBadKey = findObjectWithNonStringifiableKey(realType);
2125+
if (objectWithBadKey != null){
2126+
compiler.report(t.makeError(n, NON_STRINGIFIABLE_OBJECT_KEY, objectWithBadKey.toString()));
2127+
}
2128+
}
2129+
}
2130+
2131+
/**
2132+
* Checks whether type is stringifiable. Stringifiable is a type that can be converted to string
2133+
* and give unique results for different objects. For example objects have native toString()
2134+
* method that on chrome returns "[object Object]" for all objects making it useless when used
2135+
* as keys. At the same time native types like numbers can be safely converted to strings and
2136+
* used as keys. Also user might have provided custom toString() methods for a class making it
2137+
* suitable for using as key.
2138+
*/
2139+
private boolean isStringifiable(JSType type) {
2140+
// Check built-in types
2141+
if (type.isUnknownType() || type.isNumber() || type.isString() || type.isBooleanObjectType()
2142+
|| type.isBooleanValueType() || type.isDateType() || type.isRegexpType()
2143+
|| type.isInterface() || type.isRecordType() || type.isNullType() || type.isVoidType()) {
2144+
return true;
2145+
}
2146+
2147+
// For enums check that underlying type is stringifiable.
2148+
if (type.toMaybeEnumElementType() != null) {
2149+
return isStringifiable(type.toMaybeEnumElementType().getPrimitiveType());
2150+
}
2151+
2152+
// Array is stringifiable if it doesn't have template type or if it does have it, the template
2153+
// type must be also stringifiable.
2154+
// Good: Array, Array.<number>
2155+
// Bad: Array.<!Object>
2156+
if (type.isArrayType()) {
2157+
return true;
2158+
}
2159+
if (type.isTemplatizedType()) {
2160+
TemplatizedType templatizedType = type.toMaybeTemplatizedType();
2161+
if (templatizedType.getReferencedType().isArrayType()) {
2162+
return isStringifiable(templatizedType.getTemplateTypes().get(0));
2163+
}
2164+
}
2165+
2166+
// Handle interfaces and classes.
2167+
if (type.isObject()) {
2168+
ObjectType objectType = type.toMaybeObjectType();
2169+
JSType constructor = objectType.getConstructor();
2170+
// Interfaces considered stringifiable as user might implement toString() method in
2171+
// classes-implementations.
2172+
if (constructor != null && constructor.isInterface()) {
2173+
return true;
2174+
}
2175+
// This is user-defined class so check if it has custom toString() method.
2176+
return classHasToString(objectType);
2177+
}
2178+
2179+
// For union type every alternate must be stringifiable.
2180+
if (type.isUnionType()) {
2181+
for (JSType alternateType : type.toMaybeUnionType().getAlternates()) {
2182+
if (!isStringifiable(alternateType)) {
2183+
return false;
2184+
}
2185+
}
2186+
return true;
2187+
}
2188+
return false;
2189+
}
2190+
2191+
/**
2192+
* Checks whether current type is Object type with non-stringifable key.
2193+
*/
2194+
private boolean isObjectTypeWithNonStringifiableKey(JSType type) {
2195+
if (!type.isTemplatizedType()) {
2196+
return false;
2197+
}
2198+
TemplatizedType templatizedType = type.toMaybeTemplatizedType();
2199+
if (templatizedType.getReferencedType().isNativeObjectType()
2200+
&& templatizedType.getTemplateTypes().size() > 1) {
2201+
return !isStringifiable(templatizedType.getTemplateTypes().get(0));
2202+
} else {
2203+
return false;
2204+
}
2205+
}
2206+
2207+
/**
2208+
* Checks whether type (or one of its component if is composed type like union or templatized
2209+
* type) has Object with non-stringifiable key. For example {@code Object.<!Object, number>}.
2210+
*
2211+
* @return non-stringifiable type which is used as key or null if all there are no such types.
2212+
*/
2213+
private JSType findObjectWithNonStringifiableKey(JSType type) {
2214+
if (isObjectTypeWithNonStringifiableKey(type)) {
2215+
return type;
2216+
}
2217+
if (type.isUnionType()) {
2218+
for (JSType alternateType : type.toMaybeUnionType().getAlternates()) {
2219+
JSType result = findObjectWithNonStringifiableKey(alternateType);
2220+
if (result != null) {
2221+
return result;
2222+
}
2223+
}
2224+
}
2225+
if (type.isTemplatizedType()) {
2226+
for (JSType templateType : type.toMaybeTemplatizedType().getTemplateTypes()) {
2227+
JSType result = findObjectWithNonStringifiableKey(templateType);
2228+
if (result != null) {
2229+
return result;
2230+
}
2231+
}
2232+
}
2233+
return null;
2234+
}
2235+
2236+
/**
2237+
* Checks whether class has overridden toString() method. All objects has native toString()
2238+
* method but we ignore it as it is not useful so we need user-provided toString() method.
2239+
*/
2240+
private boolean classHasToString(ObjectType type) {
2241+
Property toStringProperty = type.getOwnSlot("toString");
2242+
if (toStringProperty != null) {
2243+
return toStringProperty.getType().isFunctionType();
2244+
}
2245+
ObjectType parent = type.getParentScope();
2246+
if (!parent.isNativeObjectType()) {
2247+
return classHasToString(parent);
2248+
}
2249+
return false;
2250+
}
20922251
}

Diff for: test/com/google/javascript/jscomp/CompilerTypeTestCase.java

+1
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ protected CompilerOptions getOptions() {
119119
DiagnosticGroups.MISPLACED_TYPE_ANNOTATION, CheckLevel.WARNING);
120120
options.setWarningLevel(
121121
DiagnosticGroups.INVALID_CASTS, CheckLevel.WARNING);
122+
options.setWarningLevel(DiagnosticGroups.LINT_CHECKS, CheckLevel.WARNING);
122123
options.setCodingConvention(getCodingConvention());
123124
return options;
124125
}

0 commit comments

Comments
 (0)