roschema 0.3.0__tar.gz → 0.4.0__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.
- {roschema-0.3.0 → roschema-0.4.0}/CHANGELOG.md +20 -0
- {roschema-0.3.0 → roschema-0.4.0}/PKG-INFO +41 -14
- {roschema-0.3.0 → roschema-0.4.0}/README.md +40 -13
- {roschema-0.3.0 → roschema-0.4.0}/action/action.yml +4 -1
- {roschema-0.3.0 → roschema-0.4.0}/pyproject.toml +1 -1
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/cli.py +150 -38
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/config.py +24 -11
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/report/__init__.py +2 -0
- roschema-0.4.0/src/roschema/report/changes.py +92 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/report/json_report.py +27 -3
- roschema-0.4.0/src/roschema/report/markdown.py +92 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/report/summary.py +42 -17
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/report/text.py +42 -11
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/rules/gendocs.py +28 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/rules/model.py +3 -1
- {roschema-0.3.0 → roschema-0.4.0}/tests/test_action.py +16 -3
- {roschema-0.3.0 → roschema-0.4.0}/tests/test_check_cli.py +95 -3
- roschema-0.4.0/tests/test_cli_explain.py +23 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/test_config.py +23 -1
- {roschema-0.3.0 → roschema-0.4.0}/tests/test_gendocs.py +14 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/test_golden.py +10 -2
- {roschema-0.3.0 → roschema-0.4.0}/tests/test_mcap.py +14 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/test_report_formats.py +29 -1
- roschema-0.4.0/tests/test_report_markdown.py +61 -0
- {roschema-0.3.0 → roschema-0.4.0}/.github/workflows/ci.yml +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/.github/workflows/release.yml +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/.gitignore +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/CONTRIBUTING.md +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/LICENSE +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/docs/rules.md +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/__init__.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/deep.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/differ.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/discovery/__init__.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/discovery/discover.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/discovery/model.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/discovery/resolve.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/formatter.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/freeze.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/ir.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/loader.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/mcap_fs.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/parser/__init__.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/parser/core.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/parser/errors.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/py.typed +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/report/github.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/rules/__init__.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/rules/breaking/__init__.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/rules/breaking/rs001_field_removed.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/rules/breaking/rs002_field_added.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/rules/breaking/rs003_field_type_changed.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/rules/breaking/rs004_fields_reordered.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/rules/breaking/rs005_field_renamed.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/rules/breaking/rs006_array_kind_changed.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/rules/breaking/rs007_bound_tightened.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/rules/breaking/rs008_bound_loosened.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/rules/breaking/rs020_constant_removed.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/rules/breaking/rs021_constant_value_changed.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/rules/breaking/rs022_constant_type_changed.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/rules/breaking/rs030_default_changed.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/rules/breaking/rs040_interface_removed.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/rules/breaking/rs060_message_version.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/rules/breaking/rs110_transitive.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/rules/lint/__init__.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/rules/lint/_common.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/rules/lint/rsl001_field_snake_case.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/rules/lint/rsl002_constant_upper_snake.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/rules/lint/rsl003_name_pascal_case.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/rules/lint/rsl004_missing_doc.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/rules/lint/rsl005_discouraged_types.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/rules/lint/rsl006_unbounded.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/rules/lint/rsl010_srv_shadow.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/rules/registry.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/src/roschema/vfs.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/_helpers.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/conftest.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/AMENT_IGNORE +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/COLCON_IGNORE +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/golden_check.json +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_new/demo_msgs/action/Move.action +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_new/demo_msgs/msg/ArrayKind.msg +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_new/demo_msgs/msg/BoundLoose.msg +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_new/demo_msgs/msg/BoundTight.msg +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_new/demo_msgs/msg/BoundedNest.msg +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_new/demo_msgs/msg/Constants.msg +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_new/demo_msgs/msg/Empty.msg +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_new/demo_msgs/msg/FieldAdded.msg +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_new/demo_msgs/msg/FieldRemoved.msg +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_new/demo_msgs/msg/Renamed.msg +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_new/demo_msgs/msg/Reordered.msg +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_new/demo_msgs/msg/Suppressed.msg +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_new/demo_msgs/msg/TypeChanged.msg +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_new/demo_msgs/msg/Unchanged.msg +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_new/demo_msgs/msg/WeirdSpacing.msg +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_new/demo_msgs/package.xml +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_new/demo_msgs/srv/DoThing.srv +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_old/demo_msgs/action/Move.action +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_old/demo_msgs/msg/ArrayKind.msg +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_old/demo_msgs/msg/BoundLoose.msg +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_old/demo_msgs/msg/BoundTight.msg +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_old/demo_msgs/msg/BoundedNest.msg +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_old/demo_msgs/msg/Constants.msg +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_old/demo_msgs/msg/Empty.msg +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_old/demo_msgs/msg/FieldAdded.msg +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_old/demo_msgs/msg/FieldRemoved.msg +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_old/demo_msgs/msg/Removed.msg +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_old/demo_msgs/msg/Renamed.msg +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_old/demo_msgs/msg/Reordered.msg +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_old/demo_msgs/msg/Suppressed.msg +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_old/demo_msgs/msg/TypeChanged.msg +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_old/demo_msgs/msg/Unchanged.msg +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_old/demo_msgs/msg/WeirdSpacing.msg +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_old/demo_msgs/package.xml +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/fixtures/ws_old/demo_msgs/srv/DoThing.srv +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/test_cli_ls.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/test_cli_version.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/test_deep.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/test_differ.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/test_discovery.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/test_fmt.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/test_freeze.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/test_lint.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/test_parser.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/test_rules.py +0 -0
- {roschema-0.3.0 → roschema-0.4.0}/tests/test_vfs.py +0 -0
|
@@ -3,6 +3,26 @@
|
|
|
3
3
|
The format follows [Keep a Changelog](https://keepachangelog.com/), and the
|
|
4
4
|
project adheres to [Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## [0.4.0] - 2026-11-03
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- JSON findings link to their rule's documentation (`help_uri`).
|
|
11
|
+
- `roschema explain RULE` prints a rule's reference section.
|
|
12
|
+
- `check --verbose` and `bag-check --verbose` list the changes that are not
|
|
13
|
+
reported (added, outside the level, rule off or suppressed) with the reason;
|
|
14
|
+
JSON adds a `changes` array.
|
|
15
|
+
- `--summary FILE` appends a Markdown report; the Action writes it to the job
|
|
16
|
+
summary, so every finding and the semver advice are visible even when GitHub
|
|
17
|
+
truncates the annotations.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- The semver advice suggests MINOR for a package that adds an interface or a
|
|
22
|
+
constant, and is printed even when there are no findings.
|
|
23
|
+
- The coverage line names the first few new interfaces and says "new" and
|
|
24
|
+
"removed" instead of "only in the target" and "only in the baseline".
|
|
25
|
+
|
|
6
26
|
## [0.3.0] - 2026-11-03
|
|
7
27
|
|
|
8
28
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: roschema
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Breaking-change detection, linting and formatting for ROS 2 interface definitions
|
|
5
5
|
Project-URL: Homepage, https://github.com/justagist/roschema
|
|
6
6
|
Project-URL: Documentation, https://github.com/justagist/roschema#readme
|
|
@@ -148,6 +148,9 @@ roschema bag-check recording.mcap
|
|
|
148
148
|
|
|
149
149
|
# what would be checked? (discovered packages and interfaces)
|
|
150
150
|
roschema ls
|
|
151
|
+
|
|
152
|
+
# what does a rule report, and why?
|
|
153
|
+
roschema explain RS004
|
|
151
154
|
```
|
|
152
155
|
|
|
153
156
|
## Checking
|
|
@@ -155,7 +158,7 @@ roschema ls
|
|
|
155
158
|
```
|
|
156
159
|
roschema check [TARGET] --against BASELINE [--level LEVEL] [--format text|json|github]
|
|
157
160
|
[--deep | --no-deep] [--exit-zero] [--warnings-as-errors] [--allow-empty]
|
|
158
|
-
[--config PATH] [--exclude GLOB]
|
|
161
|
+
[--verbose] [--summary FILE] [--config PATH] [--exclude GLOB]
|
|
159
162
|
```
|
|
160
163
|
|
|
161
164
|
`TARGET` selects what to check; omit it to check every interface package below
|
|
@@ -179,7 +182,10 @@ automatically. Initialised git submodules are compared at the commit the
|
|
|
179
182
|
baseline records for them.
|
|
180
183
|
|
|
181
184
|
Every report says how much was compared, for example `compared 12 interface(s)
|
|
182
|
-
in 3 package(s); 1
|
|
185
|
+
in 3 package(s); 1 new (my_msgs/msg/Status)`. New interfaces are not findings,
|
|
186
|
+
since adding one breaks nothing; the line names the first few so that an
|
|
187
|
+
unexpected one (say, from an unrelated checkout below the target) is easy to
|
|
188
|
+
spot. A baseline with no interfaces at all is
|
|
183
189
|
an error (exit `2`): every current interface would look newly added, so the
|
|
184
190
|
check would pass without comparing anything. That usually means the wrong
|
|
185
191
|
baseline or target; on the pull request that adds the first interfaces, pass
|
|
@@ -248,7 +254,7 @@ same. Comparison ignores it.
|
|
|
248
254
|
```
|
|
249
255
|
roschema bag-check BAG.mcap [TARGETS...] [--level LEVEL] [--config PATH]
|
|
250
256
|
[--format text|json|github] [--exit-zero] [--warnings-as-errors]
|
|
251
|
-
[--exclude GLOB]
|
|
257
|
+
[--verbose] [--summary FILE] [--exclude GLOB]
|
|
252
258
|
```
|
|
253
259
|
|
|
254
260
|
Needs the `bag` extra (`uv tool install 'roschema[bag]'`).
|
|
@@ -341,16 +347,27 @@ allow every rule for that target.
|
|
|
341
347
|
package and ends with a summary and a semver advice per package:
|
|
342
348
|
|
|
343
349
|
- **MAJOR** when the package has a wire- or source-level error;
|
|
344
|
-
- **MINOR** when
|
|
345
|
-
|
|
350
|
+
- **MINOR** when it adds an interface or a constant (neither is a finding,
|
|
351
|
+
since neither breaks anything), or an added field (RS002) whose severity you
|
|
352
|
+
lowered to `warn`;
|
|
346
353
|
- **PATCH** otherwise.
|
|
347
354
|
|
|
348
|
-
|
|
349
|
-
|
|
355
|
+
**`--verbose`** also lists, under "not reported", every change that produced no
|
|
356
|
+
reported finding, and why: a new interface or constant (no rule reports either,
|
|
357
|
+
at any level; an added field is RS002), a rule whose level is outside the one
|
|
358
|
+
being checked, a rule that is off, or a suppression, with the allowlist reason.
|
|
359
|
+
Comment-only edits are not changes, so they never appear.
|
|
350
360
|
|
|
351
361
|
**`--format github`** prints what was compared, then emits `::error`/`::warning`
|
|
352
362
|
workflow commands, which GitHub shows as annotations on the PR diff.
|
|
353
363
|
|
|
364
|
+
**`--summary FILE`** appends a Markdown report to `FILE` as well, whatever the
|
|
365
|
+
`--format`: what was compared, a table of findings linked to the rule
|
|
366
|
+
reference, the semver advice, and the unreported changes in a collapsed
|
|
367
|
+
section. Pointed at `$GITHUB_STEP_SUMMARY`, it becomes the job summary, which
|
|
368
|
+
matters because GitHub shows only the first 10 error and 10 warning annotations
|
|
369
|
+
of a step.
|
|
370
|
+
|
|
354
371
|
**`--format json`** emits a stable document (`version: 1`):
|
|
355
372
|
|
|
356
373
|
```json
|
|
@@ -368,7 +385,8 @@ workflow commands, which GitHub shows as annotations on the PR diff.
|
|
|
368
385
|
"message": "type changed float32 -> float64",
|
|
369
386
|
"old": { "file": "my_msgs/msg/Pose.msg", "line": 1 },
|
|
370
387
|
"new": { "file": "my_msgs/msg/Pose.msg", "line": 1 },
|
|
371
|
-
"suppressed": false
|
|
388
|
+
"suppressed": false,
|
|
389
|
+
"help_uri": "https://github.com/justagist/roschema/blob/v0.4.0/docs/rules.md#rs003----field-type-changed"
|
|
372
390
|
}
|
|
373
391
|
],
|
|
374
392
|
"summary": {
|
|
@@ -389,7 +407,12 @@ workflow commands, which GitHub shows as annotations on the PR diff.
|
|
|
389
407
|
|
|
390
408
|
`old`/`new` are `null` when there is no corresponding side (a removed interface
|
|
391
409
|
has no `new`). `coverage` counts interfaces present on both sides (`compared`)
|
|
392
|
-
and on one side only. `
|
|
410
|
+
and on one side only. `help_uri` links to the rule's section of the
|
|
411
|
+
[rule reference](docs/rules.md) for the installed release. With `--verbose`, a
|
|
412
|
+
`changes` array lists the unreported changes (each with `rule`, `package`,
|
|
413
|
+
`interface`, `section`, `target`, `message` and `reason`), including suppressed
|
|
414
|
+
findings with the reason they are suppressed. `lint` output has no
|
|
415
|
+
`advisories`, `coverage` or `changes`.
|
|
393
416
|
|
|
394
417
|
## Exit codes
|
|
395
418
|
|
|
@@ -406,7 +429,8 @@ turns `1` into `0` for `check`, `bag-check` and `lint`; tool errors still exit
|
|
|
406
429
|
## GitHub Action
|
|
407
430
|
|
|
408
431
|
A composite Action runs `roschema check` on the runner (no setup needed) and
|
|
409
|
-
posts the findings as annotations on the PR diff
|
|
432
|
+
posts the findings as annotations on the PR diff, with a full report (findings,
|
|
433
|
+
semver advice and unreported changes) as the job summary:
|
|
410
434
|
|
|
411
435
|
```yaml
|
|
412
436
|
# .github/workflows/interfaces.yml
|
|
@@ -421,7 +445,7 @@ jobs:
|
|
|
421
445
|
- uses: justagist/roschema/action@main
|
|
422
446
|
with:
|
|
423
447
|
against: origin/${{ github.base_ref }}
|
|
424
|
-
# version: "roschema==0.
|
|
448
|
+
# version: "roschema==0.4.0" # pin a release for reproducible results
|
|
425
449
|
# level: source
|
|
426
450
|
# deep: "true"
|
|
427
451
|
# warnings-as-errors: "true"
|
|
@@ -448,8 +472,10 @@ because it is part of the checkout.
|
|
|
448
472
|
## Troubleshooting
|
|
449
473
|
|
|
450
474
|
**"no breaking changes found", but you expected some.** The line under the
|
|
451
|
-
header says how many interfaces were compared
|
|
452
|
-
|
|
475
|
+
header says how many interfaces were compared, and `--verbose` lists the changes
|
|
476
|
+
that were seen but not reported, with the reason. Run `roschema ls` with the
|
|
477
|
+
same target to see which packages and interfaces are discovered, then check
|
|
478
|
+
that:
|
|
453
479
|
|
|
454
480
|
- the packages are not below a `COLCON_IGNORE` or `AMENT_IGNORE` marker;
|
|
455
481
|
- a package-name target did not fall back to the installed copy (the warning
|
|
@@ -466,6 +492,7 @@ shown in [Configuration](#configuration); the error suggests the closest one.
|
|
|
466
492
|
|
|
467
493
|
Every rule, its levels, default severity, rationale and an example is
|
|
468
494
|
documented in [docs/rules.md](docs/rules.md), generated from the rule metadata.
|
|
495
|
+
`roschema explain RULE` prints the same section in the terminal.
|
|
469
496
|
|
|
470
497
|
## Non-goals
|
|
471
498
|
|
|
@@ -113,6 +113,9 @@ roschema bag-check recording.mcap
|
|
|
113
113
|
|
|
114
114
|
# what would be checked? (discovered packages and interfaces)
|
|
115
115
|
roschema ls
|
|
116
|
+
|
|
117
|
+
# what does a rule report, and why?
|
|
118
|
+
roschema explain RS004
|
|
116
119
|
```
|
|
117
120
|
|
|
118
121
|
## Checking
|
|
@@ -120,7 +123,7 @@ roschema ls
|
|
|
120
123
|
```
|
|
121
124
|
roschema check [TARGET] --against BASELINE [--level LEVEL] [--format text|json|github]
|
|
122
125
|
[--deep | --no-deep] [--exit-zero] [--warnings-as-errors] [--allow-empty]
|
|
123
|
-
[--config PATH] [--exclude GLOB]
|
|
126
|
+
[--verbose] [--summary FILE] [--config PATH] [--exclude GLOB]
|
|
124
127
|
```
|
|
125
128
|
|
|
126
129
|
`TARGET` selects what to check; omit it to check every interface package below
|
|
@@ -144,7 +147,10 @@ automatically. Initialised git submodules are compared at the commit the
|
|
|
144
147
|
baseline records for them.
|
|
145
148
|
|
|
146
149
|
Every report says how much was compared, for example `compared 12 interface(s)
|
|
147
|
-
in 3 package(s); 1
|
|
150
|
+
in 3 package(s); 1 new (my_msgs/msg/Status)`. New interfaces are not findings,
|
|
151
|
+
since adding one breaks nothing; the line names the first few so that an
|
|
152
|
+
unexpected one (say, from an unrelated checkout below the target) is easy to
|
|
153
|
+
spot. A baseline with no interfaces at all is
|
|
148
154
|
an error (exit `2`): every current interface would look newly added, so the
|
|
149
155
|
check would pass without comparing anything. That usually means the wrong
|
|
150
156
|
baseline or target; on the pull request that adds the first interfaces, pass
|
|
@@ -213,7 +219,7 @@ same. Comparison ignores it.
|
|
|
213
219
|
```
|
|
214
220
|
roschema bag-check BAG.mcap [TARGETS...] [--level LEVEL] [--config PATH]
|
|
215
221
|
[--format text|json|github] [--exit-zero] [--warnings-as-errors]
|
|
216
|
-
[--exclude GLOB]
|
|
222
|
+
[--verbose] [--summary FILE] [--exclude GLOB]
|
|
217
223
|
```
|
|
218
224
|
|
|
219
225
|
Needs the `bag` extra (`uv tool install 'roschema[bag]'`).
|
|
@@ -306,16 +312,27 @@ allow every rule for that target.
|
|
|
306
312
|
package and ends with a summary and a semver advice per package:
|
|
307
313
|
|
|
308
314
|
- **MAJOR** when the package has a wire- or source-level error;
|
|
309
|
-
- **MINOR** when
|
|
310
|
-
|
|
315
|
+
- **MINOR** when it adds an interface or a constant (neither is a finding,
|
|
316
|
+
since neither breaks anything), or an added field (RS002) whose severity you
|
|
317
|
+
lowered to `warn`;
|
|
311
318
|
- **PATCH** otherwise.
|
|
312
319
|
|
|
313
|
-
|
|
314
|
-
|
|
320
|
+
**`--verbose`** also lists, under "not reported", every change that produced no
|
|
321
|
+
reported finding, and why: a new interface or constant (no rule reports either,
|
|
322
|
+
at any level; an added field is RS002), a rule whose level is outside the one
|
|
323
|
+
being checked, a rule that is off, or a suppression, with the allowlist reason.
|
|
324
|
+
Comment-only edits are not changes, so they never appear.
|
|
315
325
|
|
|
316
326
|
**`--format github`** prints what was compared, then emits `::error`/`::warning`
|
|
317
327
|
workflow commands, which GitHub shows as annotations on the PR diff.
|
|
318
328
|
|
|
329
|
+
**`--summary FILE`** appends a Markdown report to `FILE` as well, whatever the
|
|
330
|
+
`--format`: what was compared, a table of findings linked to the rule
|
|
331
|
+
reference, the semver advice, and the unreported changes in a collapsed
|
|
332
|
+
section. Pointed at `$GITHUB_STEP_SUMMARY`, it becomes the job summary, which
|
|
333
|
+
matters because GitHub shows only the first 10 error and 10 warning annotations
|
|
334
|
+
of a step.
|
|
335
|
+
|
|
319
336
|
**`--format json`** emits a stable document (`version: 1`):
|
|
320
337
|
|
|
321
338
|
```json
|
|
@@ -333,7 +350,8 @@ workflow commands, which GitHub shows as annotations on the PR diff.
|
|
|
333
350
|
"message": "type changed float32 -> float64",
|
|
334
351
|
"old": { "file": "my_msgs/msg/Pose.msg", "line": 1 },
|
|
335
352
|
"new": { "file": "my_msgs/msg/Pose.msg", "line": 1 },
|
|
336
|
-
"suppressed": false
|
|
353
|
+
"suppressed": false,
|
|
354
|
+
"help_uri": "https://github.com/justagist/roschema/blob/v0.4.0/docs/rules.md#rs003----field-type-changed"
|
|
337
355
|
}
|
|
338
356
|
],
|
|
339
357
|
"summary": {
|
|
@@ -354,7 +372,12 @@ workflow commands, which GitHub shows as annotations on the PR diff.
|
|
|
354
372
|
|
|
355
373
|
`old`/`new` are `null` when there is no corresponding side (a removed interface
|
|
356
374
|
has no `new`). `coverage` counts interfaces present on both sides (`compared`)
|
|
357
|
-
and on one side only. `
|
|
375
|
+
and on one side only. `help_uri` links to the rule's section of the
|
|
376
|
+
[rule reference](docs/rules.md) for the installed release. With `--verbose`, a
|
|
377
|
+
`changes` array lists the unreported changes (each with `rule`, `package`,
|
|
378
|
+
`interface`, `section`, `target`, `message` and `reason`), including suppressed
|
|
379
|
+
findings with the reason they are suppressed. `lint` output has no
|
|
380
|
+
`advisories`, `coverage` or `changes`.
|
|
358
381
|
|
|
359
382
|
## Exit codes
|
|
360
383
|
|
|
@@ -371,7 +394,8 @@ turns `1` into `0` for `check`, `bag-check` and `lint`; tool errors still exit
|
|
|
371
394
|
## GitHub Action
|
|
372
395
|
|
|
373
396
|
A composite Action runs `roschema check` on the runner (no setup needed) and
|
|
374
|
-
posts the findings as annotations on the PR diff
|
|
397
|
+
posts the findings as annotations on the PR diff, with a full report (findings,
|
|
398
|
+
semver advice and unreported changes) as the job summary:
|
|
375
399
|
|
|
376
400
|
```yaml
|
|
377
401
|
# .github/workflows/interfaces.yml
|
|
@@ -386,7 +410,7 @@ jobs:
|
|
|
386
410
|
- uses: justagist/roschema/action@main
|
|
387
411
|
with:
|
|
388
412
|
against: origin/${{ github.base_ref }}
|
|
389
|
-
# version: "roschema==0.
|
|
413
|
+
# version: "roschema==0.4.0" # pin a release for reproducible results
|
|
390
414
|
# level: source
|
|
391
415
|
# deep: "true"
|
|
392
416
|
# warnings-as-errors: "true"
|
|
@@ -413,8 +437,10 @@ because it is part of the checkout.
|
|
|
413
437
|
## Troubleshooting
|
|
414
438
|
|
|
415
439
|
**"no breaking changes found", but you expected some.** The line under the
|
|
416
|
-
header says how many interfaces were compared
|
|
417
|
-
|
|
440
|
+
header says how many interfaces were compared, and `--verbose` lists the changes
|
|
441
|
+
that were seen but not reported, with the reason. Run `roschema ls` with the
|
|
442
|
+
same target to see which packages and interfaces are discovered, then check
|
|
443
|
+
that:
|
|
418
444
|
|
|
419
445
|
- the packages are not below a `COLCON_IGNORE` or `AMENT_IGNORE` marker;
|
|
420
446
|
- a package-name target did not fall back to the installed copy (the warning
|
|
@@ -431,6 +457,7 @@ shown in [Configuration](#configuration); the error suggests the closest one.
|
|
|
431
457
|
|
|
432
458
|
Every rule, its levels, default severity, rationale and an example is
|
|
433
459
|
documented in [docs/rules.md](docs/rules.md), generated from the rule metadata.
|
|
460
|
+
`roschema explain RULE` prints the same section in the terminal.
|
|
434
461
|
|
|
435
462
|
## Non-goals
|
|
436
463
|
|
|
@@ -38,7 +38,7 @@ inputs:
|
|
|
38
38
|
version:
|
|
39
39
|
description: >-
|
|
40
40
|
roschema requirement for uvx. Unpinned runs the latest release (refreshed
|
|
41
|
-
on every run, even with a uv cache); pin it (e.g. "roschema==0.
|
|
41
|
+
on every run, even with a uv cache); pin it (e.g. "roschema==0.4.0") for
|
|
42
42
|
reproducible results.
|
|
43
43
|
default: roschema
|
|
44
44
|
|
|
@@ -78,6 +78,9 @@ runs:
|
|
|
78
78
|
if [ "$ROSCHEMA_WARNINGS_AS_ERRORS" = "true" ]; then
|
|
79
79
|
args+=(--warnings-as-errors)
|
|
80
80
|
fi
|
|
81
|
+
if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
|
|
82
|
+
args+=(--summary "$GITHUB_STEP_SUMMARY")
|
|
83
|
+
fi
|
|
81
84
|
read -r -a excludes <<< "${ROSCHEMA_EXCLUDE//$'\n'/ }"
|
|
82
85
|
for glob in "${excludes[@]}"; do
|
|
83
86
|
args+=(--exclude "$glob")
|
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import difflib
|
|
5
6
|
import json
|
|
6
7
|
import os
|
|
7
8
|
import posixpath
|
|
8
9
|
from collections.abc import Callable
|
|
9
|
-
from dataclasses import replace
|
|
10
|
+
from dataclasses import dataclass, replace
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
from typing import Annotated
|
|
12
13
|
|
|
13
14
|
import typer
|
|
14
15
|
from rich.console import Console
|
|
16
|
+
from rich.markdown import Markdown
|
|
15
17
|
|
|
16
18
|
from . import __version__
|
|
17
19
|
from .config import (
|
|
@@ -38,8 +40,18 @@ from .ir import InterfaceFile
|
|
|
38
40
|
from .loader import load_interfaces
|
|
39
41
|
from .mcap_fs import McapTypeFS
|
|
40
42
|
from .parser import ParseError
|
|
41
|
-
from .report import
|
|
42
|
-
|
|
43
|
+
from .report import (
|
|
44
|
+
Coverage,
|
|
45
|
+
build_github,
|
|
46
|
+
build_json,
|
|
47
|
+
build_markdown,
|
|
48
|
+
coverage,
|
|
49
|
+
render_lint,
|
|
50
|
+
render_text,
|
|
51
|
+
)
|
|
52
|
+
from .report.changes import Change, additions, by_location, suppressed_changes
|
|
53
|
+
from .rules import Finding, all_lint_rules, all_rules, evaluate, lint_interfaces
|
|
54
|
+
from .rules.gendocs import docs_url, find_rule, render_rule
|
|
43
55
|
from .vfs import GitTreeFS, RealFS, VfsError, git_toplevel
|
|
44
56
|
|
|
45
57
|
app = typer.Typer(
|
|
@@ -52,6 +64,31 @@ app = typer.Typer(
|
|
|
52
64
|
console = Console(soft_wrap=True)
|
|
53
65
|
err_console = Console(stderr=True, soft_wrap=True)
|
|
54
66
|
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True)
|
|
69
|
+
class _Result:
|
|
70
|
+
"""What a check or bag check found, for reporting.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
findings: Finalized findings, including suppressed ones.
|
|
74
|
+
new_source: Description of the target side.
|
|
75
|
+
old_source: Description of the baseline side.
|
|
76
|
+
coverage: Interface counts across both sides.
|
|
77
|
+
changes: Changes that are not findings: additions, and findings dropped
|
|
78
|
+
for their level or because their rule is off.
|
|
79
|
+
interface_files: Each current interface's source file, by interface key.
|
|
80
|
+
display_prefix: Repo-root-relative prefix for annotation paths.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
findings: list[Finding]
|
|
84
|
+
new_source: str
|
|
85
|
+
old_source: str
|
|
86
|
+
coverage: Coverage
|
|
87
|
+
changes: list[Change]
|
|
88
|
+
interface_files: dict[str, str]
|
|
89
|
+
display_prefix: str = ""
|
|
90
|
+
|
|
91
|
+
|
|
55
92
|
# A bag check asks whether current code can still decode the recording, which is
|
|
56
93
|
# a wire question; constant and other source-level changes do not affect it.
|
|
57
94
|
BAG_DEFAULT_LEVEL = "wire"
|
|
@@ -117,6 +154,21 @@ def check(
|
|
|
117
154
|
bool,
|
|
118
155
|
typer.Option("--warnings-as-errors", help="Exit 1 on warnings as well as errors."),
|
|
119
156
|
] = False,
|
|
157
|
+
verbose: Annotated[
|
|
158
|
+
bool,
|
|
159
|
+
typer.Option(
|
|
160
|
+
"--verbose",
|
|
161
|
+
help="Also list changes that are not reported (added, below the level, rule off "
|
|
162
|
+
"or suppressed) and why.",
|
|
163
|
+
),
|
|
164
|
+
] = False,
|
|
165
|
+
summary_path: Annotated[
|
|
166
|
+
Path | None,
|
|
167
|
+
typer.Option(
|
|
168
|
+
"--summary",
|
|
169
|
+
help="Also append a Markdown report to this file, e.g. $GITHUB_STEP_SUMMARY.",
|
|
170
|
+
),
|
|
171
|
+
] = None,
|
|
120
172
|
allow_empty: Annotated[
|
|
121
173
|
bool,
|
|
122
174
|
typer.Option(
|
|
@@ -138,24 +190,28 @@ def check(
|
|
|
138
190
|
raise typer.Exit(code=2)
|
|
139
191
|
|
|
140
192
|
try:
|
|
141
|
-
|
|
193
|
+
result = _compute(
|
|
142
194
|
target, lambda root: resolve_baseline(against, root), level, config_path, exclude, deep
|
|
143
195
|
)
|
|
144
196
|
except (VfsError, ParseError, ConfigError) as exc:
|
|
145
197
|
err_console.print(f"[red]error:[/red] {exc}")
|
|
146
198
|
raise typer.Exit(code=2) from exc
|
|
147
199
|
|
|
148
|
-
if old_source.startswith("git:") and Path(against).is_dir():
|
|
200
|
+
if result.old_source.startswith("git:") and Path(against).is_dir():
|
|
149
201
|
err_console.print(
|
|
150
202
|
f"[dim]note: {against!r} matched a git ref; "
|
|
151
203
|
f"use './{against}' to compare against the directory instead.[/dim]"
|
|
152
204
|
)
|
|
153
205
|
if not allow_empty:
|
|
154
206
|
_require_baseline(
|
|
155
|
-
|
|
207
|
+
result.coverage,
|
|
208
|
+
f"the baseline ({result.old_source})",
|
|
209
|
+
"pass --allow-empty if that is expected",
|
|
156
210
|
)
|
|
157
|
-
_emit(
|
|
158
|
-
|
|
211
|
+
_emit(result, fmt, verbose)
|
|
212
|
+
if summary_path is not None:
|
|
213
|
+
_write_summary(result, summary_path)
|
|
214
|
+
_exit_for(result.findings, exit_zero, warnings_as_errors)
|
|
159
215
|
|
|
160
216
|
|
|
161
217
|
@app.command(name="bag-check")
|
|
@@ -189,6 +245,21 @@ def bag_check(
|
|
|
189
245
|
bool,
|
|
190
246
|
typer.Option("--warnings-as-errors", help="Exit 1 on warnings as well as errors."),
|
|
191
247
|
] = False,
|
|
248
|
+
verbose: Annotated[
|
|
249
|
+
bool,
|
|
250
|
+
typer.Option(
|
|
251
|
+
"--verbose",
|
|
252
|
+
help="Also list changes that are not reported (added, below the level, rule off "
|
|
253
|
+
"or suppressed) and why.",
|
|
254
|
+
),
|
|
255
|
+
] = False,
|
|
256
|
+
summary_path: Annotated[
|
|
257
|
+
Path | None,
|
|
258
|
+
typer.Option(
|
|
259
|
+
"--summary",
|
|
260
|
+
help="Also append a Markdown report to this file, e.g. $GITHUB_STEP_SUMMARY.",
|
|
261
|
+
),
|
|
262
|
+
] = None,
|
|
192
263
|
exclude: Annotated[
|
|
193
264
|
list[str] | None,
|
|
194
265
|
typer.Option("--exclude", help="Glob of packages/interfaces to skip."),
|
|
@@ -224,14 +295,21 @@ def bag_check(
|
|
|
224
295
|
err_console.print(f"[red]error:[/red] {exc}")
|
|
225
296
|
raise typer.Exit(code=2) from exc
|
|
226
297
|
|
|
227
|
-
|
|
228
|
-
|
|
298
|
+
events = diff_interfaces(old_ifaces, new_ifaces)
|
|
299
|
+
dropped: list[tuple[Finding, str]] = []
|
|
300
|
+
result = _Result(
|
|
301
|
+
findings=finalize_findings(evaluate(events), config, level, BAG_DEFAULT_LEVEL, dropped),
|
|
302
|
+
new_source=new_source,
|
|
303
|
+
old_source=old_res.source,
|
|
304
|
+
coverage=coverage(old_ifaces, new_ifaces),
|
|
305
|
+
changes=additions(events) + [Change.from_finding(f, why) for f, why in dropped],
|
|
306
|
+
interface_files={key: iface.source_file for key, iface in new_ifaces.items()},
|
|
229
307
|
)
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
_exit_for(findings, exit_zero, warnings_as_errors)
|
|
308
|
+
_require_baseline(result.coverage, "the bag", "check the --exclude patterns")
|
|
309
|
+
_emit(result, fmt, verbose)
|
|
310
|
+
if summary_path is not None:
|
|
311
|
+
_write_summary(result, summary_path)
|
|
312
|
+
_exit_for(result.findings, exit_zero, warnings_as_errors)
|
|
235
313
|
|
|
236
314
|
|
|
237
315
|
def _resolve_current(
|
|
@@ -285,12 +363,8 @@ def _compute(
|
|
|
285
363
|
config_path: Path | None,
|
|
286
364
|
exclude: list[str] | None,
|
|
287
365
|
deep: bool | None = None,
|
|
288
|
-
) ->
|
|
289
|
-
"""Resolve both sides, diff and finalize; the shared check pipeline.
|
|
290
|
-
|
|
291
|
-
Also returns each current interface's source file, keyed by interface, and
|
|
292
|
-
how many interfaces were compared.
|
|
293
|
-
"""
|
|
366
|
+
) -> _Result:
|
|
367
|
+
"""Resolve both sides, diff and finalize; the shared check pipeline."""
|
|
294
368
|
new_res = _resolve_target(target, allow_empty_glob=True)
|
|
295
369
|
if not isinstance(new_res.vfs, RealFS):
|
|
296
370
|
raise VfsError("target must be a directory or workspace")
|
|
@@ -324,16 +398,24 @@ def _compute(
|
|
|
324
398
|
raise VfsError(f"target glob {target!r} matched no interface packages on either side")
|
|
325
399
|
old_packages = tuple(p for p in old_packages if p.name in scope)
|
|
326
400
|
old_ifaces = load_interfaces(old_res.vfs, apply_exclude(old_packages, excludes))
|
|
327
|
-
|
|
401
|
+
events = diff_interfaces(old_ifaces, new_ifaces)
|
|
402
|
+
dropped: list[tuple[Finding, str]] = []
|
|
403
|
+
findings = finalize_findings(evaluate(events), config, level, dropped=dropped)
|
|
328
404
|
deep_packages = {p.name for p in new_res.packages if config.effective_deep(p.name, deep)}
|
|
329
405
|
if deep_packages:
|
|
330
406
|
transitive = transitive_findings(new_ifaces, findings)
|
|
331
407
|
findings += finalize_findings(
|
|
332
|
-
[f for f in transitive if f.package in deep_packages], config, level
|
|
408
|
+
[f for f in transitive if f.package in deep_packages], config, level, dropped=dropped
|
|
333
409
|
)
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
410
|
+
return _Result(
|
|
411
|
+
findings=findings,
|
|
412
|
+
new_source=new_res.source,
|
|
413
|
+
old_source=old_res.source,
|
|
414
|
+
coverage=coverage(old_ifaces, new_ifaces),
|
|
415
|
+
changes=additions(events) + [Change.from_finding(f, why) for f, why in dropped],
|
|
416
|
+
interface_files={key: iface.source_file for key, iface in new_ifaces.items()},
|
|
417
|
+
display_prefix=prefix,
|
|
418
|
+
)
|
|
337
419
|
|
|
338
420
|
|
|
339
421
|
def _note_skipped_schemas(res: Resolution) -> None:
|
|
@@ -376,23 +458,37 @@ def _exit_for(findings: list[Finding], exit_zero: bool, warnings_as_errors: bool
|
|
|
376
458
|
raise typer.Exit(code=1 if has_failure else 0)
|
|
377
459
|
|
|
378
460
|
|
|
379
|
-
def
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
461
|
+
def _write_summary(result: _Result, path: Path) -> None:
|
|
462
|
+
"""Append the Markdown report to ``path``; GitHub collects step summaries this way."""
|
|
463
|
+
report = build_markdown(
|
|
464
|
+
result.findings, result.new_source, result.old_source, result.coverage, result.changes
|
|
465
|
+
)
|
|
466
|
+
try:
|
|
467
|
+
with path.open("a", encoding="utf-8") as handle:
|
|
468
|
+
handle.write(report)
|
|
469
|
+
except OSError as exc:
|
|
470
|
+
err_console.print(f"[red]error:[/red] cannot write the summary to {str(path)!r}: {exc}")
|
|
471
|
+
raise typer.Exit(code=2) from exc
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _emit(result: _Result, fmt: str, verbose: bool) -> None:
|
|
475
|
+
findings, cov, changes = result.findings, result.coverage, result.changes
|
|
388
476
|
if fmt == "json":
|
|
389
|
-
|
|
477
|
+
doc = build_json(findings, coverage=cov, changes=changes, verbose=verbose)
|
|
478
|
+
typer.echo(json.dumps(doc, indent=2))
|
|
390
479
|
elif fmt == "github":
|
|
391
480
|
typer.echo(cov.describe())
|
|
392
|
-
|
|
481
|
+
if verbose:
|
|
482
|
+
for change in by_location([*changes, *suppressed_changes(findings)]):
|
|
483
|
+
rule = f"{change.rule} " if change.rule else ""
|
|
484
|
+
line = f"{rule}{change.qualified_target}: {change.message} ({change.reason})"
|
|
485
|
+
# A line break (e.g. in an allowlist reason) could start a
|
|
486
|
+
# workflow command; commands are only read at line starts.
|
|
487
|
+
typer.echo("not reported: " + " ".join(line.splitlines()))
|
|
488
|
+
for line in build_github(findings, result.display_prefix, result.interface_files):
|
|
393
489
|
typer.echo(line)
|
|
394
490
|
else:
|
|
395
|
-
render_text(findings, console, new_source, old_source, cov)
|
|
491
|
+
render_text(findings, console, result.new_source, result.old_source, cov, changes, verbose)
|
|
396
492
|
|
|
397
493
|
|
|
398
494
|
@app.command()
|
|
@@ -543,6 +639,22 @@ def freeze(
|
|
|
543
639
|
console.print(f"wrote {len(interfaces)} interface(s) to {output}")
|
|
544
640
|
|
|
545
641
|
|
|
642
|
+
@app.command()
|
|
643
|
+
def explain(
|
|
644
|
+
rule_id: Annotated[str, typer.Argument(metavar="RULE", help="Rule ID, e.g. RS003.")],
|
|
645
|
+
) -> None:
|
|
646
|
+
"""Describe a rule: what it reports, its levels, rationale and an example."""
|
|
647
|
+
rule = find_rule(rule_id)
|
|
648
|
+
if rule is None:
|
|
649
|
+
ids = [r.id for r in all_rules()] + [r.id for r in all_lint_rules()]
|
|
650
|
+
close = difflib.get_close_matches(rule_id.upper(), ids, n=1)
|
|
651
|
+
hint = f"; did you mean {close[0]}?" if close else ""
|
|
652
|
+
err_console.print(f"[red]error:[/red] unknown rule {rule_id!r}{hint}")
|
|
653
|
+
raise typer.Exit(code=2)
|
|
654
|
+
console.print(Markdown(render_rule(rule)))
|
|
655
|
+
console.print(f"\n{docs_url(rule.id)}")
|
|
656
|
+
|
|
657
|
+
|
|
546
658
|
@app.command()
|
|
547
659
|
def ls(
|
|
548
660
|
target: Annotated[
|