ssc_codegen 0.37.0__tar.gz → 0.37.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/PKG-INFO +8 -3
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/README.md +10 -5
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/pyproject.toml +1 -11
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/ast/struct.py +21 -15
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/ast/typedef.py +2 -1
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/core/contexts.py +32 -22
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/core/expressions.py +14 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/core/format.py +29 -5
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/core/linter.py +374 -14
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/core/module_handler.py +60 -19
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/core/reader.py +47 -13
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/core/struct_parser.py +10 -1
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/main.py +114 -11
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/parsers/curl.py +20 -9
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/parsers/http.py +17 -6
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/request_spec.py +84 -3
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/golang/rest.py +106 -54
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/golang/runtime.py +11 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/golang/visitor.py +68 -9
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/javascript/rest.py +48 -14
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/javascript/visitor.py +2 -1
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/python/rest.py +14 -49
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/python/visitor.py +11 -16
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/.gitignore +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/LICENSE +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/__init__.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/_logging.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/ast/__init__.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/ast/array.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/ast/base.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/ast/cast.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/ast/control.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/ast/extract.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/ast/function.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/ast/helpers.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/ast/jsondef.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/ast/module.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/ast/predicate_containers.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/ast/predicate_ops.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/ast/regex.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/ast/rest.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/ast/selectors.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/ast/string.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/ast/types.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/core/__init__.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/core/predicates.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/core/rest_artifacts.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/core/type_checking.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/exceptions.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/explore.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/generation/__init__.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/generation/builder.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/generation/runtime.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/health.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/naming.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/parsers/__init__.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/regex_utils.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/__init__.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/golang/__init__.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/golang/http_libs/__init__.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/golang/http_libs/base.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/golang/http_libs/nethttp.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/golang/literals.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/golang/regex.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/javascript/__init__.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/javascript/http_libs/__init__.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/javascript/http_libs/axios.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/javascript/http_libs/base.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/javascript/http_libs/fetch.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/profile.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/python/__init__.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/python/html_libs/__init__.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/python/html_libs/base.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/python/html_libs/bs4.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/python/html_libs/lxml.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/python/html_libs/parsel.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/python/html_libs/slax.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/python/http_libs/__init__.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/python/http_libs/aiohttp.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/python/http_libs/base.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/python/http_libs/httpx.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/python/http_libs/requests.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/resolver.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/targets/spec.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/traversal/__init__.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/traversal/context.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/traversal/utils.py +0 -0
- {ssc_codegen-0.37.0 → ssc_codegen-0.37.2}/ssc_codegen/traversal/walker.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ssc_codegen
|
|
3
|
-
Version: 0.37.
|
|
3
|
+
Version: 0.37.2
|
|
4
4
|
Summary: Python-dsl code converter to html parser for web scraping
|
|
5
5
|
Project-URL: Documentation, https://github.com/vypivshiy/selector_schema_codegen#readme
|
|
6
6
|
Project-URL: Issues, https://github.com/vypivshiy/selector_schema_codegen/issues
|
|
@@ -86,6 +86,9 @@ ssc-gen generate js examples/ -o ./output
|
|
|
86
86
|
# Go (goquery + net/http)
|
|
87
87
|
ssc-gen generate go schema.kdl -o ./output
|
|
88
88
|
|
|
89
|
+
# Go package defaults to main; override explicitly when generating a library
|
|
90
|
+
ssc-gen generate go schema.kdl -o ./output --package scraper
|
|
91
|
+
|
|
89
92
|
# with custom package name
|
|
90
93
|
ssc-gen generate python schema.kdl -L bs4 -o ./parsers --package scraper
|
|
91
94
|
|
|
@@ -116,12 +119,14 @@ ssc-gen check examples/
|
|
|
116
119
|
|
|
117
120
|
```bash
|
|
118
121
|
# from file
|
|
119
|
-
ssc-gen run examples/booksToScrape.kdl:MainCatalogue -
|
|
122
|
+
ssc-gen run examples/booksToScrape.kdl:MainCatalogue -L bs4 -i page.html
|
|
120
123
|
|
|
121
124
|
# from stdin
|
|
122
|
-
curl https://books.toscrape.com/ | ssc-gen run examples/booksToScrape.kdl:MainCatalogue -
|
|
125
|
+
curl https://books.toscrape.com/ | ssc-gen run examples/booksToScrape.kdl:MainCatalogue -L bs4
|
|
123
126
|
```
|
|
124
127
|
|
|
128
|
+
`ssc-gen run` executes generated Python in-process. Run only trusted schema files.
|
|
129
|
+
|
|
125
130
|
### Health check (verify selectors match elements)
|
|
126
131
|
|
|
127
132
|
```bash
|
|
@@ -51,8 +51,11 @@ ssc-gen generate python schema.kdl -L bs4 -o ./output
|
|
|
51
51
|
# all .kdl files in a directory (JavaScript)
|
|
52
52
|
ssc-gen generate js examples/ -o ./output
|
|
53
53
|
|
|
54
|
-
# Go (goquery + net/http)
|
|
55
|
-
ssc-gen generate go schema.kdl -o ./output
|
|
54
|
+
# Go (goquery + net/http)
|
|
55
|
+
ssc-gen generate go schema.kdl -o ./output
|
|
56
|
+
|
|
57
|
+
# Go package defaults to main; override explicitly when generating a library
|
|
58
|
+
ssc-gen generate go schema.kdl -o ./output --package scraper
|
|
56
59
|
|
|
57
60
|
# with custom package name
|
|
58
61
|
ssc-gen generate python schema.kdl -L bs4 -o ./parsers --package scraper
|
|
@@ -84,11 +87,13 @@ ssc-gen check examples/
|
|
|
84
87
|
|
|
85
88
|
```bash
|
|
86
89
|
# from file
|
|
87
|
-
ssc-gen run examples/booksToScrape.kdl:MainCatalogue -
|
|
90
|
+
ssc-gen run examples/booksToScrape.kdl:MainCatalogue -L bs4 -i page.html
|
|
88
91
|
|
|
89
92
|
# from stdin
|
|
90
|
-
curl https://books.toscrape.com/ | ssc-gen run examples/booksToScrape.kdl:MainCatalogue -
|
|
91
|
-
```
|
|
93
|
+
curl https://books.toscrape.com/ | ssc-gen run examples/booksToScrape.kdl:MainCatalogue -L bs4
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`ssc-gen run` executes generated Python in-process. Run only trusted schema files.
|
|
92
97
|
|
|
93
98
|
### Health check (verify selectors match elements)
|
|
94
99
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ssc_codegen"
|
|
3
|
-
version = "0.37.
|
|
3
|
+
version = "0.37.2"
|
|
4
4
|
description = "Python-dsl code converter to html parser for web scraping "
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -78,10 +78,6 @@ exclude = ["__init__.py"]
|
|
|
78
78
|
[tool.mypy]
|
|
79
79
|
python_version = "3.10"
|
|
80
80
|
show_error_codes = true
|
|
81
|
-
exclude = [
|
|
82
|
-
"ssc_codegen/ast" # ignore built-in ast and project ast
|
|
83
|
-
|
|
84
|
-
]
|
|
85
81
|
[[tool.mypy.overrides]]
|
|
86
82
|
module="tests.*"
|
|
87
83
|
disallow_untyped_defs = false
|
|
@@ -91,12 +87,6 @@ disallow_untyped_defs = false
|
|
|
91
87
|
module = ["colorama", "lxml", "lxml.*"]
|
|
92
88
|
ignore_missing_imports = true
|
|
93
89
|
|
|
94
|
-
[[tool.mypy.overrides]]
|
|
95
|
-
# Large visitor classes with match statements may need relaxed return checks
|
|
96
|
-
module = ["ssc_codegen.targets.python.visitor", "ssc_codegen.targets.javascript.visitor"]
|
|
97
|
-
disable_error_code = ["return", "return-value"]
|
|
98
|
-
|
|
99
|
-
|
|
100
90
|
[tool.coverage.report]
|
|
101
91
|
exclude_also = [
|
|
102
92
|
'def __repr__',
|
|
@@ -136,10 +136,16 @@ class PlaceholderSpec:
|
|
|
136
136
|
def parse_placeholder(match: "_re.Match[str]") -> PlaceholderSpec:
|
|
137
137
|
return PlaceholderSpec(
|
|
138
138
|
name=match.group(1),
|
|
139
|
-
type_name=
|
|
139
|
+
type_name=cast(
|
|
140
|
+
Literal["str", "int", "float", "bool"],
|
|
141
|
+
match.group(2) or "str",
|
|
142
|
+
),
|
|
140
143
|
is_array=bool(match.group(3)),
|
|
141
144
|
is_optional=bool(match.group(4)),
|
|
142
|
-
style=
|
|
145
|
+
style=cast(
|
|
146
|
+
Literal["repeat", "csv", "bracket", "pipe", "space"] | None,
|
|
147
|
+
match.group(5) or None,
|
|
148
|
+
),
|
|
143
149
|
)
|
|
144
150
|
|
|
145
151
|
|
|
@@ -260,7 +266,7 @@ class RequestHttp(Node):
|
|
|
260
266
|
|
|
261
267
|
Created once at parse time from ``parse_to_http(raw_payload)``;
|
|
262
268
|
converters read fields directly instead of re-parsing raw_payload.
|
|
263
|
-
String fields (url, headers, cookies, params,
|
|
269
|
+
String fields (url, headers, cookies, params, payload) are ``Template``
|
|
264
270
|
instances: tokenized literal/placeholder parts.
|
|
265
271
|
"""
|
|
266
272
|
|
|
@@ -270,7 +276,7 @@ class RequestHttp(Node):
|
|
|
270
276
|
cookies: dict[str, PlaceholderTemplate] = field(default_factory=dict)
|
|
271
277
|
params: dict[str, PlaceholderTemplate] = field(default_factory=dict)
|
|
272
278
|
body_kind: str = "empty" # "empty" | "json" | "form" | "raw"
|
|
273
|
-
|
|
279
|
+
payload: PlaceholderTemplate | dict[str, PlaceholderTemplate] | None = None
|
|
274
280
|
|
|
275
281
|
@property
|
|
276
282
|
def placeholders(self) -> list[PlaceholderSpec]:
|
|
@@ -289,10 +295,10 @@ class RequestHttp(Node):
|
|
|
289
295
|
yield self.url
|
|
290
296
|
for d in (self.headers, self.cookies, self.params):
|
|
291
297
|
yield from d.values()
|
|
292
|
-
if isinstance(self.
|
|
293
|
-
yield self.
|
|
294
|
-
elif isinstance(self.
|
|
295
|
-
yield from self.
|
|
298
|
+
if isinstance(self.payload, PlaceholderTemplate):
|
|
299
|
+
yield self.payload
|
|
300
|
+
elif isinstance(self.payload, dict):
|
|
301
|
+
yield from self.payload.values()
|
|
296
302
|
|
|
297
303
|
def with_renamed_placeholders(
|
|
298
304
|
self, transform: Callable[[str], str]
|
|
@@ -312,13 +318,13 @@ class RequestHttp(Node):
|
|
|
312
318
|
) -> dict[str, PlaceholderTemplate]:
|
|
313
319
|
return {k: v.renamed(mapping) for k, v in d.items()}
|
|
314
320
|
|
|
315
|
-
|
|
321
|
+
new_payload: (
|
|
316
322
|
PlaceholderTemplate | dict[str, PlaceholderTemplate] | None
|
|
317
|
-
) = self.
|
|
318
|
-
if isinstance(self.
|
|
319
|
-
|
|
320
|
-
elif isinstance(self.
|
|
321
|
-
|
|
323
|
+
) = self.payload
|
|
324
|
+
if isinstance(self.payload, PlaceholderTemplate):
|
|
325
|
+
new_payload = self.payload.renamed(mapping)
|
|
326
|
+
elif isinstance(self.payload, dict):
|
|
327
|
+
new_payload = _rename_dict(self.payload)
|
|
322
328
|
|
|
323
329
|
return RequestHttp(
|
|
324
330
|
method=self.method,
|
|
@@ -327,7 +333,7 @@ class RequestHttp(Node):
|
|
|
327
333
|
cookies=_rename_dict(self.cookies),
|
|
328
334
|
params=_rename_dict(self.params),
|
|
329
335
|
body_kind=self.body_kind,
|
|
330
|
-
|
|
336
|
+
payload=new_payload,
|
|
331
337
|
)
|
|
332
338
|
|
|
333
339
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
from dataclasses import dataclass
|
|
3
|
+
from typing import cast
|
|
3
4
|
|
|
4
5
|
from .base import Node
|
|
5
6
|
from .types import StructType
|
|
@@ -29,4 +30,4 @@ class TypeDef(Node):
|
|
|
29
30
|
|
|
30
31
|
@property
|
|
31
32
|
def fields(self) -> list[TypeDefField]:
|
|
32
|
-
return self.body
|
|
33
|
+
return cast(list[TypeDefField], self.body)
|
|
@@ -50,6 +50,7 @@ class ParseContext:
|
|
|
50
50
|
structs: dict[str, StructBase] = field(default_factory=dict)
|
|
51
51
|
json_defs: dict[str, JsonDef] = field(default_factory=dict)
|
|
52
52
|
source_path: Path | None = None
|
|
53
|
+
node_source_paths: dict[int, Path] = field(default_factory=dict)
|
|
53
54
|
|
|
54
55
|
def all_names(self) -> set[str]:
|
|
55
56
|
return (
|
|
@@ -77,6 +78,7 @@ class LintContext:
|
|
|
77
78
|
)
|
|
78
79
|
_predicate_depth: int = field(default=0)
|
|
79
80
|
_predicate_context: str = ""
|
|
81
|
+
node_source_paths: dict[int, Path] = field(default_factory=dict)
|
|
80
82
|
|
|
81
83
|
def error(
|
|
82
84
|
self,
|
|
@@ -88,18 +90,22 @@ class LintContext:
|
|
|
88
90
|
label: str | None = None,
|
|
89
91
|
notes: list[str] | None = None,
|
|
90
92
|
) -> None:
|
|
91
|
-
self.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
93
|
+
scope = self.path
|
|
94
|
+
source_path = self.node_source_paths.get(id(node))
|
|
95
|
+
diagnostic_notes = tuple(notes) if notes else ()
|
|
96
|
+
if source_path is not None and scope:
|
|
97
|
+
diagnostic_notes = (*diagnostic_notes, f"scope: {scope}")
|
|
98
|
+
diagnostic = ReadDiagnostic(
|
|
99
|
+
message=message,
|
|
100
|
+
severity=Severity.ERROR,
|
|
101
|
+
span=node.span,
|
|
102
|
+
path=str(source_path) if source_path is not None else scope,
|
|
103
|
+
hint=hint,
|
|
104
|
+
code=code,
|
|
105
|
+
label=label,
|
|
106
|
+
notes=diagnostic_notes,
|
|
102
107
|
)
|
|
108
|
+
self.diagnostics.append(diagnostic)
|
|
103
109
|
|
|
104
110
|
def warning(
|
|
105
111
|
self,
|
|
@@ -111,18 +117,22 @@ class LintContext:
|
|
|
111
117
|
label: str | None = None,
|
|
112
118
|
notes: list[str] | None = None,
|
|
113
119
|
) -> None:
|
|
114
|
-
self.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
120
|
+
scope = self.path
|
|
121
|
+
source_path = self.node_source_paths.get(id(node))
|
|
122
|
+
diagnostic_notes = tuple(notes) if notes else ()
|
|
123
|
+
if source_path is not None and scope:
|
|
124
|
+
diagnostic_notes = (*diagnostic_notes, f"scope: {scope}")
|
|
125
|
+
diagnostic = ReadDiagnostic(
|
|
126
|
+
message=message,
|
|
127
|
+
severity=Severity.WARNING,
|
|
128
|
+
span=node.span,
|
|
129
|
+
path=str(source_path) if source_path is not None else scope,
|
|
130
|
+
hint=hint,
|
|
131
|
+
code=code,
|
|
132
|
+
label=label,
|
|
133
|
+
notes=diagnostic_notes,
|
|
125
134
|
)
|
|
135
|
+
self.diagnostics.append(diagnostic)
|
|
126
136
|
|
|
127
137
|
@property
|
|
128
138
|
def path(self) -> str:
|
|
@@ -606,6 +606,9 @@ def _reg_expr(name: str):
|
|
|
606
606
|
def _expr_css(
|
|
607
607
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
608
608
|
):
|
|
609
|
+
if not node.children and not node.args:
|
|
610
|
+
lint.error(node, message="'css' requires a selector", code="E001")
|
|
611
|
+
return None
|
|
609
612
|
if node.children:
|
|
610
613
|
queries = [
|
|
611
614
|
resolve_selector_child_name(c.name, ctx) for c in node.children
|
|
@@ -622,6 +625,9 @@ def _expr_css(
|
|
|
622
625
|
def _expr_css_all(
|
|
623
626
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
624
627
|
):
|
|
628
|
+
if not node.children and not node.args:
|
|
629
|
+
lint.error(node, message="'css-all' requires a selector", code="E001")
|
|
630
|
+
return None
|
|
625
631
|
if node.children:
|
|
626
632
|
queries = [
|
|
627
633
|
resolve_selector_child_name(c.name, ctx) for c in node.children
|
|
@@ -638,6 +644,9 @@ def _expr_css_all(
|
|
|
638
644
|
def _expr_xpath(
|
|
639
645
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
640
646
|
):
|
|
647
|
+
if not node.children and not node.args:
|
|
648
|
+
lint.error(node, message="'xpath' requires an expression", code="E001")
|
|
649
|
+
return None
|
|
641
650
|
if node.children:
|
|
642
651
|
queries = [
|
|
643
652
|
resolve_selector_child_name(c.name, ctx) for c in node.children
|
|
@@ -654,6 +663,11 @@ def _expr_xpath(
|
|
|
654
663
|
def _expr_xpath_all(
|
|
655
664
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
656
665
|
):
|
|
666
|
+
if not node.children and not node.args:
|
|
667
|
+
lint.error(
|
|
668
|
+
node, message="'xpath-all' requires an expression", code="E001"
|
|
669
|
+
)
|
|
670
|
+
return None
|
|
657
671
|
if node.children:
|
|
658
672
|
queries = [
|
|
659
673
|
resolve_selector_child_name(c.name, ctx) for c in node.children
|
|
@@ -41,6 +41,7 @@ def format_diagnostic(
|
|
|
41
41
|
context_lines: int = 1,
|
|
42
42
|
) -> str:
|
|
43
43
|
"""Format a single diagnostic as a human-readable Rust-style string."""
|
|
44
|
+
filepath = _diagnostic_filepath(d, filepath)
|
|
44
45
|
src = _resolve_src(src, filepath)
|
|
45
46
|
renderer = _Renderer(use_color=_resolve_color(use_color))
|
|
46
47
|
return renderer.render(
|
|
@@ -66,12 +67,14 @@ def format_diagnostics(
|
|
|
66
67
|
[diagnostic_to_dict(d) for d in diagnostics], indent=2
|
|
67
68
|
)
|
|
68
69
|
|
|
69
|
-
src = _resolve_src(src, filepath)
|
|
70
70
|
renderer = _Renderer(use_color=_resolve_color(use_color))
|
|
71
71
|
|
|
72
72
|
sections = [
|
|
73
73
|
renderer.render(
|
|
74
|
-
d,
|
|
74
|
+
d,
|
|
75
|
+
src=_resolve_src(src, _diagnostic_filepath(d, filepath)),
|
|
76
|
+
filepath=_diagnostic_filepath(d, filepath),
|
|
77
|
+
context_lines=context_lines,
|
|
75
78
|
)
|
|
76
79
|
for d in diagnostics
|
|
77
80
|
]
|
|
@@ -97,6 +100,19 @@ def diagnostic_to_dict(d: ReadDiagnostic) -> dict[str, Any]:
|
|
|
97
100
|
}
|
|
98
101
|
|
|
99
102
|
|
|
103
|
+
def _diagnostic_filepath(
|
|
104
|
+
diagnostic: ReadDiagnostic, fallback: str | Path | None
|
|
105
|
+
) -> str | Path | None:
|
|
106
|
+
if diagnostic.path:
|
|
107
|
+
try:
|
|
108
|
+
path = Path(diagnostic.path)
|
|
109
|
+
if path.is_file():
|
|
110
|
+
return path
|
|
111
|
+
except OSError:
|
|
112
|
+
pass
|
|
113
|
+
return fallback
|
|
114
|
+
|
|
115
|
+
|
|
100
116
|
# ── Renderer ──────────────────────────────────────────────────────────────────
|
|
101
117
|
|
|
102
118
|
|
|
@@ -271,7 +287,7 @@ class _Renderer:
|
|
|
271
287
|
parts: list[str] = []
|
|
272
288
|
eq = self._styled("=", color=self._dim)
|
|
273
289
|
|
|
274
|
-
if d.path:
|
|
290
|
+
if d.path and _diagnostic_filepath(d, None) is None:
|
|
275
291
|
scope = self._styled("scope:", color=self._cyan, bold=True)
|
|
276
292
|
parts.append(f"{pad} {eq} {scope} {d.path}")
|
|
277
293
|
|
|
@@ -280,8 +296,16 @@ class _Renderer:
|
|
|
280
296
|
parts.append(f"{pad} {eq} {help_} {d.hint}")
|
|
281
297
|
|
|
282
298
|
for note in d.notes:
|
|
283
|
-
|
|
284
|
-
|
|
299
|
+
if note.startswith("scope: "):
|
|
300
|
+
scope_label = self._styled(
|
|
301
|
+
"scope:", color=self._cyan, bold=True
|
|
302
|
+
)
|
|
303
|
+
parts.append(f"{pad} {eq} {scope_label} {note[7:]}")
|
|
304
|
+
else:
|
|
305
|
+
note_label = self._styled(
|
|
306
|
+
"note:", color=self._magenta, bold=True
|
|
307
|
+
)
|
|
308
|
+
parts.append(f"{pad} {eq} {note_label} {note}")
|
|
285
309
|
|
|
286
310
|
return parts
|
|
287
311
|
|