- 
          
 - 
                Notifications
    
You must be signed in to change notification settings  - Fork 57
 
          feat: port react-x/prefer-react-namespace-import into prefer-namespace-import
          #386
        
          New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
          
     Merged
      
      
    
  
     Merged
                    Changes from all commits
      Commits
    
    
            Show all changes
          
          
            7 commits
          
        
        Select commit
          Hold shift + click to select a range
      
      463f767
              
                feat: port 'prefer-namespace-import' rule, closes #258
              
              
                Rel1cx 0f0a4dc
              
                [autofix.ci] apply automated fixes
              
              
                autofix-ci[bot] 90c3115
              
                refactor: align with other rules, add related docs
              
              
                JounQin 73bdcc9
              
                chore: improve test coverage, add changeset for releasing
              
              
                JounQin 6cec75c
              
                fix: support `with` attributes
              
              
                JounQin 0b6a12c
              
                docs: add rule category
              
              
                JounQin 6e30b35
              
                test: improve coverage
              
              
                JounQin File filter
Filter by extension
Conversations
          Failed to load comments.   
        
        
          
      Loading
        
  Jump to
        
          Jump to file
        
      
      
          Failed to load files.   
        
        
          
      Loading
        
  Diff view
Diff view
There are no files selected for viewing
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              | Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "eslint-plugin-import-x": minor | ||
| --- | ||
| 
     | 
||
| feat: port [`react-x/prefer-react-namespace-import`](https://eslint-react.xyz/docs/rules/prefer-react-namespace-import) into `prefer-namespace-import` | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              | Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| # import-x/prefer-namespace-import | ||
| 
     | 
||
| 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). | ||
| 
     | 
||
| <!-- end auto-generated rule header --> | ||
| 
     | 
||
| Enforce using namespace imports for specific modules, like `react`/`react-dom`, etc. | ||
| 
     | 
||
| ## Rule Details | ||
| 
     | 
||
| ### rule schema | ||
| 
     | 
||
| ```jsonc | ||
| { | ||
| "import-x/prefer-namespace-import": [ | ||
| "error", // or "off", "warn" | ||
| { | ||
| "patterns": [ | ||
| // Exact match | ||
| "foo", | ||
| // RegExp | ||
| "/^prefix-/", | ||
| ], | ||
| }, | ||
| ], | ||
| } | ||
| ``` | ||
| 
     | 
||
| ### Config Options | ||
| 
     | 
||
| `patterns` is an array of strings or `RegExp` patterns that specify which modules should be imported using namespace imports. | ||
| 
     | 
||
| #### Example | ||
| 
     | 
||
| ```js | ||
| /*eslint import-x/prefer-namespace-import: [2, { patterns: ['react'] }]*/ | ||
| 
     | 
||
| // bad | ||
| import React from 'react' | ||
| 
     | 
||
| // good | ||
| import * as React from 'react' | ||
| 
     | 
||
| // ignored | ||
| import ReactDOM from 'react-dom' | ||
| ``` | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              | Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| import { createRule } from '../utils/index.js' | ||
| 
     | 
||
| export interface Options { | ||
| patterns?: readonly string[] | ||
| } | ||
| 
     | 
||
| export type MessageId = 'preferNamespaceImport' | ||
| 
     | 
||
| export default createRule<[Options?], MessageId>({ | ||
| name: 'prefer-namespace-import', | ||
| meta: { | ||
| type: 'problem', | ||
| docs: { | ||
| category: 'Style guide', | ||
| description: | ||
| 'Enforce using namespace imports for specific modules, like `react`/`react-dom`, etc.', | ||
| }, | ||
| fixable: 'code', | ||
| schema: [ | ||
| { | ||
| type: 'object', | ||
| additionalProperties: false, | ||
| properties: { | ||
| patterns: { | ||
| type: 'array', | ||
| items: { | ||
| type: 'string', | ||
| }, | ||
| uniqueItems: true, | ||
| }, | ||
| }, | ||
| }, | ||
| ], | ||
| messages: { | ||
| preferNamespaceImport: | ||
| 'Prefer importing {{specifier}} as \'import * as {{specifier}} from "{{source}}"\';', | ||
| }, | ||
| }, | ||
| defaultOptions: [], | ||
| create(context) { | ||
| const { patterns } = context.options[0] ?? {} | ||
| if (!patterns?.length) { | ||
| return {} | ||
| } | ||
| const regexps = patterns.map(toRegExp) | ||
| return { | ||
| ImportDefaultSpecifier(node) { | ||
| const importSource = node.parent.source.value | ||
| if (!regexps.some(exp => exp.test(importSource))) { | ||
| return | ||
| } | ||
| const defaultSpecifier = node.local.name | ||
| const hasOtherSpecifiers = node.parent.specifiers.length > 1 | ||
| context.report({ | ||
| messageId: 'preferNamespaceImport', | ||
| node: hasOtherSpecifiers ? node : node.parent, | ||
| data: { | ||
| source: importSource, | ||
| specifier: defaultSpecifier, | ||
| }, | ||
| fix(fixer) { | ||
| const importDeclarationText = context.sourceCode.getText( | ||
| node.parent, | ||
| ) | ||
| const localName = node.local.name | ||
| if (!hasOtherSpecifiers) { | ||
| return fixer.replaceText(node, `* as ${localName}`) | ||
| } | ||
| const isTypeImport = node.parent.importKind === 'type' | ||
| const importStringPrefix = `import${isTypeImport ? ' type' : ''}` | ||
| // remove the default specifier and prepend the namespace import specifier | ||
| const rightBraceIndex = importDeclarationText.indexOf('}') + 1 | ||
                
      
                  JounQin marked this conversation as resolved.
               
          
            Show resolved
            Hide resolved
         | 
||
| const specifiers = importDeclarationText.slice( | ||
| importDeclarationText.indexOf('{'), | ||
| rightBraceIndex, | ||
| ) | ||
| const remainingText = importDeclarationText.slice(rightBraceIndex) | ||
| return fixer.replaceText( | ||
| node.parent, | ||
| [ | ||
| `${importStringPrefix} * as ${localName} ${remainingText.trimStart()}`, | ||
| `${importStringPrefix} ${specifiers}${remainingText}`, | ||
| ].join('\n'), | ||
| ) | ||
| }, | ||
| }) | ||
| }, | ||
| } | ||
| }, | ||
| }) | ||
| 
     | 
