entrygraph 0.1.26__tar.gz → 0.1.28__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 (198) hide show
  1. {entrygraph-0.1.26 → entrygraph-0.1.28}/PKG-INFO +1 -1
  2. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/_version.py +2 -2
  3. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/api.py +23 -3
  4. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/cli/main.py +10 -0
  5. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/data/sinks/csharp.toml +23 -0
  6. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/data/sinks/go.toml +30 -0
  7. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/data/sinks/java.toml +23 -0
  8. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/data/sinks/php.toml +44 -0
  9. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/data/sinks/ruby.toml +30 -0
  10. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/data/sinks/rust.toml +16 -0
  11. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/detect/entrypoints/base.py +11 -0
  12. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/detect/entrypoints/csharp.py +22 -3
  13. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/detect/entrypoints/golang.py +7 -8
  14. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/detect/entrypoints/java.py +41 -4
  15. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/detect/entrypoints/javascript.py +26 -0
  16. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/detect/entrypoints/python.py +8 -2
  17. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/extract/javascript.py +18 -9
  18. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/graph/adjacency.py +49 -9
  19. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/graph/cte.py +9 -6
  20. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/pipeline/scanner.py +61 -10
  21. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/resolve/hierarchy.py +9 -1
  22. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/resolve/resolver.py +18 -6
  23. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/resolve/symbol_table.py +23 -4
  24. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_entrypoint_expansion.py +175 -0
  25. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_extract_go.py +30 -0
  26. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_extract_java.py +6 -5
  27. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_extract_javascript.py +27 -0
  28. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_indexer.py +27 -0
  29. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_reachability.py +34 -0
  30. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_resolver.py +82 -0
  31. entrygraph-0.1.28/tests/test_sanitizer_languages.py +56 -0
  32. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_taint.py +25 -0
  33. {entrygraph-0.1.26 → entrygraph-0.1.28}/.github/workflows/ci.yml +0 -0
  34. {entrygraph-0.1.26 → entrygraph-0.1.28}/.github/workflows/release.yml +0 -0
  35. {entrygraph-0.1.26 → entrygraph-0.1.28}/.gitignore +0 -0
  36. {entrygraph-0.1.26 → entrygraph-0.1.28}/LICENSE +0 -0
  37. {entrygraph-0.1.26 → entrygraph-0.1.28}/README.md +0 -0
  38. {entrygraph-0.1.26 → entrygraph-0.1.28}/RELEASING.md +0 -0
  39. {entrygraph-0.1.26 → entrygraph-0.1.28}/pyproject.toml +0 -0
  40. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/__init__.py +0 -0
  41. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/__main__.py +0 -0
  42. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/cli/__init__.py +0 -0
  43. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/cli/render.py +0 -0
  44. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/data/sinks/javascript.toml +0 -0
  45. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/data/sinks/lib_javascript.toml +0 -0
  46. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/data/sinks/lib_python.toml +0 -0
  47. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/data/sinks/python.toml +0 -0
  48. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/db/__init__.py +0 -0
  49. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/db/engine.py +0 -0
  50. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/db/meta.py +0 -0
  51. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/db/models.py +0 -0
  52. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/db/queries.py +0 -0
  53. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/detect/__init__.py +0 -0
  54. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/detect/entrypoints/__init__.py +0 -0
  55. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/detect/entrypoints/configs.py +0 -0
  56. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/detect/entrypoints/php.py +0 -0
  57. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/detect/entrypoints/ruby.py +0 -0
  58. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/detect/entrypoints/rust.py +0 -0
  59. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/detect/frameworks.py +0 -0
  60. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/detect/manifests.py +0 -0
  61. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/detect/taint.py +0 -0
  62. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/errors.py +0 -0
  63. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/extract/__init__.py +0 -0
  64. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/extract/base.py +0 -0
  65. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/extract/csharp.py +0 -0
  66. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/extract/golang.py +0 -0
  67. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/extract/ir.py +0 -0
  68. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/extract/java.py +0 -0
  69. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/extract/php.py +0 -0
  70. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/extract/python.py +0 -0
  71. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/extract/registry.py +0 -0
  72. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/extract/ruby.py +0 -0
  73. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/extract/rust.py +0 -0
  74. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/fs/__init__.py +0 -0
  75. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/fs/hashing.py +0 -0
  76. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/fs/lang.py +0 -0
  77. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/fs/walker.py +0 -0
  78. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/graph/__init__.py +0 -0
  79. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/graph/scoring.py +0 -0
  80. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/kinds.py +0 -0
  81. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/parsing/__init__.py +0 -0
  82. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/parsing/parsers.py +0 -0
  83. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/parsing/queries.py +0 -0
  84. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/pipeline/__init__.py +0 -0
  85. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/pipeline/worker.py +0 -0
  86. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/pipeline/writer.py +0 -0
  87. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/py.typed +0 -0
  88. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/queries/csharp/calls.scm +0 -0
  89. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/queries/csharp/definitions.scm +0 -0
  90. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/queries/csharp/imports.scm +0 -0
  91. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/queries/go/calls.scm +0 -0
  92. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/queries/go/definitions.scm +0 -0
  93. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/queries/go/imports.scm +0 -0
  94. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/queries/java/calls.scm +0 -0
  95. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/queries/java/definitions.scm +0 -0
  96. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/queries/java/imports.scm +0 -0
  97. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/queries/javascript/calls.scm +0 -0
  98. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/queries/javascript/definitions.scm +0 -0
  99. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/queries/javascript/imports.scm +0 -0
  100. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/queries/php/calls.scm +0 -0
  101. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/queries/php/definitions.scm +0 -0
  102. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/queries/php/imports.scm +0 -0
  103. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/queries/python/calls.scm +0 -0
  104. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/queries/python/definitions.scm +0 -0
  105. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/queries/python/imports.scm +0 -0
  106. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/queries/ruby/calls.scm +0 -0
  107. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/queries/ruby/definitions.scm +0 -0
  108. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/queries/ruby/imports.scm +0 -0
  109. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/queries/rust/calls.scm +0 -0
  110. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/queries/rust/definitions.scm +0 -0
  111. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/queries/rust/imports.scm +0 -0
  112. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/resolve/__init__.py +0 -0
  113. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/resolve/externals.py +0 -0
  114. {entrygraph-0.1.26 → entrygraph-0.1.28}/src/entrygraph/results.py +0 -0
  115. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/conftest.py +0 -0
  116. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/csharp/aspnet_app/Controllers/ReportsController.cs +0 -0
  117. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/csharp/aspnet_app/Program.cs +0 -0
  118. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/csharp/aspnet_app/Services/ReportService.cs +0 -0
  119. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/csharp/aspnet_app/app.csproj +0 -0
  120. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/csharp/minimalapi_app/Program.cs +0 -0
  121. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/csharp/minimalapi_app/app.csproj +0 -0
  122. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/go/gin_app/go.mod +0 -0
  123. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/go/gin_app/main.go +0 -0
  124. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/go/gin_app/service.go +0 -0
  125. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/go/nethttp_app/go.mod +0 -0
  126. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/go/nethttp_app/main.go +0 -0
  127. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/java/methodref_app/pom.xml +0 -0
  128. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/java/methodref_app/src/main/java/com/example/App.java +0 -0
  129. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/java/spring_app/pom.xml +0 -0
  130. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/java/spring_app/src/main/java/com/example/Application.java +0 -0
  131. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/java/spring_app/src/main/java/com/example/ReportRunner.java +0 -0
  132. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/java/spring_app/src/main/java/com/example/ReportService.java +0 -0
  133. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/java/spring_app/src/main/java/com/example/UserController.java +0 -0
  134. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/javascript/commonjs_app/server.js +0 -0
  135. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/javascript/express_app/package.json +0 -0
  136. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/javascript/express_app/src/routes.js +0 -0
  137. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/javascript/express_app/src/services.js +0 -0
  138. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/php/laravel_app/app/Http/Controllers/ReportController.php +0 -0
  139. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/php/laravel_app/artisan +0 -0
  140. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/php/laravel_app/composer.json +0 -0
  141. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/php/laravel_app/routes/web.php +0 -0
  142. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/python/chained_sinks/app.py +0 -0
  143. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/python/flask_app/app/__init__.py +0 -0
  144. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/python/flask_app/app/db.py +0 -0
  145. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/python/flask_app/app/routes.py +0 -0
  146. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/python/flask_app/app/services.py +0 -0
  147. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/python/flask_app/cli.py +0 -0
  148. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/python/flask_app/requirements.txt +0 -0
  149. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/python/fuzzy_sink/app.py +0 -0
  150. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/python/heal_fidelity/caller.py +0 -0
  151. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/python/heal_fidelity/worker.py +0 -0
  152. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/python/may_continue/app.py +0 -0
  153. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/python/sanitizer/app.py +0 -0
  154. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/python/taint_source/handler.py +0 -0
  155. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/ruby/sinatra_app/Gemfile +0 -0
  156. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/ruby/sinatra_app/app.rb +0 -0
  157. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/ruby/sinatra_app/services/runner.rb +0 -0
  158. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/rust/axum_app/Cargo.toml +0 -0
  159. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/rust/axum_app/src/handlers.rs +0 -0
  160. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/rust/axum_app/src/main.rs +0 -0
  161. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/rust/axum_callback_app/Cargo.toml +0 -0
  162. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/rust/axum_callback_app/src/main.rs +0 -0
  163. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/rust/scoped_sink_app/Cargo.toml +0 -0
  164. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/fixtures/rust/scoped_sink_app/src/main.rs +0 -0
  165. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_api.py +0 -0
  166. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_cli.py +0 -0
  167. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_commonjs.py +0 -0
  168. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_csharp_callbacks.py +0 -0
  169. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_cte_bounds.py +0 -0
  170. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_engine_pragmas.py +0 -0
  171. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_entrypoints.py +0 -0
  172. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_extract_csharp.py +0 -0
  173. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_extract_php.py +0 -0
  174. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_extract_python.py +0 -0
  175. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_extract_ruby.py +0 -0
  176. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_extract_rust.py +0 -0
  177. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_frameworks.py +0 -0
  178. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_fuzzy_sink.py +0 -0
  179. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_go_callbacks.py +0 -0
  180. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_hardening.py +0 -0
  181. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_heal_fidelity.py +0 -0
  182. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_incremental.py +0 -0
  183. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_java_callbacks.py +0 -0
  184. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_lang.py +0 -0
  185. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_manifests.py +0 -0
  186. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_may_continue.py +0 -0
  187. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_models.py +0 -0
  188. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_pool.py +0 -0
  189. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_registry_cache.py +0 -0
  190. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_render.py +0 -0
  191. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_rust_callbacks.py +0 -0
  192. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_rust_scoped_sinks.py +0 -0
  193. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_scoring.py +0 -0
  194. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_sink_catalog.py +0 -0
  195. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_taint_sanitizers.py +0 -0
  196. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_taint_sources.py +0 -0
  197. {entrygraph-0.1.26 → entrygraph-0.1.28}/tests/test_walker.py +0 -0
  198. {entrygraph-0.1.26 → entrygraph-0.1.28}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: entrygraph
