@eduardbar/drift 0.9.1 → 1.1.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 (129) hide show
  1. package/.github/actions/drift-scan/README.md +61 -0
  2. package/.github/actions/drift-scan/action.yml +65 -0
  3. package/.github/workflows/publish-vscode.yml +78 -0
  4. package/AGENTS.md +83 -23
  5. package/README.md +69 -2
  6. package/ROADMAP.md +130 -98
  7. package/dist/analyzer.d.ts +8 -38
  8. package/dist/analyzer.js +181 -1526
  9. package/dist/badge.js +40 -22
  10. package/dist/ci.js +32 -18
  11. package/dist/cli.js +125 -4
  12. package/dist/config.js +1 -1
  13. package/dist/diff.d.ts +0 -7
  14. package/dist/diff.js +26 -25
  15. package/dist/fix.d.ts +17 -0
  16. package/dist/fix.js +132 -0
  17. package/dist/git/blame.d.ts +22 -0
  18. package/dist/git/blame.js +227 -0
  19. package/dist/git/helpers.d.ts +36 -0
  20. package/dist/git/helpers.js +152 -0
  21. package/dist/git/trend.d.ts +21 -0
  22. package/dist/git/trend.js +81 -0
  23. package/dist/git.d.ts +0 -13
  24. package/dist/git.js +27 -21
  25. package/dist/index.d.ts +5 -1
  26. package/dist/index.js +3 -0
  27. package/dist/map.d.ts +3 -0
  28. package/dist/map.js +103 -0
  29. package/dist/metrics.d.ts +4 -0
  30. package/dist/metrics.js +176 -0
  31. package/dist/plugins.d.ts +6 -0
  32. package/dist/plugins.js +74 -0
  33. package/dist/printer.js +20 -0
  34. package/dist/report.js +654 -293
  35. package/dist/reporter.js +85 -2
  36. package/dist/review.d.ts +15 -0
  37. package/dist/review.js +80 -0
  38. package/dist/rules/comments.d.ts +4 -0
  39. package/dist/rules/comments.js +45 -0
  40. package/dist/rules/complexity.d.ts +4 -0
  41. package/dist/rules/complexity.js +51 -0
  42. package/dist/rules/coupling.d.ts +4 -0
  43. package/dist/rules/coupling.js +19 -0
  44. package/dist/rules/magic.d.ts +4 -0
  45. package/dist/rules/magic.js +33 -0
  46. package/dist/rules/nesting.d.ts +5 -0
  47. package/dist/rules/nesting.js +82 -0
  48. package/dist/rules/phase0-basic.d.ts +11 -0
  49. package/dist/rules/phase0-basic.js +183 -0
  50. package/dist/rules/phase1-complexity.d.ts +7 -0
  51. package/dist/rules/phase1-complexity.js +8 -0
  52. package/dist/rules/phase2-crossfile.d.ts +23 -0
  53. package/dist/rules/phase2-crossfile.js +135 -0
  54. package/dist/rules/phase3-arch.d.ts +23 -0
  55. package/dist/rules/phase3-arch.js +151 -0
  56. package/dist/rules/phase3-configurable.d.ts +6 -0
  57. package/dist/rules/phase3-configurable.js +97 -0
  58. package/dist/rules/phase5-ai.d.ts +8 -0
  59. package/dist/rules/phase5-ai.js +262 -0
  60. package/dist/rules/phase8-semantic.d.ts +17 -0
  61. package/dist/rules/phase8-semantic.js +110 -0
  62. package/dist/rules/promise.d.ts +4 -0
  63. package/dist/rules/promise.js +24 -0
  64. package/dist/rules/shared.d.ts +7 -0
  65. package/dist/rules/shared.js +27 -0
  66. package/dist/snapshot.d.ts +19 -0
  67. package/dist/snapshot.js +119 -0
  68. package/dist/types.d.ts +69 -0
  69. package/dist/utils.d.ts +2 -1
  70. package/dist/utils.js +1 -0
  71. package/docs/AGENTS.md +146 -0
  72. package/docs/PRD.md +208 -0
  73. package/package.json +8 -3
  74. package/packages/eslint-plugin-drift/src/index.ts +1 -1
  75. package/packages/vscode-drift/.vscodeignore +9 -0
  76. package/packages/vscode-drift/LICENSE +21 -0
  77. package/packages/vscode-drift/README.md +64 -0
  78. package/packages/vscode-drift/images/icon.png +0 -0
  79. package/packages/vscode-drift/images/icon.svg +30 -0
  80. package/packages/vscode-drift/package-lock.json +485 -0
  81. package/packages/vscode-drift/package.json +119 -0
  82. package/packages/vscode-drift/src/analyzer.ts +40 -0
  83. package/packages/vscode-drift/src/diagnostics.ts +55 -0
  84. package/packages/vscode-drift/src/extension.ts +135 -0
  85. package/packages/vscode-drift/src/statusbar.ts +55 -0
  86. package/packages/vscode-drift/src/treeview.ts +110 -0
  87. package/packages/vscode-drift/tsconfig.json +18 -0
  88. package/packages/vscode-drift/vscode-drift-0.1.0.vsix +0 -0
  89. package/packages/vscode-drift/vscode-drift-0.1.1.vsix +0 -0
  90. package/src/analyzer.ts +248 -1765
  91. package/src/badge.ts +38 -16
  92. package/src/ci.ts +38 -17
  93. package/src/cli.ts +143 -4
  94. package/src/config.ts +1 -1
  95. package/src/diff.ts +36 -30
  96. package/src/fix.ts +178 -0
  97. package/src/git/blame.ts +279 -0
  98. package/src/git/helpers.ts +198 -0
  99. package/src/git/trend.ts +117 -0
  100. package/src/git.ts +33 -24
  101. package/src/index.ts +16 -1
  102. package/src/map.ts +117 -0
  103. package/src/metrics.ts +200 -0
  104. package/src/plugins.ts +76 -0
  105. package/src/printer.ts +20 -0
  106. package/src/report.ts +666 -296
  107. package/src/reporter.ts +95 -2
  108. package/src/review.ts +98 -0
  109. package/src/rules/comments.ts +56 -0
  110. package/src/rules/complexity.ts +57 -0
  111. package/src/rules/coupling.ts +23 -0
  112. package/src/rules/magic.ts +38 -0
  113. package/src/rules/nesting.ts +88 -0
  114. package/src/rules/phase0-basic.ts +194 -0
  115. package/src/rules/phase1-complexity.ts +8 -0
  116. package/src/rules/phase2-crossfile.ts +177 -0
  117. package/src/rules/phase3-arch.ts +183 -0
  118. package/src/rules/phase3-configurable.ts +132 -0
  119. package/src/rules/phase5-ai.ts +292 -0
  120. package/src/rules/phase8-semantic.ts +136 -0
  121. package/src/rules/promise.ts +29 -0
  122. package/src/rules/shared.ts +39 -0
  123. package/src/snapshot.ts +175 -0
  124. package/src/types.ts +75 -1
  125. package/src/utils.ts +3 -1
  126. package/tests/helpers.ts +45 -0
  127. package/tests/new-features.test.ts +153 -0
  128. package/tests/rules.test.ts +1269 -0
  129. package/vitest.config.ts +15 -0
