@eduardbar/drift 0.9.1 → 1.0.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.
Files changed (68) hide show
  1. package/.github/workflows/publish-vscode.yml +76 -0
  2. package/AGENTS.md +30 -12
  3. package/README.md +1 -1
  4. package/ROADMAP.md +130 -98
  5. package/dist/analyzer.d.ts +4 -38
  6. package/dist/analyzer.js +85 -1543
  7. package/dist/cli.js +47 -4
  8. package/dist/config.js +1 -1
  9. package/dist/fix.d.ts +13 -0
  10. package/dist/fix.js +120 -0
  11. package/dist/git/blame.d.ts +22 -0
  12. package/dist/git/blame.js +227 -0
  13. package/dist/git/helpers.d.ts +36 -0
  14. package/dist/git/helpers.js +152 -0
  15. package/dist/git/trend.d.ts +21 -0
  16. package/dist/git/trend.js +80 -0
  17. package/dist/git.d.ts +0 -4
  18. package/dist/git.js +2 -2
  19. package/dist/report.js +620 -293
  20. package/dist/rules/phase0-basic.d.ts +11 -0
  21. package/dist/rules/phase0-basic.js +176 -0
  22. package/dist/rules/phase1-complexity.d.ts +31 -0
  23. package/dist/rules/phase1-complexity.js +277 -0
  24. package/dist/rules/phase2-crossfile.d.ts +27 -0
  25. package/dist/rules/phase2-crossfile.js +122 -0
  26. package/dist/rules/phase3-arch.d.ts +31 -0
  27. package/dist/rules/phase3-arch.js +148 -0
  28. package/dist/rules/phase5-ai.d.ts +8 -0
  29. package/dist/rules/phase5-ai.js +262 -0
  30. package/dist/rules/phase8-semantic.d.ts +22 -0
  31. package/dist/rules/phase8-semantic.js +109 -0
  32. package/dist/rules/shared.d.ts +7 -0
  33. package/dist/rules/shared.js +27 -0
  34. package/package.json +8 -3
  35. package/packages/vscode-drift/.vscodeignore +9 -0
  36. package/packages/vscode-drift/LICENSE +21 -0
  37. package/packages/vscode-drift/README.md +64 -0
  38. package/packages/vscode-drift/images/icon.png +0 -0
  39. package/packages/vscode-drift/images/icon.svg +30 -0
  40. package/packages/vscode-drift/package-lock.json +485 -0
  41. package/packages/vscode-drift/package.json +119 -0
  42. package/packages/vscode-drift/src/analyzer.ts +38 -0
  43. package/packages/vscode-drift/src/diagnostics.ts +55 -0
  44. package/packages/vscode-drift/src/extension.ts +111 -0
  45. package/packages/vscode-drift/src/statusbar.ts +47 -0
  46. package/packages/vscode-drift/src/treeview.ts +108 -0
  47. package/packages/vscode-drift/tsconfig.json +18 -0
  48. package/packages/vscode-drift/vscode-drift-0.1.0.vsix +0 -0
  49. package/packages/vscode-drift/vscode-drift-0.1.1.vsix +0 -0
  50. package/src/analyzer.ts +124 -1773
  51. package/src/cli.ts +53 -4
  52. package/src/config.ts +1 -1
  53. package/src/fix.ts +154 -0
  54. package/src/git/blame.ts +279 -0
  55. package/src/git/helpers.ts +198 -0
  56. package/src/git/trend.ts +116 -0
  57. package/src/git.ts +2 -2
  58. package/src/report.ts +631 -296
  59. package/src/rules/phase0-basic.ts +187 -0
  60. package/src/rules/phase1-complexity.ts +302 -0
  61. package/src/rules/phase2-crossfile.ts +149 -0
  62. package/src/rules/phase3-arch.ts +179 -0
  63. package/src/rules/phase5-ai.ts +292 -0
  64. package/src/rules/phase8-semantic.ts +132 -0
  65. package/src/rules/shared.ts +39 -0
  66. package/tests/helpers.ts +45 -0
  67. package/tests/rules.test.ts +1269 -0
  68. package/vitest.config.ts +15 -0
