entrygraph 0.1.3__tar.gz → 0.1.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. {entrygraph-0.1.3 → entrygraph-0.1.5}/PKG-INFO +1 -1
  2. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/_version.py +2 -2
  3. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/detect/entrypoints/python.py +25 -14
  4. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/detect/frameworks.py +10 -0
  5. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/detect/manifests.py +3 -1
  6. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/graph/scoring.py +1 -1
  7. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/pipeline/scanner.py +43 -22
  8. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/resolve/resolver.py +4 -1
  9. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/test_entrypoint_expansion.py +11 -0
  10. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/test_frameworks.py +12 -0
  11. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/test_manifests.py +11 -0
  12. entrygraph-0.1.5/tests/test_pool.py +64 -0
  13. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/test_resolver.py +32 -0
  14. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/test_scoring.py +9 -0
  15. {entrygraph-0.1.3 → entrygraph-0.1.5}/.github/workflows/ci.yml +0 -0
  16. {entrygraph-0.1.3 → entrygraph-0.1.5}/.github/workflows/release.yml +0 -0
  17. {entrygraph-0.1.3 → entrygraph-0.1.5}/.gitignore +0 -0
  18. {entrygraph-0.1.3 → entrygraph-0.1.5}/LICENSE +0 -0
  19. {entrygraph-0.1.3 → entrygraph-0.1.5}/README.md +0 -0
  20. {entrygraph-0.1.3 → entrygraph-0.1.5}/RELEASING.md +0 -0
  21. {entrygraph-0.1.3 → entrygraph-0.1.5}/pyproject.toml +0 -0
  22. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/__init__.py +0 -0
  23. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/__main__.py +0 -0
  24. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/api.py +0 -0
  25. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/cli/__init__.py +0 -0
  26. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/cli/main.py +0 -0
  27. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/cli/render.py +0 -0
  28. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/data/sinks/csharp.toml +0 -0
  29. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/data/sinks/go.toml +0 -0
  30. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/data/sinks/java.toml +0 -0
  31. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/data/sinks/javascript.toml +0 -0
  32. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/data/sinks/lib_javascript.toml +0 -0
  33. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/data/sinks/lib_python.toml +0 -0
  34. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/data/sinks/php.toml +0 -0
  35. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/data/sinks/python.toml +0 -0
  36. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/data/sinks/ruby.toml +0 -0
  37. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/data/sinks/rust.toml +0 -0
  38. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/db/__init__.py +0 -0
  39. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/db/engine.py +0 -0
  40. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/db/meta.py +0 -0
  41. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/db/models.py +0 -0
  42. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/db/queries.py +0 -0
  43. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/detect/__init__.py +0 -0
  44. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/detect/entrypoints/__init__.py +0 -0
  45. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/detect/entrypoints/base.py +0 -0
  46. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/detect/entrypoints/configs.py +0 -0
  47. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/detect/entrypoints/csharp.py +0 -0
  48. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/detect/entrypoints/golang.py +0 -0
  49. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/detect/entrypoints/java.py +0 -0
  50. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/detect/entrypoints/javascript.py +0 -0
  51. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/detect/entrypoints/php.py +0 -0
  52. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/detect/entrypoints/ruby.py +0 -0
  53. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/detect/entrypoints/rust.py +0 -0
  54. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/detect/taint.py +0 -0
  55. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/errors.py +0 -0
  56. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/extract/__init__.py +0 -0
  57. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/extract/base.py +0 -0
  58. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/extract/csharp.py +0 -0
  59. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/extract/golang.py +0 -0
  60. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/extract/ir.py +0 -0
  61. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/extract/java.py +0 -0
  62. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/extract/javascript.py +0 -0
  63. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/extract/php.py +0 -0
  64. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/extract/python.py +0 -0
  65. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/extract/registry.py +0 -0
  66. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/extract/ruby.py +0 -0
  67. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/extract/rust.py +0 -0
  68. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/fs/__init__.py +0 -0
  69. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/fs/hashing.py +0 -0
  70. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/fs/lang.py +0 -0
  71. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/fs/walker.py +0 -0
  72. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/graph/__init__.py +0 -0
  73. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/graph/adjacency.py +0 -0
  74. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/graph/cte.py +0 -0
  75. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/kinds.py +0 -0
  76. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/parsing/__init__.py +0 -0
  77. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/parsing/parsers.py +0 -0
  78. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/parsing/queries.py +0 -0
  79. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/pipeline/__init__.py +0 -0
  80. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/pipeline/worker.py +0 -0
  81. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/pipeline/writer.py +0 -0
  82. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/py.typed +0 -0
  83. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/queries/csharp/calls.scm +0 -0
  84. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/queries/csharp/definitions.scm +0 -0
  85. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/queries/csharp/imports.scm +0 -0
  86. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/queries/go/calls.scm +0 -0
  87. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/queries/go/definitions.scm +0 -0
  88. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/queries/go/imports.scm +0 -0
  89. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/queries/java/calls.scm +0 -0
  90. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/queries/java/definitions.scm +0 -0
  91. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/queries/java/imports.scm +0 -0
  92. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/queries/javascript/calls.scm +0 -0
  93. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/queries/javascript/definitions.scm +0 -0
  94. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/queries/javascript/imports.scm +0 -0
  95. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/queries/php/calls.scm +0 -0
  96. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/queries/php/definitions.scm +0 -0
  97. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/queries/php/imports.scm +0 -0
  98. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/queries/python/calls.scm +0 -0
  99. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/queries/python/definitions.scm +0 -0
  100. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/queries/python/imports.scm +0 -0
  101. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/queries/ruby/calls.scm +0 -0
  102. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/queries/ruby/definitions.scm +0 -0
  103. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/queries/ruby/imports.scm +0 -0
  104. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/queries/rust/calls.scm +0 -0
  105. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/queries/rust/definitions.scm +0 -0
  106. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/queries/rust/imports.scm +0 -0
  107. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/resolve/__init__.py +0 -0
  108. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/resolve/externals.py +0 -0
  109. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/resolve/hierarchy.py +0 -0
  110. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/resolve/symbol_table.py +0 -0
  111. {entrygraph-0.1.3 → entrygraph-0.1.5}/src/entrygraph/results.py +0 -0
  112. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/conftest.py +0 -0
  113. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/fixtures/csharp/aspnet_app/Controllers/ReportsController.cs +0 -0
  114. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/fixtures/csharp/aspnet_app/Program.cs +0 -0
  115. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/fixtures/csharp/aspnet_app/Services/ReportService.cs +0 -0
  116. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/fixtures/csharp/aspnet_app/app.csproj +0 -0
  117. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/fixtures/go/gin_app/go.mod +0 -0
  118. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/fixtures/go/gin_app/main.go +0 -0
  119. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/fixtures/go/gin_app/service.go +0 -0
  120. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/fixtures/java/spring_app/pom.xml +0 -0
  121. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/fixtures/java/spring_app/src/main/java/com/example/Application.java +0 -0
  122. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/fixtures/java/spring_app/src/main/java/com/example/ReportRunner.java +0 -0
  123. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/fixtures/java/spring_app/src/main/java/com/example/ReportService.java +0 -0
  124. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/fixtures/java/spring_app/src/main/java/com/example/UserController.java +0 -0
  125. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/fixtures/javascript/express_app/package.json +0 -0
  126. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/fixtures/javascript/express_app/src/routes.js +0 -0
  127. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/fixtures/javascript/express_app/src/services.js +0 -0
  128. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/fixtures/php/laravel_app/app/Http/Controllers/ReportController.php +0 -0
  129. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/fixtures/php/laravel_app/artisan +0 -0
  130. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/fixtures/php/laravel_app/composer.json +0 -0
  131. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/fixtures/php/laravel_app/routes/web.php +0 -0
  132. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/fixtures/python/flask_app/app/__init__.py +0 -0
  133. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/fixtures/python/flask_app/app/db.py +0 -0
  134. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/fixtures/python/flask_app/app/routes.py +0 -0
  135. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/fixtures/python/flask_app/app/services.py +0 -0
  136. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/fixtures/python/flask_app/cli.py +0 -0
  137. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/fixtures/python/flask_app/requirements.txt +0 -0
  138. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/fixtures/ruby/sinatra_app/Gemfile +0 -0
  139. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/fixtures/ruby/sinatra_app/app.rb +0 -0
  140. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/fixtures/ruby/sinatra_app/services/runner.rb +0 -0
  141. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/fixtures/rust/axum_app/Cargo.toml +0 -0
  142. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/fixtures/rust/axum_app/src/handlers.rs +0 -0
  143. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/fixtures/rust/axum_app/src/main.rs +0 -0
  144. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/test_api.py +0 -0
  145. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/test_cli.py +0 -0
  146. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/test_engine_pragmas.py +0 -0
  147. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/test_entrypoints.py +0 -0
  148. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/test_extract_csharp.py +0 -0
  149. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/test_extract_go.py +0 -0
  150. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/test_extract_java.py +0 -0
  151. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/test_extract_javascript.py +0 -0
  152. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/test_extract_php.py +0 -0
  153. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/test_extract_python.py +0 -0
  154. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/test_extract_ruby.py +0 -0
  155. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/test_extract_rust.py +0 -0
  156. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/test_hardening.py +0 -0
  157. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/test_incremental.py +0 -0
  158. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/test_indexer.py +0 -0
  159. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/test_lang.py +0 -0
  160. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/test_models.py +0 -0
  161. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/test_reachability.py +0 -0
  162. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/test_render.py +0 -0
  163. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/test_sink_catalog.py +0 -0
  164. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/test_taint.py +0 -0
  165. {entrygraph-0.1.3 → entrygraph-0.1.5}/tests/test_walker.py +0 -0
  166. {entrygraph-0.1.3 → entrygraph-0.1.5}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: entrygraph
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: Language-agnostic code graph: query symbols, entrypoints, and source-to-sink call paths from a SQLite index
5
5
  Project-URL: Repository, https://github.com/brettbergin/entrygraph