3
- Version: 0.1.26
3
+ Version: 0.1.28
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.26'
22
- __version_tuple__ = version_tuple = (0, 1, 26)
21
+ __version__ = version = '0.1.28'
22
+ __version_tuple__ = version_tuple = (0, 1, 28)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -88,6 +88,17 @@ class _FilteredAdjacency:
88
88
  )
89
89
 
90
90
 
91
+ class PathResults(list):
92
+ """CallPath list that also reports whether enumeration was budget-truncated.
93
+
94
+ It is a plain list for every existing use (iteration, len, indexing); the
95
+ `truncated` flag lets the CLI warn when 0 paths may mean "budget spent", not
96
+ "no reachable sink".
97
+ """
98
+
99
+ truncated: bool = False
100
+
101
+
91
102
  def _has_sanitizer(path: CallPath) -> bool:
92
103
  """True if a category sanitizer is called on the path (heuristic, no dataflow).
93
104
 
@@ -317,11 +328,15 @@ class CodeGraph:
317
328
  sources = self._source_ids(session, source, source_category)
318
329
  sinks = self._sink_ids(session, sink, sink_category)
319
330
  if not sources or not sinks:
320
- return []
331
+ return PathResults()
321
332
  traverser = self._traverser(session, engine, kinds, floor, include_cha)
322
333
  raw_paths = traverser.paths(sources, sinks, max_depth=max_depth, max_paths=max_paths)
323
334
  if not raw_paths:
324
- return []
335
+ # 0 paths may mean the visit budget was spent before any sink was
336
+ # reached — propagate the flag so this isn't mistaken for "safe".
337
+ empty = PathResults()
338
+ empty.truncated = bool(getattr(raw_paths, "truncated", False))
339
+ return empty
325
340
  all_ids = {node for path in raw_paths for node, _ in path}
326
341
  symbol_map = q.symbols_by_ids(session, all_ids)
327
342
  registry = self._registry(session)
@@ -347,7 +362,12 @@ class CodeGraph:
347
362
  results.sort(
348
363
  key=lambda p: (-(p.risk_score or 0.0), len(p.symbols), [s.id for s in p.symbols])
349
364
  )
350
- return results
365
+ # Enumeration collected a candidate pool; return the top max_paths by risk.
366
+ # Truncating after the risk rank (not during DFS) is what makes the widen
367
+ # flags monotonic — a wider edge set can only add candidates to rank.
368
+ out = PathResults(results[:max_paths])
369
+ out.truncated = bool(getattr(raw_paths, "truncated", False))
370
+ return out
351
371
 
352
372
  def reachable(
353
373
  self,
@@ -8,6 +8,7 @@ directly testable.
8
8
  from __future__ import annotations
9
9
 
10
10
  import argparse
11
+ import sys
11
12
  from dataclasses import asdict
12
13
  from pathlib import Path
13
14
 
@@ -277,6 +278,15 @@ def cmd_paths(args) -> int:
277
278
  include_callbacks=args.include_callbacks,
278
279
  prune_sanitized=args.prune_sanitized,
279
280
  )
281
+ truncated = bool(getattr(paths, "truncated", False))
282
+ if truncated:
283
+ # A budget-truncated search may have missed paths — never let an empty or
284
+ # short result read as a clean "no reachable sinks" on a large graph.
285
+ print(
286
+ "warning: path search hit its work budget and may be incomplete; "
287
+ "narrow --source/--sink or lower --max-depth for a complete answer.",
288
+ file=sys.stderr,
289
+ )
280
290
  if args.json:
281
291
  print(
282
292
  to_json(
@@ -104,3 +104,26 @@ id = "csharp.env"
104
104
  category = "env_input"
105
105
  callee = "cs:Environment.GetEnvironmentVariable"
106
106
  description = "Environment variable input"
107
+
108
+ # ---------------- sanitizers ----------------
109
+
110
+ [[sanitizer]]
111
+ id = "cs.sanitize.path-getfilename"
112
+ category = "path_traversal"
113
+ callee = "cs:*.{GetFileName,GetFullPath}"
114
+ effect = "reduces"
115
+ description = "Path.GetFileName / GetFullPath normalization"
116
+
117
+ [[sanitizer]]
118
+ id = "cs.sanitize.uri-escape"
119
+ category = "ssrf"
120
+ callee = "cs:*.{EscapeDataString,EscapeUriString}"
121
+ effect = "reduces"
122
+ description = "Uri.EscapeDataString URL encoding"
123
+
124
+ [[sanitizer]]
125
+ id = "cs.sanitize.sql-parameter"
126
+ category = "sql"
127
+ callee = "cs:*.{AddWithValue}"
128
+ effect = "reduces"
129
+ description = "Parameterized query value binding (SqlParameter)"
@@ -85,3 +85,33 @@ id = "go.env"
85
85
  category = "env_input"
86
86
  callee = "go:os.Getenv"
87
87
  description = "Environment variable input"
88
+
89
+ # ---------------- sanitizers ----------------
90
+
91
+ [[sanitizer]]
92
+ id = "go.sanitize.filepath-clean"
93
+ category = "path_traversal"
94
+ callee = "go:{path/filepath.Clean,path.Clean,path/filepath.Base,path.Base}"
95
+ effect = "reduces"
96
+ description = "Path normalization / basename"
97
+
98
+ [[sanitizer]]
99
+ id = "go.sanitize.html-escape"
100
+ category = "template_injection"
101
+ callee = "go:{html.EscapeString,template.HTMLEscapeString}"
102
+ effect = "reduces"
103
+ description = "HTML escaping"
104
+
105
+ [[sanitizer]]
106
+ id = "go.sanitize.url-escape"
107
+ category = "ssrf"
108
+ callee = "go:{url.QueryEscape,url.PathEscape}"
109
+ effect = "reduces"
110
+ description = "URL component escaping"
111
+
112
+ [[sanitizer]]
113
+ id = "go.sanitize.strconv-sql"
114
+ category = "sql"
115
+ callee = "go:{strconv.Atoi,strconv.ParseInt,strconv.ParseUint,strconv.ParseFloat}"
116
+ effect = "neutralizes"
117
+ description = "Numeric parse — a parsed number cannot carry SQL injection"
@@ -90,3 +90,26 @@ id = "java.env"
90
90
  category = "env_input"
91
91
  callee = "java:System.getenv"
92
92
  description = "Environment variable input"
93
+
94
+ # ---------------- sanitizers ----------------
95
+
96
+ [[sanitizer]]
97
+ id = "java.sanitize.html-encode"
98
+ category = "template_injection"
99
+ callee = "java:*.{forHtml,forHtmlContent,forJavaScript,escapeHtml4,escapeHtml,escapeXml11,htmlEscape}"
100
+ effect = "reduces"
101
+ description = "OWASP Encoder / commons-text / Spring HTML escaping"
102
+
103
+ [[sanitizer]]
104
+ id = "java.sanitize.path-normalize"
105
+ category = "path_traversal"
106
+ callee = "java:*.{getFileName,normalize,getName}"
107
+ effect = "reduces"
108
+ description = "Path normalization / filename extraction (Path.getFileName, FilenameUtils.getName)"
109
+
110
+ [[sanitizer]]
111
+ id = "java.sanitize.url-encode"
112
+ category = "ssrf"
113
+ callee = "java:*.{urlEncode,encode}"
114
+ effect = "reduces"
115
+ description = "URL encoding (URLEncoder.encode)"
@@ -123,3 +123,47 @@ id = "php.laravel.request"
123
123
  category = "http_input"
124
124
  callee = "php:*.{input,query,post,cookie,header}"
125
125
  description = "Laravel request accessors"
126
+
127
+ # ---------------- sanitizers ----------------
128
+
129
+ [[sanitizer]]
130
+ id = "php.sanitize.escapeshell"
131
+ category = "command_exec"
132
+ callee = "php:{escapeshellarg,escapeshellcmd}"
133
+ effect = "neutralizes"
134
+ description = "Shell argument/command escaping"
135
+
136
+ [[sanitizer]]
137
+ id = "php.sanitize.htmlspecialchars"
138
+ category = "template_injection"
139
+ callee = "php:{htmlspecialchars,htmlentities,strip_tags}"
140
+ effect = "reduces"
141
+ description = "HTML escaping"
142
+
143
+ [[sanitizer]]
144
+ id = "php.sanitize.sql-escape"
145
+ category = "sql"
146
+ callee = "php:{mysqli_real_escape_string,pg_escape_string,pg_escape_literal}"
147
+ effect = "reduces"
148
+ description = "SQL string escaping"
149
+
150
+ [[sanitizer]]
151
+ id = "php.sanitize.sql-quote"
152
+ category = "sql"
153
+ callee = "php:*.{quote,real_escape_string}"
154
+ effect = "reduces"
155
+ description = "PDO::quote / mysqli::real_escape_string"
156
+
157
+ [[sanitizer]]
158
+ id = "php.sanitize.intval"
159
+ category = "sql"
160
+ callee = "php:{intval,floatval}"
161
+ effect = "neutralizes"
162
+ description = "Numeric cast — a parsed number cannot carry SQL injection"
163
+
164
+ [[sanitizer]]
165
+ id = "php.sanitize.basename"
166
+ category = "path_traversal"
167
+ callee = "php:{basename,realpath}"
168
+ effect = "reduces"
169
+ description = "Basename / realpath resolution"
@@ -100,3 +100,33 @@ category = "env_input"
100
100
  callee = "rb:*.{fetch}"
101
101
  require_arg_hint = "ENV"
102
102
  description = "Environment variable input"
103
+
104
+ # ---------------- sanitizers ----------------
105
+
106
+ [[sanitizer]]
107
+ id = "rb.sanitize.shellwords"
108
+ category = "command_exec"
109
+ callee = "rb:*.{shellescape}"
110
+ effect = "neutralizes"
111
+ description = "Shell argument quoting (Shellwords.escape / String#shellescape)"
112
+
113
+ [[sanitizer]]
114
+ id = "rb.sanitize.html-escape"
115
+ category = "template_injection"
116
+ callee = "rb:{CGI.escapeHTML,ERB.Util.html_escape,ERB.Util.h,Rack.Utils.escape_html}"
117
+ effect = "reduces"
118
+ description = "HTML escaping"
119
+
120
+ [[sanitizer]]
121
+ id = "rb.sanitize.sql"
122
+ category = "sql"
123
+ callee = "rb:*.{sanitize_sql,sanitize_sql_array,sanitize_sql_for_conditions,quote}"
124
+ effect = "reduces"
125
+ description = "ActiveRecord SQL sanitization"
126
+
127
+ [[sanitizer]]
128
+ id = "rb.sanitize.basename"
129
+ category = "path_traversal"
130
+ callee = "rb:{File.basename}"
131
+ effect = "reduces"
132
+ description = "Basename extraction"
@@ -66,3 +66,19 @@ id = "rust.stdin"
66
66
  category = "stdin_input"
67
67
  callee = "rs:{std.io.stdin,io.stdin}"
68
68
  description = "Standard input"
69
+
70
+ # ---------------- sanitizers ----------------
71
+
72
+ [[sanitizer]]
73
+ id = "rs.sanitize.shell-escape"
74
+ category = "command_exec"
75
+ callee = "rs:{shell_escape.escape,shell_escape.unix.escape,shlex.quote,shlex.try_quote}"
76
+ effect = "neutralizes"
77
+ description = "Shell argument quoting (shell-escape / shlex crates)"
78
+
79
+ [[sanitizer]]
80
+ id = "rs.sanitize.path-filename"
81
+ category = "path_traversal"
82
+ callee = "rs:*.{file_name,canonicalize}"
83
+ effect = "reduces"
84
+ description = "Path::file_name / canonicalize"
@@ -74,6 +74,17 @@ def first_string_arg(decorator_text: str) -> str | None:
74
74
  return match.group(1) if match else None
75
75
 
76
76
 
77
+ def compose_route(prefix: str | None, path: str | None) -> str:
78
+ """Join a class/group-level route prefix with a method-level path.
79
+
80
+ Either side may be None/empty or carry stray leading/trailing slashes. The
81
+ result is a single slash-prefixed route ("/" when both are empty), matching
82
+ the NestJS rule so composed routes read consistently across frameworks.
83
+ """
84
+ parts = [seg.strip("/") for seg in (prefix, path) if seg and seg.strip("/")]
85
+ return "/" + "/".join(parts)
86
+
87
+
77
88
  def methods_kwarg(decorator_text: str) -> list[str]:
78
89
  match = _METHODS_KWARG.search(decorator_text)
79
90
  if not match:
@@ -14,6 +14,7 @@ import re
14
14
 
15
15
  from entrygraph.detect.entrypoints.base import (
16
16
  EntrypointRule,
17
+ compose_route,
17
18
  first_string_arg,
18
19
  register,
19
20
  )
@@ -48,14 +49,32 @@ def _is_controller(t: RawSymbol) -> bool:
48
49
  return any("Controller" in b for b in t.bases)
49
50
 
50
51
 
52
+ def _controller_token(qname: str) -> str:
53
+ """The `[controller]` route-token value: the class name minus a Controller suffix."""
54
+ name = qname.rsplit(".", 1)[-1]
55
+ return name[: -len("Controller")] if name.endswith("Controller") else name
56
+
57
+
58
+ def _class_route_prefix(t: RawSymbol) -> str:
59
+ """A controller's class-level [Route("...")] prefix, with [controller] expanded."""
60
+ route_dec = next((d for d in t.decorators if _ROUTE_ATTR.match(d)), None)
61
+ prefix = first_string_arg(route_dec) if route_dec else None
62
+ if not prefix:
63
+ return ""
64
+ return prefix.replace("[controller]", _controller_token(t.qualified_name))
65
+
66
+
51
67
  def _aspnet_controller_routes(x: FileExtraction) -> list[EntrypointHint]:
