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

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

There is a newer version: 9.0.8
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.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 javax.annotation.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[] lineOffsets = null; private transient volatile String code = null; 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]; } 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); } /** 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(String sourceCode) { this.code = null; this.lineOffsets = null; 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 */ @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 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 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 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 String originalPath = null; private String path = null; private Path pathWithFilesystem = null; private String zipEntryPath = null; private Supplier lazyContent = null; private Builder() {} /** Set the source kind. */ public Builder withKind(SourceKind kind) { this.kind = kind; return this; } /** Set the charset to use when reading from an input stream or file. */ 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); } public Builder withContent(String x) { this.lazyContent = x::toString; return this; } @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; } 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. */ 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