From 0bf5ac530b78373ab2bbf22376d8dc857721426b Mon Sep 17 00:00:00 2001 From: Bryan Mishkin <698306+bmish@users.noreply.github.com> Date: Tue, 13 Dec 2022 14:55:48 -0500 Subject: [PATCH] chore: enable type-aware ts linting --- .eslintrc.cjs | 11 ++++++++++- lib/cli.ts | 34 +++++++++++++++++---------------- lib/generator.ts | 7 +++++-- lib/option-parsers.ts | 4 +++- lib/package-json.ts | 15 +++++++++------ lib/plugin-config-resolution.ts | 7 +++++-- lib/rule-doc-notices.ts | 24 +++++++++++++++++++---- lib/rule-list-columns.ts | 3 +-- lib/rule-list.ts | 4 ++-- lib/types.ts | 3 ++- 10 files changed, 75 insertions(+), 37 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 9bfbb81b..95be8deb 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -37,13 +37,22 @@ module.exports = { overrides: [ { parser: '@typescript-eslint/parser', + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./tsconfig.json'], + }, files: ['*.ts'], - extends: ['plugin:@typescript-eslint/recommended'], + extends: [ + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-requiring-type-checking', + ], rules: { 'node/no-unsupported-features/es-syntax': [ 'error', { ignores: ['dynamicImport', 'modules'] }, ], + + '@typescript-eslint/require-array-sort-compare': 'error', }, }, ], diff --git a/lib/cli.ts b/lib/cli.ts index 5bf60bb0..dd8bf692 100644 --- a/lib/cli.ts +++ b/lib/cli.ts @@ -127,19 +127,21 @@ async function loadConfigFileOptions(): Promise { ); } - const config = explorerResults.config; + const config = explorerResults.config; // eslint-disable-line @typescript-eslint/no-unsafe-assignment -- Rules are disabled because we haven't applied the GenerateOptions type until after we finish validating/normalizing. // Additional validation that couldn't be handled by ajv. + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (config.postprocess && typeof config.postprocess !== 'function') { throw new Error('postprocess must be a function'); } // Perform any normalization. + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (typeof config.pathRuleList === 'string') { - config.pathRuleList = [config.pathRuleList]; + config.pathRuleList = [config.pathRuleList]; // eslint-disable-line @typescript-eslint/no-unsafe-member-access } - return explorerResults.config; + return explorerResults.config as GenerateOptions; } return {}; } @@ -164,9 +166,9 @@ export async function run( ) .option( '--check [boolean]', - `(optional) Whether to check for and fail if there is a diff. No output will be written. Typically used during CI. (default: ${ + `(optional) Whether to check for and fail if there is a diff. No output will be written. Typically used during CI. (default: ${String( OPTION_DEFAULTS[OPTION_TYPE.CHECK] - })`, + )})`, parseBoolean ) .option( @@ -183,16 +185,16 @@ export async function run( ) .option( '--ignore-deprecated-rules [boolean]', - `(optional) Whether to ignore deprecated rules from being checked, displayed, or updated. (default: ${ + `(optional) Whether to ignore deprecated rules from being checked, displayed, or updated. (default: ${String( OPTION_DEFAULTS[OPTION_TYPE.IGNORE_DEPRECATED_RULES] - })`, + )})`, parseBoolean ) .option( '--init-rule-docs [boolean]', - `(optional) Whether to create rule doc files if they don't yet exist. (default: ${ + `(optional) Whether to create rule doc files if they don't yet exist. (default: ${String( OPTION_DEFAULTS[OPTION_TYPE.INIT_RULE_DOCS] - })`, + )})`, parseBoolean ) .option( @@ -213,9 +215,9 @@ export async function run( '--rule-doc-notices ', `(optional) Ordered, comma-separated list of notices to display in rule doc. Non-applicable notices will be hidden. (choices: "${Object.values( NOTICE_TYPE - ).join('", "')}") (default: ${ + ).join('", "')}") (default: ${String( OPTION_DEFAULTS[OPTION_TYPE.RULE_DOC_NOTICES] - })`, + )})`, collectCSV, [] ) @@ -233,9 +235,9 @@ export async function run( ) .option( '--rule-doc-section-options [boolean]', - `(optional) Whether to require an "Options" or "Config" rule doc section and mention of any named options for rules with options. (default: ${ + `(optional) Whether to require an "Options" or "Config" rule doc section and mention of any named options for rules with options. (default: ${String( OPTION_DEFAULTS[OPTION_TYPE.RULE_DOC_SECTION_OPTIONS] - })`, + )})`, parseBoolean ) .addOption( @@ -250,9 +252,9 @@ export async function run( '--rule-list-columns ', `(optional) Ordered, comma-separated list of columns to display in rule list. Empty columns will be hidden. (choices: "${Object.values( COLUMN_TYPE - ).join('", "')})" (default: ${ + ).join('", "')})" (default: ${String( OPTION_DEFAULTS[OPTION_TYPE.RULE_LIST_COLUMNS] - })`, + )})`, collectCSV, [] ) @@ -268,7 +270,7 @@ export async function run( '--url-rule-doc ', '(optional) Link to documentation for each rule. Useful when it differs from the rule doc path on disk (e.g. custom documentation site in use). Use `{name}` placeholder for the rule name.' ) - .action(async function (path, options: GenerateOptions) { + .action(async function (path: string, options: GenerateOptions) { // Load config file options and merge with CLI options. // CLI options take precedence. // For this to work, we can't have any default values from the CLI options that will override the config file options (except empty arrays, as arrays will be merged). diff --git a/lib/generator.ts b/lib/generator.ts index 8742214a..7fea7fbb 100644 --- a/lib/generator.ts +++ b/lib/generator.ts @@ -269,7 +269,8 @@ export async function generate(path: string, options?: GenerateOptions) { ); } - for (const pathRuleListItem of Array.isArray(pathRuleList) + // eslint-disable-next-line unicorn/no-instanceof-array -- using Array.isArray() loses type information about the array. + for (const pathRuleListItem of pathRuleList instanceof Array ? pathRuleList : [pathRuleList]) { // Find the exact filename. @@ -277,7 +278,9 @@ export async function generate(path: string, options?: GenerateOptions) { join(path, pathRuleListItem) ); if (!pathToFile || !existsSync(pathToFile)) { - throw new Error(`Could not find ${pathRuleList} in ESLint plugin.`); + throw new Error( + `Could not find ${String(pathRuleList)} in ESLint plugin.` + ); } // Update the rules list in this file. diff --git a/lib/option-parsers.ts b/lib/option-parsers.ts index d853e36c..db2f69ae 100644 --- a/lib/option-parsers.ts +++ b/lib/option-parsers.ts @@ -40,7 +40,9 @@ export function parseConfigEmojiOptions( if (!config || !emoji || extra.length > 0) { throw new Error( - `Invalid configEmoji option: ${configEmojiItem}. Expected format: config,emoji` + `Invalid configEmoji option: ${String( + configEmojiItem + )}. Expected format: config,emoji` ); } diff --git a/lib/package-json.ts b/lib/package-json.ts index 6da61a96..e48bf024 100644 --- a/lib/package-json.ts +++ b/lib/package-json.ts @@ -17,9 +17,9 @@ function loadPackageJson(path: string): PackageJson { if (!existsSync(pluginPackageJsonPath)) { throw new Error('Could not find package.json of ESLint plugin.'); } - const pluginPackageJson: PackageJson = JSON.parse( + const pluginPackageJson = JSON.parse( readFileSync(join(pluginRoot, 'package.json'), 'utf8') - ); + ) as PackageJson; return pluginPackageJson; } @@ -28,7 +28,7 @@ export async function loadPlugin(path: string): Promise { const pluginRoot = getPluginRoot(path); try { // Try require first which should work for CJS plugins. - return require(pluginRoot); // eslint-disable-line import/no-dynamic-require + return require(pluginRoot) as Plugin; // eslint-disable-line import/no-dynamic-require } catch { // Otherwise, for ESM plugins, we'll have to try to resolve the exact plugin entry point and import it. const pluginPackageJson = loadPackageJson(path); @@ -47,6 +47,7 @@ export async function loadPlugin(path: string): Promise { ['.', 'node', 'import', 'require', 'default']; for (const prop of propertiesToCheck) { // @ts-expect-error -- The union type for the object is causing trouble. + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const value = exports[prop]; if (typeof value === 'string') { pluginEntryPoint = value; @@ -66,7 +67,9 @@ export async function loadPlugin(path: string): Promise { ); } - const { default: plugin } = await importAbs(pluginEntryPointAbs); + const { default: plugin } = (await importAbs(pluginEntryPointAbs)) as { + default: Plugin; + }; return plugin; } } @@ -108,9 +111,9 @@ export function getCurrentPackageVersion(): string { ? '../package.json' : /* istanbul ignore next -- can't test the compiled version in test */ '../../package.json'; - const packageJson: PackageJson = JSON.parse( + const packageJson = JSON.parse( readFileSync(new URL(pathToPackageJson, import.meta.url), 'utf8') - ); + ) as PackageJson; if (!packageJson.version) { throw new Error('Could not find package.json `version`.'); } diff --git a/lib/plugin-config-resolution.ts b/lib/plugin-config-resolution.ts index 4e81c369..e92f6f00 100644 --- a/lib/plugin-config-resolution.ts +++ b/lib/plugin-config-resolution.ts @@ -36,7 +36,8 @@ async function resolveConfigExtends( extendItems: readonly string[] | string ): Promise { const rules: Rules = {}; - for (const extend of Array.isArray(extendItems) + // eslint-disable-next-line unicorn/no-instanceof-array -- using Array.isArray() loses type information about the array. + for (const extend of extendItems instanceof Array ? extendItems : [extendItems]) { if ( @@ -47,7 +48,9 @@ async function resolveConfigExtends( continue; } - const { default: config } = await importAbs(extend); + const { default: config } = (await importAbs(extend)) as { + default: Config; + }; const extendedRules = await resolveConfigRules(config); Object.assign(rules, extendedRules); } diff --git a/lib/rule-doc-notices.ts b/lib/rule-doc-notices.ts index 98a1098a..bc7a2d53 100644 --- a/lib/rule-doc-notices.ts +++ b/lib/rule-doc-notices.ts @@ -32,7 +32,7 @@ function severityToTerminology(severity: SEVERITY_TYPE) { return 'is _disabled_'; /* istanbul ignore next -- this shouldn't happen */ default: - throw new Error(`Unknown severity: ${severity}`); + throw new Error(`Unknown severity: ${String(severity)}`); } } @@ -187,7 +187,7 @@ const RULE_NOTICES: { ); return `${EMOJI_DEPRECATED} This rule is deprecated.${ replacedBy && replacedBy.length > 0 - ? ` It was replaced by ${replacementRuleList}.` + ? ` It was replaced by ${String(replacementRuleList)}.` : '' }`; }, @@ -391,7 +391,9 @@ function makeRuleDocTitle( /* istanbul ignore next -- this shouldn't happen */ default: throw new Error( - `Unhandled rule doc title format fallback: ${ruleDocTitleFormatWithFallback}` + `Unhandled rule doc title format fallback: ${String( + ruleDocTitleFormatWithFallback + )}` ); } } @@ -399,10 +401,22 @@ function makeRuleDocTitle( switch (ruleDocTitleFormatWithFallback) { // Backticks (code-style) only used around rule name to differentiate it when the rule description is also present. case 'desc': + /* istanbul ignore next -- this shouldn't happen */ + if (!descriptionFormatted) { + throw new Error('Attempting to display description when none exists'); + } return `# ${descriptionFormatted}`; case 'desc-parens-name': + /* istanbul ignore next -- this shouldn't happen */ + if (!descriptionFormatted) { + throw new Error('Attempting to display description when none exists'); + } return `# ${descriptionFormatted} (\`${name}\`)`; case 'desc-parens-prefix-name': + /* istanbul ignore next -- this shouldn't happen */ + if (!descriptionFormatted) { + throw new Error('Attempting to display description when none exists'); + } return `# ${descriptionFormatted} (\`${pluginPrefix}/${name}\`)`; case 'name': return `# ${name}`; @@ -411,7 +425,9 @@ function makeRuleDocTitle( /* istanbul ignore next -- this shouldn't happen */ default: throw new Error( - `Unhandled rule doc title format: ${ruleDocTitleFormatWithFallback}` + `Unhandled rule doc title format: ${String( + ruleDocTitleFormatWithFallback + )}` ); } } diff --git a/lib/rule-list-columns.ts b/lib/rule-list-columns.ts index f54a7891..9d14e37b 100644 --- a/lib/rule-list-columns.ts +++ b/lib/rule-list-columns.ts @@ -125,8 +125,7 @@ export function getColumns( ), // Show type column only if we found at least one rule with a standard type. [COLUMN_TYPE.TYPE]: ruleDetails.some( - (ruleDetail) => - ruleDetail.type && RULE_TYPES.includes(ruleDetail.type as any) // eslint-disable-line @typescript-eslint/no-explicit-any + (ruleDetail) => ruleDetail.type && RULE_TYPES.includes(ruleDetail.type) ), }; diff --git a/lib/rule-list.ts b/lib/rule-list.ts index 7b55b5c5..de61c3d4 100644 --- a/lib/rule-list.ts +++ b/lib/rule-list.ts @@ -61,7 +61,7 @@ function getPropertyFromRule( } const rule = plugin.rules[ruleName]; - return getProperty(rule, property) as any; // eslint-disable-line @typescript-eslint/no-explicit-any -- This could be any type, not just undefined (https://github.com/sindresorhus/dot-prop/issues/95). + return getProperty(rule, property) as unknown; } function getConfigurationColumnValueForRule( @@ -307,7 +307,7 @@ function generateRulesListMarkdownWithRuleListSplit( parts.push( `${'#'.repeat(headerLevel)} ${ - isBooleanableTrue(value) ? ruleListSplitTitle : value + isBooleanableTrue(value) ? ruleListSplitTitle : value // eslint-disable-line @typescript-eslint/restrict-template-expressions -- TODO: better handling to ensure value is a string. }`, generateRulesListMarkdown( columns, diff --git a/lib/types.ts b/lib/types.ts index da619f9b..6e96b18d 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,5 +1,6 @@ import type { RuleDocTitleFormat } from './rule-doc-title-format.js'; import type { TSESLint, JSONSchema } from '@typescript-eslint/utils'; +import type { RULE_TYPE } from './rule-type.js'; // Standard ESLint types. @@ -43,7 +44,7 @@ export interface RuleDetails { requiresTypeChecking: boolean; deprecated: boolean; schema: JSONSchema.JSONSchema4; - type?: string; // Rule might not have a type. + type?: `${RULE_TYPE}`; // Rule might not have a type. } /**