Skip to content

Commit

Permalink
Identify AssertionFunctionSpecs by ClosurePrimitive as well as global…
Browse files Browse the repository at this point in the history
… name

This creates two new ClosurePrimitives:
  - ASSERTS_TRUTHY: equivalent to goog.asserts.assert
  - ASSERTS_MATCHES_RETURN: equivalent to goog.asserts.assertString/Element/Array/etc.

Creates a new structure 'AssertionFunctionMap' to simplify doing lookups by both ClosurePrimitive and string name, and makes TypeInference use that structure

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=239269399
  • Loading branch information
lauraharker authored and tjgq committed Mar 20, 2019
1 parent ead71df commit 54097a7
Show file tree
Hide file tree
Showing 15 changed files with 358 additions and 114 deletions.
4 changes: 2 additions & 2 deletions src/com/google/javascript/jscomp/ChromeCodingConvention.java
Expand Up @@ -66,8 +66,8 @@ public ImmutableCollection<String> getIndirectlyDeclaredProperties() {
@Override @Override
public ImmutableCollection<AssertionFunctionSpec> getAssertionFunctions() { public ImmutableCollection<AssertionFunctionSpec> getAssertionFunctions() {
return ImmutableList.of( return ImmutableList.of(
AssertionFunctionSpec.makeTruthyAssertion("assert"), AssertionFunctionSpec.forTruthy().setFunctionName("assert").build(),
AssertionFunctionSpec.makeReturnTypeAssertion("cr.ui.decorate")); AssertionFunctionSpec.forMatchesReturn().setFunctionName("cr.ui.decorate").build());
} }


@Override @Override
Expand Down
16 changes: 9 additions & 7 deletions src/com/google/javascript/jscomp/ClosureCodeRemoval.java
Expand Up @@ -23,6 +23,7 @@
import com.google.javascript.rhino.Node; import com.google.javascript.rhino.Node;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Set; import java.util.Set;


/** /**
Expand Down Expand Up @@ -176,15 +177,16 @@ private class FindAssertionCalls extends AbstractPostOrderCallback {
final Set<String> assertionNames; final Set<String> assertionNames;


FindAssertionCalls() { FindAssertionCalls() {
ImmutableSet.Builder<String> assertionNamesBuilder = ImmutableSet.builder(); // TODO(b/126254920): make this use ClosurePrimitive instead,
for (AssertionFunctionSpec spec : assertionNames =
compiler.getCodingConvention().getAssertionFunctions()) { compiler.getCodingConvention().getAssertionFunctions().stream()
assertionNamesBuilder.add(spec.getFunctionName()); .map(AssertionFunctionSpec::getFunctionName)
} // Filter out assertion functions with a null functionName, which are identified only
assertionNames = assertionNamesBuilder.build(); // by ClosurePrimitive id.
.filter(Objects::nonNull)
.collect(ImmutableSet.toImmutableSet());
} }



@Override @Override
public void visit(NodeTraversal t, Node n, Node parent) { public void visit(NodeTraversal t, Node n, Node parent) {
if (n.isCall()) { if (n.isCall()) {
Expand Down
30 changes: 20 additions & 10 deletions src/com/google/javascript/jscomp/ClosureCodingConvention.java
Expand Up @@ -382,16 +382,26 @@ public boolean isPrivate(String name) {


@Override @Override
public ImmutableCollection<AssertionFunctionSpec> getAssertionFunctions() { public ImmutableCollection<AssertionFunctionSpec> getAssertionFunctions() {
return ImmutableList.of( return ImmutableSet.<AssertionFunctionSpec>builder()
AssertionFunctionSpec.makeTruthyAssertion("goog.asserts.assert"), .addAll(super.getAssertionFunctions())
AssertionFunctionSpec.makeReturnTypeAssertion("goog.asserts.assertArray"), .add(
AssertionFunctionSpec.makeReturnTypeAssertion("goog.asserts.assertBoolean"), AssertionFunctionSpec.forTruthy().setFunctionName("goog.asserts.assert").build(),
AssertionFunctionSpec.makeReturnTypeAssertion("goog.asserts.assertElement"), createGoogAssertOnReturn("Array"),
AssertionFunctionSpec.makeReturnTypeAssertion("goog.asserts.assertFunction"), createGoogAssertOnReturn("Boolean"),
AssertionFunctionSpec.makeReturnTypeAssertion("goog.asserts.assertInstanceof"), createGoogAssertOnReturn("Element"),
AssertionFunctionSpec.makeReturnTypeAssertion("goog.asserts.assertNumber"), createGoogAssertOnReturn("Function"),
AssertionFunctionSpec.makeReturnTypeAssertion("goog.asserts.assertObject"), createGoogAssertOnReturn("Instanceof"),
AssertionFunctionSpec.makeReturnTypeAssertion("goog.asserts.assertString")); createGoogAssertOnReturn("Number"),
createGoogAssertOnReturn("Object"),
createGoogAssertOnReturn("String"))
.build();
}

/** Returns a new assertion function goog.asserts.assert[assertedTypeName] */
private static AssertionFunctionSpec createGoogAssertOnReturn(String assertedTypeName) {
return AssertionFunctionSpec.forMatchesReturn()
.setFunctionName("goog.asserts.assert" + assertedTypeName)
.build();
} }


