From 3c4ab605db01dbd9d11aca70761c4d77046c59e2 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Tue, 19 Sep 2023 01:38:57 +0200 Subject: [PATCH] Fix eslint-plugin-twenty (#1640) * Fixed color rule * Fixed naming * Fix effect component rule * Deactivated broken rules * Fixed lint * Complete eslint-plugin-twenty work --------- Co-authored-by: Charles Bochet --- front/.eslintrc.js | 12 +- front/package.json | 4 +- .../src/modules/apollo/utils/format-title.ts | 5 + front/src/modules/apollo/utils/index.ts | 1 + .../PeopleEntityTableDataEffect.tsx | 2 +- .../components/AnimatedCheckmark.tsx | 48 ++--- .../input/components/DoubleTextInputEdit.tsx | 10 +- .../modules/ui/input/components/Toggle.tsx | 2 +- .../SingleEntitySelect.stories.tsx | 4 +- .../src/modules/ui/theme/constants/colors.ts | 1 + front/src/modules/ui/theme/constants/theme.ts | 1 + ...nsEffect.tsx => ComputeNodeDimensions.tsx} | 2 +- .../drag-select/components/DragSelect.tsx | 8 +- front/yarn.lock | 4 +- .../src/rules/effect-components.ts | 114 ----------- .../src/rules/no-hardcoded-colors.ts | 49 ----- .../styled-components-prefixed-with-styled.ts | 51 ----- .../src/tests/matching-state-variable.spec.ts | 47 ----- ...sort-css-properties-alphabetically.spec.ts | 58 ------ ...ed-components-prefixed-with-styled.spec.ts | 46 ----- packages/eslint-plugin-twenty/.eslintrc.js | 69 +++++++ .../.gitignore | 0 .../index.ts | 0 .../jest.config.js | 0 .../package.json | 10 +- .../src/rules/effect-components.ts | 166 ++++++++++++++++ .../src/rules/matching-state-variable.ts | 47 ++--- .../src/rules/no-hardcoded-colors.ts | 64 ++++++ .../sort-css-properties-alphabetically.ts | 118 ++++++----- .../styled-components-prefixed-with-styled.ts | 62 ++++++ .../src/tests/effect-components.spec.ts | 4 +- .../src/tests/file.ts | 2 +- .../src/tests/matching-state-variable.spec.ts | 185 ++++++++++++++++++ .../src/tests/no-hardcoded-colors.spec.ts | 28 ++- .../src/tests/react.tsx | 2 +- ...sort-css-properties-alphabetically.spec.ts | 56 ++++++ ...ed-components-prefixed-with-styled.spec.ts | 47 +++++ .../src/tests/tsconfig.json | 0 .../tsconfig.json | 0 .../yarn.lock | 22 +++ 40 files changed, 864 insertions(+), 487 deletions(-) rename front/src/modules/ui/utilities/dimensions/components/{ComputeNodeDimensionsEffect.tsx => ComputeNodeDimensions.tsx} (95%) delete mode 100644 packages/eslint-plugin-twenty-ts/src/rules/effect-components.ts delete mode 100644 packages/eslint-plugin-twenty-ts/src/rules/no-hardcoded-colors.ts delete mode 100644 packages/eslint-plugin-twenty-ts/src/rules/styled-components-prefixed-with-styled.ts delete mode 100644 packages/eslint-plugin-twenty-ts/src/tests/matching-state-variable.spec.ts delete mode 100644 packages/eslint-plugin-twenty-ts/src/tests/sort-css-properties-alphabetically.spec.ts delete mode 100644 packages/eslint-plugin-twenty-ts/src/tests/styled-components-prefixed-with-styled.spec.ts create mode 100644 packages/eslint-plugin-twenty/.eslintrc.js rename packages/{eslint-plugin-twenty-ts => eslint-plugin-twenty}/.gitignore (100%) rename packages/{eslint-plugin-twenty-ts => eslint-plugin-twenty}/index.ts (100%) rename packages/{eslint-plugin-twenty-ts => eslint-plugin-twenty}/jest.config.js (100%) rename packages/{eslint-plugin-twenty-ts => eslint-plugin-twenty}/package.json (73%) create mode 100644 packages/eslint-plugin-twenty/src/rules/effect-components.ts rename packages/{eslint-plugin-twenty-ts => eslint-plugin-twenty}/src/rules/matching-state-variable.ts (76%) create mode 100644 packages/eslint-plugin-twenty/src/rules/no-hardcoded-colors.ts rename packages/{eslint-plugin-twenty-ts => eslint-plugin-twenty}/src/rules/sort-css-properties-alphabetically.ts (70%) create mode 100644 packages/eslint-plugin-twenty/src/rules/styled-components-prefixed-with-styled.ts rename packages/{eslint-plugin-twenty-ts => eslint-plugin-twenty}/src/tests/effect-components.spec.ts (93%) rename packages/{eslint-plugin-twenty-ts => eslint-plugin-twenty}/src/tests/file.ts (62%) create mode 100644 packages/eslint-plugin-twenty/src/tests/matching-state-variable.spec.ts rename packages/{eslint-plugin-twenty-ts => eslint-plugin-twenty}/src/tests/no-hardcoded-colors.spec.ts (63%) rename packages/{eslint-plugin-twenty-ts => eslint-plugin-twenty}/src/tests/react.tsx (62%) create mode 100644 packages/eslint-plugin-twenty/src/tests/sort-css-properties-alphabetically.spec.ts create mode 100644 packages/eslint-plugin-twenty/src/tests/styled-components-prefixed-with-styled.spec.ts rename packages/{eslint-plugin-twenty-ts => eslint-plugin-twenty}/src/tests/tsconfig.json (100%) rename packages/{eslint-plugin-twenty-ts => eslint-plugin-twenty}/tsconfig.json (100%) rename packages/{eslint-plugin-twenty-ts => eslint-plugin-twenty}/yarn.lock (99%) diff --git a/front/.eslintrc.js b/front/.eslintrc.js index c7a5c56fd..dc688765d 100644 --- a/front/.eslintrc.js +++ b/front/.eslintrc.js @@ -10,7 +10,7 @@ module.exports = { 'unused-imports', 'simple-import-sort', 'prefer-arrow', - 'twenty-ts', + 'twenty', ], extends: [ 'plugin:@typescript-eslint/recommended', @@ -60,11 +60,11 @@ module.exports = { '@typescript-eslint/no-explicit-any': 'off', 'simple-import-sort/imports': 'error', 'simple-import-sort/exports': 'error', - 'twenty-ts/effect-components': 'error', - 'twenty-ts/no-hardcoded-colors': 'error', - 'twenty-ts/matching-state-variable': 'error', - 'twenty-ts/sort-css-properties-alphabetically': 'error', - 'twenty-ts/styled-components-prefixed-with-styled': 'error', + 'twenty/effect-components': 'error', + 'twenty/no-hardcoded-colors': 'error', + 'twenty/matching-state-variable': 'error', + 'twenty/sort-css-properties-alphabetically': 'error', + 'twenty/styled-components-prefixed-with-styled': 'error', 'func-style':['error', 'declaration', { 'allowArrowFunctions': true }], "@typescript-eslint/no-unused-vars": "off", "no-unused-vars": "off", diff --git a/front/package.json b/front/package.json index 79a56e241..a916d1ccc 100644 --- a/front/package.json +++ b/front/package.json @@ -69,7 +69,7 @@ "test": "craco test", "coverage": "craco test --coverage .", "lint": "eslint src --max-warnings=0", - "lint:setup": "cd ../packages/eslint-plugin-twenty-ts/ && yarn && yarn build && cd ../../front/ && yarn upgrade eslint-plugin-twenty-ts", + "lint:setup": "cd ../packages/eslint-plugin-twenty/ && yarn && yarn build && cd ../../front/ && yarn upgrade eslint-plugin-twenty", "storybook:dev": "storybook dev -p 6006 -s ../public", "storybook:test": "test-storybook", "storybook:test-slow": "test-storybook --maxWorkers=3", @@ -167,7 +167,7 @@ "eslint-plugin-react": "^7.31.11", "eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-storybook": "^0.6.12", - "eslint-plugin-twenty-ts": "file:../packages/eslint-plugin-twenty-ts", + "eslint-plugin-twenty": "file:../packages/eslint-plugin-twenty", "eslint-plugin-unused-imports": "^3.0.0", "http-server": "^14.1.1", "mock-apollo-client": "^1.2.1", diff --git a/front/src/modules/apollo/utils/format-title.ts b/front/src/modules/apollo/utils/format-title.ts index 8c69309d5..b28b01dc0 100644 --- a/front/src/modules/apollo/utils/format-title.ts +++ b/front/src/modules/apollo/utils/format-title.ts @@ -1,10 +1,15 @@ import { OperationType } from '../types/operation-type'; const operationTypeColors = { + // eslint-disable-next-line twenty/no-hardcoded-colors query: '#03A9F4', + // eslint-disable-next-line twenty/no-hardcoded-colors mutation: '#61A600', + // eslint-disable-next-line twenty/no-hardcoded-colors subscription: '#61A600', + // eslint-disable-next-line twenty/no-hardcoded-colors error: '#F51818', + // eslint-disable-next-line twenty/no-hardcoded-colors default: '#61A600', }; diff --git a/front/src/modules/apollo/utils/index.ts b/front/src/modules/apollo/utils/index.ts index 35ffbb509..8c91a2b80 100644 --- a/front/src/modules/apollo/utils/index.ts +++ b/front/src/modules/apollo/utils/index.ts @@ -66,6 +66,7 @@ export const loggerLink = (getSchemaName: (operation: Operation) => string) => errors.forEach((err: any) => { console.log( `%c${err.message}`, + // eslint-disable-next-line twenty/no-hardcoded-colors 'color: #F51818; font-weight: lighter', ); }); diff --git a/front/src/modules/people/components/PeopleEntityTableDataEffect.tsx b/front/src/modules/people/components/PeopleEntityTableDataEffect.tsx index 853056d7d..9bd1d408b 100644 --- a/front/src/modules/people/components/PeopleEntityTableDataEffect.tsx +++ b/front/src/modules/people/components/PeopleEntityTableDataEffect.tsx @@ -6,7 +6,7 @@ import { import { useSetPeopleEntityTable } from '../hooks/useSetPeopleEntityTable'; -export const PeopleEntityTableData = ({ +export const PeopleEntityTableDataEffect = ({ orderBy = [ { createdAt: SortOrder.Desc, diff --git a/front/src/modules/ui/checkmark/components/AnimatedCheckmark.tsx b/front/src/modules/ui/checkmark/components/AnimatedCheckmark.tsx index 2940e1fcf..6732fce5b 100644 --- a/front/src/modules/ui/checkmark/components/AnimatedCheckmark.tsx +++ b/front/src/modules/ui/checkmark/components/AnimatedCheckmark.tsx @@ -1,3 +1,4 @@ +import { useTheme } from '@emotion/react'; import { motion } from 'framer-motion'; export type CheckmarkProps = React.ComponentProps & { @@ -9,28 +10,31 @@ export type CheckmarkProps = React.ComponentProps & { export const AnimatedCheckmark = ({ isAnimating = false, - color = '#FFF', + color, duration = 0.5, size = 28, ...restProps -}: CheckmarkProps) => ( - - - -); +}: CheckmarkProps) => { + const theme = useTheme(); + return ( + + + + ); +}; diff --git a/front/src/modules/ui/input/components/DoubleTextInputEdit.tsx b/front/src/modules/ui/input/components/DoubleTextInputEdit.tsx index 3a60ab691..a9651e82b 100644 --- a/front/src/modules/ui/input/components/DoubleTextInputEdit.tsx +++ b/front/src/modules/ui/input/components/DoubleTextInputEdit.tsx @@ -2,7 +2,7 @@ import { ChangeEvent } from 'react'; import styled from '@emotion/styled'; import { StyledInput } from '@/ui/input/components/TextInput'; -import { ComputeNodeDimensionsEffect } from '@/ui/utilities/dimensions/components/ComputeNodeDimensionsEffect'; +import { ComputeNodeDimensions } from '@/ui/utilities/dimensions/components/ComputeNodeDimensions'; export type DoubleTextInputEditProps = { firstValue: string; @@ -40,7 +40,7 @@ export const DoubleTextInputEdit = ({ onChange, }: DoubleTextInputEditProps) => ( - + {(nodeDimensions) => ( )} - - + + {(nodeDimensions) => ( )} - + ); diff --git a/front/src/modules/ui/input/components/Toggle.tsx b/front/src/modules/ui/input/components/Toggle.tsx index 594a021cc..9eedf0788 100644 --- a/front/src/modules/ui/input/components/Toggle.tsx +++ b/front/src/modules/ui/input/components/Toggle.tsx @@ -20,7 +20,7 @@ const StyledContainer = styled.div` `; const StyledCircle = styled(motion.div)` - background-color: #fff; + background-color: ${({ theme }) => theme.background.primary}; border-radius: 50%; height: 16px; width: 16px; diff --git a/front/src/modules/ui/input/relation-picker/components/__stories__/SingleEntitySelect.stories.tsx b/front/src/modules/ui/input/relation-picker/components/__stories__/SingleEntitySelect.stories.tsx index 9c7c29c19..e3978fa7c 100644 --- a/front/src/modules/ui/input/relation-picker/components/__stories__/SingleEntitySelect.stories.tsx +++ b/front/src/modules/ui/input/relation-picker/components/__stories__/SingleEntitySelect.stories.tsx @@ -35,7 +35,7 @@ const meta: Meta = { }, render: (args) => { // eslint-disable-next-line react-hooks/rules-of-hooks - const searchFilter = useRecoilScopedValue( + const relationPickerSearchFilter = useRecoilScopedValue( relationPickerSearchFilterScopedState, ); @@ -45,7 +45,7 @@ const meta: Meta = { entitiesToSelect={entities.filter( (entity) => entity.id !== args.selectedEntity?.id && - entity.name.includes(searchFilter), + entity.name.includes(relationPickerSearchFilter), )} /> ); diff --git a/front/src/modules/ui/theme/constants/colors.ts b/front/src/modules/ui/theme/constants/colors.ts index 4e2f33811..1425b06f6 100644 --- a/front/src/modules/ui/theme/constants/colors.ts +++ b/front/src/modules/ui/theme/constants/colors.ts @@ -1,3 +1,4 @@ +/* eslint-disable twenty/no-hardcoded-colors */ import hexRgb from 'hex-rgb'; export const grayScale = { diff --git a/front/src/modules/ui/theme/constants/theme.ts b/front/src/modules/ui/theme/constants/theme.ts index 2b67f1e20..4ed41a6c1 100644 --- a/front/src/modules/ui/theme/constants/theme.ts +++ b/front/src/modules/ui/theme/constants/theme.ts @@ -1,3 +1,4 @@ +/* eslint-disable twenty/no-hardcoded-colors */ import { accentDark, accentLight } from './accent'; import { animation } from './animation'; import { backgroundDark, backgroundLight } from './background'; diff --git a/front/src/modules/ui/utilities/dimensions/components/ComputeNodeDimensionsEffect.tsx b/front/src/modules/ui/utilities/dimensions/components/ComputeNodeDimensions.tsx similarity index 95% rename from front/src/modules/ui/utilities/dimensions/components/ComputeNodeDimensionsEffect.tsx rename to front/src/modules/ui/utilities/dimensions/components/ComputeNodeDimensions.tsx index ac7bfb869..b78f773b8 100644 --- a/front/src/modules/ui/utilities/dimensions/components/ComputeNodeDimensionsEffect.tsx +++ b/front/src/modules/ui/utilities/dimensions/components/ComputeNodeDimensions.tsx @@ -14,7 +14,7 @@ const StyledNodeWrapper = styled.span` visibility: hidden; `; -export const ComputeNodeDimensionsEffect = ({ +export const ComputeNodeDimensions = ({ children, node = children(undefined), }: ComputeNodeDimensionsEffectProps) => { diff --git a/front/src/modules/ui/utilities/drag-select/components/DragSelect.tsx b/front/src/modules/ui/utilities/drag-select/components/DragSelect.tsx index 48f5b375e..dba9e33cb 100644 --- a/front/src/modules/ui/utilities/drag-select/components/DragSelect.tsx +++ b/front/src/modules/ui/utilities/drag-select/components/DragSelect.tsx @@ -3,6 +3,9 @@ import { boxesIntersect, useSelectionContainer, } from '@air/react-drag-to-select'; +import { useTheme } from '@emotion/react'; + +import { rgba } from '@/ui/theme/constants/colors'; import { useDragSelect } from '../hooks/useDragSelect'; @@ -17,6 +20,7 @@ export const DragSelect = ({ onDragSelectionChange, onDragSelectionStart, }: OwnProps) => { + const theme = useTheme(); const { isDragSelectionStartEnabled } = useDragSelect(); const { DragSelection } = useSelectionContainer({ shouldStartSelecting: (target) => { @@ -56,8 +60,8 @@ export const DragSelect = ({ }, selectionProps: { style: { - border: '1px solid #4C85D8', - background: 'rgba(155, 193, 239, 0.4)', + border: `1px solid ${theme.color.blue10}`, + background: rgba(theme.color.blue30, 0.4), position: `absolute`, zIndex: 99, }, diff --git a/front/yarn.lock b/front/yarn.lock index 9d3ddd9db..a5446daf2 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -9829,8 +9829,8 @@ eslint-plugin-testing-library@^5.0.1: dependencies: "@typescript-eslint/utils" "^5.58.0" -"eslint-plugin-twenty-ts@file:../packages/eslint-plugin-twenty-ts": - version "1.0.2" +"eslint-plugin-twenty@file:../packages/eslint-plugin-twenty": + version "1.0.3" eslint-plugin-unused-imports@^3.0.0: version "3.0.0" diff --git a/packages/eslint-plugin-twenty-ts/src/rules/effect-components.ts b/packages/eslint-plugin-twenty-ts/src/rules/effect-components.ts deleted file mode 100644 index 059029bf5..000000000 --- a/packages/eslint-plugin-twenty-ts/src/rules/effect-components.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { TSESTree, ESLintUtils } from "@typescript-eslint/utils"; - -const createRule = ESLintUtils.RuleCreator((name) => `https://docs.twenty.com`); - -const checkIsPascalCase = (input: string): boolean => { - const pascalCaseRegex = /^(?:\p{Uppercase_Letter}\p{Letter}*)+$/u; - - return pascalCaseRegex.test(input); -}; - -const effectComponentsRule = createRule({ - create(context) { - const checkThatNodeIsEffectComponent = (node: TSESTree.FunctionDeclaration | TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression) => { - const isPascalCase = checkIsPascalCase(node.id?.name ?? ""); - - if(!isPascalCase) { - return; - } - - const isReturningFragmentOrNull = ( - // Direct return of JSX fragment, e.g., () => <> - (node.body.type === 'JSXFragment' && node.body.children.length === 0) || - // Direct return of null, e.g., () => null - (node.body.type === 'Literal' && node.body.value === null) || - // Return JSX fragment or null from block - (node.body.type === 'BlockStatement' && - node.body.body.some(statement => - statement.type === 'ReturnStatement' && - ( - // Empty JSX fragment return, e.g., return <>; - (statement.argument?.type === 'JSXFragment' && statement.argument.children.length === 0) || - // Empty React.Fragment return, e.g., return ; - (statement.argument?.type === 'JSXElement' && - statement.argument.openingElement.name.type === 'JSXIdentifier' && - statement.argument.openingElement.name.name === 'React.Fragment' && - statement.argument.children.length === 0) || - // Literal null return, e.g., return null; - (statement.argument?.type === 'Literal' && statement.argument.value === null) - ) - )) - ); - - const hasEffectSuffix = node.id?.name.endsWith("Effect"); - - const hasEffectSuffixButIsNotEffectComponent = hasEffectSuffix && !isReturningFragmentOrNull - const isEffectComponentButDoesNotHaveEffectSuffix = !hasEffectSuffix && isReturningFragmentOrNull; - - if(isEffectComponentButDoesNotHaveEffectSuffix) { - context.report({ - node, - messageId: "effectSuffix", - data: { - componentName: node.id?.name, - }, - fix(fixer) { - if (node.id) { - return fixer.replaceText( - node.id, - node.id?.name + "Effect", - ); - } - - return null; - }, - }); - } else if(hasEffectSuffixButIsNotEffectComponent) { - context.report({ - node, - messageId: "noEffectSuffix", - data: { - componentName: node.id?.name, - }, - fix(fixer) { - if (node.id) { - return fixer.replaceText( - node.id, - node.id?.name.replace("Effect", ""), - ); - } - - return null; - }, - }); - } - } - - return { - ArrowFunctionExpression: checkThatNodeIsEffectComponent, - FunctionDeclaration: checkThatNodeIsEffectComponent, - FunctionExpression: checkThatNodeIsEffectComponent, - }; - }, - name: "effect-components", - meta: { - docs: { - description: - "Effect components should end with the Effect suffix. This rule checks only components that are in PascalCase and that return a JSX fragment or null. Any renderProps or camelCase components are ignored.", - }, - messages: { - effectSuffix: - "Effect component {{ componentName }} should end with the Effect suffix.", - noEffectSuffix: - "Component {{ componentName }} shouldn't end with the Effect suffix because it doesn't return a JSX fragment or null.", - }, - type: "suggestion", - schema: [], - fixable: "code", - }, - defaultOptions: [], -}); - -module.exports = effectComponentsRule; - -export default effectComponentsRule; diff --git a/packages/eslint-plugin-twenty-ts/src/rules/no-hardcoded-colors.ts b/packages/eslint-plugin-twenty-ts/src/rules/no-hardcoded-colors.ts deleted file mode 100644 index 9c542d9e5..000000000 --- a/packages/eslint-plugin-twenty-ts/src/rules/no-hardcoded-colors.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { TSESTree, ESLintUtils } from "@typescript-eslint/utils"; - -const createRule = ESLintUtils.RuleCreator((name) => `https://docs.twenty.com`); - -const noHardcodedColorsRule = createRule({ - create(context) { - return { - TaggedTemplateExpression(node: TSESTree.TaggedTemplateExpression) { - if (context.getFilename().endsWith("themes.ts")) { - return; - } - - node.quasi.quasis.forEach((quasi) => { - const colorRegex = - /(?:rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})(,\s*\d+\.?\d*)?\))|(?:#[0-9a-fA-F]{6})/i; - - if (colorRegex.test(quasi.value.raw)) { - context.report({ - node, - messageId: "hardcodedColor", - data: { - color: quasi.value.raw, - }, - }); - } - }); - }, - }; - }, - name: "no-hardcoded-colors", - meta: { - docs: { - description: - "Do not use hardcoded RGBA or Hex colors. Please use a color from the theme file.", - }, - messages: { - hardcodedColor: - "Hardcoded color {{ color }} found. Please use a color from the theme file.", - }, - type: "suggestion", - schema: [], - fixable: "code", - }, - defaultOptions: [], -}); - -module.exports = noHardcodedColorsRule; - -export default noHardcodedColorsRule; \ No newline at end of file diff --git a/packages/eslint-plugin-twenty-ts/src/rules/styled-components-prefixed-with-styled.ts b/packages/eslint-plugin-twenty-ts/src/rules/styled-components-prefixed-with-styled.ts deleted file mode 100644 index 60c2e43f2..000000000 --- a/packages/eslint-plugin-twenty-ts/src/rules/styled-components-prefixed-with-styled.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { TSESTree, ESLintUtils, AST_NODE_TYPES } from "@typescript-eslint/utils"; - -const createRule = ESLintUtils.RuleCreator((name) => `https://docs.twenty.com`); - -const styledComponentsPrefixedWithStyledRule = createRule({ - create(context) { - return { - VariableDeclarator: (node: TSESTree.VariableDeclarator) => { - const templateExpr = node.init - if (templateExpr?.type !== AST_NODE_TYPES.TaggedTemplateExpression) { - return; - } - const tag = templateExpr.tag - const tagged = tag.type === AST_NODE_TYPES.MemberExpression ? tag.object - : tag.type === AST_NODE_TYPES.CallExpression ? tag.callee - : null - if (tagged?.type === AST_NODE_TYPES.Identifier && tagged.name === 'styled') { - const variable = node.id as TSESTree.Identifier; - if (variable.name.startsWith('Styled')) { - return; - } - context.report({ - node, - messageId: 'noStyledPrefix', - data: { - componentName: variable.name - } - }); - } - }, - } - }, - name: 'styled-components-prefixed-with-styled', - meta: { - type: 'suggestion', - docs: { - description: 'Warn when StyledComponents are not prefixed with Styled', - recommended: "recommended" - }, - messages: { - noStyledPrefix: '{{componentName}} is a StyledComponent and is not prefixed with Styled.', - }, - fixable: 'code', - schema: [], - }, - defaultOptions: [] -}); - -module.exports = styledComponentsPrefixedWithStyledRule; - -export default styledComponentsPrefixedWithStyledRule; \ No newline at end of file diff --git a/packages/eslint-plugin-twenty-ts/src/tests/matching-state-variable.spec.ts b/packages/eslint-plugin-twenty-ts/src/tests/matching-state-variable.spec.ts deleted file mode 100644 index f6bd2ce4a..000000000 --- a/packages/eslint-plugin-twenty-ts/src/tests/matching-state-variable.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { RuleTester } from "@typescript-eslint/rule-tester"; -import matchingStateVariableRule from "../rules/matching-state-variable"; - -const ruleTester = new RuleTester({ - parser: "@typescript-eslint/parser", - parserOptions: { - project: "./tsconfig.json", - tsconfigRootDir: __dirname, - ecmaFeatures: { - jsx: true, - }, - }, -}); - -ruleTester.run('matching-state-variable', matchingStateVariableRule, { - valid: [ - { - code: 'const variable = useRecoilValue(variableState);', - }, - { - code: 'const [variable, setVariable] = useRecoilState(variableState);', - }, - ], - invalid: [ - { - code: 'const myValue = useRecoilValue(variableState);', - errors: [ - { - messageId: 'invalidVariableName', - }, - ], - output: 'const variable = useRecoilValue(variableState);', - }, - { - code: 'const [myValue, setMyValue] = useRecoilState(variableState);', - errors: [ - { - messageId: 'invalidVariableName', - }, - { - messageId: 'invalidSetterName', - }, - ], - output: 'const [variable, setVariable] = useRecoilState(variableState);', - }, - ], -}); diff --git a/packages/eslint-plugin-twenty-ts/src/tests/sort-css-properties-alphabetically.spec.ts b/packages/eslint-plugin-twenty-ts/src/tests/sort-css-properties-alphabetically.spec.ts deleted file mode 100644 index df43067e2..000000000 --- a/packages/eslint-plugin-twenty-ts/src/tests/sort-css-properties-alphabetically.spec.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { RuleTester } from "@typescript-eslint/rule-tester"; -import sortCssPropertiesAlphabeticallyRule from "../rules/sort-css-properties-alphabetically"; - -const ruleTester = new RuleTester({ - parser: "@typescript-eslint/parser", - parserOptions: { - project: "./tsconfig.json", - tsconfigRootDir: __dirname, - ecmaFeatures: { - jsx: true, - }, - }, -}); - -ruleTester.run("sort-css-properties-alphabetically", sortCssPropertiesAlphabeticallyRule, { - valid: [ - { - code: 'const style = css`color: red;`;', - filename: 'react.tsx', - }, - { - code: 'const style = styled.div`background-color: $bgColor;`;', - filename: 'react.tsx', - }, - ], - invalid: [ - { - code: 'const style = css`color: #FF0000;`;', - filename: 'react.tsx', - errors: [ - { - messageId: "sort-css-properties-alphabetically", - suggestions: [ - { - messageId: "sort-css-properties-alphabetically", - output: 'const style = css`color: red;`;', - }, - ], - }, - ], - }, - { - code: 'const style = styled.div`background-color: $bgColor; color: #FFFFFF;`;', - filename: 'react.tsx', - errors: [ - { - messageId: "sort-css-properties-alphabetically", - suggestions: [ - { - messageId: "sort-css-properties-alphabetically", - output: 'const style = styled.div`background-color: $bgColor; color: white;`;', - }, - ], - }, - ], - }, - ], -}); diff --git a/packages/eslint-plugin-twenty-ts/src/tests/styled-components-prefixed-with-styled.spec.ts b/packages/eslint-plugin-twenty-ts/src/tests/styled-components-prefixed-with-styled.spec.ts deleted file mode 100644 index 19da8e8da..000000000 --- a/packages/eslint-plugin-twenty-ts/src/tests/styled-components-prefixed-with-styled.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { RuleTester } from "@typescript-eslint/rule-tester"; -import styledComponentsPrefixedWithStyledRule from "../rules/styled-components-prefixed-with-styled"; - -const ruleTester = new RuleTester({ - parser: "@typescript-eslint/parser", - parserOptions: { - project: "./tsconfig.json", - tsconfigRootDir: __dirname, - ecmaFeatures: { - jsx: true, - }, - }, -}); - -ruleTester.run("styled-components-prefixed-with-styled", styledComponentsPrefixedWithStyledRule, { - valid: [ - { - code: 'const StyledButton = styled.button``;', - filename: 'react.tsx', - }, - { - code: 'const StyledComponent = styled.div``;', - filename: 'react.tsx', - }, - ], - invalid: [ - { - code: 'const Button = styled.button``;', - filename: 'react.tsx', - errors: [ - { - messageId: 'noStyledPrefix', - }, - ], - }, - { - code: 'const Component = styled.div``;', - filename: 'react.tsx', - errors: [ - { - messageId: 'noStyledPrefix', - }, - ], - }, - ], -}); diff --git a/packages/eslint-plugin-twenty/.eslintrc.js b/packages/eslint-plugin-twenty/.eslintrc.js new file mode 100644 index 000000000..92957bced --- /dev/null +++ b/packages/eslint-plugin-twenty/.eslintrc.js @@ -0,0 +1,69 @@ +module.exports = { + parser: '@typescript-eslint/parser', + parserOptions: { + project: 'tsconfig.json', + tsconfigRootDir: __dirname, + sourceType: 'module', + }, + plugins: [ + '@typescript-eslint/eslint-plugin', + 'unused-imports', + 'simple-import-sort', + 'prefer-arrow', + ], + extends: [ + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ], + root: true, + env: { + node: true, + jest: true, + }, + overrides: [ + { + files: ['*.js', '*.jsx', '*.ts', '*.tsx'], + rules: { + 'no-control-regex': 0, + 'simple-import-sort/imports': [ + 'error', + { + groups: [ + ['^react', '^@?\\w'], + ['^(@|~)(/.*|$)'], + ['^\\u0000'], + ['^\\.\\.(?!/?$)', '^\\.\\./?$'], + ['^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'], + ['^.+\\.?(css)$'] + ] + } + ], + 'prefer-arrow/prefer-arrow-functions': [ + 'error', + { + "disallowPrototype": true, + "singleReturnOnly": false, + "classPropertiesAllowed": false + } + ] + } + }, + ], + ignorePatterns: ['.eslintrc.js', 'codegen.js', '**/generated/*', '*.config.js'], + rules: { + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + 'simple-import-sort/imports': 'error', + 'simple-import-sort/exports': 'error', + 'func-style':['error', 'declaration', { 'allowArrowFunctions': true }], + "@typescript-eslint/no-unused-vars": "off", + "no-unused-vars": "off", + "unused-imports/no-unused-imports": "warn", + "unused-imports/no-unused-vars": [ + "warn", + { "vars": "all", "varsIgnorePattern": "^_", "args": "after-used", "argsIgnorePattern": "^_" } + ], + } +}; diff --git a/packages/eslint-plugin-twenty-ts/.gitignore b/packages/eslint-plugin-twenty/.gitignore similarity index 100% rename from packages/eslint-plugin-twenty-ts/.gitignore rename to packages/eslint-plugin-twenty/.gitignore diff --git a/packages/eslint-plugin-twenty-ts/index.ts b/packages/eslint-plugin-twenty/index.ts similarity index 100% rename from packages/eslint-plugin-twenty-ts/index.ts rename to packages/eslint-plugin-twenty/index.ts diff --git a/packages/eslint-plugin-twenty-ts/jest.config.js b/packages/eslint-plugin-twenty/jest.config.js similarity index 100% rename from packages/eslint-plugin-twenty-ts/jest.config.js rename to packages/eslint-plugin-twenty/jest.config.js diff --git a/packages/eslint-plugin-twenty-ts/package.json b/packages/eslint-plugin-twenty/package.json similarity index 73% rename from packages/eslint-plugin-twenty-ts/package.json rename to packages/eslint-plugin-twenty/package.json index 9f6e4e489..d39010502 100644 --- a/packages/eslint-plugin-twenty-ts/package.json +++ b/packages/eslint-plugin-twenty/package.json @@ -1,6 +1,6 @@ { - "name": "eslint-plugin-twenty-ts", - "version": "1.0.2", + "name": "eslint-plugin-twenty", + "version": "1.0.3", "description": "", "main": "dist/index.js", "files": [ @@ -9,7 +9,8 @@ ], "scripts": { "test": "jest", - "build": "rimraf ./dist && tsc --outDir ./dist" + "build": "rimraf ./dist && tsc --outDir ./dist", + "lint": "eslint src --max-warnings=0" }, "keywords": [], "author": "", @@ -25,7 +26,10 @@ "eslint-config-prettier": "^9.0.0", "eslint-config-standard-with-typescript": "^39.0.0", "eslint-plugin-import": "^2.28.1", + "eslint-plugin-prefer-arrow": "^1.2.3", "eslint-plugin-prettier": "^5.0.0", + "eslint-plugin-simple-import-sort": "^10.0.0", + "eslint-plugin-unused-imports": "^3.0.0", "jest": "^28.1.3", "postcss": "^8.4.29", "prettier": "^3.0.3", diff --git a/packages/eslint-plugin-twenty/src/rules/effect-components.ts b/packages/eslint-plugin-twenty/src/rules/effect-components.ts new file mode 100644 index 000000000..9b38b8423 --- /dev/null +++ b/packages/eslint-plugin-twenty/src/rules/effect-components.ts @@ -0,0 +1,166 @@ +import { ESLintUtils, TSESTree } from "@typescript-eslint/utils"; + +const createRule = ESLintUtils.RuleCreator(() => `https://docs.twenty.com`); + +const checkIsPascalCase = (input: string): boolean => { + const pascalCaseRegex = /^[A-Z][a-zA-Z0-9_]*/g; + + return pascalCaseRegex.test(input); +}; + +type ComponentType = + | TSESTree.FunctionDeclaration + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionExpression; + +const effectComponentsRule = createRule({ + create: (context) => { + const checkThatNodeIsEffectComponent = (node: ComponentType) => { + let componentName = ""; + let identifierNode = node.id; + + const isIdentifier = ( + node: TSESTree.Node | null, + ): node is TSESTree.Identifier => + node?.type === TSESTree.AST_NODE_TYPES.Identifier; + const isVariableDeclarator = ( + node: TSESTree.Node, + ): node is TSESTree.VariableDeclarator => + node.type === TSESTree.AST_NODE_TYPES.VariableDeclarator; + + const isArrowFunction = ( + node: TSESTree.Node, + ): node is TSESTree.ArrowFunctionExpression => + node.type === TSESTree.AST_NODE_TYPES.ArrowFunctionExpression; + const isFunctionDeclaration = ( + node: TSESTree.Node, + ): node is TSESTree.FunctionDeclaration => + node.type === TSESTree.AST_NODE_TYPES.FunctionDeclaration; + const isFunctionExpression = ( + node: TSESTree.Node, + ): node is TSESTree.FunctionExpression => + node.type === TSESTree.AST_NODE_TYPES.FunctionExpression; + + if ( + isArrowFunction(node) && + isVariableDeclarator(node.parent) && + isIdentifier(node.parent.id) + ) { + componentName = node.parent.id.name; + identifierNode = node.parent.id; + } else if (isFunctionDeclaration(node) && isIdentifier(node.id)) { + componentName = node.id.name; + identifierNode = node.id; + } else if ( + isFunctionExpression(node) && + isVariableDeclarator(node.parent) && + isIdentifier(node.parent.id) + ) { + componentName = node.parent.id.name; + identifierNode = node.parent.id; + } + + if (!checkIsPascalCase(componentName)) { + return; + } + + const isReturningEmptyFragmentOrNull = + // Direct return of JSX fragment, e.g., () => <> + (node.body.type === "JSXFragment" && node.body.children.length === 0) || + // Direct return of null, e.g., () => null + (node.body.type === "Literal" && node.body.value === null) || + // Return JSX fragment or null from block + (node.body.type === "BlockStatement" && + node.body.body.some( + (statement) => + statement.type === "ReturnStatement" && + // Empty JSX fragment return, e.g., return <>; + ((statement.argument?.type === "JSXFragment" && + statement.argument.children.length === 0) || + // Empty React.Fragment return, e.g., return ; + (statement.argument?.type === "JSXElement" && + statement.argument.openingElement.name.type === + "JSXIdentifier" && + statement.argument.openingElement.name.name === + "React.Fragment" && + statement.argument.children.length === 0) || + // Literal null return, e.g., return null; + (statement.argument?.type === "Literal" && + statement.argument.value === null)), + )); + + const hasEffectSuffix = componentName.endsWith("Effect"); + + const hasEffectSuffixButIsNotEffectComponent = + hasEffectSuffix && !isReturningEmptyFragmentOrNull; + const isEffectComponentButDoesNotHaveEffectSuffix = + !hasEffectSuffix && isReturningEmptyFragmentOrNull; + + if (isEffectComponentButDoesNotHaveEffectSuffix) { + context.report({ + node, + messageId: "effectSuffix", + data: { + componentName: componentName, + }, + fix: (fixer) => { + if (isArrowFunction(node)) + if (identifierNode) { + return fixer.replaceText( + identifierNode, + componentName + "Effect", + ); + } + + return null; + }, + }); + } else if (hasEffectSuffixButIsNotEffectComponent) { + context.report({ + node, + messageId: "noEffectSuffix", + data: { + componentName: componentName, + }, + fix: (fixer) => { + if (identifierNode) { + return fixer.replaceText( + identifierNode, + componentName.replace("Effect", ""), + ); + } + + return null; + }, + }); + } + }; + + return { + ArrowFunctionExpression: checkThatNodeIsEffectComponent, + FunctionDeclaration: checkThatNodeIsEffectComponent, + FunctionExpression: checkThatNodeIsEffectComponent, + }; + }, + name: "effect-components", + meta: { + docs: { + description: + "Effect components should end with the Effect suffix. This rule checks only components that are in PascalCase and that return a JSX fragment or null. Any renderProps or camelCase components are ignored.", + }, + messages: { + effectSuffix: + "Effect component {{ componentName }} should end with the Effect suffix.", + noEffectSuffix: + "Component {{ componentName }} shouldn't end with the Effect suffix because it doesn't return a JSX fragment or null.", + }, + type: "suggestion", + schema: [], + fixable: "code", + }, + defaultOptions: [], +}); + +module.exports = effectComponentsRule; + +export default effectComponentsRule; diff --git a/packages/eslint-plugin-twenty-ts/src/rules/matching-state-variable.ts b/packages/eslint-plugin-twenty/src/rules/matching-state-variable.ts similarity index 76% rename from packages/eslint-plugin-twenty-ts/src/rules/matching-state-variable.ts rename to packages/eslint-plugin-twenty/src/rules/matching-state-variable.ts index 0423b3ad9..a64c9285b 100644 --- a/packages/eslint-plugin-twenty-ts/src/rules/matching-state-variable.ts +++ b/packages/eslint-plugin-twenty/src/rules/matching-state-variable.ts @@ -1,26 +1,25 @@ import { - TSESTree, - ESLintUtils, AST_NODE_TYPES, + ESLintUtils, + TSESTree, } from "@typescript-eslint/utils"; -const createRule = ESLintUtils.RuleCreator((name) => `https://docs.twenty.com`); +const createRule = ESLintUtils.RuleCreator(() => `https://docs.twenty.com`); const matchingStateVariableRule = createRule({ create: (context) => { return { - VariableDeclarator(node: TSESTree.VariableDeclarator) { + VariableDeclarator: (node: TSESTree.VariableDeclarator) => { if ( node?.init?.type === AST_NODE_TYPES.CallExpression && node.init.callee.type === AST_NODE_TYPES.Identifier && [ "useRecoilState", - "useRecoilFamilyState", - "useRecoilSelector", "useRecoilScopedState", + "useRecoilFamilyState", "useRecoilScopedFamilyState", - "useRecoilScopedSelector", "useRecoilValue", + "useRecoilScopedValue", ].includes(node.init.callee.name) ) { const stateNameBase = @@ -32,9 +31,9 @@ const matchingStateVariableRule = createRule({ return; } - let expectedVariableNameBase = stateNameBase.replace( + const expectedVariableNameBase = stateNameBase.replace( /(State|FamilyState|Selector|ScopedState|ScopedFamilyState|ScopedSelector)$/, - "" + "", ); if (node.id.type === AST_NODE_TYPES.Identifier) { @@ -44,11 +43,12 @@ const matchingStateVariableRule = createRule({ node, messageId: "invalidVariableName", data: { - actual: actualVariableName, - expected: expectedVariableNameBase, + actualName: actualVariableName, + expectedName: expectedVariableNameBase, + hookName: stateNameBase, callee: node.init.callee.name, }, - fix(fixer) { + fix: (fixer) => { return fixer.replaceText(node.id, expectedVariableNameBase); }, }); @@ -73,17 +73,16 @@ const matchingStateVariableRule = createRule({ expected: expectedVariableNameBase, callee: node.init.callee.name, }, - fix(fixer) { + fix: (fixer) => { if (node.id.type === AST_NODE_TYPES.ArrayPattern) { return fixer.replaceText( node.id.elements[0] as TSESTree.Node, - expectedVariableNameBase + expectedVariableNameBase, ); } return null; }, }); - return; } if (node.id.elements?.[1]?.type === AST_NODE_TYPES.Identifier) { @@ -97,14 +96,15 @@ const matchingStateVariableRule = createRule({ node, messageId: "invalidSetterName", data: { - actual: actualSetterName, - expected: expectedSetterName, + hookName: stateNameBase, + actualName: actualSetterName, + expectedName: expectedSetterName, }, - fix(fixer) { + fix: (fixer) => { if (node.id.type === AST_NODE_TYPES.ArrayPattern) { return fixer.replaceText( node.id.elements[1]!, - expectedSetterName + expectedSetterName, ); } return null; @@ -121,16 +121,17 @@ const matchingStateVariableRule = createRule({ meta: { type: "problem", docs: { - description: "Ensure recoil value and setter are named after their atom name", + description: + "Ensure recoil value and setter are named after their atom name", recommended: "recommended", }, fixable: "code", schema: [], messages: { invalidVariableName: - "Invalid usage of {{hookName}}: the value should be named '{{expectedName}}' but found '{{actualName}}'.", + "Invalid usage of {{ hookName }}: the variable should be named '{{ expectedName }}' but found '{{ actualName }}'.", invalidSetterName: - "Invalid usage of {{hookName}}: Expected setter '{{expectedName}}' but found '{{actualName}}'.", + "Invalid usage of {{ hookName }}: Expected setter '{{ expectedName }}' but found '{{ actualName }}'.", }, }, defaultOptions: [], @@ -138,4 +139,4 @@ const matchingStateVariableRule = createRule({ module.exports = matchingStateVariableRule; -export default matchingStateVariableRule; \ No newline at end of file +export default matchingStateVariableRule; diff --git a/packages/eslint-plugin-twenty/src/rules/no-hardcoded-colors.ts b/packages/eslint-plugin-twenty/src/rules/no-hardcoded-colors.ts new file mode 100644 index 000000000..8ba06bb01 --- /dev/null +++ b/packages/eslint-plugin-twenty/src/rules/no-hardcoded-colors.ts @@ -0,0 +1,64 @@ +import { ESLintUtils, TSESTree } from "@typescript-eslint/utils"; + +const createRule = ESLintUtils.RuleCreator(() => `https://docs.twenty.com`); + +const noHardcodedColorsRule = createRule({ + create: (context) => { + const testHardcodedColor = ( + literal: TSESTree.Literal | TSESTree.TemplateLiteral, + ) => { + const colorRegex = /(?:rgba?\()|(?:#[0-9a-fA-F]{2,6})/i; + + if ( + literal.type === TSESTree.AST_NODE_TYPES.Literal && + typeof literal.value === "string" + ) { + if (colorRegex.test(literal.value)) { + context.report({ + node: literal, + messageId: "hardcodedColor", + data: { + color: literal.value, + }, + }); + } + } else if (literal.type === TSESTree.AST_NODE_TYPES.TemplateLiteral) { + const firstStringValue = literal.quasis[0]?.value.raw; + + if (colorRegex.test(firstStringValue)) { + context.report({ + node: literal, + messageId: "hardcodedColor", + data: { + color: firstStringValue, + }, + }); + } + } + }; + + return { + Literal: testHardcodedColor, + TemplateLiteral: testHardcodedColor, + }; + }, + name: "no-hardcoded-colors", + meta: { + docs: { + description: + "Do not use hardcoded RGBA or Hex colors. Please use a color from the theme file.", + }, + messages: { + hardcodedColor: + "Hardcoded color {{ color }} found. Please use a color from the theme file.", + }, + type: "suggestion", + schema: [], + fixable: "code", + }, + defaultOptions: [], +}); + +module.exports = noHardcodedColorsRule; + +export default noHardcodedColorsRule; diff --git a/packages/eslint-plugin-twenty-ts/src/rules/sort-css-properties-alphabetically.ts b/packages/eslint-plugin-twenty/src/rules/sort-css-properties-alphabetically.ts similarity index 70% rename from packages/eslint-plugin-twenty-ts/src/rules/sort-css-properties-alphabetically.ts rename to packages/eslint-plugin-twenty/src/rules/sort-css-properties-alphabetically.ts index 26c290387..3fc874a29 100644 --- a/packages/eslint-plugin-twenty-ts/src/rules/sort-css-properties-alphabetically.ts +++ b/packages/eslint-plugin-twenty/src/rules/sort-css-properties-alphabetically.ts @@ -1,14 +1,14 @@ -import postcss from "postcss"; -import { AST_NODE_TYPES, TSESTree } from "@typescript-eslint/utils"; +import { TSESTree } from "@typescript-eslint/utils"; import { ESLintUtils } from "@typescript-eslint/utils"; -import type { Identifier, TaggedTemplateExpression } from "@babel/types"; import { RuleFix, RuleFixer, SourceCode, } from "@typescript-eslint/utils/ts-eslint"; -const createRule = ESLintUtils.RuleCreator((name) => `https://docs.twenty.com`); +import postcss from "postcss"; + +const createRule = ESLintUtils.RuleCreator(() => `https://docs.twenty.com`); interface loc { start: { @@ -22,35 +22,67 @@ interface loc { } const isStyledTagname = (node: TSESTree.TaggedTemplateExpression): boolean => { - return ( - (node.tag.type === "Identifier" && node.tag.name === "css") || - (node.tag.type === "MemberExpression" && - // @ts-ignore - node.tag.object.name === "styled") || - (node.tag.type === "CallExpression" && - // @ts-ignore - (node.tag.callee.name === "styled" || - // @ts-ignore - (node.tag.callee.object && - // @ts-ignore + const isMemberExpression = ( + node: TSESTree.Node, + ): node is TSESTree.MemberExpression => + node.type === TSESTree.AST_NODE_TYPES.MemberExpression; + const isCallExpression = ( + node: TSESTree.Node, + ): node is TSESTree.CallExpression => + node.type === TSESTree.AST_NODE_TYPES.CallExpression; + const isIdentifier = ( + node: TSESTree.Node | null, + ): node is TSESTree.Identifier => + node?.type === TSESTree.AST_NODE_TYPES.Identifier; - ((node.tag.callee.object.callee && - // @ts-ignore - node.tag.callee.object.callee.name === "styled") || - // @ts-ignore - (node.tag.callee.object.object && - // @ts-ignore - node.tag.callee.object.object.name === "styled"))))) - ); + if (isIdentifier(node.tag)) { + return node.tag.name === "css"; + } + + if (isMemberExpression(node.tag) && isIdentifier(node.tag.object)) { + return node.tag.object.name === "styled"; + } + + if (isCallExpression(node.tag) && isIdentifier(node.tag.callee)) { + return node.tag.callee.name === "styled"; + } + + if ( + isCallExpression(node.tag) && + isMemberExpression(node.tag.callee) && + isIdentifier(node.tag.callee.object) + ) { + return node.tag.callee.object.name === "styled"; + } + + if ( + isCallExpression(node.tag) && + isMemberExpression(node.tag.callee) && + isIdentifier(node.tag.callee.object) + ) { + return node.tag.callee.object.name === "styled"; + } + + if ( + isCallExpression(node.tag) && + isMemberExpression(node.tag.callee) && + isMemberExpression(node.tag.callee.object) && + isIdentifier(node.tag.callee.object.object) + ) { + return node.tag.callee.object.object.name === "styled"; + } + + return false; }; + /** * An atomic rule is a rule without nested rules. */ const isValidAtomicRule = ( - rule: postcss.Root + rule: postcss.Rule, ): { isValid: boolean; loc?: loc } => { const decls = rule.nodes.filter( - (node) => node.type === "decl" + (node) => node.type === "decl", ) as unknown as postcss.Declaration[]; if (decls.length < 0) { return { isValid: true }; @@ -78,15 +110,13 @@ const isValidAtomicRule = ( return { isValid: true }; }; -const isValidRule = (rule: postcss.Root): { isValid: boolean; loc?: loc } => { +const isValidRule = (rule: postcss.Rule): { isValid: boolean; loc?: loc } => { // check each rule recursively const { isValid, loc } = rule.nodes.reduce<{ isValid: boolean; loc?: loc }>( (map, node) => { - return node.type === "rule" - ? isValidRule(node as unknown as postcss.Root) - : map; + return node.type === "rule" ? isValidRule(node) : map; }, - { isValid: true } + { isValid: true }, ); // if there is any invalid rule, return result @@ -103,7 +133,7 @@ const getNodeStyles = (node: TSESTree.TaggedTemplateExpression): string => { // remove line break added to the first quasi const lineBreakCount = node.quasi.loc.start.line - 1; let styles = `${"\n".repeat(lineBreakCount)}${" ".repeat( - node.quasi.loc.start.column + 1 + node.quasi.loc.start.column + 1, )}${firstQuasi.value.raw}`; // replace expression by spaces and line breaks @@ -115,7 +145,7 @@ const getNodeStyles = (node: TSESTree.TaggedTemplateExpression): string => { ? loc.start.column - prevLoc.end.column + 2 : loc.start.column + 1; styles = `${styles}${" "}${"\n".repeat(lineBreaksCount)}${" ".repeat( - spacesCount + spacesCount, )}${value.raw}`; }); @@ -141,7 +171,7 @@ const fix = ({ }); const declarations = rule.nodes.filter( - (node) => node.type === "decl" + (node) => node.type === "decl", ) as unknown as postcss.Declaration[]; const sortedDeclarations = sortDeclarations(declarations); @@ -158,8 +188,8 @@ const fix = ({ fixings.push( fixer.insertTextAfterRange( [range.startIdx, range.startIdx], - sortedDeclText - ) + sortedDeclText, + ), ); } catch (e) { console.log(e); @@ -171,7 +201,7 @@ const fix = ({ const areSameDeclarations = ( a: postcss.ChildNode, - b: postcss.ChildNode + b: postcss.ChildNode, ): boolean => a.source!.start!.line === b.source!.start!.line && a.source!.start!.column === b.source!.start!.column; @@ -216,23 +246,23 @@ const sortDeclarations = (declarations: postcss.Declaration[]) => .sort((declA, declB) => (declA.prop > declB.prop ? 1 : -1)); const sortCssPropertiesAlphabeticallyRule = createRule({ - create(context) { + create: (context) => { return { - TaggedTemplateExpression(node: TSESTree.TaggedTemplateExpression) { + TaggedTemplateExpression: (node: TSESTree.TaggedTemplateExpression) => { if (isStyledTagname(node)) { try { - const root = postcss.parse(getNodeStyles(node)); + const root = postcss.parse( + getNodeStyles(node), + ) as unknown as postcss.Rule; - const { isValid, loc } = isValidRule(root); + const { isValid } = isValidRule(root); if (!isValid) { return context.report({ node, - messageId: "sort-css-properties-alphabetically", - loc, + messageId: "sortCssPropertiesAlphabetically", fix: (fixer) => fix({ - // @ts-ignore rule: root, fixer, src: context.getSourceCode(), @@ -253,7 +283,7 @@ const sortCssPropertiesAlphabeticallyRule = createRule({ recommended: "recommended", }, messages: { - "sort-css-properties-alphabetically": + sortCssPropertiesAlphabetically: "Declarations should be sorted alphabetically.", }, type: "suggestion", diff --git a/packages/eslint-plugin-twenty/src/rules/styled-components-prefixed-with-styled.ts b/packages/eslint-plugin-twenty/src/rules/styled-components-prefixed-with-styled.ts new file mode 100644 index 000000000..1ed98af82 --- /dev/null +++ b/packages/eslint-plugin-twenty/src/rules/styled-components-prefixed-with-styled.ts @@ -0,0 +1,62 @@ +import { + AST_NODE_TYPES, + ESLintUtils, + TSESTree, +} from "@typescript-eslint/utils"; + +const createRule = ESLintUtils.RuleCreator(() => `https://docs.twenty.com`); + +const styledComponentsPrefixedWithStyledRule = createRule({ + create: (context) => { + return { + VariableDeclarator: (node: TSESTree.VariableDeclarator) => { + const templateExpr = node.init; + if (templateExpr?.type !== AST_NODE_TYPES.TaggedTemplateExpression) { + return; + } + const tag = templateExpr.tag; + const tagged = + tag.type === AST_NODE_TYPES.MemberExpression + ? tag.object + : tag.type === AST_NODE_TYPES.CallExpression + ? tag.callee + : null; + if ( + tagged?.type === AST_NODE_TYPES.Identifier && + tagged.name === "styled" + ) { + const variable = node.id as TSESTree.Identifier; + if (variable.name.startsWith("Styled")) { + return; + } + context.report({ + node, + messageId: "noStyledPrefix", + data: { + componentName: variable.name, + }, + }); + } + }, + }; + }, + name: "styled-components-prefixed-with-styled", + meta: { + type: "suggestion", + docs: { + description: "Warn when StyledComponents are not prefixed with Styled", + recommended: "recommended", + }, + messages: { + noStyledPrefix: + "{{componentName}} is a StyledComponent and is not prefixed with Styled.", + }, + fixable: "code", + schema: [], + }, + defaultOptions: [], +}); + +module.exports = styledComponentsPrefixedWithStyledRule; + +export default styledComponentsPrefixedWithStyledRule; diff --git a/packages/eslint-plugin-twenty-ts/src/tests/effect-components.spec.ts b/packages/eslint-plugin-twenty/src/tests/effect-components.spec.ts similarity index 93% rename from packages/eslint-plugin-twenty-ts/src/tests/effect-components.spec.ts rename to packages/eslint-plugin-twenty/src/tests/effect-components.spec.ts index 2231edc3a..2f2f0e8dc 100644 --- a/packages/eslint-plugin-twenty-ts/src/tests/effect-components.spec.ts +++ b/packages/eslint-plugin-twenty/src/tests/effect-components.spec.ts @@ -65,7 +65,7 @@ ruleTester.run("effect-components", effectComponentsRule, { invalid: [ { code: "const TestComponent = () => <>;", - output: 'const TestComponentEffect = () => <>;', + output: "const TestComponentEffect = () => <>;", errors: [ { messageId: "effectSuffix", @@ -74,7 +74,7 @@ ruleTester.run("effect-components", effectComponentsRule, { }, { code: "const TestComponentEffect = () => <>
;", - output: 'const TestComponent = () => <>
;', + output: "const TestComponent = () => <>
;", errors: [ { messageId: "noEffectSuffix", diff --git a/packages/eslint-plugin-twenty-ts/src/tests/file.ts b/packages/eslint-plugin-twenty/src/tests/file.ts similarity index 62% rename from packages/eslint-plugin-twenty-ts/src/tests/file.ts rename to packages/eslint-plugin-twenty/src/tests/file.ts index 320bffd28..3608a2c10 100644 --- a/packages/eslint-plugin-twenty-ts/src/tests/file.ts +++ b/packages/eslint-plugin-twenty/src/tests/file.ts @@ -1 +1 @@ -// Required by typescript-eslint https://typescript-eslint.io/packages/rule-tester#type-aware-testing \ No newline at end of file +// Required by typescript-eslint https://typescript-eslint.io/packages/rule-tester#type-aware-testing diff --git a/packages/eslint-plugin-twenty/src/tests/matching-state-variable.spec.ts b/packages/eslint-plugin-twenty/src/tests/matching-state-variable.spec.ts new file mode 100644 index 000000000..8fcf5d84e --- /dev/null +++ b/packages/eslint-plugin-twenty/src/tests/matching-state-variable.spec.ts @@ -0,0 +1,185 @@ +import { RuleTester } from "@typescript-eslint/rule-tester"; + +import matchingStateVariableRule from "../rules/matching-state-variable"; + +const ruleTester = new RuleTester({ + parser: "@typescript-eslint/parser", + parserOptions: { + project: "./tsconfig.json", + tsconfigRootDir: __dirname, + ecmaFeatures: { + jsx: true, + }, + }, +}); + +ruleTester.run("matching-state-variable", matchingStateVariableRule, { + valid: [ + { + code: "const variable = useRecoilValue(variableState);", + }, + { + code: "const variable = useRecoilScopedValue(variableScopedState);", + }, + { + code: "const [variable, setVariable] = useRecoilState(variableScopedState);", + }, + { + code: "const [variable, setVariable] = useRecoilScopedState(variableScopedState);", + }, + { + code: "const [variable, setVariable] = useRecoilFamilyState(variableScopedState);", + }, + { + code: "const [variable, setVariable] = useRecoilScopedFamilyState(variableScopedState);", + }, + ], + invalid: [ + { + code: "const myValue = useRecoilValue(variableState);", + errors: [ + { + messageId: "invalidVariableName", + }, + ], + output: "const variable = useRecoilValue(variableState);", + }, + { + code: "const myValue = useRecoilScopedValue(variableState);", + errors: [ + { + messageId: "invalidVariableName", + }, + ], + output: "const variable = useRecoilScopedValue(variableState);", + }, + + { + code: "const [myValue, setMyValue] = useRecoilState(variableState);", + errors: [ + { + messageId: "invalidVariableName", + }, + { + messageId: "invalidSetterName", + }, + ], + output: "const [variable, setVariable] = useRecoilState(variableState);", + }, + { + code: "const [myValue] = useRecoilState(variableState);", + errors: [ + { + messageId: "invalidVariableName", + }, + ], + output: "const [variable] = useRecoilState(variableState);", + }, + { + code: "const [, setMyValue] = useRecoilState(variableState);", + errors: [ + { + messageId: "invalidSetterName", + }, + ], + output: "const [, setVariable] = useRecoilState(variableState);", + }, + + { + code: "const [myValue, setMyValue] = useRecoilScopedState(variableState);", + errors: [ + { + messageId: "invalidVariableName", + }, + { + messageId: "invalidSetterName", + }, + ], + output: + "const [variable, setVariable] = useRecoilScopedState(variableState);", + }, + { + code: "const [myValue] = useRecoilScopedState(variableState);", + errors: [ + { + messageId: "invalidVariableName", + }, + ], + output: "const [variable] = useRecoilScopedState(variableState);", + }, + { + code: "const [, setMyValue] = useRecoilScopedState(variableState);", + errors: [ + { + messageId: "invalidSetterName", + }, + ], + output: "const [, setVariable] = useRecoilScopedState(variableState);", + }, + + { + code: "const [myValue, setMyValue] = useRecoilFamilyState(variableState);", + errors: [ + { + messageId: "invalidVariableName", + }, + { + messageId: "invalidSetterName", + }, + ], + output: + "const [variable, setVariable] = useRecoilFamilyState(variableState);", + }, + { + code: "const [myValue] = useRecoilFamilyState(variableState);", + errors: [ + { + messageId: "invalidVariableName", + }, + ], + output: "const [variable] = useRecoilFamilyState(variableState);", + }, + { + code: "const [, setMyValue] = useRecoilFamilyState(variableState);", + errors: [ + { + messageId: "invalidSetterName", + }, + ], + output: "const [, setVariable] = useRecoilFamilyState(variableState);", + }, + + { + code: "const [myValue, setMyValue] = useRecoilScopedFamilyState(variableState);", + errors: [ + { + messageId: "invalidVariableName", + }, + { + messageId: "invalidSetterName", + }, + ], + output: + "const [variable, setVariable] = useRecoilScopedFamilyState(variableState);", + }, + { + code: "const [myValue] = useRecoilScopedFamilyState(variableState);", + errors: [ + { + messageId: "invalidVariableName", + }, + ], + output: "const [variable] = useRecoilScopedFamilyState(variableState);", + }, + { + code: "const [, setMyValue] = useRecoilScopedFamilyState(variableState);", + errors: [ + { + messageId: "invalidSetterName", + }, + ], + output: + "const [, setVariable] = useRecoilScopedFamilyState(variableState);", + }, + ], +}); diff --git a/packages/eslint-plugin-twenty-ts/src/tests/no-hardcoded-colors.spec.ts b/packages/eslint-plugin-twenty/src/tests/no-hardcoded-colors.spec.ts similarity index 63% rename from packages/eslint-plugin-twenty-ts/src/tests/no-hardcoded-colors.spec.ts rename to packages/eslint-plugin-twenty/src/tests/no-hardcoded-colors.spec.ts index 51e17ce63..a0e64889a 100644 --- a/packages/eslint-plugin-twenty-ts/src/tests/no-hardcoded-colors.spec.ts +++ b/packages/eslint-plugin-twenty/src/tests/no-hardcoded-colors.spec.ts @@ -1,4 +1,5 @@ import { RuleTester } from "@typescript-eslint/rule-tester"; + import noHardcodedColorsRule from "../rules/no-hardcoded-colors"; const ruleTester = new RuleTester({ @@ -17,9 +18,6 @@ ruleTester.run("no-hardcoded-colors", noHardcodedColorsRule, { { code: "const color = theme.background.secondary;", }, - { - code: 'const color = "#000000";', - }, ], invalid: [ { @@ -30,6 +28,28 @@ ruleTester.run("no-hardcoded-colors", noHardcodedColorsRule, { }, ], }, + { + code: 'const color = { test: "rgb(154,205,50)", test2: "#ADFF2F" }', + errors: [ + { + messageId: "hardcodedColor", + }, + { + messageId: "hardcodedColor", + }, + ], + }, + { + code: "const color = { test: `rgb(${r},${g},${b})`, test2: `#ADFF${test}` }", + errors: [ + { + messageId: "hardcodedColor", + }, + { + messageId: "hardcodedColor", + }, + ], + }, { code: 'const color = "#ADFF2F";', errors: [ @@ -39,4 +59,4 @@ ruleTester.run("no-hardcoded-colors", noHardcodedColorsRule, { ], }, ], -}); \ No newline at end of file +}); diff --git a/packages/eslint-plugin-twenty-ts/src/tests/react.tsx b/packages/eslint-plugin-twenty/src/tests/react.tsx similarity index 62% rename from packages/eslint-plugin-twenty-ts/src/tests/react.tsx rename to packages/eslint-plugin-twenty/src/tests/react.tsx index 320bffd28..3608a2c10 100644 --- a/packages/eslint-plugin-twenty-ts/src/tests/react.tsx +++ b/packages/eslint-plugin-twenty/src/tests/react.tsx @@ -1 +1 @@ -// Required by typescript-eslint https://typescript-eslint.io/packages/rule-tester#type-aware-testing \ No newline at end of file +// Required by typescript-eslint https://typescript-eslint.io/packages/rule-tester#type-aware-testing diff --git a/packages/eslint-plugin-twenty/src/tests/sort-css-properties-alphabetically.spec.ts b/packages/eslint-plugin-twenty/src/tests/sort-css-properties-alphabetically.spec.ts new file mode 100644 index 000000000..16972a759 --- /dev/null +++ b/packages/eslint-plugin-twenty/src/tests/sort-css-properties-alphabetically.spec.ts @@ -0,0 +1,56 @@ +import { RuleTester } from "@typescript-eslint/rule-tester"; + +import sortCssPropertiesAlphabeticallyRule from "../rules/sort-css-properties-alphabetically"; + +const ruleTester = new RuleTester({ + parser: "@typescript-eslint/parser", + parserOptions: { + project: "./tsconfig.json", + tsconfigRootDir: __dirname, + ecmaFeatures: { + jsx: true, + }, + }, +}); + +ruleTester.run( + "sort-css-properties-alphabetically", + sortCssPropertiesAlphabeticallyRule, + { + valid: [ + { + code: "const style = css`color: red;`;", + }, + { + code: "const style = css`background-color: $bgColor;color: red;`;", + }, + { + code: "const StyledComponent = styled.div`color: red;`;", + }, + { + code: "const StyledComponent = styled.div`background-color: $bgColor;color: red;`;", + }, + ], + invalid: [ + { + code: "const style = css`color: #FF0000;background-color: $bgColor`;", + output: "const style = css`background-color: $bgColorcolor: #FF0000;`;", + errors: [ + { + messageId: "sortCssPropertiesAlphabetically", + }, + ], + }, + { + code: "const StyledComponent = styled.div`color: #FF0000;background-color: $bgColor`;", + output: + "const StyledComponent = styled.div`background-color: $bgColorcolor: #FF0000;`;", + errors: [ + { + messageId: "sortCssPropertiesAlphabetically", + }, + ], + }, + ], + }, +); diff --git a/packages/eslint-plugin-twenty/src/tests/styled-components-prefixed-with-styled.spec.ts b/packages/eslint-plugin-twenty/src/tests/styled-components-prefixed-with-styled.spec.ts new file mode 100644 index 000000000..0091155ea --- /dev/null +++ b/packages/eslint-plugin-twenty/src/tests/styled-components-prefixed-with-styled.spec.ts @@ -0,0 +1,47 @@ +import { RuleTester } from "@typescript-eslint/rule-tester"; + +import styledComponentsPrefixedWithStyledRule from "../rules/styled-components-prefixed-with-styled"; + +const ruleTester = new RuleTester({ + parser: "@typescript-eslint/parser", + parserOptions: { + project: "./tsconfig.json", + tsconfigRootDir: __dirname, + ecmaFeatures: { + jsx: true, + }, + }, +}); + +ruleTester.run( + "styled-components-prefixed-with-styled", + styledComponentsPrefixedWithStyledRule, + { + valid: [ + { + code: "const StyledButton = styled.button``;", + }, + { + code: "const StyledComponent = styled.div``;", + }, + ], + invalid: [ + { + code: "const Button = styled.button``;", + errors: [ + { + messageId: "noStyledPrefix", + }, + ], + }, + { + code: "const Component = styled.div``;", + errors: [ + { + messageId: "noStyledPrefix", + }, + ], + }, + ], + }, +); diff --git a/packages/eslint-plugin-twenty-ts/src/tests/tsconfig.json b/packages/eslint-plugin-twenty/src/tests/tsconfig.json similarity index 100% rename from packages/eslint-plugin-twenty-ts/src/tests/tsconfig.json rename to packages/eslint-plugin-twenty/src/tests/tsconfig.json diff --git a/packages/eslint-plugin-twenty-ts/tsconfig.json b/packages/eslint-plugin-twenty/tsconfig.json similarity index 100% rename from packages/eslint-plugin-twenty-ts/tsconfig.json rename to packages/eslint-plugin-twenty/tsconfig.json diff --git a/packages/eslint-plugin-twenty-ts/yarn.lock b/packages/eslint-plugin-twenty/yarn.lock similarity index 99% rename from packages/eslint-plugin-twenty-ts/yarn.lock rename to packages/eslint-plugin-twenty/yarn.lock index 84cbc1249..4e164cb30 100644 --- a/packages/eslint-plugin-twenty-ts/yarn.lock +++ b/packages/eslint-plugin-twenty/yarn.lock @@ -1599,6 +1599,11 @@ eslint-plugin-import@^2.28.1: semver "^6.3.1" tsconfig-paths "^3.14.2" +eslint-plugin-prefer-arrow@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-prefer-arrow/-/eslint-plugin-prefer-arrow-1.2.3.tgz#e7fbb3fa4cd84ff1015b9c51ad86550e55041041" + integrity sha512-J9I5PKCOJretVuiZRGvPQxCbllxGAV/viI20JO3LYblAodofBxyMnZAJ+WGeClHgANnSJberTNoFWWjrWKBuXQ== + eslint-plugin-prettier@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.0.tgz#6887780ed95f7708340ec79acfdf60c35b9be57a" @@ -1607,6 +1612,23 @@ eslint-plugin-prettier@^5.0.0: prettier-linter-helpers "^1.0.0" synckit "^0.8.5" +eslint-plugin-simple-import-sort@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-10.0.0.tgz#cc4ceaa81ba73252427062705b64321946f61351" + integrity sha512-AeTvO9UCMSNzIHRkg8S6c3RPy5YEwKWSQPx3DYghLedo2ZQxowPFLGDN1AZ2evfg6r6mjBSZSLxLFsWSu3acsw== + +eslint-plugin-unused-imports@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-3.0.0.tgz#d25175b0072ff16a91892c3aa72a09ca3a9e69e7" + integrity sha512-sduiswLJfZHeeBJ+MQaG+xYzSWdRXoSw61DpU13mzWumCkR0ufD0HmO4kdNokjrkluMHpj/7PJeN35pgbhW3kw== + dependencies: + eslint-rule-composer "^0.3.0" + +eslint-rule-composer@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz#79320c927b0c5c0d3d3d2b76c8b4a488f25bbaf9" + integrity sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg== + eslint-scope@^7.2.2: version "7.2.2" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f"