|
33 | 33 | import com.google.javascript.jscomp.CodingConvention.SubclassType;
|
34 | 34 | import com.google.javascript.jscomp.type.ReverseAbstractInterpreter;
|
35 | 35 | import com.google.javascript.rhino.JSDocInfo;
|
| 36 | +import com.google.javascript.rhino.JSTypeExpression; |
36 | 37 | import com.google.javascript.rhino.Node;
|
37 | 38 | import com.google.javascript.rhino.Token;
|
38 | 39 | import com.google.javascript.rhino.jstype.EnumType;
|
|
41 | 42 | import com.google.javascript.rhino.jstype.JSTypeNative;
|
42 | 43 | import com.google.javascript.rhino.jstype.JSTypeRegistry;
|
43 | 44 | import com.google.javascript.rhino.jstype.ObjectType;
|
| 45 | +import com.google.javascript.rhino.jstype.Property; |
44 | 46 | import com.google.javascript.rhino.jstype.TemplateTypeMap;
|
45 | 47 | import com.google.javascript.rhino.jstype.TemplateTypeMapReplacer;
|
| 48 | +import com.google.javascript.rhino.jstype.TemplatizedType; |
46 | 49 | import com.google.javascript.rhino.jstype.TernaryValue;
|
47 | 50 | import com.google.javascript.rhino.jstype.UnionType;
|
48 | 51 |
|
@@ -242,6 +245,12 @@ public final class TypeCheck implements NodeTraversal.Callback, CompilerPass {
|
242 | 245 | "ILLEGAL_OBJLIT_KEY",
|
243 | 246 | "Illegal key, the object literal is a {0}");
|
244 | 247 |
|
| 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 | + |
245 | 254 | // If a diagnostic is disabled by default, do not add it in this list
|
246 | 255 | // TODO(dimvar): Either INEXISTENT_PROPERTY shouldn't be here, or we should
|
247 | 256 | // change DiagnosticGroups.setWarningLevel to not accidentally enable it.
|
@@ -848,6 +857,8 @@ public void visit(NodeTraversal t, Node n, Node parent) {
|
848 | 857 | if (typeable) {
|
849 | 858 | doPercentTypedAccounting(t, n);
|
850 | 859 | }
|
| 860 | + |
| 861 | + checkJsdocInfoContainsObjectWithBadKey(t, n); |
851 | 862 | }
|
852 | 863 |
|
853 | 864 | private void checkTypeofString(NodeTraversal t, Node n, String s) {
|
@@ -2089,4 +2100,152 @@ private void ensureTyped(NodeTraversal t, Node n, JSType type) {
|
2089 | 2100 | private JSType getNativeType(JSTypeNative typeId) {
|
2090 | 2101 | return typeRegistry.getNativeType(typeId);
|
2091 | 2102 | }
|
| 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 | + } |
2092 | 2251 | }
|
0 commit comments