||
| /** Regular expression for matching a RegExp string. */ | ||
| const REGEXP_STR = /^\/(.+)\/([A-Za-z]*)$/u | ||
| 
     | 
||
| /** | ||
| * Convert a string to the `RegExp`. Normal strings (e.g. `"foo"`) is converted | ||
| * to `/^foo$/` of `RegExp`. Strings like `"/^foo/i"` are converted to `/^foo/i` | ||
| * of `RegExp`. | ||
| * | ||
| * @param string The string to convert. | ||
| * @returns Returns the `RegExp`. | ||
| * @see https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/utils/regexp.ts | ||
| */ | ||
| function toRegExp(string: string): { test(s: string): boolean } { | ||
| const [, pattern, flags = 'u'] = REGEXP_STR.exec(string) ?? [] | ||
| if (pattern != null) { | ||
| return new RegExp(pattern, flags) | ||
| } | ||
| return { test: s => s === string } | ||
| } | ||
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              | Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| import { RuleTester as TSESLintRuleTester } from '@typescript-eslint/rule-tester' | ||
| import type { TSESLint } from '@typescript-eslint/utils' | ||
| 
     | 
||
| import { | ||
| createRuleTestCaseFunctions, | ||
| getNonDefaultParsers, | ||
| parsers, | ||
| testFilePath, | ||
| } from '../utils.js' | ||
| 
     | 
||
| import { cjsRequire as require } from 'eslint-plugin-import-x' | ||
| import rule from 'eslint-plugin-import-x/rules/prefer-namespace-import' | ||
| 
     | 
||
| const ruleTester = new TSESLintRuleTester() | ||
| 
     | 
||
| const { tValid, tInvalid } = createRuleTestCaseFunctions<typeof rule>() | ||
| 
     | 
||
| const options = [{ patterns: ['/^@scope/', '/^prefix-/', 'specific'] }] as const | ||
| 
     | 
||
| ruleTester.run('prefer-namespace-import', rule, { | ||
| valid: [ | ||
| tValid({ | ||
| code: `import * as Name from '@scope/name';`, | ||
| }), | ||
| tValid({ | ||
| code: `import * as Name from 'prefix-name';`, | ||
| }), | ||
| tValid({ | ||
| code: `import * as Name from 'specific';`, | ||
| }), | ||
| tValid({ | ||
| code: ` | ||
| import * as Name1 from '@scope/name'; | ||
| import * as Name2 from 'prefix-name'; | ||
| import * as Name2 from 'specific'; | ||
| `, | ||
| }), | ||
| tValid({ | ||
| code: `import Name from 'other-name';`, | ||
| options, | ||
| }), | ||
| ], | ||
| invalid: [ | ||
| tInvalid({ | ||
| code: ` | ||
| import Name1 from '@scope/name'; | ||
| import Name2 from 'prefix-name'; | ||
| import Name3 from 'prefix-name' with { type: 'json' }; | ||
| import Name4, { name4 } from 'prefix-name' with { type: 'json' }; | ||
| import Name5 from 'specific'; | ||
| import Name6 from 'other-name'; | ||
| `, | ||
| errors: [ | ||
| { | ||
| messageId: 'preferNamespaceImport', | ||
| data: { source: '@scope/name', specifier: 'Name1' }, | ||
| }, | ||
| { | ||
| messageId: 'preferNamespaceImport', | ||
| data: { source: 'prefix-name', specifier: 'Name2' }, | ||
| }, | ||
| { | ||
| messageId: 'preferNamespaceImport', | ||
| data: { source: 'prefix-name', specifier: 'Name3' }, | ||
| }, | ||
| { | ||
| messageId: 'preferNamespaceImport', | ||
| data: { source: 'prefix-name', specifier: 'Name4' }, | ||
| }, | ||
| { | ||
| messageId: 'preferNamespaceImport', | ||
| data: { source: 'specific', specifier: 'Name5' }, | ||
| }, | ||
| ], | ||
| options, | ||
| output: ` | ||
| import * as Name1 from '@scope/name'; | ||
| import * as Name2 from 'prefix-name'; | ||
| import * as Name3 from 'prefix-name' with { type: 'json' }; | ||
| import * as Name4 from 'prefix-name' with { type: 'json' }; | ||
| import { name4 } from 'prefix-name' with { type: 'json' }; | ||
| import * as Name5 from 'specific'; | ||
| import Name6 from 'other-name'; | ||
| `, | ||
| }), | ||
| ], | ||
| }) | ||
| 
     | 
