@eduardbar/drift 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gga +50 -0
- package/.github/actions/drift-review/README.md +60 -0
- package/.github/actions/drift-review/action.yml +131 -0
- package/.github/actions/drift-scan/README.md +28 -32
- package/.github/actions/drift-scan/action.yml +78 -14
- package/.github/workflows/publish-vscode.yml +3 -3
- package/.github/workflows/publish.yml +3 -3
- package/.github/workflows/review-pr.yml +94 -9
- package/AGENTS.md +75 -245
- package/CHANGELOG.md +28 -0
- package/README.md +308 -51
- package/ROADMAP.md +6 -5
- package/dist/analyzer.d.ts +2 -2
- package/dist/analyzer.js +420 -159
- package/dist/benchmark.d.ts +2 -0
- package/dist/benchmark.js +204 -0
- package/dist/cli.js +693 -67
- package/dist/config.js +16 -2
- package/dist/diff.js +66 -10
- package/dist/doctor.d.ts +5 -0
- package/dist/doctor.js +133 -0
- package/dist/format.d.ts +17 -0
- package/dist/format.js +45 -0
- package/dist/git.js +12 -0
- package/dist/guard-types.d.ts +57 -0
- package/dist/guard-types.js +2 -0
- package/dist/guard.d.ts +14 -0
- package/dist/guard.js +239 -0
- package/dist/index.d.ts +12 -3
- package/dist/index.js +6 -1
- package/dist/init.d.ts +15 -0
- package/dist/init.js +273 -0
- package/dist/map-cycles.d.ts +2 -0
- package/dist/map-cycles.js +34 -0
- package/dist/map-svg.d.ts +19 -0
- package/dist/map-svg.js +97 -0
- package/dist/map.js +78 -138
- package/dist/metrics.js +70 -55
- package/dist/output-metadata.d.ts +13 -0
- package/dist/output-metadata.js +17 -0
- package/dist/plugins-capabilities.d.ts +4 -0
- package/dist/plugins-capabilities.js +21 -0
- package/dist/plugins-messages.d.ts +10 -0
- package/dist/plugins-messages.js +16 -0
- package/dist/plugins-rules.d.ts +9 -0
- package/dist/plugins-rules.js +137 -0
- package/dist/plugins.d.ts +2 -1
- package/dist/plugins.js +80 -28
- package/dist/printer.js +4 -0
- package/dist/reporter-constants.d.ts +16 -0
- package/dist/reporter-constants.js +39 -0
- package/dist/reporter.d.ts +3 -3
- package/dist/reporter.js +35 -55
- package/dist/review.d.ts +2 -1
- package/dist/review.js +4 -3
- package/dist/rules/comments.js +2 -2
- package/dist/rules/complexity.js +2 -7
- package/dist/rules/nesting.js +3 -13
- package/dist/rules/phase0-basic.js +10 -10
- package/dist/rules/phase3-configurable.js +23 -15
- package/dist/rules/shared.d.ts +2 -0
- package/dist/rules/shared.js +27 -3
- package/dist/saas/constants.d.ts +15 -0
- package/dist/saas/constants.js +48 -0
- package/dist/saas/dashboard.d.ts +8 -0
- package/dist/saas/dashboard.js +132 -0
- package/dist/saas/errors.d.ts +19 -0
- package/dist/saas/errors.js +37 -0
- package/dist/saas/helpers.d.ts +21 -0
- package/dist/saas/helpers.js +110 -0
- package/dist/saas/ingest.d.ts +3 -0
- package/dist/saas/ingest.js +249 -0
- package/dist/saas/organization.d.ts +5 -0
- package/dist/saas/organization.js +82 -0
- package/dist/saas/plan-change.d.ts +10 -0
- package/dist/saas/plan-change.js +15 -0
- package/dist/saas/store.d.ts +21 -0
- package/dist/saas/store.js +159 -0
- package/dist/saas/types.d.ts +191 -0
- package/dist/saas/types.js +2 -0
- package/dist/saas.d.ts +8 -82
- package/dist/saas.js +7 -320
- package/dist/sarif.d.ts +74 -0
- package/dist/sarif.js +122 -0
- package/dist/trust-advanced.d.ts +14 -0
- package/dist/trust-advanced.js +65 -0
- package/dist/trust-kpi-fs.d.ts +3 -0
- package/dist/trust-kpi-fs.js +141 -0
- package/dist/trust-kpi-parse.d.ts +7 -0
- package/dist/trust-kpi-parse.js +186 -0
- package/dist/trust-kpi-types.d.ts +16 -0
- package/dist/trust-kpi-types.js +2 -0
- package/dist/trust-kpi.d.ts +7 -0
- package/dist/trust-kpi.js +185 -0
- package/dist/trust-policy.d.ts +32 -0
- package/dist/trust-policy.js +160 -0
- package/dist/trust-render.d.ts +9 -0
- package/dist/trust-render.js +54 -0
- package/dist/trust-scoring.d.ts +9 -0
- package/dist/trust-scoring.js +208 -0
- package/dist/trust.d.ts +37 -0
- package/dist/trust.js +168 -0
- package/dist/types/app.d.ts +30 -0
- package/dist/types/app.js +2 -0
- package/dist/types/config.d.ts +25 -0
- package/dist/types/config.js +2 -0
- package/dist/types/core.d.ts +100 -0
- package/dist/types/core.js +2 -0
- package/dist/types/diff.d.ts +55 -0
- package/dist/types/diff.js +2 -0
- package/dist/types/plugin.d.ts +41 -0
- package/dist/types/plugin.js +2 -0
- package/dist/types/trust.d.ts +120 -0
- package/dist/types/trust.js +2 -0
- package/dist/types.d.ts +8 -211
- package/docs/PRD.md +187 -109
- package/docs/plugin-contract.md +61 -0
- package/docs/release-notes-draft.md +40 -0
- package/docs/rules-catalog.md +49 -0
- package/docs/trust-core-release-checklist.md +87 -0
- package/package.json +6 -3
- package/packages/vscode-drift/src/code-actions.ts +1 -1
- package/schemas/drift-ai-output.v1.json +162 -0
- package/schemas/drift-report.v1.json +151 -0
- package/schemas/drift-trust.v1.json +131 -0
- package/scripts/smoke-repo.mjs +394 -0
- package/src/analyzer.ts +484 -155
- package/src/benchmark.ts +266 -0
- package/src/cli.ts +840 -85
- package/src/config.ts +19 -2
- package/src/diff.ts +84 -10
- package/src/doctor.ts +173 -0
- package/src/format.ts +81 -0
- package/src/git.ts +16 -0
- package/src/guard-types.ts +64 -0
- package/src/guard.ts +324 -0
- package/src/index.ts +83 -0
- package/src/init.ts +298 -0
- package/src/map-cycles.ts +38 -0
- package/src/map-svg.ts +124 -0
- package/src/map.ts +111 -142
- package/src/metrics.ts +78 -59
- package/src/output-metadata.ts +30 -0
- package/src/plugins-capabilities.ts +36 -0
- package/src/plugins-messages.ts +35 -0
- package/src/plugins-rules.ts +296 -0
- package/src/plugins.ts +148 -27
- package/src/printer.ts +4 -0
- package/src/reporter-constants.ts +46 -0
- package/src/reporter.ts +64 -65
- package/src/review.ts +6 -4
- package/src/rules/comments.ts +2 -2
- package/src/rules/complexity.ts +2 -7
- package/src/rules/nesting.ts +3 -13
- package/src/rules/phase0-basic.ts +11 -12
- package/src/rules/phase3-configurable.ts +39 -26
- package/src/rules/shared.ts +31 -3
- package/src/saas/constants.ts +56 -0
- package/src/saas/dashboard.ts +172 -0
- package/src/saas/errors.ts +45 -0
- package/src/saas/helpers.ts +140 -0
- package/src/saas/ingest.ts +278 -0
- package/src/saas/organization.ts +99 -0
- package/src/saas/plan-change.ts +19 -0
- package/src/saas/store.ts +172 -0
- package/src/saas/types.ts +216 -0
- package/src/saas.ts +49 -433
- package/src/sarif.ts +232 -0
- package/src/trust-advanced.ts +99 -0
- package/src/trust-kpi-fs.ts +169 -0
- package/src/trust-kpi-parse.ts +219 -0
- package/src/trust-kpi-types.ts +19 -0
- package/src/trust-kpi.ts +210 -0
- package/src/trust-policy.ts +246 -0
- package/src/trust-render.ts +61 -0
- package/src/trust-scoring.ts +231 -0
- package/src/trust.ts +260 -0
- package/src/types/app.ts +30 -0
- package/src/types/config.ts +27 -0
- package/src/types/core.ts +105 -0
- package/src/types/diff.ts +61 -0
- package/src/types/plugin.ts +46 -0
- package/src/types/trust.ts +134 -0
- package/src/types.ts +78 -238
- package/tests/cli-sarif.test.ts +92 -0
- package/tests/diff.test.ts +124 -0
- package/tests/format.test.ts +157 -0
- package/tests/new-features.test.ts +80 -1
- package/tests/phase1-init-doctor-guard.test.ts +199 -0
- package/tests/plugins.test.ts +219 -0
- package/tests/rules.test.ts +23 -1
- package/tests/saas-foundation.test.ts +358 -1
- package/tests/sarif.test.ts +160 -0
- package/tests/trust-kpi.test.ts +147 -0
- package/tests/trust.test.ts +602 -0
package/src/reporter.ts
CHANGED
|
@@ -1,71 +1,60 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
FileReport,
|
|
3
|
+
DriftReport,
|
|
4
|
+
DriftIssue,
|
|
5
|
+
AIOutput,
|
|
6
|
+
AIIssue,
|
|
7
|
+
AIOutputJson,
|
|
8
|
+
DriftReportJson,
|
|
9
|
+
} from './types.js'
|
|
2
10
|
import { scoreToGradeText, severityIcon } from './utils.js'
|
|
3
11
|
import { computeRepoQuality, computeMaintenanceRisk } from './metrics.js'
|
|
12
|
+
import { OUTPUT_SCHEMA, withOutputMetadata } from './output-metadata.js'
|
|
13
|
+
import {
|
|
14
|
+
AI_CODE_SMELL_BOOST,
|
|
15
|
+
AI_LIKELIHOOD_THRESHOLD,
|
|
16
|
+
AI_SIGNAL_RULES,
|
|
17
|
+
AI_SMELL_SCORE_MULTIPLIER,
|
|
18
|
+
AI_SUSPECTED_LIMIT,
|
|
19
|
+
AI_TRIGGER_LIMIT,
|
|
20
|
+
EFFORT_ORDER,
|
|
21
|
+
FIX_SUGGESTIONS,
|
|
22
|
+
RULE_EFFORT,
|
|
23
|
+
SEVERITY_ORDER,
|
|
24
|
+
type DriftIssueWithFile,
|
|
25
|
+
} from './reporter-constants.js'
|
|
4
26
|
|
|
5
|
-
|
|
6
|
-
'large-file': 'Consider splitting this file into smaller modules with single responsibility',
|
|
7
|
-
'large-function': 'Extract logic into smaller functions with descriptive names',
|
|
8
|
-
'debug-leftover': 'Remove this console.log or replace with proper logging library',
|
|
9
|
-
'dead-code': 'Remove unused import to keep code clean',
|
|
10
|
-
'duplicate-function-name': 'Consolidate with existing function or rename to clarify different behavior',
|
|
11
|
-
'any-abuse': "Replace 'any' with proper type definition",
|
|
12
|
-
'catch-swallow': 'Add error handling or logging in catch block',
|
|
13
|
-
'no-return-type': 'Add explicit return type for better type safety',
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const RULE_EFFORT: Record<string, 'low' | 'medium' | 'high'> = {
|
|
17
|
-
'debug-leftover': 'low',
|
|
18
|
-
'dead-code': 'low',
|
|
19
|
-
'no-return-type': 'low',
|
|
20
|
-
'any-abuse': 'medium',
|
|
21
|
-
'catch-swallow': 'medium',
|
|
22
|
-
'large-file': 'high',
|
|
23
|
-
'large-function': 'high',
|
|
24
|
-
'duplicate-function-name': 'high',
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const SEVERITY_ORDER: Record<string, number> = { error: 0, warning: 1, info: 2 }
|
|
28
|
-
const EFFORT_ORDER: Record<string, number> = { low: 0, medium: 1, high: 2 }
|
|
29
|
-
const AI_SIGNAL_RULES = new Set([
|
|
30
|
-
'over-commented',
|
|
31
|
-
'hardcoded-config',
|
|
32
|
-
'inconsistent-error-handling',
|
|
33
|
-
'unnecessary-abstraction',
|
|
34
|
-
'naming-inconsistency',
|
|
35
|
-
'comment-contradiction',
|
|
36
|
-
'promise-style-mix',
|
|
37
|
-
'any-abuse',
|
|
38
|
-
'ai-code-smell',
|
|
39
|
-
])
|
|
40
|
-
|
|
41
|
-
export function buildReport(targetPath: string, files: FileReport[]): DriftReport {
|
|
42
|
-
const allIssues = files.flatMap((f) => f.issues)
|
|
27
|
+
function summarizeIssues(allIssues: DriftIssue[]): DriftReport['summary'] {
|
|
43
28
|
const byRule: Record<string, number> = {}
|
|
44
29
|
|
|
45
30
|
for (const issue of allIssues) {
|
|
46
31
|
byRule[issue.rule] = (byRule[issue.rule] ?? 0) + 1
|
|
47
32
|
}
|
|
48
33
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
34
|
+
return {
|
|
35
|
+
errors: allIssues.filter((issue) => issue.severity === 'error').length,
|
|
36
|
+
warnings: allIssues.filter((issue) => issue.severity === 'warning').length,
|
|
37
|
+
infos: allIssues.filter((issue) => issue.severity === 'info').length,
|
|
38
|
+
byRule,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
53
41
|
|
|
54
|
-
|
|
42
|
+
function calculateTotalScore(files: FileReport[]): number {
|
|
43
|
+
if (files.length === 0) return 0
|
|
44
|
+
return Math.round(files.reduce((sum, file) => sum + file.score, 0) / files.length)
|
|
45
|
+
}
|
|
55
46
|
|
|
56
|
-
|
|
47
|
+
function baseReportDefaults(summary: DriftReport['summary'], targetPath: string, files: FileReport[]): DriftReportJson {
|
|
48
|
+
const filesWithIssues = files.filter((file) => file.issues.length > 0).sort((a, b) => b.score - a.score)
|
|
49
|
+
|
|
50
|
+
const report: DriftReport = {
|
|
57
51
|
scannedAt: new Date().toISOString(),
|
|
58
52
|
targetPath,
|
|
59
|
-
files:
|
|
60
|
-
totalIssues:
|
|
61
|
-
totalScore,
|
|
53
|
+
files: filesWithIssues,
|
|
54
|
+
totalIssues: files.flatMap((file) => file.issues).length,
|
|
55
|
+
totalScore: calculateTotalScore(files),
|
|
62
56
|
totalFiles: files.length,
|
|
63
|
-
summary
|
|
64
|
-
errors: allIssues.filter((i) => i.severity === 'error').length,
|
|
65
|
-
warnings: allIssues.filter((i) => i.severity === 'warning').length,
|
|
66
|
-
infos: allIssues.filter((i) => i.severity === 'info').length,
|
|
67
|
-
byRule,
|
|
68
|
-
},
|
|
57
|
+
summary,
|
|
69
58
|
quality: {
|
|
70
59
|
overall: 100,
|
|
71
60
|
dimensions: {
|
|
@@ -87,6 +76,14 @@ export function buildReport(targetPath: string, files: FileReport[]): DriftRepor
|
|
|
87
76
|
},
|
|
88
77
|
}
|
|
89
78
|
|
|
79
|
+
return withOutputMetadata(report, OUTPUT_SCHEMA.report)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function buildReport(targetPath: string, files: FileReport[]): DriftReportJson {
|
|
83
|
+
const allIssues = files.flatMap((f) => f.issues)
|
|
84
|
+
const summary = summarizeIssues(allIssues)
|
|
85
|
+
const baseReport = baseReportDefaults(summary, targetPath, files)
|
|
86
|
+
|
|
90
87
|
baseReport.quality = computeRepoQuality(targetPath, files)
|
|
91
88
|
baseReport.maintenanceRisk = computeMaintenanceRisk(baseReport)
|
|
92
89
|
|
|
@@ -158,8 +155,8 @@ export function formatMarkdown(report: DriftReport): string {
|
|
|
158
155
|
return lines.join('\n')
|
|
159
156
|
}
|
|
160
157
|
|
|
161
|
-
function collectAllIssues(report: DriftReport):
|
|
162
|
-
const all:
|
|
158
|
+
function collectAllIssues(report: DriftReport): DriftIssueWithFile[] {
|
|
159
|
+
const all: DriftIssueWithFile[] = []
|
|
163
160
|
for (const file of report.files) {
|
|
164
161
|
for (const issue of file.issues) {
|
|
165
162
|
all.push({ file: file.path, issue })
|
|
@@ -168,7 +165,7 @@ function collectAllIssues(report: DriftReport): Array<{ file: string; issue: Dri
|
|
|
168
165
|
return all
|
|
169
166
|
}
|
|
170
167
|
|
|
171
|
-
function sortIssues(issues:
|
|
168
|
+
function sortIssues(issues: DriftIssueWithFile[]): DriftIssueWithFile[] {
|
|
172
169
|
return issues.sort((a, b) => {
|
|
173
170
|
const sevDiff = SEVERITY_ORDER[a.issue.severity] - SEVERITY_ORDER[b.issue.severity]
|
|
174
171
|
if (sevDiff !== 0) return sevDiff
|
|
@@ -178,7 +175,7 @@ function sortIssues(issues: Array<{ file: string; issue: DriftIssue }>): Array<{
|
|
|
178
175
|
})
|
|
179
176
|
}
|
|
180
177
|
|
|
181
|
-
function buildAIIssue(item:
|
|
178
|
+
function buildAIIssue(item: DriftIssueWithFile, rank: number): AIIssue {
|
|
182
179
|
return {
|
|
183
180
|
rank,
|
|
184
181
|
file: item.file,
|
|
@@ -209,12 +206,12 @@ function fileAILikelihood(fileIssues: DriftIssue[]): { score: number; triggers:
|
|
|
209
206
|
triggerCounts.set(issue.rule, (triggerCounts.get(issue.rule) ?? 0) + 1)
|
|
210
207
|
}
|
|
211
208
|
const triggerTotal = [...triggerCounts.values()].reduce((sum, count) => sum + count, 0)
|
|
212
|
-
const smellBoost = fileIssues.some((issue) => issue.rule === 'ai-code-smell') ?
|
|
209
|
+
const smellBoost = fileIssues.some((issue) => issue.rule === 'ai-code-smell') ? AI_CODE_SMELL_BOOST : 0
|
|
213
210
|
const ratioScore = Math.round((triggerTotal / Math.max(fileIssues.length, 1)) * 100)
|
|
214
211
|
const score = Math.max(0, Math.min(100, ratioScore + smellBoost))
|
|
215
212
|
const triggers = [...triggerCounts.entries()]
|
|
216
213
|
.sort((a, b) => b[1] - a[1])
|
|
217
|
-
.slice(0,
|
|
214
|
+
.slice(0, AI_TRIGGER_LIMIT)
|
|
218
215
|
.map(([rule]) => rule)
|
|
219
216
|
return { score, triggers }
|
|
220
217
|
}
|
|
@@ -233,7 +230,7 @@ function computeAILikelihood(report: DriftReport): {
|
|
|
233
230
|
triggers: likelihood.triggers,
|
|
234
231
|
}
|
|
235
232
|
})
|
|
236
|
-
.filter((entry) => entry.ai_likelihood >=
|
|
233
|
+
.filter((entry) => entry.ai_likelihood >= AI_LIKELIHOOD_THRESHOLD)
|
|
237
234
|
.sort((a, b) => b.ai_likelihood - a.ai_likelihood)
|
|
238
235
|
|
|
239
236
|
const overall = suspected.length === 0
|
|
@@ -241,16 +238,16 @@ function computeAILikelihood(report: DriftReport): {
|
|
|
241
238
|
: Math.round(suspected.reduce((sum, entry) => sum + entry.ai_likelihood, 0) / suspected.length)
|
|
242
239
|
|
|
243
240
|
const smellCount = report.files.flatMap((file) => file.issues).filter((issue) => issue.rule === 'ai-code-smell').length
|
|
244
|
-
const smellScore = Math.min(100, smellCount *
|
|
241
|
+
const smellScore = Math.min(100, smellCount * AI_SMELL_SCORE_MULTIPLIER)
|
|
245
242
|
|
|
246
243
|
return {
|
|
247
244
|
overall,
|
|
248
|
-
files: suspected.slice(0,
|
|
245
|
+
files: suspected.slice(0, AI_SUSPECTED_LIMIT),
|
|
249
246
|
smellScore,
|
|
250
247
|
}
|
|
251
248
|
}
|
|
252
249
|
|
|
253
|
-
export function formatAIOutput(report: DriftReport):
|
|
250
|
+
export function formatAIOutput(report: DriftReport): AIOutputJson {
|
|
254
251
|
const allIssues = collectAllIssues(report)
|
|
255
252
|
const sortedIssues = sortIssues(allIssues)
|
|
256
253
|
const priorityOrder = sortedIssues.map((item, i) => buildAIIssue(item, i + 1))
|
|
@@ -258,7 +255,7 @@ export function formatAIOutput(report: DriftReport): AIOutput {
|
|
|
258
255
|
const grade = scoreToGradeText(report.totalScore)
|
|
259
256
|
const aiLikelihood = computeAILikelihood(report)
|
|
260
257
|
|
|
261
|
-
|
|
258
|
+
const output: AIOutput = {
|
|
262
259
|
summary: {
|
|
263
260
|
score: report.totalScore,
|
|
264
261
|
grade: grade.label.toUpperCase(),
|
|
@@ -279,4 +276,6 @@ export function formatAIOutput(report: DriftReport): AIOutput {
|
|
|
279
276
|
recommended_action: buildRecommendedAction(priorityOrder),
|
|
280
277
|
},
|
|
281
278
|
}
|
|
279
|
+
|
|
280
|
+
return withOutputMetadata(output, OUTPUT_SCHEMA.ai)
|
|
282
281
|
}
|
package/src/review.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolve } from 'node:path'
|
|
1
|
+
import { relative, resolve } from 'node:path'
|
|
2
2
|
import { analyzeProject } from './analyzer.js'
|
|
3
3
|
import { loadConfig } from './config.js'
|
|
4
4
|
import { buildReport } from './reporter.js'
|
|
@@ -6,7 +6,7 @@ import { cleanupTempDir, extractFilesAtRef } from './git.js'
|
|
|
6
6
|
import { computeDiff } from './diff.js'
|
|
7
7
|
import type { DriftDiff } from './types.js'
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
interface DriftReview {
|
|
10
10
|
baseRef: string
|
|
11
11
|
scannedAt: string
|
|
12
12
|
totalDelta: number
|
|
@@ -18,10 +18,12 @@ export interface DriftReview {
|
|
|
18
18
|
diff: DriftDiff
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
const REVIEW_TOP_FILES_LIMIT = 8
|
|
22
|
+
|
|
21
23
|
export function formatReviewMarkdown(review: DriftReview): string {
|
|
22
24
|
const trendIcon = review.status === 'regressed' ? '⚠️' : review.status === 'improved' ? '✅' : 'ℹ️'
|
|
23
25
|
const topFiles = review.diff.files
|
|
24
|
-
.slice(0,
|
|
26
|
+
.slice(0, REVIEW_TOP_FILES_LIMIT)
|
|
25
27
|
.map((file) => {
|
|
26
28
|
const sign = file.scoreDelta > 0 ? '+' : ''
|
|
27
29
|
return `- \`${file.path}\`: ${file.scoreBefore} -> ${file.scoreAfter} (${sign}${file.scoreDelta}), +${file.newIssues.length} new / -${file.resolvedIssues.length} resolved`
|
|
@@ -66,7 +68,7 @@ export async function generateReview(projectPath: string, baseRef: string): Prom
|
|
|
66
68
|
...baseReport,
|
|
67
69
|
files: baseReport.files.map((file) => ({
|
|
68
70
|
...file,
|
|
69
|
-
path:
|
|
71
|
+
path: resolve(resolvedPath, relative(tempDir!, file.path)),
|
|
70
72
|
})),
|
|
71
73
|
}
|
|
72
74
|
|
package/src/rules/comments.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SourceFile } from 'ts-morph'
|
|
2
2
|
import type { DriftIssue } from '../types.js'
|
|
3
|
-
import { hasIgnoreComment } from './shared.js'
|
|
3
|
+
import { hasIgnoreComment, getFileLines } from './shared.js'
|
|
4
4
|
|
|
5
5
|
const TRIVIAL_COMMENT_PATTERNS = [
|
|
6
6
|
{ comment: /\/\/\s*return\b/i, code: /^\s*return\b/ },
|
|
@@ -41,7 +41,7 @@ function checkLineForContradiction(
|
|
|
41
41
|
|
|
42
42
|
export function detectCommentContradiction(file: SourceFile): DriftIssue[] {
|
|
43
43
|
const issues: DriftIssue[] = []
|
|
44
|
-
const lines = file
|
|
44
|
+
const lines = getFileLines(file)
|
|
45
45
|
|
|
46
46
|
for (let i = 0; i < lines.length - 1; i++) {
|
|
47
47
|
const commentLine = lines[i].trim()
|
package/src/rules/complexity.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SourceFile, SyntaxKind } from 'ts-morph'
|
|
2
2
|
import type { DriftIssue } from '../types.js'
|
|
3
|
-
import { hasIgnoreComment, getSnippet, type FunctionLike } from './shared.js'
|
|
3
|
+
import { hasIgnoreComment, getSnippet, collectFunctionLikes, type FunctionLike } from './shared.js'
|
|
4
4
|
|
|
5
5
|
const COMPLEXITY_THRESHOLD = 10
|
|
6
6
|
|
|
@@ -31,12 +31,7 @@ function getCyclomaticComplexity(fn: FunctionLike): number {
|
|
|
31
31
|
|
|
32
32
|
export function detectHighComplexity(file: SourceFile): DriftIssue[] {
|
|
33
33
|
const issues: DriftIssue[] = []
|
|
34
|
-
const fns: FunctionLike[] =
|
|
35
|
-
...file.getFunctions(),
|
|
36
|
-
...file.getDescendantsOfKind(SyntaxKind.ArrowFunction),
|
|
37
|
-
...file.getDescendantsOfKind(SyntaxKind.FunctionExpression),
|
|
38
|
-
...file.getClasses().flatMap((c) => c.getMethods()),
|
|
39
|
-
]
|
|
34
|
+
const fns: FunctionLike[] = collectFunctionLikes(file)
|
|
40
35
|
|
|
41
36
|
for (const fn of fns) {
|
|
42
37
|
const complexity = getCyclomaticComplexity(fn)
|
package/src/rules/nesting.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SourceFile, SyntaxKind, Node } from 'ts-morph'
|
|
2
2
|
import type { DriftIssue } from '../types.js'
|
|
3
|
-
import { hasIgnoreComment, getSnippet, type FunctionLike } from './shared.js'
|
|
3
|
+
import { hasIgnoreComment, getSnippet, collectFunctionLikes, type FunctionLike } from './shared.js'
|
|
4
4
|
|
|
5
5
|
const NESTING_THRESHOLD = 3
|
|
6
6
|
const PARAMS_THRESHOLD = 4
|
|
@@ -35,12 +35,7 @@ function getMaxNestingDepth(fn: FunctionLike): number {
|
|
|
35
35
|
|
|
36
36
|
export function detectDeepNesting(file: SourceFile): DriftIssue[] {
|
|
37
37
|
const issues: DriftIssue[] = []
|
|
38
|
-
const fns: FunctionLike[] =
|
|
39
|
-
...file.getFunctions(),
|
|
40
|
-
...file.getDescendantsOfKind(SyntaxKind.ArrowFunction),
|
|
41
|
-
...file.getDescendantsOfKind(SyntaxKind.FunctionExpression),
|
|
42
|
-
...file.getClasses().flatMap((c) => c.getMethods()),
|
|
43
|
-
]
|
|
38
|
+
const fns: FunctionLike[] = collectFunctionLikes(file)
|
|
44
39
|
|
|
45
40
|
for (const fn of fns) {
|
|
46
41
|
const depth = getMaxNestingDepth(fn)
|
|
@@ -62,12 +57,7 @@ export function detectDeepNesting(file: SourceFile): DriftIssue[] {
|
|
|
62
57
|
|
|
63
58
|
export function detectTooManyParams(file: SourceFile): DriftIssue[] {
|
|
64
59
|
const issues: DriftIssue[] = []
|
|
65
|
-
const fns: FunctionLike[] =
|
|
66
|
-
...file.getFunctions(),
|
|
67
|
-
...file.getDescendantsOfKind(SyntaxKind.ArrowFunction),
|
|
68
|
-
...file.getDescendantsOfKind(SyntaxKind.FunctionExpression),
|
|
69
|
-
...file.getClasses().flatMap((c) => c.getMethods()),
|
|
70
|
-
]
|
|
60
|
+
const fns: FunctionLike[] = collectFunctionLikes(file)
|
|
71
61
|
|
|
72
62
|
for (const fn of fns) {
|
|
73
63
|
const paramCount = fn.getParameters().length
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SourceFile, SyntaxKind } from 'ts-morph'
|
|
2
2
|
import type { DriftIssue } from '../types.js'
|
|
3
|
-
import { hasIgnoreComment, getSnippet, getFunctionLikeLines,
|
|
3
|
+
import { hasIgnoreComment, getSnippet, getFunctionLikeLines, collectFunctionLikes, getFileLines } from './shared.js'
|
|
4
4
|
|
|
5
5
|
const LARGE_FILE_THRESHOLD = 300
|
|
6
6
|
const LARGE_FUNCTION_THRESHOLD = 50
|
|
@@ -26,12 +26,7 @@ export function detectLargeFile(file: SourceFile): DriftIssue[] {
|
|
|
26
26
|
|
|
27
27
|
export function detectLargeFunctions(file: SourceFile): DriftIssue[] {
|
|
28
28
|
const issues: DriftIssue[] = []
|
|
29
|
-
const fns
|
|
30
|
-
...file.getFunctions(),
|
|
31
|
-
...file.getDescendantsOfKind(SyntaxKind.ArrowFunction),
|
|
32
|
-
...file.getDescendantsOfKind(SyntaxKind.FunctionExpression),
|
|
33
|
-
...file.getClasses().flatMap((c) => c.getMethods()),
|
|
34
|
-
]
|
|
29
|
+
const fns = collectFunctionLikes(file)
|
|
35
30
|
|
|
36
31
|
for (const fn of fns) {
|
|
37
32
|
const lines = getFunctionLikeLines(fn)
|
|
@@ -70,7 +65,7 @@ export function detectDebugLeftovers(file: SourceFile): DriftIssue[] {
|
|
|
70
65
|
}
|
|
71
66
|
}
|
|
72
67
|
|
|
73
|
-
const lines = file
|
|
68
|
+
const lines = getFileLines(file)
|
|
74
69
|
lines.forEach((lineContent, i) => {
|
|
75
70
|
if (/\/\/\s*(TODO|FIXME|HACK|XXX|TEMP)\b/i.test(lineContent)) {
|
|
76
71
|
if (hasIgnoreComment(file, i + 1)) return
|
|
@@ -90,14 +85,18 @@ export function detectDebugLeftovers(file: SourceFile): DriftIssue[] {
|
|
|
90
85
|
|
|
91
86
|
export function detectDeadCode(file: SourceFile): DriftIssue[] {
|
|
92
87
|
const issues: DriftIssue[] = []
|
|
88
|
+
const identifierCounts = new Map<string, number>()
|
|
89
|
+
|
|
90
|
+
for (const id of file.getDescendantsOfKind(SyntaxKind.Identifier)) {
|
|
91
|
+
const text = id.getText()
|
|
92
|
+
identifierCounts.set(text, (identifierCounts.get(text) ?? 0) + 1)
|
|
93
|
+
}
|
|
93
94
|
|
|
94
95
|
for (const imp of file.getImportDeclarations()) {
|
|
95
96
|
for (const named of imp.getNamedImports()) {
|
|
96
97
|
const name = named.getName()
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
)
|
|
100
|
-
if (refs.length === 0) {
|
|
98
|
+
const refsCount = Math.max(0, (identifierCounts.get(name) ?? 0) - 1)
|
|
99
|
+
if (refsCount === 0) {
|
|
101
100
|
issues.push({
|
|
102
101
|
rule: 'dead-code',
|
|
103
102
|
severity: 'warning',
|
|
@@ -23,12 +23,14 @@ const HTTP_IMPORT_PATTERNS = [
|
|
|
23
23
|
|
|
24
24
|
function isControllerFile(filePath: string): boolean {
|
|
25
25
|
const normalized = filePath.replace(/\\/g, '/').toLowerCase()
|
|
26
|
-
|
|
26
|
+
const segments = normalized.split('/')
|
|
27
|
+
return segments.includes('controller') || segments.includes('controllers') || normalized.endsWith('controller.ts') || normalized.endsWith('controller.js')
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
function isServiceFile(filePath: string): boolean {
|
|
30
31
|
const normalized = filePath.replace(/\\/g, '/').toLowerCase()
|
|
31
|
-
|
|
32
|
+
const segments = normalized.split('/')
|
|
33
|
+
return segments.includes('service') || segments.includes('services') || normalized.endsWith('service.ts') || normalized.endsWith('service.js')
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
function createIssue(rule: string, message: string, line: number, snippet: string): DriftIssue {
|
|
@@ -100,33 +102,44 @@ export function detectMaxFunctionLines(file: SourceFile, config?: DriftConfig):
|
|
|
100
102
|
|
|
101
103
|
const issues: DriftIssue[] = []
|
|
102
104
|
|
|
105
|
+
collectFunctionLineIssues(file, maxLines, issues)
|
|
106
|
+
collectMethodLineIssues(file, maxLines, issues)
|
|
107
|
+
|
|
108
|
+
return issues
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function countBodyLines(
|
|
112
|
+
body: ReturnType<import('ts-morph').FunctionDeclaration['getBody']> | ReturnType<import('ts-morph').MethodDeclaration['getBody']>,
|
|
113
|
+
): number {
|
|
114
|
+
if (!body) return 0
|
|
115
|
+
return body.getEndLineNumber() - body.getStartLineNumber() - 1
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function collectFunctionLineIssues(file: SourceFile, maxLines: number, issues: DriftIssue[]): void {
|
|
103
119
|
for (const fn of file.getFunctions()) {
|
|
104
|
-
const
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
120
|
+
const lines = countBodyLines(fn.getBody())
|
|
121
|
+
if (lines <= maxLines) continue
|
|
122
|
+
|
|
123
|
+
const functionName = fn.getName() ?? '(anonymous)'
|
|
124
|
+
issues.push(createIssue(
|
|
125
|
+
'max-function-lines',
|
|
126
|
+
`Function '${functionName}' has ${lines} lines (max: ${maxLines}).`,
|
|
127
|
+
fn.getStartLineNumber(),
|
|
128
|
+
functionName,
|
|
129
|
+
))
|
|
115
130
|
}
|
|
131
|
+
}
|
|
116
132
|
|
|
133
|
+
function collectMethodLineIssues(file: SourceFile, maxLines: number, issues: DriftIssue[]): void {
|
|
117
134
|
for (const method of file.getDescendantsOfKind(SyntaxKind.MethodDeclaration)) {
|
|
118
|
-
const
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
))
|
|
128
|
-
}
|
|
135
|
+
const lines = countBodyLines(method.getBody())
|
|
136
|
+
if (lines <= maxLines) continue
|
|
137
|
+
|
|
138
|
+
issues.push(createIssue(
|
|
139
|
+
'max-function-lines',
|
|
140
|
+
`Method '${method.getName()}' has ${lines} lines (max: ${maxLines}).`,
|
|
141
|
+
method.getStartLineNumber(),
|
|
142
|
+
method.getName(),
|
|
143
|
+
))
|
|
129
144
|
}
|
|
130
|
-
|
|
131
|
-
return issues
|
|
132
145
|
}
|
package/src/rules/shared.ts
CHANGED
|
@@ -5,12 +5,40 @@ import {
|
|
|
5
5
|
ArrowFunction,
|
|
6
6
|
FunctionExpression,
|
|
7
7
|
MethodDeclaration,
|
|
8
|
+
SyntaxKind,
|
|
8
9
|
} from 'ts-morph'
|
|
9
10
|
|
|
10
11
|
export type FunctionLike = FunctionDeclaration | ArrowFunction | FunctionExpression | MethodDeclaration
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
const fileLinesCache = new WeakMap<SourceFile, string[]>()
|
|
14
|
+
const functionLikesCache = new WeakMap<SourceFile, FunctionLike[]>()
|
|
15
|
+
|
|
16
|
+
export function getFileLines(file: SourceFile): string[] {
|
|
17
|
+
const cached = fileLinesCache.get(file)
|
|
18
|
+
if (cached) return cached
|
|
19
|
+
|
|
13
20
|
const lines = file.getFullText().split('\n')
|
|
21
|
+
fileLinesCache.set(file, lines)
|
|
22
|
+
return lines
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function collectFunctionLikes(file: SourceFile): FunctionLike[] {
|
|
26
|
+
const cached = functionLikesCache.get(file)
|
|
27
|
+
if (cached) return cached
|
|
28
|
+
|
|
29
|
+
const fns: FunctionLike[] = [
|
|
30
|
+
...file.getFunctions(),
|
|
31
|
+
...file.getDescendantsOfKind(SyntaxKind.ArrowFunction),
|
|
32
|
+
...file.getDescendantsOfKind(SyntaxKind.FunctionExpression),
|
|
33
|
+
...file.getClasses().flatMap((c) => c.getMethods()),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
functionLikesCache.set(file, fns)
|
|
37
|
+
return fns
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function hasIgnoreComment(file: SourceFile, line: number): boolean {
|
|
41
|
+
const lines = getFileLines(file)
|
|
14
42
|
const currentLine = lines[line - 1] ?? ''
|
|
15
43
|
const prevLine = lines[line - 2] ?? ''
|
|
16
44
|
|
|
@@ -20,13 +48,13 @@ export function hasIgnoreComment(file: SourceFile, line: number): boolean {
|
|
|
20
48
|
}
|
|
21
49
|
|
|
22
50
|
export function isFileIgnored(file: SourceFile): boolean {
|
|
23
|
-
const firstLines = file
|
|
51
|
+
const firstLines = getFileLines(file).slice(0, 10).join('\n') // drift-ignore
|
|
24
52
|
return /\/\/\s*drift-ignore-file\b/.test(firstLines)
|
|
25
53
|
}
|
|
26
54
|
|
|
27
55
|
export function getSnippet(node: Node, file: SourceFile): string {
|
|
28
56
|
const startLine = node.getStartLineNumber()
|
|
29
|
-
const lines = file
|
|
57
|
+
const lines = getFileLines(file)
|
|
30
58
|
return lines
|
|
31
59
|
.slice(Math.max(0, startLine - 1), startLine + 1)
|
|
32
60
|
.join('\n')
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { SaasOperation, SaasPlan, SaasPolicy, SaasRole } from './types.js'
|
|
2
|
+
|
|
3
|
+
export const STORE_VERSION = 3
|
|
4
|
+
export const ACTIVE_WINDOW_DAYS = 30
|
|
5
|
+
export const DEFAULT_ORGANIZATION_ID = 'default-org'
|
|
6
|
+
const HOURS_PER_DAY = 24
|
|
7
|
+
const MINUTES_PER_HOUR = 60
|
|
8
|
+
const SECONDS_PER_MINUTE = 60
|
|
9
|
+
const MILLISECONDS_PER_SECOND = 1000
|
|
10
|
+
const RANDOM_ID_RADIX = 16
|
|
11
|
+
const RANDOM_ID_START = 2
|
|
12
|
+
const RANDOM_ID_END = 10
|
|
13
|
+
|
|
14
|
+
export const DASHBOARD_REPO_LIMIT = 15
|
|
15
|
+
export const DASHBOARD_BAR_UNIT = 8
|
|
16
|
+
export const DASHBOARD_BAR_MIN_WIDTH = 8
|
|
17
|
+
|
|
18
|
+
export const VALID_ROLES: SaasRole[] = ['owner', 'member', 'viewer']
|
|
19
|
+
export const VALID_PLANS: SaasPlan[] = ['free', 'sponsor', 'team', 'business']
|
|
20
|
+
|
|
21
|
+
export const ROLE_PRIORITY: Record<SaasRole, number> = {
|
|
22
|
+
viewer: 1,
|
|
23
|
+
member: 2,
|
|
24
|
+
owner: 3,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const REQUIRED_ROLE_BY_OPERATION: Record<SaasOperation, SaasRole> = {
|
|
28
|
+
'snapshot:write': 'member',
|
|
29
|
+
'snapshot:read': 'viewer',
|
|
30
|
+
'summary:read': 'viewer',
|
|
31
|
+
'billing:write': 'owner',
|
|
32
|
+
'billing:read': 'viewer',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const DEFAULT_SAAS_POLICY: SaasPolicy = {
|
|
36
|
+
freeUserThreshold: 7500,
|
|
37
|
+
maxRunsPerWorkspacePerMonth: 500,
|
|
38
|
+
maxReposPerWorkspace: 20,
|
|
39
|
+
retentionDays: 90,
|
|
40
|
+
strictActorEnforcement: false,
|
|
41
|
+
maxWorkspacesPerOrganizationByPlan: {
|
|
42
|
+
free: 20,
|
|
43
|
+
sponsor: 50,
|
|
44
|
+
team: 200,
|
|
45
|
+
business: 1000,
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function daysAgo(days: number): number {
|
|
50
|
+
const now = Date.now()
|
|
51
|
+
return now - days * HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function createRandomId(prefix: string): string {
|
|
55
|
+
return `${prefix}-${Math.random().toString(RANDOM_ID_RADIX).slice(RANDOM_ID_START, RANDOM_ID_END)}`
|
|
56
|
+
}
|