diff --git a/.changeset/eighty-jobs-wave.md b/.changeset/eighty-jobs-wave.md new file mode 100644 index 0000000000..42fdad5811 --- /dev/null +++ b/.changeset/eighty-jobs-wave.md @@ -0,0 +1,7 @@ +--- +"@definitelytyped/eslint-plugin": patch +"@definitelytyped/dts-critic": patch +"@definitelytyped/dtslint": patch +--- + +Move npm-naming lint rule from tslint to eslint diff --git a/packages/dts-critic/dt.ts b/packages/dts-critic/dt.ts index 3348c78656..c532dd2c64 100644 --- a/packages/dts-critic/dt.ts +++ b/packages/dts-critic/dt.ts @@ -2,26 +2,26 @@ import { dtsCritic as critic, ErrorKind } from "./index"; 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"]; +function hasNpmNamingLintRule(eslintPath: string): boolean { + if (fs.existsSync(eslintPath)) { + const eslint = JSON.parse(stripJsonComments(fs.readFileSync(eslintPath, "utf-8"))); + if (eslint.rules?.["@definitelytyped/npm-naming"] !== undefined) { + return !!eslint.rules["@definitelytyped/npm-naming"]; } 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; +function addNpmNamingLintRule(eslintPath: string): void { + if (fs.existsSync(eslintPath)) { + const eslint = JSON.parse(stripJsonComments(fs.readFileSync(eslintPath, "utf-8"))); + if (eslint.rules) { + eslint.rules["@definitelytyped/npm-naming"] = false; } else { - tslint.rules = { "npm-naming": false }; + eslint.rules = { "@definitelytyped/npm-naming": false }; } - fs.writeFileSync(tslintPath, JSON.stringify(tslint, undefined, 4), "utf-8"); + fs.writeFileSync(eslintPath, JSON.stringify(eslint, undefined, 4), "utf-8"); } } @@ -29,7 +29,7 @@ function main() { for (const item of fs.readdirSync("../DefinitelyTyped/types")) { const entry = "../DefinitelyTyped/types/" + item; try { - if (hasNpmNamingLintRule(entry + "/tslint.json")) { + if (hasNpmNamingLintRule(entry + "/.eslintrc.json")) { const errors = critic(entry + "/index.d.ts"); for (const error of errors) { switch (error.kind) { @@ -57,7 +57,7 @@ function main() { 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"); + addNpmNamingLintRule(entry + "/.eslintrc.json"); } else { console.log("could not parse error message: ", error.message); } diff --git a/packages/dts-critic/index.ts b/packages/dts-critic/index.ts index 8a298634c2..c261d1e169 100644 --- a/packages/dts-critic/index.ts +++ b/packages/dts-critic/index.ts @@ -206,7 +206,7 @@ export function getNpmInfo(name: string): NpmInfo { } return { isNpm: true, - versions: info.versions as string[], + versions: Array.isArray(info.versions) ? info.versions : [info.versions], tags: info["dist-tags"] as { [tag: string]: string | undefined }, }; } diff --git a/packages/dtslint/dt.json b/packages/dtslint/dt.json index e0da5d8948..9aa84ed656 100644 --- a/packages/dtslint/dt.json +++ b/packages/dtslint/dt.json @@ -1,6 +1,3 @@ { - "extends": "./dtslint.json", - "rules": { - "npm-naming": [true, { "mode": "code" }] - } + "extends": "./dtslint.json" } diff --git a/packages/dtslint/dtslint.json b/packages/dtslint/dtslint.json index 1162775685..bce47a8c3a 100644 --- a/packages/dtslint/dtslint.json +++ b/packages/dtslint/dtslint.json @@ -1,6 +1,4 @@ { "rulesDirectory": "./dist/rules", - "rules": { - "npm-naming": true - } + "rules": {} } diff --git a/packages/dtslint/package.json b/packages/dtslint/package.json index ca5251681b..be29bce514 100644 --- a/packages/dtslint/package.json +++ b/packages/dtslint/package.json @@ -22,7 +22,6 @@ "test": "../../node_modules/.bin/jest --config ../../jest.config.js packages/dtslint" }, "dependencies": { - "@definitelytyped/dts-critic": "workspace:*", "@definitelytyped/header-parser": "workspace:*", "@definitelytyped/typescript-versions": "workspace:*", "@definitelytyped/utils": "workspace:*", diff --git a/packages/dtslint/src/lint.ts b/packages/dtslint/src/lint.ts index 29ec277d9d..2a9fa1cff4 100644 --- a/packages/dtslint/src/lint.ts +++ b/packages/dtslint/src/lint.ts @@ -27,8 +27,8 @@ export async function lint( // TODO: To remove tslint, replace this with a ts.createProgram (probably) const lintProgram = Linter.createProgram(tsconfigPath); - // tslint no longer checks ExpectType; skip linting entirely if we're only checking ExpectType. - const linter = !expectOnly ? new Linter({ fix: false, formatter: "stylish" }, lintProgram) : undefined; + // TODO: remove tslint entirely + const linter = undefined as Linter | undefined; const configPath = getConfigPath(dirPath); // TODO: To port expect-rule, eslint's config will also need to include [minVersion, maxVersion] // Also: expect-rule should be renamed to expect-type or check-type or something @@ -67,6 +67,16 @@ export async function lint( const options: ESLint.Options = { cwd: dirPath, + baseConfig: { + overrides: [ + { + files: ["*.ts", "*.cts", "*.mts", "*.tsx"], + rules: { + "@definitelytyped/npm-naming": "error", + }, + }, + ], + }, overrideConfig: { overrides: [ { diff --git a/packages/dtslint/src/rules/npmNamingRule.ts b/packages/dtslint/src/rules/npmNamingRule.ts deleted file mode 100644 index 7743745d6b..0000000000 --- a/packages/dtslint/src/rules/npmNamingRule.ts +++ /dev/null @@ -1,311 +0,0 @@ -import { - 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"; - -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 Options = CriticOptions & { singleLine?: boolean }; - -const defaultOptions: ConfigOptions = { - 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. -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"], - }, - }, - ], - }, - 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))); - } -} - -function parseOptions(args: unknown[]): ConfigOptions { - 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; - } - - if (!arg.mode || typeof arg.mode !== "string") { - return defaultOptions; - } - - const mode = parseMode(arg.mode); - if (!mode) { - return defaultOptions; - } - - 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 }; - } -} - -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]]); - } - } - } - 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) }; - } -} - -function walk(ctx: Lint.WalkContext): void { - const { sourceFile } = ctx; - 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: - 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]; - -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; -} - -function filterErrors(diagnostics: CriticError[], ctx: Lint.WalkContext): 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; -} - -function isSuggestion(diagnostic: CriticError, options: Options): boolean { - return ( - options.mode === Mode.Code && - (enabledSuggestions as ErrorKind[]).includes(diagnostic.kind) && - !(options.errors as Map).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]] }]); - } -} - -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, -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, " "); - } - - 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(); - 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 }]; -} diff --git a/packages/dtslint/src/updateConfig.ts b/packages/dtslint/src/updateConfig.ts index 5d1a3a264e..388b2a0634 100644 --- a/packages/dtslint/src/updateConfig.ts +++ b/packages/dtslint/src/updateConfig.ts @@ -15,7 +15,6 @@ import { Configuration as Config, ILinterOptions, IRuleFailureJson, Linter, Lint import * as ts from "typescript"; import yargs = require("yargs"); import { isExternalDependency } from "./lint"; -import { disabler as npmNamingDisabler } from "./rules/npmNamingRule"; // Rule "expect" needs TypeScript version information, which this script doesn't collect. const ignoredRules: string[] = ["expect"]; @@ -216,12 +215,6 @@ function isVersionDir(dirName: string): boolean { 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; -}; - function disableRules(allFailures: RuleFailure[]): Config.RawRulesConfig { const ruleToFailures: Map = new Map(); for (const failure of allFailures) { @@ -234,14 +227,11 @@ function disableRules(allFailures: RuleFailure[]): Config.RawRulesConfig { } const newRulesConfig: Config.RawRulesConfig = {}; - ruleToFailures.forEach((failures, rule) => { - if (ignoredRules.includes(rule)) { - return; + for (const rule of ruleToFailures.keys()) { + if (!ignoredRules.includes(rule)) { + newRulesConfig[rule] = false; } - const disabler = rule === "npm-naming" ? npmNamingDisabler : defaultDisabler; - const opts: RuleOptions = disabler(failures); - newRulesConfig[rule] = opts; - }); + } return newRulesConfig; } diff --git a/packages/dtslint/src/util.ts b/packages/dtslint/src/util.ts index 3c4d8a195c..5142cf2265 100644 --- a/packages/dtslint/src/util.ts +++ b/packages/dtslint/src/util.ts @@ -12,10 +12,6 @@ export function readJson(path: string) { return JSON.parse(stripJsonComments(text)); } -export function failure(ruleName: string, s: string): string { - return `${s} See: https://github.com/microsoft/DefinitelyTyped-tools/blob/master/packages/dtslint/docs/${ruleName}.md`; -} - export function getCompilerOptions(dirPath: string): ts.CompilerOptions { const tsconfigPath = join(dirPath, "tsconfig.json"); if (!fs.existsSync(tsconfigPath)) { @@ -23,24 +19,3 @@ export function getCompilerOptions(dirPath: string): ts.CompilerOptions { } return readJson(tsconfigPath).compilerOptions as ts.CompilerOptions; } - -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; - } - - 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); - } - - // Allow "types/foo/index.d.ts", not "types/foo/utils/index.d.ts" - return basename(dirname(parent)) === "types"; -} diff --git a/packages/dtslint/test/npm-naming/code/tslint.json b/packages/dtslint/test/npm-naming/code/tslint.json deleted file mode 100644 index 3c9ea61f7d..0000000000 --- a/packages/dtslint/test/npm-naming/code/tslint.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "rulesDirectory": ["../../../dist/rules"], - "rules": { - "npm-naming": [true, {"mode": "code", "errors": [["NeedsExportEquals", true]], "single-line": true }] - } - } diff --git a/packages/dtslint/test/npm-naming/code/types/dts-critic/index.d.ts b/packages/dtslint/test/npm-naming/code/types/dts-critic/index.d.ts deleted file mode 100644 index 0d3239f05e..0000000000 --- a/packages/dtslint/test/npm-naming/code/types/dts-critic/index.d.ts +++ /dev/null @@ -1 +0,0 @@ -export default dtsCritic(); diff --git a/packages/dtslint/test/npm-naming/code/types/dts-critic/index.d.ts.lint b/packages/dtslint/test/npm-naming/code/types/dts-critic/index.d.ts.lint deleted file mode 100644 index bfbdf7ec78..0000000000 --- a/packages/dtslint/test/npm-naming/code/types/dts-critic/index.d.ts.lint +++ /dev/null @@ -1,8 +0,0 @@ -// Type definitions for package dts-critic 1.0 -~ [0] -// Project: https://https://github.com/DefinitelyTyped/dts-critic -// Definitions by: Jane Doe -// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped - -export default dtsCritic(); -[0]: The declaration doesn't match the JavaScript module 'dts-critic'. 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. If 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": [true,{"mode":"code","errors":[["NeedsExportEquals",false]]}] See: https://github.com/microsoft/DefinitelyTyped-tools/blob/master/packages/dtslint/docs/npm-naming.md diff --git a/packages/dtslint/test/npm-naming/name/tslint.json b/packages/dtslint/test/npm-naming/name/tslint.json deleted file mode 100644 index 18337910ac..0000000000 --- a/packages/dtslint/test/npm-naming/name/tslint.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "rulesDirectory": ["../../../dist/rules"], - "rules": { - "npm-naming": [true, {"mode": "name-only", "single-line": true}] - } -} diff --git a/packages/dtslint/test/npm-naming/name/types/wenceslas/index.d.ts b/packages/dtslint/test/npm-naming/name/types/wenceslas/index.d.ts deleted file mode 100644 index 81ab89bd42..0000000000 --- a/packages/dtslint/test/npm-naming/name/types/wenceslas/index.d.ts +++ /dev/null @@ -1 +0,0 @@ -// hi diff --git a/packages/dtslint/test/npm-naming/name/types/wenceslas/index.d.ts.lint b/packages/dtslint/test/npm-naming/name/types/wenceslas/index.d.ts.lint deleted file mode 100644 index 50888569ef..0000000000 --- a/packages/dtslint/test/npm-naming/name/types/wenceslas/index.d.ts.lint +++ /dev/null @@ -1,2 +0,0 @@ -// hi -~ [Declaration file must have a matching npm package. To resolve this error, either: 1. Change the name to match an npm package. 2. Add `"nonNpm": true` to the package.json to indicate that this is not an npm package. Ensure the package name is descriptive enough to avoid conflicts with future npm packages. If 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": false See: https://github.com/microsoft/DefinitelyTyped-tools/blob/master/packages/dtslint/docs/npm-naming.md] diff --git a/packages/dtslint/test/tsconfig.json b/packages/dtslint/test/tsconfig.json index 01cf16cd34..85ba8b7b57 100644 --- a/packages/dtslint/test/tsconfig.json +++ b/packages/dtslint/test/tsconfig.json @@ -9,6 +9,6 @@ { "path": ".." }, { "path": "../../utils" } ], - "exclude": ["expect", "npm-naming"] + "exclude": ["expect"] } \ No newline at end of file diff --git a/packages/dtslint/docs/npm-naming.md b/packages/eslint-plugin/docs/rules/npm-naming.md similarity index 100% rename from packages/dtslint/docs/npm-naming.md rename to packages/eslint-plugin/docs/rules/npm-naming.md diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index ec5b41dcde..d86b049994 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -22,6 +22,7 @@ "test": "../../node_modules/.bin/jest --config ../../jest.config.js packages/dtslint" }, "dependencies": { + "@definitelytyped/dts-critic": "workspace:*", "@definitelytyped/utils": "workspace:*", "@typescript-eslint/types": "^6.11.0", "@typescript-eslint/utils": "^6.11.0" @@ -35,6 +36,7 @@ }, "devDependencies": { "@definitelytyped/eslint-plugin": "link:./", + "@typescript-eslint/rule-tester": "^6.11.0", "@types/eslint": "^8.44.7", "glob": "^10.3.10", "jest-file-snapshot": "^0.5.0", diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index 92d37100e8..683a8bcabb 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -135,7 +135,13 @@ export const all: Linter.BaseConfig = { warnOnUnsupportedTypeScriptVersion: false, }, rules: { - ...Object.fromEntries(Object.keys(rules).map((name) => [`@definitelytyped/${name}`, "error"])), + ...Object.fromEntries( + Object.keys(rules) + // npm-naming is only enabled within dtslint. + // Leave it out of the preset so editors / he tests don't hit the network. + .filter((name) => name !== "npm-naming") + .map((name) => [`@definitelytyped/${name}`, "error"]), + ), "unicode-bom": ["error", "never"], "@typescript-eslint/ban-ts-comment": [ "error", diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 8ae6a10096..beb85d8436 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -16,6 +16,7 @@ import strictExportDeclareModifiers = require("./strict-export-declare-modifiers import noSingleDeclareModule = require("./no-single-declare-module"); import noOldDTHeader = require("./no-old-dt-header"); import noImportOfDevDependencies = require("./no-import-of-dev-dependencies"); +import npmNaming = require("./npm-naming"); import expect = require("./expect"); export const rules = { @@ -37,5 +38,6 @@ export const rules = { "no-single-declare-module": noSingleDeclareModule, "no-old-dt-header": noOldDTHeader, "no-import-of-dev-dependencies": noImportOfDevDependencies, + "npm-naming": npmNaming, expect, }; diff --git a/packages/eslint-plugin/src/rules/npm-naming.ts b/packages/eslint-plugin/src/rules/npm-naming.ts new file mode 100644 index 0000000000..33e5d70703 --- /dev/null +++ b/packages/eslint-plugin/src/rules/npm-naming.ts @@ -0,0 +1,211 @@ +import { + CheckOptions as CriticOptions, + dtsCritic as critic, + ErrorKind, + ExportErrorKind, + Mode, + parseExportErrorKind, + CriticError, +} from "@definitelytyped/dts-critic"; + +import { addSuggestion } from "../suggestions"; +import { createRule, isMainFile } from "../util"; +import { CodeRawOptionError, NpmNamingOptions } from "./npm-naming/types"; + +function parseEnabledErrors(errors: CodeRawOptionError[]): [ExportErrorKind, boolean][] { + const enabledChecks: [ExportErrorKind, boolean][] = []; + for (const tuple of errors) { + const error = parseExportErrorKind(tuple[0]); + if (error) { + enabledChecks.push([error, tuple[1]]); + } + } + return enabledChecks; +} + +function parseRawOptions(rawOptions: NpmNamingOptions): CriticOptions { + switch (rawOptions.mode) { + case Mode.Code: + return { ...rawOptions, errors: new Map(parseEnabledErrors(rawOptions.errors)) }; + case Mode.NameOnly: + return rawOptions; + } +} + +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) }; + + for (const err of enabledSuggestions) { + optionsWithSuggestions.errors.set(err, true); + } + + return optionsWithSuggestions; +} + +function eslintDisableOption(error: ErrorKind): string { + switch (error) { + case ErrorKind.NoMatchingNpmPackage: + case ErrorKind.NoMatchingNpmVersion: + case ErrorKind.NonNpmHasMatchingPackage: + return `"off"`; + case ErrorKind.NoDefaultExport: + case ErrorKind.NeedsExportEquals: + case ErrorKind.JsSignatureNotInDts: + case ErrorKind.JsPropertyNotInDts: + case ErrorKind.DtsSignatureNotInJs: + case ErrorKind.DtsPropertyNotInJs: + return JSON.stringify(["error", { mode: Mode.Code, errors: [[error, false]] }]); + } +} + +const rule = createRule<[NpmNamingOptions], "error">({ + name: "npm-naming", + defaultOptions: [ + { + mode: Mode.NameOnly, + }, + ], + meta: { + type: "problem", + docs: { + description: "Ensure that package name and DefinitelyTyped header match npm package info.", + }, + messages: { + error: `{{ error }} +If 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 .eslintrc.json file under "rules": + + "@definitelytyped/npm-naming": {{ option }}`, + }, + schema: [ + { + oneOf: [ + { + additionalProperties: false, + properties: { + mode: { + type: "string", + enum: [Mode.NameOnly], + }, + }, + type: "object", + }, + { + additionalProperties: false, + 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], + }, + { + description: "Whether the check is enabled or disabled.", + type: "boolean", + }, + ], + minItems: 2, + maxItems: 2, + }, + }, + }, + }, + ], + }, + ], + }, + create(context, [rawOptions]) { + if (!isMainFile(context.filename, /*allowNested*/ false)) { + return {}; + } + + const options = parseRawOptions(rawOptions); + const optionsWithSuggestions = toOptionsWithSuggestions(options); + const diagnostics = critic(context.filename, /* sourcePath */ undefined, optionsWithSuggestions); + const errors = filterErrors(diagnostics); + + for (const error of errors) { + switch (error.kind) { + case ErrorKind.NoMatchingNpmPackage: + case ErrorKind.NoMatchingNpmVersion: + case ErrorKind.NonNpmHasMatchingPackage: + case ErrorKind.DtsPropertyNotInJs: + case ErrorKind.DtsSignatureNotInJs: + case ErrorKind.JsPropertyNotInDts: + case ErrorKind.JsSignatureNotInDts: + case ErrorKind.NeedsExportEquals: + case ErrorKind.NoDefaultExport: + context.report({ + data: { + error: error.message, + option: eslintDisableOption(error.kind), + }, + loc: error.position + ? { + start: context.sourceCode.getLocFromIndex(error.position.start), + end: context.sourceCode.getLocFromIndex(error.position.start + error.position.length), + } + : { + end: { + line: 2, + column: 0, + }, + start: { + line: 1, + column: 0, + }, + }, + messageId: "error", + }); + break; + } + } + + return {}; + + function filterErrors(diagnostics: CriticError[]): CriticError[] { + const errors: CriticError[] = []; + + diagnostics.forEach((diagnostic) => { + if (isSuggestion(diagnostic)) { + addSuggestion( + context.filename, + "npm-naming", + diagnostic.message, + diagnostic.position?.start, + diagnostic.position?.length, + ); + } else { + errors.push(diagnostic); + } + }); + + return errors; + } + + function isSuggestion(diagnostic: CriticError): boolean { + return ( + options.mode === Mode.Code && + enabledSuggestions.includes(diagnostic.kind as ExportErrorKind) && + !(options.errors as Map).get(diagnostic.kind) + ); + } + }, +}); + +export = rule; diff --git a/packages/eslint-plugin/src/rules/npm-naming/types.ts b/packages/eslint-plugin/src/rules/npm-naming/types.ts new file mode 100644 index 0000000000..f2e3cd9a03 --- /dev/null +++ b/packages/eslint-plugin/src/rules/npm-naming/types.ts @@ -0,0 +1,14 @@ +import { ExportErrorKind, Mode } from "@definitelytyped/dts-critic"; + +export type CodeRawOptionError = [ExportErrorKind, boolean]; + +export interface CodeRawOptions { + mode: Mode.Code; + errors: CodeRawOptionError[]; +} + +export interface NameOnlyRawOptions { + mode: Mode.NameOnly; +} + +export type NpmNamingOptions = CodeRawOptions | NameOnlyRawOptions; diff --git a/packages/dtslint/src/suggestions.ts b/packages/eslint-plugin/src/suggestions.ts similarity index 87% rename from packages/dtslint/src/suggestions.ts rename to packages/eslint-plugin/src/suggestions.ts index 121a92f278..9029283560 100644 --- a/packages/dtslint/src/suggestions.ts +++ b/packages/eslint-plugin/src/suggestions.ts @@ -1,7 +1,6 @@ import fs = require("fs"); import os = require("os"); import path = require("path"); -import { WalkContext } from "tslint"; const suggestionsDir = path.join(os.homedir(), ".dts", "suggestions"); @@ -19,16 +18,16 @@ const existingPackages = new Set(); /** * A rule should call this function to provide a suggestion instead of a lint failure. */ -export function addSuggestion(ctx: WalkContext, message: string, start?: number, width?: number) { +export function addSuggestion(fileName: string, ruleName: string, message: string, start?: number, width?: number) { const suggestion: Suggestion = { - fileName: ctx.sourceFile.fileName, - ruleName: ctx.ruleName, + fileName, + ruleName, message, start, width, }; - const packageName = dtPackageName(ctx.sourceFile.fileName); + const packageName = dtPackageName(fileName); if (!packageName) { return; } diff --git a/packages/eslint-plugin/src/util.ts b/packages/eslint-plugin/src/util.ts index b649935c32..d04d14516c 100644 --- a/packages/eslint-plugin/src/util.ts +++ b/packages/eslint-plugin/src/util.ts @@ -116,3 +116,24 @@ export function getImportSource( return undefined; } + +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; + } + + if (path.basename(fileName) !== "index.d.ts") { + return false; + } + + let parent = path.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(path.basename(parent))) { + parent = path.dirname(parent); + } + + // Allow "types/foo/index.d.ts", not "types/foo/utils/index.d.ts" + return path.basename(path.dirname(parent)) === "types"; +} diff --git a/packages/eslint-plugin/test/__snapshots__/plugin.test.ts.snap b/packages/eslint-plugin/test/__snapshots__/plugin.test.ts.snap new file mode 100644 index 0000000000..9c3bd2f204 --- /dev/null +++ b/packages/eslint-plugin/test/__snapshots__/plugin.test.ts.snap @@ -0,0 +1,655 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`plugin should have the expected exports 1`] = ` +{ + "configs": { + "all": { + "overrides": [ + { + "files": [ + "*.cts", + "*.mts", + "*.ts", + "*.tsx", + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": true, + "warnOnUnsupportedTypeScriptVersion": false, + }, + "rules": { + "@definitelytyped/expect": "error", + "@definitelytyped/export-just-namespace": "error", + "@definitelytyped/no-any-union": "error", + "@definitelytyped/no-bad-reference": "error", + "@definitelytyped/no-const-enum": "error", + "@definitelytyped/no-dead-reference": "error", + "@definitelytyped/no-declare-current-package": "error", + "@definitelytyped/no-import-default-of-export-equals": "error", + "@definitelytyped/no-import-of-dev-dependencies": "error", + "@definitelytyped/no-old-dt-header": "error", + "@definitelytyped/no-relative-import-in-test": "error", + "@definitelytyped/no-self-import": "error", + "@definitelytyped/no-single-declare-module": "error", + "@definitelytyped/no-single-element-tuple-type": "error", + "@definitelytyped/no-unnecessary-generics": "error", + "@definitelytyped/no-useless-files": "error", + "@definitelytyped/prefer-declare-function": "error", + "@definitelytyped/redundant-undefined": "error", + "@definitelytyped/strict-export-declare-modifiers": "error", + "@typescript-eslint/adjacent-overload-signatures": "error", + "@typescript-eslint/array-type": [ + "error", + { + "default": "array-simple", + }, + ], + "@typescript-eslint/ban-ts-comment": [ + "error", + { + "ts-check": false, + "ts-expect-error": false, + "ts-ignore": "allow-with-description", + "ts-nocheck": true, + }, + ], + "@typescript-eslint/ban-types": [ + "error", + { + "extendDefaults": true, + "types": { + "{}": false, + }, + }, + ], + "@typescript-eslint/consistent-type-definitions": "error", + "@typescript-eslint/explicit-member-accessibility": [ + "error", + { + "accessibility": "no-public", + }, + ], + "@typescript-eslint/naming-convention": [ + "error", + { + "custom": { + "match": false, + "regex": "^I[A-Z]", + }, + "format": [], + "selector": "interface", + }, + ], + "@typescript-eslint/no-empty-interface": "error", + "@typescript-eslint/no-invalid-void-type": [ + "error", + { + "allowAsThisParameter": true, + "allowInGenericTypeArguments": true, + }, + ], + "@typescript-eslint/no-misused-new": "error", + "@typescript-eslint/prefer-namespace-keyword": "error", + "@typescript-eslint/triple-slash-reference": [ + "error", + { + "path": "always", + "types": "prefer-import", + }, + ], + "no-duplicate-imports": "error", + "unicode-bom": [ + "error", + "never", + ], + }, + }, + ], + "plugins": [ + "@definitelytyped", + "@typescript-eslint", + "jsdoc", + ], + "rules": { + "jsdoc/check-tag-names": [ + "error", + { + "definedTags": [ + "addVersion", + "also", + "api", + "author", + "beta", + "brief", + "category", + "cfg", + "chainable", + "check", + "checkReturnValue", + "classDescription", + "condparamprivilege", + "constraint", + "credits", + "declaration", + "defApiFeature", + "defaultValue", + "detail", + "end", + "eventproperty", + "experimental", + "export", + "expose", + "extendscript", + "factory", + "field", + "final", + "fixme", + "fluent", + "for", + "governance", + "header", + "hidden-property", + "hidden", + "id", + "jsx", + "jsxImportSource", + "label", + "language", + "legacy", + "link", + "listen", + "locus", + "methodOf", + "minVersion", + "ngdoc", + "nonstandard", + "note", + "npm", + "observable", + "option", + "optionobject", + "options", + "packageDocumentation", + "param", + "parent", + "platform", + "plugin", + "preserve", + "privateRemarks", + "privilegeLevel", + "privilegeName", + "proposed", + "range", + "readOnly", + "related", + "remark", + "remarks", + "required", + "requires", + "restriction", + "returnType", + "section", + "see", + "since", + "const", + "singleton", + "source", + "struct", + "suppress", + "targetfolder", + "enum", + "title", + "record", + "title", + "TODO", + "trigger", + "triggers", + "typeparam", + "typeParam", + "unsupported", + "url", + "usage", + "warn", + "warning", + "version", + ], + "typed": true, + }, + ], + }, + "settings": { + "jsdoc": { + "tagNamePreference": { + "argument": "argument", + "exception": "exception", + "function": "function", + "method": "method", + "param": "param", + "return": "return", + "returns": "returns", + }, + }, + }, + }, + }, + "meta": { + "name": "@definitelytyped/eslint-plugin", + "version": "0.0.197", + }, + "rules": { + "expect": { + "create": [Function], + "defaultOptions": [ + {}, + ], + "meta": { + "docs": { + "description": "Asserts types with $ExpectType.", + "url": "https://github.com/microsoft/DefinitelyTyped-tools/tree/master/packages/eslint-plugin/docs/rules/expect.md", + }, + "messages": { + "diagnostic": "TypeScript{{versionNameString}} {{message}}", + "failure": "TypeScript{{versionNameString}} expected type to be: + {{expectedType}} +got: + {{actualType}}", + "needInstall": "A module look-up failed, this often occurs when you need to run \`pnpm install\` on a dependent module before you can lint. + +Before you debug, first try running: + + pnpm install -w --filter '...{./types/{{dirPath}}}...' + +Then re-run.", + "noMatch": "Cannot match a node to this assertion. If this is a multiline function call, ensure the assertion is on the line above.", + "noTsconfig": "Could not find a tsconfig.json file.", + "programContents": "Program source files differ between TypeScript versions. This may be a dtslint bug. +Expected to find a file '{{fileName}}' present in 5.2, but did not find it in ts@{{versionName}}.", + "twoAssertions": "This line has 2 $ExpectType assertions.", + }, + "schema": [ + { + "additionalProperties": false, + "properties": { + "versionsToTest": { + "items": { + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + }, + "versionName": { + "type": "string", + }, + }, + "required": [ + "versionName", + "path", + ], + "type": "object", + }, + "type": "array", + }, + }, + "type": "object", + }, + ], + "type": "problem", + }, + }, + "export-just-namespace": { + "create": [Function], + "defaultOptions": [], + "meta": { + "docs": { + "description": "Forbids \`export = foo\` where \`foo\` is a namespace and isn't merged with a function/class/type/interface.", + "url": "https://github.com/microsoft/DefinitelyTyped-tools/tree/master/packages/eslint-plugin/docs/rules/export-just-namespace.md", + }, + "messages": { + "useTheBody": "Instead of \`export =\`-ing a namespace, use the body of the namespace as the module body.", + }, + "schema": [], + "type": "problem", + }, + }, + "no-any-union": { + "create": [Function], + "defaultOptions": [], + "meta": { + "docs": { + "description": "Forbid a union to contain \`any\`", + "url": "https://github.com/microsoft/DefinitelyTyped-tools/tree/master/packages/eslint-plugin/docs/rules/no-any-union.md", + }, + "messages": { + "anyUnion": "Including \`any\` in a union will override all other members of the union.", + }, + "schema": [], + "type": "problem", + }, + }, + "no-bad-reference": { + "create": [Function], + "defaultOptions": [], + "meta": { + "docs": { + "description": "Forbids bad references, including those that resolve outside of the package or path references in non-declaration files.", + "url": "https://github.com/microsoft/DefinitelyTyped-tools/tree/master/packages/eslint-plugin/docs/rules/no-bad-reference.md", + }, + "messages": { + "backslashes": "Use forward slashes in paths.", + "importLeaves": "The import "{{text}}" resolves to the current package, but uses relative paths.", + "importOutside": "The import "{{text}}" resolves outside of the package. Use a bare import to reference other packages.", + "referenceLeaves": "The reference "{{text}}" resolves to the current package, but uses relative paths.", + "referenceOutside": "The reference "{{text}}" resolves outside of the package. Use a global reference to reference other packages.", + "testReference": "The path reference "{{text}}" is disallowed outside declaration files. Use "" or include the file in tsconfig instead.", + }, + "schema": [], + "type": "problem", + }, + }, + "no-const-enum": { + "create": [Function], + "defaultOptions": [], + "meta": { + "docs": { + "description": "Forbid \`const enum\`", + "url": "https://github.com/microsoft/DefinitelyTyped-tools/tree/master/packages/eslint-plugin/docs/rules/no-const-enum.md", + }, + "messages": { + "constEnum": "Use of \`const enum\` is forbidden.", + }, + "schema": [], + "type": "problem", + }, + }, + "no-dead-reference": { + "create": [Function], + "defaultOptions": [], + "meta": { + "docs": { + "description": "Ensures that all \`/// \` comments go at the top of the file.", + "url": "https://github.com/microsoft/DefinitelyTyped-tools/tree/master/packages/eslint-plugin/docs/rules/no-dead-reference.md", + }, + "messages": { + "referenceAtTop": "\`/// \` directive must be at top of file to take effect.", + }, + "schema": [], + "type": "problem", + }, + }, + "no-declare-current-package": { + "create": [Function], + "defaultOptions": [], + "meta": { + "docs": { + "description": "Don't use an ambient module declaration of the current package; use a normal module.", + "url": "https://github.com/microsoft/DefinitelyTyped-tools/tree/master/packages/eslint-plugin/docs/rules/no-declare-current-package.md", + }, + "messages": { + "noDeclareCurrentPackage": "Instead of declaring a module with \`declare module "{{ text }}"\`, write its contents in directly in {{ preferred }}.", + }, + "schema": [], + "type": "problem", + }, + }, + "no-import-default-of-export-equals": { + "create": [Function], + "defaultOptions": [], + "meta": { + "docs": { + "description": "Forbid a default import to reference an \`export =\` module.", + "url": "https://github.com/microsoft/DefinitelyTyped-tools/tree/master/packages/eslint-plugin/docs/rules/no-import-default-of-export-equals.md", + }, + "messages": { + "noImportDefaultOfExportEquals": "The module {{moduleName}} uses \`export = \`. Import with \`import {{importName}} = require({{moduleName}})\`.", + }, + "schema": [], + "type": "problem", + }, + }, + "no-import-of-dev-dependencies": { + "create": [Function], + "defaultOptions": [], + "meta": { + "docs": { + "description": "Forbid imports and references to devDependencies inside .d.ts files.", + "url": "https://github.com/microsoft/DefinitelyTyped-tools/tree/master/packages/eslint-plugin/docs/rules/no-import-of-dev-dependencies.md", + }, + "messages": { + "noImportOfDevDependencies": ".d.ts files may not import packages in devDependencies.", + "noReferenceOfDevDependencies": ".d.ts files may not triple-slash reference packages in devDependencies.", + }, + "schema": [], + "type": "problem", + }, + }, + "no-old-dt-header": { + "create": [Function], + "defaultOptions": [], + "meta": { + "docs": { + "description": "Forbids in all files, in declaration files, and all in test files.", + "url": "https://github.com/microsoft/DefinitelyTyped-tools/tree/master/packages/eslint-plugin/docs/rules/no-bad-reference.md", + }, + "messages": { + "noOldDTHeader": "Specify package metadata in package.json. Do not use a header like \`// Type definitions for foo 1.2\`", + }, + "schema": [], + "type": "problem", + }, + }, + "no-relative-import-in-test": { + "create": [Function], + "defaultOptions": [], + "meta": { + "docs": { + "description": "Forbids test (non-declaration) files to use relative imports.", + "url": "https://github.com/microsoft/DefinitelyTyped-tools/tree/master/packages/eslint-plugin/docs/rules/no-relative-import-in-test.md", + }, + "messages": { + "useGlobalImport": "Test file should not use a relative import. Use a global import as if this were a user of the package.", + }, + "schema": [], + "type": "problem", + }, + }, + "no-self-import": { + "create": [Function], + "defaultOptions": [], + "meta": { + "docs": { + "description": "Forbids declaration files to import the current package using a global import or old versions with a relative import.", + "url": "https://github.com/microsoft/DefinitelyTyped-tools/tree/master/packages/eslint-plugin/docs/rules/no-self-import.md", + }, + "messages": { + "useOnlyCurrentVersion": "Don't import an old version of the current package.", + "useRelativeImport": "Declaration file should not use a global import of itself. Use a relative import.", + }, + "schema": [], + "type": "problem", + }, + }, + "no-single-declare-module": { + "create": [Function], + "defaultOptions": [], + "meta": { + "docs": { + "description": "Don't use an ambient module declaration if there's just one -- write it as a normal module.", + "url": "https://github.com/microsoft/DefinitelyTyped-tools/tree/master/packages/eslint-plugin/docs/rules/no-single-declare-module.md", + }, + "messages": { + "oneModuleDeclaration": "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.", + }, + "schema": [], + "type": "problem", + }, + }, + "no-single-element-tuple-type": { + "create": [Function], + "defaultOptions": [], + "meta": { + "docs": { + "description": "Forbids \`[T]\`, which should be \`T[]\`.", + "url": "https://github.com/microsoft/DefinitelyTyped-tools/tree/master/packages/eslint-plugin/docs/rules/no-single-element-tuple-type.md", + }, + "messages": { + "singleElementTupleType": "Type [T] is a single-element tuple type. You probably meant T[].", + }, + "schema": [], + "type": "problem", + }, + }, + "no-unnecessary-generics": { + "create": [Function], + "defaultOptions": [], + "meta": { + "docs": { + "description": "Forbids signatures using a generic parameter only once.", + "url": "https://github.com/microsoft/DefinitelyTyped-tools/tree/master/packages/eslint-plugin/docs/rules/no-unnecessary-generics.md", + }, + "messages": { + "never": "Type parameter {{name}} is never used.", + "sole": "Type parameter {{name}} is used only once.", + }, + "schema": [], + "type": "problem", + }, + }, + "no-useless-files": { + "create": [Function], + "defaultOptions": [], + "meta": { + "docs": { + "description": "Forbids files with no content.", + "url": "https://github.com/microsoft/DefinitelyTyped-tools/tree/master/packages/eslint-plugin/docs/rules/no-useless-files.md", + }, + "messages": { + "noContent": "File has no content.", + }, + "schema": [], + "type": "problem", + }, + }, + "npm-naming": { + "create": [Function], + "defaultOptions": [ + { + "mode": "name-only", + }, + ], + "meta": { + "docs": { + "description": "Ensure that package name and DefinitelyTyped header match npm package info.", + "url": "https://github.com/microsoft/DefinitelyTyped-tools/tree/master/packages/eslint-plugin/docs/rules/npm-naming.md", + }, + "messages": { + "error": "{{ error }} +If 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 .eslintrc.json file under "rules": + + "@definitelytyped/npm-naming": {{ option }}", + }, + "schema": [ + { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "mode": { + "enum": [ + "name-only", + ], + "type": "string", + }, + }, + "type": "object", + }, + { + "additionalProperties": false, + "properties": { + "errors": { + "items": { + "items": [ + { + "description": "Name of the check.", + "enum": [ + "NeedsExportEquals", + "NoDefaultExport", + ], + "type": "string", + }, + { + "description": "Whether the check is enabled or disabled.", + "type": "boolean", + }, + ], + "maxItems": 2, + "minItems": 2, + "type": "array", + }, + "type": "array", + }, + "mode": { + "enum": [ + "code", + ], + "type": "string", + }, + }, + "type": "object", + }, + ], + }, + ], + "type": "problem", + }, + }, + "prefer-declare-function": { + "create": [Function], + "defaultOptions": [], + "meta": { + "docs": { + "description": "Forbids \`const x: () => void\`.", + "url": "https://github.com/microsoft/DefinitelyTyped-tools/tree/master/packages/eslint-plugin/docs/rules/prefer-declare-function.md", + }, + "messages": { + "variableFunction": "Use a function declaration instead of a variable of function type.", + }, + "schema": [], + "type": "problem", + }, + }, + "redundant-undefined": { + "create": [Function], + "defaultOptions": [], + "meta": { + "docs": { + "description": "Forbids optional parameters from including an explicit \`undefined\` in their type; requires it in optional properties.", + "url": "https://github.com/microsoft/DefinitelyTyped-tools/tree/master/packages/eslint-plugin/docs/rules/redundant-undefined.md", + }, + "messages": { + "redundantUndefined": "Parameter is optional, so no need to include \`undefined\` in the type.", + }, + "schema": [], + "type": "problem", + }, + }, + "strict-export-declare-modifiers": { + "create": [Function], + "defaultOptions": [], + "meta": { + "docs": { + "description": "Enforces strict rules about where the 'export' and 'declare' modifiers may appear.", + "url": "https://github.com/microsoft/DefinitelyTyped-tools/tree/master/packages/eslint-plugin/docs/rules/strict-export-declare-modifiers.md", + }, + "messages": { + "missingExplicitExport": "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.", + "redundantDeclare": "'declare' keyword is redundant here.", + "redundantExport": "'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.", + }, + "schema": [], + "type": "problem", + }, + }, + }, +} +`; diff --git a/packages/eslint-plugin/test/npm-naming.test.ts b/packages/eslint-plugin/test/npm-naming.test.ts new file mode 100644 index 0000000000..c0f6622a11 --- /dev/null +++ b/packages/eslint-plugin/test/npm-naming.test.ts @@ -0,0 +1,57 @@ +import { RuleTester } from "@typescript-eslint/rule-tester"; + +import * as npmNaming from "../src/rules/npm-naming"; +import { ErrorKind, Mode } from "@definitelytyped/dts-critic"; + +const ruleTester = new RuleTester({ + parser: "@typescript-eslint/parser", +}); + +ruleTester.run("npm-naming", npmNaming, { + invalid: [ + { + code: `export default dtsCritic();`, + errors: [ + { + data: { + error: `The declaration doesn't match the JavaScript module 'dts-critic'. 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.`, + option: `["error",{"mode":"code","errors":[["NeedsExportEquals",false]]}]`, + }, + column: 1, + endColumn: 1, + line: 1, + endLine: 2, + messageId: "error", + }, + ], + filename: "packages/eslint-plugin/test/types/dts-critic/index.d.ts", + options: [{ mode: Mode.Code, errors: [[ErrorKind.NeedsExportEquals, true]] }], + }, + { + code: `// test content`, + errors: [ + { + data: { + error: `Declaration file must have a matching npm package. +To resolve this error, either: +1. Change the name to match an npm package. +2. Add \`\"nonNpm\": true\` to the package.json to indicate that this is not an npm package. + Ensure the package name is descriptive enough to avoid conflicts with future npm packages.`, + option: `"off"`, + }, + column: 1, + endColumn: 1, + line: 1, + endLine: 2, + messageId: "error", + }, + ], + filename: "packages/eslint-plugin/test/types/wenceslas/index.d.ts", + options: [{ mode: Mode.NameOnly }], + }, + ], + valid: [], +}); diff --git a/packages/eslint-plugin/test/plugin.test.ts b/packages/eslint-plugin/test/plugin.test.ts new file mode 100644 index 0000000000..eb29692182 --- /dev/null +++ b/packages/eslint-plugin/test/plugin.test.ts @@ -0,0 +1,7 @@ +import plugin = require("../src/index"); + +describe("plugin", () => { + it("should have the expected exports", () => { + expect(plugin).toMatchSnapshot(); + }); +}); diff --git a/packages/eslint-plugin/test/types/dts-critic/index.d.ts b/packages/eslint-plugin/test/types/dts-critic/index.d.ts new file mode 100644 index 0000000000..56732de851 --- /dev/null +++ b/packages/eslint-plugin/test/types/dts-critic/index.d.ts @@ -0,0 +1,9 @@ +declare function _default(): void; + +declare namespace _default { + export function findDtsName(dtsPath: string): string; + export function checkNames(names: string[]): unknown; + export function checkSource(text: string): unknown; + export function findNames(): string[]; + export function retrieveNpmHomepageOrFail(): string[]; +} diff --git a/packages/dtslint/test/npm-naming/code/types/dts-critic/package.json b/packages/eslint-plugin/test/types/dts-critic/package.json similarity index 64% rename from packages/dtslint/test/npm-naming/code/types/dts-critic/package.json rename to packages/eslint-plugin/test/types/dts-critic/package.json index 844ba94919..c75acfe312 100644 --- a/packages/dtslint/test/npm-naming/code/types/dts-critic/package.json +++ b/packages/eslint-plugin/test/types/dts-critic/package.json @@ -1,17 +1,17 @@ { - "private": true, - "name": "@types/dts-critic", - "version": "1.0.9999", - "projects": [ - "https://github.com/microsoft/TypeScript" - ], "devDependencies": { "@types/dts-critic": "workspace:." }, + "private": true, + "projects": [ + "https://typescriptlang.org" + ], + "version": "1.0.9999", + "name": "@types/dts-critic", "owners": [ { - "name": "Jane Doe", - "githubUsername": "janedoe" + "githubUsername": "ghost", + "name": "Not Default" } ] -} +} \ No newline at end of file diff --git a/packages/eslint-plugin/test/types/wenceslas/index.d.ts b/packages/eslint-plugin/test/types/wenceslas/index.d.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/dtslint/test/npm-naming/name/types/wenceslas/package.json b/packages/eslint-plugin/test/types/wenceslas/package.json similarity index 56% rename from packages/dtslint/test/npm-naming/name/types/wenceslas/package.json rename to packages/eslint-plugin/test/types/wenceslas/package.json index 0bbc875c71..d5ed8d2e19 100644 --- a/packages/dtslint/test/npm-naming/name/types/wenceslas/package.json +++ b/packages/eslint-plugin/test/types/wenceslas/package.json @@ -1,17 +1,17 @@ { - "private": true, - "name": "@types/wenceslas", - "version": "0.0.9999", - "projects": [ - "https://github.com/bobby-headers/dt-header" - ], "devDependencies": { "@types/wenceslas": "workspace:." }, + "private": true, + "projects": [ + "https://typescriptlang.org" + ], + "version": "0.9.9999", + "name": "@types/wenceslas", "owners": [ { - "name": "Jane Doe", - "githubUsername": "janedoe" + "githubUsername": "ghost", + "name": "Not Default" } ] -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1b889e47ca..5f05a74a98 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -131,9 +131,6 @@ importers: packages/dtslint: dependencies: - '@definitelytyped/dts-critic': - specifier: workspace:* - version: link:../dts-critic '@definitelytyped/header-parser': specifier: workspace:* version: link:../header-parser @@ -226,6 +223,9 @@ importers: packages/eslint-plugin: dependencies: + '@definitelytyped/dts-critic': + specifier: workspace:* + version: link:../dts-critic '@definitelytyped/utils': specifier: workspace:* version: link:../utils @@ -254,6 +254,9 @@ importers: '@types/eslint': specifier: ^8.44.7 version: 8.44.7 + '@typescript-eslint/rule-tester': + specifier: ^6.11.0 + version: 6.11.0(@eslint/eslintrc@2.1.3)(eslint@8.53.0)(typescript@5.2.2) glob: specifier: ^10.3.10 version: 10.3.10 @@ -2035,6 +2038,25 @@ packages: transitivePeerDependencies: - supports-color + /@typescript-eslint/rule-tester@6.11.0(@eslint/eslintrc@2.1.3)(eslint@8.53.0)(typescript@5.2.2): + resolution: {integrity: sha512-4OQn7HuOnqtg2yDjBvshUs7NnQ4ySKjylh+dbmGojkau7wX8laUcIRUEu3qS2+LyQAmt1yoM7lKdGzJSG3TXyQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@eslint/eslintrc': '>=2' + eslint: '>=8' + dependencies: + '@eslint/eslintrc': 2.1.3 + '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.2.2) + '@typescript-eslint/utils': 6.11.0(eslint@8.53.0)(typescript@5.2.2) + ajv: 6.12.6 + eslint: 8.53.0 + lodash.merge: 4.6.2 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /@typescript-eslint/scope-manager@6.11.0: resolution: {integrity: sha512-0A8KoVvIURG4uhxAdjSaxy8RdRE//HztaZdG8KiHLP8WOXSk0vlF7Pvogv+vlJA5Rnjj/wDcFENvDaHb+gKd1A==} engines: {node: ^16.0.0 || >=18.0.0}