subedit 0.3.0__tar.gz → 0.3.1__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.3.0 → subedit-0.3.1}/PKG-INFO +4 -2
- {subedit-0.3.0 → subedit-0.3.1}/README.md +3 -1
- {subedit-0.3.0 → subedit-0.3.1}/plugins/subedit/.claude-plugin/plugin.json +1 -1
- {subedit-0.3.0 → subedit-0.3.1}/plugins/subedit/scripts/run-subedit-hook.sh +1 -1
- {subedit-0.3.0 → subedit-0.3.1}/pyproject.toml +1 -1
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/__init__.py +1 -1
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/domain/models/comment.py +25 -2
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/domain/models/prose.py +19 -6
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/domain/rules/layout.py +3 -2
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/infrastructure/parsers/languages/language_profile.py +6 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/infrastructure/parsers/languages/python_profile.py +1 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/infrastructure/parsers/languages/typescript_profile.py +5 -1
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/infrastructure/parsers/tree_sitter_parser.py +4 -1
- {subedit-0.3.0 → subedit-0.3.1}/tests/test_fixer.py +2 -2
- {subedit-0.3.0 → subedit-0.3.1}/tests/test_language_parity.py +63 -0
- {subedit-0.3.0 → subedit-0.3.1}/tests/test_prose.py +16 -0
- {subedit-0.3.0 → subedit-0.3.1}/tests/test_rules_layout.py +21 -4
- {subedit-0.3.0 → subedit-0.3.1}/uv.lock +1 -1
- {subedit-0.3.0 → subedit-0.3.1}/.claude/settings.json +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/.claude-plugin/marketplace.json +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/.github/dependabot.yml +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/.github/release.yml +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/.github/workflows/ci.yml +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/.github/workflows/pr.yml +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/.github/workflows/release.yml +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/.gitignore +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/.vale.ini +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/LICENSE +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/docs/rules.md +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/examples/README.md +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/examples/probe.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/examples/subedit.toml +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/examples/walkthrough.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/plugins/subedit/hooks/hooks.json +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/plugins/subedit/skills/judging-findings/SKILL.md +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/application/__init__.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/application/corpus_meter.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/application/fixer.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/application/linter.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/application/report_aggregator.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/domain/__init__.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/domain/models/__init__.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/domain/models/comment_layout.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/domain/models/diagnostic.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/domain/models/document.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/domain/models/position.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/domain/models/prose_map.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/domain/models/rule_set.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/domain/models/text.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/domain/ports/__init__.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/domain/ports/external_linter.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/domain/ports/language_conventions.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/domain/ports/rule.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/domain/ports/source_parser.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/domain/rules/__init__.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/domain/rules/rewrite.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/domain/rules/structure.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/infrastructure/__init__.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/infrastructure/config_sources/__init__.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/infrastructure/config_sources/config_schema.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/infrastructure/config_sources/formatter_width.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/infrastructure/config_sources/toml_config_source.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/infrastructure/linters/__init__.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/infrastructure/linters/autocorrect_linter.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/infrastructure/linters/source_path.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/infrastructure/linters/vale_linter.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/infrastructure/parsers/__init__.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/infrastructure/parsers/languages/__init__.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/infrastructure/parsers/languages/code_line.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/presentation/__init__.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/presentation/cli.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/presentation/harnesses/__init__.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/presentation/harnesses/agent_harness.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/presentation/harnesses/claude_code_harness.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/presentation/harnesses/codex_harness.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/presentation/hook.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/presentation/report.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/presentation/rule_reference.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/vale/styles/subedit/DateRef.yml +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/vale/styles/subedit/EditHistory.yml +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/vale/styles/subedit/Ellipsis.yml +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/vale/styles/subedit/EmDash.yml +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/vale/styles/subedit/ExternalRef.yml +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/vale/styles/subedit/MidSentenceColon.yml +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/vale/styles/subedit/MidSentenceColonMarkup.yml +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/vale/styles/subedit/Semicolon.yml +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/vale/styles/subedit/SignatureTag.yml +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/src/subedit/vale/vale.ini +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/subedit.toml +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/tests/fixtures/autocorrect/unspaced.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/tests/fixtures/vale/clean.md +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/tests/fixtures/vale/violations.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/tests/test_cli.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/tests/test_comment.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/tests/test_config.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/tests/test_corpus_meter.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/tests/test_diagnostic.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/tests/test_harnesses.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/tests/test_hook.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/tests/test_linter.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/tests/test_parser.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/tests/test_parser_ocp.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/tests/test_parser_oracle.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/tests/test_plugin.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/tests/test_position.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/tests/test_report_aggregator.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/tests/test_rules_rewrite.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/tests/test_rules_structure.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/tests/test_schema.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/tests/test_self_hosting.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/tests/test_text.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/tests/test_typescript.py +0 -0
- {subedit-0.3.0 → subedit-0.3.1}/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.
|
|
3
|
+
Version: 0.3.1
|
|
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
|
|
@@ -38,7 +38,7 @@ Your house style, enforced in the places prose hides inside a codebase.
|
|
|
38
38
|
|
|
39
39
|
A subeditor is the person at a publication who makes every piece of copy match its house style. `subedit` does that job for the prose in your code.
|
|
40
40
|
|
|
41
|
-
Linters check your code. Almost nothing checks the sentences you write around it. `subedit` reads the comments and docstrings in your source, lays them out
|
|
41
|
+
Linters check your code. Almost nothing checks the sentences you write around it. `subedit` reads the comments and docstrings in your source, lays them out so no sentence breaks mid clause, and only tells you about the sentences a tool cannot fix for you.
|
|
42
42
|
|
|
43
43
|
## Example
|
|
44
44
|
|
|
@@ -56,6 +56,8 @@ An editor wraps a comment at whatever column it reaches, so sentences break mid
|
|
|
56
56
|
# Retry once, then give up.
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
Short sentences may share a line while it fits, as in `# Retry once. Then give up.` A line of them that overflows is split, one sentence a line.
|
|
60
|
+
|
|
59
61
|
A sentence that runs past the width with no comma has nowhere legal to break, and only its author knows where the meaning divides. That one comes back to you, or to the agent that wrote it.
|
|
60
62
|
|
|
61
63
|
```text
|
|
@@ -4,7 +4,7 @@ Your house style, enforced in the places prose hides inside a codebase.
|
|
|
4
4
|
|
|
5
5
|
A subeditor is the person at a publication who makes every piece of copy match its house style. `subedit` does that job for the prose in your code.
|
|
6
6
|
|
|
7
|
-
Linters check your code. Almost nothing checks the sentences you write around it. `subedit` reads the comments and docstrings in your source, lays them out
|
|
7
|
+
Linters check your code. Almost nothing checks the sentences you write around it. `subedit` reads the comments and docstrings in your source, lays them out so no sentence breaks mid clause, and only tells you about the sentences a tool cannot fix for you.
|
|
8
8
|
|
|
9
9
|
## Example
|
|
10
10
|
|
|
@@ -22,6 +22,8 @@ An editor wraps a comment at whatever column it reaches, so sentences break mid
|
|
|
22
22
|
# Retry once, then give up.
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
Short sentences may share a line while it fits, as in `# Retry once. Then give up.` A line of them that overflows is split, one sentence a line.
|
|
26
|
+
|
|
25
27
|
A sentence that runs past the width with no comma has nowhere legal to break, and only its author knows where the meaning divides. That one comes back to you, or to the agent that wrote it.
|
|
26
28
|
|
|
27
29
|
```text
|
|
@@ -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.3.
|
|
7
|
+
SPEC='subedit[vale]>=0.3.1'
|
|
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
|
|
|
@@ -137,11 +137,21 @@ class CommentGroup:
|
|
|
137
137
|
out.extend(paragraph)
|
|
138
138
|
continue
|
|
139
139
|
lines = tuple(line.strip() for line in paragraph if line.strip())
|
|
140
|
-
|
|
140
|
+
sentences = Prose(' '.join(lines)).sentences()
|
|
141
|
+
shared_until = 0
|
|
142
|
+
for sentence_index, sentence in enumerate(sentences):
|
|
143
|
+
if sentence.offset < shared_until:
|
|
144
|
+
continue
|
|
141
145
|
if sentence_index == 1 and paragraph_index == 0 and self.has_summary_line:
|
|
142
146
|
out.append('')
|
|
143
147
|
first_budget = self.text_budget(width, first_line=not out) - (reserve if not out else 0)
|
|
144
|
-
|
|
148
|
+
is_summary = self.has_summary_line and paragraph_index == 0 and sentence_index == 0
|
|
149
|
+
shared_line = None if is_summary else _shared_line(lines, sentences[sentence_index:])
|
|
150
|
+
if shared_line is not None and len(shared_line) <= first_budget:
|
|
151
|
+
out.append(shared_line)
|
|
152
|
+
shared_until = sentence.offset + len(shared_line)
|
|
153
|
+
continue
|
|
154
|
+
if is_summary:
|
|
145
155
|
best_lines: tuple[str, ...] = (sentence.text,)
|
|
146
156
|
else:
|
|
147
157
|
best_lines = reflow_sentence(sentence.text, body_budget, stub_floor, first_width=first_budget)
|
|
@@ -192,6 +202,19 @@ class CommentGroup:
|
|
|
192
202
|
return TextEdit(self.range, self.render())
|
|
193
203
|
|
|
194
204
|
|
|
205
|
+
# A line the author filled with several short sentences, each whole, reads as well as one sentence a line,
|
|
206
|
+
# so it stays while it fits.
|
|
207
|
+
def _shared_line(lines: tuple[str, ...], sentences: tuple[Sentence, ...]) -> str | None:
|
|
208
|
+
start = 0
|
|
209
|
+
for line in lines:
|
|
210
|
+
if start == sentences[0].offset:
|
|
211
|
+
end = start + len(line)
|
|
212
|
+
ending_here = [sentence for sentence in sentences[1:] if sentence.end == end]
|
|
213
|
+
return line if ending_here and sentences[0].end < end else None
|
|
214
|
+
start += len(line) + 1
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
|
|
195
218
|
# The lines a sentence already occupies, when it has them to itself.
|
|
196
219
|
def _given_lines(lines: tuple[str, ...], sentence: Sentence) -> tuple[str, ...] | None:
|
|
197
220
|
covering: list[str] = []
|
|
@@ -21,6 +21,8 @@ ABBREVIATIONS = frozenset(
|
|
|
21
21
|
)
|
|
22
22
|
|
|
23
23
|
_SENTENCE_END = re.compile(r'[.!?]')
|
|
24
|
+
# A colon ends a lead-in as a period ends a sentence, so the line after either opens something new.
|
|
25
|
+
_ENDS_A_SENTENCE = re.compile(r'[.!?:]["\')\]]*\s*$')
|
|
24
26
|
_TRAILING_WORD = re.compile(r'([A-Za-z][A-Za-z.]*)$')
|
|
25
27
|
_URL = re.compile(r'\b(?:https?://|www\.)\S+')
|
|
26
28
|
# A span opened by two backticks, as reStructuredText writes code, closes on two.
|
|
@@ -30,6 +32,7 @@ _FENCE = re.compile(r'^\s*(```|~~~)')
|
|
|
30
32
|
_LIST_DELIMITER = re.compile(r'^\s*(?:[0-9]+[.)]|[-*+]|[a-zA-Z][.)])\s')
|
|
31
33
|
_LITERAL_DELIMITER = '\\b'
|
|
32
34
|
_CLOSING_MARKS = '"\')]}'
|
|
35
|
+
_OPENING_MARKS = '"\'([{'
|
|
33
36
|
_APOSTROPHES = ("'", '\u2019')
|
|
34
37
|
|
|
35
38
|
# Only before a number, since "No. 5" abbreviates while a trailing no ends its sentence.
|
|
@@ -107,7 +110,8 @@ class Prose:
|
|
|
107
110
|
current_lines = []
|
|
108
111
|
segment_is_literal = next_is_literal
|
|
109
112
|
|
|
110
|
-
|
|
113
|
+
lines = self.physical_lines
|
|
114
|
+
for previous_line, line in zip(('', *lines), lines, strict=False):
|
|
111
115
|
stripped_line = line.strip()
|
|
112
116
|
inside_tags = inside_tags or self.markup.opens_tag_section(line)
|
|
113
117
|
if inside_tags:
|
|
@@ -133,7 +137,7 @@ class Prose:
|
|
|
133
137
|
continue
|
|
134
138
|
if self._is_list_item(line):
|
|
135
139
|
inside_list = True
|
|
136
|
-
elif not self._continues_list_item(line, inside_list):
|
|
140
|
+
elif not self._continues_list_item(line, previous_line, inside_list):
|
|
137
141
|
inside_list = False
|
|
138
142
|
# The indented body under a commented-out opening brace is code too.
|
|
139
143
|
inside_code = self.markup.is_code(line) or (inside_code and line[:1].isspace())
|
|
@@ -173,10 +177,10 @@ class Prose:
|
|
|
173
177
|
def _is_indented(self, line: str) -> bool:
|
|
174
178
|
return line.startswith((' ', '\t'))
|
|
175
179
|
|
|
176
|
-
# A wrapped list item is indented under its marker
|
|
180
|
+
# A wrapped list item is indented under its marker, or left at the margin while its sentence runs on,
|
|
177
181
|
# so the continuation belongs to the item instead of opening a paragraph.
|
|
178
|
-
def _continues_list_item(self, line: str, inside_list: bool) -> bool:
|
|
179
|
-
return inside_list and line[:1].isspace()
|
|
182
|
+
def _continues_list_item(self, line: str, previous_line: str, inside_list: bool) -> bool:
|
|
183
|
+
return inside_list and (line[:1].isspace() or not _ENDS_A_SENTENCE.search(previous_line))
|
|
180
184
|
|
|
181
185
|
def _is_list_item(self, line: str) -> bool:
|
|
182
186
|
return bool(_LIST_DELIMITER.match(line))
|
|
@@ -237,10 +241,19 @@ class Prose:
|
|
|
237
241
|
return False
|
|
238
242
|
if word == NUMBER_ABBREVIATION and text[offset + 1 :].lstrip()[:1].isdigit():
|
|
239
243
|
return False
|
|
240
|
-
if
|
|
244
|
+
if (
|
|
245
|
+
trailing_word
|
|
246
|
+
and len(trailing_word.group(1).replace('.', '')) == 1
|
|
247
|
+
and self._stands_alone(text, trailing_word)
|
|
248
|
+
):
|
|
241
249
|
return False
|
|
242
250
|
return self._followed_by_break(text, offset)
|
|
243
251
|
|
|
252
|
+
# An initial stands as a word of its own, so a letter closing Ctrl+C or a plural such as `Event`s is none.
|
|
253
|
+
def _stands_alone(self, text: str, trailing_word: re.Match[str]) -> bool:
|
|
254
|
+
before = text[: trailing_word.start()]
|
|
255
|
+
return not before or before[-1].isspace() or before[-1] in _OPENING_MARKS
|
|
256
|
+
|
|
244
257
|
def _followed_by_break(self, text: str, offset: int) -> bool:
|
|
245
258
|
rest = text[offset + 1 :]
|
|
246
259
|
if not rest:
|
|
@@ -30,8 +30,9 @@ class MidClauseBreakRule(Rule):
|
|
|
30
30
|
rule_id=self.metadata.rule_id,
|
|
31
31
|
range=group.range,
|
|
32
32
|
message=(
|
|
33
|
-
'Comment layout
|
|
34
|
-
'A sentence that fits takes one line, and a longer one breaks only at a comma ending a clause.'
|
|
33
|
+
'Comment layout breaks a sentence where it need not. '
|
|
34
|
+
'A sentence that fits takes one line, and a longer one breaks only at a comma ending a clause. '
|
|
35
|
+
'Short sentences may share a line that fits.'
|
|
35
36
|
),
|
|
36
37
|
fix=Fix.reflow(reflowed.as_edit()),
|
|
37
38
|
)
|
{subedit-0.3.0 → subedit-0.3.1}/src/subedit/infrastructure/parsers/languages/language_profile.py
RENAMED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import abc
|
|
2
2
|
import enum
|
|
3
|
+
import re
|
|
3
4
|
|
|
4
5
|
from tree_sitter import Language, Node
|
|
5
6
|
|
|
@@ -47,6 +48,11 @@ class LanguageProfile(LanguageConventions, GrammarAdapter):
|
|
|
47
48
|
width_settings: tuple[WidthSetting, ...] = ()
|
|
48
49
|
# Directories a package manager fills with code nobody in the project wrote.
|
|
49
50
|
vendor_directories: frozenset[str] = frozenset()
|
|
51
|
+
# A comment a tool reads, such as eslint-disable or noqa, which the query sets apart from prose.
|
|
52
|
+
directive: re.Pattern[str] | None = None
|
|
53
|
+
|
|
54
|
+
def is_directive(self, node: Node) -> bool:
|
|
55
|
+
return self.directive is not None and bool(self.directive.match((node.text or b'').decode('utf-8')))
|
|
50
56
|
|
|
51
57
|
def imports_for_effect(self, node: Node) -> bool:
|
|
52
58
|
"""Whether an import binds no name and runs only for its side effect, which a comment over it explains."""
|
{subedit-0.3.0 → subedit-0.3.1}/src/subedit/infrastructure/parsers/languages/python_profile.py
RENAMED
|
@@ -76,6 +76,7 @@ class PythonProfile(LanguageProfile):
|
|
|
76
76
|
signature_tags = ('Args:', 'Returns:', 'Raises:', 'Yields:', ':param', ':return', ':returns', ':rtype')
|
|
77
77
|
canonical_extension = '.py'
|
|
78
78
|
doc_layout = QUOTED_DOC_LAYOUT
|
|
79
|
+
directive = re.compile(_DIRECTIVE)
|
|
79
80
|
doc_markup = ProseMarkup(tag_section=_SECTION, example_tag='Example', code_line=_CODE_LINE)
|
|
80
81
|
line_markup = ProseMarkup(code_line=_CODE_LINE)
|
|
81
82
|
vendor_directories = frozenset({'venv', 'site-packages', '__pypackages__'})
|
{subedit-0.3.0 → subedit-0.3.1}/src/subedit/infrastructure/parsers/languages/typescript_profile.py
RENAMED
|
@@ -20,7 +20,10 @@ _DOC_CLOSING = '*/'
|
|
|
20
20
|
# A plain /* */ block is usually a license header or a lint directive, so only // and /** */ are prose.
|
|
21
21
|
# A directive such as eslint-disable-next-line, or a triple-slash reference, is read by a tool rather than a person.
|
|
22
22
|
# Three stars open a banner rather than a doc comment.
|
|
23
|
-
_DIRECTIVE =
|
|
23
|
+
_DIRECTIVE = (
|
|
24
|
+
'^(///|//[ \t]*(eslint|@ts-|prettier-ignore|biome-ignore|istanbul |c8 |#region|#endregion)'
|
|
25
|
+
'|/[*][ \t]*(eslint|prettier-ignore|biome-ignore|istanbul |c8 ))'
|
|
26
|
+
)
|
|
24
27
|
QUERY = f"""
|
|
25
28
|
((comment) @comment (#match? @comment "^//") (#not-match? @comment "{_DIRECTIVE}"))
|
|
26
29
|
((comment) @directive (#match? @directive "{_DIRECTIVE}"))
|
|
@@ -63,6 +66,7 @@ class TypeScriptProfile(LanguageProfile):
|
|
|
63
66
|
doc_delimiter = '/**'
|
|
64
67
|
doc_layout = FramedDocLayout(opening='/**', prefix=' * ', closing_line=' */')
|
|
65
68
|
doc_leads_declaration = True
|
|
69
|
+
directive = re.compile(_DIRECTIVE)
|
|
66
70
|
width_settings = (
|
|
67
71
|
*(
|
|
68
72
|
WidthSetting(name, ('printWidth',))
|
|
@@ -181,12 +181,15 @@ class TreeSitterParser(SourceParser):
|
|
|
181
181
|
# A comment on the line right above a statement describes that statement, even at the top of a file.
|
|
182
182
|
# Imports are the exception, since a comment over them is a banner rather than a note on one import,
|
|
183
183
|
# unless the import runs only for its effect, such as patching a library, which only a comment explains.
|
|
184
|
+
# A directive such as eslint-disable governs what follows it, so the comment over it explains the directive.
|
|
184
185
|
def _touches_next_code(self, profile: LanguageProfile, node: Node) -> bool:
|
|
185
186
|
candidate = node.next_named_sibling
|
|
186
187
|
if candidate is None or not self._is_adjacent(node, candidate):
|
|
187
188
|
return False
|
|
188
189
|
role = profile.role_of(candidate.type)
|
|
189
|
-
|
|
190
|
+
if role is NodeRole.COMMENT:
|
|
191
|
+
return profile.is_directive(candidate)
|
|
192
|
+
return role is not NodeRole.IMPORT or profile.imports_for_effect(candidate)
|
|
190
193
|
|
|
191
194
|
# A blank line between a comment and what follows sets the comment apart, as a section divider does.
|
|
192
195
|
def _is_adjacent(self, upper: Node, lower: Node) -> bool:
|
|
@@ -116,13 +116,13 @@ def test_targeted_edits_run_before_reflow() -> None:
|
|
|
116
116
|
fix=Fix.reflow(reflowed.as_edit()),
|
|
117
117
|
)
|
|
118
118
|
|
|
119
|
-
source = 'def f():\n # one
|
|
119
|
+
source = 'def f():\n # one\n # two.\n pass\n'
|
|
120
120
|
document = Document(uri='file:///a.py', text=source, language_id='python')
|
|
121
121
|
outcome = Fixer(Linter(DEFAULT_PARSER, RuleEngine((ReflowRule(), TargetedRule())))).run(
|
|
122
122
|
document, RuleSet(enabled_rule_ids=frozenset({'targeted', 'reflowing'}), line_width=60)
|
|
123
123
|
)
|
|
124
124
|
assert '!' in outcome.document.text
|
|
125
|
-
assert outcome.document.text.count(' # ') ==
|
|
125
|
+
assert outcome.document.text.count(' # ') == 1
|
|
126
126
|
|
|
127
127
|
|
|
128
128
|
def test_overlapping_edits_are_deferred_rather_than_dropped() -> None:
|
|
@@ -124,3 +124,66 @@ def test_one_line_of_code_is_code_in_both_languages(line: str) -> None:
|
|
|
124
124
|
)
|
|
125
125
|
def test_a_block_set_two_spaces_in_is_left_as_laid_out(source: str, language_id: str) -> None:
|
|
126
126
|
assert fixed(source, language_id) == source
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@pytest.mark.parametrize(
|
|
130
|
+
('source', 'language_id'),
|
|
131
|
+
[
|
|
132
|
+
(
|
|
133
|
+
'// Fixture strings hold literal placeholders the gateway expands.\n'
|
|
134
|
+
'/* eslint-disable no-template-curly-in-string */\nimport { describe } from "vitest";\n',
|
|
135
|
+
'typescript',
|
|
136
|
+
),
|
|
137
|
+
(
|
|
138
|
+
'# Fixture strings hold literal placeholders the gateway expands.\n'
|
|
139
|
+
'# pylint: disable=consider-using-f-string\nimport os\n',
|
|
140
|
+
'python',
|
|
141
|
+
),
|
|
142
|
+
],
|
|
143
|
+
)
|
|
144
|
+
def test_a_comment_explaining_a_file_wide_directive_is_no_file_header(source: str, language_id: str) -> None:
|
|
145
|
+
suffix = '.py' if language_id == 'python' else '.ts'
|
|
146
|
+
document = Document(uri=f'file:///a{suffix}', text=source, language_id=language_id)
|
|
147
|
+
report = Linter(DEFAULT_PARSER, RuleEngine(ALL_RULES)).run(document, RuleSet(enabled_rule_ids=ENABLED))
|
|
148
|
+
assert 'file-header-comment' not in [item.rule_id for item in report.diagnostics]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@pytest.mark.parametrize(
|
|
152
|
+
('source', 'language_id'),
|
|
153
|
+
[
|
|
154
|
+
(
|
|
155
|
+
'/**\n * End-to-end flow.\n *\n * 1. Workspace scaffold, writes PRODUCT.md, registers workspace,\n'
|
|
156
|
+
" * binds the active plugin's validators.\n * 2. Proposal submission, which the reviewer approves.\n */\n"
|
|
157
|
+
'export function flow(): void {}\n',
|
|
158
|
+
'typescript',
|
|
159
|
+
),
|
|
160
|
+
(
|
|
161
|
+
'def flow():\n """End-to-end flow.\n\n - **Graph**: reuses the workspace `GraphCanvas`,\n'
|
|
162
|
+
' so the layout matches the main view.\n - **List**: shows every proposal.\n """\n',
|
|
163
|
+
'python',
|
|
164
|
+
),
|
|
165
|
+
],
|
|
166
|
+
)
|
|
167
|
+
def test_a_list_item_whose_sentence_runs_on_at_the_margin_stays_one_item(source: str, language_id: str) -> None:
|
|
168
|
+
assert fixed(source, language_id) == source
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@pytest.mark.parametrize(
|
|
172
|
+
('source', 'language_id', 'marker'),
|
|
173
|
+
[
|
|
174
|
+
(
|
|
175
|
+
'def f():\n """Do it."""\n # Surface errors without stack traces. Reserve stacks for bugs.\n return 1\n',
|
|
176
|
+
'python',
|
|
177
|
+
'#',
|
|
178
|
+
),
|
|
179
|
+
(
|
|
180
|
+
'/** Do it. */\nexport function f() {\n'
|
|
181
|
+
' // Surface errors without stack traces. Reserve stacks for bugs.\n return 1;\n}\n',
|
|
182
|
+
'typescript',
|
|
183
|
+
'//',
|
|
184
|
+
),
|
|
185
|
+
],
|
|
186
|
+
)
|
|
187
|
+
def test_short_sentences_may_share_a_line_that_fits(source: str, language_id: str, marker: str) -> None:
|
|
188
|
+
assert fixed(source, language_id) == source
|
|
189
|
+
assert f'{marker} Surface errors without stack traces.\n' in fixed(source, language_id, width=60)
|
|
@@ -237,3 +237,19 @@ def test_an_appositive_is_no_list_item() -> None:
|
|
|
237
237
|
def test_parallel_items_led_by_articles_still_form_a_list() -> None:
|
|
238
238
|
lines = ('It feeds a cat, a dog,', 'and a bird every morning before the long walk.')
|
|
239
239
|
assert break_cost(lines) == 1
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@pytest.mark.parametrize(
|
|
243
|
+
'text',
|
|
244
|
+
[
|
|
245
|
+
'Process-local pub/sub of `WorkspaceEvent`s. Subscribers register per workspace.',
|
|
246
|
+
'Stop both on Ctrl+C. Standalone, it runs the server alone.',
|
|
247
|
+
'Retry on HTTP 5xx-A. Then give up.',
|
|
248
|
+
],
|
|
249
|
+
)
|
|
250
|
+
def test_a_letter_closing_a_longer_token_is_no_initial(text: str) -> None:
|
|
251
|
+
assert len(Prose(text).sentences()) == 2
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def test_an_initial_in_brackets_still_holds_its_sentence() -> None:
|
|
255
|
+
assert len(Prose('named after (J. Smith) originally').sentences()) == 1
|
|
@@ -32,10 +32,27 @@ def test_a_mid_clause_break_is_reported_and_reflowed() -> None:
|
|
|
32
32
|
assert '# cap the size to the limit so the mmap does not blow past it,\n' in fixed(source, width=70)
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
def
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
def test_short_sentences_sharing_a_line_that_fits_stay_together() -> None:
|
|
36
|
+
assert lint('def f():\n # first one. second one. third one.\n pass\n').is_clean
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_sentences_sharing_a_line_that_overflows_take_one_line_each() -> None:
|
|
40
|
+
source = 'def f():\n # the first sentence runs on. the second one too. the third one ends here.\n pass\n'
|
|
41
|
+
assert fixed(source, width=40).count(' # ') == 3
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_sentences_on_lines_of_their_own_are_not_joined() -> None:
|
|
45
|
+
assert lint('def f():\n # first one.\n # second one.\n pass\n').is_clean
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_a_sentence_running_on_from_a_shared_line_takes_its_own_line() -> None:
|
|
49
|
+
source = 'def f():\n # first one. second one\n # runs on.\n pass\n'
|
|
50
|
+
assert fixed(source) == 'def f():\n # first one.\n # second one runs on.\n pass\n'
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_a_docstring_summary_keeps_its_line_to_itself() -> None:
|
|
54
|
+
source = 'def f():\n """Do it. Then more.\n\n Body.\n """\n'
|
|
55
|
+
assert '"""Do it.\n\n Then more.' in fixed(source)
|
|
39
56
|
|
|
40
57
|
|
|
41
58
|
def test_a_long_sentence_splits_at_commas_only() -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{subedit-0.3.0 → subedit-0.3.1}/src/subedit/infrastructure/config_sources/formatter_width.py
RENAMED
|
File without changes
|
{subedit-0.3.0 → subedit-0.3.1}/src/subedit/infrastructure/config_sources/toml_config_source.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|