pi-lens 3.1.2 → 3.2.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 (154) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +16 -12
  3. package/clients/ast-grep-client.js +8 -1
  4. package/clients/ast-grep-client.ts +9 -1
  5. package/clients/biome-client.js +51 -38
  6. package/clients/biome-client.ts +60 -58
  7. package/clients/dependency-checker.js +30 -1
  8. package/clients/dependency-checker.ts +35 -1
  9. package/clients/dispatch/__tests__/runner-registration.test.ts +286 -282
  10. package/clients/dispatch/bus-dispatcher.js +15 -14
  11. package/clients/dispatch/bus-dispatcher.ts +32 -25
  12. package/clients/dispatch/dispatcher.js +18 -25
  13. package/clients/dispatch/dispatcher.test.ts +2 -1
  14. package/clients/dispatch/dispatcher.ts +17 -28
  15. package/clients/dispatch/plan.js +77 -32
  16. package/clients/dispatch/plan.ts +78 -32
  17. package/clients/dispatch/runners/ast-grep-napi.js +36 -376
  18. package/clients/dispatch/runners/ast-grep-napi.ts +60 -433
  19. package/clients/dispatch/runners/index.js +8 -4
  20. package/clients/dispatch/runners/index.ts +8 -4
  21. package/clients/dispatch/runners/lsp.js +65 -0
  22. package/clients/dispatch/runners/lsp.ts +125 -0
  23. package/clients/dispatch/runners/oxlint.js +2 -2
  24. package/clients/dispatch/runners/oxlint.ts +2 -2
  25. package/clients/dispatch/runners/pyright.js +24 -8
  26. package/clients/dispatch/runners/pyright.ts +28 -14
  27. package/clients/dispatch/runners/rust-clippy.js +2 -2
  28. package/clients/dispatch/runners/rust-clippy.ts +2 -4
  29. package/clients/dispatch/runners/tree-sitter.js +14 -2
  30. package/clients/dispatch/runners/tree-sitter.ts +15 -2
  31. package/clients/dispatch/runners/ts-lsp.js +3 -3
  32. package/clients/dispatch/runners/ts-lsp.ts +8 -5
  33. package/clients/dispatch/runners/yaml-rule-parser.js +292 -0
  34. package/clients/dispatch/runners/yaml-rule-parser.ts +338 -0
  35. package/clients/dispatch/types.js +3 -0
  36. package/clients/dispatch/types.ts +3 -0
  37. package/clients/formatters.js +67 -14
  38. package/clients/formatters.ts +68 -15
  39. package/clients/installer/index.js +78 -10
  40. package/clients/installer/index.ts +519 -426
  41. package/clients/jscpd-client.js +28 -0
  42. package/clients/jscpd-client.ts +41 -3
  43. package/clients/knip-client.js +30 -1
  44. package/clients/knip-client.ts +34 -2
  45. package/clients/lsp/__tests__/client.test.ts +64 -41
  46. package/clients/lsp/__tests__/config.test.ts +25 -17
  47. package/clients/lsp/__tests__/launch.test.ts +108 -43
  48. package/clients/lsp/__tests__/service.test.ts +76 -48
  49. package/clients/lsp/client.js +87 -2
  50. package/clients/lsp/client.ts +150 -6
  51. package/clients/lsp/config.js +8 -11
  52. package/clients/lsp/config.ts +24 -21
  53. package/clients/lsp/index.js +69 -0
  54. package/clients/lsp/index.ts +82 -0
  55. package/clients/lsp/interactive-install.js +19 -8
  56. package/clients/lsp/interactive-install.ts +52 -27
  57. package/clients/lsp/launch.js +182 -32
  58. package/clients/lsp/launch.ts +241 -38
  59. package/clients/lsp/path-utils.js +3 -46
  60. package/clients/lsp/path-utils.ts +11 -51
  61. package/clients/lsp/server.js +93 -71
  62. package/clients/lsp/server.ts +173 -131
  63. package/clients/path-utils.js +142 -0
  64. package/clients/path-utils.ts +153 -0
  65. package/clients/ruff-client.js +33 -4
  66. package/clients/ruff-client.ts +44 -13
  67. package/clients/safe-spawn.js +3 -1
  68. package/clients/safe-spawn.ts +3 -1
  69. package/clients/services/effect-integration.js +11 -7
  70. package/clients/services/effect-integration.ts +34 -26
  71. package/clients/sg-runner.js +51 -9
  72. package/clients/sg-runner.ts +58 -15
  73. package/clients/tree-sitter-client.js +12 -0
  74. package/clients/tree-sitter-client.ts +12 -0
  75. package/clients/typescript-client.js +6 -2
  76. package/clients/typescript-client.ts +9 -2
  77. package/commands/booboo.js +2 -4
  78. package/commands/booboo.ts +2 -4
  79. package/index.ts +377 -93
  80. package/package.json +2 -1
  81. package/rules/tree-sitter-queries/tsx/no-nested-links.yml +45 -0
  82. package/rules/tree-sitter-queries/typescript/constructor-super.yml +55 -0
  83. package/rules/tree-sitter-queries/typescript/debugger.yml +1 -1
  84. package/rules/tree-sitter-queries/typescript/no-dupe-class-members.yml +47 -0
  85. package/tsconfig.json +1 -1
  86. package/clients/__tests__/file-time.test.js +0 -216
  87. package/clients/__tests__/format-service.test.js +0 -245
  88. package/clients/__tests__/formatters.test.js +0 -271
  89. package/clients/agent-behavior-client.test.js +0 -94
  90. package/clients/ast-grep-client.test.js +0 -129
  91. package/clients/ast-grep-client.test.ts +0 -155
  92. package/clients/biome-client.test.js +0 -144
  93. package/clients/cache-manager.test.js +0 -197
  94. package/clients/complexity-client.test.js +0 -234
  95. package/clients/dependency-checker.test.js +0 -60
  96. package/clients/dispatch/__tests__/autofix-integration.test.js +0 -245
  97. package/clients/dispatch/__tests__/runner-registration.test.js +0 -236
  98. package/clients/dispatch/dispatcher.edge.test.js +0 -82
  99. package/clients/dispatch/dispatcher.format.test.js +0 -46
  100. package/clients/dispatch/dispatcher.inline.test.js +0 -74
  101. package/clients/dispatch/dispatcher.test.js +0 -115
  102. package/clients/dispatch/runners/architect.test.js +0 -138
  103. package/clients/dispatch/runners/ast-grep-napi.test.js +0 -106
  104. package/clients/dispatch/runners/oxlint.test.js +0 -230
  105. package/clients/dispatch/runners/pyright.test.js +0 -98
  106. package/clients/dispatch/runners/python-slop.test.js +0 -203
  107. package/clients/dispatch/runners/scan_codebase.test.js +0 -89
  108. package/clients/dispatch/runners/shellcheck.test.js +0 -98
  109. package/clients/dispatch/runners/spellcheck.test.js +0 -158
  110. package/clients/dispatch/runners/ts-slop.test.js +0 -180
  111. package/clients/dispatch/runners/ts-slop.test.ts +0 -230
  112. package/clients/dogfood.test.js +0 -201
  113. package/clients/file-kinds.test.js +0 -169
  114. package/clients/go-client.test.js +0 -127
  115. package/clients/jscpd-client.test.js +0 -127
  116. package/clients/knip-client.test.js +0 -112
  117. package/clients/lsp/__tests__/client.test.js +0 -325
  118. package/clients/lsp/__tests__/config.test.js +0 -166
  119. package/clients/lsp/__tests__/error-recovery.test.js +0 -213
  120. package/clients/lsp/__tests__/integration.test.js +0 -127
  121. package/clients/lsp/__tests__/launch.test.js +0 -260
  122. package/clients/lsp/__tests__/server.test.js +0 -259
  123. package/clients/lsp/__tests__/service.test.js +0 -417
  124. package/clients/metrics-client.test.js +0 -141
  125. package/clients/ruff-client.test.js +0 -132
  126. package/clients/rust-client.test.js +0 -108
  127. package/clients/sanitize.test.js +0 -177
  128. package/clients/secrets-scanner.test.js +0 -100
  129. package/clients/services/__tests__/effect-integration.test.js +0 -86
  130. package/clients/test-runner-client.test.js +0 -192
  131. package/clients/todo-scanner.test.js +0 -301
  132. package/clients/type-coverage-client.test.js +0 -105
  133. package/clients/typescript-client.codefix.test.js +0 -157
  134. package/clients/typescript-client.test.js +0 -105
  135. package/commands/clients/ast-grep-client.js +0 -250
  136. package/commands/clients/ast-grep-parser.js +0 -86
  137. package/commands/clients/ast-grep-rule-manager.js +0 -91
  138. package/commands/clients/ast-grep-types.js +0 -9
  139. package/commands/clients/biome-client.js +0 -380
  140. package/commands/clients/complexity-client.js +0 -667
  141. package/commands/clients/file-kinds.js +0 -177
  142. package/commands/clients/file-utils.js +0 -40
  143. package/commands/clients/jscpd-client.js +0 -169
  144. package/commands/clients/knip-client.js +0 -211
  145. package/commands/clients/ruff-client.js +0 -297
  146. package/commands/clients/safe-spawn.js +0 -88
  147. package/commands/clients/scan-utils.js +0 -83
  148. package/commands/clients/sg-runner.js +0 -190
  149. package/commands/clients/types.js +0 -11
  150. package/commands/clients/typescript-client.js +0 -505
  151. package/commands/rate.test.js +0 -119
  152. package/rules/ast-grep-rules/rules/no-dangerously-set-inner-html.yml +0 -13
  153. package/rules/ast-grep-rules/rules/no-debugger.yml +0 -12
  154. package/rules/ast-grep-rules/rules/no-eval.yml +0 -13
