Repository: rrd108/vue-mess-detector
Pull request #450 (2024-11-09 16:05:34)

Feature/ignore rules for file
### Summary
I have created a new capability to ignore specific rules for specific files (or indeed file matchers) by adding the following to the vue-mess-detector.json:

```
"fileIgnoreRules": {
    "src/main.ts": "tooManyProps,computedSideEffects",
    "src/router/index.ts": "noConsole,multiAttributeElements"
  }
```

### Description
1. I added a fileIgnoreRules to your config object, an array of string:string records.
2. I added the field to the analyse method and updated output to let the user know what files we're ignoring rules for
3. I updated checkFile to update the apply value to remove rules listed in matching files, with that capability pushed out to a checkFileIgnoreRules method.

### Related Issues
N/A

### Type of Change
- [ ] Bug fix (non-breaking change which fixes an issue)
- [X] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Documentation update

### Screenshots (if applicable)
N/A



Patch:
diff --git a/docs/get-started.md b/docs/get-started.md
index 08fcda4..80564f4 100644
--- a/docs/get-started.md
+++ b/docs/get-started.md
@@ -113,10 +113,16 @@ If you want to store your flags in a configuration file, you can create a `.conf
 ```json
 {
   "apply": "vue-strong,rrd",
-  "level": "error"
+  "level": "error",
+  "fileIgnoreRules": {
+    "src/main.ts": "tooManyProps,computedSideEffects",
+    "src/router/index.ts": "noConsole,multiAttributeElements"
+  }
 }
 ```
 
+Note that fileIgnoreRules is only available in the json file configuration. This field allows you to ignore specific rules for specific files.
+
 You can override the limits for multiple rules using our new `override` field in the config file:
 
 ```json
diff --git a/src/analyzer.ts b/src/analyzer.ts
index 44560d9..b1de357 100644
--- a/src/analyzer.ts
+++ b/src/analyzer.ts
@@ -15,6 +15,7 @@ import getProjectRoot from './helpers/getProjectRoot'
 import { groupRulesByRuleset } from './helpers/groupRulesByRuleset'
 import hasServerDir from './helpers/hasServerDir'
 import { isNuxtProject, isVueProject } from './helpers/projectTypeChecker'
+import { checkFileIgnoreRules } from './helpers/setupFileIgnoreList'
 import { RULESETS } from './rules/rules'
 import { checkRules } from './rulesCheck'
 import { reportRules } from './rulesReport'
@@ -23,6 +24,7 @@ let filesCount = 0
 let linesCount = 0
 let _apply: string[] = []
 let _override: OverrideConfig = {} as OverrideConfig
+let _fileIgnoreRules: { [key: string]: string } = {}
 
 // Directories to skip during analysis
 const skipDirs = ['cache', 'coverage', 'dist', '.git', 'node_modules', '.nuxt', '.output', 'vendor']
@@ -43,7 +45,9 @@ const checkFile = async (fileName: string, filePath: string) => {
       descriptor.script = { content } as SFCScriptBlock
     }
 
-    checkRules(descriptor, filePath, _apply, _override)
+    const apply = checkFileIgnoreRules(filePath, _fileIgnoreRules, _apply)
+
+    checkRules(descriptor, filePath, apply, _override)
     return `Analyzing ${filePath}...`
   }
 }
@@ -82,7 +86,7 @@ const walkAsync = async (dir: string) => {
   return overviewMessages
 }
 
