Skip to content

Commit

Permalink
Refactor some typechecking logic for getprops
Browse files Browse the repository at this point in the history
This is so we can reuse it in a future CL when checking property access in for-of loop initializers.

The only non-refactoring change is that we back off some checks when assigning to ".prototype" that we didn't before, which resolves an old TODO in TypeCheckTest.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=201705228
  • Loading branch information
lauraharker authored and brad4d committed Jun 25, 2018
1 parent a80acd8 commit 9886893
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 72 deletions.
103 changes: 48 additions & 55 deletions src/com/google/javascript/jscomp/TypeCheck.java
Expand Up @@ -64,6 +64,7 @@
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import javax.annotation.Nullable;


/** /**
* <p>Checks the types of JS expressions against any declared type * <p>Checks the types of JS expressions against any declared type
Expand All @@ -84,10 +85,6 @@ public final class TypeCheck implements NodeTraversal.Callback, CompilerPass {
// //
// User warnings // User warnings
// //

protected static final String OVERRIDING_PROTOTYPE_WITH_NON_OBJECT =
"overriding prototype with non-object";

static final DiagnosticType DETERMINISTIC_TEST = static final DiagnosticType DETERMINISTIC_TEST =
DiagnosticType.warning( DiagnosticType.warning(
"JSC_DETERMINISTIC_TEST", "JSC_DETERMINISTIC_TEST",
Expand Down Expand Up @@ -1012,7 +1009,7 @@ private void visitAssign(NodeTraversal t, Node assign) {
if (object.isGetProp()) { if (object.isGetProp()) {
JSType jsType = getJSType(object.getFirstChild()); JSType jsType = getJSType(object.getFirstChild());
if (jsType.isInterface() && object.getLastChild().getString().equals("prototype")) { if (jsType.isInterface() && object.getLastChild().getString().equals("prototype")) {
visitInterfaceGetprop(t, assign, object, rvalue); visitInterfacePropertyAssignment(t, object, rvalue);
} }
} }


Expand All @@ -1024,42 +1021,29 @@ private void visitAssign(NodeTraversal t, Node assign) {
// during TypedScopeCreator, and we only look for the "dumb" cases here. // during TypedScopeCreator, and we only look for the "dumb" cases here.
// object.prototype = ...; // object.prototype = ...;
if (pname.equals("prototype")) { if (pname.equals("prototype")) {
if (objectJsType != null && objectJsType.isFunctionType()) { validator.expectCanAssignToPrototype(t, objectJsType, rvalue, getJSType(rvalue));
FunctionType functionType = objectJsType.toMaybeFunctionType(); return;
if (functionType.isConstructor()) {
JSType rvalueType = rvalue.getJSType();
validator.expectObject(t, rvalue, rvalueType,
OVERRIDING_PROTOTYPE_WITH_NON_OBJECT);
return;
}
}
} }


// The generic checks for 'object.property' when 'object' is known, // The generic checks for 'object.property' when 'object' is known,
// and 'property' is declared on it. // and 'property' is declared on it.
// object.property = ...; // object.property = ...;
ObjectType type = ObjectType.cast( ObjectType objectCastType = ObjectType.cast(objectJsType.restrictByNotNullOrUndefined());
objectJsType.restrictByNotNullOrUndefined()); JSType expectedPropertyType = getPropertyTypeIfDeclared(objectCastType, pname);
if (type != null) {
if (type.hasProperty(pname) && !type.isPropertyTypeInferred(pname)) { checkPropertyInheritanceOnGetpropAssign(t, assign, object, pname, info, expectedPropertyType);
JSType expectedType = type.getPropertyType(pname);
if (!expectedType.isUnknownType()) { // If we successfully found a non-unknown declared type, validate the assignment and don't do
if (!propertyIsImplicitCast(type, pname)) { // any further checks.
validator.expectCanAssignToPropertyOf( if (!expectedPropertyType.isUnknownType()) {
t, assign, getJSType(rvalue), // Note: if the property has @implicitCast at its declaration, we don't check any
expectedType, object, pname); // assignments to it.
checkPropertyInheritanceOnGetpropAssign( if (!propertyIsImplicitCast(objectCastType, pname)) {
t, assign, object, pname, info, expectedType); validator.expectCanAssignToPropertyOf(
} t, assign, getJSType(rvalue), expectedPropertyType, object, pname);
return;
}
} }
return;
} }

// If we couldn't get the property type with normal object property
// lookups, then check inheritance anyway with the unknown type.
checkPropertyInheritanceOnGetpropAssign(
t, assign, object, pname, info, getNativeType(UNKNOWN_TYPE));
} }


checkCanAssignToWithScope(t, assign, lvalue, getJSType(rvalue), "assignment"); checkCanAssignToWithScope(t, assign, lvalue, getJSType(rvalue), "assignment");
Expand Down Expand Up @@ -1502,39 +1486,36 @@ static JSType getObjectLitKeyTypeFromValueType(Node key, JSType valueType) {
* Visits an ASSIGN node for cases such as * Visits an ASSIGN node for cases such as
* *
* <pre> * <pre>
* interface.property2.property = ...; * interface.prototype.property = ...;
* </pre> * </pre>
*/ */
private void visitInterfaceGetprop(NodeTraversal t, Node assign, Node object, Node rvalue) { private void visitInterfacePropertyAssignment(NodeTraversal t, Node object, Node rvalue) {

JSType rvalueType = getJSType(rvalue); JSType rvalueType = getJSType(rvalue);


// Only 2 values are allowed for methods: // Only 2 values are allowed for interface methods:
// goog.abstractMethod // goog.abstractMethod
// function () {}; // function () {};
// or for properties, no assignment such as: // Other (non-method) interface properties must be stub declarations without assignments, e.g.
// InterfaceFoo.prototype.foobar; // someinterface.prototype.nonMethodProperty;

// which is why we enforce that `rvalueType.isFunctionType()`.
String abstractMethodName =
compiler.getCodingConvention().getAbstractMethodName();
if (!rvalueType.isFunctionType()) { if (!rvalueType.isFunctionType()) {
// This is bad i18n style but we don't localize our compiler errors. reportInvalidInterfaceMemberDeclaration(t, object);
String abstractMethodMessage = (abstractMethodName != null)
? ", or " + abstractMethodName
: "";
compiler.report(
t.makeError(object, INVALID_INTERFACE_MEMBER_DECLARATION,
abstractMethodMessage));
} }


if (assign.getLastChild().isFunction() if (rvalue.isFunction() && !NodeUtil.isEmptyBlock(NodeUtil.getFunctionBody(rvalue))) {
&& !NodeUtil.isEmptyBlock(assign.getLastChild().getLastChild())) { String abstractMethodName = compiler.getCodingConvention().getAbstractMethodName();
compiler.report( compiler.report(t.makeError(object, INTERFACE_METHOD_NOT_EMPTY, abstractMethodName));
t.makeError(object, INTERFACE_METHOD_NOT_EMPTY,
abstractMethodName));
} }
} }


