vibecodedshit 0.0.0-stage → 0.2.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.
Files changed (232) hide show
  1. package/LICENSE +95 -0
  2. package/README.md +64 -2
  3. package/assets/fonts/NotoEmoji-OFL.txt +93 -0
  4. package/assets/fonts/NotoEmoji-Poop.ttf +0 -0
  5. package/assets/fonts/SpaceGrotesk-Bold.ttf +0 -0
  6. package/assets/fonts/SpaceGrotesk-OFL.txt +93 -0
  7. package/dist/bin.js +8 -0
  8. package/dist/card/previous.js +38 -0
  9. package/dist/card/render.js +71 -0
  10. package/dist/cli.js +121 -0
  11. package/dist/commands/card.js +69 -0
  12. package/dist/commands/common.js +1 -0
  13. package/dist/commands/diff.js +54 -0
  14. package/dist/commands/footer.js +25 -0
  15. package/dist/commands/report.js +49 -0
  16. package/dist/commands/route.js +62 -0
  17. package/dist/commands/submit.js +125 -0
  18. package/dist/commands/summary.js +77 -0
  19. package/dist/diff/compare.js +73 -0
  20. package/dist/diff/conditions.js +87 -0
  21. package/dist/diff/format.js +23 -0
  22. package/dist/exec/docker.js +158 -0
  23. package/dist/exec/findings.js +73 -0
  24. package/dist/exec/plan.js +50 -0
  25. package/dist/exec/reports.js +90 -0
  26. package/dist/exec/roots.js +49 -0
  27. package/dist/exec/run.js +182 -0
  28. package/dist/exec/steps/build.js +45 -0
  29. package/dist/exec/steps/coverage-install.js +12 -0
  30. package/dist/exec/steps/install.js +41 -0
  31. package/dist/exec/steps/knip.js +28 -0
  32. package/dist/exec/steps/lint.js +25 -0
  33. package/dist/exec/steps/lockfiles.js +25 -0
  34. package/dist/exec/steps/test.js +17 -0
  35. package/dist/exec/steps/typecheck.js +40 -0
  36. package/dist/exec/test-reports/coverage-py.js +23 -0
  37. package/dist/exec/test-reports/coverage-ranges.js +65 -0
  38. package/dist/exec/test-reports/coverage-summary.js +23 -0
  39. package/dist/exec/test-reports/jest-json.js +31 -0
  40. package/dist/exec/test-reports/junit.js +24 -0
  41. package/dist/exec/test-reports/mocha-json.js +18 -0
  42. package/dist/exec/test-reports/vitest-json.js +5 -0
  43. package/dist/exec/test-runners/detect.js +99 -0
  44. package/dist/exec/test-runners/index.js +6 -0
  45. package/dist/exec/test-runners/jest.js +27 -0
  46. package/dist/exec/test-runners/mocha.js +28 -0
  47. package/dist/exec/test-runners/node-test.js +24 -0
  48. package/dist/exec/test-runners/pytest.js +18 -0
  49. package/dist/exec/test-runners/script.js +48 -0
  50. package/dist/exec/test-runners/spec.js +1 -0
  51. package/dist/exec/test-runners/vitest.js +28 -0
  52. package/dist/exec/tests.js +120 -0
  53. package/dist/exec/types.js +1 -0
  54. package/dist/manifest/package-json.js +36 -0
  55. package/dist/manifest/python.js +106 -0
  56. package/dist/measure/d3/index.js +45 -0
  57. package/dist/measure/d3/presets.js +100 -0
  58. package/dist/measure/d3/py/adapter.js +67 -0
  59. package/dist/measure/d3/py/config.js +80 -0
  60. package/dist/measure/d3/py/contracts.js +57 -0
  61. package/dist/measure/d3/py/domain.js +57 -0
  62. package/dist/measure/d3/py/graph.js +214 -0
  63. package/dist/measure/d3/py/interpreter.js +31 -0
  64. package/dist/measure/d3/py/layers.js +120 -0
  65. package/dist/measure/d3/py/roots.js +58 -0
  66. package/dist/measure/d3/py/types.js +1 -0
  67. package/dist/measure/d3/scc.js +64 -0
  68. package/dist/measure/d3/ts/adapter.js +371 -0
  69. package/dist/measure/d3/ts/classify.js +107 -0
  70. package/dist/measure/d3/ts/config.js +318 -0
  71. package/dist/measure/d3/ts/domain.js +23 -0
  72. package/dist/measure/d3/ts/rules.js +65 -0
  73. package/dist/measure/d3/types.js +5 -0
  74. package/dist/measure/deps/cvss.js +66 -0
  75. package/dist/measure/deps/declared.js +195 -0
  76. package/dist/measure/deps/files.js +40 -0
  77. package/dist/measure/deps/lockfiles.js +308 -0
  78. package/dist/measure/deps/osv.js +122 -0
  79. package/dist/measure/deps/registry.js +142 -0
  80. package/dist/measure/deps/resolved.js +78 -0
  81. package/dist/measure/deps/units.js +5 -0
  82. package/dist/measure/eslint/health-plugin.js +19 -0
  83. package/dist/measure/ops/docs-commands.js +73 -0
  84. package/dist/measure/ops/docs-resolve.js +217 -0
  85. package/dist/measure/ops/entry-points.js +49 -0
  86. package/dist/measure/ops/env-example.js +20 -0
  87. package/dist/measure/ops/env-reads.js +46 -0
  88. package/dist/measure/ops/runtime.js +73 -0
  89. package/dist/measure/p1-1.js +8 -0
  90. package/dist/measure/p1-2.js +8 -0
  91. package/dist/measure/p1-3.js +8 -0
  92. package/dist/measure/p1-4.js +41 -0
  93. package/dist/measure/p1-5.js +59 -0
  94. package/dist/measure/p2-1.js +8 -0
  95. package/dist/measure/p2-2.js +44 -0
  96. package/dist/measure/p2-3.js +81 -0
  97. package/dist/measure/p2-4.js +288 -0
  98. package/dist/measure/p2-5.js +8 -0
  99. package/dist/measure/p3-1.js +16 -0
  100. package/dist/measure/p3-2.js +26 -0
  101. package/dist/measure/p3-3.js +37 -0
  102. package/dist/measure/p3-4.js +26 -0
  103. package/dist/measure/p3-5.js +26 -0
  104. package/dist/measure/p4-1.js +184 -0
  105. package/dist/measure/p4-2.js +27 -0
  106. package/dist/measure/p4-3.js +106 -0
  107. package/dist/measure/p4-4.js +309 -0
  108. package/dist/measure/p5-1.js +117 -0
  109. package/dist/measure/p5-2.js +80 -0
  110. package/dist/measure/p5-3.js +21 -0
  111. package/dist/measure/p5-4.js +161 -0
  112. package/dist/measure/p5-5.js +158 -0
  113. package/dist/measure/p6-1.js +20 -0
  114. package/dist/measure/p6-2.js +20 -0
  115. package/dist/measure/p6-3.js +40 -0
  116. package/dist/measure/p6-4.js +31 -0
  117. package/dist/measure/p6-5.js +60 -0
  118. package/dist/measure/p6-6.js +54 -0
  119. package/dist/measure/py/typecheck-config.js +95 -0
  120. package/dist/measure/registry.js +31 -0
  121. package/dist/measure/safety/py-tests.js +124 -0
  122. package/dist/measure/safety/ts-barrels.js +68 -0
  123. package/dist/measure/safety/ts-cases.js +111 -0
  124. package/dist/measure/safety/ts-credits.js +101 -0
  125. package/dist/measure/safety/ts-imports.js +63 -0
  126. package/dist/measure/safety/ts-resolve.js +43 -0
  127. package/dist/measure/ts/directives.js +37 -0
  128. package/dist/measure/ts/exports.js +60 -0
  129. package/dist/measure/ts/tsconfig.js +179 -0
  130. package/dist/measure/types.js +1 -0
  131. package/dist/measure/units.js +41 -0
  132. package/dist/methodology/load.js +159 -0
  133. package/dist/methodology/schema.js +105 -0
  134. package/dist/python/deadcode.js +27 -0
  135. package/dist/python/env.js +13 -0
  136. package/dist/python/helper.js +24 -0
  137. package/dist/python/imports.js +40 -0
  138. package/dist/python/nox.js +16 -0
  139. package/dist/python/scope.js +35 -0
  140. package/dist/python/tests.js +19 -0
  141. package/dist/python/typing.js +27 -0
  142. package/dist/report/escape.js +5 -0
  143. package/dist/report/format.js +75 -0
  144. package/dist/report/render.js +213 -0
  145. package/dist/report/roadmap.js +58 -0
  146. package/dist/report/scares.js +83 -0
  147. package/dist/report/style.js +27 -0
  148. package/dist/report-only/hotspots.js +48 -0
  149. package/dist/route/check.js +37 -0
  150. package/dist/route/files.js +74 -0
  151. package/dist/route/order.js +22 -0
  152. package/dist/route/select.js +114 -0
  153. package/dist/route/write.js +13 -0
  154. package/dist/run/read.js +89 -0
  155. package/dist/run/recompute.js +19 -0
  156. package/dist/run/scan.js +196 -0
  157. package/dist/run/schema.js +1 -0
  158. package/dist/run/score-run.js +42 -0
  159. package/dist/run/write.js +4 -0
  160. package/dist/scope/build.js +173 -0
  161. package/dist/scope/packages.js +87 -0
  162. package/dist/scope/rules.js +21 -0
  163. package/dist/scope/tracked.js +34 -0
  164. package/dist/scope/ts-lines.js +63 -0
  165. package/dist/scope/types.js +7 -0
  166. package/dist/scoring/aggregate.js +38 -0
  167. package/dist/scoring/pool.js +65 -0
  168. package/dist/scoring/score.js +81 -0
  169. package/dist/scoring/what-if.js +38 -0
  170. package/dist/scripts/checks.js +53 -0
  171. package/dist/scripts/ci.js +139 -0
  172. package/dist/scripts/entry-points/find.js +31 -0
  173. package/dist/scripts/entry-points/index.js +18 -0
  174. package/dist/scripts/entry-points/justfile.js +62 -0
  175. package/dist/scripts/entry-points/makefile.js +66 -0
  176. package/dist/scripts/entry-points/nox.js +29 -0
  177. package/dist/scripts/entry-points/package-json.js +21 -0
  178. package/dist/scripts/entry-points/pyproject.js +73 -0
  179. package/dist/scripts/entry-points/tox.js +130 -0
  180. package/dist/scripts/expand.js +300 -0
  181. package/dist/scripts/triggers.js +151 -0
  182. package/dist/scripts/types.js +1 -0
  183. package/dist/submit/constants.js +9 -0
  184. package/dist/submit/payload.js +42 -0
  185. package/dist/submit/repo-identity.js +46 -0
  186. package/dist/submit/send.js +29 -0
  187. package/dist/tools/catalog.js +46 -0
  188. package/dist/tools/image.js +180 -0
  189. package/dist/tools/kit.js +71 -0
  190. package/dist/tools/knip.js +169 -0
  191. package/dist/tools/outcome.js +21 -0
  192. package/dist/tools/resolve.js +66 -0
  193. package/dist/tools/workcopy.js +27 -0
  194. package/dist/util/ini.js +37 -0
  195. package/dist/util/pep440.js +163 -0
  196. package/dist/util/toml.js +27 -0
  197. package/docker/Dockerfile +142 -0
  198. package/docker/bin/health-capture +5 -0
  199. package/docker/bin/health-coverage-install +5 -0
  200. package/docker/bin/health-eslint +66 -0
  201. package/docker/bin/health-pytest +10 -0
  202. package/docker/bin/health-tsc +10 -0
  203. package/docker/bin/health-venv +11 -0
  204. package/docker/config/eslint.config.mjs +4 -0
  205. package/docker/config/eslint.rules.mjs +65 -0
  206. package/docker/config/eslint.untyped.config.mjs +4 -0
  207. package/docker/config/knip.json +1 -0
  208. package/docker/config/ruff.toml +7 -0
  209. package/methodology/LICENSE +396 -0
  210. package/methodology/v1.0/METHODOLOGY.md +1176 -0
  211. package/methodology/v1.0/dimensions/1-safety-net.yaml +240 -0
  212. package/methodology/v1.0/dimensions/2-type-static-rigor.yaml +289 -0
  213. package/methodology/v1.0/dimensions/3-architecture-boundaries.yaml +266 -0
  214. package/methodology/v1.0/dimensions/4-complexity-maintainability.yaml +192 -0
  215. package/methodology/v1.0/dimensions/5-dependency-security-hygiene.yaml +223 -0
  216. package/methodology/v1.0/dimensions/6-operability-feedback-loop.yaml +279 -0
  217. package/methodology/v1.0/methodology.yaml +242 -0
  218. package/methodology/v1.0/presets/django.yaml +43 -0
  219. package/methodology/v1.0/presets/express.yaml +47 -0
  220. package/methodology/v1.0/presets/fastapi.yaml +52 -0
  221. package/methodology/v1.0/presets/generic.yaml +41 -0
  222. package/methodology/v1.0/presets/nextjs.yaml +50 -0
  223. package/methodology/v1.0/scope.yaml +115 -0
  224. package/package.json +61 -4
  225. package/python/dead_code_helper.py +265 -0
  226. package/python/env_helper.py +71 -0
  227. package/python/graph_helper.py +277 -0
  228. package/python/imports_helper.py +132 -0
  229. package/python/nox_helper.py +79 -0
  230. package/python/scope_helper.py +96 -0
  231. package/python/tests_helper.py +171 -0
  232. 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()