readability-cli 0.10.2__tar.gz → 0.10.3__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.
- {readability_cli-0.10.2 → readability_cli-0.10.3}/PKG-INFO +17 -13
- {readability_cli-0.10.2 → readability_cli-0.10.3}/README.md +16 -12
- {readability_cli-0.10.2 → readability_cli-0.10.3}/pyproject.toml +2 -2
- {readability_cli-0.10.2 → readability_cli-0.10.3}/readability/checking.py +3 -2
- {readability_cli-0.10.2 → readability_cli-0.10.3}/readability/configs/ruff.toml +3 -2
- {readability_cli-0.10.2 → readability_cli-0.10.3}/readability/guide.py +2 -2
- {readability_cli-0.10.2 → readability_cli-0.10.3}/readability/tools.py +61 -2
- {readability_cli-0.10.2 → readability_cli-0.10.3}/test_checking.py +496 -0
- {readability_cli-0.10.2 → readability_cli-0.10.3}/test_outline.py +1 -1
- {readability_cli-0.10.2 → readability_cli-0.10.3}/uv.lock +1 -1
- {readability_cli-0.10.2 → readability_cli-0.10.3}/.github/workflows/ci.yml +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.3}/.github/workflows/publish.yml +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.3}/.github/workflows/update-guides.yml +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.3}/.gitignore +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.3}/.python-version +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.3}/LICENSE +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.3}/readability/__init__.py +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.3}/readability/cli.py +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.3}/readability/configs/biome-default.json +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.3}/readability/configs/pyrefly.toml +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.3}/readability/guides/Rguide.md +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.3}/readability/guides/cppguide.md +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.3}/readability/guides/csharp-style.md +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.3}/readability/guides/docguide-style.md +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.3}/readability/guides/go-guide.md +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.3}/readability/guides/htmlcssguide.md +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.3}/readability/guides/javaguide.md +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.3}/readability/guides/jsguide.md +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.3}/readability/guides/jsoncstyleguide.md +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.3}/readability/guides/objcguide.md +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.3}/readability/guides/pyguide.md +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.3}/readability/guides/shellguide.md +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.3}/readability/guides/tsguide.md +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.3}/readability/guides/vimscriptguide.md +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.3}/readability/outline.py +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.3}/test_guide.py +0 -0
- {readability_cli-0.10.2 → readability_cli-0.10.3}/test_package.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: readability-cli
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.3
|
|
4
4
|
Summary: A CLI to lint, format, and type-check code with Google-style defaults, and pull Google style guides in markdown format.
|
|
5
5
|
Project-URL: Homepage, https://github.com/owahltinez/readability
|
|
6
6
|
Project-URL: Repository, https://github.com/owahltinez/readability
|
|
@@ -23,7 +23,8 @@ quick access to style conventions without browsing HTML pages.
|
|
|
23
23
|
## Features
|
|
24
24
|
|
|
25
25
|
- **Linting & Formatting**: A `check` command that automatically detects and
|
|
26
|
-
runs relevant tools (Ruff, Pyrefly, Biome,
|
|
26
|
+
runs relevant tools (Ruff, Pyrefly, Biome, gofmt, goimports, shfmt,
|
|
27
|
+
shellcheck, and rustfmt) for your project.
|
|
27
28
|
- **Predictable Ownership**: Every supported format has fixed tools, so
|
|
28
29
|
configuration customizes checks without changing which formatter runs.
|
|
29
30
|
- **Style Guides**: A `guide` command that fetches the latest Google style
|
|
@@ -98,7 +99,7 @@ readability check . --unsafe
|
|
|
98
99
|
Fixes that may change a program's behavior or drop its comments are opt-in
|
|
99
100
|
through `--unsafe`, which every tool applies under its own name: Ruff's
|
|
100
101
|
`--unsafe-fixes` and Biome's `--unsafe`. Tools drawing no such distinction,
|
|
101
|
-
such as gofmt, are unaffected by it.
|
|
102
|
+
such as gofmt and goimports, are unaffected by it.
|
|
102
103
|
|
|
103
104
|
### Supported Formats
|
|
104
105
|
|
|
@@ -106,7 +107,9 @@ such as gofmt, are unaffected by it.
|
|
|
106
107
|
| ---------------------------------------------------------------- | --------------------------------------- | ------------------------------------- |
|
|
107
108
|
| `.py` | Ruff lint/format; Pyrefly type checking | Ruff and Pyrefly native configuration |
|
|
108
109
|
| `.js`, `.jsx`, `.ts`, `.tsx`, `.json`, `.jsonc`, `.css`, `.html` | Biome | `biome.json` or `biome.jsonc` |
|
|
109
|
-
| `.go` | gofmt
|
|
110
|
+
| `.go` | gofmt, goimports | None |
|
|
111
|
+
| `.sh`, `.bash` | shfmt format; shellcheck lint | None |
|
|
112
|
+
| `.rs` | rustfmt | None |
|
|
110
113
|
|
|
111
114
|
Markdown, YAML, SCSS, JSONL, and extensions not listed above are unsupported.
|
|
112
115
|
An unsupported-only path reports that nothing was checked and exits
|
|
@@ -114,8 +117,8 @@ successfully.
|
|
|
114
117
|
|
|
115
118
|
Biome first checks a project's `node_modules/.bin`. Ruff, Pyrefly, and Biome
|
|
116
119
|
then use `PATH`, followed by `uvx` for the Python tools or `npx` for Biome. The
|
|
117
|
-
runners cache downloads, so subsequent runs work offline. Gofmt
|
|
118
|
-
|
|
120
|
+
runners cache downloads, so subsequent runs work offline. Gofmt and goimports
|
|
121
|
+
must be available on `PATH`.
|
|
119
122
|
|
|
120
123
|
That keeps this package at ~4 MB rather than the ~54 MB it would take to carry
|
|
121
124
|
Ruff and Pyrefly itself — a cost that would fall on everyone using only `guide`.
|
|
@@ -133,8 +136,8 @@ Set `UV_OFFLINE=1` to forbid fetching. A tool that then cannot be reached fails
|
|
|
133
136
|
the run rather than passing it.
|
|
134
137
|
|
|
135
138
|
Ruff, Pyrefly, and Biome ship with bundled configurations, so they run on every
|
|
136
|
-
file they own without project setup. Gofmt
|
|
137
|
-
is no `go.mod`.
|
|
139
|
+
file they own without project setup. Gofmt and goimports run on `.go` files even
|
|
140
|
+
when there is no `go.mod`.
|
|
138
141
|
|
|
139
142
|
A tool that could not be reached at all is never skipped quietly:
|
|
140
143
|
|
|
@@ -189,8 +192,9 @@ caller happened to be standing in.
|
|
|
189
192
|
Ruff and Pyrefly defaults follow the
|
|
190
193
|
[Google Python style guide](https://google.github.io/styleguide/pyguide.html):
|
|
191
194
|
80-column lines, Google docstrings, one import per line sorted as isort's
|
|
192
|
-
`google` profile does,
|
|
193
|
-
|
|
195
|
+
`google` profile does, best-practice lint rules (bugbear, pyupgrade, simplify,
|
|
196
|
+
comprehensions, and Ruff-specific rules), and full type checking. The Biome
|
|
197
|
+
default applies the 80-column lines and two-space indentation of the
|
|
194
198
|
[Google JavaScript style guide](https://google.github.io/styleguide/jsguide.html)
|
|
195
199
|
and enables Biome's recommended lint rules. Project `biome.json` and
|
|
196
200
|
`biome.jsonc` files replace those bundled defaults for Biome-owned formats.
|
|
@@ -199,9 +203,9 @@ For Python, add `[tool.ruff]` or `[tool.pyrefly]` to `pyproject.toml`, or use
|
|
|
199
203
|
`ruff.toml`, `.ruff.toml`, or `pyrefly.toml`. Ruff, Pyrefly, and Biome load
|
|
200
204
|
their native project configurations in place of bundled defaults. Readability
|
|
201
205
|
does not interpret EditorConfig itself; a canonical tool such as Biome may opt
|
|
202
|
-
into it through that tool's native configuration.
|
|
203
|
-
The bundled Biome file requires Biome 2.5 or later,
|
|
204
|
-
runner's version floor.
|
|
206
|
+
into it through that tool's native configuration. Neither gofmt nor goimports
|
|
207
|
+
has project settings. The bundled Biome file requires Biome 2.5 or later,
|
|
208
|
+
matching the fallback runner's version floor.
|
|
205
209
|
|
|
206
210
|
Configuration is found per file, in the file's own directory and then its
|
|
207
211
|
ancestors, which is where each canonical tool looks. A package keeping its
|
|
@@ -9,7 +9,8 @@ quick access to style conventions without browsing HTML pages.
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
11
|
- **Linting & Formatting**: A `check` command that automatically detects and
|
|
12
|
-
runs relevant tools (Ruff, Pyrefly, Biome,
|
|
12
|
+
runs relevant tools (Ruff, Pyrefly, Biome, gofmt, goimports, shfmt,
|
|
13
|
+
shellcheck, and rustfmt) for your project.
|
|
13
14
|
- **Predictable Ownership**: Every supported format has fixed tools, so
|
|
14
15
|
configuration customizes checks without changing which formatter runs.
|
|
15
16
|
- **Style Guides**: A `guide` command that fetches the latest Google style
|
|
@@ -84,7 +85,7 @@ readability check . --unsafe
|
|
|
84
85
|
Fixes that may change a program's behavior or drop its comments are opt-in
|
|
85
86
|
through `--unsafe`, which every tool applies under its own name: Ruff's
|
|
86
87
|
`--unsafe-fixes` and Biome's `--unsafe`. Tools drawing no such distinction,
|
|
87
|
-
such as gofmt, are unaffected by it.
|
|
88
|
+
such as gofmt and goimports, are unaffected by it.
|
|
88
89
|
|
|
89
90
|
### Supported Formats
|
|
90
91
|
|
|
@@ -92,7 +93,9 @@ such as gofmt, are unaffected by it.
|
|
|
92
93
|
| ---------------------------------------------------------------- | --------------------------------------- | ------------------------------------- |
|
|
93
94
|
| `.py` | Ruff lint/format; Pyrefly type checking | Ruff and Pyrefly native configuration |
|
|
94
95
|
| `.js`, `.jsx`, `.ts`, `.tsx`, `.json`, `.jsonc`, `.css`, `.html` | Biome | `biome.json` or `biome.jsonc` |
|
|
95
|
-
| `.go` | gofmt
|
|
96
|
+
| `.go` | gofmt, goimports | None |
|
|
97
|
+
| `.sh`, `.bash` | shfmt format; shellcheck lint | None |
|
|
98
|
+
| `.rs` | rustfmt | None |
|
|
96
99
|
|
|
97
100
|
Markdown, YAML, SCSS, JSONL, and extensions not listed above are unsupported.
|
|
98
101
|
An unsupported-only path reports that nothing was checked and exits
|
|
@@ -100,8 +103,8 @@ successfully.
|
|
|
100
103
|
|
|
101
104
|
Biome first checks a project's `node_modules/.bin`. Ruff, Pyrefly, and Biome
|
|
102
105
|
then use `PATH`, followed by `uvx` for the Python tools or `npx` for Biome. The
|
|
103
|
-
runners cache downloads, so subsequent runs work offline. Gofmt
|
|
104
|
-
|
|
106
|
+
runners cache downloads, so subsequent runs work offline. Gofmt and goimports
|
|
107
|
+
must be available on `PATH`.
|
|
105
108
|
|
|
106
109
|
That keeps this package at ~4 MB rather than the ~54 MB it would take to carry
|
|
107
110
|
Ruff and Pyrefly itself — a cost that would fall on everyone using only `guide`.
|
|
@@ -119,8 +122,8 @@ Set `UV_OFFLINE=1` to forbid fetching. A tool that then cannot be reached fails
|
|
|
119
122
|
the run rather than passing it.
|
|
120
123
|
|
|
121
124
|
Ruff, Pyrefly, and Biome ship with bundled configurations, so they run on every
|
|
122
|
-
file they own without project setup. Gofmt
|
|
123
|
-
is no `go.mod`.
|
|
125
|
+
file they own without project setup. Gofmt and goimports run on `.go` files even
|
|
126
|
+
when there is no `go.mod`.
|
|
124
127
|
|
|
125
128
|
A tool that could not be reached at all is never skipped quietly:
|
|
126
129
|
|
|
@@ -175,8 +178,9 @@ caller happened to be standing in.
|
|
|
175
178
|
Ruff and Pyrefly defaults follow the
|
|
176
179
|
[Google Python style guide](https://google.github.io/styleguide/pyguide.html):
|
|
177
180
|
80-column lines, Google docstrings, one import per line sorted as isort's
|
|
178
|
-
`google` profile does,
|
|
179
|
-
|
|
181
|
+
`google` profile does, best-practice lint rules (bugbear, pyupgrade, simplify,
|
|
182
|
+
comprehensions, and Ruff-specific rules), and full type checking. The Biome
|
|
183
|
+
default applies the 80-column lines and two-space indentation of the
|
|
180
184
|
[Google JavaScript style guide](https://google.github.io/styleguide/jsguide.html)
|
|
181
185
|
and enables Biome's recommended lint rules. Project `biome.json` and
|
|
182
186
|
`biome.jsonc` files replace those bundled defaults for Biome-owned formats.
|
|
@@ -185,9 +189,9 @@ For Python, add `[tool.ruff]` or `[tool.pyrefly]` to `pyproject.toml`, or use
|
|
|
185
189
|
`ruff.toml`, `.ruff.toml`, or `pyrefly.toml`. Ruff, Pyrefly, and Biome load
|
|
186
190
|
their native project configurations in place of bundled defaults. Readability
|
|
187
191
|
does not interpret EditorConfig itself; a canonical tool such as Biome may opt
|
|
188
|
-
into it through that tool's native configuration.
|
|
189
|
-
The bundled Biome file requires Biome 2.5 or later,
|
|
190
|
-
runner's version floor.
|
|
192
|
+
into it through that tool's native configuration. Neither gofmt nor goimports
|
|
193
|
+
has project settings. The bundled Biome file requires Biome 2.5 or later,
|
|
194
|
+
matching the fallback runner's version floor.
|
|
191
195
|
|
|
192
196
|
Configuration is found per file, in the file's own directory and then its
|
|
193
197
|
ancestors, which is where each canonical tool looks. A package keeping its
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "readability-cli"
|
|
3
|
-
version = "0.10.
|
|
3
|
+
version = "0.10.3"
|
|
4
4
|
description = "A CLI to lint, format, and type-check code with Google-style defaults, and pull Google style guides in markdown format."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -41,7 +41,7 @@ dev = [
|
|
|
41
41
|
line-length = 80
|
|
42
42
|
|
|
43
43
|
[tool.ruff.lint]
|
|
44
|
-
select = ["E", "W", "F", "I", "N", "D", "PL"]
|
|
44
|
+
select = ["E", "W", "F", "I", "N", "D", "PL", "B", "UP", "SIM", "C4", "RUF"]
|
|
45
45
|
ignore = ["PLR0911", "PLR0912", "PLR0913", "PLR0915", "PLR2004"]
|
|
46
46
|
|
|
47
47
|
[tool.ruff.lint.pydocstyle]
|
|
@@ -204,9 +204,10 @@ def _run_tool(
|
|
|
204
204
|
result = _capture_tool_command(command, cwd=tool.cwd)
|
|
205
205
|
if _tool_checked_files(tool.name, result):
|
|
206
206
|
report.ran.add(tool.name)
|
|
207
|
-
# gofmt
|
|
207
|
+
# gofmt and goimports name files rather than using exit codes
|
|
208
208
|
if result.returncode != 0 or (
|
|
209
|
-
tool.name
|
|
209
|
+
tool.name in ("gofmt", "goimports")
|
|
210
|
+
and result.stdout.strip()
|
|
210
211
|
):
|
|
211
212
|
report.findings = True
|
|
212
213
|
click.echo(
|
|
@@ -6,8 +6,9 @@ line-length = 80
|
|
|
6
6
|
|
|
7
7
|
[lint]
|
|
8
8
|
# Pycodestyle (E, W), Pyflakes (F), import order (I), naming (N),
|
|
9
|
-
# docstrings (D),
|
|
10
|
-
|
|
9
|
+
# docstrings (D), pylint (PL), bugbear (B), pyupgrade (UP),
|
|
10
|
+
# simplify (SIM), comprehensions (C4), and ruff-specific (RUF) rules
|
|
11
|
+
select = ["E", "W", "F", "I", "N", "D", "PL", "B", "UP", "SIM", "C4", "RUF"]
|
|
11
12
|
# Complexity counters and magic-value warnings are guidance, not errors
|
|
12
13
|
ignore = ["PLR0911", "PLR0912", "PLR0913", "PLR0915", "PLR2004"]
|
|
13
14
|
|
|
@@ -82,7 +82,7 @@ def get_guide_content(url: str) -> str:
|
|
|
82
82
|
logger.error("Failed to fetch content from %s: %s", url, e)
|
|
83
83
|
raise click.ClickException(
|
|
84
84
|
f"Failed to fetch style guide from {url}: {e}"
|
|
85
|
-
)
|
|
85
|
+
) from e
|
|
86
86
|
|
|
87
87
|
return response.text
|
|
88
88
|
|
|
@@ -242,7 +242,7 @@ def get_guide(language: str, remote: bool = False) -> str:
|
|
|
242
242
|
# If remote is False, check for local file first
|
|
243
243
|
if not remote and os.path.exists(local_path):
|
|
244
244
|
logger.info("Reading style guide from local file: %s", local_path)
|
|
245
|
-
with open(local_path,
|
|
245
|
+
with open(local_path, encoding="utf-8") as f:
|
|
246
246
|
return f.read()
|
|
247
247
|
|
|
248
248
|
return refresh_guide(filename)
|
|
@@ -347,6 +347,10 @@ TOOL_EXTENSIONS = {
|
|
|
347
347
|
".html",
|
|
348
348
|
),
|
|
349
349
|
"gofmt": (".go",),
|
|
350
|
+
"goimports": (".go",),
|
|
351
|
+
"shfmt": (".sh", ".bash"),
|
|
352
|
+
"shellcheck": (".sh", ".bash"),
|
|
353
|
+
"rustfmt": (".rs",),
|
|
350
354
|
}
|
|
351
355
|
|
|
352
356
|
# Headroom below common argv limits; long target lists are split before exec
|
|
@@ -354,8 +358,20 @@ MAX_COMMAND_BYTES = 16 * 1024
|
|
|
354
358
|
|
|
355
359
|
# Named files bypass a tool's own exclusions, and Pyrefly ignores its excludes
|
|
356
360
|
PRUNED_DIRECTORIES = frozenset(
|
|
357
|
-
|
|
358
|
-
|
|
361
|
+
[
|
|
362
|
+
".git",
|
|
363
|
+
".hg",
|
|
364
|
+
".svn",
|
|
365
|
+
".venv",
|
|
366
|
+
"venv",
|
|
367
|
+
".tox",
|
|
368
|
+
".nox",
|
|
369
|
+
"node_modules",
|
|
370
|
+
"__pycache__",
|
|
371
|
+
".mypy_cache",
|
|
372
|
+
".pytest_cache",
|
|
373
|
+
".ruff_cache",
|
|
374
|
+
]
|
|
359
375
|
)
|
|
360
376
|
|
|
361
377
|
# Per tool: config filenames, pyproject section (None if it has none), flag
|
|
@@ -392,6 +408,13 @@ TOOL_PHASES = {
|
|
|
392
408
|
"format": ("format", "--write", "--no-errors-on-unmatched"),
|
|
393
409
|
},
|
|
394
410
|
"gofmt": {"check_format": ("-l",), "format": ("-w",)},
|
|
411
|
+
"goimports": {"check_format": ("-l",), "format": ("-w",)},
|
|
412
|
+
"shfmt": {
|
|
413
|
+
"check_format": ("-i", "2", "-ci", "-d"),
|
|
414
|
+
"format": ("-i", "2", "-ci", "-w"),
|
|
415
|
+
},
|
|
416
|
+
"shellcheck": {"check": ()},
|
|
417
|
+
"rustfmt": {"check_format": ("--check",), "format": ()},
|
|
395
418
|
}
|
|
396
419
|
|
|
397
420
|
# Per tool: how it names fixes that may change behavior or drop comments
|
|
@@ -549,6 +572,42 @@ def _get_tool_definitions(
|
|
|
549
572
|
path,
|
|
550
573
|
)
|
|
551
574
|
)
|
|
575
|
+
plans.append(
|
|
576
|
+
_plan(
|
|
577
|
+
"goimports",
|
|
578
|
+
("goimports",),
|
|
579
|
+
[],
|
|
580
|
+
_matching_paths(path, TOOL_EXTENSIONS["goimports"]),
|
|
581
|
+
path,
|
|
582
|
+
)
|
|
583
|
+
)
|
|
584
|
+
plans.append(
|
|
585
|
+
_plan(
|
|
586
|
+
"shfmt",
|
|
587
|
+
("shfmt",),
|
|
588
|
+
[],
|
|
589
|
+
_matching_paths(path, TOOL_EXTENSIONS["shfmt"]),
|
|
590
|
+
path,
|
|
591
|
+
)
|
|
592
|
+
)
|
|
593
|
+
plans.append(
|
|
594
|
+
_plan(
|
|
595
|
+
"shellcheck",
|
|
596
|
+
("shellcheck",),
|
|
597
|
+
[],
|
|
598
|
+
_matching_paths(path, TOOL_EXTENSIONS["shellcheck"]),
|
|
599
|
+
path,
|
|
600
|
+
)
|
|
601
|
+
)
|
|
602
|
+
plans.append(
|
|
603
|
+
_plan(
|
|
604
|
+
"rustfmt",
|
|
605
|
+
("rustfmt",),
|
|
606
|
+
[],
|
|
607
|
+
_matching_paths(path, TOOL_EXTENSIONS["rustfmt"]),
|
|
608
|
+
path,
|
|
609
|
+
)
|
|
610
|
+
)
|
|
552
611
|
|
|
553
612
|
# A directory owning its config alone lets Pyrefly select the files
|
|
554
613
|
files, section = CONFIG_SOURCES["pyrefly"][:2]
|
|
@@ -15,6 +15,8 @@ from readability.cli import cli
|
|
|
15
15
|
from readability.tools import _bundled_config
|
|
16
16
|
from readability.tools import _get_tool_definitions
|
|
17
17
|
from readability.tools import _has_project_config
|
|
18
|
+
from readability.tools import TOOL_EXTENSIONS
|
|
19
|
+
from readability.tools import TOOL_PHASES
|
|
18
20
|
from readability.tools import TOOL_RUNNERS
|
|
19
21
|
|
|
20
22
|
|
|
@@ -1045,6 +1047,17 @@ def test_bundled_default_configs_are_valid() -> None:
|
|
|
1045
1047
|
}
|
|
1046
1048
|
|
|
1047
1049
|
|
|
1050
|
+
def test_bundled_ruff_config_includes_enriched_rules() -> None:
|
|
1051
|
+
"""The default ruff config selects best-practice and modernization rules."""
|
|
1052
|
+
# Enriched rules for bug prevention, modern syntax, and simplification
|
|
1053
|
+
ruff_config = tomllib.loads(_bundled_config("ruff").read_text())
|
|
1054
|
+
selected_rules = set(ruff_config["lint"]["select"])
|
|
1055
|
+
expected_rules = {"B", "UP", "SIM", "C4", "RUF"}
|
|
1056
|
+
|
|
1057
|
+
# All enriched rule sets must be present
|
|
1058
|
+
assert expected_rules.issubset(selected_rules)
|
|
1059
|
+
|
|
1060
|
+
|
|
1048
1061
|
@patch("readability.checking._check_path")
|
|
1049
1062
|
def test_check_paths_aggregates_str_and_path_inputs(
|
|
1050
1063
|
mock_check_path: MagicMock,
|
|
@@ -1608,6 +1621,170 @@ def test_gofmt_fix_failure_is_reported(
|
|
|
1608
1621
|
assert "gofmt formatting findings" in result.output
|
|
1609
1622
|
|
|
1610
1623
|
|
|
1624
|
+
def test_goimports_tool_plan_generation(tmp_path: Path) -> None:
|
|
1625
|
+
"""Go files produce goimports plans alongside gofmt."""
|
|
1626
|
+
go_file = tmp_path / "main.go"
|
|
1627
|
+
go_file.touch()
|
|
1628
|
+
|
|
1629
|
+
# Verify tool registration in extensions and phases
|
|
1630
|
+
assert TOOL_EXTENSIONS["goimports"] == (".go",)
|
|
1631
|
+
assert TOOL_PHASES["goimports"] == {
|
|
1632
|
+
"check_format": ("-l",),
|
|
1633
|
+
"format": ("-w",),
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
# Verify tool plan creation
|
|
1637
|
+
plans = {
|
|
1638
|
+
plan.name: plan for plan in _get_tool_definitions(go_file, tmp_path)
|
|
1639
|
+
}
|
|
1640
|
+
assert "goimports" in plans
|
|
1641
|
+
goimports = plans["goimports"]
|
|
1642
|
+
assert goimports.extensions == (".go",)
|
|
1643
|
+
assert goimports.check_format == ("goimports", "-l", str(go_file))
|
|
1644
|
+
assert goimports.format == ("goimports", "-w", str(go_file))
|
|
1645
|
+
assert goimports.check == ()
|
|
1646
|
+
assert goimports.fix == ()
|
|
1647
|
+
assert goimports.targets == (str(go_file),)
|
|
1648
|
+
|
|
1649
|
+
|
|
1650
|
+
@pytest.mark.parametrize("fix", (False, True))
|
|
1651
|
+
@patch("shutil.which")
|
|
1652
|
+
@patch("subprocess.run")
|
|
1653
|
+
def test_configless_go_uses_goimports_for_check_and_fix(
|
|
1654
|
+
mock_run: MagicMock,
|
|
1655
|
+
mock_which: MagicMock,
|
|
1656
|
+
tmp_path: Path,
|
|
1657
|
+
fix: bool,
|
|
1658
|
+
) -> None:
|
|
1659
|
+
"""Standalone Go files use symmetric goimports commands without go.mod."""
|
|
1660
|
+
mock_which.side_effect = lambda name: name if name == "goimports" else None
|
|
1661
|
+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
1662
|
+
|
|
1663
|
+
# Run check or format on Go file
|
|
1664
|
+
runner = CliRunner()
|
|
1665
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
1666
|
+
Path("main.go").touch()
|
|
1667
|
+
arguments = ["check", "main.go"]
|
|
1668
|
+
if fix:
|
|
1669
|
+
arguments.append("--fix")
|
|
1670
|
+
|
|
1671
|
+
result = runner.invoke(cli, arguments)
|
|
1672
|
+
|
|
1673
|
+
# Verify goimports command invoked with correct flags
|
|
1674
|
+
assert result.exit_code == 0
|
|
1675
|
+
expected = ["goimports", "-w" if fix else "-l", "main.go"]
|
|
1676
|
+
assert [invocation.args[0] for invocation in mock_run.call_args_list] == [
|
|
1677
|
+
expected
|
|
1678
|
+
]
|
|
1679
|
+
|
|
1680
|
+
|
|
1681
|
+
@patch("shutil.which")
|
|
1682
|
+
@patch("subprocess.run")
|
|
1683
|
+
def test_goimports_scopes_a_directory_to_go_files(
|
|
1684
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
1685
|
+
) -> None:
|
|
1686
|
+
"""Goimports receives recursive Go files instead of an invalid directory."""
|
|
1687
|
+
mock_which.side_effect = lambda name: name if name == "goimports" else None
|
|
1688
|
+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
1689
|
+
|
|
1690
|
+
# Populate directory structure with mixed files
|
|
1691
|
+
runner = CliRunner()
|
|
1692
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
1693
|
+
Path("src/nested").mkdir(parents=True)
|
|
1694
|
+
Path("src/main.go").touch()
|
|
1695
|
+
Path("src/nested/helper.go").touch()
|
|
1696
|
+
Path("src/notes.txt").touch()
|
|
1697
|
+
|
|
1698
|
+
result = runner.invoke(cli, ["check", "src"])
|
|
1699
|
+
|
|
1700
|
+
# Verify only Go files are passed to goimports
|
|
1701
|
+
assert result.exit_code == 0
|
|
1702
|
+
assert [invocation.args[0] for invocation in mock_run.call_args_list] == [
|
|
1703
|
+
["goimports", "-l", "src/main.go", "src/nested/helper.go"]
|
|
1704
|
+
]
|
|
1705
|
+
|
|
1706
|
+
|
|
1707
|
+
@patch("shutil.which")
|
|
1708
|
+
@patch("subprocess.run")
|
|
1709
|
+
def test_goimports_check_reports_diff_in_stdout_as_findings(
|
|
1710
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
1711
|
+
) -> None:
|
|
1712
|
+
"""Goimports reports diffs on stdout while exiting zero."""
|
|
1713
|
+
mock_which.side_effect = lambda name: name if name == "goimports" else None
|
|
1714
|
+
mock_run.return_value = MagicMock(
|
|
1715
|
+
returncode=0, stdout="main.go\n", stderr=""
|
|
1716
|
+
)
|
|
1717
|
+
|
|
1718
|
+
# Invoke check on unformatted Go file
|
|
1719
|
+
runner = CliRunner()
|
|
1720
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
1721
|
+
Path("main.go").touch()
|
|
1722
|
+
|
|
1723
|
+
result = runner.invoke(cli, ["check", "main.go"])
|
|
1724
|
+
|
|
1725
|
+
# Findings in stdout must fail the check
|
|
1726
|
+
assert result.exit_code == 1
|
|
1727
|
+
assert "--- goimports formatting findings ---" in result.output
|
|
1728
|
+
assert "main.go" in result.output
|
|
1729
|
+
|
|
1730
|
+
|
|
1731
|
+
@patch("shutil.which")
|
|
1732
|
+
@patch("subprocess.run")
|
|
1733
|
+
def test_goimports_fix_failure_is_reported(
|
|
1734
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
1735
|
+
) -> None:
|
|
1736
|
+
"""A failed goimports write cannot be reported as a clean fix."""
|
|
1737
|
+
mock_which.side_effect = lambda name: name if name == "goimports" else None
|
|
1738
|
+
mock_run.return_value = MagicMock(
|
|
1739
|
+
returncode=1, stdout="", stderr="invalid Go syntax"
|
|
1740
|
+
)
|
|
1741
|
+
|
|
1742
|
+
# Invoke check --fix on invalid Go file
|
|
1743
|
+
runner = CliRunner()
|
|
1744
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
1745
|
+
Path("main.go").touch()
|
|
1746
|
+
|
|
1747
|
+
result = runner.invoke(cli, ["check", "main.go", "--fix"])
|
|
1748
|
+
|
|
1749
|
+
# Fix error must fail the check
|
|
1750
|
+
assert result.exit_code == 1
|
|
1751
|
+
assert "goimports formatting findings" in result.output
|
|
1752
|
+
|
|
1753
|
+
|
|
1754
|
+
@pytest.mark.parametrize("fix", (False, True))
|
|
1755
|
+
@patch("shutil.which")
|
|
1756
|
+
@patch("subprocess.run")
|
|
1757
|
+
def test_go_runs_both_gofmt_and_goimports(
|
|
1758
|
+
mock_run: MagicMock,
|
|
1759
|
+
mock_which: MagicMock,
|
|
1760
|
+
tmp_path: Path,
|
|
1761
|
+
fix: bool,
|
|
1762
|
+
) -> None:
|
|
1763
|
+
"""When both gofmt and goimports are installed, both run on Go files."""
|
|
1764
|
+
mock_which.side_effect = lambda name: (
|
|
1765
|
+
name if name in ("gofmt", "goimports") else None
|
|
1766
|
+
)
|
|
1767
|
+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
1768
|
+
|
|
1769
|
+
# Run check or fix with both Go tools available
|
|
1770
|
+
runner = CliRunner()
|
|
1771
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
1772
|
+
Path("main.go").touch()
|
|
1773
|
+
arguments = ["check", "main.go"]
|
|
1774
|
+
if fix:
|
|
1775
|
+
arguments.append("--fix")
|
|
1776
|
+
|
|
1777
|
+
result = runner.invoke(cli, arguments)
|
|
1778
|
+
|
|
1779
|
+
# Verify both tools ran with the appropriate flag
|
|
1780
|
+
assert result.exit_code == 0
|
|
1781
|
+
flag = "-w" if fix else "-l"
|
|
1782
|
+
assert [invocation.args[0] for invocation in mock_run.call_args_list] == [
|
|
1783
|
+
["gofmt", flag, "main.go"],
|
|
1784
|
+
["goimports", flag, "main.go"],
|
|
1785
|
+
]
|
|
1786
|
+
|
|
1787
|
+
|
|
1611
1788
|
@patch("shutil.which")
|
|
1612
1789
|
@patch("subprocess.run")
|
|
1613
1790
|
def test_large_biome_directory_uses_bounded_commands(
|
|
@@ -1653,6 +1830,7 @@ def test_missing_gofmt_is_reported_for_configless_go(
|
|
|
1653
1830
|
|
|
1654
1831
|
assert result.exit_code == 1
|
|
1655
1832
|
assert "gofmt" in result.stderr
|
|
1833
|
+
assert "goimports" in result.stderr
|
|
1656
1834
|
assert "nothing was verified" in result.stderr
|
|
1657
1835
|
|
|
1658
1836
|
|
|
@@ -2195,3 +2373,321 @@ def test_unsafe_is_a_no_op_for_tools_without_one(
|
|
|
2195
2373
|
assert [invocation.args[0] for invocation in mock_run.call_args_list] == [
|
|
2196
2374
|
["gofmt", "-w", "main.go"]
|
|
2197
2375
|
]
|
|
2376
|
+
|
|
2377
|
+
|
|
2378
|
+
@pytest.mark.parametrize("extension", (".sh", ".bash"))
|
|
2379
|
+
def test_tool_definitions_for_shell(extension: str, tmp_path: Path) -> None:
|
|
2380
|
+
"""Shell files yield shfmt and shellcheck plans with style arguments."""
|
|
2381
|
+
# Create the target shell file
|
|
2382
|
+
script = tmp_path / f"script{extension}"
|
|
2383
|
+
script.touch()
|
|
2384
|
+
|
|
2385
|
+
# Resolve tool plans for the file
|
|
2386
|
+
tools = {
|
|
2387
|
+
tool.name: tool for tool in _get_tool_definitions(script, tmp_path)
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
# Verify shfmt plan
|
|
2391
|
+
shfmt = tools["shfmt"]
|
|
2392
|
+
assert shfmt.extensions == (".sh", ".bash")
|
|
2393
|
+
assert shfmt.check_format == ("shfmt", "-i", "2", "-ci", "-d", str(script))
|
|
2394
|
+
assert shfmt.format == ("shfmt", "-i", "2", "-ci", "-w", str(script))
|
|
2395
|
+
assert shfmt.check == ()
|
|
2396
|
+
assert shfmt.fix == ()
|
|
2397
|
+
|
|
2398
|
+
# Verify shellcheck plan
|
|
2399
|
+
shellcheck = tools["shellcheck"]
|
|
2400
|
+
assert shellcheck.extensions == (".sh", ".bash")
|
|
2401
|
+
assert shellcheck.check == ("shellcheck", str(script))
|
|
2402
|
+
assert shellcheck.check_format == ()
|
|
2403
|
+
assert shellcheck.format == ()
|
|
2404
|
+
assert shellcheck.fix == ()
|
|
2405
|
+
|
|
2406
|
+
|
|
2407
|
+
def test_tool_definitions_for_rust(tmp_path: Path) -> None:
|
|
2408
|
+
"""Rust files yield rustfmt plans with check and in-place formatting."""
|
|
2409
|
+
# Create the target Rust file
|
|
2410
|
+
source = tmp_path / "main.rs"
|
|
2411
|
+
source.touch()
|
|
2412
|
+
|
|
2413
|
+
# Resolve tool plans for the file
|
|
2414
|
+
tools = {
|
|
2415
|
+
tool.name: tool for tool in _get_tool_definitions(source, tmp_path)
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
# Verify rustfmt plan
|
|
2419
|
+
rustfmt = tools["rustfmt"]
|
|
2420
|
+
assert rustfmt.extensions == (".rs",)
|
|
2421
|
+
assert rustfmt.check_format == ("rustfmt", "--check", str(source))
|
|
2422
|
+
assert rustfmt.format == ("rustfmt", str(source))
|
|
2423
|
+
assert rustfmt.check == ()
|
|
2424
|
+
assert rustfmt.fix == ()
|
|
2425
|
+
|
|
2426
|
+
|
|
2427
|
+
@pytest.mark.parametrize("extension", (".sh", ".bash"))
|
|
2428
|
+
@pytest.mark.parametrize("fix", (False, True))
|
|
2429
|
+
@patch("shutil.which")
|
|
2430
|
+
@patch("subprocess.run")
|
|
2431
|
+
def test_shell_uses_shfmt_and_shellcheck_for_check_and_fix(
|
|
2432
|
+
mock_run: MagicMock,
|
|
2433
|
+
mock_which: MagicMock,
|
|
2434
|
+
tmp_path: Path,
|
|
2435
|
+
extension: str,
|
|
2436
|
+
fix: bool,
|
|
2437
|
+
) -> None:
|
|
2438
|
+
"""Shell files invoke shfmt and shellcheck during check and fix."""
|
|
2439
|
+
# Allow shfmt and shellcheck to appear installed
|
|
2440
|
+
mock_which.side_effect = lambda name: (
|
|
2441
|
+
name if name in ("shfmt", "shellcheck") else None
|
|
2442
|
+
)
|
|
2443
|
+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
2444
|
+
|
|
2445
|
+
# Run check command in an isolated environment
|
|
2446
|
+
runner = CliRunner()
|
|
2447
|
+
filename = f"script{extension}"
|
|
2448
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2449
|
+
Path(filename).touch()
|
|
2450
|
+
arguments = ["check", filename]
|
|
2451
|
+
if fix:
|
|
2452
|
+
arguments.append("--fix")
|
|
2453
|
+
result = runner.invoke(cli, arguments)
|
|
2454
|
+
|
|
2455
|
+
# Verify clean exit and expected tool invocations
|
|
2456
|
+
assert result.exit_code == 0
|
|
2457
|
+
shfmt_action = "-w" if fix else "-d"
|
|
2458
|
+
expected = [
|
|
2459
|
+
["shfmt", "-i", "2", "-ci", shfmt_action, filename],
|
|
2460
|
+
["shellcheck", filename],
|
|
2461
|
+
]
|
|
2462
|
+
assert [
|
|
2463
|
+
invocation.args[0] for invocation in mock_run.call_args_list
|
|
2464
|
+
] == expected
|
|
2465
|
+
|
|
2466
|
+
|
|
2467
|
+
@pytest.mark.parametrize("fix", (False, True))
|
|
2468
|
+
@patch("shutil.which")
|
|
2469
|
+
@patch("subprocess.run")
|
|
2470
|
+
def test_rust_uses_rustfmt_for_check_and_fix(
|
|
2471
|
+
mock_run: MagicMock,
|
|
2472
|
+
mock_which: MagicMock,
|
|
2473
|
+
tmp_path: Path,
|
|
2474
|
+
fix: bool,
|
|
2475
|
+
) -> None:
|
|
2476
|
+
"""Rust files invoke rustfmt with check flag or in-place format."""
|
|
2477
|
+
# Allow rustfmt to appear installed
|
|
2478
|
+
mock_which.side_effect = lambda name: name if name == "rustfmt" else None
|
|
2479
|
+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
2480
|
+
|
|
2481
|
+
# Run check command in an isolated environment
|
|
2482
|
+
runner = CliRunner()
|
|
2483
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2484
|
+
Path("main.rs").touch()
|
|
2485
|
+
arguments = ["check", "main.rs"]
|
|
2486
|
+
if fix:
|
|
2487
|
+
arguments.append("--fix")
|
|
2488
|
+
result = runner.invoke(cli, arguments)
|
|
2489
|
+
|
|
2490
|
+
# Verify clean exit and expected tool invocations
|
|
2491
|
+
assert result.exit_code == 0
|
|
2492
|
+
expected = (
|
|
2493
|
+
[["rustfmt", "main.rs"]] if fix else [["rustfmt", "--check", "main.rs"]]
|
|
2494
|
+
)
|
|
2495
|
+
assert [
|
|
2496
|
+
invocation.args[0] for invocation in mock_run.call_args_list
|
|
2497
|
+
] == expected
|
|
2498
|
+
|
|
2499
|
+
|
|
2500
|
+
@patch("shutil.which")
|
|
2501
|
+
@patch("subprocess.run")
|
|
2502
|
+
def test_shell_tools_scope_a_directory_to_shell_files(
|
|
2503
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
2504
|
+
) -> None:
|
|
2505
|
+
"""Shell tools only receive shell files under checked directories."""
|
|
2506
|
+
# Allow shfmt and shellcheck to appear installed
|
|
2507
|
+
mock_which.side_effect = lambda name: (
|
|
2508
|
+
name if name in ("shfmt", "shellcheck") else None
|
|
2509
|
+
)
|
|
2510
|
+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
2511
|
+
|
|
2512
|
+
# Populate directory with shell and non-shell files
|
|
2513
|
+
runner = CliRunner()
|
|
2514
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2515
|
+
Path("src/nested").mkdir(parents=True)
|
|
2516
|
+
Path("src/script.sh").touch()
|
|
2517
|
+
Path("src/nested/setup.bash").touch()
|
|
2518
|
+
Path("src/notes.txt").touch()
|
|
2519
|
+
result = runner.invoke(cli, ["check", "src"])
|
|
2520
|
+
|
|
2521
|
+
# Verify tool calls are scoped to matching files
|
|
2522
|
+
assert result.exit_code == 0
|
|
2523
|
+
expected_files = ["src/nested/setup.bash", "src/script.sh"]
|
|
2524
|
+
assert [invocation.args[0] for invocation in mock_run.call_args_list] == [
|
|
2525
|
+
["shfmt", "-i", "2", "-ci", "-d", *expected_files],
|
|
2526
|
+
["shellcheck", *expected_files],
|
|
2527
|
+
]
|
|
2528
|
+
|
|
2529
|
+
|
|
2530
|
+
@patch("shutil.which")
|
|
2531
|
+
@patch("subprocess.run")
|
|
2532
|
+
def test_rustfmt_scopes_a_directory_to_rust_files(
|
|
2533
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
2534
|
+
) -> None:
|
|
2535
|
+
"""Rustfmt only receives Rust files under checked directories."""
|
|
2536
|
+
# Allow rustfmt to appear installed
|
|
2537
|
+
mock_which.side_effect = lambda name: name if name == "rustfmt" else None
|
|
2538
|
+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
2539
|
+
|
|
2540
|
+
# Populate directory with Rust and non-Rust files
|
|
2541
|
+
runner = CliRunner()
|
|
2542
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2543
|
+
Path("src/nested").mkdir(parents=True)
|
|
2544
|
+
Path("src/main.rs").touch()
|
|
2545
|
+
Path("src/nested/lib.rs").touch()
|
|
2546
|
+
Path("src/notes.txt").touch()
|
|
2547
|
+
result = runner.invoke(cli, ["check", "src"])
|
|
2548
|
+
|
|
2549
|
+
# Verify tool calls are scoped to matching files
|
|
2550
|
+
assert result.exit_code == 0
|
|
2551
|
+
assert [invocation.args[0] for invocation in mock_run.call_args_list] == [
|
|
2552
|
+
["rustfmt", "--check", "src/main.rs", "src/nested/lib.rs"]
|
|
2553
|
+
]
|
|
2554
|
+
|
|
2555
|
+
|
|
2556
|
+
@patch("shutil.which")
|
|
2557
|
+
@patch("subprocess.run")
|
|
2558
|
+
def test_shfmt_formatting_findings_reported(
|
|
2559
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
2560
|
+
) -> None:
|
|
2561
|
+
"""Non-zero exit from shfmt diff is reported as a formatting finding."""
|
|
2562
|
+
# Allow shfmt to appear installed
|
|
2563
|
+
mock_which.side_effect = lambda name: (
|
|
2564
|
+
name if name in ("shfmt", "shellcheck") else None
|
|
2565
|
+
)
|
|
2566
|
+
mock_run.side_effect = [
|
|
2567
|
+
MagicMock(
|
|
2568
|
+
returncode=1, stdout="--- script.sh\n+++ script.sh\n", stderr=""
|
|
2569
|
+
),
|
|
2570
|
+
MagicMock(returncode=0, stdout="", stderr=""),
|
|
2571
|
+
]
|
|
2572
|
+
|
|
2573
|
+
# Run check command
|
|
2574
|
+
runner = CliRunner()
|
|
2575
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2576
|
+
Path("script.sh").touch()
|
|
2577
|
+
result = runner.invoke(cli, ["check", "script.sh"])
|
|
2578
|
+
|
|
2579
|
+
# Verify exit code and finding output
|
|
2580
|
+
assert result.exit_code == 1
|
|
2581
|
+
assert "shfmt formatting findings" in result.output
|
|
2582
|
+
|
|
2583
|
+
|
|
2584
|
+
@patch("shutil.which")
|
|
2585
|
+
@patch("subprocess.run")
|
|
2586
|
+
def test_shellcheck_findings_reported(
|
|
2587
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
2588
|
+
) -> None:
|
|
2589
|
+
"""Non-zero exit from shellcheck is reported as findings."""
|
|
2590
|
+
# Allow shellcheck to appear installed
|
|
2591
|
+
mock_which.side_effect = lambda name: (
|
|
2592
|
+
name if name in ("shfmt", "shellcheck") else None
|
|
2593
|
+
)
|
|
2594
|
+
mock_run.side_effect = [
|
|
2595
|
+
MagicMock(returncode=0, stdout="", stderr=""),
|
|
2596
|
+
MagicMock(
|
|
2597
|
+
returncode=1,
|
|
2598
|
+
stdout="SC2086: Double quote to prevent globbing",
|
|
2599
|
+
stderr="",
|
|
2600
|
+
),
|
|
2601
|
+
]
|
|
2602
|
+
|
|
2603
|
+
# Run check command
|
|
2604
|
+
runner = CliRunner()
|
|
2605
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2606
|
+
Path("script.sh").touch()
|
|
2607
|
+
result = runner.invoke(cli, ["check", "script.sh"])
|
|
2608
|
+
|
|
2609
|
+
# Verify exit code and finding output
|
|
2610
|
+
assert result.exit_code == 1
|
|
2611
|
+
assert "shellcheck findings" in result.output
|
|
2612
|
+
|
|
2613
|
+
|
|
2614
|
+
@patch("shutil.which")
|
|
2615
|
+
@patch("subprocess.run")
|
|
2616
|
+
def test_rustfmt_formatting_findings_reported(
|
|
2617
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
2618
|
+
) -> None:
|
|
2619
|
+
"""Non-zero exit from rustfmt check is reported as a formatting finding."""
|
|
2620
|
+
# Allow rustfmt to appear installed
|
|
2621
|
+
mock_which.side_effect = lambda name: name if name == "rustfmt" else None
|
|
2622
|
+
mock_run.return_value = MagicMock(
|
|
2623
|
+
returncode=1, stdout="Diff in main.rs", stderr=""
|
|
2624
|
+
)
|
|
2625
|
+
|
|
2626
|
+
# Run check command
|
|
2627
|
+
runner = CliRunner()
|
|
2628
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2629
|
+
Path("main.rs").touch()
|
|
2630
|
+
result = runner.invoke(cli, ["check", "main.rs"])
|
|
2631
|
+
|
|
2632
|
+
# Verify exit code and finding output
|
|
2633
|
+
assert result.exit_code == 1
|
|
2634
|
+
assert "rustfmt formatting findings" in result.output
|
|
2635
|
+
|
|
2636
|
+
|
|
2637
|
+
@patch("shutil.which", return_value=None)
|
|
2638
|
+
def test_missing_shell_tools_reported(
|
|
2639
|
+
mock_which: MagicMock, tmp_path: Path
|
|
2640
|
+
) -> None:
|
|
2641
|
+
"""Missing shell tools leave shell files unverified."""
|
|
2642
|
+
# Run check command with missing tools
|
|
2643
|
+
runner = CliRunner()
|
|
2644
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2645
|
+
Path("script.sh").touch()
|
|
2646
|
+
result = runner.invoke(cli, ["check", "script.sh"])
|
|
2647
|
+
|
|
2648
|
+
# Verify error message and exit code
|
|
2649
|
+
assert result.exit_code == 1
|
|
2650
|
+
assert "shfmt" in result.stderr
|
|
2651
|
+
assert "shellcheck" in result.stderr
|
|
2652
|
+
assert "nothing was verified" in result.stderr
|
|
2653
|
+
|
|
2654
|
+
|
|
2655
|
+
@patch("shutil.which", return_value=None)
|
|
2656
|
+
def test_missing_rustfmt_reported(
|
|
2657
|
+
mock_which: MagicMock, tmp_path: Path
|
|
2658
|
+
) -> None:
|
|
2659
|
+
"""Missing rustfmt leaves Rust files unverified."""
|
|
2660
|
+
# Run check command with missing rustfmt
|
|
2661
|
+
runner = CliRunner()
|
|
2662
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2663
|
+
Path("main.rs").touch()
|
|
2664
|
+
result = runner.invoke(cli, ["check", "main.rs"])
|
|
2665
|
+
|
|
2666
|
+
# Verify error message and exit code
|
|
2667
|
+
assert result.exit_code == 1
|
|
2668
|
+
assert "rustfmt" in result.stderr
|
|
2669
|
+
assert "nothing was verified" in result.stderr
|
|
2670
|
+
|
|
2671
|
+
|
|
2672
|
+
@patch("shutil.which")
|
|
2673
|
+
@patch("subprocess.run")
|
|
2674
|
+
def test_partial_shell_tools_installed_warns_and_runs_installed(
|
|
2675
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
2676
|
+
) -> None:
|
|
2677
|
+
"""A lone installed shell tool runs and warns that the other is absent."""
|
|
2678
|
+
# Only shfmt is installed
|
|
2679
|
+
mock_which.side_effect = lambda name: name if name == "shfmt" else None
|
|
2680
|
+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
2681
|
+
|
|
2682
|
+
# Run check command
|
|
2683
|
+
runner = CliRunner()
|
|
2684
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2685
|
+
Path("script.sh").touch()
|
|
2686
|
+
result = runner.invoke(cli, ["check", "script.sh"])
|
|
2687
|
+
|
|
2688
|
+
# Verify shfmt ran, shellcheck was skipped with warning, exit code 0
|
|
2689
|
+
assert result.exit_code == 0
|
|
2690
|
+
assert "shellcheck" in result.stderr
|
|
2691
|
+
assert [invocation.args[0] for invocation in mock_run.call_args_list] == [
|
|
2692
|
+
["shfmt", "-i", "2", "-ci", "-d", "script.sh"]
|
|
2693
|
+
]
|
|
@@ -523,7 +523,7 @@ def _shipped_guides() -> list[tuple[str, str]]:
|
|
|
523
523
|
if filename in seen or not os.path.exists(path):
|
|
524
524
|
continue
|
|
525
525
|
seen.add(filename)
|
|
526
|
-
with open(path,
|
|
526
|
+
with open(path, encoding="utf-8") as f:
|
|
527
527
|
guides.append((language, f.read()))
|
|
528
528
|
return guides
|
|
529
529
|
|
|
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
|