Skip to content

Add language service API to get the compile and runtime dependencies of a source file #3687

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/harness/harnessLanguageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,9 @@ module Harness.LanguageService {
getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: ts.FormatCodeOptions): ts.TextChange[] {
return unwrapJSONCallResult(this.shim.getFormattingEditsAfterKeystroke(fileName, position, key, JSON.stringify(options)));
}
getDependencies(fileName: string): ts.DependencyInfo {
return unwrapJSONCallResult(this.shim.getDependencies(fileName));
}
getEmitOutput(fileName: string): ts.EmitOutput {
return unwrapJSONCallResult(this.shim.getEmitOutput(fileName));
}
Expand Down
4 changes: 4 additions & 0 deletions src/server/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,10 @@ namespace ts.server {
});
}

getDependencies(fileName: string): DependencyInfo {
throw new Error("Not Implemented Yet.");
}

getEmitOutput(fileName: string): EmitOutput {
throw new Error("Not Implemented Yet.");
}
Expand Down
76 changes: 73 additions & 3 deletions src/services/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1027,9 +1027,11 @@ namespace ts {
getFormattingEditsForRange(fileName: string, start: number, end: number, options: FormatCodeOptions): TextChange[];
getFormattingEditsForDocument(fileName: string, options: FormatCodeOptions): TextChange[];
getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: FormatCodeOptions): TextChange[];

getDependencies(fileName: string): DependencyInfo;

getEmitOutput(fileName: string): EmitOutput;

getProgram(): Program;

getSourceFile(fileName: string): SourceFile;
Expand Down Expand Up @@ -1267,11 +1269,20 @@ namespace ts {
autoCollapse: boolean;
}

export interface DependencyInfo {
/** The file name of the dependency information */
fileName: string;
/** The compile time dependencies of a file */
compileTime: string[];
/** The runtime dependencies of a file */
runtime: string[];
}

export interface EmitOutput {
outputFiles: OutputFile[];
emitSkipped: boolean;
}

export const enum OutputFileType {
JavaScript,
SourceMap,
Expand Down Expand Up @@ -5762,6 +5773,64 @@ namespace ts {
return forEach(diagnostics, diagnostic => diagnostic.category === DiagnosticCategory.Error);
}

function getDependencies(fileName: string): DependencyInfo {
synchronizeHostData();

let sourceFile = getValidSourceFile(fileName);
if (!sourceFile) {
return undefined;
}

let resolver = program.getDiagnosticsProducingTypeChecker().getEmitResolver(sourceFile);

let runtime: string[] = [];
let compileTime: string[] = [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initially i thought that compileTime is a subset of runtime, but then looking at the code, that did not turn out to be the case. i think we need better names here. maybe "compileTimeOnly", or "non-js dependencies.."

for (let node of sourceFile.amdDependencies) {
runtime.push(node.path);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

honoring these is dependent on the module target, if it is AMD or UMD, we use them, otherwise they are ignored i.e. in SystemJS and CommonJS. it might be useful to split them in their own list, instead of in "runtime". this way you can have: "imports", "amdDependencies", and "typeOnlyImports"/"designTimeImports"

}

function getModuleNameText(node: ImportDeclaration | ImportEqualsDeclaration | ExportDeclaration): string {
let moduleName = getExternalModuleName(node);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name is not sufficient. We are making some changes to the resolution logic to honor node resolution process, e.g. looking under node_moduels, and looking in package.json. so you want to leverage that here, so this should be a tuple of name (possibly for error reporting) and resolved name. @vladima has a change for this in #3616.

return (<LiteralExpression>moduleName).text;
}

for (let node of sourceFile.statements) {
switch (node.kind) {
case SyntaxKind.ImportDeclaration:
let importDeclaration = <ImportDeclaration>node;
if (!importDeclaration.importClause || resolver.isReferencedAliasDeclaration(importDeclaration.importClause, /*checkChildren*/ true)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you need to force a typecheck on the file to make sure this is set correctly. so you need to query for all semantic diagnostics.

runtime.push(getModuleNameText(importDeclaration));
} else {
compileTime.push(getModuleNameText(importDeclaration));
}
break;
case SyntaxKind.ImportEqualsDeclaration:
let importEqualsDeclaration = <ImportEqualsDeclaration>node;
if (importEqualsDeclaration.moduleReference.kind === SyntaxKind.ExternalModuleReference) {
if (resolver.isReferencedAliasDeclaration(importEqualsDeclaration)) {
runtime.push(getModuleNameText(importEqualsDeclaration));
} else {
compileTime.push(getModuleNameText(importEqualsDeclaration));
}
}
break;
case SyntaxKind.ExportDeclaration:
let exportDeclaration = <ExportDeclaration>node;
if (exportDeclaration.moduleSpecifier && (!exportDeclaration.exportClause || resolver.isValueAliasDeclaration(exportDeclaration))) {
// export * as mod from 'mod' && export {x , y } from 'mod'
runtime.push(getModuleNameText(exportDeclaration));
}
// export { x, y } does neither generate a runtime nor a compile time dependency
break;
}
}
return {
fileName: sourceFile.fileName,
compileTime: compileTime,
runtime: runtime
}
}

function getEmitOutput(fileName: string): EmitOutput {
synchronizeHostData();

Expand All @@ -5783,7 +5852,7 @@ namespace ts {
emitSkipped: emitOutput.emitSkipped
};
}

function getMeaningFromDeclaration(node: Node): SemanticMeaning {
switch (node.kind) {
case SyntaxKind.Parameter:
Expand Down Expand Up @@ -6810,6 +6879,7 @@ namespace ts {
getFormattingEditsForRange,
getFormattingEditsForDocument,
getFormattingEditsAfterKeystroke,
getDependencies,
getEmitOutput,
getSourceFile,
getProgram
Expand Down
12 changes: 12 additions & 0 deletions src/services/shims.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@ namespace ts {
getFormattingEditsForDocument(fileName: string, options: string/*Services.FormatCodeOptions*/): string;
getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: string/*Services.FormatCodeOptions*/): string;

getDependencies(fileName: string): string;

getEmitOutput(fileName: string): string;
}

Expand Down Expand Up @@ -799,6 +801,16 @@ namespace ts {
return items;
});
}

public getDependencies(fileName: string): string {
return this.forwardJSONCall(
"getDependencies('" + fileName + "')",
() => {
var dependencies = this.languageService.getDependencies(fileName);
return dependencies;
}
)
}

/// Emit
public getEmitOutput(fileName: string): string {
Expand Down