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,41 @@
1
+ preset:
2
+ key: generic
3
+ name: Generic
4
+ languages: [typescript, javascript, python]
5
+ detect: Used when no other preset matches.
6
+
7
+ conventions: >-
8
+ Globs are relative to the package root and match with or without a src/
9
+ prefix (TypeScript, JavaScript) or a top-level package directory (Python).
10
+ When a file matches several layer globs, the innermost matching directory
11
+ decides its layer.
12
+
13
+ layers:
14
+ shared: ["shared/**", "common/**", "lib/**", "utils/**", "helpers/**"]
15
+ features:
16
+ - "features/**"
17
+ - "modules/**"
18
+ - "routes/**"
19
+ - "pages/**"
20
+ - "app/**"
21
+ - "components/**"
22
+ - "views/**"
23
+ - "controllers/**"
24
+ - "api/**"
25
+
26
+ modules:
27
+ roots: ["features/*", "modules/*", "packages/*"]
28
+ public_entry:
29
+ typescript_javascript: "index.{ts,tsx,js,jsx,mts,mjs,cjs}"
30
+ python: __init__.py
31
+
32
+ rules:
33
+ - name: shared-does-not-import-features
34
+ description: Shared code does not depend on feature, route or UI code.
35
+ from: { in: [shared] }
36
+ to: { in: [features] }
37
+ severity: error
38
+ type_only_imports: included
39
+
40
+ sources:
41
+ - "Martin, R. C. Clean Architecture. Prentice Hall, 2017 (the Dependency Rule)."
@@ -0,0 +1,50 @@
1
+ preset:
2
+ key: nextjs
3
+ name: Next.js
4
+ languages: [typescript, javascript]
5
+ detect: The package declares next as a dependency.
6
+
7
+ conventions: >-
8
+ Globs are relative to the package root and match with or without a src/
9
+ prefix. When a file matches several layer globs, the innermost matching
10
+ directory decides its layer.
11
+
12
+ layers:
13
+ routes: ["app/**", "pages/**"]
14
+ components: ["components/**"]
15
+ lib: ["lib/**", "utils/**"]
16
+ server_only: >-
17
+ Files that import the server-only package, files whose name contains
18
+ .server., and files under server/**.
19
+ client: Files whose first statement is the "use client" directive.
20
+
21
+ modules:
22
+ roots: ["features/*", "modules/*", "packages/*"]
23
+ public_entry: "index.{ts,tsx,js,jsx,mts,mjs}"
24
+
25
+ rules:
26
+ - name: routes-are-leaves
27
+ description: Nothing outside the route directories imports from them.
28
+ from: { not_in: [routes] }
29
+ to: { in: [routes] }
30
+ severity: error
31
+ type_only_imports: included
32
+ - name: lib-does-not-import-ui
33
+ description: Shared code does not depend on components or routes.
34
+ from: { in: [lib] }
35
+ to: { in: [components, routes] }
36
+ severity: error
37
+ type_only_imports: included
38
+ - name: client-does-not-import-server
39
+ description: >-
40
+ A client component does not directly import a server-only file or a
41
+ database package.
42
+ from: { in: [client] }
43
+ to:
44
+ in: [server_only]
45
+ packages: ["@prisma/client", drizzle-orm, pg, mysql2, mongoose, redis, ioredis, server-only]
46
+ severity: error
47
+ type_only_imports: excluded
48
+
49
+ sources:
50
+ - "Next.js: Server and Client Components, keeping server-only code out of the client environment. https://nextjs.org/docs/app/getting-started/server-and-client-components"
@@ -0,0 +1,115 @@
1
+ # Which files count, and how lines are counted.
2
+
3
+ source_files:
4
+ start_from: git ls-files
5
+ extensions:
6
+ typescript: [.ts, .tsx, .mts, .cts]
7
+ javascript: [.js, .jsx, .mjs, .cjs]
8
+ python: [.py]
9
+
10
+ exclusions:
11
+ # Fixed by the methodology. Applied before anything else.
12
+ vendored:
13
+ - "**/node_modules/**"
14
+ - "**/.venv/**"
15
+ - "**/venv/**"
16
+ - "**/vendor/**"
17
+ - "**/third_party/**"
18
+ build_output:
19
+ - "**/dist/**"
20
+ - "**/build/**"
21
+ - "**/.next/**"
22
+ - "**/out/**"
23
+ - "**/__pycache__/**"
24
+ - "**/coverage/**"
25
+ generated:
26
+ - files whose first 5 lines contain "@generated" or "Code generated"
27
+ - "**/*_pb2.py"
28
+ - "**/*_pb2_grpc.py"
29
+ - "**/node_modules/.prisma/**"
30
+ - "**/prisma/generated/**"
31
+ - "**/generated/prisma/**"
32
+ minified:
33
+ - "**/*.min.js"
34
+ - files whose average line length is over 300 characters
35
+ migrations:
36
+ - "**/prisma/migrations/**"
37
+ - "**/alembic/versions/**"
38
+ - "**/migrations/*.py"
39
+ - "**/drizzle/**/*.sql"
40
+ declarations:
41
+ - "**/*.d.ts"
42
+ test_data:
43
+ - "**/__fixtures__/**"
44
+ - "**/__snapshots__/**"
45
+ - "**/testdata/**"
46
+ - "**/test-data/**"
47
+ - "**/test/**/fixtures/**"
48
+ - "**/tests/**/fixtures/**"
49
+ - "**/__tests__/**/fixtures/**"
50
+ - "**/e2e/**/fixtures/**"
51
+
52
+ file_roles:
53
+ test:
54
+ typescript_javascript:
55
+ - "**/*.test.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"
56
+ - "**/*.spec.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"
57
+ - "**/__tests__/**"
58
+ - "**/__mocks__/**"
59
+ - "**/test/**"
60
+ - "**/tests/**"
61
+ - "**/e2e/**"
62
+ python:
63
+ - "**/test_*.py"
64
+ - "**/*_test.py"
65
+ - "**/test/**"
66
+ - "**/tests/**"
67
+ - "**/conftest.py"
68
+ config:
69
+ - "**/*.config.{ts,mts,cts,js,mjs,cjs}"
70
+ - "**/*.config.*.{ts,mts,cts,js,mjs,cjs}"
71
+ - "**/.eslintrc.{js,cjs}"
72
+ - "**/setup.py"
73
+ - "**/noxfile.py"
74
+ logic: >-
75
+ An in-scope source file that is neither a test file nor a config file and
76
+ defines at least one function, method or class. Files that only re-export
77
+ (barrels), only declare types, or only declare constants are not logic
78
+ files.
79
+
80
+ how_tests_are_used:
81
+ dimension_1: Test files are the subject of dimension 1.
82
+ typecheck_and_lint: >-
83
+ Test files are included in 2.1 (typecheck errors) and 2.5 (baseline lint
84
+ findings), because broken tests are a real problem.
85
+ other_static_metrics: >-
86
+ Test files are excluded from every other static metric in dimensions 2
87
+ to 4: escape hatches, typed surface, boundaries, cycles, complexity, file
88
+ size, duplication and dead code.
89
+
90
+ line_counts:
91
+ logic_lines: >-
92
+ Non-blank lines that are not comment-only, in logic files. Python
93
+ docstrings count as comments. Used by 2.3, 4.2 and for unit_weighted
94
+ pooling.
95
+ source_lines: >-
96
+ Non-blank lines that are not comment-only, in logic files and test files.
97
+ Used by 2.1 and 2.5.
98
+
99
+ healthignore:
100
+ file: .healthignore
101
+ syntax: gitignore patterns, relative to the repository root
102
+ allowed: true
103
+ safeguards:
104
+ - The report shows the percentage of lines excluded by .healthignore.
105
+ - health diff refuses to compare runs whose .healthignore contents differ.
106
+ does_not_apply_to: >-
107
+ The secret scan (5.4). Secret scanning covers every tracked file and only
108
+ honours .gitleaksignore, which is part of the ignore set.
109
+
110
+ workspaces:
111
+ detection:
112
+ typescript_javascript: package.json "workspaces", pnpm-workspace.yaml
113
+ python: uv workspace members in pyproject.toml, or each directory with its own pyproject.toml
114
+ scoring: >-
115
+ One headline score for the repository. See pooling in methodology.yaml.
package/package.json CHANGED
@@ -1,6 +1,63 @@
1
1
  {
2
2
  "name": "vibecodedshit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Codebase Health benchmark CLI",
5
+ "license": "Elastic-2.0",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=22"
9
+ },
10
+ "bin": {
11
+ "vibecodedshit": "dist/bin.js"
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "methodology",
16
+ "python",
17
+ "assets",
18
+ "docker",
19
+ "LICENSE",
20
+ "README.md"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "scripts": {
26
+ "typecheck": "tsc --noEmit",
27
+ "lint": "eslint .",
28
+ "test": "vitest run",
29
+ "coverage": "vitest run --coverage --coverage.provider=v8 --coverage.reporter=json-summary --coverage.reporter=text-summary --coverage.include=src/**",
30
+ "test:tools": "HEALTH_TOOLS=1 vitest run test/tools",
31
+ "test:docker": "HEALTH_DOCKER=1 vitest run test/docker",
32
+ "image:build": "docker build -t health:1.0 -f docker/Dockerfile docker",
33
+ "verify": "npm run typecheck && npm run lint && npm run test",
34
+ "build": "tsc -p tsconfig.build.json",
35
+ "prepublishOnly": "npm run build && npm run verify",
36
+ "health": "tsx src/bin.ts"
37
+ },
38
+ "dependencies": {
39
+ "@resvg/resvg-wasm": "2.6.2",
40
+ "@typescript-eslint/parser": "8.71.0",
41
+ "dependency-cruiser": "18.5.0",
42
+ "eslint": "9.39.5",
43
+ "eslint-plugin-sonarjs": "4.2.2",
44
+ "ignore": "^7.0.12",
45
+ "jscpd": "5.4.0",
46
+ "knip": "6.39.0",
47
+ "picomatch": "^4.0.7",
48
+ "semver": "7.8.5",
49
+ "smol-toml": "1.9.0",
50
+ "typescript": "^5.9.3",
51
+ "typescript-eslint": "8.71.0",
52
+ "yaml": "^2.9.1",
53
+ "zod": "^4.6.7"
54
+ },
55
+ "devDependencies": {
56
+ "@types/node": "^26.6.4",
57
+ "@types/picomatch": "^4.0.3",
58
+ "@types/semver": "^7.8.0",
59
+ "@vitest/coverage-v8": "^4.1.11",
60
+ "tsx": "^4.23.15",
61
+ "vitest": "^4.1.11"
62
+ }
63
+ }
@@ -0,0 +1,265 @@
1
+ """Symbols, __all__, imports and class bases of Python files for the 4.4 dead-code parameter. Standard library only.
2
+
3
+ Reads {"files": [relative paths]} as JSON on stdin and prints, per path,
4
+ {"ok": bool,
5
+ "symbols": [{"name", "qualname", "kind": "function"|"class"|"method", "line", "startLine", "decorated"}],
6
+ "allNames": [str] | null,
7
+ "imports": [{"module": str, "level": int, "names": [str]}],
8
+ "classes": {class name: {"bases": [{"name": str, "file": str | null, "class": str | null}]}}}
9
+ as JSON on stdout. "line" is the def/class line, "startLine" the first decorator line (or "line").
10
+ A base with "file" and "class" resolves to a class defined in one of the scanned files; a base with
11
+ null "file" is defined outside them (standard library, third party, or not resolvable). `object` and
12
+ `Generic[...]` are left out. A file that cannot be read or parsed gives {"ok": false}.
13
+ """
14
+
15
+ import ast
16
+ import json
17
+ import sys
18
+
19
+ FUNCTIONS = (ast.FunctionDef, ast.AsyncFunctionDef)
20
+ BLOCKS = (ast.If, ast.Try, ast.With, ast.AsyncWith)
21
+
22
+
23
+ def start_line(node):
24
+ lines = [node.lineno] + [d.lineno for d in node.decorator_list]
25
+ return min(lines)
26
+
27
+
28
+ def symbol(node, kind, qualname):
29
+ return {
30
+ "name": node.name,
31
+ "qualname": qualname,
32
+ "kind": kind,
33
+ "line": node.lineno,
34
+ "startLine": start_line(node),
35
+ "decorated": bool(node.decorator_list),
36
+ }
37
+
38
+
39
+ def child_bodies(node):
40
+ if isinstance(node, ast.If):
41
+ return [node.body, node.orelse]
42
+ if isinstance(node, ast.Try):
43
+ bodies = [node.body, node.orelse, node.finalbody]
44
+ bodies.extend(handler.body for handler in node.handlers)
45
+ return bodies
46
+ return [node.body]
47
+
48
+
49
+ def collect(statements, out):
50
+ """Module-level functions and classes, also inside top-level if/try/with, plus methods of those classes."""
51
+ for node in statements:
52
+ if isinstance(node, FUNCTIONS):
53
+ out.append(symbol(node, "function", node.name))
54
+ elif isinstance(node, ast.ClassDef):
55
+ out.append(symbol(node, "class", node.name))
56
+ for member in node.body:
57
+ if isinstance(member, FUNCTIONS):
58
+ out.append(symbol(member, "method", node.name + "." + member.name))
59
+ elif isinstance(node, BLOCKS):
60
+ for body in child_bodies(node):
61
+ collect(body, out)
62
+
63
+
64
+ def string_items(value):
65
+ if isinstance(value, (ast.List, ast.Tuple)):
66
+ return [e.value for e in value.elts if isinstance(e, ast.Constant) and isinstance(e.value, str)]
67
+ return None
68
+
69
+
70
+ def all_names(tree):
71
+ names = None
72
+ for node in tree.body:
73
+ targets = []
74
+ value = None
75
+ if isinstance(node, ast.Assign):
76
+ targets, value = node.targets, node.value
77
+ elif isinstance(node, ast.AnnAssign) and node.value is not None:
78
+ targets, value = [node.target], node.value
79
+ elif isinstance(node, ast.AugAssign):
80
+ targets, value = [node.target], node.value
81
+ if not any(isinstance(t, ast.Name) and t.id == "__all__" for t in targets):
82
+ continue
83
+ items = string_items(value)
84
+ if items is not None:
85
+ names = (names or []) + items
86
+ return names
87
+
88
+
89
+ def imports(tree):
90
+ found = []
91
+ for node in ast.walk(tree):
92
+ if isinstance(node, ast.Import):
93
+ for alias in node.names:
94
+ found.append({"module": alias.name, "level": 0, "names": []})
95
+ elif isinstance(node, ast.ImportFrom):
96
+ found.append(
97
+ {
98
+ "module": node.module or "",
99
+ "level": node.level,
100
+ "names": [alias.name for alias in node.names],
101
+ }
102
+ )
103
+ return found
104
+
105
+
106
+ # ---- class bases ------------------------------------------------------------------------
107
+
108
+ IGNORED_BASES = {"object", "Generic"}
109
+ MAX_HOPS = 5
110
+
111
+
112
+ def class_nodes(statements, out):
113
+ """Module-level classes, also inside top-level if/try/with (the same ones `collect` reports)."""
114
+ for node in statements:
115
+ if isinstance(node, ast.ClassDef):
116
+ out.append(node)
117
+ elif isinstance(node, BLOCKS):
118
+ for body in child_bodies(node):
119
+ class_nodes(body, out)
120
+
121
+
122
+ def dotted(node):
123
+ """`a.b.C` as ["a", "b", "C"]; `Generic[T]` as its value; None for anything else (a call, ...)."""
124
+ if isinstance(node, ast.Subscript):
125
+ return dotted(node.value)
126
+ if isinstance(node, ast.Name):
127
+ return [node.id]
128
+ if isinstance(node, ast.Attribute):
129
+ head = dotted(node.value)
130
+ return None if head is None else head + [node.attr]
131
+ return None
132
+
133
+
134
+ def bindings(tree):
135
+ """Module-level names bound by imports: name -> ("module", level, parts) or ("from", level, parts, name)."""
136
+ bound = {}
137
+ for node in ast.walk(tree):
138
+ if isinstance(node, ast.Import):
139
+ for alias in node.names:
140
+ parts = alias.name.split(".")
141
+ if alias.asname:
142
+ bound[alias.asname] = ("module", 0, parts)
143
+ else:
144
+ bound[parts[0]] = ("module", 0, parts[:1])
145
+ elif isinstance(node, ast.ImportFrom):
146
+ parts = node.module.split(".") if node.module else []
147
+ for alias in node.names:
148
+ if alias.name != "*":
149
+ bound[alias.asname or alias.name] = ("from", node.level, parts, alias.name)
150
+ return bound
151
+
152
+
153
+ class Index:
154
+ """Classes and import bindings of every scanned file, and module lookup by path."""
155
+
156
+ def __init__(self, trees):
157
+ self.trees = trees
158
+ self.classes = {}
159
+ self.bound = {}
160
+ for path, tree in trees.items():
161
+ nodes = []
162
+ class_nodes(tree.body, nodes)
163
+ self.classes[path] = {node.name: node for node in nodes}
164
+ self.bound[path] = bindings(tree)
165
+
166
+ def module_file(self, importer, level, parts):
167
+ """The scanned file of a module, absolute (path suffix) or relative to `importer`."""
168
+ if level > 0:
169
+ base = importer.split("/")[:-1]
170
+ if level - 1 > len(base):
171
+ return None
172
+ base = base[: len(base) - (level - 1)]
173
+ stem = "/".join(base + parts)
174
+ for candidate in (stem + ".py", stem + "/__init__.py"):
175
+ if candidate in self.trees:
176
+ return candidate
177
+ return None
178
+ if not parts:
179
+ return None
180
+ stem = "/".join(parts)
181
+ for candidate in sorted(self.trees):
182
+ for suffix in (stem + ".py", stem + "/__init__.py"):
183
+ if candidate == suffix or candidate.endswith("/" + suffix):
184
+ return candidate
185
+ return None
186
+
187
+ def class_in(self, path, name, hops):
188
+ """(file, class) for `name` as seen from module `path`, following re-exports."""
189
+ if path is None or hops > MAX_HOPS:
190
+ return None
191
+ if name in self.classes.get(path, {}):
192
+ return (path, name)
193
+ binding = self.bound.get(path, {}).get(name)
194
+ if binding is None or binding[0] != "from":
195
+ return None
196
+ _, level, parts, imported = binding
197
+ return self.class_in(self.module_file(path, level, parts), imported, hops + 1)
198
+
199
+ def resolve(self, path, parts):
200
+ """Resolve base `parts` in module `path` to (file, class), or None."""
201
+ head, rest = parts[0], parts[1:]
202
+ if not rest and head in self.classes.get(path, {}):
203
+ return (path, head)
204
+ binding = self.bound.get(path, {}).get(head)
205
+ if binding is None:
206
+ return None
207
+ if binding[0] == "module":
208
+ _, level, mod = binding
209
+ module_parts = mod + rest[:-1]
210
+ if not rest:
211
+ return None
212
+ return self.class_in(self.module_file(path, level, module_parts), rest[-1], 0)
213
+ _, level, mod, imported = binding
214
+ if not rest:
215
+ return self.class_in(self.module_file(path, level, mod), imported, 0)
216
+ # `from pkg import module` then `module.Class`.
217
+ module = self.module_file(path, level, mod + [imported] + rest[:-1])
218
+ return self.class_in(module, rest[-1], 0)
219
+
220
+ def bases(self, path):
221
+ out = {}
222
+ for name, node in self.classes[path].items():
223
+ entries = []
224
+ for base in node.bases:
225
+ parts = dotted(base)
226
+ if parts is not None and parts[-1] in IGNORED_BASES:
227
+ continue
228
+ found = self.resolve(path, parts) if parts is not None else None
229
+ text = ".".join(parts) if parts is not None else ast.dump(base)[:80]
230
+ entries.append({"name": text, "file": found[0] if found else None, "class": found[1] if found else None})
231
+ out[name] = {"bases": entries}
232
+ return out
233
+
234
+
235
+ def parse(path):
236
+ try:
237
+ with open(path, "rb") as handle:
238
+ return ast.parse(handle.read(), filename=path)
239
+ except (OSError, SyntaxError, ValueError, RecursionError):
240
+ return None
241
+
242
+
243
+ def analyze(path, tree, index):
244
+ if tree is None:
245
+ return {"ok": False}
246
+ symbols = []
247
+ collect(tree.body, symbols)
248
+ return {
249
+ "ok": True,
250
+ "symbols": symbols,
251
+ "allNames": all_names(tree),
252
+ "imports": imports(tree),
253
+ "classes": index.bases(path),
254
+ }
255
+
256
+
257
+ def main():
258
+ request = json.load(sys.stdin)
259
+ trees = {path: parse(path) for path in request["files"]}
260
+ index = Index({path: tree for path, tree in trees.items() if tree is not None})
261
+ json.dump({path: analyze(path, tree, index) for path, tree in trees.items()}, sys.stdout)
262
+
263
+
264
+ if __name__ == "__main__":
265
+ main()
@@ -0,0 +1,71 @@
1
+ """Environment variable names read by Python files (6.5). Standard library only.
2
+
3
+ Reads {"files": [relative paths]} as JSON on stdin and prints {path: {"ok": false} | {"ok": true, "names": [str]}}.
4
+ Reads: os.environ["X"] (Load context only), os.environ.get("X"), os.getenv("X"), also through
5
+ "from os import environ, getenv" and "import os as <alias>". Only literal string names count.
6
+ """
7
+
8
+ import ast
9
+ import json
10
+ import sys
11
+
12
+
13
+ def literal(node):
14
+ if isinstance(node, ast.Constant) and isinstance(node.value, str):
15
+ return node.value
16
+ return None
17
+
18
+
19
+ def analyze(path):
20
+ with open(path, "rb") as handle:
21
+ tree = ast.parse(handle.read().decode("utf-8", errors="replace"), filename=path)
22
+ os_names, environ_names, getenv_names = set(), set(), set()
23
+ for node in ast.walk(tree):
24
+ if isinstance(node, ast.Import):
25
+ for alias in node.names:
26
+ if alias.name == "os":
27
+ os_names.add(alias.asname or "os")
28
+ elif isinstance(node, ast.ImportFrom) and node.level == 0 and node.module == "os":
29
+ for alias in node.names:
30
+ if alias.name == "environ":
31
+ environ_names.add(alias.asname or "environ")
32
+ elif alias.name == "getenv":
33
+ getenv_names.add(alias.asname or "getenv")
34
+
35
+ def is_environ(node):
36
+ if isinstance(node, ast.Name):
37
+ return node.id in environ_names
38
+ return isinstance(node, ast.Attribute) and node.attr == "environ" and isinstance(node.value, ast.Name) and node.value.id in os_names
39
+
40
+ names = set()
41
+ for node in ast.walk(tree):
42
+ if isinstance(node, ast.Subscript) and isinstance(node.ctx, ast.Load) and is_environ(node.value):
43
+ name = literal(node.slice)
44
+ if name is not None:
45
+ names.add(name)
46
+ elif isinstance(node, ast.Call) and node.args:
47
+ func = node.func
48
+ name = literal(node.args[0])
49
+ if name is None:
50
+ continue
51
+ if isinstance(func, ast.Attribute) and func.attr == "get" and is_environ(func.value):
52
+ names.add(name)
53
+ elif isinstance(func, ast.Attribute) and func.attr == "getenv" and isinstance(func.value, ast.Name) and func.value.id in os_names:
54
+ names.add(name)
55
+ elif isinstance(func, ast.Name) and func.id in getenv_names:
56
+ names.add(name)
57
+ return sorted(names)
58
+
59
+
60
+ def main():
61
+ request = json.load(sys.stdin)
62
+ out = {}
63
+ for path in request["files"]:
64
+ try:
65
+ out[path] = {"ok": True, "names": analyze(path)}
66
+ except (SyntaxError, ValueError, OSError, RecursionError):
67
+ out[path] = {"ok": False}
68
+ json.dump(out, sys.stdout)
69
+
70
+
71
+ main()