readability-cli 0.10.1__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.1 → readability_cli-0.10.3}/PKG-INFO +24 -12
- {readability_cli-0.10.1 → readability_cli-0.10.3}/README.md +23 -11
- {readability_cli-0.10.1 → readability_cli-0.10.3}/pyproject.toml +2 -2
- {readability_cli-0.10.1 → readability_cli-0.10.3}/readability/checking.py +16 -6
- {readability_cli-0.10.1 → readability_cli-0.10.3}/readability/cli.py +8 -2
- {readability_cli-0.10.1 → readability_cli-0.10.3}/readability/configs/ruff.toml +3 -2
- {readability_cli-0.10.1 → readability_cli-0.10.3}/readability/guide.py +2 -2
- {readability_cli-0.10.1 → readability_cli-0.10.3}/readability/tools.py +73 -3
- {readability_cli-0.10.1 → readability_cli-0.10.3}/test_checking.py +643 -4
- {readability_cli-0.10.1 → readability_cli-0.10.3}/test_outline.py +1 -1
- {readability_cli-0.10.1 → readability_cli-0.10.3}/uv.lock +1 -1
- {readability_cli-0.10.1 → readability_cli-0.10.3}/.github/workflows/ci.yml +0 -0
- {readability_cli-0.10.1 → readability_cli-0.10.3}/.github/workflows/publish.yml +0 -0
- {readability_cli-0.10.1 → readability_cli-0.10.3}/.github/workflows/update-guides.yml +0 -0
- {readability_cli-0.10.1 → readability_cli-0.10.3}/.gitignore +0 -0
- {readability_cli-0.10.1 → readability_cli-0.10.3}/.python-version +0 -0
- {readability_cli-0.10.1 → readability_cli-0.10.3}/LICENSE +0 -0
- {readability_cli-0.10.1 → readability_cli-0.10.3}/readability/__init__.py +0 -0
- {readability_cli-0.10.1 → readability_cli-0.10.3}/readability/configs/biome-default.json +0 -0
- {readability_cli-0.10.1 → readability_cli-0.10.3}/readability/configs/pyrefly.toml +0 -0
- {readability_cli-0.10.1 → readability_cli-0.10.3}/readability/guides/Rguide.md +0 -0
- {readability_cli-0.10.1 → readability_cli-0.10.3}/readability/guides/cppguide.md +0 -0
- {readability_cli-0.10.1 → readability_cli-0.10.3}/readability/guides/csharp-style.md +0 -0
- {readability_cli-0.10.1 → readability_cli-0.10.3}/readability/guides/docguide-style.md +0 -0
- {readability_cli-0.10.1 → readability_cli-0.10.3}/readability/guides/go-guide.md +0 -0
- {readability_cli-0.10.1 → readability_cli-0.10.3}/readability/guides/htmlcssguide.md +0 -0
- {readability_cli-0.10.1 → readability_cli-0.10.3}/readability/guides/javaguide.md +0 -0
- {readability_cli-0.10.1 → readability_cli-0.10.3}/readability/guides/jsguide.md +0 -0
- {readability_cli-0.10.1 → readability_cli-0.10.3}/readability/guides/jsoncstyleguide.md +0 -0
- {readability_cli-0.10.1 → readability_cli-0.10.3}/readability/guides/objcguide.md +0 -0
- {readability_cli-0.10.1 → readability_cli-0.10.3}/readability/guides/pyguide.md +0 -0
- {readability_cli-0.10.1 → readability_cli-0.10.3}/readability/guides/shellguide.md +0 -0
- {readability_cli-0.10.1 → readability_cli-0.10.3}/readability/guides/tsguide.md +0 -0
- {readability_cli-0.10.1 → readability_cli-0.10.3}/readability/guides/vimscriptguide.md +0 -0
- {readability_cli-0.10.1 → readability_cli-0.10.3}/readability/outline.py +0 -0
- {readability_cli-0.10.1 → readability_cli-0.10.3}/test_guide.py +0 -0
- {readability_cli-0.10.1 → 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
|
|
@@ -90,15 +91,25 @@ readability check src/ tests/ main.py
|
|
|
90
91
|
|
|
91
92
|
# Automatically fix and format files
|
|
92
93
|
readability check . --fix
|
|
94
|
+
|
|
95
|
+
# Also apply fixes that may change behavior (implies --fix)
|
|
96
|
+
readability check . --unsafe
|
|
93
97
|
```
|
|
94
98
|
|
|
99
|
+
Fixes that may change a program's behavior or drop its comments are opt-in
|
|
100
|
+
through `--unsafe`, which every tool applies under its own name: Ruff's
|
|
101
|
+
`--unsafe-fixes` and Biome's `--unsafe`. Tools drawing no such distinction,
|
|
102
|
+
such as gofmt and goimports, are unaffected by it.
|
|
103
|
+
|
|
95
104
|
### Supported Formats
|
|
96
105
|
|
|
97
106
|
| Formats | Owners | Project configuration |
|
|
98
107
|
| ---------------------------------------------------------------- | --------------------------------------- | ------------------------------------- |
|
|
99
108
|
| `.py` | Ruff lint/format; Pyrefly type checking | Ruff and Pyrefly native configuration |
|
|
100
109
|
| `.js`, `.jsx`, `.ts`, `.tsx`, `.json`, `.jsonc`, `.css`, `.html` | Biome | `biome.json` or `biome.jsonc` |
|
|
101
|
-
| `.go` | gofmt
|
|
110
|
+
| `.go` | gofmt, goimports | None |
|
|
111
|
+
| `.sh`, `.bash` | shfmt format; shellcheck lint | None |
|
|
112
|
+
| `.rs` | rustfmt | None |
|
|
102
113
|
|
|
103
114
|
Markdown, YAML, SCSS, JSONL, and extensions not listed above are unsupported.
|
|
104
115
|
An unsupported-only path reports that nothing was checked and exits
|
|
@@ -106,8 +117,8 @@ successfully.
|
|
|
106
117
|
|
|
107
118
|
Biome first checks a project's `node_modules/.bin`. Ruff, Pyrefly, and Biome
|
|
108
119
|
then use `PATH`, followed by `uvx` for the Python tools or `npx` for Biome. The
|
|
109
|
-
runners cache downloads, so subsequent runs work offline. Gofmt
|
|
110
|
-
|
|
120
|
+
runners cache downloads, so subsequent runs work offline. Gofmt and goimports
|
|
121
|
+
must be available on `PATH`.
|
|
111
122
|
|
|
112
123
|
That keeps this package at ~4 MB rather than the ~54 MB it would take to carry
|
|
113
124
|
Ruff and Pyrefly itself — a cost that would fall on everyone using only `guide`.
|
|
@@ -125,8 +136,8 @@ Set `UV_OFFLINE=1` to forbid fetching. A tool that then cannot be reached fails
|
|
|
125
136
|
the run rather than passing it.
|
|
126
137
|
|
|
127
138
|
Ruff, Pyrefly, and Biome ship with bundled configurations, so they run on every
|
|
128
|
-
file they own without project setup. Gofmt
|
|
129
|
-
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`.
|
|
130
141
|
|
|
131
142
|
A tool that could not be reached at all is never skipped quietly:
|
|
132
143
|
|
|
@@ -181,8 +192,9 @@ caller happened to be standing in.
|
|
|
181
192
|
Ruff and Pyrefly defaults follow the
|
|
182
193
|
[Google Python style guide](https://google.github.io/styleguide/pyguide.html):
|
|
183
194
|
80-column lines, Google docstrings, one import per line sorted as isort's
|
|
184
|
-
`google` profile does,
|
|
185
|
-
|
|
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
|
|
186
198
|
[Google JavaScript style guide](https://google.github.io/styleguide/jsguide.html)
|
|
187
199
|
and enables Biome's recommended lint rules. Project `biome.json` and
|
|
188
200
|
`biome.jsonc` files replace those bundled defaults for Biome-owned formats.
|
|
@@ -191,9 +203,9 @@ For Python, add `[tool.ruff]` or `[tool.pyrefly]` to `pyproject.toml`, or use
|
|
|
191
203
|
`ruff.toml`, `.ruff.toml`, or `pyrefly.toml`. Ruff, Pyrefly, and Biome load
|
|
192
204
|
their native project configurations in place of bundled defaults. Readability
|
|
193
205
|
does not interpret EditorConfig itself; a canonical tool such as Biome may opt
|
|
194
|
-
into it through that tool's native configuration.
|
|
195
|
-
The bundled Biome file requires Biome 2.5 or later,
|
|
196
|
-
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.
|
|
197
209
|
|
|
198
210
|
Configuration is found per file, in the file's own directory and then its
|
|
199
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
|
|
@@ -76,15 +77,25 @@ readability check src/ tests/ main.py
|
|
|
76
77
|
|
|
77
78
|
# Automatically fix and format files
|
|
78
79
|
readability check . --fix
|
|
80
|
+
|
|
81
|
+
# Also apply fixes that may change behavior (implies --fix)
|
|
82
|
+
readability check . --unsafe
|
|
79
83
|
```
|
|
80
84
|
|
|
85
|
+
Fixes that may change a program's behavior or drop its comments are opt-in
|
|
86
|
+
through `--unsafe`, which every tool applies under its own name: Ruff's
|
|
87
|
+
`--unsafe-fixes` and Biome's `--unsafe`. Tools drawing no such distinction,
|
|
88
|
+
such as gofmt and goimports, are unaffected by it.
|
|
89
|
+
|
|
81
90
|
### Supported Formats
|
|
82
91
|
|
|
83
92
|
| Formats | Owners | Project configuration |
|
|
84
93
|
| ---------------------------------------------------------------- | --------------------------------------- | ------------------------------------- |
|
|
85
94
|
| `.py` | Ruff lint/format; Pyrefly type checking | Ruff and Pyrefly native configuration |
|
|
86
95
|
| `.js`, `.jsx`, `.ts`, `.tsx`, `.json`, `.jsonc`, `.css`, `.html` | Biome | `biome.json` or `biome.jsonc` |
|
|
87
|
-
| `.go` | gofmt
|
|
96
|
+
| `.go` | gofmt, goimports | None |
|
|
97
|
+
| `.sh`, `.bash` | shfmt format; shellcheck lint | None |
|
|
98
|
+
| `.rs` | rustfmt | None |
|
|
88
99
|
|
|
89
100
|
Markdown, YAML, SCSS, JSONL, and extensions not listed above are unsupported.
|
|
90
101
|
An unsupported-only path reports that nothing was checked and exits
|
|
@@ -92,8 +103,8 @@ successfully.
|
|
|
92
103
|
|
|
93
104
|
Biome first checks a project's `node_modules/.bin`. Ruff, Pyrefly, and Biome
|
|
94
105
|
then use `PATH`, followed by `uvx` for the Python tools or `npx` for Biome. The
|
|
95
|
-
runners cache downloads, so subsequent runs work offline. Gofmt
|
|
96
|
-
|
|
106
|
+
runners cache downloads, so subsequent runs work offline. Gofmt and goimports
|
|
107
|
+
must be available on `PATH`.
|
|
97
108
|
|
|
98
109
|
That keeps this package at ~4 MB rather than the ~54 MB it would take to carry
|
|
99
110
|
Ruff and Pyrefly itself — a cost that would fall on everyone using only `guide`.
|
|
@@ -111,8 +122,8 @@ Set `UV_OFFLINE=1` to forbid fetching. A tool that then cannot be reached fails
|
|
|
111
122
|
the run rather than passing it.
|
|
112
123
|
|
|
113
124
|
Ruff, Pyrefly, and Biome ship with bundled configurations, so they run on every
|
|
114
|
-
file they own without project setup. Gofmt
|
|
115
|
-
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`.
|
|
116
127
|
|
|
117
128
|
A tool that could not be reached at all is never skipped quietly:
|
|
118
129
|
|
|
@@ -167,8 +178,9 @@ caller happened to be standing in.
|
|
|
167
178
|
Ruff and Pyrefly defaults follow the
|
|
168
179
|
[Google Python style guide](https://google.github.io/styleguide/pyguide.html):
|
|
169
180
|
80-column lines, Google docstrings, one import per line sorted as isort's
|
|
170
|
-
`google` profile does,
|
|
171
|
-
|
|
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
|
|
172
184
|
[Google JavaScript style guide](https://google.github.io/styleguide/jsguide.html)
|
|
173
185
|
and enables Biome's recommended lint rules. Project `biome.json` and
|
|
174
186
|
`biome.jsonc` files replace those bundled defaults for Biome-owned formats.
|
|
@@ -177,9 +189,9 @@ For Python, add `[tool.ruff]` or `[tool.pyrefly]` to `pyproject.toml`, or use
|
|
|
177
189
|
`ruff.toml`, `.ruff.toml`, or `pyrefly.toml`. Ruff, Pyrefly, and Biome load
|
|
178
190
|
their native project configurations in place of bundled defaults. Readability
|
|
179
191
|
does not interpret EditorConfig itself; a canonical tool such as Biome may opt
|
|
180
|
-
into it through that tool's native configuration.
|
|
181
|
-
The bundled Biome file requires Biome 2.5 or later,
|
|
182
|
-
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.
|
|
183
195
|
|
|
184
196
|
Configuration is found per file, in the file's own directory and then its
|
|
185
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]
|
|
@@ -62,6 +62,7 @@ def check_paths(
|
|
|
62
62
|
paths: Sequence[str | Path],
|
|
63
63
|
project_root: Path | None = None,
|
|
64
64
|
fix: bool = False,
|
|
65
|
+
unsafe: bool = False,
|
|
65
66
|
) -> CheckReport:
|
|
66
67
|
"""Run relevant checks for paths and aggregate what the tools did.
|
|
67
68
|
|
|
@@ -78,6 +79,8 @@ def check_paths(
|
|
|
78
79
|
and ignore files. Defaults to the repository the process is in.
|
|
79
80
|
It does not rebase paths.
|
|
80
81
|
fix: Whether to apply automatic fixes.
|
|
82
|
+
unsafe: Whether those fixes may change behavior or drop comments.
|
|
83
|
+
Tools that draw no such distinction are unaffected.
|
|
81
84
|
|
|
82
85
|
Returns:
|
|
83
86
|
A report aggregated across all provided paths.
|
|
@@ -100,7 +103,7 @@ def check_paths(
|
|
|
100
103
|
|
|
101
104
|
report = CheckReport()
|
|
102
105
|
for path in requested_paths:
|
|
103
|
-
path_report = _check_path(path, root, boundary, fix=fix)
|
|
106
|
+
path_report = _check_path(path, root, boundary, fix=fix, unsafe=unsafe)
|
|
104
107
|
if not path_report.ran:
|
|
105
108
|
path_report.unverified_paths.append(path)
|
|
106
109
|
report.absorb(path_report)
|
|
@@ -112,6 +115,7 @@ def _check_path(
|
|
|
112
115
|
project_root: Path,
|
|
113
116
|
boundary: Path | None = None,
|
|
114
117
|
fix: bool = False,
|
|
118
|
+
unsafe: bool = False,
|
|
115
119
|
) -> CheckReport:
|
|
116
120
|
"""Apply relevant tools to a single path.
|
|
117
121
|
|
|
@@ -121,6 +125,7 @@ def _check_path(
|
|
|
121
125
|
boundary: Outermost directory config discovery may consider, or
|
|
122
126
|
None to bound it by the path's own repository.
|
|
123
127
|
fix: Whether to apply automatic fixes.
|
|
128
|
+
unsafe: Whether those fixes may change behavior or drop comments.
|
|
124
129
|
|
|
125
130
|
Returns:
|
|
126
131
|
What the tools applicable to this path did.
|
|
@@ -141,7 +146,7 @@ def _check_path(
|
|
|
141
146
|
report.skipped.add(tool.name)
|
|
142
147
|
continue
|
|
143
148
|
|
|
144
|
-
report.absorb(_run_tool(tool, fix=fix))
|
|
149
|
+
report.absorb(_run_tool(tool, fix=fix, unsafe=unsafe))
|
|
145
150
|
|
|
146
151
|
return report
|
|
147
152
|
|
|
@@ -149,12 +154,14 @@ def _check_path(
|
|
|
149
154
|
def _run_tool(
|
|
150
155
|
tool: ToolPlan,
|
|
151
156
|
fix: bool = False,
|
|
157
|
+
unsafe: bool = False,
|
|
152
158
|
) -> CheckReport:
|
|
153
159
|
"""Orchestrate the execution of a specific formatting or linting tool.
|
|
154
160
|
|
|
155
161
|
Args:
|
|
156
162
|
tool: The executable plan to run.
|
|
157
163
|
fix: Whether to apply automatic fixes.
|
|
164
|
+
unsafe: Whether those fixes may change behavior or drop comments.
|
|
158
165
|
|
|
159
166
|
Returns:
|
|
160
167
|
What the tool did: whether it reported findings, and whether it
|
|
@@ -168,9 +175,11 @@ def _run_tool(
|
|
|
168
175
|
target_count = len(tool.targets or ())
|
|
169
176
|
try:
|
|
170
177
|
if fix:
|
|
178
|
+
# A tool with no unsafe mode still fixes what it safely can
|
|
179
|
+
unsafe_fix = tool.unsafe_fix if unsafe else ()
|
|
180
|
+
phases = (("format", tool.format), ("fix", unsafe_fix or tool.fix))
|
|
171
181
|
# Leftovers exit non-zero, which is a finding rather than a failure
|
|
172
|
-
for phase in
|
|
173
|
-
configured_command = getattr(tool, phase)
|
|
182
|
+
for phase, configured_command in phases:
|
|
174
183
|
if configured_command:
|
|
175
184
|
commands = _command_batches(
|
|
176
185
|
configured_command, target_count
|
|
@@ -195,9 +204,10 @@ def _run_tool(
|
|
|
195
204
|
result = _capture_tool_command(command, cwd=tool.cwd)
|
|
196
205
|
if _tool_checked_files(tool.name, result):
|
|
197
206
|
report.ran.add(tool.name)
|
|
198
|
-
# gofmt
|
|
207
|
+
# gofmt and goimports name files rather than using exit codes
|
|
199
208
|
if result.returncode != 0 or (
|
|
200
|
-
tool.name
|
|
209
|
+
tool.name in ("gofmt", "goimports")
|
|
210
|
+
and result.stdout.strip()
|
|
201
211
|
):
|
|
202
212
|
report.findings = True
|
|
203
213
|
click.echo(
|
|
@@ -167,8 +167,13 @@ def _echo_languages() -> None:
|
|
|
167
167
|
@click.option(
|
|
168
168
|
"--fix", is_flag=True, help="Automatically fix issues if possible."
|
|
169
169
|
)
|
|
170
|
+
@click.option(
|
|
171
|
+
"--unsafe",
|
|
172
|
+
is_flag=True,
|
|
173
|
+
help="Also apply fixes that may change behavior. Implies --fix.",
|
|
174
|
+
)
|
|
170
175
|
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose logging.")
|
|
171
|
-
def check(paths: Sequence[str], fix: bool, verbose: bool) -> None:
|
|
176
|
+
def check(paths: Sequence[str], fix: bool, unsafe: bool, verbose: bool) -> None:
|
|
172
177
|
"""Run relevant formatters and linters for given paths.
|
|
173
178
|
|
|
174
179
|
Exits with a non-zero status code if any tool reports findings, so the
|
|
@@ -177,7 +182,8 @@ def check(paths: Sequence[str], fix: bool, verbose: bool) -> None:
|
|
|
177
182
|
if verbose:
|
|
178
183
|
logger.setLevel(logging.DEBUG)
|
|
179
184
|
|
|
180
|
-
|
|
185
|
+
# Unsafe fixes are still fixes, so asking for them is asking to fix
|
|
186
|
+
report = check_paths(paths, fix=fix or unsafe, unsafe=unsafe)
|
|
181
187
|
|
|
182
188
|
# Coverage the caller does not know is missing reads as coverage
|
|
183
189
|
if report.skipped:
|
|
@@ -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)
|
|
@@ -21,6 +21,7 @@ class ToolPlan:
|
|
|
21
21
|
check: tuple[str, ...] = ()
|
|
22
22
|
check_format: tuple[str, ...] = ()
|
|
23
23
|
fix: tuple[str, ...] = ()
|
|
24
|
+
unsafe_fix: tuple[str, ...] = ()
|
|
24
25
|
format: tuple[str, ...] = ()
|
|
25
26
|
targets: tuple[str, ...] | None = None
|
|
26
27
|
cwd: Path | None = None
|
|
@@ -300,10 +301,16 @@ def _plan(
|
|
|
300
301
|
for exclude in excludes
|
|
301
302
|
for argument in (str(flag), str(exclude))
|
|
302
303
|
]
|
|
304
|
+
# Every tool spells its own opt-in, so callers only ask for the intent
|
|
305
|
+
words_by_phase: dict[str, tuple[str, ...]] = dict(TOOL_PHASES[tool])
|
|
306
|
+
unsafe_flag = UNSAFE_FLAGS.get(tool)
|
|
307
|
+
if unsafe_flag and "fix" in words_by_phase:
|
|
308
|
+
words_by_phase["unsafe_fix"] = (*words_by_phase["fix"], unsafe_flag)
|
|
309
|
+
|
|
303
310
|
# Named rather than unpacked, so the type checker still sees the fields
|
|
304
311
|
phases = {
|
|
305
312
|
phase: (*binary, *words, *config, *carved, *argv)
|
|
306
|
-
for phase, words in
|
|
313
|
+
for phase, words in words_by_phase.items()
|
|
307
314
|
}
|
|
308
315
|
return ToolPlan(
|
|
309
316
|
name=tool,
|
|
@@ -313,6 +320,7 @@ def _plan(
|
|
|
313
320
|
check=phases.get("check", ()),
|
|
314
321
|
check_format=phases.get("check_format", ()),
|
|
315
322
|
fix=phases.get("fix", ()),
|
|
323
|
+
unsafe_fix=phases.get("unsafe_fix", ()),
|
|
316
324
|
format=phases.get("format", ()),
|
|
317
325
|
)
|
|
318
326
|
|
|
@@ -339,6 +347,10 @@ TOOL_EXTENSIONS = {
|
|
|
339
347
|
".html",
|
|
340
348
|
),
|
|
341
349
|
"gofmt": (".go",),
|
|
350
|
+
"goimports": (".go",),
|
|
351
|
+
"shfmt": (".sh", ".bash"),
|
|
352
|
+
"shellcheck": (".sh", ".bash"),
|
|
353
|
+
"rustfmt": (".rs",),
|
|
342
354
|
}
|
|
343
355
|
|
|
344
356
|
# Headroom below common argv limits; long target lists are split before exec
|
|
@@ -346,8 +358,20 @@ MAX_COMMAND_BYTES = 16 * 1024
|
|
|
346
358
|
|
|
347
359
|
# Named files bypass a tool's own exclusions, and Pyrefly ignores its excludes
|
|
348
360
|
PRUNED_DIRECTORIES = frozenset(
|
|
349
|
-
|
|
350
|
-
|
|
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
|
+
]
|
|
351
375
|
)
|
|
352
376
|
|
|
353
377
|
# Per tool: config filenames, pyproject section (None if it has none), flag
|
|
@@ -384,8 +408,18 @@ TOOL_PHASES = {
|
|
|
384
408
|
"format": ("format", "--write", "--no-errors-on-unmatched"),
|
|
385
409
|
},
|
|
386
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": ()},
|
|
387
418
|
}
|
|
388
419
|
|
|
420
|
+
# Per tool: how it names fixes that may change behavior or drop comments
|
|
421
|
+
UNSAFE_FLAGS = {"ruff": "--unsafe-fixes", "biome": "--unsafe"}
|
|
422
|
+
|
|
389
423
|
|
|
390
424
|
def _matching_paths(path: Path, extensions: Sequence[str]) -> list[str]:
|
|
391
425
|
"""Return files under a requested path owned by one tool.
|
|
@@ -538,6 +572,42 @@ def _get_tool_definitions(
|
|
|
538
572
|
path,
|
|
539
573
|
)
|
|
540
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
|
+
)
|
|
541
611
|
|
|
542
612
|
# A directory owning its config alone lets Pyrefly select the files
|
|
543
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,
|
|
@@ -1080,8 +1093,16 @@ def test_check_paths_aggregates_str_and_path_inputs(
|
|
|
1080
1093
|
failed={"biome"},
|
|
1081
1094
|
)
|
|
1082
1095
|
assert mock_check_path.call_args_list == [
|
|
1083
|
-
call(
|
|
1084
|
-
|
|
1096
|
+
call(
|
|
1097
|
+
Path("src/example.py"),
|
|
1098
|
+
project_root,
|
|
1099
|
+
project_root,
|
|
1100
|
+
fix=True,
|
|
1101
|
+
unsafe=False,
|
|
1102
|
+
),
|
|
1103
|
+
call(
|
|
1104
|
+
Path("main.go"), project_root, project_root, fix=True, unsafe=False
|
|
1105
|
+
),
|
|
1085
1106
|
]
|
|
1086
1107
|
|
|
1087
1108
|
|
|
@@ -1128,7 +1149,7 @@ def test_check_paths_defaults_project_root_to_cwd(
|
|
|
1128
1149
|
check_paths([Path("example.py")])
|
|
1129
1150
|
|
|
1130
1151
|
mock_check_path.assert_called_once_with(
|
|
1131
|
-
Path("example.py"), tmp_path, None, fix=False
|
|
1152
|
+
Path("example.py"), tmp_path, None, fix=False, unsafe=False
|
|
1132
1153
|
)
|
|
1133
1154
|
|
|
1134
1155
|
|
|
@@ -1197,7 +1218,9 @@ def test_check_command_delegates_to_public_api(
|
|
|
1197
1218
|
Path("script.py").touch()
|
|
1198
1219
|
result = runner.invoke(cli, ["check", "script.py"])
|
|
1199
1220
|
|
|
1200
|
-
mock_check_paths.assert_called_once_with(
|
|
1221
|
+
mock_check_paths.assert_called_once_with(
|
|
1222
|
+
("script.py",), fix=False, unsafe=False
|
|
1223
|
+
)
|
|
1201
1224
|
assert result.exit_code == 0
|
|
1202
1225
|
assert result.stdout == ""
|
|
1203
1226
|
assert result.stderr == "No findings in 1 path(s) (ruff).\n"
|
|
@@ -1598,6 +1621,170 @@ def test_gofmt_fix_failure_is_reported(
|
|
|
1598
1621
|
assert "gofmt formatting findings" in result.output
|
|
1599
1622
|
|
|
1600
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
|
+
|
|
1601
1788
|
@patch("shutil.which")
|
|
1602
1789
|
@patch("subprocess.run")
|
|
1603
1790
|
def test_large_biome_directory_uses_bounded_commands(
|
|
@@ -1643,6 +1830,7 @@ def test_missing_gofmt_is_reported_for_configless_go(
|
|
|
1643
1830
|
|
|
1644
1831
|
assert result.exit_code == 1
|
|
1645
1832
|
assert "gofmt" in result.stderr
|
|
1833
|
+
assert "goimports" in result.stderr
|
|
1646
1834
|
assert "nothing was verified" in result.stderr
|
|
1647
1835
|
|
|
1648
1836
|
|
|
@@ -2052,3 +2240,454 @@ def test_fix_still_reports_what_it_could_not_fix(
|
|
|
2052
2240
|
commands = [call.args[0] for call in mock_run.call_args_list]
|
|
2053
2241
|
assert any("--fix" not in c and "check" in c for c in commands)
|
|
2054
2242
|
assert result.exit_code != 0
|
|
2243
|
+
|
|
2244
|
+
|
|
2245
|
+
@patch("shutil.which")
|
|
2246
|
+
@patch("subprocess.run")
|
|
2247
|
+
def test_unsafe_translates_to_the_ruff_flag(
|
|
2248
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
2249
|
+
) -> None:
|
|
2250
|
+
"""--unsafe reaches ruff under the name ruff gives it.
|
|
2251
|
+
|
|
2252
|
+
Args:
|
|
2253
|
+
mock_run: The mocked subprocess.run function.
|
|
2254
|
+
mock_which: The mocked shutil.which function.
|
|
2255
|
+
tmp_path: The temporary directory fixture.
|
|
2256
|
+
"""
|
|
2257
|
+
mock_which.side_effect = lambda name: name if name == "ruff" else None
|
|
2258
|
+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
2259
|
+
|
|
2260
|
+
runner = CliRunner()
|
|
2261
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2262
|
+
Path("pyproject.toml").touch()
|
|
2263
|
+
Path("script.py").touch()
|
|
2264
|
+
|
|
2265
|
+
result = runner.invoke(cli, ["check", "--fix", "--unsafe", "script.py"])
|
|
2266
|
+
|
|
2267
|
+
assert result.exit_code == 0
|
|
2268
|
+
cfg = str(_bundled_config("ruff"))
|
|
2269
|
+
called_cmds = [call.args[0] for call in mock_run.call_args_list]
|
|
2270
|
+
assert [
|
|
2271
|
+
"ruff",
|
|
2272
|
+
"check",
|
|
2273
|
+
"--fix",
|
|
2274
|
+
"--force-exclude",
|
|
2275
|
+
"--unsafe-fixes",
|
|
2276
|
+
"--config",
|
|
2277
|
+
cfg,
|
|
2278
|
+
"script.py",
|
|
2279
|
+
] in called_cmds
|
|
2280
|
+
# Formatters have no unsafe mode, so the flag must not reach them
|
|
2281
|
+
assert [
|
|
2282
|
+
"ruff",
|
|
2283
|
+
"format",
|
|
2284
|
+
"--force-exclude",
|
|
2285
|
+
"--config",
|
|
2286
|
+
cfg,
|
|
2287
|
+
"script.py",
|
|
2288
|
+
] in called_cmds
|
|
2289
|
+
|
|
2290
|
+
|
|
2291
|
+
@patch("shutil.which")
|
|
2292
|
+
@patch("subprocess.run")
|
|
2293
|
+
def test_unsafe_translates_to_the_biome_flag(
|
|
2294
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
2295
|
+
) -> None:
|
|
2296
|
+
"""Biome spells the same intent differently and gets its own spelling.
|
|
2297
|
+
|
|
2298
|
+
Args:
|
|
2299
|
+
mock_run: The mocked subprocess.run function.
|
|
2300
|
+
mock_which: The mocked shutil.which function.
|
|
2301
|
+
tmp_path: The temporary directory fixture.
|
|
2302
|
+
"""
|
|
2303
|
+
mock_which.side_effect = lambda name: name if name == "biome" else None
|
|
2304
|
+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
2305
|
+
|
|
2306
|
+
runner = CliRunner()
|
|
2307
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2308
|
+
Path("script.ts").touch()
|
|
2309
|
+
|
|
2310
|
+
result = runner.invoke(cli, ["check", "--fix", "--unsafe", "script.ts"])
|
|
2311
|
+
|
|
2312
|
+
assert result.exit_code == 0
|
|
2313
|
+
called_cmds = [call.args[0] for call in mock_run.call_args_list]
|
|
2314
|
+
lint = next(c for c in called_cmds if "lint" in c)
|
|
2315
|
+
assert lint[:5] == [
|
|
2316
|
+
"biome",
|
|
2317
|
+
"lint",
|
|
2318
|
+
"--write",
|
|
2319
|
+
"--no-errors-on-unmatched",
|
|
2320
|
+
"--unsafe",
|
|
2321
|
+
]
|
|
2322
|
+
|
|
2323
|
+
|
|
2324
|
+
@patch("shutil.which")
|
|
2325
|
+
@patch("subprocess.run")
|
|
2326
|
+
def test_unsafe_implies_fix(
|
|
2327
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
2328
|
+
) -> None:
|
|
2329
|
+
"""Asking for unsafe fixes without --fix asks for nothing otherwise.
|
|
2330
|
+
|
|
2331
|
+
Args:
|
|
2332
|
+
mock_run: The mocked subprocess.run function.
|
|
2333
|
+
mock_which: The mocked shutil.which function.
|
|
2334
|
+
tmp_path: The temporary directory fixture.
|
|
2335
|
+
"""
|
|
2336
|
+
mock_which.side_effect = lambda name: name if name == "ruff" else None
|
|
2337
|
+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
2338
|
+
|
|
2339
|
+
runner = CliRunner()
|
|
2340
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2341
|
+
Path("pyproject.toml").touch()
|
|
2342
|
+
Path("script.py").touch()
|
|
2343
|
+
|
|
2344
|
+
result = runner.invoke(cli, ["check", "--unsafe", "script.py"])
|
|
2345
|
+
|
|
2346
|
+
assert result.exit_code == 0
|
|
2347
|
+
called_cmds = [call.args[0] for call in mock_run.call_args_list]
|
|
2348
|
+
assert any("--unsafe-fixes" in command for command in called_cmds)
|
|
2349
|
+
|
|
2350
|
+
|
|
2351
|
+
@patch("shutil.which")
|
|
2352
|
+
@patch("subprocess.run")
|
|
2353
|
+
def test_unsafe_is_a_no_op_for_tools_without_one(
|
|
2354
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
2355
|
+
) -> None:
|
|
2356
|
+
"""A tool with no unsafe mode still fixes rather than being skipped.
|
|
2357
|
+
|
|
2358
|
+
Args:
|
|
2359
|
+
mock_run: The mocked subprocess.run function.
|
|
2360
|
+
mock_which: The mocked shutil.which function.
|
|
2361
|
+
tmp_path: The temporary directory fixture.
|
|
2362
|
+
"""
|
|
2363
|
+
mock_which.side_effect = lambda name: name if name == "gofmt" else None
|
|
2364
|
+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
2365
|
+
|
|
2366
|
+
runner = CliRunner()
|
|
2367
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2368
|
+
Path("main.go").touch()
|
|
2369
|
+
|
|
2370
|
+
result = runner.invoke(cli, ["check", "--fix", "--unsafe", "main.go"])
|
|
2371
|
+
|
|
2372
|
+
assert result.exit_code == 0
|
|
2373
|
+
assert [invocation.args[0] for invocation in mock_run.call_args_list] == [
|
|
2374
|
+
["gofmt", "-w", "main.go"]
|
|
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
|