code-ranker 2.0.0__tar.gz → 3.0.0a1__tar.gz

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 (110) hide show
  1. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/Cargo.lock +10 -10
  2. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/Cargo.toml +9 -9
  3. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/PKG-INFO +2 -2
  4. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/README.md +1 -1
  5. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-cli/src/check.rs +94 -3
  6. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-cli/src/cli.rs +21 -0
  7. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-cli/src/config/load.rs +33 -20
  8. code_ranker-3.0.0a1/crates/code-ranker-cli/src/config/metrics.rs +88 -0
  9. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-cli/src/config/mod.rs +1 -0
  10. code_ranker-3.0.0a1/crates/code-ranker-cli/src/config/model.rs +444 -0
  11. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-cli/src/config/rules.rs +23 -0
  12. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-cli/src/config/violations.rs +48 -41
  13. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-cli/src/main.rs +8 -0
  14. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-cli/src/plugin/mod.rs +47 -0
  15. code_ranker-3.0.0a1/crates/code-ranker-cli/src/recommend/prompt.rs +222 -0
  16. code_ranker-3.0.0a1/crates/code-ranker-cli/src/recommend/scorecard.rs +346 -0
  17. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-cli/src/recommend.rs +308 -554
  18. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-cli/src/report.rs +45 -2
  19. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-cli/tests/e2e.rs +192 -1
  20. code_ranker-3.0.0a1/crates/code-ranker-plugin-rust/src/collapse.rs +242 -0
  21. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-plugin-rust/src/lib.rs +7 -236
  22. code_ranker-3.0.0a1/crates/code-ranker-plugin-rust/src/module_graph/resolve.rs +758 -0
  23. code_ranker-3.0.0a1/crates/code-ranker-plugin-rust/src/module_graph/shared.rs +147 -0
  24. code_ranker-3.0.0a1/crates/code-ranker-plugin-rust/src/module_graph/walk.rs +691 -0
  25. code_ranker-3.0.0a1/crates/code-ranker-plugin-rust/src/module_graph.rs +206 -0
  26. code_ranker-2.0.0/crates/code-ranker-cli/src/config/model.rs +0 -256
  27. code_ranker-2.0.0/crates/code-ranker-plugin-rust/src/module_graph.rs +0 -1710
  28. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/LICENSE +0 -0
  29. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-cli/Cargo.toml +0 -0
  30. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-cli/src/analyze.rs +0 -0
  31. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-cli/src/config/ignore.rs +0 -0
  32. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-cli/src/git.rs +0 -0
  33. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-cli/src/logger.rs +0 -0
  34. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-cli/src/pipeline.rs +0 -0
  35. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-cli/src/presets.rs +0 -0
  36. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-cli/tests/grammar_single_version.rs +0 -0
  37. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-ecmascript-core/Cargo.toml +0 -0
  38. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-ecmascript-core/src/ecmascript_ts.rs +0 -0
  39. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-ecmascript-core/src/lib.rs +0 -0
  40. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-ecmascript-core/src/metrics_tests.rs +0 -0
  41. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-graph/Cargo.toml +0 -0
  42. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-graph/src/attrs.rs +0 -0
  43. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-graph/src/cycles.rs +0 -0
  44. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-graph/src/finalize.rs +0 -0
  45. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-graph/src/hk.rs +0 -0
  46. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-graph/src/level_graph.rs +0 -0
  47. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-graph/src/lib.rs +0 -0
  48. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-graph/src/metrics.rs +0 -0
  49. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-graph/src/relativize.rs +0 -0
  50. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-graph/src/serialize.rs +0 -0
  51. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-graph/src/snapshot.rs +0 -0
  52. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-graph/src/stats.rs +0 -0
  53. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-plugin-api/Cargo.toml +0 -0
  54. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-plugin-api/src/attrs.rs +0 -0
  55. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-plugin-api/src/edge.rs +0 -0
  56. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-plugin-api/src/graph.rs +0 -0
  57. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-plugin-api/src/level.rs +0 -0
  58. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-plugin-api/src/lib.rs +0 -0
  59. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-plugin-api/src/log.rs +0 -0
  60. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-plugin-api/src/node.rs +0 -0
  61. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-plugin-api/src/plugin.rs +0 -0
  62. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-plugin-javascript/Cargo.toml +0 -0
  63. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-plugin-javascript/src/lib.rs +0 -0
  64. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-plugin-python/Cargo.toml +0 -0
  65. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-plugin-python/src/lib.rs +0 -0
  66. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-plugin-python/src/metrics_tests.rs +0 -0
  67. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-plugin-python/src/python_ts.rs +0 -0
  68. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-plugin-rust/Cargo.toml +0 -0
  69. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-plugin-rust/src/crate_graph.rs +0 -0
  70. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-plugin-rust/src/ids.rs +0 -0
  71. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-plugin-rust/src/internal.rs +0 -0
  72. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-plugin-rust/src/rust_ts.rs +0 -0
  73. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-plugin-typescript/Cargo.toml +0 -0
  74. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-plugin-typescript/src/lib.rs +0 -0
  75. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-test-support/Cargo.toml +0 -0
  76. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-test-support/src/lib.rs +0 -0
  77. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-viewer/Cargo.toml +0 -0
  78. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-viewer/src/assets/app.js +0 -0
  79. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-viewer/src/assets/base.css +0 -0
  80. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-viewer/src/assets/diff.js +0 -0
  81. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-viewer/src/assets/export-popup.js +0 -0
  82. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-viewer/src/assets/export.css +0 -0
  83. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-viewer/src/assets/graphviz.umd.js +0 -0
  84. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-viewer/src/assets/grouping.js +0 -0
  85. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-viewer/src/assets/index.html +0 -0
  86. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-viewer/src/assets/layout.js +0 -0
  87. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-viewer/src/assets/map-interactions.js +0 -0
  88. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-viewer/src/assets/map-render.js +0 -0
  89. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-viewer/src/assets/map-svg.css +0 -0
  90. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-viewer/src/assets/map.css +0 -0
  91. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-viewer/src/assets/modal-content.js +0 -0
  92. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-viewer/src/assets/modal.css +0 -0
  93. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-viewer/src/assets/modal.js +0 -0
  94. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-viewer/src/assets/nav.js +0 -0
  95. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-viewer/src/assets/node-popup.js +0 -0
  96. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-viewer/src/assets/node-table.js +0 -0
  97. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-viewer/src/assets/panzoom.js +0 -0
  98. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-viewer/src/assets/schema.js +0 -0
  99. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-viewer/src/assets/snap-controls.js +0 -0
  100. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-viewer/src/assets/snap.css +0 -0
  101. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-viewer/src/assets/snarkdown.umd.js +0 -0
  102. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-viewer/src/assets/source-links.js +0 -0
  103. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-viewer/src/assets/summary.js +0 -0
  104. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-viewer/src/assets/tables.css +0 -0
  105. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-viewer/src/assets/tooltip.js +0 -0
  106. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-viewer/src/assets/ui.js +0 -0
  107. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-viewer/src/assets/utils.js +0 -0
  108. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-viewer/src/assets/view-state.js +0 -0
  109. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/crates/code-ranker-viewer/src/lib.rs +0 -0
  110. {code_ranker-2.0.0 → code_ranker-3.0.0a1}/pyproject.toml +0 -0
