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
7 changes: 6 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
{
"spinnerTipsEnabled": false,
"prefersReducedMotion": true,
"spinnerTipsEnabled": false
"permissions": {
"allow": [
"Bash(rtk gain:*)"
]
}
}
20 changes: 9 additions & 11 deletions src/main/java/io/github/randomcodespace/iq/cache/FileHasher.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,23 @@
import java.util.HexFormat;

/**
* Computes MD5 hash of file content for change detection.
* MD5 is used because it is fast and sufficient for content-change
* detection (not for cryptographic purposes).
* Computes SHA-256 hash of file content for change detection.
*/
public final class FileHasher {

private FileHasher() {
}

/**
* Compute the MD5 hex digest of a file's content.
* Compute the SHA-256 hex digest of a file's content.
*
* @param file path to the file
* @return lowercase hex MD5 hash string
* @return lowercase hex SHA-256 hash string
* @throws IOException if the file cannot be read
*/
public static String hash(Path file) throws IOException {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] buf = new byte[8192];
try (InputStream is = Files.newInputStream(file)) {
int n;
Expand All @@ -37,23 +35,23 @@
}
return HexFormat.of().formatHex(md.digest());
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MD5 not available", e);
throw new RuntimeException("SHA-256 not available", e);

Check warning on line 38 in src/main/java/io/github/randomcodespace/iq/cache/FileHasher.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace generic exceptions with specific library exceptions or a custom exception.

See more on https://sonarcloud.io/project/issues?id=RandomCodeSpace_code-iq&issues=AZ1Y7xh4iq29Pk22jrHV&open=AZ1Y7xh4iq29Pk22jrHV&pullRequest=36
}
}

/**
* Compute the MD5 hex digest of a string's content (UTF-8 bytes).
* Compute the SHA-256 hex digest of a string's content (UTF-8 bytes).
*
* @param content the string to hash
* @return lowercase hex MD5 hash string
* @return lowercase hex SHA-256 hash string
*/
public static String hashString(String content) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(content.getBytes(java.nio.charset.StandardCharsets.UTF_8));
return HexFormat.of().formatHex(md.digest());
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MD5 not available", e);
throw new RuntimeException("SHA-256 not available", e);

Check warning on line 54 in src/main/java/io/github/randomcodespace/iq/cache/FileHasher.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace generic exceptions with specific library exceptions or a custom exception.

See more on https://sonarcloud.io/project/issues?id=RandomCodeSpace_code-iq&issues=AZ1Y7xh4iq29Pk22jrHW&open=AZ1Y7xh4iq29Pk22jrHW&pullRequest=36
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package io.github.randomcodespace.iq.detector.python;

import io.github.randomcodespace.iq.detector.AbstractAntlrDetector;
import io.github.randomcodespace.iq.detector.DetectorContext;
import io.github.randomcodespace.iq.grammar.AntlrParserFactory;
import io.github.randomcodespace.iq.grammar.python.Python3Parser;
import org.antlr.v4.runtime.tree.ParseTree;

import java.util.Set;
import java.util.regex.Pattern;

