summaryrefslogtreecommitdiff
path: root/js
diff options
context:
space:
mode:
Diffstat (limited to 'js')
-rw-r--r--js/compiler.ts128
-rw-r--r--js/diagnostics.ts218
2 files changed, 285 insertions, 61 deletions
diff --git a/js/compiler.ts b/js/compiler.ts
index 15adba746..6b0881700 100644
--- a/js/compiler.ts
+++ b/js/compiler.ts
@@ -1,6 +1,7 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import * as msg from "gen/cli/msg_generated";
import { core } from "./core";
+import { Diagnostic, fromTypeScriptDiagnostic } from "./diagnostics";
import * as flatbuffers from "./flatbuffers";
import { sendSync } from "./dispatch";
import { TextDecoder } from "./text_encoding";
@@ -37,6 +38,11 @@ interface CompilerReq {
config?: string;
}
+interface ConfigureResponse {
+ ignoredOptions?: string[];
+ diagnostics?: ts.Diagnostic[];
+}
+
/** Options that either do nothing in Deno, or would cause undesired behavior
* if modified. */
const ignoredCompilerOptions: ReadonlyArray<string> = [
@@ -105,6 +111,11 @@ interface ModuleMetaData {
sourceCode: string | undefined;
}
+interface EmitResult {
+ emitSkipped: boolean;
+ diagnostics?: Diagnostic;
+}
+
function fetchModuleMetaData(
specifier: string,
referrer: string
@@ -193,22 +204,19 @@ class Host implements ts.CompilerHost {
* compiler's configuration options. The method returns an array of compiler
* options which were ignored, or `undefined`.
*/
- configure(path: string, configurationText: string): string[] | undefined {
+ configure(path: string, configurationText: string): ConfigureResponse {
util.log("compile.configure", path);
const { config, error } = ts.parseConfigFileTextToJson(
path,
configurationText
);
if (error) {
- this._logDiagnostics([error]);
+ return { diagnostics: [error] };
}
const { options, errors } = ts.convertCompilerOptionsFromJson(
config.compilerOptions,
cwd()
);
- if (errors.length) {
- this._logDiagnostics(errors);
- }
const ignoredOptions: string[] = [];
for (const key of Object.keys(options)) {
if (
@@ -220,7 +228,10 @@ class Host implements ts.CompilerHost {
}
}
Object.assign(this._options, options);
- return ignoredOptions.length ? ignoredOptions : undefined;
+ return {
+ ignoredOptions: ignoredOptions.length ? ignoredOptions : undefined,
+ diagnostics: errors.length ? errors : undefined
+ };
}
getCompilationSettings(): ts.CompilerOptions {
@@ -228,19 +239,6 @@ class Host implements ts.CompilerHost {
return this._options;
}
- /** Log TypeScript diagnostics to the console and exit */
- _logDiagnostics(diagnostics: ReadonlyArray<ts.Diagnostic>): never {
- const errMsg = os.noColor
- ? ts.formatDiagnostics(diagnostics, this)
- : ts.formatDiagnosticsWithColorAndContext(diagnostics, this);
-
- console.log(errMsg);
- // TODO The compiler isolate shouldn't call os.exit(). (In fact, it
- // shouldn't even have access to call that op.) Errors should be forwarded
- // to to the caller and the caller exit.
- return os.exit(1);
- }
-
fileExists(_fileName: string): boolean {
return notImplemented();
}
@@ -362,10 +360,17 @@ class Host implements ts.CompilerHost {
window.compilerMain = function compilerMain(): void {
// workerMain should have already been called since a compiler is a worker.
window.onmessage = ({ data }: { data: CompilerReq }): void => {
+ let emitSkipped = true;
+ let diagnostics: ts.Diagnostic[] | undefined;
+
const { rootNames, configPath, config } = data;
const host = new Host();
- if (config && config.length) {
- const ignoredOptions = host.configure(configPath!, config);
+
+ // if there is a configuration supplied, we need to parse that
+ if (config && config.length && configPath) {
+ const configResult = host.configure(configPath, config);
+ const ignoredOptions = configResult.ignoredOptions;
+ diagnostics = configResult.diagnostics;
if (ignoredOptions) {
console.warn(
yellow(`Unsupported compiler options in "${configPath}"\n`) +
@@ -377,51 +382,52 @@ window.compilerMain = function compilerMain(): void {
}
}
- const options = host.getCompilationSettings();
- const program = ts.createProgram(rootNames, options, host);
-
- const preEmitDiagnostics = ts.getPreEmitDiagnostics(program).filter(
- ({ code }): boolean => {
- // TS2691: An import path cannot end with a '.ts' extension. Consider
- // importing 'bad-module' instead.
- if (code === 2691) return false;
- // TS5009: Cannot find the common subdirectory path for the input files.
- if (code === 5009) return false;
- // TS5055: Cannot write file
- // 'http://localhost:4545/tests/subdir/mt_application_x_javascript.j4.js'
- // because it would overwrite input file.
- if (code === 5055) return false;
- // TypeScript is overly opinionated that only CommonJS modules kinds can
- // support JSON imports. Allegedly this was fixed in
- // Microsoft/TypeScript#26825 but that doesn't seem to be working here,
- // so we will ignore complaints about this compiler setting.
- if (code === 5070) return false;
- return true;
+ // if there was a configuration and no diagnostics with it, we will continue
+ // to generate the program and possibly emit it.
+ if (!diagnostics || (diagnostics && diagnostics.length === 0)) {
+ const options = host.getCompilationSettings();
+ const program = ts.createProgram(rootNames, options, host);
+
+ diagnostics = ts.getPreEmitDiagnostics(program).filter(
+ ({ code }): boolean => {
+ // TS2691: An import path cannot end with a '.ts' extension. Consider
+ // importing 'bad-module' instead.
+ if (code === 2691) return false;
+ // TS5009: Cannot find the common subdirectory path for the input files.
+ if (code === 5009) return false;
+ // TS5055: Cannot write file
+ // 'http://localhost:4545/tests/subdir/mt_application_x_javascript.j4.js'
+ // because it would overwrite input file.
+ if (code === 5055) return false;
+ // TypeScript is overly opinionated that only CommonJS modules kinds can
+ // support JSON imports. Allegedly this was fixed in
+ // Microsoft/TypeScript#26825 but that doesn't seem to be working here,
+ // so we will ignore complaints about this compiler setting.
+ if (code === 5070) return false;
+ return true;
+ }
+ );
+
+ // We will only proceed with the emit if there are no diagnostics.
+ if (diagnostics && diagnostics.length === 0) {
+ const emitResult = program.emit();
+ emitSkipped = emitResult.emitSkipped;
+ // emitResult.diagnostics is `readonly` in TS3.5+ and can't be assigned
+ // without casting.
+ diagnostics = emitResult.diagnostics as ts.Diagnostic[];
}
- );
- if (preEmitDiagnostics.length > 0) {
- host._logDiagnostics(preEmitDiagnostics);
- // The above _logDiagnostics calls os.exit(). The return is here just for
- // clarity.
- return;
}
- const emitResult = program!.emit();
-
- // TODO(ry) Print diagnostics in Rust.
- // https://github.com/denoland/deno/pull/2310
-
- const { diagnostics } = emitResult;
- if (diagnostics.length > 0) {
- host._logDiagnostics(diagnostics);
- // The above _logDiagnostics calls os.exit(). The return is here just for
- // clarity.
- return;
- }
+ const result: EmitResult = {
+ emitSkipped,
+ diagnostics: diagnostics.length
+ ? fromTypeScriptDiagnostic(diagnostics)
+ : undefined
+ };
- postMessage(emitResult);
+ postMessage(result);
- // The compiler isolate exits after a single messsage.
+ // The compiler isolate exits after a single message.
workerClose();
};
};
diff --git a/js/diagnostics.ts b/js/diagnostics.ts
new file mode 100644
index 000000000..1207eca4f
--- /dev/null
+++ b/js/diagnostics.ts
@@ -0,0 +1,218 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+
+// Diagnostic provides an abstraction for advice/errors received from a
+// compiler, which is strongly influenced by the format of TypeScript
+// diagnostics.
+
+import * as ts from "typescript";
+
+/** The log category for a diagnostic message */
+export enum DiagnosticCategory {
+ Log = 0,
+ Debug = 1,
+ Info = 2,
+ Error = 3,
+ Warning = 4,
+ Suggestion = 5
+}
+
+export interface DiagnosticMessageChain {
+ message: string;
+ category: DiagnosticCategory;
+ code: number;
+ next?: DiagnosticMessageChain;
+}
+
+export interface DiagnosticItem {
+ /** A string message summarizing the diagnostic. */
+ message: string;
+
+ /** An ordered array of further diagnostics. */
+ messageChain?: DiagnosticMessageChain;
+
+ /** Information related to the diagnostic. This is present when there is a
+ * suggestion or other additional diagnostic information */
+ relatedInformation?: DiagnosticItem[];
+
+ /** The text of the source line related to the diagnostic */
+ sourceLine?: string;
+
+ /** The line number that is related to the diagnostic */
+ lineNumber?: number;
+
+ /** The name of the script resource related to the diagnostic */
+ scriptResourceName?: string;
+
+ /** The start position related to the diagnostic */
+ startPosition?: number;
+
+ /** The end position related to the diagnostic */
+ endPosition?: number;
+
+ /** The category of the diagnostic */
+ category: DiagnosticCategory;
+
+ /** A number identifier */
+ code: number;
+
+ /** The the start column of the sourceLine related to the diagnostic */
+ startColumn?: number;
+
+ /** The end column of the sourceLine related to the diagnostic */
+ endColumn?: number;
+}
+
+export interface Diagnostic {
+ /** An array of diagnostic items. */
+ items: DiagnosticItem[];
+}
+
+interface SourceInformation {
+ sourceLine: string;
+ lineNumber: number;
+ scriptResourceName: string;
+ startColumn: number;
+ endColumn: number;
+}
+
+function fromDiagnosticCategory(
+ category: ts.DiagnosticCategory
+): DiagnosticCategory {
+ switch (category) {
+ case ts.DiagnosticCategory.Error:
+ return DiagnosticCategory.Error;
+ case ts.DiagnosticCategory.Message:
+ return DiagnosticCategory.Info;
+ case ts.DiagnosticCategory.Suggestion:
+ return DiagnosticCategory.Suggestion;
+ case ts.DiagnosticCategory.Warning:
+ return DiagnosticCategory.Warning;
+ default:
+ throw new Error(
+ `Unexpected DiagnosticCategory: "${category}"/"${
+ ts.DiagnosticCategory[category]
+ }"`
+ );
+ }
+}
+
+function getSourceInformation(
+ sourceFile: ts.SourceFile,
+ start: number,
+ length: number
+): SourceInformation {
+ const scriptResourceName = sourceFile.fileName;
+ const {
+ line: lineNumber,
+ character: startColumn
+ } = sourceFile.getLineAndCharacterOfPosition(start);
+ const endPosition = sourceFile.getLineAndCharacterOfPosition(start + length);
+ const endColumn =
+ lineNumber === endPosition.line ? endPosition.character : startColumn;
+ const lastLineInFile = sourceFile.getLineAndCharacterOfPosition(
+ sourceFile.text.length
+ ).line;
+ const lineStart = sourceFile.getPositionOfLineAndCharacter(lineNumber, 0);
+ const lineEnd =
+ lineNumber < lastLineInFile
+ ? sourceFile.getPositionOfLineAndCharacter(lineNumber + 1, 0)
+ : sourceFile.text.length;
+ const sourceLine = sourceFile.text
+ .slice(lineStart, lineEnd)
+ .replace(/\s+$/g, "")
+ .replace("\t", " ");
+ return {
+ sourceLine,
+ lineNumber,
+ scriptResourceName,
+ startColumn,
+ endColumn
+ };
+}
+
+/** Converts a TypeScript diagnostic message chain to a Deno one. */
+function fromDiagnosticMessageChain(
+ messageChain: ts.DiagnosticMessageChain | undefined
+): DiagnosticMessageChain | undefined {
+ if (!messageChain) {
+ return undefined;
+ }
+
+ const { messageText: message, code, category, next } = messageChain;
+ return {
+ message,
+ code,
+ category: fromDiagnosticCategory(category),
+ next: fromDiagnosticMessageChain(next)
+ };
+}
+
+/** Parse out information from a TypeScript diagnostic structure. */
+function parseDiagnostic(
+ item: ts.Diagnostic | ts.DiagnosticRelatedInformation
+): DiagnosticItem {
+ const {
+ messageText,
+ category: sourceCategory,
+ code,
+ file,
+ start: startPosition,
+ length
+ } = item;
+ const sourceInfo =
+ file && startPosition && length
+ ? getSourceInformation(file, startPosition, length)
+ : undefined;
+ const endPosition =
+ startPosition && length ? startPosition + length : undefined;
+ const category = fromDiagnosticCategory(sourceCategory);
+
+ let message: string;
+ let messageChain: DiagnosticMessageChain | undefined;
+ if (typeof messageText === "string") {
+ message = messageText;
+ } else {
+ message = messageText.messageText;
+ messageChain = fromDiagnosticMessageChain(messageText);
+ }
+
+ const base = {
+ message,
+ messageChain,
+ code,
+ category,
+ startPosition,
+ endPosition
+ };
+
+ return sourceInfo ? { ...base, ...sourceInfo } : base;
+}
+
+/** Convert a diagnostic related information array into a Deno diagnostic
+ * array. */
+function parseRelatedInformation(
+ relatedInformation: readonly ts.DiagnosticRelatedInformation[]
+): DiagnosticItem[] {
+ const result: DiagnosticItem[] = [];
+ for (const item of relatedInformation) {
+ result.push(parseDiagnostic(item));
+ }
+ return result;
+}
+
+/** Convert TypeScript diagnostics to Deno diagnostics. */
+export function fromTypeScriptDiagnostic(
+ diagnostics: readonly ts.Diagnostic[]
+): Diagnostic {
+ let items: DiagnosticItem[] = [];
+ for (const sourceDiagnostic of diagnostics) {
+ const item: DiagnosticItem = parseDiagnostic(sourceDiagnostic);
+ if (sourceDiagnostic.relatedInformation) {
+ item.relatedInformation = parseRelatedInformation(
+ sourceDiagnostic.relatedInformation
+ );
+ }
+ items.push(item);
+ }
+ return { items };
+}