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

Issue:
add rule (rrd): component uses js and not ts
This should be only warning level and implement only when we have the cli options to filter out rules as this one might be quite controversal.

As I understand nuxt 4 might be defaults to ts, so `<script setup>` will mean ts.

Pull request:
feat: add new rule rrd noTsLang with tests and docs
### Summary
Add new rule `noTsLang` with tests and docs

### Description
- Added rule file 
- Added test file
- Added docs file

### Related Issues
Fixes #71 

### 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 2cadc4e..4ffe346 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -85,6 +85,7 @@ export default defineConfig({
                 { text: 'Nested Ternary', link: '/rules/rrd/nested-ternary' },
                 { text: 'No Inline Styles', link: '/rules/rrd/no-inline-styles' },
                 { text: 'No Props Destructure', link: '/rules/rrd/no-prop-destructure' },
+                { text: 'No Ts Lang', link: '/rules/rrd/no-ts-lang' },
                 { text: 'No Var Declaration', link: '/rules/rrd/no-var-declaration' },
                 { text: 'Parameter Count', link: '/rules/rrd/parameter-count' },
                 { text: 'Plain Script', link: '/rules/rrd/plain-script' },
diff --git a/docs/rules/rrd/index.md b/docs/rules/rrd/index.md
index 95e2262..0b5ad6d 100644
--- a/docs/rules/rrd/index.md
+++ b/docs/rules/rrd/index.md
@@ -19,6 +19,7 @@ These ruleset is the most opinionated with rules that are not part of the _offic
 - [Nested Ternary](./nested-ternary.md)
 - [No Inline Styles](./no-inline-styles.md)
 - [No Prop Destructing](./no-prop-destructure.md)
+- [No Ts Lang](./no-ts-lang.md)
 - [No Var Declaration](./no-var-declaration.md)
 - [Parameter Count](./parameter-count.md)
 - [Plain Script](./plain-script.md)
diff --git a/docs/rules/rrd/no-ts-lang.md b/docs/rules/rrd/no-ts-lang.md
new file mode 100644
index 0000000..979cf82
--- /dev/null
+++ b/docs/rules/rrd/no-ts-lang.md
@@ -0,0 +1,45 @@
+# No Ts Lang
+
+Checks if the script section of a Vue component is using TypeScript (`lang="ts"`).
+
+## ❓ Why it's good to follow this rule?
+
+- **Type Safety**: TypeScript provides static typing, which helps catch errors early in development.
+- **Better IDE Support**: TypeScript offers enhanced autocompletion, refactoring, and navigation features in most IDEs.
+- **Improved Maintainability**: Type annotations make code more self-documenting and easier to understand.
+- **Scalability**: As projects grow, TypeScript's features become increasingly valuable for managing complexity.
+
+## 😱 Examples of code for which this rule will throw a warning
+
+::: warning
+The following code uses a plain JavaScript `<script setup>` block instead of TypeScript:
+:::
+
+```vue
+<script setup>
+import { ref } from 'vue'
+
+const products = ref([{ name: 'Apple', price: 1 }, { name: 'Banana', price: 2 }])
+
+// more content here...
+</script>
+```
+
+## 🤩 How to fix it?
+
+::: tip
+Add `lang="ts"` to your `<script>` tag and use TypeScript syntax:
+:::
+
+```vue
+<script setup lang="ts">
+import { ref } from 'vue'
+import type { Product } from '@/types/product'
+
+const products: Product[] = ref([{ name: 'Apple', price: 1 }, { name: 'Banana', price: 2 }])
+
+// more content here...
+</script>
+```
+
+By using TypeScript, you gain the benefits of type checking, better tooling support, and improved code quality. This approach aligns with modern Vue.js best practices and helps prevent potential runtime errors by catching them at compile-time.
\ No newline at end of file
diff --git a/src/rules/rrd/index.ts b/src/rules/rrd/index.ts
index 4ceb3f8..65b19de 100644
--- a/src/rules/rrd/index.ts
+++ b/src/rules/rrd/index.ts
@@ -15,6 +15,7 @@ export * from './magicNumbers'
 export * from './nestedTernary'
 export * from './noInlineStyles'
 export * from './noPropDestructure'
+export * from './noTsLang'
 export * from './noVarDeclaration'
 export * from './parameterCount'
 export * from './plainScript'