/**
* Abstract base for Python ANTLR-based detectors.
* Provides shared {@link #parse(DetectorContext)} with large-file regex fallback,
* language support declaration, and Python-specific AST helpers used across
* multiple Python detectors.
*/
public abstract class AbstractPythonAntlrDetector extends AbstractAntlrDetector {

/** Matches the start of the next class definition — used to bound class bodies in regex fallback. */
protected static final Pattern NEXT_CLASS_RE = Pattern.compile("\\nclass\\s+\\w+");

@Override
public Set<String> getSupportedLanguages() {
return Set.of("python");
}

@Override
protected ParseTree parse(DetectorContext ctx) {
// Skip ANTLR for very large files (>500KB) — regex fallback is faster
if (ctx.content().length() > 500_000) {
return null;
}
return AntlrParserFactory.parse("python", ctx.content());
}

/**
* Build a comma-separated string of base class names from an ANTLR class definition context.
*
* @param classCtx the parsed class definition
* @return base class text, or null if no base classes
*/
protected static String getBaseClassesText(Python3Parser.ClassdefContext classCtx) {
if (classCtx.arglist() == null) return null;
StringBuilder sb = new StringBuilder();
for (var arg : classCtx.arglist().argument()) {
if (sb.length() > 0) sb.append(", ");

Check warning on line 47 in src/main/java/io/github/randomcodespace/iq/detector/python/AbstractPythonAntlrDetector.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "isEmpty()" to check whether a "StringBuilder" is empty or not.

See more on https://sonarcloud.io/project/issues?id=RandomCodeSpace_code-iq&issues=AZ1Y7xd-iq29Pk22jrHS&open=AZ1Y7xd-iq29Pk22jrHS&pullRequest=36
sb.append(arg.getText());
}
return sb.toString();
}

/**
* Extract the source text of an entire class body using ANTLR token positions.
*
* @param text full source content
* @param classCtx the parsed class definition
* @return substring covering the full class body
*/
protected static String extractClassBody(String text, Python3Parser.ClassdefContext classCtx) {
int start = classCtx.getStart().getStartIndex();
int stop = classCtx.getStop() != null ? classCtx.getStop().getStopIndex() + 1 : text.length();
return text.substring(Math.min(start, text.length()), Math.min(stop, text.length()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package io.github.randomcodespace.iq.detector.python;

import io.github.randomcodespace.iq.analyzer.InfraEndpoint;
import io.github.randomcodespace.iq.analyzer.InfrastructureRegistry;
import io.github.randomcodespace.iq.model.CodeEdge;
import io.github.randomcodespace.iq.model.CodeNode;
import io.github.randomcodespace.iq.model.EdgeKind;
import io.github.randomcodespace.iq.model.NodeKind;

import java.util.List;

/**
* Abstract base for Python ORM detectors that emit DATABASE_CONNECTION nodes and CONNECTS_TO edges.
* Extends {@link AbstractPythonAntlrDetector} with shared database node/edge helpers
* used by Django and SQLAlchemy detectors.
*/
public abstract class AbstractPythonDbDetector extends AbstractPythonAntlrDetector {

/**
* Ensure a DATABASE_CONNECTION node exists in the result, creating it if needed.
* Uses the first database from the InfrastructureRegistry if available,
* otherwise creates a generic "database:unknown" node.
*
* @param registry infrastructure registry (may be null)
* @param nodes the nodes list to add the DB node to if missing
* @return the database node ID
*/
protected static String ensureDbNode(InfrastructureRegistry registry, List<CodeNode> nodes) {
String dbNodeId;
if (registry != null && !registry.getDatabases().isEmpty()) {
InfraEndpoint db = registry.getDatabases().values().iterator().next();
dbNodeId = "infra:" + db.id();
if (nodes.stream().noneMatch(n -> dbNodeId.equals(n.getId()))) {
CodeNode dbNode = new CodeNode(dbNodeId, NodeKind.DATABASE_CONNECTION,
db.name() + " (" + db.type() + ")");
dbNode.getProperties().put("type", db.type());
if (db.connectionUrl() != null) dbNode.getProperties().put("url", db.connectionUrl());
nodes.add(dbNode);
}
} else {
dbNodeId = "database:unknown";
if (nodes.stream().noneMatch(n -> dbNodeId.equals(n.getId()))) {
nodes.add(new CodeNode(dbNodeId, NodeKind.DATABASE_CONNECTION, "Database"));
}
}
return dbNodeId;
}

/**
* Add a CONNECTS_TO edge from the given source node to the database node.
*
* @param sourceId the source node ID
* @param registry infrastructure registry (may be null)
* @param nodes the nodes list (used to find/create the DB node)
* @param edges the edges list to add the edge to
*/
protected static void addDbEdge(String sourceId, InfrastructureRegistry registry,
List<CodeNode> nodes, List<CodeEdge> edges) {
String dbNodeId = ensureDbNode(registry, nodes);
CodeNode targetRef = nodes.stream()
.filter(n -> dbNodeId.equals(n.getId()))
.findFirst()
.orElseGet(() -> new CodeNode(dbNodeId, NodeKind.DATABASE_CONNECTION, "Database"));
CodeEdge edge = new CodeEdge();
edge.setId(sourceId + "->connects_to->" + dbNodeId);
edge.setKind(EdgeKind.CONNECTS_TO);
edge.setSourceId(sourceId);
edge.setTarget(targetRef);
edges.add(edge);
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package io.github.randomcodespace.iq.detector.python;

import io.github.randomcodespace.iq.detector.AbstractAntlrDetector;
import io.github.randomcodespace.iq.detector.DetectorContext;
import io.github.randomcodespace.iq.detector.DetectorResult;
import io.github.randomcodespace.iq.grammar.AntlrParserFactory;
import io.github.randomcodespace.iq.grammar.python.Python3Parser;
import io.github.randomcodespace.iq.grammar.python.Python3ParserBaseListener;
import io.github.randomcodespace.iq.model.CodeEdge;
Expand Down Expand Up @@ -33,7 +31,7 @@
properties = {"broker", "task_name"}
)
@Component
public class CeleryTaskDetector extends AbstractAntlrDetector {
public class CeleryTaskDetector extends AbstractPythonAntlrDetector {

// --- Regex patterns ---
private static final Pattern TASK_DECORATOR = Pattern.compile(
Expand All @@ -56,20 +54,6 @@ public String getName() {
return "python.celery_tasks";
}

@Override
public Set<String> getSupportedLanguages() {
return Set.of("python");
}

@Override
protected ParseTree parse(DetectorContext ctx) {
// Skip ANTLR for very large files (>500KB) — regex fallback is faster
if (ctx.content().length() > 500_000) {
return null; // triggers regex fallback
}
return AntlrParserFactory.parse("python", ctx.content());
}

@Override
protected DetectorResult detectWithAst(ParseTree tree, DetectorContext ctx) {
List<CodeNode> nodes = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package io.github.randomcodespace.iq.detector.python;

import io.github.randomcodespace.iq.detector.AbstractAntlrDetector;
import io.github.randomcodespace.iq.detector.DetectorContext;
import io.github.randomcodespace.iq.detector.DetectorResult;
import io.github.randomcodespace.iq.grammar.AntlrParserFactory;
import io.github.randomcodespace.iq.grammar.python.Python3Parser;
import io.github.randomcodespace.iq.grammar.python.Python3ParserBaseListener;
import io.github.randomcodespace.iq.model.CodeNode;
Expand All @@ -15,7 +13,6 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.github.randomcodespace.iq.detector.DetectorInfo;
Expand All @@ -31,7 +28,7 @@
properties = {"auth_type", "permissions"}
)
@Component
public class DjangoAuthDetector extends AbstractAntlrDetector {
public class DjangoAuthDetector extends AbstractPythonAntlrDetector {

// --- Regex fallback patterns ---
private static final Pattern LOGIN_REQUIRED_RE = Pattern.compile("@login_required\\b");
Expand All @@ -56,20 +53,6 @@ public String getName() {
return "django_auth";
}

@Override
public Set<String> getSupportedLanguages() {
return Set.of("python");
}

@Override
protected ParseTree parse(DetectorContext ctx) {
// Skip ANTLR for very large files (>500KB) — regex fallback is faster
if (ctx.content().length() > 500_000) {
return null; // triggers regex fallback
}
return AntlrParserFactory.parse("python", ctx.content());
}

@Override
protected DetectorResult detectWithAst(ParseTree tree, DetectorContext ctx) {
List<CodeNode> nodes = new ArrayList<>();
Expand Down
Loading
Loading