Repository navigation
fix: address the retroactive review findings on the v1.6.1 port #291
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
4ca9b98
fix(extract): give same-line Vue script functions distinct ids (#1349)
b711722
fix(extract): keep a store initializer's own calls on the store
92ee84c
fix(store): bump the extraction version to 18
35a414c
fix(mcp): claim gap names only when every gap had room for them
60d441e
fix(resolve): prefer an explicit default binding over an exported com…
7293396
docs(upstream): record the retroactive review of the v1.6.1 port
60366e4
fix(mcp): never claim that gap markers name every elided symbol
1127f07
docs(upstream): describe the trim-note fix as it now stands
59a2520
fix(extract): satisfy clippy's nonminimal_bool on the pinned toolchain
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| //! The trim note never claims a gap marker named what it could not afford to | ||
| //! (#1711, #2077). | ||
| //! | ||
| //! A gap between two rendered windows names the indexed symbols it hides, but | ||
| //! only from the budget the file has spare; a name that costs more stays out | ||
| //! and the marker is a bare `... (gap) ...`. The note closing the response | ||
| //! then said the gap markers named what was elided. Here an index-only helper | ||
| //! whose name no budget affords sits between the two windows. | ||
|
|
||
| use std::path::{Path, PathBuf}; | ||
| use std::process::Command; | ||
|
|
||
| fn bin() -> PathBuf { | ||
| PathBuf::from(env!("CARGO_BIN_EXE_codegraph")) | ||
| } | ||
|
|
||
| struct TestDir { | ||
| path: PathBuf, | ||
| } | ||
|
|
||
| impl TestDir { | ||
| fn new(label: &str) -> Self { | ||
| let path = std::env::temp_dir().join(format!( | ||
| "codegraph-cli-unnamed-gap-{label}-{}-{}", | ||
| std::process::id(), | ||
| std::time::SystemTime::now() | ||
| .duration_since(std::time::UNIX_EPOCH) | ||
| .unwrap() | ||
| .as_nanos() | ||
| )); | ||
| std::fs::create_dir_all(&path).unwrap(); | ||
| Self { path } | ||
| } | ||
| } | ||
|
|
||
| impl Drop for TestDir { | ||
| fn drop(&mut self) { | ||
| let _ = std::fs::remove_dir_all(&self.path); | ||
| } | ||
| } | ||
|
|
||
| fn run_in(cwd: &Path, args: &[&str]) -> String { | ||
| let output = Command::new(bin()) | ||
| .args(args) | ||
| .current_dir(cwd) | ||
| .env("CODEGRAPH_NO_DAEMON", "1") | ||
| .output() | ||
| .expect("run codegraph binary"); | ||
| assert!( | ||
| output.status.success(), | ||
| "{args:?} failed: {}{}", | ||
| String::from_utf8_lossy(&output.stdout), | ||
| String::from_utf8_lossy(&output.stderr) | ||
| ); | ||
| String::from_utf8_lossy(&output.stdout).into_owned() | ||
| } | ||
|
|
||
| /// What follows the last source fence: the epilogue. | ||
| fn epilogue_of(text: &str) -> &str { | ||
| text.rfind("```").map_or(text, |i| &text[i + 3..]) | ||
| } | ||
|
|
||
| /// `src/big.ts`: `alphaEntry` at the top, then (when `helper_x` is given) an | ||
| /// index-only helper named `helper` plus that many X's, then `betaEntry`, whose | ||
| /// body runs `beta_lines` lines. `padding` small files lift the project into | ||
| /// the large tiers. | ||
| fn project( | ||
| label: &str, | ||
| helper_x: Option<usize>, | ||
| beta_lines: usize, | ||
| padding: usize, | ||
| ) -> (TestDir, PathBuf) { | ||
| let dir = TestDir::new(label); | ||
| let root = dir.path.join("app"); | ||
| let src = root.join("src"); | ||
| std::fs::create_dir_all(&src).unwrap(); | ||
| let mut body: Vec<String> = (1..10).map(|i| format!("// header {i}")).collect(); | ||
| body.push("export function alphaEntry(): number {".to_string()); | ||
| body.extend((0..19).map(|i| format!(" const a{i} = {i};"))); | ||
| body.push(" return betaEntry(1);".to_string()); | ||
| body.push("}".to_string()); | ||
| while body.len() < 99 { | ||
| body.push(format!("// filler {}", body.len() + 1)); | ||
| } | ||
| if let Some(x) = helper_x { | ||
| let helper = format!("helper{}", "X".repeat(x)); | ||
| body.push(format!("function {helper}(): number {{ return 1; }}")); | ||
| } | ||
| while body.len() < 199 { | ||
| body.push(format!("// filler {}", body.len() + 1)); | ||
| } | ||
| body.push("export function betaEntry(v: number): number {".to_string()); | ||
| body.extend((0..beta_lines).map(|i| format!(" v = v + {i}; // beta stage {i}"))); | ||
| body.push(" return v;".to_string()); | ||
| body.push("}".to_string()); | ||
| while body.len() < 400 { | ||
| body.push(format!("// filler {}", body.len() + 1)); | ||
| } | ||
| std::fs::write(src.join("big.ts"), body.join("\n")).unwrap(); | ||
| if padding > 0 { | ||
| let pad = root.join("pad"); | ||
| std::fs::create_dir_all(&pad).unwrap(); | ||
| for i in 0..padding { | ||
| std::fs::write( | ||
| pad.join(format!("pad{i}.ts")), | ||
| format!("export const pad{i} = {i};\n"), | ||
| ) | ||
| .unwrap(); | ||
| } | ||
| } | ||
| run_in(&dir.path, &["init", root.to_str().unwrap()]); | ||
| (dir, root) | ||
| } | ||
|
|
||
| #[test] | ||
| fn small_tier_note_does_not_claim_an_unaffordable_gap_name() { | ||
| // No per-file budget affords an 8000-character name. | ||
| let (_dir, root) = project("small", Some(8000), 19, 0); | ||
| let text = run_in(&root, &["explore", "alphaEntry betaEntry"]); | ||
| // The fixture does what it is for: both windows render around a bare gap. | ||
| assert!(text.contains("export function alphaEntry"), "{text}"); | ||
| assert!(text.contains("export function betaEntry"), "{text}"); | ||
| assert!(text.contains("... (gap) ..."), "{text}"); | ||
| assert!(!text.contains("XXXXXXXX"), "{text}"); | ||
| let epilogue = epilogue_of(&text); | ||
| assert!( | ||
| epilogue.contains("Some file sections were trimmed for size"), | ||
| "{epilogue}" | ||
| ); | ||
| assert!( | ||
| !epilogue.contains("Elided symbols are named inside gap markers"), | ||
| "{epilogue}" | ||
| ); | ||
| assert!(epilogue.contains("only where room allowed"), "{epilogue}"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn large_tier_note_does_not_claim_an_unaffordable_gap_name() { | ||
| let (_dir, root) = project("large", Some(8000), 900, 520); | ||
| let text = run_in(&root, &["explore", "alphaEntry betaEntry"]); | ||
| assert!(text.contains("export function alphaEntry"), "{text}"); | ||
| assert!(text.contains("... (gap) ..."), "{text}"); | ||
| assert!(!text.contains("XXXXXXXX"), "{text}"); | ||
| let epilogue = epilogue_of(&text); | ||
| // `betaEntry` is windowed, so the section is reported trimmed. | ||
| assert!(epilogue.contains("Verbatim source for"), "{epilogue}"); | ||
| assert!(!epilogue.contains("name what was elided"), "{epilogue}"); | ||
| assert!(epilogue.contains("name what room allowed"), "{epilogue}"); | ||
| } | ||
|
|
||
| /// The section a windowed `betaEntry` leaves ends in a bare tail marker: what | ||
| /// it cut is never named in a gap marker, whatever the budget. Nothing indexed | ||
| /// sits between the two functions, so no inner gap had a name to withhold. | ||
| fn assert_trailing_bare_gap(text: &str) { | ||
| assert!(text.contains("export function betaEntry"), "{text}"); | ||
| assert!(text.contains("... (gap) ...\n```"), "{text}"); | ||
| assert!(!text.contains("(gap: "), "{text}"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn small_tier_note_does_not_claim_names_for_a_trailing_gap() { | ||
| let (_dir, root) = project("small-tail", None, 900, 0); | ||
| let text = run_in(&root, &["explore", "alphaEntry betaEntry"]); | ||
| assert_trailing_bare_gap(&text); | ||
| let epilogue = epilogue_of(&text); | ||
| assert!( | ||
| epilogue.contains("Some file sections were trimmed for size"), | ||
| "{epilogue}" | ||
| ); | ||
| assert!( | ||
| !epilogue.contains("Elided symbols are named inside gap markers"), | ||
| "{epilogue}" | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn large_tier_note_does_not_claim_names_for_a_trailing_gap() { | ||
| let (_dir, root) = project("large-tail", None, 900, 520); | ||
| let text = run_in(&root, &["explore", "alphaEntry betaEntry"]); | ||
| assert_trailing_bare_gap(&text); | ||
| let epilogue = epilogue_of(&text); | ||
| assert!(epilogue.contains("Verbatim source for"), "{epilogue}"); | ||
| assert!(!epilogue.contains("name what was elided"), "{epilogue}"); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| //! Same-line namesakes survive the store, not just extraction (#1349). | ||
| //! | ||
| //! Nodes are upserted by id, so two declarations sharing one id leave a single | ||
| //! row behind. The Vue extractor mints its script functions itself; this | ||
| //! indexes a real project and reads the rows back. | ||
|
|
||
| use std::fs; | ||
| use std::path::{Path, PathBuf}; | ||
| use std::process::Command; | ||
| use std::time::{Duration, Instant}; | ||
|
|
||
| use codegraph_core::IndexPaths; | ||
| use codegraph_core::types::NodeKind; | ||
| use codegraph_store::Store; | ||
|
|
||
| struct TestDir { | ||
| path: PathBuf, | ||
| } | ||
|
|
||
| impl TestDir { | ||
| fn new(label: &str) -> Self { | ||
| let path = std::env::temp_dir().join(format!( | ||
| "codegraph-cli-same-line-identity-{label}-{}-{}", | ||
| std::process::id(), | ||
| std::time::SystemTime::now() | ||
| .duration_since(std::time::UNIX_EPOCH) | ||
| .unwrap() | ||
| .as_nanos() | ||
| )); | ||
| fs::create_dir_all(&path).unwrap(); | ||
| Self { path } | ||
| } | ||
| } | ||
|
|
||
| impl Drop for TestDir { | ||
| fn drop(&mut self) { | ||
| let _ = fs::remove_dir_all(&self.path); | ||
| } | ||
| } | ||
|
|
||
| fn cli(args: &[&str]) { | ||
| let output = Command::new(env!("CARGO_BIN_EXE_codegraph")) | ||
| .args(args) | ||
| .env("CODEGRAPH_NO_DAEMON", "1") | ||
| .output() | ||
| .expect("run codegraph binary"); | ||
| assert!( | ||
| output.status.success(), | ||
| "codegraph {args:?} failed: stdout={} stderr={}", | ||
| String::from_utf8_lossy(&output.stdout), | ||
| String::from_utf8_lossy(&output.stderr) | ||
| ); | ||
| } | ||
|
|
||
| fn store(project: &Path) -> Store { | ||
| let paths = IndexPaths::resolve(project, None).expect("resolve index paths"); | ||
| Store::open_for_read(&paths, Instant::now() + Duration::from_secs(30), || false) | ||
| .expect("open the index for reading") | ||
| } | ||
|
|
||
| #[test] | ||
| fn same_line_vue_functions_are_two_rows() { | ||
| let dir = TestDir::new("vue"); | ||
| let project = dir.path.join("proj"); | ||
| fs::create_dir_all(&project).unwrap(); | ||
| fs::write( | ||
| project.join("Widget.vue"), | ||
| "<template><div/></template>\n<script>\nfunction f() {} function f() {}\n</script>\n", | ||
| ) | ||
| .unwrap(); | ||
| let p = project.to_str().unwrap(); | ||
| cli(&["init", p]); | ||
| let rows = |project: &Path| { | ||
| let mut rows = store(project) | ||
| .all_nodes() | ||
| .unwrap() | ||
| .into_iter() | ||
| .filter(|node| node.kind == NodeKind::Function && node.name == "f") | ||
| .map(|node| (node.id, node.start_line, node.start_column)) | ||
| .collect::<Vec<_>>(); | ||
| rows.sort(); | ||
| rows | ||
| }; | ||
| let indexed = rows(&project); | ||
| assert_eq!(indexed.len(), 2, "{indexed:#?}"); | ||
| assert_ne!(indexed[0].0, indexed[1].0); | ||
| cli(&["sync", p]); | ||
| assert_eq!(rows(&project), indexed, "sync keeps both rows"); | ||
| cli(&["index", "--force", p]); | ||
| assert_eq!(rows(&project), indexed, "index --force keeps both rows"); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For a valid SFC containing both
<script>and<script setup>, functions with the same name at the same block-relative row and column receive the same ID even though their actual source lines differ; the allocator treats the second declaration as a revisit, and the store upsert drops one node. Pass the line includingblock.line_offset, matching theNode.start_line, so IDs follow the required file-relative formula.AGENTS.md reference: AGENTS.md:L46-L52
Useful? React with 👍 / 👎.