diff --git a/.github/workflows/type-check.yml b/.github/workflows/type-check.yml index c0e6b4d033..0aaca2cb00 100644 --- a/.github/workflows/type-check.yml +++ b/.github/workflows/type-check.yml @@ -44,18 +44,34 @@ jobs: - name: build types run: npm run build-types + # Pack the lib into a tarball so that when we install the lib later in the + # test-published-types directory, it's only install `dependencies` of the + # lib. + - name: pack the lib + run: npm pack --pack-destination /tmp/ + + - name: find the packed lib + run: echo "ESLINT_PLUGIN_REACT_PATH=$(ls /tmp/eslint-plugin-react*.tgz | tail -n 1)" >> $GITHUB_ENV + + - name: show the path to the packed lib + run: echo "$ESLINT_PLUGIN_REACT_PATH" + - name: npm install working directory run: npm install working-directory: test-published-types - - name: install typescript version ${{ matrix.ts_version }} - run: npm install --no-save typescript@${{ matrix.ts_version }} + - name: install eslint-plugin-react and typescript version ${{ matrix.ts_version }} + run: npm install --no-save "$ESLINT_PLUGIN_REACT_PATH" typescript@${{ matrix.ts_version }} working-directory: test-published-types - name: show installed typescript version run: npm list typescript --depth=0 working-directory: test-published-types + - name: show installed eslint-plugin-react version + run: npm list eslint-plugin-react --depth=0 + working-directory: test-published-types + - name: check types with lib "${{ matrix.ts_lib }}" run: npx tsc --lib ${{ matrix.ts_lib }} working-directory: test-published-types diff --git a/CHANGELOG.md b/CHANGELOG.md index 638e053935..ff0bedbc3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,13 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ### Fixed * [`no-danger`]: avoid a crash on a nested component name ([#3833][] @ljharb) +* [Fix] types: correct generated type declaration ([#3840][] @ocavue) ### Changed * [Tests] [`jsx-no-script-url`]: Improve tests ([#3849][] @radu2147) [#3849]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3849 +[#3840]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3840 [#3833]: https://github.com/jsx-eslint/eslint-plugin-react/issues/3833 ## [7.37.2] - 2024.10.22 diff --git a/index.js b/index.js index 8426993e5b..365f17446c 100644 --- a/index.js +++ b/index.js @@ -11,7 +11,7 @@ function filterRules(rules, predicate) { /** * @param {object} rules - rules object mapping rule name to rule module - * @returns {Record} + * @returns {Record} */ function configureAsError(rules) { return fromEntries(Object.keys(rules).map((key) => [`react/${key}`, 2])); @@ -31,6 +31,10 @@ const plugins = [ 'react', ]; +// TODO: with TS 4.5+, inline this +const SEVERITY_ERROR = /** @type {2} */ (2); +const SEVERITY_OFF = /** @type {0} */ (0); + const configs = { recommended: { plugins, @@ -40,28 +44,28 @@ const configs = { }, }, rules: { - 'react/display-name': 2, - 'react/jsx-key': 2, - 'react/jsx-no-comment-textnodes': 2, - 'react/jsx-no-duplicate-props': 2, - 'react/jsx-no-target-blank': 2, - 'react/jsx-no-undef': 2, - 'react/jsx-uses-react': 2, - 'react/jsx-uses-vars': 2, - 'react/no-children-prop': 2, - 'react/no-danger-with-children': 2, - 'react/no-deprecated': 2, - 'react/no-direct-mutation-state': 2, - 'react/no-find-dom-node': 2, - 'react/no-is-mounted': 2, - 'react/no-render-return-value': 2, - 'react/no-string-refs': 2, - 'react/no-unescaped-entities': 2, - 'react/no-unknown-property': 2, - 'react/no-unsafe': 0, - 'react/prop-types': 2, - 'react/react-in-jsx-scope': 2, - 'react/require-render-return': 2, + 'react/display-name': SEVERITY_ERROR, + 'react/jsx-key': SEVERITY_ERROR, + 'react/jsx-no-comment-textnodes': SEVERITY_ERROR, + 'react/jsx-no-duplicate-props': SEVERITY_ERROR, + 'react/jsx-no-target-blank': SEVERITY_ERROR, + 'react/jsx-no-undef': SEVERITY_ERROR, + 'react/jsx-uses-react': SEVERITY_ERROR, + 'react/jsx-uses-vars': SEVERITY_ERROR, + 'react/no-children-prop': SEVERITY_ERROR, + 'react/no-danger-with-children': SEVERITY_ERROR, + 'react/no-deprecated': SEVERITY_ERROR, + 'react/no-direct-mutation-state': SEVERITY_ERROR, + 'react/no-find-dom-node': SEVERITY_ERROR, + 'react/no-is-mounted': SEVERITY_ERROR, + 'react/no-render-return-value': SEVERITY_ERROR, + 'react/no-string-refs': SEVERITY_ERROR, + 'react/no-unescaped-entities': SEVERITY_ERROR, + 'react/no-unknown-property': SEVERITY_ERROR, + 'react/no-unsafe': SEVERITY_OFF, + 'react/prop-types': SEVERITY_ERROR, + 'react/react-in-jsx-scope': SEVERITY_ERROR, + 'react/require-render-return': SEVERITY_ERROR, }, }, all: { @@ -82,8 +86,8 @@ const configs = { jsxPragma: null, // for @typescript/eslint-parser }, rules: { - 'react/react-in-jsx-scope': 0, - 'react/jsx-uses-react': 0, + 'react/react-in-jsx-scope': SEVERITY_OFF, + 'react/jsx-uses-react': SEVERITY_OFF, }, }, }; diff --git a/lib/rules/forbid-foreign-prop-types.js b/lib/rules/forbid-foreign-prop-types.js index 7724049af4..982119af1c 100644 --- a/lib/rules/forbid-foreign-prop-types.js +++ b/lib/rules/forbid-foreign-prop-types.js @@ -109,7 +109,7 @@ module.exports = { && !ast.isAssignmentLHS(node) && !isAllowedAssignment(node) )) || ( - // @ts-expect-error The JSXText type is not present in the estree type definitions + // @ts-expect-error: Literal is not a valid type (node.property.type === 'Literal' || node.property.type === 'JSXText') && 'value' in node.property && node.property.value === 'propTypes' diff --git a/lib/rules/forbid-prop-types.js b/lib/rules/forbid-prop-types.js index df40706d00..44de1fa023 100644 --- a/lib/rules/forbid-prop-types.js +++ b/lib/rules/forbid-prop-types.js @@ -195,6 +195,7 @@ module.exports = { const propTypesSpecifier = node.specifiers.find((specifier) => ( 'imported' in specifier && specifier.imported + && 'name' in specifier.imported && specifier.imported.name === 'PropTypes' )); if (propTypesSpecifier) { diff --git a/lib/rules/forward-ref-uses-ref.js b/lib/rules/forward-ref-uses-ref.js index aeedeb82df..3a0b7de4c9 100644 --- a/lib/rules/forward-ref-uses-ref.js +++ b/lib/rules/forward-ref-uses-ref.js @@ -41,6 +41,7 @@ const messages = { removeForwardRef: 'Remove forwardRef wrapper', }; +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { diff --git a/lib/rules/index.js b/lib/rules/index.js index 1e010b677b..0e73ab1a3f 100644 --- a/lib/rules/index.js +++ b/lib/rules/index.js @@ -3,7 +3,7 @@ /* eslint global-require: 0 */ /** @satisfies {Record} */ -module.exports = { +const rules = { 'boolean-prop-naming': require('./boolean-prop-naming'), 'button-has-type': require('./button-has-type'), 'checked-requires-onchange-or-readonly': require('./checked-requires-onchange-or-readonly'), @@ -108,3 +108,5 @@ module.exports = { 'style-prop-object': require('./style-prop-object'), 'void-dom-elements-no-children': require('./void-dom-elements-no-children'), }; + +module.exports = rules; diff --git a/lib/rules/jsx-fragments.js b/lib/rules/jsx-fragments.js index 6835331025..b4a545846e 100644 --- a/lib/rules/jsx-fragments.js +++ b/lib/rules/jsx-fragments.js @@ -170,7 +170,12 @@ module.exports = { ImportDeclaration(node) { if (node.source && node.source.value === 'react') { node.specifiers.forEach((spec) => { - if ('imported' in spec && spec.imported && spec.imported.name === fragmentPragma) { + if ( + 'imported' in spec + && spec.imported + && 'name' in spec.imported + && spec.imported.name === fragmentPragma + ) { if (spec.local) { fragmentNames.add(spec.local.name); } diff --git a/lib/rules/jsx-no-literals.js b/lib/rules/jsx-no-literals.js index 230d33a18d..45c9a54c48 100644 --- a/lib/rules/jsx-no-literals.js +++ b/lib/rules/jsx-no-literals.js @@ -17,6 +17,14 @@ const docsUrl = require('../util/docsUrl'); const report = require('../util/report'); const getText = require('../util/eslint').getText; +/** @typedef {import('eslint').Rule.RuleModule} RuleModule */ + +/** @typedef {import('../../types/rules/jsx-no-literals').Config} Config */ +/** @typedef {import('../../types/rules/jsx-no-literals').RawConfig} RawConfig */ +/** @typedef {import('../../types/rules/jsx-no-literals').ResolvedConfig} ResolvedConfig */ +/** @typedef {import('../../types/rules/jsx-no-literals').OverrideConfig} OverrideConfig */ +/** @typedef {import('../../types/rules/jsx-no-literals').ElementConfig} ElementConfig */ + // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ @@ -45,7 +53,7 @@ const messages = { literalNotInJSXExpressionInElement: 'Missing JSX expression container around literal string: "{{text}}" in {{element}}', }; -/** @type {Exclude['properties']} */ +/** @type {Exclude['properties']} */ const commonPropertiesSchema = { noStrings: { type: 'boolean', @@ -65,52 +73,7 @@ const commonPropertiesSchema = { }, }; -/** - * @typedef RawElementConfigProperties - * @property {boolean} [noStrings] - * @property {string[]} [allowedStrings] - * @property {boolean} [ignoreProps] - * @property {boolean} [noAttributeStrings] - * - * @typedef RawOverrideConfigProperties - * @property {boolean} [allowElement] - * @property {boolean} [applyToNestedElements=true] - * - * @typedef {RawElementConfigProperties} RawElementConfig - * @typedef {RawElementConfigProperties & RawElementConfigProperties} RawOverrideConfig - * - * @typedef RawElementOverrides - * @property {Record} [elementOverrides] - * - * @typedef {RawElementConfig & RawElementOverrides} RawConfig - * - * ---------------------------------------------------------------------- - * - * @typedef ElementConfigType - * @property {'element'} type - * - * @typedef ElementConfigProperties - * @property {boolean} noStrings - * @property {Set} allowedStrings - * @property {boolean} ignoreProps - * @property {boolean} noAttributeStrings - * - * @typedef OverrideConfigProperties - * @property {'override'} type - * @property {string} name - * @property {boolean} allowElement - * @property {boolean} applyToNestedElements - * - * @typedef {ElementConfigType & ElementConfigProperties} ElementConfig - * @typedef {OverrideConfigProperties & ElementConfigProperties} OverrideConfig - * - * @typedef ElementOverrides - * @property {Record} elementOverrides - * - * @typedef {ElementConfig & ElementOverrides} Config - * @typedef {Config | OverrideConfig} ResolvedConfig - */ - +// eslint-disable-next-line valid-jsdoc /** * Normalizes the element portion of the config * @param {RawConfig} config @@ -128,6 +91,7 @@ function normalizeElementConfig(config) { }; } +// eslint-disable-next-line valid-jsdoc /** * Normalizes the config and applies default values to all config options * @param {RawConfig} config @@ -182,8 +146,9 @@ const elementOverrides = { }, }; +/** @type {RuleModule} */ module.exports = { - meta: /** @type {import('eslint').Rule.RuleModule["meta"]} */ ({ + meta: /** @type {RuleModule['meta']} */ ({ docs: { description: 'Disallow usage of string literals in JSX', category: 'Stylistic Issues', @@ -339,6 +304,7 @@ module.exports = { return some(iterFrom([ancestors.parent, ancestors.grandParent]), (parent) => jsxElementTypes.has(parent.type)); } + // eslint-disable-next-line valid-jsdoc /** * Determines whether a given node's value and its immediate parent are * viable text nodes that can/should be reported on @@ -370,6 +336,7 @@ module.exports = { return isStandardJSXNode && parent.type !== 'JSXExpressionContainer'; } + // eslint-disable-next-line valid-jsdoc /** * Gets an override config for a given node. For any given node, we also * need to traverse the ancestor tree to determine if an ancestor's config @@ -408,6 +375,7 @@ module.exports = { } } + // eslint-disable-next-line valid-jsdoc /** * @param {ResolvedConfig} resolvedConfig * @returns {boolean} @@ -416,6 +384,7 @@ module.exports = { return resolvedConfig.type === 'override' && 'allowElement' in resolvedConfig && !!resolvedConfig.allowElement; } + // eslint-disable-next-line valid-jsdoc /** * @param {boolean} ancestorIsJSXElement * @param {ResolvedConfig} resolvedConfig @@ -433,6 +402,7 @@ module.exports = { return resolvedConfig.type === 'override' ? 'literalNotInJSXExpressionInElement' : 'literalNotInJSXExpression'; } + // eslint-disable-next-line valid-jsdoc /** * @param {ASTNode} node * @param {string} messageId diff --git a/lib/rules/jsx-props-no-spread-multi.js b/lib/rules/jsx-props-no-spread-multi.js index 2eeed0be49..3103be86da 100644 --- a/lib/rules/jsx-props-no-spread-multi.js +++ b/lib/rules/jsx-props-no-spread-multi.js @@ -16,6 +16,7 @@ const messages = { noMultiSpreading: 'Spreading the same expression multiple times is forbidden', }; +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { diff --git a/lib/rules/jsx-space-before-closing.js b/lib/rules/jsx-space-before-closing.js index ffcc357d8b..2cef238841 100644 --- a/lib/rules/jsx-space-before-closing.js +++ b/lib/rules/jsx-space-before-closing.js @@ -59,7 +59,7 @@ module.exports = { const sourceCode = getSourceCode(context); const leftToken = getTokenBeforeClosingBracket(node); - const closingSlash = /** @type {import("eslint").AST.Token} */ (sourceCode.getTokenAfter(leftToken)); + const closingSlash = /** @type {import('eslint').AST.Token} */ (sourceCode.getTokenAfter(leftToken)); if (leftToken.loc.end.line !== closingSlash.loc.start.line) { return; diff --git a/lib/rules/no-access-state-in-setstate.js b/lib/rules/no-access-state-in-setstate.js index 7126517112..1b23dd88ee 100644 --- a/lib/rules/no-access-state-in-setstate.js +++ b/lib/rules/no-access-state-in-setstate.js @@ -116,7 +116,7 @@ module.exports = { && node.object.type === 'ThisExpression' && isClassComponent(node) ) { - /** @type {import("eslint").Rule.Node} */ + /** @type {import('eslint').Rule.Node} */ let current = node; while (current.type !== 'Program') { // Reporting if this.state is directly within this.setState @@ -163,7 +163,7 @@ module.exports = { Identifier(node) { // Checks if the identifier is a variable within an object - /** @type {import("eslint").Rule.Node} */ + /** @type {import('eslint').Rule.Node} */ let current = node; while (current.parent.type === 'BinaryExpression') { current = current.parent; diff --git a/lib/rules/no-deprecated.js b/lib/rules/no-deprecated.js index 9f5ddf7082..0c5931345f 100644 --- a/lib/rules/no-deprecated.js +++ b/lib/rules/no-deprecated.js @@ -229,7 +229,7 @@ module.exports = { } node.specifiers.filter(((s) => 'imported' in s && s.imported)).forEach((specifier) => { // TODO, semver-major: remove `in` check as part of jsdoc->tsdoc migration - checkDeprecation(node, 'imported' in specifier && `${MODULES[node.source.value][0]}.${specifier.imported.name}`, specifier); + checkDeprecation(node, 'imported' in specifier && 'name' in specifier.imported && `${MODULES[node.source.value][0]}.${specifier.imported.name}`, specifier); }); }, diff --git a/lib/rules/no-unused-state.js b/lib/rules/no-unused-state.js index a71d63ebeb..c1986755c4 100644 --- a/lib/rules/no-unused-state.js +++ b/lib/rules/no-unused-state.js @@ -468,7 +468,7 @@ module.exports = { && unwrappedRight.type === 'ObjectExpression' ) { // Find the nearest function expression containing this assignment. - /** @type {import("eslint").Rule.Node} */ + /** @type {import('eslint').Rule.Node} */ let fn = node; while (fn.type !== 'FunctionExpression' && fn.parent) { fn = fn.parent; diff --git a/test-published-types/index.js b/test-published-types/index.js index 31e6005985..010d658041 100644 --- a/test-published-types/index.js +++ b/test-published-types/index.js @@ -3,10 +3,12 @@ const react = require('eslint-plugin-react'); /** @type {import('eslint').Linter.Config[]} */ -module.exports = [ +const config = [ { plugins: { react, }, }, ]; + +module.exports = config; diff --git a/test-published-types/package.json b/test-published-types/package.json index ab8a7160c6..80953c53ab 100644 --- a/test-published-types/package.json +++ b/test-published-types/package.json @@ -3,7 +3,6 @@ "private": true, "version": "0.0.0", "dependencies": { - "eslint": "^9.11.1", - "eslint-plugin-react": "file:.." + "eslint": "^9.11.1" } } diff --git a/test-published-types/tsconfig.json b/test-published-types/tsconfig.json index 7fc1500df3..2ce0bd816e 100644 --- a/test-published-types/tsconfig.json +++ b/test-published-types/tsconfig.json @@ -7,6 +7,7 @@ "compilerOptions": { "lib": ["esnext"], - "types": ["node"] + "types": ["node"], + "skipLibCheck": true } } diff --git a/tsconfig.json b/tsconfig.json index 39187b7f32..8772fab53d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,5 +19,5 @@ "alwaysStrict": false, /* Parse in strict mode and emit "use strict" for each source file. */ "resolveJsonModule": true }, - "include": ["lib"], + "include": ["lib", "types"], } diff --git a/types/rules/jsx-no-literals.d.ts b/types/rules/jsx-no-literals.d.ts new file mode 100644 index 0000000000..c1b9638958 --- /dev/null +++ b/types/rules/jsx-no-literals.d.ts @@ -0,0 +1,50 @@ +type RawElementConfig = { + noStrings?: boolean; + allowedStrings?: string[]; + ignoreProps?: boolean; + noAttributeStrings?: boolean; +}; + +type RawOverrideConfig = { + allowElement?: boolean; + applyToNestedElements?: boolean; +}; + +interface RawElementOverrides { + elementOverrides?: Record; +} + +export type RawConfig = RawElementConfig & RawElementOverrides; + +interface ElementConfigType { + type: 'element'; +} + +interface ElementConfigProperties { + noStrings: boolean; + allowedStrings: Set; + ignoreProps: boolean; + noAttributeStrings: boolean; +} + +interface OverrideConfigProperties { + type: 'override'; + name: string; + allowElement: boolean; + applyToNestedElements: boolean; +} + +export type ElementConfig = { + type: 'element'; +} & ElementConfigProperties; + +export type OverrideConfig = OverrideConfigProperties & ElementConfigProperties; + +interface ElementOverrides { + elementOverrides: Record; +} + +export type Config = ElementConfig & ElementOverrides; + +export type ResolvedConfig = Config | OverrideConfig; + diff --git a/types/string.prototype.repeat/index.d.ts b/types/string.prototype.repeat/index.d.ts index f240d9301f..b2e5992712 100644 --- a/types/string.prototype.repeat/index.d.ts +++ b/types/string.prototype.repeat/index.d.ts @@ -1,3 +1,4 @@ declare module 'string.prototype.repeat' { - export = typeof Function.call.bind(String.prototype.repeat); + function repeat(text: string, count: number): string; + export = repeat; }