@definitelytyped/dts-critic
Version:
Checks a new .d.ts against the Javascript source and tells you what problems it has
780 lines (686 loc) • 27.8 kB
text/typescript
import yargs = require("yargs");
import headerParser = require("@definitelytyped/header-parser");
import fs = require("fs");
import path = require("path");
import ts from "typescript";
export enum ErrorKind {
/** Declaration needs to use `export =` to match the JavaScript module's behavior. */
NeedsExportEquals = "NeedsExportEquals",
/** Declaration has a default export, but JavaScript module does not have a default export. */
NoDefaultExport = "NoDefaultExport",
/** JavaScript exports property not found in declaration exports. */
JsPropertyNotInDts = "JsPropertyNotInDts",
/** Declaration exports property not found in JavaScript exports. */
DtsPropertyNotInJs = "DtsPropertyNotInJs",
/** JavaScript module has signatures, but declaration module does not. */
JsSignatureNotInDts = "JsSignatureNotInDts",
/** Declaration module has signatures, but JavaScript module does not. */
DtsSignatureNotInJs = "DtsSignatureNotInJs",
}
export interface CheckOptions {
errors: Map<ExportErrorKind, boolean>;
}
export type ExportErrorKind = ExportError["kind"];
export function dtsCritic(
dtsPath: string,
sourcePath: string,
options: CheckOptions = { errors: new Map() },
debug = false,
): CriticError[] {
if (!sourcePath && require.main !== module) {
// dtslint will issue an error.
return [];
}
const name = findDtsName(dtsPath);
const packageJsonPath = path.join(path.dirname(path.resolve(dtsPath)), "package.json");
const header = parsePackageJson(name, JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")), path.dirname(dtsPath));
if (header === undefined) {
return [];
} else if (header.nonNpm) {
const errors: CriticError[] = [];
if (sourcePath) {
errors.push(...checkSource(name, dtsPath, sourcePath, options.errors, debug));
} else if (require.main === module) {
console.log(`Warning: declaration provided is for a non-npm package.
If you want to check the declaration against the JavaScript source code, you must provide a path to the source file.`);
}
return errors;
} else {
let sourceEntry;
if (!fs.statSync(sourcePath).isDirectory()) {
sourceEntry = sourcePath;
} else {
sourceEntry = require.resolve(path.resolve(sourcePath));
}
return checkSource(name, dtsPath, sourceEntry, options.errors, debug);
}
}
function parsePackageJson(
packageName: string,
packageJson: Record<string, unknown>,
dirPath: string,
): headerParser.Header | undefined {
const result = headerParser.validatePackageJson(packageName, packageJson, headerParser.getTypesVersions(dirPath));
if (Array.isArray(result)) console.log(result.join("\n"));
return Array.isArray(result) ? undefined : result;
}
export const defaultErrors: ExportErrorKind[] = [ErrorKind.NeedsExportEquals, ErrorKind.NoDefaultExport];
function main() {
const argv = yargs
.usage(
"$0 --dts path-to-d.ts --js path-to-source [--debug]\n\nIf source-folder is not provided, I will look for a matching package on npm.",
)
.option("dts", {
describe: "Path of declaration file to be critiqued.",
type: "string",
})
.demandOption("dts", "Please provide a path to a d.ts file for me to critique.")
.option("js", {
describe: "Path of JavaScript file to be used as source.",
type: "string",
})
.demandOption("js", "Please provide a path to a JavaScript file for me to compare the d.ts against.")
.option("debug", {
describe: "Turn debug logging on.",
type: "boolean",
default: false,
})
.help()
.parseSync();
const opts = { mode: argv.mode, errors: new Map() };
const errors = dtsCritic(argv.dts, argv.js, opts, argv.debug);
if (errors.length === 0) {
console.log("No errors!");
} else {
for (const error of errors) {
console.log("Error: " + error.message);
}
}
}
/**
* If dtsName is 'index' (as with DT) then look to the parent directory for the name.
*/
export function findDtsName(dtsPath: string) {
const resolved = path.resolve(dtsPath);
const baseName = path.basename(resolved, ".d.ts");
if (baseName && baseName !== "index") {
return baseName;
}
return path.basename(path.dirname(resolved));
}
export function checkSource(
name: string,
dtsPath: string,
srcPath: string,
enabledErrors: Map<ExportErrorKind, boolean>,
debug: boolean,
): ExportError[] {
const diagnostics = checkExports(name, dtsPath, srcPath);
if (debug) {
console.log(formatDebug(name, diagnostics));
}
return diagnostics.errors.filter((err) => enabledErrors.get(err.kind) ?? defaultErrors.includes(err.kind));
}
function formatDebug(name: string, diagnostics: ExportsDiagnostics): string {
const lines: string[] = [];
lines.push(`\tDiagnostics for package ${name}.`);
lines.push("\tInferred source module structure:");
if (isSuccess(diagnostics.jsExportKind)) {
lines.push(diagnostics.jsExportKind.result);
} else {
lines.push(`Could not infer type of JavaScript exports. Reason: ${diagnostics.jsExportKind.reason}`);
}
lines.push("\tInferred source export type:");
if (isSuccess(diagnostics.jsExportType)) {
lines.push(formatType(diagnostics.jsExportType.result));
} else {
lines.push(`Could not infer type of JavaScript exports. Reason: ${diagnostics.jsExportType.reason}`);
}
if (diagnostics.dtsExportKind) {
lines.push("\tInferred declaration module structure:");
if (isSuccess(diagnostics.dtsExportKind)) {
lines.push(diagnostics.dtsExportKind.result);
} else {
lines.push(`Could not infer type of declaration exports. Reason: ${diagnostics.dtsExportKind.reason}`);
}
}
if (diagnostics.dtsExportType) {
lines.push("\tInferred declaration export type:");
if (isSuccess(diagnostics.dtsExportType)) {
lines.push(formatType(diagnostics.dtsExportType.result));
} else {
lines.push(`Could not infer type of declaration exports. Reason: ${diagnostics.dtsExportType.reason}`);
}
}
return lines.join("\n");
}
function formatType(type: ts.Type): string {
const lines: string[] = [];
//@ts-ignore property `checker` of `ts.Type` is marked internal. The alternative is to have a TypeChecker parameter.
const checker: ts.TypeChecker = type.checker;
const properties = type.getProperties();
if (properties.length > 0) {
lines.push("Type's properties:");
lines.push(...properties.map((p) => p.getName()));
}
const signatures = type.getConstructSignatures().concat(type.getCallSignatures());
if (signatures.length > 0) {
lines.push("Type's signatures:");
lines.push(...signatures.map((s) => checker.signatureToString(s)));
}
lines.push(`Type string: ${checker.typeToString(type)}`);
return lines.join("\n");
}
const exportEqualsLink = "https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require";
/**
* Checks exports of a declaration file against its JavaScript source.
*/
function checkExports(name: string, dtsPath: string, sourcePath: string): ExportsDiagnostics {
const tscOpts = {
allowJs: true,
};
const jsProgram = ts.createProgram([sourcePath], tscOpts);
const jsFileNode = jsProgram.getSourceFile(sourcePath);
if (!jsFileNode) {
throw new Error(`TS compiler could not find source file ${sourcePath}.`);
}
const jsChecker = jsProgram.getTypeChecker();
const errors: ExportError[] = [];
const sourceDiagnostics = inspectJs(jsFileNode, jsChecker, name);
const dtsDiagnostics = inspectDts(dtsPath, name);
if (
isSuccess(sourceDiagnostics.exportEquals) &&
sourceDiagnostics.exportEquals.result.judgement === ExportEqualsJudgement.Required &&
isSuccess(dtsDiagnostics.exportKind) &&
dtsDiagnostics.exportKind.result !== DtsExportKind.ExportEquals
) {
const error = {
kind: ErrorKind.NeedsExportEquals,
message: `The declaration doesn't match the JavaScript module '${name}'. Reason:
The declaration should use 'export =' syntax because the JavaScript source uses 'module.exports =' syntax and ${sourceDiagnostics.exportEquals.result.reason}.
To learn more about 'export =' syntax, see ${exportEqualsLink}.`,
} as const;
errors.push(error);
}
const compatibility = exportTypesCompatibility(
name,
sourceDiagnostics.exportType,
dtsDiagnostics.exportType,
dtsDiagnostics.exportKind,
);
if (isSuccess(compatibility)) {
errors.push(...compatibility.result);
}
if (dtsDiagnostics.defaultExport && !sourceDiagnostics.exportsDefault) {
errors.push({
kind: ErrorKind.NoDefaultExport,
position: dtsDiagnostics.defaultExport,
message: `The declaration doesn't match the JavaScript module '${name}'. Reason:
The declaration specifies 'export default' but the JavaScript source does not mention 'default' anywhere.
The most common way to resolve this error is to use 'export =' syntax instead of 'export default'.
To learn more about 'export =' syntax, see ${exportEqualsLink}.`,
});
}
return {
jsExportKind: sourceDiagnostics.exportKind,
jsExportType: sourceDiagnostics.exportType,
dtsExportKind: dtsDiagnostics.exportKind,
dtsExportType: dtsDiagnostics.exportType,
errors,
};
}
function inspectJs(sourceFile: ts.SourceFile, checker: ts.TypeChecker, packageName: string): JsExportsInfo {
const exportKind = getJsExportKind(sourceFile);
const exportType = getJSExportType(sourceFile, checker, exportKind);
const exportsDefault = sourceExportsDefault(sourceFile, packageName);
let exportEquals;
if (isSuccess(exportType) && isSuccess(exportKind) && exportKind.result === JsExportKind.CommonJs) {
exportEquals = moduleTypeNeedsExportEquals(exportType.result, checker);
} else {
exportEquals = mergeErrors(exportType, exportKind);
}
return { exportKind, exportType, exportEquals, exportsDefault };
}
function getJsExportKind(sourceFile: ts.SourceFile): InferenceResult<JsExportKind> {
// @ts-ignore property `commonJsModuleIndicator` of `ts.SourceFile` is marked internal.
if (sourceFile.commonJsModuleIndicator) {
return inferenceSuccess(JsExportKind.CommonJs);
}
// @ts-ignore property `externalModuleIndicator` of `ts.SourceFile` is marked internal.
if (sourceFile.externalModuleIndicator) {
return inferenceSuccess(JsExportKind.ES6);
}
return inferenceError("Could not infer export kind of source file.");
}
function getJSExportType(
sourceFile: ts.SourceFile,
checker: ts.TypeChecker,
exportKind: InferenceResult<JsExportKind>,
): InferenceResult<ts.Type> {
if (isSuccess(exportKind)) {
switch (exportKind.result) {
case JsExportKind.CommonJs: {
checker.getSymbolAtLocation(sourceFile); // TODO: get symbol in a safer way?
//@ts-ignore property `symbol` of `ts.Node` is marked internal.
const fileSymbol: ts.Symbol | undefined = sourceFile.symbol;
if (!fileSymbol) {
return inferenceError(`TS compiler could not find symbol for file node '${sourceFile.fileName}'.`);
}
const exportType = checker.getTypeOfSymbolAtLocation(fileSymbol, sourceFile);
return inferenceSuccess(exportType);
}
case JsExportKind.ES6: {
const fileSymbol = checker.getSymbolAtLocation(sourceFile);
if (!fileSymbol) {
return inferenceError(`TS compiler could not find symbol for file node '${sourceFile.fileName}'.`);
}
const exportType = checker.getTypeOfSymbolAtLocation(fileSymbol, sourceFile);
return inferenceSuccess(exportType);
}
}
}
return inferenceError(`Could not infer type of exports because exports kind is undefined.`);
}
/**
* Decide if a JavaScript source module could have a default export.
*/
function sourceExportsDefault(sourceFile: ts.SourceFile, name: string): boolean {
const src = sourceFile.getFullText(sourceFile);
return (
isRealExportDefault(name) ||
src.indexOf("default") > -1 ||
src.indexOf("__esModule") > -1 ||
src.indexOf("react-side-effect") > -1 ||
src.indexOf("@flow") > -1 ||
src.indexOf("module.exports = require") > -1
);
}
function moduleTypeNeedsExportEquals(type: ts.Type, checker: ts.TypeChecker): InferenceResult<ExportEqualsDiagnostics> {
if (isBadType(type)) {
return inferenceError(`Inferred type '${checker.typeToString(type)}' is not good enough to be analyzed.`);
}
const isObject = type.getFlags() & ts.TypeFlags.Object;
// @ts-ignore property `isArrayLikeType` of `ts.TypeChecker` is marked internal.
if (isObject && !hasSignatures(type) && !checker.isArrayLikeType(type)) {
const judgement = ExportEqualsJudgement.NotRequired;
const reason = "'module.exports' is an object which is neither a function, class, or array";
return inferenceSuccess({ judgement, reason });
}
if (hasSignatures(type)) {
const judgement = ExportEqualsJudgement.Required;
const reason = "'module.exports' can be called or constructed";
return inferenceSuccess({ judgement, reason });
}
const primitive = ts.TypeFlags.Boolean | ts.TypeFlags.String | ts.TypeFlags.Number;
if (type.getFlags() & primitive) {
const judgement = ExportEqualsJudgement.Required;
const reason = `'module.exports' has primitive type ${checker.typeToString(type)}`;
return inferenceSuccess({ judgement, reason });
}
// @ts-ignore property `isArrayLikeType` of `ts.TypeChecker` is marked internal.
if (checker.isArrayLikeType(type)) {
const judgement = ExportEqualsJudgement.Required;
const reason = `'module.exports' has array-like type ${checker.typeToString(type)}`;
return inferenceSuccess({ judgement, reason });
}
return inferenceError(`Could not analyze type '${checker.typeToString(type)}'.`);
}
function hasSignatures(type: ts.Type): boolean {
return type.getCallSignatures().length > 0 || type.getConstructSignatures().length > 0;
}
function inspectDts(dtsPath: string, name: string): DtsExportDiagnostics {
dtsPath = path.resolve(dtsPath);
const program = createDtProgram(dtsPath);
const sourceFile = program.getSourceFile(path.resolve(dtsPath));
if (!sourceFile) {
throw new Error(`TS compiler could not find source file '${dtsPath}'.`);
}
const checker = program.getTypeChecker();
const symbolResult = getDtsModuleSymbol(sourceFile, checker, name);
const exportKindResult = getDtsExportKind(sourceFile);
const exportType = getDtsExportType(sourceFile, checker, symbolResult, exportKindResult);
const defaultExport = getDtsDefaultExport(sourceFile, exportType);
return { exportKind: exportKindResult, exportType, defaultExport };
}
function createDtProgram(dtsPath: string): ts.Program {
const dtsDir = path.dirname(dtsPath);
const configPath = path.join(dtsDir, "tsconfig.json");
const { config } = ts.readConfigFile(configPath, (p) => fs.readFileSync(p, { encoding: "utf8" }));
const parseConfigHost: ts.ParseConfigHost = {
fileExists: fs.existsSync,
readDirectory: ts.sys.readDirectory,
readFile: (file) => fs.readFileSync(file, { encoding: "utf8" }),
useCaseSensitiveFileNames: true,
};
const parsed = ts.parseJsonConfigFileContent(config, parseConfigHost, path.resolve(dtsDir));
const host = ts.createCompilerHost(parsed.options, true);
return ts.createProgram([path.resolve(dtsPath)], parsed.options, host);
}
function getDtsModuleSymbol(
sourceFile: ts.SourceFile,
checker: ts.TypeChecker,
name: string,
): InferenceResult<ts.Symbol> {
if (matches(sourceFile, (node) => ts.isModuleDeclaration(node))) {
const npmName = dtToNpmName(name);
const moduleSymbol = checker.getAmbientModules().find((symbol) => symbol.getName() === `"${npmName}"`);
if (moduleSymbol) {
return inferenceSuccess(moduleSymbol);
}
}
const fileSymbol = checker.getSymbolAtLocation(sourceFile);
if (fileSymbol && fileSymbol.getFlags() & ts.SymbolFlags.ValueModule) {
return inferenceSuccess(fileSymbol);
}
return inferenceError(`Could not find module symbol for source file node.`);
}
function getDtsExportKind(sourceFile: ts.SourceFile): InferenceResult<DtsExportKind> {
if (matches(sourceFile, isExportEquals)) {
return inferenceSuccess(DtsExportKind.ExportEquals);
}
if (matches(sourceFile, isExportConstruct)) {
return inferenceSuccess(DtsExportKind.ES6Like);
}
return inferenceError("Could not infer export kind of declaration file.");
}
const exportEqualsSymbolName = "export=";
function getDtsExportType(
sourceFile: ts.SourceFile,
checker: ts.TypeChecker,
symbolResult: InferenceResult<ts.Symbol>,
exportKindResult: InferenceResult<DtsExportKind>,
): InferenceResult<ts.Type> {
if (isSuccess(symbolResult) && isSuccess(exportKindResult)) {
const symbol = symbolResult.result;
const exportKind = exportKindResult.result;
switch (exportKind) {
case DtsExportKind.ExportEquals: {
const exportSymbol = symbol.exports!.get(exportEqualsSymbolName as ts.__String);
if (!exportSymbol) {
return inferenceError(`TS compiler could not find \`export=\` symbol.`);
}
const exportType = checker.getTypeOfSymbolAtLocation(exportSymbol, sourceFile);
return inferenceSuccess(exportType);
}
case DtsExportKind.ES6Like: {
const exportType = checker.getTypeOfSymbolAtLocation(symbol, sourceFile);
return inferenceSuccess(exportType);
}
}
}
return mergeErrors(symbolResult, exportKindResult);
}
/**
* Returns the position of the default export, if it exists.
*/
function getDtsDefaultExport(sourceFile: ts.SourceFile, moduleType: InferenceResult<ts.Type>): Position | undefined {
if (isError(moduleType)) {
const src = sourceFile.getFullText(sourceFile);
const exportDefault = src.indexOf("export default");
if (exportDefault > -1 && src.indexOf("export =") === -1 && !/declare module ['"]/.test(src)) {
return {
start: exportDefault,
length: "export default".length,
};
}
return undefined;
}
const exportDefault = moduleType.result.getProperty("default");
if (exportDefault?.declarations) {
return {
start: exportDefault.declarations[0].getStart(),
length: exportDefault.declarations[0].getWidth(),
};
}
return undefined;
}
const ignoredProperties = ["__esModule", "prototype", "default", "F", "G", "S", "P", "B", "W", "U", "R"];
function ignoreProperty(property: ts.Symbol): boolean {
const name = property.getName();
return name.startsWith("_") || ignoredProperties.includes(name);
}
/*
* Given the inferred type of the exports of both source and declaration, we make the following checks:
* 1. If source type has call or construct signatures, then declaration type should also have call or construct signatures.
* 2. If declaration type has call or construct signatures, then source type should also have call or construct signatures.
* 3. If source type has a property named "foo", then declaration type should also have a property named "foo".
* 4. If declaration type has a property named "foo", then source type should also have a property named "foo".
* Checks (2) and (4) don't work well in practice and should not be used for linting/verification purposes, because
* most of the times the error originates because the inferred type of the JavaScript source has missing information.
* Those checks are useful for finding examples where JavaScript type inference could be improved.
*/
function exportTypesCompatibility(
name: string,
sourceType: InferenceResult<ts.Type>,
dtsType: InferenceResult<ts.Type>,
dtsExportKind: InferenceResult<DtsExportKind>,
): InferenceResult<MissingExport[]> {
if (isError(sourceType)) {
return inferenceError("Could not get type of exports of source module.");
}
if (isError(dtsType)) {
return inferenceError("Could not get type of exports of declaration module.");
}
if (isBadType(sourceType.result)) {
return inferenceError("Could not infer meaningful type of exports of source module.");
}
if (isBadType(dtsType.result)) {
return inferenceError("Could not infer meaningful type of exports of declaration module.");
}
const errors: MissingExport[] = [];
if (hasSignatures(sourceType.result) && !hasSignatures(dtsType.result)) {
if (isSuccess(dtsExportKind) && dtsExportKind.result === DtsExportKind.ExportEquals) {
errors.push({
kind: ErrorKind.JsSignatureNotInDts,
message: `The declaration doesn't match the JavaScript module '${name}'. Reason:
The JavaScript module can be called or constructed, but the declaration module cannot.`,
});
} else {
errors.push({
kind: ErrorKind.JsSignatureNotInDts,
message: `The declaration doesn't match the JavaScript module '${name}'. Reason:
The JavaScript module can be called or constructed, but the declaration module cannot.
The most common way to resolve this error is to use 'export =' syntax.
To learn more about 'export =' syntax, see ${exportEqualsLink}.`,
});
}
}
if (hasSignatures(dtsType.result) && !hasSignatures(sourceType.result)) {
errors.push({
kind: ErrorKind.DtsSignatureNotInJs,
message: `The declaration doesn't match the JavaScript module '${name}'. Reason:
The declaration module can be called or constructed, but the JavaScript module cannot.`,
});
}
const sourceProperties = sourceType.result.getProperties();
const dtsProperties = dtsType.result.getProperties();
for (const sourceProperty of sourceProperties) {
// TODO: check `prototype` properties.
if (ignoreProperty(sourceProperty)) continue;
if (!dtsProperties.find((s) => s.getName() === sourceProperty.getName())) {
errors.push({
kind: ErrorKind.JsPropertyNotInDts,
message: `The declaration doesn't match the JavaScript module '${name}'. Reason:
The JavaScript module exports a property named '${sourceProperty.getName()}', which is missing from the declaration module.`,
});
}
}
for (const dtsProperty of dtsProperties) {
// TODO: check `prototype` properties.
if (ignoreProperty(dtsProperty)) continue;
if (!sourceProperties.find((s) => s.getName() === dtsProperty.getName())) {
const error: MissingExport = {
kind: ErrorKind.DtsPropertyNotInJs,
message: `The declaration doesn't match the JavaScript module '${name}'. Reason:
The declaration module exports a property named '${dtsProperty.getName()}', which is missing from the JavaScript module.`,
};
const declaration =
dtsProperty.declarations && dtsProperty.declarations.length > 0 ? dtsProperty.declarations[0] : undefined;
if (declaration) {
error.position = {
start: declaration.getStart(),
length: declaration.getWidth(),
};
}
errors.push(error);
}
}
return inferenceSuccess(errors);
}
function isBadType(type: ts.Type): boolean {
return !!(type.getFlags() & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Undefined | ts.TypeFlags.Null));
}
function isExportEquals(node: ts.Node): boolean {
return ts.isExportAssignment(node) && !!node.isExportEquals;
}
function isExportConstruct(node: ts.Node): boolean {
return ts.isExportAssignment(node) || ts.isExportDeclaration(node) || hasExportModifier(node);
}
function hasExportModifier(node: ts.Node): boolean {
if (ts.canHaveModifiers(node)) {
return !!ts.getModifiers(node)?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
}
return false;
}
function matches(srcFile: ts.SourceFile, predicate: (n: ts.Node) => boolean): boolean {
function matchesNode(node: ts.Node): boolean {
if (predicate(node)) return true;
const children = node.getChildren(srcFile);
for (const child of children) {
if (matchesNode(child)) return true;
}
return false;
}
return matchesNode(srcFile);
}
function isRealExportDefault(name: string) {
return name.indexOf("react-native") > -1 || name === "ember-feature-flags" || name === "material-ui-datatables";
}
/**
* Converts a package name from the name used in DT repository to the name used in npm.
* @param baseName DT name of a package
*/
export function dtToNpmName(baseName: string) {
if (/__/.test(baseName)) {
return "@" + baseName.replace("__", "/");
}
return baseName;
}
/**
* @param error case-insensitive name of the error
*/
export function parseExportErrorKind(error: string): ExportErrorKind | undefined {
error = error.toLowerCase();
switch (error) {
case "needsexportequals":
return ErrorKind.NeedsExportEquals;
case "nodefaultexport":
return ErrorKind.NoDefaultExport;
case "jspropertynotindts":
return ErrorKind.JsPropertyNotInDts;
case "dtspropertynotinjs":
return ErrorKind.DtsPropertyNotInJs;
case "jssignaturenotindts":
return ErrorKind.JsSignatureNotInDts;
case "dtssignaturenotinjs":
return ErrorKind.DtsSignatureNotInJs;
}
return undefined;
}
export interface CriticError {
kind: ErrorKind;
message: string;
position?: Position;
}
interface ExportEqualsError extends CriticError {
kind: ErrorKind.NeedsExportEquals;
}
interface DefaultExportError extends CriticError {
kind: ErrorKind.NoDefaultExport;
position: Position;
}
interface MissingExport extends CriticError {
kind:
| ErrorKind.JsPropertyNotInDts
| ErrorKind.DtsPropertyNotInJs
| ErrorKind.JsSignatureNotInDts
| ErrorKind.DtsSignatureNotInJs;
}
interface Position {
start: number;
length: number;
}
interface ExportsDiagnostics {
jsExportKind: InferenceResult<JsExportKind>;
jsExportType: InferenceResult<ts.Type>;
dtsExportKind: InferenceResult<DtsExportKind>;
dtsExportType: InferenceResult<ts.Type>;
errors: ExportError[];
}
type ExportError = ExportEqualsError | DefaultExportError | MissingExport;
interface JsExportsInfo {
exportKind: InferenceResult<JsExportKind>;
exportType: InferenceResult<ts.Type>;
exportEquals: InferenceResult<ExportEqualsDiagnostics>;
exportsDefault: boolean;
}
enum JsExportKind {
CommonJs = "CommonJs",
ES6 = "ES6",
}
interface ExportEqualsDiagnostics {
judgement: ExportEqualsJudgement;
reason: string;
}
enum ExportEqualsJudgement {
Required = "Required",
NotRequired = "Not required",
}
enum DtsExportKind {
ExportEquals = "export =",
ES6Like = "ES6-like",
}
interface DtsExportDiagnostics {
exportKind: InferenceResult<DtsExportKind>;
exportType: InferenceResult<ts.Type>;
defaultExport?: Position;
}
type InferenceResult<T> = InferenceError | InferenceSuccess<T>;
enum InferenceResultKind {
Error,
Success,
}
interface InferenceError {
kind: InferenceResultKind.Error;
reason?: string;
}
interface InferenceSuccess<T> {
kind: InferenceResultKind.Success;
result: T;
}
function inferenceError(reason?: string): InferenceError {
return { kind: InferenceResultKind.Error, reason };
}
function inferenceSuccess<T>(result: T): InferenceSuccess<T> {
return { kind: InferenceResultKind.Success, result };
}
function isSuccess<T>(inference: InferenceResult<T>): inference is InferenceSuccess<T> {
return inference.kind === InferenceResultKind.Success;
}
function isError<T>(inference: InferenceResult<T>): inference is InferenceError {
return inference.kind === InferenceResultKind.Error;
}
function mergeErrors(...results: (InferenceResult<unknown> | string)[]): InferenceError {
const reasons: string[] = [];
for (const result of results) {
if (typeof result === "string") {
reasons.push(result);
} else if (isError(result) && result.reason) {
reasons.push(result.reason);
}
}
return inferenceError(reasons.join(" "));
}
if (require.main === module) {
main();
}