mirror of
https://github.com/chenasraf/DefinitelyTyped-tools.git
synced 2026-05-18 01:49:03 +00:00
Format
This commit is contained in:
@@ -4,409 +4,443 @@ import headerParser = require("@definitelytyped/header-parser");
|
||||
import path = require("path");
|
||||
import cp = require("child_process");
|
||||
import {
|
||||
dtsCritic,
|
||||
dtToNpmName,
|
||||
getNpmInfo,
|
||||
parseExportErrorKind,
|
||||
CriticError,
|
||||
ExportErrorKind,
|
||||
Mode,
|
||||
checkSource,
|
||||
findDtsName,
|
||||
CheckOptions,
|
||||
parseMode} from "./index";
|
||||
dtsCritic,
|
||||
dtToNpmName,
|
||||
getNpmInfo,
|
||||
parseExportErrorKind,
|
||||
CriticError,
|
||||
ExportErrorKind,
|
||||
Mode,
|
||||
checkSource,
|
||||
findDtsName,
|
||||
CheckOptions,
|
||||
parseMode
|
||||
} from "./index";
|
||||
|
||||
const sourcesDir = "sources";
|
||||
const downloadsPath = path.join(sourcesDir, "dts-critic-internal/downloads.json");
|
||||
const isNpmPath = path.join(sourcesDir, "dts-critic-internal/npm.json");
|
||||
|
||||
function getPackageDownloads(dtName: string): number {
|
||||
const npmName = dtToNpmName(dtName);
|
||||
const url = `https://api.npmjs.org/downloads/point/last-month/${npmName}`;
|
||||
const result = JSON.parse(
|
||||
cp.execFileSync(
|
||||
"curl",
|
||||
["--silent", "-L", url],
|
||||
{ encoding: "utf8" })) as { downloads?: number };
|
||||
return result.downloads || 0;
|
||||
const npmName = dtToNpmName(dtName);
|
||||
const url = `https://api.npmjs.org/downloads/point/last-month/${npmName}`;
|
||||
const result = JSON.parse(cp.execFileSync("curl", ["--silent", "-L", url], { encoding: "utf8" })) as {
|
||||
downloads?: number;
|
||||
};
|
||||
return result.downloads || 0;
|
||||
}
|
||||
|
||||
interface DownloadsJson { [key: string]: number | undefined }
|
||||
interface DownloadsJson {
|
||||
[key: string]: number | undefined;
|
||||
}
|
||||
|
||||
function getAllPackageDownloads(dtPath: string): DownloadsJson {
|
||||
if (fs.existsSync(downloadsPath)) {
|
||||
return JSON.parse(fs.readFileSync(downloadsPath, { encoding: "utf8" })) as DownloadsJson;
|
||||
}
|
||||
if (fs.existsSync(downloadsPath)) {
|
||||
return JSON.parse(fs.readFileSync(downloadsPath, { encoding: "utf8" })) as DownloadsJson;
|
||||
}
|
||||
|
||||
initDir(path.dirname(downloadsPath));
|
||||
const downloads: DownloadsJson = {};
|
||||
const dtTypesPath = getDtTypesPath(dtPath);
|
||||
for (const item of fs.readdirSync(dtTypesPath)) {
|
||||
const d = getPackageDownloads(item);
|
||||
downloads[item] = d;
|
||||
}
|
||||
fs.writeFileSync(downloadsPath, JSON.stringify(downloads), { encoding: "utf8" });
|
||||
initDir(path.dirname(downloadsPath));
|
||||
const downloads: DownloadsJson = {};
|
||||
const dtTypesPath = getDtTypesPath(dtPath);
|
||||
for (const item of fs.readdirSync(dtTypesPath)) {
|
||||
const d = getPackageDownloads(item);
|
||||
downloads[item] = d;
|
||||
}
|
||||
fs.writeFileSync(downloadsPath, JSON.stringify(downloads), { encoding: "utf8" });
|
||||
|
||||
return downloads;
|
||||
return downloads;
|
||||
}
|
||||
|
||||
function initDir(path: string): void {
|
||||
if (!fs.existsSync(path)) {
|
||||
fs.mkdirSync(path);
|
||||
}
|
||||
if (!fs.existsSync(path)) {
|
||||
fs.mkdirSync(path);
|
||||
}
|
||||
}
|
||||
|
||||
function getDtTypesPath(dtBasePath: string): string {
|
||||
return path.join(dtBasePath, "types");
|
||||
return path.join(dtBasePath, "types");
|
||||
}
|
||||
|
||||
function compareDownloads(downloads: DownloadsJson, package1: string, package2: string): number {
|
||||
const count1 = downloads[package1] || 0;
|
||||
const count2 = downloads[package2] || 0;
|
||||
return count1 - count2;
|
||||
const count1 = downloads[package1] || 0;
|
||||
const count2 = downloads[package2] || 0;
|
||||
return count1 - count2;
|
||||
}
|
||||
|
||||
interface IsNpmJson { [key: string]: boolean | undefined }
|
||||
interface IsNpmJson {
|
||||
[key: string]: boolean | undefined;
|
||||
}
|
||||
|
||||
function getAllIsNpm(dtPath: string): IsNpmJson {
|
||||
if (fs.existsSync(isNpmPath)) {
|
||||
return JSON.parse(fs.readFileSync(isNpmPath, { encoding: "utf8" })) as IsNpmJson;
|
||||
}
|
||||
initDir(path.dirname(isNpmPath));
|
||||
const isNpm: IsNpmJson = {};
|
||||
const dtTypesPath = getDtTypesPath(dtPath);
|
||||
for (const item of fs.readdirSync(dtTypesPath)) {
|
||||
isNpm[item] = getNpmInfo(item).isNpm;
|
||||
}
|
||||
fs.writeFileSync(isNpmPath, JSON.stringify(isNpm), { encoding: "utf8" });
|
||||
return isNpm;
|
||||
if (fs.existsSync(isNpmPath)) {
|
||||
return JSON.parse(fs.readFileSync(isNpmPath, { encoding: "utf8" })) as IsNpmJson;
|
||||
}
|
||||
initDir(path.dirname(isNpmPath));
|
||||
const isNpm: IsNpmJson = {};
|
||||
const dtTypesPath = getDtTypesPath(dtPath);
|
||||
for (const item of fs.readdirSync(dtTypesPath)) {
|
||||
isNpm[item] = getNpmInfo(item).isNpm;
|
||||
}
|
||||
fs.writeFileSync(isNpmPath, JSON.stringify(isNpm), { encoding: "utf8" });
|
||||
return isNpm;
|
||||
}
|
||||
|
||||
function getPopularNpmPackages(count: number, dtPath: string): string[] {
|
||||
const dtPackages = getDtNpmPackages(dtPath);
|
||||
const downloads = getAllPackageDownloads(dtPath);
|
||||
dtPackages.sort((a, b) => compareDownloads(downloads, a, b));
|
||||
return dtPackages.slice(dtPackages.length - count);
|
||||
const dtPackages = getDtNpmPackages(dtPath);
|
||||
const downloads = getAllPackageDownloads(dtPath);
|
||||
dtPackages.sort((a, b) => compareDownloads(downloads, a, b));
|
||||
return dtPackages.slice(dtPackages.length - count);
|
||||
}
|
||||
|
||||
function getUnpopularNpmPackages(count: number, dtPath: string): string[] {
|
||||
const dtPackages = getDtNpmPackages(dtPath);
|
||||
const downloads = getAllPackageDownloads(dtPath);
|
||||
dtPackages.sort((a, b) => compareDownloads(downloads, a, b));
|
||||
return dtPackages.slice(0, count);
|
||||
const dtPackages = getDtNpmPackages(dtPath);
|
||||
const downloads = getAllPackageDownloads(dtPath);
|
||||
dtPackages.sort((a, b) => compareDownloads(downloads, a, b));
|
||||
return dtPackages.slice(0, count);
|
||||
}
|
||||
|
||||
function getDtNpmPackages(dtPath: string): string[] {
|
||||
const dtPackages = fs.readdirSync(getDtTypesPath(dtPath));
|
||||
const isNpmJson = getAllIsNpm(dtPath);
|
||||
return dtPackages.filter(pkg => isNpmPackage(pkg, /* header */ undefined, isNpmJson));
|
||||
const dtPackages = fs.readdirSync(getDtTypesPath(dtPath));
|
||||
const isNpmJson = getAllIsNpm(dtPath);
|
||||
return dtPackages.filter(pkg => isNpmPackage(pkg, /* header */ undefined, isNpmJson));
|
||||
}
|
||||
|
||||
function getNonNpm(args: { dtPath: string }): void {
|
||||
const nonNpm: string[] = [];
|
||||
const dtTypesPath = getDtTypesPath(args.dtPath);
|
||||
const isNpmJson = getAllIsNpm(args.dtPath);
|
||||
for (const item of fs.readdirSync(dtTypesPath)) {
|
||||
const entry = path.join(dtTypesPath, item);
|
||||
const dts = fs.readFileSync(entry + "/index.d.ts", "utf8");
|
||||
let header;
|
||||
try {
|
||||
header = headerParser.parseHeaderOrFail(dts);
|
||||
}
|
||||
catch (e) {
|
||||
header = undefined;
|
||||
}
|
||||
if (!isNpmPackage(item, header, isNpmJson)) {
|
||||
nonNpm.push(item);
|
||||
}
|
||||
const nonNpm: string[] = [];
|
||||
const dtTypesPath = getDtTypesPath(args.dtPath);
|
||||
const isNpmJson = getAllIsNpm(args.dtPath);
|
||||
for (const item of fs.readdirSync(dtTypesPath)) {
|
||||
const entry = path.join(dtTypesPath, item);
|
||||
const dts = fs.readFileSync(entry + "/index.d.ts", "utf8");
|
||||
let header;
|
||||
try {
|
||||
header = headerParser.parseHeaderOrFail(dts);
|
||||
} catch (e) {
|
||||
header = undefined;
|
||||
}
|
||||
console.log(`List of non-npm packages on DT:\n${nonNpm.map(name => `DT name: ${name}\n`).join("")}`);
|
||||
if (!isNpmPackage(item, header, isNpmJson)) {
|
||||
nonNpm.push(item);
|
||||
}
|
||||
}
|
||||
console.log(`List of non-npm packages on DT:\n${nonNpm.map(name => `DT name: ${name}\n`).join("")}`);
|
||||
}
|
||||
|
||||
interface CommonArgs {
|
||||
dtPath: string,
|
||||
mode: string,
|
||||
enableError: string[] | undefined,
|
||||
debug: boolean,
|
||||
json: boolean,
|
||||
dtPath: string;
|
||||
mode: string;
|
||||
enableError: string[] | undefined;
|
||||
debug: boolean;
|
||||
json: boolean;
|
||||
}
|
||||
|
||||
function checkAll(args: CommonArgs): void {
|
||||
const dtPackages = fs.readdirSync(getDtTypesPath(args.dtPath));
|
||||
checkPackages({ packages: dtPackages, ...args });
|
||||
const dtPackages = fs.readdirSync(getDtTypesPath(args.dtPath));
|
||||
checkPackages({ packages: dtPackages, ...args });
|
||||
}
|
||||
|
||||
function checkPopular(args: { count: number } & CommonArgs): void {
|
||||
checkPackages({ packages: getPopularNpmPackages(args.count, args.dtPath), ...args });
|
||||
checkPackages({ packages: getPopularNpmPackages(args.count, args.dtPath), ...args });
|
||||
}
|
||||
|
||||
function checkUnpopular(args: { count: number } & CommonArgs): void {
|
||||
checkPackages({ packages: getUnpopularNpmPackages(args.count, args.dtPath), ...args });
|
||||
checkPackages({ packages: getUnpopularNpmPackages(args.count, args.dtPath), ...args });
|
||||
}
|
||||
|
||||
function checkPackages(args: { packages: string[] } & CommonArgs): void {
|
||||
const results = args.packages.map(pkg => doCheck({ package: pkg, ...args }));
|
||||
printResults(results, args.json);
|
||||
const results = args.packages.map(pkg => doCheck({ package: pkg, ...args }));
|
||||
printResults(results, args.json);
|
||||
}
|
||||
|
||||
function checkPackage(args: { package: string } & CommonArgs): void {
|
||||
printResults([doCheck(args)], args.json);
|
||||
printResults([doCheck(args)], args.json);
|
||||
}
|
||||
|
||||
function doCheck(args: { package: string, dtPath: string, mode: string, enableError: string[] | undefined, debug: boolean }): Result {
|
||||
const dtPackage = args.package;
|
||||
const opts = getOptions(args.mode, args.enableError || []);
|
||||
try {
|
||||
const dtsPath = path.join(getDtTypesPath(args.dtPath), dtPackage, "index.d.ts");
|
||||
const errors = dtsCritic(dtsPath, /* sourcePath */ undefined, opts, args.debug);
|
||||
return { package: args.package, output: errors };
|
||||
}
|
||||
catch (e) {
|
||||
return { package: args.package, output: e.toString() };
|
||||
}
|
||||
function doCheck(args: {
|
||||
package: string;
|
||||
dtPath: string;
|
||||
mode: string;
|
||||
enableError: string[] | undefined;
|
||||
debug: boolean;
|
||||
}): Result {
|
||||
const dtPackage = args.package;
|
||||
const opts = getOptions(args.mode, args.enableError || []);
|
||||
try {
|
||||
const dtsPath = path.join(getDtTypesPath(args.dtPath), dtPackage, "index.d.ts");
|
||||
const errors = dtsCritic(dtsPath, /* sourcePath */ undefined, opts, args.debug);
|
||||
return { package: args.package, output: errors };
|
||||
} catch (e) {
|
||||
return { package: args.package, output: e.toString() };
|
||||
}
|
||||
}
|
||||
|
||||
function getOptions(modeArg: string, enabledErrors: string[]): CheckOptions {
|
||||
const mode = parseMode(modeArg);
|
||||
if (!mode) {
|
||||
throw new Error(`Could not find mode named '${modeArg}'.`);
|
||||
}
|
||||
switch (mode) {
|
||||
case Mode.NameOnly:
|
||||
return { mode };
|
||||
case Mode.Code:
|
||||
const errors = getEnabledErrors(enabledErrors);
|
||||
return { mode, errors };
|
||||
}
|
||||
const mode = parseMode(modeArg);
|
||||
if (!mode) {
|
||||
throw new Error(`Could not find mode named '${modeArg}'.`);
|
||||
}
|
||||
switch (mode) {
|
||||
case Mode.NameOnly:
|
||||
return { mode };
|
||||
case Mode.Code:
|
||||
const errors = getEnabledErrors(enabledErrors);
|
||||
return { mode, errors };
|
||||
}
|
||||
}
|
||||
|
||||
function getEnabledErrors(errorNames: string[]): Map<ExportErrorKind, boolean> {
|
||||
const errors: ExportErrorKind[] = [];
|
||||
for (const name of errorNames) {
|
||||
const error = parseExportErrorKind(name);
|
||||
if (error === undefined) {
|
||||
throw new Error(`Could not find error named '${name}'.`);
|
||||
}
|
||||
errors.push(error);
|
||||
const errors: ExportErrorKind[] = [];
|
||||
for (const name of errorNames) {
|
||||
const error = parseExportErrorKind(name);
|
||||
if (error === undefined) {
|
||||
throw new Error(`Could not find error named '${name}'.`);
|
||||
}
|
||||
return new Map(errors.map(err => [err, true]));
|
||||
errors.push(error);
|
||||
}
|
||||
return new Map(errors.map(err => [err, true]));
|
||||
}
|
||||
|
||||
function checkFile(args: { jsFile: string, dtsFile: string, debug: boolean }): void {
|
||||
console.log(`\tChecking JS file ${args.jsFile} and declaration file ${args.dtsFile}`);
|
||||
try {
|
||||
const errors = checkSource(findDtsName(args.dtsFile), args.dtsFile, args.jsFile, new Map(), args.debug);
|
||||
console.log(formatErrors(errors));
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
function checkFile(args: { jsFile: string; dtsFile: string; debug: boolean }): void {
|
||||
console.log(`\tChecking JS file ${args.jsFile} and declaration file ${args.dtsFile}`);
|
||||
try {
|
||||
const errors = checkSource(findDtsName(args.dtsFile), args.dtsFile, args.jsFile, new Map(), args.debug);
|
||||
console.log(formatErrors(errors));
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
interface Result {
|
||||
package: string,
|
||||
output: CriticError[] | string,
|
||||
package: string;
|
||||
output: CriticError[] | string;
|
||||
}
|
||||
|
||||
function printResults(results: Result[], json: boolean): void {
|
||||
if (json) {
|
||||
console.log(JSON.stringify(results));
|
||||
return;
|
||||
}
|
||||
if (json) {
|
||||
console.log(JSON.stringify(results));
|
||||
return;
|
||||
}
|
||||
|
||||
for (const result of results) {
|
||||
console.log(`\tChecking package ${result.package} ...`);
|
||||
if (typeof result.output === "string") {
|
||||
console.log(`Exception:\n${result.output}`);
|
||||
}
|
||||
else {
|
||||
console.log(formatErrors(result.output));
|
||||
}
|
||||
for (const result of results) {
|
||||
console.log(`\tChecking package ${result.package} ...`);
|
||||
if (typeof result.output === "string") {
|
||||
console.log(`Exception:\n${result.output}`);
|
||||
} else {
|
||||
console.log(formatErrors(result.output));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function formatErrors(errors: CriticError[]): string {
|
||||
const lines: string[] = [];
|
||||
for (const error of errors) {
|
||||
lines.push("Error: " + error.message);
|
||||
}
|
||||
if (errors.length === 0) {
|
||||
lines.push("No errors found! :)");
|
||||
}
|
||||
return lines.join("\n");
|
||||
const lines: string[] = [];
|
||||
for (const error of errors) {
|
||||
lines.push("Error: " + error.message);
|
||||
}
|
||||
if (errors.length === 0) {
|
||||
lines.push("No errors found! :)");
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function isNpmPackage(name: string, header?: headerParser.Header, isNpmJson: IsNpmJson = {}): boolean {
|
||||
if (header && header.nonNpm) return false;
|
||||
const isNpm = isNpmJson[name];
|
||||
if (isNpm !== undefined) {
|
||||
return isNpm;
|
||||
}
|
||||
return getNpmInfo(name).isNpm;
|
||||
if (header && header.nonNpm) return false;
|
||||
const isNpm = isNpmJson[name];
|
||||
if (isNpm !== undefined) {
|
||||
return isNpm;
|
||||
}
|
||||
return getNpmInfo(name).isNpm;
|
||||
}
|
||||
|
||||
function main() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
yargs
|
||||
.usage("$0 <command>")
|
||||
.command("check-all", "Check source and declaration of all DT packages that are on NPM.", {
|
||||
dtPath: {
|
||||
type: "string",
|
||||
default: "../DefinitelyTyped",
|
||||
describe: "Path of DT repository cloned locally.",
|
||||
},
|
||||
mode: {
|
||||
type: "string",
|
||||
required: true,
|
||||
choices: [Mode.NameOnly, Mode.Code],
|
||||
describe: "Mode that defines which group of checks will be made.",
|
||||
},
|
||||
enableError: {
|
||||
type: "array",
|
||||
string: true,
|
||||
describe: "Enable checking for a specific export error."
|
||||
},
|
||||
debug: {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
describe: "Turn debug logging on.",
|
||||
},
|
||||
json: {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
describe: "Format output result as json."
|
||||
},
|
||||
}, checkAll)
|
||||
.command("check-popular", "Check source and declaration of most popular DT packages that are on NPM.", {
|
||||
count: {
|
||||
alias: "c",
|
||||
type: "number",
|
||||
required: true,
|
||||
describe: "Number of packages to be checked.",
|
||||
},
|
||||
dtPath: {
|
||||
type: "string",
|
||||
default: "../DefinitelyTyped",
|
||||
describe: "Path of DT repository cloned locally.",
|
||||
},
|
||||
mode: {
|
||||
type: "string",
|
||||
required: true,
|
||||
choices: [Mode.NameOnly, Mode.Code],
|
||||
describe: "Mode that defines which group of checks will be made.",
|
||||
},
|
||||
enableError: {
|
||||
type: "array",
|
||||
string: true,
|
||||
describe: "Enable checking for a specific export error."
|
||||
},
|
||||
debug: {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
describe: "Turn debug logging on.",
|
||||
},
|
||||
json: {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
describe: "Format output result as json."
|
||||
},
|
||||
}, checkPopular)
|
||||
.command("check-unpopular", "Check source and declaration of least popular DT packages that are on NPM.", {
|
||||
count: {
|
||||
alias: "c",
|
||||
type: "number",
|
||||
required: true,
|
||||
describe: "Number of packages to be checked.",
|
||||
},
|
||||
dtPath: {
|
||||
type: "string",
|
||||
default: "../DefinitelyTyped",
|
||||
describe: "Path of DT repository cloned locally.",
|
||||
},
|
||||
mode: {
|
||||
type: "string",
|
||||
required: true,
|
||||
choices: [Mode.NameOnly, Mode.Code],
|
||||
describe: "Mode that defines which group of checks will be made.",
|
||||
},
|
||||
enableError: {
|
||||
type: "array",
|
||||
string: true,
|
||||
describe: "Enable checking for a specific export error."
|
||||
},
|
||||
debug: {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
describe: "Turn debug logging on.",
|
||||
},
|
||||
json: {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
describe: "Format output result as json."
|
||||
},
|
||||
}, checkUnpopular)
|
||||
.command("check-package", "Check source and declaration of a DT package.", {
|
||||
package: {
|
||||
alias: "p",
|
||||
type: "string",
|
||||
required: true,
|
||||
describe: "DT name of a package."
|
||||
},
|
||||
dtPath: {
|
||||
type: "string",
|
||||
default: "../DefinitelyTyped",
|
||||
describe: "Path of DT repository cloned locally.",
|
||||
},
|
||||
mode: {
|
||||
type: "string",
|
||||
required: true,
|
||||
choices: [Mode.NameOnly, Mode.Code],
|
||||
describe: "Mode that defines which group of checks will be made.",
|
||||
},
|
||||
enableError: {
|
||||
type: "array",
|
||||
string: true,
|
||||
describe: "Enable checking for a specific export error."
|
||||
},
|
||||
debug: {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
describe: "Turn debug logging on.",
|
||||
},
|
||||
json: {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
describe: "Format output result as json."
|
||||
},
|
||||
}, checkPackage)
|
||||
.command("check-file", "Check a JavaScript file and its matching declaration file.", {
|
||||
jsFile: {
|
||||
alias: "j",
|
||||
type: "string",
|
||||
required: true,
|
||||
describe: "Path of JavaScript file.",
|
||||
},
|
||||
dtsFile: {
|
||||
alias: "d",
|
||||
type: "string",
|
||||
required: true,
|
||||
describe: "Path of declaration file.",
|
||||
},
|
||||
debug: {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
describe: "Turn debug logging on.",
|
||||
},
|
||||
}, checkFile)
|
||||
.command("get-non-npm", "Get list of DT packages whose source package is not on NPM", {
|
||||
dtPath: {
|
||||
type: "string",
|
||||
default: "../DefinitelyTyped",
|
||||
describe: "Path of DT repository cloned locally.",
|
||||
},
|
||||
}, getNonNpm)
|
||||
.demandCommand(1)
|
||||
.help()
|
||||
.argv;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
yargs
|
||||
.usage("$0 <command>")
|
||||
.command(
|
||||
"check-all",
|
||||
"Check source and declaration of all DT packages that are on NPM.",
|
||||
{
|
||||
dtPath: {
|
||||
type: "string",
|
||||
default: "../DefinitelyTyped",
|
||||
describe: "Path of DT repository cloned locally."
|
||||
},
|
||||
mode: {
|
||||
type: "string",
|
||||
required: true,
|
||||
choices: [Mode.NameOnly, Mode.Code],
|
||||
describe: "Mode that defines which group of checks will be made."
|
||||
},
|
||||
enableError: {
|
||||
type: "array",
|
||||
string: true,
|
||||
describe: "Enable checking for a specific export error."
|
||||
},
|
||||
debug: {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
describe: "Turn debug logging on."
|
||||
},
|
||||
json: {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
describe: "Format output result as json."
|
||||
}
|
||||
},
|
||||
checkAll
|
||||
)
|
||||
.command(
|
||||
"check-popular",
|
||||
"Check source and declaration of most popular DT packages that are on NPM.",
|
||||
{
|
||||
count: {
|
||||
alias: "c",
|
||||
type: "number",
|
||||
required: true,
|
||||
describe: "Number of packages to be checked."
|
||||
},
|
||||
dtPath: {
|
||||
type: "string",
|
||||
default: "../DefinitelyTyped",
|
||||
describe: "Path of DT repository cloned locally."
|
||||
},
|
||||
mode: {
|
||||
type: "string",
|
||||
required: true,
|
||||
choices: [Mode.NameOnly, Mode.Code],
|
||||
describe: "Mode that defines which group of checks will be made."
|
||||
},
|
||||
enableError: {
|
||||
type: "array",
|
||||
string: true,
|
||||
describe: "Enable checking for a specific export error."
|
||||
},
|
||||
debug: {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
describe: "Turn debug logging on."
|
||||
},
|
||||
json: {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
describe: "Format output result as json."
|
||||
}
|
||||
},
|
||||
checkPopular
|
||||
)
|
||||
.command(
|
||||
"check-unpopular",
|
||||
"Check source and declaration of least popular DT packages that are on NPM.",
|
||||
{
|
||||
count: {
|
||||
alias: "c",
|
||||
type: "number",
|
||||
required: true,
|
||||
describe: "Number of packages to be checked."
|
||||
},
|
||||
dtPath: {
|
||||
type: "string",
|
||||
default: "../DefinitelyTyped",
|
||||
describe: "Path of DT repository cloned locally."
|
||||
},
|
||||
mode: {
|
||||
type: "string",
|
||||
required: true,
|
||||
choices: [Mode.NameOnly, Mode.Code],
|
||||
describe: "Mode that defines which group of checks will be made."
|
||||
},
|
||||
enableError: {
|
||||
type: "array",
|
||||
string: true,
|
||||
describe: "Enable checking for a specific export error."
|
||||
},
|
||||
debug: {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
describe: "Turn debug logging on."
|
||||
},
|
||||
json: {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
describe: "Format output result as json."
|
||||
}
|
||||
},
|
||||
checkUnpopular
|
||||
)
|
||||
.command(
|
||||
"check-package",
|
||||
"Check source and declaration of a DT package.",
|
||||
{
|
||||
package: {
|
||||
alias: "p",
|
||||
type: "string",
|
||||
required: true,
|
||||
describe: "DT name of a package."
|
||||
},
|
||||
dtPath: {
|
||||
type: "string",
|
||||
default: "../DefinitelyTyped",
|
||||
describe: "Path of DT repository cloned locally."
|
||||
},
|
||||
mode: {
|
||||
type: "string",
|
||||
required: true,
|
||||
choices: [Mode.NameOnly, Mode.Code],
|
||||
describe: "Mode that defines which group of checks will be made."
|
||||
},
|
||||
enableError: {
|
||||
type: "array",
|
||||
string: true,
|
||||
describe: "Enable checking for a specific export error."
|
||||
},
|
||||
debug: {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
describe: "Turn debug logging on."
|
||||
},
|
||||
json: {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
describe: "Format output result as json."
|
||||
}
|
||||
},
|
||||
checkPackage
|
||||
)
|
||||
.command(
|
||||
"check-file",
|
||||
"Check a JavaScript file and its matching declaration file.",
|
||||
{
|
||||
jsFile: {
|
||||
alias: "j",
|
||||
type: "string",
|
||||
required: true,
|
||||
describe: "Path of JavaScript file."
|
||||
},
|
||||
dtsFile: {
|
||||
alias: "d",
|
||||
type: "string",
|
||||
required: true,
|
||||
describe: "Path of declaration file."
|
||||
},
|
||||
debug: {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
describe: "Turn debug logging on."
|
||||
}
|
||||
},
|
||||
checkFile
|
||||
)
|
||||
.command(
|
||||
"get-non-npm",
|
||||
"Get list of DT packages whose source package is not on NPM",
|
||||
{
|
||||
dtPath: {
|
||||
type: "string",
|
||||
default: "../DefinitelyTyped",
|
||||
describe: "Path of DT repository cloned locally."
|
||||
}
|
||||
},
|
||||
getNonNpm
|
||||
)
|
||||
.demandCommand(1)
|
||||
.help().argv;
|
||||
}
|
||||
main();
|
||||
|
||||
@@ -3,77 +3,74 @@ import fs = require("fs");
|
||||
import stripJsonComments = require("strip-json-comments");
|
||||
|
||||
function hasNpmNamingLintRule(tslintPath: string): boolean {
|
||||
if (fs.existsSync(tslintPath)) {
|
||||
const tslint = JSON.parse(stripJsonComments(fs.readFileSync(tslintPath, "utf-8")));
|
||||
if(tslint.rules && tslint.rules["npm-naming"] !== undefined) {
|
||||
return !!tslint.rules["npm-naming"];
|
||||
}
|
||||
return true;
|
||||
if (fs.existsSync(tslintPath)) {
|
||||
const tslint = JSON.parse(stripJsonComments(fs.readFileSync(tslintPath, "utf-8")));
|
||||
if (tslint.rules && tslint.rules["npm-naming"] !== undefined) {
|
||||
return !!tslint.rules["npm-naming"];
|
||||
}
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function addNpmNamingLintRule(tslintPath: string): void {
|
||||
if (fs.existsSync(tslintPath)) {
|
||||
const tslint = JSON.parse(stripJsonComments(fs.readFileSync(tslintPath, "utf-8")));
|
||||
if (tslint.rules) {
|
||||
tslint.rules["npm-naming"] = false;
|
||||
}
|
||||
else {
|
||||
tslint.rules = { "npm-naming": false };
|
||||
}
|
||||
fs.writeFileSync(tslintPath, JSON.stringify(tslint, undefined, 4), "utf-8");
|
||||
if (fs.existsSync(tslintPath)) {
|
||||
const tslint = JSON.parse(stripJsonComments(fs.readFileSync(tslintPath, "utf-8")));
|
||||
if (tslint.rules) {
|
||||
tslint.rules["npm-naming"] = false;
|
||||
} else {
|
||||
tslint.rules = { "npm-naming": false };
|
||||
}
|
||||
fs.writeFileSync(tslintPath, JSON.stringify(tslint, undefined, 4), "utf-8");
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
for (const item of fs.readdirSync("../DefinitelyTyped/types")) {
|
||||
const entry = "../DefinitelyTyped/types/" + item;
|
||||
try {
|
||||
if (hasNpmNamingLintRule(entry + "/tslint.json")) {
|
||||
const errors = critic(entry + "/index.d.ts");
|
||||
for (const error of errors) {
|
||||
switch (error.kind) {
|
||||
case ErrorKind.NoMatchingNpmPackage:
|
||||
console.log(`No matching npm package found for ` + item);
|
||||
// const re = /\/\/ Type definitions for/;
|
||||
// const s = fs.readFileSync(entry + '/index.d.ts', 'utf-8')
|
||||
// fs.writeFileSync(entry + '/index.d.ts', s.replace(re, '// Type definitions for non-npm package'), 'utf-8')
|
||||
break;
|
||||
case ErrorKind.NoDefaultExport:
|
||||
console.log("converting", item, "to export = ...");
|
||||
const named = /export default function\s+(\w+\s*)\(/;
|
||||
const anon = /export default function\s*\(/;
|
||||
const id = /export default(\s+\w+);/;
|
||||
let s = fs.readFileSync(entry + "/index.d.ts", "utf-8");
|
||||
s = s.replace(named, "export = $1;\ndeclare function $1(");
|
||||
s = s.replace(anon, "export = _default;\ndeclare function _default(");
|
||||
s = s.replace(id, "export =$1;");
|
||||
fs.writeFileSync(entry + "/index.d.ts", s, "utf-8");
|
||||
break;
|
||||
case ErrorKind.NoMatchingNpmVersion:
|
||||
const m = error.message.match(/in the header, ([0-9.]+),[\s\S]to match one on npm: ([0-9., ]+)\./);
|
||||
if (m) {
|
||||
const headerver = parseFloat(m[1]);
|
||||
const npmvers = m[2].split(",").map((s: string) => parseFloat(s.trim()));
|
||||
const fixto = npmvers.every((v: number) => headerver > v) ? -1.0 : Math.max(...npmvers);
|
||||
console.log(`npm-version:${item}:${m[1]}:${m[2]}:${fixto}`);
|
||||
addNpmNamingLintRule(entry + "/tslint.json");
|
||||
}
|
||||
else {
|
||||
console.log("could not parse error message: ", error.message);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.log(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.log("*** ERROR for " + item + " ***");
|
||||
console.log(e);
|
||||
for (const item of fs.readdirSync("../DefinitelyTyped/types")) {
|
||||
const entry = "../DefinitelyTyped/types/" + item;
|
||||
try {
|
||||
if (hasNpmNamingLintRule(entry + "/tslint.json")) {
|
||||
const errors = critic(entry + "/index.d.ts");
|
||||
for (const error of errors) {
|
||||
switch (error.kind) {
|
||||
case ErrorKind.NoMatchingNpmPackage:
|
||||
console.log(`No matching npm package found for ` + item);
|
||||
// const re = /\/\/ Type definitions for/;
|
||||
// const s = fs.readFileSync(entry + '/index.d.ts', 'utf-8')
|
||||
// fs.writeFileSync(entry + '/index.d.ts', s.replace(re, '// Type definitions for non-npm package'), 'utf-8')
|
||||
break;
|
||||
case ErrorKind.NoDefaultExport:
|
||||
console.log("converting", item, "to export = ...");
|
||||
const named = /export default function\s+(\w+\s*)\(/;
|
||||
const anon = /export default function\s*\(/;
|
||||
const id = /export default(\s+\w+);/;
|
||||
let s = fs.readFileSync(entry + "/index.d.ts", "utf-8");
|
||||
s = s.replace(named, "export = $1;\ndeclare function $1(");
|
||||
s = s.replace(anon, "export = _default;\ndeclare function _default(");
|
||||
s = s.replace(id, "export =$1;");
|
||||
fs.writeFileSync(entry + "/index.d.ts", s, "utf-8");
|
||||
break;
|
||||
case ErrorKind.NoMatchingNpmVersion:
|
||||
const m = error.message.match(/in the header, ([0-9.]+),[\s\S]to match one on npm: ([0-9., ]+)\./);
|
||||
if (m) {
|
||||
const headerver = parseFloat(m[1]);
|
||||
const npmvers = m[2].split(",").map((s: string) => parseFloat(s.trim()));
|
||||
const fixto = npmvers.every((v: number) => headerver > v) ? -1.0 : Math.max(...npmvers);
|
||||
console.log(`npm-version:${item}:${m[1]}:${m[2]}:${fixto}`);
|
||||
addNpmNamingLintRule(entry + "/tslint.json");
|
||||
} else {
|
||||
console.log("could not parse error message: ", error.message);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.log(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("*** ERROR for " + item + " ***");
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
main();
|
||||
|
||||
@@ -1,229 +1,248 @@
|
||||
import {
|
||||
findDtsName,
|
||||
getNpmInfo,
|
||||
dtToNpmName,
|
||||
parseExportErrorKind,
|
||||
dtsCritic,
|
||||
checkSource,
|
||||
ErrorKind,
|
||||
ExportErrorKind } from "./index";
|
||||
findDtsName,
|
||||
getNpmInfo,
|
||||
dtToNpmName,
|
||||
parseExportErrorKind,
|
||||
dtsCritic,
|
||||
checkSource,
|
||||
ErrorKind,
|
||||
ExportErrorKind
|
||||
} from "./index";
|
||||
|
||||
function suite(description: string, tests: { [s: string]: () => void; }) {
|
||||
describe(description, () => {
|
||||
for (const k in tests) {
|
||||
test(k, tests[k], 10 * 1000);
|
||||
}
|
||||
});
|
||||
function suite(description: string, tests: { [s: string]: () => void }) {
|
||||
describe(description, () => {
|
||||
for (const k in tests) {
|
||||
test(k, tests[k], 10 * 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
suite("findDtsName", {
|
||||
absolutePath() {
|
||||
expect(findDtsName("~/dt/types/jquery/index.d.ts")).toBe("jquery");
|
||||
},
|
||||
relativePath() {
|
||||
expect(findDtsName("jquery/index.d.ts")).toBe("jquery");
|
||||
},
|
||||
currentDirectory() {
|
||||
expect(findDtsName("index.d.ts")).toBe("dts-critic");
|
||||
},
|
||||
relativeCurrentDirectory() {
|
||||
expect(findDtsName("./index.d.ts")).toBe("dts-critic");
|
||||
},
|
||||
emptyDirectory() {
|
||||
expect(findDtsName("")).toBe("dts-critic");
|
||||
},
|
||||
absolutePath() {
|
||||
expect(findDtsName("~/dt/types/jquery/index.d.ts")).toBe("jquery");
|
||||
},
|
||||
relativePath() {
|
||||
expect(findDtsName("jquery/index.d.ts")).toBe("jquery");
|
||||
},
|
||||
currentDirectory() {
|
||||
expect(findDtsName("index.d.ts")).toBe("dts-critic");
|
||||
},
|
||||
relativeCurrentDirectory() {
|
||||
expect(findDtsName("./index.d.ts")).toBe("dts-critic");
|
||||
},
|
||||
emptyDirectory() {
|
||||
expect(findDtsName("")).toBe("dts-critic");
|
||||
}
|
||||
});
|
||||
suite("getNpmInfo", {
|
||||
nonNpm() {
|
||||
expect(getNpmInfo("parseltongue")).toEqual({ isNpm: false });
|
||||
},
|
||||
npm() {
|
||||
expect(getNpmInfo("typescript")).toEqual({
|
||||
isNpm: true,
|
||||
versions: expect.arrayContaining(["3.7.5"]),
|
||||
tags: expect.objectContaining({ latest: expect.stringContaining("") }),
|
||||
});
|
||||
},
|
||||
nonNpm() {
|
||||
expect(getNpmInfo("parseltongue")).toEqual({ isNpm: false });
|
||||
},
|
||||
npm() {
|
||||
expect(getNpmInfo("typescript")).toEqual({
|
||||
isNpm: true,
|
||||
versions: expect.arrayContaining(["3.7.5"]),
|
||||
tags: expect.objectContaining({ latest: expect.stringContaining("") })
|
||||
});
|
||||
}
|
||||
});
|
||||
suite("dtToNpmName", {
|
||||
nonScoped() {
|
||||
expect(dtToNpmName("content-type")).toBe("content-type");
|
||||
},
|
||||
scoped() {
|
||||
expect(dtToNpmName("babel__core")).toBe("@babel/core");
|
||||
},
|
||||
nonScoped() {
|
||||
expect(dtToNpmName("content-type")).toBe("content-type");
|
||||
},
|
||||
scoped() {
|
||||
expect(dtToNpmName("babel__core")).toBe("@babel/core");
|
||||
}
|
||||
});
|
||||
suite("parseExportErrorKind", {
|
||||
existent() {
|
||||
expect(parseExportErrorKind("NoDefaultExport")).toBe(ErrorKind.NoDefaultExport);
|
||||
},
|
||||
existentDifferentCase() {
|
||||
expect(parseExportErrorKind("JspropertyNotinDTS")).toBe(ErrorKind.JsPropertyNotInDts);
|
||||
},
|
||||
nonexistent() {
|
||||
expect(parseExportErrorKind("FakeError")).toBe(undefined);
|
||||
}
|
||||
existent() {
|
||||
expect(parseExportErrorKind("NoDefaultExport")).toBe(ErrorKind.NoDefaultExport);
|
||||
},
|
||||
existentDifferentCase() {
|
||||
expect(parseExportErrorKind("JspropertyNotinDTS")).toBe(ErrorKind.JsPropertyNotInDts);
|
||||
},
|
||||
nonexistent() {
|
||||
expect(parseExportErrorKind("FakeError")).toBe(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
const allErrors: Map<ExportErrorKind, true> = new Map([
|
||||
[ErrorKind.NeedsExportEquals, true],
|
||||
[ErrorKind.NoDefaultExport, true],
|
||||
[ErrorKind.JsSignatureNotInDts, true],
|
||||
[ErrorKind.DtsSignatureNotInJs, true],
|
||||
[ErrorKind.DtsPropertyNotInJs, true],
|
||||
[ErrorKind.JsPropertyNotInDts, true],
|
||||
[ErrorKind.NeedsExportEquals, true],
|
||||
[ErrorKind.NoDefaultExport, true],
|
||||
[ErrorKind.JsSignatureNotInDts, true],
|
||||
[ErrorKind.DtsSignatureNotInJs, true],
|
||||
[ErrorKind.DtsPropertyNotInJs, true],
|
||||
[ErrorKind.JsPropertyNotInDts, true]
|
||||
]);
|
||||
|
||||
suite("checkSource", {
|
||||
noErrors() {
|
||||
expect(checkSource(
|
||||
"noErrors",
|
||||
"testsource/noErrors.d.ts",
|
||||
"testsource/noErrors.js",
|
||||
allErrors,
|
||||
false,
|
||||
)).toEqual([]);
|
||||
},
|
||||
missingJsProperty() {
|
||||
expect(checkSource(
|
||||
"missingJsProperty",
|
||||
"testsource/missingJsProperty.d.ts",
|
||||
"testsource/missingJsProperty.js",
|
||||
allErrors,
|
||||
false,
|
||||
)).toEqual(expect.arrayContaining([
|
||||
{
|
||||
kind: ErrorKind.JsPropertyNotInDts,
|
||||
message: `The declaration doesn't match the JavaScript module 'missingJsProperty'. Reason:
|
||||
noErrors() {
|
||||
expect(checkSource("noErrors", "testsource/noErrors.d.ts", "testsource/noErrors.js", allErrors, false)).toEqual([]);
|
||||
},
|
||||
missingJsProperty() {
|
||||
expect(
|
||||
checkSource(
|
||||
"missingJsProperty",
|
||||
"testsource/missingJsProperty.d.ts",
|
||||
"testsource/missingJsProperty.js",
|
||||
allErrors,
|
||||
false
|
||||
)
|
||||
).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
kind: ErrorKind.JsPropertyNotInDts,
|
||||
message: `The declaration doesn't match the JavaScript module 'missingJsProperty'. Reason:
|
||||
The JavaScript module exports a property named 'foo', which is missing from the declaration module.`
|
||||
}
|
||||
]));
|
||||
},
|
||||
noMissingWebpackProperty() {
|
||||
expect(checkSource(
|
||||
"missingJsProperty",
|
||||
"testsource/webpackPropertyNames.d.ts",
|
||||
"testsource/webpackPropertyNames.js",
|
||||
allErrors,
|
||||
false,
|
||||
)).toHaveLength(0);
|
||||
},
|
||||
missingDtsProperty() {
|
||||
expect(checkSource(
|
||||
"missingDtsProperty",
|
||||
"testsource/missingDtsProperty.d.ts",
|
||||
"testsource/missingDtsProperty.js",
|
||||
allErrors,
|
||||
false,
|
||||
)).toEqual(expect.arrayContaining([
|
||||
{
|
||||
kind: ErrorKind.DtsPropertyNotInJs,
|
||||
message: `The declaration doesn't match the JavaScript module 'missingDtsProperty'. Reason:
|
||||
}
|
||||
])
|
||||
);
|
||||
},
|
||||
noMissingWebpackProperty() {
|
||||
expect(
|
||||
checkSource(
|
||||
"missingJsProperty",
|
||||
"testsource/webpackPropertyNames.d.ts",
|
||||
"testsource/webpackPropertyNames.js",
|
||||
allErrors,
|
||||
false
|
||||
)
|
||||
).toHaveLength(0);
|
||||
},
|
||||
missingDtsProperty() {
|
||||
expect(
|
||||
checkSource(
|
||||
"missingDtsProperty",
|
||||
"testsource/missingDtsProperty.d.ts",
|
||||
"testsource/missingDtsProperty.js",
|
||||
allErrors,
|
||||
false
|
||||
)
|
||||
).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
kind: ErrorKind.DtsPropertyNotInJs,
|
||||
message: `The declaration doesn't match the JavaScript module 'missingDtsProperty'. Reason:
|
||||
The declaration module exports a property named 'foo', which is missing from the JavaScript module.`,
|
||||
position: {
|
||||
start: 65,
|
||||
length: 11,
|
||||
},
|
||||
}
|
||||
]));
|
||||
},
|
||||
missingDefaultExport() {
|
||||
expect(checkSource(
|
||||
"missingDefault",
|
||||
"testsource/missingDefault.d.ts",
|
||||
"testsource/missingDefault.js",
|
||||
allErrors,
|
||||
false,
|
||||
)).toEqual(expect.arrayContaining([
|
||||
{
|
||||
kind: ErrorKind.NoDefaultExport,
|
||||
message: `The declaration doesn't match the JavaScript module 'missingDefault'. Reason:
|
||||
position: {
|
||||
start: 65,
|
||||
length: 11
|
||||
}
|
||||
}
|
||||
])
|
||||
);
|
||||
},
|
||||
missingDefaultExport() {
|
||||
expect(
|
||||
checkSource("missingDefault", "testsource/missingDefault.d.ts", "testsource/missingDefault.js", allErrors, false)
|
||||
).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
kind: ErrorKind.NoDefaultExport,
|
||||
message: `The declaration doesn't match the JavaScript module 'missingDefault'. 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 https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require.`,
|
||||
position: {
|
||||
start: 0,
|
||||
length: 32,
|
||||
},
|
||||
}
|
||||
]));
|
||||
},
|
||||
missingJsSignatureExportEquals() {
|
||||
expect(checkSource(
|
||||
"missingJsSignatureExportEquals",
|
||||
"testsource/missingJsSignatureExportEquals.d.ts",
|
||||
"testsource/missingJsSignatureExportEquals.js",
|
||||
allErrors,
|
||||
false,
|
||||
)).toEqual(expect.arrayContaining([
|
||||
{
|
||||
kind: ErrorKind.JsSignatureNotInDts,
|
||||
message: `The declaration doesn't match the JavaScript module 'missingJsSignatureExportEquals'. Reason:
|
||||
The JavaScript module can be called or constructed, but the declaration module cannot.`,
|
||||
}
|
||||
]));
|
||||
},
|
||||
missingJsSignatureNoExportEquals() {
|
||||
expect(checkSource(
|
||||
"missingJsSignatureNoExportEquals",
|
||||
"testsource/missingJsSignatureNoExportEquals.d.ts",
|
||||
"testsource/missingJsSignatureNoExportEquals.js",
|
||||
allErrors,
|
||||
false,
|
||||
)).toEqual(expect.arrayContaining([
|
||||
{
|
||||
kind: ErrorKind.JsSignatureNotInDts,
|
||||
message: `The declaration doesn't match the JavaScript module 'missingJsSignatureNoExportEquals'. Reason:
|
||||
position: {
|
||||
start: 0,
|
||||
length: 32
|
||||
}
|
||||
}
|
||||
])
|
||||
);
|
||||
},
|
||||
missingJsSignatureExportEquals() {
|
||||
expect(
|
||||
checkSource(
|
||||
"missingJsSignatureExportEquals",
|
||||
"testsource/missingJsSignatureExportEquals.d.ts",
|
||||
"testsource/missingJsSignatureExportEquals.js",
|
||||
allErrors,
|
||||
false
|
||||
)
|
||||
).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
kind: ErrorKind.JsSignatureNotInDts,
|
||||
message: `The declaration doesn't match the JavaScript module 'missingJsSignatureExportEquals'. Reason:
|
||||
The JavaScript module can be called or constructed, but the declaration module cannot.`
|
||||
}
|
||||
])
|
||||
);
|
||||
},
|
||||
missingJsSignatureNoExportEquals() {
|
||||
expect(
|
||||
checkSource(
|
||||
"missingJsSignatureNoExportEquals",
|
||||
"testsource/missingJsSignatureNoExportEquals.d.ts",
|
||||
"testsource/missingJsSignatureNoExportEquals.js",
|
||||
allErrors,
|
||||
false
|
||||
)
|
||||
).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
kind: ErrorKind.JsSignatureNotInDts,
|
||||
message: `The declaration doesn't match the JavaScript module 'missingJsSignatureNoExportEquals'. 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 https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require.`,
|
||||
}
|
||||
]));
|
||||
},
|
||||
missingDtsSignature() {
|
||||
expect(checkSource(
|
||||
"missingDtsSignature",
|
||||
"testsource/missingDtsSignature.d.ts",
|
||||
"testsource/missingDtsSignature.js",
|
||||
allErrors,
|
||||
false,
|
||||
)).toEqual(expect.arrayContaining([
|
||||
{
|
||||
kind: ErrorKind.DtsSignatureNotInJs,
|
||||
message: `The declaration doesn't match the JavaScript module 'missingDtsSignature'. Reason:
|
||||
The declaration module can be called or constructed, but the JavaScript module cannot.`,
|
||||
}
|
||||
]));
|
||||
},
|
||||
missingExportEquals() {
|
||||
expect(checkSource(
|
||||
"missingExportEquals",
|
||||
"testsource/missingExportEquals.d.ts",
|
||||
"testsource/missingExportEquals.js",
|
||||
allErrors,
|
||||
false,
|
||||
)).toEqual(expect.arrayContaining([
|
||||
{
|
||||
kind: ErrorKind.NeedsExportEquals,
|
||||
message: `The declaration doesn't match the JavaScript module 'missingExportEquals'. Reason:
|
||||
To learn more about 'export =' syntax, see https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require.`
|
||||
}
|
||||
])
|
||||
);
|
||||
},
|
||||
missingDtsSignature() {
|
||||
expect(
|
||||
checkSource(
|
||||
"missingDtsSignature",
|
||||
"testsource/missingDtsSignature.d.ts",
|
||||
"testsource/missingDtsSignature.js",
|
||||
allErrors,
|
||||
false
|
||||
)
|
||||
).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
kind: ErrorKind.DtsSignatureNotInJs,
|
||||
message: `The declaration doesn't match the JavaScript module 'missingDtsSignature'. Reason:
|
||||
The declaration module can be called or constructed, but the JavaScript module cannot.`
|
||||
}
|
||||
])
|
||||
);
|
||||
},
|
||||
missingExportEquals() {
|
||||
expect(
|
||||
checkSource(
|
||||
"missingExportEquals",
|
||||
"testsource/missingExportEquals.d.ts",
|
||||
"testsource/missingExportEquals.js",
|
||||
allErrors,
|
||||
false
|
||||
)
|
||||
).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
kind: ErrorKind.NeedsExportEquals,
|
||||
message: `The declaration doesn't match the JavaScript module 'missingExportEquals'. Reason:
|
||||
The declaration should use 'export =' syntax because the JavaScript source uses 'module.exports =' syntax and 'module.exports' can be called or constructed.
|
||||
|
||||
To learn more about 'export =' syntax, see https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require.`,
|
||||
}
|
||||
]));
|
||||
},
|
||||
To learn more about 'export =' syntax, see https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require.`
|
||||
}
|
||||
])
|
||||
);
|
||||
}
|
||||
});
|
||||
suite("dtsCritic", {
|
||||
noErrors() {
|
||||
expect(dtsCritic("testsource/dts-critic.d.ts", "testsource/dts-critic.js")).toEqual([]);
|
||||
},
|
||||
noMatchingNpmPackage() {
|
||||
expect(dtsCritic("testsource/parseltongue.d.ts")).toEqual([
|
||||
{
|
||||
kind: ErrorKind.NoMatchingNpmPackage,
|
||||
message: `Declaration file must have a matching npm package.
|
||||
noErrors() {
|
||||
expect(dtsCritic("testsource/dts-critic.d.ts", "testsource/dts-critic.js")).toEqual([]);
|
||||
},
|
||||
noMatchingNpmPackage() {
|
||||
expect(dtsCritic("testsource/parseltongue.d.ts")).toEqual([
|
||||
{
|
||||
kind: ErrorKind.NoMatchingNpmPackage,
|
||||
message: `Declaration file must have a matching npm package.
|
||||
To resolve this error, either:
|
||||
1. Change the name to match an npm package.
|
||||
2. Add a Definitely Typed header with the first line
|
||||
@@ -231,29 +250,29 @@ To resolve this error, either:
|
||||
|
||||
// Type definitions for non-npm package parseltongue-browser
|
||||
|
||||
Add -browser to the end of your name to make sure it doesn't conflict with existing npm packages.`,
|
||||
},
|
||||
]);
|
||||
},
|
||||
noMatchingNpmVersion() {
|
||||
expect(dtsCritic("testsource/typescript.d.ts")).toEqual([
|
||||
{
|
||||
kind: ErrorKind.NoMatchingNpmVersion,
|
||||
message: expect.stringContaining(`The types for 'typescript' must match a version that exists on npm.
|
||||
You should copy the major and minor version from the package on npm.`),
|
||||
},
|
||||
]);
|
||||
},
|
||||
nonNpmHasMatchingPackage() {
|
||||
expect(dtsCritic("testsource/tslib.d.ts")).toEqual([
|
||||
{
|
||||
kind: ErrorKind.NonNpmHasMatchingPackage,
|
||||
message: `The non-npm package 'tslib' conflicts with the existing npm package 'tslib'.
|
||||
Add -browser to the end of your name to make sure it doesn't conflict with existing npm packages.`
|
||||
}
|
||||
]);
|
||||
},
|
||||
noMatchingNpmVersion() {
|
||||
expect(dtsCritic("testsource/typescript.d.ts")).toEqual([
|
||||
{
|
||||
kind: ErrorKind.NoMatchingNpmVersion,
|
||||
message: expect.stringContaining(`The types for 'typescript' must match a version that exists on npm.
|
||||
You should copy the major and minor version from the package on npm.`)
|
||||
}
|
||||
]);
|
||||
},
|
||||
nonNpmHasMatchingPackage() {
|
||||
expect(dtsCritic("testsource/tslib.d.ts")).toEqual([
|
||||
{
|
||||
kind: ErrorKind.NonNpmHasMatchingPackage,
|
||||
message: `The non-npm package 'tslib' conflicts with the existing npm package 'tslib'.
|
||||
Try adding -browser to the end of the name to get
|
||||
|
||||
tslib-browser
|
||||
`,
|
||||
},
|
||||
]);
|
||||
}
|
||||
`
|
||||
}
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,4 +5,4 @@
|
||||
|
||||
declare function _default(): void;
|
||||
|
||||
export = _default;
|
||||
export = _default;
|
||||
|
||||
@@ -1 +1 @@
|
||||
export default function(): void;
|
||||
export default function(): void;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export const a: () => void;
|
||||
export const b: number;
|
||||
export const foo: string;
|
||||
export const foo: string;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
interface Exports {
|
||||
(): void,
|
||||
foo: () => {},
|
||||
(): void;
|
||||
foo: () => {};
|
||||
}
|
||||
|
||||
declare const exp: Exports;
|
||||
export = exp;
|
||||
export = exp;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
interface Foo {
|
||||
bar: () => void,
|
||||
bar: () => void;
|
||||
}
|
||||
declare const foo: Foo;
|
||||
|
||||
export = foo;
|
||||
export = foo;
|
||||
|
||||
@@ -1 +1 @@
|
||||
export default function(): void;
|
||||
export default function(): void;
|
||||
|
||||
2
packages/dts-critic/testsource/noErrors.d.ts
vendored
2
packages/dts-critic/testsource/noErrors.d.ts
vendored
@@ -1,2 +1,2 @@
|
||||
export const a: number;
|
||||
export const b: string;
|
||||
export const b: string;
|
||||
|
||||
2
packages/dts-critic/testsource/tslib.d.ts
vendored
2
packages/dts-critic/testsource/tslib.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
// Type definitions for non-npm package tslib
|
||||
// Project: https://github.com/microsoft/TypeScript
|
||||
// Definitions by: TypeScript Bot <https://github.com/typescript-bot>
|
||||
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
||||
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Type definitions for typescript 1200000.5
|
||||
// Project: https://github.com/microsoft/TypeScript
|
||||
// Definitions by: TypeScript Bot <https://github.com/typescript-bot>
|
||||
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
||||
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
||||
|
||||
@@ -6,146 +6,149 @@ import { join as joinPaths } from "path";
|
||||
|
||||
import { getCompilerOptions, readJson } from "./util";
|
||||
|
||||
export async function checkPackageJson(
|
||||
dirPath: string,
|
||||
typesVersions: readonly TypeScriptVersion[],
|
||||
): Promise<void> {
|
||||
const pkgJsonPath = joinPaths(dirPath, "package.json");
|
||||
const needsTypesVersions = typesVersions.length !== 0;
|
||||
if (!await pathExists(pkgJsonPath)) {
|
||||
if (needsTypesVersions) {
|
||||
throw new Error(`${dirPath}: Must have 'package.json' for "typesVersions"`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const pkgJson = await readJson(pkgJsonPath) as Record<string, unknown>;
|
||||
|
||||
if ((pkgJson as any).private !== true) {
|
||||
throw new Error(`${pkgJsonPath} should set \`"private": true\``);
|
||||
}
|
||||
|
||||
export async function checkPackageJson(dirPath: string, typesVersions: readonly TypeScriptVersion[]): Promise<void> {
|
||||
const pkgJsonPath = joinPaths(dirPath, "package.json");
|
||||
const needsTypesVersions = typesVersions.length !== 0;
|
||||
if (!(await pathExists(pkgJsonPath))) {
|
||||
if (needsTypesVersions) {
|
||||
assert.strictEqual((pkgJson as any).types, "index", `"types" in '${pkgJsonPath}' should be "index".`);
|
||||
const expected = makeTypesVersionsForPackageJson(typesVersions);
|
||||
assert.deepEqual((pkgJson as any).typesVersions, expected,
|
||||
`"typesVersions" in '${pkgJsonPath}' is not set right. Should be: ${JSON.stringify(expected, undefined, 4)}`);
|
||||
throw new Error(`${dirPath}: Must have 'package.json' for "typesVersions"`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key in pkgJson) { // tslint:disable-line forin
|
||||
switch (key) {
|
||||
case "private":
|
||||
case "dependencies":
|
||||
case "license":
|
||||
case "imports":
|
||||
case "exports":
|
||||
case "type":
|
||||
// "private"/"typesVersions"/"types" checked above, "dependencies" / "license" checked by types-publisher,
|
||||
break;
|
||||
case "typesVersions":
|
||||
case "types":
|
||||
if (!needsTypesVersions) {
|
||||
throw new Error(`${pkgJsonPath} doesn't need to set "${key}" when no 'ts3.x' directories exist.`);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(`${pkgJsonPath} should not include field ${key}`);
|
||||
const pkgJson = (await readJson(pkgJsonPath)) as Record<string, unknown>;
|
||||
|
||||
if ((pkgJson as any).private !== true) {
|
||||
throw new Error(`${pkgJsonPath} should set \`"private": true\``);
|
||||
}
|
||||
|
||||
if (needsTypesVersions) {
|
||||
assert.strictEqual((pkgJson as any).types, "index", `"types" in '${pkgJsonPath}' should be "index".`);
|
||||
const expected = makeTypesVersionsForPackageJson(typesVersions);
|
||||
assert.deepEqual(
|
||||
(pkgJson as any).typesVersions,
|
||||
expected,
|
||||
`"typesVersions" in '${pkgJsonPath}' is not set right. Should be: ${JSON.stringify(expected, undefined, 4)}`
|
||||
);
|
||||
}
|
||||
|
||||
for (const key in pkgJson) {
|
||||
// tslint:disable-line forin
|
||||
switch (key) {
|
||||
case "private":
|
||||
case "dependencies":
|
||||
case "license":
|
||||
case "imports":
|
||||
case "exports":
|
||||
case "type":
|
||||
// "private"/"typesVersions"/"types" checked above, "dependencies" / "license" checked by types-publisher,
|
||||
break;
|
||||
case "typesVersions":
|
||||
case "types":
|
||||
if (!needsTypesVersions) {
|
||||
throw new Error(`${pkgJsonPath} doesn't need to set "${key}" when no 'ts3.x' directories exist.`);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(`${pkgJsonPath} should not include field ${key}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface DefinitelyTypedInfo {
|
||||
/** "../" or "../../" or "../../../". This should use '/' even on windows. */
|
||||
readonly relativeBaseUrl: string;
|
||||
/** "../" or "../../" or "../../../". This should use '/' even on windows. */
|
||||
readonly relativeBaseUrl: string;
|
||||
}
|
||||
export async function checkTsconfig(dirPath: string, dt: DefinitelyTypedInfo | undefined): Promise<void> {
|
||||
const options = await getCompilerOptions(dirPath);
|
||||
const options = await getCompilerOptions(dirPath);
|
||||
|
||||
if (dt) {
|
||||
const { relativeBaseUrl } = dt;
|
||||
if (dt) {
|
||||
const { relativeBaseUrl } = dt;
|
||||
|
||||
const mustHave = {
|
||||
module: "commonjs",
|
||||
noEmit: true,
|
||||
forceConsistentCasingInFileNames: true,
|
||||
baseUrl: relativeBaseUrl,
|
||||
typeRoots: [relativeBaseUrl],
|
||||
types: [],
|
||||
};
|
||||
const mustHave = {
|
||||
module: "commonjs",
|
||||
noEmit: true,
|
||||
forceConsistentCasingInFileNames: true,
|
||||
baseUrl: relativeBaseUrl,
|
||||
typeRoots: [relativeBaseUrl],
|
||||
types: []
|
||||
};
|
||||
|
||||
for (const key of Object.getOwnPropertyNames(mustHave) as (keyof typeof mustHave)[]) {
|
||||
const expected = mustHave[key];
|
||||
const actual = options[key];
|
||||
if (!deepEquals(expected, actual)) {
|
||||
throw new Error(`Expected compilerOptions[${JSON.stringify(key)}] === ${JSON.stringify(expected)}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const key in options) { // tslint:disable-line forin
|
||||
switch (key) {
|
||||
case "lib":
|
||||
case "noImplicitAny":
|
||||
case "noImplicitThis":
|
||||
case "strict":
|
||||
case "strictNullChecks":
|
||||
case "noUncheckedIndexedAccess":
|
||||
case "strictFunctionTypes":
|
||||
case "esModuleInterop":
|
||||
case "allowSyntheticDefaultImports":
|
||||
// Allow any value
|
||||
break;
|
||||
case "target":
|
||||
case "paths":
|
||||
case "jsx":
|
||||
case "jsxFactory":
|
||||
case "experimentalDecorators":
|
||||
case "noUnusedLocals":
|
||||
case "noUnusedParameters":
|
||||
// OK. "paths" checked further by types-publisher
|
||||
break;
|
||||
default:
|
||||
if (!(key in mustHave)) {
|
||||
throw new Error(`Unexpected compiler option ${key}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const key of Object.getOwnPropertyNames(mustHave) as (keyof typeof mustHave)[]) {
|
||||
const expected = mustHave[key];
|
||||
const actual = options[key];
|
||||
if (!deepEquals(expected, actual)) {
|
||||
throw new Error(`Expected compilerOptions[${JSON.stringify(key)}] === ${JSON.stringify(expected)}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!("lib" in options)) {
|
||||
throw new Error('Must specify "lib", usually to `"lib": ["es6"]` or `"lib": ["es6", "dom"]`.');
|
||||
for (const key in options) {
|
||||
// tslint:disable-line forin
|
||||
switch (key) {
|
||||
case "lib":
|
||||
case "noImplicitAny":
|
||||
case "noImplicitThis":
|
||||
case "strict":
|
||||
case "strictNullChecks":
|
||||
case "noUncheckedIndexedAccess":
|
||||
case "strictFunctionTypes":
|
||||
case "esModuleInterop":
|
||||
case "allowSyntheticDefaultImports":
|
||||
// Allow any value
|
||||
break;
|
||||
case "target":
|
||||
case "paths":
|
||||
case "jsx":
|
||||
case "jsxFactory":
|
||||
case "experimentalDecorators":
|
||||
case "noUnusedLocals":
|
||||
case "noUnusedParameters":
|
||||
// OK. "paths" checked further by types-publisher
|
||||
break;
|
||||
default:
|
||||
if (!(key in mustHave)) {
|
||||
throw new Error(`Unexpected compiler option ${key}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!("lib" in options)) {
|
||||
throw new Error('Must specify "lib", usually to `"lib": ["es6"]` or `"lib": ["es6", "dom"]`.');
|
||||
}
|
||||
|
||||
if ("strict" in options) {
|
||||
if (options.strict !== true) {
|
||||
throw new Error('When "strict" is present, it must be set to `true`.');
|
||||
}
|
||||
|
||||
if ("strict" in options) {
|
||||
if (options.strict !== true) {
|
||||
throw new Error('When "strict" is present, it must be set to `true`.');
|
||||
}
|
||||
|
||||
for (const key of ["noImplicitAny", "noImplicitThis", "strictNullChecks", "strictFunctionTypes"]) {
|
||||
if (key in options) {
|
||||
throw new TypeError(`Expected "${key}" to not be set when "strict" is \`true\`.`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const key of ["noImplicitAny", "noImplicitThis", "strictNullChecks", "strictFunctionTypes"]) {
|
||||
if (!(key in options)) {
|
||||
throw new Error(`Expected \`"${key}": true\` or \`"${key}": false\`.`);
|
||||
}
|
||||
}
|
||||
for (const key of ["noImplicitAny", "noImplicitThis", "strictNullChecks", "strictFunctionTypes"]) {
|
||||
if (key in options) {
|
||||
throw new TypeError(`Expected "${key}" to not be set when "strict" is \`true\`.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.types && options.types.length) {
|
||||
throw new Error(
|
||||
'Use `/// <reference types="..." />` directives in source files and ensure ' +
|
||||
'that the "types" field in your tsconfig is an empty array.');
|
||||
} else {
|
||||
for (const key of ["noImplicitAny", "noImplicitThis", "strictNullChecks", "strictFunctionTypes"]) {
|
||||
if (!(key in options)) {
|
||||
throw new Error(`Expected \`"${key}": true\` or \`"${key}": false\`.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (options.types && options.types.length) {
|
||||
throw new Error(
|
||||
'Use `/// <reference types="..." />` directives in source files and ensure ' +
|
||||
'that the "types" field in your tsconfig is an empty array.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function deepEquals(expected: unknown, actual: unknown): boolean {
|
||||
if (expected instanceof Array) {
|
||||
return actual instanceof Array
|
||||
&& actual.length === expected.length
|
||||
&& expected.every((e, i) => deepEquals(e, actual[i]));
|
||||
} else {
|
||||
return expected === actual;
|
||||
}
|
||||
if (expected instanceof Array) {
|
||||
return (
|
||||
actual instanceof Array && actual.length === expected.length && expected.every((e, i) => deepEquals(e, actual[i]))
|
||||
);
|
||||
} else {
|
||||
return expected === actual;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,255 +12,271 @@ import { checkTslintJson, lint, TsVersion } from "./lint";
|
||||
import { mapDefinedAsync, withoutPrefix } from "./util";
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = process.argv.slice(2);
|
||||
let dirPath = process.cwd();
|
||||
let onlyTestTsNext = false;
|
||||
let expectOnly = false;
|
||||
let shouldListen = false;
|
||||
let lookingForTsLocal = false;
|
||||
let tsLocal: string | undefined;
|
||||
const args = process.argv.slice(2);
|
||||
let dirPath = process.cwd();
|
||||
let onlyTestTsNext = false;
|
||||
let expectOnly = false;
|
||||
let shouldListen = false;
|
||||
let lookingForTsLocal = false;
|
||||
let tsLocal: string | undefined;
|
||||
|
||||
for (const arg of args) {
|
||||
if (lookingForTsLocal) {
|
||||
if (arg.startsWith("--")) {
|
||||
throw new Error("Looking for local path for TS, but got " + arg);
|
||||
}
|
||||
tsLocal = resolve(arg);
|
||||
lookingForTsLocal = false;
|
||||
continue;
|
||||
}
|
||||
switch (arg) {
|
||||
case "--installAll":
|
||||
console.log("Cleaning old installs and installing for all TypeScript versions...");
|
||||
console.log("Working...");
|
||||
await cleanTypeScriptInstalls();
|
||||
await installAllTypeScriptVersions();
|
||||
return;
|
||||
case "--localTs":
|
||||
lookingForTsLocal = true;
|
||||
break;
|
||||
case "--version":
|
||||
console.log(require("../package.json").version);
|
||||
return;
|
||||
case "--expectOnly":
|
||||
expectOnly = true;
|
||||
break;
|
||||
case "--onlyTestTsNext":
|
||||
onlyTestTsNext = true;
|
||||
break;
|
||||
// Only for use by types-publisher.
|
||||
// Listens for { path, onlyTestTsNext } messages and ouputs { path, status }.
|
||||
case "--listen":
|
||||
shouldListen = true;
|
||||
break;
|
||||
default: {
|
||||
if (arg.startsWith("--")) {
|
||||
console.error(`Unknown option '${arg}'`);
|
||||
usage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const path = arg.indexOf("@") === 0 && arg.indexOf("/") !== -1
|
||||
// we have a scoped module, e.g. @bla/foo
|
||||
// which should be converted to bla__foo
|
||||
? arg.substr(1).replace("/", "__")
|
||||
: arg;
|
||||
dirPath = joinPaths(dirPath, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const arg of args) {
|
||||
if (lookingForTsLocal) {
|
||||
throw new Error("Path for --localTs was not provided.");
|
||||
if (arg.startsWith("--")) {
|
||||
throw new Error("Looking for local path for TS, but got " + arg);
|
||||
}
|
||||
tsLocal = resolve(arg);
|
||||
lookingForTsLocal = false;
|
||||
continue;
|
||||
}
|
||||
switch (arg) {
|
||||
case "--installAll":
|
||||
console.log("Cleaning old installs and installing for all TypeScript versions...");
|
||||
console.log("Working...");
|
||||
await cleanTypeScriptInstalls();
|
||||
await installAllTypeScriptVersions();
|
||||
return;
|
||||
case "--localTs":
|
||||
lookingForTsLocal = true;
|
||||
break;
|
||||
case "--version":
|
||||
console.log(require("../package.json").version);
|
||||
return;
|
||||
case "--expectOnly":
|
||||
expectOnly = true;
|
||||
break;
|
||||
case "--onlyTestTsNext":
|
||||
onlyTestTsNext = true;
|
||||
break;
|
||||
// Only for use by types-publisher.
|
||||
// Listens for { path, onlyTestTsNext } messages and ouputs { path, status }.
|
||||
case "--listen":
|
||||
shouldListen = true;
|
||||
break;
|
||||
default: {
|
||||
if (arg.startsWith("--")) {
|
||||
console.error(`Unknown option '${arg}'`);
|
||||
usage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (shouldListen) {
|
||||
listen(dirPath, tsLocal, onlyTestTsNext);
|
||||
} else {
|
||||
await installTypeScriptAsNeeded(tsLocal, onlyTestTsNext);
|
||||
await runTests(dirPath, onlyTestTsNext, expectOnly, tsLocal);
|
||||
const path =
|
||||
arg.indexOf("@") === 0 && arg.indexOf("/") !== -1
|
||||
? // we have a scoped module, e.g. @bla/foo
|
||||
// which should be converted to bla__foo
|
||||
arg.substr(1).replace("/", "__")
|
||||
: arg;
|
||||
dirPath = joinPaths(dirPath, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lookingForTsLocal) {
|
||||
throw new Error("Path for --localTs was not provided.");
|
||||
}
|
||||
|
||||
if (shouldListen) {
|
||||
listen(dirPath, tsLocal, onlyTestTsNext);
|
||||
} else {
|
||||
await installTypeScriptAsNeeded(tsLocal, onlyTestTsNext);
|
||||
await runTests(dirPath, onlyTestTsNext, expectOnly, tsLocal);
|
||||
}
|
||||
}
|
||||
|
||||
async function installTypeScriptAsNeeded(tsLocal: string | undefined, onlyTestTsNext: boolean): Promise<void> {
|
||||
if (tsLocal) return;
|
||||
if (onlyTestTsNext) {
|
||||
return installTypeScriptNext();
|
||||
}
|
||||
return installAllTypeScriptVersions();
|
||||
if (tsLocal) return;
|
||||
if (onlyTestTsNext) {
|
||||
return installTypeScriptNext();
|
||||
}
|
||||
return installAllTypeScriptVersions();
|
||||
}
|
||||
|
||||
function usage(): void {
|
||||
console.error("Usage: dtslint [--version] [--installAll] [--onlyTestTsNext] [--expectOnly] [--localTs path]");
|
||||
console.error("Args:");
|
||||
console.error(" --version Print version and exit.");
|
||||
console.error(" --installAll Cleans and installs all TypeScript versions.");
|
||||
console.error(" --expectOnly Run only the ExpectType lint rule.");
|
||||
console.error(" --onlyTestTsNext Only run with `typescript@next`, not with the minimum version.");
|
||||
console.error(" --localTs path Run with *path* as the latest version of TS.");
|
||||
console.error("");
|
||||
console.error("onlyTestTsNext and localTs are (1) mutually exclusive and (2) test a single version of TS");
|
||||
console.error("Usage: dtslint [--version] [--installAll] [--onlyTestTsNext] [--expectOnly] [--localTs path]");
|
||||
console.error("Args:");
|
||||
console.error(" --version Print version and exit.");
|
||||
console.error(" --installAll Cleans and installs all TypeScript versions.");
|
||||
console.error(" --expectOnly Run only the ExpectType lint rule.");
|
||||
console.error(" --onlyTestTsNext Only run with `typescript@next`, not with the minimum version.");
|
||||
console.error(" --localTs path Run with *path* as the latest version of TS.");
|
||||
console.error("");
|
||||
console.error("onlyTestTsNext and localTs are (1) mutually exclusive and (2) test a single version of TS");
|
||||
}
|
||||
|
||||
function listen(dirPath: string, tsLocal: string | undefined, alwaysOnlyTestTsNext: boolean): void {
|
||||
// Don't await this here to ensure that messages sent during installation aren't dropped.
|
||||
const installationPromise = installTypeScriptAsNeeded(tsLocal, alwaysOnlyTestTsNext);
|
||||
process.on("message", async (message: unknown) => {
|
||||
const { path, onlyTestTsNext, expectOnly } = message as { path: string, onlyTestTsNext: boolean, expectOnly?: boolean };
|
||||
// Don't await this here to ensure that messages sent during installation aren't dropped.
|
||||
const installationPromise = installTypeScriptAsNeeded(tsLocal, alwaysOnlyTestTsNext);
|
||||
process.on("message", async (message: unknown) => {
|
||||
const { path, onlyTestTsNext, expectOnly } = message as {
|
||||
path: string;
|
||||
onlyTestTsNext: boolean;
|
||||
expectOnly?: boolean;
|
||||
};
|
||||
|
||||
await installationPromise;
|
||||
runTests(joinPaths(dirPath, path), onlyTestTsNext, !!expectOnly, tsLocal)
|
||||
.catch(e => e.stack)
|
||||
.then(maybeError => {
|
||||
process.send!({ path, status: maybeError === undefined ? "OK" : maybeError });
|
||||
})
|
||||
.catch(e => console.error(e.stack));
|
||||
});
|
||||
await installationPromise;
|
||||
runTests(joinPaths(dirPath, path), onlyTestTsNext, !!expectOnly, tsLocal)
|
||||
.catch(e => e.stack)
|
||||
.then(maybeError => {
|
||||
process.send!({ path, status: maybeError === undefined ? "OK" : maybeError });
|
||||
})
|
||||
.catch(e => console.error(e.stack));
|
||||
});
|
||||
}
|
||||
|
||||
async function runTests(
|
||||
dirPath: string,
|
||||
onlyTestTsNext: boolean,
|
||||
expectOnly: boolean,
|
||||
tsLocal: string | undefined,
|
||||
dirPath: string,
|
||||
onlyTestTsNext: boolean,
|
||||
expectOnly: boolean,
|
||||
tsLocal: string | undefined
|
||||
): Promise<void> {
|
||||
const isOlderVersion = /^v(0\.)?\d+$/.test(basename(dirPath));
|
||||
const isOlderVersion = /^v(0\.)?\d+$/.test(basename(dirPath));
|
||||
|
||||
const indexText = await readFile(joinPaths(dirPath, "index.d.ts"), "utf-8");
|
||||
// If this *is* on DefinitelyTyped, types-publisher will fail if it can't parse the header.
|
||||
const dt = indexText.includes("// Type definitions for");
|
||||
if (dt) {
|
||||
// Someone may have copied text from DefinitelyTyped to their type definition and included a header,
|
||||
// so assert that we're really on DefinitelyTyped.
|
||||
assertPathIsInDefinitelyTyped(dirPath);
|
||||
assertPathIsNotBanned(dirPath);
|
||||
const indexText = await readFile(joinPaths(dirPath, "index.d.ts"), "utf-8");
|
||||
// If this *is* on DefinitelyTyped, types-publisher will fail if it can't parse the header.
|
||||
const dt = indexText.includes("// Type definitions for");
|
||||
if (dt) {
|
||||
// Someone may have copied text from DefinitelyTyped to their type definition and included a header,
|
||||
// so assert that we're really on DefinitelyTyped.
|
||||
assertPathIsInDefinitelyTyped(dirPath);
|
||||
assertPathIsNotBanned(dirPath);
|
||||
}
|
||||
|
||||
const typesVersions = await mapDefinedAsync(await readdir(dirPath), async name => {
|
||||
if (name === "tsconfig.json" || name === "tslint.json" || name === "tsutils") {
|
||||
return undefined;
|
||||
}
|
||||
const version = withoutPrefix(name, "ts");
|
||||
if (version === undefined || !(await stat(joinPaths(dirPath, name))).isDirectory()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const typesVersions = await mapDefinedAsync(await readdir(dirPath), async name => {
|
||||
if (name === "tsconfig.json" || name === "tslint.json" || name === "tsutils") { return undefined; }
|
||||
const version = withoutPrefix(name, "ts");
|
||||
if (version === undefined || !(await stat(joinPaths(dirPath, name))).isDirectory()) { return undefined; }
|
||||
|
||||
if (!TypeScriptVersion.isTypeScriptVersion(version)) {
|
||||
throw new Error(`There is an entry named ${name}, but ${version} is not a valid TypeScript version.`);
|
||||
}
|
||||
if (!TypeScriptVersion.isRedirectable(version)) {
|
||||
throw new Error(`At ${dirPath}/${name}: TypeScript version directories only available starting with ts3.1.`);
|
||||
}
|
||||
return version;
|
||||
});
|
||||
|
||||
if (dt) {
|
||||
await checkPackageJson(dirPath, typesVersions);
|
||||
if (!TypeScriptVersion.isTypeScriptVersion(version)) {
|
||||
throw new Error(`There is an entry named ${name}, but ${version} is not a valid TypeScript version.`);
|
||||
}
|
||||
|
||||
const minVersion = maxVersion(
|
||||
getMinimumTypeScriptVersionFromComment(indexText),
|
||||
TypeScriptVersion.lowest) as TypeScriptVersion;
|
||||
if (onlyTestTsNext || tsLocal) {
|
||||
const tsVersion = tsLocal ? "local" : TypeScriptVersion.latest;
|
||||
await testTypesVersion(dirPath, tsVersion, tsVersion, isOlderVersion, dt, expectOnly, tsLocal, /*isLatest*/ true);
|
||||
} else {
|
||||
// For example, typesVersions of [3.2, 3.5, 3.6] will have
|
||||
// associated ts3.2, ts3.5, ts3.6 directories, for
|
||||
// <=3.2, <=3.5, <=3.6 respectively; the root level is for 3.7 and above.
|
||||
// so this code needs to generate ranges [lowest-3.2, 3.3-3.5, 3.6-3.6, 3.7-latest]
|
||||
const lows = [TypeScriptVersion.lowest, ...typesVersions.map(next)];
|
||||
const his = [...typesVersions, TypeScriptVersion.latest];
|
||||
assert.strictEqual(lows.length, his.length);
|
||||
for (let i = 0; i < lows.length; i++) {
|
||||
const low = maxVersion(minVersion, lows[i]);
|
||||
const hi = his[i];
|
||||
assert(
|
||||
parseFloat(hi) >= parseFloat(low),
|
||||
`'// Minimum TypeScript Version: ${minVersion}' in header skips ts${hi} folder.`);
|
||||
const isLatest = hi === TypeScriptVersion.latest;
|
||||
const versionPath = isLatest ? dirPath : joinPaths(dirPath, `ts${hi}`);
|
||||
if (lows.length > 1) {
|
||||
console.log("testing from", low, "to", hi, "in", versionPath);
|
||||
}
|
||||
await testTypesVersion(versionPath, low, hi, isOlderVersion, dt, expectOnly, undefined, isLatest);
|
||||
}
|
||||
if (!TypeScriptVersion.isRedirectable(version)) {
|
||||
throw new Error(`At ${dirPath}/${name}: TypeScript version directories only available starting with ts3.1.`);
|
||||
}
|
||||
return version;
|
||||
});
|
||||
|
||||
if (dt) {
|
||||
await checkPackageJson(dirPath, typesVersions);
|
||||
}
|
||||
|
||||
const minVersion = maxVersion(
|
||||
getMinimumTypeScriptVersionFromComment(indexText),
|
||||
TypeScriptVersion.lowest
|
||||
) as TypeScriptVersion;
|
||||
if (onlyTestTsNext || tsLocal) {
|
||||
const tsVersion = tsLocal ? "local" : TypeScriptVersion.latest;
|
||||
await testTypesVersion(dirPath, tsVersion, tsVersion, isOlderVersion, dt, expectOnly, tsLocal, /*isLatest*/ true);
|
||||
} else {
|
||||
// For example, typesVersions of [3.2, 3.5, 3.6] will have
|
||||
// associated ts3.2, ts3.5, ts3.6 directories, for
|
||||
// <=3.2, <=3.5, <=3.6 respectively; the root level is for 3.7 and above.
|
||||
// so this code needs to generate ranges [lowest-3.2, 3.3-3.5, 3.6-3.6, 3.7-latest]
|
||||
const lows = [TypeScriptVersion.lowest, ...typesVersions.map(next)];
|
||||
const his = [...typesVersions, TypeScriptVersion.latest];
|
||||
assert.strictEqual(lows.length, his.length);
|
||||
for (let i = 0; i < lows.length; i++) {
|
||||
const low = maxVersion(minVersion, lows[i]);
|
||||
const hi = his[i];
|
||||
assert(
|
||||
parseFloat(hi) >= parseFloat(low),
|
||||
`'// Minimum TypeScript Version: ${minVersion}' in header skips ts${hi} folder.`
|
||||
);
|
||||
const isLatest = hi === TypeScriptVersion.latest;
|
||||
const versionPath = isLatest ? dirPath : joinPaths(dirPath, `ts${hi}`);
|
||||
if (lows.length > 1) {
|
||||
console.log("testing from", low, "to", hi, "in", versionPath);
|
||||
}
|
||||
await testTypesVersion(versionPath, low, hi, isOlderVersion, dt, expectOnly, undefined, isLatest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function maxVersion(v1: TypeScriptVersion | undefined, v2: TypeScriptVersion): TypeScriptVersion;
|
||||
function maxVersion(v1: AllTypeScriptVersion | undefined, v2: AllTypeScriptVersion): AllTypeScriptVersion;
|
||||
function maxVersion(v1: AllTypeScriptVersion | undefined, v2: AllTypeScriptVersion) {
|
||||
if (!v1) return v2;
|
||||
if (!v2) return v1;
|
||||
if (parseFloat(v1) >= parseFloat(v2)) return v1;
|
||||
return v2;
|
||||
if (!v1) return v2;
|
||||
if (!v2) return v1;
|
||||
if (parseFloat(v1) >= parseFloat(v2)) return v1;
|
||||
return v2;
|
||||
}
|
||||
|
||||
function next(v: TypeScriptVersion): TypeScriptVersion {
|
||||
const index = TypeScriptVersion.supported.indexOf(v);
|
||||
assert.notStrictEqual(index, -1);
|
||||
assert(index < TypeScriptVersion.supported.length);
|
||||
return TypeScriptVersion.supported[index + 1];
|
||||
const index = TypeScriptVersion.supported.indexOf(v);
|
||||
assert.notStrictEqual(index, -1);
|
||||
assert(index < TypeScriptVersion.supported.length);
|
||||
return TypeScriptVersion.supported[index + 1];
|
||||
}
|
||||
|
||||
async function testTypesVersion(
|
||||
dirPath: string,
|
||||
lowVersion: TsVersion,
|
||||
hiVersion: TsVersion,
|
||||
isOlderVersion: boolean,
|
||||
dt: boolean,
|
||||
expectOnly: boolean,
|
||||
tsLocal: string | undefined,
|
||||
isLatest: boolean,
|
||||
dirPath: string,
|
||||
lowVersion: TsVersion,
|
||||
hiVersion: TsVersion,
|
||||
isOlderVersion: boolean,
|
||||
dt: boolean,
|
||||
expectOnly: boolean,
|
||||
tsLocal: string | undefined,
|
||||
isLatest: boolean
|
||||
): Promise<void> {
|
||||
await checkTslintJson(dirPath, dt);
|
||||
await checkTsconfig(dirPath, dt
|
||||
? { relativeBaseUrl: ".." + (isOlderVersion ? "/.." : "") + (isLatest ? "" : "/..") + "/" }
|
||||
: undefined);
|
||||
const err = await lint(dirPath, lowVersion, hiVersion, isLatest, expectOnly, tsLocal);
|
||||
if (err) {
|
||||
throw new Error(err);
|
||||
}
|
||||
await checkTslintJson(dirPath, dt);
|
||||
await checkTsconfig(
|
||||
dirPath,
|
||||
dt ? { relativeBaseUrl: ".." + (isOlderVersion ? "/.." : "") + (isLatest ? "" : "/..") + "/" } : undefined
|
||||
);
|
||||
const err = await lint(dirPath, lowVersion, hiVersion, isLatest, expectOnly, tsLocal);
|
||||
if (err) {
|
||||
throw new Error(err);
|
||||
}
|
||||
}
|
||||
|
||||
function assertPathIsInDefinitelyTyped(dirPath: string): void {
|
||||
const parent = dirname(dirPath);
|
||||
const types = /^v\d+(\.\d+)?$/.test(basename(dirPath)) ? dirname(parent) : parent;
|
||||
// TODO: It's not clear whether this assertion makes sense, and it's broken on Azure Pipelines
|
||||
// Re-enable it later if it makes sense.
|
||||
// const dt = dirname(types);
|
||||
// if (basename(dt) !== "DefinitelyTyped" || basename(types) !== "types") {
|
||||
if (basename(types) !== "types") {
|
||||
throw new Error("Since this type definition includes a header (a comment starting with `// Type definitions for`), "
|
||||
+ "assumed this was a DefinitelyTyped package.\n"
|
||||
+ "But it is not in a `DefinitelyTyped/types/xxx` directory: "
|
||||
+ dirPath);
|
||||
}
|
||||
const parent = dirname(dirPath);
|
||||
const types = /^v\d+(\.\d+)?$/.test(basename(dirPath)) ? dirname(parent) : parent;
|
||||
// TODO: It's not clear whether this assertion makes sense, and it's broken on Azure Pipelines
|
||||
// Re-enable it later if it makes sense.
|
||||
// const dt = dirname(types);
|
||||
// if (basename(dt) !== "DefinitelyTyped" || basename(types) !== "types") {
|
||||
if (basename(types) !== "types") {
|
||||
throw new Error(
|
||||
"Since this type definition includes a header (a comment starting with `// Type definitions for`), " +
|
||||
"assumed this was a DefinitelyTyped package.\n" +
|
||||
"But it is not in a `DefinitelyTyped/types/xxx` directory: " +
|
||||
dirPath
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function assertPathIsNotBanned(dirPath: string) {
|
||||
const basedir = basename(dirPath);
|
||||
if (/(^|\W)download($|\W)/.test(basedir) &&
|
||||
basedir !== "download" &&
|
||||
basedir !== "downloadjs" &&
|
||||
basedir !== "s3-download-stream") {
|
||||
// Since npm won't release their banned-words list, we'll have to manually add to this list.
|
||||
throw new Error(`${dirPath}: Contains the word 'download', which is banned by npm.`);
|
||||
}
|
||||
const basedir = basename(dirPath);
|
||||
if (
|
||||
/(^|\W)download($|\W)/.test(basedir) &&
|
||||
basedir !== "download" &&
|
||||
basedir !== "downloadjs" &&
|
||||
basedir !== "s3-download-stream"
|
||||
) {
|
||||
// Since npm won't release their banned-words list, we'll have to manually add to this list.
|
||||
throw new Error(`${dirPath}: Contains the word 'download', which is banned by npm.`);
|
||||
}
|
||||
}
|
||||
|
||||
function getMinimumTypeScriptVersionFromComment(text: string): AllTypeScriptVersion | undefined {
|
||||
const match = text.match(/\/\/ (?:Minimum )?TypeScript Version: /);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
const match = text.match(/\/\/ (?:Minimum )?TypeScript Version: /);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let line = text.slice(match.index, text.indexOf("\n", match.index));
|
||||
if (line.endsWith("\r")) {
|
||||
line = line.slice(0, line.length - 1);
|
||||
}
|
||||
return parseTypeScriptVersionLine(line);
|
||||
let line = text.slice(match.index, text.indexOf("\n", match.index));
|
||||
if (line.endsWith("\r")) {
|
||||
line = line.slice(0, line.length - 1);
|
||||
}
|
||||
return parseTypeScriptVersionLine(line);
|
||||
}
|
||||
|
||||
if (!module.parent) {
|
||||
main().catch(err => {
|
||||
console.error(err.stack);
|
||||
process.exit(1);
|
||||
});
|
||||
main().catch(err => {
|
||||
console.error(err.stack);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,81 +13,91 @@ import { getProgram, Options as ExpectOptions } from "./rules/expectRule";
|
||||
import { readJson, withoutPrefix } from "./util";
|
||||
|
||||
export async function lint(
|
||||
dirPath: string,
|
||||
minVersion: TsVersion,
|
||||
maxVersion: TsVersion,
|
||||
isLatest: boolean,
|
||||
expectOnly: boolean,
|
||||
tsLocal: string | undefined): Promise<string | undefined> {
|
||||
const tsconfigPath = joinPaths(dirPath, "tsconfig.json");
|
||||
const lintProgram = Linter.createProgram(tsconfigPath);
|
||||
dirPath: string,
|
||||
minVersion: TsVersion,
|
||||
maxVersion: TsVersion,
|
||||
isLatest: boolean,
|
||||
expectOnly: boolean,
|
||||
tsLocal: string | undefined
|
||||
): Promise<string | undefined> {
|
||||
const tsconfigPath = joinPaths(dirPath, "tsconfig.json");
|
||||
const lintProgram = Linter.createProgram(tsconfigPath);
|
||||
|
||||
for (const version of [maxVersion, minVersion]) {
|
||||
const errors = testDependencies(version, dirPath, lintProgram, tsLocal);
|
||||
if (errors) { return errors; }
|
||||
for (const version of [maxVersion, minVersion]) {
|
||||
const errors = testDependencies(version, dirPath, lintProgram, tsLocal);
|
||||
if (errors) {
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
|
||||
const lintOptions: ILinterOptions = {
|
||||
fix: false,
|
||||
formatter: "stylish"
|
||||
};
|
||||
const linter = new Linter(lintOptions, lintProgram);
|
||||
const configPath = expectOnly ? joinPaths(__dirname, "..", "dtslint-expect-only.json") : getConfigPath(dirPath);
|
||||
const config = await getLintConfig(configPath, tsconfigPath, minVersion, maxVersion, tsLocal);
|
||||
|
||||
for (const file of lintProgram.getSourceFiles()) {
|
||||
if (lintProgram.isSourceFileDefaultLibrary(file)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const lintOptions: ILinterOptions = {
|
||||
fix: false,
|
||||
formatter: "stylish",
|
||||
};
|
||||
const linter = new Linter(lintOptions, lintProgram);
|
||||
const configPath = expectOnly ? joinPaths(__dirname, "..", "dtslint-expect-only.json") : getConfigPath(dirPath);
|
||||
const config = await getLintConfig(configPath, tsconfigPath, minVersion, maxVersion, tsLocal);
|
||||
|
||||
for (const file of lintProgram.getSourceFiles()) {
|
||||
if (lintProgram.isSourceFileDefaultLibrary(file)) { continue; }
|
||||
|
||||
const { fileName, text } = file;
|
||||
if (!fileName.includes("node_modules")) {
|
||||
const err = testNoTsIgnore(text) || testNoTslintDisables(text);
|
||||
if (err) {
|
||||
const { pos, message } = err;
|
||||
const place = file.getLineAndCharacterOfPosition(pos);
|
||||
return `At ${fileName}:${JSON.stringify(place)}: ${message}`;
|
||||
}
|
||||
}
|
||||
|
||||
// External dependencies should have been handled by `testDependencies`;
|
||||
// typesVersions should be handled in a separate lint
|
||||
if (!isExternalDependency(file, dirPath, lintProgram) &&
|
||||
(!isLatest || !isTypesVersionPath(fileName, dirPath))) {
|
||||
linter.lint(fileName, text, config);
|
||||
}
|
||||
const { fileName, text } = file;
|
||||
if (!fileName.includes("node_modules")) {
|
||||
const err = testNoTsIgnore(text) || testNoTslintDisables(text);
|
||||
if (err) {
|
||||
const { pos, message } = err;
|
||||
const place = file.getLineAndCharacterOfPosition(pos);
|
||||
return `At ${fileName}:${JSON.stringify(place)}: ${message}`;
|
||||
}
|
||||
}
|
||||
|
||||
const result = linter.getResult();
|
||||
return result.failures.length ? result.output : undefined;
|
||||
// External dependencies should have been handled by `testDependencies`;
|
||||
// typesVersions should be handled in a separate lint
|
||||
if (!isExternalDependency(file, dirPath, lintProgram) && (!isLatest || !isTypesVersionPath(fileName, dirPath))) {
|
||||
linter.lint(fileName, text, config);
|
||||
}
|
||||
}
|
||||
|
||||
const result = linter.getResult();
|
||||
return result.failures.length ? result.output : undefined;
|
||||
}
|
||||
|
||||
function testDependencies(
|
||||
version: TsVersion,
|
||||
dirPath: string,
|
||||
lintProgram: TsType.Program,
|
||||
tsLocal: string | undefined,
|
||||
version: TsVersion,
|
||||
dirPath: string,
|
||||
lintProgram: TsType.Program,
|
||||
tsLocal: string | undefined
|
||||
): string | undefined {
|
||||
const tsconfigPath = joinPaths(dirPath, "tsconfig.json");
|
||||
assert(version !== "local" || tsLocal);
|
||||
const ts: typeof TsType = require(typeScriptPath(version, tsLocal));
|
||||
const program = getProgram(tsconfigPath, ts, version, lintProgram);
|
||||
const diagnostics = ts.getPreEmitDiagnostics(program).filter(d => !d.file || isExternalDependency(d.file, dirPath, program));
|
||||
if (!diagnostics.length) { return undefined; }
|
||||
const tsconfigPath = joinPaths(dirPath, "tsconfig.json");
|
||||
assert(version !== "local" || tsLocal);
|
||||
const ts: typeof TsType = require(typeScriptPath(version, tsLocal));
|
||||
const program = getProgram(tsconfigPath, ts, version, lintProgram);
|
||||
const diagnostics = ts
|
||||
.getPreEmitDiagnostics(program)
|
||||
.filter(d => !d.file || isExternalDependency(d.file, dirPath, program));
|
||||
if (!diagnostics.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const showDiags = ts.formatDiagnostics(diagnostics, {
|
||||
getCanonicalFileName: f => f,
|
||||
getCurrentDirectory: () => dirPath,
|
||||
getNewLine: () => "\n",
|
||||
});
|
||||
const showDiags = ts.formatDiagnostics(diagnostics, {
|
||||
getCanonicalFileName: f => f,
|
||||
getCurrentDirectory: () => dirPath,
|
||||
getNewLine: () => "\n"
|
||||
});
|
||||
|
||||
const message = `Errors in typescript@${version} for external dependencies:\n${showDiags}`;
|
||||
const message = `Errors in typescript@${version} for external dependencies:\n${showDiags}`;
|
||||
|
||||
// Add an edge-case for someone needing to `npm install` in react when they first edit a DT module which depends on it - #226
|
||||
const cannotFindDepsDiags = diagnostics.find(d => d.code === 2307 && d.messageText.toString().includes("Cannot find module"));
|
||||
if (cannotFindDepsDiags && cannotFindDepsDiags.file) {
|
||||
const path = cannotFindDepsDiags.file.fileName;
|
||||
const typesFolder = dirname(path);
|
||||
// Add an edge-case for someone needing to `npm install` in react when they first edit a DT module which depends on it - #226
|
||||
const cannotFindDepsDiags = diagnostics.find(
|
||||
d => d.code === 2307 && d.messageText.toString().includes("Cannot find module")
|
||||
);
|
||||
if (cannotFindDepsDiags && cannotFindDepsDiags.file) {
|
||||
const path = cannotFindDepsDiags.file.fileName;
|
||||
const typesFolder = dirname(path);
|
||||
|
||||
return `
|
||||
return `
|
||||
A module look-up failed, this often occurs when you need to run \`npm install\` on a dependent module before you can lint.
|
||||
|
||||
Before you debug, first try running:
|
||||
@@ -97,133 +107,140 @@ Before you debug, first try running:
|
||||
Then re-run. Full error logs are below.
|
||||
|
||||
${message}`;
|
||||
} else {
|
||||
return message;
|
||||
}
|
||||
} else {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
export function isExternalDependency(file: TsType.SourceFile, dirPath: string, program: TsType.Program): boolean {
|
||||
return !startsWithDirectory(file.fileName, dirPath) || program.isSourceFileFromExternalLibrary(file);
|
||||
return !startsWithDirectory(file.fileName, dirPath) || program.isSourceFileFromExternalLibrary(file);
|
||||
}
|
||||
|
||||
function normalizePath(file: string) {
|
||||
// replaces '\' with '/' and forces all DOS drive letters to be upper-case
|
||||
return normalize(file)
|
||||
.replace(/\\/g, "/")
|
||||
.replace(/^[a-z](?=:)/, c => c.toUpperCase());
|
||||
// replaces '\' with '/' and forces all DOS drive letters to be upper-case
|
||||
return normalize(file)
|
||||
.replace(/\\/g, "/")
|
||||
.replace(/^[a-z](?=:)/, c => c.toUpperCase());
|
||||
}
|
||||
|
||||
function isTypesVersionPath(fileName: string, dirPath: string) {
|
||||
const normalFileName = normalizePath(fileName);
|
||||
const normalDirPath = normalizePath(dirPath);
|
||||
const subdirPath = withoutPrefix(normalFileName, normalDirPath);
|
||||
return subdirPath && /^\/ts\d+\.\d/.test(subdirPath);
|
||||
const normalFileName = normalizePath(fileName);
|
||||
const normalDirPath = normalizePath(dirPath);
|
||||
const subdirPath = withoutPrefix(normalFileName, normalDirPath);
|
||||
return subdirPath && /^\/ts\d+\.\d/.test(subdirPath);
|
||||
}
|
||||
|
||||
function startsWithDirectory(filePath: string, dirPath: string): boolean {
|
||||
const normalFilePath = normalizePath(filePath);
|
||||
const normalDirPath = normalizePath(dirPath).replace(/\/$/, "");
|
||||
return normalFilePath.startsWith(normalDirPath + "/") || normalFilePath.startsWith(normalDirPath + "\\");
|
||||
const normalFilePath = normalizePath(filePath);
|
||||
const normalDirPath = normalizePath(dirPath).replace(/\/$/, "");
|
||||
return normalFilePath.startsWith(normalDirPath + "/") || normalFilePath.startsWith(normalDirPath + "\\");
|
||||
}
|
||||
|
||||
interface Err { pos: number; message: string; }
|
||||
interface Err {
|
||||
pos: number;
|
||||
message: string;
|
||||
}
|
||||
function testNoTsIgnore(text: string): Err | undefined {
|
||||
const tsIgnore = "ts-ignore";
|
||||
const pos = text.indexOf(tsIgnore);
|
||||
return pos === -1 ? undefined : { pos, message: "'ts-ignore' is forbidden." };
|
||||
const tsIgnore = "ts-ignore";
|
||||
const pos = text.indexOf(tsIgnore);
|
||||
return pos === -1 ? undefined : { pos, message: "'ts-ignore' is forbidden." };
|
||||
}
|
||||
function testNoTslintDisables(text: string): Err | undefined {
|
||||
const tslintDisable = "tslint:disable";
|
||||
let lastIndex = 0;
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const pos = text.indexOf(tslintDisable, lastIndex);
|
||||
if (pos === -1) {
|
||||
return undefined;
|
||||
}
|
||||
const end = pos + tslintDisable.length;
|
||||
const nextChar = text.charAt(end);
|
||||
if (nextChar !== "-" && nextChar !== ":") {
|
||||
const message = "'tslint:disable' is forbidden. " +
|
||||
"('tslint:disable:rulename', tslint:disable-line' and 'tslint:disable-next-line' are allowed.)";
|
||||
return { pos, message };
|
||||
}
|
||||
lastIndex = end;
|
||||
const tslintDisable = "tslint:disable";
|
||||
let lastIndex = 0;
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const pos = text.indexOf(tslintDisable, lastIndex);
|
||||
if (pos === -1) {
|
||||
return undefined;
|
||||
}
|
||||
const end = pos + tslintDisable.length;
|
||||
const nextChar = text.charAt(end);
|
||||
if (nextChar !== "-" && nextChar !== ":") {
|
||||
const message =
|
||||
"'tslint:disable' is forbidden. " +
|
||||
"('tslint:disable:rulename', tslint:disable-line' and 'tslint:disable-next-line' are allowed.)";
|
||||
return { pos, message };
|
||||
}
|
||||
lastIndex = end;
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkTslintJson(dirPath: string, dt: boolean): Promise<void> {
|
||||
const configPath = getConfigPath(dirPath);
|
||||
const shouldExtend = `dtslint/${dt ? "dt" : "dtslint"}.json`;
|
||||
const validateExtends = (extend: string | string[]) =>
|
||||
extend === shouldExtend || (!dt && Array.isArray(extend) && extend.some(val => val === shouldExtend));
|
||||
const configPath = getConfigPath(dirPath);
|
||||
const shouldExtend = `dtslint/${dt ? "dt" : "dtslint"}.json`;
|
||||
const validateExtends = (extend: string | string[]) =>
|
||||
extend === shouldExtend || (!dt && Array.isArray(extend) && extend.some(val => val === shouldExtend));
|
||||
|
||||
if (!await pathExists(configPath)) {
|
||||
if (dt) {
|
||||
throw new Error(
|
||||
`On DefinitelyTyped, must include \`tslint.json\` containing \`{ "extends": "${shouldExtend}" }\`.\n` +
|
||||
"This was inferred as a DefinitelyTyped package because it contains a `// Type definitions for` header.");
|
||||
}
|
||||
return;
|
||||
if (!(await pathExists(configPath))) {
|
||||
if (dt) {
|
||||
throw new Error(
|
||||
`On DefinitelyTyped, must include \`tslint.json\` containing \`{ "extends": "${shouldExtend}" }\`.\n` +
|
||||
"This was inferred as a DefinitelyTyped package because it contains a `// Type definitions for` header."
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const tslintJson = await readJson(configPath);
|
||||
if (!validateExtends(tslintJson.extends)) {
|
||||
throw new Error(`If 'tslint.json' is present, it should extend "${shouldExtend}"`);
|
||||
}
|
||||
const tslintJson = await readJson(configPath);
|
||||
if (!validateExtends(tslintJson.extends)) {
|
||||
throw new Error(`If 'tslint.json' is present, it should extend "${shouldExtend}"`);
|
||||
}
|
||||
}
|
||||
|
||||
function getConfigPath(dirPath: string): string {
|
||||
return joinPaths(dirPath, "tslint.json");
|
||||
return joinPaths(dirPath, "tslint.json");
|
||||
}
|
||||
|
||||
async function getLintConfig(
|
||||
expectedConfigPath: string,
|
||||
tsconfigPath: string,
|
||||
minVersion: TsVersion,
|
||||
maxVersion: TsVersion,
|
||||
tsLocal: string | undefined,
|
||||
expectedConfigPath: string,
|
||||
tsconfigPath: string,
|
||||
minVersion: TsVersion,
|
||||
maxVersion: TsVersion,
|
||||
tsLocal: string | undefined
|
||||
): Promise<IConfigurationFile> {
|
||||
const configExists = await pathExists(expectedConfigPath);
|
||||
const configPath = configExists ? expectedConfigPath : joinPaths(__dirname, "..", "dtslint.json");
|
||||
// Second param to `findConfiguration` doesn't matter, since config path is provided.
|
||||
const config = Configuration.findConfiguration(configPath, "").results;
|
||||
if (!config) {
|
||||
throw new Error(`Could not load config at ${configPath}`);
|
||||
}
|
||||
const configExists = await pathExists(expectedConfigPath);
|
||||
const configPath = configExists ? expectedConfigPath : joinPaths(__dirname, "..", "dtslint.json");
|
||||
// Second param to `findConfiguration` doesn't matter, since config path is provided.
|
||||
const config = Configuration.findConfiguration(configPath, "").results;
|
||||
if (!config) {
|
||||
throw new Error(`Could not load config at ${configPath}`);
|
||||
}
|
||||
|
||||
const expectRule = config.rules.get("expect");
|
||||
if (!expectRule || expectRule.ruleSeverity !== "error") {
|
||||
throw new Error("'expect' rule should be enabled, else compile errors are ignored");
|
||||
}
|
||||
if (expectRule) {
|
||||
const versionsToTest =
|
||||
range(minVersion, maxVersion).map(versionName => ({ versionName, path: typeScriptPath(versionName, tsLocal) }));
|
||||
const expectOptions: ExpectOptions = { tsconfigPath, versionsToTest };
|
||||
expectRule.ruleArguments = [expectOptions];
|
||||
}
|
||||
return config;
|
||||
const expectRule = config.rules.get("expect");
|
||||
if (!expectRule || expectRule.ruleSeverity !== "error") {
|
||||
throw new Error("'expect' rule should be enabled, else compile errors are ignored");
|
||||
}
|
||||
if (expectRule) {
|
||||
const versionsToTest = range(minVersion, maxVersion).map(versionName => ({
|
||||
versionName,
|
||||
path: typeScriptPath(versionName, tsLocal)
|
||||
}));
|
||||
const expectOptions: ExpectOptions = { tsconfigPath, versionsToTest };
|
||||
expectRule.ruleArguments = [expectOptions];
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
function range(minVersion: TsVersion, maxVersion: TsVersion): readonly TsVersion[] {
|
||||
if (minVersion === "local") {
|
||||
assert(maxVersion === "local");
|
||||
return ["local"];
|
||||
}
|
||||
if (minVersion === TypeScriptVersion.latest) {
|
||||
assert(maxVersion === TypeScriptVersion.latest);
|
||||
return [TypeScriptVersion.latest];
|
||||
}
|
||||
assert(maxVersion !== "local");
|
||||
if (minVersion === "local") {
|
||||
assert(maxVersion === "local");
|
||||
return ["local"];
|
||||
}
|
||||
if (minVersion === TypeScriptVersion.latest) {
|
||||
assert(maxVersion === TypeScriptVersion.latest);
|
||||
return [TypeScriptVersion.latest];
|
||||
}
|
||||
assert(maxVersion !== "local");
|
||||
|
||||
const minIdx = TypeScriptVersion.shipped.indexOf(minVersion);
|
||||
assert(minIdx >= 0);
|
||||
if (maxVersion === TypeScriptVersion.latest) {
|
||||
return [...TypeScriptVersion.shipped.slice(minIdx), TypeScriptVersion.latest];
|
||||
}
|
||||
const maxIdx = TypeScriptVersion.shipped.indexOf(maxVersion as TypeScriptVersion);
|
||||
assert(maxIdx >= minIdx);
|
||||
return TypeScriptVersion.shipped.slice(minIdx, maxIdx + 1);
|
||||
const minIdx = TypeScriptVersion.shipped.indexOf(minVersion);
|
||||
assert(minIdx >= 0);
|
||||
if (maxVersion === TypeScriptVersion.latest) {
|
||||
return [...TypeScriptVersion.shipped.slice(minIdx), TypeScriptVersion.latest];
|
||||
}
|
||||
const maxIdx = TypeScriptVersion.shipped.indexOf(maxVersion as TypeScriptVersion);
|
||||
assert(maxIdx >= minIdx);
|
||||
return TypeScriptVersion.shipped.slice(minIdx, maxIdx + 1);
|
||||
}
|
||||
|
||||
export type TsVersion = TypeScriptVersion | "local";
|
||||
|
||||
@@ -4,42 +4,44 @@ import * as ts from "typescript";
|
||||
import { failure, isMainFile } from "../util";
|
||||
|
||||
export class Rule extends Lint.Rules.AbstractRule {
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "dt-header",
|
||||
description: "Ensure consistency of DefinitelyTyped headers.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "functionality",
|
||||
typescriptOnly: true,
|
||||
};
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "dt-header",
|
||||
description: "Ensure consistency of DefinitelyTyped headers.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "functionality",
|
||||
typescriptOnly: true
|
||||
};
|
||||
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, walk);
|
||||
}
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, walk);
|
||||
}
|
||||
}
|
||||
|
||||
function walk(ctx: Lint.WalkContext<void>): void {
|
||||
const { sourceFile } = ctx;
|
||||
const { text } = sourceFile;
|
||||
const lookFor = (search: string, explanation: string) => {
|
||||
const idx = text.indexOf(search);
|
||||
if (idx !== -1) {
|
||||
ctx.addFailureAt(idx, search.length, failure(Rule.metadata.ruleName, explanation));
|
||||
}
|
||||
};
|
||||
if (!isMainFile(sourceFile.fileName, /*allowNested*/ true)) {
|
||||
lookFor("// Type definitions for", "Header should only be in `index.d.ts` of the root.");
|
||||
lookFor("// TypeScript Version", "TypeScript version should be specified under header in `index.d.ts`.");
|
||||
lookFor("// Minimum TypeScript Version", "TypeScript version should be specified under header in `index.d.ts`.");
|
||||
return;
|
||||
const { sourceFile } = ctx;
|
||||
const { text } = sourceFile;
|
||||
const lookFor = (search: string, explanation: string) => {
|
||||
const idx = text.indexOf(search);
|
||||
if (idx !== -1) {
|
||||
ctx.addFailureAt(idx, search.length, failure(Rule.metadata.ruleName, explanation));
|
||||
}
|
||||
};
|
||||
if (!isMainFile(sourceFile.fileName, /*allowNested*/ true)) {
|
||||
lookFor("// Type definitions for", "Header should only be in `index.d.ts` of the root.");
|
||||
lookFor("// TypeScript Version", "TypeScript version should be specified under header in `index.d.ts`.");
|
||||
lookFor("// Minimum TypeScript Version", "TypeScript version should be specified under header in `index.d.ts`.");
|
||||
return;
|
||||
}
|
||||
|
||||
lookFor("// Definitions by: My Self", "Author name should be your name, not the default.");
|
||||
const error = validate(text);
|
||||
if (error) {
|
||||
ctx.addFailureAt(error.index, 1, failure(
|
||||
Rule.metadata.ruleName,
|
||||
`Error parsing header. Expected: ${renderExpected(error.expected)}.`));
|
||||
}
|
||||
// Don't recurse, we're done.
|
||||
lookFor("// Definitions by: My Self", "Author name should be your name, not the default.");
|
||||
const error = validate(text);
|
||||
if (error) {
|
||||
ctx.addFailureAt(
|
||||
error.index,
|
||||
1,
|
||||
failure(Rule.metadata.ruleName, `Error parsing header. Expected: ${renderExpected(error.expected)}.`)
|
||||
);
|
||||
}
|
||||
// Don't recurse, we're done.
|
||||
}
|
||||
|
||||
@@ -14,389 +14,407 @@ const cacheDir = join(os.homedir(), ".dts");
|
||||
const perfDir = join(os.homedir(), ".dts", "perf");
|
||||
|
||||
export class Rule extends Lint.Rules.TypedRule {
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "expect",
|
||||
description: "Asserts types with $ExpectType and presence of errors with $ExpectError.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "functionality",
|
||||
typescriptOnly: true,
|
||||
requiresTypeInfo: true,
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "expect",
|
||||
description: "Asserts types with $ExpectType and presence of errors with $ExpectError.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "functionality",
|
||||
typescriptOnly: true,
|
||||
requiresTypeInfo: true
|
||||
};
|
||||
|
||||
static FAILURE_STRING_DUPLICATE_ASSERTION = "This line has 2 $ExpectType assertions.";
|
||||
static FAILURE_STRING_ASSERTION_MISSING_NODE = "Can not match a node to this assertion.";
|
||||
static FAILURE_STRING_EXPECTED_ERROR = "Expected an error on this line, but found none.";
|
||||
|
||||
// TODO: If this naming convention is required by tslint, dump it when switching to eslint
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
static FAILURE_STRING(expectedVersion: string, expectedType: string, actualType: string): string {
|
||||
return `TypeScript@${expectedVersion} expected type to be:\n ${expectedType}\ngot:\n ${actualType}`;
|
||||
}
|
||||
|
||||
applyWithProgram(sourceFile: SourceFile, lintProgram: Program): Lint.RuleFailure[] {
|
||||
const options = this.ruleArguments[0] as Options | undefined;
|
||||
if (!options) {
|
||||
return this.applyWithFunction(sourceFile, ctx =>
|
||||
walk(ctx, lintProgram, TsType, "next", /*nextHigherVersion*/ undefined)
|
||||
);
|
||||
}
|
||||
|
||||
const { tsconfigPath, versionsToTest } = options;
|
||||
|
||||
const getFailures = (
|
||||
{ versionName, path }: VersionToTest,
|
||||
nextHigherVersion: string | undefined,
|
||||
writeOutput: boolean
|
||||
) => {
|
||||
const ts = require(path);
|
||||
const program = getProgram(tsconfigPath, ts, versionName, lintProgram);
|
||||
const failures = this.applyWithFunction(sourceFile, ctx =>
|
||||
walk(ctx, program, ts, versionName, nextHigherVersion)
|
||||
);
|
||||
if (writeOutput) {
|
||||
const packageName = basename(dirname(tsconfigPath));
|
||||
if (!packageName.match(/v\d+/) && !packageName.match(/ts\d\.\d/)) {
|
||||
const d = {
|
||||
[packageName]: {
|
||||
typeCount: (program as any).getTypeCount(),
|
||||
memory: ts.sys.getMemoryUsage ? ts.sys.getMemoryUsage() : 0
|
||||
}
|
||||
};
|
||||
if (!existsSync(cacheDir)) {
|
||||
mkdirSync(cacheDir);
|
||||
}
|
||||
if (!existsSync(perfDir)) {
|
||||
mkdirSync(perfDir);
|
||||
}
|
||||
writeFileSync(join(perfDir, `${packageName}.json`), JSON.stringify(d));
|
||||
}
|
||||
}
|
||||
return failures;
|
||||
};
|
||||
|
||||
static FAILURE_STRING_DUPLICATE_ASSERTION = "This line has 2 $ExpectType assertions.";
|
||||
static FAILURE_STRING_ASSERTION_MISSING_NODE = "Can not match a node to this assertion.";
|
||||
static FAILURE_STRING_EXPECTED_ERROR = "Expected an error on this line, but found none.";
|
||||
|
||||
// TODO: If this naming convention is required by tslint, dump it when switching to eslint
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
static FAILURE_STRING(expectedVersion: string, expectedType: string, actualType: string): string {
|
||||
return `TypeScript@${expectedVersion} expected type to be:\n ${expectedType}\ngot:\n ${actualType}`;
|
||||
const maxFailures = getFailures(last(versionsToTest), undefined, /*writeOutput*/ true);
|
||||
if (maxFailures.length) {
|
||||
return maxFailures;
|
||||
}
|
||||
|
||||
applyWithProgram(sourceFile: SourceFile, lintProgram: Program): Lint.RuleFailure[] {
|
||||
const options = this.ruleArguments[0] as Options | undefined;
|
||||
if (!options) {
|
||||
return this.applyWithFunction(sourceFile, ctx =>
|
||||
walk(ctx, lintProgram, TsType, "next", /*nextHigherVersion*/ undefined));
|
||||
}
|
||||
|
||||
const { tsconfigPath, versionsToTest } = options;
|
||||
|
||||
const getFailures = (
|
||||
{ versionName, path }: VersionToTest,
|
||||
nextHigherVersion: string | undefined,
|
||||
writeOutput: boolean,
|
||||
) => {
|
||||
const ts = require(path);
|
||||
const program = getProgram(tsconfigPath, ts, versionName, lintProgram);
|
||||
const failures = this.applyWithFunction(sourceFile, ctx => walk(ctx, program, ts, versionName, nextHigherVersion));
|
||||
if (writeOutput) {
|
||||
const packageName = basename(dirname(tsconfigPath));
|
||||
if (!packageName.match(/v\d+/) && !packageName.match(/ts\d\.\d/)) {
|
||||
const d = {
|
||||
[packageName]: {
|
||||
typeCount: (program as any).getTypeCount(),
|
||||
memory: ts.sys.getMemoryUsage ? ts.sys.getMemoryUsage() : 0,
|
||||
},
|
||||
};
|
||||
if (!existsSync(cacheDir)) {
|
||||
mkdirSync(cacheDir);
|
||||
}
|
||||
if (!existsSync(perfDir)) {
|
||||
mkdirSync(perfDir);
|
||||
}
|
||||
writeFileSync(join(perfDir, `${packageName}.json`), JSON.stringify(d));
|
||||
}
|
||||
}
|
||||
return failures;
|
||||
};
|
||||
|
||||
const maxFailures = getFailures(last(versionsToTest), undefined, /*writeOutput*/ true);
|
||||
if (maxFailures.length) {
|
||||
return maxFailures;
|
||||
}
|
||||
|
||||
// As an optimization, check the earliest version for errors;
|
||||
// assume that if it works on min and max, it works for everything in between.
|
||||
const minFailures = getFailures(versionsToTest[0], undefined, /*writeOutput*/ false);
|
||||
if (!minFailures.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// There are no failures in the max version, but there are failures in the min version.
|
||||
// Work backward to find the newest version with failures.
|
||||
for (let i = versionsToTest.length - 2; i >= 0; i--) {
|
||||
const failures = getFailures(versionsToTest[i], options.versionsToTest[i + 1].versionName, /*writeOutput*/ false);
|
||||
if (failures.length) {
|
||||
return failures;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(); // unreachable -- at least the min version should have failures.
|
||||
// As an optimization, check the earliest version for errors;
|
||||
// assume that if it works on min and max, it works for everything in between.
|
||||
const minFailures = getFailures(versionsToTest[0], undefined, /*writeOutput*/ false);
|
||||
if (!minFailures.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// There are no failures in the max version, but there are failures in the min version.
|
||||
// Work backward to find the newest version with failures.
|
||||
for (let i = versionsToTest.length - 2; i >= 0; i--) {
|
||||
const failures = getFailures(versionsToTest[i], options.versionsToTest[i + 1].versionName, /*writeOutput*/ false);
|
||||
if (failures.length) {
|
||||
return failures;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(); // unreachable -- at least the min version should have failures.
|
||||
}
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
readonly tsconfigPath: string;
|
||||
// These should be sorted with oldest first.
|
||||
readonly versionsToTest: readonly VersionToTest[];
|
||||
readonly tsconfigPath: string;
|
||||
// These should be sorted with oldest first.
|
||||
readonly versionsToTest: readonly VersionToTest[];
|
||||
}
|
||||
export interface VersionToTest {
|
||||
readonly versionName: string;
|
||||
readonly path: string;
|
||||
readonly versionName: string;
|
||||
readonly path: string;
|
||||
}
|
||||
|
||||
const programCache = new WeakMap<Program, Map<string, Program>>();
|
||||
/** Maps a tslint Program to one created with the version specified in `options`. */
|
||||
export function getProgram(configFile: string, ts: typeof TsType, versionName: string, lintProgram: Program): Program {
|
||||
let versionToProgram = programCache.get(lintProgram);
|
||||
if (versionToProgram === undefined) {
|
||||
versionToProgram = new Map<string, Program>();
|
||||
programCache.set(lintProgram, versionToProgram);
|
||||
}
|
||||
let versionToProgram = programCache.get(lintProgram);
|
||||
if (versionToProgram === undefined) {
|
||||
versionToProgram = new Map<string, Program>();
|
||||
programCache.set(lintProgram, versionToProgram);
|
||||
}
|
||||
|
||||
let newProgram = versionToProgram.get(versionName);
|
||||
if (newProgram === undefined) {
|
||||
newProgram = createProgram(configFile, ts);
|
||||
versionToProgram.set(versionName, newProgram);
|
||||
}
|
||||
return newProgram;
|
||||
let newProgram = versionToProgram.get(versionName);
|
||||
if (newProgram === undefined) {
|
||||
newProgram = createProgram(configFile, ts);
|
||||
versionToProgram.set(versionName, newProgram);
|
||||
}
|
||||
return newProgram;
|
||||
}
|
||||
|
||||
function createProgram(configFile: string, ts: typeof TsType): Program {
|
||||
const projectDirectory = dirname(configFile);
|
||||
const { config } = ts.readConfigFile(configFile, ts.sys.readFile);
|
||||
const parseConfigHost: TsType.ParseConfigHost = {
|
||||
fileExists: existsSync,
|
||||
readDirectory: ts.sys.readDirectory,
|
||||
readFile: file => readFileSync(file, "utf8"),
|
||||
useCaseSensitiveFileNames: true,
|
||||
};
|
||||
const parsed = ts.parseJsonConfigFileContent(config, parseConfigHost, resolvePath(projectDirectory), {noEmit: true});
|
||||
const host = ts.createCompilerHost(parsed.options, true);
|
||||
return ts.createProgram(parsed.fileNames, parsed.options, host);
|
||||
const projectDirectory = dirname(configFile);
|
||||
const { config } = ts.readConfigFile(configFile, ts.sys.readFile);
|
||||
const parseConfigHost: TsType.ParseConfigHost = {
|
||||
fileExists: existsSync,
|
||||
readDirectory: ts.sys.readDirectory,
|
||||
readFile: file => readFileSync(file, "utf8"),
|
||||
useCaseSensitiveFileNames: true
|
||||
};
|
||||
const parsed = ts.parseJsonConfigFileContent(config, parseConfigHost, resolvePath(projectDirectory), {
|
||||
noEmit: true
|
||||
});
|
||||
const host = ts.createCompilerHost(parsed.options, true);
|
||||
return ts.createProgram(parsed.fileNames, parsed.options, host);
|
||||
}
|
||||
|
||||
function walk(
|
||||
ctx: Lint.WalkContext<void>,
|
||||
program: Program,
|
||||
ts: typeof TsType,
|
||||
versionName: string,
|
||||
nextHigherVersion: string | undefined): void {
|
||||
const { fileName } = ctx.sourceFile;
|
||||
const sourceFile = program.getSourceFile(fileName)!;
|
||||
if (!sourceFile) {
|
||||
ctx.addFailure(0, 0,
|
||||
`Program source files differ between TypeScript versions. This may be a dtslint bug.\n` +
|
||||
`Expected to find a file '${fileName}' present in ${TsType.version}, but did not find it in ts@${versionName}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const checker = program.getTypeChecker();
|
||||
// Don't care about emit errors.
|
||||
const diagnostics = ts.getPreEmitDiagnostics(program, sourceFile);
|
||||
if (sourceFile.isDeclarationFile || !/\$Expect(Type|Error)/.test(sourceFile.text)) {
|
||||
// Normal file.
|
||||
for (const diagnostic of diagnostics) {
|
||||
addDiagnosticFailure(diagnostic);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const { errorLines, typeAssertions, duplicates } = parseAssertions(sourceFile);
|
||||
|
||||
for (const line of duplicates) {
|
||||
addFailureAtLine(line, Rule.FAILURE_STRING_DUPLICATE_ASSERTION);
|
||||
}
|
||||
|
||||
const seenDiagnosticsOnLine = new Set<number>();
|
||||
ctx: Lint.WalkContext<void>,
|
||||
program: Program,
|
||||
ts: typeof TsType,
|
||||
versionName: string,
|
||||
nextHigherVersion: string | undefined
|
||||
): void {
|
||||
const { fileName } = ctx.sourceFile;
|
||||
const sourceFile = program.getSourceFile(fileName)!;
|
||||
if (!sourceFile) {
|
||||
ctx.addFailure(
|
||||
0,
|
||||
0,
|
||||
`Program source files differ between TypeScript versions. This may be a dtslint bug.\n` +
|
||||
`Expected to find a file '${fileName}' present in ${TsType.version}, but did not find it in ts@${versionName}.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const checker = program.getTypeChecker();
|
||||
// Don't care about emit errors.
|
||||
const diagnostics = ts.getPreEmitDiagnostics(program, sourceFile);
|
||||
if (sourceFile.isDeclarationFile || !/\$Expect(Type|Error)/.test(sourceFile.text)) {
|
||||
// Normal file.
|
||||
for (const diagnostic of diagnostics) {
|
||||
const line = lineOfPosition(diagnostic.start!, sourceFile);
|
||||
seenDiagnosticsOnLine.add(line);
|
||||
if (!errorLines.has(line)) {
|
||||
addDiagnosticFailure(diagnostic);
|
||||
}
|
||||
addDiagnosticFailure(diagnostic);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (const line of errorLines) {
|
||||
if (!seenDiagnosticsOnLine.has(line)) {
|
||||
addFailureAtLine(line, Rule.FAILURE_STRING_EXPECTED_ERROR);
|
||||
}
|
||||
}
|
||||
const { errorLines, typeAssertions, duplicates } = parseAssertions(sourceFile);
|
||||
|
||||
const { unmetExpectations, unusedAssertions } = getExpectTypeFailures(sourceFile, typeAssertions, checker, ts);
|
||||
for (const { node, expected, actual } of unmetExpectations) {
|
||||
ctx.addFailureAtNode(node, Rule.FAILURE_STRING(versionName, expected, actual));
|
||||
}
|
||||
for (const line of unusedAssertions) {
|
||||
addFailureAtLine(line, Rule.FAILURE_STRING_ASSERTION_MISSING_NODE);
|
||||
}
|
||||
for (const line of duplicates) {
|
||||
addFailureAtLine(line, Rule.FAILURE_STRING_DUPLICATE_ASSERTION);
|
||||
}
|
||||
|
||||
function addDiagnosticFailure(diagnostic: TsType.Diagnostic): void {
|
||||
const intro = getIntro();
|
||||
if (diagnostic.file === sourceFile) {
|
||||
const msg = `${intro}\n${ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`;
|
||||
ctx.addFailureAt(diagnostic.start!, diagnostic.length!, msg);
|
||||
} else {
|
||||
ctx.addFailureAt(0, 0, `${intro}\n${fileName}${diagnostic.messageText}`);
|
||||
}
|
||||
}
|
||||
const seenDiagnosticsOnLine = new Set<number>();
|
||||
|
||||
function getIntro(): string {
|
||||
if (nextHigherVersion === undefined) {
|
||||
return `TypeScript@${versionName} compile error: `;
|
||||
} else {
|
||||
const msg = `Compile error in typescript@${versionName} but not in typescript@${nextHigherVersion}.\n`;
|
||||
const explain = nextHigherVersion === "next"
|
||||
? "TypeScript@next features not yet supported."
|
||||
: `Fix with a comment '// Minimum TypeScript Version: ${nextHigherVersion}' just under the header.`;
|
||||
return msg + explain;
|
||||
}
|
||||
for (const diagnostic of diagnostics) {
|
||||
const line = lineOfPosition(diagnostic.start!, sourceFile);
|
||||
seenDiagnosticsOnLine.add(line);
|
||||
if (!errorLines.has(line)) {
|
||||
addDiagnosticFailure(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
function addFailureAtLine(line: number, failure: string): void {
|
||||
const start = sourceFile.getPositionOfLineAndCharacter(line, 0);
|
||||
let end = start + sourceFile.text.split("\n")[line].length;
|
||||
if (sourceFile.text[end - 1] === "\r") {
|
||||
end--;
|
||||
}
|
||||
ctx.addFailure(start, end, `TypeScript@${versionName}: ${failure}`);
|
||||
for (const line of errorLines) {
|
||||
if (!seenDiagnosticsOnLine.has(line)) {
|
||||
addFailureAtLine(line, Rule.FAILURE_STRING_EXPECTED_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
const { unmetExpectations, unusedAssertions } = getExpectTypeFailures(sourceFile, typeAssertions, checker, ts);
|
||||
for (const { node, expected, actual } of unmetExpectations) {
|
||||
ctx.addFailureAtNode(node, Rule.FAILURE_STRING(versionName, expected, actual));
|
||||
}
|
||||
for (const line of unusedAssertions) {
|
||||
addFailureAtLine(line, Rule.FAILURE_STRING_ASSERTION_MISSING_NODE);
|
||||
}
|
||||
|
||||
function addDiagnosticFailure(diagnostic: TsType.Diagnostic): void {
|
||||
const intro = getIntro();
|
||||
if (diagnostic.file === sourceFile) {
|
||||
const msg = `${intro}\n${ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`;
|
||||
ctx.addFailureAt(diagnostic.start!, diagnostic.length!, msg);
|
||||
} else {
|
||||
ctx.addFailureAt(0, 0, `${intro}\n${fileName}${diagnostic.messageText}`);
|
||||
}
|
||||
}
|
||||
|
||||
function getIntro(): string {
|
||||
if (nextHigherVersion === undefined) {
|
||||
return `TypeScript@${versionName} compile error: `;
|
||||
} else {
|
||||
const msg = `Compile error in typescript@${versionName} but not in typescript@${nextHigherVersion}.\n`;
|
||||
const explain =
|
||||
nextHigherVersion === "next"
|
||||
? "TypeScript@next features not yet supported."
|
||||
: `Fix with a comment '// Minimum TypeScript Version: ${nextHigherVersion}' just under the header.`;
|
||||
return msg + explain;
|
||||
}
|
||||
}
|
||||
|
||||
function addFailureAtLine(line: number, failure: string): void {
|
||||
const start = sourceFile.getPositionOfLineAndCharacter(line, 0);
|
||||
let end = start + sourceFile.text.split("\n")[line].length;
|
||||
if (sourceFile.text[end - 1] === "\r") {
|
||||
end--;
|
||||
}
|
||||
ctx.addFailure(start, end, `TypeScript@${versionName}: ${failure}`);
|
||||
}
|
||||
}
|
||||
|
||||
interface Assertions {
|
||||
/** Lines with an $ExpectError. */
|
||||
readonly errorLines: ReadonlySet<number>;
|
||||
/** Map from a line number to the expected type at that line. */
|
||||
readonly typeAssertions: Map<number, string>;
|
||||
/** Lines with more than one assertion (these are errors). */
|
||||
readonly duplicates: readonly number[];
|
||||
/** Lines with an $ExpectError. */
|
||||
readonly errorLines: ReadonlySet<number>;
|
||||
/** Map from a line number to the expected type at that line. */
|
||||
readonly typeAssertions: Map<number, string>;
|
||||
/** Lines with more than one assertion (these are errors). */
|
||||
readonly duplicates: readonly number[];
|
||||
}
|
||||
|
||||
function parseAssertions(sourceFile: SourceFile): Assertions {
|
||||
const errorLines = new Set<number>();
|
||||
const typeAssertions = new Map<number, string>();
|
||||
const duplicates: number[] = [];
|
||||
const errorLines = new Set<number>();
|
||||
const typeAssertions = new Map<number, string>();
|
||||
const duplicates: number[] = [];
|
||||
|
||||
const { text } = sourceFile;
|
||||
const commentRegexp = /\/\/(.*)/g;
|
||||
const lineStarts = sourceFile.getLineStarts();
|
||||
let curLine = 0;
|
||||
const { text } = sourceFile;
|
||||
const commentRegexp = /\/\/(.*)/g;
|
||||
const lineStarts = sourceFile.getLineStarts();
|
||||
let curLine = 0;
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const commentMatch = commentRegexp.exec(text);
|
||||
if (commentMatch === null) {
|
||||
break;
|
||||
}
|
||||
// Match on the contents of that comment so we do nothing in a commented-out assertion,
|
||||
// i.e. `// foo; // $ExpectType number`
|
||||
const match = /^ \$Expect((Type (.*))|Error)$/.exec(commentMatch[1]);
|
||||
if (match === null) {
|
||||
continue;
|
||||
}
|
||||
const line = getLine(commentMatch.index);
|
||||
if (match[1] === "Error") {
|
||||
if (errorLines.has(line)) {
|
||||
duplicates.push(line);
|
||||
}
|
||||
errorLines.add(line);
|
||||
} else {
|
||||
const expectedType = match[3];
|
||||
// Don't bother with the assertion if there are 2 assertions on 1 line. Just fail for the duplicate.
|
||||
if (typeAssertions.delete(line)) {
|
||||
duplicates.push(line);
|
||||
} else {
|
||||
typeAssertions.set(line, expectedType);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const commentMatch = commentRegexp.exec(text);
|
||||
if (commentMatch === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
return { errorLines, typeAssertions, duplicates };
|
||||
|
||||
function getLine(pos: number): number {
|
||||
// advance curLine to be the line preceding 'pos'
|
||||
while (lineStarts[curLine + 1] <= pos) {
|
||||
curLine++;
|
||||
}
|
||||
// If this is the first token on the line, it applies to the next line.
|
||||
// Otherwise, it applies to the text to the left of it.
|
||||
return isFirstOnLine(text, lineStarts[curLine], pos) ? curLine + 1 : curLine;
|
||||
// Match on the contents of that comment so we do nothing in a commented-out assertion,
|
||||
// i.e. `// foo; // $ExpectType number`
|
||||
const match = /^ \$Expect((Type (.*))|Error)$/.exec(commentMatch[1]);
|
||||
if (match === null) {
|
||||
continue;
|
||||
}
|
||||
const line = getLine(commentMatch.index);
|
||||
if (match[1] === "Error") {
|
||||
if (errorLines.has(line)) {
|
||||
duplicates.push(line);
|
||||
}
|
||||
errorLines.add(line);
|
||||
} else {
|
||||
const expectedType = match[3];
|
||||
// Don't bother with the assertion if there are 2 assertions on 1 line. Just fail for the duplicate.
|
||||
if (typeAssertions.delete(line)) {
|
||||
duplicates.push(line);
|
||||
} else {
|
||||
typeAssertions.set(line, expectedType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { errorLines, typeAssertions, duplicates };
|
||||
|
||||
function getLine(pos: number): number {
|
||||
// advance curLine to be the line preceding 'pos'
|
||||
while (lineStarts[curLine + 1] <= pos) {
|
||||
curLine++;
|
||||
}
|
||||
// If this is the first token on the line, it applies to the next line.
|
||||
// Otherwise, it applies to the text to the left of it.
|
||||
return isFirstOnLine(text, lineStarts[curLine], pos) ? curLine + 1 : curLine;
|
||||
}
|
||||
}
|
||||
|
||||
function isFirstOnLine(text: string, lineStart: number, pos: number): boolean {
|
||||
for (let i = lineStart; i < pos; i++) {
|
||||
if (text[i] !== " ") {
|
||||
return false;
|
||||
}
|
||||
for (let i = lineStart; i < pos; i++) {
|
||||
if (text[i] !== " ") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
interface ExpectTypeFailures {
|
||||
/** Lines with an $ExpectType, but a different type was there. */
|
||||
readonly unmetExpectations: readonly { node: TsType.Node, expected: string, actual: string }[];
|
||||
/** Lines with an $ExpectType, but no node could be found. */
|
||||
readonly unusedAssertions: Iterable<number>;
|
||||
/** Lines with an $ExpectType, but a different type was there. */
|
||||
readonly unmetExpectations: readonly { node: TsType.Node; expected: string; actual: string }[];
|
||||
/** Lines with an $ExpectType, but no node could be found. */
|
||||
readonly unusedAssertions: Iterable<number>;
|
||||
}
|
||||
|
||||
function matchReadonlyArray(actual: string, expected: string) {
|
||||
if (!(/\breadonly\b/.test(actual) && /\bReadonlyArray\b/.test(expected))) return false;
|
||||
const readonlyArrayRegExp = /\bReadonlyArray</y;
|
||||
const readonlyModifierRegExp = /\breadonly /y;
|
||||
if (!(/\breadonly\b/.test(actual) && /\bReadonlyArray\b/.test(expected))) return false;
|
||||
const readonlyArrayRegExp = /\bReadonlyArray</y;
|
||||
const readonlyModifierRegExp = /\breadonly /y;
|
||||
|
||||
// A<ReadonlyArray<B<ReadonlyArray<C>>>>
|
||||
// A<readonly B<readonly C[]>[]>
|
||||
// A<ReadonlyArray<B<ReadonlyArray<C>>>>
|
||||
// A<readonly B<readonly C[]>[]>
|
||||
|
||||
let expectedPos = 0;
|
||||
let actualPos = 0;
|
||||
let depth = 0;
|
||||
while (expectedPos < expected.length && actualPos < actual.length) {
|
||||
const expectedChar = expected.charAt(expectedPos);
|
||||
const actualChar = actual.charAt(actualPos);
|
||||
if (expectedChar === actualChar) {
|
||||
expectedPos++;
|
||||
actualPos++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// check for end of readonly array
|
||||
if (depth > 0 && expectedChar === ">" && actualChar === "[" && actualPos < actual.length - 1 &&
|
||||
actual.charAt(actualPos + 1) === "]") {
|
||||
depth--;
|
||||
expectedPos++;
|
||||
actualPos += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
// check for start of readonly array
|
||||
readonlyArrayRegExp.lastIndex = expectedPos;
|
||||
readonlyModifierRegExp.lastIndex = actualPos;
|
||||
if (readonlyArrayRegExp.test(expected) && readonlyModifierRegExp.test(actual)) {
|
||||
depth++;
|
||||
expectedPos += 14; // "ReadonlyArray<".length;
|
||||
actualPos += 9; // "readonly ".length;
|
||||
continue;
|
||||
}
|
||||
|
||||
return false;
|
||||
let expectedPos = 0;
|
||||
let actualPos = 0;
|
||||
let depth = 0;
|
||||
while (expectedPos < expected.length && actualPos < actual.length) {
|
||||
const expectedChar = expected.charAt(expectedPos);
|
||||
const actualChar = actual.charAt(actualPos);
|
||||
if (expectedChar === actualChar) {
|
||||
expectedPos++;
|
||||
actualPos++;
|
||||
continue;
|
||||
}
|
||||
|
||||
return true;
|
||||
// check for end of readonly array
|
||||
if (
|
||||
depth > 0 &&
|
||||
expectedChar === ">" &&
|
||||
actualChar === "[" &&
|
||||
actualPos < actual.length - 1 &&
|
||||
actual.charAt(actualPos + 1) === "]"
|
||||
) {
|
||||
depth--;
|
||||
expectedPos++;
|
||||
actualPos += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
// check for start of readonly array
|
||||
readonlyArrayRegExp.lastIndex = expectedPos;
|
||||
readonlyModifierRegExp.lastIndex = actualPos;
|
||||
if (readonlyArrayRegExp.test(expected) && readonlyModifierRegExp.test(actual)) {
|
||||
depth++;
|
||||
expectedPos += 14; // "ReadonlyArray<".length;
|
||||
actualPos += 9; // "readonly ".length;
|
||||
continue;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getExpectTypeFailures(
|
||||
sourceFile: SourceFile,
|
||||
typeAssertions: Map<number, string>,
|
||||
checker: TsType.TypeChecker,
|
||||
ts: typeof TsType,
|
||||
): ExpectTypeFailures {
|
||||
const unmetExpectations: { node: TsType.Node, expected: string, actual: string }[] = [];
|
||||
// Match assertions to the first node that appears on the line they apply to.
|
||||
// `forEachChild` isn't available as a method in older TypeScript versions, so must use `ts.forEachChild` instead.
|
||||
ts.forEachChild(sourceFile, function iterate(node) {
|
||||
const line = lineOfPosition(node.getStart(sourceFile), sourceFile);
|
||||
const expected = typeAssertions.get(line);
|
||||
if (expected !== undefined) {
|
||||
// https://github.com/Microsoft/TypeScript/issues/14077
|
||||
if (node.kind === ts.SyntaxKind.ExpressionStatement) {
|
||||
node = (node as TsType.ExpressionStatement).expression;
|
||||
}
|
||||
sourceFile: SourceFile,
|
||||
typeAssertions: Map<number, string>,
|
||||
checker: TsType.TypeChecker,
|
||||
ts: typeof TsType
|
||||
): ExpectTypeFailures {
|
||||
const unmetExpectations: { node: TsType.Node; expected: string; actual: string }[] = [];
|
||||
// Match assertions to the first node that appears on the line they apply to.
|
||||
// `forEachChild` isn't available as a method in older TypeScript versions, so must use `ts.forEachChild` instead.
|
||||
ts.forEachChild(sourceFile, function iterate(node) {
|
||||
const line = lineOfPosition(node.getStart(sourceFile), sourceFile);
|
||||
const expected = typeAssertions.get(line);
|
||||
if (expected !== undefined) {
|
||||
// https://github.com/Microsoft/TypeScript/issues/14077
|
||||
if (node.kind === ts.SyntaxKind.ExpressionStatement) {
|
||||
node = (node as TsType.ExpressionStatement).expression;
|
||||
}
|
||||
|
||||
const type = checker.getTypeAtLocation(getNodeForExpectType(node, ts));
|
||||
const type = checker.getTypeAtLocation(getNodeForExpectType(node, ts));
|
||||
|
||||
const actual = type
|
||||
? checker.typeToString(type, /*enclosingDeclaration*/ undefined, ts.TypeFormatFlags.NoTruncation)
|
||||
: "";
|
||||
const actual = type
|
||||
? checker.typeToString(type, /*enclosingDeclaration*/ undefined, ts.TypeFormatFlags.NoTruncation)
|
||||
: "";
|
||||
|
||||
if (!expected.split(/\s*\|\|\s*/).some(s => actual === s || matchReadonlyArray(actual, s))) {
|
||||
unmetExpectations.push({ node, expected, actual });
|
||||
}
|
||||
if (!expected.split(/\s*\|\|\s*/).some(s => actual === s || matchReadonlyArray(actual, s))) {
|
||||
unmetExpectations.push({ node, expected, actual });
|
||||
}
|
||||
|
||||
typeAssertions.delete(line);
|
||||
}
|
||||
typeAssertions.delete(line);
|
||||
}
|
||||
|
||||
ts.forEachChild(node, iterate);
|
||||
});
|
||||
return { unmetExpectations, unusedAssertions: typeAssertions.keys() };
|
||||
ts.forEachChild(node, iterate);
|
||||
});
|
||||
return { unmetExpectations, unusedAssertions: typeAssertions.keys() };
|
||||
}
|
||||
|
||||
function getNodeForExpectType(node: TsType.Node, ts: typeof TsType): TsType.Node {
|
||||
if (node.kind === ts.SyntaxKind.VariableStatement) { // ts2.0 doesn't have `isVariableStatement`
|
||||
const { declarationList: { declarations } } = node as TsType.VariableStatement;
|
||||
if (declarations.length === 1) {
|
||||
const { initializer } = declarations[0];
|
||||
if (initializer) {
|
||||
return initializer;
|
||||
}
|
||||
}
|
||||
if (node.kind === ts.SyntaxKind.VariableStatement) {
|
||||
// ts2.0 doesn't have `isVariableStatement`
|
||||
const {
|
||||
declarationList: { declarations }
|
||||
} = node as TsType.VariableStatement;
|
||||
if (declarations.length === 1) {
|
||||
const { initializer } = declarations[0];
|
||||
if (initializer) {
|
||||
return initializer;
|
||||
}
|
||||
}
|
||||
return node;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
function lineOfPosition(pos: number, sourceFile: SourceFile): number {
|
||||
return sourceFile.getLineAndCharacterOfPosition(pos).line;
|
||||
return sourceFile.getLineAndCharacterOfPosition(pos).line;
|
||||
}
|
||||
|
||||
@@ -4,79 +4,82 @@ import * as ts from "typescript";
|
||||
import { failure } from "../util";
|
||||
|
||||
export class Rule extends Lint.Rules.AbstractRule {
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "export-just-namespace",
|
||||
description:
|
||||
"Forbid to `export = foo` where `foo` is a namespace and isn't merged with a function/class/type/interface.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "functionality",
|
||||
typescriptOnly: true,
|
||||
};
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "export-just-namespace",
|
||||
description:
|
||||
"Forbid to `export = foo` where `foo` is a namespace and isn't merged with a function/class/type/interface.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "functionality",
|
||||
typescriptOnly: true
|
||||
};
|
||||
|
||||
static FAILURE_STRING = failure(
|
||||
Rule.metadata.ruleName,
|
||||
"Instead of `export =`-ing a namespace, use the body of the namespace as the module body.");
|
||||
static FAILURE_STRING = failure(
|
||||
Rule.metadata.ruleName,
|
||||
"Instead of `export =`-ing a namespace, use the body of the namespace as the module body."
|
||||
);
|
||||
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, walk);
|
||||
}
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, walk);
|
||||
}
|
||||
}
|
||||
|
||||
function walk(ctx: Lint.WalkContext<void>): void {
|
||||
const { sourceFile: { statements } } = ctx;
|
||||
const exportEqualsNode = statements.find(isExportEquals) as ts.ExportAssignment | undefined;
|
||||
if (!exportEqualsNode) {
|
||||
return;
|
||||
}
|
||||
const expr = exportEqualsNode.expression;
|
||||
if (!ts.isIdentifier(expr)) {
|
||||
return;
|
||||
}
|
||||
const exportEqualsName = expr.text;
|
||||
const {
|
||||
sourceFile: { statements }
|
||||
} = ctx;
|
||||
const exportEqualsNode = statements.find(isExportEquals) as ts.ExportAssignment | undefined;
|
||||
if (!exportEqualsNode) {
|
||||
return;
|
||||
}
|
||||
const expr = exportEqualsNode.expression;
|
||||
if (!ts.isIdentifier(expr)) {
|
||||
return;
|
||||
}
|
||||
const exportEqualsName = expr.text;
|
||||
|
||||
if (exportEqualsName && isJustNamespace(statements, exportEqualsName)) {
|
||||
ctx.addFailureAtNode(exportEqualsNode, Rule.FAILURE_STRING);
|
||||
}
|
||||
if (exportEqualsName && isJustNamespace(statements, exportEqualsName)) {
|
||||
ctx.addFailureAtNode(exportEqualsNode, Rule.FAILURE_STRING);
|
||||
}
|
||||
}
|
||||
|
||||
function isExportEquals(node: ts.Node): boolean {
|
||||
return ts.isExportAssignment(node) && !!node.isExportEquals;
|
||||
return ts.isExportAssignment(node) && !!node.isExportEquals;
|
||||
}
|
||||
|
||||
/** Returns true if there is a namespace but there are no functions/classes with the name. */
|
||||
function isJustNamespace(statements: readonly ts.Statement[], exportEqualsName: string): boolean {
|
||||
let anyNamespace = false;
|
||||
let anyNamespace = false;
|
||||
|
||||
for (const statement of statements) {
|
||||
switch (statement.kind) {
|
||||
case ts.SyntaxKind.ModuleDeclaration:
|
||||
anyNamespace = anyNamespace || nameMatches((statement as ts.ModuleDeclaration).name);
|
||||
break;
|
||||
case ts.SyntaxKind.VariableStatement:
|
||||
if ((statement as ts.VariableStatement).declarationList.declarations.some(d => nameMatches(d.name))) {
|
||||
// OK. It's merged with a variable.
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case ts.SyntaxKind.FunctionDeclaration:
|
||||
case ts.SyntaxKind.ClassDeclaration:
|
||||
case ts.SyntaxKind.TypeAliasDeclaration:
|
||||
case ts.SyntaxKind.InterfaceDeclaration:
|
||||
if (nameMatches((statement as ts.DeclarationStatement).name)) {
|
||||
// OK. It's merged with a function/class/type/interface.
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
for (const statement of statements) {
|
||||
switch (statement.kind) {
|
||||
case ts.SyntaxKind.ModuleDeclaration:
|
||||
anyNamespace = anyNamespace || nameMatches((statement as ts.ModuleDeclaration).name);
|
||||
break;
|
||||
case ts.SyntaxKind.VariableStatement:
|
||||
if ((statement as ts.VariableStatement).declarationList.declarations.some(d => nameMatches(d.name))) {
|
||||
// OK. It's merged with a variable.
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case ts.SyntaxKind.FunctionDeclaration:
|
||||
case ts.SyntaxKind.ClassDeclaration:
|
||||
case ts.SyntaxKind.TypeAliasDeclaration:
|
||||
case ts.SyntaxKind.InterfaceDeclaration:
|
||||
if (nameMatches((statement as ts.DeclarationStatement).name)) {
|
||||
// OK. It's merged with a function/class/type/interface.
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
return anyNamespace;
|
||||
return anyNamespace;
|
||||
|
||||
function nameMatches(nameNode: ts.Node | undefined): boolean {
|
||||
return nameNode !== undefined && ts.isIdentifier(nameNode) && nameNode.text === exportEqualsName;
|
||||
}
|
||||
function nameMatches(nameNode: ts.Node | undefined): boolean {
|
||||
return nameNode !== undefined && ts.isIdentifier(nameNode) && nameNode.text === exportEqualsName;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -4,29 +4,30 @@ import * as ts from "typescript";
|
||||
import { failure } from "../util";
|
||||
|
||||
export class Rule extends Lint.Rules.AbstractRule {
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "no-any-union",
|
||||
description: "Forbid a union to contain `any`",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "functionality",
|
||||
typescriptOnly: true,
|
||||
};
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "no-any-union",
|
||||
description: "Forbid a union to contain `any`",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "functionality",
|
||||
typescriptOnly: true
|
||||
};
|
||||
|
||||
static FAILURE_STRING = failure(
|
||||
Rule.metadata.ruleName,
|
||||
"Including `any` in a union will override all other members of the union.");
|
||||
static FAILURE_STRING = failure(
|
||||
Rule.metadata.ruleName,
|
||||
"Including `any` in a union will override all other members of the union."
|
||||
);
|
||||
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, walk);
|
||||
}
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, walk);
|
||||
}
|
||||
}
|
||||
|
||||
function walk(ctx: Lint.WalkContext<void>): void {
|
||||
ctx.sourceFile.forEachChild(function recur(node) {
|
||||
if (node.kind === ts.SyntaxKind.AnyKeyword && ts.isUnionTypeNode(node.parent!)) {
|
||||
ctx.addFailureAtNode(node, Rule.FAILURE_STRING);
|
||||
}
|
||||
node.forEachChild(recur);
|
||||
});
|
||||
ctx.sourceFile.forEachChild(function recur(node) {
|
||||
if (node.kind === ts.SyntaxKind.AnyKeyword && ts.isUnionTypeNode(node.parent!)) {
|
||||
ctx.addFailureAtNode(node, Rule.FAILURE_STRING);
|
||||
}
|
||||
node.forEachChild(recur);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,36 +4,38 @@ import * as ts from "typescript";
|
||||
import { failure } from "../util";
|
||||
|
||||
export class Rule extends Lint.Rules.AbstractRule {
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "no-bad-reference",
|
||||
description: 'Forbid <reference path="../etc"/> in any file, and forbid <reference path> in test files.',
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "functionality",
|
||||
typescriptOnly: true,
|
||||
};
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "no-bad-reference",
|
||||
description: 'Forbid <reference path="../etc"/> in any file, and forbid <reference path> in test files.',
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "functionality",
|
||||
typescriptOnly: true
|
||||
};
|
||||
|
||||
static FAILURE_STRING = failure(
|
||||
Rule.metadata.ruleName,
|
||||
"Don't use <reference path> to reference another package. Use an import or <reference types> instead.");
|
||||
static FAILURE_STRING_REFERENCE_IN_TEST = failure(
|
||||
Rule.metadata.ruleName,
|
||||
"Don't use <reference path> in test files. Use <reference types> or include the file in 'tsconfig.json'.");
|
||||
static FAILURE_STRING = failure(
|
||||
Rule.metadata.ruleName,
|
||||
"Don't use <reference path> to reference another package. Use an import or <reference types> instead."
|
||||
);
|
||||
static FAILURE_STRING_REFERENCE_IN_TEST = failure(
|
||||
Rule.metadata.ruleName,
|
||||
"Don't use <reference path> in test files. Use <reference types> or include the file in 'tsconfig.json'."
|
||||
);
|
||||
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, walk);
|
||||
}
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, walk);
|
||||
}
|
||||
}
|
||||
|
||||
function walk(ctx: Lint.WalkContext<void>): void {
|
||||
const { sourceFile } = ctx;
|
||||
for (const ref of sourceFile.referencedFiles) {
|
||||
if (sourceFile.isDeclarationFile) {
|
||||
if (ref.fileName.startsWith("..")) {
|
||||
ctx.addFailure(ref.pos, ref.end, Rule.FAILURE_STRING);
|
||||
}
|
||||
} else {
|
||||
ctx.addFailure(ref.pos, ref.end, Rule.FAILURE_STRING_REFERENCE_IN_TEST);
|
||||
}
|
||||
const { sourceFile } = ctx;
|
||||
for (const ref of sourceFile.referencedFiles) {
|
||||
if (sourceFile.isDeclarationFile) {
|
||||
if (ref.fileName.startsWith("..")) {
|
||||
ctx.addFailure(ref.pos, ref.end, Rule.FAILURE_STRING);
|
||||
}
|
||||
} else {
|
||||
ctx.addFailure(ref.pos, ref.end, Rule.FAILURE_STRING_REFERENCE_IN_TEST);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,29 +4,31 @@ import * as ts from "typescript";
|
||||
import { failure } from "../util";
|
||||
|
||||
export class Rule extends Lint.Rules.AbstractRule {
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "no-const-enum",
|
||||
description: "Forbid `const enum`",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "functionality",
|
||||
typescriptOnly: true,
|
||||
};
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "no-const-enum",
|
||||
description: "Forbid `const enum`",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "functionality",
|
||||
typescriptOnly: true
|
||||
};
|
||||
|
||||
static FAILURE_STRING = failure(
|
||||
Rule.metadata.ruleName,
|
||||
"Use of `const enum`s is forbidden.");
|
||||
static FAILURE_STRING = failure(Rule.metadata.ruleName, "Use of `const enum`s is forbidden.");
|
||||
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, walk);
|
||||
}
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, walk);
|
||||
}
|
||||
}
|
||||
|
||||
function walk(ctx: Lint.WalkContext<void>): void {
|
||||
ctx.sourceFile.forEachChild(function recur(node) {
|
||||
if (ts.isEnumDeclaration(node) && node.modifiers && node.modifiers.some(m => m.kind === ts.SyntaxKind.ConstKeyword)) {
|
||||
ctx.addFailureAtNode(node.name, Rule.FAILURE_STRING);
|
||||
}
|
||||
node.forEachChild(recur);
|
||||
});
|
||||
ctx.sourceFile.forEachChild(function recur(node) {
|
||||
if (
|
||||
ts.isEnumDeclaration(node) &&
|
||||
node.modifiers &&
|
||||
node.modifiers.some(m => m.kind === ts.SyntaxKind.ConstKeyword)
|
||||
) {
|
||||
ctx.addFailureAtNode(node.name, Rule.FAILURE_STRING);
|
||||
}
|
||||
node.forEachChild(recur);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,47 +4,50 @@ import * as ts from "typescript";
|
||||
import { failure } from "../util";
|
||||
|
||||
export class Rule extends Lint.Rules.AbstractRule {
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "no-dead-reference",
|
||||
description: "Ensures that all `/// <reference>` comments go at the top of the file.",
|
||||
rationale: "style",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "functionality",
|
||||
typescriptOnly: true,
|
||||
};
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "no-dead-reference",
|
||||
description: "Ensures that all `/// <reference>` comments go at the top of the file.",
|
||||
rationale: "style",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "functionality",
|
||||
typescriptOnly: true
|
||||
};
|
||||
|
||||
static FAILURE_STRING = failure(
|
||||
Rule.metadata.ruleName,
|
||||
"`/// <reference>` directive must be at top of file to take effect.");
|
||||
static FAILURE_STRING = failure(
|
||||
Rule.metadata.ruleName,
|
||||
"`/// <reference>` directive must be at top of file to take effect."
|
||||
);
|
||||
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, walk);
|
||||
}
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, walk);
|
||||
}
|
||||
}
|
||||
|
||||
function walk(ctx: Lint.WalkContext<void>): void {
|
||||
const { sourceFile: { statements, text } } = ctx;
|
||||
if (!statements.length) {
|
||||
return;
|
||||
const {
|
||||
sourceFile: { statements, text }
|
||||
} = ctx;
|
||||
if (!statements.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 'm' flag makes it multiline, so `^` matches the beginning of any line.
|
||||
// 'g' flag lets us set rgx.lastIndex
|
||||
const rgx = /^\s*(\/\/\/ <reference)/gm;
|
||||
|
||||
// Start search at the first statement. (`/// <reference>` before that is OK.)
|
||||
rgx.lastIndex = statements[0].getStart();
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const match = rgx.exec(text);
|
||||
if (match === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 'm' flag makes it multiline, so `^` matches the beginning of any line.
|
||||
// 'g' flag lets us set rgx.lastIndex
|
||||
const rgx = /^\s*(\/\/\/ <reference)/mg;
|
||||
|
||||
// Start search at the first statement. (`/// <reference>` before that is OK.)
|
||||
rgx.lastIndex = statements[0].getStart();
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const match = rgx.exec(text);
|
||||
if (match === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
const length = match[1].length;
|
||||
const start = match.index + match[0].length - length;
|
||||
ctx.addFailureAt(start, length, Rule.FAILURE_STRING);
|
||||
}
|
||||
const length = match[1].length;
|
||||
const start = match.index + match[0].length - length;
|
||||
ctx.addFailureAt(start, length, Rule.FAILURE_STRING);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,36 +4,40 @@ import * as ts from "typescript";
|
||||
import { failure, getCommonDirectoryName } from "../util";
|
||||
|
||||
export class Rule extends Lint.Rules.TypedRule {
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "no-declare-current-package",
|
||||
description: "Don't use an ambient module declaration of the current package; use a normal module.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "functionality",
|
||||
typescriptOnly: true,
|
||||
};
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "no-declare-current-package",
|
||||
description: "Don't use an ambient module declaration of the current package; use a normal module.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "functionality",
|
||||
typescriptOnly: true
|
||||
};
|
||||
|
||||
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
|
||||
if (!sourceFile.isDeclarationFile) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const packageName = getCommonDirectoryName(program.getRootFileNames());
|
||||
return this.applyWithFunction(sourceFile, ctx => walk(ctx, packageName));
|
||||
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
|
||||
if (!sourceFile.isDeclarationFile) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const packageName = getCommonDirectoryName(program.getRootFileNames());
|
||||
return this.applyWithFunction(sourceFile, ctx => walk(ctx, packageName));
|
||||
}
|
||||
}
|
||||
|
||||
function walk(ctx: Lint.WalkContext<void>, packageName: string): void {
|
||||
for (const statement of ctx.sourceFile.statements) {
|
||||
if (ts.isModuleDeclaration(statement) && ts.isStringLiteral(statement.name)) {
|
||||
const { text } = statement.name;
|
||||
if (text === packageName || text.startsWith(`${packageName}/`)) {
|
||||
const preferred = text === packageName ? '"index.d.ts"' : `"${text}.d.ts" or "${text}/index.d.ts`;
|
||||
ctx.addFailureAtNode(statement.name, failure(
|
||||
Rule.metadata.ruleName,
|
||||
`Instead of declaring a module with \`declare module "${text}"\`, ` +
|
||||
`write its contents in directly in ${preferred}.`));
|
||||
}
|
||||
}
|
||||
for (const statement of ctx.sourceFile.statements) {
|
||||
if (ts.isModuleDeclaration(statement) && ts.isStringLiteral(statement.name)) {
|
||||
const { text } = statement.name;
|
||||
if (text === packageName || text.startsWith(`${packageName}/`)) {
|
||||
const preferred = text === packageName ? '"index.d.ts"' : `"${text}.d.ts" or "${text}/index.d.ts`;
|
||||
ctx.addFailureAtNode(
|
||||
statement.name,
|
||||
failure(
|
||||
Rule.metadata.ruleName,
|
||||
`Instead of declaring a module with \`declare module "${text}"\`, ` +
|
||||
`write its contents in directly in ${preferred}.`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,48 +4,55 @@ import * as ts from "typescript";
|
||||
import { eachModuleStatement, failure, getModuleDeclarationStatements } from "../util";
|
||||
|
||||
export class Rule extends Lint.Rules.TypedRule {
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "no-import-default-of-export-equals",
|
||||
description: "Forbid a default import to reference an `export =` module.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "functionality",
|
||||
typescriptOnly: true,
|
||||
};
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "no-import-default-of-export-equals",
|
||||
description: "Forbid a default import to reference an `export =` module.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "functionality",
|
||||
typescriptOnly: true
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
static FAILURE_STRING(importName: string, moduleName: string): string {
|
||||
return failure(
|
||||
Rule.metadata.ruleName,
|
||||
`The module ${moduleName} uses \`export = \`. Import with \`import ${importName} = require(${moduleName})\`.`);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
static FAILURE_STRING(importName: string, moduleName: string): string {
|
||||
return failure(
|
||||
Rule.metadata.ruleName,
|
||||
`The module ${moduleName} uses \`export = \`. Import with \`import ${importName} = require(${moduleName})\`.`
|
||||
);
|
||||
}
|
||||
|
||||
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, ctx => walk(ctx, program.getTypeChecker()));
|
||||
}
|
||||
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, ctx => walk(ctx, program.getTypeChecker()));
|
||||
}
|
||||
}
|
||||
|
||||
function walk(ctx: Lint.WalkContext<void>, checker: ts.TypeChecker): void {
|
||||
eachModuleStatement(ctx.sourceFile, statement => {
|
||||
if (!ts.isImportDeclaration(statement)) {
|
||||
return;
|
||||
}
|
||||
const defaultName = statement.importClause && statement.importClause.name;
|
||||
if (!defaultName) {
|
||||
return;
|
||||
}
|
||||
const sym = checker.getSymbolAtLocation(statement.moduleSpecifier);
|
||||
if (sym && sym.declarations && sym.declarations.some(d => {
|
||||
const statements = getStatements(d);
|
||||
return statements !== undefined && statements.some(s => ts.isExportAssignment(s) && !!s.isExportEquals);
|
||||
})) {
|
||||
ctx.addFailureAtNode(defaultName, Rule.FAILURE_STRING(defaultName.text, statement.moduleSpecifier.getText()));
|
||||
}
|
||||
});
|
||||
eachModuleStatement(ctx.sourceFile, statement => {
|
||||
if (!ts.isImportDeclaration(statement)) {
|
||||
return;
|
||||
}
|
||||
const defaultName = statement.importClause && statement.importClause.name;
|
||||
if (!defaultName) {
|
||||
return;
|
||||
}
|
||||
const sym = checker.getSymbolAtLocation(statement.moduleSpecifier);
|
||||
if (
|
||||
sym &&
|
||||
sym.declarations &&
|
||||
sym.declarations.some(d => {
|
||||
const statements = getStatements(d);
|
||||
return statements !== undefined && statements.some(s => ts.isExportAssignment(s) && !!s.isExportEquals);
|
||||
})
|
||||
) {
|
||||
ctx.addFailureAtNode(defaultName, Rule.FAILURE_STRING(defaultName.text, statement.moduleSpecifier.getText()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getStatements(decl: ts.Declaration): readonly ts.Statement[] | undefined {
|
||||
return ts.isSourceFile(decl) ? decl.statements
|
||||
: ts.isModuleDeclaration(decl) ? getModuleDeclarationStatements(decl)
|
||||
: undefined;
|
||||
return ts.isSourceFile(decl)
|
||||
? decl.statements
|
||||
: ts.isModuleDeclaration(decl)
|
||||
? getModuleDeclarationStatements(decl)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
@@ -4,33 +4,34 @@ import * as ts from "typescript";
|
||||
import { failure } from "../util";
|
||||
|
||||
export class Rule extends Lint.Rules.TypedRule {
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "no-outside-dependencies",
|
||||
description: "Don't import things in `DefinitelyTyped/node_modules`.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "functionality",
|
||||
typescriptOnly: true,
|
||||
};
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "no-outside-dependencies",
|
||||
description: "Don't import things in `DefinitelyTyped/node_modules`.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "functionality",
|
||||
typescriptOnly: true
|
||||
};
|
||||
|
||||
applyWithProgram(_sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
|
||||
if (seenPrograms.has(program)) {
|
||||
return [];
|
||||
}
|
||||
seenPrograms.add(program);
|
||||
|
||||
const failures: Lint.RuleFailure[] = [];
|
||||
for (const sourceFile of program.getSourceFiles()) {
|
||||
const { fileName } = sourceFile;
|
||||
if (fileName.includes("/DefinitelyTyped/node_modules/") && !program.isSourceFileDefaultLibrary(sourceFile)) {
|
||||
const msg = failure(
|
||||
Rule.metadata.ruleName,
|
||||
`File ${fileName} comes from a \`node_modules\` but is not declared in this type's \`package.json\`. `);
|
||||
failures.push(new Lint.RuleFailure(sourceFile, 0, 1, msg, Rule.metadata.ruleName));
|
||||
}
|
||||
}
|
||||
return failures;
|
||||
applyWithProgram(_sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
|
||||
if (seenPrograms.has(program)) {
|
||||
return [];
|
||||
}
|
||||
seenPrograms.add(program);
|
||||
|
||||
const failures: Lint.RuleFailure[] = [];
|
||||
for (const sourceFile of program.getSourceFiles()) {
|
||||
const { fileName } = sourceFile;
|
||||
if (fileName.includes("/DefinitelyTyped/node_modules/") && !program.isSourceFileDefaultLibrary(sourceFile)) {
|
||||
const msg = failure(
|
||||
Rule.metadata.ruleName,
|
||||
`File ${fileName} comes from a \`node_modules\` but is not declared in this type's \`package.json\`. `
|
||||
);
|
||||
failures.push(new Lint.RuleFailure(sourceFile, 0, 1, msg, Rule.metadata.ruleName));
|
||||
}
|
||||
}
|
||||
return failures;
|
||||
}
|
||||
}
|
||||
|
||||
const seenPrograms = new WeakSet<ts.Program>();
|
||||
|
||||
@@ -4,83 +4,83 @@ import * as ts from "typescript";
|
||||
import { failure } from "../util";
|
||||
|
||||
export class Rule extends Lint.Rules.AbstractRule {
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "no-padding",
|
||||
description: "Forbids a blank line after `(` / `[` / `{`, or before `)` / `]` / `}`.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "style",
|
||||
typescriptOnly: true,
|
||||
};
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "no-padding",
|
||||
description: "Forbids a blank line after `(` / `[` / `{`, or before `)` / `]` / `}`.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "style",
|
||||
typescriptOnly: true
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
static FAILURE_STRING(kind: "before" | "after", token: ts.SyntaxKind) {
|
||||
return failure(
|
||||
Rule.metadata.ruleName,
|
||||
`Don't leave a blank line ${kind} '${ts.tokenToString(token)}'.`);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
static FAILURE_STRING(kind: "before" | "after", token: ts.SyntaxKind) {
|
||||
return failure(Rule.metadata.ruleName, `Don't leave a blank line ${kind} '${ts.tokenToString(token)}'.`);
|
||||
}
|
||||
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, walk);
|
||||
}
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, walk);
|
||||
}
|
||||
}
|
||||
|
||||
function walk(ctx: Lint.WalkContext<void>): void {
|
||||
const { sourceFile } = ctx;
|
||||
const { sourceFile } = ctx;
|
||||
|
||||
function fail(kind: "before" | "after", child: ts.Node): void {
|
||||
ctx.addFailureAtNode(child, Rule.FAILURE_STRING(kind, child.kind));
|
||||
function fail(kind: "before" | "after", child: ts.Node): void {
|
||||
ctx.addFailureAtNode(child, Rule.FAILURE_STRING(kind, child.kind));
|
||||
}
|
||||
|
||||
sourceFile.forEachChild(function cb(node) {
|
||||
const children = node.getChildren();
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i];
|
||||
switch (child.kind) {
|
||||
case ts.SyntaxKind.OpenParenToken:
|
||||
case ts.SyntaxKind.OpenBracketToken:
|
||||
case ts.SyntaxKind.OpenBraceToken:
|
||||
if (i < children.length - 1 && blankLineInBetween(child.getEnd(), children[i + 1].getStart())) {
|
||||
fail("after", child);
|
||||
}
|
||||
break;
|
||||
|
||||
case ts.SyntaxKind.CloseParenToken:
|
||||
case ts.SyntaxKind.CloseBracketToken:
|
||||
case ts.SyntaxKind.CloseBraceToken:
|
||||
if (i > 0 && blankLineInBetween(child.getStart() - 1, children[i - 1].getEnd() - 1)) {
|
||||
fail("before", child);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
cb(child);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Looks for two newlines (with nothing else in between besides whitespace)
|
||||
function blankLineInBetween(start: number, end: number): boolean {
|
||||
const step = start < end ? 1 : -1;
|
||||
let seenLine = false;
|
||||
for (let i = start; i !== end; i += step) {
|
||||
switch (sourceFile.text[i]) {
|
||||
case "\n":
|
||||
if (seenLine) {
|
||||
return true;
|
||||
} else {
|
||||
seenLine = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case " ":
|
||||
case "\t":
|
||||
case "\r":
|
||||
break;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
sourceFile.forEachChild(function cb(node) {
|
||||
const children = node.getChildren();
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i];
|
||||
switch (child.kind) {
|
||||
case ts.SyntaxKind.OpenParenToken:
|
||||
case ts.SyntaxKind.OpenBracketToken:
|
||||
case ts.SyntaxKind.OpenBraceToken:
|
||||
if (i < children.length - 1 && blankLineInBetween(child.getEnd(), children[i + 1].getStart())) {
|
||||
fail("after", child);
|
||||
}
|
||||
break;
|
||||
|
||||
case ts.SyntaxKind.CloseParenToken:
|
||||
case ts.SyntaxKind.CloseBracketToken:
|
||||
case ts.SyntaxKind.CloseBraceToken:
|
||||
if (i > 0 && blankLineInBetween(child.getStart() - 1, children[i - 1].getEnd() - 1)) {
|
||||
fail("before", child);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
cb(child);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Looks for two newlines (with nothing else in between besides whitespace)
|
||||
function blankLineInBetween(start: number, end: number): boolean {
|
||||
const step = start < end ? 1 : -1;
|
||||
let seenLine = false;
|
||||
for (let i = start; i !== end; i += step) {
|
||||
switch (sourceFile.text[i]) {
|
||||
case "\n":
|
||||
if (seenLine) {
|
||||
return true;
|
||||
} else {
|
||||
seenLine = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case " ": case "\t": case "\r":
|
||||
break;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,249 +5,249 @@ import * as Lint from "tslint";
|
||||
import * as ts from "typescript";
|
||||
|
||||
export class Rule extends Lint.Rules.AbstractRule {
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "no-redundant-jsdoc",
|
||||
description: "Forbids JSDoc which duplicates TypeScript functionality.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
optionExamples: [true],
|
||||
type: "style",
|
||||
typescriptOnly: true,
|
||||
};
|
||||
static readonly FAILURE_STRING_REDUNDANT_TYPE =
|
||||
"Type annotation in JSDoc is redundant in TypeScript code.";
|
||||
static readonly FAILURE_STRING_EMPTY =
|
||||
"JSDoc comment is empty.";
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
static FAILURE_STRING_REDUNDANT_TAG(tagName: string): string {
|
||||
return `JSDoc tag '@${tagName}' is redundant in TypeScript code.`;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
static FAILURE_STRING_NO_COMMENT(tagName: string): string {
|
||||
return `'@${tagName}' is redundant in TypeScript code if it has no description.`;
|
||||
}
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "no-redundant-jsdoc",
|
||||
description: "Forbids JSDoc which duplicates TypeScript functionality.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
optionExamples: [true],
|
||||
type: "style",
|
||||
typescriptOnly: true
|
||||
};
|
||||
static readonly FAILURE_STRING_REDUNDANT_TYPE = "Type annotation in JSDoc is redundant in TypeScript code.";
|
||||
static readonly FAILURE_STRING_EMPTY = "JSDoc comment is empty.";
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
static FAILURE_STRING_REDUNDANT_TAG(tagName: string): string {
|
||||
return `JSDoc tag '@${tagName}' is redundant in TypeScript code.`;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
static FAILURE_STRING_NO_COMMENT(tagName: string): string {
|
||||
return `'@${tagName}' is redundant in TypeScript code if it has no description.`;
|
||||
}
|
||||
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, walk);
|
||||
}
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, walk);
|
||||
}
|
||||
}
|
||||
|
||||
function walk(ctx: Lint.WalkContext<void>): void {
|
||||
const { sourceFile } = ctx;
|
||||
// Intentionally exclude EndOfFileToken: it can have JSDoc, but it is only relevant in JavaScript files
|
||||
return sourceFile.statements.forEach(function cb(node: ts.Node): void {
|
||||
if (node.kind !== ts.SyntaxKind.EndOfFileToken && (ts as any).hasJSDocNodes(node)) {
|
||||
for (const jd of (node as any).jsDoc) {
|
||||
const { tags } = jd as ts.JSDoc;
|
||||
if (tags === undefined || tags.length === 0) {
|
||||
if (jd.comment === undefined) {
|
||||
ctx.addFailureAtNode(
|
||||
jd,
|
||||
Rule.FAILURE_STRING_EMPTY,
|
||||
Lint.Replacement.deleteFromTo(jd.getStart(sourceFile), jd.getEnd()));
|
||||
}
|
||||
} else {
|
||||
for (const tag of tags) {
|
||||
checkTag(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ts.forEachChild(node, cb);
|
||||
});
|
||||
|
||||
function checkTag(tag: ts.JSDocTag): void {
|
||||
const jsdocSeeTag = (ts.SyntaxKind as any).JSDocSeeTag || 0;
|
||||
const jsdocDeprecatedTag = (ts.SyntaxKind as any).JSDocDeprecatedTag || 0;
|
||||
switch (tag.kind) {
|
||||
case jsdocSeeTag:
|
||||
case jsdocDeprecatedTag:
|
||||
case ts.SyntaxKind.JSDocAuthorTag:
|
||||
// @deprecated and @see always have meaning
|
||||
break;
|
||||
case ts.SyntaxKind.JSDocTag: {
|
||||
const { tagName } = tag;
|
||||
const { text } = tagName;
|
||||
// Allow "default" in an ambient context (since you can't write an initializer in an ambient context)
|
||||
if (redundantTags.has(text) && !(text === "default" && isInAmbientContext(tag))) {
|
||||
ctx.addFailureAtNode(tagName, Rule.FAILURE_STRING_REDUNDANT_TAG(text), removeTag(tag, sourceFile));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore (fallthrough)
|
||||
case ts.SyntaxKind.JSDocTemplateTag:
|
||||
if (tag.comment !== "") {
|
||||
break;
|
||||
}
|
||||
// falls through
|
||||
|
||||
case ts.SyntaxKind.JSDocPublicTag:
|
||||
case ts.SyntaxKind.JSDocPrivateTag:
|
||||
case ts.SyntaxKind.JSDocProtectedTag:
|
||||
case ts.SyntaxKind.JSDocClassTag:
|
||||
case ts.SyntaxKind.JSDocTypeTag:
|
||||
case ts.SyntaxKind.JSDocTypedefTag:
|
||||
case ts.SyntaxKind.JSDocReadonlyTag:
|
||||
case ts.SyntaxKind.JSDocPropertyTag:
|
||||
case ts.SyntaxKind.JSDocAugmentsTag:
|
||||
case ts.SyntaxKind.JSDocImplementsTag:
|
||||
case ts.SyntaxKind.JSDocCallbackTag:
|
||||
case ts.SyntaxKind.JSDocThisTag:
|
||||
case ts.SyntaxKind.JSDocEnumTag:
|
||||
|
||||
// Always redundant
|
||||
ctx.addFailureAtNode(
|
||||
tag.tagName,
|
||||
Rule.FAILURE_STRING_REDUNDANT_TAG(tag.tagName.text),
|
||||
removeTag(tag, sourceFile));
|
||||
break;
|
||||
|
||||
case ts.SyntaxKind.JSDocReturnTag:
|
||||
case ts.SyntaxKind.JSDocParameterTag: {
|
||||
const { typeExpression, comment } = tag as ts.JSDocReturnTag | ts.JSDocParameterTag;
|
||||
const noComment = comment === "";
|
||||
if (typeExpression !== undefined) {
|
||||
// If noComment, we will just completely remove it in the other fix
|
||||
const fix = noComment ? undefined : removeTypeExpression(typeExpression, sourceFile);
|
||||
ctx.addFailureAtNode(typeExpression, Rule.FAILURE_STRING_REDUNDANT_TYPE, fix);
|
||||
}
|
||||
if (noComment) {
|
||||
// Redundant if no documentation
|
||||
ctx.addFailureAtNode(
|
||||
tag.tagName,
|
||||
Rule.FAILURE_STRING_NO_COMMENT(tag.tagName.text),
|
||||
removeTag(tag, sourceFile));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unexpected tag kind: ${ts.SyntaxKind[tag.kind]}`);
|
||||
const { sourceFile } = ctx;
|
||||
// Intentionally exclude EndOfFileToken: it can have JSDoc, but it is only relevant in JavaScript files
|
||||
return sourceFile.statements.forEach(function cb(node: ts.Node): void {
|
||||
if (node.kind !== ts.SyntaxKind.EndOfFileToken && (ts as any).hasJSDocNodes(node)) {
|
||||
for (const jd of (node as any).jsDoc) {
|
||||
const { tags } = jd as ts.JSDoc;
|
||||
if (tags === undefined || tags.length === 0) {
|
||||
if (jd.comment === undefined) {
|
||||
ctx.addFailureAtNode(
|
||||
jd,
|
||||
Rule.FAILURE_STRING_EMPTY,
|
||||
Lint.Replacement.deleteFromTo(jd.getStart(sourceFile), jd.getEnd())
|
||||
);
|
||||
}
|
||||
} else {
|
||||
for (const tag of tags) {
|
||||
checkTag(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ts.forEachChild(node, cb);
|
||||
});
|
||||
|
||||
function checkTag(tag: ts.JSDocTag): void {
|
||||
const jsdocSeeTag = (ts.SyntaxKind as any).JSDocSeeTag || 0;
|
||||
const jsdocDeprecatedTag = (ts.SyntaxKind as any).JSDocDeprecatedTag || 0;
|
||||
switch (tag.kind) {
|
||||
case jsdocSeeTag:
|
||||
case jsdocDeprecatedTag:
|
||||
case ts.SyntaxKind.JSDocAuthorTag:
|
||||
// @deprecated and @see always have meaning
|
||||
break;
|
||||
case ts.SyntaxKind.JSDocTag: {
|
||||
const { tagName } = tag;
|
||||
const { text } = tagName;
|
||||
// Allow "default" in an ambient context (since you can't write an initializer in an ambient context)
|
||||
if (redundantTags.has(text) && !(text === "default" && isInAmbientContext(tag))) {
|
||||
ctx.addFailureAtNode(tagName, Rule.FAILURE_STRING_REDUNDANT_TAG(text), removeTag(tag, sourceFile));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore (fallthrough)
|
||||
case ts.SyntaxKind.JSDocTemplateTag:
|
||||
if (tag.comment !== "") {
|
||||
break;
|
||||
}
|
||||
// falls through
|
||||
|
||||
case ts.SyntaxKind.JSDocPublicTag:
|
||||
case ts.SyntaxKind.JSDocPrivateTag:
|
||||
case ts.SyntaxKind.JSDocProtectedTag:
|
||||
case ts.SyntaxKind.JSDocClassTag:
|
||||
case ts.SyntaxKind.JSDocTypeTag:
|
||||
case ts.SyntaxKind.JSDocTypedefTag:
|
||||
case ts.SyntaxKind.JSDocReadonlyTag:
|
||||
case ts.SyntaxKind.JSDocPropertyTag:
|
||||
case ts.SyntaxKind.JSDocAugmentsTag:
|
||||
case ts.SyntaxKind.JSDocImplementsTag:
|
||||
case ts.SyntaxKind.JSDocCallbackTag:
|
||||
case ts.SyntaxKind.JSDocThisTag:
|
||||
case ts.SyntaxKind.JSDocEnumTag:
|
||||
// Always redundant
|
||||
ctx.addFailureAtNode(
|
||||
tag.tagName,
|
||||
Rule.FAILURE_STRING_REDUNDANT_TAG(tag.tagName.text),
|
||||
removeTag(tag, sourceFile)
|
||||
);
|
||||
break;
|
||||
|
||||
case ts.SyntaxKind.JSDocReturnTag:
|
||||
case ts.SyntaxKind.JSDocParameterTag: {
|
||||
const { typeExpression, comment } = tag as ts.JSDocReturnTag | ts.JSDocParameterTag;
|
||||
const noComment = comment === "";
|
||||
if (typeExpression !== undefined) {
|
||||
// If noComment, we will just completely remove it in the other fix
|
||||
const fix = noComment ? undefined : removeTypeExpression(typeExpression, sourceFile);
|
||||
ctx.addFailureAtNode(typeExpression, Rule.FAILURE_STRING_REDUNDANT_TYPE, fix);
|
||||
}
|
||||
if (noComment) {
|
||||
// Redundant if no documentation
|
||||
ctx.addFailureAtNode(
|
||||
tag.tagName,
|
||||
Rule.FAILURE_STRING_NO_COMMENT(tag.tagName.text),
|
||||
removeTag(tag, sourceFile)
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unexpected tag kind: ${ts.SyntaxKind[tag.kind]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeTag(tag: ts.JSDocTag, sourceFile: ts.SourceFile): Lint.Replacement | undefined {
|
||||
const { text } = sourceFile;
|
||||
const jsdoc = tag.parent;
|
||||
if (jsdoc.kind === ts.SyntaxKind.JSDocTypeLiteral) {
|
||||
return undefined;
|
||||
}
|
||||
const { text } = sourceFile;
|
||||
const jsdoc = tag.parent;
|
||||
if (jsdoc.kind === ts.SyntaxKind.JSDocTypeLiteral) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (jsdoc.comment === undefined && jsdoc.tags!.length === 1) {
|
||||
// This is the only tag -- remove the whole comment
|
||||
return Lint.Replacement.deleteFromTo(jsdoc.getStart(sourceFile), jsdoc.getEnd());
|
||||
}
|
||||
if (jsdoc.comment === undefined && jsdoc.tags!.length === 1) {
|
||||
// This is the only tag -- remove the whole comment
|
||||
return Lint.Replacement.deleteFromTo(jsdoc.getStart(sourceFile), jsdoc.getEnd());
|
||||
}
|
||||
|
||||
let start = tag.getStart(sourceFile);
|
||||
assert(text[start] === "@");
|
||||
let start = tag.getStart(sourceFile);
|
||||
assert(text[start] === "@");
|
||||
start--;
|
||||
while (ts.isWhiteSpaceSingleLine(text.charCodeAt(start))) {
|
||||
start--;
|
||||
while (ts.isWhiteSpaceSingleLine(text.charCodeAt(start))) {
|
||||
start--;
|
||||
}
|
||||
if (text[start] !== "*") {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
if (text[start] !== "*") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let end = tag.getEnd();
|
||||
let end = tag.getEnd();
|
||||
|
||||
// For some tags, like `@param`, `end` will be the start of the next tag.
|
||||
// For some tags, like `@name`, `end` will be before the start of the comment.
|
||||
// And `@typedef` doesn't end until the last `@property` tag attached to it ends.
|
||||
switch (tag.tagName.text) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore (fallthrough)
|
||||
case "param": {
|
||||
const { isBracketed, isNameFirst, typeExpression } = tag as ts.JSDocParameterTag;
|
||||
if (!isBracketed && !(isNameFirst && typeExpression !== undefined)) {
|
||||
break;
|
||||
}
|
||||
// falls through
|
||||
}
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
case "name":
|
||||
case "return":
|
||||
case "returns":
|
||||
case "interface":
|
||||
case "default":
|
||||
case "memberof":
|
||||
case "memberOf":
|
||||
case "method":
|
||||
case "type":
|
||||
case "class":
|
||||
case "property":
|
||||
case "function":
|
||||
end--; // Might end with "\n" (test with just `@return` with no comment or type)
|
||||
// For some reason, for "@name", "end" is before the start of the comment part of the tag.
|
||||
// Also for "param" if the name is optional as in `@param {number} [x]`
|
||||
while (!ts.isLineBreak(text.charCodeAt(end))) {
|
||||
end++;
|
||||
}
|
||||
end++;
|
||||
// For some tags, like `@param`, `end` will be the start of the next tag.
|
||||
// For some tags, like `@name`, `end` will be before the start of the comment.
|
||||
// And `@typedef` doesn't end until the last `@property` tag attached to it ends.
|
||||
switch (tag.tagName.text) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore (fallthrough)
|
||||
case "param": {
|
||||
const { isBracketed, isNameFirst, typeExpression } = tag as ts.JSDocParameterTag;
|
||||
if (!isBracketed && !(isNameFirst && typeExpression !== undefined)) {
|
||||
break;
|
||||
}
|
||||
// falls through
|
||||
}
|
||||
while (ts.isWhiteSpaceSingleLine(text.charCodeAt(end))) {
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
case "name":
|
||||
case "return":
|
||||
case "returns":
|
||||
case "interface":
|
||||
case "default":
|
||||
case "memberof":
|
||||
case "memberOf":
|
||||
case "method":
|
||||
case "type":
|
||||
case "class":
|
||||
case "property":
|
||||
case "function":
|
||||
end--; // Might end with "\n" (test with just `@return` with no comment or type)
|
||||
// For some reason, for "@name", "end" is before the start of the comment part of the tag.
|
||||
// Also for "param" if the name is optional as in `@param {number} [x]`
|
||||
while (!ts.isLineBreak(text.charCodeAt(end))) {
|
||||
end++;
|
||||
}
|
||||
if (text[end] !== "*") {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
end++;
|
||||
}
|
||||
while (ts.isWhiteSpaceSingleLine(text.charCodeAt(end))) {
|
||||
end++;
|
||||
}
|
||||
if (text[end] !== "*") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return Lint.Replacement.deleteFromTo(start, end);
|
||||
return Lint.Replacement.deleteFromTo(start, end);
|
||||
}
|
||||
|
||||
function removeTypeExpression(
|
||||
typeExpression: ts.JSDocTypeExpression,
|
||||
sourceFile: ts.SourceFile,
|
||||
typeExpression: ts.JSDocTypeExpression,
|
||||
sourceFile: ts.SourceFile
|
||||
): Lint.Replacement | undefined {
|
||||
const start = typeExpression.getStart(sourceFile);
|
||||
let end = typeExpression.getEnd();
|
||||
const { text } = sourceFile;
|
||||
if (text[start] !== "{" || text[end - 1] !== "}") {
|
||||
// TypeScript parser messed up -- give up
|
||||
return undefined;
|
||||
}
|
||||
if (ts.isWhiteSpaceSingleLine(text.charCodeAt(end))) {
|
||||
end++;
|
||||
}
|
||||
return Lint.Replacement.deleteFromTo(start, end);
|
||||
const start = typeExpression.getStart(sourceFile);
|
||||
let end = typeExpression.getEnd();
|
||||
const { text } = sourceFile;
|
||||
if (text[start] !== "{" || text[end - 1] !== "}") {
|
||||
// TypeScript parser messed up -- give up
|
||||
return undefined;
|
||||
}
|
||||
if (ts.isWhiteSpaceSingleLine(text.charCodeAt(end))) {
|
||||
end++;
|
||||
}
|
||||
return Lint.Replacement.deleteFromTo(start, end);
|
||||
}
|
||||
|
||||
// TODO: improve once https://github.com/Microsoft/TypeScript/pull/17831 is in
|
||||
function isInAmbientContext(node: ts.Node): boolean {
|
||||
return ts.isSourceFile(node)
|
||||
? node.isDeclarationFile
|
||||
: Lint.hasModifier(node.modifiers, ts.SyntaxKind.DeclareKeyword) || isInAmbientContext(node.parent!);
|
||||
return ts.isSourceFile(node)
|
||||
? node.isDeclarationFile
|
||||
: Lint.hasModifier(node.modifiers, ts.SyntaxKind.DeclareKeyword) || isInAmbientContext(node.parent!);
|
||||
}
|
||||
|
||||
const redundantTags = new Set([
|
||||
"abstract",
|
||||
"access",
|
||||
"class",
|
||||
"constant",
|
||||
"constructs",
|
||||
"default",
|
||||
"enum",
|
||||
"export",
|
||||
"exports",
|
||||
"function",
|
||||
"global",
|
||||
"inherits",
|
||||
"interface",
|
||||
"instance",
|
||||
"member",
|
||||
"method",
|
||||
"memberof",
|
||||
"memberOf",
|
||||
"mixes",
|
||||
"mixin",
|
||||
"module",
|
||||
"name",
|
||||
"namespace",
|
||||
"override",
|
||||
"property",
|
||||
"requires",
|
||||
"static",
|
||||
"this",
|
||||
"abstract",
|
||||
"access",
|
||||
"class",
|
||||
"constant",
|
||||
"constructs",
|
||||
"default",
|
||||
"enum",
|
||||
"export",
|
||||
"exports",
|
||||
"function",
|
||||
"global",
|
||||
"inherits",
|
||||
"interface",
|
||||
"instance",
|
||||
"member",
|
||||
"method",
|
||||
"memberof",
|
||||
"memberOf",
|
||||
"mixes",
|
||||
"mixin",
|
||||
"module",
|
||||
"name",
|
||||
"namespace",
|
||||
"override",
|
||||
"property",
|
||||
"requires",
|
||||
"static",
|
||||
"this"
|
||||
]);
|
||||
|
||||
@@ -4,49 +4,50 @@ import * as ts from "typescript";
|
||||
import { failure } from "../util";
|
||||
|
||||
export class Rule extends Lint.Rules.TypedRule {
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "no-relative-import-in-test",
|
||||
description: "Forbids test (non-declaration) files to use relative imports.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "functionality",
|
||||
typescriptOnly: false,
|
||||
};
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "no-relative-import-in-test",
|
||||
description: "Forbids test (non-declaration) files to use relative imports.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "functionality",
|
||||
typescriptOnly: false
|
||||
};
|
||||
|
||||
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
|
||||
if (sourceFile.isDeclarationFile) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.applyWithFunction(sourceFile, ctx => walk(ctx, program.getTypeChecker()));
|
||||
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
|
||||
if (sourceFile.isDeclarationFile) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.applyWithFunction(sourceFile, ctx => walk(ctx, program.getTypeChecker()));
|
||||
}
|
||||
}
|
||||
|
||||
const failureMessage = failure(
|
||||
Rule.metadata.ruleName,
|
||||
"Test file should not use a relative import. Use a global import as if this were a user of the package.");
|
||||
Rule.metadata.ruleName,
|
||||
"Test file should not use a relative import. Use a global import as if this were a user of the package."
|
||||
);
|
||||
|
||||
function walk(ctx: Lint.WalkContext<void>, checker: ts.TypeChecker): void {
|
||||
const { sourceFile } = ctx;
|
||||
const { sourceFile } = ctx;
|
||||
|
||||
for (const i of sourceFile.imports) {
|
||||
if (i.text.startsWith(".")) {
|
||||
const moduleSymbol = checker.getSymbolAtLocation(i);
|
||||
if (!moduleSymbol || !moduleSymbol.declarations) {
|
||||
continue;
|
||||
}
|
||||
for (const i of sourceFile.imports) {
|
||||
if (i.text.startsWith(".")) {
|
||||
const moduleSymbol = checker.getSymbolAtLocation(i);
|
||||
if (!moduleSymbol || !moduleSymbol.declarations) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const decl of moduleSymbol.declarations) {
|
||||
if (decl.kind === ts.SyntaxKind.SourceFile && (decl as ts.SourceFile).isDeclarationFile) {
|
||||
ctx.addFailureAtNode(i, failureMessage);
|
||||
}
|
||||
}
|
||||
for (const decl of moduleSymbol.declarations) {
|
||||
if (decl.kind === ts.SyntaxKind.SourceFile && (decl as ts.SourceFile).isDeclarationFile) {
|
||||
ctx.addFailureAtNode(i, failureMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare module "typescript" {
|
||||
interface SourceFile {
|
||||
imports: readonly ts.StringLiteral[];
|
||||
}
|
||||
interface SourceFile {
|
||||
imports: readonly ts.StringLiteral[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,33 +4,34 @@ import * as ts from "typescript";
|
||||
import { failure, getCommonDirectoryName } from "../util";
|
||||
|
||||
export class Rule extends Lint.Rules.TypedRule {
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "no-self-import",
|
||||
description: "Forbids declaration files to import the current package using a global import.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "functionality",
|
||||
typescriptOnly: false,
|
||||
};
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "no-self-import",
|
||||
description: "Forbids declaration files to import the current package using a global import.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "functionality",
|
||||
typescriptOnly: false
|
||||
};
|
||||
|
||||
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
|
||||
if (!sourceFile.isDeclarationFile) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const name = getCommonDirectoryName(program.getRootFileNames());
|
||||
return this.applyWithFunction(sourceFile, ctx => walk(ctx, name));
|
||||
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
|
||||
if (!sourceFile.isDeclarationFile) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const name = getCommonDirectoryName(program.getRootFileNames());
|
||||
return this.applyWithFunction(sourceFile, ctx => walk(ctx, name));
|
||||
}
|
||||
}
|
||||
|
||||
const failureMessage = failure(
|
||||
Rule.metadata.ruleName,
|
||||
"Declaration file should not use a global import of itself. Use a relative import.");
|
||||
Rule.metadata.ruleName,
|
||||
"Declaration file should not use a global import of itself. Use a relative import."
|
||||
);
|
||||
|
||||
function walk(ctx: Lint.WalkContext<void>, packageName: string): void {
|
||||
for (const i of ctx.sourceFile.imports) {
|
||||
if (i.text === packageName || i.text.startsWith(packageName + "/")) {
|
||||
ctx.addFailureAtNode(i, failureMessage);
|
||||
}
|
||||
for (const i of ctx.sourceFile.imports) {
|
||||
if (i.text === packageName || i.text.startsWith(packageName + "/")) {
|
||||
ctx.addFailureAtNode(i, failureMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,51 +4,52 @@ import * as ts from "typescript";
|
||||
import { failure } from "../util";
|
||||
|
||||
export class Rule extends Lint.Rules.AbstractRule {
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "no-single-declare-module",
|
||||
description: "Don't use an ambient module declaration if there's just one -- write it as a normal module.",
|
||||
rationale: "Cuts down on nesting",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "style",
|
||||
typescriptOnly: true,
|
||||
};
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "no-single-declare-module",
|
||||
description: "Don't use an ambient module declaration if there's just one -- write it as a normal module.",
|
||||
rationale: "Cuts down on nesting",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "style",
|
||||
typescriptOnly: true
|
||||
};
|
||||
|
||||
static FAILURE_STRING = failure(
|
||||
Rule.metadata.ruleName,
|
||||
"File has only 1 ambient module declaration. Move the contents outside the ambient module block, rename the file to match the ambient module name, and remove the block.");
|
||||
static FAILURE_STRING = failure(
|
||||
Rule.metadata.ruleName,
|
||||
"File has only 1 ambient module declaration. Move the contents outside the ambient module block, rename the file to match the ambient module name, and remove the block."
|
||||
);
|
||||
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, walk);
|
||||
}
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, walk);
|
||||
}
|
||||
}
|
||||
|
||||
function walk(ctx: Lint.WalkContext<void>): void {
|
||||
const { sourceFile } = ctx;
|
||||
const { sourceFile } = ctx;
|
||||
|
||||
// If it's an external module, any module declarations inside are augmentations.
|
||||
if (ts.isExternalModule(sourceFile)) {
|
||||
// If it's an external module, any module declarations inside are augmentations.
|
||||
if (ts.isExternalModule(sourceFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let moduleDecl: ts.ModuleDeclaration | undefined;
|
||||
for (const statement of sourceFile.statements) {
|
||||
if (ts.isModuleDeclaration(statement) && ts.isStringLiteral(statement.name)) {
|
||||
if (statement.name.text.indexOf("*") !== -1) {
|
||||
// Ignore wildcard module declarations
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let moduleDecl: ts.ModuleDeclaration | undefined;
|
||||
for (const statement of sourceFile.statements) {
|
||||
if (ts.isModuleDeclaration(statement) && ts.isStringLiteral(statement.name)) {
|
||||
if (statement.name.text.indexOf('*') !== -1) {
|
||||
// Ignore wildcard module declarations
|
||||
return;
|
||||
}
|
||||
|
||||
if (moduleDecl === undefined) {
|
||||
moduleDecl = statement;
|
||||
} else {
|
||||
// Has more than 1 declaration
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (moduleDecl === undefined) {
|
||||
moduleDecl = statement;
|
||||
} else {
|
||||
// Has more than 1 declaration
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleDecl) {
|
||||
ctx.addFailureAtNode(moduleDecl, Rule.FAILURE_STRING);
|
||||
}
|
||||
if (moduleDecl) {
|
||||
ctx.addFailureAtNode(moduleDecl, Rule.FAILURE_STRING);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,28 +4,29 @@ import * as ts from "typescript";
|
||||
import { failure } from "../util";
|
||||
|
||||
export class Rule extends Lint.Rules.AbstractRule {
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "no-single-element-tuple-type",
|
||||
description: "Forbids `[T]`, which should be `T[]`.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "functionality",
|
||||
typescriptOnly: true,
|
||||
};
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "no-single-element-tuple-type",
|
||||
description: "Forbids `[T]`, which should be `T[]`.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "functionality",
|
||||
typescriptOnly: true
|
||||
};
|
||||
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, walk);
|
||||
}
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, walk);
|
||||
}
|
||||
}
|
||||
|
||||
function walk(ctx: Lint.WalkContext<void>): void {
|
||||
const { sourceFile } = ctx;
|
||||
sourceFile.forEachChild(function cb(node) {
|
||||
if (ts.isTupleTypeNode(node) && (node.elements ?? (node as any).elementTypes).length === 1) {
|
||||
ctx.addFailureAtNode(node, failure(
|
||||
Rule.metadata.ruleName,
|
||||
"Type [T] is a single-element tuple type. You probably meant T[]."));
|
||||
}
|
||||
node.forEachChild(cb);
|
||||
});
|
||||
const { sourceFile } = ctx;
|
||||
sourceFile.forEachChild(function cb(node) {
|
||||
if (ts.isTupleTypeNode(node) && (node.elements ?? (node as any).elementTypes).length === 1) {
|
||||
ctx.addFailureAtNode(
|
||||
node,
|
||||
failure(Rule.metadata.ruleName, "Type [T] is a single-element tuple type. You probably meant T[].")
|
||||
);
|
||||
}
|
||||
node.forEachChild(cb);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,118 +4,112 @@ import * as ts from "typescript";
|
||||
import { failure } from "../util";
|
||||
|
||||
export class Rule extends Lint.Rules.TypedRule {
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "no-unnecessary-generics",
|
||||
description: "Forbids signatures using a generic parameter only once.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "style",
|
||||
typescriptOnly: true,
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
static FAILURE_STRING(typeParameter: string) {
|
||||
return failure(
|
||||
Rule.metadata.ruleName,
|
||||
`Type parameter ${typeParameter} is used only once.`);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
static FAILURE_STRING_NEVER(typeParameter: string) {
|
||||
return failure(
|
||||
Rule.metadata.ruleName,
|
||||
`Type parameter ${typeParameter} is never used.`);
|
||||
}
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "no-unnecessary-generics",
|
||||
description: "Forbids signatures using a generic parameter only once.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "style",
|
||||
typescriptOnly: true
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
static FAILURE_STRING(typeParameter: string) {
|
||||
return failure(Rule.metadata.ruleName, `Type parameter ${typeParameter} is used only once.`);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
static FAILURE_STRING_NEVER(typeParameter: string) {
|
||||
return failure(Rule.metadata.ruleName, `Type parameter ${typeParameter} is never used.`);
|
||||
}
|
||||
|
||||
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, ctx => walk(ctx, program.getTypeChecker()));
|
||||
}
|
||||
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, ctx => walk(ctx, program.getTypeChecker()));
|
||||
}
|
||||
}
|
||||
|
||||
function walk(ctx: Lint.WalkContext<void>, checker: ts.TypeChecker): void {
|
||||
const { sourceFile } = ctx;
|
||||
sourceFile.forEachChild(function cb(node) {
|
||||
if (ts.isFunctionLike(node)) {
|
||||
checkSignature(node);
|
||||
}
|
||||
node.forEachChild(cb);
|
||||
});
|
||||
|
||||
function checkSignature(sig: ts.SignatureDeclaration) {
|
||||
if (!sig.typeParameters) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const tp of sig.typeParameters) {
|
||||
const typeParameter = tp.name.text;
|
||||
const res = getSoleUse(sig, assertDefined(checker.getSymbolAtLocation(tp.name)), checker);
|
||||
switch (res.type) {
|
||||
case "ok":
|
||||
break;
|
||||
case "sole":
|
||||
ctx.addFailureAtNode(res.soleUse, Rule.FAILURE_STRING(typeParameter));
|
||||
break;
|
||||
case "never":
|
||||
ctx.addFailureAtNode(tp, Rule.FAILURE_STRING_NEVER(typeParameter));
|
||||
break;
|
||||
default:
|
||||
assertNever(res);
|
||||
}
|
||||
}
|
||||
const { sourceFile } = ctx;
|
||||
sourceFile.forEachChild(function cb(node) {
|
||||
if (ts.isFunctionLike(node)) {
|
||||
checkSignature(node);
|
||||
}
|
||||
node.forEachChild(cb);
|
||||
});
|
||||
|
||||
function checkSignature(sig: ts.SignatureDeclaration) {
|
||||
if (!sig.typeParameters) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const tp of sig.typeParameters) {
|
||||
const typeParameter = tp.name.text;
|
||||
const res = getSoleUse(sig, assertDefined(checker.getSymbolAtLocation(tp.name)), checker);
|
||||
switch (res.type) {
|
||||
case "ok":
|
||||
break;
|
||||
case "sole":
|
||||
ctx.addFailureAtNode(res.soleUse, Rule.FAILURE_STRING(typeParameter));
|
||||
break;
|
||||
case "never":
|
||||
ctx.addFailureAtNode(tp, Rule.FAILURE_STRING_NEVER(typeParameter));
|
||||
break;
|
||||
default:
|
||||
assertNever(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Result =
|
||||
| { type: "ok" | "never" }
|
||||
| { type: "sole", soleUse: ts.Identifier };
|
||||
type Result = { type: "ok" | "never" } | { type: "sole"; soleUse: ts.Identifier };
|
||||
function getSoleUse(sig: ts.SignatureDeclaration, typeParameterSymbol: ts.Symbol, checker: ts.TypeChecker): Result {
|
||||
const exit = {};
|
||||
let soleUse: ts.Identifier | undefined;
|
||||
const exit = {};
|
||||
let soleUse: ts.Identifier | undefined;
|
||||
|
||||
try {
|
||||
if (sig.typeParameters) {
|
||||
for (const tp of sig.typeParameters) {
|
||||
if (tp.constraint) {
|
||||
recur(tp.constraint);
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (sig.typeParameters) {
|
||||
for (const tp of sig.typeParameters) {
|
||||
if (tp.constraint) {
|
||||
recur(tp.constraint);
|
||||
}
|
||||
for (const param of sig.parameters) {
|
||||
if (param.type) {
|
||||
recur(param.type);
|
||||
}
|
||||
}
|
||||
if (sig.type) {
|
||||
recur(sig.type);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err === exit) {
|
||||
return { type: "ok" };
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
for (const param of sig.parameters) {
|
||||
if (param.type) {
|
||||
recur(param.type);
|
||||
}
|
||||
}
|
||||
if (sig.type) {
|
||||
recur(sig.type);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err === exit) {
|
||||
return { type: "ok" };
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
return soleUse ? { type: "sole", soleUse } : { type: "never" };
|
||||
return soleUse ? { type: "sole", soleUse } : { type: "never" };
|
||||
|
||||
function recur(node: ts.TypeNode): void {
|
||||
if (ts.isIdentifier(node)) {
|
||||
if (checker.getSymbolAtLocation(node) === typeParameterSymbol) {
|
||||
if (soleUse === undefined) {
|
||||
soleUse = node;
|
||||
} else {
|
||||
throw exit;
|
||||
}
|
||||
}
|
||||
function recur(node: ts.TypeNode): void {
|
||||
if (ts.isIdentifier(node)) {
|
||||
if (checker.getSymbolAtLocation(node) === typeParameterSymbol) {
|
||||
if (soleUse === undefined) {
|
||||
soleUse = node;
|
||||
} else {
|
||||
node.forEachChild(recur);
|
||||
throw exit;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
node.forEachChild(recur);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertDefined<T>(value: T | undefined): T {
|
||||
if (value === undefined) {
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
return value;
|
||||
if (value === undefined) {
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function assertNever(_: never) {
|
||||
throw new Error("unreachable");
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
|
||||
@@ -7,25 +7,23 @@ import { failure } from "../util";
|
||||
// Remove when that PR is in.
|
||||
|
||||
export class Rule extends Lint.Rules.AbstractRule {
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "no-useless-files",
|
||||
description: "Forbids files with no content.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "functionality",
|
||||
typescriptOnly: false,
|
||||
};
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "no-useless-files",
|
||||
description: "Forbids files with no content.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "functionality",
|
||||
typescriptOnly: false
|
||||
};
|
||||
|
||||
static FAILURE_STRING = failure(
|
||||
Rule.metadata.ruleName,
|
||||
"File has no content.");
|
||||
static FAILURE_STRING = failure(Rule.metadata.ruleName, "File has no content.");
|
||||
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
const { statements, referencedFiles, typeReferenceDirectives } = sourceFile;
|
||||
if (statements.length + referencedFiles.length + typeReferenceDirectives.length !== 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [new Lint.RuleFailure(sourceFile, 0, 1, Rule.FAILURE_STRING, this.ruleName)];
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
const { statements, referencedFiles, typeReferenceDirectives } = sourceFile;
|
||||
if (statements.length + referencedFiles.length + typeReferenceDirectives.length !== 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [new Lint.RuleFailure(sourceFile, 0, 1, Rule.FAILURE_STRING, this.ruleName)];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import {
|
||||
CheckOptions as CriticOptions,
|
||||
CriticError,
|
||||
defaultErrors,
|
||||
dtsCritic as critic,
|
||||
ErrorKind,
|
||||
ExportErrorKind,
|
||||
Mode,
|
||||
parseExportErrorKind,
|
||||
parseMode } from "@definitelytyped/dts-critic";
|
||||
CheckOptions as CriticOptions,
|
||||
CriticError,
|
||||
defaultErrors,
|
||||
dtsCritic as critic,
|
||||
ErrorKind,
|
||||
ExportErrorKind,
|
||||
Mode,
|
||||
parseExportErrorKind,
|
||||
parseMode
|
||||
} from "@definitelytyped/dts-critic";
|
||||
import * as Lint from "tslint";
|
||||
import * as ts from "typescript";
|
||||
|
||||
@@ -15,295 +16,305 @@ import { addSuggestion } from "../suggestions";
|
||||
import { failure, isMainFile } from "../util";
|
||||
|
||||
/** Options as parsed from the rule configuration. */
|
||||
type ConfigOptions = {
|
||||
mode: Mode.NameOnly,
|
||||
singleLine?: boolean,
|
||||
} | {
|
||||
mode: Mode.Code,
|
||||
errors: [ExportErrorKind, boolean][],
|
||||
singleLine?: boolean,
|
||||
};
|
||||
type ConfigOptions =
|
||||
| {
|
||||
mode: Mode.NameOnly;
|
||||
singleLine?: boolean;
|
||||
}
|
||||
| {
|
||||
mode: Mode.Code;
|
||||
errors: [ExportErrorKind, boolean][];
|
||||
singleLine?: boolean;
|
||||
};
|
||||
|
||||
type Options = CriticOptions & { singleLine?: boolean };
|
||||
|
||||
const defaultOptions: ConfigOptions = {
|
||||
mode: Mode.NameOnly,
|
||||
mode: Mode.NameOnly
|
||||
};
|
||||
|
||||
export class Rule extends Lint.Rules.AbstractRule {
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "npm-naming",
|
||||
description: "Ensure that package name and DefinitelyTyped header match npm package info.",
|
||||
optionsDescription: `An object with a \`mode\` property should be provided.
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "npm-naming",
|
||||
description: "Ensure that package name and DefinitelyTyped header match npm package info.",
|
||||
optionsDescription: `An object with a \`mode\` property should be provided.
|
||||
If \`mode\` is '${Mode.Code}', then option \`errors\` can be provided.
|
||||
\`errors\` should be an array specifying which code checks should be enabled or disabled.`,
|
||||
options: {
|
||||
oneOf: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
"mode": {
|
||||
type: "string",
|
||||
enum: [Mode.NameOnly],
|
||||
},
|
||||
"single-line": {
|
||||
description: "Whether to print error messages in a single line. Used for testing.",
|
||||
type: "boolean",
|
||||
},
|
||||
"required": ["mode"],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
"mode": {
|
||||
type: "string",
|
||||
enum: [Mode.Code],
|
||||
},
|
||||
"errors": {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "array",
|
||||
items: [
|
||||
{ description: "Name of the check.",
|
||||
type: "string",
|
||||
enum: [ErrorKind.NeedsExportEquals, ErrorKind.NoDefaultExport] as ExportErrorKind[],
|
||||
},
|
||||
{
|
||||
description: "Whether the check is enabled or disabled.",
|
||||
type: "boolean",
|
||||
},
|
||||
],
|
||||
minItems: 2,
|
||||
maxItems: 2,
|
||||
},
|
||||
default: [],
|
||||
},
|
||||
"single-line": {
|
||||
description: "Whether to print error messages in a single line. Used for testing.",
|
||||
type: "boolean",
|
||||
},
|
||||
"required": ["mode"],
|
||||
},
|
||||
},
|
||||
],
|
||||
options: {
|
||||
oneOf: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
mode: {
|
||||
type: "string",
|
||||
enum: [Mode.NameOnly]
|
||||
},
|
||||
"single-line": {
|
||||
description: "Whether to print error messages in a single line. Used for testing.",
|
||||
type: "boolean"
|
||||
},
|
||||
required: ["mode"]
|
||||
}
|
||||
},
|
||||
optionExamples: [
|
||||
true,
|
||||
[true, { mode: Mode.NameOnly }],
|
||||
[
|
||||
true,
|
||||
{
|
||||
mode: Mode.Code,
|
||||
errors: [[ErrorKind.NeedsExportEquals, true], [ErrorKind.NoDefaultExport, false]],
|
||||
},
|
||||
],
|
||||
],
|
||||
type: "functionality",
|
||||
typescriptOnly: true,
|
||||
};
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
mode: {
|
||||
type: "string",
|
||||
enum: [Mode.Code]
|
||||
},
|
||||
errors: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "array",
|
||||
items: [
|
||||
{
|
||||
description: "Name of the check.",
|
||||
type: "string",
|
||||
enum: [ErrorKind.NeedsExportEquals, ErrorKind.NoDefaultExport] as ExportErrorKind[]
|
||||
},
|
||||
{
|
||||
description: "Whether the check is enabled or disabled.",
|
||||
type: "boolean"
|
||||
}
|
||||
],
|
||||
minItems: 2,
|
||||
maxItems: 2
|
||||
},
|
||||
default: []
|
||||
},
|
||||
"single-line": {
|
||||
description: "Whether to print error messages in a single line. Used for testing.",
|
||||
type: "boolean"
|
||||
},
|
||||
required: ["mode"]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
optionExamples: [
|
||||
true,
|
||||
[true, { mode: Mode.NameOnly }],
|
||||
[
|
||||
true,
|
||||
{
|
||||
mode: Mode.Code,
|
||||
errors: [
|
||||
[ErrorKind.NeedsExportEquals, true],
|
||||
[ErrorKind.NoDefaultExport, false]
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
type: "functionality",
|
||||
typescriptOnly: true
|
||||
};
|
||||
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, walk, toCriticOptions(parseOptions(this.ruleArguments)));
|
||||
}
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, walk, toCriticOptions(parseOptions(this.ruleArguments)));
|
||||
}
|
||||
}
|
||||
|
||||
function parseOptions(args: unknown[]): ConfigOptions {
|
||||
if (args.length === 0) {
|
||||
return defaultOptions;
|
||||
}
|
||||
if (args.length === 0) {
|
||||
return defaultOptions;
|
||||
}
|
||||
|
||||
const arg = args[0] as { [prop: string]: unknown } | null | undefined;
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (arg == null) {
|
||||
return defaultOptions;
|
||||
}
|
||||
const arg = args[0] as { [prop: string]: unknown } | null | undefined;
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (arg == null) {
|
||||
return defaultOptions;
|
||||
}
|
||||
|
||||
if (!arg.mode || typeof arg.mode !== "string") {
|
||||
return defaultOptions;
|
||||
}
|
||||
if (!arg.mode || typeof arg.mode !== "string") {
|
||||
return defaultOptions;
|
||||
}
|
||||
|
||||
const mode = parseMode(arg.mode);
|
||||
if (!mode) {
|
||||
return defaultOptions;
|
||||
}
|
||||
const mode = parseMode(arg.mode);
|
||||
if (!mode) {
|
||||
return defaultOptions;
|
||||
}
|
||||
|
||||
const singleLine = !!arg["single-line"];
|
||||
const singleLine = !!arg["single-line"];
|
||||
|
||||
switch (mode) {
|
||||
case Mode.NameOnly:
|
||||
return { mode, singleLine };
|
||||
case Mode.Code:
|
||||
if (!arg.errors || !Array.isArray(arg.errors)) {
|
||||
return { mode, errors: [], singleLine };
|
||||
}
|
||||
return { mode, errors: parseEnabledErrors(arg.errors), singleLine };
|
||||
}
|
||||
switch (mode) {
|
||||
case Mode.NameOnly:
|
||||
return { mode, singleLine };
|
||||
case Mode.Code:
|
||||
if (!arg.errors || !Array.isArray(arg.errors)) {
|
||||
return { mode, errors: [], singleLine };
|
||||
}
|
||||
return { mode, errors: parseEnabledErrors(arg.errors), singleLine };
|
||||
}
|
||||
}
|
||||
|
||||
function parseEnabledErrors(errors: unknown[]): [ExportErrorKind, boolean][] {
|
||||
const enabledChecks: [ExportErrorKind, boolean][] = [];
|
||||
for (const tuple of errors) {
|
||||
if (Array.isArray(tuple)
|
||||
&& tuple.length === 2
|
||||
&& typeof tuple[0] === "string"
|
||||
&& typeof tuple[1] === "boolean") {
|
||||
const error = parseExportErrorKind(tuple[0]);
|
||||
if (error) {
|
||||
enabledChecks.push([error, tuple[1]]);
|
||||
}
|
||||
}
|
||||
const enabledChecks: [ExportErrorKind, boolean][] = [];
|
||||
for (const tuple of errors) {
|
||||
if (Array.isArray(tuple) && tuple.length === 2 && typeof tuple[0] === "string" && typeof tuple[1] === "boolean") {
|
||||
const error = parseExportErrorKind(tuple[0]);
|
||||
if (error) {
|
||||
enabledChecks.push([error, tuple[1]]);
|
||||
}
|
||||
}
|
||||
return enabledChecks;
|
||||
}
|
||||
return enabledChecks;
|
||||
}
|
||||
|
||||
function toCriticOptions(options: ConfigOptions): Options {
|
||||
switch (options.mode) {
|
||||
case Mode.NameOnly:
|
||||
return options;
|
||||
case Mode.Code:
|
||||
return { ...options, errors: new Map(options.errors) };
|
||||
}
|
||||
switch (options.mode) {
|
||||
case Mode.NameOnly:
|
||||
return options;
|
||||
case Mode.Code:
|
||||
return { ...options, errors: new Map(options.errors) };
|
||||
}
|
||||
}
|
||||
|
||||
function walk(ctx: Lint.WalkContext<CriticOptions>): void {
|
||||
const { sourceFile } = ctx;
|
||||
const { text } = sourceFile;
|
||||
const lookFor = (search: string, explanation: string) => {
|
||||
const idx = text.indexOf(search);
|
||||
if (idx !== -1) {
|
||||
ctx.addFailureAt(idx, search.length, failure(Rule.metadata.ruleName, explanation));
|
||||
}
|
||||
};
|
||||
if (isMainFile(sourceFile.fileName, /*allowNested*/ false)) {
|
||||
try {
|
||||
const optionsWithSuggestions = toOptionsWithSuggestions(ctx.options);
|
||||
const diagnostics = critic(sourceFile.fileName, /* sourcePath */ undefined, optionsWithSuggestions);
|
||||
const errors = filterErrors(diagnostics, ctx);
|
||||
for (const error of errors) {
|
||||
switch (error.kind) {
|
||||
case ErrorKind.NoMatchingNpmPackage:
|
||||
case ErrorKind.NoMatchingNpmVersion:
|
||||
case ErrorKind.NonNpmHasMatchingPackage:
|
||||
lookFor("// Type definitions for", errorMessage(error, ctx.options));
|
||||
break;
|
||||
case ErrorKind.DtsPropertyNotInJs:
|
||||
case ErrorKind.DtsSignatureNotInJs:
|
||||
case ErrorKind.JsPropertyNotInDts:
|
||||
case ErrorKind.JsSignatureNotInDts:
|
||||
case ErrorKind.NeedsExportEquals:
|
||||
case ErrorKind.NoDefaultExport:
|
||||
if (error.position) {
|
||||
ctx.addFailureAt(
|
||||
error.position.start,
|
||||
error.position.length,
|
||||
failure(Rule.metadata.ruleName, errorMessage(error, ctx.options)));
|
||||
} else {
|
||||
ctx.addFailure(0, 1, failure(Rule.metadata.ruleName, errorMessage(error, ctx.options)));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// We're ignoring exceptions.
|
||||
}
|
||||
const { sourceFile } = ctx;
|
||||
const { text } = sourceFile;
|
||||
const lookFor = (search: string, explanation: string) => {
|
||||
const idx = text.indexOf(search);
|
||||
if (idx !== -1) {
|
||||
ctx.addFailureAt(idx, search.length, failure(Rule.metadata.ruleName, explanation));
|
||||
}
|
||||
// Don't recur, we're done.
|
||||
};
|
||||
if (isMainFile(sourceFile.fileName, /*allowNested*/ false)) {
|
||||
try {
|
||||
const optionsWithSuggestions = toOptionsWithSuggestions(ctx.options);
|
||||
const diagnostics = critic(sourceFile.fileName, /* sourcePath */ undefined, optionsWithSuggestions);
|
||||
const errors = filterErrors(diagnostics, ctx);
|
||||
for (const error of errors) {
|
||||
switch (error.kind) {
|
||||
case ErrorKind.NoMatchingNpmPackage:
|
||||
case ErrorKind.NoMatchingNpmVersion:
|
||||
case ErrorKind.NonNpmHasMatchingPackage:
|
||||
lookFor("// Type definitions for", errorMessage(error, ctx.options));
|
||||
break;
|
||||
case ErrorKind.DtsPropertyNotInJs:
|
||||
case ErrorKind.DtsSignatureNotInJs:
|
||||
case ErrorKind.JsPropertyNotInDts:
|
||||
case ErrorKind.JsSignatureNotInDts:
|
||||
case ErrorKind.NeedsExportEquals:
|
||||
case ErrorKind.NoDefaultExport:
|
||||
if (error.position) {
|
||||
ctx.addFailureAt(
|
||||
error.position.start,
|
||||
error.position.length,
|
||||
failure(Rule.metadata.ruleName, errorMessage(error, ctx.options))
|
||||
);
|
||||
} else {
|
||||
ctx.addFailure(0, 1, failure(Rule.metadata.ruleName, errorMessage(error, ctx.options)));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// We're ignoring exceptions.
|
||||
}
|
||||
}
|
||||
// Don't recur, we're done.
|
||||
}
|
||||
|
||||
const enabledSuggestions: ExportErrorKind[] = [
|
||||
ErrorKind.JsPropertyNotInDts,
|
||||
ErrorKind.JsSignatureNotInDts,
|
||||
];
|
||||
const enabledSuggestions: ExportErrorKind[] = [ErrorKind.JsPropertyNotInDts, ErrorKind.JsSignatureNotInDts];
|
||||
|
||||
function toOptionsWithSuggestions(options: CriticOptions): CriticOptions {
|
||||
if (options.mode === Mode.NameOnly) {
|
||||
return options;
|
||||
}
|
||||
const optionsWithSuggestions = { mode: options.mode, errors: new Map(options.errors) };
|
||||
enabledSuggestions.forEach(err => optionsWithSuggestions.errors.set(err, true));
|
||||
return optionsWithSuggestions;
|
||||
if (options.mode === Mode.NameOnly) {
|
||||
return options;
|
||||
}
|
||||
const optionsWithSuggestions = { mode: options.mode, errors: new Map(options.errors) };
|
||||
enabledSuggestions.forEach(err => optionsWithSuggestions.errors.set(err, true));
|
||||
return optionsWithSuggestions;
|
||||
}
|
||||
|
||||
function filterErrors(diagnostics: CriticError[], ctx: Lint.WalkContext<Options>): CriticError[] {
|
||||
const errors: CriticError[] = [];
|
||||
diagnostics.forEach(diagnostic => {
|
||||
if (isSuggestion(diagnostic, ctx.options)) {
|
||||
addSuggestion(ctx, diagnostic.message, diagnostic.position?.start, diagnostic.position?.length);
|
||||
} else {
|
||||
errors.push(diagnostic);
|
||||
}
|
||||
});
|
||||
return errors;
|
||||
const errors: CriticError[] = [];
|
||||
diagnostics.forEach(diagnostic => {
|
||||
if (isSuggestion(diagnostic, ctx.options)) {
|
||||
addSuggestion(ctx, diagnostic.message, diagnostic.position?.start, diagnostic.position?.length);
|
||||
} else {
|
||||
errors.push(diagnostic);
|
||||
}
|
||||
});
|
||||
return errors;
|
||||
}
|
||||
|
||||
function isSuggestion(diagnostic: CriticError, options: Options): boolean {
|
||||
return options.mode === Mode.Code
|
||||
&& (enabledSuggestions as ErrorKind[]).includes(diagnostic.kind)
|
||||
&& !(options.errors as Map<ErrorKind, boolean>).get(diagnostic.kind);
|
||||
return (
|
||||
options.mode === Mode.Code &&
|
||||
(enabledSuggestions as ErrorKind[]).includes(diagnostic.kind) &&
|
||||
!(options.errors as Map<ErrorKind, boolean>).get(diagnostic.kind)
|
||||
);
|
||||
}
|
||||
|
||||
function tslintDisableOption(error: ErrorKind): string {
|
||||
switch (error) {
|
||||
case ErrorKind.NoMatchingNpmPackage:
|
||||
case ErrorKind.NoMatchingNpmVersion:
|
||||
case ErrorKind.NonNpmHasMatchingPackage:
|
||||
return `false`;
|
||||
case ErrorKind.NoDefaultExport:
|
||||
case ErrorKind.NeedsExportEquals:
|
||||
case ErrorKind.JsSignatureNotInDts:
|
||||
case ErrorKind.JsPropertyNotInDts:
|
||||
case ErrorKind.DtsSignatureNotInJs:
|
||||
case ErrorKind.DtsPropertyNotInJs:
|
||||
return JSON.stringify([true, { mode: Mode.Code, errors: [[error, false]]}]);
|
||||
}
|
||||
switch (error) {
|
||||
case ErrorKind.NoMatchingNpmPackage:
|
||||
case ErrorKind.NoMatchingNpmVersion:
|
||||
case ErrorKind.NonNpmHasMatchingPackage:
|
||||
return `false`;
|
||||
case ErrorKind.NoDefaultExport:
|
||||
case ErrorKind.NeedsExportEquals:
|
||||
case ErrorKind.JsSignatureNotInDts:
|
||||
case ErrorKind.JsPropertyNotInDts:
|
||||
case ErrorKind.DtsSignatureNotInJs:
|
||||
case ErrorKind.DtsPropertyNotInJs:
|
||||
return JSON.stringify([true, { mode: Mode.Code, errors: [[error, false]] }]);
|
||||
}
|
||||
}
|
||||
|
||||
function errorMessage(error: CriticError, opts: Options): string {
|
||||
const message = error.message +
|
||||
`\nIf you won't fix this error now or you think this error is wrong,
|
||||
const message =
|
||||
error.message +
|
||||
`\nIf you won't fix this error now or you think this error is wrong,
|
||||
you can disable this check by adding the following options to your project's tslint.json file under "rules":
|
||||
|
||||
"npm-naming": ${tslintDisableOption(error.kind)}
|
||||
`;
|
||||
if (opts.singleLine) {
|
||||
return message.replace(/(\r\n|\n|\r|\t)/gm, " ");
|
||||
}
|
||||
if (opts.singleLine) {
|
||||
return message.replace(/(\r\n|\n|\r|\t)/gm, " ");
|
||||
}
|
||||
|
||||
return message;
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given npm-naming lint failures, returns a rule configuration that prevents such failures.
|
||||
*/
|
||||
export function disabler(failures: Lint.IRuleFailureJson[]): false | [true, ConfigOptions] {
|
||||
const disabledErrors = new Set<ExportErrorKind>();
|
||||
for (const ruleFailure of failures) {
|
||||
if (ruleFailure.ruleName !== "npm-naming") {
|
||||
throw new Error(`Expected failures of rule "npm-naming", found failures of rule ${ruleFailure.ruleName}.`);
|
||||
}
|
||||
const message = ruleFailure.failure;
|
||||
// Name errors.
|
||||
if (message.includes("must have a matching npm package")
|
||||
|| message.includes("must match a version that exists on npm")
|
||||
|| message.includes("conflicts with the existing npm package")) {
|
||||
return false;
|
||||
}
|
||||
// Code errors.
|
||||
if (message.includes("declaration should use 'export =' syntax")) {
|
||||
disabledErrors.add(ErrorKind.NeedsExportEquals);
|
||||
} else if (message.includes("declaration specifies 'export default' but the JavaScript source \
|
||||
does not mention 'default' anywhere")) {
|
||||
disabledErrors.add(ErrorKind.NoDefaultExport);
|
||||
} else {
|
||||
return [true, { mode: Mode.NameOnly }];
|
||||
}
|
||||
const disabledErrors = new Set<ExportErrorKind>();
|
||||
for (const ruleFailure of failures) {
|
||||
if (ruleFailure.ruleName !== "npm-naming") {
|
||||
throw new Error(`Expected failures of rule "npm-naming", found failures of rule ${ruleFailure.ruleName}.`);
|
||||
}
|
||||
const message = ruleFailure.failure;
|
||||
// Name errors.
|
||||
if (
|
||||
message.includes("must have a matching npm package") ||
|
||||
message.includes("must match a version that exists on npm") ||
|
||||
message.includes("conflicts with the existing npm package")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
// Code errors.
|
||||
if (message.includes("declaration should use 'export =' syntax")) {
|
||||
disabledErrors.add(ErrorKind.NeedsExportEquals);
|
||||
} else if (
|
||||
message.includes(
|
||||
"declaration specifies 'export default' but the JavaScript source \
|
||||
does not mention 'default' anywhere"
|
||||
)
|
||||
) {
|
||||
disabledErrors.add(ErrorKind.NoDefaultExport);
|
||||
} else {
|
||||
return [true, { mode: Mode.NameOnly }];
|
||||
}
|
||||
}
|
||||
|
||||
if ((defaultErrors as ExportErrorKind[]).every(error => disabledErrors.has(error))) {
|
||||
return [true, { mode: Mode.NameOnly }];
|
||||
}
|
||||
const errors: [ExportErrorKind, boolean][] = [];
|
||||
disabledErrors.forEach(error => errors.push([error, false]));
|
||||
return [true, { mode: Mode.Code, errors }];
|
||||
if ((defaultErrors as ExportErrorKind[]).every(error => disabledErrors.has(error))) {
|
||||
return [true, { mode: Mode.NameOnly }];
|
||||
}
|
||||
const errors: [ExportErrorKind, boolean][] = [];
|
||||
disabledErrors.forEach(error => errors.push([error, false]));
|
||||
return [true, { mode: Mode.Code, errors }];
|
||||
}
|
||||
|
||||
@@ -4,32 +4,33 @@ import * as ts from "typescript";
|
||||
import { eachModuleStatement, failure } from "../util";
|
||||
|
||||
export class Rule extends Lint.Rules.AbstractRule {
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "prefer-declare-function",
|
||||
description: "Forbids `export const x = () => void`.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "style",
|
||||
typescriptOnly: true,
|
||||
};
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "prefer-declare-function",
|
||||
description: "Forbids `export const x = () => void`.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "style",
|
||||
typescriptOnly: true
|
||||
};
|
||||
|
||||
static FAILURE_STRING = failure(
|
||||
Rule.metadata.ruleName,
|
||||
"Use a function declaration instead of a variable of function type.");
|
||||
static FAILURE_STRING = failure(
|
||||
Rule.metadata.ruleName,
|
||||
"Use a function declaration instead of a variable of function type."
|
||||
);
|
||||
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, walk);
|
||||
}
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, walk);
|
||||
}
|
||||
}
|
||||
|
||||
function walk(ctx: Lint.WalkContext<void>): void {
|
||||
eachModuleStatement(ctx.sourceFile, statement => {
|
||||
if (ts.isVariableStatement(statement)) {
|
||||
for (const varDecl of statement.declarationList.declarations) {
|
||||
if (varDecl.type !== undefined && varDecl.type.kind === ts.SyntaxKind.FunctionType) {
|
||||
ctx.addFailureAtNode(varDecl, Rule.FAILURE_STRING);
|
||||
}
|
||||
}
|
||||
eachModuleStatement(ctx.sourceFile, statement => {
|
||||
if (ts.isVariableStatement(statement)) {
|
||||
for (const varDecl of statement.declarationList.declarations) {
|
||||
if (varDecl.type !== undefined && varDecl.type.kind === ts.SyntaxKind.FunctionType) {
|
||||
ctx.addFailureAtNode(varDecl, Rule.FAILURE_STRING);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,37 +4,38 @@ import * as ts from "typescript";
|
||||
import { failure } from "../util";
|
||||
|
||||
export class Rule extends Lint.Rules.AbstractRule {
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "redundant-undefined",
|
||||
description: "Forbids optional parameters to include an explicit `undefined` in their type; requires it in optional properties.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "style",
|
||||
typescriptOnly: true,
|
||||
};
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "redundant-undefined",
|
||||
description:
|
||||
"Forbids optional parameters to include an explicit `undefined` in their type; requires it in optional properties.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "style",
|
||||
typescriptOnly: true
|
||||
};
|
||||
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, walk);
|
||||
}
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, walk);
|
||||
}
|
||||
}
|
||||
|
||||
function walk(ctx: Lint.WalkContext<void>): void {
|
||||
if (ctx.sourceFile.fileName.includes('node_modules')) return;
|
||||
ctx.sourceFile.forEachChild(function recur(node) {
|
||||
if (node.kind === ts.SyntaxKind.UndefinedKeyword
|
||||
&& ts.isUnionTypeNode(node.parent!)
|
||||
&& isOptionalParameter(node.parent!.parent!)) {
|
||||
ctx.addFailureAtNode(
|
||||
node,
|
||||
failure(
|
||||
Rule.metadata.ruleName,
|
||||
`Parameter is optional, so no need to include \`undefined\` in the type.`));
|
||||
}
|
||||
node.forEachChild(recur);
|
||||
});
|
||||
if (ctx.sourceFile.fileName.includes("node_modules")) return;
|
||||
ctx.sourceFile.forEachChild(function recur(node) {
|
||||
if (
|
||||
node.kind === ts.SyntaxKind.UndefinedKeyword &&
|
||||
ts.isUnionTypeNode(node.parent!) &&
|
||||
isOptionalParameter(node.parent!.parent!)
|
||||
) {
|
||||
ctx.addFailureAtNode(
|
||||
node,
|
||||
failure(Rule.metadata.ruleName, `Parameter is optional, so no need to include \`undefined\` in the type.`)
|
||||
);
|
||||
}
|
||||
node.forEachChild(recur);
|
||||
});
|
||||
}
|
||||
|
||||
function isOptionalParameter(node: ts.Node): boolean {
|
||||
return node.kind === ts.SyntaxKind.Parameter
|
||||
&& (node as ts.ParameterDeclaration).questionToken !== undefined;
|
||||
return node.kind === ts.SyntaxKind.Parameter && (node as ts.ParameterDeclaration).questionToken !== undefined;
|
||||
}
|
||||
|
||||
@@ -4,160 +4,170 @@ import * as ts from "typescript";
|
||||
import { failure } from "../util";
|
||||
|
||||
export class Rule extends Lint.Rules.AbstractRule {
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "strict-export-declare-modifiers",
|
||||
description: "Enforces strict rules about where the 'export' and 'declare' modifiers may appear.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "style",
|
||||
typescriptOnly: true,
|
||||
};
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "strict-export-declare-modifiers",
|
||||
description: "Enforces strict rules about where the 'export' and 'declare' modifiers may appear.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "style",
|
||||
typescriptOnly: true
|
||||
};
|
||||
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, walk);
|
||||
}
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, walk);
|
||||
}
|
||||
}
|
||||
|
||||
function walk(ctx: Lint.WalkContext<void>): void {
|
||||
const { sourceFile } = ctx;
|
||||
const isExternal = sourceFile.isDeclarationFile
|
||||
&& !sourceFile.statements.some(
|
||||
s => s.kind === ts.SyntaxKind.ExportAssignment ||
|
||||
s.kind === ts.SyntaxKind.ExportDeclaration && !!(s as ts.ExportDeclaration).exportClause)
|
||||
&& ts.isExternalModule(sourceFile);
|
||||
const { sourceFile } = ctx;
|
||||
const isExternal =
|
||||
sourceFile.isDeclarationFile &&
|
||||
!sourceFile.statements.some(
|
||||
s =>
|
||||
s.kind === ts.SyntaxKind.ExportAssignment ||
|
||||
(s.kind === ts.SyntaxKind.ExportDeclaration && !!(s as ts.ExportDeclaration).exportClause)
|
||||
) &&
|
||||
ts.isExternalModule(sourceFile);
|
||||
|
||||
for (const node of sourceFile.statements) {
|
||||
if (isExternal) {
|
||||
checkInExternalModule(node, isAutomaticExport(sourceFile));
|
||||
} else {
|
||||
checkInOther(node, sourceFile.isDeclarationFile);
|
||||
}
|
||||
|
||||
if (isModuleDeclaration(node) && (sourceFile.isDeclarationFile || isDeclare(node))) {
|
||||
checkModule(node);
|
||||
}
|
||||
for (const node of sourceFile.statements) {
|
||||
if (isExternal) {
|
||||
checkInExternalModule(node, isAutomaticExport(sourceFile));
|
||||
} else {
|
||||
checkInOther(node, sourceFile.isDeclarationFile);
|
||||
}
|
||||
|
||||
function checkInExternalModule(node: ts.Statement, autoExportEnabled: boolean) {
|
||||
// Ignore certain node kinds (these can't have 'export' or 'default' modifiers)
|
||||
switch (node.kind) {
|
||||
case ts.SyntaxKind.ImportDeclaration:
|
||||
case ts.SyntaxKind.ImportEqualsDeclaration:
|
||||
case ts.SyntaxKind.ExportDeclaration:
|
||||
case ts.SyntaxKind.NamespaceExportDeclaration:
|
||||
return;
|
||||
}
|
||||
if (isModuleDeclaration(node) && (sourceFile.isDeclarationFile || isDeclare(node))) {
|
||||
checkModule(node);
|
||||
}
|
||||
}
|
||||
|
||||
// `declare global` and `declare module "foo"` OK. `declare namespace N` not OK, should be `export namespace`.
|
||||
if (!isDeclareGlobalOrExternalModuleDeclaration(node)) {
|
||||
if (isDeclare(node)) {
|
||||
fail(mod(node, ts.SyntaxKind.DeclareKeyword), "'declare' keyword is redundant here.");
|
||||
}
|
||||
if (autoExportEnabled && !isExport(node)) {
|
||||
fail(
|
||||
(node as ts.DeclarationStatement).name || node,
|
||||
"All declarations in this module are exported automatically. " +
|
||||
"Prefer to explicitly write 'export' for clarity. " +
|
||||
"If you have a good reason not to export this declaration, " +
|
||||
"add 'export {}' to the module to shut off automatic exporting.");
|
||||
}
|
||||
}
|
||||
function checkInExternalModule(node: ts.Statement, autoExportEnabled: boolean) {
|
||||
// Ignore certain node kinds (these can't have 'export' or 'default' modifiers)
|
||||
switch (node.kind) {
|
||||
case ts.SyntaxKind.ImportDeclaration:
|
||||
case ts.SyntaxKind.ImportEqualsDeclaration:
|
||||
case ts.SyntaxKind.ExportDeclaration:
|
||||
case ts.SyntaxKind.NamespaceExportDeclaration:
|
||||
return;
|
||||
}
|
||||
|
||||
function checkInOther(node: ts.Statement, inDeclarationFile: boolean): void {
|
||||
// Compiler will enforce presence of 'declare' where necessary. But types do not need 'declare'.
|
||||
if (isDeclare(node)) {
|
||||
if (isExport(node) && inDeclarationFile ||
|
||||
node.kind === ts.SyntaxKind.InterfaceDeclaration ||
|
||||
node.kind === ts.SyntaxKind.TypeAliasDeclaration) {
|
||||
fail(mod(node, ts.SyntaxKind.DeclareKeyword), "'declare' keyword is redundant here.");
|
||||
}
|
||||
}
|
||||
// `declare global` and `declare module "foo"` OK. `declare namespace N` not OK, should be `export namespace`.
|
||||
if (!isDeclareGlobalOrExternalModuleDeclaration(node)) {
|
||||
if (isDeclare(node)) {
|
||||
fail(mod(node, ts.SyntaxKind.DeclareKeyword), "'declare' keyword is redundant here.");
|
||||
}
|
||||
if (autoExportEnabled && !isExport(node)) {
|
||||
fail(
|
||||
(node as ts.DeclarationStatement).name || node,
|
||||
"All declarations in this module are exported automatically. " +
|
||||
"Prefer to explicitly write 'export' for clarity. " +
|
||||
"If you have a good reason not to export this declaration, " +
|
||||
"add 'export {}' to the module to shut off automatic exporting."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkInOther(node: ts.Statement, inDeclarationFile: boolean): void {
|
||||
// Compiler will enforce presence of 'declare' where necessary. But types do not need 'declare'.
|
||||
if (isDeclare(node)) {
|
||||
if (
|
||||
(isExport(node) && inDeclarationFile) ||
|
||||
node.kind === ts.SyntaxKind.InterfaceDeclaration ||
|
||||
node.kind === ts.SyntaxKind.TypeAliasDeclaration
|
||||
) {
|
||||
fail(mod(node, ts.SyntaxKind.DeclareKeyword), "'declare' keyword is redundant here.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fail(node: ts.Node, reason: string): void {
|
||||
ctx.addFailureAtNode(node, failure(Rule.metadata.ruleName, reason));
|
||||
}
|
||||
|
||||
function mod(node: ts.Statement, kind: ts.SyntaxKind): ts.Node {
|
||||
return node.modifiers!.find(m => m.kind === kind)!;
|
||||
}
|
||||
|
||||
function checkModule(moduleDeclaration: ts.ModuleDeclaration): void {
|
||||
const body = moduleDeclaration.body;
|
||||
if (!body) {
|
||||
return;
|
||||
}
|
||||
|
||||
function fail(node: ts.Node, reason: string): void {
|
||||
ctx.addFailureAtNode(node, failure(Rule.metadata.ruleName, reason));
|
||||
switch (body.kind) {
|
||||
case ts.SyntaxKind.ModuleDeclaration:
|
||||
checkModule(body);
|
||||
break;
|
||||
case ts.SyntaxKind.ModuleBlock:
|
||||
checkBlock(body, isAutomaticExport(moduleDeclaration));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function mod(node: ts.Statement, kind: ts.SyntaxKind): ts.Node {
|
||||
return node.modifiers!.find(m => m.kind === kind)!;
|
||||
}
|
||||
|
||||
function checkModule(moduleDeclaration: ts.ModuleDeclaration): void {
|
||||
const body = moduleDeclaration.body;
|
||||
if (!body) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (body.kind) {
|
||||
case ts.SyntaxKind.ModuleDeclaration:
|
||||
checkModule(body);
|
||||
break;
|
||||
case ts.SyntaxKind.ModuleBlock:
|
||||
checkBlock(body, isAutomaticExport(moduleDeclaration));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function checkBlock(block: ts.ModuleBlock, autoExportEnabled: boolean): void {
|
||||
for (const s of block.statements) {
|
||||
// Compiler will error for 'declare' here anyway, so just check for 'export'.
|
||||
if (isExport(s) && autoExportEnabled && !isDefault(s)) {
|
||||
fail(mod(s, ts.SyntaxKind.ExportKeyword),
|
||||
"'export' keyword is redundant here because " +
|
||||
"all declarations in this module are exported automatically. " +
|
||||
"If you have a good reason to export some declarations and not others, " +
|
||||
"add 'export {}' to the module to shut off automatic exporting.");
|
||||
}
|
||||
|
||||
if (isModuleDeclaration(s)) {
|
||||
checkModule(s);
|
||||
}
|
||||
}
|
||||
function checkBlock(block: ts.ModuleBlock, autoExportEnabled: boolean): void {
|
||||
for (const s of block.statements) {
|
||||
// Compiler will error for 'declare' here anyway, so just check for 'export'.
|
||||
if (isExport(s) && autoExportEnabled && !isDefault(s)) {
|
||||
fail(
|
||||
mod(s, ts.SyntaxKind.ExportKeyword),
|
||||
"'export' keyword is redundant here because " +
|
||||
"all declarations in this module are exported automatically. " +
|
||||
"If you have a good reason to export some declarations and not others, " +
|
||||
"add 'export {}' to the module to shut off automatic exporting."
|
||||
);
|
||||
}
|
||||
|
||||
if (isModuleDeclaration(s)) {
|
||||
checkModule(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isDeclareGlobalOrExternalModuleDeclaration(node: ts.Node): boolean {
|
||||
return isModuleDeclaration(node) && (
|
||||
node.name.kind === ts.SyntaxKind.StringLiteral ||
|
||||
node.name.kind === ts.SyntaxKind.Identifier && node.name.text === "global");
|
||||
return (
|
||||
isModuleDeclaration(node) &&
|
||||
(node.name.kind === ts.SyntaxKind.StringLiteral ||
|
||||
(node.name.kind === ts.SyntaxKind.Identifier && node.name.text === "global"))
|
||||
);
|
||||
}
|
||||
|
||||
function isModuleDeclaration(node: ts.Node): node is ts.ModuleDeclaration {
|
||||
return node.kind === ts.SyntaxKind.ModuleDeclaration;
|
||||
return node.kind === ts.SyntaxKind.ModuleDeclaration;
|
||||
}
|
||||
|
||||
function isDeclare(node: ts.Node): boolean {
|
||||
return Lint.hasModifier(node.modifiers, ts.SyntaxKind.DeclareKeyword);
|
||||
return Lint.hasModifier(node.modifiers, ts.SyntaxKind.DeclareKeyword);
|
||||
}
|
||||
|
||||
function isExport(node: ts.Node): boolean {
|
||||
return Lint.hasModifier(node.modifiers, ts.SyntaxKind.ExportKeyword);
|
||||
return Lint.hasModifier(node.modifiers, ts.SyntaxKind.ExportKeyword);
|
||||
}
|
||||
|
||||
function isDefault(node: ts.Node): boolean {
|
||||
return Lint.hasModifier(node.modifiers, ts.SyntaxKind.DefaultKeyword);
|
||||
return Lint.hasModifier(node.modifiers, ts.SyntaxKind.DefaultKeyword);
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:max-line-length
|
||||
// Copied from https://github.com/Microsoft/TypeScript/blob/dd9b8cab34a3e389e924d768eb656cf50656f582/src/compiler/binder.ts#L1571-L1581
|
||||
function hasExportDeclarations(node: ts.SourceFile | ts.ModuleDeclaration): boolean {
|
||||
const body = node.kind === ts.SyntaxKind.SourceFile ? node : node.body;
|
||||
if (body && (body.kind === ts.SyntaxKind.SourceFile || body.kind === ts.SyntaxKind.ModuleBlock)) {
|
||||
for (const stat of (body as ts.BlockLike).statements) {
|
||||
if (stat.kind === ts.SyntaxKind.ExportDeclaration || stat.kind === ts.SyntaxKind.ExportAssignment) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const body = node.kind === ts.SyntaxKind.SourceFile ? node : node.body;
|
||||
if (body && (body.kind === ts.SyntaxKind.SourceFile || body.kind === ts.SyntaxKind.ModuleBlock)) {
|
||||
for (const stat of (body as ts.BlockLike).statements) {
|
||||
if (stat.kind === ts.SyntaxKind.ExportDeclaration || stat.kind === ts.SyntaxKind.ExportAssignment) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isAutomaticExport(node: ts.SourceFile | ts.ModuleDeclaration): boolean {
|
||||
// We'd like to just test ts.NodeFlags.ExportContext, but we don't run the
|
||||
// binder, so that flag won't be set, so duplicate the logic instead. :(
|
||||
//
|
||||
// ts.NodeFlags.Ambient is @internal, but all modules that get here should
|
||||
// be ambient.
|
||||
return !hasExportDeclarations(node);
|
||||
// We'd like to just test ts.NodeFlags.ExportContext, but we don't run the
|
||||
// binder, so that flag won't be set, so duplicate the logic instead. :(
|
||||
//
|
||||
// ts.NodeFlags.Ambient is @internal, but all modules that get here should
|
||||
// be ambient.
|
||||
return !hasExportDeclarations(node);
|
||||
}
|
||||
|
||||
@@ -4,33 +4,36 @@ import * as ts from "typescript";
|
||||
import { failure } from "../util";
|
||||
|
||||
export class Rule extends Lint.Rules.AbstractRule {
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "trim-file",
|
||||
description: "Forbids leading/trailing blank lines in a file. Allows file to end in '\n'.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "style",
|
||||
typescriptOnly: false,
|
||||
};
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "trim-file",
|
||||
description: "Forbids leading/trailing blank lines in a file. Allows file to end in '\n'.",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "style",
|
||||
typescriptOnly: false
|
||||
};
|
||||
|
||||
static FAILURE_STRING_LEADING = failure(Rule.metadata.ruleName, "File should not begin with a blank line.");
|
||||
static FAILURE_STRING_TRAILING = failure(
|
||||
Rule.metadata.ruleName,
|
||||
"File should not end with a blank line. (Ending in one newline OK, ending in two newlines not OK.)");
|
||||
static FAILURE_STRING_LEADING = failure(Rule.metadata.ruleName, "File should not begin with a blank line.");
|
||||
static FAILURE_STRING_TRAILING = failure(
|
||||
Rule.metadata.ruleName,
|
||||
"File should not end with a blank line. (Ending in one newline OK, ending in two newlines not OK.)"
|
||||
);
|
||||
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, walk);
|
||||
}
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, walk);
|
||||
}
|
||||
}
|
||||
|
||||
function walk(ctx: Lint.WalkContext<void>): void {
|
||||
const { sourceFile: { text } } = ctx;
|
||||
if (text.startsWith("\r") || text.startsWith("\n")) {
|
||||
ctx.addFailureAt(0, 0, Rule.FAILURE_STRING_LEADING);
|
||||
}
|
||||
const {
|
||||
sourceFile: { text }
|
||||
} = ctx;
|
||||
if (text.startsWith("\r") || text.startsWith("\n")) {
|
||||
ctx.addFailureAt(0, 0, Rule.FAILURE_STRING_LEADING);
|
||||
}
|
||||
|
||||
if (text.endsWith("\n\n") || text.endsWith("\r\n\r\n")) {
|
||||
const start = text.endsWith("\r\n") ? text.length - 2 : text.length - 1;
|
||||
ctx.addFailureAt(start, 0, Rule.FAILURE_STRING_TRAILING);
|
||||
}
|
||||
if (text.endsWith("\n\n") || text.endsWith("\r\n\r\n")) {
|
||||
const start = text.endsWith("\r\n") ? text.length - 2 : text.length - 1;
|
||||
ctx.addFailureAt(start, 0, Rule.FAILURE_STRING_TRAILING);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,67 +4,68 @@ import * as ts from "typescript";
|
||||
import { failure } from "../util";
|
||||
|
||||
export class Rule extends Lint.Rules.AbstractRule {
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "void-return",
|
||||
description: "`void` may only be used as a return type.",
|
||||
rationale: "style",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "style",
|
||||
typescriptOnly: true,
|
||||
};
|
||||
static metadata: Lint.IRuleMetadata = {
|
||||
ruleName: "void-return",
|
||||
description: "`void` may only be used as a return type.",
|
||||
rationale: "style",
|
||||
optionsDescription: "Not configurable.",
|
||||
options: null,
|
||||
type: "style",
|
||||
typescriptOnly: true
|
||||
};
|
||||
|
||||
static FAILURE_STRING = failure(
|
||||
Rule.metadata.ruleName,
|
||||
"Use the `void` type for return types only. Otherwise, use `undefined`.");
|
||||
static FAILURE_STRING = failure(
|
||||
Rule.metadata.ruleName,
|
||||
"Use the `void` type for return types only. Otherwise, use `undefined`."
|
||||
);
|
||||
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, walk);
|
||||
}
|
||||
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
|
||||
return this.applyWithFunction(sourceFile, walk);
|
||||
}
|
||||
}
|
||||
|
||||
function walk(ctx: Lint.WalkContext<void>): void {
|
||||
ctx.sourceFile.forEachChild(function cb(node) {
|
||||
if (node.kind === ts.SyntaxKind.VoidKeyword && !mayContainVoid(node.parent!) && !isReturnType(node)) {
|
||||
ctx.addFailureAtNode(node, Rule.FAILURE_STRING);
|
||||
} else {
|
||||
node.forEachChild(cb);
|
||||
}
|
||||
});
|
||||
ctx.sourceFile.forEachChild(function cb(node) {
|
||||
if (node.kind === ts.SyntaxKind.VoidKeyword && !mayContainVoid(node.parent!) && !isReturnType(node)) {
|
||||
ctx.addFailureAtNode(node, Rule.FAILURE_STRING);
|
||||
} else {
|
||||
node.forEachChild(cb);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function mayContainVoid({ kind }: ts.Node): boolean {
|
||||
switch (kind) {
|
||||
case ts.SyntaxKind.TypeReference:
|
||||
case ts.SyntaxKind.ExpressionWithTypeArguments:
|
||||
case ts.SyntaxKind.NewExpression:
|
||||
case ts.SyntaxKind.CallExpression:
|
||||
case ts.SyntaxKind.TypeParameter: // Allow f<T = void>
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
switch (kind) {
|
||||
case ts.SyntaxKind.TypeReference:
|
||||
case ts.SyntaxKind.ExpressionWithTypeArguments:
|
||||
case ts.SyntaxKind.NewExpression:
|
||||
case ts.SyntaxKind.CallExpression:
|
||||
case ts.SyntaxKind.TypeParameter: // Allow f<T = void>
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isReturnType(node: ts.Node): boolean {
|
||||
let parent = node.parent!;
|
||||
if (parent.kind === ts.SyntaxKind.UnionType) {
|
||||
[node, parent] = [parent, parent.parent!];
|
||||
}
|
||||
return isSignatureDeclaration(parent) && parent.type === node;
|
||||
let parent = node.parent!;
|
||||
if (parent.kind === ts.SyntaxKind.UnionType) {
|
||||
[node, parent] = [parent, parent.parent!];
|
||||
}
|
||||
return isSignatureDeclaration(parent) && parent.type === node;
|
||||
}
|
||||
|
||||
function isSignatureDeclaration(node: ts.Node): node is ts.SignatureDeclaration {
|
||||
switch (node.kind) {
|
||||
case ts.SyntaxKind.ArrowFunction:
|
||||
case ts.SyntaxKind.CallSignature:
|
||||
case ts.SyntaxKind.FunctionDeclaration:
|
||||
case ts.SyntaxKind.FunctionExpression:
|
||||
case ts.SyntaxKind.FunctionType:
|
||||
case ts.SyntaxKind.MethodDeclaration:
|
||||
case ts.SyntaxKind.MethodSignature:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
switch (node.kind) {
|
||||
case ts.SyntaxKind.ArrowFunction:
|
||||
case ts.SyntaxKind.CallSignature:
|
||||
case ts.SyntaxKind.FunctionDeclaration:
|
||||
case ts.SyntaxKind.FunctionExpression:
|
||||
case ts.SyntaxKind.FunctionType:
|
||||
case ts.SyntaxKind.MethodDeclaration:
|
||||
case ts.SyntaxKind.MethodSignature:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ import { WalkContext } from "tslint";
|
||||
const suggestionsDir = path.join(os.homedir(), ".dts", "suggestions");
|
||||
|
||||
export interface Suggestion {
|
||||
fileName: string;
|
||||
ruleName: string;
|
||||
message: string;
|
||||
start?: number;
|
||||
width?: number;
|
||||
fileName: string;
|
||||
ruleName: string;
|
||||
message: string;
|
||||
start?: number;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
// Packages for which suggestions were already added in this run of dtslint.
|
||||
@@ -20,56 +20,57 @@ const existingPackages = new Set();
|
||||
* A rule should call this function to provide a suggestion instead of a lint failure.
|
||||
*/
|
||||
export function addSuggestion<T>(ctx: WalkContext<T>, message: string, start?: number, width?: number) {
|
||||
const suggestion: Suggestion = {
|
||||
fileName: ctx.sourceFile.fileName,
|
||||
ruleName: ctx.ruleName,
|
||||
message,
|
||||
start,
|
||||
width,
|
||||
};
|
||||
const suggestion: Suggestion = {
|
||||
fileName: ctx.sourceFile.fileName,
|
||||
ruleName: ctx.ruleName,
|
||||
message,
|
||||
start,
|
||||
width
|
||||
};
|
||||
|
||||
const packageName = dtPackageName(ctx.sourceFile.fileName);
|
||||
if (!packageName) {
|
||||
return;
|
||||
}
|
||||
let flag = "a";
|
||||
if (!existingPackages.has(packageName)) {
|
||||
flag = "w";
|
||||
existingPackages.add(packageName);
|
||||
}
|
||||
try {
|
||||
if (!fs.existsSync(suggestionsDir)) {
|
||||
fs.mkdirSync(suggestionsDir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(
|
||||
path.join(suggestionsDir, packageName + ".txt"),
|
||||
flag === "a" ? "\n" + formatSuggestion(suggestion) : formatSuggestion(suggestion),
|
||||
{ flag, encoding: "utf8" });
|
||||
} catch (e) {
|
||||
console.log(`Could not write suggestions for package ${packageName}. ${e.message || ""}`);
|
||||
const packageName = dtPackageName(ctx.sourceFile.fileName);
|
||||
if (!packageName) {
|
||||
return;
|
||||
}
|
||||
let flag = "a";
|
||||
if (!existingPackages.has(packageName)) {
|
||||
flag = "w";
|
||||
existingPackages.add(packageName);
|
||||
}
|
||||
try {
|
||||
if (!fs.existsSync(suggestionsDir)) {
|
||||
fs.mkdirSync(suggestionsDir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(
|
||||
path.join(suggestionsDir, packageName + ".txt"),
|
||||
flag === "a" ? "\n" + formatSuggestion(suggestion) : formatSuggestion(suggestion),
|
||||
{ flag, encoding: "utf8" }
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(`Could not write suggestions for package ${packageName}. ${e.message || ""}`);
|
||||
}
|
||||
}
|
||||
|
||||
const dtPath = path.join("DefinitelyTyped", "types");
|
||||
|
||||
function dtPackageName(filePath: string): string | undefined {
|
||||
const dtIndex = filePath.indexOf(dtPath);
|
||||
if (dtIndex === -1) {
|
||||
return undefined;
|
||||
}
|
||||
const basePath = filePath.substr(dtIndex + dtPath.length);
|
||||
const dirs = basePath.split(path.sep).filter(dir => dir !== "");
|
||||
if (dirs.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const packageName = dirs[0];
|
||||
// Check if this is an old version of a package.
|
||||
if (dirs.length > 1 && /^v\d+(\.\d+)?$/.test(dirs[1])) {
|
||||
return packageName + dirs[1];
|
||||
}
|
||||
return packageName;
|
||||
const dtIndex = filePath.indexOf(dtPath);
|
||||
if (dtIndex === -1) {
|
||||
return undefined;
|
||||
}
|
||||
const basePath = filePath.substr(dtIndex + dtPath.length);
|
||||
const dirs = basePath.split(path.sep).filter(dir => dir !== "");
|
||||
if (dirs.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const packageName = dirs[0];
|
||||
// Check if this is an old version of a package.
|
||||
if (dirs.length > 1 && /^v\d+(\.\d+)?$/.test(dirs[1])) {
|
||||
return packageName + dirs[1];
|
||||
}
|
||||
return packageName;
|
||||
}
|
||||
|
||||
function formatSuggestion(suggestion: Suggestion): string {
|
||||
return JSON.stringify(suggestion, /*replacer*/ undefined, 0);
|
||||
return JSON.stringify(suggestion, /*replacer*/ undefined, 0);
|
||||
}
|
||||
|
||||
@@ -21,116 +21,117 @@ import { disabler as npmNamingDisabler } from "./rules/npmNamingRule";
|
||||
const ignoredRules: string[] = ["expect"];
|
||||
|
||||
function main() {
|
||||
const args = yargs
|
||||
.usage(`\`$0 --dt=path-to-dt\` or \`$0 --package=path-to-dt-package\`
|
||||
'dt.json' is used as the base tslint config for running the linter.`)
|
||||
.option("package", {
|
||||
describe: "Path of DT package.",
|
||||
type: "string",
|
||||
conflicts: "dt",
|
||||
})
|
||||
.option("dt", {
|
||||
describe: "Path of local DefinitelyTyped repository.",
|
||||
type: "string",
|
||||
conflicts: "package",
|
||||
})
|
||||
.option("rules", {
|
||||
describe: "Names of the rules to be updated. Leave this empty to update all rules.",
|
||||
type: "array",
|
||||
string: true,
|
||||
default: [] as string[],
|
||||
})
|
||||
.check(arg => {
|
||||
if (!arg.package && !arg.dt) {
|
||||
throw new Error("You must provide either argument 'package' or 'dt'.");
|
||||
}
|
||||
const unsupportedRules = arg.rules.filter(rule => ignoredRules.includes(rule));
|
||||
if (unsupportedRules.length > 0) {
|
||||
throw new Error(`Rules ${unsupportedRules.join(", ")} are not supported at the moment.`);
|
||||
}
|
||||
return true;
|
||||
}).argv;
|
||||
const args = yargs
|
||||
.usage(
|
||||
`\`$0 --dt=path-to-dt\` or \`$0 --package=path-to-dt-package\`
|
||||
'dt.json' is used as the base tslint config for running the linter.`
|
||||
)
|
||||
.option("package", {
|
||||
describe: "Path of DT package.",
|
||||
type: "string",
|
||||
conflicts: "dt"
|
||||
})
|
||||
.option("dt", {
|
||||
describe: "Path of local DefinitelyTyped repository.",
|
||||
type: "string",
|
||||
conflicts: "package"
|
||||
})
|
||||
.option("rules", {
|
||||
describe: "Names of the rules to be updated. Leave this empty to update all rules.",
|
||||
type: "array",
|
||||
string: true,
|
||||
default: [] as string[]
|
||||
})
|
||||
.check(arg => {
|
||||
if (!arg.package && !arg.dt) {
|
||||
throw new Error("You must provide either argument 'package' or 'dt'.");
|
||||
}
|
||||
const unsupportedRules = arg.rules.filter(rule => ignoredRules.includes(rule));
|
||||
if (unsupportedRules.length > 0) {
|
||||
throw new Error(`Rules ${unsupportedRules.join(", ")} are not supported at the moment.`);
|
||||
}
|
||||
return true;
|
||||
}).argv;
|
||||
|
||||
if (args.package) {
|
||||
updatePackage(args.package, dtConfig(args.rules));
|
||||
} else if (args.dt) {
|
||||
updateAll(args.dt, dtConfig(args.rules));
|
||||
}
|
||||
if (args.package) {
|
||||
updatePackage(args.package, dtConfig(args.rules));
|
||||
} else if (args.dt) {
|
||||
updateAll(args.dt, dtConfig(args.rules));
|
||||
}
|
||||
}
|
||||
|
||||
const dtConfigPath = "dt.json";
|
||||
|
||||
function dtConfig(updatedRules: string[]): Config.IConfigurationFile {
|
||||
const config = Config.findConfiguration(dtConfigPath).results;
|
||||
if (!config) {
|
||||
throw new Error(`Could not load config at ${dtConfigPath}.`);
|
||||
const config = Config.findConfiguration(dtConfigPath).results;
|
||||
if (!config) {
|
||||
throw new Error(`Could not load config at ${dtConfigPath}.`);
|
||||
}
|
||||
// Disable ignored or non-updated rules.
|
||||
for (const entry of config.rules.entries()) {
|
||||
const [rule, ruleOpts] = entry;
|
||||
if (ignoredRules.includes(rule) || (updatedRules.length > 0 && !updatedRules.includes(rule))) {
|
||||
ruleOpts.ruleSeverity = "off";
|
||||
}
|
||||
// Disable ignored or non-updated rules.
|
||||
for (const entry of config.rules.entries()) {
|
||||
const [rule, ruleOpts] = entry;
|
||||
if (ignoredRules.includes(rule) || (updatedRules.length > 0 && !updatedRules.includes(rule))) {
|
||||
ruleOpts.ruleSeverity = "off";
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
function updateAll(dtPath: string, config: Config.IConfigurationFile): void {
|
||||
const packages = fs.readdirSync(path.join(dtPath, "types"));
|
||||
for (const pkg of packages) {
|
||||
updatePackage(path.join(dtPath, "types", pkg), config);
|
||||
}
|
||||
const packages = fs.readdirSync(path.join(dtPath, "types"));
|
||||
for (const pkg of packages) {
|
||||
updatePackage(path.join(dtPath, "types", pkg), config);
|
||||
}
|
||||
}
|
||||
|
||||
function updatePackage(pkgPath: string, baseConfig: Config.IConfigurationFile): void {
|
||||
installDependencies(pkgPath);
|
||||
const packages = walkPackageDir(pkgPath);
|
||||
installDependencies(pkgPath);
|
||||
const packages = walkPackageDir(pkgPath);
|
||||
|
||||
const linterOpts: ILinterOptions = {
|
||||
fix: false,
|
||||
};
|
||||
const linterOpts: ILinterOptions = {
|
||||
fix: false
|
||||
};
|
||||
|
||||
for (const pkg of packages) {
|
||||
const results = pkg.lint(linterOpts, baseConfig);
|
||||
if (results.failures.length > 0) {
|
||||
const disabledRules = disableRules(results.failures);
|
||||
const newConfig = mergeConfigRules(pkg.config(), disabledRules, baseConfig);
|
||||
pkg.updateConfig(newConfig);
|
||||
}
|
||||
for (const pkg of packages) {
|
||||
const results = pkg.lint(linterOpts, baseConfig);
|
||||
if (results.failures.length > 0) {
|
||||
const disabledRules = disableRules(results.failures);
|
||||
const newConfig = mergeConfigRules(pkg.config(), disabledRules, baseConfig);
|
||||
pkg.updateConfig(newConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function installDependencies(pkgPath: string): void {
|
||||
if (fs.existsSync(path.join(pkgPath, "package.json"))) {
|
||||
cp.execSync(
|
||||
"npm install --ignore-scripts --no-shrinkwrap --no-package-lock --no-bin-links",
|
||||
{
|
||||
encoding: "utf8",
|
||||
cwd: pkgPath,
|
||||
});
|
||||
}
|
||||
if (fs.existsSync(path.join(pkgPath, "package.json"))) {
|
||||
cp.execSync("npm install --ignore-scripts --no-shrinkwrap --no-package-lock --no-bin-links", {
|
||||
encoding: "utf8",
|
||||
cwd: pkgPath
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function mergeConfigRules(
|
||||
config: Config.RawConfigFile,
|
||||
newRules: Config.RawRulesConfig,
|
||||
baseConfig: Config.IConfigurationFile): Config.RawConfigFile {
|
||||
const activeRules: string[] = [];
|
||||
baseConfig.rules.forEach((ruleOpts, ruleName) => {
|
||||
if (ruleOpts.ruleSeverity !== "off") {
|
||||
activeRules.push(ruleName);
|
||||
}
|
||||
});
|
||||
const oldRules: Config.RawRulesConfig = config.rules || {};
|
||||
let newRulesConfig: Config.RawRulesConfig = {};
|
||||
for (const rule of Object.keys(oldRules)) {
|
||||
if (activeRules.includes(rule)) {
|
||||
continue;
|
||||
}
|
||||
newRulesConfig[rule] = oldRules[rule];
|
||||
}
|
||||
newRulesConfig = { ...newRulesConfig, ...newRules };
|
||||
return { ...config, rules: newRulesConfig };
|
||||
config: Config.RawConfigFile,
|
||||
newRules: Config.RawRulesConfig,
|
||||
baseConfig: Config.IConfigurationFile
|
||||
): Config.RawConfigFile {
|
||||
const activeRules: string[] = [];
|
||||
baseConfig.rules.forEach((ruleOpts, ruleName) => {
|
||||
if (ruleOpts.ruleSeverity !== "off") {
|
||||
activeRules.push(ruleName);
|
||||
}
|
||||
});
|
||||
const oldRules: Config.RawRulesConfig = config.rules || {};
|
||||
let newRulesConfig: Config.RawRulesConfig = {};
|
||||
for (const rule of Object.keys(oldRules)) {
|
||||
if (activeRules.includes(rule)) {
|
||||
continue;
|
||||
}
|
||||
newRulesConfig[rule] = oldRules[rule];
|
||||
}
|
||||
newRulesConfig = { ...newRulesConfig, ...newRules };
|
||||
return { ...config, rules: newRulesConfig };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,71 +140,71 @@ function mergeConfigRules(
|
||||
* packages.
|
||||
*/
|
||||
class LintPackage {
|
||||
private files: ts.SourceFile[] = [];
|
||||
private program: ts.Program;
|
||||
private files: ts.SourceFile[] = [];
|
||||
private program: ts.Program;
|
||||
|
||||
constructor(private rootDir: string) {
|
||||
this.program = Linter.createProgram(path.join(this.rootDir, "tsconfig.json"));
|
||||
}
|
||||
constructor(private rootDir: string) {
|
||||
this.program = Linter.createProgram(path.join(this.rootDir, "tsconfig.json"));
|
||||
}
|
||||
|
||||
config(): Config.RawConfigFile {
|
||||
return Config.readConfigurationFile(path.join(this.rootDir, "tslint.json"));
|
||||
}
|
||||
config(): Config.RawConfigFile {
|
||||
return Config.readConfigurationFile(path.join(this.rootDir, "tslint.json"));
|
||||
}
|
||||
|
||||
addFile(filePath: string): void {
|
||||
const file = this.program.getSourceFile(filePath);
|
||||
if (file) {
|
||||
this.files.push(file);
|
||||
}
|
||||
addFile(filePath: string): void {
|
||||
const file = this.program.getSourceFile(filePath);
|
||||
if (file) {
|
||||
this.files.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
lint(opts: ILinterOptions, config: Config.IConfigurationFile): LintResult {
|
||||
const linter = new Linter(opts, this.program);
|
||||
for (const file of this.files) {
|
||||
if (ignoreFile(file, this.rootDir, this.program)) {
|
||||
continue;
|
||||
}
|
||||
linter.lint(file.fileName, file.text, config);
|
||||
}
|
||||
return linter.getResult();
|
||||
lint(opts: ILinterOptions, config: Config.IConfigurationFile): LintResult {
|
||||
const linter = new Linter(opts, this.program);
|
||||
for (const file of this.files) {
|
||||
if (ignoreFile(file, this.rootDir, this.program)) {
|
||||
continue;
|
||||
}
|
||||
linter.lint(file.fileName, file.text, config);
|
||||
}
|
||||
return linter.getResult();
|
||||
}
|
||||
|
||||
updateConfig(config: Config.RawConfigFile): void {
|
||||
fs.writeFileSync(
|
||||
path.join(this.rootDir, "tslint.json"),
|
||||
stringify(config, { space: 4 }),
|
||||
{ encoding: "utf8", flag: "w" });
|
||||
}
|
||||
updateConfig(config: Config.RawConfigFile): void {
|
||||
fs.writeFileSync(path.join(this.rootDir, "tslint.json"), stringify(config, { space: 4 }), {
|
||||
encoding: "utf8",
|
||||
flag: "w"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function ignoreFile(file: ts.SourceFile, dirPath: string, program: ts.Program): boolean {
|
||||
return program.isSourceFileDefaultLibrary(file) || isExternalDependency(file, path.resolve(dirPath), program);
|
||||
return program.isSourceFileDefaultLibrary(file) || isExternalDependency(file, path.resolve(dirPath), program);
|
||||
}
|
||||
|
||||
function walkPackageDir(rootDir: string): LintPackage[] {
|
||||
const packages: LintPackage[] = [];
|
||||
const packages: LintPackage[] = [];
|
||||
|
||||
function walk(curPackage: LintPackage, dir: string): void {
|
||||
for (const ent of fs.readdirSync(dir, { encoding: "utf8", withFileTypes: true })) {
|
||||
const entPath = path.join(dir, ent.name);
|
||||
if (ent.isFile()) {
|
||||
curPackage.addFile(entPath);
|
||||
} else if (ent.isDirectory() && ent.name !== "node_modules") {
|
||||
if (isVersionDir(ent.name)) {
|
||||
const newPackage = new LintPackage(entPath);
|
||||
packages.push(newPackage);
|
||||
walk(newPackage, entPath);
|
||||
} else {
|
||||
walk(curPackage, entPath);
|
||||
}
|
||||
}
|
||||
function walk(curPackage: LintPackage, dir: string): void {
|
||||
for (const ent of fs.readdirSync(dir, { encoding: "utf8", withFileTypes: true })) {
|
||||
const entPath = path.join(dir, ent.name);
|
||||
if (ent.isFile()) {
|
||||
curPackage.addFile(entPath);
|
||||
} else if (ent.isDirectory() && ent.name !== "node_modules") {
|
||||
if (isVersionDir(ent.name)) {
|
||||
const newPackage = new LintPackage(entPath);
|
||||
packages.push(newPackage);
|
||||
walk(newPackage, entPath);
|
||||
} else {
|
||||
walk(curPackage, entPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const lintPackage = new LintPackage(rootDir);
|
||||
packages.push(lintPackage);
|
||||
walk(lintPackage, rootDir);
|
||||
return packages;
|
||||
const lintPackage = new LintPackage(rootDir);
|
||||
packages.push(lintPackage);
|
||||
walk(lintPackage, rootDir);
|
||||
return packages;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -211,39 +212,39 @@ function walkPackageDir(rootDir: string): LintPackage[] {
|
||||
* Examples: `ts3.5`, `v11`, `v0.6` are all version names.
|
||||
*/
|
||||
function isVersionDir(dirName: string): boolean {
|
||||
return /^ts\d+\.\d$/.test(dirName) || /^v\d+(\.\d+)?$/.test(dirName);
|
||||
return /^ts\d+\.\d$/.test(dirName) || /^v\d+(\.\d+)?$/.test(dirName);
|
||||
}
|
||||
|
||||
type RuleOptions = boolean | unknown[];
|
||||
type RuleDisabler = (failures: IRuleFailureJson[]) => RuleOptions;
|
||||
const defaultDisabler: RuleDisabler = () => {
|
||||
return false;
|
||||
return false;
|
||||
};
|
||||
|
||||
function disableRules(allFailures: RuleFailure[]): Config.RawRulesConfig {
|
||||
const ruleToFailures: Map<string, IRuleFailureJson[]> = new Map();
|
||||
for (const failure of allFailures) {
|
||||
const failureJson = failure.toJson();
|
||||
if (ruleToFailures.has(failureJson.ruleName)) {
|
||||
ruleToFailures.get(failureJson.ruleName)!.push(failureJson);
|
||||
} else {
|
||||
ruleToFailures.set(failureJson.ruleName, [failureJson]);
|
||||
}
|
||||
const ruleToFailures: Map<string, IRuleFailureJson[]> = new Map();
|
||||
for (const failure of allFailures) {
|
||||
const failureJson = failure.toJson();
|
||||
if (ruleToFailures.has(failureJson.ruleName)) {
|
||||
ruleToFailures.get(failureJson.ruleName)!.push(failureJson);
|
||||
} else {
|
||||
ruleToFailures.set(failureJson.ruleName, [failureJson]);
|
||||
}
|
||||
}
|
||||
|
||||
const newRulesConfig: Config.RawRulesConfig = {};
|
||||
ruleToFailures.forEach((failures, rule) => {
|
||||
if (ignoredRules.includes(rule)) {
|
||||
return;
|
||||
}
|
||||
const disabler = rule === "npm-naming" ? npmNamingDisabler : defaultDisabler;
|
||||
const opts: RuleOptions = disabler(failures);
|
||||
newRulesConfig[rule] = opts;
|
||||
});
|
||||
const newRulesConfig: Config.RawRulesConfig = {};
|
||||
ruleToFailures.forEach((failures, rule) => {
|
||||
if (ignoredRules.includes(rule)) {
|
||||
return;
|
||||
}
|
||||
const disabler = rule === "npm-naming" ? npmNamingDisabler : defaultDisabler;
|
||||
const opts: RuleOptions = disabler(failures);
|
||||
newRulesConfig[rule] = opts;
|
||||
});
|
||||
|
||||
return newRulesConfig;
|
||||
return newRulesConfig;
|
||||
}
|
||||
|
||||
if (!module.parent) {
|
||||
main();
|
||||
main();
|
||||
}
|
||||
|
||||
@@ -5,105 +5,106 @@ import stripJsonComments = require("strip-json-comments");
|
||||
import * as ts from "typescript";
|
||||
|
||||
export async function readJson(path: string) {
|
||||
const text = await readFile(path, "utf-8");
|
||||
return JSON.parse(stripJsonComments(text));
|
||||
const text = await readFile(path, "utf-8");
|
||||
return JSON.parse(stripJsonComments(text));
|
||||
}
|
||||
|
||||
export function failure(ruleName: string, s: string): string {
|
||||
return `${s} See: https://github.com/Microsoft/dtslint/blob/master/docs/${ruleName}.md`;
|
||||
return `${s} See: https://github.com/Microsoft/dtslint/blob/master/docs/${ruleName}.md`;
|
||||
}
|
||||
|
||||
export function getCommonDirectoryName(files: readonly string[]): string {
|
||||
let minLen = 999;
|
||||
let minDir = "";
|
||||
for (const file of files) {
|
||||
const dir = dirname(file);
|
||||
if (dir.length < minLen) {
|
||||
minDir = dir;
|
||||
minLen = dir.length;
|
||||
}
|
||||
let minLen = 999;
|
||||
let minDir = "";
|
||||
for (const file of files) {
|
||||
const dir = dirname(file);
|
||||
if (dir.length < minLen) {
|
||||
minDir = dir;
|
||||
minLen = dir.length;
|
||||
}
|
||||
return basename(minDir);
|
||||
}
|
||||
return basename(minDir);
|
||||
}
|
||||
|
||||
export function eachModuleStatement(sourceFile: ts.SourceFile, action: (statement: ts.Statement) => void): void {
|
||||
if (!sourceFile.isDeclarationFile) {
|
||||
return;
|
||||
}
|
||||
if (!sourceFile.isDeclarationFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const node of sourceFile.statements) {
|
||||
if (ts.isModuleDeclaration(node)) {
|
||||
const statements = getModuleDeclarationStatements(node);
|
||||
if (statements) {
|
||||
for (const statement of statements) {
|
||||
action(statement);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
action(node);
|
||||
for (const node of sourceFile.statements) {
|
||||
if (ts.isModuleDeclaration(node)) {
|
||||
const statements = getModuleDeclarationStatements(node);
|
||||
if (statements) {
|
||||
for (const statement of statements) {
|
||||
action(statement);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
action(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getModuleDeclarationStatements(node: ts.ModuleDeclaration): readonly ts.Statement[] | undefined {
|
||||
let { body } = node;
|
||||
while (body && body.kind === ts.SyntaxKind.ModuleDeclaration) {
|
||||
body = body.body;
|
||||
}
|
||||
return body && ts.isModuleBlock(body) ? body.statements : undefined;
|
||||
let { body } = node;
|
||||
while (body && body.kind === ts.SyntaxKind.ModuleDeclaration) {
|
||||
body = body.body;
|
||||
}
|
||||
return body && ts.isModuleBlock(body) ? body.statements : undefined;
|
||||
}
|
||||
|
||||
export async function getCompilerOptions(dirPath: string): Promise<ts.CompilerOptions> {
|
||||
const tsconfigPath = join(dirPath, "tsconfig.json");
|
||||
if (!await pathExists(tsconfigPath)) {
|
||||
throw new Error(`Need a 'tsconfig.json' file in ${dirPath}`);
|
||||
}
|
||||
return (await readJson(tsconfigPath)).compilerOptions;
|
||||
const tsconfigPath = join(dirPath, "tsconfig.json");
|
||||
if (!(await pathExists(tsconfigPath))) {
|
||||
throw new Error(`Need a 'tsconfig.json' file in ${dirPath}`);
|
||||
}
|
||||
return (await readJson(tsconfigPath)).compilerOptions;
|
||||
}
|
||||
|
||||
export function withoutPrefix(s: string, prefix: string): string | undefined {
|
||||
return s.startsWith(prefix) ? s.slice(prefix.length) : undefined;
|
||||
return s.startsWith(prefix) ? s.slice(prefix.length) : undefined;
|
||||
}
|
||||
|
||||
export function last<T>(a: readonly T[]): T {
|
||||
assert(a.length !== 0);
|
||||
return a[a.length - 1];
|
||||
assert(a.length !== 0);
|
||||
return a[a.length - 1];
|
||||
}
|
||||
|
||||
export function assertDefined<T>(a: T | undefined): T {
|
||||
if (a === undefined) { throw new Error(); }
|
||||
return a;
|
||||
if (a === undefined) {
|
||||
throw new Error();
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
export async function mapDefinedAsync<T, U>(
|
||||
arr: Iterable<T>, mapper: (t: T) => Promise<U | undefined>): Promise<U[]> {
|
||||
const out = [];
|
||||
for (const a of arr) {
|
||||
const res = await mapper(a);
|
||||
if (res !== undefined) {
|
||||
out.push(res);
|
||||
}
|
||||
export async function mapDefinedAsync<T, U>(arr: Iterable<T>, mapper: (t: T) => Promise<U | undefined>): Promise<U[]> {
|
||||
const out = [];
|
||||
for (const a of arr) {
|
||||
const res = await mapper(a);
|
||||
if (res !== undefined) {
|
||||
out.push(res);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function isMainFile(fileName: string, allowNested: boolean) {
|
||||
// Linter may be run with cwd of the package. We want `index.d.ts` but not `submodule/index.d.ts` to match.
|
||||
if (fileName === "index.d.ts") {
|
||||
return true;
|
||||
}
|
||||
// Linter may be run with cwd of the package. We want `index.d.ts` but not `submodule/index.d.ts` to match.
|
||||
if (fileName === "index.d.ts") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (basename(fileName) !== "index.d.ts") {
|
||||
return false;
|
||||
}
|
||||
if (basename(fileName) !== "index.d.ts") {
|
||||
return false;
|
||||
}
|
||||
|
||||
let parent = dirname(fileName);
|
||||
// May be a directory for an older version, e.g. `v0`.
|
||||
// Note a types redirect `foo/ts3.1` should not have its own header.
|
||||
if (allowNested && /^v(0\.)?\d+$/.test(basename(parent))) {
|
||||
parent = dirname(parent);
|
||||
}
|
||||
let parent = dirname(fileName);
|
||||
// May be a directory for an older version, e.g. `v0`.
|
||||
// Note a types redirect `foo/ts3.1` should not have its own header.
|
||||
if (allowNested && /^v(0\.)?\d+$/.test(basename(parent))) {
|
||||
parent = dirname(parent);
|
||||
}
|
||||
|
||||
// Allow "types/foo/index.d.ts", not "types/foo/utils/index.d.ts"
|
||||
return basename(dirname(parent)) === "types";
|
||||
// Allow "types/foo/index.d.ts", not "types/foo/utils/index.d.ts"
|
||||
return basename(dirname(parent)) === "types";
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
declare function foo(): void;
|
||||
export = foo;
|
||||
export = foo;
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
// Definitions by: Jane Doe <https://github.com/janedoe>
|
||||
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
||||
|
||||
export default dtsCritic();
|
||||
export default dtsCritic();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Type definitions for package parseltongue 1.0
|
||||
// Project: https://github.com/bobby-headers/dt-header
|
||||
// Definitions by: Jane Doe <https://github.com/janedoe>
|
||||
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
||||
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
||||
|
||||
Reference in New Issue
Block a user