private void reportInvalidInterfaceMemberDeclaration(NodeTraversal t, Node interfaceNode) {
String abstractMethodName = compiler.getCodingConvention().getAbstractMethodName();
// This is bad i18n style but we don't localize our compiler errors.
String abstractMethodMessage = (abstractMethodName != null) ? ", or " + abstractMethodName : "";
compiler.report(
t.makeError(interfaceNode, INVALID_INTERFACE_MEMBER_DECLARATION, abstractMethodMessage));
}

/** /**
* Visits a NAME node. * Visits a NAME node.
* *
Expand Down Expand Up @@ -2570,6 +2551,18 @@ private JSType getJSType(Node n) {
} }
} }


/**
* Returns the type of the property with the given name if declared. Otherwise returns unknown.
*/
private JSType getPropertyTypeIfDeclared(@Nullable ObjectType objectType, String propertyName) {
if (objectType != null
&& objectType.hasProperty(propertyName)
&& !objectType.isPropertyTypeInferred(propertyName)) {
return objectType.getPropertyType(propertyName);
}
return getNativeType(UNKNOWN_TYPE);
}

// TODO(nicksantos): TypeCheck should never be attaching types to nodes. // TODO(nicksantos): TypeCheck should never be attaching types to nodes.
// All types should be attached by TypeInference. This is not true today // All types should be attached by TypeInference. This is not true today
// for legacy reasons. There are a number of places where TypeInference // for legacy reasons. There are a number of places where TypeInference
Expand Down
21 changes: 21 additions & 0 deletions src/com/google/javascript/jscomp/TypeValidator.java
Expand Up @@ -666,6 +666,27 @@ void expectSuperType(NodeTraversal t, Node n, ObjectType superObject,
} }
} }


