CallGraphHelper.java
/*
** Module : CallGraphHelper.java
** Abstract : common support routines for call graph processing
**
** Copyright (c) 2005-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- --------------------------------Description---------------------------------
** 001 GES 20170531 Created initial version, with some code being moved here from other files.
** 002 CA 20180327 Added index support and other performance optimizations.
** 003 CA 20180413 Enabled db-level cache and disabled vertex fast-property.
** Cache the vertex parents in a parent-node-type-<node-type> property to avoid
** expensive computation.
** Allow graph connection settings to be specified via cfg/callgraph.properties.
** CA 20180418 Fixed Windows OS detection.
** 004 CA 20181003 Added findCallSites - gets all the call sites parented by a given AST.
** 005 CA 20181123 Basic support for multiple classes with the same qualified names
** 006 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 007 TJD 20240208 Java 17 dependencies updates
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.uast;
import java.io.*;
import java.util.*;
import java.util.logging.*;
import com.goldencode.p2j.convert.*;
import org.apache.commons.configuration2.*;
import com.goldencode.ast.*;
import com.goldencode.graphdb.*;
import com.goldencode.p2j.cfg.Configuration;
import com.goldencode.p2j.util.*;
import com.goldencode.util.*;
import org.janusgraph.core.*;
import org.janusgraph.core.attribute.Text;
import org.janusgraph.graphdb.types.system.SystemTypeManager;
import org.apache.tinkerpop.gremlin.structure.*;
import org.apache.tinkerpop.gremlin.process.traversal.*;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.*;
import static org.apache.tinkerpop.gremlin.process.traversal.P.*;
import static org.janusgraph.core.attribute.Text.*;
/**
* Common support routines for call graph processing.
*/
public class CallGraphHelper
implements ProgressParserTokenTypes
{
/** Default folder for the callgraph DB. */
public static final String DEFAULT_CALLGRAPH_DB_FOLDER = "./callgraph/";
/** Id of the node to which ambiguous call-sites link. */
public static final long AMBIGUOUS_NODE_ID = -1;
/** Logger. */
private static final ConversionStatus LOG = ConversionStatus.get(CallGraphHelper.class);
/** Case sensitive state of the file names. */
private static Boolean caseSensitive;
/** Check if we are converting on Windows OS (current or legacy system). */
private static Boolean winOs;
/** The connected graph DB. */
private static Graph db = null;
/**
* Check if we are in a case-sensitive OS, by reading the {@code case-sensitive} configuration
* parameter.
*
* @return {@code true} if the OS filenames are case-sensitive.
*/
public static boolean isCaseSensitive()
{
if (caseSensitive == null)
{
caseSensitive =
"true".equalsIgnoreCase(Configuration.getParameter("case-sensitive", "false"));
}
return caseSensitive;
}
/**
* Check if we are converting on Windows OS or legacy sources were ran on Windows.
*
* @return See above.
*/
public static boolean isWindowsOs()
{
if (winOs == null)
{
winOs = PlatformHelper.isUnderWindowsFamily() ||
"win32".equalsIgnoreCase(Configuration.getParameter("opsys"));
}
return winOs;
}
/**
* Get the vertex property mapping the specified parent type.
*
* @param type
* The parent type.
*
* @return See above.
*/
public static String parentProperty(int type)
{
return String.format("parent-node-type-%d", type);
}
/**
* Find all call-sites parented by the specified AST.
*
* @param ast
* The parent AST.
*
* @return A map of call-site AST ids to their associated graph vertex.
*/
public static Map<Long, Vertex> findCallSites(Aast ast)
{
Map<Long, Vertex> res = new HashMap<>();
Vertex vp = CallGraphHelper.findUniqueNode("node-id", ast.getId());
if (vp == null)
{
return res;
}
int nodeType = (int) CallGraphHelper.property(vp, "node-type");
String parentLabelName = CallGraphHelper.parentProperty(nodeType);
db.traversal().V()
.has(parentLabelName, vp.id())
.has("entry-point", false)
.forEachRemaining(v ->
{
Long id = v.<Long>property("node-id").value();
res.put(id, v);
});
return res;
}
/**
* Get the first adjacent node on the given direction, with the specified type and edge
* label.
*
* @param node
* The current node.
* @param type
* The found "node-type" property.
* @param label
* The label of the edge defining the relation between the two vertices.
* @param dir
* {@code true} for out edges and {@code false} for in edges.
*
* @return First found node or {@code null} if it does not exist.
*/
public static Vertex findAdjacent(Vertex node, int type, String label, boolean dir)
{
GraphTraversal<?, Vertex> g = db.traversal().V(node);
Iterator<Vertex> iter = dir ? g.out(label).has("node-type", type).limit(1)
: g.in(label).has("node-type", type).limit(1);
return (iter.hasNext() ? iter.next() : null);
}
/**
* Return all class vertices with the qualified class name.
*
* @param className
* The qualified class name.
* @return An iterator with the found vertices.
*/
public static Iterator<Vertex> findClassNode(String className)
{
List<Vertex> res = new ArrayList<>();
Iterator<Vertex> iter = db.traversal().V().has("class-name", className);
while (iter.hasNext())
{
res.add(iter.next());
}
return res.iterator();
}
/**
* Find an unique node having a property with the given key and value.
*
* @param key
* The property key.
* @param val
* The property value.
*
* @return See above.
*
* @throws RuntimeException
* If more than one node is found.
*/
public static Vertex findUniqueNode(String key, Object val)
{
Vertex res = null;
Iterator<Vertex> iter = db.traversal().V().has(key, val).limit(2);
while (iter.hasNext())
{
Vertex v = iter.next();
if (res == null)
{
res = v;
}
else
{
final String msg = "More than one node found with key [%s] and value [%s]!";
throw new RuntimeException(String.format(msg, key, val));
}
}
return res;
}
/**
* Find a node by its type and ID.
*
* @param nodeType
* The node's type.
* @param id
* The node's ID.
*
* @return The found node or {@code null} if it does not exist.
*/
public static Vertex findNodeById(int nodeType, Object id)
{
Iterator<Vertex> iter = db.traversal().V().has("node-id", id)
.has("node-type", nodeType)
.limit(1);
return iter.hasNext() ? iter.next() : null;
}
/**
* Initialize the {@link #db graph DB} as necessary and return it.
* <p>
* The graph DB will connect to the folder specified via the {@code callgraph-db-folder}
* parameter and it will default to {@link #DEFAULT_CALLGRAPH_DB_FOLDER}, if not set.
* <p>
* The graph DB is configured for a JanusGraph implementation, backed by {@code persistit}
* storage and with {@code lucene} indexes.
*
* @return The graph DB instance associated with the call-graph.
*/
public static Graph getGraph()
{
if (db == null)
{
String folder = Configuration.getParameter("callgraph-db-folder",
DEFAULT_CALLGRAPH_DB_FOLDER);
// handle the case where the current directory is different from the project home
folder = Configuration.forceHome(folder);
org.apache.commons.configuration2.Configuration config = new BaseConfiguration();
config.setProperty("storage.directory", folder);
config.setProperty("storage.backend", "berkeleyje");
config.setProperty("index.search.backend", "lucene");
config.setProperty("index.search.directory", folder + File.separator + "searchindex");
// enable db-level cache
config.setProperty("cache.db-cache", true);
// do not pull all properties when reading a vertex
config.setProperty("query.fast-property", false);
// override properties
File propFile = new File("cfg/callgraph.properties");
if (propFile.exists())
{
Properties properties = new Properties();
try
{
properties.load(new FileReader(propFile));
}
catch (IOException e)
{
LOG.log(Level.SEVERE, "", e);
}
for (String prop : properties.stringPropertyNames())
{
config.setProperty(prop, properties.getProperty(prop));
}
}
db = GraphDB.obtainGraphDB(config);
}
return db;
}
/**
* Shutdown the graph DB.
*/
public static void shutdownGraph()
{
if (db != null)
{
JanusGraph graph = (JanusGraph) db;
graph.close();
db = null;
}
}
/**
* Prepare the procedure file name by replacing all windows-style separators with linux-style
* separators and making the name lowercase, if we are on a case-insensitive OS.
*
* @param filename
* Source file name.
*
* @return Prepared file name.
*/
public static String prepareFilename(String filename)
{
if (filename.trim().equals(""))
return null;
// TODO: if we have been provided an absolute path, these kind of files need to be marked in
// the graph, because an absolute path hard-coded in the 4GL code will not resolve well at
// runtime. all paths need to be relative to basepath
// transform to unix-like separators
if (isWindowsOs())
{
filename = filename.replaceAll("\\\\", "/");
}
if (!isCaseSensitive())
{
filename = filename.toLowerCase();
}
return filename;
}
/**
* Obtain the vertices associated with the specified AST files.
*
* @param files
* List of AST filenames to use as entry points.
* @param relative
* Specifies if the name is relative to the project home directory. If
* <code>false</code>, the name is either an absolute filename or it is relative to
* the current directory.
*/
public static List<Vertex> loadFileNodes(String[] files, boolean relative)
{
if (files == null)
{
return null;
}
List<Vertex> results = new ArrayList<>();
for (int i = 0; i < files.length; i++)
{
String filename = files[i];
if (!relative)
{
// convert to project-relative
filename = Configuration.normalizeFilename(filename);
}
// discard the .ast suffix
if (filename.endsWith(".ast"))
{
filename = filename.substring(0, filename.length() - ".ast".length());
}
else
{
LOG.log(Level.WARNING, "Non-AST entry point received:" + filename);
continue;
}
// when using physical files, search by OS filename
Iterator<Vertex> itrv = db.traversal().V().has("os-filename", filename).limit(2);
Vertex v = null;
if (itrv.hasNext())
{
v = itrv.next();
if (itrv.hasNext())
{
throw new RuntimeException("More than one file named " + filename + " found!");
}
}
else
{
LOG.log(Level.WARNING, "Entry point not found:" + filename);
continue;
}
results.add(v);
}
return results;
}
/**
* Find the parent vertex having the given type.
*
* @param node
* The node to find.
* @param type
* The parent's type.
*
* @return See above.
*/
public static Vertex findParentAstVertex(Vertex node, int type)
{
Vertex anode = node;
if ((int) property(anode, "node-type") == type)
{
return anode;
}
String parentLabelName = parentProperty(type);
Object parentId = property(anode, parentLabelName);
if (parentId != null)
{
Vertex parent = db.traversal().V(parentId).limit(1).next();
return parent;
}
int resDom = (int) property(anode, "resource-domain");
if (resDom == EVENT)
{
// in this case, get its 'contains' edge
anode = anode.edges(Direction.IN, "contains").next().outVertex();
resDom = (int) property(anode, "resource-domain");
}
if (resDom != AST_NODE && resDom != FILE_RESOURCE)
{
throw new IllegalArgumentException("Can work only with AST vertices!");
}
Iterator<Vertex> iter = db.traversal()
.V(anode)
.inE("contains")
.outV()
.limit(2);
if (iter.hasNext())
{
anode = iter.next();
if (iter.hasNext())
{
throw new IllegalStateException("Multiple CONTAINS edges found!");
}
Vertex parent = findParentAstVertex(anode, type);
if (parent != null)
{
// cache this parent ID
node.property(parentLabelName, parent.id());
}
return parent;
}
return null;
}
/**
* Find the procedure file associated with the given AST node.
*
* @param node
* The node to find.
*
* @return See above.
*/
public static Vertex findProcedureFile(Vertex node)
{
return findParentAstVertex(node, PROCEDURE_FILE);
}
/**
* Find the schema file associated with the given AST node.
*
* @param node
* The node to find.
*
* @return See above.
*/
public static Vertex findSchemaFile(Vertex node)
{
return findParentAstVertex(node, SCHEMA_FILE);
}
/**
* Get the specified property from the given vertex or edge.
*
* @param element
* The vertex or edge on which to set the property.
* @param key
* The property name.
*
* @return The property's value or <code>null</code> if is not present at the node.
*/
public static Object property(Element element, String key)
{
Property<?> prop = element.property(key);
return prop.isPresent() ? prop.value() : null;
}
/**
* Set the given property on the given vertex or edge. This is a helper function to handle
* the lack of generics support in TRPL.
*
* @param element
* The vertex or edge on which to set the property.
* @param key
* The property name.
* @param value
* The value to set.
*/
public static void property(Element element, String key, Object value)
{
if (value instanceof String)
{
element.<String>property(key, (String)value);
}
else if (value instanceof Long)
{
element.<Long>property(key, (Long)value);
}
else if (value instanceof Integer)
{
element.<Integer>property(key, (Integer)value);
}
else if (value instanceof Boolean)
{
element.<Boolean>property(key, (Boolean)value);
}
else
{
throw new RuntimeException("Missing support for " + value.getClass());
}
}
/**
* Get the vertex {@code node-key} property value.
*
* @param v
* The vertex.
*
* @return The property value.
*/
public static <T> T getNodeKeyValue(Vertex v)
{
return v.<T>property(v.<String>property("node-key").value()).value();
}
}