subedit 0.1.0__tar.gz → 0.1.2__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.
- {subedit-0.1.0 → subedit-0.1.2}/.claude-plugin/plugin.json +1 -1
- {subedit-0.1.0 → subedit-0.1.2}/.github/workflows/ci.yml +14 -0
- subedit-0.1.2/.vale.ini +13 -0
- {subedit-0.1.0 → subedit-0.1.2}/PKG-INFO +20 -8
- {subedit-0.1.0 → subedit-0.1.2}/README.md +14 -6
- subedit-0.1.2/docs/rules.md +18 -0
- subedit-0.1.2/examples/walkthrough.py +71 -0
- {subedit-0.1.0 → subedit-0.1.2}/hooks/hooks.json +11 -0
- subedit-0.1.2/hooks/run-subedit-hook.sh +27 -0
- {subedit-0.1.0 → subedit-0.1.2}/pyproject.toml +7 -2
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/__init__.py +1 -1
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/application/fixer.py +13 -5
- subedit-0.1.2/src/subedit/application/report_aggregator.py +35 -0
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/domain/models/comment.py +18 -7
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/domain/models/diagnostic.py +13 -0
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/domain/models/prose.py +4 -0
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/domain/models/rule_set.py +5 -0
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/domain/ports/external_linter.py +3 -0
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/domain/rules/layout.py +3 -2
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/domain/rules/rewrite.py +10 -4
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/domain/rules/structure.py +3 -1
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/infrastructure/config_sources/toml_config_source.py +7 -3
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/infrastructure/linters/autocorrect_linter.py +25 -25
- subedit-0.1.2/src/subedit/infrastructure/linters/vale_linter.py +146 -0
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/infrastructure/parsers/languages/language_profile.py +7 -0
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/infrastructure/parsers/languages/python_profile.py +14 -3
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/infrastructure/parsers/tree_sitter_parser.py +23 -8
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/presentation/cli.py +24 -5
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/presentation/hook.py +2 -2
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/presentation/report.py +23 -5
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/presentation/rule_reference.py +6 -3
- subedit-0.1.2/src/subedit/vale/styles/subedit/DateRef.yml +13 -0
- subedit-0.1.2/src/subedit/vale/styles/subedit/ExternalRef.yml +15 -0
- {subedit-0.1.0/styles/mroops → subedit-0.1.2/src/subedit/vale/styles/subedit}/MidSentenceColon.yml +2 -2
- {subedit-0.1.0/styles/mroops → subedit-0.1.2/src/subedit/vale/styles/subedit}/MidSentenceColonMarkup.yml +2 -2
- subedit-0.1.2/src/subedit/vale/vale.ini +11 -0
- subedit-0.1.2/tests/fixtures/autocorrect/unspaced.py +2 -0
- {subedit-0.1.0 → subedit-0.1.2}/tests/fixtures/vale/clean.md +7 -0
- {subedit-0.1.0 → subedit-0.1.2}/tests/test_cli.py +50 -0
- {subedit-0.1.0 → subedit-0.1.2}/tests/test_comment.py +3 -1
- {subedit-0.1.0 → subedit-0.1.2}/tests/test_config.py +9 -1
- {subedit-0.1.0 → subedit-0.1.2}/tests/test_diagnostic.py +1 -1
- {subedit-0.1.0 → subedit-0.1.2}/tests/test_fixer.py +75 -0
- {subedit-0.1.0 → subedit-0.1.2}/tests/test_hook.py +5 -0
- {subedit-0.1.0 → subedit-0.1.2}/tests/test_parser.py +28 -0
- {subedit-0.1.0 → subedit-0.1.2}/tests/test_parser_ocp.py +8 -1
- {subedit-0.1.0 → subedit-0.1.2}/tests/test_plugin.py +16 -1
- {subedit-0.1.0 → subedit-0.1.2}/tests/test_position.py +5 -5
- {subedit-0.1.0 → subedit-0.1.2}/tests/test_prose.py +5 -0
- {subedit-0.1.0 → subedit-0.1.2}/tests/test_report_aggregator.py +80 -6
- {subedit-0.1.0 → subedit-0.1.2}/tests/test_schema.py +2 -2
- {subedit-0.1.0 → subedit-0.1.2}/tests/test_self_hosting.py +9 -9
- {subedit-0.1.0 → subedit-0.1.2}/tests/test_text.py +4 -4
- subedit-0.1.2/tests/test_vale_style.py +67 -0
- {subedit-0.1.0 → subedit-0.1.2}/uv.lock +68 -15
- subedit-0.1.0/.vale.ini +0 -13
- subedit-0.1.0/docs/rules.md +0 -16
- subedit-0.1.0/examples/walkthrough.py +0 -68
- subedit-0.1.0/hooks/run-subedit-hook.sh +0 -21
- subedit-0.1.0/src/subedit/application/report_aggregator.py +0 -23
- subedit-0.1.0/src/subedit/infrastructure/linters/vale_linter.py +0 -87
- subedit-0.1.0/styles/mroops/ExternalRef.yml +0 -7
- subedit-0.1.0/tests/test_vale_style.py +0 -49
- {subedit-0.1.0 → subedit-0.1.2}/.claude/settings.json +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/.claude-plugin/marketplace.json +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/.github/dependabot.yml +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/.github/release.yml +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/.github/workflows/pr.yml +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/.github/workflows/release.yml +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/.gitignore +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/LICENSE +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/examples/README.md +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/examples/probe.py +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/examples/subedit.toml +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/application/__init__.py +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/application/corpus_meter.py +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/application/linter.py +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/domain/__init__.py +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/domain/models/__init__.py +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/domain/models/document.py +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/domain/models/position.py +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/domain/models/text.py +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/domain/ports/__init__.py +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/domain/ports/language_conventions.py +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/domain/ports/rule.py +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/domain/ports/source_parser.py +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/domain/rules/__init__.py +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/infrastructure/__init__.py +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/infrastructure/config_sources/__init__.py +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/infrastructure/config_sources/config_schema.py +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/infrastructure/linters/__init__.py +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/infrastructure/parsers/__init__.py +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/infrastructure/parsers/languages/__init__.py +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/presentation/__init__.py +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/presentation/harnesses/__init__.py +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/presentation/harnesses/agent_harness.py +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/presentation/harnesses/claude_code_harness.py +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/src/subedit/presentation/harnesses/codex_harness.py +0 -0
- {subedit-0.1.0/styles/mroops → subedit-0.1.2/src/subedit/vale/styles/subedit}/EditHistory.yml +0 -0
- {subedit-0.1.0/styles/mroops → subedit-0.1.2/src/subedit/vale/styles/subedit}/Ellipsis.yml +0 -0
- {subedit-0.1.0/styles/mroops → subedit-0.1.2/src/subedit/vale/styles/subedit}/EmDash.yml +0 -0
- {subedit-0.1.0/styles/mroops → subedit-0.1.2/src/subedit/vale/styles/subedit}/Semicolon.yml +0 -0
- {subedit-0.1.0/styles/mroops → subedit-0.1.2/src/subedit/vale/styles/subedit}/SignatureTag.yml +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/subedit.toml +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/tests/fixtures/vale/violations.py +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/tests/test_corpus_meter.py +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/tests/test_harnesses.py +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/tests/test_linter.py +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/tests/test_parser_oracle.py +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/tests/test_rules_layout.py +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/tests/test_rules_rewrite.py +0 -0
- {subedit-0.1.0 → subedit-0.1.2}/tests/test_rules_structure.py +0 -0
|
@@ -50,6 +50,20 @@ jobs:
|
|
|
50
50
|
python-version: ${{ matrix.python-version }}
|
|
51
51
|
enable-cache: true
|
|
52
52
|
- run: uv sync --extra dev
|
|
53
|
+
|
|
54
|
+
# Without Vale, every test against it skips and the self-hosting check never runs it.
|
|
55
|
+
# AutoCorrect needs no step, since it arrives as a Python dependency.
|
|
56
|
+
- name: Install Vale
|
|
57
|
+
env:
|
|
58
|
+
VALE_VERSION: 3.18.0
|
|
59
|
+
run: |
|
|
60
|
+
curl -sSfL "https://github.com/errata-ai/vale/releases/download/v${VALE_VERSION}/vale_${VALE_VERSION}_Linux_64-bit.tar.gz" \
|
|
61
|
+
| tar -xz -C "$RUNNER_TEMP" vale
|
|
62
|
+
echo "$RUNNER_TEMP" >> "$GITHUB_PATH"
|
|
63
|
+
|
|
64
|
+
- name: Confirm Vale
|
|
65
|
+
run: vale --version
|
|
66
|
+
|
|
53
67
|
- run: uv run pytest tests/ -v --tb=short
|
|
54
68
|
|
|
55
69
|
- name: Self-hosting
|
subedit-0.1.2/.vale.ini
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
StylesPath = src/subedit/vale/styles
|
|
2
|
+
MinAlertLevel = warning
|
|
3
|
+
|
|
4
|
+
[*.{py,ts,tsx,js,go,rs}]
|
|
5
|
+
BasedOnStyles = subedit
|
|
6
|
+
subedit.MidSentenceColonMarkup = NO
|
|
7
|
+
|
|
8
|
+
[*.md]
|
|
9
|
+
BasedOnStyles = subedit
|
|
10
|
+
subedit.MidSentenceColon = NO
|
|
11
|
+
|
|
12
|
+
[tests/fixtures/vale/*]
|
|
13
|
+
BasedOnStyles =
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: subedit
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: A linter and formatter for the prose inside code comments, across languages.
|
|
5
5
|
Project-URL: Homepage, https://github.com/mroops0111/subedit
|
|
6
6
|
Project-URL: Repository, https://github.com/mroops0111/subedit
|
|
@@ -18,14 +18,18 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
19
|
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
20
|
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: autocorrect-py>=2.14.0
|
|
21
22
|
Requires-Dist: pydantic>=2.13.5
|
|
22
|
-
Requires-Dist: tree-sitter-
|
|
23
|
+
Requires-Dist: tree-sitter-python>=0.25.0
|
|
23
24
|
Requires-Dist: tree-sitter>=0.26.0
|
|
24
25
|
Requires-Dist: typer>=0.27.2
|
|
25
26
|
Provides-Extra: dev
|
|
26
27
|
Requires-Dist: basedpyright>=1.40.1; extra == 'dev'
|
|
27
28
|
Requires-Dist: pytest>=9.0.0; extra == 'dev'
|
|
28
29
|
Requires-Dist: ruff>=0.16.7; extra == 'dev'
|
|
30
|
+
Requires-Dist: tree-sitter-typescript>=0.23.2; extra == 'dev'
|
|
31
|
+
Provides-Extra: vale
|
|
32
|
+
Requires-Dist: vale>=3.22.0.0; extra == 'vale'
|
|
29
33
|
Description-Content-Type: text/markdown
|
|
30
34
|
|
|
31
35
|
# subedit
|
|
@@ -70,7 +74,7 @@ Install the command line tool from PyPI.
|
|
|
70
74
|
uv tool install subedit
|
|
71
75
|
```
|
|
72
76
|
|
|
73
|
-
`pipx install subedit` and `pip install subedit` work as well.
|
|
77
|
+
`pipx install subedit` and `pip install subedit` work as well. Add the `vale` extra, as in `uv tool install 'subedit[vale]'`, to have Vale fetched for you.
|
|
74
78
|
|
|
75
79
|
## Commands
|
|
76
80
|
|
|
@@ -105,7 +109,7 @@ Rewriting a sentence needs its meaning, which a deterministic process cannot sup
|
|
|
105
109
|
|
|
106
110
|
### Claude Code
|
|
107
111
|
|
|
108
|
-
The plugin
|
|
112
|
+
The plugin needs only [uv](https://docs.astral.sh/uv/). It runs subedit from PyPI with the `vale` extra, and warms that environment in the background when a session starts, so no edit waits on the first download.
|
|
109
113
|
|
|
110
114
|
```text
|
|
111
115
|
/plugin marketplace add mroops0111/subedit
|
|
@@ -159,14 +163,22 @@ block-too-long = { severity = "warning", line = 3, doc-public = 20 }
|
|
|
159
163
|
|
|
160
164
|
Rule names describe the problem they report. `docs/rules.md` lists every rule, generated from the rules themselves.
|
|
161
165
|
|
|
166
|
+
A rule at `error` fails `check` and stops an agent, while one at `warning` is shown as advice and blocks nothing. The same table sets the delegated Vale rules by name.
|
|
167
|
+
|
|
168
|
+
```toml
|
|
169
|
+
[rules]
|
|
170
|
+
"subedit.MidSentenceColon" = "error"
|
|
171
|
+
"subedit.DateRef" = "off"
|
|
172
|
+
```
|
|
173
|
+
|
|
162
174
|
## Delegated Tools
|
|
163
175
|
|
|
164
|
-
`
|
|
176
|
+
`check`, `fix`, and the agent hook also run two tools, and fold their findings into the same report.
|
|
165
177
|
|
|
166
|
-
- **Vale**: punctuation, banned phrasing, and anything a markup aware prose linter already handles
|
|
167
|
-
- **AutoCorrect**: spacing and punctuation width between CJK and Latin text
|
|
178
|
+
- **Vale**: punctuation, banned phrasing, and anything a markup aware prose linter already handles. Vale has no autofix, so its findings come back as rewrites. It runs where it is installed, or where the `vale` extra fetched it. A repository without its own `.vale.ini` gets the style that ships with subedit, whose rules are named `subedit.*`.
|
|
179
|
+
- **AutoCorrect**: spacing and punctuation width between CJK and Latin text. Its corrections are applied silently, like any other mechanical fix. It arrives as a Python dependency, and reads `.autocorrectrc` from the working directory.
|
|
168
180
|
|
|
169
|
-
`--no-delegate` skips both
|
|
181
|
+
`--no-delegate` skips both for `check` and `fix`.
|
|
170
182
|
|
|
171
183
|
## Surfaces
|
|
172
184
|
|
|
@@ -40,7 +40,7 @@ Install the command line tool from PyPI.
|
|
|
40
40
|
uv tool install subedit
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
`pipx install subedit` and `pip install subedit` work as well.
|
|
43
|
+
`pipx install subedit` and `pip install subedit` work as well. Add the `vale` extra, as in `uv tool install 'subedit[vale]'`, to have Vale fetched for you.
|
|
44
44
|
|
|
45
45
|
## Commands
|
|
46
46
|
|
|
@@ -75,7 +75,7 @@ Rewriting a sentence needs its meaning, which a deterministic process cannot sup
|
|
|
75
75
|
|
|
76
76
|
### Claude Code
|
|
77
77
|
|
|
78
|
-
The plugin
|
|
78
|
+
The plugin needs only [uv](https://docs.astral.sh/uv/). It runs subedit from PyPI with the `vale` extra, and warms that environment in the background when a session starts, so no edit waits on the first download.
|
|
79
79
|
|
|
80
80
|
```text
|
|
81
81
|
/plugin marketplace add mroops0111/subedit
|
|
@@ -129,14 +129,22 @@ block-too-long = { severity = "warning", line = 3, doc-public = 20 }
|
|
|
129
129
|
|
|
130
130
|
Rule names describe the problem they report. `docs/rules.md` lists every rule, generated from the rules themselves.
|
|
131
131
|
|
|
132
|
+
A rule at `error` fails `check` and stops an agent, while one at `warning` is shown as advice and blocks nothing. The same table sets the delegated Vale rules by name.
|
|
133
|
+
|
|
134
|
+
```toml
|
|
135
|
+
[rules]
|
|
136
|
+
"subedit.MidSentenceColon" = "error"
|
|
137
|
+
"subedit.DateRef" = "off"
|
|
138
|
+
```
|
|
139
|
+
|
|
132
140
|
## Delegated Tools
|
|
133
141
|
|
|
134
|
-
`
|
|
142
|
+
`check`, `fix`, and the agent hook also run two tools, and fold their findings into the same report.
|
|
135
143
|
|
|
136
|
-
- **Vale**: punctuation, banned phrasing, and anything a markup aware prose linter already handles
|
|
137
|
-
- **AutoCorrect**: spacing and punctuation width between CJK and Latin text
|
|
144
|
+
- **Vale**: punctuation, banned phrasing, and anything a markup aware prose linter already handles. Vale has no autofix, so its findings come back as rewrites. It runs where it is installed, or where the `vale` extra fetched it. A repository without its own `.vale.ini` gets the style that ships with subedit, whose rules are named `subedit.*`.
|
|
145
|
+
- **AutoCorrect**: spacing and punctuation width between CJK and Latin text. Its corrections are applied silently, like any other mechanical fix. It arrives as a Python dependency, and reads `.autocorrectrc` from the working directory.
|
|
138
146
|
|
|
139
|
-
`--no-delegate` skips both
|
|
147
|
+
`--no-delegate` skips both for `check` and `fix`.
|
|
140
148
|
|
|
141
149
|
## Surfaces
|
|
142
150
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Rules
|
|
2
|
+
|
|
3
|
+
Generated from `RuleMetadata`. Edit the rule, not this file.
|
|
4
|
+
|
|
5
|
+
The fix kind states who resolves a finding. A mechanical kind is repaired without telling the author, so only a rewrite reaches an agent.
|
|
6
|
+
|
|
7
|
+
The level says whether a rewrite blocks. An error fails check and stops an agent, while a warning is shown as advice. Set either per rule under `[rules]`, and the Vale rules the same way by name, as in `"subedit.DateRef" = "off"`.
|
|
8
|
+
|
|
9
|
+
| Rule | Fix kind | Level | Summary |
|
|
10
|
+
| --- | --- | --- | --- |
|
|
11
|
+
| `line-too-long` | reflow | error | A physical comment line must fit the configured width, counting indent and delimiter. |
|
|
12
|
+
| `mid-clause-break` | reflow | error | A comment block must be laid out one sentence per line, splitting only at commas. |
|
|
13
|
+
| `block-too-long` | rewrite | warning | A comment block long enough to restate the code has stopped earning its place. |
|
|
14
|
+
| `file-header-comment` | rewrite | warning | A file must not open with a banner comment before its first declaration. |
|
|
15
|
+
| `plain-comment-on-public` | rewrite | error | A public symbol must be documented in the doc comment form its language renders. |
|
|
16
|
+
| `signature-restating-tag` | rewrite | error | A doc comment must not restate what the type signature already carries. |
|
|
17
|
+
| `stub-fragment` | rewrite | warning | A sentence forced to break must not leave a stub of a line behind. |
|
|
18
|
+
| `unbreakable-sentence` | rewrite | error | A sentence over the width must carry a comma so it has somewhere legal to break. |
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
from subedit.application import Linter, RuleEngine
|
|
4
|
+
from subedit.domain import Document, RuleSet
|
|
5
|
+
from subedit.infrastructure import ALL_RULES, DEFAULT_PARSER
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
SOURCE = """def build(value):
|
|
9
|
+
# cap the size to the shared limit so the mmap does not
|
|
10
|
+
# blow past it, an unbounded value faults the runner.
|
|
11
|
+
return value
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
WIDTH = int(sys.argv[1]) if len(sys.argv) > 1 else 60
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def show(step_number, title, body):
|
|
18
|
+
print(f'\n{"─" * 62}\n{step_number}. {title}\n{"─" * 62}')
|
|
19
|
+
print(body)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
show(1, 'Source (just a string)', SOURCE.rstrip())
|
|
23
|
+
|
|
24
|
+
document = Document(uri='file:///demo.py', text=SOURCE, language_id='python')
|
|
25
|
+
group = DEFAULT_PARSER.parse(document)[0]
|
|
26
|
+
|
|
27
|
+
show(
|
|
28
|
+
2,
|
|
29
|
+
'The parser turns it into a CommentGroup',
|
|
30
|
+
f"""form = {group.form.value} shape of the comment
|
|
31
|
+
placement = {group.placement.value} where it sits
|
|
32
|
+
attachment = {group.attachment} what it documents
|
|
33
|
+
line_count = {group.line_count}
|
|
34
|
+
widest = {group.longest_line} characters""",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
show(
|
|
38
|
+
3,
|
|
39
|
+
'Each line splits into three parts (the key to measuring width)',
|
|
40
|
+
'\n'.join(
|
|
41
|
+
f' indent={line.indent!r:6s} delimiter={line.delimiter!r:5s} text={line.text!r}' for line in group.lines
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
show(
|
|
46
|
+
4,
|
|
47
|
+
'Prose is the plain text left once the markers are stripped',
|
|
48
|
+
f"""physical_lines = {group.prose().physical_lines}
|
|
49
|
+
|
|
50
|
+
flattened = {group.prose().flattened!r}
|
|
51
|
+
|
|
52
|
+
sentences = {[sentence.text for sentence in group.prose().sentences()]}""",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
show(5, f'reflow computes the right layout (one sentence per line, width {WIDTH})', group.reflow(WIDTH).render())
|
|
56
|
+
|
|
57
|
+
report = Linter(DEFAULT_PARSER, RuleEngine(ALL_RULES)).run(
|
|
58
|
+
document, RuleSet(enabled_rule_ids=frozenset(rule.metadata.rule_id for rule in ALL_RULES), line_width=WIDTH)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
show(
|
|
62
|
+
6,
|
|
63
|
+
'Each of the 8 rules asks one question, and these answered',
|
|
64
|
+
'\n'.join(
|
|
65
|
+
f' {diagnostic.rule_id:22s} fixed by {"the tool" if diagnostic.is_mechanical else "the author"}'
|
|
66
|
+
for diagnostic in report.diagnostics
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
print(f'\n → {len(report.mechanical)} mechanical, repaired silently by the tool')
|
|
71
|
+
print(f' → {len(report.needing_author)} need the author, and only these reach an agent')
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Runs subedit from PyPI through uvx, so the plugin needs nothing installed but uv.
|
|
3
|
+
# With "prepare", it warms the same environment in the background instead,
|
|
4
|
+
# because the first run installs subedit and downloads Vale, which no edit should wait on.
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
|
|
7
|
+
SPEC='subedit[vale]>=0.1.2'
|
|
8
|
+
# A bare uvx takes the first Python it finds, which on a stock Mac is too old for subedit.
|
|
9
|
+
PYTHON='>=3.11'
|
|
10
|
+
|
|
11
|
+
if ! command -v uvx >/dev/null 2>&1; then
|
|
12
|
+
cat >&2 <<'MESSAGE'
|
|
13
|
+
subedit needs uv, so comment checks are not running.
|
|
14
|
+
|
|
15
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
16
|
+
|
|
17
|
+
Install it, or remove the subedit plugin.
|
|
18
|
+
MESSAGE
|
|
19
|
+
exit 1
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
if [ "${1:-}" = prepare ]; then
|
|
23
|
+
nohup uvx --python "$PYTHON" --from "$SPEC" vale --version >/dev/null 2>&1 &
|
|
24
|
+
exit 0
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
exec uvx --python "$PYTHON" --from "$SPEC" subedit-hook
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "subedit"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.2"
|
|
4
4
|
description = "A linter and formatter for the prose inside code comments, across languages."
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "Yun-Tai Yang"},
|
|
@@ -21,9 +21,10 @@ classifiers = [
|
|
|
21
21
|
]
|
|
22
22
|
|
|
23
23
|
dependencies = [
|
|
24
|
+
"autocorrect-py>=2.14.0",
|
|
24
25
|
"pydantic>=2.13.5",
|
|
25
26
|
"tree-sitter>=0.26.0",
|
|
26
|
-
"tree-sitter-
|
|
27
|
+
"tree-sitter-python>=0.25.0",
|
|
27
28
|
"typer>=0.27.2",
|
|
28
29
|
]
|
|
29
30
|
|
|
@@ -32,6 +33,10 @@ dev = [
|
|
|
32
33
|
"pytest>=9.0.0",
|
|
33
34
|
"basedpyright>=1.40.1",
|
|
34
35
|
"ruff>=0.16.7",
|
|
36
|
+
"tree-sitter-typescript>=0.23.2",
|
|
37
|
+
]
|
|
38
|
+
vale = [
|
|
39
|
+
"vale>=3.22.0.0",
|
|
35
40
|
]
|
|
36
41
|
|
|
37
42
|
[project.scripts]
|
|
@@ -4,7 +4,9 @@ from ..domain.models.diagnostic import Diagnostic, FixKind, Report
|
|
|
4
4
|
from ..domain.models.document import Document
|
|
5
5
|
from ..domain.models.rule_set import RuleSet
|
|
6
6
|
from ..domain.models.text import TextEdit, apply_edits
|
|
7
|
+
from ..domain.ports.external_linter import ExternalLinter
|
|
7
8
|
from .linter import Linter
|
|
9
|
+
from .report_aggregator import ReportAggregator
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
MAX_ROUNDS = 10
|
|
@@ -23,19 +25,25 @@ class FixResult:
|
|
|
23
25
|
|
|
24
26
|
@property
|
|
25
27
|
def unresolved_findings(self) -> tuple[Diagnostic, ...]:
|
|
26
|
-
return self.report.
|
|
28
|
+
return self.report.blocking
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
class Fixer:
|
|
30
|
-
def __init__(
|
|
31
|
-
self
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
linter: Linter,
|
|
35
|
+
external_linters: tuple[ExternalLinter, ...] = (),
|
|
36
|
+
max_rounds: int = MAX_ROUNDS,
|
|
37
|
+
) -> None:
|
|
38
|
+
# External findings join every round, so an edit they carry is applied like a native one.
|
|
39
|
+
self._aggregator = ReportAggregator(linter, external_linters)
|
|
32
40
|
self._max_rounds = max_rounds
|
|
33
41
|
|
|
34
42
|
def run(self, document: Document, rule_set: RuleSet) -> FixResult:
|
|
35
43
|
working_document = document
|
|
36
44
|
applied_edit_count = 0
|
|
37
45
|
round_count = 0
|
|
38
|
-
report = self.
|
|
46
|
+
report = self._aggregator.run(working_document, rule_set)
|
|
39
47
|
|
|
40
48
|
while round_count < self._max_rounds:
|
|
41
49
|
edits = self._next_edits(report)
|
|
@@ -44,7 +52,7 @@ class Fixer:
|
|
|
44
52
|
working_document = working_document.with_text(apply_edits(working_document.text, edits))
|
|
45
53
|
applied_edit_count += len(edits)
|
|
46
54
|
round_count += 1
|
|
47
|
-
report = self.
|
|
55
|
+
report = self._aggregator.run(working_document, rule_set)
|
|
48
56
|
|
|
49
57
|
return FixResult(
|
|
50
58
|
document=working_document, report=report, round_count=round_count, applied_edit_count=applied_edit_count
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
|
|
3
|
+
from ..domain.models.diagnostic import Diagnostic, Report
|
|
4
|
+
from ..domain.models.document import Document
|
|
5
|
+
from ..domain.models.rule_set import RuleSet
|
|
6
|
+
from ..domain.ports.external_linter import ExternalLinter
|
|
7
|
+
from .linter import Linter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ReportAggregator:
|
|
11
|
+
def __init__(self, linter: Linter, external_linters: tuple[ExternalLinter, ...] = ()) -> None:
|
|
12
|
+
self._linter = linter
|
|
13
|
+
self._external_linters = external_linters
|
|
14
|
+
|
|
15
|
+
def run(self, document: Document, rule_set: RuleSet) -> Report:
|
|
16
|
+
report = self._linter.run(document, rule_set)
|
|
17
|
+
missing_sources: list[str] = []
|
|
18
|
+
for external_linter in self._external_linters:
|
|
19
|
+
if not external_linter.is_available():
|
|
20
|
+
missing_sources.append(external_linter.name)
|
|
21
|
+
continue
|
|
22
|
+
report = report.merged_with(Report(self._configured(external_linter.run(document), rule_set)))
|
|
23
|
+
if missing_sources:
|
|
24
|
+
report = report.merged_with(Report(unavailable_sources=tuple(missing_sources)))
|
|
25
|
+
return report
|
|
26
|
+
|
|
27
|
+
# The project's config has the last word on a delegated rule, as it does on a native one.
|
|
28
|
+
def _configured(self, diagnostics: tuple[Diagnostic, ...], rule_set: RuleSet) -> tuple[Diagnostic, ...]:
|
|
29
|
+
configured: list[Diagnostic] = []
|
|
30
|
+
for diagnostic in diagnostics:
|
|
31
|
+
if not rule_set.allows_external(diagnostic.rule_id):
|
|
32
|
+
continue
|
|
33
|
+
severity = rule_set.settings_for(diagnostic.rule_id).severity
|
|
34
|
+
configured.append(dataclasses.replace(diagnostic, severity=severity) if severity else diagnostic)
|
|
35
|
+
return tuple(configured)
|
|
@@ -97,24 +97,34 @@ class CommentGroup:
|
|
|
97
97
|
]
|
|
98
98
|
return Prose('\n'.join(rendered_lines))
|
|
99
99
|
|
|
100
|
-
def reflow(self, width: int) -> 'CommentGroup':
|
|
101
|
-
|
|
100
|
+
def reflow(self, width: int, stub_floor: int = 0) -> 'CommentGroup':
|
|
101
|
+
# A one line docstring that fits is the summary convention, not a block to lay out.
|
|
102
|
+
if self.form is CommentForm.DOC and self.line_count == 1 and self.longest_line <= width:
|
|
103
|
+
return self
|
|
104
|
+
texts = self._reflow_into_texts(width, stub_floor)
|
|
102
105
|
if not texts:
|
|
103
106
|
return self
|
|
104
107
|
return self._rebuild_lines(texts)
|
|
105
108
|
|
|
106
|
-
|
|
109
|
+
# A sentence whose split strands a stub needs its author,
|
|
110
|
+
# so the block keeps the layout they gave it rather than one its own check rejects.
|
|
111
|
+
def _reflow_into_texts(self, width: int, stub_floor: int) -> tuple[str, ...]:
|
|
107
112
|
budget = max(20, width - self.lines[0].prefix_width)
|
|
108
113
|
out: list[str] = []
|
|
109
|
-
for paragraph, is_literal in self._split_into_paragraphs():
|
|
114
|
+
for paragraph_index, (paragraph, is_literal) in enumerate(self._split_into_paragraphs()):
|
|
110
115
|
if out:
|
|
111
116
|
out.append('')
|
|
112
117
|
if is_literal:
|
|
113
118
|
out.extend(paragraph)
|
|
114
119
|
continue
|
|
115
120
|
joined_text = ' '.join(line.strip() for line in paragraph if line.strip())
|
|
116
|
-
for sentence in Prose(joined_text).sentences():
|
|
117
|
-
|
|
121
|
+
for sentence_index, sentence in enumerate(Prose(joined_text).sentences()):
|
|
122
|
+
if sentence_index == 1 and paragraph_index == 0 and self.form is CommentForm.DOC:
|
|
123
|
+
out.append('')
|
|
124
|
+
sentence_lines = reflow_sentence(sentence.text, budget)
|
|
125
|
+
if len(sentence_lines) > 1 and min(map(len, sentence_lines)) < stub_floor:
|
|
126
|
+
return ()
|
|
127
|
+
out.extend(sentence_lines)
|
|
118
128
|
return tuple(out)
|
|
119
129
|
|
|
120
130
|
def _split_into_paragraphs(self) -> tuple[tuple[tuple[str, ...], bool], ...]:
|
|
@@ -138,7 +148,8 @@ class CommentGroup:
|
|
|
138
148
|
closes_alone = len(self.lines) > 1 and not closing.text.strip()
|
|
139
149
|
body = [dataclasses.replace(opening, delimiter='', suffix='', text=text, range=opening.range) for text in texts]
|
|
140
150
|
body[0] = dataclasses.replace(body[0], delimiter=opening.delimiter)
|
|
141
|
-
|
|
151
|
+
# Text that fits one line closes on that line, as ruff and black format it.
|
|
152
|
+
if len(texts) > 1:
|
|
142
153
|
body.append(
|
|
143
154
|
dataclasses.replace(closing, delimiter=closing.delimiter or opening.delimiter, text='', suffix='')
|
|
144
155
|
)
|
|
@@ -96,6 +96,19 @@ class Report:
|
|
|
96
96
|
def needing_author(self) -> tuple[Diagnostic, ...]:
|
|
97
97
|
return tuple(diagnostic for diagnostic in self.diagnostics if diagnostic.needs_author)
|
|
98
98
|
|
|
99
|
+
@property
|
|
100
|
+
def blocking(self) -> tuple[Diagnostic, ...]:
|
|
101
|
+
"""The findings an author must resolve, since a warning reaches them as advice and blocks nothing."""
|
|
102
|
+
return tuple(diagnostic for diagnostic in self.needing_author if diagnostic.severity is Severity.ERROR)
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def advisory(self) -> tuple[Diagnostic, ...]:
|
|
106
|
+
return tuple(diagnostic for diagnostic in self.needing_author if diagnostic.severity is not Severity.ERROR)
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def has_errors(self) -> bool:
|
|
110
|
+
return any(diagnostic.severity is Severity.ERROR for diagnostic in self.diagnostics)
|
|
111
|
+
|
|
99
112
|
@property
|
|
100
113
|
def is_clean(self) -> bool:
|
|
101
114
|
return not self.diagnostics
|
|
@@ -26,6 +26,7 @@ _FENCE = re.compile(r'^\s*(```|~~~)')
|
|
|
26
26
|
_LIST_DELIMITER = re.compile(r'^\s*(?:[0-9]+[.)]|[-*+]|[a-zA-Z][.)])\s')
|
|
27
27
|
_LITERAL_DELIMITER = '\\b'
|
|
28
28
|
_CLOSING_MARKS = '"\')]}'
|
|
29
|
+
_APOSTROPHES = ("'", '\u2019')
|
|
29
30
|
|
|
30
31
|
# Only before a number, since "No. 5" abbreviates while a trailing no ends its sentence.
|
|
31
32
|
NUMBER_ABBREVIATION = 'no'
|
|
@@ -161,6 +162,9 @@ class Prose:
|
|
|
161
162
|
if offset + 1 < len(text) and text[offset + 1].isdigit():
|
|
162
163
|
return False
|
|
163
164
|
trailing_word = _TRAILING_WORD.search(text[:offset])
|
|
165
|
+
# The s of a possessive is no initial, so a sentence closing on a word like API's still ends there.
|
|
166
|
+
if trailing_word and text[: trailing_word.start()].endswith(_APOSTROPHES):
|
|
167
|
+
return self._followed_by_break(text, offset)
|
|
164
168
|
word = trailing_word.group(1).lower().rstrip('.') if trailing_word else ''
|
|
165
169
|
if word in ABBREVIATIONS:
|
|
166
170
|
return False
|
|
@@ -19,6 +19,8 @@ class RuleSet:
|
|
|
19
19
|
enabled_rule_ids: frozenset[str]
|
|
20
20
|
settings: typing.Mapping[str, RuleSettings] = dataclasses.field(default_factory=dict)
|
|
21
21
|
line_width: int = 120
|
|
22
|
+
# A delegated tool's rules are not known up front, so they are listed when switched off instead.
|
|
23
|
+
disabled_external_ids: frozenset[str] = frozenset()
|
|
22
24
|
|
|
23
25
|
def is_enabled(self, rule_id: str) -> bool:
|
|
24
26
|
return rule_id in self.enabled_rule_ids
|
|
@@ -28,3 +30,6 @@ class RuleSet:
|
|
|
28
30
|
|
|
29
31
|
def severity_for(self, metadata: RuleMetadata) -> Severity:
|
|
30
32
|
return self.settings_for(metadata.rule_id).severity or metadata.default_severity
|
|
33
|
+
|
|
34
|
+
def allows_external(self, rule_id: str) -> bool:
|
|
35
|
+
return rule_id not in self.disabled_external_ids
|
|
@@ -12,3 +12,6 @@ class ExternalLinter(abc.ABC):
|
|
|
12
12
|
|
|
13
13
|
@abc.abstractmethod
|
|
14
14
|
def run(self, document: Document) -> tuple[Diagnostic, ...]: ...
|
|
15
|
+
|
|
16
|
+
def prepare(self) -> None: # noqa: B027, an optional hook rather than a required one
|
|
17
|
+
"""Fetch what the tool needs, for a caller that can afford to wait. Most tools need nothing."""
|
|
@@ -3,6 +3,7 @@ import typing
|
|
|
3
3
|
from ..models.comment import CommentGroup
|
|
4
4
|
from ..models.diagnostic import Diagnostic, Fix, FixKind, RuleMetadata
|
|
5
5
|
from ..ports.rule import Rule, RuleContext
|
|
6
|
+
from .rewrite import stub_floor
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
MID_CLAUSE_BREAK = RuleMetadata(
|
|
@@ -22,7 +23,7 @@ class MidClauseBreakRule(Rule):
|
|
|
22
23
|
metadata = MID_CLAUSE_BREAK
|
|
23
24
|
|
|
24
25
|
def check(self, group: CommentGroup, context: RuleContext) -> typing.Iterable[Diagnostic]:
|
|
25
|
-
reflowed = group.reflow(context.line_width)
|
|
26
|
+
reflowed = group.reflow(context.line_width, stub_floor(context))
|
|
26
27
|
if reflowed.render() == group.render():
|
|
27
28
|
return
|
|
28
29
|
yield Diagnostic(
|
|
@@ -43,7 +44,7 @@ class LineTooLongRule(Rule):
|
|
|
43
44
|
width = context.line_width
|
|
44
45
|
if group.longest_line <= width:
|
|
45
46
|
return
|
|
46
|
-
reflowed = group.reflow(width)
|
|
47
|
+
reflowed = group.reflow(width, stub_floor(context))
|
|
47
48
|
if reflowed.longest_line > width:
|
|
48
49
|
return
|
|
49
50
|
yield Diagnostic(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import typing
|
|
2
2
|
|
|
3
3
|
from ..models.comment import CommentGroup
|
|
4
|
-
from ..models.diagnostic import Diagnostic, Fix, FixKind, RuleMetadata
|
|
4
|
+
from ..models.diagnostic import Diagnostic, Fix, FixKind, RuleMetadata, Severity
|
|
5
5
|
from ..models.prose import Prose, reflow_sentence
|
|
6
6
|
from ..ports.rule import Rule, RuleContext
|
|
7
7
|
|
|
@@ -10,6 +10,7 @@ STUB_FRAGMENT = RuleMetadata(
|
|
|
10
10
|
rule_id='stub-fragment',
|
|
11
11
|
summary='A sentence forced to break must not leave a stub of a line behind.',
|
|
12
12
|
fix_kind=FixKind.REWRITE,
|
|
13
|
+
default_severity=Severity.WARNING,
|
|
13
14
|
)
|
|
14
15
|
|
|
15
16
|
UNBREAKABLE_SENTENCE = RuleMetadata(
|
|
@@ -35,14 +36,19 @@ def sentence_budget(group: CommentGroup, context: RuleContext) -> int:
|
|
|
35
36
|
return max(20, context.line_width - group.lines[0].prefix_width)
|
|
36
37
|
|
|
37
38
|
|
|
39
|
+
def stub_floor(context: RuleContext) -> int:
|
|
40
|
+
"""Reflow reads the same floor, so a fix never lays out a stub that this rule then rejects."""
|
|
41
|
+
if not context.rule_set.is_enabled(STUB_FRAGMENT.rule_id):
|
|
42
|
+
return 0
|
|
43
|
+
return context.settings(STUB_FRAGMENT.rule_id).integer('minimum_characters', DEFAULT_MINIMUM_CHARACTERS)
|
|
44
|
+
|
|
45
|
+
|
|
38
46
|
class StubFragmentRule(Rule):
|
|
39
47
|
metadata = STUB_FRAGMENT
|
|
40
48
|
|
|
41
49
|
def check(self, group: CommentGroup, context: RuleContext) -> typing.Iterable[Diagnostic]:
|
|
42
50
|
budget = sentence_budget(group, context)
|
|
43
|
-
minimum_length = context
|
|
44
|
-
'minimum_characters', DEFAULT_MINIMUM_CHARACTERS
|
|
45
|
-
)
|
|
51
|
+
minimum_length = stub_floor(context)
|
|
46
52
|
for sentence in prose_sentences(group):
|
|
47
53
|
split_pieces = reflow_sentence(sentence, budget)
|
|
48
54
|
if len(split_pieces) < 2:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import typing
|
|
2
2
|
|
|
3
3
|
from ..models.comment import CommentForm, CommentGroup, CommentPlacement
|
|
4
|
-
from ..models.diagnostic import Diagnostic, Fix, FixKind, RuleMetadata
|
|
4
|
+
from ..models.diagnostic import Diagnostic, Fix, FixKind, RuleMetadata, Severity
|
|
5
5
|
from ..ports.language_conventions import LanguageConventions
|
|
6
6
|
from ..ports.rule import Rule, RuleContext
|
|
7
7
|
|
|
@@ -10,6 +10,7 @@ FILE_HEADER_COMMENT = RuleMetadata(
|
|
|
10
10
|
rule_id='file-header-comment',
|
|
11
11
|
summary='A file must not open with a banner comment before its first declaration.',
|
|
12
12
|
fix_kind=FixKind.REWRITE,
|
|
13
|
+
default_severity=Severity.WARNING,
|
|
13
14
|
)
|
|
14
15
|
|
|
15
16
|
PLAIN_COMMENT_ON_PUBLIC = RuleMetadata(
|
|
@@ -104,6 +105,7 @@ BLOCK_TOO_LONG = RuleMetadata(
|
|
|
104
105
|
rule_id='block-too-long',
|
|
105
106
|
summary='A comment block long enough to restate the code has stopped earning its place.',
|
|
106
107
|
fix_kind=FixKind.REWRITE,
|
|
108
|
+
default_severity=Severity.WARNING,
|
|
107
109
|
)
|
|
108
110
|
|
|
109
111
|
|
{subedit-0.1.0 → subedit-0.1.2}/src/subedit/infrastructure/config_sources/toml_config_source.py
RENAMED
|
@@ -83,13 +83,16 @@ class TomlConfigSource:
|
|
|
83
83
|
|
|
84
84
|
def _to_rule_set(self, config_file: ConfigFile) -> RuleSet:
|
|
85
85
|
enabled_ids = set(self._available_rule_ids)
|
|
86
|
+
disabled_external_ids: set[str] = set()
|
|
86
87
|
settings: dict[str, RuleSettings] = {}
|
|
87
88
|
|
|
89
|
+
# A name subedit does not define belongs to a delegated tool, such as subedit.Semicolon from Vale.
|
|
88
90
|
for rule_id, rule_entry in config_file.rules.items():
|
|
89
|
-
if rule_id not in self._available_rule_ids:
|
|
90
|
-
continue
|
|
91
91
|
if rule_entry is False or rule_entry == 'off':
|
|
92
|
-
|
|
92
|
+
if rule_id in self._available_rule_ids:
|
|
93
|
+
enabled_ids.discard(rule_id)
|
|
94
|
+
else:
|
|
95
|
+
disabled_external_ids.add(rule_id)
|
|
93
96
|
elif isinstance(rule_entry, RuleTable):
|
|
94
97
|
settings[rule_id] = RuleSettings(severity=rule_entry.resolved_severity, options=rule_entry.options)
|
|
95
98
|
elif isinstance(rule_entry, str):
|
|
@@ -99,4 +102,5 @@ class TomlConfigSource:
|
|
|
99
102
|
enabled_rule_ids=frozenset(enabled_ids),
|
|
100
103
|
settings=settings,
|
|
101
104
|
line_width=config_file.subedit.line_width,
|
|
105
|
+
disabled_external_ids=frozenset(disabled_external_ids),
|
|
102
106
|
)
|