@@ -0,0 +1,119 @@
1
+ {
2
+ "name": "vscode-drift",
3
+ "displayName": "drift — Technical Debt Detector",
4
+ "description": "Detect technical debt in TypeScript & JavaScript. AST-based score 0–100: complexity, dead code, coupling, nesting and more.",
5
+ "version": "0.1.1",
6
+ "engines": {
7
+ "vscode": "^1.85.0"
8
+ },
9
+ "publisher": "eduardbar",
10
+ "license": "MIT",
11
+ "icon": "images/icon.png",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/eduardbar/drift"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/eduardbar/drift/issues"
18
+ },
19
+ "homepage": "https://github.com/eduardbar/drift#readme",
20
+ "qna": "https://github.com/eduardbar/drift/issues",
21
+ "categories": ["Linters", "Programming Languages"],
22
+ "keywords": [
23
+ "technical debt",
24
+ "debt",
25
+ "code quality",
26
+ "linter",
27
+ "typescript",
28
+ "javascript",
29
+ "complexity",
30
+ "dead code",
31
+ "coupling",
32
+ "nesting",
33
+ "score",
34
+ "refactor",
35
+ "analyzer",
36
+ "analysis",
37
+ "anti-patterns",
38
+ "ast",
39
+ "cyclomatic",
40
+ "any",
41
+ "console.log",
42
+ "catch",
43
+ "multi-root ready"
44
+ ],
45
+ "galleryBanner": {
46
+ "color": "#0a0a0f",
47
+ "theme": "dark"
48
+ },
49
+ "badges": [
50
+ {
51
+ "url": "https://img.shields.io/visual-studio-marketplace/v/eduardbar.vscode-drift?color=6366f1&label=version",
52
+ "href": "https://marketplace.visualstudio.com/items?itemName=eduardbar.vscode-drift",
53
+ "description": "Marketplace Version"
54
+ },
55
+ {
56
+ "url": "https://img.shields.io/visual-studio-marketplace/i/eduardbar.vscode-drift?color=8b5cf6&label=installs",
57
+ "href": "https://marketplace.visualstudio.com/items?itemName=eduardbar.vscode-drift",
58
+ "description": "Installs"
59
+ }
60
+ ],
61
+ "activationEvents": [
62
+ "onLanguage:typescript",
63
+ "onLanguage:typescriptreact",
64
+ "onLanguage:javascript",
65
+ "onLanguage:javascriptreact"
66
+ ],
67
+ "main": "./dist/extension.js",
68
+ "contributes": {
69
+ "views": {
70
+ "explorer": [
71
+ {
72
+ "id": "driftIssues",
73
+ "name": "Drift Issues",
74
+ "when": "workspaceFolderCount > 0"
75
+ }
76
+ ]
77
+ },
78
+ "commands": [
79
+ {
80
+ "command": "drift.scanWorkspace",
81
+ "title": "Drift: Scan Workspace"
82
+ },
83
+ {
84
+ "command": "drift.clearDiagnostics",
85
+ "title": "Drift: Clear Diagnostics"
86
+ }
87
+ ],
88
+ "configuration": {
89
+ "title": "Drift",
90
+ "properties": {
91
+ "drift.enable": {
92
+ "type": "boolean",
93
+ "default": true,
94
+ "description": "Enable drift analysis on save"
95
+ },
96
+ "drift.minSeverity": {
97
+ "type": "string",
98
+ "enum": ["error", "warning", "info"],
99
+ "default": "info",
100
+ "description": "Minimum severity level to show"
101
+ }
102
+ }
103
+ }
104
+ },
105
+ "scripts": {
106
+ "build": "tsc -p tsconfig.json",
107
+ "watch": "tsc -p tsconfig.json --watch",
108
+ "vscode:prepublish": "npm run build"
109
+ },
110
+ "devDependencies": {
111
+ "@types/vscode": "^1.85.0",
112
+ "@types/node": "^20.0.0",
113
+ "typescript": "^5.3.0"
114
+ },
115
+ "dependencies": {
116
+ "@eduardbar/drift": "*",
117
+ "ts-morph": "^23.0.0"
118
+ }
119
+ }
@@ -0,0 +1,38 @@
1
+ import { Project } from 'ts-morph'
2
+ import type { FileReport } from '@eduardbar/drift'
3
+
4
+ // Import dinámico para compatibilidad CommonJS -> ESM
5
+ // @eduardbar/drift es "type": "module" pero desde CommonJS
6
+ // se debe usar import() dinámico.
7
+ // _analyzeFile se tipea como `Function` para evitar el conflicto de
8
+ // instancias duplicadas de ts-morph (una en este paquete, otra en
9
+ // @eduardbar/drift/node_modules). En runtime son compatibles.
10
+ // eslint-disable-next-line @typescript-eslint/ban-types
11
+ let _analyzeFile: Function | null = null
12
+
13
+ async function getAnalyzeFile(): Promise<Function> {
14
+ if (!_analyzeFile) {
15
+ // drift es ES module, desde CommonJS usamos import() dinámico
16
+ const drift = await import('@eduardbar/drift')
17
+ _analyzeFile = drift.analyzeFile
18
+ }
19
+ return _analyzeFile!
20
+ }
21
+
22
+ export async function analyzeFilePath(filePath: string): Promise<FileReport | null> {
23
+ try {
24
+ const analyzeFile = await getAnalyzeFile()
25
+ const project = new Project({
26
+ compilerOptions: {
27
+ allowJs: true,
28
+ jsx: 1, // JsxEmit.Preserve
29
+ },
30
+ skipAddingFilesFromTsConfig: true,
31
+ })
32
+ const sourceFile = project.addSourceFileAtPath(filePath)
33
+ return analyzeFile(sourceFile) as FileReport
34
+ } catch (err) {
35
+ console.error('[drift] analyzeFilePath error:', err)
36
+ return null
37
+ }
38
+ }
@@ -0,0 +1,55 @@
1
+ import * as vscode from 'vscode'
2
+ import type { FileReport } from '@eduardbar/drift'
3
+
4
+ const SEVERITY_MAP: Record<string, vscode.DiagnosticSeverity> = {
5
+ error: vscode.DiagnosticSeverity.Error,
6
+ warning: vscode.DiagnosticSeverity.Warning,
7
+ info: vscode.DiagnosticSeverity.Information,
8
+ }
9
+
10
+ export class DriftDiagnosticsProvider {
11
+ private collection: vscode.DiagnosticCollection
12
+
13
+ constructor() {
14
+ this.collection = vscode.languages.createDiagnosticCollection('drift')
15
+ }
16
+
17
+ update(report: FileReport): void {
18
+ const uri = vscode.Uri.file(report.path)
19
+ const config = vscode.workspace.getConfiguration('drift')
20
+ const minSeverity = config.get<string>('minSeverity', 'info')
21
+
22
+ const severityOrder = ['error', 'warning', 'info']
23
+ const minIdx = severityOrder.indexOf(minSeverity)
24
+
25
+ const diagnostics: vscode.Diagnostic[] = report.issues
26
+ .filter(issue => severityOrder.indexOf(issue.severity) <= minIdx)
27
+ .map(issue => {
28
+ // line es 1-based en drift, VS Code usa 0-based
29
+ const line = Math.max(0, issue.line - 1)
30
+ const range = new vscode.Range(line, 0, line, Number.MAX_SAFE_INTEGER)
31
+ const diagnostic = new vscode.Diagnostic(
32
+ range,
33
+ `[drift/${issue.rule}] ${issue.message}`,
34
+ SEVERITY_MAP[issue.severity] ?? vscode.DiagnosticSeverity.Information
35
+ )
36
+ diagnostic.source = 'drift'
37
+ diagnostic.code = issue.rule
38
+ return diagnostic
39
+ })
40
+
41
+ this.collection.set(uri, diagnostics)
42
+ }
43
+
44
+ clear(uri?: vscode.Uri): void {
45
+ if (uri) {
46
+ this.collection.delete(uri)
47
+ } else {
48
+ this.collection.clear()
49
+ }
50
+ }
51
+
52
+ dispose(): void {
53
+ this.collection.dispose()
54
+ }
55
+ }
@@ -0,0 +1,111 @@
1
+ import * as vscode from 'vscode'
2
+ import { analyzeFilePath } from './analyzer'
3
+ import { DriftDiagnosticsProvider } from './diagnostics'
4
+ import { DriftTreeProvider } from './treeview'
5
+ import { DriftStatusBarItem } from './statusbar'
6
+ import type { FileReport } from '@eduardbar/drift'
7
+
8
+ const SUPPORTED_LANGUAGES = ['typescript', 'typescriptreact', 'javascript', 'javascriptreact']
9
+
10
+ export function activate(context: vscode.ExtensionContext): void {
11
+ const diagnostics = new DriftDiagnosticsProvider()
12
+ const treeProvider = new DriftTreeProvider()
13
+ const statusBar = new DriftStatusBarItem()
14
+
15
+ // Registrar TreeView
16
+ const treeView = vscode.window.createTreeView('driftIssues', {
17
+ treeDataProvider: treeProvider,
18
+ showCollapseAll: true,
19
+ })
20
+
21
+ // Cache de reports para la status bar
22
+ const reportCache = new Map<string, FileReport>()
23
+
24
+ async function analyzeAndUpdate(document: vscode.TextDocument): Promise<void> {
25
+ const config = vscode.workspace.getConfiguration('drift')
26
+ if (!config.get<boolean>('enable', true)) return
27
+
28
+ if (!SUPPORTED_LANGUAGES.includes(document.languageId)) return
29
+ if (document.uri.scheme !== 'file') return
30
+
31
+ const filePath = document.uri.fsPath
32
+
33
+ const report = await analyzeFilePath(filePath)
34
+ if (!report) return
35
+
36
+ diagnostics.update(report)
37
+ treeProvider.updateFile(report)
38
+ reportCache.set(filePath, report)
39
+ statusBar.update(Array.from(reportCache.values()))
40
+ }
41
+
42
+ // Trigger: al guardar
43
+ const onSave = vscode.workspace.onDidSaveTextDocument(analyzeAndUpdate)
44
+
45
+ // Comando: scan workspace
46
+ const scanCmd = vscode.commands.registerCommand('drift.scanWorkspace', async () => {
47
+ const files = await vscode.workspace.findFiles(
48
+ '**/*.{ts,tsx,js,jsx}',
49
+ '**/node_modules/**'
50
+ )
51
+
52
+ vscode.window.withProgress(
53
+ {
54
+ location: vscode.ProgressLocation.Notification,
55
+ title: 'drift: Scanning workspace...',
56
+ cancellable: false,
57
+ },
58
+ async (progress) => {
59
+ const total = files.length
60
+ let done = 0
61
+
62
+ for (const file of files) {
63
+ const report = await analyzeFilePath(file.fsPath)
64
+ if (report) {
65
+ diagnostics.update(report)
66
+ treeProvider.updateFile(report)
67
+ reportCache.set(file.fsPath, report)
68
+ }
69
+ done++
70
+ progress.report({ increment: (done / total) * 100 })
71
+ }
72
+
73
+ statusBar.update(Array.from(reportCache.values()))
74
+ vscode.window.showInformationMessage(`drift: ${total} files scanned.`)
75
+ }
76
+ )
77
+ })
78
+
79
+ // Comando: clear
80
+ const clearCmd = vscode.commands.registerCommand('drift.clearDiagnostics', () => {
81
+ diagnostics.clear()
82
+ treeProvider.clearAll()
83
+ reportCache.clear()
84
+ statusBar.update([])
85
+ })
86
+
87
+ // Comando: go to issue (desde TreeView click)
88
+ const goToCmd = vscode.commands.registerCommand(
89
+ 'drift.goToIssue',
90
+ async (filePath: string, line: number) => {
91
+ const uri = vscode.Uri.file(filePath)
92
+ const doc = await vscode.workspace.openTextDocument(uri)
93
+ const editor = await vscode.window.showTextDocument(doc)
94
+ const pos = new vscode.Position(Math.max(0, line - 1), 0)
95
+ editor.selection = new vscode.Selection(pos, pos)
96
+ editor.revealRange(new vscode.Range(pos, pos), vscode.TextEditorRevealType.InCenter)
97
+ }
98
+ )
99
+
100
+ context.subscriptions.push(
101
+ { dispose: () => diagnostics.dispose() },
102
+ { dispose: () => statusBar.dispose() },
103
+ treeView,
104
+ onSave,
105
+ scanCmd,
106
+ clearCmd,
107
+ goToCmd,
108
+ )
109
+ }
110
+
111
+ export function deactivate(): void {}
@@ -0,0 +1,47 @@
1
+ import * as vscode from 'vscode'
2
+ import type { FileReport } from '@eduardbar/drift'
3
+
4
+ export class DriftStatusBarItem {
5
+ private item: vscode.StatusBarItem
6
+
7
+ constructor() {
8
+ this.item = vscode.window.createStatusBarItem(
9
+ vscode.StatusBarAlignment.Right,
10
+ 100
11
+ )
12
+ this.item.command = 'drift.scanWorkspace'
13
+ this.item.tooltip = 'Click to scan workspace'
14
+ }
15
+
16
+ update(reports: FileReport[]): void {
17
+ if (reports.length === 0) {
18
+ this.item.text = '$(check) drift'
19
+ this.item.backgroundColor = undefined
20
+ this.item.show()
21
+ return
22
+ }
23
+
24
+ const totalScore = Math.round(
25
+ reports.reduce((sum, r) => sum + r.score, 0) / reports.length
26
+ )
27
+ const totalIssues = reports.reduce((sum, r) => sum + r.issues.length, 0)
28
+ const hasErrors = reports.some(r => r.issues.some(i => i.severity === 'error'))
29
+
30
+ const icon = hasErrors ? '$(error)' : totalScore < 50 ? '$(warning)' : '$(check)'
31
+ this.item.text = `${icon} drift ${totalScore}/100 · ${totalIssues} issues`
32
+
33
+ if (hasErrors || totalScore < 30) {
34
+ this.item.backgroundColor = new vscode.ThemeColor('statusBarItem.errorBackground')
35
+ } else if (totalScore < 60) {
36
+ this.item.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground')
37
+ } else {
38
+ this.item.backgroundColor = undefined
39
+ }
40
+
41
+ this.item.show()
42
+ }
43
+
44
+ dispose(): void {
45
+ this.item.dispose()
46
+ }
47
+ }
@@ -0,0 +1,108 @@
1
+ import * as vscode from 'vscode'
2
+ import * as path from 'path'
3
+ import type { FileReport } from '@eduardbar/drift'
4
+
5
+ type TreeItemType = 'file' | 'issue'
6
+
7
+ export class DriftTreeItem extends vscode.TreeItem {
8
+ constructor(
9
+ public readonly label: string,
10
+ public readonly collapsibleState: vscode.TreeItemCollapsibleState,
11
+ public readonly itemType: TreeItemType,
12
+ public readonly fileReport?: FileReport,
13
+ public readonly issueIndex?: number
14
+ ) {
15
+ super(label, collapsibleState)
16
+
17
+ if (itemType === 'file' && fileReport) {
18
+ const score = fileReport.score
19
+ const issueCount = fileReport.issues.length
20
+ this.description = `score: ${score} • ${issueCount} issue${issueCount !== 1 ? 's' : ''}`
21
+ this.tooltip = fileReport.path
22
+ this.iconPath = score >= 70
23
+ ? new vscode.ThemeIcon('check', new vscode.ThemeColor('testing.iconPassed'))
24
+ : score >= 40
25
+ ? new vscode.ThemeIcon('warning', new vscode.ThemeColor('problemsWarningIcon.foreground'))
26
+ : new vscode.ThemeIcon('error', new vscode.ThemeColor('problemsErrorIcon.foreground'))
27
+ this.contextValue = 'driftFile'
28
+ }
29
+
30
+ if (itemType === 'issue' && fileReport && issueIndex !== undefined) {
31
+ const issue = fileReport.issues[issueIndex]
32
+ this.description = `line ${issue.line}`
33
+ this.tooltip = issue.message
34
+ this.iconPath = issue.severity === 'error'
35
+ ? new vscode.ThemeIcon('circle-filled', new vscode.ThemeColor('problemsErrorIcon.foreground'))
36
+ : issue.severity === 'warning'
37
+ ? new vscode.ThemeIcon('warning', new vscode.ThemeColor('problemsWarningIcon.foreground'))
38
+ : new vscode.ThemeIcon('info', new vscode.ThemeColor('problemsInfoIcon.foreground'))
39
+
40
+ // Click para ir a la línea
41
+ this.command = {
42
+ command: 'drift.goToIssue',
43
+ title: 'Go to Issue',
44
+ arguments: [fileReport.path, issue.line],
45
+ }
46
+ this.contextValue = 'driftIssue'
47
+ }
48
+ }
49
+ }
50
+
51
+ export class DriftTreeProvider implements vscode.TreeDataProvider<DriftTreeItem> {
52
+ private _onDidChangeTreeData = new vscode.EventEmitter<DriftTreeItem | undefined | null | void>()
53
+ readonly onDidChangeTreeData = this._onDidChangeTreeData.event
54
+
55
+ private reports = new Map<string, FileReport>()
56
+
57
+ refresh(): void {
58
+ this._onDidChangeTreeData.fire()
59
+ }
60
+
61
+ updateFile(report: FileReport): void {
62
+ this.reports.set(report.path, report)
63
+ this.refresh()
64
+ }
65
+
66
+ clearFile(filePath: string): void {
67
+ this.reports.delete(filePath)
68
+ this.refresh()
69
+ }
70
+
71
+ clearAll(): void {
72
+ this.reports.clear()
73
+ this.refresh()
74
+ }
75
+
76
+ getTreeItem(element: DriftTreeItem): vscode.TreeItem {
77
+ return element
78
+ }
79
+
80
+ getChildren(element?: DriftTreeItem): DriftTreeItem[] {
81
+ if (!element) {
82
+ // Root: lista de archivos con issues, ordenados por score ascendente
83
+ return Array.from(this.reports.values())
84
+ .filter(r => r.issues.length > 0)
85
+ .sort((a, b) => a.score - b.score)
86
+ .map(r => new DriftTreeItem(
87
+ path.basename(r.path),
88
+ vscode.TreeItemCollapsibleState.Collapsed,
89
+ 'file',
90
+ r
91
+ ))
92
+ }
93
+
94
+ if (element.itemType === 'file' && element.fileReport) {
95
+ return element.fileReport.issues.map((_, i) =>
96
+ new DriftTreeItem(
97
+ element.fileReport!.issues[i].rule,
98
+ vscode.TreeItemCollapsibleState.None,
99
+ 'issue',
100
+ element.fileReport,
101
+ i
102
+ )
103
+ )
104
+ }
105
+
106
+ return []
107
+ }
108
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "commonjs",
5
+ "moduleResolution": "node",
6
+ "lib": ["ES2022"],
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "allowSyntheticDefaultImports": true,
13
+ "declaration": true,
14
+ "sourceMap": true
15
+ },
16
+ "include": ["src/**/*"],
17
+ "exclude": ["node_modules", "dist"]
18
+ }