Skip to content

Commit

Permalink
Warn on missing properties in object destructuring in TypeCheck
Browse files Browse the repository at this point in the history
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=208575066
  • Loading branch information
lauraharker committed Aug 14, 2018
1 parent adbdc4f commit 7504f6a
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 42 deletions.
6 changes: 6 additions & 0 deletions src/com/google/javascript/jscomp/DestructuredTarget.java
Expand Up @@ -89,6 +89,12 @@ boolean hasStringKey() {
return objectPatternKey != null && objectPatternKey.isStringKey(); return objectPatternKey != null && objectPatternKey.isStringKey();
} }


/** Returns a STRING_KEY node or null */
@Nullable
Node getStringKey() {
return hasStringKey() ? objectPatternKey : null;
}

@Nullable @Nullable
Node getDefaultValue() { Node getDefaultValue() {
return defaultValue; return defaultValue;
Expand Down
131 changes: 95 additions & 36 deletions src/com/google/javascript/jscomp/TypeCheck.java
Expand Up @@ -902,6 +902,11 @@ public void visit(NodeTraversal t, Node n, Node parent) {
validator.expectIndexMatch( validator.expectIndexMatch(
t, computedProperty, patternType, getJSType(computedProperty.getFirstChild())); t, computedProperty, patternType, getJSType(computedProperty.getFirstChild()));
} }

if (target.hasStringKey()) {
checkPropertyAccessForDestructuring(
t, n, getJSType(n), target.getStringKey(), getJSType(target.getNode()));
}
} }
break; break;


Expand Down Expand Up @@ -1214,7 +1219,8 @@ private void checkPropCreation(NodeTraversal t, Node lvalue) {
return; return;
} }


reportMissingProperty(objType, propName, kind, t, lvalue, true); reportMissingProperty(
lvalue.getFirstChild(), objType, lvalue.getSecondChild(), kind, t, true);
} }
} }
} }
Expand Down Expand Up @@ -1756,24 +1762,64 @@ private void visitGetProp(NodeTraversal t, Node n) {
report(t, property, TypeValidator.ILLEGAL_PROPERTY_ACCESS, "'.'", "dict"); report(t, property, TypeValidator.ILLEGAL_PROPERTY_ACCESS, "'.'", "dict");
} else if (validator.expectNotNullOrUndefined(t, n, childType, } else if (validator.expectNotNullOrUndefined(t, n, childType,
"No properties on this expression", getNativeType(OBJECT_TYPE))) { "No properties on this expression", getNativeType(OBJECT_TYPE))) {
checkPropertyAccess(childType, property.getString(), t, n); checkPropertyAccessForGetProp(t, n);
} }
ensureTyped(n); ensureTyped(n);
} }


private void checkPropertyAccessForGetProp(NodeTraversal t, Node getProp) {
checkArgument(getProp.isGetProp(), getProp);
Node objNode = getProp.getFirstChild();
JSType objType = getJSType(objNode);
Node propNode = getProp.getSecondChild();
JSType propType = getJSType(getProp);

checkPropertyAccess(objType, t, getProp, propType, propNode, objNode);
}

private void checkPropertyAccessForDestructuring(
NodeTraversal t, Node pattern, JSType objectType, Node stringKey, JSType inferredPropType) {
checkArgument(pattern.isDestructuringPattern(), pattern);
checkArgument(stringKey.isStringKey(), stringKey);

// Get the object node being destructured when it exists. These cases have an actual node `obj`:
// const {a} = obj;
// ({a} = obj);
// while these do not:
// for (const {a} of arrayOfObjects) {
// const {{a}} = obj;
Node objNode = null;
Node patternParent = pattern.getParent();
if ((patternParent.isAssign() || patternParent.isDestructuringLhs())
&& pattern.getNext() != null) {
objNode = pattern.getNext();
}
checkPropertyAccess(objectType, t, /* getProp= */ null, inferredPropType, stringKey, objNode);
}

