Skip to content

Commit

Permalink
Regenerated Program and TypeChecker after normalizing source files.
Browse files Browse the repository at this point in the history
This allows the FacadeConverter to have access to the correct type and value declarations after we have merged variables with classes.

Refactored test_support so that it shares more code with main, and performs the same normalization of source files.
  • Loading branch information
derekxu16 committed Dec 4, 2019
1 parent ea26154 commit 8c80159
Show file tree
Hide file tree
Showing 8 changed files with 221 additions and 185 deletions.
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@ Generates `package:js` JavaScript interop facades for arbitrary TypeScript libra
## Usage

### Basic
`dart_js_facade_gen <input d.ts file>`<br/>
`dart_js_facade_gen <input d.ts file>`<br>
Dart interop facade file is written to stdout.

### Advanced
`dart_js_facade_gen --destination=<destination-dir> --base-path=<input d.ts file directory> <input d.ts file> <input d.ts file> ...`

#### Flags
`--destination=<destination-dir>`: output generated code to destination-dir<br/>
`--base-path=<input d.ts file directory>`: specify the directory that contains the input d.ts files<br/>
`--generate-html`: generate facades for dart:html types rather than importing them<br/>
`--rename-conflicting-types`: rename types to avoid conflicts in cases where a variable and a type have the exact same name, but it is not clear if they are related or not.
`--explicit-static`: disables default assumption that properties declared on the anonymous types of top level variable declarations are static
`--destination=<destination-dir>`: Output generated code to destination-dir.<br>
`--base-path=<input d.ts file directory>`: Specify the directory that contains the input d.ts files.<br>
`--generate-html`: Generate facades for dart:html types rather than importing them.<br>
`--rename-conflicting-types`: Rename types to avoid conflicts in cases where a variable and a type have the exact same name, but it is not clear if they are related or not.<br>
`--explicit-static`: Disables default assumption that properties declared on the anonymous types of top level variable declarations are static.<br>
`--trust-js-types`: Emits @anonymous tags on classes that have neither constructors nor static members. This prevents the Dart Dev Compiler from checking whether or not objects are truly instances of those classes. This flag should be used if the input JS/TS library has structural types, or is otherwise claiming that types match in cases where the correct JS prototype is not there for DDC to check against.

### Example
Expand Down
16 changes: 0 additions & 16 deletions lib/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,22 +471,6 @@ export class TranspilerBase {
this.transpiler.reportError(n, message);
}

/**
* Prevents this node from being visited.
*/
suppressNode(n: ts.Node) {
const emptyNode = ts.createNode(ts.SyntaxKind.EmptyStatement);
copyLocation(n, emptyNode);
this.replaceNode(n, emptyNode);
}

/**
* Effectively replaces original with replacement in the AST.
*/
replaceNode(original: ts.Node, replacement: ts.Node) {
this.transpiler.nodeSubstitutions.set(original, replacement);
}

visitNode(n: ts.Node): boolean {
throw new Error('not implemented');
}
Expand Down
20 changes: 0 additions & 20 deletions lib/facade_converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -616,26 +616,6 @@ export class FacadeConverter extends base.TranspilerBase {
}
}

replaceNode(original: ts.Node, replacement: ts.Node) {
if (ts.isVariableDeclaration(original) &&
(ts.isInterfaceDeclaration(replacement) || ts.isClassDeclaration(replacement))) {
// Handle the speical case in mergeVariablesIntoClasses where we upgrade variable declarations
// to interfaces or classes.
const symbol = this.tc.getSymbolAtLocation(original.name);
symbol.declarations = symbol.getDeclarations().map((declaration: ts.Declaration) => {
// TODO(derekx): Changing the declarations of a symbol like this is a hack. It would be
// cleaner and safer to generate a new Program and TypeChecker after performing
// gatherClasses and mergeVariablesIntoClasses.
if (declaration === original) {
return replacement;
}
return declaration;
});
}

super.replaceNode(original, replacement);
}

