subedit 0.1.1__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.1 → subedit-0.1.2}/.claude-plugin/plugin.json +1 -1
- subedit-0.1.2/.vale.ini +13 -0
- {subedit-0.1.1 → subedit-0.1.2}/PKG-INFO +10 -2
- {subedit-0.1.1 → subedit-0.1.2}/README.md +9 -1
- subedit-0.1.2/docs/rules.md +18 -0
- {subedit-0.1.1 → subedit-0.1.2}/hooks/run-subedit-hook.sh +1 -1
- {subedit-0.1.1 → subedit-0.1.2}/pyproject.toml +1 -1
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/__init__.py +1 -1
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/application/fixer.py +1 -1
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/application/report_aggregator.py +14 -2
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/domain/models/comment.py +18 -7
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/domain/models/diagnostic.py +13 -0
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/domain/models/prose.py +4 -0
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/domain/models/rule_set.py +5 -0
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/domain/ports/external_linter.py +3 -0
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/domain/rules/layout.py +3 -2
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/domain/rules/rewrite.py +10 -4
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/domain/rules/structure.py +3 -1
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/infrastructure/config_sources/toml_config_source.py +7 -3
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/infrastructure/linters/vale_linter.py +16 -0
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/infrastructure/parsers/languages/language_profile.py +1 -0
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/infrastructure/parsers/languages/python_profile.py +3 -0
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/infrastructure/parsers/tree_sitter_parser.py +18 -2
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/presentation/cli.py +17 -4
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/presentation/report.py +23 -5
- {subedit-0.1.1 → 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.1/src/subedit/vale/styles/mroops → subedit-0.1.2/src/subedit/vale/styles/subedit}/MidSentenceColon.yml +2 -2
- {subedit-0.1.1/src/subedit/vale/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.1 → subedit-0.1.2}/tests/fixtures/vale/clean.md +7 -0
- {subedit-0.1.1 → subedit-0.1.2}/tests/test_cli.py +50 -0
- {subedit-0.1.1 → subedit-0.1.2}/tests/test_config.py +9 -1
- {subedit-0.1.1 → subedit-0.1.2}/tests/test_fixer.py +29 -0
- {subedit-0.1.1 → subedit-0.1.2}/tests/test_hook.py +5 -0
- {subedit-0.1.1 → subedit-0.1.2}/tests/test_parser.py +24 -0
- {subedit-0.1.1 → subedit-0.1.2}/tests/test_parser_ocp.py +1 -1
- {subedit-0.1.1 → subedit-0.1.2}/tests/test_prose.py +5 -0
- {subedit-0.1.1 → subedit-0.1.2}/tests/test_report_aggregator.py +25 -1
- {subedit-0.1.1 → subedit-0.1.2}/tests/test_schema.py +2 -2
- {subedit-0.1.1 → subedit-0.1.2}/tests/test_vale_style.py +20 -2
- {subedit-0.1.1 → subedit-0.1.2}/uv.lock +1 -1
- subedit-0.1.1/.vale.ini +0 -13
- subedit-0.1.1/docs/rules.md +0 -16
- subedit-0.1.1/src/subedit/vale/styles/mroops/ExternalRef.yml +0 -7
- subedit-0.1.1/src/subedit/vale/vale.ini +0 -11
- {subedit-0.1.1 → subedit-0.1.2}/.claude/settings.json +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/.claude-plugin/marketplace.json +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/.github/dependabot.yml +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/.github/release.yml +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/.github/workflows/ci.yml +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/.github/workflows/pr.yml +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/.github/workflows/release.yml +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/.gitignore +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/LICENSE +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/examples/README.md +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/examples/probe.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/examples/subedit.toml +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/examples/walkthrough.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/hooks/hooks.json +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/application/__init__.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/application/corpus_meter.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/application/linter.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/domain/__init__.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/domain/models/__init__.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/domain/models/document.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/domain/models/position.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/domain/models/text.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/domain/ports/__init__.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/domain/ports/language_conventions.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/domain/ports/rule.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/domain/ports/source_parser.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/domain/rules/__init__.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/infrastructure/__init__.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/infrastructure/config_sources/__init__.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/infrastructure/config_sources/config_schema.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/infrastructure/linters/__init__.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/infrastructure/linters/autocorrect_linter.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/infrastructure/parsers/__init__.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/infrastructure/parsers/languages/__init__.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/presentation/__init__.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/presentation/harnesses/__init__.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/presentation/harnesses/agent_harness.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/presentation/harnesses/claude_code_harness.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/presentation/harnesses/codex_harness.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/src/subedit/presentation/hook.py +0 -0
- {subedit-0.1.1/src/subedit/vale/styles/mroops → subedit-0.1.2/src/subedit/vale/styles/subedit}/EditHistory.yml +0 -0
- {subedit-0.1.1/src/subedit/vale/styles/mroops → subedit-0.1.2/src/subedit/vale/styles/subedit}/Ellipsis.yml +0 -0
- {subedit-0.1.1/src/subedit/vale/styles/mroops → subedit-0.1.2/src/subedit/vale/styles/subedit}/EmDash.yml +0 -0
- {subedit-0.1.1/src/subedit/vale/styles/mroops → subedit-0.1.2/src/subedit/vale/styles/subedit}/Semicolon.yml +0 -0
- {subedit-0.1.1/src/subedit/vale/styles/mroops → subedit-0.1.2/src/subedit/vale/styles/subedit}/SignatureTag.yml +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/subedit.toml +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/tests/fixtures/autocorrect/unspaced.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/tests/fixtures/vale/violations.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/tests/test_comment.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/tests/test_corpus_meter.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/tests/test_diagnostic.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/tests/test_harnesses.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/tests/test_linter.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/tests/test_parser_oracle.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/tests/test_plugin.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/tests/test_position.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/tests/test_rules_layout.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/tests/test_rules_rewrite.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/tests/test_rules_structure.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/tests/test_self_hosting.py +0 -0
- {subedit-0.1.1 → subedit-0.1.2}/tests/test_text.py +0 -0
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
|
|
@@ -163,11 +163,19 @@ block-too-long = { severity = "warning", line = 3, doc-public = 20 }
|
|
|
163
163
|
|
|
164
164
|
Rule names describe the problem they report. `docs/rules.md` lists every rule, generated from the rules themselves.
|
|
165
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
|
+
|
|
166
174
|
## Delegated Tools
|
|
167
175
|
|
|
168
176
|
`check`, `fix`, and the agent hook also run two tools, and fold their findings into the same report.
|
|
169
177
|
|
|
170
|
-
- **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
|
|
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.*`.
|
|
171
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.
|
|
172
180
|
|
|
173
181
|
`--no-delegate` skips both for `check` and `fix`.
|
|
@@ -129,11 +129,19 @@ 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. 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
|
|
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.*`.
|
|
137
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
147
|
`--no-delegate` skips both for `check` and `fix`.
|
|
@@ -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. |
|
|
@@ -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.1.
|
|
7
|
+
SPEC='subedit[vale]>=0.1.2'
|
|
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
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import dataclasses
|
|
2
|
+
|
|
3
|
+
from ..domain.models.diagnostic import Diagnostic, Report
|
|
2
4
|
from ..domain.models.document import Document
|
|
3
5
|
from ..domain.models.rule_set import RuleSet
|
|
4
6
|
from ..domain.ports.external_linter import ExternalLinter
|
|
@@ -17,7 +19,17 @@ class ReportAggregator:
|
|
|
17
19
|
if not external_linter.is_available():
|
|
18
20
|
missing_sources.append(external_linter.name)
|
|
19
21
|
continue
|
|
20
|
-
report = report.merged_with(Report(external_linter.run(document)))
|
|
22
|
+
report = report.merged_with(Report(self._configured(external_linter.run(document), rule_set)))
|
|
21
23
|
if missing_sources:
|
|
22
24
|
report = report.merged_with(Report(unavailable_sources=tuple(missing_sources)))
|
|
23
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.1 → 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
|
)
|
|
@@ -13,6 +13,8 @@ from ...domain.ports.external_linter import ExternalLinter
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
TIMEOUT_SECONDS = 30
|
|
16
|
+
# The first run of the vale extra downloads the binary, which a slow link can take minutes over.
|
|
17
|
+
PREPARE_TIMEOUT_SECONDS = 900
|
|
16
18
|
BUNDLED_CONFIG = pathlib.Path(__file__).resolve().parents[2] / 'vale' / 'vale.ini'
|
|
17
19
|
# Vale exits with this code when its own search finds no config.
|
|
18
20
|
NO_CONFIG_CODE = 'E100'
|
|
@@ -55,6 +57,20 @@ class ValeLinter(ExternalLinter):
|
|
|
55
57
|
def is_available(self) -> bool:
|
|
56
58
|
return shutil.which(self._executable) is not None and _launcher_is_ready()
|
|
57
59
|
|
|
60
|
+
def prepare(self) -> None:
|
|
61
|
+
"""Let the vale extra finish its download, with progress left on stderr so a waiting command shows why."""
|
|
62
|
+
if _launcher_is_ready() or shutil.which(self._executable) is None:
|
|
63
|
+
return
|
|
64
|
+
try:
|
|
65
|
+
subprocess.run( # noqa: S603
|
|
66
|
+
[self._executable, '--version'],
|
|
67
|
+
stdout=subprocess.DEVNULL,
|
|
68
|
+
check=False,
|
|
69
|
+
timeout=PREPARE_TIMEOUT_SECONDS,
|
|
70
|
+
)
|
|
71
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
72
|
+
return
|
|
73
|
+
|
|
58
74
|
def run(self, document: Document) -> tuple[Diagnostic, ...]:
|
|
59
75
|
if not self.is_available():
|
|
60
76
|
return ()
|
{subedit-0.1.1 → subedit-0.1.2}/src/subedit/infrastructure/parsers/languages/python_profile.py
RENAMED
|
@@ -32,6 +32,9 @@ class PythonProfile(LanguageProfile):
|
|
|
32
32
|
'function_definition': NodeRole.DEFINITION,
|
|
33
33
|
'class_definition': NodeRole.DEFINITION,
|
|
34
34
|
'decorated_definition': NodeRole.DEFINITION,
|
|
35
|
+
'import_statement': NodeRole.IMPORT,
|
|
36
|
+
'import_from_statement': NodeRole.IMPORT,
|
|
37
|
+
'future_import_statement': NodeRole.IMPORT,
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
def language(self) -> Language:
|
|
@@ -150,10 +150,24 @@ class TreeSitterParser(SourceParser):
|
|
|
150
150
|
return CommentPlacement.LEADING_DECLARATION
|
|
151
151
|
parent = node.parent
|
|
152
152
|
is_root = parent is not None and profile.role_of(parent.type) is NodeRole.ROOT
|
|
153
|
-
if is_root and not self._has_code_before(profile, node):
|
|
153
|
+
if is_root and not self._has_code_before(profile, node) and not self._touches_next_code(profile, node):
|
|
154
154
|
return CommentPlacement.FILE_HEADER
|
|
155
155
|
return CommentPlacement.INLINE_BODY
|
|
156
156
|
|
|
157
|
+
# A comment on the line right above a statement describes that statement, even at the top of a file.
|
|
158
|
+
# Imports are the exception, since a comment over them is a banner rather than a note on one import.
|
|
159
|
+
def _touches_next_code(self, profile: LanguageProfile, node: Node) -> bool:
|
|
160
|
+
candidate = node.next_named_sibling
|
|
161
|
+
return (
|
|
162
|
+
candidate is not None
|
|
163
|
+
and profile.role_of(candidate.type) not in (NodeRole.COMMENT, NodeRole.IMPORT)
|
|
164
|
+
and self._is_adjacent(node, candidate)
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# A blank line between a comment and what follows sets the comment apart, as a section divider does.
|
|
168
|
+
def _is_adjacent(self, upper: Node, lower: Node) -> bool:
|
|
169
|
+
return lower.start_point[0] == upper.end_point[0] + 1
|
|
170
|
+
|
|
157
171
|
def _has_code_before(self, profile: LanguageProfile, node: Node) -> bool:
|
|
158
172
|
parent = node.parent
|
|
159
173
|
if parent is None:
|
|
@@ -164,13 +178,15 @@ class TreeSitterParser(SourceParser):
|
|
|
164
178
|
)
|
|
165
179
|
|
|
166
180
|
def _find_next_definition(self, profile: LanguageProfile, node: Node) -> Node | None:
|
|
181
|
+
previous = node
|
|
167
182
|
candidate = node.next_named_sibling
|
|
168
|
-
while candidate is not None:
|
|
183
|
+
while candidate is not None and self._is_adjacent(previous, candidate):
|
|
169
184
|
role = profile.role_of(candidate.type)
|
|
170
185
|
if role is NodeRole.DEFINITION:
|
|
171
186
|
return candidate
|
|
172
187
|
if role is not NodeRole.COMMENT:
|
|
173
188
|
return None
|
|
189
|
+
previous = candidate
|
|
174
190
|
candidate = candidate.next_named_sibling
|
|
175
191
|
return None
|
|
176
192
|
|
|
@@ -38,14 +38,14 @@ def check(
|
|
|
38
38
|
raise typer.Exit(2)
|
|
39
39
|
|
|
40
40
|
aggregator = _build_report_aggregator(delegate)
|
|
41
|
-
|
|
41
|
+
has_errors = False
|
|
42
42
|
for document in documents:
|
|
43
43
|
report = aggregator.run(document, DEFAULT_CONFIG_SOURCE.resolve(_file_path(document)))
|
|
44
|
-
|
|
44
|
+
has_errors = has_errors or report.has_errors
|
|
45
45
|
rendered_report = formatter(document, report)
|
|
46
46
|
if rendered_report:
|
|
47
47
|
typer.echo(rendered_report)
|
|
48
|
-
raise typer.Exit(1 if
|
|
48
|
+
raise typer.Exit(1 if has_errors else 0)
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
@app.command()
|
|
@@ -71,6 +71,8 @@ def fix(
|
|
|
71
71
|
typer.echo(_diff(document, fix_result.document))
|
|
72
72
|
if fix_result.unresolved_findings:
|
|
73
73
|
typer.echo(reporters.brief(fix_result.document, fix_result.report))
|
|
74
|
+
if fix_result.report.advisory:
|
|
75
|
+
typer.echo(reporters.advice(fix_result.document, fix_result.report))
|
|
74
76
|
|
|
75
77
|
action_word = 'rewrote' if write else 'would rewrite'
|
|
76
78
|
typer.echo(
|
|
@@ -146,8 +148,19 @@ def _build_report_aggregator(delegate: bool) -> ReportAggregator:
|
|
|
146
148
|
return ReportAggregator(_build_linter(), _external_linters(delegate))
|
|
147
149
|
|
|
148
150
|
|
|
151
|
+
# A command can wait for a download, unlike the hook, and a skipped tool must not read as a clean run.
|
|
149
152
|
def _external_linters(delegate: bool) -> tuple[ExternalLinter, ...]:
|
|
150
|
-
|
|
153
|
+
if not delegate:
|
|
154
|
+
return ()
|
|
155
|
+
for external_linter in EXTERNAL_LINTERS:
|
|
156
|
+
external_linter.prepare()
|
|
157
|
+
if not external_linter.is_available():
|
|
158
|
+
typer.echo(
|
|
159
|
+
f'{external_linter.name} is not installed, so its checks were skipped. '
|
|
160
|
+
'Install subedit[vale] for Vale, or pass --no-delegate to skip it on purpose.',
|
|
161
|
+
err=True,
|
|
162
|
+
)
|
|
163
|
+
return EXTERNAL_LINTERS
|
|
151
164
|
|
|
152
165
|
|
|
153
166
|
def _file_path(document: Document) -> str:
|
|
@@ -16,14 +16,26 @@ def full(document: Document, report: Report) -> str:
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def brief(document: Document, report: Report) -> str:
|
|
19
|
-
if not report.
|
|
19
|
+
if not report.blocking:
|
|
20
20
|
return ''
|
|
21
21
|
return _rewrite_brief(document, report)
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
def advice(document: Document, report: Report) -> str:
|
|
25
|
+
if not report.advisory:
|
|
26
|
+
return ''
|
|
27
|
+
lines = [
|
|
28
|
+
f'{len(report.advisory)} comment {"warning" if len(report.advisory) == 1 else "warnings"} in {_path(document)}, worth a look but not required.'
|
|
29
|
+
]
|
|
30
|
+
lines.extend(_finding_lines(document, report.advisory))
|
|
31
|
+
return '\n'.join(lines)
|
|
32
|
+
|
|
33
|
+
|
|
24
34
|
def actionable(document: Document, report: Report) -> str:
|
|
25
|
-
if report.
|
|
35
|
+
if report.blocking:
|
|
26
36
|
return _rewrite_brief(document, report)
|
|
37
|
+
if report.advisory:
|
|
38
|
+
return advice(document, report)
|
|
27
39
|
if report.mechanical:
|
|
28
40
|
mechanical_count = len(report.mechanical)
|
|
29
41
|
return (
|
|
@@ -46,17 +58,23 @@ def as_json(document: Document, report: Report) -> str:
|
|
|
46
58
|
|
|
47
59
|
|
|
48
60
|
def _rewrite_brief(document: Document, report: Report) -> str:
|
|
49
|
-
finding_count = len(report.
|
|
61
|
+
finding_count = len(report.blocking)
|
|
50
62
|
lines = [
|
|
51
63
|
f'{finding_count} comment {"finding needs" if finding_count == 1 else "findings need"} rewriting in {_path(document)}.',
|
|
52
64
|
'Each states the fix. Apply it in place, do not add a suppression comment.',
|
|
53
65
|
'',
|
|
54
66
|
]
|
|
55
|
-
|
|
67
|
+
lines.extend(_finding_lines(document, report.blocking))
|
|
68
|
+
return '\n'.join(lines)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _finding_lines(document: Document, diagnostics: tuple[Diagnostic, ...]) -> list[str]:
|
|
72
|
+
lines: list[str] = []
|
|
73
|
+
for diagnostic in diagnostics:
|
|
56
74
|
position = document.positions.to_position(diagnostic.range.start)
|
|
57
75
|
lines.append(f'line {position.line + 1} [{diagnostic.rule_id}]')
|
|
58
76
|
lines.append(f' {diagnostic.message}')
|
|
59
|
-
return
|
|
77
|
+
return lines
|
|
60
78
|
|
|
61
79
|
|
|
62
80
|
def _encode(document: Document, diagnostic: Diagnostic) -> dict[str, object]:
|
|
@@ -6,16 +6,19 @@ HEADING = '# Rules'
|
|
|
6
6
|
PREAMBLE = (
|
|
7
7
|
'Generated from `RuleMetadata`. Edit the rule, not this file.\n\n'
|
|
8
8
|
'The fix kind states who resolves a finding. A mechanical kind is repaired without telling the author, '
|
|
9
|
-
'so only a rewrite reaches an agent
|
|
9
|
+
'so only a rewrite reaches an agent.\n\n'
|
|
10
|
+
'The level says whether a rewrite blocks. An error fails check and stops an agent, '
|
|
11
|
+
'while a warning is shown as advice. Set either per rule under `[rules]`, '
|
|
12
|
+
'and the Vale rules the same way by name, as in `"subedit.DateRef" = "off"`.'
|
|
10
13
|
)
|
|
11
14
|
|
|
12
15
|
|
|
13
16
|
def render(rules: tuple[Rule, ...]) -> str:
|
|
14
17
|
sorted_rules = sorted(rules, key=lambda rule: (rule.metadata.fix_kind.value, rule.metadata.rule_id))
|
|
15
|
-
lines = [HEADING, '', PREAMBLE, '', '| Rule | Fix kind | Summary |', '| --- | --- | --- |']
|
|
18
|
+
lines = [HEADING, '', PREAMBLE, '', '| Rule | Fix kind | Level | Summary |', '| --- | --- | --- | --- |']
|
|
16
19
|
lines.extend(_row(rule.metadata) for rule in sorted_rules)
|
|
17
20
|
return '\n'.join(lines) + '\n'
|
|
18
21
|
|
|
19
22
|
|
|
20
23
|
def _row(metadata: RuleMetadata) -> str:
|
|
21
|
-
return f'| `{metadata.rule_id}` | {metadata.fix_kind.value} | {metadata.summary} |'
|
|
24
|
+
return f'| `{metadata.rule_id}` | {metadata.fix_kind.value} | {metadata.default_severity.name.lower()} | {metadata.summary} |'
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
extends: existence
|
|
2
|
+
message: "Date '%s' in a comment. Keep it if it names a version. If it records when something changed, state the current constraint instead."
|
|
3
|
+
level: warning
|
|
4
|
+
nonword: true
|
|
5
|
+
tokens:
|
|
6
|
+
- '\b\d{4}-\d{2}-\d{2}\b'
|
|
7
|
+
- '\b\d{4}/\d{1,2}/\d{1,2}\b'
|
|
8
|
+
- '\b\d{1,2}/\d{1,2}/\d{4}\b'
|
|
9
|
+
- '\b(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Sept|Oct|Nov|Dec)[a-z]*\.? \d{1,2},? \d{4}\b'
|
|
10
|
+
- '\b\d{1,2} (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Sept|Oct|Nov|Dec)[a-z]* \d{4}\b'
|
|
11
|
+
- '\b(?:January|February|March|April|May|June|July|August|September|October|November|December) \d{4}\b'
|
|
12
|
+
- '\b(?:[Aa]s of|[Ss]ince|[Uu]ntil) (?:Q[1-4] )?\d{4}\b'
|
|
13
|
+
- '\b[Ii]n Q[1-4] \d{4}\b'
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
extends: existence
|
|
2
|
+
message: "External reference '%s' a reader cannot open. Restate the constraint."
|
|
3
|
+
level: error
|
|
4
|
+
nonword: true
|
|
5
|
+
tokens:
|
|
6
|
+
- '[A-Z]{2,}-\d+'
|
|
7
|
+
# Standards and encodings are names a reader can look up, not tickets.
|
|
8
|
+
exceptions:
|
|
9
|
+
- 'ISO-\d+'
|
|
10
|
+
- 'UTF-\d+'
|
|
11
|
+
- 'SHA-\d+'
|
|
12
|
+
- 'AES-\d+'
|
|
13
|
+
- 'CRC-\d+'
|
|
14
|
+
- 'IEEE-\d+'
|
|
15
|
+
- 'ECMA-\d+'
|