/** /**
* Emit a warning if we can prove that a property cannot possibly be * Emits a warning if we can prove that a property cannot possibly be defined on an object. Note
* defined on an object. Note the difference between JS and a strictly * the difference between JS and a strictly statically typed language: we're checking if the
* statically typed language: we're checking if the property * property *cannot be defined*, whereas a java compiler would check if the property *can be
* *cannot be defined*, whereas a java compiler would check if the * undefined.
* property *can be undefined*. *
* <p>This method handles property access in both GETPROPs and object destructuring.
* Consequentially some of its arguments are optional - the actual object node and the getprop -
* while others are required.
*
* @param objNode the actual node representing the object we're accessing. optional because
* destructuring accesses MAY not have an actual object node
* @param getProp the GETPROP node representing this property access. optional because
* destructuring accesses don't have actual getprops.
*/ */
private void checkPropertyAccess(JSType childType, String propName, NodeTraversal t, Node n) { private void checkPropertyAccess(
// If the property type is unknown, check the object type to see if it JSType childType,
// can ever be defined. We explicitly exclude CHECKED_UNKNOWN (for NodeTraversal t,
// properties where we've checked that it exists, or for properties on @Nullable Node getProp,
// objects that aren't in this binary). JSType propType,
JSType propType = getJSType(n); Node propNode,
@Nullable Node objNode) {
String propName = propNode.getString();
if (propType.isEquivalentTo(typeRegistry.getNativeType(UNKNOWN_TYPE))) { if (propType.isEquivalentTo(typeRegistry.getNativeType(UNKNOWN_TYPE))) {
childType = childType.autobox(); childType = childType.autobox();
ObjectType objectType = ObjectType.cast(childType); ObjectType objectType = ObjectType.cast(childType);
Expand All @@ -1784,39 +1830,45 @@ private void checkPropertyAccess(JSType childType, String propName, NodeTraversa
if (!objectType.hasProperty(propName) if (!objectType.hasProperty(propName)
|| objectType.isEquivalentTo(typeRegistry.getNativeType(UNKNOWN_TYPE))) { || objectType.isEquivalentTo(typeRegistry.getNativeType(UNKNOWN_TYPE))) {
if (objectType instanceof EnumType) { if (objectType instanceof EnumType) {
report(t, n, INEXISTENT_ENUM_ELEMENT, propName); report(t, getProp != null ? getProp : propNode, INEXISTENT_ENUM_ELEMENT, propName);
} else { } else {
checkPropertyAccessHelper(objectType, propName, t, n, false); checkPropertyAccessHelper(objectType, propNode, objNode, t, getProp, false);
} }
} }
} else { } else {
checkPropertyAccessHelper(childType, propName, t, n, false); checkPropertyAccessHelper(childType, propNode, objNode, t, getProp, false);
} }
} else if (childType.isUnionType() && !isLValueGetProp(n)) { } else if (childType.isUnionType() && getProp != null && !isLValueGetProp(getProp)) {
// NOTE: strict property assignment checks are done on assignment. // NOTE: strict property assignment checks are done on assignment.
checkPropertyAccessHelper(childType, propName, t, n, true); checkPropertyAccessHelper(childType, propNode, objNode, t, getProp, true);
} }
} }


boolean isLValueGetProp(Node n) { boolean isLValueGetProp(Node n) {
Node parent = n.getParent(); Node parent = n.getParent();
// TODO(b/77597706): this won't work for destructured lvalues
return (NodeUtil.isUpdateOperator(parent) || NodeUtil.isAssignmentOp(parent)) return (NodeUtil.isUpdateOperator(parent) || NodeUtil.isAssignmentOp(parent))
&& parent.getFirstChild() == n; && parent.getFirstChild() == n;
} }


