Skip to content

Commit d6ae520

Browse files
author
HelloWorldU
committed
feat: ast analyzer + eslint + github actions ci pipeline
- Implement ast/rules/ (vue-structure, import-restrictions, style-constraints) - Wire analyzer.ts with all rules, runs via regex analysis - Add ESLint config with Vue + TypeScript support - Add npm scripts: typecheck, lint, lint:fix, analyze, ci - Add GitHub Actions CI workflow (.github/workflows/ci.yml) - Fix all lint errors (unused props, any types, inline styles) - Add vite-env.d.ts for .vue type declarations - CI passes: typecheck + lint + analyze + build
1 parent e599376 commit d6ae520

14 files changed

Lines changed: 2025 additions & 205 deletions

.github/workflows/ci.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
check:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- name: Checkout
15+
uses: actions/checkout@v4
16+
17+
- name: Setup Node.js
18+
uses: actions/setup-node@v4
19+
with:
20+
node-version: '22'
21+
cache: 'npm'
22+
cache-dependency-path: kimi-code-swarm/package-lock.json
23+
24+
- name: Install dependencies
25+
run: cd kimi-code-swarm && npm ci
26+
27+
- name: Type check
28+
run: cd kimi-code-swarm && npm run typecheck
29+
30+
- name: Lint
31+
run: cd kimi-code-swarm && npm run lint
32+
33+
- name: AST analyze
34+
run: cd kimi-code-swarm && npm run analyze
35+
36+
- name: Build
37+
run: cd kimi-code-swarm && npm run build

ast/analyzer.ts

Lines changed: 53 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,30 @@
55
*/
66

77
import { readFileSync, readdirSync, statSync } from 'fs'
8-
import { join, extname } from 'path'
9-
import { parse as parseVue } from '@vue/compiler-sfc'
8+
import { join, extname, relative } from 'path'
109

11-
// TODO: 导入规则模块
12-
// import { checkVueStructure } from './rules/vue-structure'
13-
// import { checkImports } from './rules/import-restrictions'
10+
import { checkVueStructure } from './rules/vue-structure'
11+
import { checkImports } from './rules/import-restrictions'
12+
import { checkStyle } from './rules/style-constraints'
1413

1514
export interface AstIssue {
1615
file: string
1716
rule: string
1817
message: string
1918
line?: number
2019
fixable: boolean
20+
fix?: string
2121
}
2222

