Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
8be2d34
refactor(tree_path): extract LanguageConfig abstraction
xen0n Mar 9, 2026
a6076c9
feat(tree_path): add Python language support behind lang-python feature
xen0n Mar 9, 2026
26ab669
feat(tree_path): add Go language support behind lang-go feature
xen0n Mar 9, 2026
ead79cd
feat(tree_path): add JavaScript and TypeScript support
xen0n Mar 9, 2026
00d1a72
build: enable all language features by default
xen0n Mar 9, 2026
a8eeb2c
style: cargo fmt && liyi reanchor
xen0n Mar 9, 2026
9dd9d6d
fix(tree_path): remove feature gates from Language enum variants
xen0n Mar 10, 2026
c37ee06
refactor(tree_path): make LanguageConfig fields private
xen0n Mar 10, 2026
8f8b602
docs(tree_path): document Go method limitation and add TSX tests
xen0n Mar 10, 2026
84232fc
docs(tree_path): document extension collision behavior
xen0n Mar 10, 2026
6d908e6
feat(linter): make all languages built-in and add full Go support
xen0n Mar 10, 2026
6bcf01f
docs(linter): sync tree_path.rs sidecar specs after langconfig refactor
xen0n Mar 10, 2026
86b42a8
feat(linter): add tree-sitter support for 8 new languages
xen0n Mar 10, 2026
d405515
refactor(linter): split tree_path module into per-language files
xen0n Mar 10, 2026
e41d8c5
docs(design): update size claims after language expansion
xen0n Mar 10, 2026
16c41c6
style: cargo fmt + clippy fixes, reanchor sidecars
xen0n Mar 10, 2026
5286251
docs(liyi): add sidecar specs for all tree_path language modules
xen0n Mar 10, 2026
de7ea3a
docs(design): clarify that only checking, not the linter, skips parsing
xen0n Mar 10, 2026
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
132 changes: 132 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions crates/liyi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ ignore = "0.4"
regex = "1"
tree-sitter = "0.26.6"
tree-sitter-rust = "0.24.0"
tree-sitter-python = "0.25.0"
tree-sitter-go = "0.25.0"
tree-sitter-javascript = "0.25.0"
tree-sitter-typescript = "0.23.2"
tree-sitter-c = "0.24.1"
tree-sitter-cpp = "0.23.4"
tree-sitter-java = "0.23.5"
tree-sitter-c-sharp = "0.23.1"
tree-sitter-php = "0.24.2"
tree-sitter-objc = "3.0.2"
tree-sitter-kotlin-ng = "1.1.0"
tree-sitter-swift = "0.7.1"

[dev-dependencies]
proptest = "1"
Expand Down
2 changes: 1 addition & 1 deletion crates/liyi/src/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ mod tests {
fs::write(sub.join("inner.rs"), "").unwrap();
fs::write(sub.join("inner.rs.liyi.jsonc"), "{}").unwrap();

let scoped = discover(root, &[sub.clone()]);
let scoped = discover(root, std::slice::from_ref(&sub));
assert_eq!(scoped.sidecars.len(), 1);
assert_eq!(scoped.sidecars[0].repo_relative_source, "sub/inner.rs");

Expand Down
159 changes: 159 additions & 0 deletions crates/liyi/src/tree_path/lang_c.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
use super::LanguageConfig;

use tree_sitter::Node;

/// Extract the function name from a C/C++ `function_definition` node.
///
/// C/C++ functions store their name inside the `declarator` field chain:
/// `function_definition` → (field `declarator`) `function_declarator`
/// → (field `declarator`) `identifier` / `field_identifier`.
/// Pointer declarators and other wrappers may appear in the chain;
/// we unwrap them until we find a `function_declarator`.
pub(super) fn c_extract_declarator_name(node: &Node, source: &str) -> Option<String> {
let declarator = node.child_by_field_name("declarator")?;
let func_decl = unwrap_to_function_declarator(&declarator)?;
let name_node = func_decl.child_by_field_name("declarator")?;
Some(source[name_node.byte_range()].to_string())
}

/// Walk through pointer_declarator / parenthesized_declarator / attributed_declarator
/// wrappers to find the inner `function_declarator`.
fn unwrap_to_function_declarator<'a>(node: &Node<'a>) -> Option<Node<'a>> {
match node.kind() {
"function_declarator" => Some(*node),
"pointer_declarator" | "parenthesized_declarator" | "attributed_declarator" => {
let inner = node.child_by_field_name("declarator")?;
unwrap_to_function_declarator(&inner)
}
_ => None,
}
}