/** /**
* @param strictCheck Whether this is a check that is only performed when "strict missing * @param strictCheck Whether this is a check that is only performed when "strict missing
* properties" cheks are enabled. * properties" checks are enabled.
*/ */
private void checkPropertyAccessHelper( private void checkPropertyAccessHelper(
JSType objectType, String propName, NodeTraversal t, Node n, boolean strictCheck) { JSType objectType,
boolean isStruct = objectType.isStruct(); Node propNode,
boolean maybePropExistenceCheck = !isStruct && allowLoosePropertyAccessOnNode(n); @Nullable Node objNode,
NodeTraversal t,
Node getProp,
boolean strictCheck) {
if (!reportMissingProperties if (!reportMissingProperties
|| objectType.isEmptyType() || objectType.isEmptyType()
|| allowStrictPropertyAccessOnNode(n)) { || (getProp != null && allowStrictPropertyAccessOnNode(getProp))) {
return; return;
} }

String propName = propNode.getString();
PropDefinitionKind kind = typeRegistry.canPropertyBeDefined(objectType, propName); PropDefinitionKind kind = typeRegistry.canPropertyBeDefined(objectType, propName);
if (kind.equals(PropDefinitionKind.KNOWN)) { if (kind.equals(PropDefinitionKind.KNOWN)) {
return; return;
Expand All @@ -1830,27 +1882,34 @@ private void checkPropertyAccessHelper(
// We still don't want to report this. // We still don't want to report this.
return; return;
} }
boolean loosePropertyDeclaration = isQNameAssignmentTarget(n) && !isStruct;
boolean isStruct = objectType.isStruct();
boolean loosePropertyDeclaration =
getProp != null && isQNameAssignmentTarget(getProp) && !isStruct;
// always false for destructuring
boolean maybePropExistenceCheck =
!isStruct && getProp != null && allowLoosePropertyAccessOnNode(getProp);
// Traditionally, we would not report a warning for "loose" properties, but we want to be // Traditionally, we would not report a warning for "loose" properties, but we want to be
// able to be more strict, so introduce an optional warning. // able to be more strict, so introduce an optional warning.
boolean strictReport = strictCheck || isLooselyAssociated || loosePropertyDeclaration boolean strictReport =
|| maybePropExistenceCheck; strictCheck || isLooselyAssociated || loosePropertyDeclaration || maybePropExistenceCheck;


reportMissingProperty(objectType, propName, kind, t, n, strictReport); reportMissingProperty(objNode, objectType, propNode, kind, t, strictReport);
} }


private void reportMissingProperty( private void reportMissingProperty(
JSType objectType, String propName, PropDefinitionKind kind, NodeTraversal t, Node n, @Nullable Node objNode,
JSType objectType,
Node propNode,
PropDefinitionKind kind,
NodeTraversal t,
boolean strictReport) { boolean strictReport) {
checkState(n.isGetProp()); String propName = propNode.getString();
boolean isObjectType = objectType.isEquivalentTo(getNativeType(OBJECT_TYPE)); boolean isObjectType = objectType.isEquivalentTo(getNativeType(OBJECT_TYPE));
boolean lowConfidence = objectType.isUnknownType() || objectType.isAllType() || isObjectType; boolean lowConfidence = objectType.isUnknownType() || objectType.isAllType() || isObjectType;


boolean isKnownToUnionMember = kind.equals(PropDefinitionKind.LOOSE_UNION); boolean isKnownToUnionMember = kind.equals(PropDefinitionKind.LOOSE_UNION);


// boolean loosePropertyDeclaration = isQNameAssignmentTarget(n) && !isStruct;
// Traditionally, we would not report a warning for "loose" properties, but we want to be
// able to be more strict, so introduce an optional warning.
SuggestionPair pair = null; SuggestionPair pair = null;
if (!lowConfidence && !isKnownToUnionMember) { if (!lowConfidence && !isKnownToUnionMember) {
pair = getClosestPropertySuggestion(objectType, propName, (propName.length() - 1) / 4); pair = getClosestPropertySuggestion(objectType, propName, (propName.length() - 1) / 4);
Expand All @@ -1864,10 +1923,10 @@ private void reportMissingProperty(
} }
report( report(
t, t,
n.getLastChild(), propNode,
reportType, reportType,
propName, propName,
typeRegistry.getReadableTypeName(n.getFirstChild()), objNode != null ? typeRegistry.getReadableTypeName(objNode) : objectType.toString(),
pair.suggestion); pair.suggestion);
} else { } else {
DiagnosticType reportType; DiagnosticType reportType;
Expand All @@ -1884,10 +1943,10 @@ private void reportMissingProperty(
} }
report( report(
t, t,
n.getLastChild(), propNode,
reportType, reportType,
propName, propName,
typeRegistry.getReadableTypeName(n.getFirstChild())); objNode != null ? typeRegistry.getReadableTypeName(objNode) : objectType.toString());
} }
} }


