All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.javascript.jscomp.CheckAccessControls Maven / Gradle / Ivy

Go to download

Closure Compiler is a JavaScript optimizing compiler. It parses your JavaScript, analyzes it, removes dead code and rewrites and minimizes what's left. It also checks syntax, variable references, and types, and warns about common JavaScript pitfalls. It is used in many of Google's JavaScript apps, including Gmail, Google Web Search, Google Maps, and Google Docs.

There is a newer version: v20240317
Show newest version
/*
 * Copyright 2008 The Closure Compiler Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.javascript.jscomp;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.javascript.jscomp.base.JSCompStrings.lines;

import com.google.auto.value.AutoValue;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableMap;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.JSDocInfo.Visibility;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.StaticSourceFile;
import com.google.javascript.rhino.jstype.FunctionType;
import com.google.javascript.rhino.jstype.JSType;
import com.google.javascript.rhino.jstype.JSTypeNative;
import com.google.javascript.rhino.jstype.JSTypeRegistry;
import com.google.javascript.rhino.jstype.ObjectType;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Objects;
import java.util.function.Supplier;
import org.jspecify.nullness.Nullable;

/**
 * A compiler pass that checks that the programmer has obeyed all the access control restrictions
 * indicated by JSDoc annotations, like {@code @private} and {@code @deprecated}.
 *
 * 

Because access control restrictions are attached to type information, this pass must run after * TypeInference, and InferJSDocInfo. */ class CheckAccessControls implements NodeTraversal.Callback, CompilerPass { static final DiagnosticType DEPRECATED_NAME = DiagnosticType.disabled("JSC_DEPRECATED_VAR", "Variable {0} has been deprecated."); static final DiagnosticType DEPRECATED_NAME_REASON = DiagnosticType.disabled("JSC_DEPRECATED_VAR_REASON", "Variable {0} has been deprecated: {1}"); static final DiagnosticType DEPRECATED_PROP = DiagnosticType.disabled( "JSC_DEPRECATED_PROP", "Property {0} of type {1} has been deprecated."); static final DiagnosticType DEPRECATED_PROP_REASON = DiagnosticType.disabled( "JSC_DEPRECATED_PROP_REASON", "Property {0} of type {1} has been deprecated: {2}"); static final DiagnosticType DEPRECATED_CLASS = DiagnosticType.disabled("JSC_DEPRECATED_CLASS", "Class {0} has been deprecated."); static final DiagnosticType DEPRECATED_CLASS_REASON = DiagnosticType.disabled("JSC_DEPRECATED_CLASS_REASON", "Class {0} has been deprecated: {1}"); static final DiagnosticType BAD_PACKAGE_PROPERTY_ACCESS = DiagnosticType.error( "JSC_BAD_PACKAGE_PROPERTY_ACCESS", "Access to package-private property {0} of {1} not allowed here."); static final DiagnosticType BAD_PRIVATE_GLOBAL_ACCESS = DiagnosticType.error( "JSC_BAD_PRIVATE_GLOBAL_ACCESS", "Access to private variable {0} not allowed outside file {1}."); static final DiagnosticType BAD_PRIVATE_PROPERTY_ACCESS = DiagnosticType.warning( "JSC_BAD_PRIVATE_PROPERTY_ACCESS", "Access to private property {0} of {1} not allowed here."); static final DiagnosticType BAD_PROTECTED_PROPERTY_ACCESS = DiagnosticType.warning( "JSC_BAD_PROTECTED_PROPERTY_ACCESS", "Access to protected property {0} of {1} not allowed here."); static final DiagnosticType BAD_PROPERTY_OVERRIDE_IN_FILE_WITH_FILEOVERVIEW_VISIBILITY = DiagnosticType.error( "JSC_BAD_PROPERTY_OVERRIDE_IN_FILE_WITH_FILEOVERVIEW_VISIBILITY", "Overridden property {0} in file with fileoverview visibility {1}" + " must explicitly redeclare superclass visibility"); static final DiagnosticType PRIVATE_OVERRIDE = DiagnosticType.warning("JSC_PRIVATE_OVERRIDE", "Overriding private property of {0}."); static final DiagnosticType EXTEND_FINAL_CLASS = DiagnosticType.error( "JSC_EXTEND_FINAL_CLASS", "{0} is not allowed to extend final class {1}."); static final DiagnosticType VISIBILITY_MISMATCH = DiagnosticType.warning( "JSC_VISIBILITY_MISMATCH", "Overriding {0} property of {1} with {2} property."); static final DiagnosticType CONST_PROPERTY_REASSIGNED_VALUE = DiagnosticType.warning( "JSC_CONSTANT_PROPERTY_REASSIGNED_VALUE", lines( "constant property {0} assigned a value more than once", // "Initialized at {1}")); static final DiagnosticType FINAL_PROPERTY_OVERRIDDEN = DiagnosticType.warning( "JSC_FINAL_PROPERTY_OVERRIDDEN", lines( "@final method or property {0} overridden", // "Initialized at {1}")); static final DiagnosticType CONST_PROPERTY_DELETED = DiagnosticType.warning( "JSC_CONSTANT_PROPERTY_DELETED", "constant property {0} cannot be deleted"); private final AbstractCompiler compiler; private final JSTypeRegistry typeRegistry; // State about the current traversal. private int deprecationDepth = 0; // NOTE: LinkedList is almost always the wrong choice, but in this case we have at most a small // handful of elements, it provides the smoothest API (push, pop, and a peek that doesn't throw // on empty), and (unlike ArrayDeque) is null-permissive. No other option meets all these needs. private final Deque currentClassStack = new LinkedList<>(); private final HashBasedTable constPropertyInits = HashBasedTable.create(); /** * Distinguishes between different kinds of "constant" JSDoc to provide more useful error messages */ private enum Constancy { FINAL, // @final OTHER_CONSTANT, // e.g. @const, @define, or @desc but not @final MUTABLE } private static class ConstantDeclaration { final Node node; final Constancy annotation; ConstantDeclaration(Node node, Constancy annotation) { this.node = node; this.annotation = annotation; } } private ImmutableMap defaultVisibilityForFiles; CheckAccessControls(AbstractCompiler compiler) { this.compiler = compiler; this.typeRegistry = compiler.getTypeRegistry(); } @Override public void process(Node externs, Node root) { CollectFileOverviewVisibility collectPass = new CollectFileOverviewVisibility(compiler); collectPass.process(externs, root); defaultVisibilityForFiles = collectPass.getFileOverviewVisibilityMap(); NodeTraversal.traverse(compiler, externs, this); NodeTraversal.traverse(compiler, root, this); } private void enterAccessControlScope(Node root) { @Nullable ObjectType scopeType = bestInstanceTypeForMethodOrCtor(root); if (isMarkedDeprecated(root)) { deprecationDepth++; } currentClassStack.push(scopeType); } private void exitAccessControlScope(Node root) { if (isMarkedDeprecated(root)) { deprecationDepth--; } currentClassStack.pop(); } /** * Maps {@code node} to the primary root of an access-control scope if it is some root, * or {@code null} if it is a non-root of the scope. * *

We define access-control scopes differently from {@code Scope}s because of mismatches * between ECMAScript scoping and our AST structure (e.g. the `extends` clause of a CLASS). {@link * NodeTraversal} is designed to walk the AST in ECMAScript scope order, which is not the * pre-order traversal that we would prefer here. This requires us to treat access-control scopes * as a forest with a primary root. * *

Each access-control scope corresponds to some "owner" JavaScript type which is used when * computing access-controls. At this time, each also corresponds to an AST subtree. * *

Known mismatches: * *

    *
  • CLASS extends clause: secondary root of the scope defined by the CLASS. *
*/ private static @Nullable Node primaryAccessControlScopeRootFor(Node node) { if (isExtendsTarget(node)) { return node.getParent(); } else if (isFunctionOrClass(node)) { return node; } else { return null; } } /** * Returns the instance object type that best represents a method or constructor definition, or * {@code null} if there is no representative type. * *
    *
  • Prototype methods => The instance type having that prototype *
  • Instance methods => The type the method is attached to *
  • Constructors => The type that constructor instantiates *
  • Object-literal members => The object-literal type *
*/ private @Nullable ObjectType bestInstanceTypeForMethodOrCtor(Node n) { checkState(isFunctionOrClass(n), n); Node parent = n.getParent(); // We need to handle declaration syntaxes separately in a way that we can't determine based on // the type of just one node. // TODO(nickreid): Determine if these can be replaced with FUNCTION and CLASS cases below. if (NodeUtil.isFunctionDeclaration(n) || NodeUtil.isClassDeclaration(n)) { return instanceTypeFor(n.getJSType()); } // All the remaining cases can be isolated based on `parent`. switch (parent.getToken()) { case NAME: return instanceTypeFor(n.getJSType()); case ASSIGN: { Node lValue = parent.getFirstChild(); if (NodeUtil.isNormalGet(lValue)) { // We have an assignment of the form `a.b = ...`. JSType lValueType = lValue.getJSType(); if (lValueType != null && (lValueType.isConstructor() || lValueType.isInterface())) { // Case `a.B = ...` return instanceTypeFor(lValueType); } else if (NodeUtil.isPrototypeProperty(lValue)) { // Case `a.B.prototype = ...` return instanceTypeFor(NodeUtil.getPrototypeClassName(lValue).getJSType()); } else { // Case `a.b = ...` return instanceTypeFor(lValue.getFirstChild().getJSType()); } } else { // We have an assignment of the form "a = ...", so pull the type off the "a". return instanceTypeFor(lValue.getJSType()); } } case STRING_KEY: case GETTER_DEF: case SETTER_DEF: case MEMBER_FUNCTION_DEF: case MEMBER_FIELD_DEF: case COMPUTED_PROP: { Node grandparent = parent.getParent(); Node greatGrandparent = grandparent.getParent(); if (grandparent.isObjectLit()) { return grandparent.getJSType().isFunctionPrototypeType() // Case: `grandparent` is an object-literal prototype. // Example: `Foo.prototype = { a: function() {} };` where `parent` is "a". ? instanceTypeFor(grandparent.getJSType()) : null; } else if (greatGrandparent.isClass()) { // Case: `n` is a class member definition. // Example: `class Foo { a() {} }` where `parent` is "a". return instanceTypeFor(greatGrandparent.getJSType()); } else { // This would indicate the AST is malformed. throw new AssertionError(greatGrandparent); } } default: return null; } } /** * Returns the type that best represents the instance type for {@code type}. * *
    *
  • Prototype type => The instance type having that prototype *
  • Instance type => The type *
  • Constructor type => The type that constructor instantiates *
  • Object-literal type => The type *
*/ private static @Nullable ObjectType instanceTypeFor(JSType type) { if (type == null) { return null; } else if (type.isUnionType()) { return null; // A union has no meaningful instance type. } else if (type.isInstanceType() || type.isUnknownType()) { return type.toMaybeObjectType(); } else if (type.isConstructor() || type.isInterface()) { return type.toMaybeFunctionType().getInstanceType(); } else if (type.isFunctionType()) { return null; // Functions that aren't ctors or interfaces have no instance type. } else if (type.isFunctionPrototypeType()) { return instanceTypeFor(type.toMaybeObjectType().getOwnerFunction()); } return type.toMaybeObjectType(); } @Override public boolean shouldTraverse(NodeTraversal traversal, Node node, Node parent) { @Nullable Node accessControlRoot = primaryAccessControlScopeRootFor(node); if (accessControlRoot != null) { enterAccessControlScope(accessControlRoot); } return true; } @Override public void visit(NodeTraversal traversal, Node node, Node parent) { IdentifierBehaviour identifierBehaviour = IdentifierBehaviour.select(node); @Nullable PropertyReference propRef = createPropertyReference(node); checkDeprecation(node, propRef, identifierBehaviour, traversal); checkVisibility(node, propRef, identifierBehaviour, traversal.getScope()); checkConstantProperty(propRef, identifierBehaviour); checkFinalClassOverrides(node); @Nullable Node accessControlRoot = primaryAccessControlScopeRootFor(node); if (accessControlRoot != null) { exitAccessControlScope(accessControlRoot); } } private void checkDeprecation( Node node, @Nullable PropertyReference propRef, IdentifierBehaviour identifierBehaviour, NodeTraversal traversal) { switch (identifierBehaviour) { case ES5_CLASS_INVOCATION: case ES6_CLASS_INVOCATION: case ES6_CLASS_NAMESPACE: // At these usages, treat the deprecation applied to type-declaration as referring to the // type, not the identifier (e.g. "the use of class `Foo` is deprecated"). checkTypeDeprecation(traversal, node); break; case NON_CONSTRUCTOR: // For all identifiers that are not constructors, deprecation refers to the identifier (e.g. // "the use of variable `x` is deprecated"). checkNameDeprecation(traversal, node); break; default: break; } if (propRef != null && !identifierBehaviour.equals(IdentifierBehaviour.ES5_CLASS_NAMESPACE)) { checkPropertyDeprecation(traversal, propRef); } } private void checkVisibility( Node node, @Nullable PropertyReference propRef, IdentifierBehaviour identifierBehaviour, Scope scope) { if (identifierBehaviour.equals(IdentifierBehaviour.ES6_CLASS_INVOCATION)) { checkEs6ConstructorInvocationVisibility(node); } if (!identifierBehaviour.equals(IdentifierBehaviour.ES5_CLASS_NAMESPACE)) { checkNameVisibility(scope, node); } if (propRef != null && !identifierBehaviour.equals(IdentifierBehaviour.ES5_CLASS_NAMESPACE)) { checkPropertyVisibility(propRef); } } /** * Reports deprecation issue with regard to a type usage. * *

Precondition: {@code n} has a constructor {@link JSType}. */ private void checkTypeDeprecation(NodeTraversal t, Node n) { if (!shouldEmitDeprecationWarning(t, n)) { return; } ObjectType instanceType = n.getJSType().toMaybeFunctionType().getInstanceType(); String deprecationInfo = getTypeDeprecationInfo(instanceType); if (deprecationInfo == null) { return; } DiagnosticType message = deprecationInfo.isEmpty() ? DEPRECATED_CLASS : DEPRECATED_CLASS_REASON; compiler.report(JSError.make(n, message, instanceType.toString(), deprecationInfo)); } /** Checks the given NAME node to ensure that access restrictions are obeyed. */ private void checkNameDeprecation(NodeTraversal t, Node n) { if (!n.isName()) { return; } if (!shouldEmitDeprecationWarning(t, n)) { return; } Var var = t.getScope().getVar(n.getString()); JSDocInfo docInfo = var == null ? null : var.getJSDocInfo(); if (docInfo != null && docInfo.isDeprecated()) { if (docInfo.getDeprecationReason() != null) { compiler.report( JSError.make(n, DEPRECATED_NAME_REASON, n.getString(), docInfo.getDeprecationReason())); } else { compiler.report(JSError.make(n, DEPRECATED_NAME, n.getString())); } } } /** Checks the given GETPROP node to ensure that access restrictions are obeyed. */ private void checkPropertyDeprecation(NodeTraversal t, PropertyReference propRef) { if (!shouldEmitDeprecationWarning(t, propRef)) { return; } // Don't bother checking constructors. if (propRef.getSourceNode().getParent().isNew()) { return; } ObjectType objectType = castToObject(dereference(propRef.getReceiverType())); String propertyName = propRef.getName(); if (objectType != null) { String deprecationInfo = getPropertyDeprecationInfo(objectType, propertyName); if (deprecationInfo != null) { if (!deprecationInfo.isEmpty()) { compiler.report( JSError.make( propRef.getSourceNode(), DEPRECATED_PROP_REASON, propertyName, propRef.getReadableTypeNameOrDefault(), deprecationInfo)); } else { compiler.report( JSError.make( propRef.getSourceNode(), DEPRECATED_PROP, propertyName, propRef.getReadableTypeNameOrDefault())); } } } } /** * Reports an error if the given name is not visible in the current context. * * @param scope The current scope. * @param name The name node. */ private void checkNameVisibility(Scope scope, Node name) { if (!name.isName()) { return; } Var var = scope.getVar(name.getString()); if (var == null) { return; } Visibility v = AccessControlUtils.getEffectiveNameVisibility(name, var, defaultVisibilityForFiles); switch (v) { case PACKAGE: if (!isPackageAccessAllowed(var, name)) { compiler.report( JSError.make( name, BAD_PACKAGE_PROPERTY_ACCESS, name.getString(), var.getSourceFile().getName())); } break; case PRIVATE: if (!isPrivateAccessAllowed(var, name)) { compiler.report( JSError.make( name, BAD_PRIVATE_GLOBAL_ACCESS, name.getString(), var.getSourceFile().getName())); } break; default: // Nothing to do for PUBLIC and PROTECTED // (which is irrelevant for names). break; } } private static boolean isPrivateAccessAllowed(Var var, Node name) { StaticSourceFile varSrc = var.getSourceFile(); StaticSourceFile refSrc = name.getStaticSourceFile(); return varSrc == null || refSrc == null || Objects.equals(varSrc.getName(), refSrc.getName()); } private boolean isPackageAccessAllowed(Var var, Node name) { StaticSourceFile varSrc = var.getSourceFile(); StaticSourceFile refSrc = name.getStaticSourceFile(); if (varSrc == null && refSrc == null) { // If the source file of either var or name is unavailable, conservatively assume they belong // to different packages. // // TODO(brndn): by contrast, isPrivateAccessAllowed does allow private access when a source // file is unknown. I didn't change it in order not to break existing code. return false; } CodingConvention codingConvention = compiler.getCodingConvention(); String srcPackage = codingConvention.getPackageName(varSrc); String refPackage = codingConvention.getPackageName(refSrc); return srcPackage != null && refPackage != null && Objects.equals(srcPackage, refPackage); } private void checkPropertyOverrideVisibilityIsSame( Visibility overriding, Visibility overridden, @Nullable Visibility fileOverview, PropertyReference propRef) { if (overriding == Visibility.INHERITED && overriding != overridden && fileOverview != null && fileOverview != Visibility.INHERITED) { compiler.report( JSError.make( propRef.getSourceNode(), BAD_PROPERTY_OVERRIDE_IN_FILE_WITH_FILEOVERVIEW_VISIBILITY, propRef.getName(), fileOverview.name())); } } private static @Nullable Visibility getOverridingPropertyVisibility(PropertyReference propRef) { JSDocInfo overridingInfo = propRef.getJSDocInfo(); return overridingInfo == null || !overridingInfo.isOverride() ? null : overridingInfo.getVisibility(); } /** Checks if a constructor is trying to override a final class. */ private void checkFinalClassOverrides(Node ctor) { if (!isFunctionOrClass(ctor) // checking class constructors is redundant because we already check the same thing on // the CLASS node || NodeUtil.isEs6ConstructorMemberFunctionDef(ctor.getParent())) { return; } FunctionType ctorType = ctor.getJSType().toMaybeFunctionType(); if (ctorType == null || !ctorType.isConstructor()) { return; } ObjectType finalParentClass = getSuperClassInstanceIfFinal(ctorType); if (finalParentClass != null) { compiler.report( JSError.make( ctor, EXTEND_FINAL_CLASS, ctorType.getDisplayName(), finalParentClass.getDisplayName())); } } /** Determines whether the given constant property got reassigned */ private void checkConstantProperty( @Nullable PropertyReference propRef, IdentifierBehaviour identifierBehaviour) { if (propRef == null || identifierBehaviour.equals(IdentifierBehaviour.ES5_CLASS_NAMESPACE)) { return; } ObjectType objectType = dereference(propRef.getReceiverType()); String propertyName = propRef.getName(); Node sourceNode = propRef.getSourceNode(); Constancy constness = isPropertyDeclaredConstant(objectType, propertyName); if (constness.equals(Constancy.MUTABLE)) { return; } if (sourceNode.isFromExterns() && propRef.isDeclaration()) { // Treat stub declarations in externs as inits, but never warn on them. this.recordConstPropertyInit(propRef, objectType, constness); return; } if (!propRef.isMutation()) { return; } if (propRef.isDeletion()) { compiler.report(JSError.make(sourceNode, CONST_PROPERTY_DELETED, propertyName)); return; } // Can't check for constant properties on generic function types. // TODO(johnlenz): I'm not 100% certain this is necessary, or if // the type is being inspected incorrectly. if (objectType.isFunctionType() && !objectType.toMaybeFunctionType().isConstructor()) { return; } if (objectType.isStructuralType() && !propRef.isDeclaration()) { // We don't know the claess this structural type matches, so assume all assignments are bad. compiler.report( JSError.make( sourceNode, CONST_PROPERTY_REASSIGNED_VALUE, propertyName, "unknown location due to structural typing")); return; } ConstantDeclaration init = this.getConstPropertyInit(propRef, objectType); if (init != null) { DiagnosticType diagnostic = init.annotation.equals(Constancy.FINAL) ? FINAL_PROPERTY_OVERRIDDEN : CONST_PROPERTY_REASSIGNED_VALUE; compiler.report(JSError.make(sourceNode, diagnostic, propertyName, init.node.getLocation())); } this.recordConstPropertyInit(propRef, objectType, constness); } private @Nullable ConstantDeclaration getConstPropertyInit( PropertyReference ref, ObjectType type) { String name = ref.getName(); while (type != null) { ConstantDeclaration init = this.constPropertyInits.get(type, name); if (init != null) { return init; } ConstantDeclaration canonicalInit = this.constPropertyInits.get(getCanonicalInstance(type), name); if (canonicalInit != null) { return canonicalInit; } type = type.getImplicitPrototype(); } return null; } private void recordConstPropertyInit( PropertyReference ref, ObjectType type, Constancy annotation) { this.constPropertyInits .row(type) .putIfAbsent(ref.getName(), new ConstantDeclaration(ref.getSourceNode(), annotation)); // Add the prototype when we're looking at an instance object if (type.isInstanceType()) { ObjectType prototype = type.getImplicitPrototype(); if (prototype != null && prototype.hasProperty(ref.getName())) { this.constPropertyInits .row(prototype) .putIfAbsent(ref.getName(), new ConstantDeclaration(ref.getSourceNode(), annotation)); } } } /** * Return an object with the same nominal type as obj, but without any possible extra properties * that exist on obj. */ static ObjectType getCanonicalInstance(ObjectType obj) { FunctionType ctor = obj.getConstructor(); return ctor == null ? obj : ctor.getInstanceType(); } /** Dereference a type, autoboxing it and filtering out null. */ private static @Nullable ObjectType dereference(JSType type) { return type == null ? null : type.dereference(); } private JSType typeOrUnknown(JSType type) { return (type == null) ? typeRegistry.getNativeType(JSTypeNative.UNKNOWN_TYPE) : type; } private ObjectType typeOrUnknown(ObjectType type) { return (ObjectType) typeOrUnknown((JSType) type); } private ObjectType boxedOrUnknown(@Nullable JSType type) { return typeOrUnknown(dereference(type)); } /** * Reports an error if the given property is not visible in the current context. * *

This method covers both: * *

    *
  • accesses to properties during execution *
  • overrides of properties during declaration *
* * TODO(nickreid): Things would probably be a lot simpler, though a bit duplicated, if these two * concepts were separated. Much of the underlying logic could stop checking various inconsistent * definitions of "is this an override". */ private void checkPropertyVisibility(PropertyReference propRef) { if (NodeUtil.isEs6ConstructorMemberFunctionDef(propRef.getSourceNode())) { // Class ctor *declarations* can never violate visibility restrictions. They are not // accesses and we don't consider them overrides. // // TODO(nickreid): It would be a lot cleaner if we could model this using `PropertyReference` // rather than defining a special case here. I think the problem is that the current // implementation of this method conflates "override" with "declaration". But that only works // because it ignores cases where there's no overridden definition. return; } JSType rawReferenceType = typeOrUnknown(propRef.getReceiverType()).autobox(); ObjectType referenceType = castToObject(rawReferenceType); String propertyName = propRef.getName(); StaticSourceFile definingSource = AccessControlUtils.getDefiningSource(propRef.getSourceNode(), referenceType, propertyName); // Is this a normal property access, or are we trying to override // an existing property? boolean isOverride = propRef.isDocumentedDeclaration() || propRef.isOverride(); ObjectType objectType = AccessControlUtils.getObjectType(referenceType, isOverride, propertyName); Visibility fileOverviewVisibility = defaultVisibilityForFiles.get(definingSource); Visibility visibility = getEffectivePropertyVisibility(propRef, referenceType, defaultVisibilityForFiles); if (isOverride) { Visibility overriding = getOverridingPropertyVisibility(propRef); if (overriding != null) { checkPropertyOverrideVisibilityIsSame( overriding, visibility, fileOverviewVisibility, propRef); } } JSType reportType = rawReferenceType; if (objectType != null) { Node node = objectType.getOwnPropertyDefSite(propertyName); if (node == null) { // Assume the property is public. return; } reportType = objectType; definingSource = node.getStaticSourceFile(); } else if (fileOverviewVisibility == null) { // We can only check visibility references if we know what file // it was defined in. // Otherwise just assume the property is public. return; } StaticSourceFile referenceSource = propRef.getSourceNode().getStaticSourceFile(); if (isOverride) { boolean sameInput = referenceSource != null && referenceSource.getName().equals(definingSource.getName()); checkPropertyOverrideVisibility( propRef, visibility, fileOverviewVisibility, reportType, sameInput); } else { checkPropertyAccessVisibility( propRef, visibility, reportType, referenceSource, definingSource); } } /** * Reports visibility violations on ES6 class constructor invocations. * *

Precondition: {@code target} has an ES6 class {@link JSType}. */ private void checkEs6ConstructorInvocationVisibility(Node target) { FunctionType ctorType = target.getJSType().toMaybeFunctionType(); ObjectType prototypeType = ctorType.getPrototype(); // We use the class definition site because classes automatically get a implicit constructor, // so there may not be a definition node. @Nullable Node classDefinition = ctorType.getSource(); @Nullable StaticSourceFile definingSource = (classDefinition == null) ? null : AccessControlUtils.getDefiningSource(classDefinition, prototypeType, "constructor"); // Synthesize a `PropertyReference` for this constructor call as if we're accessing // `Foo.prototype.constructor`. This object allows us to reuse the // `checkPropertyAccessVisibility` method which actually reports violations. PropertyReference fauxCtorRef = PropertyReference.builder() .setSourceNode(target) .setName("constructor") .setReceiverType(prototypeType) .setMutation(false) // This shouldn't matter. .setDeclaration(false) // This shouldn't matter. .setOverride(false) // This shouldn't matter. .setReadableTypeName(() -> ctorType.getInstanceType().toString()) .build(); Visibility annotatedCtorVisibility = // This function defaults to `INHERITED` which isn't what we want here, but it does handle // combining inline and `@fileoverview` visibilities. getEffectiveVisibilityForNonOverriddenProperty( fauxCtorRef, prototypeType, defaultVisibilityForFiles.get(definingSource)); Visibility effectiveCtorVisibility = annotatedCtorVisibility.equals(Visibility.INHERITED) ? Visibility.PUBLIC : annotatedCtorVisibility; checkPropertyAccessVisibility( fauxCtorRef, effectiveCtorVisibility, ctorType, target.getStaticSourceFile(), definingSource); } private void checkPropertyOverrideVisibility( PropertyReference propRef, Visibility visibility, Visibility fileOverviewVisibility, JSType objectType, boolean sameInput) { Visibility overridingVisibility = propRef.isOverride() ? propRef.getJSDocInfo().getVisibility() : Visibility.INHERITED; // Check that: // (a) the property *can* be overridden, // (b) the visibility of the override is the same as the // visibility of the original property, // (c) the visibility is explicitly redeclared if the override is in // a file with default visibility in the @fileoverview block. if (visibility == Visibility.PRIVATE && !sameInput) { compiler.report( JSError.make(propRef.getSourceNode(), PRIVATE_OVERRIDE, objectType.toString())); } else if (overridingVisibility != Visibility.INHERITED && overridingVisibility != visibility && fileOverviewVisibility == null) { compiler.report( JSError.make( propRef.getSourceNode(), VISIBILITY_MISMATCH, visibility.name(), objectType.toString(), overridingVisibility.name())); } } private void checkPropertyAccessVisibility( PropertyReference propRef, Visibility visibility, JSType objectType, StaticSourceFile referenceSource, StaticSourceFile definingSource) { // private access is always allowed in the same file. if (referenceSource != null && definingSource != null && referenceSource.getName().equals(definingSource.getName())) { return; } @Nullable ObjectType ownerType = instanceTypeFor(objectType); switch (visibility) { case PACKAGE: checkPackagePropertyVisibility(propRef, referenceSource, definingSource); break; case PRIVATE: checkPrivatePropertyVisibility(propRef, ownerType); break; case PROTECTED: checkProtectedPropertyVisibility(propRef, ownerType); break; default: break; } } private void checkPackagePropertyVisibility( PropertyReference propRef, StaticSourceFile referenceSource, StaticSourceFile definingSource) { CodingConvention codingConvention = compiler.getCodingConvention(); String refPackage = codingConvention.getPackageName(referenceSource); String defPackage = codingConvention.getPackageName(definingSource); if (refPackage == null || defPackage == null || !refPackage.equals(defPackage)) { compiler.report( JSError.make( propRef.getSourceNode(), BAD_PACKAGE_PROPERTY_ACCESS, propRef.getName(), propRef.getReadableTypeNameOrDefault())); } } private void checkPrivatePropertyVisibility( PropertyReference propRef, @Nullable ObjectType ownerType) { // private access is not allowed outside the file from a different // enclosing class. // TODO(tbreisacher): Should we also include the filename where ownerType is defined? String readableTypeName = ownerType == null || ownerType.equals(propRef.getReceiverType()) ? propRef.getReadableTypeNameOrDefault() : ownerType.toString(); compiler.report( JSError.make( propRef.getSourceNode(), BAD_PRIVATE_PROPERTY_ACCESS, propRef.getName(), readableTypeName)); } private void checkProtectedPropertyVisibility( PropertyReference propRef, @Nullable ObjectType ownerType) { // There are 3 types of legal accesses of a protected property: // 1) Accesses in the same file // 2) Overriding the property in a subclass // 3) Accessing the property from inside a subclass // The first two have already been checked for. if (ownerType != null) { for (JSType scopeType : currentClassStack) { if (scopeType == null) { continue; } else if (scopeType.isSubtypeOf(ownerType)) { return; } } } compiler.report( JSError.make( propRef.getSourceNode(), BAD_PROTECTED_PROPERTY_ACCESS, propRef.getName(), propRef.getReadableTypeNameOrDefault())); } /** * Determines whether a deprecation warning should be emitted. * * @param t The current traversal. * @param n The node which we are checking. * @param parent The parent of the node which we are checking. */ private boolean shouldEmitDeprecationWarning(NodeTraversal t, Node n) { // In the global scope, there are only two kinds of accesses that should // be flagged for warnings: // 1) Calls of deprecated functions and methods. // 2) Instantiations of deprecated classes. // For now, we just let everything else by. if (t.inGlobalScope()) { if (!NodeUtil.isInvocationTarget(n) && !n.isNew()) { return false; } } return !canAccessDeprecatedTypes(t); } /** Determines whether a deprecation warning should be emitted. */ private boolean shouldEmitDeprecationWarning(NodeTraversal t, PropertyReference propRef) { // In the global scope, there are only two kinds of accesses that should // be flagged for warnings: // 1) Calls of deprecated functions and methods. // 2) Instantiations of deprecated classes. // For now, we just let everything else by. if (t.inGlobalScope()) { if (!NodeUtil.isInvocationTarget(propRef.getSourceNode())) { return false; } } // We can always assign to a deprecated property, to keep it up to date. if (propRef.isMutation()) { return false; } // Don't warn if the node is just declaring the property, not reading it. JSDocInfo jsdoc = propRef.getJSDocInfo(); if (propRef.isDeclaration() && (jsdoc != null) && jsdoc.isDeprecated()) { return false; } return !canAccessDeprecatedTypes(t); } /** * Returns whether it's currently OK to access deprecated names and properties. * *

   * There are 3 exceptions when we're allowed to use a deprecated type or property:
   * 1) When we're in a deprecated function.
   * 2) When we're in a deprecated class.
   * 3) When we're in a static method of a deprecated class.
   * 
*/ private boolean canAccessDeprecatedTypes(NodeTraversal t) { Node scopeRoot = t.getClosestHoistScopeRoot(); if (NodeUtil.isFunctionBlock(scopeRoot)) { scopeRoot = scopeRoot.getParent(); } Node scopeRootParent = scopeRoot.getParent(); // Cases 2 and 3 are required to handle ES5-style class methods since they aren't nested inside // their class. This is tested in the CheckAccessControlsOldSyntaxTest class. return // Case #1 (deprecationDepth > 0) // Case #2 || (getTypeDeprecationInfo(getTypeOfThis(scopeRoot)) != null) // Case #3 || (scopeRootParent != null && scopeRootParent.isAssign() && getTypeDeprecationInfo(bestInstanceTypeForMethodOrCtor(scopeRoot)) != null); } /** * Returns whether this node roots a subtree under which references to deprecated constructs are * allowed. */ private static boolean isMarkedDeprecated(Node n) { return getDeprecationReason(NodeUtil.getBestJSDocInfo(n)) != null; } /** * Returns the deprecation reason for the type if it is marked as being deprecated. Returns empty * string if the type is deprecated but no reason was given. Returns null if the type is not * deprecated. */ private static @Nullable String getTypeDeprecationInfo(JSType type) { if (type == null) { return null; } return getDeprecationReason(type.getJSDocInfo()); } private static @Nullable String getDeprecationReason(JSDocInfo info) { if (info != null && info.isDeprecated()) { if (info.getDeprecationReason() != null) { return info.getDeprecationReason(); } return ""; } return null; } /** Returns if a property is declared constant. */ private Constancy isPropertyDeclaredConstant(ObjectType objectType, String prop) { for (; objectType != null; objectType = objectType.getImplicitPrototype()) { JSDocInfo docInfo = objectType.getOwnPropertyJSDocInfo(prop); if (docInfo == null) { continue; } else if (docInfo.isFinal()) { return Constancy.FINAL; } else if (docInfo.isConstant()) { return Constancy.OTHER_CONSTANT; } } return Constancy.MUTABLE; } /** * Returns the deprecation reason for the property if it is marked as being deprecated. Returns * empty string if the property is deprecated but no reason was given. Returns null if the * property is not deprecated. */ private static @Nullable String getPropertyDeprecationInfo(ObjectType type, String prop) { String depReason = getDeprecationReason(type.getOwnPropertyJSDocInfo(prop)); if (depReason != null) { return depReason; } ObjectType implicitProto = type.getImplicitPrototype(); if (implicitProto != null) { return getPropertyDeprecationInfo(implicitProto, prop); } return null; } /** If the superclass is final, this method returns an instance of the superclass. */ private static @Nullable ObjectType getSuperClassInstanceIfFinal(FunctionType subCtor) { FunctionType ctor = subCtor.getSuperClassConstructor(); JSDocInfo doc = (ctor == null) ? null : ctor.getJSDocInfo(); if (doc != null && doc.isFinal()) { return ctor.getInstanceType(); } return null; } private static @Nullable ObjectType castToObject(@Nullable JSType type) { return type == null ? null : type.toMaybeObjectType(); } private static boolean isFunctionOrClass(Node n) { return n.isFunction() || n.isClass(); } private static boolean isExtendsTarget(Node node) { Node parent = node.getParent(); return parent.isClass() && node.isSecondChildOf(parent); } private @Nullable JSType getTypeOfThis(Node scopeRoot) { if (scopeRoot.isRoot() || scopeRoot.isScript()) { return castToObject(scopeRoot.getJSType()); } else if (scopeRoot.isModuleBody()) { return null; } else if (NodeUtil.isClassStaticBlock(scopeRoot)) { Node classNode = NodeUtil.getEnclosingClass(scopeRoot); return classNode.getJSType(); } checkArgument(scopeRoot.isFunction(), scopeRoot); JSType nodeType = scopeRoot.getJSType(); if (nodeType != null && nodeType.isFunctionType()) { return nodeType.toMaybeFunctionType().getTypeOfThis(); } else { // Executed when the current scope has not been typechecked. return null; } } /** * The set of ways in which JSDoc and identifier usage can interact. * *

Before ES6, for a given type, there was no way to differentate the declaration of the * constructor function, namespace object, and type; the declaration of all three constructs was * the same node. This lead to some assumptions were made about how access-control modifiers * applied to each. * *

At ES6, class-syntax separated the constructor function from the namespace object and type * declaration. This allowed finer grained control over access-control modifiers; however it broke * some of the eariler assumptions. * *

This type exists to simplify maintaining both sets of assumptions. It allows other code to * branch on behaviour in a more obvious way. * *

TODO(b/113127707): Make this unnecessary by better modeling or decomposing this pass. */ private static enum IdentifierBehaviour { NON_CONSTRUCTOR, ES5_CLASS_INVOCATION, ES5_CLASS_NAMESPACE, ES6_CLASS_INVOCATION, ES6_CLASS_NAMESPACE; public static IdentifierBehaviour select(Node target) { JSType type = target.getJSType(); if (type == null || !type.isFunctionType()) { // If we aren't sure what we're dealing with be more strict. return IdentifierBehaviour.NON_CONSTRUCTOR; } FunctionType ctorType = type.toMaybeFunctionType(); if (!ctorType.isConstructor()) { return IdentifierBehaviour.NON_CONSTRUCTOR; } boolean isInvocation = NodeUtil.isInvocationTarget(target) || isExtendsTarget(target); boolean isEs6 = (ctorType.getSource() != null) && ctorType.getSource().isClass(); if (!isEs6) { return isInvocation ? IdentifierBehaviour.ES5_CLASS_INVOCATION : IdentifierBehaviour.ES5_CLASS_NAMESPACE; } else { return isInvocation ? IdentifierBehaviour.ES6_CLASS_INVOCATION : IdentifierBehaviour.ES6_CLASS_NAMESPACE; } } } /** * A representation of an object property reference in JS code. * *

This is an abstraction to smooth over the various AST structures that can act on * properties. It is not useful for names (variables) or anonymous JS constructs. * *

This class should only be used within {@link CheckAccessControls}. Having package-private * visibility is a quirk of {@link AutoValue}. */ @AutoValue abstract static class PropertyReference { public static Builder builder() { return new AutoValue_CheckAccessControls_PropertyReference.Builder(); } /** The {@link Node} that spawned this reference. */ public abstract Node getSourceNode(); public abstract String getName(); /** The type from which the property is referenced, not necessarily the one that declared it. */ public abstract ObjectType getReceiverType(); public abstract boolean isMutation(); public abstract boolean isDeclaration(); public abstract boolean isOverride(); /** * A lazy source for a human-readable type name to use when generating messages. * *

Most users probably want {@link #getReadableTypeNameOrDefault()}. */ public abstract Supplier getReadableTypeName(); @AutoValue.Builder abstract interface Builder { Builder setSourceNode(Node node); Builder setName(String name); Builder setReceiverType(ObjectType receiverType); Builder setMutation(boolean isMutation); Builder setDeclaration(boolean isDeclaration); Builder setOverride(boolean isOverride); Builder setReadableTypeName(Supplier typeName); PropertyReference build(); } // Derived properties. public final Node getParentNode() { return getSourceNode().getParent(); } public final JSType getJSType() { return getSourceNode().getJSType(); } public final @Nullable JSDocInfo getJSDocInfo() { return NodeUtil.getBestJSDocInfo(getSourceNode()); } public final boolean isDocumentedDeclaration() { return isDeclaration() && (getJSDocInfo() != null); } public final boolean isDeletion() { return getSourceNode().getParent().isDelProp(); } public final String getReadableTypeNameOrDefault() { String preferred = getReadableTypeName().get(); return preferred.isEmpty() ? getReceiverType().toString() : preferred; } } private @Nullable PropertyReference createPropertyReference(Node sourceNode) { Node parent = sourceNode.getParent(); @Nullable JSDocInfo jsdoc = NodeUtil.getBestJSDocInfo(sourceNode); PropertyReference.Builder builder = PropertyReference.builder(); switch (sourceNode.getToken()) { case GETPROP: { boolean isLValue = NodeUtil.isLValue(sourceNode); builder .setName(sourceNode.getString()) .setReceiverType(boxedOrUnknown(sourceNode.getFirstChild().getJSType())) // Props are always mutated as L-values, even when assigned `undefined`. .setMutation(isLValue || sourceNode.getParent().isDelProp()) .setDeclaration( parent.isExprResult() || (jsdoc != null && jsdoc.isConstant() && isLValue)) // TODO(b/113704668): This definition is way too loose. It was used to prevent // breakages during refactoring and should be tightened. .setOverride((jsdoc != null) && isLValue) .setReadableTypeName( () -> typeRegistry.getReadableTypeName(sourceNode.getFirstChild())); } break; case STRING_KEY: case GETTER_DEF: case SETTER_DEF: case MEMBER_FUNCTION_DEF: case MEMBER_FIELD_DEF: { switch (parent.getToken()) { case OBJECTLIT: // TODO(b/80580110): Eventually object-literal members should be covered by // `PropertyReference`s. However, doing so initially would have caused too many errors // in existing code and delayed support for class syntax. if (!parent.getJSType().isLiteralObject()) { // Only add a mutation if the object type is actually a literal object (e.g. a // global namespace). OBJECTLIT tokens are often used to fulfill structural types, // which is fine for writing constant properties the first time. return null; } builder .setName(sourceNode.getString()) .setReceiverType(typeOrUnknown(ObjectType.cast(parent.getJSType()))) .setMutation(true) .setDeclaration(true) .setOverride(false) .setReadableTypeName(() -> typeRegistry.getReadableTypeName(parent)); break; case OBJECT_PATTERN: builder .setName(sourceNode.getString()) .setReceiverType(typeOrUnknown(ObjectType.cast(parent.getJSType()))) .setMutation(false) .setDeclaration(false) .setOverride(false) .setReadableTypeName(() -> typeRegistry.getReadableTypeName(parent)); break; case CLASS_MEMBERS: builder .setName(sourceNode.getString()) .setReceiverType(typeRegistry.getNativeObjectType(JSTypeNative.UNKNOWN_TYPE)) .setMutation(true) .setDeclaration(true) // TODO(b/113704668): This definition is way too loose. It was used to prevent // breakages during refactoring and should be tightened. .setOverride(jsdoc != null) .setReadableTypeName(() -> ""); // The default is fine for class types. JSType ctorType = parent.getParent().getJSType(); if (ctorType != null && ctorType.isFunctionType()) { FunctionType ctorFunctionType = ctorType.toMaybeFunctionType(); ObjectType owningType; if (sourceNode.isStaticMember()) { owningType = ctorFunctionType; } else if (sourceNode.isMemberFieldDef()) { owningType = ctorFunctionType.getInstanceType(); } else { owningType = ctorFunctionType.getPrototype(); } builder.setReceiverType(owningType); } break; default: throw new AssertionError(); } } break; default: return null; } return builder.setSourceNode(sourceNode).build(); } /** * Returns the effective visibility of the given property. This can differ from the property's * declared visibility if the property is inherited from a superclass, or if the file's * {@code @fileoverview} JsDoc specifies a default visibility. * *

TODO(b/111789692): The following methods are forked from `AccessControlUtils`. Consider * consolidating them. * * @param referenceType The JavaScript type of the property. * @param fileVisibilityMap A map of {@code @fileoverview} visibility annotations, used to compute * the property's default visibility. * @param codingConvention The coding convention in effect (if any), used to determine whether the * property is private by lexical convention (example: trailing underscore). */ static Visibility getEffectivePropertyVisibility( PropertyReference propRef, ObjectType referenceType, ImmutableMap fileVisibilityMap) { String propertyName = propRef.getName(); boolean isOverride = propRef.isOverride(); StaticSourceFile definingSource = AccessControlUtils.getDefiningSource(propRef.getSourceNode(), referenceType, propertyName); Visibility fileOverviewVisibility = fileVisibilityMap.get(definingSource); ObjectType objectType = AccessControlUtils.getObjectType(referenceType, isOverride, propertyName); if (isOverride) { Visibility overridden = AccessControlUtils.getOverriddenPropertyVisibility(objectType, propertyName); return AccessControlUtils.getEffectiveVisibilityForOverriddenProperty( overridden, fileOverviewVisibility, propertyName); } else { return getEffectiveVisibilityForNonOverriddenProperty( propRef, objectType, fileOverviewVisibility); } } /** * Returns the effective visibility of the given non-overridden property. Non-overridden * properties without an explicit visibility annotation receive the default visibility declared in * the file's {@code @fileoverview} block, if one exists. * *

TODO(b/111789692): The following methods are forked from `AccessControlUtils`. Consider * consolidating them. */ private static Visibility getEffectiveVisibilityForNonOverriddenProperty( PropertyReference propRef, ObjectType objectType, @Nullable Visibility fileOverviewVisibility) { String propertyName = propRef.getName(); Visibility raw = Visibility.INHERITED; if (objectType != null) { JSDocInfo jsdoc = objectType.getOwnPropertyJSDocInfo(propertyName); if (jsdoc != null) { raw = jsdoc.getVisibility(); } } JSType type = propRef.getJSType(); boolean createdFromGoogProvide = (type != null && type.isLiteralObject()); // Ignore @fileoverview visibility when computing the effective visibility // for properties created by goog.provide. // // ProcessClosurePrimitives rewrites goog.provide()s as object literal // declarations, but the exact form depends on the ordering of the // input files. If goog.provide('a.b.c') occurs in the inputs before // goog.provide('a'), it is rewritten like // // var a={};a.b={}a.b.c={}; // // If the file containing goog.provide('a.b.c') also declares // a @fileoverview visibility, it must not apply to b, as this would make // every a.b.* namespace effectively package-private. return (raw != Visibility.INHERITED || fileOverviewVisibility == null || createdFromGoogProvide) ? raw : fileOverviewVisibility; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy