francoismassart / eslint-plugin-tailwindcss

ESLint plugin for Tailwind CSS usage
https://www.npmjs.com/package/eslint-plugin-tailwindcss
MIT License
1.47k stars 70 forks source link

[BUG] size-* utilities from latest tailwindcss@v3.4 treated as custom classnames in classname-order rule set #329

Closed eramsayCB closed 6 months ago

eramsayCB commented 6 months ago

Describe the bug size-* utilities from the latest tailwindcss@v3.4 release appear to be treated as custom classnames in classname-order rule set, which when auto-resolved moves all size classes to the beginning of the classname string, ignoring responsive utilities (e.g. md:size-44)

To Reproduce Steps to reproduce the behavior: Add responsive size utilities classes to the end of a classname string:

responsive-size-lint-classnames-order-warning

Auto-resolve the warning according to the linter rule set, and responsive classes move to front of string like custom classnames:

resolved-size-classnames-order

Expected behavior Expect responsive size utility classes to align with other responsive classnames without displaying a warning: order-1 size-44 min-w-fit md:size-56 lg:order-2 lg:size-64 xl:size-72 2xl:size-[336px] 3xl:size-[theme(maxWidth.md)]

Screenshots If applicable, add screenshots to help explain your problem.

Environment (please complete the following information):

Additional context Add any other context about the problem here.

eslint config file or live demo

.eslintrc.js file...

