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

com.google.javascript.jscomp.serialization.JSTypeSerializer 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 2020 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.serialization;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static java.util.stream.Collectors.joining;

import com.google.auto.value.AutoOneOf;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Streams;
import com.google.javascript.jscomp.IdGenerator;
import com.google.javascript.jscomp.InvalidatingTypes;
import com.google.javascript.jscomp.serialization.JSTypeSerializer.SimplifiedType.Kind;
import com.google.javascript.rhino.ClosurePrimitive;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.jstype.EnumType;
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 com.google.javascript.rhino.serialization.SerializationOptions;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.annotation.Nullable;

final class JSTypeSerializer {

  // Cache some commonly used types
  private final SimplifiedType unknownType;
  private final SimplifiedType topObjectType;
  private final SimplifiedType nullType;
  private final JSTypeRegistry registry;
  // JSTypes that map to the top object type. Necessary because it's difficult to identify these
  // types other than checking for equality with JSTypeRegistry methods.
  private final ImmutableSet topObjectLikeTypes;

  private final InvalidatingTypes invalidatingTypes;
  private final StringPoolBuilder stringPoolBuilder;
  private final IdGenerator idGenerator;
  private final SerializationOptions serializationMode;
  private final LinkedHashMap seenSerializableTypes =
      new LinkedHashMap<>();
  private final Multimap disambiguateEdges = LinkedHashMultimap.create();

  private State state = State.COLLECTING_TYPES;

  // JSTypeNatives that map to the top object type.
  private static final ImmutableSet TOP_LIKE_OBJECT_IDS =
      ImmutableSet.of(
          JSTypeNative.OBJECT_TYPE,
          JSTypeNative.OBJECT_FUNCTION_TYPE,
          JSTypeNative.OBJECT_PROTOTYPE,
          JSTypeNative.FUNCTION_PROTOTYPE,
          JSTypeNative.FUNCTION_TYPE,
          JSTypeNative.FUNCTION_FUNCTION_TYPE);