Expand Down
65 changes: 59 additions & 6 deletions test/com/google/javascript/jscomp/TypeCheckNoTranspileTest.java
Expand Up @@ -4001,10 +4001,14 @@ public void testObjectDestructuringNullCausesWarning() {
"required: Object")); "required: Object"));
} }


public void testObjectDestructuringNullableDoesntCausesWarning() { public void testObjectDestructuringNullableDoesntCauseWarning() {
// This matches the behavior of typechecking transpiled destructuring patterns, although // Test that we don't get a "cannot destructure 'null' or 'undefined'" warning, which matches
// we certainly could be more strict in the future and forbid dereferencing undefined/null. // the legacy behavior when typechecking transpiled destructuring patterns.
testTypes(lines("function f(/** ?Object */ nullableObj) {", "const {x} = nullableObj;", "}")); testTypes(
lines(
"function f(/** ?{x: number} */ nullableObj) {", //
"const {x} = nullableObj;",
"}"));
} }


public void testArrayDestructuringNonIterableCausesWarning() { public void testArrayDestructuringNonIterableCausesWarning() {
Expand Down Expand Up @@ -4040,9 +4044,58 @@ public void testArrayDestructuringParameterWithElision() {
testTypes("/** @param {!Array<number>} numbers */ function f([, x, , y]) {}"); testTypes("/** @param {!Array<number>} numbers */ function f([, x, , y]) {}");
} }


public void testObjectPatternDeclarationWithMissingPropertyWarning() {
testTypes(
lines(
"function f(/** {a: number} */ obj) {", //
" const {a, b} = obj;",
"}"),
"Property b never defined on obj");
}

public void testObjectPatternAssignWithMissingPropertyWarning() {
testTypes(
lines(
"function f(/** {a: number} */ obj) {", //
" let a, b;",
" ({a, b} = obj);",
"}"),
"Property b never defined on obj");
}

public void testObjectPatternDeclarationWithMissingPropertyWarningInForOf() {
testTypes(
lines(
"function f(/** !Iterable<{a: number}> */ aNumberObj) {",
" for (const {a, b} of aNumberObj) {}",
"}"),
"Property b never defined on {a: number}");
}

public void testObjectPatternWithMissingPropertyWarningInParameters() {
testTypes(
lines(
"/** @param {{a: number}} obj */", //
"function f(/** {a: number} */ {b}) {}"),
"Property b never defined on {a: number}");
}

public void testArrayPatternAssignWithIllegalPropCreationInStruct() {
testTypes(
lines(
"class C {", //
" f(/** !Iterable<number> */ ) {",
" [this.x] = arr;",
" }",
"}"),
new String[] {
"Cannot add a property to a struct instance after it is constructed. "
+ "(If you already declared the property, make sure to give it a type.)",
"Property x never defined on C"
});
}

public void testDictClass1() { public void testDictClass1() {
testTypes("/** @dict */ var C = class { constructor() {} 'x'(){} };"); testTypes("/** @dict */ var C = class { constructor() {} 'x'(){} };");
} }

// TODO(b/77597706): add tests for missing property warnings on object destructuring string keys
} }

0 comments on commit 7504f6a

Please sign in to comment.