subedit 0.1.4__tar.gz → 0.2.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.
- {subedit-0.1.4 → subedit-0.2.0}/.vale.ini +1 -1
- {subedit-0.1.4 → subedit-0.2.0}/PKG-INFO +21 -7
- {subedit-0.1.4 → subedit-0.2.0}/README.md +18 -4
- {subedit-0.1.4 → subedit-0.2.0}/docs/rules.md +1 -1
- {subedit-0.1.4 → subedit-0.2.0}/plugins/subedit/.claude-plugin/plugin.json +1 -1
- {subedit-0.1.4 → subedit-0.2.0}/plugins/subedit/scripts/run-subedit-hook.sh +1 -1
- {subedit-0.1.4 → subedit-0.2.0}/plugins/subedit/skills/judging-findings/SKILL.md +3 -2
- {subedit-0.1.4 → subedit-0.2.0}/pyproject.toml +3 -3
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/__init__.py +1 -1
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/application/__init__.py +2 -1
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/application/linter.py +14 -2
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/application/report_aggregator.py +4 -2
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/domain/__init__.py +18 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/domain/models/__init__.py +28 -2
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/domain/models/comment.py +56 -76
- subedit-0.2.0/src/subedit/domain/models/comment_layout.py +130 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/domain/models/prose.py +104 -7
- subedit-0.2.0/src/subedit/domain/models/prose_map.py +41 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/domain/models/rule_set.py +5 -1
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/domain/rules/layout.py +13 -1
- subedit-0.2.0/src/subedit/domain/rules/rewrite.py +139 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/domain/rules/structure.py +26 -10
- subedit-0.2.0/src/subedit/infrastructure/__init__.py +75 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/infrastructure/config_sources/config_schema.py +2 -1
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/infrastructure/config_sources/toml_config_source.py +20 -9
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/infrastructure/linters/autocorrect_linter.py +13 -2
- subedit-0.2.0/src/subedit/infrastructure/linters/source_path.py +12 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/infrastructure/linters/vale_linter.py +21 -2
- subedit-0.2.0/src/subedit/infrastructure/parsers/__init__.py +24 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/infrastructure/parsers/languages/__init__.py +4 -0
- subedit-0.2.0/src/subedit/infrastructure/parsers/languages/language_profile.py +56 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/infrastructure/parsers/languages/python_profile.py +5 -1
- subedit-0.2.0/src/subedit/infrastructure/parsers/languages/typescript_profile.py +180 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/infrastructure/parsers/tree_sitter_parser.py +27 -11
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/presentation/cli.py +24 -4
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/presentation/harnesses/__init__.py +1 -3
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/presentation/hook.py +5 -2
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/vale/vale.ini +1 -1
- {subedit-0.1.4 → subedit-0.2.0}/tests/test_comment.py +9 -3
- {subedit-0.1.4 → subedit-0.2.0}/tests/test_fixer.py +2 -1
- {subedit-0.1.4 → subedit-0.2.0}/tests/test_linter.py +20 -0
- {subedit-0.1.4 → subedit-0.2.0}/tests/test_parser.py +25 -1
- {subedit-0.1.4 → subedit-0.2.0}/tests/test_parser_ocp.py +3 -1
- {subedit-0.1.4 → subedit-0.2.0}/tests/test_report_aggregator.py +49 -1
- {subedit-0.1.4 → subedit-0.2.0}/tests/test_rules_layout.py +53 -0
- {subedit-0.1.4 → subedit-0.2.0}/tests/test_rules_rewrite.py +23 -0
- {subedit-0.1.4 → subedit-0.2.0}/tests/test_rules_structure.py +5 -0
- {subedit-0.1.4 → subedit-0.2.0}/tests/test_schema.py +2 -2
- subedit-0.2.0/tests/test_typescript.py +282 -0
- {subedit-0.1.4 → subedit-0.2.0}/uv.lock +3 -3
- subedit-0.1.4/src/subedit/domain/rules/rewrite.py +0 -99
- subedit-0.1.4/src/subedit/infrastructure/__init__.py +0 -47
- subedit-0.1.4/src/subedit/infrastructure/parsers/__init__.py +0 -12
- subedit-0.1.4/src/subedit/infrastructure/parsers/languages/language_profile.py +0 -33
- {subedit-0.1.4 → subedit-0.2.0}/.claude/settings.json +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/.claude-plugin/marketplace.json +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/.github/dependabot.yml +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/.github/release.yml +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/.github/workflows/ci.yml +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/.github/workflows/pr.yml +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/.github/workflows/release.yml +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/.gitignore +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/LICENSE +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/examples/README.md +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/examples/probe.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/examples/subedit.toml +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/examples/walkthrough.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/plugins/subedit/hooks/hooks.json +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/application/corpus_meter.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/application/fixer.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/domain/models/diagnostic.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/domain/models/document.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/domain/models/position.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/domain/models/text.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/domain/ports/__init__.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/domain/ports/external_linter.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/domain/ports/language_conventions.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/domain/ports/rule.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/domain/ports/source_parser.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/domain/rules/__init__.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/infrastructure/config_sources/__init__.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/infrastructure/linters/__init__.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/presentation/__init__.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/presentation/harnesses/agent_harness.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/presentation/harnesses/claude_code_harness.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/presentation/harnesses/codex_harness.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/presentation/report.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/presentation/rule_reference.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/vale/styles/subedit/DateRef.yml +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/vale/styles/subedit/EditHistory.yml +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/vale/styles/subedit/Ellipsis.yml +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/vale/styles/subedit/EmDash.yml +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/vale/styles/subedit/ExternalRef.yml +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/vale/styles/subedit/MidSentenceColon.yml +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/vale/styles/subedit/MidSentenceColonMarkup.yml +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/vale/styles/subedit/Semicolon.yml +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/src/subedit/vale/styles/subedit/SignatureTag.yml +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/subedit.toml +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/tests/fixtures/autocorrect/unspaced.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/tests/fixtures/vale/clean.md +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/tests/fixtures/vale/violations.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/tests/test_cli.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/tests/test_config.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/tests/test_corpus_meter.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/tests/test_diagnostic.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/tests/test_harnesses.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/tests/test_hook.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/tests/test_parser_oracle.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/tests/test_plugin.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/tests/test_position.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/tests/test_prose.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/tests/test_self_hosting.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/tests/test_text.py +0 -0
- {subedit-0.1.4 → subedit-0.2.0}/tests/test_vale_style.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: subedit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
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
|
|
@@ -8,7 +8,7 @@ Project-URL: Issues, https://github.com/mroops0111/subedit/issues
|
|
|
8
8
|
Author: Yun-Tai Yang
|
|
9
9
|
License: MIT
|
|
10
10
|
License-File: LICENSE
|
|
11
|
-
Keywords: comments,formatter,linter,lsp,prose,tree-sitter
|
|
11
|
+
Keywords: comments,formatter,linter,lsp,prose,python,tree-sitter,typescript
|
|
12
12
|
Classifier: Development Status :: 1 - Planning
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
14
14
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -21,13 +21,13 @@ Requires-Python: >=3.11
|
|
|
21
21
|
Requires-Dist: autocorrect-py>=2.14.0
|
|
22
22
|
Requires-Dist: pydantic>=2.13.5
|
|
23
23
|
Requires-Dist: tree-sitter-python>=0.25.0
|
|
24
|
+
Requires-Dist: tree-sitter-typescript>=0.23.2
|
|
24
25
|
Requires-Dist: tree-sitter>=0.26.0
|
|
25
26
|
Requires-Dist: typer>=0.27.2
|
|
26
27
|
Provides-Extra: dev
|
|
27
28
|
Requires-Dist: basedpyright>=1.40.1; extra == 'dev'
|
|
28
29
|
Requires-Dist: pytest>=9.0.0; extra == 'dev'
|
|
29
30
|
Requires-Dist: ruff>=0.16.7; extra == 'dev'
|
|
30
|
-
Requires-Dist: tree-sitter-typescript>=0.23.2; extra == 'dev'
|
|
31
31
|
Provides-Extra: vale
|
|
32
32
|
Requires-Dist: vale>=3.22.0.0; extra == 'vale'
|
|
33
33
|
Description-Content-Type: text/markdown
|
|
@@ -149,13 +149,25 @@ Add this to `~/.codex/hooks.json`.
|
|
|
149
149
|
|
|
150
150
|
Registering the hook with both agents is the normal case. Each agent names its edit tools differently, so every payload identifies its sender, and a payload from anything else exits 0 untouched. `subedit-hook --help` prints the payload shapes it accepts.
|
|
151
151
|
|
|
152
|
+
## Languages
|
|
153
|
+
|
|
154
|
+
| Language | Files | Doc comment | Default width |
|
|
155
|
+
| --- | --- | --- | --- |
|
|
156
|
+
| Python | `.py`, `.pyi` | `"""` docstring | 120 |
|
|
157
|
+
| TypeScript | `.ts`, `.mts`, `.cts`, `.tsx` | `/** */` JSDoc | 80, as Prettier sets it |
|
|
158
|
+
|
|
159
|
+
A TypeScript symbol is public when it is exported, and a class member stays public unless it is `private`, `protected`, or `#named`.
|
|
160
|
+
A JSDoc block keeps its frame when reflowed. Its tag section, from the first tag such as `@throws` onward, is left as written.
|
|
161
|
+
A plain `/* */` block and a directive such as `// eslint-disable-next-line` carry no prose for subedit to lay out, so only Vale reads the block, and nothing reads the directive.
|
|
162
|
+
A directory walk skips dotted directories such as `.venv`, and the package directories each language installs into, such as `node_modules` or `venv`.
|
|
163
|
+
|
|
152
164
|
## Configuration
|
|
153
165
|
|
|
154
166
|
Settings come from `subedit.toml`, or from `[tool.subedit]` in `pyproject.toml`, whichever sits closest above the file being checked.
|
|
155
167
|
|
|
156
168
|
```toml
|
|
157
169
|
[subedit]
|
|
158
|
-
line-width =
|
|
170
|
+
line-width = 100
|
|
159
171
|
exclude = ["tests/fixtures/**"]
|
|
160
172
|
|
|
161
173
|
[rules]
|
|
@@ -163,6 +175,8 @@ mid-clause-break = "error"
|
|
|
163
175
|
block-too-long = { severity = "warning", line = 3, doc-public = 20 }
|
|
164
176
|
```
|
|
165
177
|
|
|
178
|
+
Left unset, `line-width` takes the default of each file's language.
|
|
179
|
+
|
|
166
180
|
Rule names describe the problem they report. `docs/rules.md` lists every rule, generated from the rules themselves.
|
|
167
181
|
|
|
168
182
|
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.
|
|
@@ -194,12 +208,12 @@ Code comments are the first surface, and Markdown comes next.
|
|
|
194
208
|
|
|
195
209
|
## Related Tools
|
|
196
210
|
|
|
197
|
-
`subedit` covers the one thing the usual
|
|
211
|
+
`subedit` covers the one thing the usual linters and type checkers leave alone.
|
|
198
212
|
|
|
199
213
|
| Tool | Checks |
|
|
200
214
|
| --- | --- |
|
|
201
|
-
| ruff | code structure |
|
|
202
|
-
| basedpyright | types |
|
|
215
|
+
| ruff, ESLint | code structure |
|
|
216
|
+
| basedpyright, tsc | types |
|
|
203
217
|
| **subedit** | **the natural language inside source** |
|
|
204
218
|
|
|
205
219
|
## License
|
|
@@ -115,13 +115,25 @@ Add this to `~/.codex/hooks.json`.
|
|
|
115
115
|
|
|
116
116
|
Registering the hook with both agents is the normal case. Each agent names its edit tools differently, so every payload identifies its sender, and a payload from anything else exits 0 untouched. `subedit-hook --help` prints the payload shapes it accepts.
|
|
117
117
|
|
|
118
|
+
## Languages
|
|
119
|
+
|
|
120
|
+
| Language | Files | Doc comment | Default width |
|
|
121
|
+
| --- | --- | --- | --- |
|
|
122
|
+
| Python | `.py`, `.pyi` | `"""` docstring | 120 |
|
|
123
|
+
| TypeScript | `.ts`, `.mts`, `.cts`, `.tsx` | `/** */` JSDoc | 80, as Prettier sets it |
|
|
124
|
+
|
|
125
|
+
A TypeScript symbol is public when it is exported, and a class member stays public unless it is `private`, `protected`, or `#named`.
|
|
126
|
+
A JSDoc block keeps its frame when reflowed. Its tag section, from the first tag such as `@throws` onward, is left as written.
|
|
127
|
+
A plain `/* */` block and a directive such as `// eslint-disable-next-line` carry no prose for subedit to lay out, so only Vale reads the block, and nothing reads the directive.
|
|
128
|
+
A directory walk skips dotted directories such as `.venv`, and the package directories each language installs into, such as `node_modules` or `venv`.
|
|
129
|
+
|
|
118
130
|
## Configuration
|
|
119
131
|
|
|
120
132
|
Settings come from `subedit.toml`, or from `[tool.subedit]` in `pyproject.toml`, whichever sits closest above the file being checked.
|
|
121
133
|
|
|
122
134
|
```toml
|
|
123
135
|
[subedit]
|
|
124
|
-
line-width =
|
|
136
|
+
line-width = 100
|
|
125
137
|
exclude = ["tests/fixtures/**"]
|
|
126
138
|
|
|
127
139
|
[rules]
|
|
@@ -129,6 +141,8 @@ mid-clause-break = "error"
|
|
|
129
141
|
block-too-long = { severity = "warning", line = 3, doc-public = 20 }
|
|
130
142
|
```
|
|
131
143
|
|
|
144
|
+
Left unset, `line-width` takes the default of each file's language.
|
|
145
|
+
|
|
132
146
|
Rule names describe the problem they report. `docs/rules.md` lists every rule, generated from the rules themselves.
|
|
133
147
|
|
|
134
148
|
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.
|
|
@@ -160,12 +174,12 @@ Code comments are the first surface, and Markdown comes next.
|
|
|
160
174
|
|
|
161
175
|
## Related Tools
|
|
162
176
|
|
|
163
|
-
`subedit` covers the one thing the usual
|
|
177
|
+
`subedit` covers the one thing the usual linters and type checkers leave alone.
|
|
164
178
|
|
|
165
179
|
| Tool | Checks |
|
|
166
180
|
| --- | --- |
|
|
167
|
-
| ruff | code structure |
|
|
168
|
-
| basedpyright | types |
|
|
181
|
+
| ruff, ESLint | code structure |
|
|
182
|
+
| basedpyright, tsc | types |
|
|
169
183
|
| **subedit** | **the natural language inside source** |
|
|
170
184
|
|
|
171
185
|
## License
|
|
@@ -15,4 +15,4 @@ The level says whether a rewrite blocks. An error fails check and stops an agent
|
|
|
15
15
|
| `plain-comment-on-public` | rewrite | error | A public symbol must be documented in the doc comment form its language renders. |
|
|
16
16
|
| `signature-restating-tag` | rewrite | error | A doc comment must not restate what the type signature already carries. |
|
|
17
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 clause comma wherever it has to break. |
|
|
18
|
+
| `unbreakable-sentence` | rewrite | error | A sentence over the width must carry a clause comma, splitting no list, wherever it has to break, and a summary its style guide keeps to one line must fit it. |
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# because the first run installs subedit and downloads Vale, which no edit should wait on.
|
|
5
5
|
set -uo pipefail
|
|
6
6
|
|
|
7
|
-
SPEC='subedit[vale]>=0.
|
|
7
|
+
SPEC='subedit[vale]>=0.2.0'
|
|
8
8
|
# A bare uvx takes the first Python it finds, which on a stock Mac is too old for subedit.
|
|
9
9
|
PYTHON='>=3.11'
|
|
10
10
|
|
|
@@ -36,8 +36,9 @@ A warning passes when the text meets its criterion. Leave such text exactly as i
|
|
|
36
36
|
|
|
37
37
|
An error always needs a rewrite. The message states the fix, and these notes cover what it leaves out.
|
|
38
38
|
|
|
39
|
-
- `unbreakable-sentence`: a line can break only at a comma that ends a clause, and a comma inside brackets or a code span does not count. Add a comma at a clause boundary, or split the sentence in two.
|
|
40
|
-
- `
|
|
39
|
+
- `unbreakable-sentence`: a line can break only at a comma that ends a clause, and a comma inside brackets or a code span does not count. Add a comma at a clause boundary, or split the sentence in two. When every such comma would split a list, move the list to the end of the sentence. When the message names a style guide that keeps a summary to one line, shorten the summary and move the detail into the body.
|
|
40
|
+
- `line-too-long` (trailing comment): a comment beside code cannot break, so shorten it or move it to its own line above the code.
|
|
41
|
+
- `plain-comment-on-public`: move the text into the doc comment, a docstring in Python or a `/** */` block in TypeScript.
|
|
41
42
|
- `signature-restating-tag` and `subedit.SignatureTag`: delete the tag, since the signature already says it.
|
|
42
43
|
- `subedit.EditHistory`: state the current behavior, not its history.
|
|
43
44
|
- `subedit.Semicolon`, `subedit.EmDash`: split into two sentences, or use a comma or parentheses.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "subedit"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "A linter and formatter for the prose inside code comments, across languages."
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "Yun-Tai Yang"},
|
|
@@ -8,7 +8,7 @@ authors = [
|
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
license = {text = "MIT"}
|
|
10
10
|
requires-python = ">=3.11"
|
|
11
|
-
keywords = ["linter", "formatter", "comments", "prose", "lsp", "tree-sitter"]
|
|
11
|
+
keywords = ["linter", "formatter", "comments", "prose", "lsp", "tree-sitter", "python", "typescript"]
|
|
12
12
|
classifiers = [
|
|
13
13
|
"Development Status :: 1 - Planning",
|
|
14
14
|
"Intended Audience :: Developers",
|
|
@@ -25,6 +25,7 @@ dependencies = [
|
|
|
25
25
|
"pydantic>=2.13.5",
|
|
26
26
|
"tree-sitter>=0.26.0",
|
|
27
27
|
"tree-sitter-python>=0.25.0",
|
|
28
|
+
"tree-sitter-typescript>=0.23.2",
|
|
28
29
|
"typer>=0.27.2",
|
|
29
30
|
]
|
|
30
31
|
|
|
@@ -33,7 +34,6 @@ dev = [
|
|
|
33
34
|
"pytest>=9.0.0",
|
|
34
35
|
"basedpyright>=1.40.1",
|
|
35
36
|
"ruff>=0.16.7",
|
|
36
|
-
"tree-sitter-typescript>=0.23.2",
|
|
37
37
|
]
|
|
38
38
|
vale = [
|
|
39
39
|
"vale>=3.22.0.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from .corpus_meter import CorpusMeter, CorpusStatistics, Distribution
|
|
2
2
|
from .fixer import Fixer, FixResult
|
|
3
|
-
from .linter import Linter, RuleEngine
|
|
3
|
+
from .linter import Inspection, Linter, RuleEngine
|
|
4
4
|
from .report_aggregator import ReportAggregator
|
|
5
5
|
|
|
6
6
|
|
|
@@ -10,6 +10,7 @@ __all__ = [
|
|
|
10
10
|
'Distribution',
|
|
11
11
|
'FixResult',
|
|
12
12
|
'Fixer',
|
|
13
|
+
'Inspection',
|
|
13
14
|
'Linter',
|
|
14
15
|
'ReportAggregator',
|
|
15
16
|
'RuleEngine',
|
|
@@ -3,6 +3,7 @@ import dataclasses
|
|
|
3
3
|
from ..domain.models.comment import CommentGroup
|
|
4
4
|
from ..domain.models.diagnostic import Diagnostic, Report
|
|
5
5
|
from ..domain.models.document import Document
|
|
6
|
+
from ..domain.models.prose_map import ProseMap
|
|
6
7
|
from ..domain.models.rule_set import RuleSet
|
|
7
8
|
from ..domain.ports import SourceParser
|
|
8
9
|
from ..domain.ports.rule import Rule, RuleContext
|
|
@@ -41,15 +42,26 @@ class RuleEngine:
|
|
|
41
42
|
return duplicate_ids
|
|
42
43
|
|
|
43
44
|
|
|
45
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
46
|
+
class Inspection:
|
|
47
|
+
"""What one parse of a document yields, the findings and the map of where its prose lies."""
|
|
48
|
+
|
|
49
|
+
report: Report = dataclasses.field(default_factory=Report)
|
|
50
|
+
prose_map: ProseMap = dataclasses.field(default_factory=ProseMap)
|
|
51
|
+
|
|
52
|
+
|
|
44
53
|
class Linter:
|
|
45
54
|
def __init__(self, parser: SourceParser, engine: RuleEngine) -> None:
|
|
46
55
|
self._parser = parser
|
|
47
56
|
self._engine = engine
|
|
48
57
|
|
|
49
58
|
def run(self, document: Document, rule_set: RuleSet) -> Report:
|
|
59
|
+
return self.inspect(document, rule_set).report
|
|
60
|
+
|
|
61
|
+
def inspect(self, document: Document, rule_set: RuleSet) -> Inspection:
|
|
50
62
|
if not self._parser.supports(document.language_id):
|
|
51
|
-
return
|
|
63
|
+
return Inspection()
|
|
52
64
|
groups = self._parser.parse(document)
|
|
53
65
|
diagnostics = self._engine.run(document, groups, rule_set)
|
|
54
66
|
sorted_diagnostics = sorted(diagnostics, key=lambda item: (item.range.start, item.rule_id))
|
|
55
|
-
return Report(tuple(sorted_diagnostics))
|
|
67
|
+
return Inspection(Report(tuple(sorted_diagnostics)), ProseMap.of_comments(groups))
|
|
@@ -13,13 +13,15 @@ class ReportAggregator:
|
|
|
13
13
|
self._external_linters = external_linters
|
|
14
14
|
|
|
15
15
|
def run(self, document: Document, rule_set: RuleSet) -> Report:
|
|
16
|
-
|
|
16
|
+
inspection = self._linter.inspect(document, rule_set)
|
|
17
|
+
report = inspection.report
|
|
17
18
|
missing_sources: list[str] = []
|
|
18
19
|
for external_linter in self._external_linters:
|
|
19
20
|
if not external_linter.is_available():
|
|
20
21
|
missing_sources.append(external_linter.name)
|
|
21
22
|
continue
|
|
22
|
-
|
|
23
|
+
found = tuple(item for item in external_linter.run(document) if inspection.prose_map.admits(item))
|
|
24
|
+
report = report.merged_with(Report(self._configured(found, rule_set)))
|
|
23
25
|
if missing_sources:
|
|
24
26
|
report = report.merged_with(Report(unavailable_sources=tuple(missing_sources)))
|
|
25
27
|
return report
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
from .models import (
|
|
2
|
+
LINE_LAYOUT,
|
|
3
|
+
PLAIN_MARKUP,
|
|
4
|
+
QUOTED_DOC_LAYOUT,
|
|
2
5
|
SEVERITY_BY_NAME,
|
|
3
6
|
CommentForm,
|
|
4
7
|
CommentGroup,
|
|
8
|
+
CommentLayout,
|
|
5
9
|
CommentLine,
|
|
6
10
|
CommentPlacement,
|
|
7
11
|
Diagnostic,
|
|
@@ -9,9 +13,14 @@ from .models import (
|
|
|
9
13
|
Encoding,
|
|
10
14
|
Fix,
|
|
11
15
|
FixKind,
|
|
16
|
+
FramedDocLayout,
|
|
17
|
+
LinePrefixLayout,
|
|
12
18
|
Position,
|
|
13
19
|
PositionMap,
|
|
14
20
|
Prose,
|
|
21
|
+
ProseMap,
|
|
22
|
+
ProseMarkup,
|
|
23
|
+
QuotedDocLayout,
|
|
15
24
|
Report,
|
|
16
25
|
RuleMetadata,
|
|
17
26
|
RuleSet,
|
|
@@ -29,9 +38,13 @@ from .ports import DelimiterSplit, ExternalLinter, LanguageConventions, Rule, Ru
|
|
|
29
38
|
|
|
30
39
|
|
|
31
40
|
__all__ = [
|
|
41
|
+
'LINE_LAYOUT',
|
|
42
|
+
'PLAIN_MARKUP',
|
|
43
|
+
'QUOTED_DOC_LAYOUT',
|
|
32
44
|
'SEVERITY_BY_NAME',
|
|
33
45
|
'CommentForm',
|
|
34
46
|
'CommentGroup',
|
|
47
|
+
'CommentLayout',
|
|
35
48
|
'CommentLine',
|
|
36
49
|
'CommentPlacement',
|
|
37
50
|
'DelimiterSplit',
|
|
@@ -41,10 +54,15 @@ __all__ = [
|
|
|
41
54
|
'ExternalLinter',
|
|
42
55
|
'Fix',
|
|
43
56
|
'FixKind',
|
|
57
|
+
'FramedDocLayout',
|
|
44
58
|
'LanguageConventions',
|
|
59
|
+
'LinePrefixLayout',
|
|
45
60
|
'Position',
|
|
46
61
|
'PositionMap',
|
|
47
62
|
'Prose',
|
|
63
|
+
'ProseMap',
|
|
64
|
+
'ProseMarkup',
|
|
65
|
+
'QuotedDocLayout',
|
|
48
66
|
'Report',
|
|
49
67
|
'Rule',
|
|
50
68
|
'RuleContext',
|
|
@@ -1,16 +1,37 @@
|
|
|
1
|
-
from .comment import
|
|
1
|
+
from .comment import (
|
|
2
|
+
CommentForm,
|
|
3
|
+
CommentGroup,
|
|
4
|
+
CommentLine,
|
|
5
|
+
CommentPlacement,
|
|
6
|
+
SymbolKind,
|
|
7
|
+
SymbolRef,
|
|
8
|
+
Visibility,
|
|
9
|
+
)
|
|
10
|
+
from .comment_layout import (
|
|
11
|
+
LINE_LAYOUT,
|
|
12
|
+
QUOTED_DOC_LAYOUT,
|
|
13
|
+
CommentLayout,
|
|
14
|
+
FramedDocLayout,
|
|
15
|
+
LinePrefixLayout,
|
|
16
|
+
QuotedDocLayout,
|
|
17
|
+
)
|
|
2
18
|
from .diagnostic import SEVERITY_BY_NAME, Diagnostic, Fix, FixKind, Report, RuleMetadata, Severity
|
|
3
19
|
from .document import Document
|
|
4
20
|
from .position import Encoding, Position, PositionMap, SourceRange
|
|
5
|
-
from .prose import Prose, Sentence
|
|
21
|
+
from .prose import PLAIN_MARKUP, Prose, ProseMarkup, Sentence
|
|
22
|
+
from .prose_map import ProseMap
|
|
6
23
|
from .rule_set import RuleSet, RuleSettings
|
|
7
24
|
from .text import TextEdit, apply_edits
|
|
8
25
|
|
|
9
26
|
|
|
10
27
|
__all__ = [
|
|
28
|
+
'LINE_LAYOUT',
|
|
29
|
+
'PLAIN_MARKUP',
|
|
30
|
+
'QUOTED_DOC_LAYOUT',
|
|
11
31
|
'SEVERITY_BY_NAME',
|
|
12
32
|
'CommentForm',
|
|
13
33
|
'CommentGroup',
|
|
34
|
+
'CommentLayout',
|
|
14
35
|
'CommentLine',
|
|
15
36
|
'CommentPlacement',
|
|
16
37
|
'Diagnostic',
|
|
@@ -18,9 +39,14 @@ __all__ = [
|
|
|
18
39
|
'Encoding',
|
|
19
40
|
'Fix',
|
|
20
41
|
'FixKind',
|
|
42
|
+
'FramedDocLayout',
|
|
43
|
+
'LinePrefixLayout',
|
|
21
44
|
'Position',
|
|
22
45
|
'PositionMap',
|
|
23
46
|
'Prose',
|
|
47
|
+
'ProseMap',
|
|
48
|
+
'ProseMarkup',
|
|
49
|
+
'QuotedDocLayout',
|
|
24
50
|
'Report',
|
|
25
51
|
'RuleMetadata',
|
|
26
52
|
'RuleSet',
|
|
@@ -2,8 +2,9 @@ import dataclasses
|
|
|
2
2
|
import enum
|
|
3
3
|
import itertools
|
|
4
4
|
|
|
5
|
+
from .comment_layout import LINE_LAYOUT, CommentLayout, CommentLine
|
|
5
6
|
from .position import SourceRange
|
|
6
|
-
from .prose import Prose, Sentence, break_cost, comma_breaks, reflow_sentence
|
|
7
|
+
from .prose import PLAIN_MARKUP, Prose, ProseMarkup, Sentence, break_cost, comma_breaks, reflow_sentence
|
|
7
8
|
from .text import TextEdit
|
|
8
9
|
|
|
9
10
|
|
|
@@ -38,28 +39,6 @@ class SymbolRef:
|
|
|
38
39
|
visibility: Visibility
|
|
39
40
|
|
|
40
41
|
|
|
41
|
-
@dataclasses.dataclass(frozen=True, slots=True)
|
|
42
|
-
class CommentLine:
|
|
43
|
-
range: SourceRange
|
|
44
|
-
indent: str
|
|
45
|
-
delimiter: str
|
|
46
|
-
text: str
|
|
47
|
-
suffix: str = ''
|
|
48
|
-
|
|
49
|
-
@property
|
|
50
|
-
def physical_width(self) -> int:
|
|
51
|
-
return len(self.indent) + len(self.delimiter) + len(self.text) + len(self.suffix)
|
|
52
|
-
|
|
53
|
-
@property
|
|
54
|
-
def prefix_width(self) -> int:
|
|
55
|
-
return len(self.indent) + len(self.delimiter)
|
|
56
|
-
|
|
57
|
-
def render(self) -> str:
|
|
58
|
-
if not self.text and not self.suffix:
|
|
59
|
-
return (self.indent + self.delimiter).rstrip()
|
|
60
|
-
return self.indent + self.delimiter + self.text + self.suffix
|
|
61
|
-
|
|
62
|
-
|
|
63
42
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
64
43
|
class CommentGroup:
|
|
65
44
|
range: SourceRange
|
|
@@ -67,6 +46,8 @@ class CommentGroup:
|
|
|
67
46
|
form: CommentForm
|
|
68
47
|
placement: CommentPlacement
|
|
69
48
|
attachment: SymbolRef | None = None
|
|
49
|
+
layout: CommentLayout = LINE_LAYOUT
|
|
50
|
+
markup: ProseMarkup = PLAIN_MARKUP
|
|
70
51
|
|
|
71
52
|
def __post_init__(self) -> None:
|
|
72
53
|
if not self.lines:
|
|
@@ -85,6 +66,11 @@ class CommentGroup:
|
|
|
85
66
|
def longest_line(self) -> int:
|
|
86
67
|
return max(line.physical_width for line in self.lines)
|
|
87
68
|
|
|
69
|
+
@property
|
|
70
|
+
def has_summary_line(self) -> bool:
|
|
71
|
+
"""Whether the first sentence is a one-line summary, as a style guide such as PEP 257 asks."""
|
|
72
|
+
return self.layout.summary_convention is not None
|
|
73
|
+
|
|
88
74
|
@property
|
|
89
75
|
def attaches_to_public_symbol(self) -> bool:
|
|
90
76
|
return self.attachment is not None and self.attachment.visibility is Visibility.PUBLIC
|
|
@@ -96,51 +82,72 @@ class CommentGroup:
|
|
|
96
82
|
' ' * max(0, len(line.indent) - base) + line.text.rstrip() if line.text.strip() else ''
|
|
97
83
|
for line in self.lines
|
|
98
84
|
]
|
|
99
|
-
return Prose('\n'.join(rendered_lines))
|
|
85
|
+
return Prose('\n'.join(rendered_lines), self.markup)
|
|
86
|
+
|
|
87
|
+
def code_ranges(self) -> tuple[SourceRange, ...]:
|
|
88
|
+
"""Where the lines holding code sit, which a prose checker has no business reading."""
|
|
89
|
+
return tuple(self.lines[index].range for index in sorted(self.prose().code_line_indexes()))
|
|
100
90
|
|
|
101
91
|
def reflow(self, width: int, stub_floor: int = 0) -> 'CommentGroup':
|
|
92
|
+
# A trailing comment shares its line with code, so a second line would have no code to sit beside.
|
|
93
|
+
if self.placement is CommentPlacement.TRAILING:
|
|
94
|
+
return self
|
|
102
95
|
# A one line docstring that fits is the summary convention, not a block to lay out.
|
|
103
96
|
if self.form is CommentForm.DOC and self.line_count == 1 and self.longest_line <= width:
|
|
104
97
|
return self
|
|
105
98
|
texts = self._reflow_into_texts(width, stub_floor)
|
|
106
99
|
# Text that ends up on one line closes there too, so the closing quotes need room beside it.
|
|
107
|
-
closing_width = len(self.
|
|
100
|
+
closing_width = len(self.closing())
|
|
108
101
|
if len(texts) == 1 and len(texts[0]) > self.text_budget(width, first_line=True) - closing_width:
|
|
109
102
|
texts = self._reflow_into_texts(width, stub_floor, closing_width)
|
|
110
103
|
if not texts:
|
|
111
104
|
return self
|
|
112
|
-
return self.
|
|
105
|
+
return dataclasses.replace(self, lines=self.layout.rebuild(self.lines, texts, width))
|
|
106
|
+
|
|
107
|
+
def sentences_held_by_a_list(self, width: int, stub_floor: int = 0) -> tuple[str, ...]:
|
|
108
|
+
"""The sentences reflow leaves as the author broke them, since every legal break would split a list."""
|
|
109
|
+
held: list[str] = []
|
|
110
|
+
self._reflow_into_texts(width, stub_floor, held=held)
|
|
111
|
+
return tuple(held)
|
|
113
112
|
|
|
114
113
|
def text_budget(self, width: int, *, first_line: bool) -> int:
|
|
115
114
|
"""The characters of text a line holds, which on a docstring's opening line also cede the quotes."""
|
|
116
|
-
|
|
117
|
-
quoted = first_line or self.form is not CommentForm.DOC
|
|
118
|
-
return max(20, width - (opening.prefix_width if quoted else len(opening.indent)))
|
|
115
|
+
return self.layout.text_budget(self.lines, width, first_line=first_line)
|
|
119
116
|
|
|
120
117
|
# A sentence takes as few lines as fit, and the author's layout stays when it breaks no worse than the best one.
|
|
121
|
-
# A sentence whose new layout would strand a stub or still overflow needs its author,
|
|
118
|
+
# A sentence whose new layout would strand a stub, split a list, or still overflow needs its author,
|
|
122
119
|
# so it keeps the layout they gave it rather than one its own check rejects.
|
|
123
|
-
|
|
120
|
+
# A summary never breaks where the comment's style guide keeps it to one line.
|
|
121
|
+
def _reflow_into_texts(
|
|
122
|
+
self, width: int, stub_floor: int, reserve: int = 0, held: list[str] | None = None
|
|
123
|
+
) -> tuple[str, ...]:
|
|
124
124
|
body_budget = self.text_budget(width, first_line=False)
|
|
125
125
|
out: list[str] = []
|
|
126
|
-
for paragraph_index, (paragraph, is_literal) in enumerate(self._split_into_paragraphs()):
|
|
127
|
-
if out:
|
|
126
|
+
for paragraph_index, (paragraph, is_literal, follows_blank) in enumerate(self._split_into_paragraphs()):
|
|
127
|
+
if out and (follows_blank or self.layout.parts_every_paragraph):
|
|
128
128
|
out.append('')
|
|
129
129
|
if is_literal:
|
|
130
130
|
out.extend(paragraph)
|
|
131
131
|
continue
|
|
132
132
|
lines = tuple(line.strip() for line in paragraph if line.strip())
|
|
133
133
|
for sentence_index, sentence in enumerate(Prose(' '.join(lines)).sentences()):
|
|
134
|
-
if sentence_index == 1 and paragraph_index == 0 and self.
|
|
134
|
+
if sentence_index == 1 and paragraph_index == 0 and self.has_summary_line:
|
|
135
135
|
out.append('')
|
|
136
136
|
first_budget = self.text_budget(width, first_line=not out) - (reserve if not out else 0)
|
|
137
|
-
|
|
137
|
+
if self.has_summary_line and paragraph_index == 0 and sentence_index == 0:
|
|
138
|
+
best_lines: tuple[str, ...] = (sentence.text,)
|
|
139
|
+
else:
|
|
140
|
+
best_lines = reflow_sentence(sentence.text, body_budget, stub_floor, first_width=first_budget)
|
|
138
141
|
given_lines = _given_lines(lines, sentence)
|
|
139
142
|
if given_lines and _keeps_given(given_lines, best_lines, (first_budget, body_budget), stub_floor):
|
|
140
143
|
out.extend(given_lines)
|
|
141
|
-
elif _is_legal(best_lines, (first_budget, body_budget), stub_floor)
|
|
144
|
+
elif _is_legal(best_lines, (first_budget, body_budget), stub_floor) and not (
|
|
145
|
+
given_lines and break_cost(best_lines)
|
|
146
|
+
):
|
|
142
147
|
out.extend(best_lines)
|
|
143
148
|
elif given_lines:
|
|
149
|
+
if held is not None and _is_legal(best_lines, (first_budget, body_budget), stub_floor):
|
|
150
|
+
held.append(sentence.text)
|
|
144
151
|
out.extend(given_lines)
|
|
145
152
|
elif _strands_stub(best_lines, stub_floor):
|
|
146
153
|
return ()
|
|
@@ -148,55 +155,28 @@ class CommentGroup:
|
|
|
148
155
|
out.extend(best_lines)
|
|
149
156
|
return tuple(out)
|
|
150
157
|
|
|
151
|
-
def _split_into_paragraphs(self) -> tuple[tuple[tuple[str, ...], bool], ...]:
|
|
152
|
-
|
|
158
|
+
def _split_into_paragraphs(self) -> tuple[tuple[tuple[str, ...], bool, bool], ...]:
|
|
159
|
+
"""Each paragraph's lines, whether they are literal, and whether a blank line comes before them."""
|
|
160
|
+
paragraphs: list[tuple[tuple[str, ...], bool, bool]] = []
|
|
161
|
+
follows_blank = False
|
|
153
162
|
for segment in self.prose().segments():
|
|
154
163
|
current_lines: list[str] = []
|
|
155
164
|
for line in segment.lines:
|
|
156
165
|
if line.strip():
|
|
157
166
|
current_lines.append(line)
|
|
158
|
-
|
|
159
|
-
|
|
167
|
+
continue
|
|
168
|
+
if current_lines:
|
|
169
|
+
paragraphs.append((tuple(current_lines), segment.is_literal, follows_blank))
|
|
160
170
|
current_lines = []
|
|
171
|
+
follows_blank = True
|
|
161
172
|
if current_lines:
|
|
162
|
-
paragraphs.append((tuple(current_lines), segment.is_literal))
|
|
173
|
+
paragraphs.append((tuple(current_lines), segment.is_literal, follows_blank))
|
|
174
|
+
follows_blank = False
|
|
163
175
|
return tuple(paragraphs)
|
|
164
176
|
|
|
165
|
-
def
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
opening, closing = self.lines[0], self.lines[-1]
|
|
169
|
-
closes_alone = len(self.lines) > 1 and not closing.text.strip()
|
|
170
|
-
body = [dataclasses.replace(opening, delimiter='', suffix='', text=text, range=opening.range) for text in texts]
|
|
171
|
-
body[0] = dataclasses.replace(body[0], delimiter=opening.delimiter)
|
|
172
|
-
# Text that fits one line closes on that line, as ruff and black format it.
|
|
173
|
-
if len(texts) > 1:
|
|
174
|
-
body.append(
|
|
175
|
-
dataclasses.replace(closing, delimiter=closing.delimiter or opening.delimiter, text='', suffix='')
|
|
176
|
-
)
|
|
177
|
-
if not closes_alone:
|
|
178
|
-
body[-1] = dataclasses.replace(body[-1], delimiter=opening.delimiter.strip())
|
|
179
|
-
else:
|
|
180
|
-
body[0] = dataclasses.replace(body[0], suffix=self._closing_quotes())
|
|
181
|
-
return dataclasses.replace(self, lines=tuple(body))
|
|
182
|
-
|
|
183
|
-
def _closing_quotes(self) -> str:
|
|
184
|
-
if self.form is not CommentForm.DOC:
|
|
185
|
-
return ''
|
|
186
|
-
return self.lines[-1].suffix or self.lines[0].delimiter.strip()
|
|
187
|
-
|
|
188
|
-
def with_texts(self, texts: tuple[str, ...]) -> 'CommentGroup':
|
|
189
|
-
if not texts:
|
|
190
|
-
raise ValueError('A comment block needs at least one line')
|
|
191
|
-
template = self.lines[0]
|
|
192
|
-
rebuilt = tuple(
|
|
193
|
-
dataclasses.replace(
|
|
194
|
-
self.lines[index] if index < len(self.lines) else template,
|
|
195
|
-
text=text,
|
|
196
|
-
)
|
|
197
|
-
for index, text in enumerate(texts)
|
|
198
|
-
)
|
|
199
|
-
return dataclasses.replace(self, lines=rebuilt)
|
|
177
|
+
def closing(self) -> str:
|
|
178
|
+
"""What closes the comment on its last text line, such as a docstring's quotes."""
|
|
179
|
+
return self.layout.closing(self.lines)
|
|
200
180
|
|
|
201
181
|
def render(self) -> str:
|
|
202
182
|
return '\n'.join(line.render() for line in self.lines)
|