/**
* Expect that it's valid to assign something to a given type's prototype.
*
* <p>Most of these checks occur during TypedScopeCreator, so we just handle very basic cases here
*
* <p>For example, assuming `Foo` is a constructor, `Foo.prototype = 3;` will warn because `3`
* is not an object.
*
* @param ownerType The type of the object whose prototype is being changed. (e.g. `Foo` above)
* @param node Node to issue warnings on (e.g. `3` above)
* @param rightType the rvalue type being assigned to the prototype (e.g. `number` above)
*/
void expectCanAssignToPrototype(NodeTraversal t, JSType ownerType, Node node, JSType rightType) {
if (ownerType.isFunctionType()) {
FunctionType functionType = ownerType.toMaybeFunctionType();
if (functionType.isConstructor()) {
expectObject(t, node, rightType, "cannot override prototype with non-object");
}
}
}

/** /**
* Expect that the first type can be cast to the second type. The first type * Expect that the first type can be cast to the second type. The first type
* must have some relationship with the second. * must have some relationship with the second.
Expand Down
47 changes: 30 additions & 17 deletions test/com/google/javascript/jscomp/TypeCheckTest.java
Expand Up @@ -7794,6 +7794,25 @@ public void testImplicitCast2() {
"};\n"); "};\n");
} }


public void testImplicitCast3() {
testTypesWithExterns(
lines(
"/** @constructor */ function Element() {};",
"/**",
" * @type {string}",
" * @implicitCast",
" */",
"Element.prototype.innerHTML;"),
lines(
"/** @param {?Element} element",
" * @param {string|number} text",
" */",
"function f(element, text) {",
" element.innerHTML = text;",
"}",
""));
}

public void testImplicitCastSubclassAccess() { public void testImplicitCastSubclassAccess() {
testTypesWithExterns("/** @constructor */ function Element() {};\n" + testTypesWithExterns("/** @constructor */ function Element() {};\n" +
"/** @type {string}\n" + "/** @type {string}\n" +
Expand Down Expand Up @@ -15587,23 +15606,17 @@ public void testIssue1024a() {
} }


public void testIssue1024b() { public void testIssue1024b() {
/* TODO(blickly): Make this warning go away. testTypes(
* This is old behavior, but it doesn't make sense to warn about since lines(
* both assignments are inferred. "/** @param {Object} a */",
*/ "function f(a) {",
testTypes( " a.prototype = {foo:3};",
"/** @param {Object} a */\n" "}",
+ "function f(a) {\n" "/** @param {Object} b",
+ " a.prototype = {foo:3};\n" " */",
+ "}\n" "function g(b) {",
+ "/** @param {Object} b\n" " b.prototype = function(){};",
+ " */\n" "}"));
+ "function g(b) {\n"
+ " b.prototype = function(){};\n"
+ "}\n",
"assignment to property prototype of Object\n"
+ "found : {foo: number}\n"
+ "required: function(): undefined");
} }


public void testBug12722936() { public void testBug12722936() {
Expand Down

0 comments on commit 9886893

Please sign in to comment.