  /**
   * The first N TypePointer pool offsets correspond to PrimitiveType values
   *
   * 

Subtract 1 for the auto-generated UNRECOGNIZED element. */ static final int PRIMITIVE_POOL_SIZE = PrimitiveType.values().length - 1; private enum State { COLLECTING_TYPES, GENERATING_POOL, FINISHED, } private JSTypeSerializer( JSTypeRegistry registry, InvalidatingTypes invalidatingTypes, StringPoolBuilder stringPoolBuilder, IdGenerator idGenerator, SerializationOptions serializationMode) { this.unknownType = SimplifiedType.ofJSType(registry.getNativeType(JSTypeNative.UNKNOWN_TYPE)); this.topObjectType = SimplifiedType.ofJSType(registry.getNativeType(JSTypeNative.OBJECT_TYPE)); this.registry = registry; this.topObjectLikeTypes = TOP_LIKE_OBJECT_IDS.stream().map(registry::getNativeType).collect(toImmutableSet()); this.nullType = SimplifiedType.ofJSType(registry.getNativeType(JSTypeNative.NULL_TYPE)); this.invalidatingTypes = invalidatingTypes; this.stringPoolBuilder = stringPoolBuilder; this.idGenerator = idGenerator; this.serializationMode = serializationMode; } public static JSTypeSerializer create( JSTypeRegistry registry, InvalidatingTypes invalidatingTypes, StringPoolBuilder stringPoolBuilder, SerializationOptions serializationMode) { IdGenerator idGenerator = new IdGenerator(); JSTypeSerializer serializer = new JSTypeSerializer( registry, invalidatingTypes, stringPoolBuilder, idGenerator, serializationMode); serializer.addPrimitiveTypePointers(); serializer.checkValid(); return serializer; } /** Returns a pointer to the given type. If it is not already serialized, serializes it too */ TypePointer serializeType(JSType originalType) { checkValid(); SimplifiedType type = simplifyTypeInternal(originalType); return serializeSimplifiedType(type); } /** Returns a pointer to the given type. If it is not already serialized, serializes it too */ private TypePointer serializeSimplifiedType(SimplifiedType type) { checkValid(); SeenTypeRecord existing = this.seenSerializableTypes.get(type); if (existing != null) { return existing.pointer; } checkState(State.COLLECTING_TYPES == this.state || State.GENERATING_POOL == this.state); TypePointer.Builder pointer = TypePointer.newBuilder().setPoolOffset(this.seenSerializableTypes.size()); if (this.serializationMode.includeDebugInfo()) { pointer.setDebugInfo( TypePointer.DebugInfo.newBuilder().setDescription(getDebugDescription(type))); } SeenTypeRecord record = new SeenTypeRecord(pointer.build()); this.seenSerializableTypes.put(type, record); // Serialize after the pointer is in the pool in case serialization requires a pool lookup. record.type = typeToProto(type, record.pointer); checkValid(); return record.pointer; } private static String getDebugDescription(SimplifiedType type) { switch (type.getKind()) { case SINGLE: return type.single().toString(); case UNION: return "(" + type.union().stream().map(JSTypeSerializer::getDebugDescription).collect(joining(",")) + ")"; } throw new AssertionError(); } /** Returns the canonical form of a given type. */ private SimplifiedType simplifyTypeInternal(JSType type) { if (type.isEnumElementType()) { // replace with the corresponding primitive return simplifyTypeInternal(type.toMaybeEnumElementType().getPrimitiveType()); } if (type.isTemplateType()) { // template types are not serialized because optimizations don't seem to care about them. // serialize as the UNKNOWN_TYPE because bounded generics are unsupported return unknownType; } if (type.isTemplatizedType()) { return simplifyTypeInternal(type.toMaybeTemplatizedType().getReferencedType()); } if (type.isUnionType()) { ImmutableSet alternates = type.toMaybeUnionType().getAlternates().stream() .map(this::simplifyTypeInternal) .collect(toImmutableSet()); // Simplification may collapse some members of the union return alternates.size() > 1 ? SimplifiedType.ofUnion(alternates) : Iterables.getOnlyElement(alternates); } if (type.isFunctionType() && type.toMaybeFunctionType().getCanonicalRepresentation() != null) { return simplifyTypeInternal(type.toMaybeFunctionType().getCanonicalRepresentation()); } if (type.isNoResolvedType() || type.isAllType() || type.isCheckedUnknownType() || type.isUnknownType() || type.isNoType() || type.isNoObjectType()) { // Merge all the various top/bottom-like/unknown types into a single unknown type. return unknownType; } if (type.toObjectType() != null) { // Smoosh top-like objects into a single type. // The isNativeObjectType() check is only for performance reasons. It avoids computing // equals/hashCode for every object type as most types are not native. if (type.toObjectType().isNativeObjectType() && topObjectLikeTypes.contains(type)) { return topObjectType; } return SimplifiedType.ofJSType(type); } if (type.isVoidType() || type.isNullType()) { // Canonicalize the void type to the null type return nullType; } if (type.isBoxableScalar()) { return SimplifiedType.ofJSType(type); } throw new IllegalStateException("Unsupported type " + type); } /** * Constructs a {@link TypeProto} representation of the given union or object * *

Only call this method after checking the cache in {@link #seenSerializableTypes}, as this * method will unilaterally create a new object. */ private TypeProto typeToProto(SimplifiedType type, TypePointer pointer) { switch (type.getKind()) { case UNION: UnionTypeProto.Builder union = UnionTypeProto.newBuilder(); type.union().stream() .map(this::serializeSimplifiedType) .forEachOrdered(union::addUnionMember); return TypeProto.newBuilder().setUnion(union).build(); case SINGLE: // Primitive types should have been added to the "seenSerializableTypes" map in // "addPrimitiveTypePointers", as they do not have corresponding TypeProtos. ObjectType objectType = checkNotNull( type.single().toMaybeObjectType(), "Unexpected non-object type %s", type.single()); addSupertypeEdges(objectType, pointer); return TypeProto.newBuilder().setObject(serializeObjectType(objectType)).build(); } throw new AssertionError(); } private void addSupertypeEdges(ObjectType subtype, TypePointer serializedSubtype) { this.disambiguateEdges.putAll(serializedSubtype, ownAncestorInterfacesOf(subtype)); if (subtype.getImplicitPrototype() != null) { TypePointer supertype = serializeType(subtype.getImplicitPrototype()); this.disambiguateEdges.put(serializedSubtype, supertype); } } private static ObjectTypeProto.DebugInfo getDebugInfo(ObjectType type) { ObjectTypeProto.DebugInfo defaultDebugInfo = defaultDebugInfo(type); if (type.isInstanceType()) { return instanceDebugInfo(type, defaultDebugInfo); } else if (type.isEnumType()) { return enumDebugInfo(type.toMaybeEnumType(), defaultDebugInfo); } else if (type.isFunctionType()) { return functionDebugInfo(type.toMaybeFunctionType(), defaultDebugInfo); } return defaultDebugInfo; } private static ObjectTypeProto.DebugInfo defaultDebugInfo(ObjectType type) { ObjectTypeProto.DebugInfo.Builder builder = ObjectTypeProto.DebugInfo.newBuilder(); Node ownerNode = type.getOwnerFunction() != null ? type.getOwnerFunction().getSource() : null; if (ownerNode != null) { builder.setFilename(ownerNode.getSourceFileName()); } String className = type.getReferenceName(); if (className != null) { builder.setClassName(className); } return builder.build(); } private ObjectTypeProto serializeObjectType(ObjectType type) { ObjectTypeProto.Builder objBuilder = ObjectTypeProto.newBuilder(); if (type.isFunctionType()) { FunctionType fnType = type.toMaybeFunctionType(); // Serialize prototypes and instance types for instantiable types. Even if these types never // appear on the AST, optimizations need to know that at runtime these types may be present. if (fnType.hasInstanceType() && fnType.getInstanceType() != null) { objBuilder .setPrototype(serializeType(fnType.getPrototype())) .setInstanceType(serializeType(fnType.getInstanceType())); if (fnType.isConstructor()) { objBuilder.setMarkedConstructor(true); } } objBuilder.setClosureAssert(isClosureAssert(fnType.getClosurePrimitive())); } if (this.serializationMode.includeDebugInfo()) { ObjectTypeProto.DebugInfo debugInfo = getDebugInfo(type); if (!debugInfo.equals(ObjectTypeProto.DebugInfo.getDefaultInstance())) { objBuilder.setDebugInfo(debugInfo); } } for (String ownProperty : type.getOwnPropertyNames()) { // TODO(b/169899789): consider omitting common, well-known properties like "prototype" to save // space. objBuilder.addOwnProperty(this.stringPoolBuilder.put(ownProperty)); } return objBuilder .setIsInvalidating(invalidatingTypes.isInvalidating(type)) // To support legacy code, property disambiguation never renames properties of enums // (e.g. 'A' in '/** @enum */ const E = {A: 0}`). In // theory this would be safe to remove if we clean up code depending on the lack of renaming .setPropertiesKeepOriginalName(type.isEnumType()) // NOTE: We need a better format than sequential integers in order to have an id that // can be consistent across compilation units. For now, using a sequential integers for each // type depends on the invariant that we serialize each distinct type exactly once and from // a single compilation unit. .setUuid(Integer.toHexString(idGenerator.newId())) .build(); } private static ObjectTypeProto.DebugInfo instanceDebugInfo( ObjectType type, ObjectTypeProto.DebugInfo defaultDebugInfo) { FunctionType constructor = type.getConstructor(); String className = constructor.getReferenceName(); ObjectTypeProto.DebugInfo.Builder builder = ObjectTypeProto.DebugInfo.newBuilder(defaultDebugInfo); if (className != null && !className.isEmpty()) { builder.setClassName(className); } if (builder.getFilename().isEmpty() && constructor.getSource() != null) { String filename = constructor.getSource().getSourceFileName(); builder.setFilename(filename); } return builder.build(); } private static ObjectTypeProto.DebugInfo enumDebugInfo( EnumType type, ObjectTypeProto.DebugInfo defaultDebugInfo) { ObjectTypeProto.DebugInfo.Builder builder = ObjectTypeProto.DebugInfo.newBuilder(defaultDebugInfo); if (type.getSource() != null) { builder.setFilename(type.getSource().getSourceFileName()); } return builder.build(); } private static ObjectTypeProto.DebugInfo functionDebugInfo( FunctionType type, ObjectTypeProto.DebugInfo defaultDebugInfo) { Node source = type.getSource(); ObjectTypeProto.DebugInfo.Builder builder = ObjectTypeProto.DebugInfo.newBuilder(defaultDebugInfo); if (source != null) { String filename = source.getSourceFileName(); if (filename != null) { builder.setFilename(filename); } } if (type.hasInstanceType() && type.getSource() != null) { // Render function types known to be type definitions as "(typeof Foo)". This includes types // defined like "/** @constructor */ function Foo() { }" but not to those defined like "@param // {function(new:Foo)}". Only the former will have a source node. builder.setClassName("(typeof " + builder.getClassName() + ")"); } return builder.build(); } /** * Returns the interfaces directly implemented and extended by {@code type}. * *

Some of these relationships represent type errors; however, the graph needs to contain those * edges for safe disambiguation. In particular, code generated from other languages (e.g TS) * might have more flexible subtyping rules. */ private ImmutableList ownAncestorInterfacesOf(ObjectType type) { FunctionType ctorType = type.getConstructor(); if (ctorType == null) { return ImmutableList.of(); } return Streams.concat( ctorType.getExtendedInterfaces().stream(), ctorType.getOwnImplementedInterfaces().stream()) .map(this::serializeType) .collect(toImmutableList()); } /** * Inserts dummy pointers corresponding to all {@link PrimitiveType}s in the type pool. * *

These types will never correspond to an actual {@link TypeProto}. Instead, all normal {@link * TypePointer} offsets into the pool are offset by a number equivalent to the number of {@link * PrimitiveType} enum elements. */ private void addPrimitiveTypePointers() { for (PrimitiveType primitive : PrimitiveType.values()) { if (primitive.equals(PrimitiveType.UNRECOGNIZED)) { continue; } checkState( primitive.getNumber() == seenSerializableTypes.size(), "Expected all PrimitiveTypes to be added in order; %s added at index %s.", primitive, seenSerializableTypes.size()); TypePointer.Builder pointer = TypePointer.newBuilder().setPoolOffset(primitive.getNumber()); if (this.serializationMode.includeDebugInfo()) { pointer.setDebugInfo( TypePointer.DebugInfo.newBuilder().setDescription(primitive.toString())); } SeenTypeRecord record = new SeenTypeRecord(pointer.build()); JSTypeNative jsTypeNative = canonicalizePrimitive(primitive); SimplifiedType simplified = SimplifiedType.ofJSType(this.registry.getNativeType(jsTypeNative)); this.seenSerializableTypes.put(simplified, record); } checkState(this.seenSerializableTypes.size() == PRIMITIVE_POOL_SIZE); } /** Checks that this instance is in a valid state. */ private void checkValid() { if (!this.serializationMode.runValidation()) { return; } final int totalTypeCount = this.seenSerializableTypes.size(); for (SeenTypeRecord seen : this.seenSerializableTypes.values()) { int offset = seen.pointer.getPoolOffset(); checkState(offset >= 0); checkState( offset <= totalTypeCount, "Found invalid pointer %s, out of a total of %s user-defined types", offset, totalTypeCount); } } /** * Generates a "type-pool" representing all the types that this class has encountered through * calls to {@link #serializeType(JSType)}. * *

After generation, no new types can be added, so subsequent calls to {@link * #serializeType(JSType)} can only be used to retrieve pointers to existing types in the type * pool. */ TypePool generateTypePool() { checkState(this.state == State.COLLECTING_TYPES); checkValid(); this.state = State.GENERATING_POOL; TypePool.Builder builder = TypePool.newBuilder().setNativeObjectTable(this.createNativeObjectTable()); for (SeenTypeRecord seen : this.seenSerializableTypes.values()) { if (seen.type == null) { // seen.type is if and only this is a native type without a TypeProto representation. checkState( seen.pointer.getPoolOffset() < PRIMITIVE_POOL_SIZE, "Missing .type for SeenTypeRecord %s", seen); continue; } builder.addType(seen.type); } for (TypePointer subtype : this.disambiguateEdges.keySet()) { for (TypePointer supertype : this.disambiguateEdges.get(subtype)) { builder.addDisambiguationEdges( SubtypingEdge.newBuilder().setSubtype(subtype).setSupertype(supertype)); } } TypePool pool = builder.build(); this.state = State.FINISHED; checkValid(); return pool; } /** * Returns a map from {@link ObjectTypeProto#getUuid()} to the originating {@link JSType}. * *

Excludes unions and primitive types as they do not have their own UUIDs * *

Only intended to be used for debug logging. */ ImmutableMap getObjectUuidMapForDebugging() { ImmutableMap.Builder uuidToType = ImmutableMap.builder(); for (Map.Entry entry : this.seenSerializableTypes.entrySet()) { if (entry.getKey().getKind().equals(Kind.SINGLE) && entry.getValue().type != null && entry.getValue().type.hasObject()) { uuidToType.put( entry.getValue().type.getObject().getUuid(), entry.getKey().single().toString()); } } return uuidToType.build(); } private static final class SeenTypeRecord { final TypePointer pointer; // If null, indicates that this SeenTypeRecord represents a native type pointer with no // corresponding TypeProto. @Nullable TypeProto type; SeenTypeRecord(TypePointer pointer) { this.pointer = pointer; } } /** * Wraps a "simplified" JSType to ensure that we never accidentally look up or serialize a * non-simplified type. */ @AutoOneOf(SimplifiedType.Kind.class) abstract static class SimplifiedType { public enum Kind { SINGLE, UNION } abstract Kind getKind(); abstract JSType single(); abstract ImmutableSet union(); private static SimplifiedType ofJSType(JSType t) { checkArgument(!t.isUnionType(), "Unions must be simplified to Set but found %s", t); return AutoOneOf_JSTypeSerializer_SimplifiedType.single(t); } private static SimplifiedType ofUnion(ImmutableSet union) { checkArgument(union.size() > 1, "Union %s must have > 1 element", union); return AutoOneOf_JSTypeSerializer_SimplifiedType.union(union); } } /** * Maps between {@link JSTypeNative} and {@link NativeType}. * *

In practice, there are multiple {@link JSTypeNative}s that may correspond to the same {@link * NativeType}. The {@link #simplifyTypeInternal(JSType)} is responsible for doing this * simplification. */ private static JSTypeNative canonicalizePrimitive(PrimitiveType primitive) { switch (primitive) { case BOOLEAN_TYPE: return JSTypeNative.BOOLEAN_TYPE; case BIGINT_TYPE: return JSTypeNative.BIGINT_TYPE; case NUMBER_TYPE: return JSTypeNative.NUMBER_TYPE; case STRING_TYPE: return JSTypeNative.STRING_TYPE; case SYMBOL_TYPE: return JSTypeNative.SYMBOL_TYPE; case NULL_OR_VOID_TYPE: return JSTypeNative.NULL_TYPE; // The optimizer doesn't distinguish between any of these types: they are all // invalidating objects. case TOP_OBJECT: return JSTypeNative.OBJECT_TYPE; case UNKNOWN_TYPE: return JSTypeNative.UNKNOWN_TYPE; case UNRECOGNIZED: throw new AssertionError(); } throw new AssertionError(); } /** * Creates a table pointing from native objects to their {@link TypePointer} into the type pool */ private NativeObjectTable createNativeObjectTable() { return NativeObjectTable.newBuilder() .setBigintObject( this.serializeType(registry.getNativeType(JSTypeNative.BIGINT_OBJECT_TYPE))) .setBooleanObject( this.serializeType(registry.getNativeType(JSTypeNative.BOOLEAN_OBJECT_TYPE))) .setNumberObject( this.serializeType(registry.getNativeType(JSTypeNative.NUMBER_OBJECT_TYPE))) .setStringObject( this.serializeType(registry.getNativeType(JSTypeNative.STRING_OBJECT_TYPE))) .setSymbolObject( this.serializeType(registry.getNativeType(JSTypeNative.SYMBOL_OBJECT_TYPE))) .build(); } /** * Returns whether this is some assertion call that should be removed by optimizations when * --remove_closure_asserts is enabled. */ private static boolean isClosureAssert(@Nullable ClosurePrimitive primitive) { if (primitive == null) { return false; } switch (primitive) { case ASSERTS_TRUTHY: case ASSERTS_MATCHES_RETURN: return true; case ASSERTS_FAIL: // technically an assertion function, but not removed by ClosureCodeRemoval return false; } throw new AssertionError(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy