Repository: rrd108/vue-mess-detector (ts)

Issue:
add rule (rrd) huge files
script + template + css together
warning on 300 lines and error on 500 lines

Pull request description:
feat: add new rule rrd ~ huge files with tests (#201)
### Summary
Add new rule huge files for rrd ruleset with tests

### Description
- Add rule file plus tests
- Add rule docs  

### Related Issues
Fixes #201 

### 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)
- [X] Documentation update

Patch:
diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index 4cf47c4..2cadc4e 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -79,6 +79,7 @@ export default defineConfig({
                 { text: 'Function Size', link: '/rules/rrd/function-size' },
                 { text: 'HTML Image Elements', link: '/rules/rrd/html-image-elements' },
                 { text: 'HTML links', link: '/rules/rrd/html-link' },
+                { text: 'Huge Files', link: '/rules/rrd/huge-files' },
                 { text: 'If Without Curly Braces', link: '/rules/rrd/if-without-curly-braces' },
                 { text: 'Magic Numbers', link: '/rules/rrd/magic-numbers' },
                 { text: 'Nested Ternary', link: '/rules/rrd/nested-ternary' },
diff --git a/docs/rules/rrd/huge-files.md b/docs/rules/rrd/huge-files.md
new file mode 100644
index 0000000..5bfed30
--- /dev/null
+++ b/docs/rules/rrd/huge-files.md
@@ -0,0 +1,23 @@
+# Huge Files
+
+Checks if the combined length of script, template, and CSS in a Vue component is too long. The default threshold for a warning is **300** lines, and for an error is **twice that length**.
+
+## ❓ Why it's good to follow this rule?
+
+- **Maintainability**: Smaller files are easier to maintain and understand.
+- **Readability**: Shorter files are easier to read and navigate.
+- **Testability**: Smaller components are easier to test.
+- **Reusability**: Smaller, more focused components are more likely to be reusable.
+
+## 😱 Examples of code for which this rule will throw a warning
+
+::: warning
+A Vue component file with more than 300 lines of combined script, template, and style content.
+:::
+
+## 🤩 How to fix it?
+
+- Split the component into smaller, more focused components.
+- Extract complex logic into separate composable functions.
+- Move styles to separate CSS files or use a CSS-in-JS solution.
+- Consider using mixins or extending base components for shared functionality.
\ No newline at end of file
diff --git a/docs/rules/rrd/index.md b/docs/rules/rrd/index.md
index a8cf96d..95e2262 100644
--- a/docs/rules/rrd/index.md
+++ b/docs/rules/rrd/index.md
@@ -13,6 +13,7 @@ These ruleset is the most opinionated with rules that are not part of the _offic
 - [Function Size](./function-size.md)
 - [HTML Image Elements](./html-image-elements.md)
 - [HTML links](./html-link.md)
+- [Huge Files](./huge-files.md)
 - [If Without Curly Braces](./if-without-curly-braces.md)
 - [Magic Numbers](./magic-numbers.md)
 - [Nested Ternary](./nested-ternary.md)
diff --git a/src/rules/rrd/hugeFiles.ts b/src/rules/rrd/hugeFiles.ts
new file mode 100644
index 0000000..f9d3b5d
--- /dev/null
+++ b/src/rules/rrd/hugeFiles.ts
@@ -0,0 +1,55 @@
+import type { SFCDescriptor } from '@vue/compiler-sfc'
+import type { FileCheckResult, Offense } from '../../types'
+
+const results: FileCheckResult[] = []
+
+const WARNING_THRESHOLD = 300
+const ERROR_THRESHOLD = 2 * WARNING_THRESHOLD
+
+const checkHugeFiles = (descriptor: SFCDescriptor, filePath: string, isVueFile: boolean) => {
+  let totalLines = 0
+
+  if (isVueFile) {
+    totalLines = descriptor.scriptSetup?.content.trim().split('\n').length ?? 0
+    totalLines += descriptor.template?.content.trim().split('\n').length ?? 0
+    totalLines += descriptor.styles?.reduce((acc, style) => acc + style.content.trim().split('\n').length, 0) ?? 0
+  }
+  else {
+    totalLines = descriptor.scriptSetup?.content.trim().split('\n').length ?? 0
+  }
+
+  if (totalLines > ERROR_THRESHOLD) {
+    results.push({
+      filePath,
+      message: `<bg_err>huge file (${totalLines} lines)</bg_err>`,
+    })
+  }
+  else if (totalLines > WARNING_THRESHOLD) {
+    results.push({
+      filePath,
+      message: `<bg_warn>large file (${totalLines} lines)</bg_warn>`,
+    })
+  }
+}
+
+const reportHugeFiles = () => {
+  const offenses: Offense[] = []
+
+  if (results.length > 0) {
+    results.forEach((result) => {
+      offenses.push({
+        file: result.filePath,
+        rule: `<text_info>rrd ~ huge files</text_info>`,
+        description: `👉 <text_warn>Try to split this component into smaller components or extract logic into composables.</text_warn> See: https://vue-mess-detector.webmania.cc/rules/rrd/huge-files.html`,
+        message: `${result.message} 🚨`,
+      })
+    })
+  }
+  return offenses
+}
+
+const resetHugeFiles = () => {
+  results.length = 0
+}
+
+export { checkHugeFiles, reportHugeFiles, resetHugeFiles }
diff --git a/src/rules/rrd/index.ts b/src/rules/rrd/index.ts
index e3b8056..4ceb3f8 100644
--- a/src/rules/rrd/index.ts
+++ b/src/rules/rrd/index.ts
@@ -9,6 +9,7 @@ export * from './elseCondition'
 export * from './functionSize'
 export * from './htmlImageElements'
 export * from './htmlLink'
+export * from './hugeFiles'
 export * from './ifWithoutCurlyBraces'
 export * from './magicNumbers'
 export * from './nestedTernary'
diff --git a/src/rules/rules.ts b/src/rules/rules.ts
index f6512e0..2250c5c 100644
--- a/src/rules/rules.ts
+++ b/src/rules/rules.ts
@@ -38,6 +38,7 @@ export const RULES = {
     'functionSize',
     'htmlImageElements',
     'htmlLink',
+    'hugeFiles',
     'ifWithoutCurlyBraces',
     'magicNumbers',
     'nestedTernary',
diff --git a/src/rulesCheck.ts b/src/rulesCheck.ts
index 2e9617d..39a5d77 100644
--- a/src/rulesCheck.ts
+++ b/src/rulesCheck.ts
@@ -1,7 +1,7 @@
 import type { SFCDescriptor } from '@vue/compiler-sfc'
 import type { OverrideConfig } from './types/Override'
 import { getIsNuxt } from './context'
-import { checkApiWithoutMethod, checkBigVif, checkBigVshow, checkComplicatedConditions, checkComputedSideEffects, checkCyclomaticComplexity, checkDeepIndentation, checkElseCondition, checkFunctionSize, checkHtmlImageElements, checkHtmlLink, checkIfWithoutCurlyBraces, checkMagicNumbers, checkNestedTernary, checkNoInlineStyles, checkNoPropDestructure, checkNoVarDeclaration, checkParameterCount, checkPlainScript, checkPropsDrilling, checkScriptLength, checkShortVariableName, checkTooManyProps, checkVForWithIndexKey, checkZeroLengthComparison } from './rules/rrd'
+import { checkApiWithoutMethod, checkBigVif, checkBigVshow, checkComplicatedConditions, checkComputedSideEffects, checkCyclomaticComplexity, checkDeepIndentation, checkElseCondition, checkFunctionSize, checkHtmlImageElements, checkHtmlLink, checkHugeFiles, checkIfWithoutCurlyBraces, checkMagicNumbers, checkNestedTernary, checkNoInlineStyles, checkNoPropDestructure, checkNoVarDeclaration, checkParameterCount, checkPlainScript, checkPropsDrilling, checkScriptLength, checkShortVariableName, checkTooManyProps, checkVForWithIndexKey, checkZeroLengthComparison } from './rules/rrd'
 import { RULES } from './rules/rules'
 import { checkElementSelectorsWithScoped, checkImplicitParentChildCommunication } from './rules/vue-caution'
 import { checkGlobalStyle, checkSimpleProp, checkSingleNameComponent, checkVforNoKey, checkVifWithVfor } from './rules/vue-essential'
@@ -55,6 +55,7 @@ export const checkRules = (descriptor: SFCDescriptor, filePath: string, apply: s
     functionSize: () => checkFunctionSize(script, filePath, override.maxFunctionSize),
     htmlImageElements: () => getIsNuxt() && checkHtmlImageElements(descriptor.template, filePath),
     htmlLink: () => isVueFile && checkHtmlLink(descriptor.template, filePath),
+    hugeFiles: () => checkHugeFiles(descriptor, filePath, isVueFile),
     ifWithoutCurlyBraces: () => checkIfWithoutCurlyBraces(script, filePath),
     magicNumbers: () => checkMagicNumbers(script, filePath),
     nestedTernary: () => checkNestedTernary(script, filePath),
diff --git a/src/rulesReport.ts b/src/rulesReport.ts
index 553d2cf..2e5650f 100644
--- a/src/rulesReport.ts
+++ b/src/rulesReport.ts
@@ -1,7 +1,7 @@
 import type { GroupBy, Health, Offense, OffensesGrouped, OutputLevel, ReportFunction, SortBy } from './types'
 import type { OverrideConfig } from './types/Override'
 import type { ReportOutput } from './types/ReportOutput'
-import { reportApiWithoutMethod, reportBigVif, reportBigVshow, reportComplicatedConditions, reportComputedSideEffects, reportCyclomaticComplexity, reportDeepIndentation, reportElseCondition, reportFunctionSize, reportHtmlImageElements, reportHtmlLink, reportIfWithoutCurlyBraces, reportMagicNumbers, reportNestedTernary, reportNoInlineStyles, reportNoPropDestructure, reportNoVarDeclaration, reportParameterCount, reportPlainScript, reportPropsDrilling, reportScriptLength, reportShortVariableName, reportTooManyProps, reportVForWithIndexKey, reportZeroLengthComparison } from './rules/rrd'
+import { reportApiWithoutMethod, reportBigVif, reportBigVshow, reportComplicatedConditions, reportComputedSideEffects, reportCyclomaticComplexity, reportDeepIndentation, reportElseCondition, reportFunctionSize, reportHtmlImageElements, reportHtmlLink, reportHugeFiles, reportIfWithoutCurlyBraces, reportMagicNumbers, reportNestedTernary, reportNoInlineStyles, reportNoPropDestructure, reportNoVarDeclaration, reportParameterCount, reportPlainScript, reportPropsDrilling, reportScriptLength, reportShortVariableName, reportTooManyProps, reportVForWithIndexKey, reportZeroLengthComparison } from './rules/rrd'
 import { reportElementSelectorsWithScoped, reportImplicitParentChildCommunication } from './rules/vue-caution'
 import { reportGlobalStyle, reportSimpleProp, reportSingleNameComponent, reportVforNoKey, reportVifWithVfor } from './rules/vue-essential'
 import { reportElementAttributeOrder, reportTopLevelElementOrder } from './rules/vue-recommended'
@@ -69,6 +69,7 @@ export const reportRules = (groupBy: GroupBy, sortBy: SortBy, level: OutputLevel
   processOffenses(() => reportFunctionSize(override.maxFunctionSize))
   processOffenses(reportHtmlImageElements)
   processOffenses(reportHtmlLink)
+  processOffenses(reportHugeFiles)
   processOffenses(reportIfWithoutCurlyBraces)
   processOffenses(reportMagicNumbers)
   processOffenses(reportNestedTernary)


Test patch:
diff --git a/src/rules/rrd/hugeFiles.test.ts b/src/rules/rrd/hugeFiles.test.ts
new file mode 100644
index 0000000..41a2989
--- /dev/null
+++ b/src/rules/rrd/hugeFiles.test.ts
@@ -0,0 +1,182 @@
+import type { SFCDescriptor } from '@vue/compiler-sfc'
+import { beforeEach, describe, expect, it } from 'vitest'
+import { checkHugeFiles, reportHugeFiles, resetHugeFiles } from './hugeFiles'
+
+describe('checkHugeFiles', () => {
+  beforeEach(() => {
+    resetHugeFiles()
+  })
+
+  it('should not report vue sfc files with small content', () => {
+    const filePath = 'small-file.vue'
+    const smallFile = {
+      template: {
+        type: 'template',
+        content: '<template>\n'.repeat(100),
+        loc: {
+          source: '',
+          start: { line: 1, column: 1, offset: 0 },
+          end: { line: 100, column: 1, offset: 100 * 10 },
+        },
+        attrs: {},
+      },
+      scriptSetup: {
+        type: 'script',
+        content: '<script setup>\n'.repeat(100),
+        loc: {
+          source: '',
+          start: { line: 1, column: 1, offset: 0 },
+          end: { line: 100, column: 1, offset: 100 * 9 },
+        },
+        attrs: {},
+      },
+      styles: [{
+        type: 'style',
+        content: '<style scoped>\n'.repeat(50),
+        loc: {
+          source: '',
+          start: { line: 1, column: 1, offset: 0 },
+          end: { line: 50, column: 1, offset: 50 * 8 },
+        },
+        attrs: {},
+      }],
+    } as SFCDescriptor
+    checkHugeFiles(smallFile, filePath, true)
+    expect(reportHugeFiles().length).toBe(0)
+    expect(reportHugeFiles()).toStrictEqual([])
+  })
+
+  it('should not report js/ts files with small content', () => {
+    const filePath = 'small-file.ts'
+    const smallNotVueFile = {
+      scriptSetup: {
+        content: 'const a = 1\nconst b = 2\nconst c = 3\n'.repeat(100),
+      },
+    } as SFCDescriptor
+    checkHugeFiles(smallNotVueFile, filePath, true)
+    expect(reportHugeFiles().length).toBe(0)
+    expect(reportHugeFiles()).toStrictEqual([])
+  })
+
+  it('should report vue sfc files with large content', () => {
+    const largeFile = {
+      template: {
+        type: 'template',
+        content: '<template>\n'.repeat(150),
+        loc: {
+          source: '',
+          start: { line: 1, column: 1, offset: 0 },
+          end: { line: 150, column: 1, offset: 150 * 10 },
+        },
+        attrs: {},
+      },
+      scriptSetup: {
+        type: 'script',
+        content: '<script setup>\n'.repeat(100),
+        loc: {
+          source: '',
+          start: { line: 1, column: 1, offset: 0 },
+          end: { line: 100, column: 1, offset: 100 * 9 },
+        },
+        attrs: {},
+      },
+      styles: [{
+        type: 'style',
+        content: '<style scoped>\n'.repeat(100),
+        loc: {
+          source: '',
+          start: { line: 1, column: 1, offset: 0 },
+          end: { line: 100, column: 1, offset: 100 * 8 },
+        },
+        attrs: {},
+      }],
+    } as SFCDescriptor
+    const filePath = 'large-file.vue'
+    checkHugeFiles(largeFile, filePath, true)
+    expect(reportHugeFiles().length).toBe(1)
+    expect(reportHugeFiles()).toStrictEqual([{
+      file: filePath,
+      rule: `<text_info>rrd ~ huge files</text_info>`,
+      description: `👉 <text_warn>Try to split this component into smaller components or extract logic into composables.</text_warn> See: https://vue-mess-detector.webmania.cc/rules/rrd/huge-files.html`,
+      message: '<bg_warn>large file (350 lines)</bg_warn> 🚨',
+    }])
+  })
+
+  it('should report a warning for a large js/ts file', () => {
+    const largeNotVueFile = {
+      scriptSetup: {
+        content: 'console.log("hello world")\n'.repeat(400),
+      },
+    } as SFCDescriptor
+    const filePath = 'large-file.ts'
+    checkHugeFiles(largeNotVueFile, filePath, true)
+    expect(reportHugeFiles().length).toBe(1)
+    expect(reportHugeFiles()).toStrictEqual([{
+      file: filePath,
+      rule: `<text_info>rrd ~ huge files</text_info>`,
+      description: `👉 <text_warn>Try to split this component into smaller components or extract logic into composables.</text_warn> See: https://vue-mess-detector.webmania.cc/rules/rrd/huge-files.html`,
+      message: '<bg_warn>large file (400 lines)</bg_warn> 🚨',
+    }])
+  })
+
+  it('should report vue sfc files with huge content', () => {
+    const hugeFile = {
+      filename: 'huge.vue',
+      source: '',
+      template: {
+        type: 'template',
+        content: '<template>\n'.repeat(300),
+        loc: {
+          source: '',
+          start: { line: 1, column: 1, offset: 0 },
+          end: { line: 300, column: 1, offset: 300 * 9 },
+        },
+      },
+      scriptSetup: {
+        type: 'script',
+        content: '<script setup>\n'.repeat(200),
+        loc: {
+          source: '',
+          start: { line: 1, column: 1, offset: 0 },
+          end: { line: 200, column: 1, offset: 200 * 9 },
+        },
+      },
+      styles: [{
+        type: 'style',
+        content: '<style scoped>\n'.repeat(150),
+        loc: {
+          source: '',
+          start: { line: 1, column: 1, offset: 0 },
+          end: { line: 150, column: 1, offset: 150 * 8 },
+        },
+      }],
+    } as SFCDescriptor
+    const filePath = 'huge.vue'
+    checkHugeFiles(hugeFile, filePath, true)
+    const result = reportHugeFiles()
+    expect(result.length).toBe(1)
+    expect(result).toStrictEqual([{
+      file: filePath,
+      rule: `<text_info>rrd ~ huge files</text_info>`,
+      description: `👉 <text_warn>Try to split this component into smaller components or extract logic into composables.</text_warn> See: https://vue-mess-detector.webmania.cc/rules/rrd/huge-files.html`,
+      message: '<bg_err>huge file (650 lines)</bg_err> 🚨',
+    }])
+  })
+
+  it('should report an error for a huge js/ts file', () => {
+    const hugeNotVueFile = {
+      scriptSetup: {
+        content: 'console.log("hello world")\n'.repeat(700),
+      },
+    } as SFCDescriptor
+    const filePath = 'huge-file.ts'
+    checkHugeFiles(hugeNotVueFile, filePath, true)
+    expect(reportHugeFiles().length).toBe(1)
+    expect(reportHugeFiles()).toStrictEqual([{
+      file: filePath,
+      rule: `<text_info>rrd ~ huge files</text_info>`,
+      description: `👉 <text_warn>Try to split this component into smaller components or extract logic into composables.</text_warn> See: https://vue-mess-detector.webmania.cc/rules/rrd/huge-files.html`,
+      message: '<bg_err>huge file (700 lines)</bg_err> 🚨',
+    }])
+  })
+})