diff --git a/src/rules/rrd/noTsLang.ts b/src/rules/rrd/noTsLang.ts
new file mode 100644
index 0000000..f542a18
--- /dev/null
+++ b/src/rules/rrd/noTsLang.ts
@@ -0,0 +1,41 @@
+import type { SFCScriptBlock } from '@vue/compiler-sfc'
+import type { FileCheckResult, Offense } from '../../types'
+
+const results: FileCheckResult[] = []
+
+const resetResults = () => (results.length = 0)
+
+const checkNoTsLang = (script: SFCScriptBlock | null, filePath: string) => {
+  if (!script) {
+    return
+  }
+
+  if (script.lang !== 'ts') {
+    results.push({
+      filePath,
+      message: `<bg_warn>component uses js instead of ts</bg_warn>`,
+    })
+  }
+}
+
+const reportNoTsLang = () => {
+  const offenses: Offense[] = []
+
+  if (results.length > 0) {
+    results.forEach((result) => {
+      offenses.push({
+        file: result.filePath,
+        rule: `<text_info>rrd ~ no ts lang</text_info>`,
+        description: `👉 <text_warn>Use typescript instead of javascript.</text_warn> See: https://vue-mess-detector.webmania.cc/rules/rrd/no-ts-lang.html`,
+        message: `${result.message} 🚨`,
+
+      })
+    })
+  }
+
+  resetResults()
+
+  return offenses
+}
+
+export { checkNoTsLang, reportNoTsLang, resetResults }
diff --git a/src/rules/rules.ts b/src/rules/rules.ts
index 2250c5c..7284a5f 100644
--- a/src/rules/rules.ts
+++ b/src/rules/rules.ts
@@ -31,8 +31,8 @@ export const RULES = {
     'bigVif',
     'bigVshow',
     'complicatedConditions',
-    'cyclomaticComplexity',
     'computedSideEffects',
+    'cyclomaticComplexity',
     'deepIndentation',
     'elseCondition',
     'functionSize',
@@ -44,6 +44,7 @@ export const RULES = {
     'nestedTernary',
     'noInlineStyles',
     'noPropDestructure',
+    'noTsLang',
     'noVarDeclaration',
     'parameterCount',
     'plainScript',
diff --git a/src/rulesCheck.ts b/src/rulesCheck.ts
index b2bb8e8..6626f35 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, checkHugeFiles, 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, checkNoTsLang, 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'
@@ -60,6 +60,7 @@ export const checkRules = (descriptor: SFCDescriptor, filePath: string, apply: s
     magicNumbers: () => checkMagicNumbers(script, filePath),
     nestedTernary: () => checkNestedTernary(script, filePath),
     noPropDestructure: () => checkNoPropDestructure(script, filePath),
+    noTsLang: () => isVueFile && checkNoTsLang(script, filePath),
     noVarDeclaration: () => checkNoVarDeclaration(script, filePath),
     parameterCount: () => checkParameterCount(script, filePath, override.maxParameterCount),
     plainScript: () => isVueFile && checkPlainScript(descriptor.script, filePath),
diff --git a/src/rulesReport.ts b/src/rulesReport.ts
index 2e5650f..04934fd 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, reportHugeFiles, 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, reportNoTsLang, 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'
@@ -74,6 +74,7 @@ export const reportRules = (groupBy: GroupBy, sortBy: SortBy, level: OutputLevel
   processOffenses(reportMagicNumbers)
   processOffenses(reportNestedTernary)
   processOffenses(reportNoPropDestructure)
+  processOffenses(reportNoTsLang)
   processOffenses(reportNoVarDeclaration)
   processOffenses(() => reportParameterCount(override.maxParameterCount))
   processOffenses(reportPlainScript)


Test patch:
diff --git a/src/rules/rrd/noTsLang.test.ts b/src/rules/rrd/noTsLang.test.ts
new file mode 100644
index 0000000..76ee741
--- /dev/null
+++ b/src/rules/rrd/noTsLang.test.ts
@@ -0,0 +1,54 @@
+import type { SFCScriptBlock } from '@vue/compiler-sfc'
+import { beforeEach, describe, expect, it } from 'vitest'
+import { checkNoTsLang, reportNoTsLang, resetResults } from './noTsLang'
+
+describe('checkNoTsLang', () => {
+  beforeEach(() => {
+    resetResults()
+  })
+
+  it('should not report files with ts lang', () => {
+    const script = {
+      content: `
+        <script setup lang="ts">
+        import { ref } from 'vue'
+        import type { Product } from '@/types/product'
+
+        const products: Product[] = ref([{ name: 'Apple', price: 1 }, { name: 'Banana', price: 2 }])
+
+        // more content here...
+        </script>
+      `,
+      lang: 'ts',
+    } as SFCScriptBlock
+    const fileName = 'ts-lang.vue'
+    checkNoTsLang(script, fileName)
+    const result = reportNoTsLang()
+    expect(result.length).toBe(0)
+    expect(result).toStrictEqual([])
+  })
+
+  it('should report files where lang ts is not used', () => {
+    const script = {
+      content: `
+        <script setup>
+        import { ref } from 'vue'
+
+        const products = ref([{ name: 'Apple', price: 1 }, { name: 'Banana', price: 2 }])
+
+        // more content here...
+        </script>
+      `,
+    } as SFCScriptBlock
+    const fileName = 'no-ts-lang-problem.vue'
+    checkNoTsLang(script, fileName)
+    const result = reportNoTsLang()
+    expect(result.length).toBe(1)
+    expect(result).toStrictEqual([{
+      file: fileName,
+      rule: `<text_info>rrd ~ no ts lang</text_info>`,
+      description: `👉 <text_warn>Use typescript instead of javascript.</text_warn> See: https://vue-mess-detector.webmania.cc/rules/rrd/no-ts-lang.html`,
+      message: `<bg_warn>component uses js instead of ts</bg_warn> 🚨`,
+    }])
+  })
+})