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
27 changes: 27 additions & 0 deletions crates/codegraph-core/src/graph/classifiers/roles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,16 @@ fn classify_node(
median_fan_in: f64,
median_fan_out: f64,
) -> &'static str {
// Declarative-only language (#2385) — never subject to call-graph
// dead-code detection; there is no call graph for it by design (e.g.
// Terraform/HCL — everything is a resource/module/data/variable/output
// block, never invoked or invoking anything). Distinct from a language
// where call resolution is merely not implemented yet.
let declarative_exts = [".tf", ".hcl"];
if declarative_exts.iter().any(|ext| file.ends_with(ext)) {
return "leaf";
}

// Interface/type members (#1723) — never subject to call-graph dead-code
// detection, regardless of fan-in/fan-out/export status.
if is_type_member {
Expand Down Expand Up @@ -1796,6 +1806,23 @@ mod tests {
assert_eq!(role_of(conn, 1).as_deref(), Some("dead-unresolved"));
}

// ── Declarative-only language carve-out (#2385) ─────────────────────

#[test]
fn hcl_resource_with_zero_fan_in_is_leaf_not_dead() {
let db = setup_db();
let conn = db.conn().expect("connection should still be open");

// A Terraform resource with no incoming call edges — HCL never
// produces call edges by design, so fan_in == 0 carries no
// dead-code signal here, unlike a real function/method.
insert_node(conn, 1, "aws_kms_key.state", "resource", "main.tf", 0);

do_classify_full(conn).expect("do_classify_full should succeed");

assert_eq!(role_of(conn, 1).as_deref(), Some("leaf"));
}

#[test]
fn does_not_downgrade_when_reachable_via_a_real_call_chain() {
let db = setup_db();
Expand Down
28 changes: 28 additions & 0 deletions src/graph/classifiers/roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
* never gain inbound call edges by construction, so call-graph reachability
* doesn't apply to them either (#1723).
*
* Every node whose file belongs to a declarative-only language (currently
* just Terraform/HCL) is likewise classified `leaf` unconditionally, via
* `isDeclarativeLanguageNode` — see its doc comment (#2385).
*
* `entry` requires `kind IN ('function', 'method')` (plus the framework-prefix/
* Commander-dispatch shortcuts, which are already kind-appropriate by
* construction). An exported interface/type/constant/class with zero fan-in is
Expand Down Expand Up @@ -72,6 +76,26 @@ const TYPE_DEF_KINDS = new Set(['struct', 'enum', 'trait', 'type', 'interface',

const FFI_EXTENSIONS = new Set(['.rs', '.c', '.cpp', '.h', '.go', '.java', '.cs']);

/**
* Extensions of declarative-only languages that have no functions, classes,
* or call graph by design (e.g. Terraform/HCL — everything is a
* resource/module/data/variable/output block; nothing is ever invoked or
* invokes anything). A `fanIn === 0` reading for these carries zero
* dead-code signal, unlike a language where call resolution is merely not
* implemented yet (see the resolution-benchmark's 0.0/0.0 thresholds for
* e.g. bash/ruby/lua, which DO have real dead code a future resolver could
* find). Emitting `dead-*` anyway invited destructive action on live
* infrastructure (#2385).
*/
const DECLARATIVE_EXTENSIONS = new Set(['.tf', '.hcl']);

/** True when `node.file`'s extension belongs to a declarative-only language (#2385). */
function isDeclarativeLanguageNode(node: { file?: string }): boolean {
if (!node.file) return false;
const dotIdx = node.file.lastIndexOf('.');
return dotIdx !== -1 && DECLARATIVE_EXTENSIONS.has(node.file.slice(dotIdx));
}

/** Path patterns indicating framework-dispatched entry points. */
const ENTRY_PATH_PATTERNS: readonly RegExp[] = [
/cli[/\\]commands[/\\]/,
Expand Down Expand Up @@ -345,6 +369,10 @@ function classifyNodeRole(
medFanOut: number,
typeDefNamesByFile: Map<string, Set<string>>,
): Role {
// Declarative-only language (#2385) — never subject to call-graph dead-code
// detection; there is no call graph for it by design.
if (isDeclarativeLanguageNode(node)) return 'leaf';

// Interface/type members (#1723) — never subject to call-graph dead-code
// detection, regardless of fan-in/fan-out/export status.
if (isTypeDeclarationMember(node, typeDefNamesByFile)) return 'leaf';
Expand Down
28 changes: 28 additions & 0 deletions tests/graph/classifiers/roles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,34 @@ describe('classifyRoles', () => {
expect(roles.get('1')).toBe('dead-ffi');
});

it('classifies HCL resources as leaf, never dead-* (#2385)', () => {
// Terraform never produces call edges by design — a fanIn === 0 reading
// carries zero dead-code signal here, unlike a real function/method.
const nodes = [
{
id: '1',
name: 'aws_kms_key.state',
kind: 'resource',
file: 'main.tf',
fanIn: 0,
fanOut: 0,
isExported: false,
},
{
id: '2',
name: 'bucket_name',
kind: 'output',
file: 'outputs.hcl',
fanIn: 0,
fanOut: 0,
isExported: true,
},
];
const roles = classifyRoles(nodes);
expect(roles.get('1')).toBe('leaf');
expect(roles.get('2')).toBe('leaf');
});

it('classifies execute/validate as entry (not dead-entry) in CLI command files (#1585)', () => {
// Commander.js dispatch methods (execute, validate) in cli/commands/ are
// confirmed entry points — promoted directly to `entry` so they don't
Expand Down
Loading