@@ -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,135 @@
1
+ // drift-ignore-file
2
+
3
+ import * as vscode from 'vscode'
4
+ import { analyzeFilePath } from './analyzer'
5
+ import { DriftDiagnosticsProvider } from './diagnostics'
6
+ import { DriftTreeProvider } from './treeview'
7
+ import { DriftStatusBarItem } from './statusbar'
8
+ import type { FileReport } from '@eduardbar/drift'
9
+
10
+ const SUPPORTED_LANGUAGES = ['typescript', 'typescriptreact', 'javascript', 'javascriptreact']
11
+
12
+ async function analyzeAndUpdate(
13
+ document: vscode.TextDocument,
14
+ diagnostics: DriftDiagnosticsProvider,
15
+ treeProvider: DriftTreeProvider,
16
+ reportCache: Map<string, FileReport>,
17
+ statusBar: DriftStatusBarItem
18
+ ): Promise<void> {
19
+ const config = vscode.workspace.getConfiguration('drift')
20
+ if (!config.get<boolean>('enable', true)) return
21
+
22
+ if (!SUPPORTED_LANGUAGES.includes(document.languageId)) return
23
+ if (document.uri.scheme !== 'file') return
24
+
25
+ const filePath = document.uri.fsPath
26
+
27
+ const report = await analyzeFilePath(filePath)
28
+ if (!report) return
29
+
30
+ diagnostics.update(report)
31
+ treeProvider.updateFile(report)
32
+ reportCache.set(filePath, report)
33
+ statusBar.update(Array.from(reportCache.values()))
34
+ }
35
+
36
+ async function scanWorkspace(
37
+ diagnostics: DriftDiagnosticsProvider,
38
+ treeProvider: DriftTreeProvider,
39
+ reportCache: Map<string, FileReport>,
40
+ statusBar: DriftStatusBarItem
41
+ ): Promise<void> {
42
+ const files = await vscode.workspace.findFiles(
43
+ '**/*.{ts,tsx,js,jsx}',
44
+ '**/node_modules/**'
45
+ )
46
+
47
+ vscode.window.withProgress(
48
+ {
49
+ location: vscode.ProgressLocation.Notification,
50
+ title: 'drift: Scanning workspace...',
51
+ cancellable: false,
52
+ },
53
+ async (progress) => {
54
+ const total = files.length
55
+ let done = 0
56
+
57
+ for (const file of files) {
58
+ const report = await analyzeFilePath(file.fsPath)
59
+ if (report) {
60
+ diagnostics.update(report)
61
+ treeProvider.updateFile(report)
62
+ reportCache.set(file.fsPath, report)
63
+ }
64
+ done++
65
+ progress.report({ increment: (done / total) * 100 })
66
+ }
67
+
68
+ statusBar.update(Array.from(reportCache.values()))
69
+ vscode.window.showInformationMessage(`drift: ${total} files scanned.`)
70
+ }
71
+ )
72
+ }
73
+
74
+ function clearDiagnostics(
75
+ diagnostics: DriftDiagnosticsProvider,
76
+ treeProvider: DriftTreeProvider,
77
+ reportCache: Map<string, FileReport>,
78
+ statusBar: DriftStatusBarItem
79
+ ): void {
80
+ diagnostics.clear()
81
+ treeProvider.clearAll()
82
+ reportCache.clear()
83
+ statusBar.update([])
84
+ }
85
+
86
+ export function activate(context: vscode.ExtensionContext): void {
87
+ const diagnostics = new DriftDiagnosticsProvider()
88
+ const treeProvider = new DriftTreeProvider()
89
+ const statusBar = new DriftStatusBarItem()
90
+
91
+ const treeView = vscode.window.createTreeView('driftIssues', {
92
+ treeDataProvider: treeProvider,
93
+ showCollapseAll: true,
94
+ })
95
+
96
+ const reportCache = new Map<string, FileReport>()
97
+
98
+ const onSave = vscode.workspace.onDidSaveTextDocument(
99
+ (doc) => analyzeAndUpdate(doc, diagnostics, treeProvider, reportCache, statusBar)
100
+ )
101
+
102
+ const scanCmd = vscode.commands.registerCommand(
103
+ 'drift.scanWorkspace',
104
+ () => scanWorkspace(diagnostics, treeProvider, reportCache, statusBar)
105
+ )
106
+
107
+ const clearCmd = vscode.commands.registerCommand(
108
+ 'drift.clearDiagnostics',
109
+ () => clearDiagnostics(diagnostics, treeProvider, reportCache, statusBar)
110
+ )
111
+
112
+ const goToCmd = vscode.commands.registerCommand(
113
+ 'drift.goToIssue',
114
+ async (filePath: string, line: number) => {
115
+ const uri = vscode.Uri.file(filePath)
116
+ const doc = await vscode.workspace.openTextDocument(uri)
117
+ const editor = await vscode.window.showTextDocument(doc)
118
+ const pos = new vscode.Position(Math.max(0, line - 1), 0)
119
+ editor.selection = new vscode.Selection(pos, pos)
120
+ editor.revealRange(new vscode.Range(pos, pos), vscode.TextEditorRevealType.InCenter)
121
+ }
122
+ )
123
+
124
+ context.subscriptions.push(
125
+ { dispose: () => diagnostics.dispose() },
126
+ { dispose: () => statusBar.dispose() },
127
+ treeView,
128
+ onSave,
129
+ scanCmd,
130
+ clearCmd,
131
+ goToCmd,
132
+ )
133
+ }
134
+
135
+ export function deactivate(): void {}
@@ -0,0 +1,55 @@
1
+ import * as vscode from 'vscode'
2
+ import type { FileReport } from '@eduardbar/drift'
3
+
4
+ const STATUSBAR_PRIORITY = 100
5
+
6
+ const SCORE_THRESHOLDS = {
7
+ WARNING: 50,
8
+ ERROR: 30,
9
+ WARNING_BG: 60,
10
+ }
11
+
12
+ export class DriftStatusBarItem {
13
+ private item: vscode.StatusBarItem
14
+
15
+ constructor() {
16
+ this.item = vscode.createStatusBarItem(
17
+ vscode.StatusBarAlignment.Right,
18
+ STATUSBAR_PRIORITY
19
+ )
20
+ this.item.command = 'drift.scanWorkspace'
21
+ this.item.tooltip = 'Click to scan workspace'
22
+ }
23
+
24
+ update(reports: FileReport[]): void {
25
+ if (reports.length === 0) {
26
+ this.item.text = '$(check) drift'
27
+ this.item.backgroundColor = undefined
28
+ this.item.show()
29
+ return
30
+ }
31
+
32
+ const totalScore = Math.round(
33
+ reports.reduce((sum, r) => sum + r.score, 0) / reports.length
34
+ )
35
+ const totalIssues = reports.reduce((sum, r) => sum + r.issues.length, 0)
36
+ const hasErrors = reports.some(r => r.issues.some(i => i.severity === 'error'))
37
+
38
+ const icon = hasErrors ? '$(error)' : totalScore < SCORE_THRESHOLDS.WARNING ? '$(warning)' : '$(check)'
39
+ this.item.text = `${icon} drift ${totalScore}/100 · ${totalIssues} issues`
40
+
41
+ if (hasErrors || totalScore < SCORE_THRESHOLDS.ERROR) {
42
+ this.item.backgroundColor = new vscode.ThemeColor('statusBarItem.errorBackground')
43
+ } else if (totalScore < SCORE_THRESHOLDS.WARNING_BG) {
44
+ this.item.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground')
45
+ } else {
46
+ this.item.backgroundColor = undefined
47
+ }
48
+
49
+ this.item.show()
50
+ }
51
+
52
+ dispose(): void {
53
+ this.item.dispose()
54
+ }
55
+ }
@@ -0,0 +1,110 @@
1
+ // drift-ignore-file
2
+
3
+ import * as vscode from 'vscode'
4
+ import * as path from 'path'
5
+ import type { FileReport } from '@eduardbar/drift'
6
+
7
+ type TreeItemType = 'file' | 'issue'
8
+
9
+ export class DriftTreeItem extends vscode.TreeItem {
10
+ constructor(
11
+ public readonly label: string,
12
+ public readonly collapsibleState: vscode.TreeItemCollapsibleState,
13
+ public readonly itemType: TreeItemType,
14
+ public readonly fileReport?: FileReport,
15
+ public readonly issueIndex?: number
16
+ ) {
17
+ super(label, collapsibleState)
18
+
19
+ if (itemType === 'file' && fileReport) {
20
+ const score = fileReport.score
21
+ const issueCount = fileReport.issues.length
22
+ this.description = `score: ${score} • ${issueCount} issue${issueCount !== 1 ? 's' : ''}`
23
+ this.tooltip = fileReport.path
24
+ this.iconPath = score >= 70
25
+ ? new vscode.ThemeIcon('check', new vscode.ThemeColor('testing.iconPassed'))
26
+ : score >= 40
27
+ ? new vscode.ThemeIcon('warning', new vscode.ThemeColor('problemsWarningIcon.foreground'))
28
+ : new vscode.ThemeIcon('error', new vscode.ThemeColor('problemsErrorIcon.foreground'))
29
+ this.contextValue = 'driftFile'
30
+ }
31
+
32
+ if (itemType === 'issue' && fileReport && issueIndex !== undefined) {
33
+ const issue = fileReport.issues[issueIndex]
34
+ this.description = `line ${issue.line}`
35
+ this.tooltip = issue.message
36
+ this.iconPath = issue.severity === 'error'
37
+ ? new vscode.ThemeIcon('circle-filled', new vscode.ThemeColor('problemsErrorIcon.foreground'))
38
+ : issue.severity === 'warning'
39
+ ? new vscode.ThemeIcon('warning', new vscode.ThemeColor('problemsWarningIcon.foreground'))
40
+ : new vscode.ThemeIcon('info', new vscode.ThemeColor('problemsInfoIcon.foreground'))
41
+
42
+ // Click para ir a la línea
43
+ this.command = {
44
+ command: 'drift.goToIssue',
45
+ title: 'Go to Issue',
46
+ arguments: [fileReport.path, issue.line],
47
+ }
48
+ this.contextValue = 'driftIssue'
49
+ }
50
+ }
51
+ }
52
+
53
+ export class DriftTreeProvider implements vscode.TreeDataProvider<DriftTreeItem> {
54
+ private _onDidChangeTreeData = new vscode.EventEmitter<DriftTreeItem | undefined | null | void>()
55
+ readonly onDidChangeTreeData = this._onDidChangeTreeData.event
56
+
57
+ private reports = new Map<string, FileReport>()
58
+
59
+ refresh(): void {
60
+ this._onDidChangeTreeData.fire()
61
+ }
62
+
63
+ updateFile(report: FileReport): void {
64
+ this.reports.set(report.path, report)
65
+ this.refresh()
66
+ }
67
+
68
+ clearFile(filePath: string): void {
69
+ this.reports.delete(filePath)
70
+ this.refresh()
71
+ }
72
+
73
+ clearAll(): void {
74
+ this.reports.clear()
75
+ this.refresh()
76
+ }
77
+
78
+ getTreeItem(element: DriftTreeItem): vscode.TreeItem {
79
+ return element
80
+ }
81
+
82
+ getChildren(element?: DriftTreeItem): DriftTreeItem[] {
83
+ if (!element) {
84
+ // Root: lista de archivos con issues, ordenados por score ascendente
85
+ return Array.from(this.reports.values())
86
+ .filter(r => r.issues.length > 0)
87
+ .sort((a, b) => a.score - b.score)
88
+ .map(r => new DriftTreeItem(
89
+ path.basename(r.path),
90
+ vscode.TreeItemCollapsibleState.Collapsed,
91
+ 'file',
92
+ r
93
+ ))
94
+ }
95
+
96
+ if (element.itemType === 'file' && element.fileReport) {
97
+ return element.fileReport.issues.map((_, i) =>
98
+ new DriftTreeItem(
99
+ element.fileReport!.issues[i].rule,
100
+ vscode.TreeItemCollapsibleState.None,
101
+ 'issue',
102
+ element.fileReport,
103
+ i
104
+ )
105
+ )
106
+ }
107
+
108
+ return []
109
+ }
110
+ }
@@ -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
+ }