diff --git a/its/ruling/src/test/expected/jsts/reddit-mobile/javascript-S1874.json b/its/ruling/src/test/expected/jsts/reddit-mobile/javascript-S1874.json new file mode 100644 index 00000000000..2af1eb1e6df --- /dev/null +++ b/its/ruling/src/test/expected/jsts/reddit-mobile/javascript-S1874.json @@ -0,0 +1,9 @@ +{ +"reddit-mobile:assets/js/client.es6.js": [ +212, +297 +], +"reddit-mobile:src/propTypes.es6.js": [ +3 +] +} diff --git a/its/ruling/src/test/expected/jsts/redux/javascript-S1874.json b/its/ruling/src/test/expected/jsts/redux/javascript-S1874.json new file mode 100644 index 00000000000..d3e53781ef3 --- /dev/null +++ b/its/ruling/src/test/expected/jsts/redux/javascript-S1874.json @@ -0,0 +1,122 @@ +{ +"redux:examples/async/components/Picker.js": [ +1 +], +"redux:examples/async/components/Posts.js": [ +1 +], +"redux:examples/async/containers/App.js": [ +1, +19 +], +"redux:examples/async/index.js": [ +3 +], +"redux:examples/counter/components/Counter.js": [ +1 +], +"redux:examples/counter/index.js": [ +2 +], +"redux:examples/real-world/components/Explore.js": [ +1, +12 +], +"redux:examples/real-world/components/List.js": [ +1 +], +"redux:examples/real-world/components/Repo.js": [ +1 +], +"redux:examples/real-world/components/User.js": [ +1 +], +"redux:examples/real-world/containers/App.js": [ +1 +], +"redux:examples/real-world/containers/RepoPage.js": [ +1, +21, +25 +], +"redux:examples/real-world/containers/Root.dev.js": [ +1 +], +"redux:examples/real-world/containers/Root.prod.js": [ +1 +], +"redux:examples/real-world/containers/UserPage.js": [ +1, +22, +26 +], +"redux:examples/real-world/index.js": [ +3 +], +"redux:examples/shopping-cart/components/Cart.js": [ +1 +], +"redux:examples/shopping-cart/components/Product.js": [ +1 +], +"redux:examples/shopping-cart/components/ProductItem.js": [ +1 +], +"redux:examples/shopping-cart/components/ProductsList.js": [ +1 +], +"redux:examples/shopping-cart/containers/CartContainer.js": [ +1 +], +"redux:examples/shopping-cart/containers/ProductsContainer.js": [ +1 +], +"redux:examples/shopping-cart/index.js": [ +2 +], +"redux:examples/todomvc/components/Footer.js": [ +1 +], +"redux:examples/todomvc/components/Header.js": [ +1 +], +"redux:examples/todomvc/components/MainSection.js": [ +1 +], +"redux:examples/todomvc/components/TodoItem.js": [ +1 +], +"redux:examples/todomvc/components/TodoTextInput.js": [ +1 +], +"redux:examples/todomvc/containers/App.js": [ +1 +], +"redux:examples/todomvc/index.js": [ +3 +], +"redux:examples/todos-with-undo/components/AddTodo.js": [ +1 +], +"redux:examples/todos-with-undo/components/Footer.js": [ +1 +], +"redux:examples/todos-with-undo/components/Todo.js": [ +1 +], +"redux:examples/todos-with-undo/components/TodoList.js": [ +1 +], +"redux:examples/todos-with-undo/containers/App.js": [ +1 +], +"redux:examples/todos-with-undo/index.js": [ +2 +], +"redux:examples/universal/client/index.js": [ +3 +], +"redux:examples/universal/common/components/Counter.js": [ +1 +] +} diff --git a/its/ruling/src/test/expected/jsts/sonar-web/javascript-S1874.json b/its/ruling/src/test/expected/jsts/sonar-web/javascript-S1874.json index 057cd8a7a68..94c1fa605cd 100644 --- a/its/ruling/src/test/expected/jsts/sonar-web/javascript-S1874.json +++ b/its/ruling/src/test/expected/jsts/sonar-web/javascript-S1874.json @@ -1,7 +1,482 @@ { +"sonar-web:src/main/js/apps/background-tasks/app.js": [ +7 +], +"sonar-web:src/main/js/apps/background-tasks/header.js": [ +3 +], +"sonar-web:src/main/js/apps/background-tasks/main.js": [ +14 +], +"sonar-web:src/main/js/apps/background-tasks/search.js": [ +8 +], +"sonar-web:src/main/js/apps/background-tasks/stats.js": [ +4 +], +"sonar-web:src/main/js/apps/background-tasks/tasks.js": [ +9, +11, +11 +], +"sonar-web:src/main/js/apps/global-permissions/app.js": [ +8 +], +"sonar-web:src/main/js/apps/global-permissions/main.js": [ +5 +], +"sonar-web:src/main/js/apps/global-permissions/permission-groups.js": [ +5 +], +"sonar-web:src/main/js/apps/global-permissions/permission-users-groups-mixin.js": [ +5, +6, +7, +8, +9 +], +"sonar-web:src/main/js/apps/global-permissions/permission-users.js": [ +5 +], +"sonar-web:src/main/js/apps/global-permissions/permission.js": [ +9, +11 +], +"sonar-web:src/main/js/apps/global-permissions/permissions-list.js": [ +7, +9, +9 +], +"sonar-web:src/main/js/apps/overview/app.js": [ +22, +24 +], +"sonar-web:src/main/js/apps/overview/components/complexity-distribution.js": [ +10, +12 +], +"sonar-web:src/main/js/apps/overview/components/coverage-measure.js": [ +9 +], +"sonar-web:src/main/js/apps/overview/components/coverage-measures-list.js": [ +27 +], +"sonar-web:src/main/js/apps/overview/components/detailed-measure.js": [ +8 +], +"sonar-web:src/main/js/apps/overview/components/domain-bubble-chart.js": [ +122, +123, +124, +124 +], +"sonar-web:src/main/js/apps/overview/components/domain-timeline.js": [ +20, +22, +22, +23, +23, +24 +], +"sonar-web:src/main/js/apps/overview/components/domain-treemap.js": [ +99, +100, +101 +], +"sonar-web:src/main/js/apps/overview/components/issue-measure.js": [ +11, +60, +113, +166, +211 +], +"sonar-web:src/main/js/apps/overview/components/issues-tags.js": [ +8 +], +"sonar-web:src/main/js/apps/overview/components/language-distribution.js": [ +9, +11, +12 +], +"sonar-web:src/main/js/apps/overview/components/legend.js": [ +6 +], +"sonar-web:src/main/js/apps/overview/components/timeline-chart.js": [ +10, +14, +14, +15, +15, +16, +17 +], +"sonar-web:src/main/js/apps/overview/domains/coverage-domain.js": [ +19 +], +"sonar-web:src/main/js/apps/overview/domains/debt-domain.js": [ +28 +], +"sonar-web:src/main/js/apps/overview/domains/duplications-domain.js": [ +16 +], +"sonar-web:src/main/js/apps/overview/domains/size-domain.js": [ +15 +], +"sonar-web:src/main/js/apps/overview/gate/gate-condition.js": [ +8, +19 +], +"sonar-web:src/main/js/apps/overview/gate/gate-conditions.js": [ +4, +6, +7 +], +"sonar-web:src/main/js/apps/overview/gate/gate-empty.js": [ +3 +], +"sonar-web:src/main/js/apps/overview/gate/gate.js": [ +7 +], +"sonar-web:src/main/js/apps/overview/main/components.js": [ +9, +16, +38, +51, +61, +63, +74, +80, +87, +94, +96, +97 +], +"sonar-web:src/main/js/apps/overview/main/coverage.js": [ +10, +14, +15, +16 +], +"sonar-web:src/main/js/apps/overview/main/duplications.js": [ +10, +14, +15 +], +"sonar-web:src/main/js/apps/overview/main/issues.js": [ +14, +18, +19 +], +"sonar-web:src/main/js/apps/overview/main/main.js": [ +44, +46 +], +"sonar-web:src/main/js/apps/overview/main/size.js": [ +11, +15, +16 +], +"sonar-web:src/main/js/apps/overview/main/timeline.js": [ +44, +44, +45, +46 +], +"sonar-web:src/main/js/apps/overview/meta.js": [ +6 +], +"sonar-web:src/main/js/apps/overview/overview.js": [ +15, +92 +], +"sonar-web:src/main/js/apps/permission-templates/app.js": [ +8 +], +"sonar-web:src/main/js/apps/permission-templates/header.js": [ +4 +], +"sonar-web:src/main/js/apps/permission-templates/main.js": [ +9, +11 +], +"sonar-web:src/main/js/apps/permission-templates/permission-template-defaults.js": [ +5, +7, +8 +], +"sonar-web:src/main/js/apps/permission-templates/permission-template-set-defaults.js": [ +6, +8, +9, +10 +], +"sonar-web:src/main/js/apps/permission-templates/permission-template.js": [ +11, +13, +14, +15 +], +"sonar-web:src/main/js/apps/permission-templates/permission-templates.js": [ +8, +10, +10, +11, +11, +12, +13 +], +"sonar-web:src/main/js/apps/permission-templates/permissions-header.js": [ +3, +5, +5 +], +"sonar-web:src/main/js/apps/project-permissions/app.js": [ +14 +], +"sonar-web:src/main/js/apps/project-permissions/main.js": [ +11, +13, +13 +], +"sonar-web:src/main/js/apps/project-permissions/permissions-footer.js": [ +5, +7, +8, +9 +], +"sonar-web:src/main/js/apps/project-permissions/permissions-header.js": [ +3, +5, +5 +], +"sonar-web:src/main/js/apps/project-permissions/permissions.js": [ +8, +10, +10, +11, +11, +12, +12, +13 +], +"sonar-web:src/main/js/apps/project-permissions/project.js": [ +8, +10, +11, +11, +12 +], +"sonar-web:src/main/js/apps/project-permissions/search.js": [ +4, +6 +], +"sonar-web:src/main/js/apps/projects/app.js": [ +12 +], +"sonar-web:src/main/js/apps/projects/header.js": [ +4, +6 +], +"sonar-web:src/main/js/apps/projects/main.js": [ +10, +12, +13 +], +"sonar-web:src/main/js/apps/projects/projects.js": [ +7, +9, +10, +11 +], +"sonar-web:src/main/js/apps/projects/search.js": [ +8, +10 +], +"sonar-web:src/main/js/apps/system/app.js": [ +7 +], +"sonar-web:src/main/js/apps/system/item-boolean.js": [ +3 +], +"sonar-web:src/main/js/apps/system/item-log-level.js": [ +6 +], +"sonar-web:src/main/js/apps/system/item-object.js": [ +4 +], +"sonar-web:src/main/js/apps/system/item-value.js": [ +6 +], +"sonar-web:src/main/js/apps/system/main.js": [ +9 +], +"sonar-web:src/main/js/apps/system/section.js": [ +4 +], +"sonar-web:src/main/js/components/charts/bar-chart.js": [ +7, +11, +11, +12, +12, +13, +13, +14, +15, +15, +16 +], +"sonar-web:src/main/js/components/charts/bubble-chart.js": [ +9, +11, +12, +13, +14, +15, +39, +43, +43, +44, +44, +45, +46, +47, +48, +49, +50, +50, +51, +52 +], +"sonar-web:src/main/js/components/charts/donut-chart.js": [ +8, +18, +22, +22 +], +"sonar-web:src/main/js/components/charts/histogram.js": [ +7, +11, +11, +12, +12, +13, +13, +14, +15, +16, +16, +17 +], +"sonar-web:src/main/js/components/charts/line-chart.js": [ +8, +12, +12, +13, +13, +14, +14, +15, +15, +16, +16, +17, +18, +19, +20, +21 +], +"sonar-web:src/main/js/components/charts/treemap.js": [ +30, +32, +33, +34, +35, +36, +37, +38, +67, +71, +71, +72 +], +"sonar-web:src/main/js/components/charts/word-cloud.js": [ +7, +9, +10, +11, +12, +28, +32, +32, +33, +33 +], "sonar-web:src/main/js/components/router/router.js": [ 16 ], +"sonar-web:src/main/js/components/shared/avatar.js": [ +4, +6, +7 +], +"sonar-web:src/main/js/components/shared/checkbox.js": [ +3, +5, +6, +7 +], +"sonar-web:src/main/js/components/shared/drilldown-link.js": [ +28 +], +"sonar-web:src/main/js/components/shared/favorite.js": [ +4, +6, +7 +], +"sonar-web:src/main/js/components/shared/issues-link.js": [ +6 +], +"sonar-web:src/main/js/components/shared/list-footer.js": [ +5, +7, +8, +9 +], +"sonar-web:src/main/js/components/shared/pending-icon.js": [ +3 +], +"sonar-web:src/main/js/components/shared/qualifier-icon.js": [ +3 +], +"sonar-web:src/main/js/components/shared/quality-gate-link.js": [ +4 +], +"sonar-web:src/main/js/components/shared/quality-profile-link.js": [ +4 +], +"sonar-web:src/main/js/components/shared/radio-toggle.js": [ +3, +5, +6, +7, +8 +], +"sonar-web:src/main/js/components/shared/rating.js": [ +6 +], +"sonar-web:src/main/js/components/shared/severity-helper.js": [ +4 +], +"sonar-web:src/main/js/components/shared/severity-icon.js": [ +3 +], +"sonar-web:src/main/js/components/shared/status-helper.js": [ +4 +], +"sonar-web:src/main/js/components/shared/status-icon.js": [ +3 +], "sonar-web:src/main/js/components/source-viewer/helpers/code-with-issue-locations-helper.js": [ 27, 27 @@ -11,5 +486,43 @@ ], "sonar-web:src/main/js/libs/translate.js": [ 76 +], +"sonar-web:src/main/js/main/nav/app.js": [ +40, +50, +61 +], +"sonar-web:src/main/js/main/nav/component/component-nav-breadcrumbs.js": [ +4 +], +"sonar-web:src/main/js/main/nav/component/component-nav-favorite.js": [ +4 +], +"sonar-web:src/main/js/main/nav/component/component-nav-menu.js": [ +12 +], +"sonar-web:src/main/js/main/nav/component/component-nav-meta.js": [ +5 +], +"sonar-web:src/main/js/main/nav/component/component-nav.js": [ +13 +], +"sonar-web:src/main/js/main/nav/global/global-nav-branding.js": [ +3 +], +"sonar-web:src/main/js/main/nav/global/global-nav-menu.js": [ +5 +], +"sonar-web:src/main/js/main/nav/global/global-nav-search.js": [ +15 +], +"sonar-web:src/main/js/main/nav/global/global-nav-user.js": [ +5 +], +"sonar-web:src/main/js/main/nav/global/global-nav.js": [ +8 +], +"sonar-web:src/main/js/main/nav/settings/settings-nav.js": [ +4 ] } diff --git a/jest.config.js b/jest.config.js index cc1b1b64748..6facd701dd8 100644 --- a/jest.config.js +++ b/jest.config.js @@ -6,9 +6,7 @@ module.exports = { moduleNameMapper: { '^@sonar/(\\w+)(.*)$': '/packages/$1/src$2', }, - modulePathIgnorePatterns: [ - '/packages/jsts/src/rules/S4328/fixtures/bom-package-json-project/package.json', - ], + modulePathIgnorePatterns: ['/packages/jsts/src/rules/.*/package.json$'], testResultsProcessor: 'jest-sonar-reporter', transform: { '^.+\\.ts$': ['ts-jest', { tsconfig: 'packages/tsconfig.test.json' }], diff --git a/packages/jsts/src/rules/S1874/cb.options.json b/packages/jsts/src/rules/S1874/cb.options.json new file mode 100644 index 00000000000..e6e97861c5c --- /dev/null +++ b/packages/jsts/src/rules/S1874/cb.options.json @@ -0,0 +1,5 @@ +[ + { + "react-version": "19.0" + } +] diff --git a/packages/jsts/src/rules/S1874/cb.react.fixture.tsx b/packages/jsts/src/rules/S1874/cb.react.fixture.tsx new file mode 100644 index 00000000000..7bfe411c6ec --- /dev/null +++ b/packages/jsts/src/rules/S1874/cb.react.fixture.tsx @@ -0,0 +1,38 @@ +import React, { PropTypes, Component } from 'react'; // Noncompliant +// ^^^^^^^^^ +import ReactDOM from 'react-dom'; + +React.render(, root); // Noncompliant + +React.unmountComponentAtNode(root); // Noncompliant + +React.findDOMNode(this.refs.foo); // Noncompliant + +React.renderToString(); // Noncompliant + +React.renderToStaticMarkup(); // Noncompliant + +React.createClass({ /* Class object */ }); // Noncompliant + +const propTypes = { + foo: PropTypes.bar, +}; + +//Any factories under React.DOM +React.DOM.div(); // Noncompliant + +class ApiCall extends Component { +// old lifecycles (since React 16.9) + componentWillMount() {} // Noncompliant + componentWillReceiveProps() {} // Noncompliant + componentWillUpdate() {} // Noncompliant +} + +// React 18 deprecations +ReactDOM.render(
, container); // Noncompliant + +ReactDOM.hydrate(
, container); // Noncompliant + +ReactDOM.unmountComponentAtNode(container); // Noncompliant + +ReactDOMServer.renderToNodeStream(element); // Noncompliant diff --git a/packages/jsts/src/rules/S1874/cb.test.ts b/packages/jsts/src/rules/S1874/cb.test.ts index 8fe3ff0701c..b08d5821fc8 100644 --- a/packages/jsts/src/rules/S1874/cb.test.ts +++ b/packages/jsts/src/rules/S1874/cb.test.ts @@ -20,9 +20,16 @@ import { check } from '../tools'; import { rule } from './'; import path from 'path'; +import { getAllPackageJsons, searchPackageJsonFiles } from '@sonar/jsts'; const sonarId = path.basename(__dirname); describe(`Rule ${sonarId}`, () => { + beforeEach(() => { + searchPackageJsonFiles(__dirname, []); + }); + afterAll(() => { + getAllPackageJsons().clear(); + }); check(sonarId, rule, __dirname); }); diff --git a/packages/jsts/src/rules/S1874/fixtures/noreact1/package.json b/packages/jsts/src/rules/S1874/fixtures/noreact1/package.json new file mode 100644 index 00000000000..eee4489a20e --- /dev/null +++ b/packages/jsts/src/rules/S1874/fixtures/noreact1/package.json @@ -0,0 +1,5 @@ +{ + "name": "no-react-app", + "version": "1.0.0", + "author": "Your Name " +} diff --git a/packages/jsts/src/rules/S1874/fixtures/noreact2/package.json b/packages/jsts/src/rules/S1874/fixtures/noreact2/package.json new file mode 100644 index 00000000000..9dbf3dc8436 --- /dev/null +++ b/packages/jsts/src/rules/S1874/fixtures/noreact2/package.json @@ -0,0 +1,11 @@ +{ + "name": "no-react-app2", + "version": "1.0.0", + "author": "Your Name ", + "dependencies": { + + }, + "devDependencies": { + + } +} diff --git a/packages/jsts/src/rules/S1874/fixtures/react15/package.json b/packages/jsts/src/rules/S1874/fixtures/react15/package.json new file mode 100644 index 00000000000..ccd1fb755fa --- /dev/null +++ b/packages/jsts/src/rules/S1874/fixtures/react15/package.json @@ -0,0 +1,8 @@ +{ + "name": "react15-app", + "version": "1.0.0", + "author": "Your Name ", + "dependencies": { + "react": "15.0" + } +} diff --git a/packages/jsts/src/rules/S1874/fixtures/react19/package.json b/packages/jsts/src/rules/S1874/fixtures/react19/package.json new file mode 100644 index 00000000000..622e19c0203 --- /dev/null +++ b/packages/jsts/src/rules/S1874/fixtures/react19/package.json @@ -0,0 +1,8 @@ +{ + "name": "react19-app", + "version": "1.0.0", + "author": "Your Name ", + "devDependencies": { + "react": "19.0" + } +} diff --git a/packages/jsts/src/rules/S1874/package.json b/packages/jsts/src/rules/S1874/package.json new file mode 100644 index 00000000000..334c7c72be3 --- /dev/null +++ b/packages/jsts/src/rules/S1874/package.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "react": "19.0" + }, + "devDependencies": { + "react": "15.0" + } +} diff --git a/packages/jsts/src/rules/S1874/rule.diagnostics.ts b/packages/jsts/src/rules/S1874/rule.diagnostics.ts new file mode 100644 index 00000000000..8afaae2952b --- /dev/null +++ b/packages/jsts/src/rules/S1874/rule.diagnostics.ts @@ -0,0 +1,63 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2011-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// https://sonarsource.github.io/rspec/#/rspec/S1874/javascript + +import { Rule } from 'eslint'; +import { isRequiredParserServices } from '../helpers'; +import * as ts from 'typescript'; + +export const rule: Rule.RuleModule = { + meta: { + messages: { + deprecation: '{{deprecation}}', + }, + }, + create(context: Rule.RuleContext) { + const services = context.parserServices; + if (!isRequiredParserServices(services)) { + return {}; + } + return { + Program: () => { + const program = services.program; + const checker = program.getTypeChecker(); + const sourceFile = program.getSourceFile(context.filename); + const diagnostics: ts.DiagnosticWithLocation[] = + // @ts-ignore: TypeChecker#getSuggestionDiagnostics is not publicly exposed + checker.getSuggestionDiagnostics(sourceFile); + for (const diagnostic of diagnostics) { + if (diagnostic.reportsDeprecated === true) { + const sourceCode = context.sourceCode; + const start = sourceCode.getLocFromIndex(diagnostic.start); + const end = sourceCode.getLocFromIndex(diagnostic.start + diagnostic.length); + const loc = { start, end }; + context.report({ + loc, + messageId: 'deprecation', + data: { + deprecation: diagnostic.messageText as string, + }, + }); + } + } + }, + }; + }, +}; diff --git a/packages/jsts/src/rules/S1874/rule.ts b/packages/jsts/src/rules/S1874/rule.ts index 8afaae2952b..64d4b02f759 100644 --- a/packages/jsts/src/rules/S1874/rule.ts +++ b/packages/jsts/src/rules/S1874/rule.ts @@ -20,44 +20,43 @@ // https://sonarsource.github.io/rspec/#/rspec/S1874/javascript import { Rule } from 'eslint'; -import { isRequiredParserServices } from '../helpers'; -import * as ts from 'typescript'; +import { rule as diagnosticsRule } from './rule.diagnostics'; +import { rules } from 'eslint-plugin-react'; +import { mergeRules } from '../helpers'; +import { getNearestPackageJsons } from '@sonar/jsts'; + +const reactNoDeprecated = rules['no-deprecated']; export const rule: Rule.RuleModule = { meta: { - messages: { - deprecation: '{{deprecation}}', - }, + messages: { ...reactNoDeprecated.meta!.messages, ...diagnosticsRule.meta!.messages }, }, create(context: Rule.RuleContext) { - const services = context.parserServices; - if (!isRequiredParserServices(services)) { - return {}; + function getVersionFromOptions() { + return context.options?.[0]?.['react-version']; } - return { - Program: () => { - const program = services.program; - const checker = program.getTypeChecker(); - const sourceFile = program.getSourceFile(context.filename); - const diagnostics: ts.DiagnosticWithLocation[] = - // @ts-ignore: TypeChecker#getSuggestionDiagnostics is not publicly exposed - checker.getSuggestionDiagnostics(sourceFile); - for (const diagnostic of diagnostics) { - if (diagnostic.reportsDeprecated === true) { - const sourceCode = context.sourceCode; - const start = sourceCode.getLocFromIndex(diagnostic.start); - const end = sourceCode.getLocFromIndex(diagnostic.start + diagnostic.length); - const loc = { start, end }; - context.report({ - loc, - messageId: 'deprecation', - data: { - deprecation: diagnostic.messageText as string, - }, - }); - } + function getVersionFromPackageJson() { + for (const { contents: packageJson } of getNearestPackageJsons(context.filename)) { + if (packageJson.dependencies?.react) { + return packageJson.dependencies.react; + } + if (packageJson.devDependencies?.react) { + return packageJson.devDependencies.react; } - }, - }; + } + return null; + } + + const reactVersion = getVersionFromOptions() || getVersionFromPackageJson(); + + const patchedContext = reactVersion + ? Object.create(context, { + settings: { + value: { react: { version: reactVersion } }, + writable: false, + }, + }) + : context; + return mergeRules(reactNoDeprecated.create(patchedContext), diagnosticsRule.create(context)); }, }; diff --git a/packages/jsts/src/rules/S1874/unit.test.ts b/packages/jsts/src/rules/S1874/unit.test.ts new file mode 100644 index 00000000000..7e0c5ea8174 --- /dev/null +++ b/packages/jsts/src/rules/S1874/unit.test.ts @@ -0,0 +1,214 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2011-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { RuleTester } from 'eslint'; +import { rule } from './'; +import path from 'path'; +import { getAllPackageJsons, searchPackageJsonFiles } from '@sonar/jsts'; + +//reset and search package.json files in rule dir +getAllPackageJsons().clear(); +searchPackageJsonFiles(__dirname, []); + +const fixtures = path.join(__dirname, 'fixtures'); +const filenameReact15 = path.join(fixtures, 'react15/file.js'); + +const tsParserPath = require.resolve('@typescript-eslint/parser'); +const ruleTester = new RuleTester({ + parser: tsParserPath, + parserOptions: { ecmaVersion: 2018, sourceType: 'module' }, +}); + +ruleTester.run('React15', rule, { + valid: [ + { + code: ` +import React from 'react'; +import ReactDOM from 'react-dom'; + +React.createClass({ /* Class object */ }); + +const propTypes = { + foo: React.PropTypes.bar, +}; + +//Any factories under React.DOM +React.DOM.div(); + +class ApiCall extends React.Component { +// old lifecycles (since React 16.9) + componentWillMount() {} + componentWillReceiveProps() {} + componentWillUpdate() {} +} + +// React 18 deprecations +ReactDOM.render(
, container); + +ReactDOM.hydrate(
, container); + +ReactDOM.unmountComponentAtNode(container); + +ReactDOMServer.renderToNodeStream(element); +`, + filename: filenameReact15, + }, + ], + invalid: [ + { + code: ` +import React from 'react'; +import ReactDOM from 'react-dom'; + +React.render(, root); + +React.unmountComponentAtNode(root); + +React.findDOMNode(this.refs.foo); + +React.renderToString(); + +React.renderToStaticMarkup(); +`, + filename: filenameReact15, + errors: 5, + }, + ], +}); + +const filenameReact19 = path.join(fixtures, 'react19/file.js'); + +ruleTester.run('React19', rule, { + valid: [ + { + code: ` +import React from 'react'; +import ReactDOM from 'react-dom'; +`, + filename: filenameReact19, + }, + ], + invalid: [ + { + code: ` +import React from 'react'; +import ReactDOM from 'react-dom'; + +React.render(, root); + +React.unmountComponentAtNode(root); + +React.findDOMNode(this.refs.foo); + +React.renderToString(); + +React.renderToStaticMarkup(); + +React.createClass({ /* Class object */ }); + +const propTypes = { + foo: React.PropTypes.bar, +}; + +//Any factories under React.DOM +React.DOM.div(); + +class ApiCall extends React.Component { +// old lifecycles (since React 16.9) + componentWillMount() {} + componentWillReceiveProps() {} + componentWillUpdate() {} +} + +// React 18 deprecations +ReactDOM.render(
, container); + +ReactDOM.hydrate(
, container); + +ReactDOM.unmountComponentAtNode(container); + +ReactDOMServer.renderToNodeStream(element); +`, + filename: filenameReact19, + errors: 15, + }, + ], +}); + +shouldRaiseAllIssues(path.join(fixtures, 'noreact1/file.js')); +shouldRaiseAllIssues(path.join(fixtures, 'noreact2/file.js')); + +function shouldRaiseAllIssues(filename) { + ruleTester.run(`No React ${filename}`, rule, { + valid: [ + { + code: ` +import React from 'react'; +import ReactDOM from 'react-dom'; +`, + filename, + }, + ], + invalid: [ + { + code: ` +import React from 'react'; +import ReactDOM from 'react-dom'; + +React.render(, root); + +React.unmountComponentAtNode(root); + +React.findDOMNode(this.refs.foo); + +React.renderToString(); + +React.renderToStaticMarkup(); + +React.createClass({ /* Class object */ }); + +const propTypes = { + foo: React.PropTypes.bar, +}; + +//Any factories under React.DOM +React.DOM.div(); + +class ApiCall extends React.Component { +// old lifecycles (since React 16.9) + componentWillMount() {} + componentWillReceiveProps() {} + componentWillUpdate() {} +} + +// React 18 deprecations +ReactDOM.render(
, container); + +ReactDOM.hydrate(
, container); + +ReactDOM.unmountComponentAtNode(container); + +ReactDOMServer.renderToNodeStream(element); +`, + filename, + errors: 15, + }, + ], + }); +}