fix(dead-code): exclude annotated Tauri commands (#1543) (#2019)

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:
Colby Mchenry
2026-09-27 13:10:17 +00:00
committed by GitHub
parent 9b5c8e49dd
commit 87e79c6ee8
4 changed files with 124 additions and 3 deletions
+22
View File
@@ -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);
+60
View File
@@ -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);
});
});
+27 -3
View File
@@ -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);
+15
View File
@@ -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