mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-16 20:53:57 +08:00
Rust command attributes were not retained as decorators, so runtime entry points could appear as dead code. Preserve tauri::command metadata in both native and fallback extraction and reuse the existing decorated exclusion. Add SQLite regression coverage and native/fallback parity checks, including negative cases. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -91,6 +91,28 @@ describe.skipIf(!kernelBuilt)('kernel Rust extraction parity', () => {
|
||||
expect(viaWasm.nodes.length).toBeGreaterThanOrEqual(minNodes);
|
||||
}
|
||||
|
||||
it('preserves Tauri command attributes without treating comments or strings as registrations', () => {
|
||||
const source = `
|
||||
#[tauri::command]
|
||||
fn port() -> u16 { 4000 }
|
||||
#[tauri :: command(rename_all = "snake_case")]
|
||||
/* between attributes */
|
||||
#[allow(dead_code)]
|
||||
pub async fn settings() {}
|
||||
// #[tauri::command]
|
||||
fn ordinary() {}
|
||||
const TEXT: &str = r#"#[tauri::command]"#;
|
||||
fn after_string() {}
|
||||
#[other::command]
|
||||
fn unrelated() {}
|
||||
#[tauri::command]
|
||||
fn outer() { fn inner() {} }
|
||||
fn next() {}
|
||||
`;
|
||||
assertParity('commands.rs', source);
|
||||
assertParity('commands.rs', source.replace(/\n/g, '\r\n'));
|
||||
});
|
||||
|
||||
it('torture fixture: impl/trait quirks, use bindings, chains, fn-refs, value-refs, route macros', () => {
|
||||
const file = path.join(FIXTURE_DIR, 'torture.rs');
|
||||
assertParity('fixtures/torture.rs', fs.readFileSync(file, 'utf8'), 20);
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import CodeGraph from '../src/index';
|
||||
import { buildDeadCodeReport } from '../src/graph/dead-code';
|
||||
import { buildDeadCode } from '../src/ui-server/api/deadcode';
|
||||
|
||||
let root: string;
|
||||
let cg: CodeGraph | undefined;
|
||||
afterEach(() => {
|
||||
cg?.close();
|
||||
cg = undefined;
|
||||
if (root) fs.rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('Tauri command dead-code exclusion (#1543)', () => {
|
||||
it('counts commands as decorated entry points while retaining unannotated helpers', async () => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-tauri-dead-'));
|
||||
fs.writeFileSync(path.join(root, 'commands.rs'), `
|
||||
pub fn reached() {}
|
||||
#[tauri::command]
|
||||
fn get_mcp_port() -> u16 { 4000 }
|
||||
#[tauri :: command(rename_all = "snake_case")]
|
||||
// A comment between attributes and the function is allowed.
|
||||
#[allow(non_snake_case)]
|
||||
pub async fn readSettings() {}
|
||||
fn unused_helper() {}
|
||||
// #[tauri::command]
|
||||
fn comment_only() {}
|
||||
#[other::command]
|
||||
fn unrelated_attribute() {}
|
||||
const TEXT: &str = r#"#[tauri::command]"#;
|
||||
fn string_only() {}
|
||||
#[tauri::command]
|
||||
fn outer_command() { fn nested_helper() {} }
|
||||
fn next_function() {}
|
||||
`);
|
||||
fs.writeFileSync(path.join(root, 'main.rs'), 'mod commands;\nfn main() { commands::reached(); }\n');
|
||||
cg = CodeGraph.initSync(root);
|
||||
await cg.indexAll();
|
||||
const nodes = cg.getNodesInFile('commands.rs');
|
||||
for (const name of ['get_mcp_port', 'readSettings', 'outer_command']) {
|
||||
expect(nodes.find(n => n.name === name)?.decorators, name).toEqual(['tauri::command']);
|
||||
}
|
||||
// Rust export coverage is incomplete; includeExported exercises the actual
|
||||
// decorator rule rather than letting exportsUnknown hide the regression.
|
||||
const report = buildDeadCodeReport(cg, { includeExported: true });
|
||||
expect(report.excluded.decorated).toBe(3);
|
||||
const wire = buildDeadCode(cg, root, new URLSearchParams('exported=1'));
|
||||
expect(wire.excluded).toContainEqual({
|
||||
reason: 'decorated', count: 3, label: 'carrying a decorator, so a framework registers them',
|
||||
});
|
||||
expect(buildDeadCodeReport(cg, { includeExported: true, readSource: null }).excluded.decorated).toBe(3);
|
||||
expect(report.entries.map(e => e.node.name)).toEqual(expect.arrayContaining([
|
||||
'unused_helper', 'comment_only', 'unrelated_attribute', 'string_only', 'nested_helper', 'next_function',
|
||||
]));
|
||||
expect(report.entries.some(e => ['get_mcp_port', 'readSettings', 'outer_command'].includes(e.node.name))).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -76,6 +76,7 @@ struct Scope {
|
||||
#[derive(Default)]
|
||||
struct Extra {
|
||||
docstring: Option<String>,
|
||||
decorators: Option<String>,
|
||||
signature: Option<String>,
|
||||
return_type: Option<String>,
|
||||
qualified_name: Option<String>,
|
||||
@@ -281,6 +282,7 @@ impl<'t> Walker<'t> {
|
||||
let id_ref = self.arena.put(&id);
|
||||
let doc_ref = opt_str(&mut self.arena, extra.docstring.as_deref());
|
||||
let sig_ref = opt_str(&mut self.arena, extra.signature.as_deref());
|
||||
let decorators_ref = opt_str(&mut self.arena, extra.decorators.as_deref());
|
||||
let ret_ref = opt_str(&mut self.arena, extra.return_type.as_deref());
|
||||
let row = self.tables.push_node(&NodeRow {
|
||||
kind: node_kind_index(kind).unwrap(),
|
||||
@@ -295,7 +297,7 @@ impl<'t> Walker<'t> {
|
||||
id: id_ref,
|
||||
docstring: doc_ref,
|
||||
signature: sig_ref,
|
||||
decorators: NONE_STR,
|
||||
decorators: decorators_ref,
|
||||
type_parameters: NONE_STR,
|
||||
return_type: ret_ref,
|
||||
extra_json: NONE_STR,
|
||||
@@ -515,7 +517,31 @@ impl<'t> Walker<'t> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Match the wasm walker's sibling-attribute handling for Tauri commands.
|
||||
let mut decorators = None;
|
||||
if node.kind() == "function_item" {
|
||||
let mut sibling = node.prev_named_sibling();
|
||||
while let Some(item) = sibling {
|
||||
match item.kind() {
|
||||
"line_comment" | "block_comment" => {},
|
||||
"attribute_item" => {
|
||||
let name = item.named_child(0).and_then(|a| a.named_child(0));
|
||||
if let Some(name) = name {
|
||||
let text: String = self.src[name.byte_range()].chars()
|
||||
.filter(|c| !c.is_whitespace()).collect();
|
||||
if text == "tauri::command" {
|
||||
decorators = Some("tauri::command".to_string());
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => break,
|
||||
}
|
||||
sibling = item.prev_named_sibling();
|
||||
}
|
||||
}
|
||||
let extra = Extra {
|
||||
decorators,
|
||||
docstring: preceding_docstring(node, self.src),
|
||||
signature: self.signature_of(node),
|
||||
visibility: Some(self.visibility_of(node)),
|
||||
@@ -558,8 +584,6 @@ impl<'t> Walker<'t> {
|
||||
}
|
||||
|
||||
self.extract_type_annotations(node, row);
|
||||
// extractDecoratorsFor: rust attribute_items are siblings, not
|
||||
// decorator/annotation/attribute node types — complete no-op.
|
||||
self.stack.push(Scope { row, kind, name });
|
||||
if let Some(body) = node.child_by_field_name("body") {
|
||||
self.visit_function_body(body);
|
||||
|
||||
@@ -5313,6 +5313,21 @@ export class TreeSitterExtractor {
|
||||
* (most non-decorator-using languages), the function is a no-op.
|
||||
*/
|
||||
private extractDecoratorsFor(declNode: SyntaxNode, decoratedId: string): void {
|
||||
// Rust outer attributes are siblings, not children of the function.
|
||||
// Preserve Tauri's runtime registration for the dead-code decorator rule.
|
||||
if (this.language === 'rust' && declNode.type === 'function_item') {
|
||||
for (let sibling = declNode.previousNamedSibling; sibling; sibling = sibling.previousNamedSibling) {
|
||||
if (sibling.type === 'line_comment' || sibling.type === 'block_comment') continue;
|
||||
if (sibling.type !== 'attribute_item') break;
|
||||
const attribute = sibling.namedChild(0);
|
||||
const name = attribute?.namedChild(0)?.text.replace(/\s/g, '');
|
||||
if (name === 'tauri::command') {
|
||||
const decorated = this.nodes.find(n => n.id === decoratedId);
|
||||
if (decorated) decorated.decorators = ['tauri::command'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const consider = (n: SyntaxNode | null): void => {
|
||||
if (!n) return;
|
||||
// Solidity `modifier_invocation` (unique to that grammar) sits
|
||||
|
||||
Reference in New Issue
Block a user