44import com .github .javaparser .ast .body .ClassOrInterfaceDeclaration ;
55import com .github .javaparser .ast .body .FieldDeclaration ;
66import com .github .javaparser .ast .body .MethodDeclaration ;
7+ import com .github .javaparser .ast .body .Parameter ;
78import com .github .javaparser .ast .expr .AnnotationExpr ;
89import com .github .javaparser .ast .expr .MethodCallExpr ;
910import com .github .javaparser .ast .expr .StringLiteralExpr ;
2223import io .github .randomcodespace .iq .detector .ParserType ;
2324
2425/**
25- * Detects Kafka ConfigDef.define() configuration definitions and Spring @ConfigurationProperties
26- * using JavaParser AST with regex fallback .
26+ * Detects Kafka ConfigDef.define() configuration definitions, Spring @Value bindings,
27+ * and Spring @ConfigurationProperties class-level prefix declarations .
2728 */
2829@ DetectorInfo (
2930 name = "config_def" ,
3031 category = "config" ,
31- description = "Detects Spring @Value and @ConfigurationProperties definitions " ,
32+ description = "Detects Kafka ConfigDef definitions, Spring @Value bindings, and @ConfigurationProperties prefixes " ,
3233 parser = ParserType .JAVAPARSER ,
3334 languages = {"java" },
3435 nodeKinds = {NodeKind .CONFIG_DEFINITION },
@@ -40,6 +41,11 @@ public class ConfigDefDetector extends AbstractJavaParserDetector {
4041 // ---- Regex fallback patterns ----
4142 private static final Pattern CLASS_RE = Pattern .compile ("(?:public\\ s+)?class\\ s+(\\ w+)" );
4243 private static final Pattern DEFINE_RE = Pattern .compile ("\\ .define\\ s*\\ (\\ s*\" ([^\" ]+)\" " );
44+ private static final Pattern VALUE_RE = Pattern .compile ("@Value\\ s*\\ (\\ s*\" \\ $\\ {([^}]+)\\ }\" " );
45+ private static final Pattern CONFIG_PROPS_RE = Pattern .compile ("@ConfigurationProperties\\ s*\\ (\\ s*(?:prefix\\ s*=\\ s*)?\" ([^\" ]+)\" " );
46+
47+ private static final String VALUE_ANNOTATION = "Value" ;
48+ private static final String CONFIG_PROPS_ANNOTATION = "ConfigurationProperties" ;
4349
4450 @ Override
4551 public String getName () {
@@ -54,7 +60,13 @@ public Set<String> getSupportedLanguages() {
5460 @ Override
5561 public DetectorResult detect (DetectorContext ctx ) {
5662 String text = ctx .content ();
57- if (text == null || !text .contains ("ConfigDef" )) return DetectorResult .empty ();
63+ if (text == null ) return DetectorResult .empty ();
64+
65+ boolean hasConfigDef = text .contains ("ConfigDef" );
66+ boolean hasValue = text .contains ("@Value" );
67+ boolean hasConfigProps = text .contains ("@ConfigurationProperties" );
68+
69+ if (!hasConfigDef && !hasValue && !hasConfigProps ) return DetectorResult .empty ();
5870
5971 Optional <CompilationUnit > cu = parse (ctx );
6072 if (cu .isPresent ()) {
@@ -72,46 +84,102 @@ private DetectorResult detectWithAst(CompilationUnit cu, DetectorContext ctx) {
7284 cu .findAll (ClassOrInterfaceDeclaration .class ).forEach (classDecl -> {
7385 String className = classDecl .getNameAsString ();
7486 String classNodeId = ctx .filePath () + ":" + className ;
75-
7687 Set <String > seenKeys = new LinkedHashSet <>();
7788
78- // Find all .define() method calls in the class
89+ // 1. Kafka ConfigDef .define() calls
7990 classDecl .findAll (MethodCallExpr .class ).forEach (call -> {
8091 if (!"define" .equals (call .getNameAsString ())) return ;
8192 if (call .getArguments ().isEmpty ()) return ;
82-
8393 var firstArg = call .getArguments ().get (0 );
8494 if (!firstArg .isStringLiteralExpr ()) return ;
85-
8695 String configKey = firstArg .asStringLiteralExpr ().getValue ();
87- if (seenKeys .contains (configKey )) return ;
88- seenKeys .add (configKey );
89-
90- int line = call .getBegin ().map (p -> p .line ).orElse (1 );
91-
92- String nodeId = "config:" + configKey ;
93- CodeNode node = new CodeNode ();
94- node .setId (nodeId );
95- node .setKind (NodeKind .CONFIG_DEFINITION );
96- node .setLabel (configKey );
97- node .setFilePath (ctx .filePath ());
98- node .setLineStart (line );
99- node .getProperties ().put ("config_key" , configKey );
100- nodes .add (node );
101-
102- CodeEdge edge = new CodeEdge ();
103- edge .setId (classNodeId + "->reads_config->" + nodeId );
104- edge .setKind (EdgeKind .READS_CONFIG );
105- edge .setSourceId (classNodeId );
106- edge .setTarget (node );
107- edge .setProperties (Map .of ("config_key" , configKey ));
108- edges .add (edge );
96+ if (seenKeys .add (configKey )) {
97+ int line = call .getBegin ().map (p -> p .line ).orElse (1 );
98+ addConfigNode (configKey , "kafka_configdef" , classNodeId , ctx .filePath (), line , nodes , edges );
99+ }
100+ });
101+
102+ // 2. Spring @Value("${some.key}") on fields and method parameters
103+ classDecl .findAll (FieldDeclaration .class ).forEach (field -> {
104+ field .getAnnotations ().forEach (ann -> {
105+ if (!VALUE_ANNOTATION .equals (ann .getNameAsString ())) return ;
106+ extractValueKey (ann ).ifPresent (key -> {
107+ if (seenKeys .add (key )) {
108+ int line = ann .getBegin ().map (p -> p .line ).orElse (1 );
109+ addConfigNode (key , "spring_value" , classNodeId , ctx .filePath (), line , nodes , edges );
110+ }
111+ });
112+ });
113+ });
114+
115+ classDecl .findAll (MethodDeclaration .class ).forEach (method -> {
116+ method .getParameters ().forEach (param -> {
117+ param .getAnnotations ().forEach (ann -> {
118+ if (!VALUE_ANNOTATION .equals (ann .getNameAsString ())) return ;
119+ extractValueKey (ann ).ifPresent (key -> {
120+ if (seenKeys .add (key )) {
121+ int line = ann .getBegin ().map (p -> p .line ).orElse (1 );
122+ addConfigNode (key , "spring_value" , classNodeId , ctx .filePath (), line , nodes , edges );
123+ }
124+ });
125+ });
126+ });
127+ });
128+
129+ // 3. @ConfigurationProperties(prefix="some.prefix") on class
130+ classDecl .getAnnotations ().forEach (ann -> {
131+ if (!CONFIG_PROPS_ANNOTATION .equals (ann .getNameAsString ())) return ;
132+ extractAnnotationStringValue (ann ).ifPresent (prefix -> {
133+ if (seenKeys .add (prefix )) {
134+ int line = ann .getBegin ().map (p -> p .line ).orElse (1 );
135+ addConfigNode (prefix , "spring_config_props" , classNodeId , ctx .filePath (), line , nodes , edges );
136+ }
137+ });
109138 });
110139 });
111140
112141 return DetectorResult .of (nodes , edges );
113142 }
114143
144+ private Optional <String > extractValueKey (AnnotationExpr ann ) {
145+ // @Value("${some.key}") or @Value(value = "${some.key}")
146+ String raw = ann .toString ();
147+ Matcher m = Pattern .compile ("\" \\ $\\ {([^}]+)\\ }\" " ).matcher (raw );
148+ if (m .find ()) return Optional .of (m .group (1 ));
149+ return Optional .empty ();
150+ }
151+
152+ private Optional <String > extractAnnotationStringValue (AnnotationExpr ann ) {
153+ // @ConfigurationProperties("prefix") or @ConfigurationProperties(prefix = "prefix")
154+ String raw = ann .toString ();
155+ Matcher m = Pattern .compile ("\" ([^\" ]+)\" " ).matcher (raw );
156+ if (m .find ()) return Optional .of (m .group (1 ));
157+ return Optional .empty ();
158+ }
159+
160+ private void addConfigNode (String key , String source , String classNodeId ,
161+ String filePath , int line ,
162+ List <CodeNode > nodes , List <CodeEdge > edges ) {
163+ String nodeId = "config:" + key ;
164+ CodeNode node = new CodeNode ();
165+ node .setId (nodeId );
166+ node .setKind (NodeKind .CONFIG_DEFINITION );
167+ node .setLabel (key );
168+ node .setFilePath (filePath );
169+ node .setLineStart (line );
170+ node .getProperties ().put ("config_key" , key );
171+ node .getProperties ().put ("config_source" , source );
172+ nodes .add (node );
173+
174+ CodeEdge edge = new CodeEdge ();
175+ edge .setId (classNodeId + "->reads_config->" + nodeId );
176+ edge .setKind (EdgeKind .READS_CONFIG );
177+ edge .setSourceId (classNodeId );
178+ edge .setTarget (node );
179+ edge .setProperties (Map .of ("config_key" , key ));
180+ edges .add (edge );
181+ }
182+
115183 // ==================== Regex fallback ====================
116184
117185 private DetectorResult detectWithRegex (DetectorContext ctx ) {
@@ -131,29 +199,32 @@ private DetectorResult detectWithRegex(DetectorContext ctx) {
131199 Set <String > seenKeys = new LinkedHashSet <>();
132200
133201 for (int i = 0 ; i < lines .length ; i ++) {
202+ // Kafka ConfigDef.define()
134203 Matcher m = DEFINE_RE .matcher (lines [i ]);
135- if (!m .find ()) continue ;
136- String configKey = m .group (1 );
137- if (seenKeys .contains (configKey )) continue ;
138- seenKeys .add (configKey );
139-
140- String nodeId = "config:" + configKey ;
141- CodeNode node = new CodeNode ();
142- node .setId (nodeId );
143- node .setKind (NodeKind .CONFIG_DEFINITION );
144- node .setLabel (configKey );
145- node .setFilePath (ctx .filePath ());
146- node .setLineStart (i + 1 );
147- node .getProperties ().put ("config_key" , configKey );
148- nodes .add (node );
149-
150- CodeEdge edge = new CodeEdge ();
151- edge .setId (classNodeId + "->reads_config->" + nodeId );
152- edge .setKind (EdgeKind .READS_CONFIG );
153- edge .setSourceId (classNodeId );
154- edge .setTarget (node );
155- edge .setProperties (Map .of ("config_key" , configKey ));
156- edges .add (edge );
204+ if (m .find ()) {
205+ String configKey = m .group (1 );
206+ if (seenKeys .add (configKey )) {
207+ addConfigNode (configKey , "kafka_configdef" , classNodeId , ctx .filePath (), i + 1 , nodes , edges );
208+ }
209+ }
210+
211+ // Spring @Value("${...}")
212+ Matcher vm = VALUE_RE .matcher (lines [i ]);
213+ while (vm .find ()) {
214+ String key = vm .group (1 );
215+ if (seenKeys .add (key )) {
216+ addConfigNode (key , "spring_value" , classNodeId , ctx .filePath (), i + 1 , nodes , edges );
217+ }
218+ }
219+
220+ // Spring @ConfigurationProperties("prefix")
221+ Matcher cpm = CONFIG_PROPS_RE .matcher (lines [i ]);
222+ if (cpm .find ()) {
223+ String prefix = cpm .group (1 );
224+ if (seenKeys .add (prefix )) {
225+ addConfigNode (prefix , "spring_config_props" , classNodeId , ctx .filePath (), i + 1 , nodes , edges );
226+ }
227+ }
157228 }
158229
159230 return DetectorResult .of (nodes , edges );
0 commit comments