diff --git a/.editorconfig b/.editorconfig index 4a7ea3036a..319299684a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,6 +7,7 @@ end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true +quote_type = single [*.md] trim_trailing_whitespace = false diff --git a/.eslintignore b/.eslintignore index a96fced2b4..4ebc8aea50 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1 @@ -reports/** +coverage diff --git a/.eslintrc b/.eslintrc index ca692feb48..c324fcd75b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,6 +1,7 @@ { "root": true, - "extends": "airbnb-base", + "extends": ["airbnb-base", "plugin:eslint-plugin/recommended"], + "plugins": ["eslint-plugin"], "env": { "es6": true, "node": true @@ -15,24 +16,28 @@ "rules": { "comma-dangle": [2, "never"], "object-curly-spacing": [2, "never"], + "object-shorthand": [2, "always"], "array-bracket-spacing": [2, "never"], "max-len": [2, 120, { "ignoreStrings": true, "ignoreTemplateLiterals": true, "ignoreComments": true, }], - "operator-linebreak": [2, "after"], "consistent-return": 0, "prefer-destructuring": [2, { "array": false, "object": false }, { "enforceForRenamedProperties": false }], - + "prefer-object-spread": 0, "function-paren-newline": 0, "no-plusplus": 1, "no-param-reassign": 1, "no-mixed-operators": 1, - "global-require": 1, "no-restricted-syntax": 1, - "valid-jsdoc": 1, + "strict": [2, "safe"], + "valid-jsdoc": [2, { + "requireReturn": false, + "requireParamDescription": false, + "requireReturnDescription": false, + }], }, "overrides": [ { diff --git a/.gitignore b/.gitignore index f54d9f3c6a..a7d00f359e 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ lib-cov pids logs reports +coverage build node_modules !tests/**/node_modules diff --git a/.travis.yml b/.travis.yml index 33f1c0ff16..d7637abed9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ sudo: false language: node_js node_js: + - '13' - '12' - '11' - '10' @@ -14,10 +15,12 @@ before_script: script: - 'if [ -n "${PRETEST-}" ]; then npm run pretest ; fi' - 'if [ -n "${TEST-}" ]; then npm run unit-test ; fi' + - 'if [ -n "${README-}" ]; then npm run generate-list-of-rules:check ; fi' env: global: - TEST=true matrix: + - ESLINT=7 - ESLINT=6 - ESLINT=5 - ESLINT=4 @@ -28,6 +31,8 @@ matrix: include: - node_js: 'lts/*' env: PRETEST=true + - node_js: 'lts/*' + env: README=true exclude: - node_js: '4' env: ESLINT=5 @@ -35,5 +40,11 @@ matrix: env: ESLINT=6 - node_js: '6' env: ESLINT=6 + - node_js: '4' + env: ESLINT=7 + - node_js: '6' + env: ESLINT=7 + - node_js: '8' + env: ESLINT=7 allow_failures: - node_js: '11' diff --git a/CHANGELOG.md b/CHANGELOG.md index fd14b6becd..9d13c1bf97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,318 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). This change log adheres to standards from [Keep a CHANGELOG](http://keepachangelog.com). +## [7.20.0] - 2020-05-12 + +### Added +* support eslint v7 ([#2635][] @ljharb, @toshi-toma) +* [`forbid-component-props`][]/[`forbid-dom-props`][]: Allow a custom message with forbid props ([#2615][] @mtamhankar1) +* [`jsx-no-literals`][]: add `ignoreProps` option to ignore props validation ([#2146][] @iiison) + +### Fixed +* [`jsx-sort-props`][]: only use localeCompare when case is ignored ([#2556][] @tanmoyopenroot) +* [`jsx-key`][]: add a failing test case for optional chaining ([#2610][] @JonathanLee-LX) +* [`no-unused-state`][]: handle optional chaining ([#2588][] @golopot) +* [`jsx-pascal-case`][]: Do not consider namespaces when checking for DOM ([#2638][] @yacinehmito) +* [`jsx-curly-spacing`][], [`jsx-no-bind`][], `usedPropTypes` util: avoid node.start and node.end ([25b1936][] @toshi-toma) +* [`jsx-no-target-blank`][]: allow `no-referrer` without `noopener` by default ([#2043][] @seancrater) +* [`button-has-type`][]: improve message when non-static value is used ([aecff62][] @golopot) +* [`no-adjacent-inline-elements`][]: prevent crash on nullish children ([#2621][] @Rogdham) +* [`prop-types`][]: avoid crash when spreading any type ([#2606][] @golopot)) +* [`require-render-return`][]: add missing "a" ([#2604][] @leothorp) +* [`jsx-no-comment-textnodes`][]: fix for `@typescript-eslint/parser` ([#2601][] @Axnyff) +* [`displayName`][]: avoid a crash when using React.memo ([#2587][] @golopot) + +### Docs +* Clean up examples in rule docs ([#2546][] @silvenon) +* [readme] Add Rules of Hooks to Other useful plugins section ([#2633][] @petetnt) +* [`no-this-in-sfc`][]: backtick `this` ([#2616][] @mrflip) +* [`function-component-definition`][]: Fix unnamedComponents option examples ([#2608][] @vkrol)) + +### Changed +* [Deps] Move "semver" to devDependencies ([#2595][] @rajivshah3) +* [eslint] remove `operator-linebreak` override ([#2578][] @golopot) +* [Tests] `button-has-type`: ensure no mistakenly allowed identifiers named `button`/`submit`/`reset` ([#2625][] @golopot) +* [Tests] `displayName`: add a test case ([#2593][] @golopot) +* [Dev Deps] update `@types/eslint`, `@types/estree`, `@types/node`, `@typescript-eslint/parser`, `coveralls`, `eslint-config-airbnb-base`, `eslint-plugin-import`, `typescript` + +[7.20.0]: https://github.com/yannickcr/eslint-plugin-react/compare/v7.19.0...v7.20.0 +[#2638]: https://github.com/yannickcr/eslint-plugin-react/pull/2638 +[#2635]: https://github.com/yannickcr/eslint-plugin-react/pull/2635 +[#2633]: https://github.com/yannickcr/eslint-plugin-react/pull/2633 +[#2625]: https://github.com/yannickcr/eslint-plugin-react/pull/2625 +[#2621]: https://github.com/yannickcr/eslint-plugin-react/pull/2621 +[#2616]: https://github.com/yannickcr/eslint-plugin-react/pull/2616 +[#2615]: https://github.com/yannickcr/eslint-plugin-react/pull/2615 +[#2610]: https://github.com/yannickcr/eslint-plugin-react/pull/2610 +[#2608]: https://github.com/yannickcr/eslint-plugin-react/pull/2608 +[#2606]: https://github.com/yannickcr/eslint-plugin-react/pull/2606 +[#2604]: https://github.com/yannickcr/eslint-plugin-react/pull/2604 +[#2601]: https://github.com/yannickcr/eslint-plugin-react/pull/2601 +[#2595]: https://github.com/yannickcr/eslint-plugin-react/pull/2595 +[#2593]: https://github.com/yannickcr/eslint-plugin-react/pull/2593 +[#2588]: https://github.com/yannickcr/eslint-plugin-react/pull/2588 +[#2587]: https://github.com/yannickcr/eslint-plugin-react/pull/2587 +[#2578]: https://github.com/yannickcr/eslint-plugin-react/pull/2578 +[#2556]: https://github.com/yannickcr/eslint-plugin-react/pull/2556 +[#2546]: https://github.com/yannickcr/eslint-plugin-react/pull/2546 +[#2146]: https://github.com/yannickcr/eslint-plugin-react/pull/2146 +[#2043]: https://github.com/yannickcr/eslint-plugin-react/pull/2043 +[25b1936]: https://github.com/yannickcr/eslint-plugin-react/commit/25b19365e6cc3f188d6a5ed6cecc70fe6f1af7cd +[aecff62]: https://github.com/yannickcr/eslint-plugin-react/commit/aecff625bf0590ed4d80ed6b58b81af11901f5f6 + +## [7.19.0] - 2020-03-06 + +### Added + * [`style-prop-object`][]: Add `allow` option ([#1819][] @hornta) + * [`jsx-pascal-case`][]: Support unicode characters ([#2557][] @Svish) + +### Fixed + * [`prefer-stateless-function`][]: avoid crash on ts empty constructor ([#2582][] @golopot) + * [`no-adjacent-inline-elements`][]: avoid a crash ([#2575] @ljharb) + * [`no-unused-prop-types`][]: Change the reporting to point to a more accurate node ([#2292][] @jseminck) + * [`self-closing-comp`][]: consider JSXMemberExpression as component too ([#2572][] @Belco90) + * [`no-unused-prop-types`][]: make `markPropTypesAsUsed` work with `TSEmptyBodyFunctionExpression` AST node ([#2560][] @guillaumewuip) + * [`displayName`][] (but really, `propTypes` detection): do not crash on empty flow type spreads ([#2570][] @ljharb) + +### Changed + * [readme] Small visual inconsistency ([#2568] @arvigeus) + * [docs] add `react/` prefix to rule name, for consistency + * [`no-unescaped-entities`][]: skip test cases that are now parsing errors in acorn-jsx@5.2.0 ([#2583] @golopot) + +[7.19.0]: https://github.com/yannickcr/eslint-plugin-react/compare/v7.18.3...v7.19.0 +[#2583]: https://github.com/yannickcr/eslint-plugin-react/pull/2583 +[#2582]: https://github.com/yannickcr/eslint-plugin-react/pull/2582 +[#2575]: https://github.com/yannickcr/eslint-plugin-react/issue/2575 +[#2572]: https://github.com/yannickcr/eslint-plugin-react/pull/2572 +[#2570]: https://github.com/yannickcr/eslint-plugin-react/issue/2570 +[#2568]: https://github.com/yannickcr/eslint-plugin-react/pull/2568 +[#2560]: https://github.com/yannickcr/eslint-plugin-react/pull/2560 +[#2557]: https://github.com/yannickcr/eslint-plugin-react/pull/2557 +[#2292]: https://github.com/yannickcr/eslint-plugin-react/pull/2292 +[#1819]: https://github.com/yannickcr/eslint-plugin-react/pull/1819 + +## [7.18.3] - 2020-02-02 + +### Fixed + * [`jsx-indent`][]: don't check literals not within JSX ([#2564][] @toshi-toma) + +[7.18.3]: https://github.com/yannickcr/eslint-plugin-react/compare/v7.18.2...v7.18.3 +[#2564]: https://github.com/yannickcr/eslint-plugin-react/issue/2564 + +## [7.18.2] - 2020-02-01 + +### Fixed + * [`jsx-indent`][]: avoid a crash on non-string literals ([#2561][] @ljharb) + +[7.18.2]: https://github.com/yannickcr/eslint-plugin-react/compare/v7.18.1...v7.18.2 +[#2561]: https://github.com/yannickcr/eslint-plugin-react/issue/2561 + +## [7.18.1] - 2020-02-01 + +### Fixed + * [`jsx-indent`][]: Does not check indents for JSXText ([#2542][] @toshi-toma) + * [`jsx-props-no-spreading`][]: add support for namespaced jsx components ([#2534][] @jonathanpalma) + * [`jsx-no-target-blank`][]: allow rel to be an expression ([#2544][] @odinho) + * [`sort-comp`][]: `|` isn’t a valid regex flag; `u` and `s` are (@ljharb) + +### Changed + * [Docs] use `markdown-magic` to automatically sort all rules alphabetically ([#1742][] @ybiquitous) + * [Docs] [`jsx-props-no-spreading`][]: fix typo to use correct rule ([#2547][] @jonggyun)) + +[7.18.1]: https://github.com/yannickcr/eslint-plugin-react/compare/v7.18.0...v7.18.1 +[#2547]: https://github.com/yannickcr/eslint-plugin-react/pull/2547 +[#2544]: https://github.com/yannickcr/eslint-plugin-react/pull/2544 +[#2542]: https://github.com/yannickcr/eslint-plugin-react/pull/2542 +[#2534]: https://github.com/yannickcr/eslint-plugin-react/pull/2534 +[#1742]: https://github.com/yannickcr/eslint-plugin-react/pull/1742 + +## [7.18.0] - 2020-01-15 + +### Added + * [`require-default-props`][]: add option to ignore functional components ([#2532][] @RedTn) + * [`function-component-definition`][]: Enforce a specific function type for function components ([#2414][] @Stefanwullems) + * [`no-adjacent-inline-elements`][]: Prevent adjacent inline elements not separated by whitespace ([#1155][] @SeanHayes) + * [`jsx-no-script-url`][]: prevent usage of `javascript:` URLs ([#2419][] @sergei-startsev) + +### Fixed + * [`jsx-pascal-case`][]: false negative with namespacing ([#1337][] @mfyuce) + * [`jsx-curly-brace-presence`][]: Fix `curly-brace-presence` edge cases ([#2523][] @rafbgarcia) + * [`prop-types`][]: Does not validate missing propTypes for LogicalExpression ([#2533][] @toshi-toma) + * [`no-unknown-property`][]: allowTransparency does not exist in React >= v16.1 ([#1538][] @dawidvdh) + * [`jsx-curly-brace-presence`][]: Fix error related to tags line break ([#2521][] @rafbgarcia) + * [`no-typos`][]: Compilation error when method name is string instead of identifier ([#2514][] @shijistar) + * [`jsx-curly-brace-presence`][]: allow trailing spaces in TemplateLiteral ([#2507][] @doochik) + * [`no-unused-prop-types`], [`no-unused-state`]: fix false positives when using TS type assertions ([#2536][] @kdmadej) + +### Changed + * [Docs] [`no-render-return-value`][]: Fix title ([#2540][] @micnic) + * [Refactor]: remove unused codes in util/propTypes ([#2288][] @golopot) + * [`no-typos`]: check static lifecycle methods ([#2006][] @bsonntag) + * [Docs] [`jsx-first-prop-new-line`][]: Fix rule name in "Rule Options" section ([#2535][] @barreira) + * [Tests] [`no-unused-prop-types`][]: Added test cases ([#977][] @dozoisch) + * [Tests] avoid running tests on pretest job + * [meta] Move eslint-plugin-eslint-plugin to devDeps ([#2510][] @nstepien) + * [Deps] update `array-includes`, `object.entries`, `object.fromentries`, `object.values`, `resolve` + +[7.18.0]: https://github.com/yannickcr/eslint-plugin-react/compare/v7.17.0...v7.18.0 +[#2540]: https://github.com/yannickcr/eslint-plugin-react/pull/2540 +[#2536]: https://github.com/yannickcr/eslint-plugin-react/pull/2536 +[#2535]: https://github.com/yannickcr/eslint-plugin-react/pull/2535 +[#2533]: https://github.com/yannickcr/eslint-plugin-react/pull/2533 +[#2532]: https://github.com/yannickcr/eslint-plugin-react/pull/2532 +[#2523]: https://github.com/yannickcr/eslint-plugin-react/pull/2523 +[#2521]: https://github.com/yannickcr/eslint-plugin-react/pull/2521 +[#2514]: https://github.com/yannickcr/eslint-plugin-react/pull/2514 +[#2510]: https://github.com/yannickcr/eslint-plugin-react/pull/2510 +[#2507]: https://github.com/yannickcr/eslint-plugin-react/pull/2507 +[#2419]: https://github.com/yannickcr/eslint-plugin-react/pull/2419 +[#2414]: https://github.com/yannickcr/eslint-plugin-react/pull/2414 +[#2288]: https://github.com/yannickcr/eslint-plugin-react/pull/2288 +[#2006]: https://github.com/yannickcr/eslint-plugin-react/pull/2006 +[#1538]: https://github.com/yannickcr/eslint-plugin-react/pull/1538 +[#1337]: https://github.com/yannickcr/eslint-plugin-react/pull/1337 +[#1155]: https://github.com/yannickcr/eslint-plugin-react/pull/1155 +[#977]: https://github.com/yannickcr/eslint-plugin-react/pull/977 + +## [7.17.0] - 2019-11-28 + +### Added + * [`jsx-no-target-blank`][]: add `allowReferrer` option ([#2478][] @eps1lon) + * [`jsx-handler-names`][]: add `checkLocalVariables` option ([#2470][] @aub) + * [`prop-types`][]: Support Flow Type spread ([#2446][] @moroine) + * [`jsx-props-no-spreading`][]: add `explicitSpread` option to allow explicit spread of props ([#2449][] @pawelnvk) + * [`jsx-no-target-blank`][]: warn on `target={'_blank'}` expressions ([#2451][] @timkraut) + * [`function-component-definition`]: Enforce a specific function type for function components ([#2414][] @Stefanwullems) + +### Fixed + * [`sort-prop-types`][], [`jsx-sort-default-props`][]: disable broken autofix ([#2505][] @webOS101) + * [`no-typos`][]: improve report location ([#2468][] @golopot) + * [`jsx-no-literals`][]: trim whitespace for `allowedStrings` check ([#2436][] @cainlevy) + * [`jsx-curly-brace-presence`][]: Fix filter of undefined error with whitespace inside jsx attr curlies ([#2460][] @dustinyoste) + * [`no-render-return-value`][]: should warn when used in assignment expression ([#2462][] @jichu4n) + * [`jsx-curly-brace-presence`][]: allow trailing spaces in literal ([#2448][] @doochik) + +### Changed + * [Deps] update `jsx-ast-utils`, `object.fromentries`, `resolve` + * [eslint] fix func-names and change object-shorthand to 'always' ([#2483][] @golopot) + * [Docs] `jsx-first-prop-new-line`: Fix documentation formatting ([#2489][] @pjg) + * [Docs] [`prop-types`][]: Update 'skipUndeclared' in rule options ([#2504][] @cjnickel) + * [Docs] [`jsx-first-prop-new-line`][]: fix wrong rule name ([#2500][] @zgayjjf) + * [eslint] enable eslint-plugin-eslint-plugin ([#2469][] @golopot) + * [Docs] [`jsx-props-no-multi-spaces`][]: suggest using core rule instead ([#2463][] @golopot) + * [Docs] [`jsx-first-prop-new-line`][]: add rule options ([#2465][] @SerdarMustafa1) + * [Docs] [`jsx-no-target-blank`][]: Add section about overriding for trusted links ([#2438][] @aschriner) + * [Docs] fix typo ([#2453][] @cainwatson) + * [Docs] [`no-unused-prop-types`][]: clean up prose ([#2273][] @coryhouse) + * [Docs] [`jsx-no-bind`][]: add section about React Hooks ([#2443][] @kdex) + +[7.17.0]: https://github.com/yannickcr/eslint-plugin-react/compare/v7.16.0...v7.17.0 +[#2532]: https://github.com/yannickcr/eslint-plugin-react/pull/2532 +[#2505]: https://github.com/yannickcr/eslint-plugin-react/pull/2505 +[#2504]: https://github.com/yannickcr/eslint-plugin-react/pull/2504 +[#2500]: https://github.com/yannickcr/eslint-plugin-react/pull/2500 +[#2489]: https://github.com/yannickcr/eslint-plugin-react/pull/2489 +[#2483]: https://github.com/yannickcr/eslint-plugin-react/pull/2483 +[#2478]: https://github.com/yannickcr/eslint-plugin-react/pull/2478 +[#2470]: https://github.com/yannickcr/eslint-plugin-react/pull/2470 +[#2469]: https://github.com/yannickcr/eslint-plugin-react/pull/2469 +[#2468]: https://github.com/yannickcr/eslint-plugin-react/pull/2468 +[#2465]: https://github.com/yannickcr/eslint-plugin-react/pull/2465 +[#2463]: https://github.com/yannickcr/eslint-plugin-react/pull/2463 +[#2460]: https://github.com/yannickcr/eslint-plugin-react/pull/2460 +[#2453]: https://github.com/yannickcr/eslint-plugin-react/pull/2453 +[#2451]: https://github.com/yannickcr/eslint-plugin-react/pull/2451 +[#2449]: https://github.com/yannickcr/eslint-plugin-react/pull/2449 +[#2448]: https://github.com/yannickcr/eslint-plugin-react/pull/2448 +[#2446]: https://github.com/yannickcr/eslint-plugin-react/pull/2446 +[#2443]: https://github.com/yannickcr/eslint-plugin-react/pull/2443 +[#2438]: https://github.com/yannickcr/eslint-plugin-react/pull/2438 +[#2436]: https://github.com/yannickcr/eslint-plugin-react/pull/2436 +[#2414]: https://github.com/yannickcr/eslint-plugin-react/pull/2414 +[#2273]: https://github.com/yannickcr/eslint-plugin-react/pull/2273 + +## [7.16.0] - 2019-10-04 + +### Added +* [`jsx-sort-default-props`][]: make rule fixable ([#2429][] @emroussel) + +### Fixed +* [`jsx-no-useless-fragment`][]: use `array-includes` over `.includes` for back compat (@ljharb) +* [`jsx-curly-brace-presence`][]: allow necessary white-space literal ([#2437][] @uniqname) +* [`jsx-curly-brace-presence`][]: warns incorrectly on trailing whitespace ([#2431][] @BC-M) +* [`no-unused-prop-types`][]: false positive when nested destructuring ([#2428][] @golopot) + +[7.16.0]: https://github.com/yannickcr/eslint-plugin-react/compare/v7.15.1...v7.16.0 +[#2437]: https://github.com/yannickcr/eslint-plugin-react/pull/2437 +[#2431]: https://github.com/yannickcr/eslint-plugin-react/pull/2431 +[#2429]: https://github.com/yannickcr/eslint-plugin-react/pull/2429 +[#2428]: https://github.com/yannickcr/eslint-plugin-react/pull/2428 + +## [7.15.1] - 2019-10-01 + +### Fixed +* [`jsx-curly-brace-presence`][]: bail out checks when JSXElements are passed as props ([#2426][] @vedadeepta) + +### Changed +* [Docs] [`prefer-es6-class`][]: Fix typos ([#2425][] @spencerbyw) + +[7.15.1]: https://github.com/yannickcr/eslint-plugin-react/compare/v7.15.0...v7.15.1 +[#2426]: https://github.com/yannickcr/eslint-plugin-react/pull/2426 +[#2425]: https://github.com/yannickcr/eslint-plugin-react/pull/2425 + +## [7.15.0] - 2019-09-30 + +### Added +* add [`jsx-no-useless-fragment`][] rule ([#2261][] @golopot) +* [`jsx-handler-name`][]: allow `false` to disable `eventHandlerPrefix`/`eventHandlerPropPrefix` ([#2410][] @tanmoyopenroot) +* [`sort-comp`][]: add `static-variables` grouping ([#2408][] @vedadeepta) +* [`jsx-no-literals`][]: Add `allowedStrings` option ([#2380][] @benhollander) +* [`no-multi-comp`][]: Added handling for `forwardRef` and `memo` wrapping components declared in the same file ([#2184][] @jenil94) +* [`jsx-pascal-case`][]: `allowAllCaps` option now allows `SCREAMING_SNAKE_CASE` ([#2364][] @TylerR909) + +### Fixed +* [`jsx-indent`][]: Fix false positive when a jsx element is the last statement within a do expression (with tests) ([#2200][] @Kenneth-KT) +* [`jsx-curly-brace-presence`][]: fix jsx tags in braces ([#2422][] @tanmoyopenroot) +* [`display-name`][]: Fix false positives ([#2399][] @BPScott) +* [`jsx-curly-brace-presence`][]: report unnecessary curly braces with children on next line ([#2409][] @vedadeepta) +* [`no-unused-prop-types`][]: false positive with callback ([#2375][] @golopot) +* Fix prop-types detection collision on renamed props ([#2383][] @yannickcr) +* [`jsx-sort-props`][]: use localeCompare rather than comparison operator ([#2391][] @tanmoyopenroot) +* [`jsx-pascal-case`][]: allow one-letter-named components ([#2395][] @Haegin) +* [`jsx-wrap-multilines`][]: fix incorrect formatting ([#2392][] @tanmoyopenroot) +* [`require-optimization`][]: fix when using arrow function in class components ([#2385][] @jenil94) +* [`no-deprecated`][]: Deprecate cWM/cWRP/cWU lifecycle methods since React 16.9.0 ([#2378][] @meowtec) +* [`jsx-key`][]: improve docs and confusing error message ([#2367][] @kaykayehnn) +* Recognize props wrapped in flow $ReadOnly<> utility type ([#2361][] @lukeapage) +* [`prop-types`][]: false positive with setState updator ([#2359][] @golopot) + +### Changed +* [Docs] [`no-access-state-in-setstate`][]: update grammar ([#2418][] @neaumusic) +* [`jsx-curly-brace-presence`][], [`jsx-one-expression-per-line`][], [`no-danger-with-children`][]: add `isWhiteSpaces` to `lib/util/jsx` ([#2409][] @vedadeepta) + +[7.15.0]: https://github.com/yannickcr/eslint-plugin-react/compare/v7.14.3...v7.15.0 +[#2422]: https://github.com/yannickcr/eslint-plugin-react/pull/2422 +[#2410]: https://github.com/yannickcr/eslint-plugin-react/pull/2410 +[#2409]: https://github.com/yannickcr/eslint-plugin-react/pull/2409 +[#2408]: https://github.com/yannickcr/eslint-plugin-react/pull/2408 +[#2402]: https://github.com/yannickcr/eslint-plugin-react/pull/2402 +[#2399]: https://github.com/yannickcr/eslint-plugin-react/pull/2399 +[#2395]: https://github.com/yannickcr/eslint-plugin-react/pull/2395 +[#2392]: https://github.com/yannickcr/eslint-plugin-react/pull/2392 +[#2391]: https://github.com/yannickcr/eslint-plugin-react/pull/2391 +[#2385]: https://github.com/yannickcr/eslint-plugin-react/pull/2385 +[#2383]: https://github.com/yannickcr/eslint-plugin-react/issue/2383 +[#2380]: https://github.com/yannickcr/eslint-plugin-react/pull/2380 +[#2378]: https://github.com/yannickcr/eslint-plugin-react/pull/2378 +[#2375]: https://github.com/yannickcr/eslint-plugin-react/pull/2375 +[#2367]: https://github.com/yannickcr/eslint-plugin-react/pull/2367 +[#2364]: https://github.com/yannickcr/eslint-plugin-react/pull/2364 +[#2361]: https://github.com/yannickcr/eslint-plugin-react/pull/2361 +[#2359]: https://github.com/yannickcr/eslint-plugin-react/pull/2359 +[#2261]: https://github.com/yannickcr/eslint-plugin-react/pull/2261 +[#2200]: https://github.com/yannickcr/eslint-plugin-react/pull/2200 +[#2184]: https://github.com/yannickcr/eslint-plugin-react/pull/2184 + ## [7.14.3] - 2019-07-23 ### Fixed @@ -2674,3 +2986,7 @@ If you're still not using React 15 you can keep the old behavior by setting the [`jsx-props-no-spreading`]: docs/rules/jsx-props-no-spreading.md [`static-property-placement`]: docs/rules/static-property-placement.md [`jsx-curly-newline`]: docs/rules/jsx-curly-newline.md +[`jsx-no-useless-fragment`]: docs/rules/jsx-no-useless-fragment.md +[`jsx-no-script-url`]: docs/rules/jsx-no-script-url.md +[`no-adjacent-inline-elements`]: docs/rules/no-adjacent-inline-elements.md +[`function-component-definition`]: docs/rules/function-component-definition.md diff --git a/README.md b/README.md index 3f2ae8e809..3a06ef705d 100644 --- a/README.md +++ b/README.md @@ -97,95 +97,104 @@ Enable the rules that you would like to use. # List of supported rules + * [react/boolean-prop-naming](docs/rules/boolean-prop-naming.md): Enforces consistent naming for boolean props * [react/button-has-type](docs/rules/button-has-type.md): Forbid "button" element without an explicit "type" attribute -* [react/default-props-match-prop-types](docs/rules/default-props-match-prop-types.md): Prevent extraneous defaultProps on components -* [react/destructuring-assignment](docs/rules/destructuring-assignment.md): Rule enforces consistent usage of destructuring assignment in component -* [react/display-name](docs/rules/display-name.md): Prevent missing `displayName` in a React component definition -* [react/forbid-component-props](docs/rules/forbid-component-props.md): Forbid certain props on Components +* [react/default-props-match-prop-types](docs/rules/default-props-match-prop-types.md): Enforce all defaultProps are defined and not "required" in propTypes. +* [react/destructuring-assignment](docs/rules/destructuring-assignment.md): Enforce consistent usage of destructuring assignment of props, state, and context +* [react/display-name](docs/rules/display-name.md): Prevent missing displayName in a React component definition +* [react/forbid-component-props](docs/rules/forbid-component-props.md): Forbid certain props on components * [react/forbid-dom-props](docs/rules/forbid-dom-props.md): Forbid certain props on DOM Nodes * [react/forbid-elements](docs/rules/forbid-elements.md): Forbid certain elements +* [react/forbid-foreign-prop-types](docs/rules/forbid-foreign-prop-types.md): Forbid using another component's propTypes * [react/forbid-prop-types](docs/rules/forbid-prop-types.md): Forbid certain propTypes -* [react/forbid-foreign-prop-types](docs/rules/forbid-foreign-prop-types.md): Forbid foreign propTypes -* [react/no-access-state-in-setstate](docs/rules/no-access-state-in-setstate.md): Prevent using this.state inside this.setState -* [react/no-array-index-key](docs/rules/no-array-index-key.md): Prevent using Array index in `key` props -* [react/no-children-prop](docs/rules/no-children-prop.md): Prevent passing children as props -* [react/no-danger](docs/rules/no-danger.md): Prevent usage of dangerous JSX properties -* [react/no-danger-with-children](docs/rules/no-danger-with-children.md): Prevent problem with children and props.dangerouslySetInnerHTML -* [react/no-deprecated](docs/rules/no-deprecated.md): Prevent usage of deprecated methods, including component lifecycle methods -* [react/no-did-mount-set-state](docs/rules/no-did-mount-set-state.md): Prevent usage of `setState` in `componentDidMount` -* [react/no-did-update-set-state](docs/rules/no-did-update-set-state.md): Prevent usage of `setState` in `componentDidUpdate` -* [react/no-direct-mutation-state](docs/rules/no-direct-mutation-state.md): Prevent direct mutation of `this.state` -* [react/no-find-dom-node](docs/rules/no-find-dom-node.md): Prevent usage of `findDOMNode` -* [react/no-is-mounted](docs/rules/no-is-mounted.md): Prevent usage of `isMounted` +* [react/function-component-definition](docs/rules/function-component-definition.md): Standardize the way function component get defined (fixable) +* [react/no-access-state-in-setstate](docs/rules/no-access-state-in-setstate.md): Reports when this.state is accessed within setState +* [react/no-adjacent-inline-elements](docs/rules/no-adjacent-inline-elements.md): Prevent adjacent inline elements not separated by whitespace. +* [react/no-array-index-key](docs/rules/no-array-index-key.md): Prevent usage of Array index in keys +* [react/no-children-prop](docs/rules/no-children-prop.md): Prevent passing of children as props. +* [react/no-danger](docs/rules/no-danger.md): Prevent usage of dangerous JSX props +* [react/no-danger-with-children](docs/rules/no-danger-with-children.md): Report when a DOM element is using both children and dangerouslySetInnerHTML +* [react/no-deprecated](docs/rules/no-deprecated.md): Prevent usage of deprecated methods +* [react/no-did-mount-set-state](docs/rules/no-did-mount-set-state.md): Prevent usage of setState in componentDidMount +* [react/no-did-update-set-state](docs/rules/no-did-update-set-state.md): Prevent usage of setState in componentDidUpdate +* [react/no-direct-mutation-state](docs/rules/no-direct-mutation-state.md): Prevent direct mutation of this.state +* [react/no-find-dom-node](docs/rules/no-find-dom-node.md): Prevent usage of findDOMNode +* [react/no-is-mounted](docs/rules/no-is-mounted.md): Prevent usage of isMounted * [react/no-multi-comp](docs/rules/no-multi-comp.md): Prevent multiple component definition per file -* [react/no-redundant-should-component-update](docs/rules/no-redundant-should-component-update.md): Prevent usage of `shouldComponentUpdate` when extending React.PureComponent -* [react/no-render-return-value](docs/rules/no-render-return-value.md): Prevent usage of the return value of `React.render` -* [react/no-set-state](docs/rules/no-set-state.md): Prevent usage of `setState` -* [react/no-typos](docs/rules/no-typos.md): Prevent common casing typos -* [react/no-string-refs](docs/rules/no-string-refs.md): Prevent using string references in `ref` attribute. -* [react/no-this-in-sfc](docs/rules/no-this-in-sfc.md): Prevent using `this` in stateless functional components -* [react/no-unescaped-entities](docs/rules/no-unescaped-entities.md): Prevent invalid characters from appearing in markup +* [react/no-redundant-should-component-update](docs/rules/no-redundant-should-component-update.md): Flag shouldComponentUpdate when extending PureComponent +* [react/no-render-return-value](docs/rules/no-render-return-value.md): Prevent usage of the return value of React.render +* [react/no-set-state](docs/rules/no-set-state.md): Prevent usage of setState +* [react/no-string-refs](docs/rules/no-string-refs.md): Prevent string definitions for references and prevent referencing this.refs +* [react/no-this-in-sfc](docs/rules/no-this-in-sfc.md): Report "this" being used in stateless components +* [react/no-typos](docs/rules/no-typos.md): Prevent common typos +* [react/no-unescaped-entities](docs/rules/no-unescaped-entities.md): Detect unescaped HTML entities, which might represent malformed tags * [react/no-unknown-property](docs/rules/no-unknown-property.md): Prevent usage of unknown DOM property (fixable) * [react/no-unsafe](docs/rules/no-unsafe.md): Prevent usage of unsafe lifecycle methods * [react/no-unused-prop-types](docs/rules/no-unused-prop-types.md): Prevent definitions of unused prop types -* [react/no-unused-state](docs/rules/no-unused-state.md): Prevent definitions of unused state properties -* [react/no-will-update-set-state](docs/rules/no-will-update-set-state.md): Prevent usage of `setState` in `componentWillUpdate` +* [react/no-unused-state](docs/rules/no-unused-state.md): Prevent definition of unused state fields +* [react/no-will-update-set-state](docs/rules/no-will-update-set-state.md): Prevent usage of setState in componentWillUpdate * [react/prefer-es6-class](docs/rules/prefer-es6-class.md): Enforce ES5 or ES6 class for React Components -* [react/prefer-read-only-props](docs/rules/prefer-read-only-props.md): Enforce that props are read-only -* [react/prefer-stateless-function](docs/rules/prefer-stateless-function.md): Enforce stateless React Components to be written as a pure function +* [react/prefer-read-only-props](docs/rules/prefer-read-only-props.md): Require read-only props. (fixable) +* [react/prefer-stateless-function](docs/rules/prefer-stateless-function.md): Enforce stateless components to be written as a pure function * [react/prop-types](docs/rules/prop-types.md): Prevent missing props validation in a React component definition -* [react/react-in-jsx-scope](docs/rules/react-in-jsx-scope.md): Prevent missing `React` when using JSX -* [react/require-default-props](docs/rules/require-default-props.md): Enforce a defaultProps definition for every prop that is not a required prop -* [react/require-optimization](docs/rules/require-optimization.md): Enforce React components to have a `shouldComponentUpdate` method +* [react/react-in-jsx-scope](docs/rules/react-in-jsx-scope.md): Prevent missing React when using JSX +* [react/require-default-props](docs/rules/require-default-props.md): Enforce a defaultProps definition for every prop that is not a required prop. +* [react/require-optimization](docs/rules/require-optimization.md): Enforce React components to have a shouldComponentUpdate method * [react/require-render-return](docs/rules/require-render-return.md): Enforce ES5 or ES6 class for returning value in render function * [react/self-closing-comp](docs/rules/self-closing-comp.md): Prevent extra closing tags for components without children (fixable) -* [react/sort-comp](docs/rules/sort-comp.md): Enforce component methods order (fixable) +* [react/sort-comp](docs/rules/sort-comp.md): Enforce component methods order * [react/sort-prop-types](docs/rules/sort-prop-types.md): Enforce propTypes declarations alphabetical sorting -* [react/state-in-constructor](docs/rules/state-in-constructor.md): Enforce the state initialization style to be either in a constructor or with a class property -* [react/static-property-placement](docs/rules/static-property-placement.md): Enforces where React component static properties should be positioned. -* [react/style-prop-object](docs/rules/style-prop-object.md): Enforce style prop value being an object -* [react/void-dom-elements-no-children](docs/rules/void-dom-elements-no-children.md): Prevent void DOM elements (e.g. ``, `
`) from receiving children +* [react/state-in-constructor](docs/rules/state-in-constructor.md): State initialization in an ES6 class component should be in a constructor +* [react/static-property-placement](docs/rules/static-property-placement.md): Defines where React component static properties should be positioned. +* [react/style-prop-object](docs/rules/style-prop-object.md): Enforce style prop value is an object +* [react/void-dom-elements-no-children](docs/rules/void-dom-elements-no-children.md): Prevent passing of children to void DOM elements (e.g. `
`). + ## JSX-specific rules + * [react/jsx-boolean-value](docs/rules/jsx-boolean-value.md): Enforce boolean attributes notation in JSX (fixable) -* [react/jsx-child-element-spacing](docs/rules/jsx-child-element-spacing.md): Enforce or disallow spaces inside of curly braces in JSX attributes and expressions. +* [react/jsx-child-element-spacing](docs/rules/jsx-child-element-spacing.md): Ensures inline tags are not rendered without spaces between them * [react/jsx-closing-bracket-location](docs/rules/jsx-closing-bracket-location.md): Validate closing bracket location in JSX (fixable) -* [react/jsx-closing-tag-location](docs/rules/jsx-closing-tag-location.md): Validate closing tag location in JSX (fixable) -* [react/jsx-curly-newline](docs/rules/jsx-curly-newline.md): Enforce or disallow newlines inside of curly braces in JSX attributes and expressions (fixable) -* [react/jsx-curly-spacing](docs/rules/jsx-curly-spacing.md): Enforce or disallow spaces inside of curly braces in JSX attributes and expressions (fixable) -* [react/jsx-equals-spacing](docs/rules/jsx-equals-spacing.md): Enforce or disallow spaces around equal signs in JSX attributes (fixable) +* [react/jsx-closing-tag-location](docs/rules/jsx-closing-tag-location.md): Validate closing tag location for multiline JSX (fixable) +* [react/jsx-curly-brace-presence](docs/rules/jsx-curly-brace-presence.md): Disallow unnecessary JSX expressions when literals alone are sufficient or enfore JSX expressions on literals in JSX children or attributes (fixable) +* [react/jsx-curly-newline](docs/rules/jsx-curly-newline.md): Enforce consistent line breaks inside jsx curly (fixable) +* [react/jsx-curly-spacing](docs/rules/jsx-curly-spacing.md): Enforce or disallow spaces inside of curly braces in JSX attributes (fixable) +* [react/jsx-equals-spacing](docs/rules/jsx-equals-spacing.md): Disallow or enforce spaces around equal signs in JSX attributes (fixable) * [react/jsx-filename-extension](docs/rules/jsx-filename-extension.md): Restrict file extensions that may contain JSX -* [react/jsx-first-prop-new-line](docs/rules/jsx-first-prop-new-line.md): Enforce position of the first prop in JSX (fixable) +* [react/jsx-first-prop-new-line](docs/rules/jsx-first-prop-new-line.md): Ensure proper position of the first property in JSX (fixable) +* [react/jsx-fragments](docs/rules/jsx-fragments.md): Enforce shorthand or standard form for React fragments (fixable) * [react/jsx-handler-names](docs/rules/jsx-handler-names.md): Enforce event handler naming conventions in JSX * [react/jsx-indent](docs/rules/jsx-indent.md): Validate JSX indentation (fixable) * [react/jsx-indent-props](docs/rules/jsx-indent-props.md): Validate props indentation in JSX (fixable) -* [react/jsx-key](docs/rules/jsx-key.md): Validate JSX has key prop when in array or iterator +* [react/jsx-key](docs/rules/jsx-key.md): Report missing `key` props in iterators/collection literals * [react/jsx-max-depth](docs/rules/jsx-max-depth.md): Validate JSX maximum depth * [react/jsx-max-props-per-line](docs/rules/jsx-max-props-per-line.md): Limit maximum of props on a single line in JSX (fixable) -* [react/jsx-no-bind](docs/rules/jsx-no-bind.md): Prevent usage of `.bind()` and arrow functions in JSX props -* [react/jsx-no-comment-textnodes](docs/rules/jsx-no-comment-textnodes.md): Prevent comments from being inserted as text nodes -* [react/jsx-no-duplicate-props](docs/rules/jsx-no-duplicate-props.md): Prevent duplicate props in JSX -* [react/jsx-no-literals](docs/rules/jsx-no-literals.md): Prevent usage of unwrapped JSX strings -* [react/jsx-no-target-blank](docs/rules/jsx-no-target-blank.md): Prevent usage of unsafe `target='_blank'` +* [react/jsx-no-bind](docs/rules/jsx-no-bind.md): Prevents usage of Function.prototype.bind and arrow functions in React component props +* [react/jsx-no-comment-textnodes](docs/rules/jsx-no-comment-textnodes.md): Comments inside children section of tag should be placed inside braces +* [react/jsx-no-duplicate-props](docs/rules/jsx-no-duplicate-props.md): Enforce no duplicate props +* [react/jsx-no-literals](docs/rules/jsx-no-literals.md): Prevent using string literals in React component definition +* [react/jsx-no-script-url](docs/rules/jsx-no-script-url.md): Forbid `javascript:` URLs +* [react/jsx-no-target-blank](docs/rules/jsx-no-target-blank.md): Forbid `target="_blank"` attribute without `rel="noreferrer"` * [react/jsx-no-undef](docs/rules/jsx-no-undef.md): Disallow undeclared variables in JSX -* [react/jsx-one-expression-per-line](docs/rules/jsx-one-expression-per-line.md): Limit to one expression per line in JSX -* [react/jsx-curly-brace-presence](docs/rules/jsx-curly-brace-presence.md): Enforce curly braces or disallow unnecessary curly braces in JSX -* [react/jsx-fragments](docs/rules/jsx-fragments.md): Enforce shorthand or standard form for React fragments +* [react/jsx-no-useless-fragment](docs/rules/jsx-no-useless-fragment.md): Disallow unnecessary fragments (fixable) +* [react/jsx-one-expression-per-line](docs/rules/jsx-one-expression-per-line.md): Limit to one expression per line in JSX (fixable) * [react/jsx-pascal-case](docs/rules/jsx-pascal-case.md): Enforce PascalCase for user-defined JSX components * [react/jsx-props-no-multi-spaces](docs/rules/jsx-props-no-multi-spaces.md): Disallow multiple spaces between inline JSX props (fixable) -* [react/jsx-props-no-spreading](docs/rules/jsx-props-no-spreading.md): Disallow JSX props spreading +* [react/jsx-props-no-spreading](docs/rules/jsx-props-no-spreading.md): Prevent JSX prop spreading * [react/jsx-sort-default-props](docs/rules/jsx-sort-default-props.md): Enforce default props alphabetical sorting * [react/jsx-sort-props](docs/rules/jsx-sort-props.md): Enforce props alphabetical sorting (fixable) * [react/jsx-space-before-closing](docs/rules/jsx-space-before-closing.md): Validate spacing before closing bracket in JSX (fixable) * [react/jsx-tag-spacing](docs/rules/jsx-tag-spacing.md): Validate whitespace in and around the JSX opening and closing brackets (fixable) -* [react/jsx-uses-react](docs/rules/jsx-uses-react.md): Prevent React to be incorrectly marked as unused -* [react/jsx-uses-vars](docs/rules/jsx-uses-vars.md): Prevent variables used in JSX to be incorrectly marked as unused +* [react/jsx-uses-react](docs/rules/jsx-uses-react.md): Prevent React to be marked as unused +* [react/jsx-uses-vars](docs/rules/jsx-uses-vars.md): Prevent variables used in JSX to be marked as unused * [react/jsx-wrap-multilines](docs/rules/jsx-wrap-multilines.md): Prevent missing parentheses around multilines JSX (fixable) + ## Other useful plugins +- Rules of Hooks: [eslint-plugin-react-hooks](https://github.com/facebook/react/tree/master/packages/eslint-plugin-react-hooks) - JSX accessibility: [eslint-plugin-jsx-a11y](https://github.com/evcohen/eslint-plugin-jsx-a11y) - React Native: [eslint-plugin-react-native](https://github.com/Intellicode/eslint-plugin-react-native) diff --git a/docs/rules/boolean-prop-naming.md b/docs/rules/boolean-prop-naming.md index 541b8296f2..7f88640343 100644 --- a/docs/rules/boolean-prop-naming.md +++ b/docs/rules/boolean-prop-naming.md @@ -2,6 +2,10 @@ Allows you to enforce a consistent naming pattern for props which expect a boolean value. +> **Note**: You can provide types in runtime types using [PropTypes] and/or +statically using [TypeScript] or [Flow]. This rule will validate your prop types +regardless of how you define them. + ## Rule Details The following patterns are considered warnings: @@ -15,6 +19,13 @@ var Hello = createReactClass({ }); ``` +```jsx +type Props = { + enabled: boolean +} +const Hello = (props: Props) =>
; +``` + The following patterns are **not** considered warnings: ```jsx @@ -25,16 +36,22 @@ var Hello = createReactClass({ render: function() { return
; }; }); ``` +```jsx +type Props = { + isEnabled: boolean +} +const Hello = (props: Props) =>
+``` ## Rule Options ```js ... -"react/boolean-prop-naming": [, { - "propTypeNames": Array, - "rule": , - "message": , - "validateNested": +"react/boolean-prop-naming": [, { + "propTypeNames": Array, + "rule": , + "message": , + "validateNested": }] ... ``` @@ -99,3 +116,7 @@ This value is boolean. It tells if nested props should be validated as well. By ```jsx "react/boolean-prop-naming": ["error", { "validateNested": true }] ``` + +[PropTypes]: https://reactjs.org/docs/typechecking-with-proptypes.html +[TypeScript]: http://www.typescriptlang.org/ +[Flow]: https://flow.org/ diff --git a/docs/rules/button-has-type.md b/docs/rules/button-has-type.md index 7708f9eb1d..63a6038e29 100644 --- a/docs/rules/button-has-type.md +++ b/docs/rules/button-has-type.md @@ -10,6 +10,7 @@ The following patterns are considered errors: ```jsx var Hello = var Hello = +var Hello = var Hello = React.createElement('button', {}, 'Hello') var Hello = React.createElement('button', {type: 'foo'}, 'Hello') diff --git a/docs/rules/default-props-match-prop-types.md b/docs/rules/default-props-match-prop-types.md index 305d14ad75..ceb03564a9 100644 --- a/docs/rules/default-props-match-prop-types.md +++ b/docs/rules/default-props-match-prop-types.md @@ -1,10 +1,15 @@ # Enforce all defaultProps have a corresponding non-required PropType (react/default-props-match-prop-types) -This rule aims to ensure that any `defaultProp` has a non-required `PropType` declaration. +This rule aims to ensure that any prop in `defaultProps` has a non-required type +definition. -Having `defaultProps` for non-existent `propTypes` is likely the result of errors in refactoring -or a sign of a missing `propType`. Having a `defaultProp` for a required property similarly -indicates a possible refactoring problem. +> **Note**: You can provide types in runtime types using [PropTypes] and/or +statically using [TypeScript] or [Flow]. This rule will validate your prop types +regardless of how you define them. + +Having `defaultProps` for non-existent prop types is likely the result of errors +in refactoring or a sign of a missing prop type. Having a `defaultProp` for a +required property similarly indicates a possible refactoring problem. ## Rule Details @@ -160,7 +165,7 @@ NotAComponent.propTypes = { ### `allowRequiredDefaults` -When `true` the rule will ignore `defaultProps` for `isRequired` `propTypes`. +When `true` the rule will ignore `defaultProps` for required prop types. The following patterns are considered okay and do not cause warnings: @@ -190,3 +195,6 @@ If you don't care about stray `defaultsProps` in your components, you can disabl # Resources - [Official React documentation on defaultProps](https://facebook.github.io/react/docs/typechecking-with-proptypes.html#default-prop-values) +[PropTypes]: https://reactjs.org/docs/typechecking-with-proptypes.html +[TypeScript]: http://www.typescriptlang.org/ +[Flow]: https://flow.org/ diff --git a/docs/rules/forbid-component-props.md b/docs/rules/forbid-component-props.md index 8e0ece521e..b44b36d929 100644 --- a/docs/rules/forbid-component-props.md +++ b/docs/rules/forbid-component-props.md @@ -42,16 +42,17 @@ The following patterns are **not** considered warnings: ### `forbid` An array specifying the names of props that are forbidden. The default value of this option is `['className', 'style']`. -Each array element can either be a string with the property name or object specifying the property name and a component whitelist: +Each array element can either be a string with the property name or object specifying the property name, an optional +custom message, and a component whitelist: ```js { "propName": "someProp", - "allowedFor": [SomeComponent, AnotherComponent] + "allowedFor": [SomeComponent, AnotherComponent], + "message": "Avoid using someProp" } ``` - ### Related rules - [forbid-dom-props](./forbid-dom-props.md) diff --git a/docs/rules/forbid-dom-props.md b/docs/rules/forbid-dom-props.md index 8dca2826ae..600cd30b8c 100644 --- a/docs/rules/forbid-dom-props.md +++ b/docs/rules/forbid-dom-props.md @@ -36,14 +36,22 @@ The following patterns are **not** considered warnings: ```js ... -"react/forbid-dom-props": [, { "forbid": [] }] +"react/forbid-dom-props": [, { "forbid": [|] }] ... ``` ### `forbid` An array of strings, with the names of props that are forbidden. The default value of this option `[]`. +Each array element can either be a string with the property name or object specifying the property name and an optional +custom message: +```js +{ + "propName": "someProp", + "message": "Avoid using someProp" +} +``` ### Related rules diff --git a/docs/rules/function-component-definition.md b/docs/rules/function-component-definition.md new file mode 100644 index 0000000000..107cc20100 --- /dev/null +++ b/docs/rules/function-component-definition.md @@ -0,0 +1,254 @@ +# Enforce a specific function type for function components (react/function-component-definition) + +This option enforces a specific function type for function components. + +**Fixable:** This rule is automatically fixable using the `--fix` flag on the command line. + +## Rule Details + +This rule is aimed to enforce consistent function types for function components. By default it prefers function declarations for named components and function expressions for unnamed components. + +The following patterns are considered warnings: + +```jsx +// function expression for named component +var Component = function (props) { + return
{props.content}
; +}; + +// arrow function for named component +var Component = (props) => { + return
{props.content}
; +}; + +// arrow function for unnamed component +function getComponent() { + return (props) => { + return
{props.content}
; + }; +} +``` + +## Rule Options + +This rule takes an options object as a second parameter where the preferred function type for components can be specified. The first property of the options object is `"namedComponents"` which can be `"function-declaration"`, `"function-expression"`, or `"arrow-function"` and has `'function-declaration'` as its default. The second property is `"unnamedComponents"` that can be either `"function-expression"` or `"arrow-function"`, and has `'function-expression'` as its default. + +```js +... +"react/function-component-definition": [, { + "namedComponents": "function-declaration" | "function-expression" | "arrow-function", + "unnamedComponents": "function-expression" | "arrow-function" +}] +... +``` + +The following patterns are considered warnings: + +```jsx +// only function declarations for named components +// [2, { "namedComponents": "function-declaration" }] +var Component = function (props) { + return
; +}; + +var Component = (props) => { + return
; +}; + +// only function expressions for named components +// [2, { "namedComponents": "function-expression" }] +function Component (props) { + return
; +}; + +var Component = (props) => { + return
; +}; + +// only arrow functions for named components +// [2, { "namedComponents": "arrow-function" }] +function Component (props) { + return
; +}; + +var Component = function (props) { + return
; +}; + +// only function expressions for unnamed components +// [2, { "unnamedComponents": "function-expression" }] +function getComponent () { + return (props) => { + return
; + }; +} + +// only arrow functions for unnamed components +// [2, { "unnamedComponents": "arrow-function" }] +function getComponent () { + return function (props) { + return
; + }; +} + +``` + +The following patterns are **not** warnings: + +```jsx + +// only function declarations for named components +// [2, { "namedComponents": "function-declaration" }] +function Component (props) { + return
; +} + +// only function expressions for named components +// [2, { "namedComponents": "function-expression" }] +var Component = function (props) { + return
; +}; + +// only arrow functions for named components +// [2, { "namedComponents": "arrow-function" }] +var Component = (props) => { + return
; +}; + +// only function expressions for unnamed components +// [2, { "unnamedComponents": "function-expression" }] +function getComponent () { + return function (props) { + return
; + }; +} + +// only arrow functions for unnamed components +// [2, { "unnamedComponents": "arrow-function" }] +function getComponent () { + return (props) => { + return
; + }; +} + +``` + +## Unfixable patterns + +There is one unfixable pattern in JavaScript. + +It has to do with the fact that this is valid syntax: + +```js +export default function getComponent () { + return
; +} +``` + +While these are not: + +```js +export default var getComponent = () => { + return
; +} + +export default var getComponent = function () { + return
; +} +``` + +These patterns have to be manually fixed. + +## Heads up, TypeScript users + +Note that the autofixer is somewhat constrained for TypeScript users. + +The following patterns can **not** be autofixed in TypeScript: + +```tsx +// function expressions and arrow functions that have type annotations cannot be autofixed to function declarations +// [2, { "namedComponents": "function-declaration" }] +var Component: React.FC = function (props) { + return
; +}; + +var Component: React.FC = (props) => { + return
; +}; + +// function components with one unconstrained type parameter cannot be autofixed to arrow functions because the syntax conflicts with jsx +// [2, { "namedComponents": "arrow-function" }] +function Component(props: Props) { + return
; +}; + +var Component = function (props: Props) { + return
; +}; + +// [2, { "unnamedComponents": "arrow-function" }] +function getComponent() { + return function (props: Props) => { + return
; + } +} +``` + +Type parameters do not produce syntax conflicts if either there are multiple type parameters or, if there is only one constrained type parameter. + +The following patterns can be autofixed in TypeScript: + +```tsx +// autofix to function expression with type annotation +// [2, { "namedComponents": "function-expression" }] +var Component: React.FC = (props) => { + return
; +}; + +// autofix to arrow function with type annotation +// [2, { "namedComponents": "function-expression" }] +var Component: React.FC = function (props) { + return
; +}; + +// autofix to named arrow function with one constrained type parameter +// [2, { "namedComponents": "arrow-function" }] +function Component(props: Props) { + return
; +} + +var Component = function (props: Props) { + return
; +}; + +// autofix to named arrow function with multiple type parameters +// [2, { "namedComponents": "arrow-function" }] +function Component(props: Props) { + return
; +} + +var Component = function (props: Props) { + return
; +}; + +// autofix to unnamed arrow function with one constrained type parameter +// [2, { "unnamedComponents": "arrow-function" }] +function getComponent() { + return function (props: Props) => { + return
; + }; +} + +// autofix to unnamed arrow function with multiple type parameters +// [2, { "unnamedComponents": "arrow-function" }] +function getComponent() { + return function (props: Props) => { + return
; + } +} + +``` + +## When not to use + +If you are not interested in consistent types of function components. diff --git a/docs/rules/jsx-first-prop-new-line.md b/docs/rules/jsx-first-prop-new-line.md index fc6481642a..9810eb07e1 100644 --- a/docs/rules/jsx-first-prop-new-line.md +++ b/docs/rules/jsx-first-prop-new-line.md @@ -99,6 +99,12 @@ The following patterns are **not** considered warnings when configured `"multili /> ``` +## Rule Options + +```jsx +"react/jsx-first-prop-new-line": `"always" | "never" | "multiline" | "multiline-multiprop"` +``` + ## When not to use If you are not using JSX then you can disable this rule. diff --git a/docs/rules/jsx-handler-names.md b/docs/rules/jsx-handler-names.md index b567b414db..42d285ae99 100644 --- a/docs/rules/jsx-handler-names.md +++ b/docs/rules/jsx-handler-names.md @@ -30,13 +30,15 @@ The following patterns are **not** considered warnings: ... "react/jsx-handler-names": [, { "eventHandlerPrefix": , - "eventHandlerPropPrefix": + "eventHandlerPropPrefix": , + "checkLocalVariables": }] ... ``` * `eventHandlerPrefix`: Prefix for component methods used as event handlers. Defaults to `handle` * `eventHandlerPropPrefix`: Prefix for props that are used as event handlers. Defaults to `on` +* `checkLocalVariables`: Determines whether event handlers stored as local variables are checked. Defaults to `false` ## When Not To Use It diff --git a/docs/rules/jsx-key.md b/docs/rules/jsx-key.md index 6e7b71ef94..705c627043 100644 --- a/docs/rules/jsx-key.md +++ b/docs/rules/jsx-key.md @@ -27,6 +27,26 @@ data.map((x, i) => {x}); ``` +## Rule Options + +```js +... +"react/jsx-key": [, { "checkFragmentShorthand": }] +... +``` + +### `checkFragmentShorthand` (default: `false`) + +When `true` the rule will check if usage of the [shorthand fragment syntax][short_syntax] requires a key. This option was added to avoid a breaking change and will be the default in the next major version. + +The following patterns are considered warnings: + +```jsx +[<>, <>, <>]; + +data.map(x => <>{x}); +``` + ## When not to use If you are not using JSX then you can disable this rule. @@ -34,3 +54,5 @@ If you are not using JSX then you can disable this rule. Also, if you have some prevalent situation where you use arrow functions to return JSX that will not be held in an iterable, you may want to disable this rule. + +[short_syntax]: https://reactjs.org/docs/fragments.html#short-syntax diff --git a/docs/rules/jsx-no-bind.md b/docs/rules/jsx-no-bind.md index e44321e79f..12128ee5c6 100644 --- a/docs/rules/jsx-no-bind.md +++ b/docs/rules/jsx-no-bind.md @@ -37,7 +37,7 @@ When `true` the following are **not** considered warnings: ```jsx
console.log("Hello!")} /> - + ); +}; +``` + +Otherwise, the idiomatic way to avoid redefining callbacks on every render would be to memoize them using the [`useCallback`](https://reactjs.org/docs/hooks-reference.html#usecallback) hook: + +```jsx +const Button = () => { + const [text, setText] = useState("Before click"); + const onClick = useCallback(() => { + setText("After click"); + }, [setText]); // Array of dependencies for which the memoization should update + return ( + + ); +}; +``` + ## When Not To Use It If you do not use JSX or do not want to enforce that `bind` or arrow functions are not used in props, then you can disable this rule. diff --git a/docs/rules/jsx-no-literals.md b/docs/rules/jsx-no-literals.md index 3933357f52..06ba9a907c 100644 --- a/docs/rules/jsx-no-literals.md +++ b/docs/rules/jsx-no-literals.md @@ -1,11 +1,10 @@ # Prevent usage of string literals in JSX (react/jsx-no-literals) -There are a couple of scenarios where you want to avoid string literals in JSX. Either to enforce consistency and reducing strange behaviour, or for enforcing that literals aren't kept in JSX so they can be translated. +There are a few scenarios where you want to avoid string literals in JSX. You may want to enforce consistency, reduce syntax highlighting issues, or ensure that strings are part of a translation system. ## Rule Details -In JSX when using a literal string you can wrap it in a JSX container `{'TEXT'}`. This rules by default requires that you wrap all literal strings. -Prevents any odd artifacts of highlighters if your unwrapped string contains an enclosing character like `'` in contractions and enforces consistency. +By default this rule requires that you wrap all literal strings in a JSX container `{'TEXT'}`. The following patterns are considered warnings: @@ -19,16 +18,24 @@ The following patterns are **not** considered warnings: var Hello =
{'test'}
; ``` -### Options +```jsx +var Hello =
+ {'test'} +
; +``` -There is only one option: +## Rule Options -* `noStrings` - Enforces no string literals used as children, wrapped or unwrapped. +There are two options: -To use, you can specify like the following: +* `noStrings` (default: `false`) - Enforces no string literals used as children, wrapped or unwrapped. +* `allowedStrings` - An array of unique string values that would otherwise warn, but will be ignored. +* `ignoreProps` (default: `false`) - When `true` the rule ignores literals used in props, wrapped or unwrapped. + +To use, you can specify as follows: ```js -"react/jsx-no-literals": [, {"noStrings": true}] +"react/jsx-no-literals": [, {"noStrings": true, "allowedStrings": ["allowed"], "ignoreProps": false}] ``` In this configuration, the following are considered warnings: @@ -41,6 +48,25 @@ var Hello =
test
; var Hello =
{'test'}
; ``` +```jsx +var Hello =
+ {'test'} +
; +``` + +```jsx +var Hello =
; +``` + +```jsx +var Hello =
; +``` + +```jsx +var Hello =
; +``` + + The following are **not** considered warnings: ```jsx @@ -53,6 +79,45 @@ var Hello =
var Hello =
{translate('my.translation.key')}
``` +```jsx +// an allowed string +var Hello =
allowed
+``` + +```jsx +// an allowed string surrounded by only whitespace +var Hello =
+ allowed +
; +``` + +```jsx +// spread props object +var Hello = +``` + +```jsx +// use variable for prop values +var Hello =
+``` + +```jsx +// cache +class Comp1 extends Component { + asdf() {} + + render() { + return ( +
+ {'asdjfl'} + test + {'foo'} +
+ ); + } +} +``` + ## When Not To Use It If you do not want to enforce any style JSX literals, then you can disable this rule. diff --git a/docs/rules/jsx-no-script-url.md b/docs/rules/jsx-no-script-url.md new file mode 100644 index 0000000000..a73e8e72f2 --- /dev/null +++ b/docs/rules/jsx-no-script-url.md @@ -0,0 +1,57 @@ +# Prevent usage of `javascript:` URLs (react/jsx-no-script-url) + +**In React 16.9** any URLs starting with `javascript:` [scheme](https://wiki.whatwg.org/wiki/URL_schemes#javascript:_URLs) log a warning. +React considers the pattern as a dangerous attack surface, see [details](https://reactjs.org/blog/2019/08/08/react-v16.9.0.html#deprecating-javascript-urls). +**In a future major release**, React will throw an error if it encounters a `javascript:` URL. + +## Rule Details + +The following patterns are considered warnings: + +```jsx + + + +``` + +The following patterns are **not** considered warnings: + +```jsx + + +``` + +## Rule Options +```json +{ + "react/jsx-no-script-url": [ + "error", + [ + { + "name": "Link", + "props": ["to"] + }, + { + "name": "Foo", + "props": ["href", "to"] + } + ] + ] +} +``` + +Allows you to indicate a specific list of properties used by a custom component to be checked. + +### name +Component name. + +### props +List of properties that should be validated. + +The following patterns are considered warnings with the options listed above: + +```jsx + + + +``` diff --git a/docs/rules/jsx-no-target-blank.md b/docs/rules/jsx-no-target-blank.md index a8bee1ac72..d87eef3dfb 100644 --- a/docs/rules/jsx-no-target-blank.md +++ b/docs/rules/jsx-no-target-blank.md @@ -2,22 +2,23 @@ When creating a JSX element that has an `a` tag, it is often desired to have the link open in a new tab using the `target='_blank'` attribute. Using this -attribute unaccompanied by `rel='noreferrer noopener'`, however, is a severe -security vulnerability ([see here for more details](https://mathiasbynens.github.io/rel-noopener)) -This rules requires that you accompany `target='_blank'` attributes with `rel='noreferrer noopener'`. +attribute unaccompanied by `rel='noreferrer'`, however, is a severe +security vulnerability ([see here for more details](https://html.spec.whatwg.org/multipage/links.html#link-type-noopener)) +This rules requires that you accompany `target='_blank'` attributes with `rel='noreferrer'`. ## Rule Details This rule aims to prevent user generated links from creating security vulnerabilities by requiring -`rel='noreferrer noopener'` for external links, and optionally any dynamically generated links. +`rel='noreferrer'` for external links, and optionally any dynamically generated links. ## Rule Options ```json ... -"react/jsx-no-target-blank": [, { "enforceDynamicLinks": }] +"react/jsx-no-target-blank": [, { "allowReferrer": , "enforceDynamicLinks": }] ... ``` +* allow-referrer: optional boolean. If `true` does not require `noreferrer`. Defaults to `false`. * enabled: for enabling the rule. 0=off, 1=warn, 2=error. Defaults to 0. * enforce: optional string, 'always' or 'never' @@ -38,6 +39,7 @@ The following patterns are **not** considered errors: ```jsx var Hello =

+var Hello = var Hello = var Hello = var Hello = @@ -74,6 +76,9 @@ var Hello = var Hello = ``` +## When To Override It +For links to a trusted host (e.g. internal links to your own site, or links to a another host you control, where you can be certain this security vulnerability does not exist), you may want to keep the HTTP Referer header for analytics purposes. + ## When Not To Use It -If you do not have any external links, you can disable this rule +If you do not have any external links, you can disable this rule. diff --git a/docs/rules/jsx-no-useless-fragment.md b/docs/rules/jsx-no-useless-fragment.md new file mode 100644 index 0000000000..fce91f901f --- /dev/null +++ b/docs/rules/jsx-no-useless-fragment.md @@ -0,0 +1,54 @@ +# Disallow unnecessary fragments (react/jsx-no-useless-fragment) + +A fragment is redundant if it contains only one child, or if it is the child of a html element, and is not a [keyed fragment](https://reactjs.org/docs/fragments.html#keyed-fragments). + +**Fixable:** This rule is sometimes automatically fixable using the `--fix` flag on the command line. + +## Rule Details + +The following patterns are considered warnings: + +```jsx +<>{foo} + +<> + +

<>foo

+ +<> + +foo + +foo + +
+ <> +
+
+ +
+``` + +The following patterns are **not** considered warnings: + +```jsx +<> + + + + +<>foo {bar} + +<> {foo} + +const cat = <>meow + + + <> +
+
+ + + +{item.value} +``` diff --git a/docs/rules/jsx-pascal-case.md b/docs/rules/jsx-pascal-case.md index 00af2eb013..462e59876d 100644 --- a/docs/rules/jsx-pascal-case.md +++ b/docs/rules/jsx-pascal-case.md @@ -46,7 +46,16 @@ The following patterns are **not** considered warnings: * `enabled`: for enabling the rule. 0=off, 1=warn, 2=error. Defaults to 0. * `allowAllCaps`: optional boolean set to `true` to allow components name in all caps (default to `false`). -* `ignore`: optional array of components name to ignore during validation. +* `ignore`: optional string-array of component names to ignore during validation. + +### `allowAllCaps` + +The following patterns are **not** considered warnings when `allowAllCaps` is `true`: + +```jsx + + +``` ## When Not To Use It diff --git a/docs/rules/jsx-props-no-multi-spaces.md b/docs/rules/jsx-props-no-multi-spaces.md index c1b02dec45..933e3cb5d9 100644 --- a/docs/rules/jsx-props-no-multi-spaces.md +++ b/docs/rules/jsx-props-no-multi-spaces.md @@ -29,3 +29,5 @@ The following patterns are **not** considered warnings: ## When Not To Use It If you are not using JSX or don't care about the space between two props in the same line. + +If you have enabled the core rule `no-multi-spaces` with eslint >= 3, you don't need this rule. diff --git a/docs/rules/jsx-props-no-spreading.md b/docs/rules/jsx-props-no-spreading.md index bc03b32abc..3e97917d00 100644 --- a/docs/rules/jsx-props-no-spreading.md +++ b/docs/rules/jsx-props-no-spreading.md @@ -21,14 +21,14 @@ const {one_prop, two_prop} = otherProps; {alt} ``` - ## Rule Options ```js ... -"react/jsx-props-no-spreading": [{ - "html": "ignore" / "enforce", - "custom": "ignore" / "enforce", +"react/jsx-props-no-spreading": [, { + "html": "ignore" | "enforce", + "custom": "ignore" | "enforce", + "explicitSpread": "ignore" | "enforce", "exceptions": [] }] ... @@ -61,10 +61,21 @@ The following patterns are **not** considered warnings when `custom` is set to ` ``` The following patterns are still considered warnings: + ```jsx ``` +### explicitSpread + +`explicitSpread` set to `ignore` will ignore spread operators that are explicilty listing all object properties within that spread. Default is set to `enforce`. + +The following pattern is **not** considered warning when `explicitSpread` is set to `ignore`: + +```jsx + +``` + ### exceptions An "exception" will always flip the resulting html or custom setting for that component - ie, html set to `ignore`, with an exception of `div` will enforce on an `div`; custom set to `enforce` with an exception of `Foo` will ignore `Foo`. @@ -82,6 +93,7 @@ const {src, alt} = props; ``` The following patterns are considered warnings: + ```jsx ``` @@ -100,6 +112,7 @@ const {one_prop, two_prop} = otherProps; ``` The following patterns are considered warnings: + ```jsx ``` diff --git a/docs/rules/no-access-state-in-setstate.md b/docs/rules/no-access-state-in-setstate.md index 9f907d51aa..bf29f53d04 100644 --- a/docs/rules/no-access-state-in-setstate.md +++ b/docs/rules/no-access-state-in-setstate.md @@ -11,12 +11,12 @@ function increment() { } ``` -If these two `setState` operations is grouped together in a batch it will -look be something like the following, given that value is 1: +If two `setState` operations are grouped together in a batch, they +both evaluate the old state. Given that `state.value` is 1: ```javascript -setState({value: 1 + 1}) -setState({value: 1 + 1}) +setState({value: this.state.value + 1}) // 2 +setState({value: this.state.value + 1}) // 2, not 3 ``` This can be avoided with using callbacks which takes the previous state diff --git a/docs/rules/no-adjacent-inline-elements.md b/docs/rules/no-adjacent-inline-elements.md new file mode 100644 index 0000000000..7d3548cc8b --- /dev/null +++ b/docs/rules/no-adjacent-inline-elements.md @@ -0,0 +1,24 @@ +# Prevent adjacent inline elements not separated by whitespace. (react/no-adjacent-inline-elements) + +Adjacent inline elements not separated by whitespace will bump up against each +other when viewed in an unstyled manner, which usually isn't desirable. + +## Rule Details + +The following patterns are considered warnings: + +```jsx +
+
+ +React.createElement("div", undefined, [React.createElement("a"), React.createElement("span")]); +``` + +The following patterns are not considered warnings: + +```jsx +
+
+ +React.createElement("div", undefined, [React.createElement("a"), " ", React.createElement("a")]); +``` diff --git a/docs/rules/no-deprecated.md b/docs/rules/no-deprecated.md index 77cf0ff7bb..604d7c9e2d 100644 --- a/docs/rules/no-deprecated.md +++ b/docs/rules/no-deprecated.md @@ -27,6 +27,11 @@ const propTypes = { React.DOM.div(); import React, { PropTypes } from 'react'; + +// old lifecycles (since React 16.9) +componentWillMount() { } +componentWillReceiveProps() { } +componentWillUpdate() { } ``` The following patterns are **not** considered warnings: @@ -38,4 +43,8 @@ ReactDOM.render(, root); ReactDOM.findDOMNode(this.refs.foo); import { PropTypes } from 'prop-types'; + +UNSAFE_componentWillMount() { } +UNSAFE_componentWillReceiveProps() { } +UNSAFE_componentWillUpdate() { } ``` diff --git a/docs/rules/no-render-return-value.md b/docs/rules/no-render-return-value.md index dbafb0b5b1..5345d7c707 100644 --- a/docs/rules/no-render-return-value.md +++ b/docs/rules/no-render-return-value.md @@ -1,4 +1,4 @@ -# Prevent usage of the return value of React.render (react/no-render-return-value) +# Prevent usage of the return value of ReactDOM.render (react/no-render-return-value) > `ReactDOM.render()` currently returns a reference to the root `ReactComponent` instance. However, using this return value is legacy and should be avoided because future versions of React may render components asynchronously in some cases. If you need a reference to the root `ReactComponent` instance, the preferred solution is to attach a [callback ref](http://facebook.github.io/react/docs/more-about-refs.html#the-ref-callback-attribute) to the root element. diff --git a/docs/rules/no-this-in-sfc.md b/docs/rules/no-this-in-sfc.md index e622408626..cfff38640d 100644 --- a/docs/rules/no-this-in-sfc.md +++ b/docs/rules/no-this-in-sfc.md @@ -1,9 +1,8 @@ # Prevent `this` from being used in stateless functional components (react/no-this-in-sfc) -When using a stateless functional component (SFC), props/context aren't accessed in the same way as a class component or the `create-react-class` format. Both props and context are passed as separate arguments to the component instead. Also, as the name suggests, a stateless component does not have state on `this.state`. - -Attempting to access properties on `this` can be a potential error if someone is unaware of the differences when writing a SFC or missed when converting a class component to a SFC. +In React, there are two styles of component. One is a class component: `class Foo extends React.Component {...}`, which accesses its props, context, and state as properties of `this`: `this.props.foo`, etc. The other are stateless functional components (SFCs): `function Foo(props, context) {...}`. As you can see, there's no `state` (hence the name - hooks do not change this), and the props and context are provided as its two functional arguments. In an SFC, state is usually best implements with a [React hook](https://reactjs.org/docs/hooks-overview.html) such as `React.useState()`. +Attempting to access properties on `this` can sometimes be valid, but it's very commonly an error caused by unfamiliarity with the differences between the two styles of components, or a missed reference when converting a class component to an SFC. ## Rule Details diff --git a/docs/rules/no-typos.md b/docs/rules/no-typos.md index 87d0a94fd9..1a1c3d92ce 100644 --- a/docs/rules/no-typos.md +++ b/docs/rules/no-typos.md @@ -86,6 +86,9 @@ class MyComponent extends React.Component { } } +class MyComponent extends React.Component { + getDerivedStateFromProps() {} +} ``` The following patterns are **not** considered warnings: diff --git a/docs/rules/no-unused-prop-types.md b/docs/rules/no-unused-prop-types.md index 6760e8c857..9f309f3a04 100644 --- a/docs/rules/no-unused-prop-types.md +++ b/docs/rules/no-unused-prop-types.md @@ -1,44 +1,53 @@ -# Prevent definitions of unused prop types (react/no-unused-prop-types) +# Prevent definitions of unused propTypes (react/no-unused-prop-types) -Warns you if you have defined a prop type but it is never being used anywhere. +Warns if a prop with a defined type isn't being used. + +> **Note**: You can provide types in runtime types using [PropTypes] and/or +statically using [TypeScript] or [Flow]. This rule will validate your prop types +regardless of how you define them. ## Rule Details The following patterns are considered warnings: ```jsx -var Hello = createReactClass({ - propTypes: { - name: PropTypes.string - }, - render: function() { +class Hello extends React.Component { + render() { return
Hello Bob
; } }); -var Hello = createReactClass({ - propTypes: { - firstname: PropTypes.string.isRequired, - middlename: PropTypes.string.isRequired, // middlename is never used below - lastname: PropTypes.string.isRequired - }, - render: function() { +Hello.propTypes = { + name: PropTypes.string +}, +``` + +```jsx +type Props = { + firstname: string, + middlename: string, // middlename is never used above + lastname: string +} + +class Hello extends React.Component { + render() { return
Hello {this.props.firstname} {this.props.lastname}
; } -}); +} ``` The following patterns are **not** considered warnings: ```jsx -var Hello = createReactClass({ - propTypes: { - name: PropTypes.string - }, - render: function() { +class Hello extends React.Component { + render() { return
Hello {this.props.name}
; } -}); +}; + +Hello.propTypes: { + name: PropTypes.string +}, ``` ## Rule Options @@ -59,12 +68,13 @@ This rule can take one argument to ignore some specific props during validation. - [False Positives: SFC (helper render methods)](#false-positives-sfc) -### False positives SFC -For components with Stateless Functional Components (often used as helper render methods); -SFC is a function that takes prop(s) as an argument and returns a JSX expression. -Even if this function gets called from a component the props that are only used inside SFC would not be considered used by a component. +### False Positives SFC + +Stateless Function Components (SFCs) accept props as an argument and return a JSX expression. +Even if the function gets called from a component, the props that are only used inside the component are not be considered used by a component. Triggers false positive: + ```js function AComponent(props) { function helperRenderer(aProp) { // is considered SFC @@ -108,3 +118,7 @@ AComponent.propTypes = { bProp: PropTypes.string }; ``` + +[PropTypes]: https://reactjs.org/docs/typechecking-with-proptypes.html +[TypeScript]: http://www.typescriptlang.org/ +[Flow]: https://flow.org/ diff --git a/docs/rules/prefer-es6-class.md b/docs/rules/prefer-es6-class.md index c044211183..8c876b4342 100644 --- a/docs/rules/prefer-es6-class.md +++ b/docs/rules/prefer-es6-class.md @@ -1,6 +1,6 @@ # Enforce ES5 or ES6 class for React Components (react/prefer-es6-class) -React offers you two way to create traditional components: using the ES5 `create-react-class` module or the new ES6 class system. This rule allow you to enforce one way or another. +React offers you two ways to create traditional components: using the ES5 `create-react-class` module or the new ES6 class system. This rule allows you to enforce one way or another. ## Rule Options @@ -36,7 +36,7 @@ class Hello extends React.Component { ### `never` mode -Will enforce ES5 classes for React Components +Will enforce ES5 classes for React Components. The following patterns are considered warnings: diff --git a/docs/rules/prop-types.md b/docs/rules/prop-types.md index 627b93bb94..b0e65baddf 100644 --- a/docs/rules/prop-types.md +++ b/docs/rules/prop-types.md @@ -1,37 +1,66 @@ # Prevent missing props validation in a React component definition (react/prop-types) -PropTypes improve the reusability of your component by validating the received data. +Defining types for component props improves reusability of your components by +validating received data. It can warn other developers if they make a mistake while reusing the component with improper data type. -It can warn other developers if they make a mistake while reusing the component with improper data type. +> **Note**: You can provide types in runtime types using [PropTypes] and/or +statically using [TypeScript] or [Flow]. This rule will validate your prop types +regardless of how you define them. ## Rule Details The following patterns are considered warnings: ```jsx -var Hello = createReactClass({ - render: function() { - return
Hello {this.props.name}
; - } -}); +function Hello({ name }) { + return
Hello {name}
; + // 'name' is missing in props validation +} var Hello = createReactClass({ propTypes: { firstname: PropTypes.string.isRequired }, render: function() { - return
Hello {this.props.firstname} {this.props.lastname}
; // lastname type is not defined in propTypes + return
Hello {this.props.firstname} {this.props.lastname}
; + // 'lastname' type is missing in props validation } }); -function Hello({ name }) { +// Or in ES6 +class Hello extends React.Component { + render() { + return
Hello {this.props.firstname} {this.props.lastname}
; + // 'lastname' type is missing in props validation + } +} +Hello.propTypes = { + firstname: PropTypes.string.isRequired +} +``` + +In TypeScript: + +```tsx +interface Props = { + age: number +} +function Hello({ name }: Props) { return
Hello {name}
; + // 'name' type is missing in props validation } ``` Examples of correct usage without warnings: ```jsx +function Hello({ name }) { + return
Hello {name}
; +} +Hello.propTypes = { + name: PropTypes.string.isRequired +} + var Hello = createReactClass({ propTypes: { name: PropTypes.string.isRequired, @@ -62,38 +91,31 @@ class HelloEs6WithPublicClassField extends React.Component { } ``` -The following patterns are **not** considered warnings: +In Flow: -```jsx -var Hello = createReactClass({ - render: function() { - return
Hello World
; - } -}); - -var Hello = createReactClass({ - propTypes: { - name: PropTypes.string.isRequired - }, - render: function() { +```tsx +type Props = { + name: string +} +class Hello extends React.Component { + render() { return
Hello {this.props.name}
; } -}); +} +``` -// Referencing an external object disable the rule for the component -var Hello = createReactClass({ - propTypes: myPropTypes, - render: function() { - return
Hello {this.props.name}
; - } -}); +The following patterns are **not** considered warnings: +```jsx +function Hello() { + return
Hello World
; +} + +// Referencing an external object disable the rule for the component function Hello({ name }) { return
Hello {name}
; } -Hello.propTypes = { - name: PropTypes.string.isRequired, -}; +Hello.propTypes = myPropTypes; ``` ## Rule Options @@ -102,14 +124,14 @@ This rule can take one argument to ignore some specific props during validation. ```js ... -"react/prop-types": [, { ignore: , customValidators: }] +"react/prop-types": [, { ignore: , customValidators: , skipUndeclared: }] ... ``` * `enabled`: for enabling the rule. 0=off, 1=warn, 2=error. Defaults to 0. * `ignore`: optional array of props name to ignore during validation. * `customValidators`: optional array of validators used for propTypes validation. -* `skipUndeclared`: only error on components that have a propTypes block declared +* `skipUndeclared`: optional boolean to only error on components that have a propTypes block declared. ### As for "exceptions" @@ -121,11 +143,11 @@ As it aptly noticed in > Why should children be an exception? > Most components don't need `this.props.children`, so that makes it extra important -to document `children` in the propTypes. +to document `children` in the prop types. -Generally, you should use `PropTypes.node` for `children`. It accepts -anything that can be rendered: numbers, strings, elements or an array containing -these types. +Generally, you should use `PropTypes.node` or static type `React.Node` for +`children`. It accepts anything that can be rendered: numbers, strings, elements +or an array containing these types. Since 2.0.0 children is no longer ignored for props validation. @@ -135,6 +157,10 @@ For this rule to work we need to detect React components, this could be very har For now we should detect components created with: +* a function that return JSX or the result of a `React.createElement` call. * `createReactClass()` * an ES6 class that inherit from `React.Component` or `Component` -* a stateless function that return JSX or the result of a `React.createElement` call. + +[PropTypes]: https://reactjs.org/docs/typechecking-with-proptypes.html +[TypeScript]: http://www.typescriptlang.org/ +[Flow]: https://flow.org/ diff --git a/docs/rules/require-default-props.md b/docs/rules/require-default-props.md index 0d7923ad5d..9cc4322afa 100644 --- a/docs/rules/require-default-props.md +++ b/docs/rules/require-default-props.md @@ -1,9 +1,18 @@ # Enforce a defaultProps definition for every prop that is not a required prop (react/require-default-props) -This rule aims to ensure that any non-required `PropType` declaration of a component has a corresponding `defaultProps` value. +This rule aims to ensure that any non-required prop types of a component has a +corresponding `defaultProps` value. -One advantage of `defaultProps` over custom default logic in your code is that `defaultProps` are resolved by React before the `PropTypes` typechecking happens, so typechecking will also apply to your `defaultProps`. -The same also holds true for stateless functional components: default function parameters do not behave the same as `defaultProps` and thus using `defaultProps` is still preferred. +> **Note**: You can provide types in runtime types using [PropTypes] and/or +statically using [TypeScript] or [Flow]. This rule will validate your prop types +regardless of how you define them. + +One advantage of `defaultProps` over custom default logic in your code is that +`defaultProps` are resolved by React before the `PropTypes` typechecking +happens, so typechecking will also apply to your `defaultProps`. The same also +holds true for stateless functional components: default function parameters do +not behave the same as `defaultProps` and thus using `defaultProps` is still +preferred. To illustrate, consider the following example: @@ -188,12 +197,13 @@ NotAComponent.propTypes = { ```js ... -"react/require-default-props": [, { forbidDefaultForRequired: }] +"react/require-default-props": [, { forbidDefaultForRequired: , ignoreFunctionalComponents: }] ... ``` * `enabled`: for enabling the rule. 0=off, 1=warn, 2=error. Defaults to 0. * `forbidDefaultForRequired`: optional boolean to forbid prop default for a required prop. Defaults to false. +* `ignoreFunctionalComponents`: optional boolean to ignore this rule for functional components. Defaults to false. ### `forbidDefaultForRequired` @@ -269,9 +279,74 @@ MyStatelessComponent.propTypes = { }; ``` +### `ignoreFunctionalComponents` + +When set to `true`, ignores this rule for all functional components. + +The following patterns are warnings: + +```jsx +class Greeting extends React.Component { + render() { + return ( +

Hello, {this.props.foo} {this.props.bar}

+ ); + } + + static propTypes = { + foo: PropTypes.string, + bar: PropTypes.string.isRequired + }; + + static defaultProps = { + foo: "foo", + bar: "bar" + }; +} +``` + +The following patterns are **not** warnings: + +```jsx +function MyStatelessComponent({ foo, bar }) { + return
{foo}{bar}
; +} + +MyStatelessComponent.propTypes = { + foo: PropTypes.string, + bar: PropTypes.string +}; +``` + +```jsx +const MyStatelessComponent = ({ foo, bar }) => { + return
{foo}{bar}
; +} + +MyStatelessComponent.propTypes = { + foo: PropTypes.string, + bar: PropTypes.string +}; +``` + +```jsx +const MyStatelessComponent = function({ foo, bar }) { + return
{foo}{bar}
; +} + +MyStatelessComponent.propTypes = { + foo: PropTypes.string, + bar: PropTypes.string +}; +``` + ## When Not To Use It If you don't care about using `defaultsProps` for your component's props that are not required, you can disable this rule. # Resources - [Official React documentation on defaultProps](https://facebook.github.io/react/docs/typechecking-with-proptypes.html#default-prop-values) + +[PropTypes]: https://reactjs.org/docs/typechecking-with-proptypes.html +[TypeScript]: http://www.typescriptlang.org/ +[Flow]: https://flow.org/ diff --git a/docs/rules/self-closing-comp.md b/docs/rules/self-closing-comp.md index 989f38f98f..64a9c98b28 100644 --- a/docs/rules/self-closing-comp.md +++ b/docs/rules/self-closing-comp.md @@ -10,6 +10,8 @@ The following patterns are considered warnings: ```jsx var HelloJohn = ; + +var HelloJohnCompound = ; ``` The following patterns are **not** considered warnings: @@ -21,8 +23,12 @@ var intentionalSpace =
{' '}
; var HelloJohn = ; +var HelloJohnCompound = ; + var Profile = ; +var ProfileCompound = ; + var HelloSpace = {' '}; ``` @@ -58,7 +64,11 @@ var intentionalSpace =
{' '}
; var HelloJohn = ; +var HelloJohnCompound = ; + var Profile = ; + +var ProfileCompound = ; ``` ### `html` diff --git a/docs/rules/sort-comp.md b/docs/rules/sort-comp.md index 1e03d12ed1..d0b26486c6 100644 --- a/docs/rules/sort-comp.md +++ b/docs/rules/sort-comp.md @@ -90,7 +90,7 @@ The default configuration is: } } ``` - +* `static-variables` This group is not specified by default, but can be used to enforce class static variable positioning. * `static-methods` is a special keyword that refers to static class methods. * `lifecycle` refers to the `lifecycle` group defined in `groups`. * `everything-else` is a special group that matches all of the methods that do not match any of the other groups. diff --git a/docs/rules/sort-prop-types.md b/docs/rules/sort-prop-types.md index ac5798e3c2..2d23824fe0 100644 --- a/docs/rules/sort-prop-types.md +++ b/docs/rules/sort-prop-types.md @@ -1,9 +1,6 @@ # Enforce propTypes declarations alphabetical sorting (react/sort-prop-types) -Some developers prefer to sort propTypes declarations alphabetically to be able to find necessary declaration easier at the later time. Others feel that it adds complexity and becomes burden to maintain. - -**Fixable:** This rule is automatically fixable using the `--fix` flag on the command line. - +Some developers prefer to sort prop type declaratioms alphabetically to be able to find necessary declaration easier at the later time. Others feel that it adds complexity and becomes burden to maintain. ## Rule Details @@ -20,16 +17,18 @@ var Component = createReactClass({ }, ... }); - -class Component extends React.Component { +``` +```jsx +type Props = { + z: number, + a: any, + b: string +} +class Component extends React.Component { ... } -Component.propTypes = { - z: PropTypes.number, - a: PropTypes.any, - b: PropTypes.string -}; - +``` +```jsx class Component extends React.Component { static propTypes = { z: PropTypes.any, @@ -53,16 +52,18 @@ var Component = createReactClass({ }, ... }); - -class Component extends React.Component { +``` +```jsx +type Props = { + a: string, + b: any, + c: string, +} +class Component extends React.Component { ... } -Component.propTypes = { - a: PropTypes.string, - b: PropTypes.any, - c: PropTypes.string -}; - +``` +```jsx class Component extends React.Component { static propTypes = { a: PropTypes.any, diff --git a/docs/rules/style-prop-object.md b/docs/rules/style-prop-object.md index 464f889cb6..386c09cfb3 100644 --- a/docs/rules/style-prop-object.md +++ b/docs/rules/style-prop-object.md @@ -49,3 +49,33 @@ React.createElement("Hello", { style: { color: 'red' }}); const styles = { height: '100px' }; React.createElement("div", { style: styles }); ``` +## Rule Options + +```js +... +"react/style-prop-object": [, { + "allow": [] +}] +... +``` + +### `allow` +A list of elements that are allowed to have a non-object value in their style attribute. The default value is `[]`. + +#### Example +```js +{ + "allow": ["MyComponent"] +} +``` +The following patterns are considered warnings: +```js + +React.createElement(Hello, { style: "some styling" }); +``` + +The following patterns are **not** considered warnings: +```js + +React.createElement(MyComponent, { style: "some styling" }); +``` diff --git a/index.js b/index.js index f628c55ff6..bc0c30a43d 100644 --- a/index.js +++ b/index.js @@ -3,6 +3,7 @@ const fromEntries = require('object.fromentries'); const entries = require('object.entries'); +/* eslint-disable global-require */ const allRules = { 'boolean-prop-naming': require('./lib/rules/boolean-prop-naming'), 'button-has-type': require('./lib/rules/button-has-type'), @@ -14,6 +15,7 @@ const allRules = { 'forbid-elements': require('./lib/rules/forbid-elements'), 'forbid-foreign-prop-types': require('./lib/rules/forbid-foreign-prop-types'), 'forbid-prop-types': require('./lib/rules/forbid-prop-types'), + 'function-component-definition': require('./lib/rules/function-component-definition'), 'jsx-boolean-value': require('./lib/rules/jsx-boolean-value'), 'jsx-child-element-spacing': require('./lib/rules/jsx-child-element-spacing'), 'jsx-closing-bracket-location': require('./lib/rules/jsx-closing-bracket-location'), @@ -33,7 +35,9 @@ const allRules = { 'jsx-no-comment-textnodes': require('./lib/rules/jsx-no-comment-textnodes'), 'jsx-no-duplicate-props': require('./lib/rules/jsx-no-duplicate-props'), 'jsx-no-literals': require('./lib/rules/jsx-no-literals'), + 'jsx-no-script-url': require('./lib/rules/jsx-no-script-url'), 'jsx-no-target-blank': require('./lib/rules/jsx-no-target-blank'), + 'jsx-no-useless-fragment': require('./lib/rules/jsx-no-useless-fragment'), 'jsx-one-expression-per-line': require('./lib/rules/jsx-one-expression-per-line'), 'jsx-no-undef': require('./lib/rules/jsx-no-undef'), 'jsx-curly-brace-presence': require('./lib/rules/jsx-curly-brace-presence'), @@ -49,6 +53,7 @@ const allRules = { 'jsx-uses-vars': require('./lib/rules/jsx-uses-vars'), 'jsx-wrap-multilines': require('./lib/rules/jsx-wrap-multilines'), 'no-access-state-in-setstate': require('./lib/rules/no-access-state-in-setstate'), + 'no-adjacent-inline-elements': require('./lib/rules/no-adjacent-inline-elements'), 'no-array-index-key': require('./lib/rules/no-array-index-key'), 'no-children-prop': require('./lib/rules/no-children-prop'), 'no-danger': require('./lib/rules/no-danger'), @@ -88,19 +93,20 @@ const allRules = { 'style-prop-object': require('./lib/rules/style-prop-object'), 'void-dom-elements-no-children': require('./lib/rules/void-dom-elements-no-children') }; +/* eslint-enable */ function filterRules(rules, predicate) { - return fromEntries(entries(rules).filter(entry => predicate(entry[1]))); + return fromEntries(entries(rules).filter((entry) => predicate(entry[1]))); } function configureAsError(rules) { - return fromEntries(Object.keys(rules).map(key => [`react/${key}`, 2])); + return fromEntries(Object.keys(rules).map((key) => [`react/${key}`, 2])); } -const activeRules = filterRules(allRules, rule => !rule.meta.deprecated); +const activeRules = filterRules(allRules, (rule) => !rule.meta.deprecated); const activeRulesConfig = configureAsError(activeRules); -const deprecatedRules = filterRules(allRules, rule => rule.meta.deprecated); +const deprecatedRules = filterRules(allRules, (rule) => rule.meta.deprecated); module.exports = { deprecatedRules, diff --git a/lib/rules/boolean-prop-naming.js b/lib/rules/boolean-prop-naming.js index ee8007443d..e61415b969 100644 --- a/lib/rules/boolean-prop-naming.js +++ b/lib/rules/boolean-prop-naming.js @@ -69,6 +69,7 @@ module.exports = { * required: PropTypes.bool.isRequired * } * @param {Object} node The node we're getting the name of + * @returns {string | null} */ function getPropKey(node) { // Check for `ExperimentalSpreadProperty` (ESLint 3/4) and `SpreadElement` (ESLint 5) @@ -96,6 +97,7 @@ module.exports = { /** * Returns the name of the given node (prop) * @param {Object} node The node we're getting the name of + * @returns {string} */ function getPropName(node) { // Due to this bug https://github.com/babel/babel-eslint/issues/307 @@ -115,9 +117,9 @@ module.exports = { */ function flowCheck(prop) { return ( - prop.type === 'ObjectTypeProperty' && - prop.value.type === 'BooleanTypeAnnotation' && - rule.test(getPropName(prop)) === false + prop.type === 'ObjectTypeProperty' + && prop.value.type === 'BooleanTypeAnnotation' + && rule.test(getPropName(prop)) === false ); } @@ -129,9 +131,9 @@ module.exports = { function regularCheck(prop) { const propKey = getPropKey(prop); return ( - propKey && - propTypeNames.indexOf(propKey) >= 0 && - rule.test(getPropName(prop)) === false + propKey + && propTypeNames.indexOf(propKey) >= 0 + && rule.test(getPropName(prop)) === false ); } @@ -142,8 +144,8 @@ module.exports = { */ function nestedPropTypes(prop) { return ( - prop.type === 'Property' && - prop.value.type === 'CallExpression' + prop.type === 'Property' + && prop.value.type === 'CallExpression' ); } @@ -207,7 +209,7 @@ module.exports = { if (!node || !Array.isArray(args)) { return; } - args.filter(arg => arg.type === 'ObjectExpression').forEach(object => validatePropNaming(node, object.properties)); + args.filter((arg) => arg.type === 'ObjectExpression').forEach((object) => validatePropNaming(node, object.properties)); } // -------------------------------------------------------------------------- @@ -220,9 +222,9 @@ module.exports = { return; } if ( - node.value && - node.value.type === 'CallExpression' && - propWrapperUtil.isPropWrapperFunction( + node.value + && node.value.type === 'CallExpression' + && propWrapperUtil.isPropWrapperFunction( context, context.getSourceCode().getText(node.value.callee) ) @@ -247,8 +249,8 @@ module.exports = { } const right = node.parent.right; if ( - right.type === 'CallExpression' && - propWrapperUtil.isPropWrapperFunction( + right.type === 'CallExpression' + && propWrapperUtil.isPropWrapperFunction( context, context.getSourceCode().getText(right.callee) ) @@ -280,7 +282,8 @@ module.exports = { } }, - 'Program:exit': function () { + // eslint-disable-next-line object-shorthand + 'Program:exit'() { if (!rule) { return; } @@ -289,10 +292,10 @@ module.exports = { Object.keys(list).forEach((component) => { // If this is a functional component that uses a global type, check it if ( - list[component].node.type === 'FunctionDeclaration' && - list[component].node.params && - list[component].node.params.length && - list[component].node.params[0].typeAnnotation + list[component].node.type === 'FunctionDeclaration' + && list[component].node.params + && list[component].node.params.length + && list[component].node.params[0].typeAnnotation ) { const typeNode = list[component].node.params[0].typeAnnotation; const annotation = typeNode.typeAnnotation; diff --git a/lib/rules/button-has-type.js b/lib/rules/button-has-type.js index a7d4694b77..76e09c605b 100644 --- a/lib/rules/button-has-type.js +++ b/lib/rules/button-has-type.js @@ -16,12 +16,12 @@ const pragmaUtil = require('../util/pragma'); function isCreateElement(node, context) { const pragma = pragmaUtil.getFromContext(context); - return node.callee && - node.callee.type === 'MemberExpression' && - node.callee.property.name === 'createElement' && - node.callee.object && - node.callee.object.name === pragma && - node.arguments.length > 0; + return node.callee + && node.callee.type === 'MemberExpression' + && node.callee.property.name === 'createElement' + && node.callee.object + && node.callee.object.name === pragma + && node.arguments.length > 0; } // ------------------------------------------------------------------------------ @@ -72,8 +72,8 @@ module.exports = { }); } - function checkValue(node, value, quoteFn) { - const q = quoteFn || (x => `"${x}"`); + function checkValue(node, value) { + const q = (x) => `"${x}"`; if (!(value in configuration)) { context.report({ node, @@ -100,12 +100,16 @@ module.exports = { return; } - const propValue = getLiteralPropValue(typeProp); - if (!propValue && typeProp.value && typeProp.value.expression) { - checkValue(node, typeProp.value.expression.name, x => `\`${x}\``); - } else { - checkValue(node, propValue); + if (typeProp.value.type === 'JSXExpressionContainer') { + context.report({ + node: typeProp, + message: 'The button type attribute must be specified by a static string' + }); + return; } + + const propValue = getLiteralPropValue(typeProp); + checkValue(node, propValue); }, CallExpression(node) { if (!isCreateElement(node, context)) { @@ -122,7 +126,7 @@ module.exports = { } const props = node.arguments[1].properties; - const typeProp = props.find(prop => prop.key && prop.key.name === 'type'); + const typeProp = props.find((prop) => prop.key && prop.key.name === 'type'); if (!typeProp || typeProp.value.type !== 'Literal') { reportMissing(node); diff --git a/lib/rules/default-props-match-prop-types.js b/lib/rules/default-props-match-prop-types.js index 19d47a0e65..4d096416d8 100644 --- a/lib/rules/default-props-match-prop-types.js +++ b/lib/rules/default-props-match-prop-types.js @@ -80,11 +80,11 @@ module.exports = { // -------------------------------------------------------------------------- return { - 'Program:exit': function () { + 'Program:exit'() { const list = components.list(); // If no defaultProps could be found, we don't report anything. - Object.keys(list).filter(component => list[component].defaultProps).forEach((component) => { + Object.keys(list).filter((component) => list[component].defaultProps).forEach((component) => { reportInvalidDefaultProps( list[component].declaredPropTypes, list[component].defaultProps || {} diff --git a/lib/rules/destructuring-assignment.js b/lib/rules/destructuring-assignment.js index 02999116a2..4716b2aa04 100644 --- a/lib/rules/destructuring-assignment.js +++ b/lib/rules/destructuring-assignment.js @@ -85,14 +85,14 @@ module.exports = { function handleClassUsage(node) { // this.props.Aprop || this.context.aProp || this.state.aState const isPropUsed = ( - node.object.type === 'MemberExpression' && node.object.object.type === 'ThisExpression' && - (node.object.property.name === 'props' || node.object.property.name === 'context' || node.object.property.name === 'state') && - !isAssignmentLHS(node) + node.object.type === 'MemberExpression' && node.object.object.type === 'ThisExpression' + && (node.object.property.name === 'props' || node.object.property.name === 'context' || node.object.property.name === 'state') + && !isAssignmentLHS(node) ); if ( - isPropUsed && configuration === 'always' && - !(ignoreClassFields && isInClassProperty(node)) + isPropUsed && configuration === 'always' + && !(ignoreClassFields && isInClassProperty(node)) ) { context.report({ node, @@ -140,8 +140,8 @@ module.exports = { } if ( - classComponent && destructuringClass && configuration === 'never' && - !(ignoreClassFields && node.parent.type === 'ClassProperty') + classComponent && destructuringClass && configuration === 'never' + && !(ignoreClassFields && node.parent.type === 'ClassProperty') ) { context.report({ node, diff --git a/lib/rules/display-name.js b/lib/rules/display-name.js index ecfeaa7b4f..55ecd9a389 100644 --- a/lib/rules/display-name.js +++ b/lib/rules/display-name.js @@ -71,45 +71,45 @@ module.exports = { */ function hasTranspilerName(node) { const namedObjectAssignment = ( - node.type === 'ObjectExpression' && - node.parent && - node.parent.parent && - node.parent.parent.type === 'AssignmentExpression' && - ( - !node.parent.parent.left.object || - node.parent.parent.left.object.name !== 'module' || - node.parent.parent.left.property.name !== 'exports' + node.type === 'ObjectExpression' + && node.parent + && node.parent.parent + && node.parent.parent.type === 'AssignmentExpression' + && ( + !node.parent.parent.left.object + || node.parent.parent.left.object.name !== 'module' + || node.parent.parent.left.property.name !== 'exports' ) ); const namedObjectDeclaration = ( - node.type === 'ObjectExpression' && - node.parent && - node.parent.parent && - node.parent.parent.type === 'VariableDeclarator' + node.type === 'ObjectExpression' + && node.parent + && node.parent.parent + && node.parent.parent.type === 'VariableDeclarator' ); const namedClass = ( - (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') && - node.id && - node.id.name + (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') + && node.id + && !!node.id.name ); const namedFunctionDeclaration = ( - (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression') && - node.id && - node.id.name + (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression') + && node.id + && !!node.id.name ); const namedFunctionExpression = ( - astUtil.isFunctionLikeExpression(node) && - node.parent && - (node.parent.type === 'VariableDeclarator' || node.parent.method === true) && - (!node.parent.parent || !utils.isES5Component(node.parent.parent)) + astUtil.isFunctionLikeExpression(node) + && node.parent + && (node.parent.type === 'VariableDeclarator' || node.parent.method === true) + && (!node.parent.parent || !utils.isES5Component(node.parent.parent)) ); if ( - namedObjectAssignment || namedObjectDeclaration || - namedClass || - namedFunctionDeclaration || namedFunctionExpression + namedObjectAssignment || namedObjectDeclaration + || namedClass + || namedFunctionDeclaration || namedFunctionExpression ) { return true; } @@ -202,10 +202,35 @@ module.exports = { markDisplayNameAsDeclared(node); }, - 'Program:exit': function () { + CallExpression(node) { + if (!utils.isPragmaComponentWrapper(node)) { + return; + } + + if (node.arguments.length > 0 && astUtil.isFunctionLikeExpression(node.arguments[0])) { + // Skip over React.forwardRef declarations that are embeded within + // a React.memo i.e. React.memo(React.forwardRef(/* ... */)) + // This means that we raise a single error for the call to React.memo + // instead of one for React.memo and one for React.forwardRef + const isWrappedInAnotherPragma = utils.getPragmaComponentWrapper(node); + + if ( + !isWrappedInAnotherPragma + && (ignoreTranspilerName || !hasTranspilerName(node.arguments[0])) + ) { + return; + } + + if (components.get(node)) { + markDisplayNameAsDeclared(node); + } + } + }, + + 'Program:exit'() { const list = components.list(); // Report missing display name for all components - Object.keys(list).filter(component => !list[component].hasDisplayName).forEach((component) => { + Object.keys(list).filter((component) => !list[component].hasDisplayName).forEach((component) => { reportMissingDisplayName(list[component]); }); } diff --git a/lib/rules/forbid-component-props.js b/lib/rules/forbid-component-props.js index 6c11687315..e5d8185961 100644 --- a/lib/rules/forbid-component-props.js +++ b/lib/rules/forbid-component-props.js @@ -46,6 +46,9 @@ module.exports = { items: { type: 'string' } + }, + message: { + type: 'string' } } }] @@ -59,14 +62,18 @@ module.exports = { const configuration = context.options[0] || {}; const forbid = new Map((configuration.forbid || DEFAULTS).map((value) => { const propName = typeof value === 'string' ? value : value.propName; - const whitelist = typeof value === 'string' ? [] : (value.allowedFor || []); - return [propName, whitelist]; + const options = { + allowList: typeof value === 'string' ? [] : (value.allowedFor || []), + message: typeof value === 'string' ? null : value.message + }; + return [propName, options]; })); function isForbidden(prop, tagName) { - const whitelist = forbid.get(prop); + const options = forbid.get(prop); + const allowList = options ? options.allowList : undefined; // if the tagName is undefined (``), we assume it's a forbidden element - return typeof whitelist !== 'undefined' && (typeof tagName === 'undefined' || whitelist.indexOf(tagName) === -1); + return typeof allowList !== 'undefined' && (typeof tagName === 'undefined' || allowList.indexOf(tagName) === -1); } return { @@ -83,9 +90,12 @@ module.exports = { return; } + const customMessage = forbid.get(prop).message; + const errorMessage = customMessage || `Prop \`${prop}\` is forbidden on Components`; + context.report({ node, - message: `Prop \`${prop}\` is forbidden on Components` + message: errorMessage }); } }; diff --git a/lib/rules/forbid-dom-props.js b/lib/rules/forbid-dom-props.js index 08853a412d..d39121187a 100644 --- a/lib/rules/forbid-dom-props.js +++ b/lib/rules/forbid-dom-props.js @@ -32,7 +32,19 @@ module.exports = { forbid: { type: 'array', items: { - type: 'string', + onfOf: [{ + type: 'string' + }, { + type: 'object', + properties: { + propName: { + type: 'string' + }, + message: { + type: 'string' + } + } + }], minLength: 1 }, uniqueItems: true @@ -43,11 +55,17 @@ module.exports = { }, create(context) { - function isForbidden(prop) { - const configuration = context.options[0] || {}; + const configuration = context.options[0] || {}; + const forbid = new Map((configuration.forbid || DEFAULTS).map((value) => { + const propName = typeof value === 'string' ? value : value.propName; + const options = { + message: typeof value === 'string' ? null : value.message + }; + return [propName, options]; + })); - const forbid = configuration.forbid || DEFAULTS; - return forbid.indexOf(prop) >= 0; + function isForbidden(prop) { + return forbid.has(prop); } return { @@ -64,9 +82,12 @@ module.exports = { return; } + const customMessage = forbid.get(prop).message; + const errorMessage = customMessage || `Prop \`${prop}\` is forbidden on DOM Nodes`; + context.report({ node, - message: `Prop \`${prop}\` is forbidden on DOM Nodes` + message: errorMessage }); } }; diff --git a/lib/rules/forbid-elements.js b/lib/rules/forbid-elements.js index bb005172f9..ad333d2bbc 100644 --- a/lib/rules/forbid-elements.js +++ b/lib/rules/forbid-elements.js @@ -72,11 +72,11 @@ module.exports = { } function isValidCreateElement(node) { - return node.callee && - node.callee.type === 'MemberExpression' && - node.callee.object.name === 'React' && - node.callee.property.name === 'createElement' && - node.arguments.length > 0; + return node.callee + && node.callee.type === 'MemberExpression' + && node.callee.object.name === 'React' + && node.callee.property.name === 'createElement' + && node.arguments.length > 0; } function reportIfForbidden(element, node) { diff --git a/lib/rules/forbid-foreign-prop-types.js b/lib/rules/forbid-foreign-prop-types.js index ad23d508ef..630e21a634 100644 --- a/lib/rules/forbid-foreign-prop-types.js +++ b/lib/rules/forbid-foreign-prop-types.js @@ -70,10 +70,10 @@ module.exports = { const assignmentExpression = findParentAssignmentExpression(node); if ( - assignmentExpression && - assignmentExpression.left && - assignmentExpression.left.property && - assignmentExpression.left.property.name === 'propTypes' + assignmentExpression + && assignmentExpression.left + && assignmentExpression.left.property + && assignmentExpression.left.property.name === 'propTypes' ) { return true; } @@ -81,9 +81,9 @@ module.exports = { const classProperty = findParentClassProperty(node); if ( - classProperty && - classProperty.key && - classProperty.key.name === 'propTypes' + classProperty + && classProperty.key + && classProperty.key.name === 'propTypes' ) { return true; } @@ -93,18 +93,18 @@ module.exports = { return { MemberExpression(node) { if ( - node.property && - ( - !node.computed && - node.property.type === 'Identifier' && - node.property.name === 'propTypes' && - !ast.isAssignmentLHS(node) && - !isAllowedAssignment(node) + node.property + && ( + !node.computed + && node.property.type === 'Identifier' + && node.property.name === 'propTypes' + && !ast.isAssignmentLHS(node) + && !isAllowedAssignment(node) ) || ( - (node.property.type === 'Literal' || node.property.type === 'JSXText') && - node.property.value === 'propTypes' && - !ast.isAssignmentLHS(node) && - !isAllowedAssignment(node) + (node.property.type === 'Literal' || node.property.type === 'JSXText') + && node.property.value === 'propTypes' + && !ast.isAssignmentLHS(node) + && !isAllowedAssignment(node) ) ) { context.report({ @@ -115,7 +115,7 @@ module.exports = { }, ObjectPattern(node) { - const propTypesNode = node.properties.find(property => property.type === 'Property' && property.key.name === 'propTypes'); + const propTypesNode = node.properties.find((property) => property.type === 'Property' && property.key.name === 'propTypes'); if (propTypesNode) { context.report({ diff --git a/lib/rules/forbid-prop-types.js b/lib/rules/forbid-prop-types.js index 04b61bf734..a5bc7a4b02 100644 --- a/lib/rules/forbid-prop-types.js +++ b/lib/rules/forbid-prop-types.js @@ -86,16 +86,16 @@ module.exports = { let target; let value = declaration.value; if ( - value.type === 'MemberExpression' && - value.property && - value.property.name && - value.property.name === 'isRequired' + value.type === 'MemberExpression' + && value.property + && value.property.name + && value.property.name === 'isRequired' ) { value = value.object; } if ( - value.type === 'CallExpression' && - value.callee.type === 'MemberExpression' + value.type === 'CallExpression' + && value.callee.type === 'MemberExpression' ) { value = value.callee; } @@ -140,9 +140,9 @@ module.exports = { return { ClassProperty(node) { if ( - !propsUtil.isPropTypesDeclaration(node) && - !shouldCheckContextTypes(node) && - !shouldCheckChildContextTypes(node) + !propsUtil.isPropTypesDeclaration(node) + && !shouldCheckContextTypes(node) + && !shouldCheckChildContextTypes(node) ) { return; } @@ -151,9 +151,9 @@ module.exports = { MemberExpression(node) { if ( - !propsUtil.isPropTypesDeclaration(node) && - !shouldCheckContextTypes(node) && - !shouldCheckChildContextTypes(node) + !propsUtil.isPropTypesDeclaration(node) + && !shouldCheckContextTypes(node) + && !shouldCheckChildContextTypes(node) ) { return; } @@ -163,9 +163,9 @@ module.exports = { MethodDefinition(node) { if ( - !propsUtil.isPropTypesDeclaration(node) && - !shouldCheckContextTypes(node) && - !shouldCheckChildContextTypes(node) + !propsUtil.isPropTypesDeclaration(node) + && !shouldCheckContextTypes(node) + && !shouldCheckChildContextTypes(node) ) { return; } @@ -184,9 +184,9 @@ module.exports = { } if ( - !propsUtil.isPropTypesDeclaration(property) && - !shouldCheckContextTypes(property) && - !shouldCheckChildContextTypes(property) + !propsUtil.isPropTypesDeclaration(property) + && !shouldCheckContextTypes(property) + && !shouldCheckChildContextTypes(property) ) { return; } diff --git a/lib/rules/function-component-definition.js b/lib/rules/function-component-definition.js new file mode 100644 index 0000000000..3833188a81 --- /dev/null +++ b/lib/rules/function-component-definition.js @@ -0,0 +1,193 @@ +/** + * @fileoverview Standardize the way function component get defined + * @author Stefan Wullems + */ + +'use strict'; + +const Components = require('../util/Components'); +const docsUrl = require('../util/docsUrl'); + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +function buildFunction(template, parts) { + return Object.keys(parts) + .reduce((acc, key) => acc.replace(`{${key}}`, parts[key] || ''), template); +} + +const NAMED_FUNCTION_TEMPLATES = { + 'function-declaration': 'function {name}{typeParams}({params}){returnType} {body}', + 'arrow-function': 'var {name}{typeAnnotation} = {typeParams}({params}){returnType} => {body}', + 'function-expression': 'var {name}{typeAnnotation} = function{typeParams}({params}){returnType} {body}' +}; + +const UNNAMED_FUNCTION_TEMPLATES = { + 'function-expression': 'function{typeParams}({params}){returnType} {body}', + 'arrow-function': '{typeParams}({params}){returnType} => {body}' +}; + +const ERROR_MESSAGES = { + 'function-declaration': 'Function component is not a function declaration', + 'function-expression': 'Function component is not a function expression', + 'arrow-function': 'Function component is not an arrow function' +}; + +function hasOneUnconstrainedTypeParam(node) { + if (node.typeParameters) { + return node.typeParameters.params.length === 1 && !node.typeParameters.params[0].constraint; + } + + return false; +} + +function hasName(node) { + return node.type === 'FunctionDeclaration' || node.parent.type === 'VariableDeclarator'; +} + +function getNodeText(prop, source) { + if (!prop) return null; + return source.slice(prop.range[0], prop.range[1]); +} + +function getName(node) { + if (node.type === 'FunctionDeclaration') { + return node.id.name; + } + + if (node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression') { + return hasName(node) && node.parent.id.name; + } +} + +function getParams(node, source) { + if (node.params.length === 0) return null; + return source.slice(node.params[0].range[0], node.params[node.params.length - 1].range[1]); +} + +function getBody(node, source) { + const range = node.body.range; + + if (node.body.type !== 'BlockStatement') { + return [ + '{', + ` return ${source.slice(range[0], range[1])}`, + '}' + ].join('\n'); + } + + return source.slice(range[0], range[1]); +} + +function getTypeAnnotation(node, source) { + if (!hasName(node) || node.type === 'FunctionDeclaration') return; + + if (node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression') { + return getNodeText(node.parent.id.typeAnnotation, source); + } +} + +function isUnfixableBecauseOfExport(node) { + return node.type === 'FunctionDeclaration' && node.parent && node.parent.type === 'ExportDefaultDeclaration'; +} + +function isFunctionExpressionWithName(node) { + return node.type === 'FunctionExpression' && node.id && node.id.name; +} + +module.exports = { + meta: { + docs: { + description: 'Standardize the way function component get defined', + category: 'Stylistic issues', + recommended: false, + url: docsUrl('function-component-definition') + }, + fixable: 'code', + + schema: [{ + type: 'object', + properties: { + namedComponents: { + enum: ['function-declaration', 'arrow-function', 'function-expression'] + }, + unnamedComponents: { + enum: ['arrow-function', 'function-expression'] + } + } + }] + }, + + create: Components.detect((context, components) => { + const configuration = context.options[0] || {}; + + const namedConfig = configuration.namedComponents || 'function-declaration'; + const unnamedConfig = configuration.unnamedComponents || 'function-expression'; + + function getFixer(node, options) { + const sourceCode = context.getSourceCode(); + const source = sourceCode.getText(); + + const typeAnnotation = getTypeAnnotation(node, source); + + if (options.type === 'function-declaration' && typeAnnotation) return; + if (options.type === 'arrow-function' && hasOneUnconstrainedTypeParam(node)) return; + if (isUnfixableBecauseOfExport(node)) return; + if (isFunctionExpressionWithName(node)) return; + + return (fixer) => fixer.replaceTextRange(options.range, buildFunction(options.template, { + typeAnnotation, + typeParams: getNodeText(node.typeParameters, source), + params: getParams(node, source), + returnType: getNodeText(node.returnType, source), + body: getBody(node, source), + name: getName(node) + })); + } + + function report(node, options) { + context.report({ + node, + message: options.message, + fix: getFixer(node, options.fixerOptions) + }); + } + + function validate(node, functionType) { + if (!components.get(node)) return; + if (hasName(node) && namedConfig !== functionType) { + report(node, { + message: ERROR_MESSAGES[namedConfig], + fixerOptions: { + type: namedConfig, + template: NAMED_FUNCTION_TEMPLATES[namedConfig], + range: node.type === 'FunctionDeclaration' + ? node.range + : node.parent.parent.range + } + }); + } + if (!hasName(node) && unnamedConfig !== functionType) { + report(node, { + message: ERROR_MESSAGES[unnamedConfig], + fixerOptions: { + type: unnamedConfig, + template: UNNAMED_FUNCTION_TEMPLATES[unnamedConfig], + range: node.range + } + }); + } + } + + // -------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- + + return { + FunctionDeclaration(node) { validate(node, 'function-declaration'); }, + ArrowFunctionExpression(node) { validate(node, 'arrow-function'); }, + FunctionExpression(node) { validate(node, 'function-expression'); } + }; + }) +}; diff --git a/lib/rules/jsx-boolean-value.js b/lib/rules/jsx-boolean-value.js index 50768b5875..be3cb57d5a 100644 --- a/lib/rules/jsx-boolean-value.js +++ b/lib/rules/jsx-boolean-value.js @@ -23,7 +23,7 @@ const NEVER = 'never'; const errorData = new WeakMap(); function getErrorData(exceptions) { if (!errorData.has(exceptions)) { - const exceptionProps = Array.from(exceptions, name => `\`${name}\``).join(', '); + const exceptionProps = Array.from(exceptions, (name) => `\`${name}\``).join(', '); const exceptionsMessage = exceptions.size > 0 ? ` for the following props: ${exceptionProps}` : ''; errorData.set(exceptions, {exceptionsMessage}); } diff --git a/lib/rules/jsx-child-element-spacing.js b/lib/rules/jsx-child-element-spacing.js index 0d0756fef0..fcde13bc74 100644 --- a/lib/rules/jsx-child-element-spacing.js +++ b/lib/rules/jsx-child-element-spacing.js @@ -45,7 +45,7 @@ module.exports = { recommended: false, url: docsUrl('jsx-child-element-spacing') }, - fixable: false, + fixable: null, schema: [ { type: 'object', @@ -59,16 +59,16 @@ module.exports = { const TEXT_FOLLOWING_ELEMENT_PATTERN = /^\s*\n\s*\S/; const TEXT_PRECEDING_ELEMENT_PATTERN = /\S\s*\n\s*$/; - const elementName = node => ( - node.openingElement && - node.openingElement.name && - node.openingElement.name.type === 'JSXIdentifier' && - node.openingElement.name.name + const elementName = (node) => ( + node.openingElement + && node.openingElement.name + && node.openingElement.name.type === 'JSXIdentifier' + && node.openingElement.name.name ); - const isInlineElement = node => ( - node.type === 'JSXElement' && - INLINE_ELEMENTS.has(elementName(node)) + const isInlineElement = (node) => ( + node.type === 'JSXElement' + && INLINE_ELEMENTS.has(elementName(node)) ); const handleJSX = (node) => { @@ -76,11 +76,11 @@ module.exports = { let child = null; (node.children.concat([null])).forEach((nextChild) => { if ( - (lastChild || nextChild) && - (!lastChild || isInlineElement(lastChild)) && - (child && (child.type === 'Literal' || child.type === 'JSXText')) && - (!nextChild || isInlineElement(nextChild)) && - true + (lastChild || nextChild) + && (!lastChild || isInlineElement(lastChild)) + && (child && (child.type === 'Literal' || child.type === 'JSXText')) + && (!nextChild || isInlineElement(nextChild)) + && true ) { if (lastChild && child.value.match(TEXT_FOLLOWING_ELEMENT_PATTERN)) { context.report({ diff --git a/lib/rules/jsx-closing-bracket-location.js b/lib/rules/jsx-closing-bracket-location.js index a84bdfd699..b49abdc0bf 100644 --- a/lib/rules/jsx-closing-bracket-location.js +++ b/lib/rules/jsx-closing-bracket-location.js @@ -236,7 +236,7 @@ module.exports = { lastAttributeNode[getOpeningElementId(node.parent)] = node; }, - 'JSXOpeningElement:exit': function (node) { + 'JSXOpeningElement:exit'(node) { const attributeNode = lastAttributeNode[getOpeningElementId(node)]; const cachedLastAttributeEndPos = attributeNode ? attributeNode.range[1] : null; let expectedNextLine; @@ -251,8 +251,8 @@ module.exports = { const correctColumn = getCorrectColumn(tokens, expectedLocation); if (correctColumn !== null) { - expectedNextLine = tokens.lastProp && - (tokens.lastProp.lastLine === tokens.closing.line); + expectedNextLine = tokens.lastProp + && (tokens.lastProp.lastLine === tokens.closing.line); data.details = ` (expected column ${correctColumn + 1}${expectedNextLine ? ' on the next line)' : ')'}`; } diff --git a/lib/rules/jsx-curly-brace-presence.js b/lib/rules/jsx-curly-brace-presence.js index 5bddc5b98f..f3c3f7e25d 100755 --- a/lib/rules/jsx-curly-brace-presence.js +++ b/lib/rules/jsx-curly-brace-presence.js @@ -6,6 +6,8 @@ 'use strict'; +const arrayIncludes = require('array-includes'); + const docsUrl = require('../util/docsUrl'); const jsxUtil = require('../util/jsx'); @@ -32,8 +34,8 @@ module.exports = { meta: { docs: { description: - 'Disallow unnecessary JSX expressions when literals alone are sufficient ' + - 'or enfore JSX expressions on literals in JSX children or attributes', + 'Disallow unnecessary JSX expressions when literals alone are sufficient ' + + 'or enfore JSX expressions on literals in JSX children or attributes', category: 'Stylistic Issues', recommended: false, url: docsUrl('jsx-curly-brace-presence') @@ -60,21 +62,26 @@ module.exports = { }, create(context) { + const HTML_ENTITY_REGEX = () => /&[A-Za-z\d#]+;/g; const ruleOptions = context.options[0]; - const userConfig = typeof ruleOptions === 'string' ? - {props: ruleOptions, children: ruleOptions} : - Object.assign({}, DEFAULT_CONFIG, ruleOptions); + const userConfig = typeof ruleOptions === 'string' + ? {props: ruleOptions, children: ruleOptions} + : Object.assign({}, DEFAULT_CONFIG, ruleOptions); function containsLineTerminators(rawStringValue) { return /[\n\r\u2028\u2029]/.test(rawStringValue); } function containsBackslash(rawStringValue) { - return rawStringValue.includes('\\'); + return arrayIncludes(rawStringValue, '\\'); } function containsHTMLEntity(rawStringValue) { - return /&[A-Za-z\d#]+;/.test(rawStringValue); + return HTML_ENTITY_REGEX().test(rawStringValue); + } + + function containsOnlyHtmlEntities(rawStringValue) { + return rawStringValue.replace(HTML_ENTITY_REGEX(), '').trim() === ''; } function containsDisallowedJSXTextChars(rawStringValue) { @@ -95,20 +102,56 @@ module.exports = { function needToEscapeCharacterForJSX(raw) { return ( - containsBackslash(raw) || - containsHTMLEntity(raw) || - containsDisallowedJSXTextChars(raw) + containsBackslash(raw) + || containsHTMLEntity(raw) + || containsDisallowedJSXTextChars(raw) ); } function containsWhitespaceExpression(child) { if (child.type === 'JSXExpressionContainer') { const value = child.expression.value; - return value ? !(/\S/.test(value)) : false; + return value ? jsxUtil.isWhiteSpaces(value) : false; } return false; } + function isLineBreak(text) { + return containsLineTerminators(text) && text.trim() === ''; + } + + function wrapNonHTMLEntities(text) { + const HTML_ENTITY = ''; + const withCurlyBraces = text.split(HTML_ENTITY_REGEX()).map((word) => ( + word === '' ? '' : `{${JSON.stringify(word)}}` + )).join(HTML_ENTITY); + + const htmlEntities = text.match(HTML_ENTITY_REGEX()); + return htmlEntities.reduce((acc, htmlEntitiy) => ( + acc.replace(HTML_ENTITY, htmlEntitiy) + ), withCurlyBraces); + } + + function wrapWithCurlyBraces(rawText) { + if (!containsLineTerminators(rawText)) { + return `{${JSON.stringify(rawText)}}`; + } + + return rawText.split('\n').map((line) => { + if (line.trim() === '') { + return line; + } + const firstCharIndex = line.search(/[^\s]/); + const leftWhitespace = line.slice(0, firstCharIndex); + const text = line.slice(firstCharIndex); + + if (containsHTMLEntity(line)) { + return `${leftWhitespace}${wrapNonHTMLEntities(text)}`; + } + return `${leftWhitespace}{${JSON.stringify(text)}}`; + }).join('\n'); + } + /** * Report and fix an unnecessary curly brace violation on a node * @param {ASTNode} JSXExpressionNode - The AST node with an unnecessary JSX expression @@ -124,13 +167,17 @@ module.exports = { let textToReplace; if (parentType === 'JSXAttribute') { - textToReplace = `"${expressionType === 'TemplateLiteral' ? - expression.quasis[0].value.raw : - expression.raw.substring(1, expression.raw.length - 1) + textToReplace = `"${expressionType === 'TemplateLiteral' + ? expression.quasis[0].value.raw + : expression.raw.substring(1, expression.raw.length - 1) }"`; + } else if (jsxUtil.isJSX(expression)) { + const sourceCode = context.getSourceCode(); + + textToReplace = sourceCode.getText(expression); } else { - textToReplace = expressionType === 'TemplateLiteral' ? - expression.quasis[0].value.cooked : expression.value; + textToReplace = expressionType === 'TemplateLiteral' + ? expression.quasis[0].value.cooked : expression.value; } return fixer.replaceText(JSXExpressionNode, textToReplace); @@ -147,23 +194,36 @@ module.exports = { // by either using the real character or the unicode equivalent. // If it contains any line terminator character, bail out as well. if ( - containsHTMLEntity(literalNode.raw) || - containsLineTerminators(literalNode.raw) + containsOnlyHtmlEntities(literalNode.raw) + || (literalNode.parent.type === 'JSXAttribute' && containsLineTerminators(literalNode.raw)) + || isLineBreak(literalNode.raw) ) { return null; } - const expression = literalNode.parent.type === 'JSXAttribute' ? - `{"${escapeDoubleQuotes(escapeBackslashes( + const expression = literalNode.parent.type === 'JSXAttribute' + ? `{"${escapeDoubleQuotes(escapeBackslashes( literalNode.raw.substring(1, literalNode.raw.length - 1) - ))}"}` : - `{${JSON.stringify(literalNode.value)}}`; + ))}"}` + : wrapWithCurlyBraces(literalNode.raw); return fixer.replaceText(literalNode, expression); } }); } + function isWhiteSpaceLiteral(node) { + return node.type && node.type === 'Literal' && node.value && jsxUtil.isWhiteSpaces(node.value); + } + + function isStringWithTrailingWhiteSpaces(value) { + return /^\s|\s$/.test(value); + } + + function isLiteralWithTrailingWhiteSpaces(node) { + return node.type && node.type === 'Literal' && node.value && isStringWithTrailingWhiteSpaces(node.value); + } + // Bail out if there is any character that needs to be escaped in JSX // because escaping decreases readiblity and the original code may be more // readible anyway or intentional for other specific reasons @@ -172,50 +232,107 @@ module.exports = { const expressionType = expression.type; if ( - (expressionType === 'Literal' || expressionType === 'JSXText') && - typeof expression.value === 'string' && - !needToEscapeCharacterForJSX(expression.raw) && ( - jsxUtil.isJSX(JSXExpressionNode.parent) || - !containsQuoteCharacters(expression.value) + (expressionType === 'Literal' || expressionType === 'JSXText') + && typeof expression.value === 'string' + && ( + (JSXExpressionNode.parent.type === 'JSXAttribute' && !isWhiteSpaceLiteral(expression)) + || !isLiteralWithTrailingWhiteSpaces(expression) + ) + && !needToEscapeCharacterForJSX(expression.raw) && ( + jsxUtil.isJSX(JSXExpressionNode.parent) + || !containsQuoteCharacters(expression.value) ) ) { reportUnnecessaryCurly(JSXExpressionNode); } else if ( - expressionType === 'TemplateLiteral' && - expression.expressions.length === 0 && - expression.quasis[0].value.raw.indexOf('\n') === -1 && - !needToEscapeCharacterForJSX(expression.quasis[0].value.raw) && ( - jsxUtil.isJSX(JSXExpressionNode.parent) || - !containsQuoteCharacters(expression.quasis[0].value.cooked) + expressionType === 'TemplateLiteral' + && expression.expressions.length === 0 + && expression.quasis[0].value.raw.indexOf('\n') === -1 + && !isStringWithTrailingWhiteSpaces(expression.quasis[0].value.raw) + && !needToEscapeCharacterForJSX(expression.quasis[0].value.raw) && ( + jsxUtil.isJSX(JSXExpressionNode.parent) + || !containsQuoteCharacters(expression.quasis[0].value.cooked) ) ) { reportUnnecessaryCurly(JSXExpressionNode); + } else if (jsxUtil.isJSX(expression)) { + reportUnnecessaryCurly(JSXExpressionNode); } } function areRuleConditionsSatisfied(parent, config, ruleCondition) { return ( - parent.type === 'JSXAttribute' && - typeof config.props === 'string' && - config.props === ruleCondition + parent.type === 'JSXAttribute' + && typeof config.props === 'string' + && config.props === ruleCondition ) || ( - jsxUtil.isJSX(parent) && - typeof config.children === 'string' && - config.children === ruleCondition + jsxUtil.isJSX(parent) + && typeof config.children === 'string' + && config.children === ruleCondition ); } - function shouldCheckForUnnecessaryCurly(parent, config) { - // If there are more than one JSX child, there is no need to check for - // unnecessary curly braces. - if (jsxUtil.isJSX(parent) && parent.children.length !== 1) { + function getAdjacentSiblings(node, children) { + for (let i = 1; i < children.length - 1; i++) { + const child = children[i]; + if (node === child) { + return [children[i - 1], children[i + 1]]; + } + } + if (node === children[0] && children[1]) { + return [children[1]]; + } + if (node === children[children.length - 1] && children[children.length - 2]) { + return [children[children.length - 2]]; + } + return []; + } + + function hasAdjacentJsxExpressionContainers(node, children) { + if (!children) { + return false; + } + const childrenExcludingWhitespaceLiteral = children.filter((child) => !isWhiteSpaceLiteral(child)); + const adjSiblings = getAdjacentSiblings(node, childrenExcludingWhitespaceLiteral); + + return adjSiblings.some((x) => x.type && x.type === 'JSXExpressionContainer'); + } + function hasAdjacentJsx(node, children) { + if (!children) { + return false; + } + const childrenExcludingWhitespaceLiteral = children.filter((child) => !isWhiteSpaceLiteral(child)); + const adjSiblings = getAdjacentSiblings(node, childrenExcludingWhitespaceLiteral); + + return adjSiblings.some((x) => x.type && arrayIncludes(['JSXExpressionContainer', 'JSXElement'], x.type)); + } + function shouldCheckForUnnecessaryCurly(parent, node, config) { + // Bail out if the parent is a JSXAttribute & its contents aren't + // StringLiteral or TemplateLiteral since e.g + // } prop2={...} /> + + if ( + parent.type && parent.type === 'JSXAttribute' + && (node.expression && node.expression.type + && node.expression.type !== 'Literal' + && node.expression.type !== 'StringLiteral' + && node.expression.type !== 'TemplateLiteral') + ) { return false; } + // If there are adjacent `JsxExpressionContainer` then there is no need, + // to check for unnecessary curly braces. + if (jsxUtil.isJSX(parent) && hasAdjacentJsxExpressionContainers(node, parent.children)) { + return false; + } + if (containsWhitespaceExpression(node) && hasAdjacentJsx(node, parent.children)) { + return false; + } if ( - parent.children && - parent.children.length === 1 && - containsWhitespaceExpression(parent.children[0]) + parent.children + && parent.children.length === 1 + && containsWhitespaceExpression(node) ) { return false; } @@ -223,11 +340,18 @@ module.exports = { return areRuleConditionsSatisfied(parent, config, OPTION_NEVER); } - function shouldCheckForMissingCurly(parent, config) { + function shouldCheckForMissingCurly(node, config) { + if ( + isLineBreak(node.raw) + || containsOnlyHtmlEntities(node.raw) + ) { + return false; + } + const parent = node.parent; if ( - parent.children && - parent.children.length === 1 && - containsWhitespaceExpression(parent.children[0]) + parent.children + && parent.children.length === 1 + && containsWhitespaceExpression(parent.children[0]) ) { return false; } @@ -241,13 +365,13 @@ module.exports = { return { JSXExpressionContainer: (node) => { - if (shouldCheckForUnnecessaryCurly(node.parent, userConfig)) { + if (shouldCheckForUnnecessaryCurly(node.parent, node, userConfig)) { lintUnnecessaryCurly(node); } }, 'Literal, JSXText': (node) => { - if (shouldCheckForMissingCurly(node.parent, userConfig)) { + if (shouldCheckForMissingCurly(node, userConfig)) { reportMissingCurly(node); } } diff --git a/lib/rules/jsx-curly-newline.js b/lib/rules/jsx-curly-newline.js index 2c67f58e9d..57448fb9fa 100644 --- a/lib/rules/jsx-curly-newline.js +++ b/lib/rules/jsx-curly-newline.js @@ -38,7 +38,7 @@ module.exports = { type: 'layout', docs: { - description: 'enforce consistent line breaks inside jsx curly', + description: 'Enforce consistent line breaks inside jsx curly', category: 'Stylistic Issues', recommended: false, url: docsUrl('jsx-curly-newline') @@ -131,16 +131,16 @@ module.exports = { return sourceCode .getText() .slice(leftCurly.range[1], tokenAfterLeftCurly.range[0]) - .trim() ? - null : // If there is a comment between the { and the first element, don't do a fix. - fixer.removeRange([leftCurly.range[1], tokenAfterLeftCurly.range[0]]); + .trim() + ? null // If there is a comment between the { and the first element, don't do a fix. + : fixer.removeRange([leftCurly.range[1], tokenAfterLeftCurly.range[0]]); } }); } else if (!hasLeftNewline && needsNewlines) { context.report({ node: leftCurly, messageId: 'expectedAfter', - fix: fixer => fixer.insertTextAfter(leftCurly, '\n') + fix: (fixer) => fixer.insertTextAfter(leftCurly, '\n') }); } @@ -152,9 +152,9 @@ module.exports = { return sourceCode .getText() .slice(tokenBeforeRightCurly.range[1], rightCurly.range[0]) - .trim() ? - null : // If there is a comment between the last element and the }, don't do a fix. - fixer.removeRange([ + .trim() + ? null // If there is a comment between the last element and the }, don't do a fix. + : fixer.removeRange([ tokenBeforeRightCurly.range[1], rightCurly.range[0] ]); @@ -164,7 +164,7 @@ module.exports = { context.report({ node: rightCurly, messageId: 'expectedBefore', - fix: fixer => fixer.insertTextBefore(rightCurly, '\n') + fix: (fixer) => fixer.insertTextBefore(rightCurly, '\n') }); } } diff --git a/lib/rules/jsx-curly-spacing.js b/lib/rules/jsx-curly-spacing.js index a53f24a740..b587d2fe52 100644 --- a/lib/rules/jsx-curly-spacing.js +++ b/lib/rules/jsx-curly-spacing.js @@ -244,7 +244,7 @@ module.exports = { // Take comments into consideration to narrow the fix range to what is actually affected. (See #1414) if (nextComment.length > 0) { - return fixByTrimmingWhitespace(fixer, token.range[1], Math.min(nextToken.range[0], nextComment[0].start), 'start'); + return fixByTrimmingWhitespace(fixer, token.range[1], Math.min(nextToken.range[0], nextComment[0].range[0]), 'start'); } return fixByTrimmingWhitespace(fixer, token.range[1], nextToken.range[0], 'start'); @@ -279,7 +279,7 @@ module.exports = { // Take comments into consideration to narrow the fix range to what is actually affected. (See #1414) if (previousComment.length > 0) { - return fixByTrimmingWhitespace(fixer, Math.max(previousToken.range[1], previousComment[0].end), token.range[0], 'end'); + return fixByTrimmingWhitespace(fixer, Math.max(previousToken.range[1], previousComment[0].range[1]), token.range[0], 'end'); } return fixByTrimmingWhitespace(fixer, previousToken.range[1], token.range[0], 'end'); diff --git a/lib/rules/jsx-filename-extension.js b/lib/rules/jsx-filename-extension.js index d636b068ae..fe433d6ecb 100644 --- a/lib/rules/jsx-filename-extension.js +++ b/lib/rules/jsx-filename-extension.js @@ -62,7 +62,7 @@ module.exports = { } const allowedExtensions = getExtensionsConfig(); - const isAllowedExtension = allowedExtensions.some(extension => filename.slice(-extension.length) === extension); + const isAllowedExtension = allowedExtensions.some((extension) => filename.slice(-extension.length) === extension); if (isAllowedExtension) { return; @@ -80,7 +80,7 @@ module.exports = { JSXElement: handleJSX, JSXFragment: handleJSX, - 'Program:exit': function () { + 'Program:exit'() { if (!invalidNode) { return; } diff --git a/lib/rules/jsx-first-prop-new-line.js b/lib/rules/jsx-first-prop-new-line.js index 30b4a574be..339d761025 100644 --- a/lib/rules/jsx-first-prop-new-line.js +++ b/lib/rules/jsx-first-prop-new-line.js @@ -36,9 +36,9 @@ module.exports = { return { JSXOpeningElement(node) { if ( - (configuration === 'multiline' && isMultilineJSX(node)) || - (configuration === 'multiline-multiprop' && isMultilineJSX(node) && node.attributes.length > 1) || - (configuration === 'always') + (configuration === 'multiline' && isMultilineJSX(node)) + || (configuration === 'multiline-multiprop' && isMultilineJSX(node) && node.attributes.length > 1) + || (configuration === 'always') ) { node.attributes.some((decl) => { if (decl.loc.start.line === node.loc.start.line) { diff --git a/lib/rules/jsx-fragments.js b/lib/rules/jsx-fragments.js index 353446c37d..020b079624 100644 --- a/lib/rules/jsx-fragments.js +++ b/lib/rules/jsx-fragments.js @@ -47,8 +47,8 @@ module.exports = { if (!versionUtil.testReactVersion(context, '16.2.0')) { context.report({ node, - message: 'Fragments are only supported starting from React v16.2. ' + - 'Please disable the `react/jsx-fragments` rule in ESLint settings or upgrade your version of React.' + message: 'Fragments are only supported starting from React v16.2. ' + + 'Please disable the `react/jsx-fragments` rule in ESLint settings or upgrade your version of React.' }); return true; } @@ -58,12 +58,12 @@ module.exports = { function getFixerToLong(jsxFragment) { const sourceCode = context.getSourceCode(); - return function (fixer) { + return function fix(fixer) { let source = sourceCode.getText(); source = replaceNode(source, jsxFragment.closingFragment, closeFragLong); source = replaceNode(source, jsxFragment.openingFragment, openFragLong); - const lengthDiff = openFragLong.length - sourceCode.getText(jsxFragment.openingFragment).length + - closeFragLong.length - sourceCode.getText(jsxFragment.closingFragment).length; + const lengthDiff = openFragLong.length - sourceCode.getText(jsxFragment.openingFragment).length + + closeFragLong.length - sourceCode.getText(jsxFragment.closingFragment).length; const range = jsxFragment.range; return fixer.replaceTextRange(range, source.slice(range[0], range[1] + lengthDiff)); }; @@ -71,18 +71,18 @@ module.exports = { function getFixerToShort(jsxElement) { const sourceCode = context.getSourceCode(); - return function (fixer) { + return function fix(fixer) { let source = sourceCode.getText(); let lengthDiff; if (jsxElement.closingElement) { source = replaceNode(source, jsxElement.closingElement, closeFragShort); source = replaceNode(source, jsxElement.openingElement, openFragShort); - lengthDiff = sourceCode.getText(jsxElement.openingElement).length - openFragShort.length + - sourceCode.getText(jsxElement.closingElement).length - closeFragShort.length; + lengthDiff = sourceCode.getText(jsxElement.openingElement).length - openFragShort.length + + sourceCode.getText(jsxElement.closingElement).length - closeFragShort.length; } else { source = replaceNode(source, jsxElement.openingElement, `${openFragShort}${closeFragShort}`); - lengthDiff = sourceCode.getText(jsxElement.openingElement).length - openFragShort.length - - closeFragShort.length; + lengthDiff = sourceCode.getText(jsxElement.openingElement).length - openFragShort.length + - closeFragShort.length; } const range = jsxElement.range; @@ -103,22 +103,22 @@ module.exports = { // const Fragment = React.Fragment; if ( - variableInit.type === 'MemberExpression' && - variableInit.object.type === 'Identifier' && - variableInit.object.name === reactPragma && - variableInit.property.type === 'Identifier' && - variableInit.property.name === fragmentPragma + variableInit.type === 'MemberExpression' + && variableInit.object.type === 'Identifier' + && variableInit.object.name === reactPragma + && variableInit.property.type === 'Identifier' + && variableInit.property.name === fragmentPragma ) { return true; } // const { Fragment } = require('react'); if ( - variableInit.callee && - variableInit.callee.name === 'require' && - variableInit.arguments && - variableInit.arguments[0] && - variableInit.arguments[0].value === 'react' + variableInit.callee + && variableInit.callee.name === 'require' + && variableInit.arguments + && variableInit.arguments[0] + && variableInit.arguments[0].value === 'react' ) { return true; } @@ -164,7 +164,7 @@ module.exports = { } }, - 'Program:exit': function () { + 'Program:exit'() { jsxElements.forEach((node) => { const openingEl = node.openingElement; const elName = elementType(openingEl); diff --git a/lib/rules/jsx-handler-names.js b/lib/rules/jsx-handler-names.js index 42ef5c02c5..fa1afdb68e 100644 --- a/lib/rules/jsx-handler-names.js +++ b/lib/rules/jsx-handler-names.js @@ -21,30 +21,74 @@ module.exports = { }, schema: [{ - type: 'object', - properties: { - eventHandlerPrefix: { - type: 'string' - }, - eventHandlerPropPrefix: { - type: 'string' + anyOf: [ + { + type: 'object', + properties: { + eventHandlerPrefix: {type: 'string'}, + eventHandlerPropPrefix: {type: 'string'}, + checkLocalVariables: {type: 'boolean'} + }, + additionalProperties: false + }, { + type: 'object', + properties: { + eventHandlerPrefix: {type: 'string'}, + eventHandlerPropPrefix: { + type: 'boolean', + enum: [false] + }, + checkLocalVariables: {type: 'boolean'} + }, + additionalProperties: false + }, { + type: 'object', + properties: { + eventHandlerPrefix: { + type: 'boolean', + enum: [false] + }, + eventHandlerPropPrefix: {type: 'string'}, + checkLocalVariables: {type: 'boolean'} + }, + additionalProperties: false + }, { + type: 'object', + properties: { + checkLocalVariables: {type: 'boolean'} + }, + additionalProperties: false } - }, - additionalProperties: false + ] }] }, create(context) { + function isPrefixDisabled(prefix) { + return prefix === false; + } + const configuration = context.options[0] || {}; - const eventHandlerPrefix = configuration.eventHandlerPrefix || 'handle'; - const eventHandlerPropPrefix = configuration.eventHandlerPropPrefix || 'on'; - const EVENT_HANDLER_REGEX = new RegExp(`^((props\\.${eventHandlerPropPrefix})|((.*\\.)?${eventHandlerPrefix}))[A-Z].*$`); - const PROP_EVENT_HANDLER_REGEX = new RegExp(`^(${eventHandlerPropPrefix}[A-Z].*|ref)$`); + const eventHandlerPrefix = isPrefixDisabled(configuration.eventHandlerPrefix) + ? null + : configuration.eventHandlerPrefix || 'handle'; + const eventHandlerPropPrefix = isPrefixDisabled(configuration.eventHandlerPropPrefix) + ? null + : configuration.eventHandlerPropPrefix || 'on'; + + const EVENT_HANDLER_REGEX = !eventHandlerPrefix + ? null + : new RegExp(`^((props\\.${eventHandlerPropPrefix || ''})|((.*\\.)?${eventHandlerPrefix}))[A-Z].*$`); + const PROP_EVENT_HANDLER_REGEX = !eventHandlerPropPrefix + ? null + : new RegExp(`^(${eventHandlerPropPrefix}[A-Z].*|ref)$`); + + const checkLocal = !!configuration.checkLocalVariables; return { JSXAttribute(node) { - if (!node.value || !node.value.expression || !node.value.expression.object) { + if (!node.value || !node.value.expression || (!checkLocal && !node.value.expression.object)) { return; } @@ -55,15 +99,23 @@ module.exports = { return; } - const propIsEventHandler = PROP_EVENT_HANDLER_REGEX.test(propKey); - const propFnIsNamedCorrectly = EVENT_HANDLER_REGEX.test(propValue); + const propIsEventHandler = PROP_EVENT_HANDLER_REGEX && PROP_EVENT_HANDLER_REGEX.test(propKey); + const propFnIsNamedCorrectly = EVENT_HANDLER_REGEX && EVENT_HANDLER_REGEX.test(propValue); - if (propIsEventHandler && !propFnIsNamedCorrectly) { + if ( + propIsEventHandler + && propFnIsNamedCorrectly !== null + && !propFnIsNamedCorrectly + ) { context.report({ node, message: `Handler function for ${propKey} prop key must begin with '${eventHandlerPrefix}'` }); - } else if (propFnIsNamedCorrectly && !propIsEventHandler) { + } else if ( + propFnIsNamedCorrectly + && propIsEventHandler !== null + && !propIsEventHandler + ) { context.report({ node, message: `Prop key for ${propValue} must begin with '${eventHandlerPropPrefix}'` diff --git a/lib/rules/jsx-indent-props.js b/lib/rules/jsx-indent-props.js index c2bbe511dd..27a63fb4b1 100644 --- a/lib/rules/jsx-indent-props.js +++ b/lib/rules/jsx-indent-props.js @@ -131,8 +131,8 @@ module.exports = { nodes.forEach((node) => { const nodeIndent = getNodeIndent(node); if ( - node.type !== 'ArrayExpression' && node.type !== 'ObjectExpression' && - nodeIndent !== indent && astUtil.isNodeFirstInLine(context, node) + node.type !== 'ArrayExpression' && node.type !== 'ObjectExpression' + && nodeIndent !== indent && astUtil.isNodeFirstInLine(context, node) ) { report(node, indent, nodeIndent); } diff --git a/lib/rules/jsx-indent.js b/lib/rules/jsx-indent.js index 9373e02616..9e11f60afa 100644 --- a/lib/rules/jsx-indent.js +++ b/lib/rules/jsx-indent.js @@ -30,6 +30,8 @@ 'use strict'; +const matchAll = require('string.prototype.matchall'); + const astUtil = require('../util/ast'); const docsUrl = require('../util/docsUrl'); @@ -95,8 +97,13 @@ module.exports = { * @private */ function getFixerFunction(node, needed) { - return function (fixer) { + return function fix(fixer) { const indent = Array(needed + 1).join(indentChar); + if (node.type === 'JSXText' || node.type === 'Literal') { + const regExp = /\n[\t ]*(\S)/g; + const fixedText = node.raw.replace(regExp, (match, p1) => `\n${indent}${p1}`); + return fixer.replaceText(node, fixedText); + } return fixer.replaceTextRange( [node.range[0] - node.loc.start.column, node.range[0]], indent @@ -176,11 +183,11 @@ module.exports = { */ function isRightInLogicalExp(node) { return ( - node.parent && - node.parent.parent && - node.parent.parent.type === 'LogicalExpression' && - node.parent.parent.right === node.parent && - !indentLogicalExpressions + node.parent + && node.parent.parent + && node.parent.parent.type === 'LogicalExpression' + && node.parent.parent.right === node.parent + && !indentLogicalExpressions ); } @@ -191,14 +198,85 @@ module.exports = { */ function isAlternateInConditionalExp(node) { return ( - node.parent && - node.parent.parent && - node.parent.parent.type === 'ConditionalExpression' && - node.parent.parent.alternate === node.parent && - context.getSourceCode().getTokenBefore(node).value !== '(' + node.parent + && node.parent.parent + && node.parent.parent.type === 'ConditionalExpression' + && node.parent.parent.alternate === node.parent + && context.getSourceCode().getTokenBefore(node).value !== '(' ); } + /** + * Check if the node is within a DoExpression block but not the first expression (which need to be indented) + * @param {ASTNode} node The node to check + * @return {Boolean} true if its the case, false if not + */ + function isSecondOrSubsequentExpWithinDoExp(node) { + /* + It returns true when node.parent.parent.parent.parent matches: + + DoExpression({ + ..., + body: BlockStatement({ + ..., + body: [ + ..., // 1-n times + ExpressionStatement({ + ..., + expression: JSXElement({ + ..., + openingElement: JSXOpeningElement() // the node + }) + }), + ... // 0-n times + ] + }) + }) + + except: + + DoExpression({ + ..., + body: BlockStatement({ + ..., + body: [ + ExpressionStatement({ + ..., + expression: JSXElement({ + ..., + openingElement: JSXOpeningElement() // the node + }) + }), + ... // 0-n times + ] + }) + }) + */ + const isInExpStmt = ( + node.parent + && node.parent.parent + && node.parent.parent.type === 'ExpressionStatement' + ); + if (!isInExpStmt) { + return false; + } + + const expStmt = node.parent.parent; + const isInBlockStmtWithinDoExp = ( + expStmt.parent + && expStmt.parent.type === 'BlockStatement' + && expStmt.parent.parent + && expStmt.parent.parent.type === 'DoExpression' + ); + if (!isInBlockStmtWithinDoExp) { + return false; + } + + const blockStmt = expStmt.parent; + const blockStmtFirstExp = blockStmt.body[0]; + return !(blockStmtFirstExp === expStmt); + } + /** * Check indent for nodes list * @param {ASTNode} node The node to check @@ -210,15 +288,38 @@ module.exports = { const isCorrectRightInLogicalExp = isRightInLogicalExp(node) && (nodeIndent - indent) === indentSize; const isCorrectAlternateInCondExp = isAlternateInConditionalExp(node) && (nodeIndent - indent) === 0; if ( - nodeIndent !== indent && - astUtil.isNodeFirstInLine(context, node) && - !isCorrectRightInLogicalExp && - !isCorrectAlternateInCondExp + nodeIndent !== indent + && astUtil.isNodeFirstInLine(context, node) + && !isCorrectRightInLogicalExp + && !isCorrectAlternateInCondExp ) { report(node, indent, nodeIndent); } } + /** + * Check indent for Literal Node or JSXText Node + * @param {ASTNode} node The node to check + * @param {Number} indent needed indent + */ + function checkLiteralNodeIndent(node, indent) { + const value = node.value; + const regExp = indentType === 'space' ? /\n( *)[\t ]*\S/g : /\n(\t*)[\t ]*\S/g; + const nodeIndentsPerLine = Array.from( + matchAll(String(value), regExp), + (match) => (match[1] ? match[1].length : 0) + ); + const hasFirstInLineNode = nodeIndentsPerLine.length > 0; + if ( + hasFirstInLineNode + && !nodeIndentsPerLine.every((actualIndent) => actualIndent === indent) + ) { + nodeIndentsPerLine.forEach((nodeIndent) => { + report(node, indent, nodeIndent); + }); + } + } + function handleOpeningElement(node) { const sourceCode = context.getSourceCode(); let prevToken = sourceCode.getTokenBefore(node); @@ -242,9 +343,10 @@ module.exports = { prevToken = prevToken.type === 'JSXExpressionContainer' ? prevToken.expression : prevToken; const parentElementIndent = getNodeIndent(prevToken); const indent = ( - prevToken.loc.start.line === node.loc.start.line || - isRightInLogicalExp(node) || - isAlternateInConditionalExp(node) + prevToken.loc.start.line === node.loc.start.line + || isRightInLogicalExp(node) + || isAlternateInConditionalExp(node) + || isSecondOrSubsequentExpWithinDoExp(node) ) ? 0 : indentSize; checkNodesIndent(node, parentElementIndent + indent); } @@ -268,6 +370,17 @@ module.exports = { checkNodesIndent(firstInLine, indent); } + function handleLiteral(node) { + if (!node.parent) { + return; + } + if (node.parent.type !== 'JSXElement' && node.parent.type !== 'JSXFragment') { + return; + } + const parentNodeIndent = getNodeIndent(node.parent); + checkLiteralNodeIndent(node, parentNodeIndent + indentSize); + } + return { JSXOpeningElement: handleOpeningElement, JSXOpeningFragment: handleOpeningElement, @@ -280,7 +393,9 @@ module.exports = { } const parentNodeIndent = getNodeIndent(node.parent); checkNodesIndent(node, parentNodeIndent + indentSize); - } + }, + Literal: handleLiteral, + JSXText: handleLiteral }; } }; diff --git a/lib/rules/jsx-key.js b/lib/rules/jsx-key.js index c40e476d26..25405bb13b 100644 --- a/lib/rules/jsx-key.js +++ b/lib/rules/jsx-key.js @@ -7,6 +7,7 @@ const hasProp = require('jsx-ast-utils/hasProp'); const docsUrl = require('../util/docsUrl'); +const pragmaUtil = require('../util/pragma'); // ------------------------------------------------------------------------------ @@ -40,6 +41,8 @@ module.exports = { create(context) { const options = Object.assign({}, defaultOptions, context.options[0]); const checkFragmentShorthand = options.checkFragmentShorthand; + const reactPragma = pragmaUtil.getFromContext(context); + const fragmentPragma = pragmaUtil.getFragmentFromContext(context); function checkIteratorElement(node) { if (node.type === 'JSXElement' && !hasProp(node.openingElement.attributes, 'key')) { @@ -50,13 +53,13 @@ module.exports = { } else if (checkFragmentShorthand && node.type === 'JSXFragment') { context.report({ node, - message: 'Missing "key" prop for element in iterator. Shorthand fragment syntax does support providing keys' + message: `Missing "key" prop for element in iterator. Shorthand fragment syntax does not support providing keys. Use ${reactPragma}.${fragmentPragma} instead` }); } } function getReturnStatement(body) { - return body.filter(item => item.type === 'ReturnStatement')[0]; + return body.filter((item) => item.type === 'ReturnStatement')[0]; } return { @@ -81,14 +84,14 @@ module.exports = { if (node.parent.type === 'ArrayExpression') { context.report({ node, - message: 'Missing "key" prop for element in array. Shorthand fragment syntax does support providing keys' + message: `Missing "key" prop for element in array. Shorthand fragment syntax does not support providing keys. Use ${reactPragma}.${fragmentPragma} instead` }); } }, // Array.prototype.map - CallExpression(node) { - if (node.callee && node.callee.type !== 'MemberExpression') { + 'CallExpression, OptionalCallExpression'(node) { + if (node.callee && node.callee.type !== 'MemberExpression' && node.callee.type !== 'OptionalMemberExpression') { return; } diff --git a/lib/rules/jsx-max-depth.js b/lib/rules/jsx-max-depth.js index 1e0edd5369..50aa4780c5 100644 --- a/lib/rules/jsx-max-depth.js +++ b/lib/rules/jsx-max-depth.js @@ -88,10 +88,10 @@ module.exports = { if (has(refs[i], 'writeExpr')) { const writeExpr = refs[i].writeExpr; - return jsxUtil.isJSX(writeExpr) && - writeExpr || - (writeExpr && writeExpr.type === 'Identifier') && - findJSXElementOrFragment(variables, writeExpr.name); + return jsxUtil.isJSX(writeExpr) + && writeExpr + || (writeExpr && writeExpr.type === 'Identifier') + && findJSXElementOrFragment(variables, writeExpr.name); } } diff --git a/lib/rules/jsx-max-props-per-line.js b/lib/rules/jsx-max-props-per-line.js index 06dad9d147..387a9ff0c8 100644 --- a/lib/rules/jsx-max-props-per-line.js +++ b/lib/rules/jsx-max-props-per-line.js @@ -62,7 +62,7 @@ module.exports = { }, '')); } const code = output.join('\n'); - return function (fixer) { + return function fix(fixer) { return fixer.replaceTextRange([front, back], code); }; } diff --git a/lib/rules/jsx-no-bind.js b/lib/rules/jsx-no-bind.js index 612f492cea..0ddee24996 100644 --- a/lib/rules/jsx-no-bind.js +++ b/lib/rules/jsx-no-bind.js @@ -80,18 +80,18 @@ module.exports = { const nodeType = node.type; if ( - !configuration.allowBind && - nodeType === 'CallExpression' && - node.callee.type === 'MemberExpression' && - node.callee.property.type === 'Identifier' && - node.callee.property.name === 'bind' + !configuration.allowBind + && nodeType === 'CallExpression' + && node.callee.type === 'MemberExpression' + && node.callee.property.type === 'Identifier' + && node.callee.property.name === 'bind' ) { return 'bindCall'; } if (nodeType === 'ConditionalExpression') { - return getNodeViolationType(node.test) || - getNodeViolationType(node.consequent) || - getNodeViolationType(node.alternate); + return getNodeViolationType(node.test) + || getNodeViolationType(node.consequent) + || getNodeViolationType(node.alternate); } if (!configuration.allowArrowFunctions && nodeType === 'ArrowFunctionExpression') { return 'arrowFunc'; @@ -112,7 +112,7 @@ module.exports = { function getBlockStatementAncestors(node) { return context.getAncestors(node).reverse().filter( - ancestor => ancestor.type === 'BlockStatement' + (ancestor) => ancestor.type === 'BlockStatement' ); } @@ -132,13 +132,13 @@ module.exports = { function findVariableViolation(node, name) { getBlockStatementAncestors(node).find( - block => reportVariableViolation(node, name, block.start) + (block) => reportVariableViolation(node, name, block.range[0]) ); } return { BlockStatement(node) { - setBlockVariableNameSet(node.start); + setBlockVariableNameSet(node.range[0]); }, VariableDeclarator(node) { @@ -149,12 +149,12 @@ module.exports = { const variableViolationType = getNodeViolationType(node.init); if ( - blockAncestors.length > 0 && - variableViolationType && - node.parent.kind === 'const' // only support const right now + blockAncestors.length > 0 + && variableViolationType + && node.parent.kind === 'const' // only support const right now ) { addVariableNameToSet( - variableViolationType, node.id.name, blockAncestors[0].start + variableViolationType, node.id.name, blockAncestors[0].range[0] ); } }, diff --git a/lib/rules/jsx-no-comment-textnodes.js b/lib/rules/jsx-no-comment-textnodes.js index e245d11fc0..0d7fba9f17 100644 --- a/lib/rules/jsx-no-comment-textnodes.js +++ b/lib/rules/jsx-no-comment-textnodes.js @@ -11,6 +11,24 @@ const docsUrl = require('../util/docsUrl'); // Rule Definition // ------------------------------------------------------------------------------ +function checkText(node, context) { + // since babel-eslint has the wrong node.raw, we'll get the source text + const rawValue = context.getSourceCode().getText(node); + if (/^\s*\/(\/|\*)/m.test(rawValue)) { + // inside component, e.g.
literal
+ if ( + node.parent.type !== 'JSXAttribute' + && node.parent.type !== 'JSXExpressionContainer' + && node.parent.type.indexOf('JSX') !== -1 + ) { + context.report({ + node, + message: 'Comments inside children section of tag should be placed inside braces' + }); + } + } +} + module.exports = { meta: { docs: { @@ -28,29 +46,16 @@ module.exports = { }, create(context) { - function reportLiteralNode(node) { - context.report({ - node, - message: 'Comments inside children section of tag should be placed inside braces' - }); - } - // -------------------------------------------------------------------------- // Public // -------------------------------------------------------------------------- return { Literal(node) { - // since babel-eslint has the wrong node.raw, we'll get the source text - const rawValue = context.getSourceCode().getText(node); - if (/^\s*\/(\/|\*)/m.test(rawValue)) { - // inside component, e.g.
literal
- if (node.parent.type !== 'JSXAttribute' && - node.parent.type !== 'JSXExpressionContainer' && - node.parent.type.indexOf('JSX') !== -1) { - reportLiteralNode(node); - } - } + checkText(node, context); + }, + JSXText(node) { + checkText(node, context); } }; } diff --git a/lib/rules/jsx-no-literals.js b/lib/rules/jsx-no-literals.js index bebddb067a..9ae90d8daf 100644 --- a/lib/rules/jsx-no-literals.js +++ b/lib/rules/jsx-no-literals.js @@ -12,6 +12,10 @@ const docsUrl = require('../util/docsUrl'); // Rule Definition // ------------------------------------------------------------------------------ +function trimIfString(val) { + return typeof val === 'string' ? val.trim() : val; +} + module.exports = { meta: { docs: { @@ -26,6 +30,16 @@ module.exports = { properties: { noStrings: { type: 'boolean' + }, + allowedStrings: { + type: 'array', + uniqueItems: true, + items: { + type: 'string' + } + }, + ignoreProps: { + type: 'boolean' } }, additionalProperties: false @@ -33,16 +47,20 @@ module.exports = { }, create(context) { - const isNoStrings = context.options[0] ? context.options[0].noStrings : false; + const defaults = {noStrings: false, allowedStrings: [], ignoreProps: false}; + const config = Object.assign({}, defaults, context.options[0] || {}); + config.allowedStrings = new Set(config.allowedStrings.map(trimIfString)); + + const message = config.noStrings + ? 'Strings not allowed in JSX files' + : 'Missing JSX expression container around literal string'; - const message = isNoStrings ? - 'Strings not allowed in JSX files' : - 'Missing JSX expression container around literal string'; + function reportLiteralNode(node, customMessage) { + const errorMessage = customMessage || message; - function reportLiteralNode(node) { context.report({ node, - message: `${message}: “${context.getSourceCode().getText(node).trim()}”` + message: `${errorMessage}: “${context.getSourceCode().getText(node).trim()}”` }); } @@ -55,29 +73,61 @@ module.exports = { } function getValidation(node) { + if (config.allowedStrings.has(trimIfString(node.value))) { + return false; + } const parent = getParentIgnoringBinaryExpressions(node); - const standard = !/^[\s]+$/.test(node.value) && - typeof node.value === 'string' && - parent.type.indexOf('JSX') !== -1 && - parent.type !== 'JSXAttribute'; - if (isNoStrings) { + const standard = !/^[\s]+$/.test(node.value) + && typeof node.value === 'string' + && parent.type.indexOf('JSX') !== -1 + && parent.type !== 'JSXAttribute'; + if (config.noStrings) { return standard; } return standard && parent.type !== 'JSXExpressionContainer'; } + function getParentAndGrandParentType(node) { + const parent = getParentIgnoringBinaryExpressions(node); + const parentType = parent.type; + const grandParentType = parent.parent.type; + + return { + parent, + parentType, + grandParentType, + grandParent: parent.parent + }; + } + + function hasJSXElementParentOrGrandParent(node) { + const parents = getParentAndGrandParentType(node); + const parentType = parents.parentType; + const grandParentType = parents.grandParentType; + + return parentType === 'JSXFragment' || parentType === 'JSXElement' || grandParentType === 'JSXElement'; + } + // -------------------------------------------------------------------------- // Public // -------------------------------------------------------------------------- return { - Literal(node) { - if (getValidation(node)) { + if (getValidation(node) && (hasJSXElementParentOrGrandParent(node) || !config.ignoreProps)) { reportLiteralNode(node); } }, + JSXAttribute(node) { + const isNodeValueString = node.value && node.value && node.value.type === 'Literal' && typeof node.value.value === 'string'; + + if (config.noStrings && !config.ignoreProps && isNodeValueString) { + const customMessage = 'Invalid prop value'; + reportLiteralNode(node, customMessage); + } + }, + JSXText(node) { if (getValidation(node)) { reportLiteralNode(node); @@ -85,12 +135,16 @@ module.exports = { }, TemplateLiteral(node) { - const parent = getParentIgnoringBinaryExpressions(node); - if (isNoStrings && parent.type === 'JSXExpressionContainer') { + const parents = getParentAndGrandParentType(node); + const parentType = parents.parentType; + const grandParentType = parents.grandParentType; + const isParentJSXExpressionCont = parentType === 'JSXExpressionContainer'; + const isParentJSXElement = parentType === 'JSXElement' || grandParentType === 'JSXElement'; + + if (isParentJSXExpressionCont && config.noStrings && (isParentJSXElement || !config.ignoreProps)) { reportLiteralNode(node); } } - }; } }; diff --git a/lib/rules/jsx-no-script-url.js b/lib/rules/jsx-no-script-url.js new file mode 100644 index 0000000000..6e42f93df4 --- /dev/null +++ b/lib/rules/jsx-no-script-url.js @@ -0,0 +1,91 @@ +/** + * @fileoverview Prevent usage of `javascript:` URLs + * @author Sergei Startsev + */ + +'use strict'; + +const docsUrl = require('../util/docsUrl'); + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +// https://github.com/facebook/react/blob/d0ebde77f6d1232cefc0da184d731943d78e86f2/packages/react-dom/src/shared/sanitizeURL.js#L30 +/* eslint-disable-next-line max-len, no-control-regex */ +const isJavaScriptProtocol = /^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*:/i; + +function hasJavaScriptProtocol(attr) { + return attr.value.type === 'Literal' + && isJavaScriptProtocol.test(attr.value.value); +} + +function shouldVerifyElement(node, config) { + const name = node.name && node.name.name; + return name === 'a' || config.find((i) => i.name === name); +} + +function shouldVerifyProp(node, config) { + const name = node.name && node.name.name; + const parentName = node.parent.name && node.parent.name.name; + + if (parentName === 'a' && name === 'href') { + return true; + } + + const el = config.find((i) => i.name === parentName); + if (!el) { + return false; + } + + const props = el.props || []; + return node.name && props.indexOf(name) !== -1; +} + +module.exports = { + meta: { + docs: { + description: 'Forbid `javascript:` URLs', + category: 'Best Practices', + recommended: false, + url: docsUrl('jsx-no-script-url') + }, + schema: [{ + type: 'array', + uniqueItems: true, + items: { + type: 'object', + properties: { + name: { + type: 'string' + }, + props: { + type: 'array', + items: { + type: 'string', + uniqueItems: true + } + } + }, + required: ['name', 'props'], + additionalProperties: false + } + }] + }, + + create(context) { + const config = context.options[0] || []; + return { + JSXAttribute(node) { + const parent = node.parent; + if (shouldVerifyElement(parent, config) && shouldVerifyProp(node, config) && hasJavaScriptProtocol(node)) { + context.report({ + node, + message: 'A future version of React will block javascript: URLs as a security precaution. ' + + 'Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.' + }); + } + } + }; + } +}; diff --git a/lib/rules/jsx-no-target-blank.js b/lib/rules/jsx-no-target-blank.js index be20eb788e..9b4794dcb2 100644 --- a/lib/rules/jsx-no-target-blank.js +++ b/lib/rules/jsx-no-target-blank.js @@ -13,31 +13,47 @@ const linkComponentsUtil = require('../util/linkComponents'); // ------------------------------------------------------------------------------ function isTargetBlank(attr) { - return attr.name && - attr.name.name === 'target' && - attr.value && - attr.value.type === 'Literal' && - attr.value.value.toLowerCase() === '_blank'; + return attr.name + && attr.name.name === 'target' + && attr.value + && (( + attr.value.type === 'Literal' + && attr.value.value.toLowerCase() === '_blank' + ) || ( + attr.value.type === 'JSXExpressionContainer' + && attr.value.expression + && attr.value.expression.value + && attr.value.expression.value.toLowerCase() === '_blank' + )); } function hasExternalLink(element, linkAttribute) { - return element.attributes.some(attr => attr.name && - attr.name.name === linkAttribute && - attr.value.type === 'Literal' && - /^(?:\w+:|\/\/)/.test(attr.value.value)); + return element.attributes.some((attr) => attr.name + && attr.name.name === linkAttribute + && attr.value.type === 'Literal' + && /^(?:\w+:|\/\/)/.test(attr.value.value)); } function hasDynamicLink(element, linkAttribute) { - return element.attributes.some(attr => attr.name && - attr.name.name === linkAttribute && - attr.value.type === 'JSXExpressionContainer'); + return element.attributes.some((attr) => attr.name + && attr.name.name === linkAttribute + && attr.value.type === 'JSXExpressionContainer'); } -function hasSecureRel(element) { +function hasSecureRel(element, allowReferrer) { return element.attributes.find((attr) => { if (attr.type === 'JSXAttribute' && attr.name.name === 'rel') { - const tags = attr.value && attr.value.type === 'Literal' && attr.value.value.toLowerCase().split(' '); - return tags && (tags.indexOf('noopener') >= 0 && tags.indexOf('noreferrer') >= 0); + const value = attr.value + && (( + attr.value.type === 'Literal' + && attr.value.value + ) || ( + attr.value.type === 'JSXExpressionContainer' + && attr.value.expression + && attr.value.expression.value + )); + const tags = value && value.toLowerCase && value.toLowerCase().split(' '); + return tags && (allowReferrer ? tags.indexOf('noopener') >= 0 : tags.indexOf('noreferrer') >= 0); } return false; }); @@ -46,7 +62,7 @@ function hasSecureRel(element) { module.exports = { meta: { docs: { - description: 'Forbid target="_blank" attribute without rel="noopener noreferrer"', + description: 'Forbid `target="_blank"` attribute without `rel="noreferrer"`', category: 'Best Practices', recommended: true, url: docsUrl('jsx-no-target-blank') @@ -54,6 +70,9 @@ module.exports = { schema: [{ type: 'object', properties: { + allowReferrer: { + type: 'boolean' + }, enforceDynamicLinks: { enum: ['always', 'never'] } @@ -64,12 +83,17 @@ module.exports = { create(context) { const configuration = context.options[0] || {}; + const allowReferrer = configuration.allowReferrer || false; const enforceDynamicLinks = configuration.enforceDynamicLinks || 'always'; const components = linkComponentsUtil.getLinkComponents(context); return { JSXAttribute(node) { - if (!components.has(node.parent.name.name) || !isTargetBlank(node) || hasSecureRel(node.parent)) { + if ( + !components.has(node.parent.name.name) + || !isTargetBlank(node) + || hasSecureRel(node.parent, allowReferrer) + ) { return; } @@ -78,8 +102,8 @@ module.exports = { if (hasExternalLink(node.parent, linkAttribute) || (enforceDynamicLinks === 'always' && hasDynamicLink(node.parent, linkAttribute))) { context.report({ node, - message: 'Using target="_blank" without rel="noopener noreferrer" ' + - 'is a security risk: see https://mathiasbynens.github.io/rel-noopener' + message: 'Using target="_blank" without rel="noreferrer" ' + + 'is a security risk: see https://html.spec.whatwg.org/multipage/links.html#link-type-noopener' }); } } diff --git a/lib/rules/jsx-no-useless-fragment.js b/lib/rules/jsx-no-useless-fragment.js new file mode 100644 index 0000000000..641ba8239f --- /dev/null +++ b/lib/rules/jsx-no-useless-fragment.js @@ -0,0 +1,212 @@ +/** + * @fileoverview Disallow useless fragments + */ + +'use strict'; + +const arrayIncludes = require('array-includes'); + +const pragmaUtil = require('../util/pragma'); +const jsxUtil = require('../util/jsx'); +const docsUrl = require('../util/docsUrl'); + +function isJSXText(node) { + return !!node && (node.type === 'JSXText' || node.type === 'Literal'); +} + +/** + * @param {string} text + * @returns {boolean} + */ +function isOnlyWhitespace(text) { + return text.trim().length === 0; +} + +/** + * @param {ASTNode} node + * @returns {boolean} + */ +function isNonspaceJSXTextOrJSXCurly(node) { + return (isJSXText(node) && !isOnlyWhitespace(node.raw)) || node.type === 'JSXExpressionContainer'; +} + +/** + * Somehow fragment like this is useful: ee eeee eeee ...} /> + * @param {ASTNode} node + * @returns {boolean} + */ +function isFragmentWithOnlyTextAndIsNotChild(node) { + return node.children.length === 1 + && isJSXText(node.children[0]) + && !(node.parent.type === 'JSXElement' || node.parent.type === 'JSXFragment'); +} + +/** + * @param {string} text + * @returns {string} + */ +function trimLikeReact(text) { + const leadingSpaces = /^\s*/.exec(text)[0]; + const trailingSpaces = /\s*$/.exec(text)[0]; + + const start = arrayIncludes(leadingSpaces, '\n') ? leadingSpaces.length : 0; + const end = arrayIncludes(trailingSpaces, '\n') ? text.length - trailingSpaces.length : text.length; + + return text.slice(start, end); +} + +/** + * Test if node is like `_` + * @param {JSXElement} node + * @returns {boolean} + */ +function isKeyedElement(node) { + return node.type === 'JSXElement' + && node.openingElement.attributes + && node.openingElement.attributes.some(jsxUtil.isJSXAttributeKey); +} + +module.exports = { + meta: { + type: 'suggestion', + fixable: 'code', + docs: { + description: 'Disallow unnecessary fragments', + category: 'Possible Errors', + recommended: false, + url: docsUrl('jsx-no-useless-fragment') + }, + messages: { + NeedsMoreChidren: 'Fragments should contain more than one child - otherwise, there‘s no need for a Fragment at all.', + ChildOfHtmlElement: 'Passing a fragment to an HTML element is useless.' + } + }, + + create(context) { + const reactPragma = pragmaUtil.getFromContext(context); + const fragmentPragma = pragmaUtil.getFragmentFromContext(context); + + /** + * Test whether a node is an padding spaces trimmed by react runtime. + * @param {ASTNode} node + * @returns {boolean} + */ + function isPaddingSpaces(node) { + return isJSXText(node) + && isOnlyWhitespace(node.raw) + && arrayIncludes(node.raw, '\n'); + } + + /** + * Test whether a JSXElement has less than two children, excluding paddings spaces. + * @param {JSXElement|JSXFragment} node + * @returns {boolean} + */ + function hasLessThanTwoChildren(node) { + if (!node || !node.children || node.children.length < 2) { + return true; + } + + return ( + node.children.length + - (+isPaddingSpaces(node.children[0])) + - (+isPaddingSpaces(node.children[node.children.length - 1])) + ) < 2; + } + + /** + * @param {JSXElement|JSXFragment} node + * @returns {boolean} + */ + function isChildOfHtmlElement(node) { + return node.parent.type === 'JSXElement' + && node.parent.openingElement.name.type === 'JSXIdentifier' + && /^[a-z]+$/.test(node.parent.openingElement.name.name); + } + + /** + * @param {JSXElement|JSXFragment} node + * @return {boolean} + */ + function isChildOfComponentElement(node) { + return node.parent.type === 'JSXElement' + && !isChildOfHtmlElement(node) + && !jsxUtil.isFragment(node.parent, reactPragma, fragmentPragma); + } + + /** + * @param {ASTNode} node + * @returns {boolean} + */ + function canFix(node) { + // Not safe to fix fragments without a jsx parent. + if (!(node.parent.type === 'JSXElement' || node.parent.type === 'JSXFragment')) { + // const a = <> + if (node.children.length === 0) { + return false; + } + + // const a = <>cat {meow} + if (node.children.some(isNonspaceJSXTextOrJSXCurly)) { + return false; + } + } + + // Not safe to fix `<>foo` because `Eeee` might require its children be a ReactElement. + if (isChildOfComponentElement(node)) { + return false; + } + + return true; + } + + /** + * @param {ASTNode} node + * @returns {Function | undefined} + */ + function getFix(node) { + if (!canFix(node)) { + return undefined; + } + + return function fix(fixer) { + const opener = node.type === 'JSXFragment' ? node.openingFragment : node.openingElement; + const closer = node.type === 'JSXFragment' ? node.closingFragment : node.closingElement; + const childrenText = context.getSourceCode().getText().slice(opener.range[1], closer.range[0]); + + return fixer.replaceText(node, trimLikeReact(childrenText)); + }; + } + + function checkNode(node) { + if (isKeyedElement(node)) { + return; + } + + if (hasLessThanTwoChildren(node) && !isFragmentWithOnlyTextAndIsNotChild(node)) { + context.report({ + node, + messageId: 'NeedsMoreChidren', + fix: getFix(node) + }); + } + + if (isChildOfHtmlElement(node)) { + context.report({ + node, + messageId: 'ChildOfHtmlElement', + fix: getFix(node) + }); + } + } + + return { + JSXElement(node) { + if (jsxUtil.isFragment(node, reactPragma, fragmentPragma)) { + checkNode(node); + } + }, + JSXFragment: checkNode + }; + } +}; diff --git a/lib/rules/jsx-one-expression-per-line.js b/lib/rules/jsx-one-expression-per-line.js index 4ce00a9063..e0fb19d9da 100644 --- a/lib/rules/jsx-one-expression-per-line.js +++ b/lib/rules/jsx-one-expression-per-line.js @@ -6,6 +6,7 @@ 'use strict'; const docsUrl = require('../util/docsUrl'); +const jsxUtil = require('../util/jsx'); // ------------------------------------------------------------------------------ // Rule Definition @@ -66,15 +67,15 @@ module.exports = { if (children.length === 1) { const child = children[0]; if ( - openingElementStartLine === openingElementEndLine && - openingElementEndLine === closingElementStartLine && - closingElementStartLine === closingElementEndLine && - closingElementEndLine === child.loc.start.line && - child.loc.start.line === child.loc.end.line + openingElementStartLine === openingElementEndLine + && openingElementEndLine === closingElementStartLine + && closingElementStartLine === closingElementEndLine + && closingElementEndLine === child.loc.start.line + && child.loc.start.line === child.loc.end.line ) { if ( - options.allow === 'single-child' || - options.allow === 'literal' && (child.type === 'Literal' || child.type === 'JSXText') + options.allow === 'single-child' + || options.allow === 'literal' && (child.type === 'Literal' || child.type === 'JSXText') ) { return; } @@ -89,7 +90,7 @@ module.exports = { let countNewLinesAfterContent = 0; if (child.type === 'Literal' || child.type === 'JSXText') { - if (/^\s*$/.test(child.raw)) { + if (jsxUtil.isWhiteSpaces(child.raw)) { return; } @@ -144,15 +145,15 @@ module.exports = { } function spaceBetweenPrev() { - return ((prevChild.type === 'Literal' || prevChild.type === 'JSXText') && / $/.test(prevChild.raw)) || - ((child.type === 'Literal' || child.type === 'JSXText') && /^ /.test(child.raw)) || - context.getSourceCode().isSpaceBetweenTokens(prevChild, child); + return ((prevChild.type === 'Literal' || prevChild.type === 'JSXText') && / $/.test(prevChild.raw)) + || ((child.type === 'Literal' || child.type === 'JSXText') && /^ /.test(child.raw)) + || context.getSourceCode().isSpaceBetweenTokens(prevChild, child); } function spaceBetweenNext() { - return ((nextChild.type === 'Literal' || nextChild.type === 'JSXText') && /^ /.test(nextChild.raw)) || - ((child.type === 'Literal' || child.type === 'JSXText') && / $/.test(child.raw)) || - context.getSourceCode().isSpaceBetweenTokens(child, nextChild); + return ((nextChild.type === 'Literal' || nextChild.type === 'JSXText') && /^ /.test(nextChild.raw)) + || ((child.type === 'Literal' || child.type === 'JSXText') && / $/.test(child.raw)) + || context.getSourceCode().isSpaceBetweenTokens(child, nextChild); } if (!prevChild && !nextChild) { diff --git a/lib/rules/jsx-pascal-case.js b/lib/rules/jsx-pascal-case.js index 34de9ae67b..343bcd3b55 100644 --- a/lib/rules/jsx-pascal-case.js +++ b/lib/rules/jsx-pascal-case.js @@ -6,6 +6,7 @@ 'use strict'; const elementType = require('jsx-ast-utils/elementType'); +const XRegExp = require('xregexp'); const docsUrl = require('../util/docsUrl'); const jsxUtil = require('../util/jsx'); @@ -13,8 +14,11 @@ const jsxUtil = require('../util/jsx'); // Constants // ------------------------------------------------------------------------------ -const PASCAL_CASE_REGEX = /^([A-Z0-9]|[A-Z0-9]+[a-z0-9]+(?:[A-Z0-9]+[a-z0-9]*)*)$/; -const ALL_CAPS_TAG_REGEX = /^[A-Z0-9]+$/; +// eslint-disable-next-line no-new +const hasU = (function hasU() { try { new RegExp('o', 'u'); return true; } catch (e) { return false; } }()); + +const PASCAL_CASE_REGEX = XRegExp('^(.*[.])*([\\p{Lu}]|[\\p{Lu}]+[\\p{Ll}0-9]+(?:[\\p{Lu}0-9]+[\\p{Ll}0-9]*)*)$', hasU ? 'u' : ''); +const ALL_CAPS_TAG_REGEX = XRegExp('^[\\p{Lu}0-9]+([\\p{Lu}0-9_]*[\\p{Lu}0-9]+)?$', hasU ? 'u' : ''); // ------------------------------------------------------------------------------ // Rule Definition @@ -50,25 +54,31 @@ module.exports = { return { JSXOpeningElement(node) { + const isCompatTag = jsxUtil.isDOMComponent(node); + if (isCompatTag) return undefined; + let name = elementType(node); + if (name.length === 1) return undefined; - // Get namespace if the type is JSXNamespacedName or JSXMemberExpression - if (name.indexOf(':') > -1) { - name = name.substring(0, name.indexOf(':')); - } else if (name.indexOf('.') > -1) { - name = name.substring(0, name.indexOf('.')); + // Get JSXIdentifier if the type is JSXNamespacedName or JSXMemberExpression + if (name.lastIndexOf(':') > -1) { + name = name.substring(name.lastIndexOf(':') + 1); + } else if (name.lastIndexOf('.') > -1) { + name = name.substring(name.lastIndexOf('.') + 1); } const isPascalCase = PASCAL_CASE_REGEX.test(name); - const isCompatTag = jsxUtil.isDOMComponent(node); const isAllowedAllCaps = allowAllCaps && ALL_CAPS_TAG_REGEX.test(name); const isIgnored = ignore.indexOf(name) !== -1; - if (!isPascalCase && !isCompatTag && !isAllowedAllCaps && !isIgnored) { - context.report({ - node, - message: `Imported JSX component ${name} must be in PascalCase` - }); + if (!isPascalCase && !isAllowedAllCaps && !isIgnored) { + let message = `Imported JSX component ${name} must be in PascalCase`; + + if (allowAllCaps) { + message += ' or SCREAMING_SNAKE_CASE'; + } + + context.report({node, message}); } } }; diff --git a/lib/rules/jsx-props-no-spreading.js b/lib/rules/jsx-props-no-spreading.js index f5ad8087f5..3d54a8e665 100644 --- a/lib/rules/jsx-props-no-spreading.js +++ b/lib/rules/jsx-props-no-spreading.js @@ -12,7 +12,12 @@ const docsUrl = require('../util/docsUrl'); // ------------------------------------------------------------------------------ const OPTIONS = {ignore: 'ignore', enforce: 'enforce'}; -const DEFAULTS = {html: OPTIONS.enforce, custom: OPTIONS.enforce, exceptions: []}; +const DEFAULTS = { + html: OPTIONS.enforce, + custom: OPTIONS.enforce, + explicitSpread: OPTIONS.enforce, + exceptions: [] +}; // ------------------------------------------------------------------------------ // Rule Definition @@ -70,21 +75,46 @@ module.exports = { const configuration = context.options[0] || {}; const ignoreHtmlTags = (configuration.html || DEFAULTS.html) === OPTIONS.ignore; const ignoreCustomTags = (configuration.custom || DEFAULTS.custom) === OPTIONS.ignore; + const ignoreExplicitSpread = (configuration.explicitSpread || DEFAULTS.explicitSpread) === OPTIONS.ignore; const exceptions = configuration.exceptions || DEFAULTS.exceptions; const isException = (tag, allExceptions) => allExceptions.indexOf(tag) !== -1; + const isProperty = (property) => property.type === 'Property'; + const getTagNameFromMemberExpression = (node) => `${node.property.parent.object.name}.${node.property.name}`; return { JSXSpreadAttribute(node) { - const tagName = node.parent.name.name; + const jsxOpeningElement = node.parent.name; + const type = jsxOpeningElement.type; + + let tagName; + if (type === 'JSXIdentifier') { + tagName = jsxOpeningElement.name; + } else if (type === 'JSXMemberExpression') { + tagName = getTagNameFromMemberExpression(jsxOpeningElement); + } else { + tagName = undefined; + } + const isHTMLTag = tagName && tagName[0] !== tagName[0].toUpperCase(); - const isCustomTag = tagName && tagName[0] === tagName[0].toUpperCase(); - if (isHTMLTag && - ((ignoreHtmlTags && !isException(tagName, exceptions)) || - (!ignoreHtmlTags && isException(tagName, exceptions)))) { + const isCustomTag = tagName && (tagName[0] === tagName[0].toUpperCase() || tagName.includes('.')); + if ( + isHTMLTag + && ((ignoreHtmlTags && !isException(tagName, exceptions)) + || (!ignoreHtmlTags && isException(tagName, exceptions))) + ) { + return; + } + if ( + isCustomTag + && ((ignoreCustomTags && !isException(tagName, exceptions)) + || (!ignoreCustomTags && isException(tagName, exceptions))) + ) { return; } - if (isCustomTag && - ((ignoreCustomTags && !isException(tagName, exceptions)) || - (!ignoreCustomTags && isException(tagName, exceptions)))) { + if ( + ignoreExplicitSpread + && node.argument.type === 'ObjectExpression' + && node.argument.properties.every(isProperty) + ) { return; } context.report({ diff --git a/lib/rules/jsx-sort-default-props.js b/lib/rules/jsx-sort-default-props.js index 7ba0447f90..d45ca94e1f 100644 --- a/lib/rules/jsx-sort-default-props.js +++ b/lib/rules/jsx-sort-default-props.js @@ -8,6 +8,7 @@ const variableUtil = require('../util/variable'); const docsUrl = require('../util/docsUrl'); const propWrapperUtil = require('../util/propWrapper'); +// const propTypesSortUtil = require('../util/propTypesSort'); // ------------------------------------------------------------------------------ // Rule Definition @@ -22,6 +23,8 @@ module.exports = { url: docsUrl('jsx-sort-default-props') }, + // fixable: 'code', + schema: [{ type: 'object', properties: { @@ -78,7 +81,7 @@ module.exports = { * @returns {ASTNode|null} Return null if the variable could not be found, ASTNode otherwise. */ function findVariableByName(name) { - const variable = variableUtil.variablesInScope(context).find(item => item.name === name); + const variable = variableUtil.variablesInScope(context).find((item) => item.name === name); if (!variable || !variable.defs[0] || !variable.defs[0].node) { return null; @@ -97,6 +100,10 @@ module.exports = { * @returns {void} */ function checkSorted(declarations) { + // function fix(fixer) { + // return propTypesSortUtil.fixPropTypesSort(fixer, context, declarations, ignoreCase); + // } + declarations.reduce((prev, curr, idx, decls) => { if (/Spread(?:Property|Element)$/.test(curr.type)) { return decls[idx + 1]; @@ -114,6 +121,7 @@ module.exports = { context.report({ node: curr, message: 'Default prop types declarations should be sorted alphabetically' + // fix }); return prev; diff --git a/lib/rules/jsx-sort-props.js b/lib/rules/jsx-sort-props.js index ccb7cec206..5eb9206b97 100644 --- a/lib/rules/jsx-sort-props.js +++ b/lib/rules/jsx-sort-props.js @@ -71,8 +71,12 @@ function contextCompare(a, b, options) { if (options.ignoreCase) { aProp = aProp.toLowerCase(); bProp = bProp.toLowerCase(); + return aProp.localeCompare(bProp); } - return aProp.localeCompare(bProp); + if (aProp === bProp) { + return 0; + } + return aProp < bProp ? -1 : 1; } /** @@ -90,9 +94,9 @@ function getGroupsOfSortableAttributes(attributes) { // then we start a new group. Append attributes to the group until we // come across another JSXSpreadAttribute or exhaust the array. if ( - !lastAttr || - (lastAttr.type === 'JSXSpreadAttribute' && - attributes[i].type !== 'JSXSpreadAttribute') + !lastAttr + || (lastAttr.type === 'JSXSpreadAttribute' + && attributes[i].type !== 'JSXSpreadAttribute') ) { groupCount++; sortableAttributeGroups[groupCount - 1] = []; @@ -130,9 +134,9 @@ const generateFixerFunction = (node, context, reservedList) => { const sortableAttributeGroups = getGroupsOfSortableAttributes(attributes); const sortedAttributeGroups = sortableAttributeGroups .slice(0) - .map(group => group.slice(0).sort((a, b) => contextCompare(a, b, options))); + .map((group) => group.slice(0).sort((a, b) => contextCompare(a, b, options))); - return function (fixer) { + return function fixFunction(fixer) { const fixers = []; let source = sourceCode.getText(); @@ -172,13 +176,13 @@ function validateReservedFirstConfig(context, reservedFirst) { if (reservedFirst) { if (Array.isArray(reservedFirst)) { // Only allow a subset of reserved words in customized lists - const nonReservedWords = reservedFirst.filter(word => !isReservedPropName( + const nonReservedWords = reservedFirst.filter((word) => !isReservedPropName( word, RESERVED_PROPS_LIST )); if (reservedFirst.length === 0) { - return function (decl) { + return function report(decl) { context.report({ node: decl, message: 'A customized reserved first list must not be empty' @@ -186,11 +190,11 @@ function validateReservedFirstConfig(context, reservedFirst) { }; } if (nonReservedWords.length > 0) { - return function (decl) { + return function report(decl) { context.report({ node: decl, - message: 'A customized reserved first list must only contain a subset of React reserved props.' + - ' Remove: {{ nonReservedWords }}', + message: 'A customized reserved first list must only contain a subset of React reserved props.' + + ' Remove: {{ nonReservedWords }}', data: { nonReservedWords: nonReservedWords.toString() } @@ -256,7 +260,7 @@ module.exports = { JSXOpeningElement(node) { // `dangerouslySetInnerHTML` is only "reserved" on DOM components if (reservedFirst && !jsxUtil.isDOMComponent(node)) { - reservedList = reservedList.filter(prop => prop !== 'dangerouslySetInnerHTML'); + reservedList = reservedList.filter((prop) => prop !== 'dangerouslySetInnerHTML'); } node.attributes.reduce((memo, decl, idx, attrs) => { @@ -342,7 +346,14 @@ module.exports = { } } - if (!noSortAlphabetically && currentPropName < previousPropName) { + if ( + !noSortAlphabetically + && ( + ignoreCase + ? previousPropName.localeCompare(currentPropName) > 0 + : previousPropName > currentPropName + ) + ) { context.report({ node: decl.name, message: 'Props should be sorted alphabetically', diff --git a/lib/rules/jsx-space-before-closing.js b/lib/rules/jsx-space-before-closing.js index 284aa06784..feeae4506d 100644 --- a/lib/rules/jsx-space-before-closing.js +++ b/lib/rules/jsx-space-before-closing.js @@ -82,9 +82,9 @@ module.exports = { return; } - log('The react/jsx-space-before-closing rule is deprecated. ' + - 'Please use the react/jsx-tag-spacing rule with the ' + - '"beforeSelfClosing" option instead.'); + log('The react/jsx-space-before-closing rule is deprecated. ' + + 'Please use the react/jsx-tag-spacing rule with the ' + + '"beforeSelfClosing" option instead.'); isWarnedForDeprecation = true; } }; diff --git a/lib/rules/jsx-wrap-multilines.js b/lib/rules/jsx-wrap-multilines.js index 374d0db5dc..4a8eb65f53 100644 --- a/lib/rules/jsx-wrap-multilines.js +++ b/lib/rules/jsx-wrap-multilines.js @@ -89,9 +89,9 @@ module.exports = { const previousToken = sourceCode.getTokenBefore(node); const nextToken = sourceCode.getTokenAfter(node); - return previousToken && nextToken && - previousToken.value === '(' && previousToken.range[1] <= node.range[0] && - nextToken.value === ')' && nextToken.range[0] >= node.range[1]; + return previousToken && nextToken + && previousToken.value === '(' && previousToken.range[1] <= node.range[0] + && nextToken.value === ')' && nextToken.range[0] >= node.range[1]; } function needsOpeningNewLine(node) { @@ -150,7 +150,7 @@ module.exports = { const option = getOption(type); if ((option === true || option === 'parens') && !isParenthesised(node) && isMultilines(node)) { - report(node, MISSING_PARENS, fixer => fixer.replaceText(node, `(${sourceCode.getText(node)})`)); + report(node, MISSING_PARENS, (fixer) => fixer.replaceText(node, `(${sourceCode.getText(node)})`)); } if (option === 'parens-new-line' && isMultilines(node)) { @@ -162,13 +162,13 @@ module.exports = { report( node, MISSING_PARENS, - fixer => fixer.replaceTextRange( - [tokenBefore.range[0], tokenAfter ? tokenAfter.range[0] : node.range[1]], - `${trimTokenBeforeNewline(node, tokenBefore)}(\n${sourceCode.getText(node)}\n)` + (fixer) => fixer.replaceTextRange( + [tokenBefore.range[0], tokenAfter && (tokenAfter.value === ';' || tokenAfter.value === '}') ? tokenAfter.range[0] : node.range[1]], + `${trimTokenBeforeNewline(node, tokenBefore)}(\n${' '.repeat(node.loc.start.column)}${sourceCode.getText(node)}\n${' '.repeat(node.loc.start.column - 2)})` ) ); } else { - report(node, MISSING_PARENS, fixer => fixer.replaceText(node, `(\n${sourceCode.getText(node)}\n)`)); + report(node, MISSING_PARENS, (fixer) => fixer.replaceText(node, `(\n${sourceCode.getText(node)}\n)`)); } } else { const needsOpening = needsOpeningNewLine(node); @@ -229,7 +229,7 @@ module.exports = { } }, - 'ArrowFunctionExpression:exit': function (node) { + 'ArrowFunctionExpression:exit': (node) => { const arrowBody = node.body; const type = 'arrow'; diff --git a/lib/rules/no-access-state-in-setstate.js b/lib/rules/no-access-state-in-setstate.js index 9a0af61d61..8bbf3ba56d 100644 --- a/lib/rules/no-access-state-in-setstate.js +++ b/lib/rules/no-access-state-in-setstate.js @@ -23,10 +23,10 @@ module.exports = { create(context) { function isSetStateCall(node) { - return node.type === 'CallExpression' && - node.callee.property && - node.callee.property.name === 'setState' && - node.callee.object.type === 'ThisExpression'; + return node.type === 'CallExpression' + && node.callee.property + && node.callee.property.name === 'setState' + && node.callee.object.type === 'ThisExpression'; } function isFirstArgumentInSetStateCall(current, node) { @@ -87,8 +87,8 @@ module.exports = { MemberExpression(node) { if ( - node.property.name === 'state' && - node.object.type === 'ThisExpression' + node.property.name === 'state' + && node.object.type === 'ThisExpression' ) { let current = node; while (current.type !== 'Program') { @@ -138,13 +138,13 @@ module.exports = { current = current.parent; } if ( - current.parent.value === current || - current.parent.object === current + current.parent.value === current + || current.parent.object === current ) { while (current.type !== 'Program') { if (isFirstArgumentInSetStateCall(current, node)) { vars - .filter(v => v.scope === context.getScope() && v.variableName === node.name) + .filter((v) => v.scope === context.getScope() && v.variableName === node.name) .forEach((v) => { context.report({ node: v.node, diff --git a/lib/rules/no-adjacent-inline-elements.js b/lib/rules/no-adjacent-inline-elements.js new file mode 100644 index 0000000000..bdddd79bef --- /dev/null +++ b/lib/rules/no-adjacent-inline-elements.js @@ -0,0 +1,118 @@ +/** + * @fileoverview Prevent adjacent inline elements not separated by whitespace. + * @author Sean Hayes + */ + +'use strict'; + +// ------------------------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------------------------ + +// https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elements +const inlineNames = [ + 'a', + 'b', + 'big', + 'i', + 'small', + 'tt', + 'abbr', + 'acronym', + 'cite', + 'code', + 'dfn', + 'em', + 'kbd', + 'strong', + 'samp', + 'time', + 'var', + 'bdo', + 'br', + 'img', + 'map', + 'object', + 'q', + 'script', + 'span', + 'sub', + 'sup', + 'button', + 'input', + 'label', + 'select', + 'textarea' +]; +// Note: raw   will be transformed into \u00a0. +const whitespaceRegex = /(?:^\s|\s$)/; + +function isInline(node) { + if (node.type === 'Literal') { + // Regular whitespace will be removed. + const value = node.value; + // To properly separate inline elements, each end of the literal will need + // whitespace. + return !whitespaceRegex.test(value); + } + if (node.type === 'JSXElement' && inlineNames.indexOf(node.openingElement.name.name) > -1) { + return true; + } + if (node.type === 'CallExpression' && inlineNames.indexOf(node.arguments[0].value) > -1) { + return true; + } + return false; +} + +const ERROR = 'Child elements which render as inline HTML elements should be separated by a space or wrapped in block level elements.'; + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + ERROR, + meta: { + docs: { + description: 'Prevent adjacent inline elements not separated by whitespace.', + category: 'Best Practices', + recommended: false + }, + schema: [] + }, + create(context) { + function validate(node, children) { + let currentIsInline = false; + let previousIsInline = false; + if (!children) { + return; + } + for (let i = 0; i < children.length; i++) { + currentIsInline = isInline(children[i]); + if (previousIsInline && currentIsInline) { + context.report({ + node, + message: ERROR + }); + return; + } + previousIsInline = currentIsInline; + } + } + return { + JSXElement(node) { + validate(node, node.children); + }, + CallExpression(node) { + if (!node.callee || node.callee.type !== 'MemberExpression' || node.callee.property.name !== 'createElement') { + return; + } + if (node.arguments.length < 2 || !node.arguments[2]) { + return; + } + const children = node.arguments[2].elements; + validate(node, children); + } + }; + } +}; diff --git a/lib/rules/no-array-index-key.js b/lib/rules/no-array-index-key.js index d3cbddd2d9..ceb2a1676a 100644 --- a/lib/rules/no-array-index-key.js +++ b/lib/rules/no-array-index-key.js @@ -45,16 +45,16 @@ module.exports = { const ERROR_MESSAGE = 'Do not use Array index in keys'; function isArrayIndex(node) { - return node.type === 'Identifier' && - indexParamNames.indexOf(node.name) !== -1; + return node.type === 'Identifier' + && indexParamNames.indexOf(node.name) !== -1; } function isUsingReactChildren(node) { const callee = node.callee; if ( - !callee || - !callee.property || - !callee.object + !callee + || !callee.property + || !callee.object ) { return null; } @@ -87,9 +87,9 @@ module.exports = { return null; } - const callbackArg = isUsingReactChildren(node) ? - node.arguments[1] : - node.arguments[0]; + const callbackArg = isUsingReactChildren(node) + ? node.arguments[1] + : node.arguments[0]; if (!callbackArg) { return null; @@ -156,10 +156,10 @@ module.exports = { return { CallExpression(node) { if ( - node.callee && - node.callee.type === 'MemberExpression' && - ['createElement', 'cloneElement'].indexOf(node.callee.property.name) !== -1 && - node.arguments.length > 1 + node.callee + && node.callee.type === 'MemberExpression' + && ['createElement', 'cloneElement'].indexOf(node.callee.property.name) !== -1 + && node.arguments.length > 1 ) { // React.createElement if (!indexParamNames.length) { @@ -213,7 +213,7 @@ module.exports = { checkPropValue(value.expression); }, - 'CallExpression:exit': function (node) { + 'CallExpression:exit'(node) { const mapIndexParamName = getMapIndexParamName(node); if (!mapIndexParamName) { return; diff --git a/lib/rules/no-children-prop.js b/lib/rules/no-children-prop.js index 0e9e011bf7..7bc650cffd 100644 --- a/lib/rules/no-children-prop.js +++ b/lib/rules/no-children-prop.js @@ -18,11 +18,11 @@ const docsUrl = require('../util/docsUrl'); * object literal, False if not. */ function isCreateElementWithProps(node) { - return node.callee && - node.callee.type === 'MemberExpression' && - node.callee.property.name === 'createElement' && - node.arguments.length > 1 && - node.arguments[1].type === 'ObjectExpression'; + return node.callee + && node.callee.type === 'MemberExpression' + && node.callee.property.name === 'createElement' + && node.arguments.length > 1 + && node.arguments[1].type === 'ObjectExpression'; } // ------------------------------------------------------------------------------ @@ -57,7 +57,7 @@ module.exports = { } const props = node.arguments[1].properties; - const childrenProp = props.find(prop => prop.key && prop.key.name === 'children'); + const childrenProp = props.find((prop) => prop.key && prop.key.name === 'children'); if (childrenProp) { context.report({ diff --git a/lib/rules/no-danger-with-children.js b/lib/rules/no-danger-with-children.js index 5df28af07b..95c15e460e 100644 --- a/lib/rules/no-danger-with-children.js +++ b/lib/rules/no-danger-with-children.js @@ -6,6 +6,7 @@ 'use strict'; const variableUtil = require('../util/variable'); +const jsxUtil = require('../util/jsx'); const docsUrl = require('../util/docsUrl'); // ------------------------------------------------------------------------------ @@ -23,13 +24,14 @@ module.exports = { }, create(context) { function findSpreadVariable(name) { - return variableUtil.variablesInScope(context).find(item => item.name === name); + return variableUtil.variablesInScope(context).find((item) => item.name === name); } /** * Takes a ObjectExpression and returns the value of the prop if it has it * @param {object} node - ObjectExpression node * @param {string} propName - name of the prop to look for * @param {string[]} seenProps + * @returns {object | boolean} */ function findObjectProp(node, propName, seenProps) { if (!node.properties) { @@ -57,6 +59,7 @@ module.exports = { * Takes a JSXElement and returns the value of the prop if it has it * @param {object} node - JSXElement node * @param {string} propName - name of the prop to look for + * @returns {object | boolean} */ function findJsxProp(node, propName) { const attributes = node.openingElement.attributes; @@ -79,7 +82,7 @@ module.exports = { function isLineBreak(node) { const isLiteral = node.type === 'Literal' || node.type === 'JSXText'; const isMultiline = node.loc.start.line !== node.loc.end.line; - const isWhiteSpaces = /^\s*$/.test(node.value); + const isWhiteSpaces = jsxUtil.isWhiteSpaces(node.value); return isLiteral && isMultiline && isWhiteSpaces; } @@ -95,9 +98,9 @@ module.exports = { } if ( - node.openingElement.attributes && - hasChildren && - findJsxProp(node, 'dangerouslySetInnerHTML') + node.openingElement.attributes + && hasChildren + && findJsxProp(node, 'dangerouslySetInnerHTML') ) { context.report({ node, @@ -107,17 +110,17 @@ module.exports = { }, CallExpression(node) { if ( - node.callee && - node.callee.type === 'MemberExpression' && - node.callee.property.name === 'createElement' && - node.arguments.length > 1 + node.callee + && node.callee.type === 'MemberExpression' + && node.callee.property.name === 'createElement' + && node.arguments.length > 1 ) { let hasChildren = false; let props = node.arguments[1]; if (props.type === 'Identifier') { - const variable = variableUtil.variablesInScope(context).find(item => item.name === props.name); + const variable = variableUtil.variablesInScope(context).find((item) => item.name === props.name); if (variable && variable.defs.length && variable.defs[0].node.init) { props = variable.defs[0].node.init; } diff --git a/lib/rules/no-deprecated.js b/lib/rules/no-deprecated.js index 25dda5f780..a630c4d12f 100644 --- a/lib/rules/no-deprecated.js +++ b/lib/rules/no-deprecated.js @@ -76,27 +76,27 @@ module.exports = { deprecated[`${pragma}.PropTypes`] = ['15.5.0', 'the npm module prop-types']; // 15.6.0 deprecated[`${pragma}.DOM`] = ['15.6.0', 'the npm module react-dom-factories']; - // 16.999.0 + // 16.9.0 // For now the following life-cycle methods are just legacy, not deprecated: // `componentWillMount`, `componentWillReceiveProps`, `componentWillUpdate` // https://github.com/yannickcr/eslint-plugin-react/pull/1750#issuecomment-425975934 deprecated.componentWillMount = [ - '16.999.0', + '16.9.0', 'UNSAFE_componentWillMount', - 'https://reactjs.org/docs/react-component.html#unsafe_componentwillmount. ' + - 'Use https://github.com/reactjs/react-codemod#rename-unsafe-lifecycles to automatically update your components.' + 'https://reactjs.org/docs/react-component.html#unsafe_componentwillmount. ' + + 'Use https://github.com/reactjs/react-codemod#rename-unsafe-lifecycles to automatically update your components.' ]; deprecated.componentWillReceiveProps = [ - '16.999.0', + '16.9.0', 'UNSAFE_componentWillReceiveProps', - 'https://reactjs.org/docs/react-component.html#unsafe_componentwillreceiveprops. ' + - 'Use https://github.com/reactjs/react-codemod#rename-unsafe-lifecycles to automatically update your components.' + 'https://reactjs.org/docs/react-component.html#unsafe_componentwillreceiveprops. ' + + 'Use https://github.com/reactjs/react-codemod#rename-unsafe-lifecycles to automatically update your components.' ]; deprecated.componentWillUpdate = [ - '16.999.0', + '16.9.0', 'UNSAFE_componentWillUpdate', - 'https://reactjs.org/docs/react-component.html#unsafe_componentwillupdate. ' + - 'Use https://github.com/reactjs/react-codemod#rename-unsafe-lifecycles to automatically update your components.' + 'https://reactjs.org/docs/react-component.html#unsafe_componentwillupdate. ' + + 'Use https://github.com/reactjs/react-codemod#rename-unsafe-lifecycles to automatically update your components.' ]; return deprecated; } @@ -105,10 +105,10 @@ module.exports = { const deprecated = getDeprecated(); return ( - deprecated && - deprecated[method] && - deprecated[method][0] && - versionUtil.testReactVersion(context, deprecated[method][0]) + deprecated + && deprecated[method] + && deprecated[method][0] + && versionUtil.testReactVersion(context, deprecated[method][0]) ); } @@ -139,7 +139,7 @@ module.exports = { } values(MODULES).some((moduleNames) => { - moduleName = moduleNames.find(name => name === node.init.name); + moduleName = moduleNames.find((name) => name === node.init.name); return moduleName; }); @@ -153,7 +153,7 @@ module.exports = { */ function getLifeCycleMethods(node) { const properties = astUtil.getComponentProperties(node); - return properties.map(property => ({ + return properties.map((property) => ({ name: astUtil.getPropertyName(property), node: astUtil.getPropertyNameNode(property) })); @@ -166,7 +166,7 @@ module.exports = { function checkLifeCycleMethods(node) { if (utils.isES5Component(node) || utils.isES6Component(node)) { const methods = getLifeCycleMethods(node); - methods.forEach(method => checkDeprecation(node, method.name, method.node)); + methods.forEach((method) => checkDeprecation(node, method.name, method.node)); } } @@ -195,15 +195,15 @@ module.exports = { VariableDeclarator(node) { const reactModuleName = getReactModuleName(node); const isRequire = node.init && node.init.callee && node.init.callee.name === 'require'; - const isReactRequire = node.init && - node.init.arguments && - node.init.arguments.length && - typeof MODULES[node.init.arguments[0].value] !== 'undefined'; + const isReactRequire = node.init + && node.init.arguments + && node.init.arguments.length + && typeof MODULES[node.init.arguments[0].value] !== 'undefined'; const isDestructuring = node.id && node.id.type === 'ObjectPattern'; if ( - !(isDestructuring && reactModuleName) && - !(isDestructuring && isRequire && isReactRequire) + !(isDestructuring && reactModuleName) + && !(isDestructuring && isRequire && isReactRequire) ) { return; } diff --git a/lib/rules/no-direct-mutation-state.js b/lib/rules/no-direct-mutation-state.js index 30a88cfbff..816fbeb3d2 100644 --- a/lib/rules/no-direct-mutation-state.js +++ b/lib/rules/no-direct-mutation-state.js @@ -119,13 +119,13 @@ module.exports = { } }, - 'CallExpression:exit': function (node) { + 'CallExpression:exit'(node) { components.set(node, { inCallExpression: false }); }, - 'MethodDefinition:exit': function (node) { + 'MethodDefinition:exit'(node) { if (node.kind === 'constructor') { components.set(node, { inConstructor: false @@ -133,7 +133,7 @@ module.exports = { } }, - 'Program:exit': function () { + 'Program:exit'() { const list = components.list(); Object.keys(list).forEach((key) => { diff --git a/lib/rules/no-find-dom-node.js b/lib/rules/no-find-dom-node.js index f62e821136..ae3c5e3d6a 100644 --- a/lib/rules/no-find-dom-node.js +++ b/lib/rules/no-find-dom-node.js @@ -32,8 +32,8 @@ module.exports = { CallExpression(node) { const callee = node.callee; - const isfindDOMNode = (callee.name === 'findDOMNode') || - (callee.property && callee.property.name === 'findDOMNode'); + const isfindDOMNode = (callee.name === 'findDOMNode') + || (callee.property && callee.property.name === 'findDOMNode'); if (!isfindDOMNode) { return; } diff --git a/lib/rules/no-multi-comp.js b/lib/rules/no-multi-comp.js index a86662be58..304314bb3c 100644 --- a/lib/rules/no-multi-comp.js +++ b/lib/rules/no-multi-comp.js @@ -47,8 +47,8 @@ module.exports = { function isIgnored(component) { return ( ignoreStateless && ( - /Function/.test(component.node.type) || - utils.isPragmaComponentWrapper(component.node) + /Function/.test(component.node.type) + || utils.isPragmaComponentWrapper(component.node) ) ); } @@ -58,14 +58,14 @@ module.exports = { // -------------------------------------------------------------------------- return { - 'Program:exit': function () { + 'Program:exit'() { if (components.length() <= 1) { return; } const list = components.list(); - Object.keys(list).filter(component => !isIgnored(list[component])).forEach((component, i) => { + Object.keys(list).filter((component) => !isIgnored(list[component])).forEach((component, i) => { if (i >= 1) { context.report({ node: list[component].node, diff --git a/lib/rules/no-render-return-value.js b/lib/rules/no-render-return-value.js index d0349f3e01..b525590f10 100644 --- a/lib/rules/no-render-return-value.js +++ b/lib/rules/no-render-return-value.js @@ -47,18 +47,19 @@ module.exports = { } if ( - callee.object.type !== 'Identifier' || - !calleeObjectName.test(callee.object.name) || - callee.property.name !== 'render' + callee.object.type !== 'Identifier' + || !calleeObjectName.test(callee.object.name) + || callee.property.name !== 'render' ) { return; } if ( - parent.type === 'VariableDeclarator' || - parent.type === 'Property' || - parent.type === 'ReturnStatement' || - parent.type === 'ArrowFunctionExpression' + parent.type === 'VariableDeclarator' + || parent.type === 'Property' + || parent.type === 'ReturnStatement' + || parent.type === 'ArrowFunctionExpression' + || parent.type === 'AssignmentExpression' ) { context.report({ node: callee, diff --git a/lib/rules/no-set-state.js b/lib/rules/no-set-state.js index 7af70116f8..269cb6f6ed 100644 --- a/lib/rules/no-set-state.js +++ b/lib/rules/no-set-state.js @@ -57,9 +57,9 @@ module.exports = { CallExpression(node) { const callee = node.callee; if ( - callee.type !== 'MemberExpression' || - callee.object.type !== 'ThisExpression' || - callee.property.name !== 'setState' + callee.type !== 'MemberExpression' + || callee.object.type !== 'ThisExpression' + || callee.property.name !== 'setState' ) { return; } @@ -72,9 +72,9 @@ module.exports = { }); }, - 'Program:exit': function () { + 'Program:exit'() { const list = components.list(); - Object.keys(list).filter(component => !isValid(list[component])).forEach((component) => { + Object.keys(list).filter((component) => !isValid(list[component])).forEach((component) => { reportSetStateUsages(list[component]); }); } diff --git a/lib/rules/no-string-refs.js b/lib/rules/no-string-refs.js index 96ebc2e48c..b9ef0d9df3 100644 --- a/lib/rules/no-string-refs.js +++ b/lib/rules/no-string-refs.js @@ -41,11 +41,11 @@ module.exports = { function isRefsUsage(node) { return Boolean( ( - utils.getParentES6Component() || - utils.getParentES5Component() - ) && - node.object.type === 'ThisExpression' && - node.property.name === 'refs' + utils.getParentES6Component() + || utils.getParentES5Component() + ) + && node.object.type === 'ThisExpression' + && node.property.name === 'refs' ); } @@ -56,9 +56,9 @@ module.exports = { */ function isRefAttribute(node) { return Boolean( - node.type === 'JSXAttribute' && - node.name && - node.name.name === 'ref' + node.type === 'JSXAttribute' + && node.name + && node.name.name === 'ref' ); } @@ -69,9 +69,9 @@ module.exports = { */ function containsStringLiteral(node) { return Boolean( - node.value && - node.value.type === 'Literal' && - typeof node.value.value === 'string' + node.value + && node.value.type === 'Literal' + && typeof node.value.value === 'string' ); } @@ -82,11 +82,11 @@ module.exports = { */ function containsStringExpressionContainer(node) { return Boolean( - node.value && - node.value.type === 'JSXExpressionContainer' && - node.value.expression && - ((node.value.expression.type === 'Literal' && typeof node.value.expression.value === 'string') || - (node.value.expression.type === 'TemplateLiteral' && detectTemplateLiterals)) + node.value + && node.value.type === 'JSXExpressionContainer' + && node.value.expression + && ((node.value.expression.type === 'Literal' && typeof node.value.expression.value === 'string') + || (node.value.expression.type === 'TemplateLiteral' && detectTemplateLiterals)) ); } @@ -101,8 +101,8 @@ module.exports = { }, JSXAttribute(node) { if ( - isRefAttribute(node) && - (containsStringLiteral(node) || containsStringExpressionContainer(node)) + isRefAttribute(node) + && (containsStringLiteral(node) || containsStringExpressionContainer(node)) ) { context.report({ node, diff --git a/lib/rules/no-this-in-sfc.js b/lib/rules/no-this-in-sfc.js index df86ba55ca..54c1488414 100644 --- a/lib/rules/no-this-in-sfc.js +++ b/lib/rules/no-this-in-sfc.js @@ -11,7 +11,7 @@ const docsUrl = require('../util/docsUrl'); // Constants // ------------------------------------------------------------------------------ -const ERROR_MESSAGE = 'Stateless functional components should not use this'; +const ERROR_MESSAGE = 'Stateless functional components should not use `this`'; // ------------------------------------------------------------------------------ // Rule Definition diff --git a/lib/rules/no-typos.js b/lib/rules/no-typos.js index 96be82b943..1f59133b1c 100644 --- a/lib/rules/no-typos.js +++ b/lib/rules/no-typos.js @@ -13,6 +13,7 @@ const docsUrl = require('../util/docsUrl'); // ------------------------------------------------------------------------------ const STATIC_CLASS_PROPERTIES = ['propTypes', 'contextTypes', 'childContextTypes', 'defaultProps']; +const STATIC_LIFECYCLE_METHODS = ['getDerivedStateFromProps']; const LIFECYCLE_METHODS = [ 'getDerivedStateFromProps', 'componentWillMount', @@ -49,28 +50,30 @@ module.exports = { if (node.name !== 'isRequired') { context.report({ node, - message: `Typo in prop type chain qualifier: ${node.name}` + message: 'Typo in prop type chain qualifier: {{name}}', + data: {name: node.name} }); } } function checkValidPropType(node) { - if (node.name && !PROP_TYPES.some(propTypeName => propTypeName === node.name)) { + if (node.name && !PROP_TYPES.some((propTypeName) => propTypeName === node.name)) { context.report({ node, - message: `Typo in declared prop type: ${node.name}` + message: 'Typo in declared prop type: {{name}}', + data: {name: node.name} }); } } function isPropTypesPackage(node) { return ( - node.type === 'Identifier' && - node.name === propTypesPackageName + node.type === 'Identifier' + && node.name === propTypesPackageName ) || ( - node.type === 'MemberExpression' && - node.property.name === 'PropTypes' && - node.object.name === reactPackageName + node.type === 'MemberExpression' + && node.property.name === 'PropTypes' + && node.object.name === reactPackageName ); } @@ -97,14 +100,14 @@ module.exports = { if (node.type === 'MemberExpression') { if ( - node.object.type === 'MemberExpression' && - isPropTypesPackage(node.object.object) + node.object.type === 'MemberExpression' + && isPropTypesPackage(node.object.object) ) { // PropTypes.myProp.isRequired checkValidPropType(node.object.property); checkValidPropTypeQualifier(node.property); } else if ( - isPropTypesPackage(node.object) && - node.property.name !== 'isRequired' + isPropTypesPackage(node.object) + && node.property.name !== 'isRequired' ) { // PropTypes.myProp checkValidPropType(node.property); } else if (node.object.type === 'CallExpression') { @@ -120,21 +123,22 @@ module.exports = { function checkValidPropObject(node) { if (node && node.type === 'ObjectExpression') { - node.properties.forEach(prop => checkValidProp(prop.value)); + node.properties.forEach((prop) => checkValidProp(prop.value)); } } - function reportErrorIfPropertyCasingTypo(node, propertyName, isClassProperty) { + function reportErrorIfPropertyCasingTypo(propertyValue, propertyKey, isClassProperty) { + const propertyName = propertyKey.name; if (propertyName === 'propTypes' || propertyName === 'contextTypes' || propertyName === 'childContextTypes') { - checkValidPropObject(node); + checkValidPropObject(propertyValue); } STATIC_CLASS_PROPERTIES.forEach((CLASS_PROP) => { if (propertyName && CLASS_PROP.toLowerCase() === propertyName.toLowerCase() && CLASS_PROP !== propertyName) { - const message = isClassProperty ? - 'Typo in static class property declaration' : - 'Typo in property declaration'; + const message = isClassProperty + ? 'Typo in static class property declaration' + : 'Typo in property declaration'; context.report({ - node, + node: propertyKey, message }); } @@ -142,11 +146,26 @@ module.exports = { } function reportErrorIfLifecycleMethodCasingTypo(node) { + let nodeKeyName = node.key.name; + if (node.key.type === 'Literal') { + nodeKeyName = node.key.value; + } + + STATIC_LIFECYCLE_METHODS.forEach((method) => { + if (!node.static && nodeKeyName.toLowerCase() === method.toLowerCase()) { + context.report({ + node, + message: `Lifecycle method should be static: ${nodeKeyName}` + }); + } + }); + LIFECYCLE_METHODS.forEach((method) => { - if (method.toLowerCase() === node.key.name.toLowerCase() && method !== node.key.name) { + if (method.toLowerCase() === nodeKeyName.toLowerCase() && method !== nodeKeyName) { context.report({ node, - message: 'Typo in component lifecycle method declaration' + message: 'Typo in component lifecycle method declaration: {{actual}} should be {{expected}}', + data: {actual: nodeKeyName, expected: method} }); } }); @@ -161,7 +180,7 @@ module.exports = { reactPackageName = node.specifiers[0].local.name; // guard against accidental anonymous `import "react"` } if (node.specifiers.length >= 1) { - const propTypesSpecifier = node.specifiers.find(specifier => ( + const propTypesSpecifier = node.specifiers.find((specifier) => ( specifier.imported && specifier.imported.name === 'PropTypes' )); if (propTypesSpecifier) { @@ -176,17 +195,15 @@ module.exports = { return; } - const tokens = context.getFirstTokens(node, 2); - const propertyName = tokens[1].value; - reportErrorIfPropertyCasingTypo(node.value, propertyName, true); + reportErrorIfPropertyCasingTypo(node.value, node.key, true); }, MemberExpression(node) { const propertyName = node.property.name; if ( - !propertyName || - STATIC_CLASS_PROPERTIES.map(prop => prop.toLocaleLowerCase()).indexOf(propertyName.toLowerCase()) === -1 + !propertyName + || STATIC_CLASS_PROPERTIES.map((prop) => prop.toLocaleLowerCase()).indexOf(propertyName.toLowerCase()) === -1 ) { return; } @@ -194,11 +211,11 @@ module.exports = { const relatedComponent = utils.getRelatedComponent(node); if ( - relatedComponent && - (utils.isES6Component(relatedComponent.node) || utils.isReturningJSX(relatedComponent.node)) && - (node.parent && node.parent.type === 'AssignmentExpression' && node.parent.right) + relatedComponent + && (utils.isES6Component(relatedComponent.node) || utils.isReturningJSX(relatedComponent.node)) + && (node.parent && node.parent.type === 'AssignmentExpression' && node.parent.right) ) { - reportErrorIfPropertyCasingTypo(node.parent.right, propertyName, true); + reportErrorIfPropertyCasingTypo(node.parent.right, node.property, true); } }, @@ -218,7 +235,7 @@ module.exports = { } node.properties.forEach((property) => { - reportErrorIfPropertyCasingTypo(property.value, property.key.name, false); + reportErrorIfPropertyCasingTypo(property.value, property.key, false); reportErrorIfLifecycleMethodCasingTypo(property); }); } diff --git a/lib/rules/no-unescaped-entities.js b/lib/rules/no-unescaped-entities.js index b8f5e76e38..db3ca2c0e8 100644 --- a/lib/rules/no-unescaped-entities.js +++ b/lib/rules/no-unescaped-entities.js @@ -99,7 +99,7 @@ module.exports = { } else if (c === entities[j].char) { context.report({ loc: {line: i, column: start + index}, - message: `\`${entities[j].char}\` can be escaped with ${entities[j].alternatives.map(alt => `\`${alt}\``).join(', ')}.`, + message: `\`${entities[j].char}\` can be escaped with ${entities[j].alternatives.map((alt) => `\`${alt}\``).join(', ')}.`, node }); } @@ -109,7 +109,7 @@ module.exports = { } return { - 'Literal, JSXText': function (node) { + 'Literal, JSXText'(node) { if (jsxUtil.isJSX(node.parent)) { reportInvalidEntity(node); } diff --git a/lib/rules/no-unknown-property.js b/lib/rules/no-unknown-property.js index b3c643514d..18634951c3 100644 --- a/lib/rules/no-unknown-property.js +++ b/lib/rules/no-unknown-property.js @@ -6,6 +6,7 @@ 'use strict'; const docsUrl = require('../util/docsUrl'); +const versionUtil = require('../util/version'); // ------------------------------------------------------------------------------ // Constants @@ -117,7 +118,7 @@ const SVGDOM_ATTRIBUTE_NAMES = { const DOM_PROPERTY_NAMES = [ // Standard - 'acceptCharset', 'accessKey', 'allowFullScreen', 'allowTransparency', 'autoComplete', 'autoFocus', 'autoPlay', + 'acceptCharset', 'accessKey', 'allowFullScreen', 'autoComplete', 'autoFocus', 'autoPlay', 'cellPadding', 'cellSpacing', 'classID', 'className', 'colSpan', 'contentEditable', 'contextMenu', 'dateTime', 'encType', 'formAction', 'formEncType', 'formMethod', 'formNoValidate', 'formTarget', 'frameBorder', 'hrefLang', 'htmlFor', 'httpEquiv', 'inputMode', 'keyParams', 'keyType', 'marginHeight', 'marginWidth', @@ -133,6 +134,13 @@ const DOM_PROPERTY_NAMES = [ 'autoSave', 'itemProp', 'itemScope', 'itemType', 'itemRef', 'itemID' ]; +function getDOMPropertyNames(context) { + // this was removed in React v16.1+, see https://github.com/facebook/react/pull/10823 + if (!versionUtil.testReactVersion(context, '16.1.0')) { + return ['allowTransparency'].concat(DOM_PROPERTY_NAMES); + } + return DOM_PROPERTY_NAMES; +} // ------------------------------------------------------------------------------ // Helpers @@ -147,10 +155,10 @@ const tagConvention = /^[a-z][^-]*$/; function isTagName(node) { if (tagConvention.test(node.parent.name.name)) { // http://www.w3.org/TR/custom-elements/#type-extension-semantics - return !node.parent.attributes.some(attrNode => ( - attrNode.type === 'JSXAttribute' && - attrNode.name.type === 'JSXIdentifier' && - attrNode.name.name === 'is' + return !node.parent.attributes.some((attrNode) => ( + attrNode.type === 'JSXAttribute' + && attrNode.name.type === 'JSXIdentifier' + && attrNode.name.name === 'is' )); } return false; @@ -176,18 +184,19 @@ function getTagName(node) { */ function tagNameHasDot(node) { return !!( - node.parent && - node.parent.name && - node.parent.name.type === 'JSXMemberExpression' + node.parent + && node.parent.name + && node.parent.name.type === 'JSXMemberExpression' ); } /** * Get the standard name of the attribute. * @param {String} name - Name of the attribute. + * @param {String} context - eslint context * @returns {String} The standard name of the attribute. */ -function getStandardName(name) { +function getStandardName(name, context) { if (DOM_ATTRIBUTE_NAMES[name]) { return DOM_ATTRIBUTE_NAMES[name]; } @@ -195,11 +204,12 @@ function getStandardName(name) { return SVGDOM_ATTRIBUTE_NAMES[name]; } let i = -1; - const found = DOM_PROPERTY_NAMES.some((element, index) => { + const names = getDOMPropertyNames(context); + const found = names.some((element, index) => { i = index; return element.toLowerCase() === name; }); - return found ? DOM_PROPERTY_NAMES[i] : null; + return found ? names[i] : null; } // ------------------------------------------------------------------------------ @@ -262,7 +272,7 @@ module.exports = { }); } - const standardName = getStandardName(name); + const standardName = getStandardName(name, context); if (!isTagName(node) || !standardName) { return; } diff --git a/lib/rules/no-unsafe.js b/lib/rules/no-unsafe.js index 59b691b461..81c7eccacc 100644 --- a/lib/rules/no-unsafe.js +++ b/lib/rules/no-unsafe.js @@ -113,7 +113,7 @@ module.exports = { */ function getLifeCycleMethods(node) { const properties = astUtil.getComponentProperties(node); - return properties.map(property => astUtil.getPropertyName(property)); + return properties.map((property) => astUtil.getPropertyName(property)); } /** @@ -123,7 +123,7 @@ module.exports = { function checkLifeCycleMethods(node) { if (utils.isES5Component(node) || utils.isES6Component(node)) { const methods = getLifeCycleMethods(node); - methods.forEach(method => checkUnsafe(node, method)); + methods.forEach((method) => checkUnsafe(node, method)); } } diff --git a/lib/rules/no-unused-prop-types.js b/lib/rules/no-unused-prop-types.js index f7d9074656..f6a95aca0a 100644 --- a/lib/rules/no-unused-prop-types.js +++ b/lib/rules/no-unused-prop-types.js @@ -53,8 +53,8 @@ module.exports = { */ function mustBeValidated(component) { return Boolean( - component && - !component.ignoreUnusedPropTypesValidation + component + && !component.ignoreUnusedPropTypesValidation ); } @@ -69,9 +69,9 @@ module.exports = { for (let i = 0, l = usedPropTypes.length; i < l; i++) { const usedProp = usedPropTypes[i]; if ( - prop.type === 'shape' || - prop.name === '__ANY_KEY__' || - usedProp.name === prop.name + prop.type === 'shape' + || prop.name === '__ANY_KEY__' + || usedProp.name === prop.name ) { return true; } @@ -104,7 +104,7 @@ module.exports = { if (prop.node && !isPropUsed(component, prop)) { context.report({ - node: prop.node.value || prop.node, + node: prop.node.key || prop.node, message: UNUSED_MESSAGE, data: { name: prop.fullName @@ -131,10 +131,10 @@ module.exports = { // -------------------------------------------------------------------------- return { - 'Program:exit': function () { + 'Program:exit'() { const list = components.list(); // Report undeclared proptypes for all classes - Object.keys(list).filter(component => mustBeValidated(list[component])).forEach((component) => { + Object.keys(list).filter((component) => mustBeValidated(list[component])).forEach((component) => { if (!mustBeValidated(list[component])) { return; } diff --git a/lib/rules/no-unused-state.js b/lib/rules/no-unused-state.js index fcdc749810..22925f49bc 100644 --- a/lib/rules/no-unused-state.js +++ b/lib/rules/no-unused-state.js @@ -11,6 +11,7 @@ const Components = require('../util/Components'); const docsUrl = require('../util/docsUrl'); +const ast = require('../util/ast'); // Descend through all wrapping TypeCastExpressions and return the expression // that was cast. @@ -41,7 +42,7 @@ function getName(node) { } function isThisExpression(node) { - return uncast(node).type === 'ThisExpression'; + return ast.unwrapTSAsExpression(uncast(node)).type === 'ThisExpression'; } function getInitialClassInfo() { @@ -62,10 +63,12 @@ function getInitialClassInfo() { } function isSetStateCall(node) { + const unwrappedCalleeNode = ast.unwrapTSAsExpression(node.callee); + return ( - node.callee.type === 'MemberExpression' && - isThisExpression(node.callee.object) && - getName(node.callee.property) === 'setState' + unwrappedCalleeNode.type === 'MemberExpression' + && isThisExpression(unwrappedCalleeNode.object) + && getName(unwrappedCalleeNode.property) === 'setState' ); } @@ -100,14 +103,14 @@ module.exports = { while (scope) { const parent = scope.block && scope.block.parent; if ( - parent && - parent.type === 'MethodDefinition' && ( - parent.static && parent.key.name === 'getDerivedStateFromProps' || - classMethods.indexOf(parent.key.name) !== -1 - ) && - parent.value.type === 'FunctionExpression' && - parent.value.params[1] && - parent.value.params[1].name === node.name + parent + && parent.type === 'MethodDefinition' && ( + parent.static && parent.key.name === 'getDerivedStateFromProps' + || classMethods.indexOf(parent.key.name) !== -1 + ) + && parent.value.type === 'FunctionExpression' + && parent.value.params[1] + && parent.value.params[1].name === node.name ) { return true; } @@ -122,13 +125,13 @@ module.exports = { function isStateReference(node) { node = uncast(node); - const isDirectStateReference = node.type === 'MemberExpression' && - isThisExpression(node.object) && - node.property.name === 'state'; + const isDirectStateReference = node.type === 'MemberExpression' + && isThisExpression(node.object) + && node.property.name === 'state'; - const isAliasedStateReference = node.type === 'Identifier' && - classInfo.aliases && - classInfo.aliases.has(node.name); + const isAliasedStateReference = node.type === 'Identifier' + && classInfo.aliases + && classInfo.aliases.has(node.name); return isDirectStateReference || isAliasedStateReference || isStateParameterReference(node); } @@ -140,11 +143,11 @@ module.exports = { const key = prop.key; if ( - prop.type === 'Property' && - (key.type === 'Literal' || - (key.type === 'TemplateLiteral' && key.expressions.length === 0) || - (prop.computed === false && key.type === 'Identifier')) && - getName(prop.key) !== null + prop.type === 'Property' + && (key.type === 'Literal' + || (key.type === 'TemplateLiteral' && key.expressions.length === 0) + || (prop.computed === false && key.type === 'Identifier')) + && getName(prop.key) !== null ) { classInfo.stateFields.add(prop); } @@ -167,8 +170,8 @@ module.exports = { if (prop.type === 'Property') { addUsedStateField(prop.key); } else if ( - (prop.type === 'ExperimentalRestProperty' || prop.type === 'RestElement') && - classInfo.aliases + (prop.type === 'ExperimentalRestProperty' || prop.type === 'RestElement') + && classInfo.aliases ) { classInfo.aliases.add(getName(prop.argument)); } @@ -178,16 +181,18 @@ module.exports = { // Used to record used state fields and new aliases for both // AssignmentExpressions and VariableDeclarators. function handleAssignment(left, right) { + const unwrappedRight = ast.unwrapTSAsExpression(right); + switch (left.type) { case 'Identifier': - if (isStateReference(right) && classInfo.aliases) { + if (isStateReference(unwrappedRight) && classInfo.aliases) { classInfo.aliases.add(left.name); } break; case 'ObjectPattern': - if (isStateReference(right)) { + if (isStateReference(unwrappedRight)) { handleStateDestructuring(left); - } else if (isThisExpression(right) && classInfo.aliases) { + } else if (isThisExpression(unwrappedRight) && classInfo.aliases) { for (const prop of left.properties) { if (prop.type === 'Property' && getName(prop.key) === 'state') { const name = getName(prop.value); @@ -231,7 +236,7 @@ module.exports = { } }, - 'ObjectExpression:exit': function (node) { + 'ObjectExpression:exit'(node) { if (!classInfo) { return; } @@ -242,7 +247,7 @@ module.exports = { } }, - 'ClassDeclaration:exit': function () { + 'ClassDeclaration:exit'() { if (!classInfo) { return; } @@ -254,24 +259,30 @@ module.exports = { if (!classInfo) { return; } + + const unwrappedNode = ast.unwrapTSAsExpression(node); + const unwrappedArgumentNode = ast.unwrapTSAsExpression(unwrappedNode.arguments[0]); + // If we're looking at a `this.setState({})` invocation, record all the // properties as state fields. if ( - isSetStateCall(node) && - node.arguments.length > 0 && - node.arguments[0].type === 'ObjectExpression' + isSetStateCall(unwrappedNode) + && unwrappedNode.arguments.length > 0 + && unwrappedArgumentNode.type === 'ObjectExpression' ) { - addStateFields(node.arguments[0]); + addStateFields(unwrappedArgumentNode); } else if ( - isSetStateCall(node) && - node.arguments.length > 0 && - node.arguments[0].type === 'ArrowFunctionExpression' + isSetStateCall(unwrappedNode) + && unwrappedNode.arguments.length > 0 + && unwrappedArgumentNode.type === 'ArrowFunctionExpression' ) { - if (node.arguments[0].body.type === 'ObjectExpression') { - addStateFields(node.arguments[0].body); + const unwrappedBodyNode = ast.unwrapTSAsExpression(unwrappedArgumentNode.body); + + if (unwrappedBodyNode.type === 'ObjectExpression') { + addStateFields(unwrappedBodyNode); } - if (node.arguments[0].params.length > 0 && classInfo.aliases) { - const firstParam = node.arguments[0].params[0]; + if (unwrappedArgumentNode.params.length > 0 && classInfo.aliases) { + const firstParam = unwrappedArgumentNode.params[0]; if (firstParam.type === 'ObjectPattern') { handleStateDestructuring(firstParam); } else { @@ -287,31 +298,33 @@ module.exports = { } // If we see state being assigned as a class property using an object // expression, record all the fields of that object as state fields. + const unwrappedValueNode = ast.unwrapTSAsExpression(node.value); + if ( - getName(node.key) === 'state' && - !node.static && - node.value && - node.value.type === 'ObjectExpression' + getName(node.key) === 'state' + && !node.static + && unwrappedValueNode + && unwrappedValueNode.type === 'ObjectExpression' ) { - addStateFields(node.value); + addStateFields(unwrappedValueNode); } if ( - !node.static && - node.value && - node.value.type === 'ArrowFunctionExpression' + !node.static + && unwrappedValueNode + && unwrappedValueNode.type === 'ArrowFunctionExpression' ) { // Create a new set for this.state aliases local to this method. classInfo.aliases = new Set(); } }, - 'ClassProperty:exit': function (node) { + 'ClassProperty:exit'(node) { if ( - classInfo && - !node.static && - node.value && - node.value.type === 'ArrowFunctionExpression' + classInfo + && !node.static + && node.value + && node.value.type === 'ArrowFunctionExpression' ) { // Forget our set of local aliases. classInfo.aliases = null; @@ -326,7 +339,7 @@ module.exports = { classInfo.aliases = new Set(); }, - 'MethodDefinition:exit': function () { + 'MethodDefinition:exit'() { if (!classInfo) { return; } @@ -349,8 +362,8 @@ module.exports = { const lastBodyNode = body[body.length - 1]; if ( - lastBodyNode.type === 'ReturnStatement' && - lastBodyNode.argument.type === 'ObjectExpression' + lastBodyNode.type === 'ReturnStatement' + && lastBodyNode.argument.type === 'ObjectExpression' ) { addStateFields(lastBodyNode.argument); } @@ -364,12 +377,16 @@ module.exports = { if (!classInfo) { return; } + + const unwrappedLeft = ast.unwrapTSAsExpression(node.left); + const unwrappedRight = ast.unwrapTSAsExpression(node.right); + // Check for assignments like `this.state = {}` if ( - node.left.type === 'MemberExpression' && - isThisExpression(node.left.object) && - getName(node.left.property) === 'state' && - node.right.type === 'ObjectExpression' + unwrappedLeft.type === 'MemberExpression' + && isThisExpression(unwrappedLeft.object) + && getName(unwrappedLeft.property) === 'state' + && unwrappedRight.type === 'ObjectExpression' ) { // Find the nearest function expression containing this assignment. let fn = node; @@ -379,15 +396,15 @@ module.exports = { // If the nearest containing function is the constructor, then we want // to record all the assigned properties as state fields. if ( - fn.parent && - fn.parent.type === 'MethodDefinition' && - fn.parent.kind === 'constructor' + fn.parent + && fn.parent.type === 'MethodDefinition' + && fn.parent.kind === 'constructor' ) { - addStateFields(node.right); + addStateFields(unwrappedRight); } } else { // Check for assignments like `alias = this.state` and record the alias. - handleAssignment(node.left, node.right); + handleAssignment(unwrappedLeft, unwrappedRight); } }, @@ -398,11 +415,11 @@ module.exports = { handleAssignment(node.id, node.init); }, - MemberExpression(node) { + 'MemberExpression, OptionalMemberExpression'(node) { if (!classInfo) { return; } - if (isStateReference(node.object)) { + if (isStateReference(ast.unwrapTSAsExpression(node.object))) { // If we see this.state[foo] access, give up. if (node.computed && node.property.type !== 'Literal') { classInfo = null; @@ -422,7 +439,7 @@ module.exports = { } }, - 'ExperimentalSpreadProperty, SpreadElement': function (node) { + 'ExperimentalSpreadProperty, SpreadElement'(node) { if (classInfo && isStateReference(node.argument)) { classInfo = null; } diff --git a/lib/rules/no-will-update-set-state.js b/lib/rules/no-will-update-set-state.js index 0267227a73..4283fa036f 100644 --- a/lib/rules/no-will-update-set-state.js +++ b/lib/rules/no-will-update-set-state.js @@ -10,5 +10,5 @@ const versionUtil = require('../util/version'); module.exports = makeNoMethodSetStateRule( 'componentWillUpdate', - context => versionUtil.testReactVersion(context, '16.3.0') + (context) => versionUtil.testReactVersion(context, '16.3.0') ); diff --git a/lib/rules/prefer-read-only-props.js b/lib/rules/prefer-read-only-props.js index 225dac0b27..8349435f24 100644 --- a/lib/rules/prefer-read-only-props.js +++ b/lib/rules/prefer-read-only-props.js @@ -33,7 +33,7 @@ module.exports = { }, create: Components.detect((context, components) => ({ - 'Program:exit': function () { + 'Program:exit'() { const list = components.list(); Object.keys(list).forEach((key) => { diff --git a/lib/rules/prefer-stateless-function.js b/lib/rules/prefer-stateless-function.js index ce5637399e..2800de3e0d 100644 --- a/lib/rules/prefer-stateless-function.js +++ b/lib/rules/prefer-stateless-function.js @@ -52,10 +52,10 @@ module.exports = { */ function isSingleSuperCall(body) { return ( - body.length === 1 && - body[0].type === 'ExpressionStatement' && - body[0].expression.type === 'CallExpression' && - body[0].expression.callee.type === 'Super' + body.length === 1 + && body[0].type === 'ExpressionStatement' + && body[0].expression.type === 'CallExpression' + && body[0].expression.callee.type === 'Super' ); } @@ -79,10 +79,10 @@ module.exports = { */ function isSpreadArguments(superArgs) { return ( - superArgs.length === 1 && - superArgs[0].type === 'SpreadElement' && - superArgs[0].argument.type === 'Identifier' && - superArgs[0].argument.name === 'arguments' + superArgs.length === 1 + && superArgs[0].type === 'SpreadElement' + && superArgs[0].argument.type === 'Identifier' + && superArgs[0].argument.name === 'arguments' ); } @@ -96,9 +96,9 @@ module.exports = { */ function isValidIdentifierPair(ctorParam, superArg) { return ( - ctorParam.type === 'Identifier' && - superArg.type === 'Identifier' && - ctorParam.name === superArg.name + ctorParam.type === 'Identifier' + && superArg.type === 'Identifier' + && ctorParam.name === superArg.name ); } @@ -112,9 +112,9 @@ module.exports = { */ function isValidRestSpreadPair(ctorParam, superArg) { return ( - ctorParam.type === 'RestElement' && - superArg.type === 'SpreadElement' && - isValidIdentifierPair(ctorParam.argument, superArg.argument) + ctorParam.type === 'RestElement' + && superArg.type === 'SpreadElement' + && isValidIdentifierPair(ctorParam.argument, superArg.argument) ); } @@ -127,8 +127,8 @@ module.exports = { */ function isValidPair(ctorParam, superArg) { return ( - isValidIdentifierPair(ctorParam, superArg) || - isValidRestSpreadPair(ctorParam, superArg) + isValidIdentifierPair(ctorParam, superArg) + || isValidRestSpreadPair(ctorParam, superArg) ); } @@ -163,11 +163,11 @@ module.exports = { */ function isRedundantSuperCall(body, ctorParams) { return ( - isSingleSuperCall(body) && - ctorParams.every(isSimple) && - ( - isSpreadArguments(body[0].expression.arguments) || - isPassingThrough(ctorParams, body[0].expression.arguments) + isSingleSuperCall(body) + && ctorParams.every(isSimple) + && ( + isSpreadArguments(body[0].expression.arguments) + || isPassingThrough(ctorParams, body[0].expression.arguments) ) ); } @@ -185,8 +185,9 @@ module.exports = { const isPropTypes = name === 'propTypes' || name === 'props' && property.typeAnnotation; const contextTypes = name === 'contextTypes'; const defaultProps = name === 'defaultProps'; - const isUselessConstructor = property.kind === 'constructor' && - isRedundantSuperCall(property.value.body.body, property.value.params); + const isUselessConstructor = property.kind === 'constructor' + && !!property.value.body + && isRedundantSuperCall(property.value.body.body, property.value.params); const isRender = name === 'render'; return !isDisplayName && !isPropTypes && !contextTypes && !defaultProps && !isUselessConstructor && !isRender; }); @@ -196,21 +197,21 @@ module.exports = { * Mark component as pure as declared * @param {ASTNode} node The AST node being checked. */ - const markSCUAsDeclared = function (node) { + function markSCUAsDeclared(node) { components.set(node, { hasSCU: true }); - }; + } /** * Mark childContextTypes as declared * @param {ASTNode} node The AST node being checked. */ - const markChildContextTypesAsDeclared = function (node) { + function markChildContextTypesAsDeclared(node) { components.set(node, { hasChildContextTypes: true }); - }; + } /** * Mark a setState as used @@ -308,8 +309,8 @@ module.exports = { // Ignore calls to `this.props` and `this.context` } if ( - (node.property.name || node.property.value) === 'props' || - (node.property.name || node.property.value) === 'context' + (node.property.name || node.property.value) === 'props' + || (node.property.name || node.property.value) === 'context' ) { markPropsOrContextAsUsed(node); return; @@ -342,26 +343,26 @@ module.exports = { const isReturningJSX = utils.isReturningJSX(node, !allowNull); const isReturningNull = node.argument && (node.argument.value === null || node.argument.value === false); if ( - !isRender || - (allowNull && (isReturningJSX || isReturningNull)) || - (!allowNull && isReturningJSX) + !isRender + || (allowNull && (isReturningJSX || isReturningNull)) + || (!allowNull && isReturningJSX) ) { return; } markReturnAsInvalid(node); }, - 'Program:exit': function () { + 'Program:exit'() { const list = components.list(); Object.keys(list).forEach((component) => { if ( - hasOtherProperties(list[component].node) || - list[component].useThis || - list[component].useRef || - list[component].invalidReturn || - list[component].hasChildContextTypes || - list[component].useDecorators || - (!utils.isES5Component(list[component].node) && !utils.isES6Component(list[component].node)) + hasOtherProperties(list[component].node) + || list[component].useThis + || list[component].useRef + || list[component].invalidReturn + || list[component].hasChildContextTypes + || list[component].useDecorators + || (!utils.isES5Component(list[component].node) && !utils.isES6Component(list[component].node)) ) { return; } diff --git a/lib/rules/prop-types.js b/lib/rules/prop-types.js index c652e5bc12..b1538193fb 100644 --- a/lib/rules/prop-types.js +++ b/lib/rules/prop-types.js @@ -71,10 +71,10 @@ module.exports = { function mustBeValidated(component) { const isSkippedByConfig = skipUndeclared && typeof component.declaredPropTypes === 'undefined'; return Boolean( - component && - component.usedPropTypes && - !component.ignorePropsValidation && - !isSkippedByConfig + component + && component.usedPropTypes + && !component.ignorePropsValidation + && !isSkippedByConfig ); } @@ -90,8 +90,8 @@ module.exports = { const propType = ( declaredPropTypes && ( // Check if this key is declared - (declaredPropTypes[key] || // If not, check if this type accepts any key - declaredPropTypes.__ANY_KEY__) // eslint-disable-line no-underscore-dangle + (declaredPropTypes[key] // If not, check if this type accepts any key + || declaredPropTypes.__ANY_KEY__) // eslint-disable-line no-underscore-dangle ) ); @@ -103,7 +103,7 @@ module.exports = { return true; } // Consider every children as declared - if (propType.children === true || propType.containsSpread || propType.containsIndexers) { + if (propType.children === true || propType.containsUnresolvedSpread || propType.containsIndexers) { return true; } if (propType.acceptedProperties) { @@ -147,8 +147,8 @@ module.exports = { while (node) { const component = components.get(node); - const isDeclared = component && component.confidence === 2 && - internalIsDeclaredInComponent(component.declaredPropTypes || {}, names); + const isDeclared = component && component.confidence === 2 + && internalIsDeclaredInComponent(component.declaredPropTypes || {}, names); if (isDeclared) { return true; } @@ -162,10 +162,10 @@ module.exports = { * @param {Object} component The component to process */ function reportUndeclaredPropTypes(component) { - const undeclareds = component.usedPropTypes.filter(propType => ( - propType.node && - !isIgnored(propType.allNames[0]) && - !isDeclaredInComponent(component.node, propType.allNames) + const undeclareds = component.usedPropTypes.filter((propType) => ( + propType.node + && !isIgnored(propType.allNames[0]) + && !isDeclaredInComponent(component.node, propType.allNames) )); undeclareds.forEach((propType) => { context.report({ @@ -183,10 +183,10 @@ module.exports = { // -------------------------------------------------------------------------- return { - 'Program:exit': function () { + 'Program:exit'() { const list = components.list(); // Report undeclared proptypes for all classes - Object.keys(list).filter(component => mustBeValidated(list[component])).forEach((component) => { + Object.keys(list).filter((component) => mustBeValidated(list[component])).forEach((component) => { reportUndeclaredPropTypes(list[component]); }); } diff --git a/lib/rules/require-default-props.js b/lib/rules/require-default-props.js index 12a1515439..a1213f36e0 100644 --- a/lib/rules/require-default-props.js +++ b/lib/rules/require-default-props.js @@ -7,7 +7,7 @@ const Components = require('../util/Components'); const docsUrl = require('../util/docsUrl'); - +const astUtil = require('../util/ast'); // ------------------------------------------------------------------------------ // Rule Definition @@ -26,6 +26,9 @@ module.exports = { properties: { forbidDefaultForRequired: { type: 'boolean' + }, + ignoreFunctionalComponents: { + type: 'boolean' } }, additionalProperties: false @@ -35,7 +38,7 @@ module.exports = { create: Components.detect((context, components) => { const configuration = context.options[0] || {}; const forbidDefaultForRequired = configuration.forbidDefaultForRequired || false; - + const ignoreFunctionalComponents = configuration.ignoreFunctionalComponents || false; /** * Reports all propTypes passed in that don't have a defaultProps counterpart. @@ -80,10 +83,16 @@ module.exports = { // -------------------------------------------------------------------------- return { - 'Program:exit': function () { + 'Program:exit'() { const list = components.list(); - Object.keys(list).filter(component => list[component].declaredPropTypes).forEach((component) => { + Object.keys(list).filter((component) => { + if (ignoreFunctionalComponents + && (astUtil.isFunction(list[component].node) || astUtil.isFunctionLikeExpression(list[component].node))) { + return false; + } + return list[component].declaredPropTypes; + }).forEach((component) => { reportPropTypesWithoutDefault( list[component].declaredPropTypes, list[component].defaultProps || {} diff --git a/lib/rules/require-optimization.js b/lib/rules/require-optimization.js index 329354581d..54f633fd0c 100644 --- a/lib/rules/require-optimization.js +++ b/lib/rules/require-optimization.js @@ -41,19 +41,19 @@ module.exports = { * @param {ASTNode} node The AST node being checked. * @returns {Boolean} True if node is decorated with a PureRenderMixin, false if not. */ - const hasPureRenderDecorator = function (node) { + function hasPureRenderDecorator(node) { if (node.decorators && node.decorators.length) { for (let i = 0, l = node.decorators.length; i < l; i++) { if ( - node.decorators[i].expression && - node.decorators[i].expression.callee && - node.decorators[i].expression.callee.object && - node.decorators[i].expression.callee.object.name === 'reactMixin' && - node.decorators[i].expression.callee.property && - node.decorators[i].expression.callee.property.name === 'decorate' && - node.decorators[i].expression.arguments && - node.decorators[i].expression.arguments.length && - node.decorators[i].expression.arguments[0].name === 'PureRenderMixin' + node.decorators[i].expression + && node.decorators[i].expression.callee + && node.decorators[i].expression.callee.object + && node.decorators[i].expression.callee.object.name === 'reactMixin' + && node.decorators[i].expression.callee.property + && node.decorators[i].expression.callee.property.name === 'decorate' + && node.decorators[i].expression.arguments + && node.decorators[i].expression.arguments.length + && node.decorators[i].expression.arguments[0].name === 'PureRenderMixin' ) { return true; } @@ -61,22 +61,22 @@ module.exports = { } return false; - }; + } /** * Checks to see if our component is custom decorated * @param {ASTNode} node The AST node being checked. * @returns {Boolean} True if node is decorated name with a custom decorated, false if not. */ - const hasCustomDecorator = function (node) { + function hasCustomDecorator(node) { const allowLength = allowDecorators.length; if (allowLength && node.decorators && node.decorators.length) { for (let i = 0; i < allowLength; i++) { for (let j = 0, l = node.decorators.length; j < l; j++) { if ( - node.decorators[j].expression && - node.decorators[j].expression.name === allowDecorators[i] + node.decorators[j].expression + && node.decorators[j].expression.name === allowDecorators[i] ) { return true; } @@ -85,26 +85,26 @@ module.exports = { } return false; - }; + } /** * Checks if we are declaring a shouldComponentUpdate method * @param {ASTNode} node The AST node being checked. * @returns {Boolean} True if we are declaring a shouldComponentUpdate method, false if not. */ - const isSCUDeclared = function (node) { + function isSCUDeclared(node) { return Boolean( - node && - node.name === 'shouldComponentUpdate' + node + && node.name === 'shouldComponentUpdate' ); - }; + } /** * Checks if we are declaring a PureRenderMixin mixin * @param {ASTNode} node The AST node being checked. * @returns {Boolean} True if we are declaring a PureRenderMixin method, false if not. */ - const isPureRenderDeclared = function (node) { + function isPureRenderDeclared(node) { let hasPR = false; if (node.value && node.value.elements) { for (let i = 0, l = node.value.elements.length; i < l; i++) { @@ -116,27 +116,27 @@ module.exports = { } return Boolean( - node && - node.key.name === 'mixins' && - hasPR + node + && node.key.name === 'mixins' + && hasPR ); - }; + } /** * Mark shouldComponentUpdate as declared * @param {ASTNode} node The AST node being checked. */ - const markSCUAsDeclared = function (node) { + function markSCUAsDeclared(node) { components.set(node, { hasSCU: true }); - }; + } /** * Reports missing optimization for a given component * @param {Object} component The component to process */ - const reportMissingOptimization = function (component) { + function reportMissingOptimization(component) { context.report({ node: component.node, message: MISSING_MESSAGE, @@ -144,13 +144,13 @@ module.exports = { component: component.name } }); - }; + } /** * Checks if we are declaring function in class * @returns {Boolean} True if we are declaring function in class, false if not. */ - const isFunctionInClass = function () { + function isFunctionInClass() { let blockNode; let scope = context.getScope(); while (scope) { @@ -162,10 +162,14 @@ module.exports = { } return false; - }; + } return { ArrowFunctionExpression(node) { + // Skip if the function is declared in the class + if (isFunctionInClass()) { + return; + } // Stateless Functional Components cannot be optimized (yet) markSCUAsDeclared(node); }, @@ -204,20 +208,20 @@ module.exports = { ObjectExpression(node) { // Search for the shouldComponentUpdate declaration - const found = node.properties.some(property => ( - property.key && - (isSCUDeclared(property.key) || isPureRenderDeclared(property)) + const found = node.properties.some((property) => ( + property.key + && (isSCUDeclared(property.key) || isPureRenderDeclared(property)) )); if (found) { markSCUAsDeclared(node); } }, - 'Program:exit': function () { + 'Program:exit'() { const list = components.list(); // Report missing shouldComponentUpdate for all components - Object.keys(list).filter(component => !list[component].hasSCU).forEach((component) => { + Object.keys(list).filter((component) => !list[component].hasSCU).forEach((component) => { reportMissingOptimization(list[component]); }); } diff --git a/lib/rules/require-render-return.js b/lib/rules/require-render-return.js index c659e1f50d..106ae6b8fb 100644 --- a/lib/rules/require-render-return.js +++ b/lib/rules/require-render-return.js @@ -43,8 +43,8 @@ module.exports = { function findRenderMethod(node) { const properties = astUtil.getComponentProperties(node); return properties - .filter(property => astUtil.getPropertyName(property) === 'render' && property.value) - .find(property => astUtil.isFunctionLikeExpression(property.value)); + .filter((property) => astUtil.getPropertyName(property) === 'render' && property.value) + .find((property) => astUtil.isFunctionLikeExpression(property.value)); } return { @@ -56,9 +56,9 @@ module.exports = { depth++; } if ( - /(MethodDefinition|(Class)?Property)$/.test(ancestor.type) && - astUtil.getPropertyName(ancestor) === 'render' && - depth <= 1 + /(MethodDefinition|(Class)?Property)$/.test(ancestor.type) + && astUtil.getPropertyName(ancestor) === 'render' + && depth <= 1 ) { markReturnStatementPresent(node); } @@ -72,19 +72,19 @@ module.exports = { markReturnStatementPresent(node); }, - 'Program:exit': function () { + 'Program:exit'() { const list = components.list(); Object.keys(list).forEach((component) => { if ( - !findRenderMethod(list[component].node) || - list[component].hasReturnStatement || - (!utils.isES5Component(list[component].node) && !utils.isES6Component(list[component].node)) + !findRenderMethod(list[component].node) + || list[component].hasReturnStatement + || (!utils.isES5Component(list[component].node) && !utils.isES6Component(list[component].node)) ) { return; } context.report({ node: findRenderMethod(list[component].node), - message: 'Your render method should have return statement' + message: 'Your render method should have a return statement' }); }); } diff --git a/lib/rules/self-closing-comp.js b/lib/rules/self-closing-comp.js index 94ca78a374..8c8472f683 100644 --- a/lib/rules/self-closing-comp.js +++ b/lib/rules/self-closing-comp.js @@ -42,7 +42,11 @@ module.exports = { create(context) { function isComponent(node) { - return node.name && node.name.type === 'JSXIdentifier' && !jsxUtil.isDOMComponent(node); + return ( + node.name + && (node.name.type === 'JSXIdentifier' || node.name.type === 'JSXMemberExpression') + && !jsxUtil.isDOMComponent(node) + ); } function childrenIsEmpty(node) { @@ -53,18 +57,18 @@ module.exports = { const childrens = node.parent.children; return ( - childrens.length === 1 && - (childrens[0].type === 'Literal' || childrens[0].type === 'JSXText') && - childrens[0].value.indexOf('\n') !== -1 && - childrens[0].value.replace(/(?!\xA0)\s/g, '') === '' + childrens.length === 1 + && (childrens[0].type === 'Literal' || childrens[0].type === 'JSXText') + && childrens[0].value.indexOf('\n') !== -1 + && childrens[0].value.replace(/(?!\xA0)\s/g, '') === '' ); } function isShouldBeSelfClosed(node) { const configuration = Object.assign({}, optionDefaults, context.options[0]); return ( - configuration.component && isComponent(node) || - configuration.html && jsxUtil.isDOMComponent(node) + configuration.component && isComponent(node) + || configuration.html && jsxUtil.isDOMComponent(node) ) && !node.selfClosing && (childrenIsEmpty(node) || childrenIsMultilineSpaces(node)); } diff --git a/lib/rules/sort-comp.js b/lib/rules/sort-comp.js index 293b830f8f..7d1f725866 100644 --- a/lib/rules/sort-comp.js +++ b/lib/rules/sort-comp.js @@ -125,7 +125,7 @@ module.exports = { // Public // -------------------------------------------------------------------------- - const regExpRegExp = /\/(.*)\/([g|y|i|m]*)/; + const regExpRegExp = /\/(.*)\/([gimsuy]*)/; /** * Get indexes of the matching patterns in methods order configuration @@ -148,8 +148,12 @@ module.exports = { if (method.typeAnnotation) { methodGroupIndexes.push(groupIndex); } + } else if (currentGroup === 'static-variables') { + if (method.staticVariable) { + methodGroupIndexes.push(groupIndex); + } } else if (currentGroup === 'static-methods') { - if (method.static) { + if (method.staticMethod) { methodGroupIndexes.push(groupIndex); } } else if (currentGroup === 'instance-variables') { @@ -348,11 +352,11 @@ module.exports = { if ( // Comparing the same properties - refIndexA === refIndexB || + refIndexA === refIndexB // 1st property is placed before the 2nd one in reference and in current component - refIndexA < refIndexB && classIndexA < classIndexB || + || refIndexA < refIndexB && classIndexA < classIndexB // 1st property is placed after the 2nd one in reference and in current component - refIndexA > refIndexB && classIndexA > classIndexB + || refIndexA > refIndexB && classIndexA > classIndexB ) { return { correct: true, @@ -376,18 +380,24 @@ module.exports = { * @param {Array} properties Array containing all the properties. */ function checkPropsOrder(properties) { - const propertiesInfos = properties.map(node => ({ + const propertiesInfos = properties.map((node) => ({ name: getPropertyName(node), getter: node.kind === 'get', setter: node.kind === 'set', - static: node.static, - instanceVariable: !node.static && - node.type === 'ClassProperty' && - (!node.value || !astUtil.isFunctionLikeExpression(node.value)), - instanceMethod: !node.static && - node.type === 'ClassProperty' && - node.value && - (astUtil.isFunctionLikeExpression(node.value)), + staticVariable: node.static + && node.type === 'ClassProperty' + && (!node.value || !astUtil.isFunctionLikeExpression(node.value)), + staticMethod: node.static + && (node.type === 'ClassProperty' || node.type === 'MethodDefinition') + && node.value + && (astUtil.isFunctionLikeExpression(node.value)), + instanceVariable: !node.static + && node.type === 'ClassProperty' + && (!node.value || !astUtil.isFunctionLikeExpression(node.value)), + instanceMethod: !node.static + && node.type === 'ClassProperty' + && node.value + && (astUtil.isFunctionLikeExpression(node.value)), typeAnnotation: !!node.typeAnnotation && node.value === null })); @@ -417,7 +427,7 @@ module.exports = { } return { - 'Program:exit': function () { + 'Program:exit'() { const list = components.list(); Object.keys(list).forEach((component) => { const properties = astUtil.getComponentProperties(list[component].node); diff --git a/lib/rules/sort-prop-types.js b/lib/rules/sort-prop-types.js index 8589d317fc..9dbf13ad1a 100644 --- a/lib/rules/sort-prop-types.js +++ b/lib/rules/sort-prop-types.js @@ -8,6 +8,7 @@ const variableUtil = require('../util/variable'); const propsUtil = require('../util/props'); const docsUrl = require('../util/docsUrl'); const propWrapperUtil = require('../util/propWrapper'); +// const propTypesSortUtil = require('../util/propTypesSort'); // ------------------------------------------------------------------------------ // Rule Definition @@ -22,7 +23,7 @@ module.exports = { url: docsUrl('sort-prop-types') }, - fixable: 'code', + // fixable: 'code', schema: [{ type: 'object', @@ -81,50 +82,10 @@ module.exports = { ); } - function getShapeProperties(node) { - return node.arguments && node.arguments[0] && node.arguments[0].properties; - } - function toLowerCase(item) { return String(item).toLowerCase(); } - function sorter(a, b) { - let aKey = getKey(a); - let bKey = getKey(b); - if (requiredFirst) { - if (isRequiredProp(a) && !isRequiredProp(b)) { - return -1; - } - if (!isRequiredProp(a) && isRequiredProp(b)) { - return 1; - } - } - - if (callbacksLast) { - if (isCallbackPropName(aKey) && !isCallbackPropName(bKey)) { - return 1; - } - if (!isCallbackPropName(aKey) && isCallbackPropName(bKey)) { - return -1; - } - } - - if (ignoreCase) { - aKey = toLowerCase(aKey); - bKey = toLowerCase(bKey); - } - - if (aKey < bKey) { - return -1; - } - if (aKey > bKey) { - return 1; - } - return 0; - } - - /** * Checks if propTypes declarations are sorted * @param {Array} declarations The array of AST nodes being checked. @@ -137,47 +98,17 @@ module.exports = { return; } - function fix(fixer) { - function sortInSource(allNodes, source) { - const originalSource = source; - const nodeGroups = allNodes.reduce((acc, curr) => { - if (curr.type === 'ExperimentalSpreadProperty' || curr.type === 'SpreadElement') { - acc.push([]); - } else { - acc[acc.length - 1].push(curr); - } - return acc; - }, [[]]); - - nodeGroups.forEach((nodes) => { - const sortedAttributes = nodes.slice().sort(sorter); - - for (let i = nodes.length - 1; i >= 0; i--) { - const sortedAttr = sortedAttributes[i]; - const attr = nodes[i]; - let sortedAttrText = context.getSourceCode().getText(sortedAttr); - if (sortShapeProp && isShapeProp(sortedAttr.value)) { - const shape = getShapeProperties(sortedAttr.value); - if (shape) { - const attrSource = sortInSource( - shape, - originalSource - ); - sortedAttrText = attrSource.slice(sortedAttr.range[0], sortedAttr.range[1]); - } - } - source = `${source.slice(0, attr.range[0])}${sortedAttrText}${source.slice(attr.range[1])}`; - } - }); - return source; - } - - const source = sortInSource(declarations, context.getSourceCode().getText()); - - const rangeStart = declarations[0].range[0]; - const rangeEnd = declarations[declarations.length - 1].range[1]; - return fixer.replaceTextRange([rangeStart, rangeEnd], source.slice(rangeStart, rangeEnd)); - } + // function fix(fixer) { + // return propTypesSortUtil.fixPropTypesSort( + // fixer, + // context, + // declarations, + // ignoreCase, + // requiredFirst, + // callbacksLast, + // sortShapeProp + // ); + // } declarations.reduce((prev, curr, idx, decls) => { if (curr.type === 'ExperimentalSpreadProperty' || curr.type === 'SpreadElement') { @@ -205,8 +136,8 @@ module.exports = { // Encountered a non-required prop after a required prop context.report({ node: curr, - message: 'Required prop types must be listed before all other prop types', - fix + message: 'Required prop types must be listed before all other prop types' + // fix }); return curr; } @@ -221,8 +152,8 @@ module.exports = { // Encountered a non-callback prop after a callback prop context.report({ node: prev, - message: 'Callback prop types must be listed after all other prop types', - fix + message: 'Callback prop types must be listed after all other prop types' + // fix }); return prev; } @@ -231,8 +162,8 @@ module.exports = { if (!noSortAlphabetically && currentPropName < prevPropName) { context.report({ node: curr, - message: 'Prop types declarations should be sorted alphabetically', - fix + message: 'Prop types declarations should be sorted alphabetically' + // fix }); return prev; } diff --git a/lib/rules/state-in-constructor.js b/lib/rules/state-in-constructor.js index 91a5d5d9db..f8f878051d 100644 --- a/lib/rules/state-in-constructor.js +++ b/lib/rules/state-in-constructor.js @@ -30,10 +30,10 @@ module.exports = { return { ClassProperty(node) { if ( - option === 'always' && - !node.static && - node.key.name === 'state' && - utils.getParentES6Component() + option === 'always' + && !node.static + && node.key.name === 'state' + && utils.getParentES6Component() ) { context.report({ node, @@ -43,10 +43,10 @@ module.exports = { }, AssignmentExpression(node) { if ( - option === 'never' && - utils.isStateMemberExpression(node.left) && - utils.inConstructor() && - utils.getParentES6Component() + option === 'never' + && utils.isStateMemberExpression(node.left) + && utils.inConstructor() + && utils.getParentES6Component() ) { context.report({ node, diff --git a/lib/rules/static-property-placement.js b/lib/rules/static-property-placement.js index 8e2a89a55a..69bc6b0d98 100644 --- a/lib/rules/static-property-placement.js +++ b/lib/rules/static-property-placement.js @@ -37,11 +37,11 @@ const propertiesToCheck = { childContextTypes: propsUtil.isChildContextTypesDeclaration, contextTypes: propsUtil.isContextTypesDeclaration, contextType: propsUtil.isContextTypeDeclaration, - displayName: node => propsUtil.isDisplayNameDeclaration(astUtil.getPropertyNameNode(node)) + displayName: (node) => propsUtil.isDisplayNameDeclaration(astUtil.getPropertyNameNode(node)) }; const classProperties = Object.keys(propertiesToCheck); -const schemaProperties = fromEntries(classProperties.map(property => [property, {enum: POSITION_SETTINGS}])); +const schemaProperties = fromEntries(classProperties.map((property) => [property, {enum: POSITION_SETTINGS}])); // ------------------------------------------------------------------------------ // Rule Definition @@ -74,7 +74,7 @@ module.exports = { const additionalConfig = hasAdditionalConfig ? options[1] : {}; // Set config - const config = fromEntries(classProperties.map(property => [ + const config = fromEntries(classProperties.map((property) => [ property, additionalConfig[property] || defaultCheckType ])); @@ -131,7 +131,7 @@ module.exports = { // Public // ---------------------------------------------------------------------- return { - ClassProperty: node => reportNodeIncorrectlyPositioned(node, STATIC_PUBLIC_FIELD), + ClassProperty: (node) => reportNodeIncorrectlyPositioned(node, STATIC_PUBLIC_FIELD), MemberExpression: (node) => { // If definition type is undefined then it must not be a defining expression or if the definition is inside a diff --git a/lib/rules/style-prop-object.js b/lib/rules/style-prop-object.js index 33fc1a40ea..0e6f968b18 100644 --- a/lib/rules/style-prop-object.js +++ b/lib/rules/style-prop-object.js @@ -20,12 +20,29 @@ module.exports = { recommended: false, url: docsUrl('style-prop-object') }, - schema: [] + schema: [ + { + type: 'object', + properties: { + allow: { + type: 'array', + items: { + type: 'string' + }, + additionalItems: false, + uniqueItems: true + } + } + } + ] }, create(context) { + const allowed = new Set(context.options.length > 0 && context.options[0].allow || []); + /** * @param {ASTNode} expression An Identifier node + * @returns {boolean} */ function isNonNullaryLiteral(expression) { return expression.type === 'Literal' && expression.value !== null; @@ -35,7 +52,7 @@ module.exports = { * @param {object} node A Identifier node */ function checkIdentifiers(node) { - const variable = variableUtil.variablesInScope(context).find(item => item.name === node.name); + const variable = variableUtil.variablesInScope(context).find((item) => item.name === node.name); if (!variable || !variable.defs[0] || !variable.defs[0].node.init) { return; @@ -52,13 +69,23 @@ module.exports = { return { CallExpression(node) { if ( - node.callee && - node.callee.type === 'MemberExpression' && - node.callee.property.name === 'createElement' && - node.arguments.length > 1 + node.callee + && node.callee.type === 'MemberExpression' + && node.callee.property.name === 'createElement' + && node.arguments.length > 1 ) { + if (node.arguments[0].name) { + // store name of component + const componentName = node.arguments[0].name; + + // allowed list contains the name + if (allowed.has(componentName)) { + // abort operation + return; + } + } if (node.arguments[1].type === 'ObjectExpression') { - const style = node.arguments[1].properties.find(property => property.key && property.key.name === 'style' && !property.computed); + const style = node.arguments[1].properties.find((property) => property.key && property.key.name === 'style' && !property.computed); if (style) { if (style.value.type === 'Identifier') { checkIdentifiers(style.value); @@ -77,6 +104,20 @@ module.exports = { if (!node.value || node.name.name !== 'style') { return; } + // store parent element + const parentElement = node.parent; + + // parent element is a JSXOpeningElement + if (parentElement && parentElement.type === 'JSXOpeningElement') { + // get the name of the JSX element + const name = parentElement.name && parentElement.name.name; + + // allowed list contains the name + if (allowed.has(name)) { + // abort operation + return; + } + } if (node.value.type !== 'JSXExpressionContainer' || isNonNullaryLiteral(node.value.expression)) { context.report({ diff --git a/lib/rules/void-dom-elements-no-children.js b/lib/rules/void-dom-elements-no-children.js index e4e1ce6643..d25cc49dde 100644 --- a/lib/rules/void-dom-elements-no-children.js +++ b/lib/rules/void-dom-elements-no-children.js @@ -51,7 +51,7 @@ function errorMessage(elementName) { module.exports = { meta: { docs: { - description: 'Prevent passing of children to void DOM elements (e.g.
).', + description: 'Prevent passing of children to void DOM elements (e.g. `
`).', category: 'Best Practices', recommended: false, url: docsUrl('void-dom-elements-no-children') diff --git a/lib/types.d.ts b/lib/types.d.ts index d1ae21f948..7e22f2788b 100644 --- a/lib/types.d.ts +++ b/lib/types.d.ts @@ -9,6 +9,8 @@ declare global { type Token = eslint.AST.Token; type Fixer = eslint.Rule.RuleFixer; type JSXAttribute = ASTNode; + type JSXElement = ASTNode; + type JSXFragment = ASTNode; type JSXSpreadAttribute = ASTNode; interface Context extends eslint.SourceCode { @@ -21,9 +23,8 @@ declare global { [k in string]: TypeDeclarationBuilder; }; - type UnionTypeDefinitionChildren = unknown[]; type UnionTypeDefinition = { type: 'union' | 'shape'; - children: UnionTypeDefinitionChildren | true; + children: unknown[]; }; } diff --git a/lib/util/Components.js b/lib/util/Components.js index 04d36be260..3ded3e39f4 100644 --- a/lib/util/Components.js +++ b/lib/util/Components.js @@ -7,6 +7,7 @@ const doctrine = require('doctrine'); const arrayIncludes = require('array-includes'); +const values = require('object.values'); const variableUtil = require('./variable'); const pragmaUtil = require('./pragma'); @@ -36,7 +37,7 @@ function usedPropTypesAreEquivalent(propA, propB) { function mergeUsedPropTypes(propsList, newPropsList) { const propsToAdd = []; newPropsList.forEach((newProp) => { - const newPropisAlreadyInTheList = propsList.some(prop => usedPropTypesAreEquivalent(prop, newProp)); + const newPropisAlreadyInTheList = propsList.some((prop) => usedPropTypesAreEquivalent(prop, newProp)); if (!newPropisAlreadyInTheList) { propsToAdd.push(newProp); } @@ -45,6 +46,30 @@ function mergeUsedPropTypes(propsList, newPropsList) { return propsList.concat(propsToAdd); } +function isReturnsConditionalJSX(node, property, strict) { + const returnsConditionalJSXConsequent = node[property] + && node[property].type === 'ConditionalExpression' + && jsxUtil.isJSX(node[property].consequent); + const returnsConditionalJSXAlternate = node[property] + && node[property].type === 'ConditionalExpression' + && jsxUtil.isJSX(node[property].alternate); + return strict + ? (returnsConditionalJSXConsequent && returnsConditionalJSXAlternate) + : (returnsConditionalJSXConsequent || returnsConditionalJSXAlternate); +} + +function isReturnsLogicalJSX(node, property, strict) { + const returnsLogicalJSXLeft = node[property] + && node[property].type === 'LogicalExpression' + && jsxUtil.isJSX(node[property].left); + const returnsLogicalJSXRight = node[property] + && node[property].type === 'LogicalExpression' + && jsxUtil.isJSX(node[property].right); + return strict + ? (returnsLogicalJSXLeft && returnsLogicalJSXRight) + : (returnsLogicalJSXLeft || returnsLogicalJSXRight); +} + const Lists = new WeakMap(); /** @@ -136,7 +161,7 @@ class Components { const usedPropTypes = {}; // Find props used in components for which we are not confident - Object.keys(thisList).filter(i => thisList[i].confidence < 2).forEach((i) => { + Object.keys(thisList).filter((i) => thisList[i].confidence < 2).forEach((i) => { let component = null; let node = null; node = thisList[i].node; @@ -149,7 +174,7 @@ class Components { component = this.get(node); } if (component) { - const newUsedProps = (thisList[i].usedPropTypes || []).filter(propType => !propType.node || propType.node.kind !== 'init'); + const newUsedProps = (thisList[i].usedPropTypes || []).filter((propType) => !propType.node || propType.node.kind !== 'init'); const componentId = getId(component.node); @@ -158,7 +183,7 @@ class Components { }); // Assign used props in not confident components to the parent component - Object.keys(thisList).filter(j => thisList[j].confidence >= 2).forEach((j) => { + Object.keys(thisList).filter((j) => thisList[j].confidence >= 2).forEach((j) => { const id = getId(thisList[j].node); list[j] = thisList[j]; if (usedPropTypes[id]) { @@ -176,7 +201,7 @@ class Components { */ length() { const list = Lists.get(this); - return Object.keys(list).filter(i => list[i].confidence >= 2).length; + return Object.keys(list).filter((i) => list[i].confidence >= 2).length; } } @@ -246,7 +271,7 @@ function componentRule(rule, context) { tags: ['extends', 'augments'] }); - const relevantTags = commentAst.tags.filter(tag => tag.name === 'React.Component' || tag.name === 'React.PureComponent'); + const relevantTags = commentAst.tags.filter((tag) => tag.name === 'React.Component' || tag.name === 'React.PureComponent'); return relevantTags.length > 0; }, @@ -288,18 +313,18 @@ function componentRule(rule, context) { */ isCreateElement(node) { const calledOnPragma = ( - node && - node.callee && - node.callee.object && - node.callee.object.name === pragma && - node.callee.property && - node.callee.property.name === 'createElement' + node + && node.callee + && node.callee.object + && node.callee.object.name === pragma + && node.callee.property + && node.callee.property.name === 'createElement' ); const calledDirectly = ( - node && - node.callee && - node.callee.name === 'createElement' + node + && node.callee + && node.callee.name === 'createElement' ); if (this.isDestructuredFromPragmaImport('createElement')) { @@ -372,24 +397,17 @@ function componentRule(rule, context) { return false; } - const returnsConditionalJSXConsequent = node[property] && - node[property].type === 'ConditionalExpression' && - jsxUtil.isJSX(node[property].consequent); - const returnsConditionalJSXAlternate = node[property] && - node[property].type === 'ConditionalExpression' && - jsxUtil.isJSX(node[property].alternate); - const returnsConditionalJSX = strict ? - (returnsConditionalJSXConsequent && returnsConditionalJSXAlternate) : - (returnsConditionalJSXConsequent || returnsConditionalJSXAlternate); + const returnsConditionalJSX = isReturnsConditionalJSX(node, property, strict); + const returnsLogicalJSX = isReturnsLogicalJSX(node, property, strict); - const returnsJSX = node[property] && - jsxUtil.isJSX(node[property]); + const returnsJSX = node[property] && jsxUtil.isJSX(node[property]); const returnsPragmaCreateElement = this.isCreateElement(node[property]); - return Boolean( - returnsConditionalJSX || - returnsJSX || - returnsPragmaCreateElement + return !!( + returnsConditionalJSX + || returnsLogicalJSX + || returnsJSX + || returnsPragmaCreateElement ); }, @@ -414,7 +432,7 @@ function componentRule(rule, context) { /** * Check if the node is returning JSX or null * - * @param {ASTNode} ASTnode The AST node being checked + * @param {ASTNode} ASTNode The AST node being checked * @param {Boolean} [strict] If true, in a ternary condition the node must return JSX in both cases * @returns {Boolean} True if the node is returning JSX or null, false if not */ @@ -437,6 +455,78 @@ function componentRule(rule, context) { return prevNode; }, + getComponentNameFromJSXElement(node) { + if (node.type !== 'JSXElement') { + return null; + } + if (node.openingElement && node.openingElement.name && node.openingElement.name.name) { + return node.openingElement.name.name; + } + return null; + }, + + /** + * Getting the first JSX element's name. + * @param {object} node + * @returns {string | null} + */ + getNameOfWrappedComponent(node) { + if (node.length < 1) { + return null; + } + const body = node[0].body; + if (!body) { + return null; + } + if (body.type === 'JSXElement') { + return this.getComponentNameFromJSXElement(body); + } + if (body.type === 'BlockStatement') { + const jsxElement = body.body.find((item) => item.type === 'ReturnStatement'); + return jsxElement + && jsxElement.argument + && this.getComponentNameFromJSXElement(jsxElement.argument); + } + return null; + }, + + /** + * Get the list of names of components created till now + * @returns {string | boolean} + */ + getDetectedComponents() { + const list = components.list(); + return values(list).filter((val) => { + if (val.node.type === 'ClassDeclaration') { + return true; + } + if ( + val.node.type === 'ArrowFunctionExpression' + && val.node.parent + && val.node.parent.type === 'VariableDeclarator' + && val.node.parent.id + ) { + return true; + } + return false; + }).map((val) => { + if (val.node.type === 'ArrowFunctionExpression') return val.node.parent.id.name; + return val.node.id.name; + }); + }, + + /** + * It will check wheater memo/forwardRef is wrapping existing component or + * creating a new one. + * @param {object} node + * @returns {boolean} + */ + nodeWrapsComponent(node) { + const childComponent = this.getNameOfWrappedComponent(node.arguments); + const componentList = this.getDetectedComponents(); + return !!childComponent && arrayIncludes(componentList, childComponent); + }, + isPragmaComponentWrapper(node) { if (!node || node.type !== 'CallExpression') { return false; @@ -444,7 +534,9 @@ function componentRule(rule, context) { const propertyNames = ['forwardRef', 'memo']; const calleeObject = node.callee.object; if (calleeObject && node.callee.property) { - return arrayIncludes(propertyNames, node.callee.property.name) && calleeObject.name === pragma; + return arrayIncludes(propertyNames, node.callee.property.name) + && calleeObject.name === pragma + && !this.nodeWrapsComponent(node); } return arrayIncludes(propertyNames, node.callee.name) && this.isDestructuredFromPragmaImport(node.callee.name); }, @@ -463,9 +555,9 @@ function componentRule(rule, context) { */ getParentComponent() { return ( - utils.getParentES6Component() || - utils.getParentES5Component() || - utils.getParentStatelessComponent() + utils.getParentES6Component() + || utils.getParentES5Component() + || utils.getParentStatelessComponent() ); }, @@ -504,50 +596,67 @@ function componentRule(rule, context) { }, /** - * Get the parent stateless component node from the current scope - * - * @returns {ASTNode} component node, null if we are not in a component + * @param {ASTNode} node + * @returns {boolean} */ - getParentStatelessComponent() { - let scope = context.getScope(); - while (scope) { - const node = scope.block; - const isFunction = /Function/.test(node.type); // Functions - const isArrowFunction = astUtil.isArrowFunction(node); - const enclosingScope = isArrowFunction ? utils.getArrowFunctionScope(scope) : scope; - const enclosingScopeParent = enclosingScope && enclosingScope.block.parent; - const isClass = enclosingScope && astUtil.isClass(enclosingScope.block); - const isMethod = enclosingScopeParent && enclosingScopeParent.type === 'MethodDefinition'; // Classes methods - const isArgument = node.parent && node.parent.type === 'CallExpression'; // Arguments (callback, etc.) - // Attribute Expressions inside JSX Elements () - const isJSXExpressionContainer = node.parent && node.parent.type === 'JSXExpressionContainer'; - const pragmaComponentWrapper = this.getPragmaComponentWrapper(node); - if (isFunction && pragmaComponentWrapper) { - return pragmaComponentWrapper; + isInAllowedPositionForComponent(node) { + switch (node.parent.type) { + case 'VariableDeclarator': + case 'AssignmentExpression': + case 'Property': + case 'ReturnStatement': + case 'ExportDefaultDeclaration': { + return true; } - // Stop moving up if we reach a class or an argument (like a callback) - if (isClass || isArgument) { - return null; + case 'SequenceExpression': { + return utils.isInAllowedPositionForComponent(node.parent) + && node === node.parent.expressions[node.parent.expressions.length - 1]; } - // Return the node if it is a function that is not a class method and is not inside a JSX Element - if (isFunction && !isMethod && !isJSXExpressionContainer && utils.isReturningJSXOrNull(node)) { + default: + return false; + } + }, + + /** + * Get node if node is a stateless component, or node.parent in cases like + * `React.memo` or `React.forwardRef`. Otherwise returns `undefined`. + * @param {ASTNode} node + * @returns {ASTNode | undefined} + */ + getStatelessComponent(node) { + if (node.type === 'FunctionDeclaration') { + if (utils.isReturningJSXOrNull(node)) { return node; } - scope = scope.upper; } - return null; + + if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') { + if (utils.isInAllowedPositionForComponent(node) && utils.isReturningJSXOrNull(node)) { + return node; + } + + // Case like `React.memo(() => <>)` or `React.forwardRef(...)` + const pragmaComponentWrapper = utils.getPragmaComponentWrapper(node); + if (pragmaComponentWrapper) { + return pragmaComponentWrapper; + } + } + + return undefined; }, /** - * Get an enclosing scope used to find `this` value by an arrow function - * @param {Scope} scope Current scope - * @returns {Scope} An enclosing scope used by an arrow function + * Get the parent stateless component node from the current scope + * + * @returns {ASTNode} component node, null if we are not in a component */ - getArrowFunctionScope(scope) { - scope = scope.upper; + getParentStatelessComponent() { + let scope = context.getScope(); while (scope) { - if (astUtil.isFunction(scope.block) || astUtil.isClass(scope.block)) { - return scope; + const node = scope.block; + const statelessComponent = utils.getStatelessComponent(node); + if (statelessComponent) { + return statelessComponent; } scope = scope.upper; } @@ -610,10 +719,10 @@ function componentRule(rule, context) { if (refId.type === 'MemberExpression') { componentNode = refId.parent.right; } else if ( - refId.parent && - refId.parent.type === 'VariableDeclarator' && - refId.parent.init && - refId.parent.init.type !== 'Identifier' + refId.parent + && refId.parent.type === 'VariableDeclarator' + && refId.parent.init + && refId.parent.init.type !== 'Identifier' ) { componentNode = refId.parent.init; } @@ -627,10 +736,10 @@ function componentRule(rule, context) { // Try to find the component using variable declarations const defs = variableInScope.defs; - const defInScope = defs.find(def => ( - def.type === 'ClassName' || - def.type === 'FunctionName' || - def.type === 'Variable' + const defInScope = defs.find((def) => ( + def.type === 'ClassName' + || def.type === 'FunctionName' + || def.type === 'Variable' )); if (!defInScope || !defInScope.node) { return null; @@ -706,8 +815,8 @@ function componentRule(rule, context) { } const component = utils.getParentComponent(); if ( - !component || - (component.parent && component.parent.type === 'JSXExpressionContainer') + !component + || (component.parent && component.parent.type === 'JSXExpressionContainer') ) { // Ban the node if we cannot find a parent component components.add(node, 0); @@ -735,8 +844,8 @@ function componentRule(rule, context) { } const component = utils.getParentComponent(); if ( - !component || - (component.parent && component.parent.type === 'JSXExpressionContainer') + !component + || (component.parent && component.parent.type === 'JSXExpressionContainer') ) { // Ban the node if we cannot find a parent component components.add(node, 0); @@ -785,7 +894,7 @@ function componentRule(rule, context) { )); allKeys.forEach((instruction) => { - updatedRuleInstructions[instruction] = function (node) { + updatedRuleInstructions[instruction] = (node) => { if (instruction in detectionInstructions) { detectionInstructions[instruction](node); } diff --git a/lib/util/ast.js b/lib/util/ast.js index 3f39beebce..48df95bd0c 100644 --- a/lib/util/ast.js +++ b/lib/util/ast.js @@ -8,11 +8,12 @@ * Find a return statment in the current node * * @param {ASTNode} node The AST node being checked + * @returns {ASTNode | false} */ function findReturnStatement(node) { if ( - (!node.value || !node.value.body || !node.value.body.body) && - (!node.body || !node.body.body) + (!node.value || !node.value.body || !node.value.body.body) + && (!node.body || !node.body.body) ) { return false; } @@ -91,12 +92,12 @@ function getFirstNodeInLine(context, node) { let lines; do { token = sourceCode.getTokenBefore(token); - lines = token.type === 'JSXText' ? - token.value.split('\n') : - null; + lines = token.type === 'JSXText' + ? token.value.split('\n') + : null; } while ( - token.type === 'JSXText' && - /^\s*$/.test(lines[lines.length - 1]) + token.type === 'JSXText' + && /^\s*$/.test(lines[lines.length - 1]) ); return token; } @@ -132,15 +133,6 @@ function isFunction(node) { return node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration'; } -/** - * Checks if the node is an arrow function. - * @param {ASTNode} node The node to check - * @return {Boolean} true if it's an arrow function - */ -function isArrowFunction(node) { - return node.type === 'ArrowFunctionExpression'; -} - /** * Checks if the node is a class. * @param {ASTNode} node The node to check @@ -153,6 +145,7 @@ function isClass(node) { /** * Removes quotes from around an identifier. * @param {string} string the identifier to strip + * @returns {string} */ function stripQuotes(string) { return string.replace(/^'|'$/g, ''); @@ -162,17 +155,26 @@ function stripQuotes(string) { * Retrieve the name of a key node * @param {Context} context The AST node with the key. * @param {ASTNode} node The AST node with the key. - * @return {string} the name of the key + * @return {string | undefined} the name of the key */ function getKeyValue(context, node) { if (node.type === 'ObjectTypeProperty') { const tokens = context.getFirstTokens(node, 2); - return (tokens[0].value === '+' || tokens[0].value === '-' ? - tokens[1].value : - stripQuotes(tokens[0].value) + return (tokens[0].value === '+' || tokens[0].value === '-' + ? tokens[1].value + : stripQuotes(tokens[0].value) ); } + if (node.type === 'GenericTypeAnnotation') { + return node.id.name; + } + if (node.type === 'ObjectTypeAnnotation') { + return; + } const key = node.key || node.argument; + if (!key) { + return; + } return key.type === 'Identifier' ? key.name : key.value; } @@ -183,12 +185,23 @@ function getKeyValue(context, node) { */ function isAssignmentLHS(node) { return ( - node.parent && - node.parent.type === 'AssignmentExpression' && - node.parent.left === node + node.parent + && node.parent.type === 'AssignmentExpression' + && node.parent.left === node ); } +/** + * Extracts the expression node that is wrapped inside a TS type assertion + * + * @param {ASTNode} node - potential TS node + * @returns {ASTNode} - unwrapped expression node + */ +function unwrapTSAsExpression(node) { + if (node && node.type === 'TSAsExpression') return node.expression; + return node; +} + module.exports = { findReturnStatement, getFirstNodeInLine, @@ -196,10 +209,10 @@ module.exports = { getPropertyNameNode, getComponentProperties, getKeyValue, - isArrowFunction, isAssignmentLHS, isClass, isFunction, isFunctionLikeExpression, - isNodeFirstInLine + isNodeFirstInLine, + unwrapTSAsExpression }; diff --git a/lib/util/defaultProps.js b/lib/util/defaultProps.js index cf8ab46d14..5c1a7ec205 100644 --- a/lib/util/defaultProps.js +++ b/lib/util/defaultProps.js @@ -8,7 +8,7 @@ const fromEntries = require('object.fromentries'); const astUtil = require('./ast'); const propsUtil = require('./props'); const variableUtil = require('./variable'); -const propWrapperUtil = require('../util/propWrapper'); +const propWrapperUtil = require('./propWrapper'); const QUOTES_REGEX = /^["']|["']$/g; @@ -26,9 +26,9 @@ module.exports = function defaultPropsInstructions(context, components, utils) { return variableUtil.findVariableByName(context, node.name); } if ( - node.type === 'CallExpression' && - propWrapperUtil.isPropWrapperFunction(context, node.callee.name) && - node.arguments && node.arguments[0] + node.type === 'CallExpression' + && propWrapperUtil.isPropWrapperFunction(context, node.callee.name) + && node.arguments && node.arguments[0] ) { return resolveNodeValue(node.arguments[0]); } @@ -43,13 +43,13 @@ module.exports = function defaultPropsInstructions(context, components, utils) { * from this ObjectExpression can't be resolved. */ function getDefaultPropsFromObjectExpression(objectExpression) { - const hasSpread = objectExpression.properties.find(property => property.type === 'ExperimentalSpreadProperty' || property.type === 'SpreadElement'); + const hasSpread = objectExpression.properties.find((property) => property.type === 'ExperimentalSpreadProperty' || property.type === 'SpreadElement'); if (hasSpread) { return 'unresolved'; } - return objectExpression.properties.map(defaultProp => ({ + return objectExpression.properties.map((defaultProp) => ({ name: sourceCode.getText(defaultProp.key).replace(QUOTES_REGEX, ''), node: defaultProp })); @@ -90,7 +90,7 @@ module.exports = function defaultPropsInstructions(context, components, utils) { const newDefaultProps = Object.assign( {}, defaults, - fromEntries(defaultProps.map(prop => [prop.name, prop])) + fromEntries(defaultProps.map((prop) => [prop.name, prop])) ); components.set(component.node, { @@ -141,8 +141,8 @@ module.exports = function defaultPropsInstructions(context, components, utils) { // e.g.: // MyComponent.propTypes.baz = React.PropTypes.string; - if (node.parent.type === 'MemberExpression' && node.parent.parent && - node.parent.parent.type === 'AssignmentExpression') { + if (node.parent.type === 'MemberExpression' && node.parent.parent + && node.parent.parent.type === 'AssignmentExpression') { addDefaultPropsToComponent(component, [{ name: node.parent.property.name, node: node.parent.parent diff --git a/lib/util/jsx.js b/lib/util/jsx.js index 3b7aac324b..363ea8d609 100644 --- a/lib/util/jsx.js +++ b/lib/util/jsx.js @@ -6,24 +6,47 @@ const elementType = require('jsx-ast-utils/elementType'); -const COMPAT_TAG_REGEX = /^[a-z]|-/; +// See https://github.com/babel/babel/blob/ce420ba51c68591e057696ef43e028f41c6e04cd/packages/babel-types/src/validators/react/isCompatTag.js +// for why we only test for the first character +const COMPAT_TAG_REGEX = /^[a-z]/; /** - * Checks if a node represents a DOM element. + * Checks if a node represents a DOM element according to React. * @param {object} node - JSXOpeningElement to check. * @returns {boolean} Whether or not the node corresponds to a DOM element. */ function isDOMComponent(node) { - let name = elementType(node); + const name = elementType(node); + return COMPAT_TAG_REGEX.test(name); +} + +/** + * Test whether a JSXElement is a fragment + * @param {JSXElement} node + * @param {string} reactPragma + * @param {string} fragmentPragma + * @returns {boolean} + */ +function isFragment(node, reactPragma, fragmentPragma) { + const name = node.openingElement.name; - // Get namespace if the type is JSXNamespacedName or JSXMemberExpression - if (name.indexOf(':') > -1) { - name = name.slice(0, name.indexOf(':')); - } else if (name.indexOf('.') > -1) { - name = name.slice(0, name.indexOf('.')); + // + if (name.type === 'JSXIdentifier' && name.name === fragmentPragma) { + return true; } - return COMPAT_TAG_REGEX.test(name); + // + if ( + name.type === 'JSXMemberExpression' + && name.object.type === 'JSXIdentifier' + && name.object.name === reactPragma + && name.property.type === 'JSXIdentifier' + && name.property.name === fragmentPragma + ) { + return true; + } + + return false; } /** @@ -35,7 +58,31 @@ function isJSX(node) { return node && ['JSXElement', 'JSXFragment'].indexOf(node.type) >= 0; } +/** + * Check if node is like `key={...}` as in `` + * @param {ASTNode} node + * @returns {boolean} + */ +function isJSXAttributeKey(node) { + return node.type === 'JSXAttribute' + && node.name + && node.name.type === 'JSXIdentifier' + && node.name.name === 'key'; +} + +/** + * Check if value has only whitespaces + * @param {string} value + * @returns {boolean} + */ +function isWhiteSpaces(value) { + return typeof value === 'string' ? /^\s*$/.test(value) : false; +} + module.exports = { isDOMComponent, - isJSX + isFragment, + isJSX, + isJSXAttributeKey, + isWhiteSpaces }; diff --git a/lib/util/makeNoMethodSetStateRule.js b/lib/util/makeNoMethodSetStateRule.js index 3a46b5e93b..00a1e1ce0f 100644 --- a/lib/util/makeNoMethodSetStateRule.js +++ b/lib/util/makeNoMethodSetStateRule.js @@ -63,9 +63,9 @@ function makeNoMethodSetStateRule(methodName, shouldCheckUnsafeCb) { CallExpression(node) { const callee = node.callee; if ( - callee.type !== 'MemberExpression' || - callee.object.type !== 'ThisExpression' || - callee.property.name !== 'setState' + callee.type !== 'MemberExpression' + || callee.object.type !== 'ThisExpression' + || callee.property.name !== 'setState' ) { return; } @@ -76,9 +76,9 @@ function makeNoMethodSetStateRule(methodName, shouldCheckUnsafeCb) { depth++; } if ( - (ancestor.type !== 'Property' && ancestor.type !== 'MethodDefinition' && ancestor.type !== 'ClassProperty') || - !nameMatches(ancestor.key.name) || - (mode !== 'disallow-in-func' && depth > 1) + (ancestor.type !== 'Property' && ancestor.type !== 'MethodDefinition' && ancestor.type !== 'ClassProperty') + || !nameMatches(ancestor.key.name) + || (mode !== 'disallow-in-func' && depth > 1) ) { return false; } diff --git a/lib/util/pragma.js b/lib/util/pragma.js index 47682847d5..4a49ac52d2 100644 --- a/lib/util/pragma.js +++ b/lib/util/pragma.js @@ -38,7 +38,7 @@ function getFromContext(context) { let pragma = 'React'; const sourceCode = context.getSourceCode(); - const pragmaNode = sourceCode.getAllComments().find(node => JSX_ANNOTATION_REGEX.test(node.value)); + const pragmaNode = sourceCode.getAllComments().find((node) => JSX_ANNOTATION_REGEX.test(node.value)); if (pragmaNode) { const matches = JSX_ANNOTATION_REGEX.exec(pragmaNode.value); diff --git a/lib/util/propTypes.js b/lib/util/propTypes.js index ae8a3edacb..7cd4bc05a4 100644 --- a/lib/util/propTypes.js +++ b/lib/util/propTypes.js @@ -32,13 +32,19 @@ function isSuperTypeParameterPropsDeclaration(node) { * @param {Object[]} properties Array of properties to iterate. * @param {Function} fn Function to call on each property, receives property key and property value. (key, value) => void + * @param {Function} [handleSpreadFn] Function to call on each ObjectTypeSpreadProperty, receives the + argument */ -function iterateProperties(context, properties, fn) { +function iterateProperties(context, properties, fn, handleSpreadFn) { if (properties && properties.length && typeof fn === 'function') { for (let i = 0, j = properties.length; i < j; i++) { const node = properties[i]; const key = getKeyValue(context, node); + if (node.type === 'ObjectTypeSpreadProperty' && typeof handleSpreadFn === 'function') { + handleSpreadFn(node.argument); + } + const value = node.value; fn(key, value, node); } @@ -121,7 +127,8 @@ module.exports = function propTypesInstructions(context, components, utils) { }, ObjectTypeAnnotation(annotation, parentName, seen) { - let containsObjectTypeSpread = false; + let containsUnresolvedObjectTypeSpread = false; + let containsSpread = false; const containsIndexers = Boolean(annotation.indexers && annotation.indexers.length); const shapeTypeDefinition = { type: 'shape', @@ -129,9 +136,7 @@ module.exports = function propTypesInstructions(context, components, utils) { }; iterateProperties(context, annotation.properties, (childKey, childValue, propNode) => { const fullName = [parentName, childKey].join('.'); - if (!childKey && !childValue) { - containsObjectTypeSpread = true; - } else { + if (childKey || childValue) { const types = buildTypeAnnotationDeclarationTypes(childValue, fullName, seen); types.fullName = fullName; types.name = childKey; @@ -139,12 +144,24 @@ module.exports = function propTypesInstructions(context, components, utils) { types.isRequired = !childValue.optional; shapeTypeDefinition.children[childKey] = types; } + }, + (spreadNode) => { + const key = getKeyValue(context, spreadNode); + const types = buildTypeAnnotationDeclarationTypes(spreadNode, key, seen); + if (!types.children) { + containsUnresolvedObjectTypeSpread = true; + } else { + Object.assign(shapeTypeDefinition, types.children); + } + containsSpread = true; }); // Mark if this shape has spread or an indexer. We will know to consider all props from this shape as having propTypes, // but still have the ability to detect unused children of this shape. - shapeTypeDefinition.containsSpread = containsObjectTypeSpread; + shapeTypeDefinition.containsUnresolvedSpread = containsUnresolvedObjectTypeSpread; shapeTypeDefinition.containsIndexers = containsIndexers; + // Deprecated: containsSpread is not used anymore in the codebase, ensure to keep API backward compatibility + shapeTypeDefinition.containsSpread = containsSpread; return shapeTypeDefinition; }, @@ -153,22 +170,9 @@ module.exports = function propTypesInstructions(context, components, utils) { /** @type {UnionTypeDefinition} */ const unionTypeDefinition = { type: 'union', - children: [] + children: annotation.types.map((type) => buildTypeAnnotationDeclarationTypes(type, parentName, seen)) }; - for (let i = 0, j = annotation.types.length; i < j; i++) { - const type = buildTypeAnnotationDeclarationTypes(annotation.types[i], parentName, seen); - // keep only complex type - if (type.type) { - if (type.children === true) { - // every child is accepted for one type, abort type analysis - unionTypeDefinition.children = true; - return unionTypeDefinition; - } - } - - /** @type {UnionTypeDefinitionChildren} */(unionTypeDefinition.children).push(type); - } - if (/** @type {UnionTypeDefinitionChildren} */(unionTypeDefinition.children).length === 0) { + if (unionTypeDefinition.children.length === 0) { // no complex type found, simply accept everything return {}; } @@ -241,7 +245,7 @@ module.exports = function propTypesInstructions(context, components, utils) { } /** - * Marks all props found inside ObjectTypeAnnotaiton as declared. + * Marks all props found inside ObjectTypeAnnotation as declared. * * Modifies the declaredProperties object * @param {ASTNode} propTypes @@ -253,7 +257,7 @@ module.exports = function propTypesInstructions(context, components, utils) { iterateProperties(context, propTypes.properties, (key, value, propNode) => { if (!value) { - ignorePropsValidation = true; + ignorePropsValidation = ignorePropsValidation || propNode.type !== 'ObjectTypeSpreadProperty'; return; } @@ -263,6 +267,15 @@ module.exports = function propTypesInstructions(context, components, utils) { types.node = propNode; types.isRequired = !propNode.optional; declaredPropTypes[key] = types; + }, (spreadNode) => { + const key = getKeyValue(context, spreadNode); + const spreadAnnotation = getInTypeScope(key); + if (!spreadAnnotation) { + ignorePropsValidation = true; + } else { + const spreadIgnoreValidation = declarePropTypesForObjectTypeAnnotation(spreadAnnotation, declaredPropTypes); + ignorePropsValidation = ignorePropsValidation || spreadIgnoreValidation; + } }); return ignorePropsValidation; @@ -315,33 +328,33 @@ module.exports = function propTypesInstructions(context, components, utils) { */ function buildReactDeclarationTypes(value, parentName) { if ( - value && - value.callee && - value.callee.object && - hasCustomValidator(value.callee.object.name) + value + && value.callee + && value.callee.object + && hasCustomValidator(value.callee.object.name) ) { return {}; } if ( - value && - value.type === 'MemberExpression' && - value.property && - value.property.name && - value.property.name === 'isRequired' + value + && value.type === 'MemberExpression' + && value.property + && value.property.name + && value.property.name === 'isRequired' ) { value = value.object; } // Verify PropTypes that are functions if ( - value && - value.type === 'CallExpression' && - value.callee && - value.callee.property && - value.callee.property.name && - value.arguments && - value.arguments.length > 0 + value + && value.type === 'CallExpression' + && value.callee + && value.callee.property + && value.callee.property.name + && value.arguments + && value.arguments.length > 0 ) { const callName = value.callee.property.name; const argument = value.arguments[0]; @@ -383,8 +396,8 @@ module.exports = function propTypesInstructions(context, components, utils) { } case 'oneOfType': { if ( - !argument.elements || - !argument.elements.length + !argument.elements + || !argument.elements.length ) { // Invalid proptype or cannot analyse statically return {}; @@ -393,34 +406,14 @@ module.exports = function propTypesInstructions(context, components, utils) { /** @type {UnionTypeDefinition} */ const unionTypeDefinition = { type: 'union', - children: [] + children: argument.elements.map((element) => buildReactDeclarationTypes(element, parentName)) }; - for (let i = 0, j = argument.elements.length; i < j; i++) { - const type = buildReactDeclarationTypes(argument.elements[i], parentName); - // keep only complex type - if (type.type) { - if (type.children === true) { - // every child is accepted for one type, abort type analysis - unionTypeDefinition.children = true; - return unionTypeDefinition; - } - } - - /** @type {UnionTypeDefinitionChildren} */(unionTypeDefinition.children).push(type); - } - if (/** @type {UnionTypeDefinitionChildren} */(unionTypeDefinition.children).length === 0) { + if (unionTypeDefinition.children.length === 0) { // no complex type found, simply accept everything return {}; } return unionTypeDefinition; } - case 'instanceOf': - return { - type: 'instance', - // Accept all children because we can't know what type they are - children: true - }; - case 'oneOf': default: return {}; } @@ -466,11 +459,11 @@ module.exports = function propTypesInstructions(context, components, utils) { // Walk the list of properties, until we reach the assignment // ie: ClassX.propTypes.a.b.c = ... while ( - propTypes && - propTypes.parent && - propTypes.parent.type !== 'AssignmentExpression' && - propTypes.property && - curDeclaredPropTypes + propTypes + && propTypes.parent + && propTypes.parent.type !== 'AssignmentExpression' + && propTypes.property + && curDeclaredPropTypes ) { const propName = propTypes.property.name; if (propName in curDeclaredPropTypes) { @@ -502,8 +495,8 @@ module.exports = function propTypesInstructions(context, components, utils) { let isUsedInPropTypes = false; let n = propTypes; while (n) { - if (n.type === 'AssignmentExpression' && propsUtil.isPropTypesDeclaration(n.left) || - (n.type === 'ClassProperty' || n.type === 'Property') && propsUtil.isPropTypesDeclaration(n)) { + if (n.type === 'AssignmentExpression' && propsUtil.isPropTypesDeclaration(n.left) + || (n.type === 'ClassProperty' || n.type === 'Property') && propsUtil.isPropTypesDeclaration(n)) { // Found a propType used inside of another propType. This is not considered usage, we'll still validate // this component. isUsedInPropTypes = true; @@ -520,7 +513,7 @@ module.exports = function propTypesInstructions(context, components, utils) { case 'Identifier': { const variablesInScope = variableUtil.variablesInScope(context); const firstMatchingVariable = variablesInScope - .find(variableInScope => variableInScope.name === propTypes.name); + .find((variableInScope) => variableInScope.name === propTypes.name); if (firstMatchingVariable) { const defInScope = firstMatchingVariable.defs[firstMatchingVariable.defs.length - 1]; markPropTypesAsDeclared(node, defInScope.node && defInScope.node.init); @@ -534,8 +527,8 @@ module.exports = function propTypesInstructions(context, components, utils) { propWrapperUtil.isPropWrapperFunction( context, context.getSourceCode().getText(propTypes.callee) - ) && - propTypes.arguments && propTypes.arguments[0] + ) + && propTypes.arguments && propTypes.arguments[0] ) { markPropTypesAsDeclared(node, propTypes.arguments[0]); return; @@ -545,6 +538,16 @@ module.exports = function propTypesInstructions(context, components, utils) { case 'IntersectionTypeAnnotation': ignorePropsValidation = declarePropTypesForIntersectionTypeAnnotation(propTypes, declaredPropTypes); break; + case 'GenericTypeAnnotation': + if (propTypes.id.name === '$ReadOnly') { + ignorePropsValidation = declarePropTypesForObjectTypeAnnotation( + propTypes.typeParameters.params[0], + declaredPropTypes + ); + } else { + ignorePropsValidation = true; + } + break; case null: break; default: @@ -623,8 +626,8 @@ module.exports = function propTypesInstructions(context, components, utils) { const tokens = context.getFirstTokens(node, 2); if ( node.typeAnnotation && ( - tokens[0].value === 'props' || - (tokens[1] && tokens[1].value === 'props') + tokens[0].value === 'props' + || (tokens[1] && tokens[1].value === 'props') ) ) { return true; @@ -722,11 +725,11 @@ module.exports = function propTypesInstructions(context, components, utils) { stack.push(Object.create(typeScope())); }, - 'BlockStatement:exit': function () { + 'BlockStatement:exit'() { stack.pop(); }, - 'Program:exit': function () { + 'Program:exit'() { classExpressions.forEach((node) => { if (isSuperTypeParameterPropsDeclaration(node)) { markPropTypesAsDeclared(node, resolveSuperParameterPropsType(node)); diff --git a/lib/util/propTypesSort.js b/lib/util/propTypesSort.js new file mode 100644 index 0000000000..badcf79896 --- /dev/null +++ b/lib/util/propTypesSort.js @@ -0,0 +1,164 @@ +/** + * @fileoverview Common propTypes sorting functionality. + */ + +'use strict'; + +const astUtil = require('./ast'); + +/** + * Returns the value name of a node. + * + * @param {ASTNode} node the node to check. + * @returns {String} The name of the node. + */ +function getValueName(node) { + return node.type === 'Property' && node.value.property && node.value.property.name; +} + +/** + * Checks if the prop is required or not. + * + * @param {ASTNode} node the prop to check. + * @returns {Boolean} true if the prop is required. + */ +function isRequiredProp(node) { + return getValueName(node) === 'isRequired'; +} + +/** + * Checks if the proptype is a callback by checking if it starts with 'on'. + * + * @param {String} propName the name of the proptype to check. + * @returns {Boolean} true if the proptype is a callback. + */ +function isCallbackPropName(propName) { + return /^on[A-Z]/.test(propName); +} + +/** + * Checks if the prop is PropTypes.shape. + * + * @param {ASTNode} node the prop to check. + * @returns {Boolean} true if the prop is PropTypes.shape. + */ +function isShapeProp(node) { + return Boolean( + node && node.callee && node.callee.property && node.callee.property.name === 'shape' + ); +} + +/** + * Returns the properties of a PropTypes.shape. + * + * @param {ASTNode} node the prop to check. + * @returns {Array} the properties of the PropTypes.shape node. + */ +function getShapeProperties(node) { + return node.arguments && node.arguments[0] && node.arguments[0].properties; +} + +/** + * Compares two elements. + * + * @param {ASTNode} a the first element to compare. + * @param {ASTNode} b the second element to compare. + * @param {Context} context The context of the two nodes. + * @param {Boolean=} ignoreCase whether or not to ignore case when comparing the two elements. + * @param {Boolean=} requiredFirst whether or not to sort required elements first. + * @param {Boolean=} callbacksLast whether or not to sort callbacks after everyting else. + * @returns {Number} the sort order of the two elements. + */ +function sorter(a, b, context, ignoreCase, requiredFirst, callbacksLast) { + const aKey = String(astUtil.getKeyValue(context, a)); + const bKey = String(astUtil.getKeyValue(context, b)); + + if (requiredFirst) { + if (isRequiredProp(a) && !isRequiredProp(b)) { + return -1; + } + if (!isRequiredProp(a) && isRequiredProp(b)) { + return 1; + } + } + + if (callbacksLast) { + if (isCallbackPropName(aKey) && !isCallbackPropName(bKey)) { + return 1; + } + if (!isCallbackPropName(aKey) && isCallbackPropName(bKey)) { + return -1; + } + } + + if (ignoreCase) { + return aKey.localeCompare(bKey); + } + + if (aKey < bKey) { + return -1; + } + if (aKey > bKey) { + return 1; + } + return 0; +} + +/** + * Fixes sort order of prop types. + * + * @param {Fixer} fixer the first element to compare. + * @param {Object} context the second element to compare. + * @param {Array} declarations The context of the two nodes. + * @param {Boolean=} ignoreCase whether or not to ignore case when comparing the two elements. + * @param {Boolean=} requiredFirst whether or not to sort required elements first. + * @param {Boolean=} callbacksLast whether or not to sort callbacks after everyting else. + * @param {Boolean=} sortShapeProp whether or not to sort propTypes defined in PropTypes.shape. + * @returns {Object|*|{range, text}} the sort order of the two elements. + */ +function fixPropTypesSort(fixer, context, declarations, ignoreCase, requiredFirst, callbacksLast, sortShapeProp) { + function sortInSource(allNodes, source) { + const originalSource = source; + const nodeGroups = allNodes.reduce((acc, curr) => { + if (curr.type === 'ExperimentalSpreadProperty' || curr.type === 'SpreadElement') { + acc.push([]); + } else { + acc[acc.length - 1].push(curr); + } + return acc; + }, [[]]); + + nodeGroups.forEach((nodes) => { + const sortedAttributes = nodes + .slice() + .sort((a, b) => sorter(a, b, context, ignoreCase, requiredFirst, callbacksLast)); + + source = nodes.reduceRight((acc, attr, index) => { + const sortedAttr = sortedAttributes[index]; + let sortedAttrText = context.getSourceCode().getText(sortedAttr); + if (sortShapeProp && isShapeProp(sortedAttr.value)) { + const shape = getShapeProperties(sortedAttr.value); + if (shape) { + const attrSource = sortInSource( + shape, + originalSource + ); + sortedAttrText = attrSource.slice(sortedAttr.range[0], sortedAttr.range[1]); + } + } + return `${acc.slice(0, attr.range[0])}${sortedAttrText}${acc.slice(attr.range[1])}`; + }, source); + }); + return source; + } + + const source = sortInSource(declarations, context.getSourceCode().getText()); + + const rangeStart = declarations[0].range[0]; + const rangeEnd = declarations[declarations.length - 1].range[1]; + return fixer.replaceTextRange([rangeStart, rangeEnd], source.slice(rangeStart, rangeEnd)); +} + +module.exports = { + fixPropTypesSort +}; diff --git a/lib/util/usedPropTypes.js b/lib/util/usedPropTypes.js index 9e19fe8b14..94f56c36d4 100755 --- a/lib/util/usedPropTypes.js +++ b/lib/util/usedPropTypes.js @@ -34,6 +34,7 @@ function createPropVariables() { * Add a variable name to the current scope * @param {string} name * @param {string[]} allNames Example: `props.a.b` should be formatted as `['a', 'b']` + * @returns {Map} */ set(name, allNames) { if (!hasBeenWritten) { @@ -75,6 +76,8 @@ function mustBeValidated(component) { /** * Check if we are in a lifecycle method + * @param {object} context + * @param {boolean} checkAsyncSafeLifeCycles * @return {boolean} true if we are in a class constructor, false if not */ function inLifeCycleMethod(context, checkAsyncSafeLifeCycles) { @@ -98,6 +101,7 @@ function inLifeCycleMethod(context, checkAsyncSafeLifeCycles) { /** * Returns true if the given node is a React Component lifecycle method * @param {ASTNode} node The AST node being checked. + * @param {boolean} checkAsyncSafeLifeCycles * @return {Boolean} True if the node is a lifecycle method */ function isNodeALifeCycleMethod(node, checkAsyncSafeLifeCycles) { @@ -120,6 +124,7 @@ function isNodeALifeCycleMethod(node, checkAsyncSafeLifeCycles) { * Returns true if the given node is inside a React Component lifecycle * method. * @param {ASTNode} node The AST node being checked. + * @param {boolean} checkAsyncSafeLifeCycles * @return {Boolean} True if the node is inside a lifecycle method */ function isInLifeCycleMethod(node, checkAsyncSafeLifeCycles) { @@ -135,25 +140,19 @@ function isInLifeCycleMethod(node, checkAsyncSafeLifeCycles) { } /** - * Check if the current node is in a setState updater method - * @return {boolean} true if we are in a setState updater, false if not + * Check if a function node is a setState updater + * @param {ASTNode} node a function node + * @return {boolean} */ -function inSetStateUpdater(context) { - let scope = context.getScope(); - while (scope) { - if ( - scope.block && scope.block.parent && - scope.block.parent.type === 'CallExpression' && - scope.block.parent.callee.property && - scope.block.parent.callee.property.name === 'setState' && - // Make sure we are in the updater not the callback - scope.block.parent.arguments[0].start === scope.block.start - ) { - return true; - } - scope = scope.upper; - } - return false; +function isSetStateUpdater(node) { + const unwrappedParentCalleeNode = node.parent.type === 'CallExpression' + && ast.unwrapTSAsExpression(node.parent.callee); + + return unwrappedParentCalleeNode + && unwrappedParentCalleeNode.property + && unwrappedParentCalleeNode.property.name === 'setState' + // Make sure we are in the updater not the callback + && node.parent.arguments[0] === node; } function isPropArgumentInSetStateUpdater(context, name) { @@ -162,15 +161,18 @@ function isPropArgumentInSetStateUpdater(context, name) { } let scope = context.getScope(); while (scope) { + const unwrappedParentCalleeNode = scope.block + && scope.block.parent + && scope.block.parent.type === 'CallExpression' + && ast.unwrapTSAsExpression(scope.block.parent.callee); if ( - scope.block && scope.block.parent && - scope.block.parent.type === 'CallExpression' && - scope.block.parent.callee.property && - scope.block.parent.callee.property.name === 'setState' && + unwrappedParentCalleeNode + && unwrappedParentCalleeNode.property + && unwrappedParentCalleeNode.property.name === 'setState' // Make sure we are in the updater not the callback - scope.block.parent.arguments[0].start === scope.block.start && - scope.block.parent.arguments[0].params && - scope.block.parent.arguments[0].params.length > 1 + && scope.block.parent.arguments[0].range[0] === scope.block.range[0] + && scope.block.parent.arguments[0].params + && scope.block.parent.arguments[0].params.length > 1 ) { return scope.block.parent.arguments[0].params[1].name === name; } @@ -189,14 +191,15 @@ function isInClassComponent(utils) { * @returns {boolean} */ function isThisDotProps(node) { - return !!node && - node.type === 'MemberExpression' && - node.object.type === 'ThisExpression' && - node.property.name === 'props'; + return !!node + && node.type === 'MemberExpression' + && ast.unwrapTSAsExpression(node.object).type === 'ThisExpression' + && node.property.name === 'props'; } /** * Checks if the prop has spread operator. + * @param {object} context * @param {ASTNode} node The AST node being marked. * @returns {Boolean} True if the prop has spread operator, false if not. */ @@ -245,26 +248,28 @@ function getPropertyName(node) { * @returns {boolean} */ function isPropTypesUsageByMemberExpression(node, context, utils, checkAsyncSafeLifeCycles) { + const unwrappedObjectNode = ast.unwrapTSAsExpression(node.object); + if (isInClassComponent(utils)) { // this.props.* - if (isThisDotProps(node.object)) { + if (isThisDotProps(unwrappedObjectNode)) { return true; } // props.* or prevProps.* or nextProps.* if ( - isCommonVariableNameForProps(node.object.name) && - (inLifeCycleMethod(context, checkAsyncSafeLifeCycles) || utils.inConstructor()) + isCommonVariableNameForProps(unwrappedObjectNode.name) + && (inLifeCycleMethod(context, checkAsyncSafeLifeCycles) || utils.inConstructor()) ) { return true; } // this.setState((_, props) => props.*)) - if (isPropArgumentInSetStateUpdater(context, node.object.name)) { + if (isPropArgumentInSetStateUpdater(context, unwrappedObjectNode.name)) { return true; } return false; } // props.* in function component - return node.object.name === 'props' && !ast.isAssignmentLHS(node); + return unwrappedObjectNode.name === 'props' && !ast.isAssignmentLHS(node); } module.exports = function usedPropTypesInstructions(context, components, utils) { @@ -292,15 +297,15 @@ module.exports = function usedPropTypesInstructions(context, components, utils) allNames = parentNames.concat(name); if ( // Match props.foo.bar, don't match bar[props.foo] - node.parent.type === 'MemberExpression' && - node.parent.object === node + node.parent.type === 'MemberExpression' + && node.parent.object === node ) { markPropTypesAsUsed(node.parent, allNames); } // Handle the destructuring part of `const {foo} = props.a.b` if ( - node.parent.type === 'VariableDeclarator' && - node.parent.id.type === 'ObjectPattern' + node.parent.type === 'VariableDeclarator' + && node.parent.id.type === 'ObjectPattern' ) { node.parent.id.parent = node.parent; // patch for bug in eslint@4 in which ObjectPattern has no parent markPropTypesAsUsed(node.parent.id, allNames); @@ -308,8 +313,8 @@ module.exports = function usedPropTypesInstructions(context, components, utils) // const a = props.a if ( - node.parent.type === 'VariableDeclarator' && - node.parent.id.type === 'Identifier' + node.parent.type === 'VariableDeclarator' + && node.parent.id.type === 'Identifier' ) { propVariables.set(node.parent.id.name, allNames); } @@ -324,16 +329,18 @@ module.exports = function usedPropTypesInstructions(context, components, utils) break; } type = 'destructuring'; - const propParam = inSetStateUpdater(context) ? node.params[1] : node.params[0]; - properties = propParam.type === 'AssignmentPattern' ? - propParam.left.properties : - propParam.properties; + const propParam = isSetStateUpdater(node) ? node.params[1] : node.params[0]; + properties = propParam.type === 'AssignmentPattern' + ? propParam.left.properties + : propParam.properties; break; } case 'ObjectPattern': type = 'destructuring'; properties = node.properties; break; + case 'TSEmptyBodyFunctionExpression': + break; default: throw new Error(`${node.type} ASTNodes are not handled by markPropTypesAsUsed`); } @@ -365,21 +372,20 @@ module.exports = function usedPropTypesInstructions(context, components, utils) } const propName = ast.getKeyValue(context, properties[k]); - if (propName) { - propVariables.set(propName, parentNames.concat(propName)); - usedPropTypes.push({ - allNames: parentNames.concat([propName]), - name: propName, - node: properties[k] - }); + if (!propName || properties[k].type !== 'Property') { + break; } - if ( - propName && - properties[k].type === 'Property' && - properties[k].value.type === 'ObjectPattern' - ) { + usedPropTypes.push({ + allNames: parentNames.concat([propName]), + name: propName, + node: properties[k] + }); + + if (properties[k].value.type === 'ObjectPattern') { markPropTypesAsUsed(properties[k].value, parentNames.concat([propName])); + } else if (properties[k].value.type === 'Identifier') { + propVariables.set(propName, parentNames.concat(propName)); } } break; @@ -399,11 +405,11 @@ module.exports = function usedPropTypesInstructions(context, components, utils) * FunctionDeclaration, or FunctionExpression */ function markDestructuredFunctionArgumentsAsUsed(node) { - const param = node.params && inSetStateUpdater(context) ? node.params[1] : node.params[0]; + const param = node.params && isSetStateUpdater(node) ? node.params[1] : node.params[0]; const destructuring = param && ( - param.type === 'ObjectPattern' || - param.type === 'AssignmentPattern' && param.left.type === 'ObjectPattern' + param.type === 'ObjectPattern' + || param.type === 'AssignmentPattern' && param.left.type === 'ObjectPattern' ); if (destructuring && (components.get(node) || components.get(node.parent))) { @@ -412,7 +418,7 @@ module.exports = function usedPropTypesInstructions(context, components, utils) } function handleSetStateUpdater(node) { - if (!node.params || node.params.length < 2 || !inSetStateUpdater(context)) { + if (!node.params || node.params.length < 2 || !isSetStateUpdater(node)) { return; } markPropTypesAsUsed(node); @@ -446,50 +452,53 @@ module.exports = function usedPropTypesInstructions(context, components, utils) return { VariableDeclarator(node) { + const unwrappedInitNode = ast.unwrapTSAsExpression(node.init); + // let props = this.props - if (isThisDotProps(node.init) && isInClassComponent(utils) && node.id.type === 'Identifier') { + if (isThisDotProps(unwrappedInitNode) && isInClassComponent(utils) && node.id.type === 'Identifier') { propVariables.set(node.id.name, []); } // Only handles destructuring - if (node.id.type !== 'ObjectPattern' || !node.init) { + if (node.id.type !== 'ObjectPattern' || !unwrappedInitNode) { return; } // let {props: {firstname}} = this - const propsProperty = node.id.properties.find(property => ( - property.key && - (property.key.name === 'props' || property.key.value === 'props') + const propsProperty = node.id.properties.find((property) => ( + property.key + && (property.key.name === 'props' || property.key.value === 'props') )); - if (node.init.type === 'ThisExpression' && propsProperty && propsProperty.value.type === 'ObjectPattern') { + + if (unwrappedInitNode.type === 'ThisExpression' && propsProperty && propsProperty.value.type === 'ObjectPattern') { markPropTypesAsUsed(propsProperty.value); return; } // let {props} = this - if (node.init.type === 'ThisExpression' && propsProperty && propsProperty.value.name === 'props') { + if (unwrappedInitNode.type === 'ThisExpression' && propsProperty && propsProperty.value.name === 'props') { propVariables.set('props', []); return; } // let {firstname} = props if ( - isCommonVariableNameForProps(node.init.name) && - (utils.getParentStatelessComponent() || isInLifeCycleMethod(node, checkAsyncSafeLifeCycles)) + isCommonVariableNameForProps(unwrappedInitNode.name) + && (utils.getParentStatelessComponent() || isInLifeCycleMethod(node, checkAsyncSafeLifeCycles)) ) { markPropTypesAsUsed(node.id); return; } // let {firstname} = this.props - if (isThisDotProps(node.init) && isInClassComponent(utils)) { + if (isThisDotProps(unwrappedInitNode) && isInClassComponent(utils)) { markPropTypesAsUsed(node.id); return; } // let {firstname} = thing, where thing is defined by const thing = this.props.**.* - if (propVariables.get(node.init.name)) { - markPropTypesAsUsed(node.id, propVariables.get(node.init.name)); + if (propVariables.get(unwrappedInitNode.name)) { + markPropTypesAsUsed(node.id, propVariables.get(unwrappedInitNode.name)); } }, @@ -518,8 +527,9 @@ module.exports = function usedPropTypesInstructions(context, components, utils) return; } - if (propVariables.get(node.object.name)) { - markPropTypesAsUsed(node, propVariables.get(node.object.name)); + const propVariable = propVariables.get(ast.unwrapTSAsExpression(node.object).name); + if (propVariable) { + markPropTypesAsUsed(node, propVariable); } }, @@ -531,10 +541,10 @@ module.exports = function usedPropTypesInstructions(context, components, utils) } }, - 'Program:exit': function () { + 'Program:exit'() { const list = components.list(); - Object.keys(list).filter(component => mustBeValidated(list[component])).forEach((component) => { + Object.keys(list).filter((component) => mustBeValidated(list[component])).forEach((component) => { handleCustomValidators(list[component]); }); } diff --git a/lib/util/variable.js b/lib/util/variable.js index cbc30f4ad0..0575c75d50 100644 --- a/lib/util/variable.js +++ b/lib/util/variable.js @@ -12,7 +12,7 @@ * @returns {Boolean} True if the variable was found, false if not. */ function findVariable(variables, name) { - return variables.some(variable => variable.name === name); + return variables.some((variable) => variable.name === name); } /** @@ -22,7 +22,7 @@ function findVariable(variables, name) { * @returns {Object} Variable if the variable was found, null if not. */ function getVariable(variables, name) { - return variables.find(variable => variable.name === name); + return variables.find((variable) => variable.name === name); } /** diff --git a/lib/util/version.js b/lib/util/version.js index 932866bc69..a919fb4646 100644 --- a/lib/util/version.js +++ b/lib/util/version.js @@ -17,13 +17,13 @@ function resetWarningFlag() { function detectReactVersion() { try { const reactPath = resolve.sync('react', {basedir: process.cwd()}); - const react = require(reactPath); // eslint-disable-line import/no-dynamic-require + const react = require(reactPath); // eslint-disable-line global-require, import/no-dynamic-require return react.version; } catch (e) { if (e.code === 'MODULE_NOT_FOUND') { if (!warnedForMissingVersion) { - error('Warning: React version was set to "detect" in eslint-plugin-react settings, ' + - 'but the "react" package is not installed. Assuming latest React version for linting.'); + error('Warning: React version was set to "detect" in eslint-plugin-react settings, ' + + 'but the "react" package is not installed. Assuming latest React version for linting.'); warnedForMissingVersion = true; } return '999.999.999'; @@ -35,34 +35,34 @@ function detectReactVersion() { function getReactVersionFromContext(context) { let confVer = '999.999.999'; // .eslintrc shared settings (http://eslint.org/docs/user-guide/configuring#adding-shared-settings) - if (context.settings.react && context.settings.react.version) { + if (context.settings && context.settings.react && context.settings.react.version) { let settingsVersion = context.settings.react.version; if (settingsVersion === 'detect') { settingsVersion = detectReactVersion(); } if (typeof settingsVersion !== 'string') { - error('Warning: React version specified in eslint-plugin-react-settings must be a string; ' + - `got “${typeof settingsVersion}”`); + error('Warning: React version specified in eslint-plugin-react-settings must be a string; ' + + `got “${typeof settingsVersion}”`); } confVer = String(settingsVersion); } else if (!warnedForMissingVersion) { - error('Warning: React version not specified in eslint-plugin-react settings. ' + - 'See https://github.com/yannickcr/eslint-plugin-react#configuration .'); + error('Warning: React version not specified in eslint-plugin-react settings. ' + + 'See https://github.com/yannickcr/eslint-plugin-react#configuration .'); warnedForMissingVersion = true; } confVer = /^[0-9]+\.[0-9]+$/.test(confVer) ? `${confVer}.0` : confVer; - return confVer.split('.').map(part => Number(part)); + return confVer.split('.').map((part) => Number(part)); } function detectFlowVersion() { try { const flowPackageJsonPath = resolve.sync('flow-bin/package.json', {basedir: process.cwd()}); - const flowPackageJson = require(flowPackageJsonPath); // eslint-disable-line import/no-dynamic-require + const flowPackageJson = require(flowPackageJsonPath); // eslint-disable-line global-require, import/no-dynamic-require return flowPackageJson.version; } catch (e) { if (e.code === 'MODULE_NOT_FOUND') { - error('Warning: Flow version was set to "detect" in eslint-plugin-react settings, ' + - 'but the "flow-bin" package is not installed. Assuming latest Flow version for linting.'); + error('Warning: Flow version was set to "detect" in eslint-plugin-react settings, ' + + 'but the "flow-bin" package is not installed. Assuming latest Flow version for linting.'); return '999.999.999'; } throw e; @@ -78,15 +78,15 @@ function getFlowVersionFromContext(context) { flowVersion = detectFlowVersion(); } if (typeof flowVersion !== 'string') { - error('Warning: Flow version specified in eslint-plugin-react-settings must be a string; ' + - `got “${typeof flowVersion}”`); + error('Warning: Flow version specified in eslint-plugin-react-settings must be a string; ' + + `got “${typeof flowVersion}”`); } confVer = String(flowVersion); } else { throw 'Could not retrieve flowVersion from settings'; // eslint-disable-line no-throw-literal } confVer = /^[0-9]+\.[0-9]+$/.test(confVer) ? `${confVer}.0` : confVer; - return confVer.split('.').map(part => Number(part)); + return confVer.split('.').map((part) => Number(part)); } function normalizeParts(parts) { @@ -94,13 +94,13 @@ function normalizeParts(parts) { } function test(context, methodVer, confVer) { - const methodVers = normalizeParts(String(methodVer || '').split('.').map(part => Number(part))); + const methodVers = normalizeParts(String(methodVer || '').split('.').map((part) => Number(part))); const confVers = normalizeParts(confVer); const higherMajor = methodVers[0] < confVers[0]; const higherMinor = methodVers[0] === confVers[0] && methodVers[1] < confVers[1]; - const higherOrEqualPatch = methodVers[0] === confVers[0] && - methodVers[1] === confVers[1] && - methodVers[2] <= confVers[2]; + const higherOrEqualPatch = methodVers[0] === confVers[0] + && methodVers[1] === confVers[1] + && methodVers[2] <= confVers[2]; return higherMajor || higherMinor || higherOrEqualPatch; } diff --git a/markdown.config.js b/markdown.config.js new file mode 100644 index 0000000000..d1bc849983 --- /dev/null +++ b/markdown.config.js @@ -0,0 +1,25 @@ +'use strict'; + +const {rules} = require('./index'); + +const ruleListItems = Object.keys(rules) + .sort() + .map((id) => { + const {meta} = rules[id]; + const {fixable, docs} = meta; + return `* [react/${id}](docs/rules/${id}.md): ${docs.description}${fixable ? ' (fixable)' : ''}`; + }); + +const BASIC_RULES = () => ruleListItems.filter((rule) => !rule.includes('react/jsx-')).join('\n'); +const JSX_RULES = () => ruleListItems.filter((rule) => rule.includes('react/jsx-')).join('\n'); + +module.exports = { + transforms: { + BASIC_RULES, + JSX_RULES + }, + callback: () => { + // eslint-disable-next-line no-console + console.log('The auto-generating of rules finished!'); + } +}; diff --git a/package.json b/package.json index 1102a0d303..610564f489 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,19 @@ { "name": "eslint-plugin-react", - "version": "7.14.3", + "version": "7.20.0", "author": "Yannick Croissant ", "description": "React specific linting rules for ESLint", "main": "index.js", "scripts": { - "coveralls": "cat ./reports/coverage/lcov.info | coveralls", + "coveralls": "cat ./coverage/lcov.info | coveralls", "lint": "eslint ./", "postlint": "npm run type-check", "pretest": "npm run lint", "test": "npm run unit-test", "type-check": "tsc", - "unit-test": "istanbul cover --dir reports/coverage node_modules/mocha/bin/_mocha tests/lib/**/*.js tests/util/**/*.js tests/index.js" + "unit-test": "istanbul cover node_modules/mocha/bin/_mocha tests/lib/**/*.js tests/util/**/*.js tests/index.js", + "generate-list-of-rules": "md-magic --path README.md", + "generate-list-of-rules:check": "npm run generate-list-of-rules && git diff --exit-code README.md" }, "files": [ "LICENSE", @@ -26,33 +28,39 @@ "homepage": "https://github.com/yannickcr/eslint-plugin-react", "bugs": "https://github.com/yannickcr/eslint-plugin-react/issues", "dependencies": { - "array-includes": "^3.0.3", + "array-includes": "^3.1.1", "doctrine": "^2.1.0", "has": "^1.0.3", - "jsx-ast-utils": "^2.1.0", - "object.entries": "^1.1.0", - "object.fromentries": "^2.0.0", - "object.values": "^1.1.0", + "jsx-ast-utils": "^2.2.3", + "object.entries": "^1.1.1", + "object.fromentries": "^2.0.2", + "object.values": "^1.1.1", "prop-types": "^15.7.2", - "resolve": "^1.10.1" + "resolve": "^1.15.1", + "string.prototype.matchall": "^4.0.2", + "xregexp": "^4.3.0" }, "devDependencies": { - "@types/eslint": "^4.16.6", - "@types/estree": "0.0.39", - "@types/node": "^12.0.0", + "@types/eslint": "^6.8.0", + "@types/estree": "0.0.44", + "@types/node": "^13.13.6", + "@typescript-eslint/parser": "^2.33.0", "babel-eslint": "^8.2.6", - "coveralls": "^3.0.2", - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", - "eslint-config-airbnb-base": "^13.1.0", - "eslint-plugin-import": "^2.17.2", + "coveralls": "^3.1.0", + "eslint": "^3 || ^4 || ^5 || ^6 || ^7", + "eslint-config-airbnb-base": "^14.1.0", + "eslint-plugin-eslint-plugin": "^2.2.1", + "eslint-plugin-import": "^2.20.2", "istanbul": "^0.4.5", + "markdown-magic": "^1.0.0", "mocha": "^5.2.0", - "sinon": "^7.2.2", - "typescript": "^3.2.2", + "semver": "^6.3.0", + "sinon": "^7.5.0", + "typescript": "^3.9.2", "typescript-eslint-parser": "^20.1.1" }, "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + "eslint": "^3 || ^4 || ^5 || ^6 || ^7" }, "engines": { "node": ">=4" @@ -63,5 +71,10 @@ "eslintplugin", "react" ], - "license": "MIT" + "license": "MIT", + "greenkeeper": { + "ignore": [ + "semver" + ] + } } diff --git a/tests/helpers/parsers.js b/tests/helpers/parsers.js index 64291b6125..73b7a2248a 100644 --- a/tests/helpers/parsers.js +++ b/tests/helpers/parsers.js @@ -1,10 +1,19 @@ 'use strict'; const path = require('path'); +const semver = require('semver'); +const version = require('eslint/package.json').version; const NODE_MODULES = '../../node_modules'; module.exports = { BABEL_ESLINT: path.join(__dirname, NODE_MODULES, 'babel-eslint'), - TYPESCRIPT_ESLINT: path.join(__dirname, NODE_MODULES, 'typescript-eslint-parser') + TYPESCRIPT_ESLINT: path.join(__dirname, NODE_MODULES, 'typescript-eslint-parser'), + '@TYPESCRIPT_ESLINT': path.join(__dirname, NODE_MODULES, '@typescript-eslint/parser'), + TS: function TS(tests) { + if (semver.satisfies(version, '>= 5')) { + return tests; + } + return []; + } }; diff --git a/tests/index.js b/tests/index.js index 4af8a22063..ef18b6d3cc 100644 --- a/tests/index.js +++ b/tests/index.js @@ -9,14 +9,14 @@ const path = require('path'); const plugin = require('..'); const ruleFiles = fs.readdirSync(path.resolve(__dirname, '../lib/rules/')) - .map(f => path.basename(f, '.js')); + .map((f) => path.basename(f, '.js')); describe('all rule files should be exported by the plugin', () => { ruleFiles.forEach((ruleName) => { it(`should export ${ruleName}`, () => { assert.equal( plugin.rules[ruleName], - require(path.join('../lib/rules', ruleName)) // eslint-disable-line import/no-dynamic-require + require(path.join('../lib/rules', ruleName)) // eslint-disable-line global-require, import/no-dynamic-require ); }); }); diff --git a/tests/lib/rules/button-has-type.js b/tests/lib/rules/button-has-type.js index 948626ec42..da343af73e 100644 --- a/tests/lib/rules/button-has-type.js +++ b/tests/lib/rules/button-has-type.js @@ -73,7 +73,13 @@ ruleTester.run('button-has-type', rule, { { code: '