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.
- {diffctx-1.19.0 → diffctx-1.19.1}/CHANGELOG.md +81 -1
- {diffctx-1.19.0 → diffctx-1.19.1}/Cargo.lock +1 -1
- {diffctx-1.19.0 → diffctx-1.19.1}/PKG-INFO +1 -1
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/Cargo.toml +1 -1
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/bindings.rs +109 -3
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/weights.rs +1 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/javascript.rs +16 -8
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/jvm.rs +25 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/shell.rs +75 -1
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/fragmentation.rs +6 -1
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/git.rs +44 -8
- diffctx-1.19.1/crates/diffctx-native/src/go_bindings.rs +603 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/hook.rs +62 -28
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/impact.rs +469 -59
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/kotlin_bindings.rs +1 -27
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/lib.rs +1 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/main.rs +11 -5
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/parsers/tree_sitter_strategy.rs +15 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/pipeline.rs +23 -8
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/testfiles.rs +2 -1
- {diffctx-1.19.0 → diffctx-1.19.1}/pyproject.toml +1 -1
- {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/_app.py +4 -2
- diffctx-1.19.1/src/diffctx/version.py +1 -0
- diffctx-1.19.0/src/diffctx/version.py +0 -1
- {diffctx-1.19.0 → diffctx-1.19.1}/Cargo.toml +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/LICENSE +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/README.md +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/README.md +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/analytics.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/candidate_files.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/change_class.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/analytics.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/bm25.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/budget.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/category_weights.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/edge_weights.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/env_overrides.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/extensions.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/filtering.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/fragmentation.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/git.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/graph_filtering.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/importance.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/limits.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/mod.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/mode.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/needs.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/parsers.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/render.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/scoring.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/selection.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/config/tokenization.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/container.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/core.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/discovery.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/base.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/config_edges/build_system.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/config_edges/cicd.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/config_edges/docker.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/config_edges/generic.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/config_edges/helm.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/config_edges/kubernetes.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/config_edges/mod.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/document/mod.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/history/cochange.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/history/mod.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/mod.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/ansible.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/bazel.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/c_family.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/cargo_edges.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/clojure.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/css.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/dart.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/dbt.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/dotnet.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/elixir.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/erlang.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/go.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/graphql.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/haskell.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/julia.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/latex.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/lua.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/mod.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/nim.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/nix.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/ocaml.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/openapi.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/perl.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/php.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/prisma.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/protobuf.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/python.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/r_lang.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/ruby.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/rust_lang.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/sql.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/swift.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/tags.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/terraform.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/semantic/zig.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/similarity/lexical.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/similarity/mod.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/structural/containment.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/structural/mod.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/structural/sibling.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/edges/structural/testing.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/effective_config.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/excerpt.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/facts/kotlin.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/facts/mod.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/facts/scala.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/filtering.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/graph.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/graph_export.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/in_memory_harness.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/interval.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/languages.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/locate.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/mode.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/parsers/config_parser.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/parsers/generic.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/parsers/markdown.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/parsers/mod.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/paths.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/peak_rss.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/postpass.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/ppr.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/project_graph.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/provenance.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/pybridge.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/render.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/resource.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/run_provenance.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/sanitize.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/scoring.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/select.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/signatures.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/source.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/stopwords/code.txt +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/stopwords/docs.txt +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/stopwords.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/test_rng.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/token_corpus.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/tokenizer.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/types.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/utility/boltzmann.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/utility/importance.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/utility/mod.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/utility/needs.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/crates/diffctx-native/src/utility/objective.rs +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/rust-toolchain.toml +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/__init__.py +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/__main__.py +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/_diffctx.pyi +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/_native/__init__.py +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/_native/graph_analytics.py +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/_native/graph_export.py +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/_native/pipeline.py +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/_native/project_graph.py +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/cli.py +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/clipboard.py +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/ignore.py +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/logger.py +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/mcp/__init__.py +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/mcp/__main__.py +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/mcp/fetch.py +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/mcp/launch.py +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/mcp/legacy.py +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/mcp/security.py +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/mcp/server.py +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/py.typed +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/tokens.py +0 -0
- {diffctx-1.19.0 → diffctx-1.19.1}/src/diffctx/tree.py +0 -0
- {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.
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "diffctx"
|
|
3
|
-
version = "1.19.
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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() {
|