Skip to content

Framework for discover typings extensions #9848

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
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
50 changes: 35 additions & 15 deletions src/compiler/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ namespace ts {

export interface BaseProviderStatic {
readonly ["extension-kind"]: ExtensionKind;
new (state: {ts: typeof ts, args: any}): any;
}

export interface SyntacticLintProviderStatic extends BaseProviderStatic {
Expand Down Expand Up @@ -125,13 +124,16 @@ namespace ts {
export type SyntacticLint = "syntactic-lint";
export const LanguageService: "language-service" = "language-service";
export type LanguageService = "language-service";
export const TypeDiscovery: "type-discovery" = "type-discovery";
export type TypeDiscovery = "type-discovery";
}
export type ExtensionKind = ExtensionKind.SemanticLint | ExtensionKind.SyntacticLint | ExtensionKind.LanguageService;
export type ExtensionKind = ExtensionKind.SemanticLint | ExtensionKind.SyntacticLint | ExtensionKind.LanguageService | ExtensionKind.TypeDiscovery;

export interface ExtensionCollectionMap {
"syntactic-lint"?: SyntacticLintExtension[];
"semantic-lint"?: SemanticLintExtension[];
"language-service"?: LanguageServiceExtension[];
"type-discovery"?: TypeDiscoveryExtension[];
[index: string]: Extension[] | undefined;
}

Expand Down Expand Up @@ -163,7 +165,12 @@ namespace ts {
ctor: LanguageServiceProviderStatic;
}

export type Extension = SyntacticLintExtension | SemanticLintExtension | LanguageServiceExtension;
export interface TypeDiscoveryExtension extends ExtensionBase {
kind: ExtensionKind.TypeDiscovery;
lookup: (searchDir: string, args: any) => string[];
}

export type Extension = SyntacticLintExtension | SemanticLintExtension | LanguageServiceExtension | TypeDiscoveryExtension;

export interface ExtensionCache {
getCompilerExtensions(): ExtensionCollectionMap;
Expand Down Expand Up @@ -226,6 +233,21 @@ namespace ts {
completeProfile(/*enabled*/true, longTask);
}

function verifyType(thing: any, type: string, diagnostics: Diagnostic[], extName: string, extMember: string, extKind: ExtensionKind) {
if (typeof thing !== type) {
diagnostics.push(createCompilerDiagnostic(
Diagnostics.Extension_0_exported_member_1_has_extension_kind_2_but_was_type_3_when_type_4_was_expected,
extName,
extMember,
extKind,
typeof thing,
type
));
return false;
}
return true;
}

export function createExtensionCache(options: CompilerOptions, host: ExtensionHost, resolvedExtensionNames?: Map<string>): ExtensionCache {

const diagnostics: Diagnostic[] = [];
Expand Down Expand Up @@ -312,20 +334,18 @@ namespace ts {
switch (ext.kind) {
case ExtensionKind.SemanticLint:
case ExtensionKind.SyntacticLint:
case ExtensionKind.LanguageService:
if (typeof potentialExtension !== "function") {
diagnostics.push(createCompilerDiagnostic(
Diagnostics.Extension_0_exported_member_1_has_extension_kind_2_but_was_type_3_when_type_4_was_expected,
res.name,
key,
(ts as any).ExtensionKind[annotatedKind],
typeof potentialExtension,
"function"
));
return;
}
case ExtensionKind.LanguageService: {
const verified = verifyType(potentialExtension, "function", diagnostics, res.name, key, annotatedKind);
if (!verified) return;
(ext as (SemanticLintExtension | SyntacticLintExtension | LanguageServiceExtension)).ctor = potentialExtension as (SemanticLintProviderStatic | SyntacticLintProviderStatic | LanguageServiceProviderStatic);
break;
}
case ExtensionKind.TypeDiscovery: {
const verified = verifyType(potentialExtension, "function", diagnostics, res.name, key, annotatedKind);
if (!verified) return;
(ext as TypeDiscoveryExtension).lookup = potentialExtension as any;
break;
}
default:
// Include a default case which just puts the extension unchecked onto the base extension
// This can allow language service extensions to query for custom extension kinds
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1526,7 +1526,7 @@ namespace ts {
* ExtensionKind.SyntacticLint or ExtensionKind.SemanticLint only
*/
function performLintPassOnFile(sourceFile: SourceFile, kind: ExtensionKind): Diagnostic[] | undefined {
const lints = extensionCache.getCompilerExtensions()[kind];
const lints = extensionCache.getCompilerExtensions()[kind] as (SemanticLintExtension | SyntacticLintExtension)[];
if (!lints || !lints.length) {
return;
}
Expand Down
27 changes: 25 additions & 2 deletions src/services/jsTyping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
/* @internal */
namespace ts.JsTyping {

export interface TypingResolutionHost {
export interface TypingResolutionHost extends ExtensionHost {
directoryExists: (path: string) => boolean;
fileExists: (fileName: string) => boolean;
readFile: (path: string, encoding?: string) => string;
Expand Down Expand Up @@ -35,6 +35,7 @@ namespace ts.JsTyping {
* @param packageNameToTypingLocation is the map of package names to their cached typing locations
* @param typingOptions are used to customize the typing inference process
* @param compilerOptions are used as a source for typing inference
* @param cache is the optional extension cache to lookup type discovery extensions in - one will be made if it cannot be provided
*/
export function discoverTypings(
host: TypingResolutionHost,
Expand All @@ -43,7 +44,8 @@ namespace ts.JsTyping {
safeListPath: Path,
packageNameToTypingLocation: Map<string>,
typingOptions: TypingOptions,
compilerOptions: CompilerOptions):
compilerOptions: CompilerOptions,
cache: ExtensionCache = createExtensionCache(compilerOptions, host)):
{ cachedTypingPaths: string[], newTypingNames: string[], filesToWatch: string[] } {

// A typing name to typing file path mapping
Expand Down Expand Up @@ -88,6 +90,8 @@ namespace ts.JsTyping {

const nodeModulesPath = combinePaths(searchDir, "node_modules");
getTypingNamesFromNodeModuleFolder(nodeModulesPath);

getTypingNamesFromExtensions(searchDir, cache);
}
getTypingNamesFromSourceFileNames(fileNames);

Expand Down Expand Up @@ -223,5 +227,24 @@ namespace ts.JsTyping {
mergeTypings(typingNames);
}


function getTypingNamesFromExtensions(searchPath: string, cache: ExtensionCache) {
// We don't report issues loading type discovery extensions (if the host cares about them, it should load them in advance and pass in a cache)
const extensions = cache.getCompilerExtensions()["type-discovery"];
for (const extension of extensions) {
let results: string[];
try {
startExtensionProfile(compilerOptions.extendedDiagnostics, extension.name, "lookup");
results = extension.lookup(searchPath, extension.args);
completeExtensionProfile(compilerOptions.extendedDiagnostics, extension.name, "lookup");
}
catch (e) {
// There's presently no way to report errors during discover typings other than a hard failure (which is to be avoided)
}
if (results) {
mergeTypings(results);
}
}
}
}
}