52
- controllers = {t.qualified_name for t in _type_symbols(x) if _is_controller(t)}
68
+ controller_syms = [t for t in _type_symbols(x) if _is_controller(t)]
69
+ controllers = {t.qualified_name for t in controller_syms}
53
70
  if not controllers:
54
71
  return []
72
+ prefixes = {t.qualified_name: _class_route_prefix(t) for t in controller_syms}
55
73
  hints: list[EntrypointHint] = []
56
74
  for method in _methods(x):
57
75
  if method.parent_qualified_name not in controllers:
58
76
  continue
77
+ prefix = prefixes.get(method.parent_qualified_name or "", "")
59
78
  for decorator in method.decorators:
60
79
  m = _HTTP_ATTR.match(decorator)
61
80
  if m:
@@ -64,7 +83,7 @@ def _aspnet_controller_routes(x: FileExtraction) -> list[EntrypointHint]:
64
83
  rule_id="csharp.aspnet.controller-route",
65
84
  kind=EntrypointKind.HTTP_ROUTE,
66
85
  handler_qualified_name=method.qualified_name,
67
- route=first_string_arg(decorator) or "",
86
+ route=compose_route(prefix, first_string_arg(decorator)),
68
87
  http_methods=[m.group(1).upper()],
69
88
  framework="aspnetcore",