||
| describe('TypeScript', () => { | ||
| for (const parser of getNonDefaultParsers()) { | ||
| const parserConfig = { | ||
| languageOptions: { | ||
| ...(parser === parsers.BABEL && { | ||
| parser: require<TSESLint.Parser.LooseParserModule>(parsers.BABEL), | ||
| }), | ||
| }, | ||
| filename: testFilePath('foo.ts'), | ||
| } | ||
| 
     | 
||
| ruleTester.run('prefer-namespace-import', rule, { | ||
| valid: [ | ||
| tValid({ | ||
| code: ` | ||
| import type * as Name1 from '@scope/name'; | ||
| import type * as Name2 from 'prefix-name'; | ||
| `, | ||
| ...parserConfig, | ||
| }), | ||
| tValid({ | ||
| code: `import type Name from 'other-name';`, | ||
| options, | ||
| ...parserConfig, | ||
| }), | ||
| ], | ||
| invalid: [ | ||
| tInvalid({ | ||
| code: ` | ||
| import type Name1 from '@scope/name'; | ||
| import type Name2 from 'prefix-name'; | ||
| import type Name3 from 'prefix-name' with { type: 'json' }; | ||
| import Name4, { type name4 } from 'prefix-name' with { type: 'json' }; | ||
| import type Name5 from 'specific'; | ||
| import type Name6 from 'other-name'; | ||
| `, | ||
| errors: [ | ||
| { | ||
| messageId: 'preferNamespaceImport', | ||
| data: { source: '@scope/name', specifier: 'Name1' }, | ||
| }, | ||
| { | ||
| messageId: 'preferNamespaceImport', | ||
| data: { source: 'prefix-name', specifier: 'Name2' }, | ||
| }, | ||
| { | ||
| messageId: 'preferNamespaceImport', | ||
| data: { source: 'prefix-name', specifier: 'Name3' }, | ||
| }, | ||
| { | ||
| messageId: 'preferNamespaceImport', | ||
| data: { source: 'prefix-name', specifier: 'Name4' }, | ||
| }, | ||
| { | ||
| messageId: 'preferNamespaceImport', | ||
| data: { source: 'specific', specifier: 'Name5' }, | ||
| }, | ||
| ], | ||
| options, | ||
| output: ` | ||
| import type * as Name1 from '@scope/name'; | ||
| import type * as Name2 from 'prefix-name'; | ||
| import type * as Name3 from 'prefix-name' with { type: 'json' }; | ||
| import * as Name4 from 'prefix-name' with { type: 'json' }; | ||
| import { type name4 } from 'prefix-name' with { type: 'json' }; | ||
| import type * as Name5 from 'specific'; | ||
| import type Name6 from 'other-name'; | ||
| `, | ||
| ...parserConfig, | ||
| }), | ||
| ], | ||
| }) | ||
| } | ||
| }) | 
      
      Oops, something went wrong.
        
    
  
  Add this suggestion to a batch that can be applied as a single commit.
  This suggestion is invalid because no changes were made to the code.
  Suggestions cannot be applied while the pull request is closed.
  Suggestions cannot be applied while viewing a subset of changes.
  Only one suggestion per line can be applied in a batch.
  Add this suggestion to a batch that can be applied as a single commit.
  Applying suggestions on deleted lines is not supported.
  You must change the existing code in this line in order to create a valid suggestion.
  Outdated suggestions cannot be applied.
  This suggestion has been applied or marked resolved.
  Suggestions cannot be applied from pending reviews.
  Suggestions cannot be applied on multi-line comments.
  Suggestions cannot be applied while the pull request is queued to merge.
  Suggestion cannot be applied right now. Please check back later.
  
    
  
    
Uh oh!
There was an error while loading. Please reload this page.