Skip to content

Commit 95b859a

Browse files
authored
feat: migrate package to TypeScript and publish types (#534)
* change test-setup to ts * add build config * add types packages * rename files to ts * migrate fixer-return migrate fixer-return migrate fixer-return * migrate consistent-output migrate consistent-output * migrate meta-property-ordering * migrate no-deprecated-context-methods * migrate no-deprecated-report-api * migrate no-identical-tests * migrate no-meta-replaced-by * migrate no-meta-schema-default * migrate no-missing-message-ids * migrate no-missing-placeholders * migrate no-only-tests * migrate no-property-in-node * migrate no-unused-message-ids * migrate no-unused-placeholders * migrate no-useless-token-range * migrate prefer-message-ids * migrate prefer-object-rule * migrate prefer-output-null * migrate prefer-placeholders * migrate prefer-replace-text * migrate report-message-format * migrate require-meta-default-options * migrate require-meta-docs-description * migrate prefer-replace-text * migrate require-meta-docs-recommended * migrate require-meta-docs-url * migrate require-meta-fixable * migrate require-meta-has-suggestions * migrate require-meta-schema-description * migrate require-meta-schema * migrate require-meta-type * migrate test-case-property-ordering * migrate test-case-shorthand-strings * migrate plugin (index) * migrate indext.ts test * git mv all rule tests * fix type issues in no-meta-replaced-by test * fix type issues with no-missing-placeholders tests * fix type issues with no-unused-placeholders tests * fix type issues with no-useless-token-range tests * fix type issues with report-message-format tests * remove invalid case from valid array * fix type issues with test-case-shorthand-strings * fix type issues in rule-setup tests * Add explicit extensions to imports without them * fix type issues in utils tests * fix merge issue * adjust import order * fix plugin type * fix utils tests * switch to tsup for build * Change import of `package.json` to require, for backwards compatibility * fix utils tests * update rule-setup tests * add build to publish workflow * add slashes to .gitignore * remove jsdoc type annotation from `fixer-return` * remove unnecessary param from `no-indentical-tests` * add early return in `no-missing-placeholders` * removed assert in `no-only-tests` * remove empty param annotation from `no-property-in-node` * remove casting from no-unused-message-ids * add back valid test case to require-meta-type * add explanatory comment to estree.d.ts * remove type annotation from comment in test-case-shorthand-string * removed casting from no-meta-schema-default * remove casts from no-useless-token-range * remove cast from report-message-format * remove cast from require-meta-default-options * remove cast from require-meta-docs-url * remove casts from require-meta-fixables * remove cast from require-meta-type * Adjust PartialRuleInfo types * addressed feedback in utils * ci: add typecheck step to CI workflow * removed unneeded ts-expect-error from eslint.config.ts * Address feedback in utils * remove non-null assertion from require-meta-docs-recommended
1 parent 96be606 commit 95b859a

File tree

83 files changed

+1757
-1029
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

83 files changed

+1757
-1029
lines changed

.github/workflows/main.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,13 @@ jobs:
4545
node-version: 'lts/*'
4646
- run: npm install
4747
- run: npm run test:remote
48+
49+
typecheck:
50+
runs-on: ubuntu-latest
51+
steps:
52+
- uses: actions/checkout@v4
53+
- uses: actions/setup-node@v4
54+
with:
55+
node-version: 'lts/*'
56+
- run: npm install
57+
- run: npm run typecheck

.github/workflows/release-please.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ jobs:
3838
if: ${{ steps.release.outputs.release_created }}
3939
- run: |
4040
npm install --force
41+
npm run build
4142
npm publish --provenance
4243
env:
4344
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

.gitignore

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
.idea
2-
coverage
1+
.idea/
2+
coverage/
33
.vscode
44
node_modules/
55
npm-debug.log
66
yarn.lock
77
.eslintcache
8+
dist/
89

910
# eslint-remote-tester
1011
eslint-remote-tester-results

eslint.config.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { FlatCompat } from '@eslint/eslintrc';
55
import { defineConfig } from 'eslint/config';
66
import markdown from 'eslint-plugin-markdown';
77
import pluginN from 'eslint-plugin-n';
8-
// @ts-expect-error - eslint-plugin is not typed yet
98
import eslintPlugin from './lib/index.js';
109

1110
const dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -17,7 +16,7 @@ const compat = new FlatCompat({
1716
export default defineConfig([
1817
// Global ignores
1918
{
20-
ignores: ['node_modules', 'coverage'],
19+
ignores: ['node_modules', 'coverage', 'dist'],
2120
},
2221
// Global settings
2322
{

lib/index.js renamed to lib/index.ts

Lines changed: 56 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@
22
* @fileoverview An ESLint plugin for linting ESLint plugins
33
* @author Teddy Katz
44
*/
5+
import { createRequire } from 'node:module';
56

6-
// ------------------------------------------------------------------------------
7-
// Requirements
8-
// ------------------------------------------------------------------------------
7+
import type { ESLint, Linter, Rule } from 'eslint';
98

10-
import packageMetadata from '../package.json' with { type: 'json' };
119
import consistentOutput from './rules/consistent-output.js';
1210
import fixerReturn from './rules/fixer-return.js';
1311
import metaPropertyOrdering from './rules/meta-property-ordering.js';
@@ -41,20 +39,56 @@ import requireMetaType from './rules/require-meta-type.js';
4139
import testCasePropertyOrdering from './rules/test-case-property-ordering.js';
4240
import testCaseShorthandStrings from './rules/test-case-shorthand-strings.js';
4341

42+
const require = createRequire(import.meta.url);
43+
44+
const packageMetadata = require("../package.json") as {
45+
name: string;
46+
version: string;
47+
};
48+
4449
const PLUGIN_NAME = packageMetadata.name.replace(/^eslint-plugin-/, '');
50+
const CONFIG_NAMES = [
51+
'all',
52+
'all-type-checked',
53+
'recommended',
54+
'rules',
55+
'tests',
56+
'rules-recommended',
57+
'tests-recommended',
58+
] as const;
59+
type ConfigName = (typeof CONFIG_NAMES)[number];
4560

46-
const configFilters = {
47-
all: (rule) => !rule.meta.docs.requiresTypeChecking,
61+
const configFilters: Record<ConfigName, (rule: Rule.RuleModule) => boolean> = {
62+
all: (rule: Rule.RuleModule) =>
63+
!(
64+
rule.meta?.docs &&
65+
'requiresTypeChecking' in rule.meta.docs &&
66+
rule.meta.docs.requiresTypeChecking
67+
),
4868
'all-type-checked': () => true,
49-
recommended: (rule) => rule.meta.docs.recommended,
50-
rules: (rule) => rule.meta.docs.category === 'Rules',
51-
tests: (rule) => rule.meta.docs.category === 'Tests',
52-
'rules-recommended': (rule) =>
69+
recommended: (rule: Rule.RuleModule) => !!rule.meta?.docs?.recommended,
70+
rules: (rule: Rule.RuleModule) => rule.meta?.docs?.category === 'Rules',
71+
tests: (rule: Rule.RuleModule) => rule.meta?.docs?.category === 'Tests',
72+
'rules-recommended': (rule: Rule.RuleModule) =>
5373
configFilters.recommended(rule) && configFilters.rules(rule),
54-
'tests-recommended': (rule) =>
74+
'tests-recommended': (rule: Rule.RuleModule) =>
5575
configFilters.recommended(rule) && configFilters.tests(rule),
5676
};
5777

78+
const createConfig = (configName: ConfigName): Linter.Config => ({
79+
name: `${PLUGIN_NAME}/${configName}`,
80+
plugins: {
81+
get [PLUGIN_NAME](): ESLint.Plugin {
82+
return plugin;
83+
},
84+
},
85+
rules: Object.fromEntries(
86+
(Object.keys(allRules) as (keyof typeof allRules)[])
87+
.filter((ruleName) => configFilters[configName](allRules[ruleName]))
88+
.map((ruleName) => [`${PLUGIN_NAME}/${ruleName}`, 'error']),
89+
),
90+
});
91+
5892
// ------------------------------------------------------------------------------
5993
// Plugin Definition
6094
// ------------------------------------------------------------------------------
@@ -93,34 +127,23 @@ const allRules = {
93127
'require-meta-type': requireMetaType,
94128
'test-case-property-ordering': testCasePropertyOrdering,
95129
'test-case-shorthand-strings': testCaseShorthandStrings,
96-
};
130+
} satisfies Record<string, Rule.RuleModule>;
97131

98-
/** @type {import("eslint").ESLint.Plugin} */
99132
const plugin = {
100133
meta: {
101134
name: packageMetadata.name,
102135
version: packageMetadata.version,
103136
},
104137
rules: allRules,
105-
configs: {}, // assigned later
106-
};
107-
108-
// configs
109-
Object.assign(
110-
plugin.configs,
111-
Object.keys(configFilters).reduce((configs, configName) => {
112-
return Object.assign(configs, {
113-
[configName]: {
114-
name: `${PLUGIN_NAME}/${configName}`,
115-
plugins: { [PLUGIN_NAME]: plugin },
116-
rules: Object.fromEntries(
117-
Object.keys(allRules)
118-
.filter((ruleName) => configFilters[configName](allRules[ruleName]))
119-
.map((ruleName) => [`${PLUGIN_NAME}/${ruleName}`, 'error']),
120-
),
121-
},
122-
});
123-
}, {}),
124-
);
138+
configs: {
139+
all: createConfig('all'),
140+
'all-type-checked': createConfig('all-type-checked'),
141+
recommended: createConfig('recommended'),
142+
rules: createConfig('rules'),
143+
tests: createConfig('tests'),
144+
'rules-recommended': createConfig('rules-recommended'),
145+
'tests-recommended': createConfig('tests-recommended'),
146+
},
147+
} satisfies ESLint.Plugin;
125148

126149
export default plugin;

lib/rules/consistent-output.js renamed to lib/rules/consistent-output.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@
22
* @fileoverview Enforce consistent use of `output` assertions in rule tests
33
* @author Teddy Katz
44
*/
5+
import type { Rule } from 'eslint';
56

67
import { getKeyName, getTestInfo } from '../utils.js';
78

9+
const keyNameMapper = (property: Parameters<typeof getKeyName>[0]) =>
10+
getKeyName(property);
11+
812
// ------------------------------------------------------------------------------
913
// Rule Definition
1014
// ------------------------------------------------------------------------------
11-
12-
/** @type {import('eslint').Rule.RuleModule} */
13-
const rule = {
15+
const rule: Rule.RuleModule = {
1416
meta: {
1517
type: 'suggestion',
1618
docs: {
@@ -20,7 +22,7 @@ const rule = {
2022
recommended: false,
2123
url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/consistent-output.md',
2224
},
23-
fixable: null, // or "code" or "whitespace"
25+
fixable: undefined, // or "code" or "whitespace"
2426
schema: [
2527
{
2628
type: 'string',
@@ -37,20 +39,18 @@ const rule = {
3739
},
3840

3941
create(context) {
40-
// ----------------------------------------------------------------------
41-
// Public
42-
// ----------------------------------------------------------------------
43-
const always = context.options[0] && context.options[0] === 'always';
42+
const always: boolean =
43+
context.options[0] && context.options[0] === 'always';
4444

4545
return {
4646
Program(ast) {
4747
getTestInfo(context, ast).forEach((testRun) => {
4848
const readableCases = testRun.invalid.filter(
49-
(testCase) => testCase.type === 'ObjectExpression',
49+
(testCase) => testCase?.type === 'ObjectExpression',
5050
);
5151
const casesWithoutOutput = readableCases.filter(
5252
(testCase) =>
53-
!testCase.properties.map(getKeyName).includes('output'),
53+
!testCase.properties.map(keyNameMapper).includes('output'),
5454
);
5555

5656
if (

lib/rules/fixer-return.js renamed to lib/rules/fixer-return.ts

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,37 @@
22
* @fileoverview require fixer functions to return a fix
33
* @author 薛定谔的猫<[email protected]>
44
*/
5-
65
import { getStaticValue } from '@eslint-community/eslint-utils';
6+
import type { Rule } from 'eslint';
7+
import type {
8+
ArrowFunctionExpression,
9+
FunctionExpression,
10+
Identifier,
11+
Node,
12+
Position,
13+
SourceLocation,
14+
} from 'estree';
715

816
import {
917
getContextIdentifiers,
1018
isAutoFixerFunction,
1119
isSuggestionFixerFunction,
1220
} from '../utils.js';
21+
import type { FunctionInfo } from '../types.js';
22+
23+
const DEFAULT_FUNC_INFO: FunctionInfo = {
24+
upper: null,
25+
codePath: null,
26+
hasReturnWithFixer: false,
27+
hasYieldWithFixer: false,
28+
shouldCheck: false,
29+
node: null,
30+
};
1331

1432
// ------------------------------------------------------------------------------
1533
// Rule Definition
1634
// ------------------------------------------------------------------------------
17-
18-
/** @type {import('eslint').Rule.RuleModule} */
19-
const rule = {
35+
const rule: Rule.RuleModule = {
2036
meta: {
2137
type: 'problem',
2238
docs: {
@@ -25,36 +41,32 @@ const rule = {
2541
recommended: true,
2642
url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/fixer-return.md',
2743
},
28-
fixable: null,
44+
fixable: undefined,
2945
schema: [],
3046
messages: {
3147
missingFix: 'Fixer function never returned a fix.',
3248
},
3349
},
3450

3551
create(context) {
36-
let funcInfo = {
37-
upper: null,
38-
codePath: null,
39-
hasReturnWithFixer: false,
40-
hasYieldWithFixer: false,
41-
shouldCheck: false,
42-
node: null,
43-
};
44-
let contextIdentifiers;
52+
let funcInfo: FunctionInfo = DEFAULT_FUNC_INFO;
53+
let contextIdentifiers = new Set<Identifier>();
4554

4655
/**
4756
* As we exit the fix() function, ensure we have returned or yielded a real fix by this point.
4857
* If not, report the function as a violation.
4958
*
50-
* @param {ASTNode} node - A node to check.
51-
* @param {Location} loc - Optional location to report violation on.
52-
* @returns {void}
59+
* @param node - A node to check.
60+
* @param loc - Optional location to report violation on.
5361
*/
5462
function ensureFunctionReturnedFix(
55-
node,
56-
loc = (node.id || node).loc.start,
57-
) {
63+
node: ArrowFunctionExpression | FunctionExpression,
64+
loc: Position | SourceLocation | undefined = (node.type ===
65+
'FunctionExpression' && node.id
66+
? node.id
67+
: node
68+
).loc?.start,
69+
): void {
5870
if (
5971
(node.generator && !funcInfo.hasYieldWithFixer) || // Generator function never yielded a fix
6072
(!node.generator && !funcInfo.hasReturnWithFixer) // Non-generator function never returned a fix
@@ -70,10 +82,9 @@ const rule = {
7082
/**
7183
* Check if a returned/yielded node is likely to be a fix or not.
7284
* A fix is an object created by fixer.replaceText() for example and returned by the fix function.
73-
* @param {ASTNode} node - node to check
74-
* @returns {boolean}
85+
* @param node - node to check
7586
*/
76-
function isFix(node) {
87+
function isFix(node: Node): boolean {
7788
if (node.type === 'ArrayExpression' && node.elements.length === 0) {
7889
// An empty array is not a fix.
7990
return false;
@@ -104,22 +115,22 @@ const rule = {
104115
},
105116

106117
// Stacks this function's information.
107-
onCodePathStart(codePath, node) {
118+
onCodePathStart(codePath: Rule.CodePath, node: Node) {
108119
funcInfo = {
109120
upper: funcInfo,
110121
codePath,
111122
hasYieldWithFixer: false,
112123
hasReturnWithFixer: false,
113124
shouldCheck:
114-
isAutoFixerFunction(node, contextIdentifiers) ||
115-
isSuggestionFixerFunction(node, contextIdentifiers),
125+
isAutoFixerFunction(node, contextIdentifiers, context) ||
126+
isSuggestionFixerFunction(node, contextIdentifiers, context),
116127
node,
117128
};
118129
},
119130

120131
// Pops this function's information.
121132
onCodePathEnd() {
122-
funcInfo = funcInfo.upper;
133+
funcInfo = funcInfo.upper ?? DEFAULT_FUNC_INFO;
123134
},
124135

125136
// Yield in generators
@@ -147,7 +158,7 @@ const rule = {
147158
'ArrowFunctionExpression:exit'(node) {
148159
if (funcInfo.shouldCheck) {
149160
const sourceCode = context.sourceCode;
150-
const loc = sourceCode.getTokenBefore(node.body).loc; // Show violation on arrow (=>).
161+
const loc = sourceCode.getTokenBefore(node.body)?.loc; // Show violation on arrow (=>).
151162
if (node.expression) {
152163
// When the return is implied (no curly braces around the body), we have to check the single body node directly.
153164
if (!isFix(node.body)) {

0 commit comments

Comments
 (0)