70
89
  )
@@ -79,7 +98,7 @@ def _aspnet_controller_routes(x: FileExtraction) -> list[EntrypointHint]:
79
98
  rule_id="csharp.aspnet.controller-route",
80
99
  kind=EntrypointKind.HTTP_ROUTE,
81
100
  handler_qualified_name=method.qualified_name,
82
- route=first_string_arg(route_dec) or "",
101
+ route=compose_route(prefix, first_string_arg(route_dec)),
83
102
  http_methods=["*"],
84
103
  framework="aspnetcore",
85
104
  )
@@ -11,8 +11,11 @@ from entrygraph.detect.entrypoints.base import (
11
11
  from entrygraph.extract.ir import EntrypointHint, FileExtraction
12
12
  from entrygraph.kinds import EntrypointKind, SymbolKind
13
13
 
14
- _GIN_METHODS = frozenset(
15
- {"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "Any", "Handle"}
14
+ # gin exports UPPERCASE verbs (r.GET); chi/fiber use TitleCase (r.Get). Accept
15
+ # both so the shared _gin_style matcher recognizes chi/fiber, not just gin.
16
+ _HTTP_VERBS = frozenset({"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"})
17
+ _GIN_METHODS = (
18
+ _HTTP_VERBS | frozenset(v.title() for v in _HTTP_VERBS) | frozenset({"Any", "Handle"})
16
19
  )
17
20
  _NETHTTP_REGISTER = frozenset({"HandleFunc", "Handle"})
18
21
 
@@ -73,12 +76,8 @@ def _gin_routes(x: FileExtraction) -> list[EntrypointHint]:
73
76
  ):
74
77
  route = first_string_arg("(" + ref.arg_preview.lstrip("("))
75
78
  if route is not None and route.startswith("/"):
76
- method = (
77
- ref.callee_name.upper()
78
- if ref.callee_name
79
- in {"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"}
80
- else "*"
81
- )
79
+ verb = ref.callee_name.upper()
80
+ method = verb if verb in _HTTP_VERBS else "*"
82
81
  hints.append(
83
82
  EntrypointHint(
84
83
  rule_id="go.gin.route",
@@ -13,6 +13,7 @@ import re
13
13
 
14
14
  from entrygraph.detect.entrypoints.base import (
15
15
  EntrypointRule,
16
+ compose_route,
16
17
  first_string_arg,
17
18
  register,
18
19
  )
@@ -38,6 +39,10 @@ def _annotation_names(decorators: list[str], pattern: re.Pattern) -> bool:
38
39
  return any(pattern.match(d) for d in decorators)
39
40
 
40
41
 
42
+ def _class_prefix(qname: str | None, prefixes: dict[str, str]) -> str:
43
+ return prefixes.get(qname or "", "")
44
+
45
+
41
46
  def _type_symbols(x: FileExtraction) -> list[RawSymbol]:
42
47
  return [s for s in x.symbols if s.kind in (SymbolKind.CLASS, SymbolKind.INTERFACE)]
43
48
 
@@ -50,6 +55,17 @@ def _enclosing_type(method: RawSymbol, types: list[RawSymbol]) -> RawSymbol | No
50
55
  return next((t for t in types if t.qualified_name == method.parent_qualified_name), None)
51
56
 
52
57
 
58
+ def _class_route_prefixes(types: list[RawSymbol], pattern: re.Pattern) -> dict[str, str]:
59
+ """Map controller/resource qname -> its class-level route prefix (via `pattern`)."""
60
+ prefixes: dict[str, str] = {}
61
+ for t in types:
62
+ for decorator in t.decorators:
63
+ if pattern.match(decorator):
64
+ prefixes[t.qualified_name] = first_string_arg(decorator) or ""
65
+ break
66
+ return prefixes
67
+
68
+
53
69
  def _spring_routes(x: FileExtraction) -> list[EntrypointHint]:
54
70
  types = _type_symbols(x)
55
71
  controllers = {
@@ -57,6 +73,10 @@ def _spring_routes(x: FileExtraction) -> list[EntrypointHint]:
57
73
  }
58
74
  if not controllers:
59
75
  return []
76
+ # A controller's own @RequestMapping("/api") is its route prefix.
77
+ prefixes = _class_route_prefixes(
78
+ [t for t in types if t.qualified_name in controllers], _SPRING_MAPPING
79
+ )
60
80
  hints = []
61
81
  for method in _methods(x):
62
82
  if method.parent_qualified_name not in controllers:
@@ -67,12 +87,15 @@ def _spring_routes(x: FileExtraction) -> list[EntrypointHint]:
67
87
  continue
68
88
  verb = _MAPPING_VERB.get(match.group(1))
69
89
  methods = [verb] if verb else ["*"]
90
+ route = compose_route(
91
+ _class_prefix(method.parent_qualified_name, prefixes), first_string_arg(decorator)
92
+ )
70
93
  hints.append(
71
94
  EntrypointHint(
72
95
  rule_id="java.spring.route",
73
96
  kind=EntrypointKind.HTTP_ROUTE,
74
97
  handler_qualified_name=method.qualified_name,
75
- route=first_string_arg(decorator) or "",
98
+ route=route,
76
99
  http_methods=methods,
77
100
  framework="spring-boot",
78
101
  )
@@ -85,6 +108,10 @@ def _jaxrs_routes(x: FileExtraction) -> list[EntrypointHint]:
85
108
  resources = {t.qualified_name for t in types if _annotation_names(t.decorators, _JAXRS_PATH)}
86
109
  if not resources:
87
110
  return []
111
+ # A resource class's own @Path("/api") prefixes every sub-resource method.
112
+ prefixes = _class_route_prefixes(
113
+ [t for t in types if t.qualified_name in resources], _JAXRS_PATH
114
+ )
88
115
  hints = []
89
116
  for method in _methods(x):
90
117
  if method.parent_qualified_name not in resources:
@@ -95,13 +122,16 @@ def _jaxrs_routes(x: FileExtraction) -> list[EntrypointHint]:
95
122
  )
96
123
  if verb is None:
97
124
  continue
98
- route = next((first_string_arg(d) for d in method.decorators if _JAXRS_PATH.match(d)), None)
125
+ method_path = next(
126
+ (first_string_arg(d) for d in method.decorators if _JAXRS_PATH.match(d)), None
127
+ )
128
+ route = compose_route(_class_prefix(method.parent_qualified_name, prefixes), method_path)
99
129
  hints.append(
100
130
  EntrypointHint(
101
131
  rule_id="java.jaxrs",
102
132
  kind=EntrypointKind.HTTP_ROUTE,
103
133
  handler_qualified_name=method.qualified_name,
104
- route=route or "",
134
+ route=route,
105
135
  http_methods=[verb],
106
136
  framework="jax-rs",
107
137
  )
@@ -121,6 +151,9 @@ def _micronaut_routes(x: FileExtraction) -> list[EntrypointHint]:
121
151
  }
122
152
  if not controllers:
123
153
  return []
154
+ prefixes = _class_route_prefixes(
155
+ [t for t in types if t.qualified_name in controllers], _MICRONAUT_CONTROLLER
156
+ )
124
157
  hints = []
125
158
  for method in _methods(x):
126
159
  if method.parent_qualified_name not in controllers:
@@ -128,12 +161,16 @@ def _micronaut_routes(x: FileExtraction) -> list[EntrypointHint]:
128
161
  for decorator in method.decorators:
129
162
  m = _MICRONAUT_MAPPING.match(decorator)
130
163
  if m:
164
+ route = compose_route(
165
+ _class_prefix(method.parent_qualified_name, prefixes),
166
+ first_string_arg(decorator),
167
+ )
131
168
  hints.append(
132
169
  EntrypointHint(
133
170
  rule_id="java.micronaut.route",
134
171
  kind=EntrypointKind.HTTP_ROUTE,
135
172
  handler_qualified_name=method.qualified_name,
136
- route=first_string_arg(decorator) or "",
173
+ route=route,
137
174
  http_methods=[m.group(1).upper()],
138
175
  framework="micronaut",
139
176
  )
@@ -14,6 +14,31 @@ _NEST_DECORATOR = re.compile(r"^@(Get|Post|Put|Delete|Patch|All)\b")
14
14
 
15
15
 
16
16
  _TRAILING_IDENT = re.compile(r",\s*([A-Za-z_$][\w$]*)\s*\)?\s*$")
17
+ _RECEIVER_ROOT = re.compile(r"^\s*([A-Za-z_$][\w$]*)")
18
+ # HTTP-client and test-harness roots whose .get()/.post() are *requests*, not
19
+ # route registrations: supertest/nock/chai/superagent and Node/browser clients.
20
+ # Excludes them so test suites don't flood the route table (ghost 72%, strapi 99%).
21
+ _CLIENT_ROOTS = frozenset(
22
+ {
23
+ "nock",
24
+ "supertest",
25
+ "request",
26
+ "superagent",
27
+ "agent",
28
+ "chai",
29
+ "axios",
30
+ "got",
31
+ "fetch",
32
+ "http",
33
+ "https",
34
+ "fastify",
35
+ } # fastify.inject(...).get is a test call
36
+ )
37
+
38
+
39
+ def _receiver_root(receiver_text: str) -> str | None:
40
+ m = _RECEIVER_ROOT.match(receiver_text)
41
+ return m.group(1) if m else None
17
42
 
18
43
 
19
44
  def _router_routes(framework: str):
@@ -29,6 +54,7 @@ def _router_routes(framework: str):
29
54
  if (
30
55
  ref.kind == "call"
31
56
  and ref.receiver_text is not None
57
+ and _receiver_root(ref.receiver_text) not in _CLIENT_ROOTS
32
58
  and ref.callee_name in _HTTP_METHODS
33
59
  and ref.arg_preview
34
60
  ):
@@ -53,8 +53,14 @@ def _flask_routes(x: FileExtraction) -> list[EntrypointHint]:
53
53
  else:
54
54
  verb = _HTTP_VERB.match(decorator)
55
55
  if verb:
56
- route = first_string_arg(decorator)
57
- methods = [verb.group(2).upper()]
56
+ candidate = first_string_arg(decorator)
57
+ # The @app.get/@router.post shorthand shares its shape with
58
+ # @mock.patch("dotted.target"), which the verb `patch` matches.
59
+ # A real Flask/FastAPI route is a URL path ("/..."); a mock target
60
+ # is a dotted import path. Require the URL shape to exclude mocks.
61
+ if candidate is not None and candidate.startswith("/"):
62
+ route = candidate
63
+ methods = [verb.group(2).upper()]
58
64
  if route is not None:
59
65
  hints.append(
60
66
  EntrypointHint(
@@ -36,6 +36,22 @@ _SCOPE_TYPES = frozenset(
36
36
  _NAMED_SCOPES = frozenset({"function_declaration", "method_definition", "class_declaration"})
37
37
  _SRC_ROOTS = ("src", "lib", "app")
38
38
  _EXPORT_PARENTS = frozenset({"export_statement"})
39
+ _JS_EXTS = (".d.ts", ".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".cjs", ".jsx")
40
+
41
+
42
+ def _atomic_segment(filename: str) -> str:
43
+ """A filename -> one module-path segment: strip a JS/TS extension, then map any
44
+ remaining internal dots to `_`.
45
+
46
+ A dotted stem like ``article.service`` must NOT become two dotted segments, or
47
+ ``.split(".")`` in relative-import resolution mis-counts package depth and
48
+ duplicates a directory name (``article.article.article.service``) — issue #42.
49
+ """
50
+ for ext in _JS_EXTS:
51
+ if filename.endswith(ext):
52
+ filename = filename[: -len(ext)]
53
+ break
54
+ return filename.replace(".", "_")
39
55
 
40
56
 
41
57
  class JavaScriptExtractor:
@@ -45,11 +61,7 @@ class JavaScriptExtractor:
45
61
  parts = repo_relative_path.split("/")
46
62
  if parts[0] in _SRC_ROOTS and len(parts) > 1:
47
63
  parts = parts[1:]
48
- stem = parts[-1]
49
- for ext in (".d.ts", ".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".cjs", ".jsx"):
50
- if stem.endswith(ext):
51
- stem = stem[: -len(ext)]
52
- break
64
+ stem = _atomic_segment(parts[-1])
53
65
  is_package = stem in ("index", "mod")
54
66
  parts = parts[:-1] if is_package else [*parts[:-1], stem]
55
67
  return ".".join(p for p in parts if p) or "_root", is_package
@@ -358,10 +370,7 @@ class JavaScriptExtractor:
358
370
  if segment == "..":
359
371
  parts = parts[:-1]
360
372
  else:
361
- seg = segment
362
- for ext in (".ts", ".tsx", ".js", ".jsx"):
363
- if seg.endswith(ext):
364
- seg = seg[: -len(ext)]
373
+ seg = _atomic_segment(segment)
365
374
  if seg not in ("index", "mod"):
366
375
  parts.append(seg)
367
376
  return ".".join(p for p in parts if p) or "_root"