diff options
Diffstat (limited to 'js')
-rw-r--r-- | js/compiler.ts | 128 | ||||
-rw-r--r-- | js/diagnostics.ts | 218 |
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 }; +} |