@@ -0,0 +1,55 @@
1
+ # Constructor Super Call Detection
2
+ # Detects derived class constructors that don't call super()
3
+ id: constructor-super
4
+ name: Missing super() call in derived class constructor
5
+ severity: error
6
+ category: correctness
7
+ language: typescript
8
+
9
+ message: "Constructor of derived class must call super() before accessing 'this'"
10
+
11
+ description: |
12
+ Constructors of derived classes must call super() before accessing 'this'.
13
+ This is a JavaScript runtime requirement that throws if violated.
14
+
15
+ query: |
16
+ (class_declaration
17
+ (class_heritage
18
+ (extends_clause) @EXTENDS)
19
+ body: (class_body
20
+ (method_definition
21
+ name: (property_identifier) @CONSTRUCTOR
22
+ (#eq? @CONSTRUCTOR "constructor")
23
+ body: (statement_block) @BODY)))
24
+
25
+ metavars:
26
+ - EXTENDS
27
+ - CONSTRUCTOR
28
+ - BODY
29
+
30
+ post_filter: "no_super_call"
31
+ post_filter_params: {}
32
+
33
+ tags:
34
+ - correctness
35
+ - javascript
36
+ - typescript
37
+ - constructor
38
+
39
+ examples:
40
+ bad: |
41
+ class Derived extends Base {
42
+ constructor() {
43
+ this.value = 1; // ERROR: must call super() first
44
+ }
45
+ }
46
+
47
+ good: |
48
+ class Derived extends Base {
49
+ constructor() {
50
+ super();
51
+ this.value = 1;
52
+ }
53
+ }
54
+
55
+ has_fix: false
@@ -2,7 +2,7 @@
2
2
  # Detects debugger statements left in code
3
3
  id: debugger-statement
4
4
  name: Debugger Statement
5
- severity: warning
5
+ severity: error
6
6
  category: debugging
7
7
  language: typescript
8
8
 
@@ -0,0 +1,47 @@
1
+ # Duplicate Class Members Detection
2
+ # Detects duplicate method/property definitions in classes
3
+ id: no-dupe-class-members
4
+ name: Duplicate class member
5
+ severity: error
6
+ category: correctness
7
+ language: typescript
8
+
9
+ message: "Duplicate class member '$NAME' - previous declaration will be overwritten"
10
+
11
+ description: |
12
+ Duplicate class member declarations silently overwrite each other.
13
+ The last declaration wins, which can cause unexpected behavior.
14
+
15
+ query: |
16
+ (class_body
17
+ (method_definition
18
+ name: (property_identifier) @NAME1
19
+ (#match? @NAME1 "^[^#]"))
20
+ (method_definition
21
+ name: (property_identifier) @NAME2
22
+ (#eq? @NAME1 @NAME2)))
23
+
24
+ metavars:
25
+ - NAME1
26
+ - NAME2
27
+
28
+ tags:
29
+ - correctness
30
+ - javascript
31
+ - typescript
32
+ - class
33
+
34
+ examples:
35
+ bad: |
36
+ class MyClass {
37
+ foo() { return 1; }
38
+ foo() { return 2; } // Overwrites first foo()
39
+ }
40
+
41
+ good: |
42
+ class MyClass {
43
+ foo() { return 1; }
44
+ bar() { return 2; }
45
+ }
46
+
47
+ has_fix: false
package/tsconfig.json CHANGED
@@ -10,5 +10,5 @@
10
10
  "types": ["node"]
11
11
  },
12
12
  "include": ["**/*.ts"],
13
- "exclude": ["node_modules"]
13
+ "exclude": ["node_modules", "test-lsp-files"]
14
14
  }
@@ -1,216 +0,0 @@
1
- /**
2
- * FileTime Tracking Tests
3
- *
4
- * Tests the safety mechanism that prevents race conditions
5
- * between auto-formatting and agent edits.
6
- */
7
- import { describe, it, expect, beforeEach } from "vitest";
8
- import * as fs from "node:fs";
9
- import * as path from "node:path";
10
- import { FileTime, FileTimeError, createFileTime, clearAllSessions } from "../file-time.js";
11
- import { fileURLToPath } from "url";
12
- import { dirname } from "path";
13
- const __filename = fileURLToPath(import.meta.url);
14
- const __dirname = dirname(__filename);
15
- const TEST_DIR = path.join(__dirname, "..", "..", "test-filetime");
16
- describe("FileTime", () => {
17
- let fileTime;
18
- const sessionID = "test-session";
19
- beforeEach(() => {
20
- clearAllSessions();
21
- fileTime = new FileTime(sessionID);
22
- // Ensure test directory exists
23
- if (!fs.existsSync(TEST_DIR)) {
24
- fs.mkdirSync(TEST_DIR, { recursive: true });
25
- }
26
- });
27
- describe("read()", () => {
28
- it("should record file stamp with mtime/ctime/size", () => {
29
- const testFile = path.join(TEST_DIR, "test1.txt");
30
- fs.writeFileSync(testFile, "hello");
31
- const stamp = fileTime.read(testFile);
32
- expect(stamp.readAt).toBeInstanceOf(Date);
33
- expect(stamp.mtime).toBeDefined();
34
- expect(stamp.ctime).toBeDefined();
35
- expect(stamp.size).toBe(5);
36
- });
37
- it("should handle non-existent files gracefully", () => {
38
- const testFile = path.join(TEST_DIR, "nonexistent.txt");
39
- const stamp = fileTime.read(testFile);
40
- expect(stamp.readAt).toBeInstanceOf(Date);
41
- expect(stamp.mtime).toBeUndefined();
42
- expect(stamp.ctime).toBeUndefined();
43
- expect(stamp.size).toBeUndefined();
44
- });
45
- it("should track multiple files per session", () => {
46
- const file1 = path.join(TEST_DIR, "file1.txt");
47
- const file2 = path.join(TEST_DIR, "file2.txt");
48
- fs.writeFileSync(file1, "content1");
49
- fs.writeFileSync(file2, "content2");
50
- fileTime.read(file1);
51
- fileTime.read(file2);
52
- expect(fileTime.get(file1)).toBeDefined();
53
- expect(fileTime.get(file2)).toBeDefined();
54
- });
55
- });
56
- describe("get()", () => {
57
- it("should return undefined for unread files", () => {
58
- const testFile = path.join(TEST_DIR, "unread.txt");
59
- const stamp = fileTime.get(testFile);
60
- expect(stamp).toBeUndefined();
61
- });
62
- it("should return recorded stamp for read files", () => {
63
- const testFile = path.join(TEST_DIR, "test2.txt");
64
- fs.writeFileSync(testFile, "content");
65
- const recorded = fileTime.read(testFile);
66
- const retrieved = fileTime.get(testFile);
67
- expect(retrieved?.mtime).toBe(recorded.mtime);
68
- expect(retrieved?.ctime).toBe(recorded.ctime);
69
- expect(retrieved?.size).toBe(recorded.size);
70
- });
71
- });
72
- describe("assert()", () => {
73
- it("should throw FileTimeError for unread files", () => {
74
- const testFile = path.join(TEST_DIR, "never-read.txt");
75
- fs.writeFileSync(testFile, "content");
76
- expect(() => fileTime.assert(testFile)).toThrow(FileTimeError);
77
- expect(() => fileTime.assert(testFile)).toThrow(/must read file/);
78
- });
79
- it("should not throw for unchanged files", () => {
80
- const testFile = path.join(TEST_DIR, "unchanged.txt");
81
- fs.writeFileSync(testFile, "content");
82
- fileTime.read(testFile);
83
- expect(() => fileTime.assert(testFile)).not.toThrow();
84
- });
85
- it("should throw FileTimeError when file modified externally", () => {
86
- const testFile = path.join(TEST_DIR, "modified.txt");
87
- fs.writeFileSync(testFile, "original");
88
- fileTime.read(testFile);
89
- // Simulate external modification
90
- fs.writeFileSync(testFile, "modified content");
91
- expect(() => fileTime.assert(testFile)).toThrow(FileTimeError);
92
- expect(() => fileTime.assert(testFile)).toThrow(/modified since it was last read/);
93
- });
94
- it("should detect size changes", () => {
95
- const testFile = path.join(TEST_DIR, "size-change.txt");
96
- fs.writeFileSync(testFile, "original content");
97
- fileTime.read(testFile);
98
- // Truncate file (size change, mtime change)
99
- fs.writeFileSync(testFile, "x");
100
- expect(() => fileTime.assert(testFile)).toThrow(FileTimeError);
101
- });
102
- });
103
- describe("hasChanged()", () => {
104
- it("should return true for unread files", () => {
105
- const testFile = path.join(TEST_DIR, "unread-check.txt");
106
- fs.writeFileSync(testFile, "content");
107
- expect(fileTime.hasChanged(testFile)).toBe(true);
108
- });
109
- it("should return false for unchanged files", () => {
110
- const testFile = path.join(TEST_DIR, "unchanged-check.txt");
111
- fs.writeFileSync(testFile, "content");
112
- fileTime.read(testFile);
113
- expect(fileTime.hasChanged(testFile)).toBe(false);
114
- });
115
- it("should return true when file modified", () => {
116
- const testFile = path.join(TEST_DIR, "changed-check.txt");
117
- fs.writeFileSync(testFile, "original");
118
- fileTime.read(testFile);
119
- fs.writeFileSync(testFile, "changed");
120
- expect(fileTime.hasChanged(testFile)).toBe(true);
121
- });
122
- });
123
- describe("withLock()", () => {
124
- it("should execute function exclusively", async () => {
125
- const testFile = path.join(TEST_DIR, "locked.txt");
126
- const executionOrder = [];
127
- const fn1 = async () => {
128
- executionOrder.push("start1");
129
- await new Promise(r => setTimeout(r, 50));
130
- executionOrder.push("end1");
131
- return "result1";
132
- };
133
- const fn2 = async () => {
134
- executionOrder.push("start2");
135
- await new Promise(r => setTimeout(r, 50));
136
- executionOrder.push("end2");
137
- return "result2";
138
- };
139
- // Start both, but they should execute sequentially
140
- const promise1 = fileTime.withLock(testFile, fn1);
141
- const promise2 = fileTime.withLock(testFile, fn2);
142
- await Promise.all([promise1, promise2]);
143
- // Should be sequential, not interleaved
144
- expect(executionOrder).toEqual(["start1", "end1", "start2", "end2"]);
145
- });
146
- it("should return function result", async () => {
147
- const testFile = path.join(TEST_DIR, "lock-result.txt");
148
- const result = await fileTime.withLock(testFile, async () => {
149
- return "success";
150
- });
151
- expect(result).toBe("success");
152
- });
153
- });
154
- describe("clear()", () => {
155
- it("should clear all tracked files for session", () => {
156
- const file1 = path.join(TEST_DIR, "clear1.txt");
157
- const file2 = path.join(TEST_DIR, "clear2.txt");
158
- fs.writeFileSync(file1, "a");
159
- fs.writeFileSync(file2, "b");
160
- fileTime.read(file1);
161
- fileTime.read(file2);
162
- fileTime.clear();
163
- expect(fileTime.get(file1)).toBeUndefined();
164
- expect(fileTime.get(file2)).toBeUndefined();
165
- });
166
- });
167
- describe("cross-session isolation", () => {
168
- it("should isolate file tracking between sessions", () => {
169
- const testFile = path.join(TEST_DIR, "isolated.txt");
170
- fs.writeFileSync(testFile, "content");
171
- const session1 = new FileTime("session1");
172
- const session2 = new FileTime("session2");
173
- session1.read(testFile);
174
- // session2 should not see session1's reads
175
- expect(() => session2.assert(testFile)).toThrow(FileTimeError);
176
- expect(() => session2.assert(testFile)).toThrow(/must read file/);
177
- });
178
- });
179
- describe("FileTimeError", () => {
180
- it("should have correct error properties", () => {
181
- const testFile = path.join(TEST_DIR, "error.txt");
182
- fs.writeFileSync(testFile, "content");
183
- try {
184
- fileTime.assert(testFile);
185
- }
186
- catch (error) {
187
- expect(error).toBeInstanceOf(FileTimeError);
188
- expect(error.name).toBe("FileTimeError");
189
- expect(error.filePath).toBe(path.resolve(testFile));
190
- expect(error.reason).toBe("not-read");
191
- }
192
- });
193
- });
194
- });
195
- describe("createFileTime helper", () => {
196
- it("should create FileTime instance with session ID", () => {
197
- const ft = createFileTime("my-session");
198
- expect(ft).toBeInstanceOf(FileTime);
199
- });
200
- });
201
- describe("clearAllSessions helper", () => {
202
- it("should clear all session tracking", () => {
203
- const ft1 = createFileTime("session1");
204
- const ft2 = createFileTime("session2");
205
- const testFile = path.join(TEST_DIR, "clearall.txt");
206
- fs.writeFileSync(testFile, "x");
207
- ft1.read(testFile);
208
- ft2.read(testFile);
209
- clearAllSessions();
210
- // After clearing, both should throw "not read"
211
- const ft1New = createFileTime("session1");
212
- const ft2New = createFileTime("session2");
213
- expect(() => ft1New.assert(testFile)).toThrow(/must read file/);
214
- expect(() => ft2New.assert(testFile)).toThrow(/must read file/);
215
- });
216
- });
@@ -1,245 +0,0 @@
1
- /**
2
- * Format Service Tests
3
- *
4
- * Tests concurrent formatter execution via Effect-TS
5
- * and FileTime integration for safety.
6
- */
7
- import * as fs from "node:fs";
8
- import * as path from "node:path";
9
- import { dirname } from "node:path";
10
- import { fileURLToPath } from "node:url";
11
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
12
- import { FileTimeError } from "../file-time.js";
13
- import { FormatService, getFormatService, resetFormatService, } from "../format-service.js";
14
- import { clearAllSessions } from "../file-time.js";
15
- const __filename = fileURLToPath(import.meta.url);
16
- const __dirname = dirname(__filename);
17
- const TEST_DIR = path.join(__dirname, "..", "..", "test-format-service");
18
- describe("FormatService", () => {
19
- let formatService;
20
- const sessionID = "test-format-session";
21
- beforeEach(() => {
22
- resetFormatService();
23
- clearAllSessions(); // Clear FileTime global state for test isolation
24
- formatService = new FormatService(sessionID, true);
25
- if (fs.existsSync(TEST_DIR)) {
26
- fs.rmSync(TEST_DIR, { recursive: true });
27
- }
28
- fs.mkdirSync(TEST_DIR, { recursive: true });
29
- });
30
- afterEach(() => {
31
- resetFormatService();
32
- clearAllSessions(); // Clear FileTime global state for test isolation
33
- if (fs.existsSync(TEST_DIR)) {
34
- fs.rmSync(TEST_DIR, { recursive: true });
35
- }
36
- });
37
- describe("formatFile()", () => {
38
- it("should skip formatting when disabled", async () => {
39
- const disabledService = new FormatService(sessionID, false);
40
- const testFile = path.join(TEST_DIR, "disabled.txt");
41
- fs.writeFileSync(testFile, "content");
42
- const result = await disabledService.formatFile(testFile);
43
- expect(result.formatters).toEqual([]);
44
- expect(result.anyChanged).toBe(false);
45
- expect(result.allSucceeded).toBe(true);
46
- });
47
- it("should skip formatting with skip option", async () => {
48
- const testFile = path.join(TEST_DIR, "skipped.txt");
49
- fs.writeFileSync(testFile, "content");
50
- const result = await formatService.formatFile(testFile, { skip: true });
51
- expect(result.formatters).toEqual([]);
52
- expect(result.anyChanged).toBe(false);
53
- expect(result.allSucceeded).toBe(true);
54
- });
55
- it("should skip when file modified externally", async () => {
56
- const testFile = path.join(TEST_DIR, "external.txt");
57
- fs.writeFileSync(testFile, "original");
58
- // Record read
59
- formatService.recordRead(testFile);
60
- // Modify externally
61
- fs.writeFileSync(testFile, "modified");
62
- const result = await formatService.formatFile(testFile);
63
- expect(result.formatters).toEqual([]);
64
- expect(result.anyChanged).toBe(false);
65
- expect(result.allSucceeded).toBe(false);
66
- });
67
- it("should format TypeScript file with biome config", async () => {
68
- fs.writeFileSync(path.join(TEST_DIR, "biome.json"), '{"formatter": {}}');
69
- const tsFile = path.join(TEST_DIR, "test.ts");
70
- fs.writeFileSync(tsFile, "const x=1;");
71
- // Record read so format service knows initial state
72
- formatService.recordRead(tsFile);
73
- const result = await formatService.formatFile(tsFile);
74
- expect(result.filePath).toBe(tsFile);
75
- expect(result.formatters.some((f) => f.name === "biome")).toBe(true);
76
- });
77
- it("should format Python file with ruff config", async () => {
78
- fs.writeFileSync(path.join(TEST_DIR, "pyproject.toml"), "[tool.ruff]\nline-length = 100");
79
- const pyFile = path.join(TEST_DIR, "test.py");
80
- fs.writeFileSync(pyFile, "x=1");
81
- // Record read so format service knows initial state
82
- formatService.recordRead(pyFile);
83
- const result = await formatService.formatFile(pyFile);
84
- expect(result.filePath).toBe(pyFile);
85
- expect(result.formatters.some((f) => f.name === "ruff")).toBe(true);
86
- });
87
- it("should run multiple formatters for same file", async () => {
88
- fs.writeFileSync(path.join(TEST_DIR, "biome.json"), "{}");
89
- fs.writeFileSync(path.join(TEST_DIR, "package.json"), JSON.stringify({ devDependencies: { prettier: "^3.0.0" } }));
90
- const tsFile = path.join(TEST_DIR, "test.ts");
91
- fs.writeFileSync(tsFile, "const x = 1;");
92
- formatService.recordRead(tsFile);
93
- const result = await formatService.formatFile(tsFile);
94
- const names = result.formatters.map((f) => f.name);
95
- expect(names).toContain("biome");
96
- });
97
- it("should return empty result for unsupported file", async () => {
98
- const txtFile = path.join(TEST_DIR, "test.txt");
99
- fs.writeFileSync(txtFile, "content");
100
- formatService.recordRead(txtFile);
101
- const result = await formatService.formatFile(txtFile);
102
- expect(result.formatters).toEqual([]);
103
- expect(result.anyChanged).toBe(false);
104
- expect(result.allSucceeded).toBe(true);
105
- });
106
- it("should record FileTime after formatting", async () => {
107
- fs.writeFileSync(path.join(TEST_DIR, "biome.json"), "{}");
108
- const tsFile = path.join(TEST_DIR, "test.ts");
109
- fs.writeFileSync(tsFile, "const x = 1;");
110
- formatService.recordRead(tsFile);
111
- await formatService.formatFile(tsFile);
112
- expect(() => formatService.assertUnchanged(tsFile)).not.toThrow();
113
- });
114
- it("should report success/failure for each formatter", async () => {
115
- fs.writeFileSync(path.join(TEST_DIR, "biome.json"), "{}");
116
- const tsFile = path.join(TEST_DIR, "test.ts");
117
- fs.writeFileSync(tsFile, "const x = 1;");
118
- formatService.recordRead(tsFile);
119
- const result = await formatService.formatFile(tsFile);
120
- for (const formatter of result.formatters) {
121
- expect(formatter).toHaveProperty("name");
122
- expect(formatter).toHaveProperty("success");
123
- expect(formatter).toHaveProperty("changed");
124
- expect(typeof formatter.success).toBe("boolean");
125
- expect(typeof formatter.changed).toBe("boolean");
126
- }
127
- });
128
- it("should re-read file content after formatting", async () => {
129
- fs.writeFileSync(path.join(TEST_DIR, "biome.json"), "{}");
130
- const tsFile = path.join(TEST_DIR, "test.ts");
131
- fs.writeFileSync(tsFile, "const x = 1;");
132
- formatService.recordRead(tsFile);
133
- const _result = await formatService.formatFile(tsFile);
134
- const after = fs.readFileSync(tsFile, "utf-8");
135
- expect(after).toBeDefined();
136
- });
137
- });
138
- describe("assertUnchanged()", () => {
139
- it("should not throw for unchanged files", async () => {
140
- const testFile = path.join(TEST_DIR, "unchanged.txt");
141
- fs.writeFileSync(testFile, "content");
142
- formatService.recordRead(testFile);
143
- expect(() => formatService.assertUnchanged(testFile)).not.toThrow();
144
- });
145
- it("should throw FileTimeError when file modified", async () => {
146
- const testFile = path.join(TEST_DIR, "modified.txt");
147
- fs.writeFileSync(testFile, "original");
148
- formatService.recordRead(testFile);
149
- fs.writeFileSync(testFile, "changed");
150
- expect(() => formatService.assertUnchanged(testFile)).toThrow(FileTimeError);
151
- });
152
- });
153
- describe("hasChanged()", () => {
154
- it("should return false for unchanged files", async () => {
155
- const testFile = path.join(TEST_DIR, "unchanged-check.txt");
156
- fs.writeFileSync(testFile, "content");
157
- formatService.recordRead(testFile);
158
- expect(formatService.hasChanged(testFile)).toBe(false);
159
- });
160
- it("should return true when file modified", async () => {
161
- const testFile = path.join(TEST_DIR, "changed-check.txt");
162
- fs.writeFileSync(testFile, "original");
163
- formatService.recordRead(testFile);
164
- // Small delay to ensure different mtime (Windows has ~16ms resolution)
165
- await new Promise((r) => setTimeout(r, 50));
166
- fs.writeFileSync(testFile, "modified");
167
- expect(formatService.hasChanged(testFile)).toBe(true);
168
- });
169
- it("should return true for unread files", async () => {
170
- const testFile = path.join(TEST_DIR, "never-read.txt");
171
- fs.writeFileSync(testFile, "content");
172
- expect(formatService.hasChanged(testFile)).toBe(true);
173
- });
174
- });
175
- describe("recordRead()", () => {
176
- it("should record file read for tracking", async () => {
177
- const testFile = path.join(TEST_DIR, "tracked.txt");
178
- fs.writeFileSync(testFile, "content");
179
- formatService.recordRead(testFile);
180
- expect(formatService.hasChanged(testFile)).toBe(false);
181
- });
182
- });
183
- describe("clearCache()", () => {
184
- it("should clear formatter detection cache", async () => {
185
- fs.writeFileSync(path.join(TEST_DIR, "biome.json"), "{}");
186
- const tsFile = path.join(TEST_DIR, "test.ts");
187
- fs.writeFileSync(tsFile, "const x = 1;");
188
- formatService.recordRead(tsFile);
189
- await formatService.formatFile(tsFile);
190
- formatService.clearCache();
191
- await formatService.formatFile(tsFile);
192
- });
193
- });
194
- describe("Concurrency", () => {
195
- it("should handle multiple files concurrently", async () => {
196
- fs.writeFileSync(path.join(TEST_DIR, "biome.json"), "{}");
197
- const files = [
198
- path.join(TEST_DIR, "file1.ts"),
199
- path.join(TEST_DIR, "file2.ts"),
200
- path.join(TEST_DIR, "file3.ts"),
201
- ];
202
- for (const file of files) {
203
- fs.writeFileSync(file, "const x = 1;");
204
- formatService.recordRead(file);
205
- }
206
- const results = await Promise.all(files.map((f) => formatService.formatFile(f)));
207
- expect(results).toHaveLength(3);
208
- for (const result of results) {
209
- expect(result.filePath).toBeDefined();
210
- expect(result.formatters.length).toBeGreaterThan(0);
211
- }
212
- });
213
- });
214
- });
215
- describe("getFormatService singleton", () => {
216
- beforeEach(() => {
217
- resetFormatService();
218
- });
219
- afterEach(() => {
220
- resetFormatService();
221
- });
222
- it("should return singleton instance", () => {
223
- const instance1 = getFormatService();
224
- const instance2 = getFormatService();
225
- expect(instance1).toBe(instance2);
226
- });
227
- it("should create new instance when session ID provided", () => {
228
- const instance1 = getFormatService("session1");
229
- const instance2 = getFormatService("session2");
230
- expect(instance1).not.toBe(instance2);
231
- });
232
- it("should use cached instance regardless of enabled flag changes", () => {
233
- const first = getFormatService("test", true);
234
- const second = getFormatService("test", false);
235
- expect(first).toBe(second);
236
- });
237
- });
238
- describe("resetFormatService", () => {
239
- it("should reset singleton instance", () => {
240
- const instance1 = getFormatService();
241
- resetFormatService();
242
- const instance2 = getFormatService();
243
- expect(instance1).not.toBe(instance2);
244
- });
245
- });