/// Custom name extraction for C nodes.
///
/// Handles `function_definition` (name in declarator chain) and
/// `type_definition` (name in declarator field, which is a type_identifier).
fn c_node_name(node: &Node, source: &str) -> Option<String> {
match node.kind() {
"function_definition" => c_extract_declarator_name(node, source),
"type_definition" => {
// typedef: the 'declarator' field holds the new type name
let declarator = node.child_by_field_name("declarator")?;
Some(source[declarator.byte_range()].to_string())
}
_ => None,
}
}

/// C language configuration.
pub(super) static CONFIG: LanguageConfig = LanguageConfig {
ts_language: || tree_sitter_c::LANGUAGE.into(),
extensions: &["c", "h"],
kind_map: &[
("fn", "function_definition"),
("struct", "struct_specifier"),
("enum", "enum_specifier"),
("typedef", "type_definition"),
],
name_field: "name",
name_overrides: &[],
body_fields: &["body"],
custom_name: Some(c_node_name),
};

#[cfg(test)]
mod tests {
use crate::tree_path::*;
use std::path::Path;

const SAMPLE_C: &str = r#"#include <stdio.h>

struct Point {
int x;
int y;
};

enum Color { RED, GREEN, BLUE };

typedef struct Point Point_t;

void process(int x, int y) {
printf("hello");
}

static int helper(void) {
return 42;
}
"#;

#[test]
fn resolve_c_function() {
let span = resolve_tree_path(SAMPLE_C, "fn::process", Language::C);
assert!(span.is_some(), "should resolve fn::process");
let [start, _end] = span.unwrap();
let lines: Vec<&str> = SAMPLE_C.lines().collect();
assert!(
lines[start - 1].contains("void process"),
"span should point to process function, got: {}",
lines[start - 1]
);
}

#[test]
fn resolve_c_struct() {
let span = resolve_tree_path(SAMPLE_C, "struct::Point", Language::C);
assert!(span.is_some(), "should resolve struct::Point");
let [start, _end] = span.unwrap();
let lines: Vec<&str> = SAMPLE_C.lines().collect();
assert!(
lines[start - 1].contains("struct Point"),
"span should point to Point struct"
);
}

#[test]
fn resolve_c_enum() {
let span = resolve_tree_path(SAMPLE_C, "enum::Color", Language::C);
assert!(span.is_some(), "should resolve enum::Color");
let [start, _end] = span.unwrap();
let lines: Vec<&str> = SAMPLE_C.lines().collect();
assert!(
lines[start - 1].contains("enum Color"),
"span should point to Color enum"
);
}

#[test]
fn resolve_c_typedef() {
let span = resolve_tree_path(SAMPLE_C, "typedef::Point_t", Language::C);
assert!(span.is_some(), "should resolve typedef::Point_t");
let [start, _end] = span.unwrap();
let lines: Vec<&str> = SAMPLE_C.lines().collect();
assert!(
lines[start - 1].contains("typedef"),
"span should point to typedef"
);
}

#[test]
fn compute_c_function_path() {
let span = resolve_tree_path(SAMPLE_C, "fn::process", Language::C).unwrap();
let path = compute_tree_path(SAMPLE_C, span, Language::C);
assert_eq!(path, "fn::process");
}

#[test]
fn roundtrip_c() {
for tp in &["fn::process", "fn::helper", "struct::Point", "enum::Color"] {
let span = resolve_tree_path(SAMPLE_C, tp, Language::C).unwrap();
let path = compute_tree_path(SAMPLE_C, span, Language::C);
assert_eq!(&path, tp, "roundtrip failed for {tp}");
}
}

#[test]
fn detect_c_extensions() {
assert_eq!(detect_language(Path::new("main.c")), Some(Language::C));
assert_eq!(detect_language(Path::new("header.h")), Some(Language::C));
}
}
Loading