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

com.google.javascript.jscomp.SourceFile 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 2009 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.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Strings.isNullOrEmpty;
import static java.lang.Math.max;
import static java.lang.Math.min;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.annotations.GwtIncompatible;
import com.google.common.io.CharStreams;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.InlineMe;
import com.google.javascript.jscomp.serialization.SourceFileProto;
import com.google.javascript.jscomp.serialization.SourceFileProto.FileOnDisk;
import com.google.javascript.jscomp.serialization.SourceFileProto.ZipEntryOnDisk;
import com.google.javascript.rhino.StaticSourceFile;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Reader;
import java.io.Serializable;
import java.io.StringReader;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Supplier;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.jspecify.nullness.Nullable;

/**
 * An abstract representation of a source file that provides access to language-neutral features.
 *
 * 

The source file can be loaded from various locations, such as from disk or from a preloaded * string. Loading is done as lazily as possible to minimize IO delays and memory cost of source * text. */ public final class SourceFile implements StaticSourceFile, Serializable { private static final long serialVersionUID = 1L; private static final String UTF8_BOM = "\uFEFF"; private static final String BANG_SLASH = "!" + Platform.getFileSeperator(); /** Number of lines in the region returned by {@link #getRegion(int)}. This length must be odd. */ private static final int SOURCE_EXCERPT_REGION_LENGTH = 5; /** * The file name of the source file. * *

It does not necessarily need to correspond to a real path. But it should be unique. Will * appear in warning messages emitted by the compiler. */ private final String fileName; private SourceKind kind; private final CodeLoader loader; // Source Line Information private transient int @Nullable [] lineOffsets = null; private transient volatile @Nullable String code = null; // Code statistics used for metrics. // // These are non-transient, because we want to save and restore them to avoid // rereading source files when we report metrics in the later compilation stages. // See JsCompileMetricsRecorder#recordSourcesStartState // For both of these, a negative value means we haven't counted yet. private int numLines = -1; private int numBytes = -1; private SourceFile(CodeLoader loader, String fileName, SourceKind kind) { if (isNullOrEmpty(fileName)) { throw new IllegalArgumentException("a source must have a name"); } if (!"/".equals(Platform.getFileSeperator())) { fileName = fileName.replace(Platform.getFileSeperator(), "/"); } this.loader = loader; this.fileName = fileName; this.kind = kind; } @Override public int getLineOffset(int lineno) { findLineOffsets(); if (lineno < 1 || lineno > lineOffsets.length) { throw new IllegalArgumentException( "Expected line number between 1 and " + lineOffsets.length + "\nActual: " + lineno); } return lineOffsets[lineno - 1]; } /** * @return the number of lines in the source file */ int getNumLines() { if (numLines < 0) { // A negative value means we need to read in the code and calculate this information. // Otherwise, assume the file hasn't changed since we read it. findLineOffsets(); } return numLines; } /** * @return the number of bytes in the source file */ int getNumBytes() { if (numBytes < 0) { // A negative value means we need to read in the code and calculate this information. // Otherwise, assume the file hasn't changed since we read it. findLineOffsets(); } return numBytes; } private void findLineOffsets() { if (this.lineOffsets != null) { return; } String localCode = this.code; if (localCode == null) { try { localCode = this.getCode(); } catch (IOException e) { localCode = ""; } } int[] offsets = new int[256]; // default is arbitrary. Assume most files are around 200 lines int index = 1; // start at 1 since the offset for line 0 is always at byte 0 int offset = 0; while ((offset = localCode.indexOf('\n', offset)) != -1) { // +1 because this is the offset of the next line which is one past the newline offset++; offsets[index++] = offset; if (index == offsets.length) { offsets = Arrays.copyOf(offsets, offsets.length * 2); } } this.lineOffsets = index == offsets.length ? offsets : Arrays.copyOf(offsets, index); // Update numLines and numBytes // NOTE: In some edit/refresh development workflows, we may end up re-reading the file // and getting different numbers than we got last time we read it, so we should // not have checkState() calls here to assert they have not changed. this.numLines = index; this.numBytes = localCode.length(); } /** Gets all the code in this source file. */ public final String getCode() throws IOException { if (this.code == null) { // Only synchronize if we need to synchronized (this) { // Make sure another thread hasn't loaded the code while we waited. if (this.code == null) { this.setCodeAndDoBookkeeping(this.loader.loadUncachedCode()); } } } return this.code; } @Deprecated final void setCodeDeprecated(String code) { this.setCodeAndDoBookkeeping(code); } /** Gets a reader for the code in this source file. */ @GwtIncompatible("java.io.Reader") public Reader getCodeReader() throws IOException { // Only synchronize if we need to if (this.code == null) { synchronized (this) { // Make sure another thread hasn't loaded the code while we waited. if (this.code == null) { Reader uncachedReader = this.loader.openUncachedReader(); if (uncachedReader != null) { return uncachedReader; } } } } return new StringReader(this.getCode()); } private void setCodeAndDoBookkeeping(@Nullable String sourceCode) { this.code = null; // Force recalculation of all of these values when they are requested. this.lineOffsets = null; this.numLines = -1; this.numBytes = -1; if (sourceCode != null) { if (sourceCode.startsWith(UTF8_BOM)) { sourceCode = sourceCode.substring(UTF8_BOM.length()); } this.code = sourceCode; } } /** * @deprecated alias of {@link #getName()}. Use that instead */ @InlineMe(replacement = "this.getName()") @Deprecated public String getOriginalPath() { return this.getName(); } /** * For SourceFile types which cache source code that can be regenerated easily, flush the cache. * *

We maintain the cache mostly to speed up generating source when displaying error messages, * so dumping the file contents after the compile is a fine thing to do. */ public void clearCachedSource() { this.setCodeAndDoBookkeeping(null); } boolean hasSourceInMemory() { return code != null; } /** * Returns a unique name for the source file. * *

This name is not required to be an actual file path on disk. */ @Override public String getName() { return fileName; } /** Returns the source kind. */ @Override public SourceKind getKind() { return kind; } /** Sets the source kind. */ public void setKind(SourceKind kind) { this.kind = kind; } @Override public int getLineOfOffset(int offset) { findLineOffsets(); int search = Arrays.binarySearch(lineOffsets, offset); if (search >= 0) { return search + 1; // lines are 1-based. } else { int insertionPoint = -1 * (search + 1); return min(insertionPoint - 1, lineOffsets.length - 1) + 1; } } @Override public int getColumnOfOffset(int offset) { int line = getLineOfOffset(offset); return offset - lineOffsets[line - 1]; } /** * Gets the source line for the indicated line number. * * @param lineNumber the line number, 1 being the first line of the file. * @return The line indicated. Does not include the newline at the end of the file. Returns {@code * null} if it does not exist, or if there was an IO exception. */ public @Nullable String getLine(int lineNumber) { findLineOffsets(); if (lineNumber > lineOffsets.length) { return null; } if (lineNumber < 1) { lineNumber = 1; } int pos = lineOffsets[lineNumber - 1]; String js = ""; try { // NOTE(nicksantos): Right now, this is optimized for few warnings. // This is probably the right trade-off, but will be slow if there // are lots of warnings in one file. js = getCode(); } catch (IOException e) { return null; } if (js.indexOf('\n', pos) == -1) { // If next new line cannot be found, there are two cases // 1. pos already reaches the end of file, then null should be returned // 2. otherwise, return the contents between pos and the end of file. if (pos >= js.length()) { return null; } else { return js.substring(pos); } } else { return js.substring(pos, js.indexOf('\n', pos)); } } /** * Gets the source lines starting at `lineNumber` and continuing until `length`. Omits any * trailing newlines. * * @param lineNumber the line number, 1 being the first line of the file. * @param length the number of characters desired, starting at the 0th character of the specified * line. If negative or 0, returns a single line. * @return The line(s) indicated. Returns {@code null} if it does not exist or if there was an IO * exception. */ public @Nullable Region getLines(int lineNumber, int length) { findLineOffsets(); if (lineNumber > lineOffsets.length) { return null; } if (lineNumber < 1) { lineNumber = 1; } if (length <= 0) { length = 1; } String js = ""; try { js = getCode(); } catch (IOException e) { return null; } int pos = lineOffsets[lineNumber - 1]; if (pos == js.length()) { return new SimpleRegion( lineNumber, lineNumber, ""); // Happens when asking for the last empty line in a file. } int endChar = pos; int endLine = lineNumber; // go through lines until we've reached the end of the file or met the specified length. for (; endChar < pos + length && endLine <= lineOffsets.length; endLine++) { endChar = (endLine < lineOffsets.length) ? lineOffsets[endLine] : js.length(); } if (js.charAt(endChar - 1) == '\n') { return new SimpleRegion(lineNumber, endLine, js.substring(pos, endChar - 1)); } return new SimpleRegion(lineNumber, endLine, js.substring(pos, endChar)); } /** * Get a region around the indicated line number. The exact definition of a region is * implementation specific, but it must contain the line indicated by the line number. A region * must not start or end by a carriage return. * * @param lineNumber the line number, 1 being the first line of the file. * @return The line indicated. Returns {@code null} if it does not exist, or if there was an IO * exception. */ public @Nullable Region getRegion(int lineNumber) { String js = ""; try { js = getCode(); } catch (IOException e) { return null; } int pos = 0; int startLine = max(1, lineNumber - (SOURCE_EXCERPT_REGION_LENGTH + 1) / 2 + 1); for (int n = 1; n < startLine; n++) { int nextpos = js.indexOf('\n', pos); if (nextpos == -1) { break; } pos = nextpos + 1; } int end = pos; int endLine = startLine; for (int n = 0; n < SOURCE_EXCERPT_REGION_LENGTH; n++, endLine++) { end = js.indexOf('\n', end); if (end == -1) { break; } end++; } if (lineNumber >= endLine) { return null; } if (end == -1) { int last = js.length() - 1; if (js.charAt(last) == '\n') { return new SimpleRegion(startLine, endLine, js.substring(pos, last)); } else { return new SimpleRegion(startLine, endLine, js.substring(pos)); } } else { return new SimpleRegion(startLine, endLine, js.substring(pos, end)); } } @Override public String toString() { return fileName; } @GwtIncompatible("fromZipInput") public static List fromZipFile(String zipName, Charset inputCharset) throws IOException { try (InputStream input = new FileInputStream(zipName)) { return fromZipInput(zipName, input, inputCharset); } } @GwtIncompatible("java.util.zip.ZipInputStream") public static List fromZipInput( String zipName, InputStream input, Charset inputCharset) throws IOException { final String absoluteZipPath = new File(zipName).getAbsolutePath(); List sourceFiles = new ArrayList<>(); try (ZipInputStream in = new ZipInputStream(input, inputCharset)) { ZipEntry zipEntry; while ((zipEntry = in.getNextEntry()) != null) { String entryName = zipEntry.getName(); if (!entryName.endsWith(".js")) { // Only accept js files continue; } sourceFiles.add( builder() .withCharset(inputCharset) .withOriginalPath(zipName + BANG_SLASH + entryName) .withZipEntryPath(absoluteZipPath, entryName) .build()); } } return sourceFiles; } @GwtIncompatible("java.io.File") public static SourceFile fromFile(String fileName, Charset charset) { return builder().withPath(fileName).withCharset(charset).build(); } @GwtIncompatible("java.io.File") public static SourceFile fromFile(String fileName) { return builder().withPath(fileName).build(); } @GwtIncompatible("java.io.File") public static SourceFile fromPath(Path path, Charset charset) { return builder().withPath(path).withCharset(charset).build(); } public static SourceFile fromCode(String fileName, String code, SourceKind kind) { return builder().withPath(fileName).withKind(kind).withContent(code).build(); } public static SourceFile fromCode(String fileName, String code) { return builder().withPath(fileName).withContent(code).build(); } @GwtIncompatible("java.io.Reader") public static SourceFile fromProto(SourceFileProto protoSourceFile) { SourceKind sourceKind = getSourceKindFromProto(protoSourceFile); switch (protoSourceFile.getLoaderCase()) { case PRELOADED_CONTENTS: return SourceFile.fromCode( protoSourceFile.getFilename(), protoSourceFile.getPreloadedContents(), sourceKind); case FILE_ON_DISK: String pathOnDisk = protoSourceFile.getFileOnDisk().getActualPath().isEmpty() ? protoSourceFile.getFilename() : protoSourceFile.getFileOnDisk().getActualPath(); return SourceFile.builder() .withCharset(toCharset(protoSourceFile.getFileOnDisk().getCharset())) .withOriginalPath(protoSourceFile.getFilename()) .withKind(sourceKind) .withPath(pathOnDisk) .build(); case ZIP_ENTRY: { SourceFileProto.ZipEntryOnDisk zipEntry = protoSourceFile.getZipEntry(); return SourceFile.builder() .withKind(sourceKind) .withOriginalPath(protoSourceFile.getFilename()) .withCharset(toCharset(zipEntry.getCharset())) .withZipEntryPath(zipEntry.getZipPath(), zipEntry.getEntryName()) .build(); } case LOADER_NOT_SET: break; } throw new AssertionError(); } private static SourceKind getSourceKindFromProto(SourceFileProto protoSourceFile) { switch (protoSourceFile.getSourceKind()) { case EXTERN: return SourceKind.EXTERN; case CODE: return SourceKind.STRONG; case NOT_SPECIFIED: case UNRECOGNIZED: break; } throw new AssertionError(); } private static Charset toCharset(String protoCharset) { if (protoCharset.isEmpty()) { return UTF_8; } return Charset.forName(protoCharset); } /** Create a new builder for source files. */ public static Builder builder() { return new Builder(); } /** * A builder interface for source files. * *

Allows users to customize the Charset, and the original path of the source file (if it * differs from the path on disk). */ public static final class Builder { private SourceKind kind = SourceKind.STRONG; private Charset charset = UTF_8; private @Nullable String originalPath = null; private @Nullable String path = null; private @Nullable Path pathWithFilesystem = null; private @Nullable String zipEntryPath = null; private @Nullable Supplier lazyContent = null; private Builder() {} /** Set the source kind. */ @CanIgnoreReturnValue public Builder withKind(SourceKind kind) { this.kind = kind; return this; } /** Set the charset to use when reading from an input stream or file. */ @CanIgnoreReturnValue public Builder withCharset(Charset charset) { this.charset = charset; return this; } public Builder withPath(String path) { return this.withPathInternal(path, null); } public Builder withPath(Path path) { return this.withPathInternal(path.toString(), path); } @CanIgnoreReturnValue public Builder withContent(String x) { this.lazyContent = x::toString; return this; } @CanIgnoreReturnValue @GwtIncompatible public Builder withContent(InputStream x) { this.lazyContent = () -> { checkState(this.charset != null); try { return CharStreams.toString(new InputStreamReader(x, this.charset)); } catch (IOException e) { throw new RuntimeException(e); } }; return this; } @CanIgnoreReturnValue public Builder withZipEntryPath(String zipPath, String entryPath) { this.path = zipPath; this.zipEntryPath = entryPath; return this; } /** * Sets a name for this source file that does not need to correspond to a path on disk. * *

Allow passing a reasonable human-readable name in cases like for zip files and for * generated files with unstable artifact prefixes. * *

The name must still be unique. */ @CanIgnoreReturnValue public Builder withOriginalPath(String originalPath) { this.originalPath = originalPath; return this; } public SourceFile build() { String displayPath = (this.originalPath != null) ? this.originalPath : ((this.zipEntryPath == null) ? this.path : this.path + BANG_SLASH + this.zipEntryPath); if (this.lazyContent != null) { return new SourceFile( new CodeLoader.Preloaded(this.lazyContent.get()), displayPath, this.kind); } if (this.zipEntryPath != null) { return new SourceFile( new CodeLoader.AtZip(this.path, this.zipEntryPath, this.charset), displayPath, this.kind); } return new SourceFile( new CodeLoader.OnDisk( (this.pathWithFilesystem != null) ? this.pathWithFilesystem : Paths.get(this.path), this.charset), displayPath, this.kind); } private Builder withPathInternal(String path, @Nullable Path pathWithFilesystem) { // Check if this path should be inferred as a ZIP entry path. int bangSlashIndex = path.indexOf(BANG_SLASH); if (bangSlashIndex >= 0) { String zipPath = path.substring(0, bangSlashIndex); String entryPath = path.substring(bangSlashIndex + BANG_SLASH.length()); if (zipPath.endsWith(".zip") && (entryPath.endsWith(".js") || entryPath.endsWith(".js.map"))) { return this.withZipEntryPath(zipPath, entryPath); } } // Path instances have an implicit reference to a FileSystem. Make sure to preserve it. this.path = path; this.pathWithFilesystem = pathWithFilesystem; return this; } } ////////////////////////////////////////////////////////////////////////////// // Implementations private abstract static class CodeLoader implements Serializable { /** * Return the source text of this file from its original storage. * *

The implementation may be a slow operation such as reading from a file. SourceFile * guarantees that this method is only called under synchronization. */ String loadUncachedCode() throws IOException { throw new AssertionError(); } /** * Return a Reader for the source text of this file from its original storage. * *

The implementation may be a slow operation such as reading from a file. SourceFile * guarantees that this method is only called under synchronization. */ Reader openUncachedReader() throws IOException { return null; } /** * Returns a representation of this loader that can be serialized/deserialized to reconstruct * this SourceFile */ abstract SourceFileProto.Builder toProtoLocationBuilder(String fileName); static final class Preloaded extends CodeLoader { private static final long serialVersionUID = 2L; private final String preloadedCode; Preloaded(String preloadedCode) { super(); this.preloadedCode = checkNotNull(preloadedCode); } @Override String loadUncachedCode() { return this.preloadedCode; } @Override SourceFileProto.Builder toProtoLocationBuilder(String fileName) { return SourceFileProto.newBuilder().setPreloadedContents(this.preloadedCode); } } static final class OnDisk extends CodeLoader { private static final long serialVersionUID = 1L; private final String serializableCharset; // TODO(b/180553215): We shouldn't store this Path. We already have to reconstruct it from a // string during deserialization. private transient Path relativePath; OnDisk(Path relativePath, Charset c) { super(); this.serializableCharset = c.name(); this.relativePath = relativePath; } @Override @GwtIncompatible String loadUncachedCode() throws IOException { try { return Files.readString(this.relativePath, this.getCharset()); } catch (CharacterCodingException e) { throw new IOException( "Failed to read: " + this.relativePath + ", is this input UTF-8 encoded?", e); } } @Override @GwtIncompatible Reader openUncachedReader() throws IOException { return Files.newBufferedReader(this.relativePath, this.getCharset()); } @GwtIncompatible private void writeObject(ObjectOutputStream out) throws Exception { out.defaultWriteObject(); out.writeObject(this.relativePath.toString()); } @GwtIncompatible private void readObject(ObjectInputStream in) throws Exception { in.defaultReadObject(); this.relativePath = Paths.get((String) in.readObject()); } private Charset getCharset() { return Charset.forName(this.serializableCharset); } @Override SourceFileProto.Builder toProtoLocationBuilder(String fileName) { String actualPath = this.relativePath.toString(); return SourceFileProto.newBuilder() .setFileOnDisk( FileOnDisk.newBuilder() .setActualPath( // to save space, don't serialize the path if equal to the fileName. fileName.equals(actualPath) ? "" : actualPath) // save space by not serializing UTF_8 (the default charset) .setCharset(this.getCharset().equals(UTF_8) ? "" : this.serializableCharset)); } } static final class AtZip extends CodeLoader { private static final long serialVersionUID = 1L; private final String zipName; private final String entryName; private final String serializableCharset; AtZip(String zipName, String entryName, Charset c) { super(); this.zipName = zipName; this.entryName = entryName; this.serializableCharset = c.name(); } @Override @GwtIncompatible String loadUncachedCode() throws IOException { return CharStreams.toString(this.openUncachedReader()); } @Override @GwtIncompatible Reader openUncachedReader() throws IOException { return new InputStreamReader( JSCompZipFileCache.getEntryStream(this.zipName, this.entryName), this.getCharset()); } private Charset getCharset() { return Charset.forName(this.serializableCharset); } @Override SourceFileProto.Builder toProtoLocationBuilder(String fileName) { return SourceFileProto.newBuilder() .setFilename(fileName) .setZipEntry( ZipEntryOnDisk.newBuilder() .setEntryName(this.entryName) .setZipPath(this.zipName) // save space by not serializing UTF_8 (the default charset) .setCharset(this.getCharset().equals(UTF_8) ? "" : this.serializableCharset) .build()); } } } @GwtIncompatible("ObjectOutputStream") private void writeObject(ObjectOutputStream os) throws Exception { checkState( this.getKind().equals(SourceKind.NON_CODE), "JS SourceFiles must not be serialized and are reconstructed by TypedAstDeserializer. " + "\nHit on: %s", this); os.defaultWriteObject(); this.serializeLineOffsetsToVarintDeltas(os); } @GwtIncompatible("ObjectInputStream") private void readObject(ObjectInputStream in) throws Exception { in.defaultReadObject(); this.deserializeVarintDeltasToLineOffsets(in); } private static final int SEVEN_BITS = 0b01111111; @GwtIncompatible("ObjectOutputStream") private void serializeLineOffsetsToVarintDeltas(ObjectOutputStream os) throws Exception { if (this.lineOffsets == null) { os.writeInt(-1); return; } os.writeInt(this.lineOffsets.length); // The first offset is always 0. for (int intIndex = 1; intIndex < this.lineOffsets.length; intIndex++) { int delta = this.lineOffsets[intIndex] - this.lineOffsets[intIndex - 1]; while (delta > SEVEN_BITS) { os.writeByte(delta | ~SEVEN_BITS); delta = delta >>> 7; } os.writeByte(delta); } } @GwtIncompatible("ObjectInputStream") private void deserializeVarintDeltasToLineOffsets(ObjectInputStream in) throws Exception { int lineCount = in.readInt(); if (lineCount == -1) { this.lineOffsets = null; return; } int[] lineOffsets = new int[lineCount]; // The first offset is always 0. for (int intIndex = 1; intIndex < lineCount; intIndex++) { int delta = 0; int shift = 0; byte segment = in.readByte(); for (; segment < 0; segment = in.readByte()) { delta |= (segment & SEVEN_BITS) << shift; shift += 7; } delta |= segment << shift; lineOffsets[intIndex] = delta + lineOffsets[intIndex - 1]; } this.lineOffsets = lineOffsets; } public SourceFileProto getProto() { return this.loader .toProtoLocationBuilder(this.getName()) .setFilename(this.getName()) .setSourceKind(sourceKindToProto(this.getKind())) .build(); } private static SourceFileProto.SourceKind sourceKindToProto(SourceKind sourceKind) { switch (sourceKind) { case EXTERN: return SourceFileProto.SourceKind.EXTERN; case STRONG: case WEAK: return SourceFileProto.SourceKind.CODE; case NON_CODE: break; } throw new AssertionError(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy