diffctx 1.19.0__tar.gz → 1.19.1__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 (176) hide show
  1. {diffctx-1.19.0 → diffctx-1.19.1}/CHANGELOG.md +81 -1
  2. {diffctx-1.19.0 → diffctx-1.19.1}/Cargo.lock +1 -1
  3. {diffctx-1.19.0 → diffctx-1.19.1}/PKG-INFO +1 -1
  4. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/Cargo.toml +1 -1
  5. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/bindings.rs +109 -3
  6. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/weights.rs +1 -0
  7. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/javascript.rs +16 -8
  8. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/jvm.rs +25 -0
  9. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/shell.rs +75 -1
  10. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/fragmentation.rs +6 -1
  11. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/git.rs +44 -8
  12. diffctx-1.19.1/crates/diffctx-native/src/go_bindings.rs +603 -0
  13. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/hook.rs +62 -28
  14. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/impact.rs +469 -59
  15. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/kotlin_bindings.rs +1 -27
  16. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/lib.rs +1 -0
  17. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/main.rs +11 -5
  18. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/parsers/tree_sitter_strategy.rs +15 -0
  19. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/pipeline.rs +23 -8
  20. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/testfiles.rs +2 -1
  21. {diffctx-1.19.0 → diffctx-1.19.1}/pyproject.toml +1 -1
  22. {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/_app.py +4 -2
  23. diffctx-1.19.1/src/diffctx/version.py +1 -0
  24. diffctx-1.19.0/src/diffctx/version.py +0 -1
  25. {diffctx-1.19.0 → diffctx-1.19.1}/Cargo.toml +0 -0
  26. {diffctx-1.19.0 → diffctx-1.19.1}/LICENSE +0 -0
  27. {diffctx-1.19.0 → diffctx-1.19.1}/README.md +0 -0
  28. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/README.md +0 -0
  29. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/analytics.rs +0 -0
  30. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/candidate_files.rs +0 -0
  31. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/change_class.rs +0 -0
  32. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/analytics.rs +0 -0
  33. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/bm25.rs +0 -0
  34. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/budget.rs +0 -0
  35. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/category_weights.rs +0 -0
  36. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/edge_weights.rs +0 -0
  37. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/env_overrides.rs +0 -0
  38. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/extensions.rs +0 -0
  39. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/filtering.rs +0 -0
  40. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/fragmentation.rs +0 -0
  41. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/git.rs +0 -0
  42. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/graph_filtering.rs +0 -0
  43. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/importance.rs +0 -0
  44. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/limits.rs +0 -0
  45. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/mod.rs +0 -0
  46. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/mode.rs +0 -0
  47. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/needs.rs +0 -0
  48. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/parsers.rs +0 -0
  49. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/render.rs +0 -0
  50. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/scoring.rs +0 -0
  51. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/selection.rs +0 -0
  52. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/tokenization.rs +0 -0
  53. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/container.rs +0 -0
  54. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/core.rs +0 -0
  55. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/discovery.rs +0 -0
  56. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/base.rs +0 -0
  57. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/config_edges/build_system.rs +0 -0
  58. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/config_edges/cicd.rs +0 -0
  59. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/config_edges/docker.rs +0 -0
  60. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/config_edges/generic.rs +0 -0
  61. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/config_edges/helm.rs +0 -0
  62. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/config_edges/kubernetes.rs +0 -0
  63. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/config_edges/mod.rs +0 -0
  64. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/document/mod.rs +0 -0
  65. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/history/cochange.rs +0 -0
  66. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/history/mod.rs +0 -0
  67. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/mod.rs +0 -0
  68. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/ansible.rs +0 -0
  69. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/bazel.rs +0 -0
  70. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/c_family.rs +0 -0
  71. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/cargo_edges.rs +0 -0
  72. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/clojure.rs +0 -0
  73. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/css.rs +0 -0
  74. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/dart.rs +0 -0
  75. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/dbt.rs +0 -0
  76. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/dotnet.rs +0 -0
  77. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/elixir.rs +0 -0
  78. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/erlang.rs +0 -0
  79. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/go.rs +0 -0
  80. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/graphql.rs +0 -0
  81. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/haskell.rs +0 -0
  82. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/julia.rs +0 -0
  83. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/latex.rs +0 -0
  84. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/lua.rs +0 -0
  85. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/mod.rs +0 -0
  86. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/nim.rs +0 -0
  87. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/nix.rs +0 -0
  88. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/ocaml.rs +0 -0
  89. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/openapi.rs +0 -0
  90. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/perl.rs +0 -0
  91. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/php.rs +0 -0
  92. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/prisma.rs +0 -0
  93. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/protobuf.rs +0 -0
  94. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/python.rs +0 -0
  95. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/r_lang.rs +0 -0
  96. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/ruby.rs +0 -0
  97. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/rust_lang.rs +0 -0
  98. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/sql.rs +0 -0
  99. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/swift.rs +0 -0
  100. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/tags.rs +0 -0
  101. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/terraform.rs +0 -0
  102. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/zig.rs +0 -0
  103. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/similarity/lexical.rs +0 -0
  104. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/similarity/mod.rs +0 -0
  105. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/structural/containment.rs +0 -0
  106. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/structural/mod.rs +0 -0
  107. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/structural/sibling.rs +0 -0
  108. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/structural/testing.rs +0 -0
  109. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/effective_config.rs +0 -0
  110. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/excerpt.rs +0 -0
  111. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/facts/kotlin.rs +0 -0
  112. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/facts/mod.rs +0 -0
  113. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/facts/scala.rs +0 -0
  114. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/filtering.rs +0 -0
  115. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/graph.rs +0 -0
  116. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/graph_export.rs +0 -0
  117. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/in_memory_harness.rs +0 -0
  118. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/interval.rs +0 -0
  119. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/languages.rs +0 -0
  120. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/locate.rs +0 -0
  121. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/mode.rs +0 -0
  122. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/parsers/config_parser.rs +0 -0
  123. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/parsers/generic.rs +0 -0
  124. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/parsers/markdown.rs +0 -0
  125. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/parsers/mod.rs +0 -0
  126. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/paths.rs +0 -0
  127. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/peak_rss.rs +0 -0
  128. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/postpass.rs +0 -0
  129. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/ppr.rs +0 -0
  130. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/project_graph.rs +0 -0
  131. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/provenance.rs +0 -0
  132. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/pybridge.rs +0 -0
  133. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/render.rs +0 -0
  134. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/resource.rs +0 -0
  135. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/run_provenance.rs +0 -0
  136. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/sanitize.rs +0 -0
  137. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/scoring.rs +0 -0
  138. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/select.rs +0 -0
  139. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/signatures.rs +0 -0
  140. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/source.rs +0 -0
  141. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/stopwords/code.txt +0 -0
  142. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/stopwords/docs.txt +0 -0
  143. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/stopwords.rs +0 -0
  144. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/test_rng.rs +0 -0
  145. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/token_corpus.rs +0 -0
  146. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/tokenizer.rs +0 -0
  147. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/types.rs +0 -0
  148. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/utility/boltzmann.rs +0 -0
  149. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/utility/importance.rs +0 -0
  150. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/utility/mod.rs +0 -0
  151. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/utility/needs.rs +0 -0
  152. {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/utility/objective.rs +0 -0
  153. {diffctx-1.19.0 → diffctx-1.19.1}/rust-toolchain.toml +0 -0
  154. {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/__init__.py +0 -0
  155. {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/__main__.py +0 -0
  156. {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/_diffctx.pyi +0 -0
  157. {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/_native/__init__.py +0 -0
  158. {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/_native/graph_analytics.py +0 -0
  159. {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/_native/graph_export.py +0 -0
  160. {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/_native/pipeline.py +0 -0
  161. {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/_native/project_graph.py +0 -0
  162. {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/cli.py +0 -0
  163. {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/clipboard.py +0 -0
  164. {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/ignore.py +0 -0
  165. {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/logger.py +0 -0
  166. {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/mcp/__init__.py +0 -0
  167. {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/mcp/__main__.py +0 -0
  168. {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/mcp/fetch.py +0 -0
  169. {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/mcp/launch.py +0 -0
  170. {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/mcp/legacy.py +0 -0
  171. {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/mcp/security.py +0 -0
  172. {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/mcp/server.py +0 -0
  173. {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/py.typed +0 -0
  174. {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/tokens.py +0 -0
  175. {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/tree.py +0 -0
  176. {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/writer.py +0 -0
@@ -7,6 +7,85 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.19.1] - 2026-10-14
11
+
12
+ ### Added
13
+
14
+ - Go callers come from syntax facts: a package is its directory, an import
15
+ binds it by the path `go.mod` gives it, an alias or a dot import names it,
16
+ an unexported name never leaves its package, and a method call resolves
17
+ when the syntax states the value's type (a receiver, a parameter,
18
+ `x := &pkg.T{}`). A same-named function of another package, a struct
19
+ field, a local and a string are no longer callers (#506).
20
+
21
+ ### Fixed
22
+
23
+ - Module-level calls are listed beside a function-level caller in the same
24
+ file (#449), and a caller split into chunks spans every call site (#472).
25
+ - A call inside a function's local `const` names the function as the caller,
26
+ with its test link, not the local (#452).
27
+ - `async function` and generator functions are JS/TS definitions: a call
28
+ from the same file read as "nothing calls them" (#481).
29
+ - Shell functions are called from their own script's body and from scripts
30
+ that source them; a script calling its own `run_tool` read as no caller
31
+ (#497).
32
+ - Callers of a removed definition keep their test links; a removed `pub fn`
33
+ of a private Rust module or a binary is no removed API; possible callers
34
+ are shed before contracts, and the header counts every caller found and
35
+ every one without a test link at any budget (#413).
36
+ - `--full` reads under the run's deadline and names it, instead of exiting
37
+ with "files over the size cap" when a git read stalls (#439).
38
+ - A tree the hook builds from the session's edits is "the snapshot tree",
39
+ not "staged changes"; prose beside migrations is no schema contract; the
40
+ hook asks to check callers only when it lists some (#463).
41
+ - `selftest*` files are tests (#428).
42
+ - Deleting a module of tens of thousands of definitions no longer overflows
43
+ the argument list of the name search: the names go to `git grep` through a
44
+ pattern file, and the callers are found instead of a failed read (#440).
45
+ - The search hook reads `git grep PATTERN REV -- PATH`'s revision as one,
46
+ not as a directory outside any repository (#486), and a qualified search
47
+ word (`tests.direct`) searches for its last name, not its qualifier
48
+ (#402).
49
+ - A line the change itself wrote about a deletion (a changelog entry, a
50
+ comment) is no stale reference, and a path that ends a sentence still is
51
+ one (#456).
52
+ - A private Rust function removed in favour of an imported shared one has
53
+ no callers: a private item is reachable only from its own module and the
54
+ modules beneath it, a `use` from another module binds a file's calls
55
+ there, and a `use` line is no reference (#519).
56
+ - The search hook answers a pattern branch only when it names exactly one
57
+ identifier: a grep for a line of code is no search for each of its words
58
+ (#402).
59
+ - A changed constructor (`__init__`, `constructor`) is called by its
60
+ class's name: `cp.Model(…)`, `Model(…)` and `new Store(…)` are its callers
61
+ (#318).
62
+ - JS/TS class methods, object-literal methods and a store's actions built
63
+ inside `create(...)` are definitions with callers — through `this`, a typed
64
+ instance, the object, or possibly any receiver (#444, #473); a call wrapped
65
+ in parentheses at module level is a caller, and a call inside an anonymous
66
+ arrow belongs to the definition around it (#520); a component reached
67
+ through `lazy(() => import(…))` or `await import(…)` is referenced there
68
+ (#479); a workspace package's specifier (`@scope/core`) resolves into its
69
+ source (#474).
70
+ - Python: a pytest fixture is called by the tests and fixtures that take it
71
+ as a parameter (#401); an `Any` receiver and an object from a factory
72
+ classmethod are possible callers (#425).
73
+ - Java: a method calling a sibling of its own class with no qualifier is a
74
+ caller (#496). Rust: a local closure named like a changed function is no
75
+ caller of it (#391).
76
+ - A file over the size cap that names a changed definition is said as a
77
+ place callers may be missing, instead of the answer reading complete
78
+ (#484); a file of another runtime that breaks its parse is no such hedge
79
+ (#521).
80
+ - A hand-written "do not edit" header no longer marks a file generated;
81
+ a generator's header still does (#411).
82
+ - The native CLI refuses `--symbol` with another `--mode` named outright,
83
+ reads `PATH` in `--symbol PATH:NAME` from the path it was given, and parses
84
+ the watchdog override as the Python CLI does (#410).
85
+ - A file that declares a removed definition's name itself calls its own,
86
+ not the removed one: deleting a private `declared_name` in one Rust file
87
+ listed another file's calls to its own `declared_name` as broken callers.
88
+
10
89
  ## [1.19.0] - 2026-10-14
11
90
 
12
91
  ### Added
@@ -2299,7 +2378,8 @@ Earlier releases shipped as `treemapper`; see
2299
2378
  <https://github.com/nikolay-e/diffctx/releases> for the corresponding GitHub
2300
2379
  release notes (`1.0.0` through `1.6.1`).
2301
2380
 
2302
- [Unreleased]: https://github.com/nikolay-e/diffctx/compare/v1.19.0...HEAD
2381
+ [Unreleased]: https://github.com/nikolay-e/diffctx/compare/v1.19.1...HEAD
2382
+ [1.19.1]: https://github.com/nikolay-e/diffctx/compare/v1.19.0...v1.19.1
2303
2383
  [1.19.0]: https://github.com/nikolay-e/diffctx/compare/v1.18.4...v1.19.0
2304
2384
  [1.18.4]: https://github.com/nikolay-e/diffctx/compare/v1.18.3...v1.18.4
2305
2385
  [1.18.3]: https://github.com/nikolay-e/diffctx/compare/v1.18.2...v1.18.3
@@ -204,7 +204,7 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
204
204
 
205
205
  [[package]]
206
206
  name = "diffctx"
207
- version = "1.19.0"
207
+ version = "1.19.1"
208
208
  dependencies = [
209
209
  "aho-corasick",
210
210
  "anyhow",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diffctx
3
- Version: 1.19.0
3
+ Version: 1.19.1
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "diffctx"
3
- version = "1.19.0"
3
+ version = "1.19.1"
4
4
  edition = "2024"
5
5
  rust-version = "1.85"
6
6
  description = "Selects the minimum code an LLM needs to review a git diff: walks the dependency graph outward from changed lines and stops when extra context stops paying for itself"
@@ -154,7 +154,7 @@ static JS_EXPORT_FROM: Lazy<Regex> = Lazy::new(|| {
154
154
  });
155
155
  static JS_REQUIRE: Lazy<Regex> = Lazy::new(|| {
156
156
  Regex::new(
157
- r#"(?s)\b(?:const|let|var)\s+(\{[^}]*\}|[\w$]+)\s*=\s*require\(\s*['"]([^'"]+)['"]\s*\)"#,
157
+ r#"(?s)\b(?:const|let|var)\s+(\{[^}]*\}|[\w$]+)\s*=\s*(?:require|await\s+import)\(\s*['"]([^'"]+)['"]\s*\)"#,
158
158
  )
159
159
  .unwrap()
160
160
  });
@@ -188,6 +188,15 @@ fn is_ident(s: &str) -> bool {
188
188
  && !s.starts_with(|c: char| c.is_ascii_digit())
189
189
  }
190
190
 
191
+ /// `import('./x').then(m => …)`, `.then((m) => …)`, `.then(function (m)`:
192
+ /// the callback's parameter is the module (#479, `React.lazy`).
193
+ static JS_DYNAMIC_THEN: Lazy<Regex> = Lazy::new(|| {
194
+ Regex::new(
195
+ r#"\bimport\(\s*['"]([^'"]+)['"]\s*\)\s*\.then\(\s*(?:async\s+)?(?:function\s*)?\(?\s*([A-Za-z_$][\w$]*)"#,
196
+ )
197
+ .unwrap()
198
+ });
199
+
191
200
  pub fn js_bindings(text: &str) -> FileBindings {
192
201
  let mut b = FileBindings::default();
193
202
  for c in JS_IMPORT.captures_iter(text) {
@@ -205,6 +214,10 @@ pub fn js_bindings(text: &str) -> FileBindings {
205
214
  .insert(lhs.to_string(), Target::Module(c[2].to_string()));
206
215
  }
207
216
  }
217
+ for c in JS_DYNAMIC_THEN.captures_iter(text) {
218
+ b.names
219
+ .insert(c[2].to_string(), Target::Module(c[1].to_string()));
220
+ }
208
221
  b
209
222
  }
210
223
 
@@ -565,6 +578,8 @@ pub struct Resolver<'a> {
565
578
  /// Python resolution scans every file of the snapshot; the same
566
579
  /// importer asks about the same specifier once per caller it checks.
567
580
  resolved: RefCell<FxHashMap<(String, String), Resolution>>,
581
+ /// The repository's own npm packages, by name: (directory, entries).
582
+ workspace: std::cell::OnceCell<FxHashMap<String, (String, Vec<String>)>>,
568
583
  }
569
584
 
570
585
  impl<'a> Resolver<'a> {
@@ -577,6 +592,7 @@ impl<'a> Resolver<'a> {
577
592
  ts_configs,
578
593
  cache: RefCell::default(),
579
594
  resolved: RefCell::default(),
595
+ workspace: std::cell::OnceCell::new(),
580
596
  }
581
597
  }
582
598
 
@@ -677,6 +693,63 @@ impl<'a> Resolver<'a> {
677
693
  .find(|p| self.files.contains(p))
678
694
  }
679
695
 
696
+ /// A specifier naming one of the repository's own packages — an npm
697
+ /// workspace member — resolves into its source, not to an external
698
+ /// dependency (#474).
699
+ fn workspace_package(&self, spec: &str) -> Option<String> {
700
+ let packages = self.workspace.get_or_init(|| {
701
+ let mut out = FxHashMap::default();
702
+ for file in self.files.iter().filter(|f| {
703
+ (f.as_str() == "package.json" || f.ends_with("/package.json"))
704
+ && !f.contains("node_modules/")
705
+ }) {
706
+ let Some(doc) = self
707
+ .source
708
+ .read_to_string(&self.root.join(file))
709
+ .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
710
+ else {
711
+ continue;
712
+ };
713
+ let Some(name) = doc["name"].as_str() else {
714
+ continue;
715
+ };
716
+ let exports = &doc["exports"];
717
+ let dot = exports.get(".").unwrap_or(exports);
718
+ let entries: Vec<String> = [
719
+ doc["source"].as_str(),
720
+ dot.as_str(),
721
+ dot["import"].as_str(),
722
+ dot["default"].as_str(),
723
+ doc["module"].as_str(),
724
+ doc["main"].as_str(),
725
+ ]
726
+ .into_iter()
727
+ .flatten()
728
+ .map(|e| e.trim_start_matches("./").to_string())
729
+ .collect();
730
+ out.insert(name.to_string(), (parent_dir(file).to_string(), entries));
731
+ }
732
+ out
733
+ });
734
+ let (name, (dir, entries)) = packages
735
+ .iter()
736
+ .filter(|(n, _)| spec == n.as_str() || spec.starts_with(&format!("{n}/")))
737
+ .max_by_key(|(n, _)| n.len())?;
738
+ let at = |rel: &str| normalize(&Path::new(dir).join(rel));
739
+ let sub = spec[name.len()..].trim_start_matches('/');
740
+ if sub.is_empty() {
741
+ entries
742
+ .iter()
743
+ .map(|e| at(e))
744
+ .chain(["src/index", "index"].map(at))
745
+ .find_map(|p| self.probe_js(&p))
746
+ } else {
747
+ [at(sub), at(&format!("src/{sub}"))]
748
+ .iter()
749
+ .find_map(|p| self.probe_js(p))
750
+ }
751
+ }
752
+
680
753
  fn resolve_js(&self, importer: &str, spec: &str) -> Resolution {
681
754
  if spec.starts_with("./") || spec.starts_with("../") || spec == "." || spec == ".." {
682
755
  let joined = normalize(&PathBuf::from(parent_dir(importer)).join(spec));
@@ -701,6 +774,9 @@ impl<'a> Resolver<'a> {
701
774
  return Resolution::File(found);
702
775
  }
703
776
  }
777
+ if let Some(found) = self.workspace_package(spec) {
778
+ return Resolution::File(found);
779
+ }
704
780
  let alias_like = spec.starts_with("@/")
705
781
  || spec.starts_with("~/")
706
782
  || spec.starts_with('#')
@@ -713,6 +789,10 @@ impl<'a> Resolver<'a> {
713
789
  }
714
790
  }
715
791
 
792
+ /// The owner of a method the syntax does not name: an object built inside a
793
+ /// call. A call through any receiver may reach it.
794
+ pub(crate) const UNKNOWN_OWNER: &str = "<object>";
795
+
716
796
  /// How a reference relates to the changed definition.
717
797
  #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
718
798
  pub enum Evidence {
@@ -813,6 +893,14 @@ fn params_of(content: &str) -> FxHashSet<String> {
813
893
  _ => {}
814
894
  }
815
895
  }
896
+ // `const x = (f(y) || []).map(…)` opens a parenthesised expression, not
897
+ // an arrow's parameters: those are followed by `=>` or a return type
898
+ // (#520).
899
+ let arrow_form = found.as_str().trim_end().ends_with('(') && found.as_str().contains('=');
900
+ let rest = head.get(end + 1..).unwrap_or("").trim_start();
901
+ if arrow_form && !(rest.starts_with("=>") || rest.starts_with(':')) {
902
+ return FxHashSet::default();
903
+ }
816
904
  head[start..end]
817
905
  .split(',')
818
906
  .filter_map(|p| {
@@ -1030,7 +1118,7 @@ impl Resolver<'_> {
1030
1118
 
1031
1119
  /// What `receiver.method` says about the receiver being `class`.
1032
1120
  fn receiver_is(&self, site: &Site, chain: &str, def: &Definition, class: &str) -> Evidence {
1033
- if chain.is_empty() {
1121
+ if chain.is_empty() || class == UNKNOWN_OWNER {
1034
1122
  return Evidence::Candidate("receiver_unresolved");
1035
1123
  }
1036
1124
  if matches!(chain, "self" | "cls" | "this") {
@@ -1069,10 +1157,23 @@ impl Resolver<'_> {
1069
1157
  /// Whether `ctor` (`Bot`, `bots.Bot`) constructs `class`. A name nothing
1070
1158
  /// binds — a factory passed in, a local helper — leaves it unknown.
1071
1159
  fn constructor_is(&self, site: &Site, ctor: &str, def: &Definition, class: &str) -> Evidence {
1160
+ // `model: Any` states no type: the receiver is unknown, not another
1161
+ // class (#425).
1162
+ if matches!(ctor, "Any" | "typing.Any" | "t.Any" | "object") {
1163
+ return Evidence::Candidate("receiver_unresolved");
1164
+ }
1072
1165
  let ctor_head = ctor.rsplit('.').next().unwrap_or(ctor);
1073
1166
  if ctor.contains('.') {
1074
1167
  let module = &ctor[..ctor.len() - ctor_head.len() - 1];
1075
1168
  if ctor_head != class {
1169
+ // `await Model.from_config(…)`: a factory on the class most
1170
+ // often returns one, which the syntax cannot confirm (#425).
1171
+ if module.rsplit('.').next() == Some(class) {
1172
+ return match self.constructor_is(site, module, def, class) {
1173
+ Evidence::Rejected => Evidence::Rejected,
1174
+ _ => Evidence::Candidate("receiver_unresolved"),
1175
+ };
1176
+ }
1076
1177
  return Evidence::Rejected;
1077
1178
  }
1078
1179
  let bound = self
@@ -1206,7 +1307,12 @@ pub(crate) fn is_import_line(code: &str) -> bool {
1206
1307
 
1207
1308
  /// Folds one occurrence into the finding: stronger evidence replaces the
1208
1309
  /// lines, equal evidence adds its line, a call outranks a reference.
1209
- fn absorb(best: &mut Option<Finding>, evidence: Evidence, relation: Relation, line: u32) {
1310
+ pub(crate) fn absorb(
1311
+ best: &mut Option<Finding>,
1312
+ evidence: Evidence,
1313
+ relation: Relation,
1314
+ line: u32,
1315
+ ) {
1210
1316
  let Some(f) = best else {
1211
1317
  *best = Some(Finding {
1212
1318
  evidence,
@@ -67,6 +67,7 @@ pub static EDGE_WEIGHTS: Lazy<FxHashMap<&'static str, EdgeWeightConfig>> = Lazy:
67
67
  ("php_type", EdgeWeightConfig::new(0.55, 0.40)),
68
68
  ("shell_source", EdgeWeightConfig::new(0.60, 0.35)),
69
69
  ("shell_script", EdgeWeightConfig::new(0.50, 0.35)),
70
+ ("shell_call", EdgeWeightConfig::new(0.55, 0.35)),
70
71
  ("swift_import", EdgeWeightConfig::new(0.65, 0.40)),
71
72
  ("swift_conformance", EdgeWeightConfig::new(0.70, 0.40)),
72
73
  ("swift_extension", EdgeWeightConfig::new(0.65, 0.40)),
@@ -32,19 +32,19 @@ fn is_ts_file(path: &Path) -> bool {
32
32
 
33
33
  static IMPORT_SOURCE_RE: Lazy<Regex> = Lazy::new(|| {
34
34
  Regex::new(
35
- r#"(?m)(?:import\s+.*?\s+from\s+['"]([^'"]+)['"]|require\s*\(\s*['"]([^'"]+)['"]\s*\))"#,
35
+ r#"(?m)(?:import\s+.*?\s+from\s+['"]([^'"]+)['"]|require\s*\(\s*['"]([^'"]+)['"]\s*\)|\bimport\s*\(\s*['"]([^'"]+)['"]\s*\))"#,
36
36
  )
37
37
  .unwrap()
38
38
  });
39
39
  static EXPORT_RE: Lazy<Regex> = Lazy::new(|| {
40
- Regex::new(r"(?m)^\s*export\s+(?:default\s+)?(?:function|class|const|let|var|interface|type|enum|abstract\s+class)\s+([A-Za-z_$]\w*)").unwrap()
40
+ Regex::new(r"(?m)^\s*export\s+(?:default\s+)?(?:async\s+)?(?:function(?:\s*\*\s*|\s+)|(?:class|const|let|var|interface|type|enum|abstract\s+class)\s+)([A-Za-z_$]\w*)").unwrap()
41
41
  });
42
42
  static NAMED_IMPORT_NAMES_RE: Lazy<Regex> =
43
43
  Lazy::new(|| Regex::new(r"(?m)import\s*\{([^}]+)\}\s*from").unwrap());
44
44
  static CALL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b([A-Za-z_$]\w*)\s*\(").unwrap());
45
45
  static TYPE_REF_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b([A-Z]\w*)\b").unwrap());
46
46
  static DEF_RE: Lazy<Regex> = Lazy::new(|| {
47
- Regex::new(r"(?m)^\s*(?:export\s+)?(?:default\s+)?(?:function|class|const|let|var|interface|type|enum)\s+([A-Za-z_$]\w*)").unwrap()
47
+ Regex::new(r"(?m)^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function(?:\s*\*\s*|\s+)|(?:class|const|let|var|interface|type|enum)\s+)([A-Za-z_$]\w*)").unwrap()
48
48
  });
49
49
 
50
50
  static JS_KEYWORDS: Lazy<FxHashSet<&str>> = Lazy::new(|| {
@@ -59,10 +59,7 @@ static JS_KEYWORDS: Lazy<FxHashSet<&str>> = Lazy::new(|| {
59
59
  fn extract_import_sources(content: &str) -> FxHashSet<String> {
60
60
  let mut sources = FxHashSet::default();
61
61
  for cap in IMPORT_SOURCE_RE.captures_iter(content) {
62
- if let Some(m) = cap.get(1) {
63
- sources.insert(m.as_str().to_string());
64
- }
65
- if let Some(m) = cap.get(2) {
62
+ for m in [cap.get(1), cap.get(2), cap.get(3)].into_iter().flatten() {
66
63
  sources.insert(m.as_str().to_string());
67
64
  }
68
65
  }
@@ -137,7 +134,18 @@ impl EdgeBuilder for JavaScriptEdgeBuilder {
137
134
  let mut frag_defines: FxHashMap<FragmentId, FxHashSet<String>> = FxHashMap::default();
138
135
 
139
136
  for f in &js_frags {
140
- let defines = extract_defines(&f.content);
137
+ let mut defines = extract_defines(&f.content);
138
+ // A class or object-literal method is a fragment the grammar
139
+ // named; no declaration keyword spells it (#444, #473).
140
+ if f.kind == crate::types::FragmentKind::Function {
141
+ if let Some(name) = f.symbol_name.as_deref() {
142
+ let name = name.split('[').next().unwrap_or(name);
143
+ let name = name.rsplit('.').next().unwrap_or(name);
144
+ if !name.is_empty() {
145
+ defines.insert(name.to_string());
146
+ }
147
+ }
148
+ }
141
149
  for name in &defines {
142
150
  name_to_defs
143
151
  .entry(name.clone())
@@ -72,6 +72,9 @@ static SCALA_CLASS_RE: Lazy<Regex> = Lazy::new(|| {
72
72
  static TYPE_REF_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b([A-Z]\w*)\b").unwrap());
73
73
  static ANNOTATION_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"@([A-Z]\w*)").unwrap());
74
74
  static MEMBER_USE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\.\s*([a-zA-Z_]\w{2,})").unwrap());
75
+ /// A call with no receiver: `map(x)`, never `.map(x)` or `obj.map(x)`.
76
+ static UNQUALIFIED_CALL_RE: Lazy<Regex> =
77
+ Lazy::new(|| Regex::new(r"(?:^|[^.\w$])([a-zA-Z_]\w{2,})\s*\(").unwrap());
75
78
  /// `UI.DP`: a member too short for the bare channel, spelled with its owner.
76
79
  static QUALIFIED_SHORT_USE_RE: Lazy<Regex> =
77
80
  Lazy::new(|| Regex::new(r"\b([A-Z]\w*)\s*\.\s*([A-Za-z_]\w?)\b").unwrap());
@@ -867,6 +870,28 @@ impl EdgeBuilder for JVMEdgeBuilder {
867
870
  }
868
871
  }
869
872
  }
873
+ // A Java method calls a sibling of its own file with no
874
+ // qualifier — `map(x)` in `toDto` — which the dotted member uses
875
+ // never see (#496). Scala's `apply` and paren-less calls make a
876
+ // bare name no such call.
877
+ let java = jf.path().ends_with(".java");
878
+ let code = crate::parsers::code_only(jf.path(), &jf.content)
879
+ .filter(|_| java)
880
+ .unwrap_or_default();
881
+ for cap in UNQUALIFIED_CALL_RE.captures_iter(&code) {
882
+ let m = cap[1].to_lowercase();
883
+ if own.as_deref() == Some(m.as_str()) {
884
+ continue;
885
+ }
886
+ for d in member_defs.get(&m).into_iter().flatten() {
887
+ if d.path.as_ref() == jf.path()
888
+ && d != &jf.id
889
+ && !(d.start_line <= jf.id.start_line && d.end_line >= jf.id.end_line)
890
+ {
891
+ add_edge(&mut edges, &jf.id, d, member_weight, reverse_factor);
892
+ }
893
+ }
894
+ }
870
895
  for cap in QUALIFIED_SHORT_USE_RE.captures_iter(&jf.content) {
871
896
  let key = (cap[1].to_string(), cap[2].to_string());
872
897
  for d in owned_short_members.get(&key).into_iter().flatten() {
@@ -6,7 +6,7 @@ use rustc_hash::{FxHashMap, FxHashSet};
6
6
 
7
7
  use crate::config::extensions::SHELL_EXTENSIONS;
8
8
  use crate::config::weights::EDGE_WEIGHTS;
9
- use crate::types::Fragment;
9
+ use crate::types::{Fragment, FragmentKind};
10
10
 
11
11
  use super::super::EdgeDict;
12
12
  use super::super::base::{self, EdgeBuilder, FragmentIndex, link_by_name};
@@ -39,6 +39,35 @@ fn extract_refs(content: &str) -> FxHashSet<String> {
39
39
  refs
40
40
  }
41
41
 
42
+ /// Words that run the command after them rather than being it.
43
+ const PREFIX_WORDS: &[&str] = &[
44
+ "if", "then", "else", "elif", "do", "while", "until", "time", "exec", "command", "builtin",
45
+ "sudo", "nohup", "env", "!",
46
+ ];
47
+
48
+ /// The command word of every simple command: `run_tool x`, `out=$(run_tool)`,
49
+ /// `a && run_tool`, `if run_tool; then`.
50
+ fn command_words(content: &str) -> FxHashSet<&str> {
51
+ let mut words = FxHashSet::default();
52
+ for line in content.lines() {
53
+ if line.trim_start().starts_with('#') {
54
+ continue;
55
+ }
56
+ for part in line.split([';', '|', '&', '(', ')', '`', '{', '}']) {
57
+ let word = part
58
+ .split_whitespace()
59
+ .find(|t| !t.contains('=') && !PREFIX_WORDS.contains(t));
60
+ if let Some(w) = word.filter(|w| {
61
+ w.chars()
62
+ .all(|c| c.is_alphanumeric() || matches!(c, '_' | '-' | ':' | '.'))
63
+ }) {
64
+ words.insert(w);
65
+ }
66
+ }
67
+ }
68
+ words
69
+ }
70
+
42
71
  pub struct ShellEdgeBuilder;
43
72
 
44
73
  impl EdgeBuilder for ShellEdgeBuilder {
@@ -103,6 +132,51 @@ impl EdgeBuilder for ShellEdgeBuilder {
103
132
  }
104
133
  }
105
134
 
135
+ // A function is called by name as a command word, from its own file
136
+ // or from one that sources it. Without this edge a call from a
137
+ // script's body was a lexical match only, and impact read the
138
+ // function as one nothing calls (#497).
139
+ let call = EDGE_WEIGHTS["shell_call"];
140
+ let file_name = |p: &str| p.rsplit('/').next().unwrap_or(p).to_string();
141
+ let mut sourced: FxHashMap<&str, FxHashSet<String>> = FxHashMap::default();
142
+ for f in &sh_frags {
143
+ for cap in SOURCE_RE.captures_iter(&f.content) {
144
+ sourced
145
+ .entry(f.path())
146
+ .or_default()
147
+ .insert(file_name(&cap[1]));
148
+ }
149
+ }
150
+ let functions: Vec<&Fragment> = sh_frags
151
+ .iter()
152
+ .copied()
153
+ .filter(|f| f.kind == FragmentKind::Function && f.symbol_name.is_some())
154
+ .collect();
155
+ for f in &sh_frags {
156
+ let words = command_words(&f.content);
157
+ for def in &functions {
158
+ let reachable = def.path() == f.path()
159
+ || sourced
160
+ .get(f.path())
161
+ .is_some_and(|s| s.contains(&file_name(def.path())));
162
+ if def.id != f.id
163
+ && reachable
164
+ && def
165
+ .symbol_name
166
+ .as_deref()
167
+ .is_some_and(|n| words.contains(n))
168
+ {
169
+ base::add_edge(
170
+ &mut edges,
171
+ &f.id,
172
+ &def.id,
173
+ call.forward,
174
+ call.reverse_factor,
175
+ );
176
+ }
177
+ }
178
+ }
179
+
106
180
  edges
107
181
  }
108
182
 
@@ -55,7 +55,6 @@ static GENERATED_PATH_SEGMENTS: Lazy<FxHashSet<&'static str>> = Lazy::new(|| {
55
55
 
56
56
  const GENERATED_CONTENT_MARKERS: &[&str] = &[
57
57
  "@generated",
58
- "do not edit",
59
58
  "code generated",
60
59
  "auto-generated",
61
60
  "this file is generated",
@@ -108,6 +107,12 @@ fn has_generated_content_marker(content: &str) -> bool {
108
107
  .collect::<Vec<_>>()
109
108
  .join("\n")
110
109
  .to_lowercase();
110
+ // "Do not edit" is a generator's warning only beside the word that says
111
+ // a generator wrote it; a hand-written "do not edit without approval"
112
+ // marks a file people do edit (#411).
113
+ if header.contains("do not edit") && header.contains("generat") {
114
+ return true;
115
+ }
111
116
  for marker in GENERATED_CONTENT_MARKERS {
112
117
  if !header.contains(marker) {
113
118
  continue;
@@ -1522,11 +1522,9 @@ pub fn grep_code_words(
1522
1522
  .iter()
1523
1523
  .map(|ext| format!(":(glob,icase)**/*{ext}"))
1524
1524
  .collect();
1525
+ let patterns = Patterns::new(words);
1525
1526
  let mut args: Vec<&str> = vec!["grep", "-n", "-z", "-I", "-F", "-w", "-m", &per_file];
1526
- for w in words {
1527
- args.push("-e");
1528
- args.push(w);
1529
- }
1527
+ args.extend(patterns.args.iter().map(String::as_str));
1530
1528
  args.extend(rev);
1531
1529
  args.push("--");
1532
1530
  args.extend(globs.iter().map(String::as_str));
@@ -1543,15 +1541,53 @@ pub fn grep_lines(
1543
1541
  if needles.is_empty() {
1544
1542
  return Ok(Grepped::default());
1545
1543
  }
1544
+ let patterns = Patterns::new(needles);
1546
1545
  let mut args: Vec<&str> = vec!["grep", "-n", "-z", "-I", "-F"];
1547
- for n in needles {
1548
- args.push("-e");
1549
- args.push(n);
1550
- }
1546
+ args.extend(patterns.args.iter().map(String::as_str));
1551
1547
  args.extend(rev);
1552
1548
  grep_rows(repo_root, &args, rev)
1553
1549
  }
1554
1550
 
1551
+ /// `-f FILE` naming the patterns one per line, or `-e` per pattern when the
1552
+ /// file cannot be written: tens of thousands of `-e` arguments overflow
1553
+ /// ARG_MAX and the whole search fails (#440). The file goes with the value.
1554
+ struct Patterns {
1555
+ file: Option<PathBuf>,
1556
+ args: Vec<String>,
1557
+ }
1558
+
1559
+ impl Patterns {
1560
+ fn new(words: &[String]) -> Self {
1561
+ // An empty line would match every line.
1562
+ let listed: Vec<&str> = words
1563
+ .iter()
1564
+ .map(String::as_str)
1565
+ .filter(|w| !w.is_empty())
1566
+ .collect();
1567
+ match write_private_temp_file(&listed.join("\n")) {
1568
+ Some(file) => Self {
1569
+ args: vec!["-f".to_string(), file.to_string_lossy().into_owned()],
1570
+ file: Some(file),
1571
+ },
1572
+ None => Self {
1573
+ file: None,
1574
+ args: listed
1575
+ .iter()
1576
+ .flat_map(|w| ["-e".to_string(), (*w).to_string()])
1577
+ .collect(),
1578
+ },
1579
+ }
1580
+ }
1581
+ }
1582
+
1583
+ impl Drop for Patterns {
1584
+ fn drop(&mut self) {
1585
+ if let Some(file) = &self.file {
1586
+ let _ = std::fs::remove_file(file);
1587
+ }
1588
+ }
1589
+ }
1590
+
1555
1591
  pub fn get_untracked_files(repo_root: &Path, pathspec: &[String]) -> Result<Vec<PathBuf>> {
1556
1592
  let mut args: Vec<&str> = vec!["ls-files", "--others", "--exclude-standard", "-z"];
1557
1593
  if !pathspec.is_empty() {