@@ -208,7 +208,7 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
208
208
 
209
209
  [[package]]
210
210
  name = "code-ranker"
211
- version = "2.0.0"
211
+ version = "3.0.0-alpha.1"
212
212
  dependencies = [
213
213
  "anyhow",
214
214
  "chrono",
@@ -231,7 +231,7 @@ dependencies = [
231
231
 
232
232
  [[package]]
233
233
  name = "code-ranker-ecmascript-core"
234
- version = "2.0.0"
234
+ version = "3.0.0-alpha.1"
235
235
  dependencies = [
236
236
  "anyhow",
237
237
  "code-ranker-graph",
@@ -246,7 +246,7 @@ dependencies = [
246
246
 
247
247
  [[package]]
248
248
  name = "code-ranker-graph"
249
- version = "2.0.0"
249
+ version = "3.0.0-alpha.1"
250
250
  dependencies = [
251
251
  "anyhow",
252
252
  "chrono",
@@ -257,7 +257,7 @@ dependencies = [
257
257
 
258
258
  [[package]]
259
259
  name = "code-ranker-plugin-api"
260
- version = "2.0.0"
260
+ version = "3.0.0-alpha.1"
261
261
  dependencies = [
262
262
  "anyhow",
263
263
  "chrono",
@@ -266,7 +266,7 @@ dependencies = [
266
266
 
267
267
  [[package]]
268
268
  name = "code-ranker-plugin-javascript"
269
- version = "2.0.0"
269
+ version = "3.0.0-alpha.1"
270
270
  dependencies = [
271
271
  "anyhow",
272
272
  "code-ranker-ecmascript-core",
@@ -278,7 +278,7 @@ dependencies = [
278
278
 
279
279
  [[package]]
280
280
  name = "code-ranker-plugin-python"
281
- version = "2.0.0"
281
+ version = "3.0.0-alpha.1"
282
282
  dependencies = [
283
283
  "anyhow",
284
284
  "code-ranker-graph",
@@ -292,7 +292,7 @@ dependencies = [
292
292
 
293
293
  [[package]]
294
294
  name = "code-ranker-plugin-rust"
295
- version = "2.0.0"
295
+ version = "3.0.0-alpha.1"
296
296
  dependencies = [
297
297
  "anyhow",
298
298
  "cargo_metadata",
@@ -307,7 +307,7 @@ dependencies = [
307
307
 
308
308
  [[package]]
309
309
  name = "code-ranker-plugin-typescript"
310
- version = "2.0.0"
310
+ version = "3.0.0-alpha.1"
311
311
  dependencies = [
312
312
  "anyhow",
313
313
  "code-ranker-ecmascript-core",
@@ -319,14 +319,14 @@ dependencies = [
319
319
 
320
320
  [[package]]
321
321
  name = "code-ranker-test-support"
322
- version = "2.0.0"
322
+ version = "3.0.0-alpha.1"
323
323
  dependencies = [
324
324
  "code-ranker-plugin-api",
325
325
  ]
326
326
 
327
327
  [[package]]
328
328
  name = "code-ranker-viewer"
329
- version = "2.0.0"
329
+ version = "3.0.0-alpha.1"
330
330
  dependencies = [
331
331
  "anyhow",
332
332
  "code-ranker-graph",
@@ -3,7 +3,7 @@ members = ["crates/*"]
3
3
  resolver = "3"
4
4
 
5
5
  [workspace.package]
6
- version = "2.0.0"
6
+ version = "3.0.0-alpha.1"
7
7
  edition = "2024"
8
8
  rust-version = "1.88"
9
9
  license = "Apache-2.0"
@@ -12,14 +12,14 @@ keywords = ["dependency-graph", "coupling", "refactoring", "code-quality", "stat
12
12
  categories = ["development-tools", "command-line-utilities"]
13
13
 
14
14
  [workspace.dependencies]
15
- code-ranker-graph = { path = "crates/code-ranker-graph", version = "2.0.0" }
16
- code-ranker-plugin-api = { path = "crates/code-ranker-plugin-api", version = "2.0.0" }
17
- code-ranker-ecmascript-core = { path = "crates/code-ranker-ecmascript-core", version = "2.0.0" }
18
- code-ranker-plugin-rust = { path = "crates/code-ranker-plugin-rust", version = "2.0.0" }
19
- code-ranker-plugin-python = { path = "crates/code-ranker-plugin-python", version = "2.0.0" }
20
- code-ranker-plugin-javascript = { path = "crates/code-ranker-plugin-javascript", version = "2.0.0" }
21
- code-ranker-plugin-typescript = { path = "crates/code-ranker-plugin-typescript", version = "2.0.0" }
22
- code-ranker-viewer = { path = "crates/code-ranker-viewer", version = "2.0.0" }
15
+ code-ranker-graph = { path = "crates/code-ranker-graph", version = "3.0.0-alpha.1" }
16
+ code-ranker-plugin-api = { path = "crates/code-ranker-plugin-api", version = "3.0.0-alpha.1" }
17
+ code-ranker-ecmascript-core = { path = "crates/code-ranker-ecmascript-core", version = "3.0.0-alpha.1" }
18
+ code-ranker-plugin-rust = { path = "crates/code-ranker-plugin-rust", version = "3.0.0-alpha.1" }
19
+ code-ranker-plugin-python = { path = "crates/code-ranker-plugin-python", version = "3.0.0-alpha.1" }
20
+ code-ranker-plugin-javascript = { path = "crates/code-ranker-plugin-javascript", version = "3.0.0-alpha.1" }
21
+ code-ranker-plugin-typescript = { path = "crates/code-ranker-plugin-typescript", version = "3.0.0-alpha.1" }
22
+ code-ranker-viewer = { path = "crates/code-ranker-viewer", version = "3.0.0-alpha.1" }
23
23
 
24
24
  anyhow = "1.0"
25
25
  globset = "0.4"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-ranker
3
- Version: 2.0.0
3
+ Version: 3.0.0a1
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -41,7 +41,7 @@ Structural-analysis tool for **Rust, Python, JavaScript and TypeScript** codebas
41
41
 
42
42
  ```sh
43
43
  cargo install code-ranker --version 1.1.0 # install the CLI
44
- code-ranker report . # make html report in .code-ranker/ folder
44
+ code-ranker report . # make html report in .code-ranker/ folder
45
45
  ```
46
46
 
47
47
  `report .` needs no flags: it writes a self-contained HTML report (plus a JSON
@@ -18,7 +18,7 @@ Structural-analysis tool for **Rust, Python, JavaScript and TypeScript** codebas
18
18
 
19
19
  ```sh
20
20
  cargo install code-ranker --version 1.1.0 # install the CLI
21
- code-ranker report . # make html report in .code-ranker/ folder
21
+ code-ranker report . # make html report in .code-ranker/ folder
22
22
  ```
23
23
 
24
24
  `report .` needs no flags: it writes a self-contained HTML report (plus a JSON
@@ -1,6 +1,6 @@
1
1
  //! `check` — the linter: evaluate rules (and, with `--baseline`, regressions),
2
- //! render diagnostics (human / json / github / sarif), and the `--suggest-config`
3
- //! current-values dump.
2
+ //! render diagnostics (human / json / github / sarif / codequality), and the
3
+ //! `--suggest-config` current-values dump.
4
4
 
5
5
  use crate::analyze::{analyze_input, load_snapshot_any, project_name};
6
6
  use crate::cli::{AnalyzeArgs, OutputFormat};
@@ -136,6 +136,7 @@ fn emit_diagnostics(
136
136
  }
137
137
  }
138
138
  OutputFormat::Sarif => println!("{}", sarif_document(violations)),
139
+ OutputFormat::Codequality => println!("{}", codequality_document(violations)),
139
140
  }
140
141
  }
141
142
 
@@ -336,7 +337,10 @@ fn group_digits(n: u64) -> String {
336
337
  /// Minimal SARIF 2.1.0 document. `ruleId` is the dotted rule id (e.g.
337
338
  /// `threshold.file.loc`); the rules that actually fired are described under
338
339
  /// `tool.driver.rules` (id, group, rationale, helpUri) so the report is self-documenting.
339
- fn sarif_document(violations: &[config::Violation]) -> String {
340
+ /// Each result carries a `partialFingerprints` entry keyed on `(rule, location)` (no
341
+ /// line number) so a consumer matches the same finding across runs even when code
342
+ /// shifts — the same identity `check --baseline` uses internally.
343
+ pub(crate) fn sarif_document(violations: &[config::Violation]) -> String {
340
344
  // Distinct fired rule ids, first-seen order, so each results.ruleId resolves.
341
345
  let mut seen: Vec<&config::Violation> = Vec::new();
342
346
  for v in violations {
@@ -367,6 +371,15 @@ fn sarif_document(violations: &[config::Violation]) -> String {
367
371
  "ruleId": v.rule,
368
372
  "level": "error",
369
373
  "message": { "text": v.summary() },
374
+ // Stable cross-run identity for the consumer (GitHub code scanning,
375
+ // SARIF viewers): the same `(rule, location)` signature `check
376
+ // --baseline` matches on internally. The line number is deliberately
377
+ // excluded, so shifting a finding up/down the file does not reopen it
378
+ // as "new". The value is the readable composite key (no hashing) — a
379
+ // metric finding has at most one `(rule, location)`, so it is unique.
380
+ "partialFingerprints": {
381
+ "codeRankerRuleLocation/v1": format!("{}:{}", v.rule, v.location),
382
+ },
370
383
  "properties": { "group": v.group, "graph": v.graph, "weight": v.weight },
371
384
  });
372
385
  // A physical location lets GitHub code scanning render the result
@@ -398,6 +411,36 @@ fn sarif_document(violations: &[config::Violation]) -> String {
398
411
  serde_json::to_string_pretty(&doc).unwrap_or_else(|_| "{}".into())
399
412
  }
400
413
 
414
+ /// GitLab **Code Quality** report (the CodeClimate-derived JSON GitLab ingests as
415
+ /// `artifacts:reports:codequality`). A flat array of issues; GitLab renders them
416
+ /// in the MR widget / diff. Each issue carries the dotted rule id as `check_name`,
417
+ /// the human message, a `major` severity, the repo-relative `location.path` +
418
+ /// `lines.begin`, and a stable `fingerprint` keyed on `(rule, location)` — no line
419
+ /// number, so GitLab tracks the same finding across pipelines even when code
420
+ /// shifts (the same identity SARIF and `check --baseline` use). Unlike GitHub
421
+ /// SARIF this needs no feature flag and works on current GitLab.
422
+ pub(crate) fn codequality_document(violations: &[config::Violation]) -> String {
423
+ let issues: Vec<serde_json::Value> = violations
424
+ .iter()
425
+ .map(|v| {
426
+ serde_json::json!({
427
+ "description": v.summary(),
428
+ "check_name": v.rule,
429
+ // Readable composite identity (no hashing) — a finding has at most
430
+ // one (rule, location), so it is unique; line excluded so a shift
431
+ // does not reopen it.
432
+ "fingerprint": format!("{}:{}", v.rule, v.location),
433
+ "severity": "major",
434
+ "location": {
435
+ "path": violation_rel_path(&v.location).unwrap_or(v.location.as_str()),
436
+ "lines": { "begin": v.line.unwrap_or(1) },
437
+ },
438
+ })
439
+ })
440
+ .collect();
441
+ serde_json::to_string_pretty(&issues).unwrap_or_else(|_| "[]".into())
442
+ }
443
+
401
444
  #[cfg(test)]
402
445
  mod tests {
403
446
  use super::*;
@@ -454,4 +497,52 @@ mod tests {
454
497
  let v: serde_json::Value = serde_json::from_str(&doc).unwrap();
455
498
  assert!(v["runs"][0]["results"][0].get("locations").is_none());
456
499
  }
500
+
501
+ #[test]
502
+ fn codequality_issue_has_fingerprint_path_and_line() {
503
+ let doc = codequality_document(&[viol("{target}/src/x.rs", Some(7))]);
504
+ let v: serde_json::Value = serde_json::from_str(&doc).unwrap();
505
+ let issue = &v[0];
506
+ assert_eq!(issue["check_name"], "threshold.file.loc");
507
+ assert_eq!(issue["severity"], "major");
508
+ assert_eq!(issue["location"]["path"], "src/x.rs");
509
+ assert_eq!(issue["location"]["lines"]["begin"], 7);
510
+ // Stable identity = rule:location, no line (so a shift does not reopen it).
511
+ assert_eq!(issue["fingerprint"], "threshold.file.loc:{target}/src/x.rs");
512
+ }
513
+
514
+ #[test]
515
+ fn codequality_whole_file_metric_defaults_line_to_one() {
516
+ // A whole-file metric has no line → CodeClimate needs one, default 1.
517
+ let doc = codequality_document(&[viol("{target}/src/x.rs", None)]);
518
+ let v: serde_json::Value = serde_json::from_str(&doc).unwrap();
519
+ assert_eq!(v[0]["location"]["lines"]["begin"], 1);
520
+ }
521
+
522
+ #[test]
523
+ fn sarif_partial_fingerprint_is_rule_and_location() {
524
+ let doc = sarif_document(&[viol("{target}/src/x.rs", Some(7))]);
525
+ let v: serde_json::Value = serde_json::from_str(&doc).unwrap();
526
+ let fp = &v["runs"][0]["results"][0]["partialFingerprints"];
527
+ assert_eq!(
528
+ fp["codeRankerRuleLocation/v1"],
529
+ "threshold.file.loc:{target}/src/x.rs"
530
+ );
531
+ }
532
+
533
+ #[test]
534
+ fn sarif_partial_fingerprint_is_stable_across_line_shifts() {
535
+ // The same finding at a different line keeps the same fingerprint, so a
536
+ // code shift does not reopen it for the consumer.
537
+ let at_7 = sarif_document(&[viol("{target}/src/x.rs", Some(7))]);
538
+ let at_42 = sarif_document(&[viol("{target}/src/x.rs", Some(42))]);
539
+ let fp = |doc: &str| -> String {
540
+ let v: serde_json::Value = serde_json::from_str(doc).unwrap();
541
+ v["runs"][0]["results"][0]["partialFingerprints"]["codeRankerRuleLocation/v1"]
542
+ .as_str()
543
+ .unwrap()
544
+ .to_owned()
545
+ };
546
+ assert_eq!(fp(&at_7), fp(&at_42));
547
+ }
457
548
  }
@@ -23,6 +23,7 @@ pub(crate) enum OutputFormat {
23
23
  Json,
24
24
  Github,
25
25
  Sarif,
26
+ Codequality,
26
27
  }
27
28
 
28
29
  /// Common input + analysis options shared by `check` and `report`.
@@ -137,6 +138,26 @@ pub(crate) enum Command {
137
138
  #[arg(long = "output.html.path", value_name = "PATH")]
138
139
  output_html_path: Option<String>,
139
140
 
141
+ /// Emit a SARIF 2.1.0 report of rule violations (path from
142
+ /// --output.sarif.path / config / default).
143
+ #[arg(long = "output.sarif")]
144
+ output_sarif: bool,
145
+
146
+ /// SARIF destination: a path or name template, or `stdout`/`-`.
147
+ /// Placeholders: {project-dir}, {ts}, {git-hash}, {git-hash-N}. Selects SARIF.
148
+ #[arg(long = "output.sarif.path", value_name = "PATH")]
149
+ output_sarif_path: Option<String>,
150
+
151
+ /// Emit a GitLab Code Quality (CodeClimate) report of rule violations
152
+ /// (path from --output.codequality.path / config / default).
153
+ #[arg(long = "output.codequality")]
154
+ output_codequality: bool,
155
+
156
+ /// Code Quality destination: a path or name template, or `stdout`/`-`.
157
+ /// Placeholders: {project-dir}, {ts}, {git-hash}, {git-hash-N}. Selects it.
158
+ #[arg(long = "output.codequality.path", value_name = "PATH")]
159
+ output_codequality_path: Option<String>,
160
+
140
161
  /// Emit the AI prompt for one principle (default to a `…-{preset}.md` file).
141
162
  #[arg(long = "output.prompt")]
142
163
  output_prompt: bool,
@@ -1,7 +1,7 @@
1
1
  //! Config loading: discover `code-ranker.toml` (or `Cargo.toml` metadata),
2
2
  //! apply inline `KEY=VALUE` and `--cycle-rule` / `--threshold` CLI overrides.
3
3
 
4
- use super::model::{Config, CycleRule, MetricThresholds, parse_number};
4
+ use super::model::{Config, CycleRule, MetricThresholds, parse_number, quote_suffixed_thresholds};
5
5
  use anyhow::{Context, Result};
6
6
  use code_ranker_plugin_api::log;
7
7
  use std::path::Path;
@@ -46,7 +46,8 @@ fn load_file(workspace: &Path, explicit: Option<&Path>) -> Result<(Config, Optio
46
46
  if let Some(path) = explicit {
47
47
  let text =
48
48
  std::fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?;
49
- let cfg = toml::from_str(&text).with_context(|| format!("parsing {}", path.display()))?;
49
+ let cfg = toml::from_str(&quote_suffixed_thresholds(&text))
50
+ .with_context(|| format!("parsing {}", path.display()))?;
50
51
  return Ok((cfg, Some(path.display().to_string())));
51
52
  }
52
53
 
@@ -57,7 +58,8 @@ fn load_file(workspace: &Path, explicit: Option<&Path>) -> Result<(Config, Optio
57
58
  if p.exists() {
58
59
  let text =
59
60
  std::fs::read_to_string(&p).with_context(|| format!("reading {}", p.display()))?;
60
- let cfg = toml::from_str(&text).with_context(|| format!("parsing {}", p.display()))?;
61
+ let cfg = toml::from_str(&quote_suffixed_thresholds(&text))
62
+ .with_context(|| format!("parsing {}", p.display()))?;
61
63
  let canonical = p.canonicalize().unwrap_or(p);
62
64
  return Ok((cfg, Some(canonical.display().to_string())));
63
65
  }
@@ -79,8 +81,8 @@ fn load_from_cargo_toml(dir: &Path) -> Result<Option<(Config, String)>> {
79
81
  }
80
82
  let text =
81
83
  std::fs::read_to_string(&cargo).with_context(|| format!("reading {}", cargo.display()))?;
82
- let val: toml::Value =
83
- toml::from_str(&text).with_context(|| format!("parsing {}", cargo.display()))?;
84
+ let val: toml::Value = toml::from_str(&quote_suffixed_thresholds(&text))
85
+ .with_context(|| format!("parsing {}", cargo.display()))?;
84
86
 
85
87
  let section = val
86
88
  .get("workspace")
@@ -202,17 +204,13 @@ fn set_threshold(cfg: &mut Config, scope: &str, metric: &str, val: f64) -> Resul
202
204
  }
203
205
 
204
206
  fn set_metric(bucket: &mut MetricThresholds, metric: &str, val: f64) -> Result<()> {
205
- match metric {
206
- "hk" => bucket.hk = Some(val),
207
- "cyclomatic" => bucket.cyclomatic = Some(val),
208
- "cognitive" => bucket.cognitive = Some(val),
209
- "fan_in" => bucket.fan_in = Some(val),
210
- "fan_out" => bucket.fan_out = Some(val),
211
- "loc" => bucket.loc = Some(val),
212
- other => anyhow::bail!(
213
- "unknown metric {other:?}; expected hk|cyclomatic|cognitive|fan_in|fan_out|loc"
214
- ),
207
+ if !super::metrics::is_threshold_metric(metric) {
208
+ anyhow::bail!(
209
+ "unknown threshold metric {metric:?}; expected a per-file metric such as \
210
+ sloc, loc, cyclomatic, cognitive, hk, fan_in, fan_out, mi, volume, bugs"
211
+ );
215
212
  }
213
+ bucket.set(metric.to_string(), val);
216
214
  Ok(())
217
215
  }
218
216
 
@@ -258,8 +256,8 @@ mod tests {
258
256
  .unwrap();
259
257
  assert_eq!(cfg.rules.cycles.chain, CycleRule::Max(0));
260
258
  assert_eq!(cfg.rules.cycles.mutual, CycleRule::Off);
261
- assert_eq!(cfg.rules.thresholds.file.cognitive, Some(25.0));
262
- assert_eq!(cfg.rules.thresholds.file.hk, Some(1000.0));
259
+ assert_eq!(cfg.rules.thresholds.file.get("cognitive"), Some(25.0));
260
+ assert_eq!(cfg.rules.thresholds.file.get("hk"), Some(1000.0));
263
261
  }
264
262
 
265
263
  #[test]
@@ -278,6 +276,7 @@ mod tests {
278
276
  "output.html.enabled=true",
279
277
  "rules.cycles.chain=7",
280
278
  "rules.thresholds.file.loc=800",
279
+ "rules.thresholds.file.sloc=1200",
281
280
  ],
282
281
  )
283
282
  .unwrap();
@@ -289,7 +288,8 @@ mod tests {
289
288
  assert_eq!(cfg.output.json.enabled, Some(false));
290
289
  assert_eq!(cfg.output.html.enabled, Some(true));
291
290
  assert_eq!(cfg.rules.cycles.chain, CycleRule::Max(7));
292
- assert_eq!(cfg.rules.thresholds.file.loc, Some(800.0));
291
+ assert_eq!(cfg.rules.thresholds.file.get("loc"), Some(800.0));
292
+ assert_eq!(cfg.rules.thresholds.file.get("sloc"), Some(1200.0));
293
293
  }
294
294
 
295
295
  #[test]
@@ -320,8 +320,21 @@ mod tests {
320
320
  #[test]
321
321
  fn set_metric_each_then_unknown() {
322
322
  let mut b = MetricThresholds::default();
323
- for m in ["hk", "cyclomatic", "cognitive", "fan_in", "fan_out", "loc"] {
323
+ // The full open vocabulary is accepted not just the legacy six.
324
+ for m in [
325
+ "hk",
326
+ "cyclomatic",
327
+ "cognitive",
328
+ "fan_in",
329
+ "fan_out",
330
+ "loc",
331
+ "sloc",
332
+ "mi",
333
+ "bugs",
334
+ "volume",
335
+ ] {
324
336
  set_metric(&mut b, m, 1.0).unwrap();
337
+ assert_eq!(b.get(m), Some(1.0));
325
338
  }
326
339
  assert!(set_metric(&mut b, "bogus", 1.0).is_err());
327
340
  }
@@ -331,7 +344,7 @@ mod tests {
331
344
  let mut cfg = Config::default();
332
345
  assert!(set_threshold(&mut cfg, "function", "loc", 1.0).is_err());
333
346
  set_threshold(&mut cfg, "file", "hk", 5.0).unwrap();
334
- assert_eq!(cfg.rules.thresholds.file.hk, Some(5.0));
347
+ assert_eq!(cfg.rules.thresholds.file.get("hk"), Some(5.0));
335
348
  assert!(set_cycle(&mut cfg, "weird", CycleRule::Off).is_err());
336
349
  }
337
350
 
@@ -0,0 +1,88 @@
1
+ //! The per-file threshold metric vocabulary: every metric that can carry a
2
+ //! `[rules.thresholds.file]` limit, with its concern group and human label.
3
+ //!
4
+ //! This is a **leaf** module — it depends on nothing else in `config`, so both
5
+ //! the data model (`model`, which validates config keys against it) and the rule
6
+ //! catalog (`rules`, which resolves a metric's group through it) can use it
7
+ //! without forming a `model ↔ rules` dependency cycle.
8
+
9
+ /// A per-file metric that can carry a threshold: its key, the concern `group`
10
+ /// (one of `CPX` / `CPL` / `SIZ`, matching the [`super::rules::RULES`] groups),
11
+ /// and the human `label` used in the breach message. A threshold is a
12
+ /// `value > limit` gate, so this is the whole numeric per-file vocabulary — every
13
+ /// metric the engine emits is accepted, not a hand-picked subset.
14
+ /// `threshold_metrics_cover_engine_specs` (test) guards that this list stays in
15
+ /// sync with the engine's metric specs.
16
+ pub struct ThresholdMetric {
17
+ pub key: &'static str,
18
+ pub group: &'static str,
19
+ pub label: &'static str,
20
+ }
21
+
22
+ pub const THRESHOLD_METRICS: &[ThresholdMetric] = &[
23
+ // CPX — control-flow complexity, maintainability, and Halstead effort.
24
+ tm("cyclomatic", "CPX", "cyclomatic complexity"),
25
+ tm("cognitive", "CPX", "cognitive complexity"),
26
+ tm("exits", "CPX", "exit points"),
27
+ tm("args", "CPX", "argument count"),
28
+ tm("closures", "CPX", "closure count"),
29
+ tm("mi", "CPX", "maintainability index"),
30
+ tm("mi_sei", "CPX", "maintainability index (SEI)"),
31
+ tm("length", "CPX", "Halstead length"),
32
+ tm("vocabulary", "CPX", "Halstead vocabulary"),
33
+ tm("volume", "CPX", "Halstead volume"),
34
+ tm("effort", "CPX", "Halstead effort"),
35
+ tm("time", "CPX", "Halstead time"),
36
+ tm("bugs", "CPX", "Halstead bugs"),
37
+ tm("unsafe", "CPX", "unsafe blocks"),
38
+ // SIZ — size.
39
+ tm("sloc", "SIZ", "source loc"),
40
+ tm("loc", "SIZ", "source loc"),
41
+ tm("lloc", "SIZ", "logical loc"),
42
+ tm("cloc", "SIZ", "comment loc"),
43
+ tm("blank", "SIZ", "blank lines"),
44
+ tm("tloc", "SIZ", "test loc"),
45
+ tm("items", "SIZ", "item count"),
46
+ // CPL — coupling.
47
+ tm("fan_in", "CPL", "fan-in"),
48
+ tm("fan_out", "CPL", "fan-out"),
49
+ tm("fan_out_external", "CPL", "external fan-out"),
50
+ tm("hk", "CPL", "Henry-Kafura hk"),
51
+ ];
52
+
53
+ const fn tm(key: &'static str, group: &'static str, label: &'static str) -> ThresholdMetric {
54
+ ThresholdMetric { key, group, label }
55
+ }
56
+
57
+ /// The threshold metadata for a metric key, if it is a known per-file metric.
58
+ pub fn threshold_metric(key: &str) -> Option<&'static ThresholdMetric> {
59
+ THRESHOLD_METRICS.iter().find(|m| m.key == key)
60
+ }
61
+
62
+ /// Is `key` a metric that can carry a per-file threshold?
63
+ pub fn is_threshold_metric(key: &str) -> bool {
64
+ threshold_metric(key).is_some()
65
+ }
66
+
67
+ #[cfg(test)]
68
+ mod tests {
69
+ use super::*;
70
+
71
+ #[test]
72
+ fn threshold_metrics_cover_engine_specs() {
73
+ // Every numeric per-file metric the engine emits must be thresholdable, so
74
+ // the config accepts the full vocabulary. `cycle` is a string attribute
75
+ // (a cycle kind), not a numeric threshold, so it is excluded.
76
+ let (metrics, _) = code_ranker_graph::metric_specs();
77
+ let (coupling, _) = code_ranker_graph::coupling_specs();
78
+ for key in metrics.keys().chain(coupling.keys()) {
79
+ if key == "cycle" {
80
+ continue;
81
+ }
82
+ assert!(
83
+ is_threshold_metric(key),
84
+ "engine metric {key:?} is not in THRESHOLD_METRICS — add it (with a group)"
85
+ );
86
+ }
87
+ }
88
+ }
@@ -6,6 +6,7 @@
6
6
 
7
7
  pub mod ignore;
8
8
  pub mod load;
9
+ pub mod metrics;
9
10
  pub mod model;
10
11
  pub mod rules;
11
12
  pub mod violations;