@Override @Override
Expand Down
122 changes: 97 additions & 25 deletions src/com/google/javascript/jscomp/CodingConvention.java
Expand Up @@ -17,17 +17,23 @@


import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkArgument;


import com.google.auto.value.AutoValue;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.annotations.Immutable; import com.google.errorprone.annotations.Immutable;
import com.google.javascript.rhino.ClosurePrimitive;
import com.google.javascript.rhino.Node; import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.NominalTypeBuilder; import com.google.javascript.rhino.NominalTypeBuilder;
import com.google.javascript.rhino.StaticSourceFile; import com.google.javascript.rhino.StaticSourceFile;
import com.google.javascript.rhino.jstype.FunctionType; import com.google.javascript.rhino.jstype.FunctionType;
import com.google.javascript.rhino.jstype.JSType;
import com.google.javascript.rhino.jstype.JSTypeRegistry; import com.google.javascript.rhino.jstype.JSTypeRegistry;
import com.google.javascript.rhino.jstype.ObjectType; import com.google.javascript.rhino.jstype.ObjectType;
import java.io.Serializable; import java.io.Serializable;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Function;
import javax.annotation.Nullable; import javax.annotation.Nullable;


/** /**
Expand Down Expand Up @@ -486,50 +492,66 @@ static class ObjectLiteralCast {
* match-return-type handling does not have. * match-return-type handling does not have.
* </ul> * </ul>
*/ */
final class AssertionFunctionSpec { @Immutable
private final String functionName; @AutoValue
private final AssertionKind kind; abstract class AssertionFunctionSpec {
private final int paramIndex; // the index of the formal parameter that is actually asserted // TODO(b/126254920): remove this field and always use ClosurePrimitive
@Nullable
abstract String getFunctionName();

@Nullable
abstract ClosurePrimitive getClosurePrimitive();

abstract AssertionKind getAssertionKind();

abstract int getParamIndex(); // the index of the formal parameter that is actually asserted


public enum AssertionKind { public enum AssertionKind {
TRUTHY, // an assertion that the parameter is 'truthy' TRUTHY, // an assertion that the parameter is 'truthy'
MATCHES_RETURN_TYPE // an assertion that the parameter matches the inferred return kind MATCHES_RETURN_TYPE // an assertion that the parameter matches the inferred return kind
} }


private AssertionFunctionSpec(String functionName, AssertionKind kind, int paramIndex) { static Builder builder() {
this.functionName = functionName; return new AutoValue_CodingConvention_AssertionFunctionSpec.Builder().setParamIndex(0);
this.kind = kind;
this.paramIndex = paramIndex;
} }


/** Returns a truthy assertion function on the first param */ public static Builder forTruthy() {
public static AssertionFunctionSpec makeTruthyAssertion(String functionName) { return builder().setAssertionKind(AssertionKind.TRUTHY);
return new AssertionFunctionSpec(functionName, AssertionKind.TRUTHY, /* paramIndex= */ 0);
} }


/** Returns an assertion function asserting the first param matches the return type */ public static Builder forMatchesReturn() {
public static AssertionFunctionSpec makeReturnTypeAssertion(String functionName) { return builder().setAssertionKind(AssertionKind.MATCHES_RETURN_TYPE);
return new AssertionFunctionSpec(functionName, AssertionKind.MATCHES_RETURN_TYPE, 0);
} }


/** Returns an assertion function asserting the nth arg is truthy (0-indexed) */ @AutoValue.Builder
public static AssertionFunctionSpec makeTruthyAssertion(String functionName, int paramIndex) { public abstract static class Builder {
return new AssertionFunctionSpec(functionName, AssertionKind.TRUTHY, paramIndex); public abstract Builder setFunctionName(String name);
}
abstract Builder setClosurePrimitive(ClosurePrimitive primitive);

public abstract Builder setParamIndex(int paramIndex);


/** Returns the name of the function. */ abstract Builder setAssertionKind(AssertionKind kind);
public String getFunctionName() {
return functionName; abstract AssertionFunctionSpec autoBuild();

public AssertionFunctionSpec build() {
AssertionFunctionSpec spec = autoBuild();
Preconditions.checkState(
spec.getFunctionName() != null || spec.getClosurePrimitive() != null,
"Must provide a function name or ClosurePrimitive for each spec");
return spec;
}
} }


AssertionKind getAssertionKind() { private Object getId() {
return this.kind; return getClosurePrimitive() != null ? getClosurePrimitive() : getFunctionName();
} }


/** Returns which argument is actually being asserted, or null if fewer args than expected */ /** Returns which argument is actually being asserted, or null if fewer args than expected */
@Nullable @Nullable
public Node getAssertedArg(Node firstArg) { Node getAssertedArg(Node firstArg) {
for (int i = 0; i < paramIndex; i++) { for (int i = 0; i < getParamIndex(); i++) {
if (firstArg == null) { if (firstArg == null) {
// If there are fewer arguments than expected, return null instead of crashing in this // If there are fewer arguments than expected, return null instead of crashing in this
// function. // function.
Expand All @@ -540,4 +562,54 @@ public Node getAssertedArg(Node firstArg) {
return firstArg; return firstArg;
} }
} }

/** This stores a relation from either name or Closure Primitive to assertion function */
@Immutable
final class AssertionFunctionLookup {
// the key type 'Object' is mutable, but at runtime is only ever a string or ClosurePrimitive
@SuppressWarnings("Immutable")
private final ImmutableMap<Object, AssertionFunctionSpec> internal;

private AssertionFunctionLookup(ImmutableMap<Object, AssertionFunctionSpec> internal) {
this.internal = internal;
}

/**
* Returns a new map containing all the given {@link AssertionFunctionSpec}s
*
* <p>Assumes that in the input, there is a unique mapping from string name to spec and closure
* primitive to spec.
*/
static AssertionFunctionLookup of(Collection<AssertionFunctionSpec> specs) {
ImmutableMap<Object, AssertionFunctionSpec> idToSpecMap =
specs.stream()
.collect(
ImmutableMap.toImmutableMap(AssertionFunctionSpec::getId, Function.identity()));

return new AssertionFunctionLookup(idToSpecMap);
}

/**
* Returns the {@link AssertionFunctionSpec} matching the given function reference.
*
* <p>This first looks up specs by their ClosurePrimitive, then falls back to qualified name
*/
@Nullable
AssertionFunctionSpec lookupByCallee(Node callee) {
FunctionType fnType = JSType.toMaybeFunctionType(callee.getJSType());
if (fnType != null && fnType.getClosurePrimitive() != null) {
AssertionFunctionSpec spec = internal.get(fnType.getClosurePrimitive());
if (spec != null) {
return spec;
}
}

// TODO(b/126254920): remove this
if (callee.isQualifiedName()) {
return internal.get(callee.getQualifiedName());
}

return null;
}
}
} }
12 changes: 9 additions & 3 deletions src/com/google/javascript/jscomp/CodingConventions.java
Expand Up @@ -19,6 +19,7 @@
import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Preconditions.checkState;


import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.annotations.Immutable; import com.google.errorprone.annotations.Immutable;
import com.google.javascript.rhino.ClosurePrimitive; import com.google.javascript.rhino.ClosurePrimitive;
import com.google.javascript.rhino.Node; import com.google.javascript.rhino.Node;
Expand All @@ -28,7 +29,6 @@
import com.google.javascript.rhino.jstype.JSTypeRegistry; import com.google.javascript.rhino.jstype.JSTypeRegistry;
import com.google.javascript.rhino.jstype.ObjectType; import com.google.javascript.rhino.jstype.ObjectType;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;


Expand Down Expand Up @@ -551,8 +551,14 @@ public ObjectLiteralCast getObjectLiteralCast(Node callNode) {
} }


@Override @Override
public Collection<AssertionFunctionSpec> getAssertionFunctions() { public ImmutableSet<AssertionFunctionSpec> getAssertionFunctions() {
return Collections.emptySet(); return ImmutableSet.of(
AssertionFunctionSpec.forTruthy()
.setClosurePrimitive(ClosurePrimitive.ASSERTS_TRUTHY)
.build(),
AssertionFunctionSpec.forMatchesReturn()
.setClosurePrimitive(ClosurePrimitive.ASSERTS_MATCHES_RETURN)
.build());
} }


@Override @Override
Expand Down
14 changes: 9 additions & 5 deletions src/com/google/javascript/jscomp/ConformanceRules.java
Expand Up @@ -55,6 +55,7 @@
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException; import java.util.regex.PatternSyntaxException;
Expand Down Expand Up @@ -322,11 +323,14 @@ public AbstractTypeRestrictionRule(AbstractCompiler compiler, Requirement requir
List<String> whitelistedTypeNames = requirement.getValueList(); List<String> whitelistedTypeNames = requirement.getValueList();
whitelistedTypes = union(whitelistedTypeNames); whitelistedTypes = union(whitelistedTypeNames);


ImmutableList.Builder<Node> builder = ImmutableList.builder(); // TODO(b/126254920): make this use ClosurePrimitive instead
for (AssertionFunctionSpec fn : compiler.getCodingConvention().getAssertionFunctions()) { // so that we don't have to filter out the null functionNames.
builder.add(NodeUtil.newQName(compiler, fn.getFunctionName())); assertionsFunctionNames =
} compiler.getCodingConvention().getAssertionFunctions().stream()
assertionsFunctionNames = builder.build(); .map(AssertionFunctionSpec::getFunctionName)
.filter(Objects::nonNull)
.map(name -> NodeUtil.newQName(compiler, name))
.collect(ImmutableList.toImmutableList());
} }


protected boolean isWhitelistedType(Node n) { protected boolean isWhitelistedType(Node n) {
Expand Down
26 changes: 17 additions & 9 deletions src/com/google/javascript/jscomp/TypeInference.java
Expand Up @@ -36,6 +36,7 @@
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import com.google.javascript.jscomp.CodingConvention.AssertionFunctionLookup;
import com.google.javascript.jscomp.CodingConvention.AssertionFunctionSpec; import com.google.javascript.jscomp.CodingConvention.AssertionFunctionSpec;
import com.google.javascript.jscomp.ControlFlowGraph.Branch; import com.google.javascript.jscomp.ControlFlowGraph.Branch;
import com.google.javascript.jscomp.graph.DiGraph.DiGraphEdge; import com.google.javascript.jscomp.graph.DiGraph.DiGraphEdge;
Expand Down Expand Up @@ -89,18 +90,21 @@ class TypeInference
private final FlowScope bottomScope; private final FlowScope bottomScope;
private final TypedScope containerScope; private final TypedScope containerScope;
private final TypedScopeCreator scopeCreator; private final TypedScopeCreator scopeCreator;
private final Map<String, AssertionFunctionSpec> assertionFunctionsMap; private final AssertionFunctionLookup assertionFunctionLookup;


// Scopes that have had their unbound untyped vars inferred as undefined. // Scopes that have had their unbound untyped vars inferred as undefined.
private final Set<TypedScope> inferredUnboundVars = new HashSet<>(); private final Set<TypedScope> inferredUnboundVars = new HashSet<>();


// For convenience // For convenience
private final ObjectType unknownType; private final ObjectType unknownType;


TypeInference(AbstractCompiler compiler, ControlFlowGraph<Node> cfg, TypeInference(
ReverseAbstractInterpreter reverseInterpreter, AbstractCompiler compiler,
TypedScope syntacticScope, TypedScopeCreator scopeCreator, ControlFlowGraph<Node> cfg,
Map<String, AssertionFunctionSpec> assertionFunctionsMap) { ReverseAbstractInterpreter reverseInterpreter,
TypedScope syntacticScope,
TypedScopeCreator scopeCreator,
AssertionFunctionLookup assertionFunctionLookup) {
super(cfg, new LinkedFlowScope.FlowScopeJoinOp()); super(cfg, new LinkedFlowScope.FlowScopeJoinOp());
this.compiler = compiler; this.compiler = compiler;
this.registry = compiler.getTypeRegistry(); this.registry = compiler.getTypeRegistry();
Expand All @@ -110,7 +114,7 @@ class TypeInference
this.containerScope = syntacticScope; this.containerScope = syntacticScope;


this.scopeCreator = scopeCreator; this.scopeCreator = scopeCreator;
this.assertionFunctionsMap = assertionFunctionsMap; this.assertionFunctionLookup = assertionFunctionLookup;


FlowScope entryScope = FlowScope entryScope =
inferDeclarativelyUnboundVarsWithoutTypes( inferDeclarativelyUnboundVarsWithoutTypes(
Expand Down Expand Up @@ -1519,9 +1523,13 @@ private FlowScope traverseFunctionInvocation(Node n, FlowScope scope) {
private FlowScope tightenTypesAfterAssertions(FlowScope scope, Node callNode) { private FlowScope tightenTypesAfterAssertions(FlowScope scope, Node callNode) {
Node left = callNode.getFirstChild(); Node left = callNode.getFirstChild();
Node firstParam = left.getNext(); Node firstParam = left.getNext();
AssertionFunctionSpec assertionFunctionSpec = if (firstParam == null) {
assertionFunctionsMap.get(left.getQualifiedName()); // this may be an assertion call but there are no arguments to assert
if (assertionFunctionSpec == null || firstParam == null) { return scope;
}
AssertionFunctionSpec assertionFunctionSpec = assertionFunctionLookup.lookupByCallee(left);
if (assertionFunctionSpec == null) {
// this is not a recognized assertion function
return scope; return scope;
} }
Node assertedNode = assertionFunctionSpec.getAssertedArg(firstParam); Node assertedNode = assertionFunctionSpec.getAssertedArg(firstParam);
Expand Down

0 comments on commit 54097a7

Please sign in to comment.