6
6
  Author-email: Brett Bergin <brettberginbc@yahoo.com>
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.1.3'
22
- __version_tuple__ = version_tuple = (0, 1, 3)
21
+ __version__ = version = '0.1.5'
22
+ __version_tuple__ = version_tuple = (0, 1, 5)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -7,6 +7,7 @@ import re
7
7
 
8
8
  from entrygraph.detect.entrypoints.base import (
9
9
  EntrypointRule,
10
+ Matcher,
10
11
  first_string_arg,
11
12
  identifier_args,
12
13
  methods_kwarg,
@@ -157,20 +158,30 @@ def _django_urls(x: FileExtraction) -> list[EntrypointHint]:
157
158
  return hints
158
159
 
159
160
 
160
- def _click_commands(x: FileExtraction) -> list[EntrypointHint]:
161
- hints = []
162
- for symbol, decorator in _decorated(x):
163
- if _CLICK_CMD.match(decorator):
164
- hints.append(
165
- EntrypointHint(
166
- rule_id="python.click.command",
167
- kind=EntrypointKind.CLI_COMMAND,
168
- handler_qualified_name=symbol.qualified_name,
169
- name=first_string_arg(decorator) or symbol.name,
170
- framework="click",
161
+ def _click_like(framework: str, rule_id: str) -> Matcher:
162
+ """click and typer share decorator shape (`@app.command`); only the
163
+ reported framework/rule_id differ, so bind them per rule."""
164
+
165
+ def _match(x: FileExtraction) -> list[EntrypointHint]:
166
+ hints = []
167
+ for symbol, decorator in _decorated(x):
168
+ if _CLICK_CMD.match(decorator):
169
+ hints.append(
170
+ EntrypointHint(
171
+ rule_id=rule_id,
172
+ kind=EntrypointKind.CLI_COMMAND,
173
+ handler_qualified_name=symbol.qualified_name,
174
+ name=first_string_arg(decorator) or symbol.name,
175
+ framework=framework,
176
+ )
171
177
  )
172
- )
173
- return hints
178
+ return hints
179
+
180
+ return _match
181
+
182
+
183
+ _click_commands = _click_like("click", "python.click.command")
184
+ _typer_commands = _click_like("typer", "python.typer.command")
174
185
 
175
186
 
176
187
  def _celery_tasks(x: FileExtraction) -> list[EntrypointHint]:
@@ -363,7 +374,7 @@ register(
363
374
  )
364
375
  register(
365
376
  EntrypointRule(
366
- "python.typer.command", "python", "typer", EntrypointKind.CLI_COMMAND, _click_commands
377
+ "python.typer.command", "python", "typer", EntrypointKind.CLI_COMMAND, _typer_commands
367
378
  )
368
379
  )
369
380
  register(
@@ -104,6 +104,16 @@ FRAMEWORKS: list[FrameworkSpec] = [
104
104
  ),
105
105
  ),
106
106
  # JS/TS, Go, Java, Ruby specs land with their extractors (M8)
107
+ FrameworkSpec(
108
+ "aws-lambda-js",
109
+ "javascript",
110
+ (
111
+ FrameworkSignal("file_presence", "serverless.y*ml", 0.5),
112
+ FrameworkSignal("file_presence", "template.y*ml", 0.3),
113
+ FrameworkSignal("manifest_dep", "@types/aws-lambda", 0.6),
114
+ FrameworkSignal("manifest_dep", "aws-lambda", 0.5),
115
+ ),
116
+ ),
107
117
  FrameworkSpec(
108
118
  "express",
109
119
  "javascript",
@@ -223,9 +223,11 @@ def parse_manifests(root: str | Path) -> ManifestDeps:
223
223
  if not manifest.is_file():
224
224
  continue
225
225
  if any(
226
- part in ("node_modules", "vendor", ".venv", "venv", "target", "packages")
226
+ part in ("node_modules", "vendor", ".venv", "venv", "target")
227
227
  for part in manifest.parts
228
228
  ):
229
+ # note: "packages" is intentionally NOT excluded — it is the
230
+ # standard JS workspace layout (packages/<name>/package.json).
229
231
  continue
230
232
  try:
231
233
  text = manifest.read_text(encoding="utf-8", errors="replace")
@@ -33,7 +33,7 @@ _LENGTH_DECAY = 0.97
33
33
  _CONST_TOKEN = re.compile(
34
34
  r"""^(
35
35
  \s | , | = | \( | \) | \[ | \] | \{ | \} | : |
36
- '[^']*' | "[^"]*" | `[^`]*` |
36
+ '[^']*' | "[^"]*" | `(?:[^`$]|\$(?!\{))*` | # backtick: no ${...} interpolation
37
37
  \d[\d_.eExXaAbBcCdDfF]* |
38
38
  True|False|None|null|true|false|nil |
39
39
  [A-Za-z_]\w*\s*= # kwarg name before '='
@@ -11,8 +11,10 @@ from __future__ import annotations
11
11
 
12
12
  import json
13
13
  import os
14
+ import sys
14
15
  import time
15
16
  from concurrent.futures import ProcessPoolExecutor
17
+ from concurrent.futures.process import BrokenProcessPool
16
18
  from datetime import UTC, datetime
17
19
  from pathlib import Path
18
20
 
@@ -147,19 +149,36 @@ def index_repository(
147
149
  def _pool_context():
148
150
  """multiprocessing context for the parse pool.
149
151
 
150
- Prefer ``fork`` where available (POSIX): unlike the ``spawn`` default on
151
- macOS/Windows, fork does not re-import the caller's ``__main__`` module, so
152
- ``CodeGraph.index()`` works from any calling context scripts without an
153
- ``if __name__ == "__main__"`` guard, notebooks, web request handlers rather
154
- than raising a bootstrapping RuntimeError and re-running top-level code in
155
- every worker. tree-sitter parsers are created lazily inside each worker, so
156
- there is no pre-fork C state to corrupt.
152
+ On Linux, prefer ``fork``: unlike ``spawn``, fork does not re-import the
153
+ caller's ``__main__`` module, so ``CodeGraph.index()`` works from any calling
154
+ context scripts without an ``if __name__ == "__main__"`` guard, notebooks,
155
+ web request handlers rather than raising a bootstrapping RuntimeError and
156
+ re-running top-level code in every worker. tree-sitter parsers are created
157
+ lazily inside each worker, so there is no pre-fork C state to corrupt.
158
+
159
+ macOS is excluded: ``fork`` without ``exec`` is unsafe there because Apple
160
+ system frameworks (Objective-C runtime, libdispatch) abort the child if they
161
+ were touched by any thread in the parent (``+[NSNumber initialize] may have
162
+ been in progress ... when fork() was called``). macOS and Windows use
163
+ ``spawn``; the CLI entry points are ``__main__``-guarded, and unguarded
164
+ library callers fall back to sequential extraction in ``_parse_phase``.
157
165
  """
158
166
  import multiprocessing as mp
159
167
 
160
- if "fork" in mp.get_all_start_methods():
168
+ if sys.platform != "darwin" and "fork" in mp.get_all_start_methods():
161
169
  return mp.get_context("fork")
162
- return mp.get_context() # spawn (Windows); callers must guard __main__
170
+ return mp.get_context("spawn")
171
+
172
+
173
+ def _extract_sequential(
174
+ to_index: list[WalkedFile],
175
+ ) -> list[tuple[str, FileExtraction, bool]]:
176
+ results = []
177
+ for wf in to_index:
178
+ result = extract_one(wf)
179
+ if result is not None:
180
+ results.append(result)
181
+ return results
163
182
 
164
183
 
165
184
  def _parse_phase(
@@ -169,19 +188,20 @@ def _parse_phase(
169
188
  return []
170
189
  workers = max_workers if max_workers is not None else (os.cpu_count() or 2)
171
190
  if len(to_index) < _PARALLEL_THRESHOLD or workers <= 1:
172
- results = []
173
- for wf in to_index:
174
- result = extract_one(wf)
175
- if result is not None:
176
- results.append(result)
177
- return results
191
+ return _extract_sequential(to_index)
178
192
 
179
193
  batches = [to_index[i : i + _BATCH] for i in range(0, len(to_index), _BATCH)]
180
- results = []
181
- with ProcessPoolExecutor(max_workers=workers, mp_context=_pool_context()) as pool:
182
- for batch_result in pool.map(extract_batch, batches):
183
- results.extend(batch_result)
184
- return results
194
+ try:
195
+ results = []
196
+ with ProcessPoolExecutor(max_workers=workers, mp_context=_pool_context()) as pool:
197
+ for batch_result in pool.map(extract_batch, batches):
198
+ results.extend(batch_result)
199
+ return results
200
+ except BrokenProcessPool:
201
+ # A worker pool couldn't start or died (e.g. an unguarded __main__ under
202
+ # spawn, or a sandbox with no subprocess support). Degrade to correct,
203
+ # single-threaded extraction rather than crashing the whole index.
204
+ return _extract_sequential(to_index)
185
205
 
186
206
 
187
207
  def _load_or_create_repo(session: Session, root: Path, incremental: bool) -> Repository:
@@ -285,12 +305,13 @@ def _write_files(
285
305
  for path, fid in session.execute(select(File.path, File.id).where(File.repo_id == repo.id)):
286
306
  file_id_by_path[path] = fid
287
307
 
288
- reindexed = {w.path for w in diff.to_index} | {
308
+ to_index_paths = {w.path for w in diff.to_index}
309
+ reindexed = to_index_paths | {
289
310
  w.path for w in walked if w.skip_reason and w.path not in file_id_by_path
290
311
  }
291
312
  new_rows = []
292
313
  for wf in walked:
293
- if wf.path in file_id_by_path and wf.path not in {w.path for w in diff.to_index}:
314
+ if wf.path in file_id_by_path and wf.path not in to_index_paths:
294
315
  continue # unchanged; row already present
295
316
  if wf.path not in reindexed and not wf.skip_reason:
296
317
  continue
@@ -71,6 +71,7 @@ class FileResolver:
71
71
  self.module_symbol_id = module_symbol_id
72
72
  self.table = table
73
73
  self.externals = externals
74
+ self.is_package = is_package
74
75
  self.prefix = LANG_PREFIX.get(extraction.language, extraction.language)
75
76
  self.import_map, self.wildcard_modules = build_import_map(extraction, is_package)
76
77
 
@@ -122,7 +123,9 @@ class FileResolver:
122
123
  seen: set[str] = set()
123
124
  for imp in self.x.imports:
124
125
  module = (
125
- expand_relative(imp, self.x.module_path, False) if imp.is_relative else imp.module
126
+ expand_relative(imp, self.x.module_path, self.is_package)
127
+ if imp.is_relative
128
+ else imp.module
126
129
  )
127
130
  if not module or module in seen:
128
131
  continue
@@ -160,6 +160,17 @@ def test_lambda_js_handler_rule():
160
160
  assert hints and hints[0].kind is EntrypointKind.LAMBDA_HANDLER
161
161
 
162
162
 
163
+ def test_typer_command_reports_typer_not_click():
164
+ # typer reuses click's decorator shape; the hint must carry framework/rule_id
165
+ # for typer, not be mislabeled as click.
166
+ sym = _sym("serve", "cli.serve")
167
+ sym.decorators = ["@app.command()"]
168
+ hints = _run("python.typer.command", _extraction([sym]), frameworks=frozenset({"typer"}))
169
+ assert hints
170
+ assert hints[0].framework == "typer"
171
+ assert hints[0].rule_id == "python.typer.command"
172
+
173
+
163
174
  def test_sidekiq_worker_rule():
164
175
  from entrygraph.detect.entrypoints import rules_for
165
176
 
@@ -33,6 +33,18 @@ def test_language_gating():
33
33
  assert not any(d.name == "rails" for d in detected)
34
34
 
35
35
 
36
+ def test_aws_lambda_js_detected_from_serverless_config():
37
+ # the javascript.aws-lambda.handler entrypoint rule is gated on this spec;
38
+ # regression: the spec was missing, so the rule could never fire.
39
+ detected = detect_frameworks(
40
+ ManifestDeps(javascript={"@types/aws-lambda"}),
41
+ import_signals=set(),
42
+ file_paths=["serverless.yml"],
43
+ languages_present={"javascript"},
44
+ )
45
+ assert any(d.name == "aws-lambda-js" for d in detected)
46
+
47
+
36
48
  def test_typescript_counts_for_js_frameworks():
37
49
  manifests = ManifestDeps(javascript={"express"})
38
50
  detected = detect_frameworks(
@@ -78,6 +78,17 @@ def test_parse_manifests_fixture():
78
78
  assert "requirements.txt" in deps.sources
79
79
 
80
80
 
81
+ def test_parse_manifests_reads_js_workspace_packages(tmp_path: Path):
82
+ # `packages/<name>/package.json` is the standard JS workspace layout and must
83
+ # not be excluded (regression: "packages" was in the skip list).
84
+ pkg = tmp_path / "packages" / "api"
85
+ pkg.mkdir(parents=True)
86
+ (pkg / "package.json").write_text('{"dependencies": {"express": "^4"}}')
87
+ deps = parse_manifests(tmp_path)
88
+ assert "express" in deps.javascript
89
+ assert "packages/api/package.json" in deps.sources
90
+
91
+
81
92
  # ---------------- C1: C#, PHP, Rust manifests ----------------
82
93
  from entrygraph.detect.manifests import ( # noqa: E402
83
94
  parse_cargo_toml,
@@ -0,0 +1,64 @@
1
+ """Parse-pool start method selection and graceful fallback (issue #5)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import multiprocessing as mp
6
+
7
+ import pytest
8
+
9
+ from entrygraph.fs.walker import WalkedFile
10
+ from entrygraph.pipeline import scanner
11
+
12
+
13
+ def _walked(n: int) -> list[WalkedFile]:
14
+ return [
15
+ WalkedFile(
16
+ path=f"f{i}.py",
17
+ abs_path=f"/repo/f{i}.py",
18
+ language="python",
19
+ size_bytes=1,
20
+ mtime_ns=0,
21
+ )
22
+ for i in range(n)
23
+ ]
24
+
25
+
26
+ def test_pool_context_avoids_fork_on_macos(monkeypatch: pytest.MonkeyPatch) -> None:
27
+ monkeypatch.setattr("sys.platform", "darwin")
28
+ ctx = scanner._pool_context()
29
+ assert ctx.get_start_method() != "fork"
30
+
31
+
32
+ def test_pool_context_uses_fork_on_linux(monkeypatch: pytest.MonkeyPatch) -> None:
33
+ if "fork" not in mp.get_all_start_methods():
34
+ pytest.skip("fork start method unavailable on this platform")
35
+ monkeypatch.setattr("sys.platform", "linux")
36
+ ctx = scanner._pool_context()
37
+ assert ctx.get_start_method() == "fork"
38
+
39
+
40
+ def test_parse_phase_falls_back_when_pool_breaks(monkeypatch: pytest.MonkeyPatch) -> None:
41
+ to_index = _walked(scanner._PARALLEL_THRESHOLD + 5)
42
+
43
+ def fake_extract_one(wf: WalkedFile):
44
+ return (wf.path, object(), True)
45
+
46
+ class _BrokenPool:
47
+ def __init__(self, *args, **kwargs):
48
+ pass
49
+
50
+ def __enter__(self):
51
+ return self
52
+
53
+ def __exit__(self, *exc):
54
+ return False
55
+
56
+ def map(self, *args, **kwargs):
57
+ raise scanner.BrokenProcessPool("simulated worker death")
58
+
59
+ monkeypatch.setattr(scanner, "extract_one", fake_extract_one)
60
+ monkeypatch.setattr(scanner, "ProcessPoolExecutor", _BrokenPool)
61
+
62
+ results = scanner._parse_phase(to_index, max_workers=4)
63
+
64
+ assert [r[0] for r in results] == [wf.path for wf in to_index]
@@ -57,6 +57,38 @@ def test_import_based_project_resolution():
57
57
  assert call.confidence is Confidence.IMPORT
58
58
 
59
59
 
60
+ def test_relative_import_from_package_init_resolves_submodule():
61
+ # `from .sub import thing` in the package's own __init__.py (module_path
62
+ # "app", is_package=True) must resolve to the project module "app.sub".
63
+ # Regression: is_package was hardcoded False, dropping "app" and yielding a
64
+ # bogus ".sub" external placeholder.
65
+ table = make_table()
66
+ table.add_module("app.sub", 20)
67
+ x = FileExtraction(
68
+ path="app/__init__.py",
69
+ language="python",
70
+ module_path="app",
71
+ parse_ok=True,
72
+ error_count=0,
73
+ imports=[
74
+ RawImport(
75
+ module="sub",
76
+ imported_name="thing",
77
+ alias="thing",
78
+ span=SPAN,
79
+ is_relative=True,
80
+ relative_level=1,
81
+ )
82
+ ],
83
+ )
84
+ externals = ExternalRegistry(iter(range(100, 200)).__next__)
85
+ resolver = FileResolver(x, 1, table, externals, is_package=True)
86
+ edges = resolver.resolve()
87
+ imp = next(e for e in edges if e.kind is EdgeKind.IMPORTS)
88
+ assert imp.dst_qname == "app.sub"
89
+ assert imp.dst_symbol_id == 20
90
+
91
+
60
92
  def test_external_import_creates_placeholder():
61
93
  table = make_table()
62
94
  imports = [RawImport(module="subprocess", imported_name=None, alias="sub", span=SPAN)]
@@ -17,6 +17,15 @@ def test_is_constant_args():
17
17
  assert not is_constant_args("(a, b, c, d, e, f, ...")
18
18
 
19
19
 
20
+ def test_js_template_literal_interpolation_is_not_constant():
21
+ # a plain backtick template with no interpolation is a literal
22
+ assert is_constant_args("(`ls -la`)")
23
+ # `${...}` carries a variable into the sink -> the archetypal JS command
24
+ # injection must NOT be scored as constant/safe
25
+ assert not is_constant_args("(`ls ${req.query.dir}`)")
26
+ assert not is_constant_args("(`${userInput}`)")
27
+
28
+
20
29
  def test_score_path_ordering_by_severity():
21
30
  common = {
22
31
  "hop_confidences": [int(Confidence.EXACT)],
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes