pi-lens 3.8.39 → 3.8.41

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 (65) hide show
  1. package/CHANGELOG.md +84 -5
  2. package/README.md +37 -1
  3. package/clients/biome-client.ts +5 -4
  4. package/clients/cache/rule-cache.ts +1 -1
  5. package/clients/complexity-client.ts +1 -1
  6. package/clients/dependency-checker.ts +1 -1
  7. package/clients/dispatch/diagnostic-taxonomy.ts +13 -1
  8. package/clients/dispatch/dispatcher.ts +9 -0
  9. package/clients/dispatch/fact-scheduler.ts +1 -1
  10. package/clients/dispatch/integration.ts +58 -3
  11. package/clients/dispatch/runners/index.ts +2 -0
  12. package/clients/dispatch/runners/semgrep.ts +269 -0
  13. package/clients/dispatch/runners/shellcheck.ts +2 -8
  14. package/clients/dispatch/runners/tree-sitter.ts +32 -11
  15. package/clients/dispatch/tool-profile.ts +1 -0
  16. package/clients/format-service.ts +10 -0
  17. package/clients/formatters.ts +22 -8
  18. package/clients/installer/index.ts +3 -3
  19. package/clients/knip-client.ts +360 -362
  20. package/clients/lsp/aggregation.ts +91 -0
  21. package/clients/lsp/client.ts +91 -38
  22. package/clients/lsp/index.ts +88 -72
  23. package/clients/lsp/launch.ts +107 -34
  24. package/clients/lsp/server-strategies.ts +71 -0
  25. package/clients/lsp/server.ts +76 -57
  26. package/clients/path-utils.ts +17 -0
  27. package/clients/pipeline.ts +23 -5
  28. package/clients/production-readiness.ts +2 -2
  29. package/clients/read-guard-logger.ts +41 -1
  30. package/clients/read-guard-tool-lines.ts +17 -4
  31. package/clients/read-guard.ts +95 -46
  32. package/clients/runtime-agent-end.ts +3 -0
  33. package/clients/runtime-session.ts +5 -0
  34. package/clients/runtime-tool-result.ts +48 -1
  35. package/clients/runtime-turn.ts +48 -4
  36. package/clients/sanitize.ts +1 -1
  37. package/clients/semgrep-config.ts +213 -0
  38. package/clients/tool-policy.ts +1982 -1936
  39. package/clients/tree-sitter-client.ts +1 -1
  40. package/clients/widget-state.ts +283 -0
  41. package/commands/booboo.ts +34 -2
  42. package/index.ts +231 -17
  43. package/package.json +3 -2
  44. package/rules/rule-catalog.json +25 -1
  45. package/rules/tree-sitter-queries/cobol/lock-table-cobol.yml +35 -0
  46. package/rules/tree-sitter-queries/cpp/unnecessary-bit-ops.yml +58 -0
  47. package/rules/tree-sitter-queries/java/infinite-loop.yml +58 -0
  48. package/rules/tree-sitter-queries/java/infinite-recursion.yml +58 -0
  49. package/rules/tree-sitter-queries/java/mockito-initialized.yml +66 -0
  50. package/rules/tree-sitter-queries/java/name-capitalization-conflict.yml +54 -0
  51. package/rules/tree-sitter-queries/java/no-octal-values.yml +48 -0
  52. package/rules/tree-sitter-queries/java/resources-closed.yml +57 -0
  53. package/rules/tree-sitter-queries/java/short-circuit-logic.yml +57 -0
  54. package/rules/tree-sitter-queries/java/tests-include-assertions.yml +60 -0
  55. package/rules/tree-sitter-queries/java/unnecessary-bit-ops-java.yml +57 -0
  56. package/rules/tree-sitter-queries/javascript/switch-case-termination-js.yml +64 -0
  57. package/rules/tree-sitter-queries/plsql/lock-table.yml +42 -0
  58. package/rules/tree-sitter-queries/plsql/nchar-nvarchar2-bytes.yml +54 -0
  59. package/rules/tree-sitter-queries/python/no-super-torchscript.yml +52 -0
  60. package/rules/tree-sitter-queries/typescript/default-not-last.yml +54 -0
  61. package/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +51 -0
  62. package/rules/tree-sitter-queries/typescript/empty-switch-case.yml +54 -0
  63. package/rules/tree-sitter-queries/typescript/infinite-loop.yml +55 -0
  64. package/rules/tree-sitter-queries/typescript/self-assignment.yml +46 -0
  65. package/rules/tree-sitter-queries/typescript/switch-case-termination.yml +64 -0
@@ -1,1936 +1,1982 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import { logLatency } from "./latency-logger.js";
4
-
5
- export type ToolGate = "config-first" | "smart-default" | "mixed";
6
-
7
- export interface FormatterPolicy {
8
- formatterNames: string[];
9
- defaultFormatter?: string;
10
- defaultWhenUnconfigured: boolean;
11
- gate: ToolGate;
12
- }
13
-
14
- const FORMATTER_POLICY_BY_EXTENSION = new Map<string, FormatterPolicy>([
15
- [
16
- ".js",
17
- {
18
- formatterNames: ["biome", "prettier", "oxfmt"],
19
- defaultFormatter: "biome",
20
- defaultWhenUnconfigured: true,
21
- gate: "smart-default",
22
- },
23
- ],
24
- [
25
- ".jsx",
26
- {
27
- formatterNames: ["biome", "prettier", "oxfmt"],
28
- defaultFormatter: "biome",
29
- defaultWhenUnconfigured: true,
30
- gate: "smart-default",
31
- },
32
- ],
33
- [
34
- ".mjs",
35
- {
36
- formatterNames: ["biome", "prettier", "oxfmt"],
37
- defaultFormatter: "biome",
38
- defaultWhenUnconfigured: true,
39
- gate: "smart-default",
40
- },
41
- ],
42
- [
43
- ".cjs",
44
- {
45
- formatterNames: ["biome", "prettier", "oxfmt"],
46
- defaultFormatter: "biome",
47
- defaultWhenUnconfigured: true,
48
- gate: "smart-default",
49
- },
50
- ],
51
- [
52
- ".ts",
53
- {
54
- formatterNames: ["biome", "prettier", "oxfmt"],
55
- defaultFormatter: "biome",
56
- defaultWhenUnconfigured: true,
57
- gate: "smart-default",
58
- },
59
- ],
60
- [
61
- ".tsx",
62
- {
63
- formatterNames: ["biome", "prettier", "oxfmt"],
64
- defaultFormatter: "biome",
65
- defaultWhenUnconfigured: true,
66
- gate: "smart-default",
67
- },
68
- ],
69
- [
70
- ".mts",
71
- {
72
- formatterNames: ["biome", "prettier", "oxfmt"],
73
- defaultFormatter: "biome",
74
- defaultWhenUnconfigured: true,
75
- gate: "smart-default",
76
- },
77
- ],
78
- [
79
- ".cts",
80
- {
81
- formatterNames: ["biome", "prettier", "oxfmt"],
82
- defaultFormatter: "biome",
83
- defaultWhenUnconfigured: true,
84
- gate: "smart-default",
85
- },
86
- ],
87
- [
88
- ".py",
89
- {
90
- formatterNames: ["black", "ruff"],
91
- defaultFormatter: "ruff",
92
- defaultWhenUnconfigured: true,
93
- gate: "smart-default",
94
- },
95
- ],
96
- [
97
- ".pyi",
98
- {
99
- formatterNames: ["black", "ruff"],
100
- defaultFormatter: "ruff",
101
- defaultWhenUnconfigured: true,
102
- gate: "smart-default",
103
- },
104
- ],
105
- [
106
- ".json",
107
- {
108
- formatterNames: ["biome", "prettier"],
109
- defaultFormatter: "biome",
110
- defaultWhenUnconfigured: false,
111
- gate: "mixed",
112
- },
113
- ],
114
- [
115
- ".jsonc",
116
- {
117
- formatterNames: ["biome", "prettier"],
118
- defaultFormatter: "biome",
119
- defaultWhenUnconfigured: false,
120
- gate: "mixed",
121
- },
122
- ],
123
- [
124
- ".css",
125
- {
126
- formatterNames: ["biome", "prettier", "oxfmt"],
127
- defaultFormatter: "biome",
128
- defaultWhenUnconfigured: true,
129
- gate: "smart-default",
130
- },
131
- ],
132
- [
133
- ".scss",
134
- {
135
- formatterNames: ["biome", "prettier", "oxfmt"],
136
- defaultFormatter: "biome",
137
- defaultWhenUnconfigured: true,
138
- gate: "smart-default",
139
- },
140
- ],
141
- [
142
- ".sass",
143
- {
144
- formatterNames: ["biome", "prettier", "oxfmt"],
145
- defaultFormatter: "biome",
146
- defaultWhenUnconfigured: true,
147
- gate: "smart-default",
148
- },
149
- ],
150
- [
151
- ".less",
152
- {
153
- formatterNames: ["prettier"],
154
- defaultFormatter: "prettier",
155
- defaultWhenUnconfigured: true,
156
- gate: "smart-default",
157
- },
158
- ],
159
- [
160
- ".html",
161
- {
162
- formatterNames: ["prettier"],
163
- defaultFormatter: "prettier",
164
- defaultWhenUnconfigured: true,
165
- gate: "smart-default",
166
- },
167
- ],
168
- [
169
- ".htm",
170
- {
171
- formatterNames: ["prettier"],
172
- defaultFormatter: "prettier",
173
- defaultWhenUnconfigured: true,
174
- gate: "smart-default",
175
- },
176
- ],
177
- [
178
- ".yaml",
179
- {
180
- formatterNames: ["prettier"],
181
- defaultFormatter: "prettier",
182
- defaultWhenUnconfigured: true,
183
- gate: "smart-default",
184
- },
185
- ],
186
- [
187
- ".yml",
188
- {
189
- formatterNames: ["prettier"],
190
- defaultFormatter: "prettier",
191
- defaultWhenUnconfigured: true,
192
- gate: "smart-default",
193
- },
194
- ],
195
- [
196
- ".md",
197
- {
198
- formatterNames: ["prettier"],
199
- defaultFormatter: "prettier",
200
- defaultWhenUnconfigured: true,
201
- gate: "smart-default",
202
- },
203
- ],
204
- [
205
- ".mdx",
206
- {
207
- formatterNames: ["prettier"],
208
- defaultFormatter: "prettier",
209
- defaultWhenUnconfigured: true,
210
- gate: "smart-default",
211
- },
212
- ],
213
- [
214
- ".graphql",
215
- {
216
- formatterNames: ["prettier"],
217
- defaultFormatter: "prettier",
218
- defaultWhenUnconfigured: true,
219
- gate: "smart-default",
220
- },
221
- ],
222
- [
223
- ".gql",
224
- {
225
- formatterNames: ["prettier"],
226
- defaultFormatter: "prettier",
227
- defaultWhenUnconfigured: true,
228
- gate: "smart-default",
229
- },
230
- ],
231
- [
232
- ".kt",
233
- {
234
- formatterNames: ["ktlint"],
235
- defaultFormatter: "ktlint",
236
- defaultWhenUnconfigured: true,
237
- gate: "smart-default",
238
- },
239
- ],
240
- [
241
- ".kts",
242
- {
243
- formatterNames: ["ktlint"],
244
- defaultFormatter: "ktlint",
245
- defaultWhenUnconfigured: true,
246
- gate: "smart-default",
247
- },
248
- ],
249
- [
250
- ".swift",
251
- {
252
- formatterNames: ["swiftformat"],
253
- defaultFormatter: "swiftformat",
254
- defaultWhenUnconfigured: true,
255
- gate: "smart-default",
256
- },
257
- ],
258
- [
259
- ".fs",
260
- {
261
- formatterNames: ["fantomas"],
262
- defaultFormatter: "fantomas",
263
- defaultWhenUnconfigured: true,
264
- gate: "smart-default",
265
- },
266
- ],
267
- [
268
- ".fsi",
269
- {
270
- formatterNames: ["fantomas"],
271
- defaultFormatter: "fantomas",
272
- defaultWhenUnconfigured: true,
273
- gate: "smart-default",
274
- },
275
- ],
276
- [
277
- ".fsx",
278
- {
279
- formatterNames: ["fantomas"],
280
- defaultFormatter: "fantomas",
281
- defaultWhenUnconfigured: true,
282
- gate: "smart-default",
283
- },
284
- ],
285
- [
286
- ".nix",
287
- {
288
- formatterNames: ["nixfmt"],
289
- defaultFormatter: "nixfmt",
290
- defaultWhenUnconfigured: true,
291
- gate: "smart-default",
292
- },
293
- ],
294
- [
295
- ".ex",
296
- {
297
- formatterNames: ["mix"],
298
- defaultFormatter: "mix",
299
- defaultWhenUnconfigured: true,
300
- gate: "smart-default",
301
- },
302
- ],
303
- [
304
- ".exs",
305
- {
306
- formatterNames: ["mix"],
307
- defaultFormatter: "mix",
308
- defaultWhenUnconfigured: true,
309
- gate: "smart-default",
310
- },
311
- ],
312
- [
313
- ".eex",
314
- {
315
- formatterNames: ["mix"],
316
- defaultFormatter: "mix",
317
- defaultWhenUnconfigured: true,
318
- gate: "smart-default",
319
- },
320
- ],
321
- [
322
- ".heex",
323
- {
324
- formatterNames: ["mix"],
325
- defaultFormatter: "mix",
326
- defaultWhenUnconfigured: true,
327
- gate: "smart-default",
328
- },
329
- ],
330
- [
331
- ".leex",
332
- {
333
- formatterNames: ["mix"],
334
- defaultFormatter: "mix",
335
- defaultWhenUnconfigured: true,
336
- gate: "smart-default",
337
- },
338
- ],
339
- [
340
- ".gleam",
341
- {
342
- formatterNames: ["gleam"],
343
- defaultFormatter: "gleam",
344
- defaultWhenUnconfigured: true,
345
- gate: "smart-default",
346
- },
347
- ],
348
- [
349
- ".c",
350
- {
351
- formatterNames: ["clang-format"],
352
- defaultFormatter: "clang-format",
353
- defaultWhenUnconfigured: false,
354
- gate: "config-first",
355
- },
356
- ],
357
- [
358
- ".cc",
359
- {
360
- formatterNames: ["clang-format"],
361
- defaultFormatter: "clang-format",
362
- defaultWhenUnconfigured: false,
363
- gate: "config-first",
364
- },
365
- ],
366
- [
367
- ".cpp",
368
- {
369
- formatterNames: ["clang-format"],
370
- defaultFormatter: "clang-format",
371
- defaultWhenUnconfigured: false,
372
- gate: "config-first",
373
- },
374
- ],
375
- [
376
- ".cxx",
377
- {
378
- formatterNames: ["clang-format"],
379
- defaultFormatter: "clang-format",
380
- defaultWhenUnconfigured: false,
381
- gate: "config-first",
382
- },
383
- ],
384
- [
385
- ".h",
386
- {
387
- formatterNames: ["clang-format"],
388
- defaultFormatter: "clang-format",
389
- defaultWhenUnconfigured: false,
390
- gate: "config-first",
391
- },
392
- ],
393
- [
394
- ".hpp",
395
- {
396
- formatterNames: ["clang-format"],
397
- defaultFormatter: "clang-format",
398
- defaultWhenUnconfigured: false,
399
- gate: "config-first",
400
- },
401
- ],
402
- [
403
- ".ino",
404
- {
405
- formatterNames: ["clang-format"],
406
- defaultFormatter: "clang-format",
407
- defaultWhenUnconfigured: false,
408
- gate: "config-first",
409
- },
410
- ],
411
- [
412
- ".php",
413
- {
414
- formatterNames: ["php-cs-fixer"],
415
- defaultFormatter: "php-cs-fixer",
416
- defaultWhenUnconfigured: false,
417
- gate: "config-first",
418
- },
419
- ],
420
- [
421
- ".cs",
422
- {
423
- formatterNames: ["csharpier"],
424
- defaultFormatter: "csharpier",
425
- defaultWhenUnconfigured: true,
426
- gate: "smart-default",
427
- },
428
- ],
429
- [
430
- ".lua",
431
- {
432
- formatterNames: ["stylua"],
433
- defaultFormatter: "stylua",
434
- defaultWhenUnconfigured: false,
435
- gate: "config-first",
436
- },
437
- ],
438
- [
439
- ".hs",
440
- {
441
- formatterNames: ["ormolu"],
442
- defaultFormatter: "ormolu",
443
- defaultWhenUnconfigured: true,
444
- gate: "smart-default",
445
- },
446
- ],
447
- [
448
- ".lhs",
449
- {
450
- formatterNames: ["ormolu"],
451
- defaultFormatter: "ormolu",
452
- defaultWhenUnconfigured: true,
453
- gate: "smart-default",
454
- },
455
- ],
456
- [
457
- ".ml",
458
- {
459
- formatterNames: ["ocamlformat"],
460
- defaultFormatter: "ocamlformat",
461
- defaultWhenUnconfigured: false,
462
- gate: "config-first",
463
- },
464
- ],
465
- [
466
- ".mli",
467
- {
468
- formatterNames: ["ocamlformat"],
469
- defaultFormatter: "ocamlformat",
470
- defaultWhenUnconfigured: false,
471
- gate: "config-first",
472
- },
473
- ],
474
- [
475
- ".go",
476
- {
477
- formatterNames: ["gofmt"],
478
- defaultFormatter: "gofmt",
479
- defaultWhenUnconfigured: true,
480
- gate: "smart-default",
481
- },
482
- ],
483
- [
484
- ".rs",
485
- {
486
- formatterNames: ["rustfmt"],
487
- defaultFormatter: "rustfmt",
488
- defaultWhenUnconfigured: true,
489
- gate: "smart-default",
490
- },
491
- ],
492
- [
493
- ".sh",
494
- {
495
- formatterNames: ["shfmt"],
496
- defaultFormatter: "shfmt",
497
- defaultWhenUnconfigured: true,
498
- gate: "smart-default",
499
- },
500
- ],
501
- [
502
- ".bash",
503
- {
504
- formatterNames: ["shfmt"],
505
- defaultFormatter: "shfmt",
506
- defaultWhenUnconfigured: true,
507
- gate: "smart-default",
508
- },
509
- ],
510
- [
511
- ".toml",
512
- {
513
- formatterNames: ["taplo"],
514
- defaultFormatter: "taplo",
515
- defaultWhenUnconfigured: true,
516
- gate: "smart-default",
517
- },
518
- ],
519
- [
520
- ".tf",
521
- {
522
- formatterNames: ["terraform"],
523
- defaultFormatter: "terraform",
524
- defaultWhenUnconfigured: true,
525
- gate: "smart-default",
526
- },
527
- ],
528
- [
529
- ".tfvars",
530
- {
531
- formatterNames: ["terraform"],
532
- defaultFormatter: "terraform",
533
- defaultWhenUnconfigured: true,
534
- gate: "smart-default",
535
- },
536
- ],
537
- [
538
- ".dart",
539
- {
540
- formatterNames: ["dart"],
541
- defaultFormatter: "dart",
542
- defaultWhenUnconfigured: true,
543
- gate: "smart-default",
544
- },
545
- ],
546
- [
547
- ".zig",
548
- {
549
- formatterNames: ["zig"],
550
- defaultFormatter: "zig",
551
- defaultWhenUnconfigured: true,
552
- gate: "smart-default",
553
- },
554
- ],
555
- [
556
- ".zon",
557
- {
558
- formatterNames: ["zig"],
559
- defaultFormatter: "zig",
560
- defaultWhenUnconfigured: true,
561
- gate: "smart-default",
562
- },
563
- ],
564
- [
565
- ".java",
566
- {
567
- formatterNames: ["google-java-format"],
568
- defaultFormatter: "google-java-format",
569
- defaultWhenUnconfigured: false,
570
- gate: "config-first",
571
- },
572
- ],
573
- [
574
- ".clj",
575
- {
576
- formatterNames: ["cljfmt"],
577
- defaultFormatter: "cljfmt",
578
- defaultWhenUnconfigured: false,
579
- gate: "config-first",
580
- },
581
- ],
582
- [
583
- ".cljc",
584
- {
585
- formatterNames: ["cljfmt"],
586
- defaultFormatter: "cljfmt",
587
- defaultWhenUnconfigured: false,
588
- gate: "config-first",
589
- },
590
- ],
591
- [
592
- ".cljs",
593
- {
594
- formatterNames: ["cljfmt"],
595
- defaultFormatter: "cljfmt",
596
- defaultWhenUnconfigured: false,
597
- gate: "config-first",
598
- },
599
- ],
600
- [
601
- ".cmake",
602
- {
603
- formatterNames: ["cmake-format"],
604
- defaultFormatter: "cmake-format",
605
- defaultWhenUnconfigured: false,
606
- gate: "config-first",
607
- },
608
- ],
609
- [
610
- ".ps1",
611
- {
612
- formatterNames: ["psscriptanalyzer-format"],
613
- defaultFormatter: "psscriptanalyzer-format",
614
- defaultWhenUnconfigured: true,
615
- gate: "smart-default",
616
- },
617
- ],
618
- [
619
- ".psm1",
620
- {
621
- formatterNames: ["psscriptanalyzer-format"],
622
- defaultFormatter: "psscriptanalyzer-format",
623
- defaultWhenUnconfigured: true,
624
- gate: "smart-default",
625
- },
626
- ],
627
- [
628
- ".psd1",
629
- {
630
- formatterNames: ["psscriptanalyzer-format"],
631
- defaultFormatter: "psscriptanalyzer-format",
632
- defaultWhenUnconfigured: true,
633
- gate: "smart-default",
634
- },
635
- ],
636
- ]);
637
-
638
- const AUTO_INSTALLABLE_DEFAULT_FORMATTERS = new Map<string, string>([
639
- ["biome", "biome"],
640
- ["ruff", "ruff"],
641
- ["prettier", "prettier"],
642
- ["shfmt", "shfmt"],
643
- ["taplo", "taplo"],
644
- ["ktlint", "ktlint"],
645
- ]);
646
-
647
- export function getFormatterPolicyForExtension(
648
- ext: string,
649
- ): FormatterPolicy | undefined {
650
- return FORMATTER_POLICY_BY_EXTENSION.get(ext.toLowerCase());
651
- }
652
-
653
- export function getFormatterPolicyForFile(
654
- filePath: string,
655
- ): FormatterPolicy | undefined {
656
- return getFormatterPolicyForExtension(path.extname(filePath));
657
- }
658
-
659
- export function getSmartDefaultFormatterName(
660
- filePath: string,
661
- ): string | undefined {
662
- const policy = getFormatterPolicyForFile(filePath);
663
- if (!policy?.defaultWhenUnconfigured) return undefined;
664
- return policy.defaultFormatter;
665
- }
666
-
667
- export function getAutoInstallToolIdForFormatter(
668
- formatterName: string,
669
- ): string | undefined {
670
- return AUTO_INSTALLABLE_DEFAULT_FORMATTERS.get(formatterName);
671
- }
672
-
673
- export function getToolExecutionPolicy(
674
- toolId: string,
675
- ): ToolExecutionPolicy | undefined {
676
- return TOOL_EXECUTION_POLICY.get(toolId);
677
- }
678
-
679
- export function shouldAutoInstallTool(toolId: string): boolean {
680
- return getToolExecutionPolicy(toolId)?.autoInstall ?? false;
681
- }
682
-
683
- export function getAutofixCapability(
684
- toolId: string,
685
- ): AutofixCapability | undefined {
686
- return AUTOFIX_CAPABILITIES.get(toolId);
687
- }
688
-
689
- export function canToolAutoFix(toolId: string): boolean {
690
- return getAutofixCapability(toolId)?.toolSupportsFix ?? false;
691
- }
692
-
693
- export function isSafePipelineAutofixTool(toolId: string): boolean {
694
- return getAutofixCapability(toolId)?.safePipelineAutofix ?? false;
695
- }
696
-
697
- export function getToolCommandSpec(
698
- toolId: string,
699
- ): ToolCommandSpec | undefined {
700
- return TOOL_COMMAND_SPECS.get(toolId);
701
- }
702
-
703
- export type AutofixToolName =
704
- | "biome"
705
- | "eslint"
706
- | "ruff"
707
- | "stylelint"
708
- | "sqlfluff"
709
- | "rubocop"
710
- | "ktlint"
711
- | "rust-clippy"
712
- | "dart-analyze";
713
-
714
- export type LintRunnerName =
715
- | JstsLintRunnerName
716
- | "ruff-lint"
717
- | "stylelint"
718
- | "sqlfluff"
719
- | "rubocop"
720
- | "yamllint"
721
- | "markdownlint"
722
- | "htmlhint"
723
- | "hadolint"
724
- | "golangci-lint"
725
- | "phpstan"
726
- | "ktlint"
727
- | "taplo"
728
- | "rust-clippy"
729
- | "shellcheck"
730
- | "tflint"
731
- | "credo"
732
- | "cpp-check"
733
- | "dart-analyze"
734
- | "gleam-check"
735
- | "psscriptanalyzer"
736
- | "prisma-validate"
737
- | "mypy"
738
- | "detekt";
739
-
740
- export interface LinterPolicy {
741
- runnerNames: LintRunnerName[];
742
- preferredRunners: LintRunnerName[];
743
- defaultRunner?: LintRunnerName;
744
- defaultWhenUnconfigured: boolean;
745
- gate: ToolGate;
746
- }
747
-
748
- export interface AutofixPolicy {
749
- toolNames: AutofixToolName[];
750
- preferredTools: AutofixToolName[];
751
- defaultTool?: AutofixToolName;
752
- defaultWhenUnconfigured: boolean;
753
- gate: ToolGate;
754
- safe: boolean;
755
- }
756
-
757
- export interface AutofixCapability {
758
- toolSupportsFix: boolean;
759
- safePipelineAutofix: boolean;
760
- fixKind: "pipeline" | "manual" | "suggestion" | "none";
761
- }
762
-
763
- export interface ToolExecutionPolicy {
764
- gate: ToolGate;
765
- autoInstall: boolean;
766
- }
767
-
768
- export interface ToolCommandSpec {
769
- command: string;
770
- windowsExt?: string;
771
- versionArgs?: string[];
772
- managedToolId?: string;
773
- }
774
-
775
- const AUTOFIX_CAPABILITIES = new Map<string, AutofixCapability>([
776
- [
777
- "biome",
778
- { toolSupportsFix: true, safePipelineAutofix: true, fixKind: "pipeline" },
779
- ],
780
- [
781
- "eslint",
782
- { toolSupportsFix: true, safePipelineAutofix: true, fixKind: "pipeline" },
783
- ],
784
- [
785
- "ruff",
786
- { toolSupportsFix: true, safePipelineAutofix: true, fixKind: "pipeline" },
787
- ],
788
- [
789
- "stylelint",
790
- { toolSupportsFix: true, safePipelineAutofix: true, fixKind: "pipeline" },
791
- ],
792
- [
793
- "sqlfluff",
794
- { toolSupportsFix: true, safePipelineAutofix: true, fixKind: "pipeline" },
795
- ],
796
- [
797
- "rubocop",
798
- { toolSupportsFix: true, safePipelineAutofix: true, fixKind: "pipeline" },
799
- ],
800
- [
801
- "ktlint",
802
- { toolSupportsFix: true, safePipelineAutofix: true, fixKind: "pipeline" },
803
- ],
804
- [
805
- "rust-clippy",
806
- { toolSupportsFix: true, safePipelineAutofix: true, fixKind: "pipeline" },
807
- ],
808
- [
809
- "dart-analyze",
810
- { toolSupportsFix: true, safePipelineAutofix: true, fixKind: "pipeline" },
811
- ],
812
- ]);
813
-
814
- const TOOL_EXECUTION_POLICY = new Map<string, ToolExecutionPolicy>([
815
- ["biome", { gate: "smart-default", autoInstall: true }],
816
- ["ruff", { gate: "smart-default", autoInstall: true }],
817
- ["oxlint", { gate: "smart-default", autoInstall: true }],
818
- ["stylelint", { gate: "smart-default", autoInstall: true }],
819
- ["sqlfluff", { gate: "smart-default", autoInstall: true }],
820
- ["rubocop", { gate: "smart-default", autoInstall: true }],
821
- ["yamllint", { gate: "smart-default", autoInstall: true }],
822
- ["markdownlint", { gate: "smart-default", autoInstall: true }],
823
- ["mypy", { gate: "config-first", autoInstall: true }],
824
- ["taplo", { gate: "smart-default", autoInstall: true }],
825
- ["hadolint", { gate: "smart-default", autoInstall: true }],
826
- ["htmlhint", { gate: "smart-default", autoInstall: true }],
827
- ["ktlint", { gate: "smart-default", autoInstall: true }],
828
- ["golangci-lint", { gate: "config-first", autoInstall: true }],
829
- ["phpstan", { gate: "config-first", autoInstall: false }],
830
- ["eslint", { gate: "config-first", autoInstall: false }],
831
- ["prettier", { gate: "smart-default", autoInstall: true }],
832
- ]);
833
-
834
- const TOOL_COMMAND_SPECS = new Map<string, ToolCommandSpec>([
835
- [
836
- "eslint",
837
- {
838
- command: "eslint",
839
- windowsExt: ".cmd",
840
- versionArgs: ["--version"],
841
- managedToolId: "eslint",
842
- },
843
- ],
844
- [
845
- "stylelint",
846
- {
847
- command: "stylelint",
848
- windowsExt: ".cmd",
849
- versionArgs: ["--version"],
850
- managedToolId: "stylelint",
851
- },
852
- ],
853
- [
854
- "sqlfluff",
855
- {
856
- command: "sqlfluff",
857
- windowsExt: ".exe",
858
- versionArgs: ["--version"],
859
- managedToolId: "sqlfluff",
860
- },
861
- ],
862
- [
863
- "oxlint",
864
- {
865
- command: "oxlint",
866
- windowsExt: ".exe",
867
- versionArgs: ["--version"],
868
- managedToolId: "oxlint",
869
- },
870
- ],
871
- [
872
- "ruff",
873
- {
874
- command: "ruff",
875
- windowsExt: ".exe",
876
- versionArgs: ["--version"],
877
- managedToolId: "ruff",
878
- },
879
- ],
880
- [
881
- "biome",
882
- {
883
- command: "biome",
884
- windowsExt: ".cmd",
885
- versionArgs: ["--version"],
886
- managedToolId: "biome",
887
- },
888
- ],
889
- [
890
- "rubocop",
891
- {
892
- command: "rubocop",
893
- versionArgs: ["--version"],
894
- managedToolId: "rubocop",
895
- },
896
- ],
897
- [
898
- "yamllint",
899
- {
900
- command: "yamllint",
901
- windowsExt: ".exe",
902
- versionArgs: ["--version"],
903
- managedToolId: "yamllint",
904
- },
905
- ],
906
- [
907
- "markdownlint",
908
- {
909
- command: "markdownlint-cli2",
910
- windowsExt: ".cmd",
911
- versionArgs: ["--version"],
912
- managedToolId: "markdownlint",
913
- },
914
- ],
915
- [
916
- "mypy",
917
- {
918
- command: "mypy",
919
- versionArgs: ["--version"],
920
- managedToolId: "mypy",
921
- },
922
- ],
923
- [
924
- "phpstan",
925
- {
926
- command: "phpstan",
927
- windowsExt: ".bat",
928
- versionArgs: ["--version"],
929
- managedToolId: "phpstan",
930
- },
931
- ],
932
- [
933
- "taplo",
934
- {
935
- command: "taplo",
936
- windowsExt: ".exe",
937
- versionArgs: ["--version"],
938
- managedToolId: "taplo",
939
- },
940
- ],
941
- [
942
- "hadolint",
943
- {
944
- command: "hadolint",
945
- windowsExt: ".exe",
946
- versionArgs: ["--version"],
947
- managedToolId: "hadolint",
948
- },
949
- ],
950
- [
951
- "htmlhint",
952
- {
953
- command: "htmlhint",
954
- versionArgs: ["--version"],
955
- managedToolId: "htmlhint",
956
- },
957
- ],
958
- [
959
- "ktlint",
960
- {
961
- command: "ktlint",
962
- windowsExt: ".exe",
963
- versionArgs: ["--version"],
964
- managedToolId: "ktlint",
965
- },
966
- ],
967
- [
968
- "prettier",
969
- {
970
- command: "prettier",
971
- windowsExt: ".cmd",
972
- versionArgs: ["--version"],
973
- managedToolId: "prettier",
974
- },
975
- ],
976
- ]);
977
-
978
- const STYLELINT_CONFIGS = [
979
- ".stylelintrc",
980
- ".stylelintrc.json",
981
- ".stylelintrc.jsonc",
982
- ".stylelintrc.yaml",
983
- ".stylelintrc.yml",
984
- ".stylelintrc.js",
985
- ".stylelintrc.cjs",
986
- "stylelint.config.js",
987
- "stylelint.config.cjs",
988
- "stylelint.config.mjs",
989
- ];
990
-
991
- const SQLFLUFF_CONFIGS = [
992
- ".sqlfluff",
993
- "pyproject.toml",
994
- "setup.cfg",
995
- "tox.ini",
996
- ];
997
-
998
- const RUBOCOP_CONFIGS = [".rubocop.yml", ".rubocop.yaml"];
999
-
1000
- const MYPY_CONFIGS = ["mypy.ini", ".mypy.ini", "setup.cfg", "pyproject.toml"];
1001
-
1002
- const YAMLLINT_CONFIGS = [
1003
- ".yamllint",
1004
- ".yamllint.yml",
1005
- ".yamllint.yaml",
1006
- "pyproject.toml",
1007
- "setup.cfg",
1008
- "tox.ini",
1009
- ];
1010
-
1011
- const MARKDOWNLINT_CONFIGS = [
1012
- ".markdownlint.json",
1013
- ".markdownlint.jsonc",
1014
- ".markdownlint.yaml",
1015
- ".markdownlint.yml",
1016
- ".markdownlintrc",
1017
- ];
1018
-
1019
- const PRETTIER_CONFIGS = [
1020
- ".prettierrc",
1021
- ".prettierrc.json",
1022
- ".prettierrc.yml",
1023
- ".prettierrc.yaml",
1024
- ".prettierrc.js",
1025
- ".prettierrc.cjs",
1026
- ".prettierrc.mjs",
1027
- "prettier.config.js",
1028
- "prettier.config.cjs",
1029
- "prettier.config.mjs",
1030
- "prettier.config.ts",
1031
- ];
1032
-
1033
- const RUFF_PROJECT_CONFIGS = ["ruff.toml", ".ruff.toml"];
1034
-
1035
- const GOLANGCI_CONFIGS = [
1036
- ".golangci.yml",
1037
- ".golangci.yaml",
1038
- ".golangci.toml",
1039
- ".golangci.json",
1040
- ];
1041
-
1042
- const PHPSTAN_CONFIGS = [
1043
- "phpstan.neon",
1044
- "phpstan.neon.dist",
1045
- "phpstan.dist.neon",
1046
- ];
1047
-
1048
- export type JstsLintRunnerName = "eslint" | "oxlint" | "biome-check-json";
1049
-
1050
- export interface JstsLintPolicyContext {
1051
- hasEslintConfig?: boolean;
1052
- hasOxlintConfig?: boolean;
1053
- hasBiomeConfig?: boolean;
1054
- }
1055
-
1056
- export interface JstsLintPolicy extends Required<JstsLintPolicyContext> {
1057
- preferredRunners: JstsLintRunnerName[];
1058
- hasExplicitNonBiomeLinter: boolean;
1059
- }
1060
-
1061
- export interface LinterPolicyContext {
1062
- hasEslintConfig?: boolean;
1063
- hasOxlintConfig?: boolean;
1064
- hasBiomeConfig?: boolean;
1065
- hasStylelintConfig?: boolean;
1066
- hasSqlfluffConfig?: boolean;
1067
- hasRubocopConfig?: boolean;
1068
- hasYamllintConfig?: boolean;
1069
- hasMarkdownlintConfig?: boolean;
1070
- hasGolangciConfig?: boolean;
1071
- hasPhpstanConfig?: boolean;
1072
- hasMypyConfig?: boolean;
1073
- hasDetektConfig?: boolean;
1074
- }
1075
-
1076
- export interface AutofixPolicyContext {
1077
- hasEslintConfig?: boolean;
1078
- hasStylelintConfig?: boolean;
1079
- hasSqlfluffConfig?: boolean;
1080
- hasRubocopConfig?: boolean;
1081
- hasBiomeConfig?: boolean;
1082
- }
1083
-
1084
- export function getLinterPolicyForFile(
1085
- filePath: string,
1086
- context: LinterPolicyContext = {},
1087
- ): LinterPolicy | undefined {
1088
- const ext = path.extname(filePath).toLowerCase();
1089
-
1090
- if ([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"].includes(ext)) {
1091
- const policy = getJstsLintPolicy({
1092
- hasEslintConfig: context.hasEslintConfig,
1093
- hasOxlintConfig: context.hasOxlintConfig,
1094
- hasBiomeConfig: context.hasBiomeConfig,
1095
- });
1096
- return {
1097
- runnerNames: ["eslint", "oxlint", "biome-check-json"],
1098
- preferredRunners: policy.preferredRunners,
1099
- defaultRunner: policy.preferredRunners[0],
1100
- defaultWhenUnconfigured:
1101
- !policy.hasEslintConfig && !policy.hasOxlintConfig,
1102
- gate: policy.hasEslintConfig ? "config-first" : "smart-default",
1103
- };
1104
- }
1105
-
1106
- if ([".py", ".pyi"].includes(ext)) {
1107
- const preferredRunners: LintRunnerName[] = ["ruff-lint"];
1108
- if (context.hasMypyConfig) preferredRunners.push("mypy");
1109
- return {
1110
- runnerNames: ["ruff-lint", "mypy"],
1111
- preferredRunners,
1112
- defaultRunner: "ruff-lint",
1113
- defaultWhenUnconfigured: true,
1114
- gate: context.hasMypyConfig ? "mixed" : "smart-default",
1115
- };
1116
- }
1117
-
1118
- if ([".css", ".scss", ".sass", ".less"].includes(ext)) {
1119
- return {
1120
- runnerNames: ["stylelint"],
1121
- preferredRunners: ["stylelint"],
1122
- defaultRunner: "stylelint",
1123
- defaultWhenUnconfigured: true,
1124
- gate: "smart-default",
1125
- };
1126
- }
1127
-
1128
- if (ext === ".sql") {
1129
- return {
1130
- runnerNames: ["sqlfluff"],
1131
- preferredRunners: ["sqlfluff"],
1132
- defaultRunner: "sqlfluff",
1133
- defaultWhenUnconfigured: true,
1134
- gate: "smart-default",
1135
- };
1136
- }
1137
-
1138
- if ([".rb", ".rake", ".gemspec", ".ru"].includes(ext)) {
1139
- return {
1140
- runnerNames: ["rubocop"],
1141
- preferredRunners: ["rubocop"],
1142
- defaultRunner: "rubocop",
1143
- defaultWhenUnconfigured: true,
1144
- gate: "smart-default",
1145
- };
1146
- }
1147
-
1148
- if ([".yaml", ".yml"].includes(ext)) {
1149
- return {
1150
- runnerNames: ["yamllint"],
1151
- preferredRunners: ["yamllint"],
1152
- defaultRunner: "yamllint",
1153
- defaultWhenUnconfigured: true,
1154
- gate: "smart-default",
1155
- };
1156
- }
1157
-
1158
- if ([".md", ".mdx"].includes(ext)) {
1159
- return {
1160
- runnerNames: ["markdownlint"],
1161
- preferredRunners: ["markdownlint"],
1162
- defaultRunner: "markdownlint",
1163
- defaultWhenUnconfigured: true,
1164
- gate: "smart-default",
1165
- };
1166
- }
1167
-
1168
- if ([".html", ".htm"].includes(ext)) {
1169
- return {
1170
- runnerNames: ["htmlhint"],
1171
- preferredRunners: ["htmlhint"],
1172
- defaultRunner: "htmlhint",
1173
- defaultWhenUnconfigured: true,
1174
- gate: "smart-default",
1175
- };
1176
- }
1177
-
1178
- if (path.basename(filePath).toLowerCase() === "dockerfile") {
1179
- return {
1180
- runnerNames: ["hadolint"],
1181
- preferredRunners: ["hadolint"],
1182
- defaultRunner: "hadolint",
1183
- defaultWhenUnconfigured: true,
1184
- gate: "smart-default",
1185
- };
1186
- }
1187
-
1188
- if ([".kt", ".kts"].includes(ext)) {
1189
- const preferredRunners: LintRunnerName[] = ["ktlint"];
1190
- if (context.hasDetektConfig) preferredRunners.push("detekt");
1191
- return {
1192
- runnerNames: ["ktlint", "detekt"],
1193
- preferredRunners,
1194
- defaultRunner: "ktlint",
1195
- defaultWhenUnconfigured: true,
1196
- gate: context.hasDetektConfig ? "mixed" : "smart-default",
1197
- };
1198
- }
1199
-
1200
- if (ext === ".toml") {
1201
- return {
1202
- runnerNames: ["taplo"],
1203
- preferredRunners: ["taplo"],
1204
- defaultRunner: "taplo",
1205
- defaultWhenUnconfigured: true,
1206
- gate: "smart-default",
1207
- };
1208
- }
1209
-
1210
- if (ext === ".go") {
1211
- return {
1212
- runnerNames: ["golangci-lint"],
1213
- preferredRunners: context.hasGolangciConfig ? ["golangci-lint"] : [],
1214
- defaultRunner: "golangci-lint",
1215
- defaultWhenUnconfigured: false,
1216
- gate: "config-first",
1217
- };
1218
- }
1219
-
1220
- if (ext === ".php") {
1221
- return {
1222
- runnerNames: ["phpstan"],
1223
- preferredRunners: context.hasPhpstanConfig ? ["phpstan"] : [],
1224
- defaultRunner: "phpstan",
1225
- defaultWhenUnconfigured: false,
1226
- gate: "config-first",
1227
- };
1228
- }
1229
-
1230
- if (ext === ".rs") {
1231
- return {
1232
- runnerNames: ["rust-clippy"],
1233
- preferredRunners: ["rust-clippy"],
1234
- defaultRunner: "rust-clippy",
1235
- defaultWhenUnconfigured: true,
1236
- gate: "smart-default",
1237
- };
1238
- }
1239
-
1240
- if ([".sh", ".bash"].includes(ext)) {
1241
- return {
1242
- runnerNames: ["shellcheck"],
1243
- preferredRunners: ["shellcheck"],
1244
- defaultRunner: "shellcheck",
1245
- defaultWhenUnconfigured: true,
1246
- gate: "smart-default",
1247
- };
1248
- }
1249
-
1250
- if ([".tf", ".tfvars"].includes(ext)) {
1251
- return {
1252
- runnerNames: ["tflint"],
1253
- preferredRunners: ["tflint"],
1254
- defaultRunner: "tflint",
1255
- defaultWhenUnconfigured: true,
1256
- gate: "smart-default",
1257
- };
1258
- }
1259
-
1260
- if ([".ex", ".exs", ".eex", ".heex", ".leex"].includes(ext)) {
1261
- return {
1262
- runnerNames: ["credo"],
1263
- preferredRunners: ["credo"],
1264
- defaultRunner: "credo",
1265
- defaultWhenUnconfigured: true,
1266
- gate: "smart-default",
1267
- };
1268
- }
1269
-
1270
- if ([".c", ".cc", ".cpp", ".cxx", ".h", ".hpp", ".ino"].includes(ext)) {
1271
- return {
1272
- runnerNames: ["cpp-check"],
1273
- preferredRunners: ["cpp-check"],
1274
- defaultRunner: "cpp-check",
1275
- defaultWhenUnconfigured: true,
1276
- gate: "smart-default",
1277
- };
1278
- }
1279
-
1280
- if (ext === ".dart") {
1281
- return {
1282
- runnerNames: ["dart-analyze"],
1283
- preferredRunners: ["dart-analyze"],
1284
- defaultRunner: "dart-analyze",
1285
- defaultWhenUnconfigured: true,
1286
- gate: "smart-default",
1287
- };
1288
- }
1289
-
1290
- if (ext === ".gleam") {
1291
- return {
1292
- runnerNames: ["gleam-check"],
1293
- preferredRunners: ["gleam-check"],
1294
- defaultRunner: "gleam-check",
1295
- defaultWhenUnconfigured: true,
1296
- gate: "smart-default",
1297
- };
1298
- }
1299
-
1300
- if ([".ps1", ".psm1", ".psd1"].includes(ext)) {
1301
- return {
1302
- runnerNames: ["psscriptanalyzer"],
1303
- preferredRunners: ["psscriptanalyzer"],
1304
- defaultRunner: "psscriptanalyzer",
1305
- defaultWhenUnconfigured: true,
1306
- gate: "smart-default",
1307
- };
1308
- }
1309
-
1310
- if (ext === ".prisma") {
1311
- return {
1312
- runnerNames: ["prisma-validate"],
1313
- preferredRunners: ["prisma-validate"],
1314
- defaultRunner: "prisma-validate",
1315
- defaultWhenUnconfigured: true,
1316
- gate: "smart-default",
1317
- };
1318
- }
1319
-
1320
- return undefined;
1321
- }
1322
-
1323
- export function getLinterPolicyForCwd(
1324
- filePath: string,
1325
- cwd: string,
1326
- ): LinterPolicy | undefined {
1327
- const context: LinterPolicyContext = {
1328
- hasEslintConfig: hasEslintConfig(cwd),
1329
- hasOxlintConfig: hasOxlintConfig(cwd),
1330
- hasBiomeConfig: hasBiomeConfig(cwd),
1331
- hasStylelintConfig: hasStylelintConfig(cwd),
1332
- hasSqlfluffConfig: hasSqlfluffConfig(cwd),
1333
- hasRubocopConfig: hasRubocopConfig(cwd),
1334
- hasYamllintConfig: hasYamllintConfig(cwd),
1335
- hasMarkdownlintConfig: hasMarkdownlintConfig(cwd),
1336
- hasGolangciConfig: hasGolangciConfig(cwd),
1337
- hasPhpstanConfig: hasPhpstanConfig(cwd),
1338
- hasMypyConfig: hasMypyConfig(cwd),
1339
- hasDetektConfig: hasDetektConfig(cwd),
1340
- };
1341
- const policy = getLinterPolicyForFile(filePath, context);
1342
- logLatency({
1343
- type: "phase",
1344
- phase: "linter_selected",
1345
- filePath,
1346
- durationMs: 0,
1347
- metadata: {
1348
- runner: policy?.defaultRunner ?? null,
1349
- gate: policy?.gate ?? null,
1350
- cwd,
1351
- context,
1352
- },
1353
- });
1354
- return policy;
1355
- }
1356
-
1357
- export function getAutofixPolicyForFile(
1358
- filePath: string,
1359
- context: AutofixPolicyContext = {},
1360
- ): AutofixPolicy | undefined {
1361
- const ext = path.extname(filePath).toLowerCase();
1362
-
1363
- if ([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"].includes(ext)) {
1364
- if (context.hasEslintConfig) {
1365
- return {
1366
- toolNames: ["eslint", "biome"],
1367
- preferredTools: ["eslint"],
1368
- defaultTool: "eslint",
1369
- defaultWhenUnconfigured: false,
1370
- gate: "config-first",
1371
- safe: true,
1372
- };
1373
- }
1374
- return {
1375
- toolNames: ["eslint", "biome"],
1376
- preferredTools: ["biome"],
1377
- defaultTool: "biome",
1378
- defaultWhenUnconfigured: true,
1379
- gate: "smart-default",
1380
- safe: true,
1381
- };
1382
- }
1383
-
1384
- if ([".json", ".jsonc"].includes(ext)) {
1385
- if (!context.hasBiomeConfig) {
1386
- return undefined;
1387
- }
1388
- return {
1389
- toolNames: ["biome"],
1390
- preferredTools: ["biome"],
1391
- defaultTool: "biome",
1392
- defaultWhenUnconfigured: false,
1393
- gate: "config-first",
1394
- safe: true,
1395
- };
1396
- }
1397
-
1398
- if ([".py", ".pyi"].includes(ext)) {
1399
- return {
1400
- toolNames: ["ruff"],
1401
- preferredTools: ["ruff"],
1402
- defaultTool: "ruff",
1403
- defaultWhenUnconfigured: true,
1404
- gate: "smart-default",
1405
- safe: true,
1406
- };
1407
- }
1408
-
1409
- if ([".css", ".scss", ".sass", ".less"].includes(ext)) {
1410
- return {
1411
- toolNames: ["stylelint"],
1412
- preferredTools: ["stylelint"],
1413
- defaultTool: "stylelint",
1414
- defaultWhenUnconfigured: true,
1415
- gate: "smart-default",
1416
- safe: true,
1417
- };
1418
- }
1419
-
1420
- if (ext === ".sql") {
1421
- return {
1422
- toolNames: ["sqlfluff"],
1423
- preferredTools: ["sqlfluff"],
1424
- defaultTool: "sqlfluff",
1425
- defaultWhenUnconfigured: true,
1426
- gate: "smart-default",
1427
- safe: true,
1428
- };
1429
- }
1430
-
1431
- if ([".rb", ".rake", ".gemspec", ".ru"].includes(ext)) {
1432
- return {
1433
- toolNames: ["rubocop"],
1434
- preferredTools: ["rubocop"],
1435
- defaultTool: "rubocop",
1436
- defaultWhenUnconfigured: true,
1437
- gate: "smart-default",
1438
- safe: true,
1439
- };
1440
- }
1441
-
1442
- if ([".kt", ".kts"].includes(ext)) {
1443
- return {
1444
- toolNames: ["ktlint"],
1445
- preferredTools: ["ktlint"],
1446
- defaultTool: "ktlint",
1447
- defaultWhenUnconfigured: true,
1448
- gate: "smart-default",
1449
- safe: true,
1450
- };
1451
- }
1452
-
1453
- if (ext === ".rs") {
1454
- return {
1455
- toolNames: ["rust-clippy"],
1456
- preferredTools: ["rust-clippy"],
1457
- defaultTool: "rust-clippy",
1458
- defaultWhenUnconfigured: true,
1459
- gate: "smart-default",
1460
- safe: true,
1461
- };
1462
- }
1463
-
1464
- if (ext === ".dart") {
1465
- return {
1466
- toolNames: ["dart-analyze"],
1467
- preferredTools: ["dart-analyze"],
1468
- defaultTool: "dart-analyze",
1469
- defaultWhenUnconfigured: true,
1470
- gate: "smart-default",
1471
- safe: true,
1472
- };
1473
- }
1474
-
1475
- return undefined;
1476
- }
1477
-
1478
- export function getPreferredAutofixTools(
1479
- filePath: string,
1480
- context: AutofixPolicyContext,
1481
- ): AutofixToolName[] {
1482
- return getAutofixPolicyForFile(filePath, context)?.preferredTools ?? [];
1483
- }
1484
-
1485
- const ESLINT_CONFIGS = [
1486
- ".eslintrc",
1487
- ".eslintrc.js",
1488
- ".eslintrc.cjs",
1489
- ".eslintrc.json",
1490
- ".eslintrc.yaml",
1491
- ".eslintrc.yml",
1492
- "eslint.config.js",
1493
- "eslint.config.mjs",
1494
- "eslint.config.cjs",
1495
- "eslint.config.ts",
1496
- ];
1497
-
1498
- function walkUpDirsUntilPackageJson(cwd: string): string[] {
1499
- const dirs: string[] = [];
1500
- let dir = cwd;
1501
- const root = path.parse(dir).root;
1502
- while (true) {
1503
- dirs.push(dir);
1504
- if (fs.existsSync(path.join(dir, "package.json"))) break;
1505
- if (dir === root) break;
1506
- const parent = path.dirname(dir);
1507
- if (parent === dir) break;
1508
- dir = parent;
1509
- }
1510
- return dirs;
1511
- }
1512
-
1513
- function findNearestPackageJsonPath(cwd: string): string | undefined {
1514
- let dir = cwd;
1515
- const root = path.parse(dir).root;
1516
- while (true) {
1517
- const pkgPath = path.join(dir, "package.json");
1518
- if (fs.existsSync(pkgPath)) return pkgPath;
1519
- if (dir === root) break;
1520
- const parent = path.dirname(dir);
1521
- if (parent === dir) break;
1522
- dir = parent;
1523
- }
1524
- return undefined;
1525
- }
1526
-
1527
- export function hasNearestPackageJsonDependency(
1528
- cwd: string,
1529
- dependencyName: string,
1530
- ): boolean {
1531
- const pkgPath = findNearestPackageJsonPath(cwd);
1532
- if (!pkgPath) return false;
1533
- try {
1534
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) as {
1535
- dependencies?: Record<string, string>;
1536
- devDependencies?: Record<string, string>;
1537
- };
1538
- return Boolean(
1539
- pkg.dependencies?.[dependencyName] ??
1540
- pkg.devDependencies?.[dependencyName],
1541
- );
1542
- } catch {}
1543
- return false;
1544
- }
1545
-
1546
- export function hasNearestPackageJsonField(
1547
- cwd: string,
1548
- fieldName: string,
1549
- ): boolean {
1550
- const pkgPath = findNearestPackageJsonPath(cwd);
1551
- if (!pkgPath) return false;
1552
- try {
1553
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) as Record<
1554
- string,
1555
- unknown
1556
- >;
1557
- return pkg[fieldName] !== undefined;
1558
- } catch {}
1559
- return false;
1560
- }
1561
-
1562
- export function hasEslintConfig(cwd: string): boolean {
1563
- for (const dir of walkUpDirsUntilPackageJson(cwd)) {
1564
- for (const cfg of ESLINT_CONFIGS) {
1565
- if (fs.existsSync(path.join(dir, cfg))) return true;
1566
- }
1567
- const pkgPath = path.join(dir, "package.json");
1568
- if (fs.existsSync(pkgPath)) {
1569
- try {
1570
- if (JSON.parse(fs.readFileSync(pkgPath, "utf-8")).eslintConfig)
1571
- return true;
1572
- } catch {}
1573
- }
1574
- }
1575
- return false;
1576
- }
1577
-
1578
- export function hasBiomeConfig(cwd: string): boolean {
1579
- return getBiomeConfigPath(cwd) !== undefined;
1580
- }
1581
-
1582
- export function getBiomeConfigPath(cwd: string): string | undefined {
1583
- for (const dir of walkUpDirsUntilPackageJson(cwd)) {
1584
- const jsoncPath = path.join(dir, "biome.jsonc");
1585
- if (fs.existsSync(jsoncPath)) return jsoncPath;
1586
- const jsonPath = path.join(dir, "biome.json");
1587
- if (fs.existsSync(jsonPath)) return jsonPath;
1588
- }
1589
- return undefined;
1590
- }
1591
-
1592
- export function hasOxfmtConfig(cwd: string): boolean {
1593
- for (const dir of walkUpDirsUntilPackageJson(cwd)) {
1594
- if (fs.existsSync(path.join(dir, "oxfmt.toml"))) return true;
1595
- if (fs.existsSync(path.join(dir, ".oxfmtrc.json"))) return true;
1596
- const pkgPath = path.join(dir, "package.json");
1597
- if (fs.existsSync(pkgPath)) {
1598
- try {
1599
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) as Record<
1600
- string,
1601
- unknown
1602
- >;
1603
- const deps = {
1604
- ...(pkg.dependencies as Record<string, unknown> | undefined),
1605
- ...(pkg.devDependencies as Record<string, unknown> | undefined),
1606
- };
1607
- if (deps["@oxc-project/oxfmt"]) return true;
1608
- } catch {}
1609
- }
1610
- }
1611
- return false;
1612
- }
1613
-
1614
- export function hasStylelintConfig(cwd: string): boolean {
1615
- for (const dir of walkUpDirsUntilPackageJson(cwd)) {
1616
- if (STYLELINT_CONFIGS.some((cfg) => fs.existsSync(path.join(dir, cfg)))) {
1617
- return true;
1618
- }
1619
- const pkgPath = path.join(dir, "package.json");
1620
- if (fs.existsSync(pkgPath)) {
1621
- try {
1622
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
1623
- if (pkg.stylelint) return true;
1624
- } catch {}
1625
- }
1626
- }
1627
- return false;
1628
- }
1629
-
1630
- export function hasSqlfluffConfig(cwd: string): boolean {
1631
- for (const cfg of SQLFLUFF_CONFIGS) {
1632
- const cfgPath = path.join(cwd, cfg);
1633
- if (!fs.existsSync(cfgPath)) continue;
1634
- if (cfg === "pyproject.toml") {
1635
- try {
1636
- const content = fs.readFileSync(cfgPath, "utf-8");
1637
- if (content.includes("[tool.sqlfluff]")) return true;
1638
- } catch {}
1639
- continue;
1640
- }
1641
- if (cfg === "setup.cfg" || cfg === "tox.ini") {
1642
- try {
1643
- const content = fs.readFileSync(cfgPath, "utf-8");
1644
- if (content.includes("[sqlfluff]")) return true;
1645
- } catch {}
1646
- continue;
1647
- }
1648
- return true;
1649
- }
1650
-
1651
- for (const depFile of ["requirements.txt", "Pipfile", "pyproject.toml"]) {
1652
- const depPath = path.join(cwd, depFile);
1653
- if (!fs.existsSync(depPath)) continue;
1654
- try {
1655
- const content = fs.readFileSync(depPath, "utf-8").toLowerCase();
1656
- if (content.includes("sqlfluff")) return true;
1657
- } catch {}
1658
- }
1659
-
1660
- return false;
1661
- }
1662
-
1663
- export function hasRubocopConfig(cwd: string): boolean {
1664
- for (const cfg of RUBOCOP_CONFIGS) {
1665
- if (fs.existsSync(path.join(cwd, cfg))) return true;
1666
- }
1667
- const gemfile = path.join(cwd, "Gemfile");
1668
- if (fs.existsSync(gemfile)) {
1669
- try {
1670
- const content = fs.readFileSync(gemfile, "utf-8");
1671
- return content.includes("rubocop");
1672
- } catch {}
1673
- }
1674
- return false;
1675
- }
1676
-
1677
- export function hasMypyConfig(cwd: string): boolean {
1678
- for (const dir of walkUpDirsUntilPackageJson(cwd)) {
1679
- for (const cfg of MYPY_CONFIGS) {
1680
- const cfgPath = path.join(dir, cfg);
1681
- if (!fs.existsSync(cfgPath)) continue;
1682
- if (cfg === "setup.cfg") {
1683
- try {
1684
- if (fs.readFileSync(cfgPath, "utf-8").includes("[mypy]")) return true;
1685
- } catch {}
1686
- continue;
1687
- }
1688
- if (cfg === "pyproject.toml") {
1689
- try {
1690
- if (fs.readFileSync(cfgPath, "utf-8").includes("[tool.mypy]"))
1691
- return true;
1692
- } catch {}
1693
- continue;
1694
- }
1695
- return true;
1696
- }
1697
- }
1698
- return false;
1699
- }
1700
-
1701
- export function hasYamllintConfig(cwd: string): boolean {
1702
- for (const cfg of YAMLLINT_CONFIGS) {
1703
- const cfgPath = path.join(cwd, cfg);
1704
- if (!fs.existsSync(cfgPath)) continue;
1705
- if (cfg === "pyproject.toml") {
1706
- try {
1707
- const content = fs.readFileSync(cfgPath, "utf-8");
1708
- if (content.includes("[tool.yamllint]")) return true;
1709
- } catch {}
1710
- continue;
1711
- }
1712
- if (cfg === "setup.cfg" || cfg === "tox.ini") {
1713
- try {
1714
- const content = fs.readFileSync(cfgPath, "utf-8");
1715
- if (content.includes("[yamllint]")) return true;
1716
- } catch {}
1717
- continue;
1718
- }
1719
- return true;
1720
- }
1721
-
1722
- for (const depFile of ["requirements.txt", "Pipfile", "pyproject.toml"]) {
1723
- const depPath = path.join(cwd, depFile);
1724
- if (!fs.existsSync(depPath)) continue;
1725
- try {
1726
- const content = fs.readFileSync(depPath, "utf-8").toLowerCase();
1727
- if (content.includes("yamllint")) return true;
1728
- } catch {}
1729
- }
1730
-
1731
- return false;
1732
- }
1733
-
1734
- export function hasMarkdownlintConfig(cwd: string): boolean {
1735
- return MARKDOWNLINT_CONFIGS.some((cfg) => fs.existsSync(path.join(cwd, cfg)));
1736
- }
1737
-
1738
- export function hasPrettierConfig(cwd: string): boolean {
1739
- for (const dir of walkUpDirsUntilPackageJson(cwd)) {
1740
- if (PRETTIER_CONFIGS.some((cfg) => fs.existsSync(path.join(dir, cfg))))
1741
- return true;
1742
- const pkgPath = path.join(dir, "package.json");
1743
- if (fs.existsSync(pkgPath)) {
1744
- try {
1745
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
1746
- if (Object.prototype.hasOwnProperty.call(pkg, "prettier")) return true;
1747
- } catch {}
1748
- }
1749
- }
1750
- return false;
1751
- }
1752
-
1753
- export function hasBlackConfig(cwd: string): boolean {
1754
- for (const dir of walkUpDirsUntilPackageJson(cwd)) {
1755
- const pyproject = path.join(dir, "pyproject.toml");
1756
- if (fs.existsSync(pyproject)) {
1757
- try {
1758
- if (fs.readFileSync(pyproject, "utf-8").includes("[tool.black]"))
1759
- return true;
1760
- } catch {}
1761
- }
1762
- }
1763
-
1764
- // Dependency file checks are cwd-only (weaker signal, avoid false positives up the tree)
1765
- for (const depFile of ["requirements.txt", "Pipfile"]) {
1766
- const depPath = path.join(cwd, depFile);
1767
- if (!fs.existsSync(depPath)) continue;
1768
- try {
1769
- if (fs.readFileSync(depPath, "utf-8").toLowerCase().includes("black"))
1770
- return true;
1771
- } catch {}
1772
- }
1773
-
1774
- return false;
1775
- }
1776
-
1777
- export function hasRuffConfig(cwd: string): boolean {
1778
- for (const dir of walkUpDirsUntilPackageJson(cwd)) {
1779
- for (const cfg of RUFF_PROJECT_CONFIGS) {
1780
- if (fs.existsSync(path.join(dir, cfg))) return true;
1781
- }
1782
- const pyproject = path.join(dir, "pyproject.toml");
1783
- if (fs.existsSync(pyproject)) {
1784
- try {
1785
- if (fs.readFileSync(pyproject, "utf-8").includes("[tool.ruff]"))
1786
- return true;
1787
- } catch {}
1788
- }
1789
- }
1790
- return false;
1791
- }
1792
-
1793
- export function hasGolangciConfig(cwd: string): boolean {
1794
- return GOLANGCI_CONFIGS.some((cfg) => fs.existsSync(path.join(cwd, cfg)));
1795
- }
1796
-
1797
- export function hasClangFormatConfig(cwd: string): boolean {
1798
- return [".clang-format", "_clang-format"].some((cfg) =>
1799
- fs.existsSync(path.join(cwd, cfg)),
1800
- );
1801
- }
1802
-
1803
- export function hasPhpCsFixerConfig(cwd: string): boolean {
1804
- return [".php-cs-fixer.php", ".php-cs-fixer.dist.php"].some((cfg) =>
1805
- fs.existsSync(path.join(cwd, cfg)),
1806
- );
1807
- }
1808
-
1809
- export function hasStyluaConfig(cwd: string): boolean {
1810
- return ["stylua.toml", ".stylua.toml"].some((cfg) =>
1811
- fs.existsSync(path.join(cwd, cfg)),
1812
- );
1813
- }
1814
-
1815
- export function hasOcamlformatConfig(cwd: string): boolean {
1816
- return fs.existsSync(path.join(cwd, ".ocamlformat"));
1817
- }
1818
-
1819
- export function hasGoogleJavaFormatConfig(cwd: string): boolean {
1820
- // google-java-format has no standard config file — gate on .editorconfig
1821
- // with indent_size defined (common Java project signal) or explicit opt-in marker.
1822
- return (
1823
- fs.existsSync(path.join(cwd, ".google-java-format")) ||
1824
- fs.existsSync(path.join(cwd, ".editorconfig"))
1825
- );
1826
- }
1827
-
1828
- export function hasCljfmtConfig(cwd: string): boolean {
1829
- return [".cljfmt.edn", "cljfmt.edn", ".cljfmt"].some((cfg) =>
1830
- fs.existsSync(path.join(cwd, cfg)),
1831
- );
1832
- }
1833
-
1834
- export function hasCmakeFormatConfig(cwd: string): boolean {
1835
- return [
1836
- ".cmake-format",
1837
- ".cmake-format.yaml",
1838
- ".cmake-format.yml",
1839
- ".cmake-format.json",
1840
- ".cmake-format.py",
1841
- "cmake-format.yaml",
1842
- "cmake-format.yml",
1843
- ].some((cfg) => fs.existsSync(path.join(cwd, cfg)));
1844
- }
1845
-
1846
- export function hasPhpstanConfig(cwd: string): boolean {
1847
- return PHPSTAN_CONFIGS.some((cfg) => fs.existsSync(path.join(cwd, cfg)));
1848
- }
1849
-
1850
- const DETEKT_CONFIGS = [
1851
- "detekt.yml",
1852
- ".detekt.yml",
1853
- path.join("config", "detekt", "detekt.yml"),
1854
- path.join("detekt", "detekt.yml"),
1855
- ];
1856
-
1857
- export function hasDetektConfig(cwd: string): boolean {
1858
- for (const dir of walkUpDirsUntilPackageJson(cwd)) {
1859
- if (DETEKT_CONFIGS.some((cfg) => fs.existsSync(path.join(dir, cfg))))
1860
- return true;
1861
- }
1862
- return false;
1863
- }
1864
-
1865
- export function hasStandardrbConfig(cwd: string): boolean {
1866
- const gemfile = path.join(cwd, "Gemfile");
1867
- if (fs.existsSync(gemfile)) {
1868
- try {
1869
- return fs.readFileSync(gemfile, "utf-8").includes("standard");
1870
- } catch {}
1871
- }
1872
- return false;
1873
- }
1874
-
1875
- export function getRubocopCommand(cwd: string): {
1876
- cmd: string;
1877
- args: string[];
1878
- } {
1879
- const gemfile = path.join(cwd, "Gemfile");
1880
- if (fs.existsSync(gemfile)) {
1881
- try {
1882
- const content = fs.readFileSync(gemfile, "utf-8");
1883
- if (content.includes("rubocop")) {
1884
- return { cmd: "bundle", args: ["exec", "rubocop"] };
1885
- }
1886
- } catch {}
1887
- }
1888
- return { cmd: "rubocop", args: [] };
1889
- }
1890
-
1891
- export function hasOxlintConfig(cwd: string): boolean {
1892
- for (const dir of walkUpDirsUntilPackageJson(cwd)) {
1893
- if (
1894
- fs.existsSync(path.join(dir, ".oxlintrc.json")) ||
1895
- fs.existsSync(path.join(dir, "oxlint.json"))
1896
- )
1897
- return true;
1898
- }
1899
- return false;
1900
- }
1901
-
1902
- export function getPreferredJstsLintRunners(
1903
- context: JstsLintPolicyContext,
1904
- ): JstsLintRunnerName[] {
1905
- if (context.hasEslintConfig) return ["eslint"];
1906
- if (context.hasOxlintConfig) return ["oxlint"];
1907
- if (context.hasBiomeConfig) return ["biome-check-json"];
1908
- return ["oxlint", "biome-check-json"];
1909
- }
1910
-
1911
- export function getJstsLintPolicy(
1912
- context: JstsLintPolicyContext,
1913
- ): JstsLintPolicy {
1914
- const hasEslint = !!context.hasEslintConfig;
1915
- const hasOxlint = !!context.hasOxlintConfig;
1916
- const hasBiome = !!context.hasBiomeConfig;
1917
- return {
1918
- hasEslintConfig: hasEslint,
1919
- hasOxlintConfig: hasOxlint,
1920
- hasBiomeConfig: hasBiome,
1921
- preferredRunners: getPreferredJstsLintRunners({
1922
- hasEslintConfig: hasEslint,
1923
- hasOxlintConfig: hasOxlint,
1924
- hasBiomeConfig: hasBiome,
1925
- }),
1926
- hasExplicitNonBiomeLinter: hasEslint || hasOxlint,
1927
- };
1928
- }
1929
-
1930
- export function getJstsLintPolicyForCwd(cwd: string): JstsLintPolicy {
1931
- return getJstsLintPolicy({
1932
- hasEslintConfig: hasEslintConfig(cwd),
1933
- hasOxlintConfig: hasOxlintConfig(cwd),
1934
- hasBiomeConfig: hasBiomeConfig(cwd),
1935
- });
1936
- }
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { logLatency } from "./latency-logger.js";
4
+
5
+ export type ToolGate = "config-first" | "smart-default" | "mixed";
6
+
7
+ export interface FormatterPolicy {
8
+ formatterNames: string[];
9
+ defaultFormatter?: string;
10
+ defaultWhenUnconfigured: boolean;
11
+ gate: ToolGate;
12
+ }
13
+
14
+ const FORMATTER_POLICY_BY_EXTENSION = new Map<string, FormatterPolicy>([
15
+ [
16
+ ".js",
17
+ {
18
+ formatterNames: ["biome", "prettier", "oxfmt"],
19
+ defaultFormatter: "biome",
20
+ defaultWhenUnconfigured: true,
21
+ gate: "smart-default",
22
+ },
23
+ ],
24
+ [
25
+ ".jsx",
26
+ {
27
+ formatterNames: ["biome", "prettier", "oxfmt"],
28
+ defaultFormatter: "biome",
29
+ defaultWhenUnconfigured: true,
30
+ gate: "smart-default",
31
+ },
32
+ ],
33
+ [
34
+ ".mjs",
35
+ {
36
+ formatterNames: ["biome", "prettier", "oxfmt"],
37
+ defaultFormatter: "biome",
38
+ defaultWhenUnconfigured: true,
39
+ gate: "smart-default",
40
+ },
41
+ ],
42
+ [
43
+ ".cjs",
44
+ {
45
+ formatterNames: ["biome", "prettier", "oxfmt"],
46
+ defaultFormatter: "biome",
47
+ defaultWhenUnconfigured: true,
48
+ gate: "smart-default",
49
+ },
50
+ ],
51
+ [
52
+ ".ts",
53
+ {
54
+ formatterNames: ["biome", "prettier", "oxfmt"],
55
+ defaultFormatter: "biome",
56
+ defaultWhenUnconfigured: true,
57
+ gate: "smart-default",
58
+ },
59
+ ],
60
+ [
61
+ ".tsx",
62
+ {
63
+ formatterNames: ["biome", "prettier", "oxfmt"],
64
+ defaultFormatter: "biome",
65
+ defaultWhenUnconfigured: true,
66
+ gate: "smart-default",
67
+ },
68
+ ],
69
+ [
70
+ ".mts",
71
+ {
72
+ formatterNames: ["biome", "prettier", "oxfmt"],
73
+ defaultFormatter: "biome",
74
+ defaultWhenUnconfigured: true,
75
+ gate: "smart-default",
76
+ },
77
+ ],
78
+ [
79
+ ".cts",
80
+ {
81
+ formatterNames: ["biome", "prettier", "oxfmt"],
82
+ defaultFormatter: "biome",
83
+ defaultWhenUnconfigured: true,
84
+ gate: "smart-default",
85
+ },
86
+ ],
87
+ [
88
+ ".py",
89
+ {
90
+ formatterNames: ["black", "ruff"],
91
+ defaultFormatter: "ruff",
92
+ defaultWhenUnconfigured: true,
93
+ gate: "smart-default",
94
+ },
95
+ ],
96
+ [
97
+ ".pyi",
98
+ {
99
+ formatterNames: ["black", "ruff"],
100
+ defaultFormatter: "ruff",
101
+ defaultWhenUnconfigured: true,
102
+ gate: "smart-default",
103
+ },
104
+ ],
105
+ [
106
+ ".json",
107
+ {
108
+ formatterNames: ["biome", "prettier"],
109
+ defaultFormatter: "biome",
110
+ defaultWhenUnconfigured: false,
111
+ gate: "mixed",
112
+ },
113
+ ],
114
+ [
115
+ ".jsonc",
116
+ {
117
+ formatterNames: ["biome", "prettier"],
118
+ defaultFormatter: "biome",
119
+ defaultWhenUnconfigured: false,
120
+ gate: "mixed",
121
+ },
122
+ ],
123
+ [
124
+ ".css",
125
+ {
126
+ formatterNames: ["biome", "prettier", "oxfmt"],
127
+ defaultFormatter: "biome",
128
+ defaultWhenUnconfigured: true,
129
+ gate: "smart-default",
130
+ },
131
+ ],
132
+ [
133
+ ".scss",
134
+ {
135
+ formatterNames: ["biome", "prettier", "oxfmt"],
136
+ defaultFormatter: "biome",
137
+ defaultWhenUnconfigured: true,
138
+ gate: "smart-default",
139
+ },
140
+ ],
141
+ [
142
+ ".sass",
143
+ {
144
+ formatterNames: ["biome", "prettier", "oxfmt"],
145
+ defaultFormatter: "biome",
146
+ defaultWhenUnconfigured: true,
147
+ gate: "smart-default",
148
+ },
149
+ ],
150
+ [
151
+ ".less",
152
+ {
153
+ formatterNames: ["prettier"],
154
+ defaultFormatter: "prettier",
155
+ defaultWhenUnconfigured: true,
156
+ gate: "smart-default",
157
+ },
158
+ ],
159
+ [
160
+ ".html",
161
+ {
162
+ formatterNames: ["prettier"],
163
+ defaultFormatter: "prettier",
164
+ defaultWhenUnconfigured: true,
165
+ gate: "smart-default",
166
+ },
167
+ ],
168
+ [
169
+ ".htm",
170
+ {
171
+ formatterNames: ["prettier"],
172
+ defaultFormatter: "prettier",
173
+ defaultWhenUnconfigured: true,
174
+ gate: "smart-default",
175
+ },
176
+ ],
177
+ [
178
+ ".yaml",
179
+ {
180
+ formatterNames: ["prettier"],
181
+ defaultFormatter: "prettier",
182
+ defaultWhenUnconfigured: true,
183
+ gate: "smart-default",
184
+ },
185
+ ],
186
+ [
187
+ ".yml",
188
+ {
189
+ formatterNames: ["prettier"],
190
+ defaultFormatter: "prettier",
191
+ defaultWhenUnconfigured: true,
192
+ gate: "smart-default",
193
+ },
194
+ ],
195
+ [
196
+ ".md",
197
+ {
198
+ formatterNames: ["prettier"],
199
+ defaultFormatter: "prettier",
200
+ defaultWhenUnconfigured: true,
201
+ gate: "smart-default",
202
+ },
203
+ ],
204
+ [
205
+ ".mdx",
206
+ {
207
+ formatterNames: ["prettier"],
208
+ defaultFormatter: "prettier",
209
+ defaultWhenUnconfigured: true,
210
+ gate: "smart-default",
211
+ },
212
+ ],
213
+ [
214
+ ".graphql",
215
+ {
216
+ formatterNames: ["prettier"],
217
+ defaultFormatter: "prettier",
218
+ defaultWhenUnconfigured: true,
219
+ gate: "smart-default",
220
+ },
221
+ ],
222
+ [
223
+ ".gql",
224
+ {
225
+ formatterNames: ["prettier"],
226
+ defaultFormatter: "prettier",
227
+ defaultWhenUnconfigured: true,
228
+ gate: "smart-default",
229
+ },
230
+ ],
231
+ [
232
+ ".kt",
233
+ {
234
+ formatterNames: ["ktlint"],
235
+ defaultFormatter: "ktlint",
236
+ defaultWhenUnconfigured: true,
237
+ gate: "smart-default",
238
+ },
239
+ ],
240
+ [
241
+ ".kts",
242
+ {
243
+ formatterNames: ["ktlint"],
244
+ defaultFormatter: "ktlint",
245
+ defaultWhenUnconfigured: true,
246
+ gate: "smart-default",
247
+ },
248
+ ],
249
+ [
250
+ ".swift",
251
+ {
252
+ formatterNames: ["swiftformat"],
253
+ defaultFormatter: "swiftformat",
254
+ defaultWhenUnconfigured: true,
255
+ gate: "smart-default",
256
+ },
257
+ ],
258
+ [
259
+ ".fs",
260
+ {
261
+ formatterNames: ["fantomas"],
262
+ defaultFormatter: "fantomas",
263
+ defaultWhenUnconfigured: true,
264
+ gate: "smart-default",
265
+ },
266
+ ],
267
+ [
268
+ ".fsi",
269
+ {
270
+ formatterNames: ["fantomas"],
271
+ defaultFormatter: "fantomas",
272
+ defaultWhenUnconfigured: true,
273
+ gate: "smart-default",
274
+ },
275
+ ],
276
+ [
277
+ ".fsx",
278
+ {
279
+ formatterNames: ["fantomas"],
280
+ defaultFormatter: "fantomas",
281
+ defaultWhenUnconfigured: true,
282
+ gate: "smart-default",
283
+ },
284
+ ],
285
+ [
286
+ ".nix",
287
+ {
288
+ formatterNames: ["nixfmt"],
289
+ defaultFormatter: "nixfmt",
290
+ defaultWhenUnconfigured: true,
291
+ gate: "smart-default",
292
+ },
293
+ ],
294
+ [
295
+ ".ex",
296
+ {
297
+ formatterNames: ["mix"],
298
+ defaultFormatter: "mix",
299
+ defaultWhenUnconfigured: true,
300
+ gate: "smart-default",
301
+ },
302
+ ],
303
+ [
304
+ ".exs",
305
+ {
306
+ formatterNames: ["mix"],
307
+ defaultFormatter: "mix",
308
+ defaultWhenUnconfigured: true,
309
+ gate: "smart-default",
310
+ },
311
+ ],
312
+ [
313
+ ".eex",
314
+ {
315
+ formatterNames: ["mix"],
316
+ defaultFormatter: "mix",
317
+ defaultWhenUnconfigured: true,
318
+ gate: "smart-default",
319
+ },
320
+ ],
321
+ [
322
+ ".heex",
323
+ {
324
+ formatterNames: ["mix"],
325
+ defaultFormatter: "mix",
326
+ defaultWhenUnconfigured: true,
327
+ gate: "smart-default",
328
+ },
329
+ ],
330
+ [
331
+ ".leex",
332
+ {
333
+ formatterNames: ["mix"],
334
+ defaultFormatter: "mix",
335
+ defaultWhenUnconfigured: true,
336
+ gate: "smart-default",
337
+ },
338
+ ],
339
+ [
340
+ ".gleam",
341
+ {
342
+ formatterNames: ["gleam"],
343
+ defaultFormatter: "gleam",
344
+ defaultWhenUnconfigured: true,
345
+ gate: "smart-default",
346
+ },
347
+ ],
348
+ [
349
+ ".c",
350
+ {
351
+ formatterNames: ["clang-format"],
352
+ defaultFormatter: "clang-format",
353
+ defaultWhenUnconfigured: false,
354
+ gate: "config-first",
355
+ },
356
+ ],
357
+ [
358
+ ".cc",
359
+ {
360
+ formatterNames: ["clang-format"],
361
+ defaultFormatter: "clang-format",
362
+ defaultWhenUnconfigured: false,
363
+ gate: "config-first",
364
+ },
365
+ ],
366
+ [
367
+ ".cpp",
368
+ {
369
+ formatterNames: ["clang-format"],
370
+ defaultFormatter: "clang-format",
371
+ defaultWhenUnconfigured: false,
372
+ gate: "config-first",
373
+ },
374
+ ],
375
+ [
376
+ ".cxx",
377
+ {
378
+ formatterNames: ["clang-format"],
379
+ defaultFormatter: "clang-format",
380
+ defaultWhenUnconfigured: false,
381
+ gate: "config-first",
382
+ },
383
+ ],
384
+ [
385
+ ".h",
386
+ {
387
+ formatterNames: ["clang-format"],
388
+ defaultFormatter: "clang-format",
389
+ defaultWhenUnconfigured: false,
390
+ gate: "config-first",
391
+ },
392
+ ],
393
+ [
394
+ ".hpp",
395
+ {
396
+ formatterNames: ["clang-format"],
397
+ defaultFormatter: "clang-format",
398
+ defaultWhenUnconfigured: false,
399
+ gate: "config-first",
400
+ },
401
+ ],
402
+ [
403
+ ".ino",
404
+ {
405
+ formatterNames: ["clang-format"],
406
+ defaultFormatter: "clang-format",
407
+ defaultWhenUnconfigured: false,
408
+ gate: "config-first",
409
+ },
410
+ ],
411
+ [
412
+ ".php",
413
+ {
414
+ formatterNames: ["php-cs-fixer"],
415
+ defaultFormatter: "php-cs-fixer",
416
+ defaultWhenUnconfigured: false,
417
+ gate: "config-first",
418
+ },
419
+ ],
420
+ [
421
+ ".cs",
422
+ {
423
+ formatterNames: ["csharpier"],
424
+ defaultFormatter: "csharpier",
425
+ defaultWhenUnconfigured: true,
426
+ gate: "smart-default",
427
+ },
428
+ ],
429
+ [
430
+ ".lua",
431
+ {
432
+ formatterNames: ["stylua"],
433
+ defaultFormatter: "stylua",
434
+ defaultWhenUnconfigured: false,
435
+ gate: "config-first",
436
+ },
437
+ ],
438
+ [
439
+ ".hs",
440
+ {
441
+ formatterNames: ["ormolu"],
442
+ defaultFormatter: "ormolu",
443
+ defaultWhenUnconfigured: true,
444
+ gate: "smart-default",
445
+ },
446
+ ],
447
+ [
448
+ ".lhs",
449
+ {
450
+ formatterNames: ["ormolu"],
451
+ defaultFormatter: "ormolu",
452
+ defaultWhenUnconfigured: true,
453
+ gate: "smart-default",
454
+ },
455
+ ],
456
+ [
457
+ ".ml",
458
+ {
459
+ formatterNames: ["ocamlformat"],
460
+ defaultFormatter: "ocamlformat",
461
+ defaultWhenUnconfigured: false,
462
+ gate: "config-first",
463
+ },
464
+ ],
465
+ [
466
+ ".mli",
467
+ {
468
+ formatterNames: ["ocamlformat"],
469
+ defaultFormatter: "ocamlformat",
470
+ defaultWhenUnconfigured: false,
471
+ gate: "config-first",
472
+ },
473
+ ],
474
+ [
475
+ ".go",
476
+ {
477
+ formatterNames: ["gofmt"],
478
+ defaultFormatter: "gofmt",
479
+ defaultWhenUnconfigured: true,
480
+ gate: "smart-default",
481
+ },
482
+ ],
483
+ [
484
+ ".rs",
485
+ {
486
+ formatterNames: ["rustfmt"],
487
+ defaultFormatter: "rustfmt",
488
+ defaultWhenUnconfigured: true,
489
+ gate: "smart-default",
490
+ },
491
+ ],
492
+ [
493
+ ".sh",
494
+ {
495
+ formatterNames: ["shfmt"],
496
+ defaultFormatter: "shfmt",
497
+ defaultWhenUnconfigured: true,
498
+ gate: "smart-default",
499
+ },
500
+ ],
501
+ [
502
+ ".bash",
503
+ {
504
+ formatterNames: ["shfmt"],
505
+ defaultFormatter: "shfmt",
506
+ defaultWhenUnconfigured: true,
507
+ gate: "smart-default",
508
+ },
509
+ ],
510
+ [
511
+ ".toml",
512
+ {
513
+ formatterNames: ["taplo"],
514
+ defaultFormatter: "taplo",
515
+ defaultWhenUnconfigured: true,
516
+ gate: "smart-default",
517
+ },
518
+ ],
519
+ [
520
+ ".tf",
521
+ {
522
+ formatterNames: ["terraform"],
523
+ defaultFormatter: "terraform",
524
+ defaultWhenUnconfigured: true,
525
+ gate: "smart-default",
526
+ },
527
+ ],
528
+ [
529
+ ".tfvars",
530
+ {
531
+ formatterNames: ["terraform"],
532
+ defaultFormatter: "terraform",
533
+ defaultWhenUnconfigured: true,
534
+ gate: "smart-default",
535
+ },
536
+ ],
537
+ [
538
+ ".dart",
539
+ {
540
+ formatterNames: ["dart"],
541
+ defaultFormatter: "dart",
542
+ defaultWhenUnconfigured: true,
543
+ gate: "smart-default",
544
+ },
545
+ ],
546
+ [
547
+ ".zig",
548
+ {
549
+ formatterNames: ["zig"],
550
+ defaultFormatter: "zig",
551
+ defaultWhenUnconfigured: true,
552
+ gate: "smart-default",
553
+ },
554
+ ],
555
+ [
556
+ ".zon",
557
+ {
558
+ formatterNames: ["zig"],
559
+ defaultFormatter: "zig",
560
+ defaultWhenUnconfigured: true,
561
+ gate: "smart-default",
562
+ },
563
+ ],
564
+ [
565
+ ".java",
566
+ {
567
+ formatterNames: ["google-java-format"],
568
+ defaultFormatter: "google-java-format",
569
+ defaultWhenUnconfigured: false,
570
+ gate: "config-first",
571
+ },
572
+ ],
573
+ [
574
+ ".clj",
575
+ {
576
+ formatterNames: ["cljfmt"],
577
+ defaultFormatter: "cljfmt",
578
+ defaultWhenUnconfigured: false,
579
+ gate: "config-first",
580
+ },
581
+ ],
582
+ [
583
+ ".cljc",
584
+ {
585
+ formatterNames: ["cljfmt"],
586
+ defaultFormatter: "cljfmt",
587
+ defaultWhenUnconfigured: false,
588
+ gate: "config-first",
589
+ },
590
+ ],
591
+ [
592
+ ".cljs",
593
+ {
594
+ formatterNames: ["cljfmt"],
595
+ defaultFormatter: "cljfmt",
596
+ defaultWhenUnconfigured: false,
597
+ gate: "config-first",
598
+ },
599
+ ],
600
+ [
601
+ ".cmake",
602
+ {
603
+ formatterNames: ["cmake-format"],
604
+ defaultFormatter: "cmake-format",
605
+ defaultWhenUnconfigured: false,
606
+ gate: "config-first",
607
+ },
608
+ ],
609
+ [
610
+ ".ps1",
611
+ {
612
+ formatterNames: ["psscriptanalyzer-format"],
613
+ defaultFormatter: "psscriptanalyzer-format",
614
+ defaultWhenUnconfigured: true,
615
+ gate: "smart-default",
616
+ },
617
+ ],
618
+ [
619
+ ".psm1",
620
+ {
621
+ formatterNames: ["psscriptanalyzer-format"],
622
+ defaultFormatter: "psscriptanalyzer-format",
623
+ defaultWhenUnconfigured: true,
624
+ gate: "smart-default",
625
+ },
626
+ ],
627
+ [
628
+ ".psd1",
629
+ {
630
+ formatterNames: ["psscriptanalyzer-format"],
631
+ defaultFormatter: "psscriptanalyzer-format",
632
+ defaultWhenUnconfigured: true,
633
+ gate: "smart-default",
634
+ },
635
+ ],
636
+ ]);
637
+
638
+ const AUTO_INSTALLABLE_DEFAULT_FORMATTERS = new Map<string, string>([
639
+ ["biome", "biome"],
640
+ ["ruff", "ruff"],
641
+ ["prettier", "prettier"],
642
+ ["shfmt", "shfmt"],
643
+ ["taplo", "taplo"],
644
+ ["ktlint", "ktlint"],
645
+ ]);
646
+
647
+ export function getFormatterPolicyForExtension(
648
+ ext: string,
649
+ ): FormatterPolicy | undefined {
650
+ return FORMATTER_POLICY_BY_EXTENSION.get(ext.toLowerCase());
651
+ }
652
+
653
+ export function getFormatterPolicyForFile(
654
+ filePath: string,
655
+ ): FormatterPolicy | undefined {
656
+ return getFormatterPolicyForExtension(path.extname(filePath));
657
+ }
658
+
659
+ export function getSmartDefaultFormatterName(
660
+ filePath: string,
661
+ ): string | undefined {
662
+ const policy = getFormatterPolicyForFile(filePath);
663
+ if (!policy?.defaultWhenUnconfigured) return undefined;
664
+ return policy.defaultFormatter;
665
+ }
666
+
667
+ export function getAutoInstallToolIdForFormatter(
668
+ formatterName: string,
669
+ ): string | undefined {
670
+ return AUTO_INSTALLABLE_DEFAULT_FORMATTERS.get(formatterName);
671
+ }
672
+
673
+ export function getToolExecutionPolicy(
674
+ toolId: string,
675
+ ): ToolExecutionPolicy | undefined {
676
+ return TOOL_EXECUTION_POLICY.get(toolId);
677
+ }
678
+
679
+ export function shouldAutoInstallTool(toolId: string): boolean {
680
+ return getToolExecutionPolicy(toolId)?.autoInstall ?? false;
681
+ }
682
+
683
+ export function getAutofixCapability(
684
+ toolId: string,
685
+ ): AutofixCapability | undefined {
686
+ return AUTOFIX_CAPABILITIES.get(toolId);
687
+ }
688
+
689
+ export function canToolAutoFix(toolId: string): boolean {
690
+ return getAutofixCapability(toolId)?.toolSupportsFix ?? false;
691
+ }
692
+
693
+ export function isSafePipelineAutofixTool(toolId: string): boolean {
694
+ return getAutofixCapability(toolId)?.safePipelineAutofix ?? false;
695
+ }
696
+
697
+ export function getToolCommandSpec(
698
+ toolId: string,
699
+ ): ToolCommandSpec | undefined {
700
+ return TOOL_COMMAND_SPECS.get(toolId);
701
+ }
702
+
703
+ export type AutofixToolName =
704
+ | "biome"
705
+ | "eslint"
706
+ | "ruff"
707
+ | "stylelint"
708
+ | "sqlfluff"
709
+ | "rubocop"
710
+ | "ktlint"
711
+ | "rust-clippy"
712
+ | "dart-analyze";
713
+
714
+ export type LintRunnerName =
715
+ | JstsLintRunnerName
716
+ | "ruff-lint"
717
+ | "stylelint"
718
+ | "sqlfluff"
719
+ | "rubocop"
720
+ | "yamllint"
721
+ | "markdownlint"
722
+ | "htmlhint"
723
+ | "hadolint"
724
+ | "golangci-lint"
725
+ | "phpstan"
726
+ | "ktlint"
727
+ | "taplo"
728
+ | "rust-clippy"
729
+ | "shellcheck"
730
+ | "tflint"
731
+ | "credo"
732
+ | "cpp-check"
733
+ | "dart-analyze"
734
+ | "gleam-check"
735
+ | "psscriptanalyzer"
736
+ | "prisma-validate"
737
+ | "mypy"
738
+ | "detekt";
739
+
740
+ export interface LinterPolicy {
741
+ runnerNames: LintRunnerName[];
742
+ preferredRunners: LintRunnerName[];
743
+ defaultRunner?: LintRunnerName;
744
+ defaultWhenUnconfigured: boolean;
745
+ gate: ToolGate;
746
+ }
747
+
748
+ export interface AutofixPolicy {
749
+ toolNames: AutofixToolName[];
750
+ preferredTools: AutofixToolName[];
751
+ defaultTool?: AutofixToolName;
752
+ defaultWhenUnconfigured: boolean;
753
+ gate: ToolGate;
754
+ safe: boolean;
755
+ }
756
+
757
+ export interface AutofixCapability {
758
+ toolSupportsFix: boolean;
759
+ safePipelineAutofix: boolean;
760
+ fixKind: "pipeline" | "manual" | "suggestion" | "none";
761
+ }
762
+
763
+ export interface ToolExecutionPolicy {
764
+ gate: ToolGate;
765
+ autoInstall: boolean;
766
+ }
767
+
768
+ export interface ToolCommandSpec {
769
+ command: string;
770
+ windowsExt?: string;
771
+ versionArgs?: string[];
772
+ managedToolId?: string;
773
+ }
774
+
775
+ const AUTOFIX_CAPABILITIES = new Map<string, AutofixCapability>([
776
+ [
777
+ "biome",
778
+ { toolSupportsFix: true, safePipelineAutofix: true, fixKind: "pipeline" },
779
+ ],
780
+ [
781
+ "eslint",
782
+ { toolSupportsFix: true, safePipelineAutofix: true, fixKind: "pipeline" },
783
+ ],
784
+ [
785
+ "ruff",
786
+ { toolSupportsFix: true, safePipelineAutofix: true, fixKind: "pipeline" },
787
+ ],
788
+ [
789
+ "stylelint",
790
+ { toolSupportsFix: true, safePipelineAutofix: true, fixKind: "pipeline" },
791
+ ],
792
+ [
793
+ "sqlfluff",
794
+ { toolSupportsFix: true, safePipelineAutofix: true, fixKind: "pipeline" },
795
+ ],
796
+ [
797
+ "rubocop",
798
+ { toolSupportsFix: true, safePipelineAutofix: true, fixKind: "pipeline" },
799
+ ],
800
+ [
801
+ "ktlint",
802
+ { toolSupportsFix: true, safePipelineAutofix: true, fixKind: "pipeline" },
803
+ ],
804
+ [
805
+ "rust-clippy",
806
+ { toolSupportsFix: true, safePipelineAutofix: true, fixKind: "pipeline" },
807
+ ],
808
+ [
809
+ "dart-analyze",
810
+ { toolSupportsFix: true, safePipelineAutofix: true, fixKind: "pipeline" },
811
+ ],
812
+ ]);
813
+
814
+ const TOOL_EXECUTION_POLICY = new Map<string, ToolExecutionPolicy>([
815
+ ["biome", { gate: "smart-default", autoInstall: true }],
816
+ ["ruff", { gate: "smart-default", autoInstall: true }],
817
+ ["oxlint", { gate: "smart-default", autoInstall: true }],
818
+ ["stylelint", { gate: "smart-default", autoInstall: true }],
819
+ ["sqlfluff", { gate: "smart-default", autoInstall: true }],
820
+ ["rubocop", { gate: "smart-default", autoInstall: true }],
821
+ ["yamllint", { gate: "smart-default", autoInstall: true }],
822
+ ["markdownlint", { gate: "smart-default", autoInstall: true }],
823
+ ["mypy", { gate: "config-first", autoInstall: true }],
824
+ ["taplo", { gate: "smart-default", autoInstall: true }],
825
+ ["hadolint", { gate: "smart-default", autoInstall: true }],
826
+ ["htmlhint", { gate: "smart-default", autoInstall: true }],
827
+ ["ktlint", { gate: "smart-default", autoInstall: true }],
828
+ ["golangci-lint", { gate: "config-first", autoInstall: true }],
829
+ ["phpstan", { gate: "config-first", autoInstall: false }],
830
+ ["eslint", { gate: "config-first", autoInstall: false }],
831
+ ["prettier", { gate: "smart-default", autoInstall: true }],
832
+ ]);
833
+
834
+ const TOOL_COMMAND_SPECS = new Map<string, ToolCommandSpec>([
835
+ [
836
+ "eslint",
837
+ {
838
+ command: "eslint",
839
+ windowsExt: ".cmd",
840
+ versionArgs: ["--version"],
841
+ managedToolId: "eslint",
842
+ },
843
+ ],
844
+ [
845
+ "stylelint",
846
+ {
847
+ command: "stylelint",
848
+ windowsExt: ".cmd",
849
+ versionArgs: ["--version"],
850
+ managedToolId: "stylelint",
851
+ },
852
+ ],
853
+ [
854
+ "sqlfluff",
855
+ {
856
+ command: "sqlfluff",
857
+ windowsExt: ".exe",
858
+ versionArgs: ["--version"],
859
+ managedToolId: "sqlfluff",
860
+ },
861
+ ],
862
+ [
863
+ "oxlint",
864
+ {
865
+ command: "oxlint",
866
+ windowsExt: ".exe",
867
+ versionArgs: ["--version"],
868
+ managedToolId: "oxlint",
869
+ },
870
+ ],
871
+ [
872
+ "ruff",
873
+ {
874
+ command: "ruff",
875
+ windowsExt: ".exe",
876
+ versionArgs: ["--version"],
877
+ managedToolId: "ruff",
878
+ },
879
+ ],
880
+ [
881
+ "biome",
882
+ {
883
+ command: "biome",
884
+ windowsExt: ".cmd",
885
+ versionArgs: ["--version"],
886
+ managedToolId: "biome",
887
+ },
888
+ ],
889
+ [
890
+ "rubocop",
891
+ {
892
+ command: "rubocop",
893
+ versionArgs: ["--version"],
894
+ managedToolId: "rubocop",
895
+ },
896
+ ],
897
+ [
898
+ "yamllint",
899
+ {
900
+ command: "yamllint",
901
+ windowsExt: ".exe",
902
+ versionArgs: ["--version"],
903
+ managedToolId: "yamllint",
904
+ },
905
+ ],
906
+ [
907
+ "markdownlint",
908
+ {
909
+ command: "markdownlint-cli2",
910
+ windowsExt: ".cmd",
911
+ versionArgs: ["--version"],
912
+ managedToolId: "markdownlint",
913
+ },
914
+ ],
915
+ [
916
+ "mypy",
917
+ {
918
+ command: "mypy",
919
+ versionArgs: ["--version"],
920
+ managedToolId: "mypy",
921
+ },
922
+ ],
923
+ [
924
+ "phpstan",
925
+ {
926
+ command: "phpstan",
927
+ windowsExt: ".bat",
928
+ versionArgs: ["--version"],
929
+ managedToolId: "phpstan",
930
+ },
931
+ ],
932
+ [
933
+ "taplo",
934
+ {
935
+ command: "taplo",
936
+ windowsExt: ".exe",
937
+ versionArgs: ["--version"],
938
+ managedToolId: "taplo",
939
+ },
940
+ ],
941
+ [
942
+ "hadolint",
943
+ {
944
+ command: "hadolint",
945
+ windowsExt: ".exe",
946
+ versionArgs: ["--version"],
947
+ managedToolId: "hadolint",
948
+ },
949
+ ],
950
+ [
951
+ "htmlhint",
952
+ {
953
+ command: "htmlhint",
954
+ versionArgs: ["--version"],
955
+ managedToolId: "htmlhint",
956
+ },
957
+ ],
958
+ [
959
+ "ktlint",
960
+ {
961
+ command: "ktlint",
962
+ windowsExt: ".exe",
963
+ versionArgs: ["--version"],
964
+ managedToolId: "ktlint",
965
+ },
966
+ ],
967
+ [
968
+ "prettier",
969
+ {
970
+ command: "prettier",
971
+ windowsExt: ".cmd",
972
+ versionArgs: ["--version"],
973
+ managedToolId: "prettier",
974
+ },
975
+ ],
976
+ ]);
977
+
978
+ const STYLELINT_CONFIGS = [
979
+ ".stylelintrc",
980
+ ".stylelintrc.json",
981
+ ".stylelintrc.jsonc",
982
+ ".stylelintrc.yaml",
983
+ ".stylelintrc.yml",
984
+ ".stylelintrc.js",
985
+ ".stylelintrc.cjs",
986
+ "stylelint.config.js",
987
+ "stylelint.config.cjs",
988
+ "stylelint.config.mjs",
989
+ ];
990
+
991
+ const SQLFLUFF_CONFIGS = [
992
+ ".sqlfluff",
993
+ "pyproject.toml",
994
+ "setup.cfg",
995
+ "tox.ini",
996
+ ];
997
+
998
+ const RUBOCOP_CONFIGS = [".rubocop.yml", ".rubocop.yaml"];
999
+
1000
+ const MYPY_CONFIGS = ["mypy.ini", ".mypy.ini", "setup.cfg", "pyproject.toml"];
1001
+
1002
+ const YAMLLINT_CONFIGS = [
1003
+ ".yamllint",
1004
+ ".yamllint.yml",
1005
+ ".yamllint.yaml",
1006
+ "pyproject.toml",
1007
+ "setup.cfg",
1008
+ "tox.ini",
1009
+ ];
1010
+
1011
+ const MARKDOWNLINT_CONFIGS = [
1012
+ ".markdownlint.json",
1013
+ ".markdownlint.jsonc",
1014
+ ".markdownlint.yaml",
1015
+ ".markdownlint.yml",
1016
+ ".markdownlintrc",
1017
+ ];
1018
+
1019
+ const PRETTIER_CONFIGS = [
1020
+ ".prettierrc",
1021
+ ".prettierrc.json",
1022
+ ".prettierrc.yml",
1023
+ ".prettierrc.yaml",
1024
+ ".prettierrc.js",
1025
+ ".prettierrc.cjs",
1026
+ ".prettierrc.mjs",
1027
+ "prettier.config.js",
1028
+ "prettier.config.cjs",
1029
+ "prettier.config.mjs",
1030
+ "prettier.config.ts",
1031
+ ];
1032
+
1033
+ const RUFF_PROJECT_CONFIGS = ["ruff.toml", ".ruff.toml"];
1034
+
1035
+ const GOLANGCI_CONFIGS = [
1036
+ ".golangci.yml",
1037
+ ".golangci.yaml",
1038
+ ".golangci.toml",
1039
+ ".golangci.json",
1040
+ ];
1041
+
1042
+ const PHPSTAN_CONFIGS = [
1043
+ "phpstan.neon",
1044
+ "phpstan.neon.dist",
1045
+ "phpstan.dist.neon",
1046
+ ];
1047
+
1048
+ export type JstsLintRunnerName = "eslint" | "oxlint" | "biome-check-json";
1049
+
1050
+ export interface JstsLintPolicyContext {
1051
+ hasEslintConfig?: boolean;
1052
+ hasOxlintConfig?: boolean;
1053
+ hasBiomeConfig?: boolean;
1054
+ }
1055
+
1056
+ export interface JstsLintPolicy extends Required<JstsLintPolicyContext> {
1057
+ preferredRunners: JstsLintRunnerName[];
1058
+ hasExplicitNonBiomeLinter: boolean;
1059
+ }
1060
+
1061
+ export interface LinterPolicyContext {
1062
+ hasEslintConfig?: boolean;
1063
+ hasOxlintConfig?: boolean;
1064
+ hasBiomeConfig?: boolean;
1065
+ hasStylelintConfig?: boolean;
1066
+ hasSqlfluffConfig?: boolean;
1067
+ hasRubocopConfig?: boolean;
1068
+ hasYamllintConfig?: boolean;
1069
+ hasMarkdownlintConfig?: boolean;
1070
+ hasGolangciConfig?: boolean;
1071
+ hasPhpstanConfig?: boolean;
1072
+ hasMypyConfig?: boolean;
1073
+ hasDetektConfig?: boolean;
1074
+ }
1075
+
1076
+ export interface AutofixPolicyContext {
1077
+ hasEslintConfig?: boolean;
1078
+ hasStylelintConfig?: boolean;
1079
+ hasSqlfluffConfig?: boolean;
1080
+ hasRubocopConfig?: boolean;
1081
+ hasBiomeConfig?: boolean;
1082
+ }
1083
+
1084
+ export function getLinterPolicyForFile(
1085
+ filePath: string,
1086
+ context: LinterPolicyContext = {},
1087
+ ): LinterPolicy | undefined {
1088
+ const ext = path.extname(filePath).toLowerCase();
1089
+
1090
+ if ([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"].includes(ext)) {
1091
+ const policy = getJstsLintPolicy({
1092
+ hasEslintConfig: context.hasEslintConfig,
1093
+ hasOxlintConfig: context.hasOxlintConfig,
1094
+ hasBiomeConfig: context.hasBiomeConfig,
1095
+ });
1096
+ return {
1097
+ runnerNames: ["eslint", "oxlint", "biome-check-json"],
1098
+ preferredRunners: policy.preferredRunners,
1099
+ defaultRunner: policy.preferredRunners[0],
1100
+ defaultWhenUnconfigured:
1101
+ !policy.hasEslintConfig && !policy.hasOxlintConfig,
1102
+ gate: policy.hasEslintConfig ? "config-first" : "smart-default",
1103
+ };
1104
+ }
1105
+
1106
+ if ([".py", ".pyi"].includes(ext)) {
1107
+ const preferredRunners: LintRunnerName[] = ["ruff-lint"];
1108
+ if (context.hasMypyConfig) preferredRunners.push("mypy");
1109
+ return {
1110
+ runnerNames: ["ruff-lint", "mypy"],
1111
+ preferredRunners,
1112
+ defaultRunner: "ruff-lint",
1113
+ defaultWhenUnconfigured: true,
1114
+ gate: context.hasMypyConfig ? "mixed" : "smart-default",
1115
+ };
1116
+ }
1117
+
1118
+ if ([".css", ".scss", ".sass", ".less"].includes(ext)) {
1119
+ return {
1120
+ runnerNames: ["stylelint"],
1121
+ preferredRunners: ["stylelint"],
1122
+ defaultRunner: "stylelint",
1123
+ defaultWhenUnconfigured: true,
1124
+ gate: "smart-default",
1125
+ };
1126
+ }
1127
+
1128
+ if (ext === ".sql") {
1129
+ return {
1130
+ runnerNames: ["sqlfluff"],
1131
+ preferredRunners: ["sqlfluff"],
1132
+ defaultRunner: "sqlfluff",
1133
+ defaultWhenUnconfigured: true,
1134
+ gate: "smart-default",
1135
+ };
1136
+ }
1137
+
1138
+ if ([".rb", ".rake", ".gemspec", ".ru"].includes(ext)) {
1139
+ return {
1140
+ runnerNames: ["rubocop"],
1141
+ preferredRunners: ["rubocop"],
1142
+ defaultRunner: "rubocop",
1143
+ defaultWhenUnconfigured: true,
1144
+ gate: "smart-default",
1145
+ };
1146
+ }
1147
+
1148
+ if ([".yaml", ".yml"].includes(ext)) {
1149
+ return {
1150
+ runnerNames: ["yamllint"],
1151
+ preferredRunners: ["yamllint"],
1152
+ defaultRunner: "yamllint",
1153
+ defaultWhenUnconfigured: true,
1154
+ gate: "smart-default",
1155
+ };
1156
+ }
1157
+
1158
+ if ([".md", ".mdx"].includes(ext)) {
1159
+ return {
1160
+ runnerNames: ["markdownlint"],
1161
+ preferredRunners: ["markdownlint"],
1162
+ defaultRunner: "markdownlint",
1163
+ defaultWhenUnconfigured: true,
1164
+ gate: "smart-default",
1165
+ };
1166
+ }
1167
+
1168
+ if ([".html", ".htm"].includes(ext)) {
1169
+ return {
1170
+ runnerNames: ["htmlhint"],
1171
+ preferredRunners: ["htmlhint"],
1172
+ defaultRunner: "htmlhint",
1173
+ defaultWhenUnconfigured: true,
1174
+ gate: "smart-default",
1175
+ };
1176
+ }
1177
+
1178
+ if (path.basename(filePath).toLowerCase() === "dockerfile") {
1179
+ return {
1180
+ runnerNames: ["hadolint"],
1181
+ preferredRunners: ["hadolint"],
1182
+ defaultRunner: "hadolint",
1183
+ defaultWhenUnconfigured: true,
1184
+ gate: "smart-default",
1185
+ };
1186
+ }
1187
+
1188
+ if ([".kt", ".kts"].includes(ext)) {
1189
+ const preferredRunners: LintRunnerName[] = ["ktlint"];
1190
+ if (context.hasDetektConfig) preferredRunners.push("detekt");
1191
+ return {
1192
+ runnerNames: ["ktlint", "detekt"],
1193
+ preferredRunners,
1194
+ defaultRunner: "ktlint",
1195
+ defaultWhenUnconfigured: true,
1196
+ gate: context.hasDetektConfig ? "mixed" : "smart-default",
1197
+ };
1198
+ }
1199
+
1200
+ if (ext === ".toml") {
1201
+ return {
1202
+ runnerNames: ["taplo"],
1203
+ preferredRunners: ["taplo"],
1204
+ defaultRunner: "taplo",
1205
+ defaultWhenUnconfigured: true,
1206
+ gate: "smart-default",
1207
+ };
1208
+ }
1209
+
1210
+ if (ext === ".go") {
1211
+ return {
1212
+ runnerNames: ["golangci-lint"],
1213
+ preferredRunners: context.hasGolangciConfig ? ["golangci-lint"] : [],
1214
+ defaultRunner: "golangci-lint",
1215
+ defaultWhenUnconfigured: false,
1216
+ gate: "config-first",
1217
+ };
1218
+ }
1219
+
1220
+ if (ext === ".php") {
1221
+ return {
1222
+ runnerNames: ["phpstan"],
1223
+ preferredRunners: context.hasPhpstanConfig ? ["phpstan"] : [],
1224
+ defaultRunner: "phpstan",
1225
+ defaultWhenUnconfigured: false,
1226
+ gate: "config-first",
1227
+ };
1228
+ }
1229
+
1230
+ if (ext === ".rs") {
1231
+ return {
1232
+ runnerNames: ["rust-clippy"],
1233
+ preferredRunners: ["rust-clippy"],
1234
+ defaultRunner: "rust-clippy",
1235
+ defaultWhenUnconfigured: true,
1236
+ gate: "smart-default",
1237
+ };
1238
+ }
1239
+
1240
+ if ([".sh", ".bash"].includes(ext)) {
1241
+ return {
1242
+ runnerNames: ["shellcheck"],
1243
+ preferredRunners: ["shellcheck"],
1244
+ defaultRunner: "shellcheck",
1245
+ defaultWhenUnconfigured: true,
1246
+ gate: "smart-default",
1247
+ };
1248
+ }
1249
+
1250
+ if ([".tf", ".tfvars"].includes(ext)) {
1251
+ return {
1252
+ runnerNames: ["tflint"],
1253
+ preferredRunners: ["tflint"],
1254
+ defaultRunner: "tflint",
1255
+ defaultWhenUnconfigured: true,
1256
+ gate: "smart-default",
1257
+ };
1258
+ }
1259
+
1260
+ if ([".ex", ".exs", ".eex", ".heex", ".leex"].includes(ext)) {
1261
+ return {
1262
+ runnerNames: ["credo"],
1263
+ preferredRunners: ["credo"],
1264
+ defaultRunner: "credo",
1265
+ defaultWhenUnconfigured: true,
1266
+ gate: "smart-default",
1267
+ };
1268
+ }
1269
+
1270
+ if ([".c", ".cc", ".cpp", ".cxx", ".h", ".hpp", ".ino"].includes(ext)) {
1271
+ return {
1272
+ runnerNames: ["cpp-check"],
1273
+ preferredRunners: ["cpp-check"],
1274
+ defaultRunner: "cpp-check",
1275
+ defaultWhenUnconfigured: true,
1276
+ gate: "smart-default",
1277
+ };
1278
+ }
1279
+
1280
+ if (ext === ".dart") {
1281
+ return {
1282
+ runnerNames: ["dart-analyze"],
1283
+ preferredRunners: ["dart-analyze"],
1284
+ defaultRunner: "dart-analyze",
1285
+ defaultWhenUnconfigured: true,
1286
+ gate: "smart-default",
1287
+ };
1288
+ }
1289
+
1290
+ if (ext === ".gleam") {
1291
+ return {
1292
+ runnerNames: ["gleam-check"],
1293
+ preferredRunners: ["gleam-check"],
1294
+ defaultRunner: "gleam-check",
1295
+ defaultWhenUnconfigured: true,
1296
+ gate: "smart-default",
1297
+ };
1298
+ }
1299
+
1300
+ if ([".ps1", ".psm1", ".psd1"].includes(ext)) {
1301
+ return {
1302
+ runnerNames: ["psscriptanalyzer"],
1303
+ preferredRunners: ["psscriptanalyzer"],
1304
+ defaultRunner: "psscriptanalyzer",
1305
+ defaultWhenUnconfigured: true,
1306
+ gate: "smart-default",
1307
+ };
1308
+ }
1309
+
1310
+ if (ext === ".prisma") {
1311
+ return {
1312
+ runnerNames: ["prisma-validate"],
1313
+ preferredRunners: ["prisma-validate"],
1314
+ defaultRunner: "prisma-validate",
1315
+ defaultWhenUnconfigured: true,
1316
+ gate: "smart-default",
1317
+ };
1318
+ }
1319
+
1320
+ return undefined;
1321
+ }
1322
+
1323
+ export function getLinterPolicyForCwd(
1324
+ filePath: string,
1325
+ cwd: string,
1326
+ ): LinterPolicy | undefined {
1327
+ const context: LinterPolicyContext = {
1328
+ hasEslintConfig: hasEslintConfig(cwd),
1329
+ hasOxlintConfig: hasOxlintConfig(cwd),
1330
+ hasBiomeConfig: hasBiomeConfig(cwd),
1331
+ hasStylelintConfig: hasStylelintConfig(cwd),
1332
+ hasSqlfluffConfig: hasSqlfluffConfig(cwd),
1333
+ hasRubocopConfig: hasRubocopConfig(cwd),
1334
+ hasYamllintConfig: hasYamllintConfig(cwd),
1335
+ hasMarkdownlintConfig: hasMarkdownlintConfig(cwd),
1336
+ hasGolangciConfig: hasGolangciConfig(cwd),
1337
+ hasPhpstanConfig: hasPhpstanConfig(cwd),
1338
+ hasMypyConfig: hasMypyConfig(cwd),
1339
+ hasDetektConfig: hasDetektConfig(cwd),
1340
+ };
1341
+ const policy = getLinterPolicyForFile(filePath, context);
1342
+ logLatency({
1343
+ type: "phase",
1344
+ phase: "linter_selected",
1345
+ filePath,
1346
+ durationMs: 0,
1347
+ metadata: {
1348
+ runner: policy?.defaultRunner ?? null,
1349
+ gate: policy?.gate ?? null,
1350
+ cwd,
1351
+ context,
1352
+ },
1353
+ });
1354
+ return policy;
1355
+ }
1356
+
1357
+ export function getAutofixPolicyForFile(
1358
+ filePath: string,
1359
+ context: AutofixPolicyContext = {},
1360
+ ): AutofixPolicy | undefined {
1361
+ const ext = path.extname(filePath).toLowerCase();
1362
+
1363
+ if ([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"].includes(ext)) {
1364
+ if (context.hasEslintConfig) {
1365
+ return {
1366
+ toolNames: ["eslint", "biome"],
1367
+ preferredTools: ["eslint"],
1368
+ defaultTool: "eslint",
1369
+ defaultWhenUnconfigured: false,
1370
+ gate: "config-first",
1371
+ safe: true,
1372
+ };
1373
+ }
1374
+ return {
1375
+ toolNames: ["eslint", "biome"],
1376
+ preferredTools: ["biome"],
1377
+ defaultTool: "biome",
1378
+ defaultWhenUnconfigured: true,
1379
+ gate: "smart-default",
1380
+ safe: true,
1381
+ };
1382
+ }
1383
+
1384
+ if ([".json", ".jsonc"].includes(ext)) {
1385
+ if (!context.hasBiomeConfig) {
1386
+ return undefined;
1387
+ }
1388
+ return {
1389
+ toolNames: ["biome"],
1390
+ preferredTools: ["biome"],
1391
+ defaultTool: "biome",
1392
+ defaultWhenUnconfigured: false,
1393
+ gate: "config-first",
1394
+ safe: true,
1395
+ };
1396
+ }
1397
+
1398
+ if ([".py", ".pyi"].includes(ext)) {
1399
+ return {
1400
+ toolNames: ["ruff"],
1401
+ preferredTools: ["ruff"],
1402
+ defaultTool: "ruff",
1403
+ defaultWhenUnconfigured: true,
1404
+ gate: "smart-default",
1405
+ safe: true,
1406
+ };
1407
+ }
1408
+
1409
+ if ([".css", ".scss", ".sass", ".less"].includes(ext)) {
1410
+ return {
1411
+ toolNames: ["stylelint"],
1412
+ preferredTools: ["stylelint"],
1413
+ defaultTool: "stylelint",
1414
+ defaultWhenUnconfigured: true,
1415
+ gate: "smart-default",
1416
+ safe: true,
1417
+ };
1418
+ }
1419
+
1420
+ if (ext === ".sql") {
1421
+ return {
1422
+ toolNames: ["sqlfluff"],
1423
+ preferredTools: ["sqlfluff"],
1424
+ defaultTool: "sqlfluff",
1425
+ defaultWhenUnconfigured: true,
1426
+ gate: "smart-default",
1427
+ safe: true,
1428
+ };
1429
+ }
1430
+
1431
+ if ([".rb", ".rake", ".gemspec", ".ru"].includes(ext)) {
1432
+ return {
1433
+ toolNames: ["rubocop"],
1434
+ preferredTools: ["rubocop"],
1435
+ defaultTool: "rubocop",
1436
+ defaultWhenUnconfigured: true,
1437
+ gate: "smart-default",
1438
+ safe: true,
1439
+ };
1440
+ }
1441
+
1442
+ if ([".kt", ".kts"].includes(ext)) {
1443
+ return {
1444
+ toolNames: ["ktlint"],
1445
+ preferredTools: ["ktlint"],
1446
+ defaultTool: "ktlint",
1447
+ defaultWhenUnconfigured: true,
1448
+ gate: "smart-default",
1449
+ safe: true,
1450
+ };
1451
+ }
1452
+
1453
+ if (ext === ".rs") {
1454
+ return {
1455
+ toolNames: ["rust-clippy"],
1456
+ preferredTools: ["rust-clippy"],
1457
+ defaultTool: "rust-clippy",
1458
+ defaultWhenUnconfigured: true,
1459
+ gate: "smart-default",
1460
+ safe: true,
1461
+ };
1462
+ }
1463
+
1464
+ if (ext === ".dart") {
1465
+ return {
1466
+ toolNames: ["dart-analyze"],
1467
+ preferredTools: ["dart-analyze"],
1468
+ defaultTool: "dart-analyze",
1469
+ defaultWhenUnconfigured: true,
1470
+ gate: "smart-default",
1471
+ safe: true,
1472
+ };
1473
+ }
1474
+
1475
+ return undefined;
1476
+ }
1477
+
1478
+ export function getPreferredAutofixTools(
1479
+ filePath: string,
1480
+ context: AutofixPolicyContext,
1481
+ ): AutofixToolName[] {
1482
+ return getAutofixPolicyForFile(filePath, context)?.preferredTools ?? [];
1483
+ }
1484
+
1485
+ const ESLINT_CONFIGS = [
1486
+ ".eslintrc",
1487
+ ".eslintrc.js",
1488
+ ".eslintrc.cjs",
1489
+ ".eslintrc.json",
1490
+ ".eslintrc.yaml",
1491
+ ".eslintrc.yml",
1492
+ "eslint.config.js",
1493
+ "eslint.config.mjs",
1494
+ "eslint.config.cjs",
1495
+ "eslint.config.ts",
1496
+ ];
1497
+
1498
+ function walkUpDirs(cwd: string): string[] {
1499
+ const dirs: string[] = [];
1500
+ let dir = cwd;
1501
+ const root = path.parse(dir).root;
1502
+ while (true) {
1503
+ dirs.push(dir);
1504
+ if (dir === root) break;
1505
+ const parent = path.dirname(dir);
1506
+ if (parent === dir) break;
1507
+ dir = parent;
1508
+ }
1509
+ return dirs;
1510
+ }
1511
+
1512
+ function walkUpDirsUntilPackageJson(cwd: string): string[] {
1513
+ const dirs: string[] = [];
1514
+ for (const dir of walkUpDirs(cwd)) {
1515
+ dirs.push(dir);
1516
+ if (fs.existsSync(path.join(dir, "package.json"))) break;
1517
+ }
1518
+ return dirs;
1519
+ }
1520
+
1521
+ function findNearestPackageJsonPath(cwd: string): string | undefined {
1522
+ let dir = cwd;
1523
+ const root = path.parse(dir).root;
1524
+ while (true) {
1525
+ const pkgPath = path.join(dir, "package.json");
1526
+ if (fs.existsSync(pkgPath)) return pkgPath;
1527
+ if (dir === root) break;
1528
+ const parent = path.dirname(dir);
1529
+ if (parent === dir) break;
1530
+ dir = parent;
1531
+ }
1532
+ return undefined;
1533
+ }
1534
+
1535
+ export function hasNearestPackageJsonDependency(
1536
+ cwd: string,
1537
+ dependencyName: string,
1538
+ ): boolean {
1539
+ const pkgPath = findNearestPackageJsonPath(cwd);
1540
+ if (!pkgPath) return false;
1541
+ try {
1542
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) as {
1543
+ dependencies?: Record<string, string>;
1544
+ devDependencies?: Record<string, string>;
1545
+ };
1546
+ return Boolean(
1547
+ pkg.dependencies?.[dependencyName] ??
1548
+ pkg.devDependencies?.[dependencyName],
1549
+ );
1550
+ } catch {}
1551
+ return false;
1552
+ }
1553
+
1554
+ export function hasNearestPackageJsonField(
1555
+ cwd: string,
1556
+ fieldName: string,
1557
+ ): boolean {
1558
+ const pkgPath = findNearestPackageJsonPath(cwd);
1559
+ if (!pkgPath) return false;
1560
+ try {
1561
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) as Record<
1562
+ string,
1563
+ unknown
1564
+ >;
1565
+ return pkg[fieldName] !== undefined;
1566
+ } catch {}
1567
+ return false;
1568
+ }
1569
+
1570
+ export function hasEslintConfig(cwd: string): boolean {
1571
+ for (const dir of walkUpDirsUntilPackageJson(cwd)) {
1572
+ for (const cfg of ESLINT_CONFIGS) {
1573
+ if (fs.existsSync(path.join(dir, cfg))) return true;
1574
+ }
1575
+ const pkgPath = path.join(dir, "package.json");
1576
+ if (fs.existsSync(pkgPath)) {
1577
+ try {
1578
+ if (JSON.parse(fs.readFileSync(pkgPath, "utf-8")).eslintConfig)
1579
+ return true;
1580
+ } catch {}
1581
+ }
1582
+ }
1583
+ return false;
1584
+ }
1585
+
1586
+ export function hasBiomeConfig(cwd: string): boolean {
1587
+ return getBiomeConfigPath(cwd) !== undefined;
1588
+ }
1589
+
1590
+ export function getBiomeConfigPath(cwd: string): string | undefined {
1591
+ for (const dir of walkUpDirs(cwd)) {
1592
+ const jsoncPath = path.join(dir, "biome.jsonc");
1593
+ if (fs.existsSync(jsoncPath)) return jsoncPath;
1594
+ const jsonPath = path.join(dir, "biome.json");
1595
+ if (fs.existsSync(jsonPath)) return jsonPath;
1596
+ }
1597
+ return undefined;
1598
+ }
1599
+
1600
+ export function hasOxfmtConfig(cwd: string): boolean {
1601
+ let dir = cwd;
1602
+ const root = path.parse(dir).root;
1603
+ while (true) {
1604
+ if (fs.existsSync(path.join(dir, "oxfmt.toml"))) return true;
1605
+ if (fs.existsSync(path.join(dir, ".oxfmtrc.json"))) return true;
1606
+ const pkgPath = path.join(dir, "package.json");
1607
+ if (fs.existsSync(pkgPath)) {
1608
+ try {
1609
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) as Record<
1610
+ string,
1611
+ unknown
1612
+ >;
1613
+ const deps = {
1614
+ ...(pkg.dependencies as Record<string, unknown> | undefined),
1615
+ ...(pkg.devDependencies as Record<string, unknown> | undefined),
1616
+ };
1617
+ if (deps["@oxc-project/oxfmt"]) return true;
1618
+ } catch {}
1619
+ }
1620
+ if (dir === root) break;
1621
+ const parent = path.dirname(dir);
1622
+ if (parent === dir) break;
1623
+ dir = parent;
1624
+ }
1625
+ return false;
1626
+ }
1627
+
1628
+ export function hasStylelintConfig(cwd: string): boolean {
1629
+ for (const dir of walkUpDirs(cwd)) {
1630
+ if (STYLELINT_CONFIGS.some((cfg) => fs.existsSync(path.join(dir, cfg)))) {
1631
+ return true;
1632
+ }
1633
+ const pkgPath = path.join(dir, "package.json");
1634
+ if (fs.existsSync(pkgPath)) {
1635
+ try {
1636
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
1637
+ if (pkg.stylelint) return true;
1638
+ } catch {}
1639
+ }
1640
+ }
1641
+ return false;
1642
+ }
1643
+
1644
+ export function hasSqlfluffConfig(cwd: string): boolean {
1645
+ for (const dir of walkUpDirs(cwd)) {
1646
+ for (const cfg of SQLFLUFF_CONFIGS) {
1647
+ const cfgPath = path.join(dir, cfg);
1648
+ if (!fs.existsSync(cfgPath)) continue;
1649
+ if (cfg === "pyproject.toml") {
1650
+ try {
1651
+ const content = fs.readFileSync(cfgPath, "utf-8");
1652
+ if (content.includes("[tool.sqlfluff]")) return true;
1653
+ } catch {}
1654
+ continue;
1655
+ }
1656
+ if (cfg === "setup.cfg" || cfg === "tox.ini") {
1657
+ try {
1658
+ const content = fs.readFileSync(cfgPath, "utf-8");
1659
+ if (content.includes("[sqlfluff]")) return true;
1660
+ } catch {}
1661
+ continue;
1662
+ }
1663
+ return true;
1664
+ }
1665
+ }
1666
+
1667
+ for (const dir of walkUpDirs(cwd)) {
1668
+ for (const depFile of ["requirements.txt", "Pipfile", "pyproject.toml"]) {
1669
+ const depPath = path.join(dir, depFile);
1670
+ if (!fs.existsSync(depPath)) continue;
1671
+ try {
1672
+ const content = fs.readFileSync(depPath, "utf-8").toLowerCase();
1673
+ if (content.includes("sqlfluff")) return true;
1674
+ } catch {}
1675
+ }
1676
+ }
1677
+
1678
+ return false;
1679
+ }
1680
+
1681
+ export function hasRubocopConfig(cwd: string): boolean {
1682
+ for (const dir of walkUpDirs(cwd)) {
1683
+ for (const cfg of RUBOCOP_CONFIGS) {
1684
+ if (fs.existsSync(path.join(dir, cfg))) return true;
1685
+ }
1686
+ const gemfile = path.join(dir, "Gemfile");
1687
+ if (fs.existsSync(gemfile)) {
1688
+ try {
1689
+ const content = fs.readFileSync(gemfile, "utf-8");
1690
+ if (content.includes("rubocop")) return true;
1691
+ } catch {}
1692
+ }
1693
+ }
1694
+ return false;
1695
+ }
1696
+
1697
+ export function hasMypyConfig(cwd: string): boolean {
1698
+ for (const dir of walkUpDirs(cwd)) {
1699
+ for (const cfg of MYPY_CONFIGS) {
1700
+ const cfgPath = path.join(dir, cfg);
1701
+ if (!fs.existsSync(cfgPath)) continue;
1702
+ if (cfg === "setup.cfg") {
1703
+ try {
1704
+ if (fs.readFileSync(cfgPath, "utf-8").includes("[mypy]")) return true;
1705
+ } catch {}
1706
+ continue;
1707
+ }
1708
+ if (cfg === "pyproject.toml") {
1709
+ try {
1710
+ if (fs.readFileSync(cfgPath, "utf-8").includes("[tool.mypy]"))
1711
+ return true;
1712
+ } catch {}
1713
+ continue;
1714
+ }
1715
+ return true;
1716
+ }
1717
+ }
1718
+ return false;
1719
+ }
1720
+
1721
+ export function hasYamllintConfig(cwd: string): boolean {
1722
+ for (const dir of walkUpDirs(cwd)) {
1723
+ for (const cfg of YAMLLINT_CONFIGS) {
1724
+ const cfgPath = path.join(dir, cfg);
1725
+ if (!fs.existsSync(cfgPath)) continue;
1726
+ if (cfg === "pyproject.toml") {
1727
+ try {
1728
+ const content = fs.readFileSync(cfgPath, "utf-8");
1729
+ if (content.includes("[tool.yamllint]")) return true;
1730
+ } catch {}
1731
+ continue;
1732
+ }
1733
+ if (cfg === "setup.cfg" || cfg === "tox.ini") {
1734
+ try {
1735
+ const content = fs.readFileSync(cfgPath, "utf-8");
1736
+ if (content.includes("[yamllint]")) return true;
1737
+ } catch {}
1738
+ continue;
1739
+ }
1740
+ return true;
1741
+ }
1742
+ }
1743
+
1744
+ for (const dir of walkUpDirs(cwd)) {
1745
+ for (const depFile of ["requirements.txt", "Pipfile", "pyproject.toml"]) {
1746
+ const depPath = path.join(dir, depFile);
1747
+ if (!fs.existsSync(depPath)) continue;
1748
+ try {
1749
+ const content = fs.readFileSync(depPath, "utf-8").toLowerCase();
1750
+ if (content.includes("yamllint")) return true;
1751
+ } catch {}
1752
+ }
1753
+ }
1754
+
1755
+ return false;
1756
+ }
1757
+
1758
+ export function hasMarkdownlintConfig(cwd: string): boolean {
1759
+ return walkUpDirs(cwd).some((dir) =>
1760
+ MARKDOWNLINT_CONFIGS.some((cfg) => fs.existsSync(path.join(dir, cfg))),
1761
+ );
1762
+ }
1763
+
1764
+ export function hasPrettierConfig(cwd: string): boolean {
1765
+ for (const dir of walkUpDirs(cwd)) {
1766
+ if (PRETTIER_CONFIGS.some((cfg) => fs.existsSync(path.join(dir, cfg))))
1767
+ return true;
1768
+ const pkgPath = path.join(dir, "package.json");
1769
+ if (fs.existsSync(pkgPath)) {
1770
+ try {
1771
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
1772
+ if (Object.hasOwn(pkg, "prettier")) return true;
1773
+ } catch {}
1774
+ }
1775
+ }
1776
+ return false;
1777
+ }
1778
+
1779
+ export function hasBlackConfig(cwd: string): boolean {
1780
+ for (const dir of walkUpDirs(cwd)) {
1781
+ const pyproject = path.join(dir, "pyproject.toml");
1782
+ if (fs.existsSync(pyproject)) {
1783
+ try {
1784
+ if (fs.readFileSync(pyproject, "utf-8").includes("[tool.black]"))
1785
+ return true;
1786
+ } catch {}
1787
+ }
1788
+ }
1789
+
1790
+ for (const dir of walkUpDirs(cwd)) {
1791
+ for (const depFile of ["requirements.txt", "Pipfile"]) {
1792
+ const depPath = path.join(dir, depFile);
1793
+ if (!fs.existsSync(depPath)) continue;
1794
+ try {
1795
+ if (fs.readFileSync(depPath, "utf-8").toLowerCase().includes("black"))
1796
+ return true;
1797
+ } catch {}
1798
+ }
1799
+ }
1800
+
1801
+ return false;
1802
+ }
1803
+
1804
+ export function hasRuffConfig(cwd: string): boolean {
1805
+ for (const dir of walkUpDirs(cwd)) {
1806
+ for (const cfg of RUFF_PROJECT_CONFIGS) {
1807
+ if (fs.existsSync(path.join(dir, cfg))) return true;
1808
+ }
1809
+ const pyproject = path.join(dir, "pyproject.toml");
1810
+ if (fs.existsSync(pyproject)) {
1811
+ try {
1812
+ if (fs.readFileSync(pyproject, "utf-8").includes("[tool.ruff]"))
1813
+ return true;
1814
+ } catch {}
1815
+ }
1816
+ }
1817
+ return false;
1818
+ }
1819
+
1820
+ export function hasGolangciConfig(cwd: string): boolean {
1821
+ return walkUpDirs(cwd).some((dir) =>
1822
+ GOLANGCI_CONFIGS.some((cfg) => fs.existsSync(path.join(dir, cfg))),
1823
+ );
1824
+ }
1825
+
1826
+ export function hasClangFormatConfig(cwd: string): boolean {
1827
+ return walkUpDirs(cwd).some((dir) =>
1828
+ [".clang-format", "_clang-format"].some((cfg) =>
1829
+ fs.existsSync(path.join(dir, cfg)),
1830
+ ),
1831
+ );
1832
+ }
1833
+
1834
+ export function hasPhpCsFixerConfig(cwd: string): boolean {
1835
+ return walkUpDirs(cwd).some((dir) =>
1836
+ [".php-cs-fixer.php", ".php-cs-fixer.dist.php"].some((cfg) =>
1837
+ fs.existsSync(path.join(dir, cfg)),
1838
+ ),
1839
+ );
1840
+ }
1841
+
1842
+ export function hasStyluaConfig(cwd: string): boolean {
1843
+ return walkUpDirs(cwd).some((dir) =>
1844
+ ["stylua.toml", ".stylua.toml"].some((cfg) =>
1845
+ fs.existsSync(path.join(dir, cfg)),
1846
+ ),
1847
+ );
1848
+ }
1849
+
1850
+ export function hasOcamlformatConfig(cwd: string): boolean {
1851
+ return walkUpDirs(cwd).some((dir) =>
1852
+ fs.existsSync(path.join(dir, ".ocamlformat")),
1853
+ );
1854
+ }
1855
+
1856
+ export function hasGoogleJavaFormatConfig(cwd: string): boolean {
1857
+ // google-java-format has no standard config file — gate on .editorconfig
1858
+ // with indent_size defined (common Java project signal) or explicit opt-in marker.
1859
+ return walkUpDirs(cwd).some(
1860
+ (dir) =>
1861
+ fs.existsSync(path.join(dir, ".google-java-format")) ||
1862
+ fs.existsSync(path.join(dir, ".editorconfig")),
1863
+ );
1864
+ }
1865
+
1866
+ export function hasCljfmtConfig(cwd: string): boolean {
1867
+ return walkUpDirs(cwd).some((dir) =>
1868
+ [".cljfmt.edn", "cljfmt.edn", ".cljfmt"].some((cfg) =>
1869
+ fs.existsSync(path.join(dir, cfg)),
1870
+ ),
1871
+ );
1872
+ }
1873
+
1874
+ export function hasCmakeFormatConfig(cwd: string): boolean {
1875
+ return walkUpDirs(cwd).some((dir) =>
1876
+ [
1877
+ ".cmake-format",
1878
+ ".cmake-format.yaml",
1879
+ ".cmake-format.yml",
1880
+ ".cmake-format.json",
1881
+ ".cmake-format.py",
1882
+ "cmake-format.yaml",
1883
+ "cmake-format.yml",
1884
+ ].some((cfg) => fs.existsSync(path.join(dir, cfg))),
1885
+ );
1886
+ }
1887
+
1888
+ export function hasPhpstanConfig(cwd: string): boolean {
1889
+ return walkUpDirs(cwd).some((dir) =>
1890
+ PHPSTAN_CONFIGS.some((cfg) => fs.existsSync(path.join(dir, cfg))),
1891
+ );
1892
+ }
1893
+
1894
+ const DETEKT_CONFIGS = [
1895
+ "detekt.yml",
1896
+ ".detekt.yml",
1897
+ path.join("config", "detekt", "detekt.yml"),
1898
+ path.join("detekt", "detekt.yml"),
1899
+ ];
1900
+
1901
+ export function hasDetektConfig(cwd: string): boolean {
1902
+ for (const dir of walkUpDirs(cwd)) {
1903
+ if (DETEKT_CONFIGS.some((cfg) => fs.existsSync(path.join(dir, cfg))))
1904
+ return true;
1905
+ }
1906
+ return false;
1907
+ }
1908
+
1909
+ export function hasStandardrbConfig(cwd: string): boolean {
1910
+ for (const dir of walkUpDirs(cwd)) {
1911
+ const gemfile = path.join(dir, "Gemfile");
1912
+ if (fs.existsSync(gemfile)) {
1913
+ try {
1914
+ if (fs.readFileSync(gemfile, "utf-8").includes("standard")) return true;
1915
+ } catch {}
1916
+ }
1917
+ }
1918
+ return false;
1919
+ }
1920
+
1921
+ export function getRubocopCommand(cwd: string): {
1922
+ cmd: string;
1923
+ args: string[];
1924
+ } {
1925
+ const gemfile = path.join(cwd, "Gemfile");
1926
+ if (fs.existsSync(gemfile)) {
1927
+ try {
1928
+ const content = fs.readFileSync(gemfile, "utf-8");
1929
+ if (content.includes("rubocop")) {
1930
+ return { cmd: "bundle", args: ["exec", "rubocop"] };
1931
+ }
1932
+ } catch {}
1933
+ }
1934
+ return { cmd: "rubocop", args: [] };
1935
+ }
1936
+
1937
+ export function hasOxlintConfig(cwd: string): boolean {
1938
+ for (const dir of walkUpDirsUntilPackageJson(cwd)) {
1939
+ if (
1940
+ fs.existsSync(path.join(dir, ".oxlintrc.json")) ||
1941
+ fs.existsSync(path.join(dir, "oxlint.json"))
1942
+ )
1943
+ return true;
1944
+ }
1945
+ return false;
1946
+ }
1947
+
1948
+ export function getPreferredJstsLintRunners(
1949
+ context: JstsLintPolicyContext,
1950
+ ): JstsLintRunnerName[] {
1951
+ if (context.hasEslintConfig) return ["eslint"];
1952
+ if (context.hasOxlintConfig) return ["oxlint"];
1953
+ if (context.hasBiomeConfig) return ["biome-check-json"];
1954
+ return ["oxlint", "biome-check-json"];
1955
+ }
1956
+
1957
+ export function getJstsLintPolicy(
1958
+ context: JstsLintPolicyContext,
1959
+ ): JstsLintPolicy {
1960
+ const hasEslint = !!context.hasEslintConfig;
1961
+ const hasOxlint = !!context.hasOxlintConfig;
1962
+ const hasBiome = !!context.hasBiomeConfig;
1963
+ return {
1964
+ hasEslintConfig: hasEslint,
1965
+ hasOxlintConfig: hasOxlint,
1966
+ hasBiomeConfig: hasBiome,
1967
+ preferredRunners: getPreferredJstsLintRunners({
1968
+ hasEslintConfig: hasEslint,
1969
+ hasOxlintConfig: hasOxlint,
1970
+ hasBiomeConfig: hasBiome,
1971
+ }),
1972
+ hasExplicitNonBiomeLinter: hasEslint || hasOxlint,
1973
+ };
1974
+ }
1975
+
1976
+ export function getJstsLintPolicyForCwd(cwd: string): JstsLintPolicy {
1977
+ return getJstsLintPolicy({
1978
+ hasEslintConfig: hasEslintConfig(cwd),
1979
+ hasOxlintConfig: hasOxlintConfig(cwd),
1980
+ hasBiomeConfig: hasBiomeConfig(cwd),
1981
+ });
1982
+ }