Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### Fixes

- **Code written without a space before a name resolves like spaced code.** Where one token ends exactly where the next begins, the resolver read the syntax at that point as the earlier token: in `x=1;local function helper()` it saw the `;`, so the Lua local read as global and calls from other files linked to it, and in `if ($x instanceof Foo)$x->bar();` it saw the `)`, so the PHP guard did not type `$x`. It now reads the token that starts there, the same way the TypeScript side already did. On json, jq and CPython the graph is unchanged.
- **React Router's own `flatRoutes() satisfies RouteConfig` template now gets file routes, and RSC route modules link to their `ServerComponent`.** An `app/routes.ts` written as `export default flatRoutes() satisfies RouteConfig;` (or `as RouteConfig`), the form React Router's templates use, was taken for custom configuration, so the app got no file routes at all. In React Router's RSC framework mode a route module exports `ServerComponent` instead of a default component; such a page now gets its route and links to that component, both for files under `app/routes/` and for modules listed in `app/routes.ts`. A module that exports both keeps its default component.
- **An inherited method no longer links to a same-named class in another package.** When a call on a `Child` reached `m()` through `Child extends Base`, the lookup past the first parent matched any class called `Base` in the project, so with an unrelated `Base` in another package the call could land on that package's `Base.m()`, or skip a grandparent's `m()` for it. The lookup now follows the parent the declaration names, for Java, Kotlin, C#, Scala and Swift, where a Java or Kotlin receiver comes from a factory call, or a C#, Scala or Swift receiver is typed. In Scala, a same-named object or a package block in the same file is told apart too; in Swift the other `Base` is one declared in another file, since modules are not modeled. Dart, TypeScript, Python and C++ are unchanged: their wrong links did not reproduce, or Dart's member lookup matches parents by name elsewhere.
- **Python calls on a local built through a module, or through one of its fields, now resolve.** `bp = pkg.Blueprint()`, `c.Blueprint()` and `core.Blueprint()` type the local like a class imported by name, and `app.config.from_mapping()` resolves on the class the field holds, as `self.config.from_mapping()` already did. On Flask and requests this adds 98 and 103 call edges (almost all from tests, none removed); a 30-edge sample was all correct.
Expand Down
55 changes: 55 additions & 0 deletions __tests__/kernel-point-lookup-boundary.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* The kernel finds the syntax node at a reference or declaration's start
* position. Where the previous token ends exactly there (`x=1;local function`,
* `instanceof Foo)$x->bar()`), the node at that position is the token that
* starts there, not the `;` or `)` before it. Finding the earlier token made a
* Lua `local` function read as global and hid a PHP `instanceof` guard.
* The spaced forms are the controls: they resolved the same way before.
*/
import { describe, it, expect, afterAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';

const roots: string[] = [];
afterAll(() => {
for (const r of roots.splice(0)) fs.rmSync(r, { recursive: true, force: true });
});

async function callTargets(files: Record<string, string>, from: string): Promise<string[]> {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-point-lookup-'));
roots.push(root);
for (const [rel, content] of Object.entries(files)) fs.writeFileSync(path.join(root, rel), content);
const cg = await CodeGraph.init(root, { index: true });
try {
return cg
.getOutgoingEdgesFrom(cg.getNodesInFile(from).map((n) => n.id), ['calls'])
.map((e) => cg.getNode(e.target)!.qualifiedName)
.sort();
} finally {
cg.close();
}
}

describe('point lookup at a token boundary', () => {
it('reads a Lua local function written right after `;` as local', async () => {
const files = {
'a.lua': 'x=1;local function helper() return 1 end\nlocal y = 2; local function spaced() return 2 end\nreturn x\n',
'b.lua': 'local function main()\n helper()\n spaced()\nend\nreturn main\n',
};
expect(await callTargets(files, 'b.lua')).toEqual([]);
});

it('applies a PHP instanceof guard to a call written right after `)`', async () => {
const files = {
'a.php': `<?php
class Foo { public function bar() { return 1; } }
class Qux { public function bar() { return 2; } }
function tight($x) { if ($x instanceof Foo)$x->bar(); }
function spaced($x) { if ($x instanceof Foo) $x->bar(); }
`,
};
expect(await callTargets(files, 'a.php')).toEqual(['Foo::bar', 'Foo::bar']);
});
});
2 changes: 1 addition & 1 deletion codegraph-kernel/src/resolve/call_shape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ impl KernelResolver {
let lines = self.read_file(&r.file_path)?;
if r.language=="rust" && r.reference_kind=="calls" {
if let Some(tree)=self.parsed_tree(&lines,r) {
let mut node=super::iteration::descendant_for_position(tree.root_node(),lines.text(),((r.line-1).max(0) as usize,r.column.max(0) as usize+1));
let mut node=super::iteration::descendant_for_position(tree.root_node(),lines.text(),((r.line-1).max(0) as usize,r.column.max(0) as usize));
loop {
if node.kind()=="field_expression" {
if let (Some(field),Some(value))=(node.child_by_field_name("field"),node.child_by_field_name("value")) {
Expand Down
4 changes: 2 additions & 2 deletions codegraph-kernel/src/resolve/cpp_aliases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ impl KernelResolver {
let Some(tree) = self.parsed_tree(&source, r) else { continue; };
for keyword in re!(r"\b(?:using|typedef)\b").find_iter(line) {
let column = line[..keyword.start()].encode_utf16().count();
let mut node = super::iteration::descendant_for_position(tree.root_node(), source.text(), (i, column + 1));
let mut node = super::iteration::descendant_for_position(tree.root_node(), source.text(), (i, column));
let mut code = true;
loop {
if matches!(node.kind(), "comment" | "string_literal" | "raw_string_literal" | "char_literal") { code = false; break; }
Expand Down Expand Up @@ -312,7 +312,7 @@ impl KernelResolver {
let Some(source) = self.read_file(&owner.file_path) else { return Ok(false); };
let site = r.clone().at(owner);
let Some(tree) = self.parsed_tree(&source, &site) else { return Ok(false); };
let mut node = super::iteration::descendant_for_position(tree.root_node(), source.text(), ((owner.start_line - 1).max(0) as usize, owner.start_column.max(0) as usize + 1));
let mut node = super::iteration::descendant_for_position(tree.root_node(), source.text(), ((owner.start_line - 1).max(0) as usize, owner.start_column.max(0) as usize));
while let Some(parent) = node.parent() {
if parent.kind() == "template_declaration" { return Ok(true); }
node = parent;
Expand Down
4 changes: 2 additions & 2 deletions codegraph-kernel/src/resolve/cpp_range_for.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ impl KernelResolver {
if r.language != "cpp" { return Ok(None); }
let Some(source) = self.read_file(&r.file_path) else { return Ok(None) };
let Some(tree) = self.parsed_tree(&source, r) else { return Ok(None) };
let mut at = descendant_for_position(tree.root_node(), source.text(), ((r.line - 1).max(0) as usize, r.column.max(0) as usize + 1));
let mut at = descendant_for_position(tree.root_node(), source.text(), ((r.line - 1).max(0) as usize, r.column.max(0) as usize));
while let Some(parent) = at.parent() {
// A nearer declaration of the name, `{ Gadget* w = g; w->Self(); }` in the body, hides the loop's.
if cpp_binding_type(parent, at, receiver, &source).is_some() { return Ok(None); }
Expand Down Expand Up @@ -270,7 +270,7 @@ impl KernelResolver {
let Some(source) = self.read_file(&function.file_path) else { return Ok(None) };
let site = site_at(r, function);
let Some(tree) = self.parsed_tree(&source, &site) else { return Ok(None) };
let mut node = descendant_for_position(tree.root_node(), source.text(), ((function.start_line - 1).max(0) as usize, function.start_column.max(0) as usize + 1));
let mut node = descendant_for_position(tree.root_node(), source.text(), ((function.start_line - 1).max(0) as usize, function.start_column.max(0) as usize));
let declaration = loop {
match node.kind() {
"function_definition" | "field_declaration" | "declaration" => break node,
Expand Down
10 changes: 5 additions & 5 deletions codegraph-kernel/src/resolve/cpp_receivers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ impl KernelResolver {
if !re!(r"^[A-Za-z_]\w*$").is_match(method) { return Ok(None); }
let Some(source) = self.read_file(&r.file_path) else { return Ok(None) };
let Some(tree) = self.parsed_tree(&source, r) else { return Ok(None) };
let mut at = descendant_for_position(tree.root_node(), source.text(), ((r.line - 1).max(0) as usize, r.column.max(0) as usize + 1));
let mut at = descendant_for_position(tree.root_node(), source.text(), ((r.line - 1).max(0) as usize, r.column.max(0) as usize));
while at.kind() != "call_expression" {
let Some(parent) = at.parent() else { return Ok(None) }; at = parent;
}
Expand Down Expand Up @@ -149,7 +149,7 @@ impl KernelResolver {
if r.language != "cpp" || r.reference_kind != "calls" || !re!(r"^[A-Za-z_]\w*$").is_match(&r.reference_name) { return Ok(None); }
let Some(source) = self.read_file(&r.file_path) else { return Ok(None) };
let Some(tree) = self.parsed_tree(&source, r) else { return Ok(None) };
let mut node = descendant_for_position(tree.root_node(), source.text(), ((r.line - 1).max(0) as usize, r.column.max(0) as usize + 1));
let mut node = descendant_for_position(tree.root_node(), source.text(), ((r.line - 1).max(0) as usize, r.column.max(0) as usize));
loop {
if node.kind() == "call_expression" {
if let Some(function) = node.child_by_field_name("function").filter(|n| n.kind() == "field_expression") {
Expand Down Expand Up @@ -240,7 +240,7 @@ impl KernelResolver {
let Some(source) = self.read_file(&method.file_path) else { return Ok(None) };
let mut site = r.clone().at(method); site.from_node_id = method.id.clone(); site.column = method.start_column;
let Some(tree) = self.parsed_tree(&source, &site) else { return Ok(None) };
let mut node = descendant_for_position(tree.root_node(), source.text(), ((method.start_line - 1).max(0) as usize, method.start_column.max(0) as usize + 1));
let mut node = descendant_for_position(tree.root_node(), source.text(), ((method.start_line - 1).max(0) as usize, method.start_column.max(0) as usize));
loop {
if matches!(node.kind(), "function_definition" | "field_declaration" | "declaration") {
return Ok(node.child_by_field_name("type").map(|ty| source.text()[ty.start_byte()..ty.end_byte()].to_string()));
Expand Down Expand Up @@ -279,7 +279,7 @@ impl KernelResolver {
let Some(source) = self.read_file(&owner.file_path) else { return Ok(None) };
let mut site = r.clone().at(owner); site.from_node_id = owner.id.clone(); site.column = owner.start_column;
let Some(tree) = self.cpp_field_tree(&source, &site) else { return Ok(None) };
let mut node = descendant_for_position(tree.root_node(), source.text(), ((owner.start_line - 1).max(0) as usize, owner.start_column.max(0) as usize + 1));
let mut node = descendant_for_position(tree.root_node(), source.text(), ((owner.start_line - 1).max(0) as usize, owner.start_column.max(0) as usize));
while !matches!(node.kind(), "class_specifier" | "struct_specifier" | "union_specifier") {
let Some(parent) = node.parent() else { break }; node = parent;
}
Expand Down Expand Up @@ -313,7 +313,7 @@ impl KernelResolver {
fn cpp_container_element(&mut self, name: &str, r: &ResolveRefIn) -> Res<Option<Arc<KNode>>> {
let Some(source) = self.read_file(&r.file_path) else { return Ok(None) };
if let Some(tree) = self.parsed_tree(&source, r) {
let mut at = descendant_for_position(tree.root_node(), source.text(), ((r.line - 1).max(0) as usize, r.column.max(0) as usize + 1));
let mut at = descendant_for_position(tree.root_node(), source.text(), ((r.line - 1).max(0) as usize, r.column.max(0) as usize));
while let Some(parent) = at.parent() {
if let Some(raw) = cpp_binding_type(parent, at, name, &source) {
let Some(element) = cpp_element_type(&raw) else { return Ok(None) };
Expand Down
4 changes: 2 additions & 2 deletions codegraph-kernel/src/resolve/cpp_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ impl KernelResolver {
let mut directives = Vec::new();
if let Some(tree) = self.parsed_tree(&lines, r) {
let mut site = super::iteration::descendant_for_position(tree.root_node(), lines.text(),
((r.line - 1).max(0) as usize, r.column.max(0) as usize + 1));
((r.line - 1).max(0) as usize, r.column.max(0) as usize));
let before = site.start_byte();
loop {
if matches!(site.kind(), "translation_unit" | "declaration_list" | "field_declaration_list" | "compound_statement") {
Expand Down Expand Up @@ -174,7 +174,7 @@ impl KernelResolver {
let Some(source) = self.read_file(&owner.file_path) else { return Ok(false) };
let mut site = r.clone().at(owner); site.from_node_id = owner.id.clone(); site.column = owner.start_column;
let Some(tree) = self.parsed_tree(&source, &site) else { return Ok(false) };
let mut node = super::iteration::descendant_for_position(tree.root_node(), source.text(), ((owner.start_line - 1).max(0) as usize, owner.start_column.max(0) as usize + 1));
let mut node = super::iteration::descendant_for_position(tree.root_node(), source.text(), ((owner.start_line - 1).max(0) as usize, owner.start_column.max(0) as usize));
while let Some(parent) = node.parent() {
if parent.kind() == "template_declaration" {
if let Some(parameters) = parent.child_by_field_name("parameters") {
Expand Down
4 changes: 2 additions & 2 deletions codegraph-kernel/src/resolve/fields.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ impl KernelResolver {
let Some(tree) = self.parsed_tree(&source, &site) else { return Ok(false); };
let mut declaration = super::iteration::descendant_for_position(
tree.root_node(), source.text(),
((target.start_line - 1).max(0) as usize, target.start_column.max(0) as usize + 1),
((target.start_line - 1).max(0) as usize, target.start_column.max(0) as usize),
);
while !matches!(declaration.kind(), "type_spec" | "type_alias") {
let Some(parent) = declaration.parent() else { return Ok(false); };
Expand Down Expand Up @@ -231,7 +231,7 @@ impl KernelResolver {
if let Some(tree) = self.parsed_tree(&lines, &site) {
let mut declaration = super::iteration::descendant_for_position(
tree.root_node(), lines.text(),
((owner.start_line - 1).max(0) as usize, owner.start_column.max(0) as usize + 1),
((owner.start_line - 1).max(0) as usize, owner.start_column.max(0) as usize),
);
while !matches!(declaration.kind(), "type_alias" | "type_spec") {
let Some(parent) = declaration.parent() else { break; };
Expand Down
2 changes: 1 addition & 1 deletion codegraph-kernel/src/resolve/go_asserted.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ impl KernelResolver {
if !text.contains(".(") { return None; }
let tree = self.parsed_tree(&file, r)?;
let site = super::iteration::descendant_for_position(tree.root_node(), text,
((r.line - 1).max(0) as usize, r.column.max(0) as usize + 1));
((r.line - 1).max(0) as usize, r.column.max(0) as usize));
let at = site.start_byte();
let mut scope = Some(site);
while let Some(node) = scope {
Expand Down
30 changes: 27 additions & 3 deletions codegraph-kernel/src/resolve/iteration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,18 @@ fn before(a: (usize, usize), b: (usize, usize)) -> bool {
}

/// descendantForPosition (tree.ts): descend into the first child, named or
/// not, whose span contains the point, until none does.
/// not, whose span contains the point, until none does. A child must end
/// after the point, so where one token ends and the next begins (`)` then
/// `Foo` in `if(ok)Foo f;`) the next one is found: pass a token's start to
/// find that token.
pub(super) fn descendant_for_position<'t>(root: TsNode<'t>, text: &str, at: (usize, usize)) -> TsNode<'t> {
let mut node = root;
'down: loop {
let mut cursor = node.walk();
for c in node.children(&mut cursor) {
let start = point16(text, c.start_byte(), c.start_position());
let end = point16(text, c.end_byte(), c.end_position());
if before(start, at) && before(at, end) {
if before(start, at) && !before(end, at) {
node = c;
continue 'down;
}
Expand Down Expand Up @@ -266,7 +269,7 @@ impl KernelResolver {
pub(super) fn kotlin_chain_receiver_call(&mut self, r: &ResolveRefIn) -> Option<String> {
let file = self.read_file(&r.file_path)?;
let tree = self.parsed_tree(&file, r)?;
let mut node = descendant_for_position(tree.root_node(), file.text(), ((r.line - 1).max(0) as usize, r.column.max(0) as usize + 1));
let mut node = descendant_for_position(tree.root_node(), file.text(), ((r.line - 1).max(0) as usize, r.column.max(0) as usize));
if node.kind() != "simple_identifier" { return None; }
while let Some(parent) = node.parent() {
if parent.kind() == "navigation_expression" {
Expand Down Expand Up @@ -753,6 +756,27 @@ impl KernelResolver {
mod tests {
use super::kotlin_takes_trailing_lambda as trailing;

/// The node `descendant_for_position` finds at (0, `column`) of `code`.
fn token_at(code: &str, language: &str, column: usize) -> (String, String) {
let mut parser = tree_sitter::Parser::new();
parser.set_language(&crate::langs::grammar_for(language).unwrap()).unwrap();
let tree = parser.parse(code, None).unwrap();
let node = super::descendant_for_position(tree.root_node(), code, (0, column));
(node.kind().to_string(), code[node.start_byte()..node.end_byte()].to_string())
}

#[test]
fn a_point_where_one_token_ends_and_the_next_begins_finds_the_next() {
// `)` ends where the name starts; the name is the token at that point.
let code = "void g(){if(ok)Foo f;}";
assert_eq!(token_at(code, "cpp", code.find("Foo").unwrap()).1, "Foo");
let code = "void g(){if(ok)target();}";
assert_eq!(token_at(code, "cpp", code.find("target").unwrap()), ("identifier".into(), "target".into()));
// A one-character name followed by `(`: its start finds the name.
let code = "void g(){f(x);}";
assert_eq!(token_at(code, "cpp", code.find("f(").unwrap()), ("identifier".into(), "f".into()));
}

#[test]
fn a_trailing_lambda_needs_a_function_typed_last_parameter() {
for sig in [
Expand Down
Loading
Loading