23+
const SHOULD_FIX = process.argv.includes('--fix')
24+
2325
function findFiles(dir: string): string[] {
2426
const results: string[] = []
2527
for (const entry of readdirSync(dir)) {
28+
if (entry === 'node_modules' || entry === 'dist') continue
2629
const full = join(dir, entry)
2730
const stat = statSync(full)
28-
if (stat.isDirectory() && entry !== 'node_modules') {
31+
if (stat.isDirectory()) {
2932
results.push(...findFiles(full))
3033
} else if (stat.isFile() && ['.vue', '.ts'].includes(extname(entry))) {
3134
results.push(full)
@@ -34,30 +37,24 @@ function findFiles(dir: string): string[] {
3437
return results
3538
}
3639

37-
function analyzeFile(filePath: string, content: string): AstIssue[] {
40+
function analyzeVueFile(filePath: string, content: string): AstIssue[] {
3841
const issues: AstIssue[] = []
39-
const ext = extname(filePath)
4042

41-
if (ext === '.vue') {
42-
const { descriptor, errors } = parseVue(content)
43-
if (errors.length) {
44-
issues.push({ file: filePath, rule: 'vue/parse-error', message: String(errors[0]), fixable: false })
45-
return issues
46-
}
43+
// Vue 结构规则
44+
issues.push(...checkVueStructure(content, filePath))
4745

48-
// TODO: 接入规则检查
49-
// issues.push(...checkVueStructure(descriptor))
50-
// issues.push(...checkImports(content, filePath))
46+
// 导入限制规则(script 内容)
47+
issues.push(...checkImports(content, filePath))
5148

52-
// 临时占位检查
53-
if (!descriptor.scriptSetup) {
54-
issues.push({ file: filePath, rule: 'vue/no-script-setup', message: '必须使用 <script setup lang="ts">', line: 1, fixable: true })
55-
}
56-
if (descriptor.styles.some(s => s.scoped)) {
57-
issues.push({ file: filePath, rule: 'vue/no-scoped-style', message: '禁止 <style scoped>,用 Tailwind', fixable: true })
58-
}
59-
}
49+
// 样式约束规则
50+
issues.push(...checkStyle(content, filePath))
51+
52+
return issues
53+
}
6054

55+
function analyzeTsFile(filePath: string, content: string): AstIssue[] {
56+
const issues: AstIssue[] = []
57+
issues.push(...checkImports(content, filePath))
6158
return issues
6259
}
6360

@@ -68,20 +65,43 @@ function main() {
6865
process.exit(1)
6966
}
7067

71-
const files = statSync(target).isDirectory() ? findFiles(target) : [target]
68+
const stat = statSync(target)
69+
const files: string[] = stat.isDirectory() ? findFiles(target) : [target]
70+
7271
let total = 0
72+
let fixable = 0
7373

7474
for (const file of files) {
75-
const issues = analyzeFile(file, readFileSync(file, 'utf-8'))
76-
if (issues.length) {
77-
console.log(`\n📄 ${file}`)
78-
issues.forEach(i => console.log(` ${i.fixable ? '🔧' : '❌'} [${i.rule}] ${i.message}`))
79-
total += issues.length
75+
const content = readFileSync(file, 'utf-8')
76+
const ext = extname(file)
77+
78+
const issues = ext === '.vue'
79+
? analyzeVueFile(file, content)
80+
: analyzeTsFile(file, content)
81+
82+
if (issues.length > 0) {
83+
const relPath = relative(process.cwd(), file)
84+
console.log(`\n📄 ${relPath}`)
85+
for (const issue of issues) {
86+
const icon = issue.fixable ? '🔧' : '❌'
87+
const loc = issue.line ? `:${issue.line}` : ''
88+
console.log(` ${icon} [${issue.rule}]${loc} ${issue.message}`)
89+
if (issue.fix && SHOULD_FIX) {
90+
console.log(` 💡 ${issue.fix}`)
91+
}
92+
total++
93+
if (issue.fixable) fixable++
94+
}
8095
}
8196
}
8297

83-
console.log(`\n📊 总计: ${total} 个问题`)
84-
process.exit(total > 0 ? 1 : 0)
98+
if (total === 0) {
99+
console.log('✅ 所有 AST 检查通过')
100+
process.exit(0)
101+
} else {
102+
console.log(`\n📊 总计: ${total} 个问题,${fixable} 个可修复`)
103+
process.exit(1)
104+
}
85105
}
86106

87107
main()

ast/rules/import-restrictions.ts

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,65 @@
1-
/**
2-
* 导入限制规则
3-
* TODO: 实现 import 语句的 AST 检查
4-
*/
5-
6-
export interface ImportIssue {
7-
rule: string
8-
message: string
9-
line?: number
10-
fixable: boolean
11-
}
1+
import type { AstIssue } from '../analyzer'
2+
3+
const FORBIDDEN_IMPORTS = [
4+
'lucide-react',
5+
'element-plus',
6+
'vuetify',
7+
'@element-plus',
8+
]
9+
10+
export function checkImports(content: string, filePath: string): AstIssue[] {
11+
const issues: AstIssue[] = []
12+
const lines = content.split('\n')
13+
14+
// 匹配 import 语句
15+
const importRegex = /import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+)?['"]([^'"]+)['"];?/g
16+
17+
let match: RegExpExecArray | null
18+
while ((match = importRegex.exec(content)) !== null) {
19+
const moduleName = match[1]
20+
const lineIndex = content.slice(0, match.index).split('\n').length
21+
22+
// 检查黑名单
23+
for (const forbidden of FORBIDDEN_IMPORTS) {
24+
if (moduleName === forbidden || moduleName.startsWith(forbidden + '/')) {
25+
issues.push({
26+
file: filePath,
27+
rule: 'import/forbidden',
28+
message: `禁止导入 "${moduleName}",此库不在项目白名单中`,
29+
line: lineIndex,
30+
fixable: true,
31+
fix: `移除 import,改用允许的方案替代`,
32+
})
33+
}
34+
}
35+
36+
// 检查图标库来源
37+
if (moduleName.includes('lucide') && moduleName !== 'lucide-vue-next') {
38+
issues.push({
39+
file: filePath,
40+
rule: 'import/wrong-icon-lib',
41+
message: `图标库必须使用 "lucide-vue-next",当前导入 "${moduleName}"`,
42+
line: lineIndex,
43+
fixable: true,
44+
fix: `改为: import { Icon } from 'lucide-vue-next'`,
45+
})
46+
}
47+
48+
// 检查深层相对路径
49+
if (moduleName.startsWith('..')) {
50+
const depth = (moduleName.match(/\.\.\//g) || []).length
51+
if (depth > 2) {
52+
issues.push({
53+
file: filePath,
54+
rule: 'import/deep-relative',
55+
message: `相对路径导入层级过深 (${depth} 层): "${moduleName}",建议使用路径别名`,
56+
line: lineIndex,
57+
fixable: true,
58+
fix: '在 vite.config.ts 中配置路径别名,如 @/components/xxx',
59+
})
60+
}
61+
}
62+
}
1263

13-
export function checkImports(content: string, filePath: string): ImportIssue[] {
14-
const issues: ImportIssue[] = []
15-
// TODO: 检查禁止导入的库,图标库来源,深层相对路径等
1664
return issues
1765
}

ast/rules/style-constraints.ts

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,57 @@
1-
/**
2-
* 样式约束规则
3-
* TODO: 实现 <style> 节点和内联 style 属性的检查
4-
*/
1+
import type { AstIssue } from '../analyzer'
52

6-
import type { SFCDescriptor } from '@vue/compiler-sfc'
3+
export function checkStyle(content: string, filePath: string): AstIssue[] {
4+
const issues: AstIssue[] = []
5+
const lines = content.split('\n')
76

8-
export interface StyleIssue {
9-
rule: string
10-
message: string
11-
line?: number
12-
fixable: boolean
13-
}
7+
// 1. 检查 <style> 块中的原始 CSS(非 @apply)
8+
const styleMatches = content.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/g)
9+
for (const styleMatch of styleMatches) {
10+
const styleContent = styleMatch[1]
11+
const styleStartIdx = content.indexOf(styleMatch[0])
12+
const lineIndex = content.slice(0, styleStartIdx).split('\n').length
13+
14+
// 检查是否有原始 CSS 属性(key: value; 格式,排除 @apply 和 CSS 变量)
15+
const rawCssMatches = styleContent.matchAll(/([a-z-]+)\s*:\s*([^;{}]+);/g)
16+
for (const cssMatch of rawCssMatches) {
17+
const prop = cssMatch[1].trim()
18+
const value = cssMatch[2].trim()
19+
// 排除 @apply 和 CSS 变量
20+
if (prop !== '@apply' && !prop.startsWith('--') && !value.startsWith('var(')) {
21+
issues.push({
22+
file: filePath,
23+
rule: 'style/raw-css-detected',
24+
message: `<style> 中包含原始 CSS: "${prop}: ${value}",项目规范要求用 Tailwind`,
25+
line: lineIndex,
26+
fixable: true,
27+
fix: `将 "${prop}: ${value}" 替换为等效 Tailwind class`,
28+
})
29+
// 只报第一个,避免刷屏
30+
break
31+
}
32+
}
33+
}
34+
35+
// 2. 检查模板中的内联 style 属性
36+
const templateMatch = content.match(/<template>([\s\S]*?)<\/template>/)
37+
if (templateMatch) {
38+
const templateContent = templateMatch[1]
39+
const styleAttrRegex = /\sstyle\s*=\s*["']([^"']*)["']/g
40+
let attrMatch: RegExpExecArray | null
41+
while ((attrMatch = styleAttrRegex.exec(templateContent)) !== null) {
42+
const templateStart = content.indexOf('<template>')
43+
const attrPos = templateStart + templateContent.indexOf(attrMatch[0])
44+
const lineIndex = content.slice(0, attrPos).split('\n').length
45+
issues.push({
46+
file: filePath,
47+
rule: 'style/inline-style-attr',
48+
message: '模板中使用了内联 style 属性,必须使用 Tailwind class',
49+
line: lineIndex,
50+
fixable: true,
51+
fix: '改为 Tailwind 原子类,如 class="bg-gray-800 text-white"',
52+
})
53+
}
54+
}
1455

15-
export function checkStyle(descriptor: SFCDescriptor): StyleIssue[] {
16-
const issues: StyleIssue[] = []
17-
// TODO: 检查 <style> 中的原始 CSS,模板中的内联 style 属性
1856
return issues
1957
}

0 commit comments

Comments
 (0)