Skip to content

Commit

Permalink
Make default nullability in the type system work identically for glob…
Browse files Browse the repository at this point in the history
…al and local types

This is mostly important for moving module rewriting post-checks, as module types are local.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=253823662
  • Loading branch information
lauraharker committed Jun 19, 2019
1 parent 4877e4f commit af41c2c
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 94 deletions.
82 changes: 32 additions & 50 deletions src/com/google/javascript/jscomp/TypedScopeCreator.java
Expand Up @@ -61,8 +61,8 @@
import com.google.javascript.jscomp.CodingConvention.ObjectLiteralCast; import com.google.javascript.jscomp.CodingConvention.ObjectLiteralCast;
import com.google.javascript.jscomp.CodingConvention.SubclassRelationship; import com.google.javascript.jscomp.CodingConvention.SubclassRelationship;
import com.google.javascript.jscomp.FunctionTypeBuilder.AstFunctionContents; import com.google.javascript.jscomp.FunctionTypeBuilder.AstFunctionContents;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.jscomp.NodeTraversal.AbstractScopedCallback; import com.google.javascript.jscomp.NodeTraversal.AbstractScopedCallback;
import com.google.javascript.jscomp.NodeTraversal.AbstractShallowStatementCallback;
import com.google.javascript.jscomp.ProcessClosureProvidesAndRequires.ProvidedName; import com.google.javascript.jscomp.ProcessClosureProvidesAndRequires.ProvidedName;
import com.google.javascript.jscomp.modules.Export; import com.google.javascript.jscomp.modules.Export;
import com.google.javascript.jscomp.modules.Module; import com.google.javascript.jscomp.modules.Module;
Expand Down Expand Up @@ -413,9 +413,6 @@ private TypedScope createScopeInternal(Node root, TypedScope typedParent) {
externsRoot.setJSType(globalThis); externsRoot.setJSType(globalThis);
jsRoot.setJSType(globalThis); jsRoot.setJSType(globalThis);


// Run a first-order analysis over the syntax tree.
new FirstOrderFunctionAnalyzer().process(root.getFirstChild(), root.getLastChild());

// Find all the classes in the global scope. // Find all the classes in the global scope.
newScope = createInitialScope(root); newScope = createInitialScope(root);
} else { } else {
Expand Down Expand Up @@ -615,7 +612,7 @@ void patchGlobalScope(TypedScope globalScope, Node scriptRoot) {
assignedVarNames.removeIf(var -> inScript.test(var.getScopeRoot())); assignedVarNames.removeIf(var -> inScript.test(var.getScopeRoot()));
functionsWithNonEmptyReturns.removeIf(inScript); functionsWithNonEmptyReturns.removeIf(inScript);


new FirstOrderFunctionAnalyzer().process(null, scriptRoot); NodeTraversal.traverse(compiler, scriptRoot, new FirstOrderFunctionAnalyzer());


// TODO(bashir): Variable declaration is not the only side effect of last // TODO(bashir): Variable declaration is not the only side effect of last
// global scope generation but here we only wipe that part off. // global scope generation but here we only wipe that part off.
Expand Down Expand Up @@ -657,10 +654,19 @@ void patchGlobalScope(TypedScope globalScope, Node scriptRoot) {
TypedScope createInitialScope(Node root) { TypedScope createInitialScope(Node root) {
checkArgument(root.isRoot(), root); checkArgument(root.isRoot(), root);


NodeTraversal.traverse( // Gather global information used in typed scope creation. Use a memoized scope creator because
compiler, // scope-building takes a nontrivial amount of time.
root, MemoizedScopeCreator scopeCreator =
new IdentifyGlobalEnumsAndTypedefsAsNonNullable(typeRegistry, codingConvention)); new MemoizedScopeCreator(new Es6SyntacticScopeCreator(compiler));

new NodeTraversal(compiler, new FirstOrderFunctionAnalyzer(), scopeCreator)
.traverseRoots(root.getFirstChild(), root.getLastChild());

new NodeTraversal(
compiler,
new IdentifyEnumsAndTypedefsAsNonNullable(typeRegistry, codingConvention),
scopeCreator)
.traverse(root);


TypedScope s = TypedScope.createGlobalScope(root); TypedScope s = TypedScope.createGlobalScope(root);
declareNativeFunctionType(s, ARRAY_FUNCTION_TYPE); declareNativeFunctionType(s, ARRAY_FUNCTION_TYPE);
Expand Down Expand Up @@ -733,17 +739,12 @@ void resolveTypes() {
typeRegistry.resolveTypes(); typeRegistry.resolveTypes();
} }


/** /** Adds all enums and typedefs to the registry's list of non-nullable types. */
* Adds all globally-defined enums and typedefs to the registry's list of non-nullable types. private static class IdentifyEnumsAndTypedefsAsNonNullable extends AbstractPostOrderCallback {
*
* <p>TODO(b/123710194): We should also make locally-defined enums and typedefs non-nullable.
*/
private static class IdentifyGlobalEnumsAndTypedefsAsNonNullable
extends AbstractShallowStatementCallback {
private final JSTypeRegistry registry; private final JSTypeRegistry registry;
private final CodingConvention codingConvention; private final CodingConvention codingConvention;


IdentifyGlobalEnumsAndTypedefsAsNonNullable( IdentifyEnumsAndTypedefsAsNonNullable(
JSTypeRegistry registry, CodingConvention codingConvention) { JSTypeRegistry registry, CodingConvention codingConvention) {
this.registry = registry; this.registry = registry;
this.codingConvention = codingConvention; this.codingConvention = codingConvention;
Expand All @@ -755,29 +756,22 @@ public void visit(NodeTraversal t, Node node, Node parent) {
case LET: case LET:
case CONST: case CONST:
case VAR: case VAR:
// Note that this class expects to be invoked on the root node and does not traverse into
// functions.
Scope scope = t.getScope();
if (!(scope.isGlobal() || scope.getClosestHoistScope().isGlobal())) {
return;
}
if (node.isVar() || scope.isGlobal()) {
for (Node child = node.getFirstChild(); child != null; child = child.getNext()) { for (Node child = node.getFirstChild(); child != null; child = child.getNext()) {
// TODO(b/116853368): make this work for destructuring aliases as well. // TODO(b/116853368): make this work for destructuring aliases as well.
identifyEnumOrTypedefDeclaration( identifyEnumOrTypedefDeclaration(
child, child.getFirstChild(), NodeUtil.getBestJSDocInfo(child)); t, child, child.getFirstChild(), NodeUtil.getBestJSDocInfo(child));
} }
}
break; break;
case EXPR_RESULT: case EXPR_RESULT:
Node firstChild = node.getFirstChild(); Node firstChild = node.getFirstChild();
if (firstChild.isAssign()) { if (firstChild.isAssign()) {
Node assign = firstChild; Node assign = firstChild;
identifyEnumOrTypedefDeclaration( identifyEnumOrTypedefDeclaration(
assign.getFirstChild(), assign.getSecondChild(), assign.getJSDocInfo()); t, assign.getFirstChild(), assign.getSecondChild(), assign.getJSDocInfo());
} else { } else if (firstChild.isGetProp()) {
identifyEnumOrTypedefDeclaration( identifyEnumOrTypedefDeclaration(
firstChild, /* rvalue= */ null, firstChild.getJSDocInfo()); t, firstChild, /* rvalue= */ null, firstChild.getJSDocInfo());
} }
break; break;
default: default:
Expand All @@ -786,19 +780,19 @@ public void visit(NodeTraversal t, Node node, Node parent) {
} }


private void identifyEnumOrTypedefDeclaration( private void identifyEnumOrTypedefDeclaration(
Node nameNode, @Nullable Node rvalue, JSDocInfo info) { NodeTraversal t, Node nameNode, @Nullable Node rvalue, JSDocInfo info) {
if (!nameNode.isQualifiedName()) { if (!nameNode.isQualifiedName()) {
return; return;
} }
if (info != null && info.hasEnumParameterType()) { if (info != null && info.hasEnumParameterType()) {
registry.identifyNonNullableName(nameNode.getQualifiedName()); registry.identifyNonNullableName(t.getScope(), nameNode.getQualifiedName());
} else if (info != null && info.hasTypedefType()) { } else if (info != null && info.hasTypedefType()) {
registry.identifyNonNullableName(nameNode.getQualifiedName()); registry.identifyNonNullableName(t.getScope(), nameNode.getQualifiedName());
} else if (rvalue != null } else if (rvalue != null
&& rvalue.isQualifiedName() && rvalue.isQualifiedName()
&& registry.isNonNullableName(rvalue.getQualifiedName()) && registry.isNonNullableName(t.getScope(), rvalue.getQualifiedName())
&& NodeUtil.isConstantDeclaration(codingConvention, info, nameNode)) { && NodeUtil.isConstantDeclaration(codingConvention, info, nameNode)) {
registry.identifyNonNullableName(nameNode.getQualifiedName()); registry.identifyNonNullableName(t.getScope(), nameNode.getQualifiedName());
} }
} }
} }
Expand Down Expand Up @@ -2335,7 +2329,7 @@ private void declareAliasTypeIfRvalueIsAliasable(
// Propagate typedef type to typedef aliases. // Propagate typedef type to typedef aliases.
actualLvalueNode.setTypedefTypeProp(typedefType); actualLvalueNode.setTypedefTypeProp(typedefType);
if (lValueName != null) { if (lValueName != null) {
typeRegistry.identifyNonNullableName(lValueName); typeRegistry.identifyNonNullableName(aliasDeclarationScope, lValueName);
typeRegistry.declareType(aliasDeclarationScope, lValueName, typedefType); typeRegistry.declareType(aliasDeclarationScope, lValueName, typedefType);
} }
return; return;
Expand Down Expand Up @@ -2363,10 +2357,7 @@ private void declareAliasTypeIfRvalueIsAliasable(
// Look for cases where the rValue is an Enum namespace // Look for cases where the rValue is an Enum namespace
typeRegistry.declareType( typeRegistry.declareType(
aliasDeclarationScope, lValueName, rValueType.toMaybeEnumType().getElementsType()); aliasDeclarationScope, lValueName, rValueType.toMaybeEnumType().getElementsType());
if (isLValueRootedInGlobalScope(actualLvalueNode)) { typeRegistry.identifyNonNullableName(aliasDeclarationScope, lValueName);
// TODO(b/123710194): Also make local aliases non-nullable
typeRegistry.identifyNonNullableName(lValueName);
}
} }
} }


Expand Down Expand Up @@ -3433,15 +3424,6 @@ private JSType determineGetterType(FunctionType methodType) {
* <p>The syntactic scopes created in this traversal are also stored for later use. * <p>The syntactic scopes created in this traversal are also stored for later use.
*/ */
private class FirstOrderFunctionAnalyzer extends AbstractScopedCallback { private class FirstOrderFunctionAnalyzer extends AbstractScopedCallback {

void process(Node externs, Node root) {
if (externs == null) {
NodeTraversal.traverse(compiler, root, this);
} else {
NodeTraversal.traverseRoots(compiler, this, externs, root);
}
}

@Override @Override
public void enterScope(NodeTraversal t) { public void enterScope(NodeTraversal t) {
Scope scope = t.getScope(); Scope scope = t.getScope();
Expand Down
47 changes: 23 additions & 24 deletions src/com/google/javascript/rhino/jstype/JSTypeRegistry.java
Expand Up @@ -78,7 +78,6 @@
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
Expand Down Expand Up @@ -158,20 +157,22 @@ public class JSTypeRegistry implements Serializable {
// when serializing and deserializing compiler state for multistage builds. // when serializing and deserializing compiler state for multistage builds.
private final Node nameTableGlobalRoot = new Node(Token.ROOT); private final Node nameTableGlobalRoot = new Node(Token.ROOT);


// NOTE(nicksantos): This is a terrible terrible hack. When type expressions // NOTE(nicksantos): This is a terrible terrible hack. When type expressions are evaluated, we
// are evaluated, we need to be able to decide whether that type name // need to be able to decide whether that type name resolves to a nullable type or a non-nullable
// resolves to a nullable type or a non-nullable type. Object types are // type. Object types are nullable, but enum types and typedefs are not.
// nullable, but enum types are not.
// //
// Notice that it's not good enough to just declare enum types sooner. // Notice that it's not good enough to just declare enum types and typedefs sooner.
// For example, if we have // For example, if we have
// /** @enum {MyObject} */ var MyEnum = ...; // /** @enum {MyObject} */ var MyEnum = ...;
// we won't be to declare "MyEnum" without evaluating the expression // we won't be to declare "MyEnum" without evaluating the expression {MyObject}, and following
// {MyObject}, and following those dependencies starts to lead us into // those dependencies starts to lead us into undecidable territory. Instead, we "pre-declare" enum
// undecidable territory. Instead, we "pre-declare" enum types and typedefs, // types and typedefs, so that the expression resolver can decide whether a given name is
// so that the expression resolver can decide whether a given name is
// nullable or not. // nullable or not.
private final Set<String> nonNullableTypeNames = new LinkedHashSet<>(); // Also, note that this solution is buggy and the type resolution still gets default nullability
// wrong in some cases. See b/116853368. Probably NamedType should be responsible for
// applying nullability to forward references instead of the type expression evaluation.
private final Multimap<Node, String> nonNullableTypeNames =
MultimapBuilder.hashKeys().hashSetValues().build();


// Types that have been "forward-declared." // Types that have been "forward-declared."
// If these types are not declared anywhere in the binary, we shouldn't // If these types are not declared anywhere in the binary, we shouldn't
Expand Down Expand Up @@ -490,7 +491,7 @@ private void initializeBuiltInTypes() {


// Thenable is an @typedef // Thenable is an @typedef
JSType thenableType = createRecordType(ImmutableMap.of("then", unknownType)); JSType thenableType = createRecordType(ImmutableMap.of("then", unknownType));
identifyNonNullableName("Thenable"); identifyNonNullableName(null, "Thenable");
registerNativeType(JSTypeNative.THENABLE_TYPE, thenableType); registerNativeType(JSTypeNative.THENABLE_TYPE, thenableType);


// Create built-in Promise type, whose constructor takes one parameter. // Create built-in Promise type, whose constructor takes one parameter.
Expand Down Expand Up @@ -1900,20 +1901,18 @@ public NamedType createNamedType(
return new NamedType(scope, this, reference, sourceName, lineno, charno); return new NamedType(scope, this, reference, sourceName, lineno, charno);
} }


/** /** Identifies the name of a typedef or enum before we actually declare it. */
* Identifies the name of a typedef or enum before we actually declare it. public void identifyNonNullableName(StaticScope scope, String name) {
*/
public void identifyNonNullableName(String name) {
checkNotNull(name); checkNotNull(name);
nonNullableTypeNames.add(name); StaticScope lookupScope = getLookupScope(scope, name);
nonNullableTypeNames.put(getRootNodeForScope(lookupScope), name);
} }


/** /** Identifies the name of a typedef or enum before we actually declare it. */
* Identifies the name of a typedef or enum before we actually declare it. public boolean isNonNullableName(StaticScope scope, String name) {
*/
public boolean isNonNullableName(String name) {
checkNotNull(name); checkNotNull(name);
return nonNullableTypeNames.contains(name); scope = getLookupScope(scope, name);
return nonNullableTypeNames.containsEntry(getRootNodeForScope(scope), name);
} }


public JSType evaluateTypeExpression(JSTypeExpression expr, StaticTypedScope scope) { public JSType evaluateTypeExpression(JSTypeExpression expr, StaticTypedScope scope) {
Expand Down Expand Up @@ -2019,7 +2018,7 @@ private JSType createFromTypeNodesInternal(
n.getLineno(), n.getLineno(),
n.getCharno(), n.getCharno(),
recordUnresolvedTypes); recordUnresolvedTypes);
if (namedType instanceof ObjectType && !nonNullableTypeNames.contains(n.getString())) { if (namedType instanceof ObjectType && !isNonNullableName(scope, n.getString())) {
Node typeList = n.getFirstChild(); Node typeList = n.getFirstChild();
boolean isForwardDeclared = namedType instanceof NamedType; boolean isForwardDeclared = namedType instanceof NamedType;
if ((!namedType.isUnknownType() || isForwardDeclared) && typeList != null) { if ((!namedType.isUnknownType() || isForwardDeclared) && typeList != null) {
Expand Down
31 changes: 14 additions & 17 deletions test/com/google/javascript/jscomp/TypeCheckNoTranspileTest.java
Expand Up @@ -1027,7 +1027,6 @@ public void testGlobalEnumWithConst() {


@Test @Test
public void testLocalEnumWithLet() { public void testLocalEnumWithLet() {
// TODO(bradfordcsmith): Local enum types should be non-nullable just like the global ones.
testTypes( testTypes(
lines( lines(
"{", "{",
Expand All @@ -1039,16 +1038,11 @@ public void testLocalEnumWithLet() {
" * @return {number}", " * @return {number}",
" */", " */",
" function f(x) {return x}", " function f(x) {return x}",
"}"), "}"));
lines(
"inconsistent return type",
"found : (E<number>|null)",
"required: number"));
} }


@Test @Test
public void testLocalEnumWithConst() { public void testLocalEnumWithConst() {
// TODO(bradfordcsmith): Local enum types should be non-nullable just like the global ones.
testTypes( testTypes(
lines( lines(
"{", "{",
Expand All @@ -1060,11 +1054,7 @@ public void testLocalEnumWithConst() {
" * @return {number}", " * @return {number}",
" */", " */",
" function f(x) {return x}", " function f(x) {return x}",
"}"), "}"));
lines(
"inconsistent return type",
"found : (E<number>|null)",
"required: number"));
} }


@Test @Test
Expand Down Expand Up @@ -6157,18 +6147,25 @@ public void testLocalEnumDoesNotInfluenceGlobalDefaultNullablity() {


@Test @Test
public void testGlobalEnumDoesNotInfluenceLocalDefaultNullablity() { public void testGlobalEnumDoesNotInfluenceLocalDefaultNullablity() {
// TODO(b/123710194): the local Foo should be nullable and this should not warn
testTypes( testTypes(
lines( lines(
"/** @enum {number} */ const Foo = {A: 1};", "/** @enum {number} */ const Foo = {A: 1};",
"function f() {", "function f() {",
" class Foo {};", " class Foo {};",
" /** @type {Foo} */ let x = null;", " /** @type {Foo} */ let x = null;",
"}"), "}"));
}

@Test
public void testLocalEnumAliasDoesNotInfluenceGlobalDefaultNullablity() {
testTypes(
lines( lines(
"initializing variable", // "class Foo {};",
"found : null", "/** @enum {number} */ const Bar = {A: 1};",
"required: Foo")); "function f() {",
" const Foo = Bar;",
"}",
"/** @type {Foo} */ let x = null;"));
} }


@Test @Test
Expand Down
6 changes: 3 additions & 3 deletions test/com/google/javascript/jscomp/TypeCheckTest.java
Expand Up @@ -77,9 +77,9 @@ protected CompilerOptions getDefaultOptions() {


@Test @Test
public void testInitialTypingScope() { public void testInitialTypingScope() {
TypedScope s = new TypedScopeCreator(compiler, TypedScope s =
CodingConventions.getDefault()).createInitialScope( new TypedScopeCreator(compiler, CodingConventions.getDefault())
new Node(Token.ROOT)); .createInitialScope(new Node(Token.ROOT, new Node(Token.ROOT), new Node(Token.ROOT)));


assertTypeEquals(getNativeArrayConstructorType(), s.getVar("Array").getType()); assertTypeEquals(getNativeArrayConstructorType(), s.getVar("Array").getType());
assertTypeEquals(getNativeBooleanObjectConstructorType(), s.getVar("Boolean").getType()); assertTypeEquals(getNativeBooleanObjectConstructorType(), s.getVar("Boolean").getType());
Expand Down
37 changes: 37 additions & 0 deletions test/com/google/javascript/jscomp/TypedScopeCreatorTest.java
Expand Up @@ -4825,6 +4825,43 @@ public void testGoogRequire_exportObjectLiteralWithLiteralValue() {
assertNode(xNode).hasJSTypeThat().toStringIsEqualTo("{x: number}"); assertNode(xNode).hasJSTypeThat().toStringIsEqualTo("{x: number}");
} }


@Test
public void testModuleTypedef_isNonnullableWithinModule() {
testSame(
lines(
"goog.module('a.b'); var /** strType */ earlyRef; ",
"/** @typedef {string} */ let strType; ",
"var /** strType */ normalRef; MODULE: mod;"));

TypedScope moduleScope = getLabeledStatement("MODULE").enclosingScope;
assertThat(moduleScope.getSlot("earlyRef")).hasJSTypeThat().isString();
assertThat(moduleScope.getSlot("normalRef")).hasJSTypeThat().isString();
}

@Test
public void testModuleTypedef_isNonnullableWhenRequired() {

testSame(
srcs(
lines(
"goog.module('root');",
"/** @typedef {string} */ let strType; ",
"exports.strType = strType;"),
lines(
"goog.module('a.b'); const {strType} = goog.require('root');",
"/** @param {strType} earlyRef */ function f(earlyRef) { EARLY_REF: earlyRef; }",
"var /** strType */ normalRef; NORMAL_REF: normalRef;")));

assertNode(getLabeledStatement("EARLY_REF").statementNode.getOnlyChild())
.hasJSTypeThat()
// TODO(b/116853368): This should just be 'string', but the function declaration is
// hoisted so it hits bugs in nullability of forward references to types.
.toStringIsEqualTo("(null|string)");
assertNode(getLabeledStatement("NORMAL_REF").statementNode.getOnlyChild())
.hasJSTypeThat()
.isString();
}

@Test @Test
public void testGoogRequire_namedExportImportedAsNamespace() { public void testGoogRequire_namedExportImportedAsNamespace() {
testSame( testSame(
Expand Down

0 comments on commit af41c2c

Please sign in to comment.