This commit is contained in:
TypeScript Bot
2021-12-01 15:42:37 +00:00
parent dae5c76848
commit 068061ac92
47 changed files with 3846 additions and 5100 deletions

View File

@@ -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();

View File

@@ -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();

View File

@@ -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

View File

@@ -5,4 +5,4 @@
declare function _default(): void;
export = _default;
export = _default;

View File

@@ -1 +1 @@
export default function(): void;
export default function(): void;

View File

@@ -1,3 +1,3 @@
export const a: () => void;
export const b: number;
export const foo: string;
export const foo: string;

View File

@@ -1,7 +1,7 @@
interface Exports {
(): void,
foo: () => {},
(): void;
foo: () => {};
}
declare const exp: Exports;
export = exp;
export = exp;

View File

@@ -1,6 +1,6 @@
interface Foo {
bar: () => void,
bar: () => void;
}
declare const foo: Foo;
export = foo;
export = foo;

View File

@@ -1 +1 @@
export default function(): void;
export default function(): void;

View File

@@ -1,2 +1,2 @@
export const a: number;
export const b: string;
export const b: string;

View File

@@ -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

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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);
});
}

View File

@@ -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";

View File

@@ -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.
}

View File

@@ -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;
}

View File

@@ -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;
}
}
/*

View File

@@ -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);
});
}

View File

@@ -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);
}
}
}

View File

@@ -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);
});
}

View File

@@ -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);
}
}

View File

@@ -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}.`
)
);
}
}
}
}

View File

@@ -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;
}

View File

@@ -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>();

View File

@@ -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;
}
}

View File

@@ -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"
]);

View File

@@ -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[];
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
});
}

View File

@@ -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");
}

View File

@@ -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)];
}
}

View File

@@ -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 }];
}

View File

@@ -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);
}
});
}
}
});
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -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";
}

View File

@@ -1,2 +1,2 @@
declare function foo(): void;
export = foo;
export = foo;

View File

@@ -3,4 +3,4 @@
// Definitions by: Jane Doe <https://github.com/janedoe>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
export default dtsCritic();
export default dtsCritic();

View File

@@ -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

1578
yarn.lock

File diff suppressed because it is too large Load Diff