module.exports = {
  root: true,
  parser: '@typescript-eslint/parser',
  plugins: [
    '@typescript-eslint',
    'react',
    'cypress',
    'security',
    'unicorn',
    'sonarjs',
    'tailwindcss'
  ],
  extends: [
    'eslint:recommended',
    'plugin:import/recommended',
    'plugin:react/recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:security/recommended',
    'plugin:sonarjs/recommended',
    'plugin:tailwindcss/recommended'
  ],
  settings: {
    'import/resolver': {
      alias: {
        map: [
          ['pages', './pages'],
          ['components', './components'],
          ['commands', './commands'],
          ['lib', './lib'],
          ['lib-react', './lib-react'],
          ['services', './services'],
          ['static', './static']
        ],
        extensions: ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.json', '.d.ts']
      }
    },
    react: {
      version: 'detect'
    }
  },
  env: {
    browser: true,
    es6: true,
    node: true,
    'cypress/globals': true
  },
  globals: {
    Atomics: 'readonly',
    SharedArrayBuffer: 'readonly'
  },
  parserOptions: {
    ecmaFeatures: {
      jsx: true
    },
    ecmaVersion: 2018,
    project: 'tsconfig.json',
    tsconfigRootDir: __dirname,
    sourceType: 'module'
  },
  ignorePatterns: ["services/contentful-models.d.ts", "services/contentful/generated/*.d.ts"],
  overrides: [
    {
      files: ['lib/**'], // Lib can only depend on some directories and, in particular, CANNOT depend on React
      settings: {
        'import/resolver': {
          alias: {
            map: [
              ['lib', './lib'],
              ['components', './components'],
              ['pages/api', './pages/api'],
              ['services', './services'],
              ['static', './static']
            ],
            extensions: ['.js', '.ts', '.json', '.d.ts']
          }
        }
      }
    },
    {
      files: ['components/**/*.stories.tsx'],
      rules: {
        'import/no-default-export': 'off',
      }
    },
    {
      files: ['migrations/**'],
      rules: {
        '@typescript-eslint/explicit-function-return-type': 'off',
        'no-console': 'off',
        'import/extensions': 'off',
        'import/no-unresolved': 'off',
        '@typescript-eslint/no-triple-slash-reference': 'off',
        '@typescript-eslint/no-explicit-any': ['off'] // cause transformEntryForLocale returns any
      }
    },
    {
      files: ['commands/**'],
      rules: {
        'no-console': 'off'
      }
    },
    {
      files: ['[id].tsx'],
      rules: {
        'unicorn/filename-case': 'off'
      }
    },
    {
      files: [
        '**/*.test.ts',
        '**/*.test.tsx'
      ],
      env: {
        jest: true
      },
      plugins: ['jest'],
      extends: ['plugin:jest/recommended'],
      rules: {
        // allow global requires in test files
        'global-require': ['off'],
        'sonarjs/no-duplicate-string': ['off'],
        'import/no-unresolved': ['off'], // Because of auto generated Storybook
        '@typescript-eslint/no-var-requires': ['off'],
        // tests (mostly mocks) can do whatever they want, re: exports
        'import/no-default-export': ['off'],
        // tests (mostly mocks) can do whatever they want, re: exports
        'import/prefer-default-export': ['off'],
        '@typescript-eslint/explicit-function-return-type': 0,
        '@typescript-eslint/no-explicit-any': ['off']
      }
    }
  ],
  rules: {
    'security/detect-non-literal-fs-filename': 'off',
    '@typescript-eslint/triple-slash-reference': ['off'], // typescript referencies
    'security/detect-object-injection': ['off'],
    'comma-dangle': ['error', 'never'],
    'import/extensions': ['error', 'ignorePackages', {
      js: 'never', ts: 'never', mjs: 'never', jsx: 'never', tsx: 'never'
    }],
    'import/no-default-export': ['error'],
    'import/no-extraneous-dependencies': ['error', {
      devDependencies: [
        '**/*.test.tsx',
        '**/*.test.ts',
        'migrations/*',
        'jest.setup.ts',
        '!commands/**/*.tsx',
        '!commands/**/*.ts'
      ],
      optionalDependencies: false,
      peerDependencies: false
    }],
    'import/no-unresolved': [2, {ignore: ['^\\@cbdc\\/', '^csstype']}],
    'import/prefer-default-export': 'off', // We should not use default export
    indent: ['error', 2, {SwitchCase: 1}],
    'newline-before-return': 'error',
    'no-plusplus': ["error", { "allowForLoopAfterthoughts": true }],
    'no-multiple-empty-lines': ['error', {max: 1}],
    'no-multi-spaces': ['error', {ignoreEOLComments: true}],
    'no-param-reassign': ['error', {props: false}],
    'no-restricted-imports': ['error', {patterns: ['*..*']}],
    'no-shadow': 'error',
    'no-use-before-define': ['error', {functions: false, classes: false}],
    'object-curly-spacing': ['error', 'never'],
    quotes: ['error', 'single'],
    'react/jsx-filename-extension': ['error', {extensions: ['.tsx', '.jsx']}],
    'jsx-quotes': ["error", "prefer-double"],
    'react/jsx-closing-bracket-location': 1,
    'react/jsx-closing-bracket-location': [1, 'tag-aligned'],
    'react/jsx-closing-bracket-location': [1, 'line-aligned'],
    "react/jsx-indent": [2, 2, {indentLogicalExpressions: true}],
    'react/jsx-wrap-multilines': ['error', {
      "declaration": "parens-new-line",
      "assignment": "parens-new-line",
      "return": "parens-new-line",
      "arrow": "parens-new-line",
      "condition": "parens-new-line",
      "logical": "parens-new-line",
      "prop": "parens-new-line"
    }],
    'react/prop-types': 'off', // Typechecking done by Typescript
    semi: 'off', // use typescript version instead
    'space-infix-ops': ['error'],
    '@typescript-eslint/explicit-function-return-type': ['error', {allowTypedFunctionExpressions: true}],
    '@typescript-eslint/explicit-member-accessibility': ['error', {accessibility: 'no-public'}],
    '@typescript-eslint/indent': ['error', 2, {SwitchCase: 1}],
    '@typescript-eslint/interface-name-prefix': ['error', 'always'],
    '@typescript-eslint/member-delimiter-style': ['error', {multiline: {delimiter: 'none', requireLast: false}, singleline: {delimiter: 'semi', requireLast: false}}],
    '@typescript-eslint/no-empty-interface': 'off', // Empty interfaces could be imported and updated later. So makes sense to allow it and avoid futher refactoring of {} to interfaces
    '@typescript-eslint/no-use-before-define': ['error', {functions: false, classes: false}],
    '@typescript-eslint/no-var-requires': 'error',
    '@typescript-eslint/semi': ['error', 'never'],
    '@typescript-eslint/type-annotation-spacing': ['error', {before: false, after: true, overrides: {arrow: {before: true, after: true}}}],
    '@typescript-eslint/no-unnecessary-type-assertion': ['error'],
    'padding-line-between-statements': ['error',
      {blankLine: 'always', prev: 'if', next: '*'},
      {blankLine: 'always', prev: '*', next: 'if'},
      {blankLine: 'always', prev: '*', next: 'return'},
      {blankLine: 'always', prev: 'const', next: 'export'},
      {blankLine: 'always', prev: '*', next: 'class'}
    ],
    'tailwindcss/no-custom-classname': 'off'
  }
}
kachkaev commented 6 months ago

This is not the case in my project. Seeing ordering like ml-0.5 mr-2 mt-1.5 size-3 self-start. So it might be to do with dependencies (or sub-dependencies) in your project.

You can try creating a new repo to reproduce this behaviour and then build it up step by step. This will help you reveal the peculiarity of your project.

eramsayCB commented 6 months ago

This is not the case in my project. Seeing ordering like ml-0.5 mr-2 mt-1.5 size-3 self-start. So it might be to do with dependencies (or sub-dependencies) in your project.

You can try creating a new repo to reproduce this behaviour and then build it up step by step. This will help you reveal the peculiarity of your project.

Thanks @kachkaev. After seeing your comment I dug into it a bit further and believe I have conflicting linter rules between my packages/configs for this project and my VSCode IDE plugins. Closing this bug out.

Thanks again!