vibecodedshit 0.0.0-stage → 0.3.0
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.
- package/LICENSE +95 -0
- package/README.md +73 -2
- package/assets/fonts/NotoEmoji-OFL.txt +93 -0
- package/assets/fonts/NotoEmoji-Poop.ttf +0 -0
- package/assets/fonts/SpaceGrotesk-Bold.ttf +0 -0
- package/assets/fonts/SpaceGrotesk-OFL.txt +93 -0
- package/dist/bin.js +11 -0
- package/dist/card/previous.js +38 -0
- package/dist/card/render.js +71 -0
- package/dist/cli.js +138 -0
- package/dist/commands/card.js +69 -0
- package/dist/commands/common.js +1 -0
- package/dist/commands/diff.js +54 -0
- package/dist/commands/footer.js +28 -0
- package/dist/commands/report.js +49 -0
- package/dist/commands/route.js +62 -0
- package/dist/commands/submit.js +125 -0
- package/dist/commands/summary.js +77 -0
- package/dist/diff/compare.js +73 -0
- package/dist/diff/conditions.js +87 -0
- package/dist/diff/format.js +23 -0
- package/dist/exec/docker.js +158 -0
- package/dist/exec/findings.js +73 -0
- package/dist/exec/plan.js +50 -0
- package/dist/exec/reports.js +90 -0
- package/dist/exec/roots.js +49 -0
- package/dist/exec/run.js +182 -0
- package/dist/exec/steps/build.js +45 -0
- package/dist/exec/steps/coverage-install.js +12 -0
- package/dist/exec/steps/install.js +41 -0
- package/dist/exec/steps/knip.js +28 -0
- package/dist/exec/steps/lint.js +25 -0
- package/dist/exec/steps/lockfiles.js +25 -0
- package/dist/exec/steps/test.js +17 -0
- package/dist/exec/steps/typecheck.js +40 -0
- package/dist/exec/test-reports/coverage-py.js +23 -0
- package/dist/exec/test-reports/coverage-ranges.js +65 -0
- package/dist/exec/test-reports/coverage-summary.js +23 -0
- package/dist/exec/test-reports/jest-json.js +31 -0
- package/dist/exec/test-reports/junit.js +24 -0
- package/dist/exec/test-reports/mocha-json.js +18 -0
- package/dist/exec/test-reports/vitest-json.js +5 -0
- package/dist/exec/test-runners/detect.js +99 -0
- package/dist/exec/test-runners/index.js +6 -0
- package/dist/exec/test-runners/jest.js +27 -0
- package/dist/exec/test-runners/mocha.js +28 -0
- package/dist/exec/test-runners/node-test.js +24 -0
- package/dist/exec/test-runners/pytest.js +18 -0
- package/dist/exec/test-runners/script.js +48 -0
- package/dist/exec/test-runners/spec.js +1 -0
- package/dist/exec/test-runners/vitest.js +28 -0
- package/dist/exec/tests.js +120 -0
- package/dist/exec/types.js +1 -0
- package/dist/interactive/after-scan.js +121 -0
- package/dist/interactive/docker-setup.js +97 -0
- package/dist/interactive/prompt.js +17 -0
- package/dist/interactive/system.js +10 -0
- package/dist/manifest/package-json.js +36 -0
- package/dist/manifest/python.js +106 -0
- package/dist/measure/d3/index.js +45 -0
- package/dist/measure/d3/presets.js +100 -0
- package/dist/measure/d3/py/adapter.js +67 -0
- package/dist/measure/d3/py/config.js +80 -0
- package/dist/measure/d3/py/contracts.js +57 -0
- package/dist/measure/d3/py/domain.js +57 -0
- package/dist/measure/d3/py/graph.js +214 -0
- package/dist/measure/d3/py/interpreter.js +31 -0
- package/dist/measure/d3/py/layers.js +120 -0
- package/dist/measure/d3/py/roots.js +58 -0
- package/dist/measure/d3/py/types.js +1 -0
- package/dist/measure/d3/scc.js +64 -0
- package/dist/measure/d3/ts/adapter.js +371 -0
- package/dist/measure/d3/ts/classify.js +107 -0
- package/dist/measure/d3/ts/config.js +318 -0
- package/dist/measure/d3/ts/domain.js +23 -0
- package/dist/measure/d3/ts/rules.js +65 -0
- package/dist/measure/d3/types.js +5 -0
- package/dist/measure/deps/cvss.js +66 -0
- package/dist/measure/deps/declared.js +195 -0
- package/dist/measure/deps/files.js +40 -0
- package/dist/measure/deps/lockfiles.js +308 -0
- package/dist/measure/deps/osv.js +122 -0
- package/dist/measure/deps/registry.js +142 -0
- package/dist/measure/deps/resolved.js +78 -0
- package/dist/measure/deps/units.js +5 -0
- package/dist/measure/eslint/health-plugin.js +19 -0
- package/dist/measure/ops/docs-commands.js +73 -0
- package/dist/measure/ops/docs-resolve.js +217 -0
- package/dist/measure/ops/entry-points.js +49 -0
- package/dist/measure/ops/env-example.js +20 -0
- package/dist/measure/ops/env-reads.js +46 -0
- package/dist/measure/ops/runtime.js +73 -0
- package/dist/measure/p1-1.js +8 -0
- package/dist/measure/p1-2.js +8 -0
- package/dist/measure/p1-3.js +8 -0
- package/dist/measure/p1-4.js +41 -0
- package/dist/measure/p1-5.js +59 -0
- package/dist/measure/p2-1.js +8 -0
- package/dist/measure/p2-2.js +44 -0
- package/dist/measure/p2-3.js +81 -0
- package/dist/measure/p2-4.js +288 -0
- package/dist/measure/p2-5.js +8 -0
- package/dist/measure/p3-1.js +16 -0
- package/dist/measure/p3-2.js +26 -0
- package/dist/measure/p3-3.js +37 -0
- package/dist/measure/p3-4.js +26 -0
- package/dist/measure/p3-5.js +26 -0
- package/dist/measure/p4-1.js +184 -0
- package/dist/measure/p4-2.js +27 -0
- package/dist/measure/p4-3.js +106 -0
- package/dist/measure/p4-4.js +309 -0
- package/dist/measure/p5-1.js +117 -0
- package/dist/measure/p5-2.js +80 -0
- package/dist/measure/p5-3.js +21 -0
- package/dist/measure/p5-4.js +161 -0
- package/dist/measure/p5-5.js +158 -0
- package/dist/measure/p6-1.js +20 -0
- package/dist/measure/p6-2.js +20 -0
- package/dist/measure/p6-3.js +40 -0
- package/dist/measure/p6-4.js +31 -0
- package/dist/measure/p6-5.js +60 -0
- package/dist/measure/p6-6.js +54 -0
- package/dist/measure/py/typecheck-config.js +95 -0
- package/dist/measure/registry.js +31 -0
- package/dist/measure/safety/py-tests.js +124 -0
- package/dist/measure/safety/ts-barrels.js +68 -0
- package/dist/measure/safety/ts-cases.js +111 -0
- package/dist/measure/safety/ts-credits.js +101 -0
- package/dist/measure/safety/ts-imports.js +63 -0
- package/dist/measure/safety/ts-resolve.js +43 -0
- package/dist/measure/ts/directives.js +37 -0
- package/dist/measure/ts/exports.js +60 -0
- package/dist/measure/ts/tsconfig.js +179 -0
- package/dist/measure/types.js +1 -0
- package/dist/measure/units.js +41 -0
- package/dist/methodology/load.js +159 -0
- package/dist/methodology/schema.js +105 -0
- package/dist/python/deadcode.js +27 -0
- package/dist/python/env.js +13 -0
- package/dist/python/helper.js +24 -0
- package/dist/python/imports.js +40 -0
- package/dist/python/nox.js +16 -0
- package/dist/python/scope.js +35 -0
- package/dist/python/tests.js +19 -0
- package/dist/python/typing.js +27 -0
- package/dist/report/escape.js +5 -0
- package/dist/report/format.js +75 -0
- package/dist/report/render.js +213 -0
- package/dist/report/roadmap.js +58 -0
- package/dist/report/scares.js +83 -0
- package/dist/report/style.js +27 -0
- package/dist/report-only/hotspots.js +48 -0
- package/dist/route/check.js +37 -0
- package/dist/route/files.js +74 -0
- package/dist/route/order.js +22 -0
- package/dist/route/select.js +114 -0
- package/dist/route/write.js +13 -0
- package/dist/run/read.js +89 -0
- package/dist/run/recompute.js +19 -0
- package/dist/run/scan.js +196 -0
- package/dist/run/schema.js +1 -0
- package/dist/run/score-run.js +42 -0
- package/dist/run/write.js +4 -0
- package/dist/scope/build.js +173 -0
- package/dist/scope/packages.js +87 -0
- package/dist/scope/rules.js +21 -0
- package/dist/scope/tracked.js +34 -0
- package/dist/scope/ts-lines.js +63 -0
- package/dist/scope/types.js +7 -0
- package/dist/scoring/aggregate.js +38 -0
- package/dist/scoring/pool.js +65 -0
- package/dist/scoring/score.js +81 -0
- package/dist/scoring/what-if.js +38 -0
- package/dist/scripts/checks.js +53 -0
- package/dist/scripts/ci.js +139 -0
- package/dist/scripts/entry-points/find.js +31 -0
- package/dist/scripts/entry-points/index.js +18 -0
- package/dist/scripts/entry-points/justfile.js +62 -0
- package/dist/scripts/entry-points/makefile.js +66 -0
- package/dist/scripts/entry-points/nox.js +29 -0
- package/dist/scripts/entry-points/package-json.js +21 -0
- package/dist/scripts/entry-points/pyproject.js +73 -0
- package/dist/scripts/entry-points/tox.js +130 -0
- package/dist/scripts/expand.js +300 -0
- package/dist/scripts/triggers.js +151 -0
- package/dist/scripts/types.js +1 -0
- package/dist/submit/constants.js +10 -0
- package/dist/submit/payload.js +42 -0
- package/dist/submit/repo-identity.js +46 -0
- package/dist/submit/send.js +29 -0
- package/dist/tools/catalog.js +46 -0
- package/dist/tools/image.js +180 -0
- package/dist/tools/kit.js +71 -0
- package/dist/tools/knip.js +169 -0
- package/dist/tools/outcome.js +21 -0
- package/dist/tools/resolve.js +66 -0
- package/dist/tools/workcopy.js +27 -0
- package/dist/util/ini.js +37 -0
- package/dist/util/pep440.js +163 -0
- package/dist/util/toml.js +27 -0
- package/docker/Dockerfile +142 -0
- package/docker/bin/health-capture +5 -0
- package/docker/bin/health-coverage-install +5 -0
- package/docker/bin/health-eslint +66 -0
- package/docker/bin/health-pytest +10 -0
- package/docker/bin/health-tsc +10 -0
- package/docker/bin/health-venv +11 -0
- package/docker/config/eslint.config.mjs +4 -0
- package/docker/config/eslint.rules.mjs +65 -0
- package/docker/config/eslint.untyped.config.mjs +4 -0
- package/docker/config/knip.json +1 -0
- package/docker/config/ruff.toml +7 -0
- package/methodology/LICENSE +396 -0
- package/methodology/v1.0/METHODOLOGY.md +1176 -0
- package/methodology/v1.0/dimensions/1-safety-net.yaml +240 -0
- package/methodology/v1.0/dimensions/2-type-static-rigor.yaml +289 -0
- package/methodology/v1.0/dimensions/3-architecture-boundaries.yaml +266 -0
- package/methodology/v1.0/dimensions/4-complexity-maintainability.yaml +192 -0
- package/methodology/v1.0/dimensions/5-dependency-security-hygiene.yaml +223 -0
- package/methodology/v1.0/dimensions/6-operability-feedback-loop.yaml +279 -0
- package/methodology/v1.0/methodology.yaml +242 -0
- package/methodology/v1.0/presets/django.yaml +43 -0
- package/methodology/v1.0/presets/express.yaml +47 -0
- package/methodology/v1.0/presets/fastapi.yaml +52 -0
- package/methodology/v1.0/presets/generic.yaml +41 -0
- package/methodology/v1.0/presets/nextjs.yaml +50 -0
- package/methodology/v1.0/scope.yaml +115 -0
- package/package.json +61 -4
- package/python/dead_code_helper.py +265 -0
- package/python/env_helper.py +71 -0
- package/python/graph_helper.py +277 -0
- package/python/imports_helper.py +132 -0
- package/python/nox_helper.py +79 -0
- package/python/scope_helper.py +96 -0
- package/python/tests_helper.py +171 -0
- package/python/typing_helper.py +235 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""Import graph and contract violations for one unit, through grimp and import-linter.
|
|
2
|
+
|
|
3
|
+
Run with the interpreter that holds import-linter (see src/measure/d3/py/interpreter.ts), in isolated mode:
|
|
4
|
+
python -I graph_helper.py --in in.json --out out.json
|
|
5
|
+
|
|
6
|
+
Input: {"workDir", "roots": [{"dir", "name"}], "declared": "<abs path>" | null, "contracts": [options]}
|
|
7
|
+
Output: {"versions", "emptied", "edges", "violations", "skipped"} or {"error": "<message>"}
|
|
8
|
+
|
|
9
|
+
Repository code is never executed: grimp and import-linter read source files, and each root package is
|
|
10
|
+
looked up with importlib.util.find_spec (top-level names only, no package __init__ runs).
|
|
11
|
+
"""
|
|
12
|
+
import argparse
|
|
13
|
+
import ast
|
|
14
|
+
import copy
|
|
15
|
+
import importlib.metadata
|
|
16
|
+
import importlib.util
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
|
|
21
|
+
DIRECTION_TYPES = ("layers", "forbidden", "independence")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def fail(message):
|
|
25
|
+
return {"error": message}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_import_linter():
|
|
29
|
+
try:
|
|
30
|
+
import grimp
|
|
31
|
+
from importlinter import configuration
|
|
32
|
+
from importlinter.application import use_cases
|
|
33
|
+
except ImportError as exc:
|
|
34
|
+
return None, fail("import-linter cannot be imported: %s" % exc)
|
|
35
|
+
version = importlib.metadata.version("import-linter")
|
|
36
|
+
for name in ("read_user_options", "create_report", "_register_contract_types"):
|
|
37
|
+
if not hasattr(use_cases, name):
|
|
38
|
+
return None, fail("unsupported import-linter %s: missing %s" % (version, name))
|
|
39
|
+
for name in ("build_graph",):
|
|
40
|
+
if not hasattr(grimp, name):
|
|
41
|
+
return None, fail("unsupported grimp: missing %s" % name)
|
|
42
|
+
configuration.configure()
|
|
43
|
+
return (grimp, use_cases, configuration, version), None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def empty_unparseable(work_dir, roots):
|
|
47
|
+
emptied = []
|
|
48
|
+
for root in roots:
|
|
49
|
+
base = os.path.join(work_dir, root["dir"], root["name"])
|
|
50
|
+
for current, _dirs, files in os.walk(base):
|
|
51
|
+
for name in files:
|
|
52
|
+
if not name.endswith(".py"):
|
|
53
|
+
continue
|
|
54
|
+
path = os.path.join(current, name)
|
|
55
|
+
try:
|
|
56
|
+
with open(path, "rb") as handle:
|
|
57
|
+
ast.parse(handle.read())
|
|
58
|
+
except (SyntaxError, ValueError, RecursionError, MemoryError):
|
|
59
|
+
with open(path, "wb"):
|
|
60
|
+
pass
|
|
61
|
+
emptied.append(os.path.relpath(path, work_dir).replace(os.sep, "/"))
|
|
62
|
+
return sorted(emptied)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class FileMap:
|
|
66
|
+
"""Module name to repository-relative file, for the modules that have one."""
|
|
67
|
+
|
|
68
|
+
def __init__(self, work_dir, roots):
|
|
69
|
+
self.work_dir = work_dir
|
|
70
|
+
self.roots = roots
|
|
71
|
+
self.cache = {}
|
|
72
|
+
|
|
73
|
+
def file_of(self, module):
|
|
74
|
+
if module in self.cache:
|
|
75
|
+
return self.cache[module]
|
|
76
|
+
found = None
|
|
77
|
+
top = module.split(".")[0]
|
|
78
|
+
parts = module.split(".")
|
|
79
|
+
for root in self.roots:
|
|
80
|
+
if root["name"] != top:
|
|
81
|
+
continue
|
|
82
|
+
base = os.path.join(self.work_dir, root["dir"], *parts)
|
|
83
|
+
for candidate in (base + ".py", os.path.join(base, "__init__.py")):
|
|
84
|
+
if os.path.isfile(candidate):
|
|
85
|
+
found = os.path.relpath(candidate, self.work_dir).replace(os.sep, "/")
|
|
86
|
+
break
|
|
87
|
+
if found:
|
|
88
|
+
break
|
|
89
|
+
self.cache[module] = found
|
|
90
|
+
return found
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def emit_edges(graph, files):
|
|
94
|
+
edges = []
|
|
95
|
+
for module in sorted(graph.modules):
|
|
96
|
+
source = files.file_of(module)
|
|
97
|
+
if source is None:
|
|
98
|
+
continue
|
|
99
|
+
for imported in sorted(graph.find_modules_directly_imported_by(module)):
|
|
100
|
+
target = files.file_of(imported)
|
|
101
|
+
if target is None or target == source:
|
|
102
|
+
continue
|
|
103
|
+
lines = sorted(
|
|
104
|
+
{d["line_number"] for d in graph.get_import_details(importer=module, imported=imported)}
|
|
105
|
+
)
|
|
106
|
+
edges.append({"from": source, "to": target, "lines": lines})
|
|
107
|
+
return edges
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def one_step_chains(raw_chains):
|
|
111
|
+
for item in raw_chains:
|
|
112
|
+
chain = item["chain"] if isinstance(item, dict) else item
|
|
113
|
+
if len(chain) == 1:
|
|
114
|
+
yield chain[0]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def violations_of(contract_type, metadata):
|
|
118
|
+
if contract_type == "layers":
|
|
119
|
+
for dependency in metadata.get("invalid_dependencies", []):
|
|
120
|
+
for step in one_step_chains(dependency["routes"]):
|
|
121
|
+
yield step
|
|
122
|
+
elif contract_type == "independence":
|
|
123
|
+
for entry in metadata.get("invalid_chains", []):
|
|
124
|
+
for step in one_step_chains(entry["chains"]):
|
|
125
|
+
yield step
|
|
126
|
+
else:
|
|
127
|
+
for entry in metadata.get("invalid_chains", []):
|
|
128
|
+
for step in one_step_chains(entry["chains"]):
|
|
129
|
+
yield step
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def contract_type_of(contract):
|
|
133
|
+
name = type(contract).__name__.lower()
|
|
134
|
+
if "layer" in name:
|
|
135
|
+
return "layers"
|
|
136
|
+
if "independ" in name:
|
|
137
|
+
return "independence"
|
|
138
|
+
return "forbidden"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def run_report(use_cases, session, contracts):
|
|
142
|
+
from importlinter.application.user_options import UserOptions
|
|
143
|
+
|
|
144
|
+
options = UserOptions(session_options=session, contracts_options=contracts)
|
|
145
|
+
use_cases._register_contract_types(options)
|
|
146
|
+
report = use_cases.create_report(options, cache_dir=None)
|
|
147
|
+
if report.could_not_run:
|
|
148
|
+
reasons = "; ".join(
|
|
149
|
+
"%s: %s" % (name, exc) for name, exc in report.invalid_contract_options.items()
|
|
150
|
+
)
|
|
151
|
+
raise ValueError(reasons)
|
|
152
|
+
return report
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def collect(report, files, out):
|
|
156
|
+
for contract, check in report.get_contracts_and_checks():
|
|
157
|
+
for step in violations_of(contract_type_of(contract), check.metadata):
|
|
158
|
+
imported_file = files.file_of(step["imported"])
|
|
159
|
+
importer_file = files.file_of(step["importer"])
|
|
160
|
+
if importer_file is None:
|
|
161
|
+
continue
|
|
162
|
+
out.append(
|
|
163
|
+
{
|
|
164
|
+
"contract": contract.name,
|
|
165
|
+
"importer": importer_file,
|
|
166
|
+
"imported": imported_file or step["imported"],
|
|
167
|
+
"external": imported_file is None,
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def run_contracts(use_cases, session, contracts, files, violations, skipped):
|
|
173
|
+
if not contracts:
|
|
174
|
+
return
|
|
175
|
+
try:
|
|
176
|
+
collect(run_report(use_cases, session, contracts), files, violations)
|
|
177
|
+
return
|
|
178
|
+
except Exception: # noqa: BLE001 - one bad contract aborts the whole report; retry one by one
|
|
179
|
+
pass
|
|
180
|
+
for options in contracts:
|
|
181
|
+
try:
|
|
182
|
+
collect(run_report(use_cases, copy.deepcopy(session), [options]), files, violations)
|
|
183
|
+
except Exception as exc: # noqa: BLE001
|
|
184
|
+
skipped.append({"contract": options.get("name", "*"), "reason": "%s: %s" % (type(exc).__name__, exc)})
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def declared_contracts(use_cases, declared, root_names, skipped):
|
|
188
|
+
try:
|
|
189
|
+
options = use_cases.read_user_options(declared)
|
|
190
|
+
except Exception as exc: # noqa: BLE001
|
|
191
|
+
skipped.append({"contract": "*", "reason": "%s: %s" % (type(exc).__name__, exc)})
|
|
192
|
+
return None, []
|
|
193
|
+
session = dict(options.session_options)
|
|
194
|
+
session.pop("contract_types", None)
|
|
195
|
+
names = [name for name in session.get("root_packages", []) if name in root_names]
|
|
196
|
+
if not names:
|
|
197
|
+
skipped.append({"contract": "*", "reason": "the declared rules name no first-party package of this unit"})
|
|
198
|
+
return None, []
|
|
199
|
+
session["root_packages"] = names
|
|
200
|
+
# import-linter reads boolean session options as strings
|
|
201
|
+
session["include_external_packages"] = "true"
|
|
202
|
+
session["exclude_type_checking_imports"] = "false"
|
|
203
|
+
contracts = [c for c in options.contracts_options if c.get("type") in DIRECTION_TYPES]
|
|
204
|
+
return session, contracts
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def main():
|
|
208
|
+
parser = argparse.ArgumentParser()
|
|
209
|
+
parser.add_argument("--in", dest="infile", required=True)
|
|
210
|
+
parser.add_argument("--out", dest="outfile", required=True)
|
|
211
|
+
args = parser.parse_args()
|
|
212
|
+
with open(args.infile, encoding="utf8") as handle:
|
|
213
|
+
request = json.load(handle)
|
|
214
|
+
|
|
215
|
+
def write(result):
|
|
216
|
+
with open(args.outfile, "w", encoding="utf8") as handle:
|
|
217
|
+
json.dump(result, handle)
|
|
218
|
+
|
|
219
|
+
loaded, error = load_import_linter()
|
|
220
|
+
if error:
|
|
221
|
+
write(error)
|
|
222
|
+
return
|
|
223
|
+
grimp, use_cases, _configuration, il_version = loaded
|
|
224
|
+
|
|
225
|
+
work_dir = os.path.abspath(request["workDir"])
|
|
226
|
+
roots = request["roots"]
|
|
227
|
+
for root in reversed(roots):
|
|
228
|
+
sys.path.insert(0, os.path.join(work_dir, root["dir"]))
|
|
229
|
+
skipped = []
|
|
230
|
+
emptied = empty_unparseable(work_dir, roots)
|
|
231
|
+
|
|
232
|
+
# A root whose spec is not inside the work copy is a first-party package named like an imported module.
|
|
233
|
+
usable = []
|
|
234
|
+
for root in roots:
|
|
235
|
+
spec = importlib.util.find_spec(root["name"])
|
|
236
|
+
location = spec.origin if spec and spec.origin else None
|
|
237
|
+
locations = list(spec.submodule_search_locations or []) if spec else []
|
|
238
|
+
inside = [p for p in ([location] if location else []) + locations if os.path.abspath(p).startswith(work_dir + os.sep)]
|
|
239
|
+
if spec is None or not inside:
|
|
240
|
+
skipped.append({"contract": "*", "reason": "root package %s is shadowed outside the work copy" % root["name"]})
|
|
241
|
+
continue
|
|
242
|
+
usable.append(root)
|
|
243
|
+
names = [root["name"] for root in usable]
|
|
244
|
+
result = {
|
|
245
|
+
"versions": {"grimp": importlib.metadata.version("grimp"), "import-linter": il_version},
|
|
246
|
+
"emptied": emptied,
|
|
247
|
+
"edges": [],
|
|
248
|
+
"violations": [],
|
|
249
|
+
"skipped": skipped,
|
|
250
|
+
}
|
|
251
|
+
if not usable:
|
|
252
|
+
write(result)
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
files = FileMap(work_dir, usable)
|
|
256
|
+
graph = grimp.build_graph(*names, include_external_packages=True, cache_dir=None)
|
|
257
|
+
result["edges"] = emit_edges(graph, files)
|
|
258
|
+
|
|
259
|
+
session = {"root_packages": names, "include_external_packages": "true", "exclude_type_checking_imports": "false"}
|
|
260
|
+
run_contracts(use_cases, session, request.get("contracts", []), files, result["violations"], skipped)
|
|
261
|
+
if request.get("declared"):
|
|
262
|
+
declared_session, declared = declared_contracts(use_cases, request["declared"], set(names), skipped)
|
|
263
|
+
if declared_session is not None:
|
|
264
|
+
run_contracts(use_cases, declared_session, declared, files, result["violations"], skipped)
|
|
265
|
+
write(result)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
if __name__ == "__main__":
|
|
269
|
+
try:
|
|
270
|
+
main()
|
|
271
|
+
except Exception as exc: # noqa: BLE001
|
|
272
|
+
try:
|
|
273
|
+
outfile = sys.argv[sys.argv.index("--out") + 1]
|
|
274
|
+
with open(outfile, "w", encoding="utf8") as handle:
|
|
275
|
+
json.dump(fail("%s: %s" % (type(exc).__name__, exc)), handle)
|
|
276
|
+
except (ValueError, IndexError, OSError):
|
|
277
|
+
raise exc
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Import statements, their flags and builtin open() calls per file. Stdlib only.
|
|
2
|
+
|
|
3
|
+
Input (stdin): {"root": "<abs>", "files": ["<rel>", ...]}
|
|
4
|
+
Output (stdout): {"files": {"<rel>": {"syntaxError": bool, "imports": [...], "opensBuiltin": bool}}}
|
|
5
|
+
"""
|
|
6
|
+
import ast
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def is_type_checking(test):
|
|
13
|
+
if isinstance(test, ast.Name):
|
|
14
|
+
return test.id == "TYPE_CHECKING"
|
|
15
|
+
if isinstance(test, ast.Attribute):
|
|
16
|
+
return test.attr == "TYPE_CHECKING"
|
|
17
|
+
return False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def bound_names(tree):
|
|
21
|
+
"""Every name the file binds anywhere, for the 'defines its own open' rule."""
|
|
22
|
+
names = set()
|
|
23
|
+
for node in ast.walk(tree):
|
|
24
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
25
|
+
names.add(node.name)
|
|
26
|
+
elif isinstance(node, ast.Name) and isinstance(node.ctx, (ast.Store, ast.Del)):
|
|
27
|
+
names.add(node.id)
|
|
28
|
+
elif isinstance(node, ast.arg):
|
|
29
|
+
names.add(node.arg)
|
|
30
|
+
elif isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
31
|
+
for alias in node.names:
|
|
32
|
+
names.add(alias.asname or alias.name.split(".")[0])
|
|
33
|
+
elif isinstance(node, ast.ExceptHandler) and node.name:
|
|
34
|
+
names.add(node.name)
|
|
35
|
+
elif isinstance(node, (ast.Global, ast.Nonlocal)):
|
|
36
|
+
names.update(node.names)
|
|
37
|
+
elif isinstance(node, (ast.MatchAs, ast.MatchStar)) and node.name:
|
|
38
|
+
names.add(node.name)
|
|
39
|
+
elif isinstance(node, ast.MatchMapping) and node.rest:
|
|
40
|
+
names.add(node.rest)
|
|
41
|
+
return names
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def analyze(source):
|
|
45
|
+
tree = ast.parse(source)
|
|
46
|
+
imports = []
|
|
47
|
+
|
|
48
|
+
def visit(node, lazy, type_only):
|
|
49
|
+
if isinstance(node, ast.Import):
|
|
50
|
+
imports.append(
|
|
51
|
+
{
|
|
52
|
+
"line": node.lineno,
|
|
53
|
+
"level": 0,
|
|
54
|
+
"modules": [alias.name for alias in node.names],
|
|
55
|
+
"lazy": lazy,
|
|
56
|
+
"typeOnly": type_only,
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
return
|
|
60
|
+
if isinstance(node, ast.ImportFrom):
|
|
61
|
+
imports.append(
|
|
62
|
+
{
|
|
63
|
+
"line": node.lineno,
|
|
64
|
+
"level": node.level,
|
|
65
|
+
"modules": [node.module or ""],
|
|
66
|
+
"lazy": lazy,
|
|
67
|
+
"typeOnly": type_only,
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
return
|
|
71
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
72
|
+
for child in node.body:
|
|
73
|
+
visit(child, True, type_only)
|
|
74
|
+
# decorators, defaults and annotations run at definition time
|
|
75
|
+
for child in ast.iter_child_nodes(node):
|
|
76
|
+
if child not in node.body:
|
|
77
|
+
visit(child, lazy, type_only)
|
|
78
|
+
return
|
|
79
|
+
if isinstance(node, ast.Lambda):
|
|
80
|
+
visit(node.body, True, type_only)
|
|
81
|
+
for child in ast.iter_child_nodes(node):
|
|
82
|
+
if child is not node.body:
|
|
83
|
+
visit(child, lazy, type_only)
|
|
84
|
+
return
|
|
85
|
+
if isinstance(node, ast.If) and is_type_checking(node.test):
|
|
86
|
+
visit(node.test, lazy, type_only)
|
|
87
|
+
for child in node.body:
|
|
88
|
+
visit(child, lazy, True)
|
|
89
|
+
for child in node.orelse:
|
|
90
|
+
visit(child, lazy, type_only)
|
|
91
|
+
return
|
|
92
|
+
for child in ast.iter_child_nodes(node):
|
|
93
|
+
visit(child, lazy, type_only)
|
|
94
|
+
|
|
95
|
+
visit(tree, False, False)
|
|
96
|
+
|
|
97
|
+
opens = False
|
|
98
|
+
shadowed = "open" in bound_names(tree)
|
|
99
|
+
for node in ast.walk(tree):
|
|
100
|
+
if not isinstance(node, ast.Call):
|
|
101
|
+
continue
|
|
102
|
+
func = node.func
|
|
103
|
+
if isinstance(func, ast.Name) and func.id == "open" and not shadowed:
|
|
104
|
+
opens = True
|
|
105
|
+
elif (
|
|
106
|
+
isinstance(func, ast.Attribute)
|
|
107
|
+
and func.attr == "open"
|
|
108
|
+
and isinstance(func.value, ast.Name)
|
|
109
|
+
and func.value.id == "builtins"
|
|
110
|
+
):
|
|
111
|
+
opens = True
|
|
112
|
+
return {"syntaxError": False, "imports": imports, "opensBuiltin": opens}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def main():
|
|
116
|
+
request = json.load(sys.stdin)
|
|
117
|
+
root = request["root"]
|
|
118
|
+
result = {}
|
|
119
|
+
for rel in request["files"]:
|
|
120
|
+
try:
|
|
121
|
+
with open(os.path.join(root, rel), "rb") as handle:
|
|
122
|
+
source = handle.read()
|
|
123
|
+
result[rel] = analyze(source)
|
|
124
|
+
except (SyntaxError, ValueError, RecursionError, MemoryError):
|
|
125
|
+
result[rel] = {"syntaxError": True, "imports": [], "opensBuiltin": False}
|
|
126
|
+
except OSError:
|
|
127
|
+
result[rel] = {"syntaxError": True, "imports": [], "opensBuiltin": False}
|
|
128
|
+
json.dump({"files": result}, sys.stdout)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
main()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Sessions of noxfile.py files. Standard library only.
|
|
2
|
+
|
|
3
|
+
Reads {"files": [relative paths]} as JSON on stdin and prints
|
|
4
|
+
{path: {"ok": false} | {"ok": true, "sessions": [{"name", "commands": [str], "notify": [str]}]}}.
|
|
5
|
+
|
|
6
|
+
A session is a function decorated with nox.session (with or without arguments). Its name is the name=
|
|
7
|
+
keyword when that is a string literal, else the function name. Commands are session.run(...) and
|
|
8
|
+
session.run_always(...) calls with the positional arguments joined by spaces (a non-literal argument
|
|
9
|
+
becomes "?"; keyword arguments are ignored). session.notify("<name>") is a call to another session.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import ast
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def literal(node):
|
|
18
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
19
|
+
return node.value
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def session_decorator(node):
|
|
24
|
+
target = node.func if isinstance(node, ast.Call) else node
|
|
25
|
+
if isinstance(target, ast.Attribute):
|
|
26
|
+
return target.attr == "session" and isinstance(target.value, ast.Name) and target.value.id == "nox"
|
|
27
|
+
return isinstance(target, ast.Name) and target.id == "session"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def session_name(func, decorator):
|
|
31
|
+
if isinstance(decorator, ast.Call):
|
|
32
|
+
for keyword in decorator.keywords:
|
|
33
|
+
if keyword.arg == "name":
|
|
34
|
+
name = literal(keyword.value)
|
|
35
|
+
if name is not None:
|
|
36
|
+
return name
|
|
37
|
+
return func.name
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def analyze(path):
|
|
41
|
+
with open(path, "rb") as handle:
|
|
42
|
+
tree = ast.parse(handle.read().decode("utf-8", errors="replace"), filename=path)
|
|
43
|
+
sessions = []
|
|
44
|
+
for func in ast.walk(tree):
|
|
45
|
+
if not isinstance(func, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
46
|
+
continue
|
|
47
|
+
decorators = [d for d in func.decorator_list if session_decorator(d)]
|
|
48
|
+
if not decorators:
|
|
49
|
+
continue
|
|
50
|
+
arg = func.args.args[0].arg if func.args.args else "session"
|
|
51
|
+
commands = []
|
|
52
|
+
notify = []
|
|
53
|
+
for node in ast.walk(func):
|
|
54
|
+
if not (isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute)):
|
|
55
|
+
continue
|
|
56
|
+
if not (isinstance(node.func.value, ast.Name) and node.func.value.id == arg):
|
|
57
|
+
continue
|
|
58
|
+
if node.func.attr in ("run", "run_always"):
|
|
59
|
+
parts = [literal(a) if literal(a) is not None else "?" for a in node.args if not isinstance(a, ast.Starred)]
|
|
60
|
+
if parts:
|
|
61
|
+
commands.append(" ".join(parts))
|
|
62
|
+
elif node.func.attr == "notify" and node.args and literal(node.args[0]) is not None:
|
|
63
|
+
notify.append(literal(node.args[0]))
|
|
64
|
+
sessions.append({"name": session_name(func, decorators[0]), "commands": commands, "notify": notify})
|
|
65
|
+
return sessions
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def main():
|
|
69
|
+
request = json.load(sys.stdin)
|
|
70
|
+
out = {}
|
|
71
|
+
for path in request["files"]:
|
|
72
|
+
try:
|
|
73
|
+
out[path] = {"ok": True, "sessions": analyze(path)}
|
|
74
|
+
except (SyntaxError, ValueError, OSError):
|
|
75
|
+
out[path] = {"ok": False}
|
|
76
|
+
json.dump(out, sys.stdout)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
main()
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Line classification for Python files. Standard library only.
|
|
2
|
+
|
|
3
|
+
Reads {"files": [relative paths]} as JSON on stdin and prints
|
|
4
|
+
{path: {"parsed": bool, "hasDef": bool, "lines": int}} as JSON on stdout.
|
|
5
|
+
"lines" is the number of non-blank lines that are not comment-only and not
|
|
6
|
+
docstring lines.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import ast
|
|
10
|
+
import io
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
import tokenize
|
|
14
|
+
|
|
15
|
+
NON_CODE = {
|
|
16
|
+
tokenize.COMMENT,
|
|
17
|
+
tokenize.NL,
|
|
18
|
+
tokenize.NEWLINE,
|
|
19
|
+
tokenize.INDENT,
|
|
20
|
+
tokenize.DEDENT,
|
|
21
|
+
tokenize.ENDMARKER,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def read_text(path):
|
|
26
|
+
with open(path, "rb") as handle:
|
|
27
|
+
raw = handle.read()
|
|
28
|
+
try:
|
|
29
|
+
encoding, _ = tokenize.detect_encoding(io.BytesIO(raw).readline)
|
|
30
|
+
text = raw.decode(encoding)
|
|
31
|
+
except (SyntaxError, UnicodeDecodeError, LookupError):
|
|
32
|
+
text = raw.decode("utf-8", errors="replace")
|
|
33
|
+
return text.replace("\r\n", "\n").replace("\r", "\n")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def fallback(text):
|
|
37
|
+
count = 0
|
|
38
|
+
for line in text.split("\n"):
|
|
39
|
+
stripped = line.strip()
|
|
40
|
+
if stripped and not stripped.startswith("#"):
|
|
41
|
+
count += 1
|
|
42
|
+
return {"parsed": False, "hasDef": True, "lines": count}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def docstring_lines(tree):
|
|
46
|
+
lines = set()
|
|
47
|
+
for node in ast.walk(tree):
|
|
48
|
+
if isinstance(node, (ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
49
|
+
body = node.body
|
|
50
|
+
if body:
|
|
51
|
+
first = body[0]
|
|
52
|
+
if (
|
|
53
|
+
isinstance(first, ast.Expr)
|
|
54
|
+
and isinstance(first.value, ast.Constant)
|
|
55
|
+
and isinstance(first.value.value, str)
|
|
56
|
+
):
|
|
57
|
+
lines.update(range(first.lineno, first.end_lineno + 1))
|
|
58
|
+
return lines
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def analyze(path):
|
|
62
|
+
text = read_text(path)
|
|
63
|
+
try:
|
|
64
|
+
tree = ast.parse(text)
|
|
65
|
+
code = set()
|
|
66
|
+
for tok in tokenize.generate_tokens(io.StringIO(text).readline):
|
|
67
|
+
if tok.type not in NON_CODE:
|
|
68
|
+
code.update(range(tok.start[0], tok.end[0] + 1))
|
|
69
|
+
except (SyntaxError, ValueError, tokenize.TokenError, RecursionError):
|
|
70
|
+
return fallback(text)
|
|
71
|
+
|
|
72
|
+
doc = docstring_lines(tree)
|
|
73
|
+
all_lines = text.split("\n")
|
|
74
|
+
count = 0
|
|
75
|
+
for number in code:
|
|
76
|
+
if number in doc:
|
|
77
|
+
continue
|
|
78
|
+
if number - 1 < len(all_lines) and all_lines[number - 1].strip() != "":
|
|
79
|
+
count += 1
|
|
80
|
+
has_def = any(
|
|
81
|
+
isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef))
|
|
82
|
+
for node in ast.walk(tree)
|
|
83
|
+
)
|
|
84
|
+
return {"parsed": True, "hasDef": has_def, "lines": count}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def main():
|
|
88
|
+
request = json.load(sys.stdin)
|
|
89
|
+
result = {}
|
|
90
|
+
for path in request["files"]:
|
|
91
|
+
result[path] = analyze(path)
|
|
92
|
+
json.dump(result, sys.stdout)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
if __name__ == "__main__":
|
|
96
|
+
main()
|