-export const analyze = async ({ dir, apply = [], ignore = [], exclude = '', groupBy = 'rule', level = 'all', sortBy = 'desc' }: AnalyzeParams): Promise<AnalyzeOutput> => {
+export const analyze = async ({ dir, apply = [], ignore = [], exclude = '', groupBy = 'rule', level = 'all', sortBy = 'desc', fileIgnoreRules = {} }: AnalyzeParams): Promise<AnalyzeOutput> => {
   filesCount = 0
   linesCount = 0
   const projectRoot = await getProjectRoot(dir)
@@ -96,6 +100,7 @@ export const analyze = async ({ dir, apply = [], ignore = [], exclude = '', grou
   groupBy = groupBy || config.group
   level = level || config.level
   sortBy = sortBy || config.sort
+  _fileIgnoreRules = { ...config.fileIgnoreRules, ...fileIgnoreRules }
 
   _override = config.override
 
@@ -143,6 +148,7 @@ export const analyze = async ({ dir, apply = [], ignore = [], exclude = '', grou
     info: `${applyingMessage}
       Ignoring ${ignoredRulesets.length} rulesets: ${ignoreRulesetsOutput}
       Ignoring ${ignoredRules.length} individual rules: ${ignoredRulesOutput}
+      Ignoring file-specific rules: ${Object.entries(_fileIgnoreRules).map(([file, rules]) => `${file}: ${rules}`).join(', ')}
       Excluding ${exclude || '-'}
       Output level <bg_info>${level}</bg_info>
       Grouping by <bg_info>${groupBy}</bg_info>
diff --git a/src/helpers/getConfig.ts b/src/helpers/getConfig.ts
index 5c02112..43ee4e8 100644
--- a/src/helpers/getConfig.ts
+++ b/src/helpers/getConfig.ts
@@ -19,6 +19,7 @@ export const getConfig = async (projectRoot: string): Promise<Config> => {
   const defaultConfig: Config = {
     apply: Object.values(RULESETS).join(','),
     ignore: '',
+    fileIgnoreRules: {},
     exclude: '',
     group: 'rule',
     level: 'all',
@@ -54,6 +55,10 @@ export const getConfig = async (projectRoot: string): Promise<Config> => {
         ...defaultConfig.override,
         ...configFileContent?.override,
       },
+      fileIgnoreRules: {
+        ...defaultConfig.fileIgnoreRules,
+        ...configFileContent?.fileIgnoreRules,
+      },
     }
   }
   catch {
diff --git a/src/helpers/setupFileIgnoreList.ts b/src/helpers/setupFileIgnoreList.ts
new file mode 100644
index 0000000..684208d
--- /dev/null
+++ b/src/helpers/setupFileIgnoreList.ts
@@ -0,0 +1,28 @@
+import type { RuleSetType } from '../rules/rules'
+import { minimatch } from 'minimatch'
+import { RULES, RULESETS } from '../rules/rules'
+
+export const checkFileIgnoreRules = (filePath: string, fileIgnoreRules: { [key: string]: string }, _apply: string[]) => {
+  let apply = [..._apply] // Here we get a list of rules only - no rule sets
+
+  for (const [pattern, rules] of Object.entries(fileIgnoreRules)) {
+    if (minimatch(filePath, pattern, { matchBase: true })) {
+      const ignoreRules = rules.split(',').map(rule => rule.trim())
+      // Expand ignoreRules by replacing rulesets with individual rules
+      const expandedIgnoreRules = ignoreRules.flatMap((rule) => {
+        if (RULESETS.includes(rule as RuleSetType)) {
+          return RULES[rule as RuleSetType]
+        }
+        else {
+          return rule
+        }
+      })
+
+      // Remove duplicates by converting to a Set and back to an array
+      const uniqueIgnoreRules = Array.from(new Set(expandedIgnoreRules))
+      apply = apply.filter(rule => !uniqueIgnoreRules.includes(rule))
+    }
+  }
+
+  return apply
+}
diff --git a/src/types/Config.ts b/src/types/Config.ts
index ec1545c..eb6af86 100644
--- a/src/types/Config.ts
+++ b/src/types/Config.ts
@@ -4,6 +4,7 @@ export interface Config {
   path?: string
   apply: string
   ignore: string
+  fileIgnoreRules?: Record<string, string>
   exclude: string
   group: string
   level: string
diff --git a/src/types/index.ts b/src/types/index.ts
index ed657b8..a9389a2 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -27,6 +27,7 @@ export interface AnalyzeParams {
   groupBy: GroupBy
   level: OutputLevel
   sortBy: SortBy
+  fileIgnoreRules?: { [key: string]: string }
 }
 
 export interface Offense {


Test patch:
diff --git a/src/helpers/setupFileIgnoreList.test.ts b/src/helpers/setupFileIgnoreList.test.ts
new file mode 100644
index 0000000..201f4cb
--- /dev/null
+++ b/src/helpers/setupFileIgnoreList.test.ts
@@ -0,0 +1,42 @@
+import type minimatch from 'minimatch'
+import { describe, expect, it, vi } from 'vitest'
+import { checkFileIgnoreRules } from './setupFileIgnoreList'
+
+// Use `importActual` to partially mock `minimatch`
+vi.mock('minimatch', async () => {
+  const actual = await vi.importActual<typeof minimatch>('minimatch')
+  return {
+    ...actual,
+    default: (filePath: string, pattern: string) => filePath === pattern,
+  }
+})
+
+describe('checkFileIgnoreRules', () => {
+  it('should return apply unchanged if no matching rules in fileIgnoreRules (Scenario 1)', () => {
+    const filePath = 'test/file/path'
+    const fileIgnoreRules = {} // No ignore rules
+    const _apply = ['rule1', 'rule3']
+    const result = checkFileIgnoreRules(filePath, fileIgnoreRules, _apply)
+    expect(result).toEqual(_apply) // Should return unchanged
+  })
+
+  it('should remove matching rules from apply if they exist in fileIgnoreRules (Scenario 2)', () => {
+    const filePath = 'test/file/path'
+    const fileIgnoreRules = {
+      'test/file/path': 'rule1, rule3', // Matching rules
+    }
+    const _apply = ['rule1', 'rule2', 'rule3']
+    const result = checkFileIgnoreRules(filePath, fileIgnoreRules, _apply)
+    expect(result).toEqual(['rule2']) // Should remove 'rule1' and 'rule3'
+  })
+
+  it('should remove rules from apply if fileIgnoreRules has rulesets containing those rules (Scenario 3)', () => {
+    const filePath = 'test/file/path'
+    const fileIgnoreRules = {
+      'test/file/path': 'vue-caution', // ruleSet1 contains rule1 and rule2
+    }
+    const _apply = ['rule1', 'rule2', 'elementSelectorsWithScoped', 'implicitParentChildCommunication']
+    const result = checkFileIgnoreRules(filePath, fileIgnoreRules, _apply)
+    expect(result).toEqual(['rule1', 'rule2']) // Should remove 'elementSelectorsWithScoped' and 'implicitParentChildCommunication'
+  })
+})