Skip to content
Open
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
4 changes: 3 additions & 1 deletion src/main/java/com/ibm/cldk/CodeAnalyzer.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import com.ibm.cldk.neo4j.BoltConfig;
import com.ibm.cldk.neo4j.Neo4jEmitter;
import com.ibm.cldk.schema.Analysis;
import com.ibm.cldk.schema.CanId;
import com.ibm.cldk.schema.JArtifact;
import com.ibm.cldk.schema.JDependency;
import com.ibm.cldk.schema.JEntrypointReport;
Expand Down Expand Up @@ -531,7 +532,8 @@ private void analyzeV2() throws Exception {
}
// Apply WALA L3 overlays while the dependency jars are still live (PDG/CFG need class files).
if (wala != null) {
L3WalaOverlays.apply(wala, input, modules, graphFieldDepth);
L3WalaOverlays.apply(wala, input, modules, graphFieldDepth,
CanId.applicationId(application));
}
// L4: the semantic ddg needs a WALA build regardless of --l3-engine, so build it here (or
// reuse the instance --l3-engine wala already built above) while the jars are still live.
Expand Down
31 changes: 5 additions & 26 deletions src/main/java/com/ibm/cldk/L3WalaOverlays.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,20 +66,21 @@ private L3WalaOverlays() {}
* @param input the project root directory
* @param modules the L1 module map (mutated in place: cfg/cdg/ddg are set on callables)
* @param fieldDepth the DDG access-path bound k ({@code --graph-field-depth})
* @param applicationId the {@code can://<app>} root, passed in rather than reverse-engineered
* from a module id — a module id no longer ends with its {@code symbol_table} key, since the
* id additionally carries the module's declared coordinate (see {@code ModulePrefixes})
*/
public static void apply(
WalaAnalysis wala,
String input,
Map<String, JModule> modules,
int fieldDepth) {
int fieldDepth,
String applicationId) {

if (modules.isEmpty()) {
return;
}

// Derive the applicationId from any module in the map.
String applicationId = deriveApplicationId(modules);

// Build a binary-type-name → (moduleKey, JType) index.
Map<String, TypeEntry> typeIndex = buildTypeIndex(modules);

Expand Down Expand Up @@ -362,28 +363,6 @@ private static TypeDeclaration<?> findTypeDecl(
return current;
}

// ----- applicationId derivation -------------------------------------------------------------

/**
* Derives the {@code can://<app>} applicationId from the first entry in {@code modules}.
* The module id has the form {@code applicationId/normalizedFileKey}, so strip the suffix.
*/
private static String deriveApplicationId(Map<String, JModule> modules) {
Map.Entry<String, JModule> first = modules.entrySet().iterator().next();
String moduleId = first.getValue().getId();
if (moduleId == null) {
return CanId.applicationId("unknown");
}
String normalizedFileKey = first.getKey().replace("\\", "/").replaceFirst("^[./]+", "");
int sep = moduleId.lastIndexOf("/" + normalizedFileKey);
if (sep > 0) {
return moduleId.substring(0, sep);
}
// Fallback: trim the last slash-delimited segment matching the key.
int last = moduleId.lastIndexOf('/');
return last > 0 ? moduleId.substring(0, last) : moduleId;
}

// ----- inner types --------------------------------------------------------------------------

/** One WALA method successfully joined to its L1 callable and re-parsed body block. */
Expand Down
121 changes: 121 additions & 0 deletions src/main/java/com/ibm/cldk/artifacts/ModuleCoordinates.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package com.ibm.cldk.artifacts;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
* The name a build module gives <em>itself</em> — Maven's {@code <artifactId>}, Gradle's
* {@code rootProject.name}.
*
* <p>This is the coordinate the {@code can://} module segment is built from, and the reason it is
* read from the manifest rather than taken from the directory name: a declared coordinate survives
* a rename, a relocated checkout and a differently-laid-out CI workspace, where a path-derived
* segment would make every id a function of where the tree happens to sit on disk.
*
* <p>Every method is total — a missing, malformed or hostile manifest yields
* {@link Optional#empty()} rather than throwing, because a module with no readable coordinate is a
* module that simply gets no prefix, not an analysis failure.
*/
public final class ModuleCoordinates {

private ModuleCoordinates() {}

/** {@code rootProject.name = 'x'} / {@code rootProject.name = "x"}, Groovy and Kotlin alike. */
private static final Pattern GRADLE_ROOT_NAME =
Pattern.compile("rootProject\\s*\\.\\s*name\\s*=\\s*[\"']([^\"']+)[\"']");

/**
* The coordinate declared by the manifest in {@code directory}, or empty when it declares none.
*
* <p>Maven is consulted first because a directory carrying both is a Maven module with a Gradle
* build bolted alongside far more often than the reverse.
*/
public static Optional<String> nameOf(Path directory) {
Path pom = directory.resolve("pom.xml");
if (Files.isRegularFile(pom)) {
return mavenArtifactId(pom);
}
for (String settings : new String[] {"settings.gradle", "settings.gradle.kts"}) {
Path path = directory.resolve(settings);
if (Files.isRegularFile(path)) {
return gradleRootName(path);
}
}
// A bare build.gradle deliberately yields nothing. Gradle's project name lives in the
// settings file, and falling back to the directory name here would quietly reintroduce the
// location-dependence this class exists to avoid.
return Optional.empty();
}

/**
* {@code /project/artifactId} — a <em>direct</em> child of the root element only.
*
* <p>Descending would find {@code <parent><artifactId>} and every
* {@code <dependency><artifactId>}, each of which names a different module. A pom that inherits
* its artifactId from a parent declares none of its own and correctly yields empty.
*/
private static Optional<String> mavenArtifactId(Path pom) {
try {
DocumentBuilder builder =
ManifestParsers.newSecureDocumentBuilderFactory().newDocumentBuilder();
Element root = builder.parse(new ByteArrayInputStream(Files.readAllBytes(pom)))
.getDocumentElement();
if (root == null) {
return Optional.empty();
}
NodeList children = root.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
if (child.getNodeType() != Node.ELEMENT_NODE) {
continue;
}
String name = child.getLocalName() != null ? child.getLocalName() : child.getNodeName();
if ("artifactId".equals(name)) {
return nonBlank(child.getTextContent());
}
}
return Optional.empty();
} catch (ParserConfigurationException | SAXException | IOException e) {
// Exactly the checked contract of the DOM helpers plus the read. A pom declaring a
// DOCTYPE arrives here as a SAXException, which is the categorical refusal
// ManifestParsers documents for manifests, and it means "no coordinate", not "fail".
return Optional.empty();
}
}

private static Optional<String> gradleRootName(Path settings) {
try {
Matcher matcher =
GRADLE_ROOT_NAME.matcher(Files.readString(settings, StandardCharsets.UTF_8));
return matcher.find() ? nonBlank(matcher.group(1)) : Optional.empty();
} catch (IOException | RuntimeException e) {
// RuntimeException covers the unreadable-as-UTF-8 case (MalformedInputException arrives
// wrapped) and an oversized file; neither is an analysis failure.
return Optional.empty();
}
}

private static Optional<String> nonBlank(String text) {
if (text == null || text.isBlank()) {
return Optional.empty();
}
String trimmed = text.trim();
// A coordinate is one path segment of an id. One carrying a separator would silently add a
// level to the containment path, so it is refused rather than sanitized.
return trimmed.indexOf('/') >= 0 || trimmed.indexOf('\\') >= 0
? Optional.empty()
: Optional.of(trimmed);
}
}
26 changes: 24 additions & 2 deletions src/main/java/com/ibm/cldk/syntactic_analysis/L1BuildContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,20 @@ public final class L1BuildContext {

private final String applicationId;
private final String fileKey;

/**
* The path segment the {@code can://} id is built from, which is NOT always {@link #fileKey}.
*
* <p>The key is the real {@code --input}-relative path, because uniqueness is the key's job and a
* path is unique by construction. The id path additionally carries the module's declared
* coordinate when {@link ModulePrefixes} resolved an unambiguous one
* ({@code <artifactId>/<path within that module>}), so an id names the module a file belongs to
* rather than where the tree happened to be checked out. The two coincide whenever no coordinate
* applies, which is why every convenience constructor here defaults one to the other — but
* nothing downstream may assume the key is the tail of the id.
*/
private final String idPath;

private final String source;

/** The requested analysis level; the L3 dataflow pass runs at parse time when this is {@code >= 3}. */
Expand Down Expand Up @@ -78,18 +92,26 @@ public L1BuildContext(String applicationId, String fileKey, String source, int a

public L1BuildContext(String applicationId, String fileKey, String source, int analysisLevel,
int graphFieldDepth, String l3Engine, JEntrypointReport entrypointReport) {
this(applicationId, fileKey, fileKey, source, analysisLevel, graphFieldDepth, l3Engine,
entrypointReport);
}

public L1BuildContext(String applicationId, String fileKey, String idPath, String source,
int analysisLevel, int graphFieldDepth, String l3Engine,
JEntrypointReport entrypointReport) {
this.entrypointReport = entrypointReport != null ? entrypointReport : new JEntrypointReport();
this.applicationId = applicationId;
this.fileKey = fileKey;
this.idPath = idPath != null ? idPath : fileKey;
this.source = source;
this.analysisLevel = analysisLevel;
this.graphFieldDepth = graphFieldDepth;
this.l3Engine = l3Engine != null ? l3Engine.toLowerCase(java.util.Locale.ROOT) : "ast";
}

/** The {@code can://<app>/java/<file>} id for this module. */
/** The {@code can://<app>/java/<idPath>} id for this module. */
public String moduleId() {
return CanId.moduleId(applicationId, fileKey);
return CanId.moduleId(applicationId, idPath);
}

/**
Expand Down
20 changes: 18 additions & 2 deletions src/main/java/com/ibm/cldk/syntactic_analysis/L1Extractor.java
Original file line number Diff line number Diff line change
Expand Up @@ -143,15 +143,31 @@ public static Map<String, JModule> extractAll(
String applicationId = CanId.applicationId(appName);
JavaParser parser = new JavaParser(config);
int reused = 0;

// Resolved over the WHOLE file set before any module is built: the uniqueness rule needs to
// see every claimant of a coordinate before it can decide whether to apply it.
List<Path> allFiles = new ArrayList<>();
for (SourceRoot sourceRoot : sourceRoots) {
allFiles.addAll(javaFilesUnder(sourceRoot.getRoot()));
}
Path analysisRoot = projectRoot;
ModulePrefixes prefixes = ModulePrefixes.resolve(analysisRoot, allFiles);
prefixes.contested().forEach((coordinate, dirs) -> Log.warn(
"Module coordinate '" + coordinate + "' is declared by " + dirs.size()
+ " modules (" + dirs.stream().map(d -> analysisRoot.relativize(d).toString())
.collect(java.util.stream.Collectors.joining(", "))
+ "); ids for these keep their plain relative path, since a shared coordinate"
+ " would collide them"));

for (SourceRoot sourceRoot : sourceRoots) {
for (Path path : javaFilesUnder(sourceRoot.getRoot())) {
String fileKey = fileKey(projectRoot, path);
// Read the file's own text rather than printing the AST: `span.bytes` must index the
// real file, byte for byte.
String source = Files.readString(path, StandardCharsets.UTF_8);
L1BuildContext ctx = new L1BuildContext(
applicationId, fileKey, source, analysisLevel, graphFieldDepth, l3Engine,
entrypointReport);
applicationId, fileKey, prefixes.idPath(path), source, analysisLevel,
graphFieldDepth, l3Engine, entrypointReport);

// Reuse the cached module when the file is byte-for-byte what it was last time. This
// skips the parse as well as the build, which is where the cost is.
Expand Down
Loading