getSymbolAtLocation(identifier: ts.EntityName) {
let symbol = this.tc.getSymbolAtLocation(identifier);
while (symbol && symbol.flags & ts.SymbolFlags.Alias) symbol = this.tc.getAliasedSymbol(symbol);
Expand Down
203 changes: 128 additions & 75 deletions lib/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ export interface TranspilerOptions {
* reason for the failure. When falsey, semantic diagnostics will never be output.
*/
semanticDiagnostics?: boolean;
/**
* Skip running dart-format on the output. This is useful for large files (like dom.d.ts) since
* the node package version of dart-format is significantly slower than the version in the SDK.
*/
skipFormatting?: boolean;
/**
* Specify the module name (e.g.) d3 instead of determining the module name from the d.ts files.
* This is useful for libraries that assume they will be loaded with a JS module loader but that
Expand Down Expand Up @@ -82,7 +87,7 @@ export const COMPILER_OPTIONS: ts.CompilerOptions = {
/**
* Context to ouput code into.
*/
export enum OutputContext {
export const enum OutputContext {
Import = 0,
Header = 1,
Default = 2,
Expand Down Expand Up @@ -132,6 +137,8 @@ export class Transpiler {
* @param destination Location to write files to. Outputs file contents to stdout if absent.
*/
transpile(fileNames: string[], destination?: string): void {
this.errors = [];

if (this.options.basePath) {
this.options.basePath = this.normalizeSlashes(path.resolve(this.options.basePath));
}
Expand All @@ -143,98 +150,144 @@ export class Transpiler {
}
return normalizedName;
});
const host = this.createCompilerHost();
const program = ts.createProgram(fileNames, this.getCompilerOptions(), host);
this.fc.setTypeChecker(program.getTypeChecker());
this.declarationTranspiler.setTypeChecker(program.getTypeChecker());

// Only write files that were explicitly passed in.
const fileSet = new Set(fileNames);
const sourceFiles = program.getSourceFiles().filter((sourceFile) => {
return fileSet.has(sourceFile.fileName);
});

this.errors = [];

const sourceFileMap: Map<string, ts.SourceFile> = new Map();
sourceFiles.forEach((f: ts.SourceFile) => {
sourceFileMap.set(f.fileName, f);
});

// Check for global module export declarations and propogate them to all modules they export.
sourceFiles.forEach((f: ts.SourceFile) => {
f.statements.forEach((n: ts.Node) => {
if (!ts.isNamespaceExportDeclaration(n)) return;
// This is the name we are interested in for Dart purposes until Dart supports AMD module
// loaders. This module name should all be reflected by all modules exported by this
// library as we need to specify a global module location for every Dart library.
let globalModuleName = base.ident(n.name);
f.moduleName = globalModuleName;

const missingFiles: string[] = [];
f.statements.forEach((e: ts.Node) => {
if (!ts.isExportDeclaration(e)) return;
let exportDecl = e;
if (!exportDecl.moduleSpecifier) return;
let moduleLocation = <ts.StringLiteral>exportDecl.moduleSpecifier;
let location = moduleLocation.text;
let resolvedPath = host.resolveModuleNames(
[location], f.fileName, undefined, undefined, this.getCompilerOptions());
resolvedPath.forEach((p) => {
if (p.isExternalLibraryImport) return;
const exportedFile = sourceFileMap.get(p.resolvedFileName);
if (exportedFile) {
exportedFile.moduleName = globalModuleName;
} else {
missingFiles.push(p.resolvedFileName);
}
});
});
if (missingFiles.length) {
const error = new Error();
error.message =
'The following files were referenced but were not supplied as a command line arguments. Reference the README for usage instructions.';
for (const file of missingFiles) {
error.message += '\n';
error.message += file;
}
error.name = 'DartFacadeError';
throw error;
}
});
});
const host = this.createCompilerHost(sourceFileMap);

sourceFiles.forEach((f: ts.SourceFile) => {
const dartCode = this.translate(f, fileSet);
this.normalizeSourceFiles(fileSet, sourceFileMap, host);
// Create a new program after performing source file transformations.
const normalizedProgram = ts.createProgram(fileNames, this.getCompilerOptions(), host);
const translatedResults = this.translateProgram(normalizedProgram, fileNames);

for (const fileName of translatedResults.keys()) {
if (destination) {
const outputFile = this.getOutputPath(path.resolve(f.fileName), destination);
const outputFile = this.getOutputPath(path.resolve(fileName), destination);
console.log('Output file:', outputFile);
mkdirP(path.dirname(outputFile));
fs.writeFileSync(outputFile, dartCode);
fs.writeFileSync(outputFile, translatedResults.get(fileName));
} else {
// Write source code directly to the console when no destination is specified.
console.log(dartCode);
console.log(translatedResults.get(fileName));
}
});
this.checkForErrors(program);
}
this.checkForErrors(normalizedProgram);
}

translateProgram(program: ts.Program): {[path: string]: string} {
translateProgram(program: ts.Program, entryPoints: string[]): Map<string, string> {
this.fc.setTypeChecker(program.getTypeChecker());
this.declarationTranspiler.setTypeChecker(program.getTypeChecker());

let paths: {[path: string]: string} = {};
const paths: Map<string, string> = new Map();
this.errors = [];
program.getSourceFiles()
.filter(
(sourceFile: ts.SourceFile) =>
(!sourceFile.fileName.match(/\.d\.ts$/) && !!sourceFile.fileName.match(/\.[jt]s$/)))
.forEach((f) => paths[f.fileName] = this.translate(f, new Set([f.fileName])));
.filter((f: ts.SourceFile) => entryPoints.includes(f.fileName))
.forEach((f) => paths.set(f.fileName, this.translate(f)));
this.checkForErrors(program);
return paths;
}

/**
* Preliminary processing of source files to make them compatible with Dart.
*
* Propagates namespace export declarations and merges related classes and variables.
*
* @param fileNames The input files.
* @param sourceFileMap A map that is used to access SourceFiles by their file names. The
* normalized files will be stored in this map.
* @param compilerHost The TS compiler host.
*/
normalizeSourceFiles(
fileNames: Set<string>, sourceFileMap: Map<string, ts.SourceFile>,
compilerHost: ts.CompilerHost) {
const program =
ts.createProgram(Array.from(fileNames), this.getCompilerOptions(), compilerHost);

if (program.getSyntacticDiagnostics().length > 0) {
// Throw first error.
const first = program.getSyntacticDiagnostics()[0];
const error = new Error(`${first.start}: ${first.messageText} in ${first.file.fileName}`);
error.name = 'DartFacadeError';
throw error;
}

this.fc.setTypeChecker(program.getTypeChecker());

const sourceFiles =
program.getSourceFiles().filter((f: ts.SourceFile) => fileNames.has(f.fileName));

sourceFiles.forEach((f: ts.SourceFile) => {
sourceFileMap.set(f.fileName, f);
});

sourceFiles.forEach((f: ts.SourceFile) => {
this.propagateNamespaceExportDeclarations(f, sourceFileMap, compilerHost);
});

sourceFiles.forEach((f: ts.SourceFile) => {
const normalizedFile = merge.normalizeSourceFile(
f, this.fc, fileNames, this.options.renameConflictingTypes, this.options.explicitStatic);

sourceFileMap.set(f.fileName, normalizedFile);
});
}

/**
* Check for namespace export declarations and propagate them to all modules they export.
*
* Namespace export declarations are used to declare UMD modules. The syntax for
* them is 'export as namespace MyNamespace;'. This means that exported members of module
* MyNamespace can be accessed through the global variable 'MyNamespace' within script files. Or
* within source files, by importing them like you would import other modular libraries.
*/
private propagateNamespaceExportDeclarations(
sourceFile: ts.SourceFile,
sourceFileMap: Map<string, ts.SourceFile>,
compilerHost: ts.CompilerHost,
) {
let globalModuleName: string;
sourceFile.forEachChild((n: ts.Node) => {
if (!ts.isNamespaceExportDeclaration(n)) return;
// This is the name we are interested in for Dart purposes until Dart supports AMD module
// loaders. This module name should all be reflected by all modules exported by this
// library as we need to specify a global module location for every Dart library.
globalModuleName = base.ident(n.name);
sourceFile.moduleName = globalModuleName;
});

const missingFiles: string[] = [];
sourceFile.statements.forEach((e: ts.Node) => {
if (!ts.isExportDeclaration(e)) return;
let exportDecl = e;
if (!exportDecl.moduleSpecifier) return;
let moduleLocation = <ts.StringLiteral>exportDecl.moduleSpecifier;
let location = moduleLocation.text;
let resolvedPath = compilerHost.resolveModuleNames(
[location], sourceFile.fileName, undefined, undefined, this.getCompilerOptions());
resolvedPath.forEach((p) => {
if (!p || p.isExternalLibraryImport) return;
const exportedFile = sourceFileMap.get(p.resolvedFileName);
if (exportedFile) {
exportedFile.moduleName = globalModuleName;
} else {
missingFiles.push(p.resolvedFileName);
}
});
});
if (missingFiles.length) {
const error = new Error();
error.message =
'The following files were referenced but were not supplied as a command line arguments. Reference the README for usage instructions.';
for (const file of missingFiles) {
error.message += '\n';
error.message += file;
}
error.name = 'DartFacadeError';
throw error;
}
}

getCompilerOptions() {
const opts: ts.CompilerOptions = Object.assign({}, COMPILER_OPTIONS);
opts.rootDir = this.options.basePath;
Expand All @@ -245,14 +298,16 @@ export class Transpiler {
return opts;
}

private createCompilerHost(): ts.CompilerHost {
private createCompilerHost(sourceFileMap: Map<string, ts.SourceFile>): ts.CompilerHost {
const compilerOptions = this.getCompilerOptions();
const compilerHost = ts.createCompilerHost(compilerOptions);
const defaultLibFileName = this.normalizeSlashes(ts.getDefaultLibFileName(compilerOptions));
compilerHost.getSourceFile = (sourceName) => {
let sourcePath = sourceName;
if (sourceName === defaultLibFileName) {
sourcePath = ts.getDefaultLibFilePath(compilerOptions);
} else if (sourceFileMap.has(sourceName)) {
return sourceFileMap.get(sourceName);
}
if (!fs.existsSync(sourcePath)) {
return undefined;
Expand Down Expand Up @@ -288,7 +343,7 @@ export class Transpiler {
this.outputStack.pop();
}

private translate(sourceFile: ts.SourceFile, fileSet: Set<string>): string {
private translate(sourceFile: ts.SourceFile): string {
this.currentFile = sourceFile;
this.outputs = [];
this.outputStack = [];
Expand All @@ -298,9 +353,6 @@ export class Transpiler {
}

this.lastCommentIdx = -1;
merge.normalizeSourceFile(
sourceFile, this.fc, fileSet, this.options.renameConflictingTypes,
this.options.explicitStatic);
this.pushContext(OutputContext.Default);
this.visit(sourceFile);
this.popContext();
Expand Down Expand Up @@ -332,6 +384,7 @@ export class Transpiler {
for (let output of this.outputs) {
result += output.getResult();
}

return this.formatCode(result, sourceFile);
}

Expand Down
Loading

0 comments on commit 8c80159

Please sign in to comment.