Skip to content

Commit 8e254b5

Browse files
aksOpsclaude
andcommitted
Fix README: resolve merge conflicts, update with current stats
97 detectors, 35 languages, 1,662 tests, 3 backends, flow generator. Complete rewrite reflecting all Phase 1-4 features. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fdd31e9 commit 8e254b5

23 files changed

Lines changed: 4272 additions & 211 deletions

README.md

Lines changed: 139 additions & 211 deletions
Large diffs are not rendered by default.
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
"""AWS CloudFormation template detector.
2+
3+
Detects CloudFormation resources, parameters, outputs, and cross-resource references.
4+
"""
5+
6+
from __future__ import annotations
7+
8+
import re
9+
from typing import Any
10+
11+
from code_intelligence.detectors.base import DetectorContext, DetectorResult
12+
from code_intelligence.models.graph import (
13+
EdgeKind,
14+
GraphEdge,
15+
GraphNode,
16+
NodeKind,
17+
SourceLocation,
18+
)
19+
20+
# Pattern for !GetAtt in text-based detection
21+
_GETATT_RE = re.compile(r"!GetAtt\s+(\w+)\.", re.MULTILINE)
22+
_REF_RE = re.compile(r"!Ref\s+(\w+)", re.MULTILINE)
23+
24+
25+
def _is_cfn_template(data: dict[str, Any]) -> bool:
26+
"""Check whether parsed data looks like a CloudFormation template."""
27+
if "AWSTemplateFormatVersion" in data:
28+
return True
29+
resources = data.get("Resources")
30+
if isinstance(resources, dict):
31+
for _key, val in resources.items():
32+
if isinstance(val, dict):
33+
rtype = val.get("Type", "")
34+
if isinstance(rtype, str) and rtype.startswith("AWS::"):
35+
return True
36+
return False
37+
38+
39+
def _get_data(ctx: DetectorContext) -> dict[str, Any] | None:
40+
"""Extract data from parsed_data for YAML or JSON types."""
41+
if not ctx.parsed_data:
42+
return None
43+
44+
ptype = ctx.parsed_data.get("type")
45+
if ptype in ("yaml", "json"):
46+
data = ctx.parsed_data.get("data")
47+
if isinstance(data, dict) and _is_cfn_template(data):
48+
return data
49+
return None
50+
51+
52+
def _collect_refs(value: Any, refs: set[str]) -> None:
53+
"""Recursively collect Ref and Fn::GetAtt references from a value tree."""
54+
if isinstance(value, dict):
55+
if "Ref" in value:
56+
ref = value["Ref"]
57+
if isinstance(ref, str):
58+
refs.add(ref)
59+
if "Fn::GetAtt" in value:
60+
getatt = value["Fn::GetAtt"]
61+
if isinstance(getatt, list) and len(getatt) >= 1:
62+
refs.add(str(getatt[0]))
63+
elif isinstance(getatt, str) and "." in getatt:
64+
refs.add(getatt.split(".")[0])
65+
for v in value.values():
66+
_collect_refs(v, refs)
67+
elif isinstance(value, list):
68+
for item in value:
69+
_collect_refs(item, refs)
70+
71+
72+
class CloudFormationDetector:
73+
"""Detects AWS CloudFormation resources, parameters, outputs, and dependencies."""
74+
75+
name: str = "cloudformation"
76+
supported_languages: tuple[str, ...] = ("yaml", "json")
77+
78+
def detect(self, ctx: DetectorContext) -> DetectorResult:
79+
result = DetectorResult()
80+
fp = ctx.file_path
81+
82+
data = _get_data(ctx)
83+
if not data:
84+
return result
85+
86+
resource_ids: set[str] = set()
87+
88+
# Process Resources
89+
resources = data.get("Resources")
90+
if isinstance(resources, dict):
91+
for logical_id, resource in sorted(resources.items()):
92+
if not isinstance(resource, dict):
93+
continue
94+
95+
resource_type = resource.get("Type", "unknown")
96+
node_id = f"cfn:{fp}:resource:{logical_id}"
97+
resource_ids.add(logical_id)
98+
99+
result.nodes.append(GraphNode(
100+
id=node_id,
101+
kind=NodeKind.INFRA_RESOURCE,
102+
label=f"{logical_id} ({resource_type})",
103+
fqn=f"cfn:{logical_id}",
104+
module=ctx.module_name,
105+
location=SourceLocation(file_path=fp),
106+
properties={
107+
"logical_id": str(logical_id),
108+
"resource_type": str(resource_type),
109+
},
110+
))
111+
112+
# Collect Ref and Fn::GetAtt references within this resource
113+
refs: set[str] = set()
114+
_collect_refs(resource, refs)
115+
# Remove self-reference
116+
refs.discard(logical_id)
117+
118+
for ref in sorted(refs):
119+
result.edges.append(GraphEdge(
120+
source=node_id,
121+
target=f"cfn:{fp}:resource:{ref}",
122+
kind=EdgeKind.DEPENDS_ON,
123+
label=f"{logical_id} -> {ref}",
124+
properties={"ref_type": "Ref/GetAtt"},
125+
))
126+
127+
# Process Parameters
128+
parameters = data.get("Parameters")
129+
if isinstance(parameters, dict):
130+
for param_name, param_def in sorted(parameters.items()):
131+
if not isinstance(param_def, dict):
132+
continue
133+
134+
param_type = param_def.get("Type", "String")
135+
default = param_def.get("Default")
136+
description = param_def.get("Description", "")
137+
138+
props: dict[str, Any] = {
139+
"param_type": str(param_type),
140+
"cfn_type": "parameter",
141+
}
142+
if default is not None:
143+
props["default"] = str(default)
144+
if description:
145+
props["description"] = str(description)
146+
147+
result.nodes.append(GraphNode(
148+
id=f"cfn:{fp}:parameter:{param_name}",
149+
kind=NodeKind.CONFIG_DEFINITION,
150+
label=f"param:{param_name}",
151+
fqn=f"cfn:param:{param_name}",
152+
module=ctx.module_name,
153+
location=SourceLocation(file_path=fp),
154+
properties=props,
155+
))
156+
157+
# Process Outputs
158+
outputs = data.get("Outputs")
159+
if isinstance(outputs, dict):
160+
for output_name, output_def in sorted(outputs.items()):
161+
if not isinstance(output_def, dict):
162+
continue
163+
164+
description = output_def.get("Description", "")
165+
props_out: dict[str, Any] = {"cfn_type": "output"}
166+
if description:
167+
props_out["description"] = str(description)
168+
169+
export = output_def.get("Export")
170+
if isinstance(export, dict) and "Name" in export:
171+
props_out["export_name"] = str(export["Name"])
172+
173+
result.nodes.append(GraphNode(
174+
id=f"cfn:{fp}:output:{output_name}",
175+
kind=NodeKind.CONFIG_DEFINITION,
176+
label=f"output:{output_name}",
177+
fqn=f"cfn:output:{output_name}",
178+
module=ctx.module_name,
179+
location=SourceLocation(file_path=fp),
180+
properties=props_out,
181+
))
182+
183+
return result
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
"""Helm chart detector for Kubernetes Helm chart patterns.
2+
3+
Detects Chart.yaml, values.yaml, and template references.
4+
"""
5+
6+
from __future__ import annotations
7+
8+
import re
9+
from typing import Any
10+
11+
from code_intelligence.detectors.base import DetectorContext, DetectorResult
12+
from code_intelligence.detectors.utils import decode_text
13+
from code_intelligence.models.graph import (
14+
EdgeKind,
15+
GraphEdge,
16+
GraphNode,
17+
NodeKind,
18+
SourceLocation,
19+
)
20+
21+
# Template value references: {{ .Values.key }}
22+
_VALUES_REF_RE = re.compile(
23+
r"\{\{\s*\.Values\.([a-zA-Z0-9_.]+)\s*\}\}", re.MULTILINE
24+
)
25+
26+
# Include helper references: {{ include "helper" }}
27+
_INCLUDE_RE = re.compile(
28+
r'\{\{-?\s*include\s+["\']([^"\']+)["\']', re.MULTILINE
29+
)
30+
31+
32+
def _get_yaml_data(ctx: DetectorContext) -> dict[str, Any] | None:
33+
"""Extract YAML data from parsed_data."""
34+
if not ctx.parsed_data:
35+
return None
36+
37+
ptype = ctx.parsed_data.get("type")
38+
if ptype == "yaml":
39+
data = ctx.parsed_data.get("data")
40+
if isinstance(data, dict):
41+
return data
42+
return None
43+
44+
45+
class HelmChartDetector:
46+
"""Detects Helm chart patterns in Chart.yaml, values.yaml, and templates."""
47+
48+
name: str = "helm_chart"
49+
supported_languages: tuple[str, ...] = ("yaml",)
50+
51+
def detect(self, ctx: DetectorContext) -> DetectorResult:
52+
result = DetectorResult()
53+
fp = ctx.file_path
54+
55+
if fp.endswith("Chart.yaml"):
56+
self._detect_chart_yaml(ctx, result)
57+
elif fp.endswith("values.yaml") and ("charts/" in fp or "helm/" in fp):
58+
self._detect_values_yaml(ctx, result)
59+
elif "/templates/" in fp and fp.endswith(".yaml"):
60+
self._detect_template(ctx, result)
61+
else:
62+
return result
63+
64+
return result
65+
66+
def _detect_chart_yaml(
67+
self, ctx: DetectorContext, result: DetectorResult
68+
) -> None:
69+
"""Parse Chart.yaml and emit MODULE + DEPENDS_ON edges."""
70+
fp = ctx.file_path
71+
data = _get_yaml_data(ctx)
72+
if not data:
73+
return
74+
75+
chart_name = data.get("name", "unknown")
76+
chart_version = data.get("version", "0.0.0")
77+
78+
chart_node_id = f"helm:{fp}:chart:{chart_name}"
79+
result.nodes.append(GraphNode(
80+
id=chart_node_id,
81+
kind=NodeKind.MODULE,
82+
label=f"helm:{chart_name}",
83+
fqn=f"helm:{chart_name}:{chart_version}",
84+
module=ctx.module_name,
85+
location=SourceLocation(file_path=fp),
86+
properties={
87+
"chart_name": str(chart_name),
88+
"chart_version": str(chart_version),
89+
"type": "helm_chart",
90+
},
91+
))
92+
93+
# Process dependencies
94+
dependencies = data.get("dependencies")
95+
if isinstance(dependencies, list):
96+
for dep in dependencies:
97+
if not isinstance(dep, dict):
98+
continue
99+
dep_name = dep.get("name", "")
100+
dep_version = dep.get("version", "")
101+
dep_repo = dep.get("repository", "")
102+
if not dep_name:
103+
continue
104+
105+
dep_node_id = f"helm:{fp}:dep:{dep_name}"
106+
result.nodes.append(GraphNode(
107+
id=dep_node_id,
108+
kind=NodeKind.MODULE,
109+
label=f"helm-dep:{dep_name}",
110+
fqn=f"helm:{dep_name}:{dep_version}",
111+
module=ctx.module_name,
112+
location=SourceLocation(file_path=fp),
113+
properties={
114+
"chart_name": str(dep_name),
115+
"chart_version": str(dep_version),
116+
"repository": str(dep_repo),
117+
"type": "helm_dependency",
118+
},
119+
))
120+
121+
result.edges.append(GraphEdge(
122+
source=chart_node_id,
123+
target=dep_node_id,
124+
kind=EdgeKind.DEPENDS_ON,
125+
label=f"{chart_name} depends on {dep_name}",
126+
properties={"version": str(dep_version)},
127+
))
128+
129+
def _detect_values_yaml(
130+
self, ctx: DetectorContext, result: DetectorResult
131+
) -> None:
132+
"""Parse values.yaml and emit CONFIG_KEY nodes for top-level keys."""
133+
fp = ctx.file_path
134+
data = _get_yaml_data(ctx)
135+
if not data:
136+
return
137+
138+
for key in sorted(data.keys()):
139+
result.nodes.append(GraphNode(
140+
id=f"helm:{fp}:value:{key}",
141+
kind=NodeKind.CONFIG_KEY,
142+
label=f"helm-value:{key}",
143+
module=ctx.module_name,
144+
location=SourceLocation(file_path=fp),
145+
properties={"helm_value": True, "key": str(key)},
146+
))
147+
148+
def _detect_template(
149+
self, ctx: DetectorContext, result: DetectorResult
150+
) -> None:
151+
"""Parse template files for .Values references and include directives."""
152+
fp = ctx.file_path
153+
text = decode_text(ctx)
154+
lines = text.split("\n")
155+
file_node_id = f"helm:{fp}:template"
156+
157+
seen_values: set[str] = set()
158+
seen_includes: set[str] = set()
159+
160+
for i, line in enumerate(lines):
161+
lineno = i + 1
162+
163+
# Detect {{ .Values.key }}
164+
for m in _VALUES_REF_RE.finditer(line):
165+
key = m.group(1)
166+
if key not in seen_values:
167+
seen_values.add(key)
168+
result.edges.append(GraphEdge(
169+
source=file_node_id,
170+
target=f"helm:values:{key}",
171+
kind=EdgeKind.READS_CONFIG,
172+
label=f"reads .Values.{key}",
173+
properties={"key": key, "line": lineno},
174+
))
175+
176+
# Detect {{ include "helper" }}
177+
for m in _INCLUDE_RE.finditer(line):
178+
helper = m.group(1)
179+
if helper not in seen_includes:
180+
seen_includes.add(helper)
181+
result.edges.append(GraphEdge(
182+
source=file_node_id,
183+
target=f"helm:helper:{helper}",
184+
kind=EdgeKind.IMPORTS,
185+
label=f"includes {helper}",
186+
properties={"helper": helper, "line": lineno},
187+
))

0 commit comments

Comments
 (0)