readability-cli 0.8.1__tar.gz → 0.8.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.8.1 → readability_cli-0.8.3}/PKG-INFO +29 -18
- {readability_cli-0.8.1 → readability_cli-0.8.3}/README.md +28 -17
- readability_cli-0.8.3/configs/biome-default.json +14 -0
- {readability_cli-0.8.1 → readability_cli-0.8.3}/pyproject.toml +1 -1
- {readability_cli-0.8.1 → readability_cli-0.8.3}/readability.py +111 -41
- {readability_cli-0.8.1 → readability_cli-0.8.3}/test_readability.py +287 -10
- {readability_cli-0.8.1 → readability_cli-0.8.3}/uv.lock +1 -1
- {readability_cli-0.8.1 → readability_cli-0.8.3}/.github/workflows/ci.yml +0 -0
- {readability_cli-0.8.1 → readability_cli-0.8.3}/.github/workflows/publish.yml +0 -0
- {readability_cli-0.8.1 → readability_cli-0.8.3}/.github/workflows/update-guides.yml +0 -0
- {readability_cli-0.8.1 → readability_cli-0.8.3}/.gitignore +0 -0
- {readability_cli-0.8.1 → readability_cli-0.8.3}/.python-version +0 -0
- {readability_cli-0.8.1 → readability_cli-0.8.3}/LICENSE +0 -0
- {readability_cli-0.8.1 → readability_cli-0.8.3}/configs/pyrefly.toml +0 -0
- {readability_cli-0.8.1 → readability_cli-0.8.3}/configs/ruff.toml +0 -0
- {readability_cli-0.8.1 → readability_cli-0.8.3}/guides/Rguide.md +0 -0
- {readability_cli-0.8.1 → readability_cli-0.8.3}/guides/cppguide.md +0 -0
- {readability_cli-0.8.1 → readability_cli-0.8.3}/guides/csharp-style.md +0 -0
- {readability_cli-0.8.1 → readability_cli-0.8.3}/guides/docguide-style.md +0 -0
- {readability_cli-0.8.1 → readability_cli-0.8.3}/guides/go-guide.md +0 -0
- {readability_cli-0.8.1 → readability_cli-0.8.3}/guides/htmlcssguide.md +0 -0
- {readability_cli-0.8.1 → readability_cli-0.8.3}/guides/javaguide.md +0 -0
- {readability_cli-0.8.1 → readability_cli-0.8.3}/guides/jsguide.md +0 -0
- {readability_cli-0.8.1 → readability_cli-0.8.3}/guides/jsoncstyleguide.md +0 -0
- {readability_cli-0.8.1 → readability_cli-0.8.3}/guides/objcguide.md +0 -0
- {readability_cli-0.8.1 → readability_cli-0.8.3}/guides/pyguide.md +0 -0
- {readability_cli-0.8.1 → readability_cli-0.8.3}/guides/shellguide.md +0 -0
- {readability_cli-0.8.1 → readability_cli-0.8.3}/guides/tsguide.md +0 -0
- {readability_cli-0.8.1 → readability_cli-0.8.3}/guides/vimscriptguide.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: readability-cli
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.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
|
|
@@ -24,8 +24,9 @@ and quick access to style conventions without browsing HTML pages.
|
|
|
24
24
|
|
|
25
25
|
- **Linting & Formatting**: A `check` command that automatically detects and
|
|
26
26
|
runs relevant tools (Ruff, Pyrefly, Biome, Prettier, gofmt) for your project.
|
|
27
|
-
- **Sensible Defaults**: Bundled Google-style configurations for Ruff
|
|
28
|
-
Pyrefly are used automatically when a project does not define its
|
|
27
|
+
- **Sensible Defaults**: Bundled Google-style configurations for Ruff,
|
|
28
|
+
Pyrefly, and Biome are used automatically when a project does not define its
|
|
29
|
+
own.
|
|
29
30
|
- **Style Guides**: A `guide` command that fetches the latest Google style
|
|
30
31
|
guides (Python, Shell, C++, Java, JS/TS, Go, etc.) converted to Markdown,
|
|
31
32
|
and outlines, addresses, and searches them by section rather than serving
|
|
@@ -98,7 +99,7 @@ readability check . --fix
|
|
|
98
99
|
|------|----------------------|---------------|------------|
|
|
99
100
|
| **Ruff** | `.py` | none, bundled | `uvx` |
|
|
100
101
|
| **Pyrefly** | `.py` | none, bundled | `uvx` |
|
|
101
|
-
| **Biome** | `.js`, `.ts`, `.jsx`, `.tsx`, `.json`, `.jsonc`, `.css`, `.html` |
|
|
102
|
+
| **Biome** | `.js`, `.ts`, `.jsx`, `.tsx`, `.json`, `.jsonc`, `.css`, `.html` | none, bundled | `npx` |
|
|
102
103
|
| **Prettier** | `.js`, `.ts`, `.jsx`, `.tsx`, `.json`, `.css`, `.scss`, `.html`, `.md`, `.yml`, `.yaml` | `.prettierrc*` or `prettier.config.*` | `npx` |
|
|
103
104
|
| **gofmt** | `.go` | `go.mod` | — |
|
|
104
105
|
|
|
@@ -123,10 +124,9 @@ always wins over a fetched one.
|
|
|
123
124
|
Set `UV_OFFLINE=1` to forbid fetching. A tool that then cannot be reached
|
|
124
125
|
fails the run rather than passing it.
|
|
125
126
|
|
|
126
|
-
Ruff and
|
|
127
|
-
they handle without the project arranging anything.
|
|
128
|
-
bring no defaults here, so they wait until the project asks with a config
|
|
129
|
-
file.
|
|
127
|
+
Ruff, Pyrefly, and Biome ship with bundled configurations, so they run on any
|
|
128
|
+
file they handle without the project arranging anything. Prettier and gofmt
|
|
129
|
+
bring no defaults here, so they wait until the project asks with a config file.
|
|
130
130
|
|
|
131
131
|
A tool that could not be reached at all is never skipped quietly:
|
|
132
132
|
|
|
@@ -144,8 +144,10 @@ Error: Every tool for 1 path(s) is missing, so nothing was verified.
|
|
|
144
144
|
That second case needs neither the tool nor a runner to be present, which is
|
|
145
145
|
the state a container image is usually in.
|
|
146
146
|
|
|
147
|
-
`check` exits non-zero on findings
|
|
148
|
-
gate CI without a clean exit
|
|
147
|
+
`check` exits non-zero on findings, tool failures, and when every applicable
|
|
148
|
+
tool is absent, so it can gate CI without a clean exit meaning required tools
|
|
149
|
+
were missing. If no tool applies to the requested paths, it reports that fact
|
|
150
|
+
and exits successfully.
|
|
149
151
|
|
|
150
152
|
### Python API
|
|
151
153
|
|
|
@@ -164,17 +166,26 @@ if report.findings or report.failed or not report.ran:
|
|
|
164
166
|
|
|
165
167
|
The returned `CheckReport` records only whether findings occurred and which
|
|
166
168
|
tools ran, were skipped, or failed. Detailed tool findings are still written
|
|
167
|
-
as each tool runs.
|
|
169
|
+
as each tool runs. Every path is validated before any tool runs, and a missing
|
|
170
|
+
one raises `FileNotFoundError` rather than being misreported as a finding.
|
|
171
|
+
Relative paths remain relative to the process working directory;
|
|
172
|
+
`project_root` controls configuration discovery only.
|
|
168
173
|
|
|
169
174
|
### Default Configurations
|
|
170
175
|
|
|
171
|
-
|
|
172
|
-
[Google Python style guide](https://google.github.io/styleguide/pyguide.html)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
project
|
|
176
|
+
Ruff and Pyrefly defaults follow the
|
|
177
|
+
[Google Python style guide](https://google.github.io/styleguide/pyguide.html):
|
|
178
|
+
80-column lines, Google docstrings, import ordering, and full type checking.
|
|
179
|
+
The Biome default applies the 80-column lines and two-space indentation of the
|
|
180
|
+
[Google JavaScript style guide](https://google.github.io/styleguide/jsguide.html)
|
|
181
|
+
and enables Biome's recommended lint rules. These defaults apply only when the
|
|
182
|
+
project does not define its own configuration. To override them, add a
|
|
183
|
+
`[tool.ruff]` or `[tool.pyrefly]` section to your `pyproject.toml`, or a
|
|
184
|
+
dedicated `ruff.toml`, `pyrefly.toml`, `biome.json`, or `biome.jsonc` — any
|
|
185
|
+
project-level configuration takes full precedence. The bundled Biome file
|
|
186
|
+
intentionally uses only settings supported by both Biome 1.x and 2.x, so a
|
|
187
|
+
project-local installation remains authoritative without creating a schema
|
|
188
|
+
mismatch.
|
|
178
189
|
|
|
179
190
|
## Style Guides
|
|
180
191
|
|
|
@@ -10,8 +10,9 @@ and quick access to style conventions without browsing HTML pages.
|
|
|
10
10
|
|
|
11
11
|
- **Linting & Formatting**: A `check` command that automatically detects and
|
|
12
12
|
runs relevant tools (Ruff, Pyrefly, Biome, Prettier, gofmt) for your project.
|
|
13
|
-
- **Sensible Defaults**: Bundled Google-style configurations for Ruff
|
|
14
|
-
Pyrefly are used automatically when a project does not define its
|
|
13
|
+
- **Sensible Defaults**: Bundled Google-style configurations for Ruff,
|
|
14
|
+
Pyrefly, and Biome are used automatically when a project does not define its
|
|
15
|
+
own.
|
|
15
16
|
- **Style Guides**: A `guide` command that fetches the latest Google style
|
|
16
17
|
guides (Python, Shell, C++, Java, JS/TS, Go, etc.) converted to Markdown,
|
|
17
18
|
and outlines, addresses, and searches them by section rather than serving
|
|
@@ -84,7 +85,7 @@ readability check . --fix
|
|
|
84
85
|
|------|----------------------|---------------|------------|
|
|
85
86
|
| **Ruff** | `.py` | none, bundled | `uvx` |
|
|
86
87
|
| **Pyrefly** | `.py` | none, bundled | `uvx` |
|
|
87
|
-
| **Biome** | `.js`, `.ts`, `.jsx`, `.tsx`, `.json`, `.jsonc`, `.css`, `.html` |
|
|
88
|
+
| **Biome** | `.js`, `.ts`, `.jsx`, `.tsx`, `.json`, `.jsonc`, `.css`, `.html` | none, bundled | `npx` |
|
|
88
89
|
| **Prettier** | `.js`, `.ts`, `.jsx`, `.tsx`, `.json`, `.css`, `.scss`, `.html`, `.md`, `.yml`, `.yaml` | `.prettierrc*` or `prettier.config.*` | `npx` |
|
|
89
90
|
| **gofmt** | `.go` | `go.mod` | — |
|
|
90
91
|
|
|
@@ -109,10 +110,9 @@ always wins over a fetched one.
|
|
|
109
110
|
Set `UV_OFFLINE=1` to forbid fetching. A tool that then cannot be reached
|
|
110
111
|
fails the run rather than passing it.
|
|
111
112
|
|
|
112
|
-
Ruff and
|
|
113
|
-
they handle without the project arranging anything.
|
|
114
|
-
bring no defaults here, so they wait until the project asks with a config
|
|
115
|
-
file.
|
|
113
|
+
Ruff, Pyrefly, and Biome ship with bundled configurations, so they run on any
|
|
114
|
+
file they handle without the project arranging anything. Prettier and gofmt
|
|
115
|
+
bring no defaults here, so they wait until the project asks with a config file.
|
|
116
116
|
|
|
117
117
|
A tool that could not be reached at all is never skipped quietly:
|
|
118
118
|
|
|
@@ -130,8 +130,10 @@ Error: Every tool for 1 path(s) is missing, so nothing was verified.
|
|
|
130
130
|
That second case needs neither the tool nor a runner to be present, which is
|
|
131
131
|
the state a container image is usually in.
|
|
132
132
|
|
|
133
|
-
`check` exits non-zero on findings
|
|
134
|
-
gate CI without a clean exit
|
|
133
|
+
`check` exits non-zero on findings, tool failures, and when every applicable
|
|
134
|
+
tool is absent, so it can gate CI without a clean exit meaning required tools
|
|
135
|
+
were missing. If no tool applies to the requested paths, it reports that fact
|
|
136
|
+
and exits successfully.
|
|
135
137
|
|
|
136
138
|
### Python API
|
|
137
139
|
|
|
@@ -150,17 +152,26 @@ if report.findings or report.failed or not report.ran:
|
|
|
150
152
|
|
|
151
153
|
The returned `CheckReport` records only whether findings occurred and which
|
|
152
154
|
tools ran, were skipped, or failed. Detailed tool findings are still written
|
|
153
|
-
as each tool runs.
|
|
155
|
+
as each tool runs. Every path is validated before any tool runs, and a missing
|
|
156
|
+
one raises `FileNotFoundError` rather than being misreported as a finding.
|
|
157
|
+
Relative paths remain relative to the process working directory;
|
|
158
|
+
`project_root` controls configuration discovery only.
|
|
154
159
|
|
|
155
160
|
### Default Configurations
|
|
156
161
|
|
|
157
|
-
|
|
158
|
-
[Google Python style guide](https://google.github.io/styleguide/pyguide.html)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
project
|
|
162
|
+
Ruff and Pyrefly defaults follow the
|
|
163
|
+
[Google Python style guide](https://google.github.io/styleguide/pyguide.html):
|
|
164
|
+
80-column lines, Google docstrings, import ordering, and full type checking.
|
|
165
|
+
The Biome default applies the 80-column lines and two-space indentation of the
|
|
166
|
+
[Google JavaScript style guide](https://google.github.io/styleguide/jsguide.html)
|
|
167
|
+
and enables Biome's recommended lint rules. These defaults apply only when the
|
|
168
|
+
project does not define its own configuration. To override them, add a
|
|
169
|
+
`[tool.ruff]` or `[tool.pyrefly]` section to your `pyproject.toml`, or a
|
|
170
|
+
dedicated `ruff.toml`, `pyrefly.toml`, `biome.json`, or `biome.jsonc` — any
|
|
171
|
+
project-level configuration takes full precedence. The bundled Biome file
|
|
172
|
+
intentionally uses only settings supported by both Biome 1.x and 2.x, so a
|
|
173
|
+
project-local installation remains authoritative without creating a schema
|
|
174
|
+
mismatch.
|
|
164
175
|
|
|
165
176
|
## Style Guides
|
|
166
177
|
|
|
@@ -1116,6 +1116,11 @@ def check(paths: Sequence[str], fix: bool, verbose: bool) -> None:
|
|
|
1116
1116
|
)
|
|
1117
1117
|
sys.exit(1)
|
|
1118
1118
|
|
|
1119
|
+
# Findings remain a failed check even when a tool reports that it could
|
|
1120
|
+
# not process any files (for example, a Biome configuration error).
|
|
1121
|
+
if report.findings:
|
|
1122
|
+
sys.exit(1)
|
|
1123
|
+
|
|
1119
1124
|
# Having run nothing is not a pass. Reporting it as one is how this
|
|
1120
1125
|
# command became a silent no-op wherever its tools were absent, gating
|
|
1121
1126
|
# nothing while every caller read the exit code as approval.
|
|
@@ -1137,13 +1142,19 @@ def check(paths: Sequence[str], fix: bool, verbose: bool) -> None:
|
|
|
1137
1142
|
)
|
|
1138
1143
|
return
|
|
1139
1144
|
|
|
1140
|
-
|
|
1141
|
-
|
|
1145
|
+
checked_path_count = len(paths) - len(report.unverified_paths)
|
|
1146
|
+
if report.unverified_paths:
|
|
1147
|
+
click.echo(
|
|
1148
|
+
f"Warning: nothing was checked for "
|
|
1149
|
+
f"{len(report.unverified_paths)} path(s): "
|
|
1150
|
+
f"{', '.join(map(str, report.unverified_paths))}.",
|
|
1151
|
+
err=True,
|
|
1152
|
+
)
|
|
1142
1153
|
|
|
1143
1154
|
# Findings are the only thing this command printed, so a clean run said
|
|
1144
1155
|
# nothing at all and left the caller unable to tell it from a no-op.
|
|
1145
1156
|
click.echo(
|
|
1146
|
-
f"No findings in {
|
|
1157
|
+
f"No findings in {checked_path_count} path(s) "
|
|
1147
1158
|
f"({', '.join(sorted(report.ran))}).",
|
|
1148
1159
|
err=True,
|
|
1149
1160
|
)
|
|
@@ -1155,21 +1166,25 @@ class CheckReport:
|
|
|
1155
1166
|
|
|
1156
1167
|
Attributes:
|
|
1157
1168
|
findings: Whether any tool that ran reported something.
|
|
1158
|
-
ran: Names of the tools that
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1169
|
+
ran: Names of the tools that processed at least one file. This records
|
|
1170
|
+
what happened, not what was intended. A tool that resolved but
|
|
1171
|
+
never started belongs in failed, and a tool that explicitly
|
|
1172
|
+
reported processing no files is not recorded as having run.
|
|
1162
1173
|
skipped: Names of the tools that were applicable but not installed.
|
|
1163
1174
|
Tools with no trigger file are in neither set: they were never
|
|
1164
1175
|
wanted, so passing without them is not a gap in coverage.
|
|
1165
1176
|
failed: Names of the tools that started and could not finish, by
|
|
1166
1177
|
failing to exec or by running past the timeout.
|
|
1178
|
+
unverified_paths: Requested paths for which no tool processed a file.
|
|
1179
|
+
Keeping these paths through aggregation prevents a checked path
|
|
1180
|
+
from hiding another path that a tool ignored.
|
|
1167
1181
|
"""
|
|
1168
1182
|
|
|
1169
1183
|
findings: bool = False
|
|
1170
1184
|
ran: set[str] = dataclasses.field(default_factory=set)
|
|
1171
1185
|
skipped: set[str] = dataclasses.field(default_factory=set)
|
|
1172
1186
|
failed: set[str] = dataclasses.field(default_factory=set)
|
|
1187
|
+
unverified_paths: list[Path] = dataclasses.field(default_factory=list)
|
|
1173
1188
|
|
|
1174
1189
|
def absorb(self, other: "CheckReport") -> None:
|
|
1175
1190
|
"""Fold another report into this one.
|
|
@@ -1181,6 +1196,7 @@ class CheckReport:
|
|
|
1181
1196
|
self.ran |= other.ran
|
|
1182
1197
|
self.skipped |= other.skipped
|
|
1183
1198
|
self.failed |= other.failed
|
|
1199
|
+
self.unverified_paths.extend(other.unverified_paths)
|
|
1184
1200
|
|
|
1185
1201
|
|
|
1186
1202
|
def check_paths(
|
|
@@ -1196,18 +1212,33 @@ def check_paths(
|
|
|
1196
1212
|
the result.
|
|
1197
1213
|
|
|
1198
1214
|
Args:
|
|
1199
|
-
paths: Files or directories to check, as strings or paths.
|
|
1200
|
-
|
|
1201
|
-
|
|
1215
|
+
paths: Files or directories to check, as strings or paths. Relative
|
|
1216
|
+
paths are interpreted from the current working directory.
|
|
1217
|
+
project_root: Root used only to discover tool configuration. Defaults
|
|
1218
|
+
to the current working directory; it does not rebase paths.
|
|
1202
1219
|
fix: Whether to apply automatic fixes.
|
|
1203
1220
|
|
|
1204
1221
|
Returns:
|
|
1205
1222
|
A report aggregated across all provided paths.
|
|
1223
|
+
|
|
1224
|
+
Raises:
|
|
1225
|
+
FileNotFoundError: If any requested path does not exist. Every path is
|
|
1226
|
+
validated before any tools run.
|
|
1206
1227
|
"""
|
|
1207
1228
|
root = Path.cwd() if project_root is None else project_root
|
|
1229
|
+
requested_paths = [Path(path) for path in paths]
|
|
1230
|
+
missing_path = next(
|
|
1231
|
+
(path for path in requested_paths if not path.exists()), None
|
|
1232
|
+
)
|
|
1233
|
+
if missing_path is not None:
|
|
1234
|
+
raise FileNotFoundError(f"Path does not exist: {missing_path}")
|
|
1235
|
+
|
|
1208
1236
|
report = CheckReport()
|
|
1209
|
-
for path in
|
|
1210
|
-
|
|
1237
|
+
for path in requested_paths:
|
|
1238
|
+
path_report = _check_path(path, root, fix=fix)
|
|
1239
|
+
if not path_report.ran:
|
|
1240
|
+
path_report.unverified_paths.append(path)
|
|
1241
|
+
report.absorb(path_report)
|
|
1211
1242
|
return report
|
|
1212
1243
|
|
|
1213
1244
|
|
|
@@ -1266,12 +1297,20 @@ def _should_run_tool(
|
|
|
1266
1297
|
(project_root / t).exists() for t in tool["trigger"]
|
|
1267
1298
|
)
|
|
1268
1299
|
|
|
1269
|
-
#
|
|
1300
|
+
# Files must match one of the tool's supported extensions.
|
|
1270
1301
|
if path.is_file():
|
|
1271
1302
|
return has_trigger and path.suffix in tool["extensions"]
|
|
1272
1303
|
|
|
1273
|
-
|
|
1274
|
-
|
|
1304
|
+
if not has_trigger:
|
|
1305
|
+
return False
|
|
1306
|
+
|
|
1307
|
+
# A directory trigger alone is not evidence that the tool checked a file.
|
|
1308
|
+
# Several tools deliberately exit zero when nothing matches, which would
|
|
1309
|
+
# otherwise turn an unsupported-only tree into a reported clean result.
|
|
1310
|
+
return any(
|
|
1311
|
+
candidate.is_file() and candidate.suffix in tool["extensions"]
|
|
1312
|
+
for candidate in path.rglob("*")
|
|
1313
|
+
)
|
|
1275
1314
|
|
|
1276
1315
|
|
|
1277
1316
|
def _tool_is_installed(tool_name: str, tool_config: dict[str, Any]) -> bool:
|
|
@@ -1307,12 +1346,15 @@ def _bundled_config(tool_name: str) -> Path:
|
|
|
1307
1346
|
"""Get the path to the bundled default configuration for a tool.
|
|
1308
1347
|
|
|
1309
1348
|
Args:
|
|
1310
|
-
tool_name: The name of the tool (e.g. "ruff", "pyrefly").
|
|
1349
|
+
tool_name: The name of the tool (e.g. "ruff", "pyrefly", "biome").
|
|
1311
1350
|
|
|
1312
1351
|
Returns:
|
|
1313
1352
|
The path to the bundled default config file.
|
|
1314
1353
|
"""
|
|
1315
|
-
|
|
1354
|
+
filename = (
|
|
1355
|
+
"biome-default.json" if tool_name == "biome" else f"{tool_name}.toml"
|
|
1356
|
+
)
|
|
1357
|
+
return Path(__file__).parent / "configs" / filename
|
|
1316
1358
|
|
|
1317
1359
|
|
|
1318
1360
|
def _has_project_config(
|
|
@@ -1444,6 +1486,26 @@ def _get_tool_definitions(
|
|
|
1444
1486
|
pyrefly_config = _default_config_args(
|
|
1445
1487
|
project_root, ["pyrefly.toml"], "pyrefly"
|
|
1446
1488
|
)
|
|
1489
|
+
biome_config = []
|
|
1490
|
+
if not any(
|
|
1491
|
+
(project_root / filename).exists()
|
|
1492
|
+
for filename in ("biome.json", "biome.jsonc")
|
|
1493
|
+
):
|
|
1494
|
+
biome_config = ["--config-path", str(_bundled_config("biome"))]
|
|
1495
|
+
ignore_files = (
|
|
1496
|
+
project_root / ".gitignore",
|
|
1497
|
+
project_root / ".ignore",
|
|
1498
|
+
project_root / ".git" / "info" / "exclude",
|
|
1499
|
+
)
|
|
1500
|
+
if any(ignore_file.is_file() for ignore_file in ignore_files):
|
|
1501
|
+
biome_config.extend(
|
|
1502
|
+
[
|
|
1503
|
+
"--vcs-enabled=true",
|
|
1504
|
+
"--vcs-client-kind=git",
|
|
1505
|
+
"--vcs-use-ignore-file=true",
|
|
1506
|
+
f"--vcs-root={project_root}",
|
|
1507
|
+
]
|
|
1508
|
+
)
|
|
1447
1509
|
|
|
1448
1510
|
# Resolved once so every command for a tool reaches the same executable
|
|
1449
1511
|
ruff = _tool_command("ruff", project_root)
|
|
@@ -1499,12 +1561,14 @@ def _get_tool_definitions(
|
|
|
1499
1561
|
"check": [
|
|
1500
1562
|
*biome,
|
|
1501
1563
|
"lint",
|
|
1564
|
+
*biome_config,
|
|
1502
1565
|
"--no-errors-on-unmatched",
|
|
1503
1566
|
path_str,
|
|
1504
1567
|
],
|
|
1505
1568
|
"check_format": [
|
|
1506
1569
|
*biome,
|
|
1507
1570
|
"format",
|
|
1571
|
+
*biome_config,
|
|
1508
1572
|
"--no-errors-on-unmatched",
|
|
1509
1573
|
path_str,
|
|
1510
1574
|
],
|
|
@@ -1512,6 +1576,7 @@ def _get_tool_definitions(
|
|
|
1512
1576
|
*biome,
|
|
1513
1577
|
"lint",
|
|
1514
1578
|
"--write",
|
|
1579
|
+
*biome_config,
|
|
1515
1580
|
"--no-errors-on-unmatched",
|
|
1516
1581
|
path_str,
|
|
1517
1582
|
],
|
|
@@ -1519,6 +1584,7 @@ def _get_tool_definitions(
|
|
|
1519
1584
|
*biome,
|
|
1520
1585
|
"format",
|
|
1521
1586
|
"--write",
|
|
1587
|
+
*biome_config,
|
|
1522
1588
|
"--no-errors-on-unmatched",
|
|
1523
1589
|
path_str,
|
|
1524
1590
|
],
|
|
@@ -1610,11 +1676,13 @@ def _run_tool(
|
|
|
1610
1676
|
# and report what they could not deal with.
|
|
1611
1677
|
for phase in ("format", "fix"):
|
|
1612
1678
|
if phase in tool_config:
|
|
1613
|
-
|
|
1614
|
-
|
|
1679
|
+
result = _capture_tool_command(tool_config[phase])
|
|
1680
|
+
if _tool_checked_files(tool_name, result):
|
|
1681
|
+
report.ran.add(tool_name)
|
|
1615
1682
|
elif "check_format" in tool_config:
|
|
1616
1683
|
result = _capture_tool_command(tool_config["check_format"])
|
|
1617
|
-
|
|
1684
|
+
if _tool_checked_files(tool_name, result):
|
|
1685
|
+
report.ran.add(tool_name)
|
|
1618
1686
|
# gofmt reports by naming files rather than by exit code
|
|
1619
1687
|
if result.returncode != 0 or (
|
|
1620
1688
|
tool_name == "go fmt" and result.stdout.strip()
|
|
@@ -1627,7 +1695,8 @@ def _run_tool(
|
|
|
1627
1695
|
|
|
1628
1696
|
if "check" in tool_config:
|
|
1629
1697
|
result = _capture_tool_command(tool_config["check"])
|
|
1630
|
-
|
|
1698
|
+
if _tool_checked_files(tool_name, result):
|
|
1699
|
+
report.ran.add(tool_name)
|
|
1631
1700
|
if result.returncode != 0:
|
|
1632
1701
|
report.findings = True
|
|
1633
1702
|
click.echo(
|
|
@@ -1644,6 +1713,26 @@ def _run_tool(
|
|
|
1644
1713
|
return report
|
|
1645
1714
|
|
|
1646
1715
|
|
|
1716
|
+
def _tool_checked_files(
|
|
1717
|
+
tool_name: str, result: subprocess.CompletedProcess
|
|
1718
|
+
) -> bool:
|
|
1719
|
+
"""Report whether a completed command actually processed any files.
|
|
1720
|
+
|
|
1721
|
+
Args:
|
|
1722
|
+
tool_name: The tool whose command completed.
|
|
1723
|
+
result: The completed subprocess.
|
|
1724
|
+
|
|
1725
|
+
Returns:
|
|
1726
|
+
False when Biome explicitly reports that it checked zero files.
|
|
1727
|
+
"""
|
|
1728
|
+
if tool_name != "biome":
|
|
1729
|
+
return True
|
|
1730
|
+
output = f"{result.stdout or ''}\n{result.stderr or ''}"
|
|
1731
|
+
# Biome 1.x and 2.x use these summaries for an unmatched target.
|
|
1732
|
+
zero_file_summaries = ("Checked 0 files", "Formatted 0 files")
|
|
1733
|
+
return not any(summary in output for summary in zero_file_summaries)
|
|
1734
|
+
|
|
1735
|
+
|
|
1647
1736
|
def _capture_tool_command(cmd: list[str]) -> subprocess.CompletedProcess:
|
|
1648
1737
|
"""Run a tool and capture what it said, whatever its exit code.
|
|
1649
1738
|
|
|
@@ -1651,7 +1740,7 @@ def _capture_tool_command(cmd: list[str]) -> subprocess.CompletedProcess:
|
|
|
1651
1740
|
cmd: The command list to execute.
|
|
1652
1741
|
|
|
1653
1742
|
Returns:
|
|
1654
|
-
The completed process,
|
|
1743
|
+
The completed process, including captured output and its exit code.
|
|
1655
1744
|
|
|
1656
1745
|
Raises:
|
|
1657
1746
|
subprocess.SubprocessError: If the command outlives the timeout.
|
|
@@ -1667,25 +1756,6 @@ def _capture_tool_command(cmd: list[str]) -> subprocess.CompletedProcess:
|
|
|
1667
1756
|
)
|
|
1668
1757
|
|
|
1669
1758
|
|
|
1670
|
-
def _execute_tool_command(cmd: list[str]) -> None:
|
|
1671
|
-
"""Execute a tool command, raising if it exits with a non-zero code.
|
|
1672
|
-
|
|
1673
|
-
Args:
|
|
1674
|
-
cmd: The command list to execute.
|
|
1675
|
-
|
|
1676
|
-
Raises:
|
|
1677
|
-
subprocess.SubprocessError: If the command outlives the timeout.
|
|
1678
|
-
OSError: If the command cannot be started.
|
|
1679
|
-
"""
|
|
1680
|
-
logger.debug("Executing: %s", " ".join(cmd))
|
|
1681
|
-
# check=False: a fixer exits non-zero when findings are left over, and
|
|
1682
|
-
# treating that as an error aborted the run before the check that
|
|
1683
|
-
# reports them, so --fix passed on files the plain check failed.
|
|
1684
|
-
subprocess.run(
|
|
1685
|
-
cmd, capture_output=True, check=False, timeout=DEFAULT_TIMEOUT
|
|
1686
|
-
)
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
1759
|
# Main entry point for the CLI
|
|
1690
1760
|
def main() -> None:
|
|
1691
1761
|
"""Main entry point for the CLI."""
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import logging
|
|
2
3
|
import os
|
|
3
4
|
import subprocess
|
|
@@ -505,15 +506,72 @@ def test_default_configs_omitted_when_project_configured(
|
|
|
505
506
|
assert "--config" not in tools["ruff"]["format"]
|
|
506
507
|
assert "--config" not in tools["pyrefly"]["check"]
|
|
507
508
|
|
|
509
|
+
ts_file = tmp_path / "script.ts"
|
|
510
|
+
ts_file.touch()
|
|
511
|
+
(tmp_path / "biome.json").write_text("{}")
|
|
512
|
+
tools = {t["name"]: t for t in _get_tool_definitions(ts_file, tmp_path)}
|
|
513
|
+
for command in ("check", "check_format", "fix", "format"):
|
|
514
|
+
assert "--config-path" not in tools["biome"][command]
|
|
508
515
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
516
|
+
|
|
517
|
+
def test_biome_bundled_config_is_injected_into_every_command(
|
|
518
|
+
tmp_path: Path,
|
|
519
|
+
) -> None:
|
|
520
|
+
"""Linting, formatting, and their fix forms share the safe default."""
|
|
521
|
+
ts_file = tmp_path / "script.ts"
|
|
522
|
+
ts_file.touch()
|
|
523
|
+
|
|
524
|
+
tools = {t["name"]: t for t in _get_tool_definitions(ts_file, tmp_path)}
|
|
525
|
+
config_path = str(_bundled_config("biome"))
|
|
526
|
+
|
|
527
|
+
for command_name in ("check", "check_format", "fix", "format"):
|
|
528
|
+
command = tools["biome"][command_name]
|
|
529
|
+
assert command[command.index("--config-path") + 1] == config_path
|
|
530
|
+
assert "--vcs-enabled=true" not in command
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
@pytest.mark.parametrize(
|
|
534
|
+
"ignore_file", (".gitignore", ".ignore", ".git/info/exclude")
|
|
535
|
+
)
|
|
536
|
+
def test_biome_bundled_config_respects_project_ignore_files(
|
|
537
|
+
tmp_path: Path, ignore_file: str
|
|
538
|
+
) -> None:
|
|
539
|
+
"""An external default keeps ignore discovery rooted in the project."""
|
|
540
|
+
ts_file = tmp_path / "script.ts"
|
|
541
|
+
ts_file.touch()
|
|
542
|
+
ignore_path = tmp_path / ignore_file
|
|
543
|
+
ignore_path.parent.mkdir(parents=True, exist_ok=True)
|
|
544
|
+
ignore_path.write_text("node_modules/\n")
|
|
545
|
+
|
|
546
|
+
tools = {t["name"]: t for t in _get_tool_definitions(ts_file, tmp_path)}
|
|
547
|
+
|
|
548
|
+
for command_name in ("check", "check_format", "fix", "format"):
|
|
549
|
+
command = tools["biome"][command_name]
|
|
550
|
+
assert "--vcs-enabled=true" in command
|
|
551
|
+
assert "--vcs-client-kind=git" in command
|
|
552
|
+
assert "--vcs-use-ignore-file=true" in command
|
|
553
|
+
assert f"--vcs-root={tmp_path}" in command
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def test_bundled_default_configs_are_valid() -> None:
|
|
557
|
+
"""Tests that the bundled default configs exist and parse."""
|
|
558
|
+
# Both Python configs must exist and be valid TOML
|
|
512
559
|
for tool in ("ruff", "pyrefly"):
|
|
513
560
|
config_path = _bundled_config(tool)
|
|
514
561
|
assert config_path.exists()
|
|
515
562
|
tomllib.loads(config_path.read_text())
|
|
516
563
|
|
|
564
|
+
biome_config_path = _bundled_config("biome")
|
|
565
|
+
assert biome_config_path.exists()
|
|
566
|
+
biome_config = json.loads(biome_config_path.read_text())
|
|
567
|
+
assert biome_config["formatter"] == {
|
|
568
|
+
"enabled": True,
|
|
569
|
+
"indentStyle": "space",
|
|
570
|
+
"indentWidth": 2,
|
|
571
|
+
"lineWidth": 80,
|
|
572
|
+
}
|
|
573
|
+
assert biome_config["linter"]["rules"]["recommended"] is True
|
|
574
|
+
|
|
517
575
|
# The ruff defaults follow the Google Python style guide
|
|
518
576
|
ruff_config = tomllib.loads(_bundled_config("ruff").read_text())
|
|
519
577
|
assert ruff_config["line-length"] == 80
|
|
@@ -522,10 +580,17 @@ def test_bundled_default_configs_are_valid(tmp_path: Path) -> None:
|
|
|
522
580
|
|
|
523
581
|
@patch("readability._check_path")
|
|
524
582
|
def test_check_paths_aggregates_str_and_path_inputs(
|
|
525
|
-
mock_check_path: MagicMock,
|
|
583
|
+
mock_check_path: MagicMock,
|
|
584
|
+
tmp_path: Path,
|
|
585
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
526
586
|
) -> None:
|
|
527
587
|
"""The public API folds each per-path result into one limited report."""
|
|
528
588
|
project_root = tmp_path / "project"
|
|
589
|
+
project_root.mkdir()
|
|
590
|
+
(tmp_path / "src").mkdir()
|
|
591
|
+
(tmp_path / "src" / "example.py").touch()
|
|
592
|
+
(tmp_path / "README.md").touch()
|
|
593
|
+
monkeypatch.chdir(tmp_path)
|
|
529
594
|
mock_check_path.side_effect = [
|
|
530
595
|
CheckReport(ran={"ruff"}, skipped={"pyrefly"}),
|
|
531
596
|
CheckReport(
|
|
@@ -553,6 +618,29 @@ def test_check_paths_aggregates_str_and_path_inputs(
|
|
|
553
618
|
]
|
|
554
619
|
|
|
555
620
|
|
|
621
|
+
@patch("readability._check_path")
|
|
622
|
+
def test_check_paths_preserves_unverified_paths_when_another_path_ran(
|
|
623
|
+
mock_check_path: MagicMock,
|
|
624
|
+
tmp_path: Path,
|
|
625
|
+
) -> None:
|
|
626
|
+
"""One checked path must not hide another path that processed no files."""
|
|
627
|
+
checked = tmp_path / "src"
|
|
628
|
+
unchecked = tmp_path / "ignored"
|
|
629
|
+
checked.mkdir()
|
|
630
|
+
unchecked.mkdir()
|
|
631
|
+
mock_check_path.side_effect = [
|
|
632
|
+
CheckReport(ran={"biome"}),
|
|
633
|
+
CheckReport(),
|
|
634
|
+
]
|
|
635
|
+
|
|
636
|
+
report = check_paths([checked, unchecked], project_root=tmp_path)
|
|
637
|
+
|
|
638
|
+
assert report == CheckReport(
|
|
639
|
+
ran={"biome"},
|
|
640
|
+
unverified_paths=[unchecked],
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
|
|
556
644
|
@patch("readability._check_path", return_value=CheckReport(ran={"ruff"}))
|
|
557
645
|
def test_check_paths_defaults_project_root_to_cwd(
|
|
558
646
|
mock_check_path: MagicMock,
|
|
@@ -561,6 +649,7 @@ def test_check_paths_defaults_project_root_to_cwd(
|
|
|
561
649
|
) -> None:
|
|
562
650
|
"""The public API discovers configuration from the caller's directory."""
|
|
563
651
|
monkeypatch.chdir(tmp_path)
|
|
652
|
+
(tmp_path / "example.py").touch()
|
|
564
653
|
|
|
565
654
|
check_paths([Path("example.py")])
|
|
566
655
|
|
|
@@ -581,6 +670,23 @@ def test_check_paths_with_no_paths_reports_that_nothing_ran(
|
|
|
581
670
|
mock_check_path.assert_not_called()
|
|
582
671
|
|
|
583
672
|
|
|
673
|
+
@patch("readability._check_path")
|
|
674
|
+
def test_check_paths_rejects_missing_paths_before_dispatch(
|
|
675
|
+
mock_check_path: MagicMock,
|
|
676
|
+
tmp_path: Path,
|
|
677
|
+
) -> None:
|
|
678
|
+
"""A missing path prevents partial checks and fixes on earlier paths."""
|
|
679
|
+
existing = tmp_path / "exists.py"
|
|
680
|
+
missing = tmp_path / "missing.py"
|
|
681
|
+
existing.touch()
|
|
682
|
+
|
|
683
|
+
with pytest.raises(FileNotFoundError) as excinfo:
|
|
684
|
+
check_paths([existing, missing], project_root=tmp_path, fix=True)
|
|
685
|
+
|
|
686
|
+
assert str(missing) in str(excinfo.value)
|
|
687
|
+
mock_check_path.assert_not_called()
|
|
688
|
+
|
|
689
|
+
|
|
584
690
|
@patch(
|
|
585
691
|
"readability._check_path",
|
|
586
692
|
return_value=CheckReport(
|
|
@@ -590,10 +696,15 @@ def test_check_paths_with_no_paths_reports_that_nothing_ran(
|
|
|
590
696
|
),
|
|
591
697
|
)
|
|
592
698
|
def test_check_paths_returns_report_without_cli_status_prose(
|
|
593
|
-
mock_check_path: MagicMock,
|
|
699
|
+
mock_check_path: MagicMock,
|
|
700
|
+
capsys: pytest.CaptureFixture[str],
|
|
701
|
+
tmp_path: Path,
|
|
594
702
|
) -> None:
|
|
595
703
|
"""Library callers receive report states without CLI policy or summaries."""
|
|
596
|
-
|
|
704
|
+
path = tmp_path / "example.py"
|
|
705
|
+
path.touch()
|
|
706
|
+
|
|
707
|
+
report = check_paths([path])
|
|
597
708
|
|
|
598
709
|
assert report.findings is True
|
|
599
710
|
assert report.skipped == {"pyrefly"}
|
|
@@ -1759,10 +1870,10 @@ def test_check_runs_bundled_default_tools_without_a_trigger(
|
|
|
1759
1870
|
|
|
1760
1871
|
@patch("shutil.which")
|
|
1761
1872
|
@patch("subprocess.run")
|
|
1762
|
-
def
|
|
1873
|
+
def test_check_runs_biome_without_a_project_config(
|
|
1763
1874
|
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
1764
1875
|
) -> None:
|
|
1765
|
-
"""
|
|
1876
|
+
"""A bundled Biome config covers web files in unconfigured projects."""
|
|
1766
1877
|
mock_which.side_effect = lambda x: x
|
|
1767
1878
|
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
1768
1879
|
|
|
@@ -1773,13 +1884,179 @@ def test_check_still_gates_tools_without_bundled_defaults(
|
|
|
1773
1884
|
result = runner.invoke(cli, ["check", "script.ts"])
|
|
1774
1885
|
|
|
1775
1886
|
assert result.exit_code == 0
|
|
1887
|
+
called = [call.args[0] for call in mock_run.call_args_list]
|
|
1888
|
+
assert any(command[0] == "biome" for command in called)
|
|
1889
|
+
assert all("prettier" not in command for command in called)
|
|
1890
|
+
config_path = str(_bundled_config("biome"))
|
|
1891
|
+
biome_commands = [command for command in called if command[0] == "biome"]
|
|
1892
|
+
assert biome_commands
|
|
1893
|
+
assert all(
|
|
1894
|
+
command[command.index("--config-path") + 1] == config_path
|
|
1895
|
+
for command in biome_commands
|
|
1896
|
+
)
|
|
1897
|
+
|
|
1898
|
+
|
|
1899
|
+
@patch("shutil.which")
|
|
1900
|
+
@patch("subprocess.run")
|
|
1901
|
+
def test_bundled_tools_do_not_claim_an_unsupported_directory(
|
|
1902
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
1903
|
+
) -> None:
|
|
1904
|
+
"""A zero-file tool run must not turn nothing checked into a clean pass."""
|
|
1905
|
+
mock_which.side_effect = lambda x: x
|
|
1906
|
+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
1907
|
+
|
|
1908
|
+
runner = CliRunner()
|
|
1909
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
1910
|
+
Path("notes.md").touch()
|
|
1911
|
+
|
|
1912
|
+
result = runner.invoke(cli, ["check", "."])
|
|
1913
|
+
|
|
1914
|
+
assert result.exit_code == 0
|
|
1915
|
+
output = result.stdout + result.stderr
|
|
1916
|
+
assert "nothing was checked" in output
|
|
1917
|
+
assert "No findings" not in output
|
|
1918
|
+
mock_run.assert_not_called()
|
|
1919
|
+
|
|
1920
|
+
|
|
1921
|
+
@patch("shutil.which")
|
|
1922
|
+
@patch("subprocess.run")
|
|
1923
|
+
def test_biome_zero_file_result_is_not_reported_as_clean(
|
|
1924
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
1925
|
+
) -> None:
|
|
1926
|
+
"""Ignored matches must not make an unverified directory look clean."""
|
|
1927
|
+
mock_which.side_effect = lambda x: x if x == "biome" else None
|
|
1928
|
+
checked_nothing = MagicMock(
|
|
1929
|
+
returncode=0,
|
|
1930
|
+
stdout="",
|
|
1931
|
+
stderr="Checked 0 files in 1ms. No fixes applied.\n",
|
|
1932
|
+
)
|
|
1933
|
+
formatted_nothing = MagicMock(
|
|
1934
|
+
returncode=0,
|
|
1935
|
+
stdout="Formatted 0 files in 1ms. No fixes applied.\n",
|
|
1936
|
+
stderr="",
|
|
1937
|
+
)
|
|
1938
|
+
|
|
1939
|
+
runner = CliRunner()
|
|
1940
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
1941
|
+
Path(".gitignore").write_text("node_modules/\n")
|
|
1942
|
+
Path("node_modules/example").mkdir(parents=True)
|
|
1943
|
+
Path("node_modules/example/index.js").touch()
|
|
1944
|
+
Path("README.md").touch()
|
|
1945
|
+
|
|
1946
|
+
for extra_args, expected_calls in (([], 2), (["--fix"], 3)):
|
|
1947
|
+
mock_run.side_effect = (
|
|
1948
|
+
[checked_nothing, checked_nothing]
|
|
1949
|
+
if not extra_args
|
|
1950
|
+
else [formatted_nothing, checked_nothing, checked_nothing]
|
|
1951
|
+
)
|
|
1952
|
+
result = runner.invoke(cli, ["check", ".", *extra_args])
|
|
1953
|
+
|
|
1954
|
+
assert result.exit_code == 0
|
|
1955
|
+
output = result.stdout + result.stderr
|
|
1956
|
+
assert "nothing was checked" in output
|
|
1957
|
+
assert "No findings" not in output
|
|
1958
|
+
assert mock_run.call_count == expected_calls
|
|
1959
|
+
mock_run.reset_mock()
|
|
1960
|
+
|
|
1961
|
+
|
|
1962
|
+
@patch("shutil.which")
|
|
1963
|
+
@patch("subprocess.run")
|
|
1964
|
+
def test_biome_zero_file_path_is_not_hidden_by_a_checked_path(
|
|
1965
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
1966
|
+
) -> None:
|
|
1967
|
+
"""A clean aggregate identifies requested paths Biome did not inspect."""
|
|
1968
|
+
mock_which.side_effect = lambda x: x if x == "biome" else None
|
|
1969
|
+
checked_files = MagicMock(returncode=0, stdout="", stderr="")
|
|
1970
|
+
checked_nothing = MagicMock(
|
|
1971
|
+
returncode=0,
|
|
1972
|
+
stdout="Checked 0 files in 1ms. No fixes applied.\n",
|
|
1973
|
+
stderr="",
|
|
1974
|
+
)
|
|
1975
|
+
|
|
1976
|
+
runner = CliRunner()
|
|
1977
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
1978
|
+
Path(".gitignore").write_text("ignored/\n")
|
|
1979
|
+
Path("src").mkdir()
|
|
1980
|
+
Path("src/good.js").touch()
|
|
1981
|
+
Path("ignored").mkdir()
|
|
1982
|
+
Path("ignored/generated.js").touch()
|
|
1983
|
+
mock_run.side_effect = [
|
|
1984
|
+
checked_files,
|
|
1985
|
+
checked_files,
|
|
1986
|
+
checked_nothing,
|
|
1987
|
+
checked_nothing,
|
|
1988
|
+
]
|
|
1989
|
+
|
|
1990
|
+
result = runner.invoke(cli, ["check", "src", "ignored"])
|
|
1991
|
+
|
|
1992
|
+
assert result.exit_code == 0
|
|
1993
|
+
assert "nothing was checked for 1 path(s): ignored" in result.stderr
|
|
1994
|
+
assert "No findings in 1 path(s) (biome)." in result.stderr
|
|
1995
|
+
assert "No findings in 2 path(s)" not in result.stderr
|
|
1996
|
+
|
|
1997
|
+
|
|
1998
|
+
@patch("shutil.which")
|
|
1999
|
+
@patch("subprocess.run")
|
|
2000
|
+
def test_biome_zero_file_failure_still_exits_nonzero(
|
|
2001
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
2002
|
+
) -> None:
|
|
2003
|
+
"""A configuration failure must not become a successful no-tool result."""
|
|
2004
|
+
mock_which.side_effect = lambda x: x if x == "biome" else None
|
|
2005
|
+
checked_nothing = MagicMock(
|
|
2006
|
+
returncode=1,
|
|
2007
|
+
stdout="Checked 0 files in 1ms.\nConfiguration error.\n",
|
|
2008
|
+
stderr="",
|
|
2009
|
+
)
|
|
2010
|
+
formatted_nothing = MagicMock(
|
|
2011
|
+
returncode=1,
|
|
2012
|
+
stdout="Formatted 0 files in 1ms.\nConfiguration error.\n",
|
|
2013
|
+
stderr="",
|
|
2014
|
+
)
|
|
2015
|
+
|
|
2016
|
+
runner = CliRunner()
|
|
2017
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2018
|
+
Path("script.ts").touch()
|
|
2019
|
+
|
|
2020
|
+
for extra_args in ([], ["--fix"]):
|
|
2021
|
+
mock_run.side_effect = (
|
|
2022
|
+
[checked_nothing, checked_nothing]
|
|
2023
|
+
if not extra_args
|
|
2024
|
+
else [formatted_nothing, checked_nothing, checked_nothing]
|
|
2025
|
+
)
|
|
2026
|
+
result = runner.invoke(cli, ["check", "script.ts", *extra_args])
|
|
2027
|
+
|
|
2028
|
+
assert result.exit_code == 1
|
|
2029
|
+
output = result.stdout + result.stderr
|
|
2030
|
+
assert "biome findings" in output
|
|
2031
|
+
assert "nothing was checked" not in output
|
|
2032
|
+
mock_run.reset_mock()
|
|
2033
|
+
|
|
2034
|
+
|
|
2035
|
+
@patch("shutil.which")
|
|
2036
|
+
@patch("subprocess.run")
|
|
2037
|
+
def test_bundled_biome_applies_to_a_nested_web_file(
|
|
2038
|
+
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
2039
|
+
) -> None:
|
|
2040
|
+
"""Directory applicability still finds nested web files."""
|
|
2041
|
+
mock_which.side_effect = lambda x: x if x == "biome" else None
|
|
2042
|
+
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
2043
|
+
|
|
2044
|
+
runner = CliRunner()
|
|
2045
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
2046
|
+
Path("src").mkdir()
|
|
2047
|
+
Path("src/script.ts").touch()
|
|
2048
|
+
|
|
2049
|
+
result = runner.invoke(cli, ["check", "."])
|
|
2050
|
+
|
|
2051
|
+
assert result.exit_code == 0
|
|
2052
|
+
assert "No findings in 1 path(s) (biome)." in result.stderr
|
|
1776
2053
|
called = [call.args[0][0] for call in mock_run.call_args_list]
|
|
1777
|
-
assert "
|
|
2054
|
+
assert called == ["biome", "biome"]
|
|
1778
2055
|
|
|
1779
2056
|
|
|
1780
2057
|
@patch("shutil.which")
|
|
1781
2058
|
@patch("subprocess.run")
|
|
1782
|
-
def
|
|
2059
|
+
def test_check_with_project_config_runs_biome(
|
|
1783
2060
|
mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path
|
|
1784
2061
|
) -> None:
|
|
1785
2062
|
"""With biome.json present, biome runs as it always did."""
|
|
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
|