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.
- package/CHANGELOG.md +55 -0
- package/README.md +16 -12
- package/clients/ast-grep-client.js +8 -1
- package/clients/ast-grep-client.ts +9 -1
- package/clients/biome-client.js +51 -38
- package/clients/biome-client.ts +60 -58
- package/clients/dependency-checker.js +30 -1
- package/clients/dependency-checker.ts +35 -1
- package/clients/dispatch/__tests__/runner-registration.test.ts +286 -282
- package/clients/dispatch/bus-dispatcher.js +15 -14
- package/clients/dispatch/bus-dispatcher.ts +32 -25
- package/clients/dispatch/dispatcher.js +18 -25
- package/clients/dispatch/dispatcher.test.ts +2 -1
- package/clients/dispatch/dispatcher.ts +17 -28
- package/clients/dispatch/plan.js +77 -32
- package/clients/dispatch/plan.ts +78 -32
- package/clients/dispatch/runners/ast-grep-napi.js +36 -376
- package/clients/dispatch/runners/ast-grep-napi.ts +60 -433
- package/clients/dispatch/runners/index.js +8 -4
- package/clients/dispatch/runners/index.ts +8 -4
- package/clients/dispatch/runners/lsp.js +65 -0
- package/clients/dispatch/runners/lsp.ts +125 -0
- package/clients/dispatch/runners/oxlint.js +2 -2
- package/clients/dispatch/runners/oxlint.ts +2 -2
- package/clients/dispatch/runners/pyright.js +24 -8
- package/clients/dispatch/runners/pyright.ts +28 -14
- package/clients/dispatch/runners/rust-clippy.js +2 -2
- package/clients/dispatch/runners/rust-clippy.ts +2 -4
- package/clients/dispatch/runners/tree-sitter.js +14 -2
- package/clients/dispatch/runners/tree-sitter.ts +15 -2
- package/clients/dispatch/runners/ts-lsp.js +3 -3
- package/clients/dispatch/runners/ts-lsp.ts +8 -5
- package/clients/dispatch/runners/yaml-rule-parser.js +292 -0
- package/clients/dispatch/runners/yaml-rule-parser.ts +338 -0
- package/clients/dispatch/types.js +3 -0
- package/clients/dispatch/types.ts +3 -0
- package/clients/formatters.js +67 -14
- package/clients/formatters.ts +68 -15
- package/clients/installer/index.js +78 -10
- package/clients/installer/index.ts +519 -426
- package/clients/jscpd-client.js +28 -0
- package/clients/jscpd-client.ts +41 -3
- package/clients/knip-client.js +30 -1
- package/clients/knip-client.ts +34 -2
- package/clients/lsp/__tests__/client.test.ts +64 -41
- package/clients/lsp/__tests__/config.test.ts +25 -17
- package/clients/lsp/__tests__/launch.test.ts +108 -43
- package/clients/lsp/__tests__/service.test.ts +76 -48
- package/clients/lsp/client.js +87 -2
- package/clients/lsp/client.ts +150 -6
- package/clients/lsp/config.js +8 -11
- package/clients/lsp/config.ts +24 -21
- package/clients/lsp/index.js +69 -0
- package/clients/lsp/index.ts +82 -0
- package/clients/lsp/interactive-install.js +19 -8
- package/clients/lsp/interactive-install.ts +52 -27
- package/clients/lsp/launch.js +182 -32
- package/clients/lsp/launch.ts +241 -38
- package/clients/lsp/path-utils.js +3 -46
- package/clients/lsp/path-utils.ts +11 -51
- package/clients/lsp/server.js +93 -71
- package/clients/lsp/server.ts +173 -131
- package/clients/path-utils.js +142 -0
- package/clients/path-utils.ts +153 -0
- package/clients/ruff-client.js +33 -4
- package/clients/ruff-client.ts +44 -13
- package/clients/safe-spawn.js +3 -1
- package/clients/safe-spawn.ts +3 -1
- package/clients/services/effect-integration.js +11 -7
- package/clients/services/effect-integration.ts +34 -26
- package/clients/sg-runner.js +51 -9
- package/clients/sg-runner.ts +58 -15
- package/clients/tree-sitter-client.js +12 -0
- package/clients/tree-sitter-client.ts +12 -0
- package/clients/typescript-client.js +6 -2
- package/clients/typescript-client.ts +9 -2
- package/commands/booboo.js +2 -4
- package/commands/booboo.ts +2 -4
- package/index.ts +377 -93
- package/package.json +2 -1
- package/rules/tree-sitter-queries/tsx/no-nested-links.yml +45 -0
- package/rules/tree-sitter-queries/typescript/constructor-super.yml +55 -0
- package/rules/tree-sitter-queries/typescript/debugger.yml +1 -1
- package/rules/tree-sitter-queries/typescript/no-dupe-class-members.yml +47 -0
- package/tsconfig.json +1 -1
- package/clients/__tests__/file-time.test.js +0 -216
- package/clients/__tests__/format-service.test.js +0 -245
- package/clients/__tests__/formatters.test.js +0 -271
- package/clients/agent-behavior-client.test.js +0 -94
- package/clients/ast-grep-client.test.js +0 -129
- package/clients/ast-grep-client.test.ts +0 -155
- package/clients/biome-client.test.js +0 -144
- package/clients/cache-manager.test.js +0 -197
- package/clients/complexity-client.test.js +0 -234
- package/clients/dependency-checker.test.js +0 -60
- package/clients/dispatch/__tests__/autofix-integration.test.js +0 -245
- package/clients/dispatch/__tests__/runner-registration.test.js +0 -236
- package/clients/dispatch/dispatcher.edge.test.js +0 -82
- package/clients/dispatch/dispatcher.format.test.js +0 -46
- package/clients/dispatch/dispatcher.inline.test.js +0 -74
- package/clients/dispatch/dispatcher.test.js +0 -115
- package/clients/dispatch/runners/architect.test.js +0 -138
- package/clients/dispatch/runners/ast-grep-napi.test.js +0 -106
- package/clients/dispatch/runners/oxlint.test.js +0 -230
- package/clients/dispatch/runners/pyright.test.js +0 -98
- package/clients/dispatch/runners/python-slop.test.js +0 -203
- package/clients/dispatch/runners/scan_codebase.test.js +0 -89
- package/clients/dispatch/runners/shellcheck.test.js +0 -98
- package/clients/dispatch/runners/spellcheck.test.js +0 -158
- package/clients/dispatch/runners/ts-slop.test.js +0 -180
- package/clients/dispatch/runners/ts-slop.test.ts +0 -230
- package/clients/dogfood.test.js +0 -201
- package/clients/file-kinds.test.js +0 -169
- package/clients/go-client.test.js +0 -127
- package/clients/jscpd-client.test.js +0 -127
- package/clients/knip-client.test.js +0 -112
- package/clients/lsp/__tests__/client.test.js +0 -325
- package/clients/lsp/__tests__/config.test.js +0 -166
- package/clients/lsp/__tests__/error-recovery.test.js +0 -213
- package/clients/lsp/__tests__/integration.test.js +0 -127
- package/clients/lsp/__tests__/launch.test.js +0 -260
- package/clients/lsp/__tests__/server.test.js +0 -259
- package/clients/lsp/__tests__/service.test.js +0 -417
- package/clients/metrics-client.test.js +0 -141
- package/clients/ruff-client.test.js +0 -132
- package/clients/rust-client.test.js +0 -108
- package/clients/sanitize.test.js +0 -177
- package/clients/secrets-scanner.test.js +0 -100
- package/clients/services/__tests__/effect-integration.test.js +0 -86
- package/clients/test-runner-client.test.js +0 -192
- package/clients/todo-scanner.test.js +0 -301
- package/clients/type-coverage-client.test.js +0 -105
- package/clients/typescript-client.codefix.test.js +0 -157
- package/clients/typescript-client.test.js +0 -105
- package/commands/clients/ast-grep-client.js +0 -250
- package/commands/clients/ast-grep-parser.js +0 -86
- package/commands/clients/ast-grep-rule-manager.js +0 -91
- package/commands/clients/ast-grep-types.js +0 -9
- package/commands/clients/biome-client.js +0 -380
- package/commands/clients/complexity-client.js +0 -667
- package/commands/clients/file-kinds.js +0 -177
- package/commands/clients/file-utils.js +0 -40
- package/commands/clients/jscpd-client.js +0 -169
- package/commands/clients/knip-client.js +0 -211
- package/commands/clients/ruff-client.js +0 -297
- package/commands/clients/safe-spawn.js +0 -88
- package/commands/clients/scan-utils.js +0 -83
- package/commands/clients/sg-runner.js +0 -190
- package/commands/clients/types.js +0 -11
- package/commands/clients/typescript-client.js +0 -505
- package/commands/rate.test.js +0 -119
- package/rules/ast-grep-rules/rules/no-dangerously-set-inner-html.yml +0 -13
- package/rules/ast-grep-rules/rules/no-debugger.yml +0 -12
- 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
|
|
@@ -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
|
@@ -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
|
-
});
|