Codellm-Devkit (CLDK) is a multilingual program analysis framework that bridges the gap between traditional static analysis tools and Large Language Models (LLMs) specialized for code (CodeLLMs). Codellm-Devkit allows developers to streamline the process of transforming raw code into actionable insights by providing a unified interface for integrating outputs from various analysis tools and preparing them for effective use by CodeLLMs.
diff --git a/cldk/analysis/c/c_analysis.py b/cldk/analysis/c/c_analysis.py
index 8461913..12cf9d9 100644
--- a/cldk/analysis/c/c_analysis.py
+++ b/cldk/analysis/c/c_analysis.py
@@ -142,7 +142,7 @@ def get_functions(self) -> Dict[str, CFunction]:
"""Should return all functions in the project.
Raises:
- NotImplementedError: Raised when current AnalysisEngine does not support this function.
+ NotImplementedError: Raised when we do not support this function.
Returns:
Dict[str, Dict[str, JCallable]]: Dictionary of dictionaries of all methods in the C code with qualified class name as key and dictionary of methods in that class.
@@ -169,7 +169,7 @@ def get_C_file(self, file_name: str) -> str:
file_name (str): The name of the file.
Raises:
- NotImplementedError: Raised when current AnalysisEngine does not support this function.
+ NotImplementedError: Raised when we do not support this function.
Returns:
str: C file name containing the given qualified class.
@@ -183,7 +183,7 @@ def get_C_compilation_unit(self, file_path: str) -> CTranslationUnit:
file_path (str): Absolute path to C source file
Raises:
- NotImplementedError: Raised when current AnalysisEngine does not support this function.
+ NotImplementedError: Raised when we do not support this function.
Returns:
CTranslationUnit: Compilation unit object for C source file
@@ -197,7 +197,7 @@ def get_functions_in_file(self, file_name: str) -> List[CFunction]:
file_name (str): The name of the file.
Raises:
- NotImplementedError: Raised when current AnalysisEngine does not support this function.
+ NotImplementedError: Raised when we do not support this function.
Returns:
Dict[str, JCallable]: A dictionary of all constructors of the given class.
@@ -208,7 +208,7 @@ def get_macros(self) -> List[CMacro]:
"""Should return a list of all macros in the C code.
Raises:
- NotImplementedError: Raised when current AnalysisEngine does not support this function.
+ NotImplementedError: Raised when we do not support this function.
Returns:
List[CMacro]: A list of all macros in the C code.
@@ -222,7 +222,7 @@ def get_macros_in_file(self, file_name: str) -> List[CMacro] | None:
file_name (str): The name of the file.
Raises:
- NotImplementedError: Raised when current AnalysisEngine does not support this function.
+ NotImplementedError: Raised when we do not support this function.
Returns:
List[CMacro]: A list of all macros in the given file. Returns None if no macros are found.
diff --git a/cldk/analysis/common/__init__.py b/cldk/analysis/commons/__init__.py
similarity index 100%
rename from cldk/analysis/common/__init__.py
rename to cldk/analysis/commons/__init__.py
diff --git a/cldk/analysis/common/lsp/__init__.py b/cldk/analysis/commons/lsp/__init__.py
similarity index 100%
rename from cldk/analysis/common/lsp/__init__.py
rename to cldk/analysis/commons/lsp/__init__.py
diff --git a/cldk/analysis/common/lsp/lsp.py b/cldk/analysis/commons/lsp/lsp.py
similarity index 100%
rename from cldk/analysis/common/lsp/lsp.py
rename to cldk/analysis/commons/lsp/lsp.py
diff --git a/cldk/analysis/python/treesitter/__init__.py b/cldk/analysis/commons/treesitter/__init__.py
similarity index 84%
rename from cldk/analysis/python/treesitter/__init__.py
rename to cldk/analysis/commons/treesitter/__init__.py
index 7774f00..58c211a 100644
--- a/cldk/analysis/python/treesitter/__init__.py
+++ b/cldk/analysis/commons/treesitter/__init__.py
@@ -18,6 +18,7 @@
Treesitter package
"""
-from cldk.analysis.python.treesitter.python_sitter import PythonSitter
+from .treesitter_java import TreesitterJava
+from .treesitter_python import TreesitterPython
-__all__ = ["PythonSitter"]
+__all__ = ["TreesitterJava", "TreesitterPython"]
diff --git a/cldk/models/treesitter/models.py b/cldk/analysis/commons/treesitter/models.py
similarity index 100%
rename from cldk/models/treesitter/models.py
rename to cldk/analysis/commons/treesitter/models.py
diff --git a/cldk/analysis/java/treesitter/java_sitter.py b/cldk/analysis/commons/treesitter/treesitter_java.py
similarity index 99%
rename from cldk/analysis/java/treesitter/java_sitter.py
rename to cldk/analysis/commons/treesitter/treesitter_java.py
index 3eeb761..3c0033d 100644
--- a/cldk/analysis/java/treesitter/java_sitter.py
+++ b/cldk/analysis/commons/treesitter/treesitter_java.py
@@ -15,20 +15,20 @@
################################################################################
"""
-JavaSitter module
+TreesitterJava module
"""
import logging
from itertools import groupby
from typing import List, Set, Dict
from tree_sitter import Language, Node, Parser, Query, Tree
import tree_sitter_java as tsjava
-from cldk.models.treesitter import Captures
+from cldk.analysis.commons.treesitter.models import Captures
logger = logging.getLogger(__name__)
# pylint: disable=too-many-public-methods
-class JavaSitter:
+class TreesitterJava:
"""
Treesitter for Java usecases.
"""
diff --git a/cldk/analysis/python/treesitter/python_sitter.py b/cldk/analysis/commons/treesitter/treesitter_python.py
similarity index 98%
rename from cldk/analysis/python/treesitter/python_sitter.py
rename to cldk/analysis/commons/treesitter/treesitter_python.py
index dad49f0..d423ab9 100644
--- a/cldk/analysis/python/treesitter/python_sitter.py
+++ b/cldk/analysis/commons/treesitter/treesitter_python.py
@@ -15,22 +15,21 @@
################################################################################
"""
-PythonSitter module
+TreesitterPython module
"""
-import glob
import os
from pathlib import Path
from typing import List
-from tree_sitter import Language, Parser, Query, Node, Tree
+from tree_sitter import Language, Parser, Node, Tree
import tree_sitter_python as tspython
from cldk.models.python.models import PyMethod, PyClass, PyArg, PyImport, PyModule, PyCallSite
-from cldk.models.treesitter import Captures
-from cldk.utils.treesitter.tree_sitter_utils import TreeSitterUtils
+from cldk.analysis.commons.treesitter.models import Captures
+from cldk.analysis.commons.treesitter.utils.treesitter_utils import TreeSitterUtils
-class PythonSitter:
+class TreesitterPython:
"""
Tree sitter for Python use cases.
"""
@@ -49,6 +48,7 @@ def is_parsable(self, code: str) -> bool:
Returns:
True if the code is parsable, False otherwise
"""
+
def syntax_error(node):
if node.type == "ERROR":
return True
diff --git a/cldk/utils/treesitter/__init__.py b/cldk/analysis/commons/treesitter/utils/__init__.py
similarity index 90%
rename from cldk/utils/treesitter/__init__.py
rename to cldk/analysis/commons/treesitter/utils/__init__.py
index 7644d5b..2eb30b9 100644
--- a/cldk/utils/treesitter/__init__.py
+++ b/cldk/analysis/commons/treesitter/utils/__init__.py
@@ -17,3 +17,6 @@
"""
Treesitter package
"""
+from .treesitter_utils import TreeSitterUtils
+
+__all__ = ["TreeSitterUtils"]
diff --git a/cldk/utils/treesitter/tree_sitter_utils.py b/cldk/analysis/commons/treesitter/utils/treesitter_utils.py
similarity index 100%
rename from cldk/utils/treesitter/tree_sitter_utils.py
rename to cldk/analysis/commons/treesitter/utils/treesitter_utils.py
diff --git a/cldk/analysis/java/codeanalyzer/codeanalyzer.py b/cldk/analysis/java/codeanalyzer/codeanalyzer.py
index bafafb5..61d0fd2 100644
--- a/cldk/analysis/java/codeanalyzer/codeanalyzer.py
+++ b/cldk/analysis/java/codeanalyzer/codeanalyzer.py
@@ -28,7 +28,7 @@
import networkx as nx
from cldk.analysis import AnalysisLevel
-from cldk.analysis.java.treesitter import JavaSitter
+from cldk.analysis.commons.treesitter import TreesitterJava
from cldk.models.java import JGraphEdges
from cldk.models.java.enums import CRUDOperationType
from cldk.models.java.models import JApplication, JCRUDOperation, JCallable, JField, JMethodDetail, JType, JCompilationUnit, JGraphEdgesST
@@ -303,7 +303,7 @@ def _generate_call_graph(self, using_symbol_table) -> nx.DiGraph:
NotImplementedError("Call graph generation using symbol table is not implemented yet.")
else:
sdg = self.get_system_dependency_graph()
- tsu = JavaSitter()
+ tsu = TreesitterJava()
edge_list = [
(
(jge.source.method.signature, jge.source.klass),
@@ -511,6 +511,16 @@ def get_java_file(self, qualified_class_name) -> str:
if (qualified_class_name) in v.type_declarations.keys():
return k
+ def get_compilation_units(self) -> List[JCompilationUnit]:
+ """Get all the compilation units in the symbol table.
+
+ Returns:
+ List[JCompilationUnit]: A list of compilation units.
+ """
+ if self.application is None:
+ self.application = self._init_codeanalyzer()
+ return self.get_symbol_table().values()
+
def get_java_compilation_unit(self, file_path: str) -> JCompilationUnit:
"""Given the path of a Java source file, returns the compilation unit object from the symbol table.
@@ -672,7 +682,7 @@ def __call_graph_using_symbol_table(self, qualified_class_name: str, method_sign
sdg = self.__raw_call_graph_using_symbol_table_target_method(target_class_name=qualified_class_name, target_method_signature=method_signature)
else:
sdg = self.__raw_call_graph_using_symbol_table(qualified_class_name=qualified_class_name, method_signature=method_signature)
- tsu = JavaSitter()
+ tsu = TreesitterJava()
edge_list = [
(
(jge.source.method.signature, jge.source.klass),
@@ -895,7 +905,7 @@ def get_all_crud_operations(self) -> List[Dict[str, Union[JType, JCallable, List
"""Should return a dictionary of all CRUD operations in the source code.
Raises:
- NotImplementedError: Raised when current AnalysisEngine does not support this function.
+ NotImplementedError: Raised when we do not support this function.
Returns:
Dict[str, List[str]]: A dictionary of all CRUD operations in the source code.
@@ -912,7 +922,7 @@ def get_all_read_operations(self) -> List[Dict[str, Union[JType, JCallable, List
"""Should return a list of all read operations in the source code.
Raises:
- NotImplementedError: Raised when current AnalysisEngine does not support this function.
+ NotImplementedError: Raised when we do not support this function.
Returns:
List[Dict[str, Union[str, JCallable, List[CRUDOperation]]]]:: A list of all read operations in the source code.
@@ -934,7 +944,7 @@ def get_all_create_operations(self) -> List[Dict[str, Union[JType, JCallable, Li
"""Should return a list of all create operations in the source code.
Raises:
- NotImplementedError: Raised when current AnalysisEngine does not support this function.
+ NotImplementedError: Raised when we do not support this function.
Returns:
List[Dict[str, Union[str, JCallable, List[CRUDOperation]]]]: A list of all create operations in the source code.
@@ -956,7 +966,7 @@ def get_all_update_operations(self) -> List[Dict[str, Union[JType, JCallable, Li
"""Should return a list of all update operations in the source code.
Raises:
- NotImplementedError: Raised when current AnalysisEngine does not support this function.
+ NotImplementedError: Raised when we do not support this function.
Returns:
List[Dict[str, Union[str, JCallable, List[CRUDOperation]]]]: A list of all update operations in the source code.
@@ -979,7 +989,7 @@ def get_all_delete_operations(self) -> List[Dict[str, Union[JType, JCallable, Li
"""Should return a list of all delete operations in the source code.
Raises:
- NotImplementedError: Raised when current AnalysisEngine does not support this function.
+ NotImplementedError: Raised when we do not support this function.
Returns:
List[Dict[str, Union[str, JCallable, List[CRUDOperation]]]]: A list of all delete operations in the source code.
diff --git a/cldk/analysis/java/codeql/__init__.py b/cldk/analysis/java/codeql/__init__.py
deleted file mode 100644
index 0c1a3f7..0000000
--- a/cldk/analysis/java/codeql/__init__.py
+++ /dev/null
@@ -1,23 +0,0 @@
-################################################################################
-# Copyright IBM Corporation 2024
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-################################################################################
-
-"""
-CodeQL package
-"""
-
-from .codeql import JCodeQL
-
-__all__ = ["JCodeQL"]
diff --git a/cldk/analysis/java/codeql/backend.py b/cldk/analysis/java/codeql/backend.py
deleted file mode 100644
index ce287de..0000000
--- a/cldk/analysis/java/codeql/backend.py
+++ /dev/null
@@ -1,168 +0,0 @@
-################################################################################
-# Copyright IBM Corporation 2024
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-################################################################################
-
-"""
-Backend module
-"""
-
-import subprocess
-import tempfile
-from pathlib import Path
-import shlex
-from typing import List
-import pandas as pd
-from pandas import DataFrame
-
-from cldk.utils.exceptions import CodeQLQueryExecutionException
-
-
-class CodeQLQueryRunner:
- """
- A class for executing CodeQL queries against a CodeQL database.
-
- Parameters
- ----------
- database_path : str
- The path to the CodeQL database.
-
- Attributes
- ----------
- database_path : Path
- The path to the CodeQL database.
- temp_file_path : Path
- The path to the temporary query file.
- csv_output_file : Path
- The path to the CSV output file.
- temp_bqrs_file_path : Path
- The path to the temporary bqrs file.
- temp_qlpack_file : Path
- The path to the temporary qlpack file.
-
- Methods
- -------
- __enter__()
- Context entry that creates temporary files to execute a CodeQL query.
- execute(query_string, column_names)
- Writes the query to the temporary file and executes it against the specified CodeQL database.
- __exit__(exc_type, exc_val, exc_tb)
- Clean up resources used by the CodeQL analysis.
-
- Raises
- ------
- CodeQLQueryExecutionException
- If there is an error executing the query.
- """
-
- def __init__(self, database_path: str):
- self.database_path: Path = Path(database_path)
- self.temp_file_path: Path = None
-
- def __enter__(self):
- """
- Context entry that creates temporary files to execute a CodeQL query.
-
- Returns
- -------
- instance : object
- The instance of the class.
-
- Notes
- -----
- This method creates temporary files to hold the query and store their paths.
- """
-
- # Create a temporary file to hold the query and store its path
- temp_file = tempfile.NamedTemporaryFile("w", delete=False, suffix=".ql")
- csv_file = tempfile.NamedTemporaryFile("w", delete=False, suffix=".csv")
- bqrs_file = tempfile.NamedTemporaryFile("w", delete=False, suffix=".bqrs")
- self.temp_file_path = Path(temp_file.name)
- self.csv_output_file = Path(csv_file.name)
- self.temp_bqrs_file_path = Path(bqrs_file.name)
-
- # Let's close the files, we'll reopen them by path when needed.
- temp_file.close()
- bqrs_file.close()
- csv_file.close()
-
- # Create a temporary qlpack.yml file
- self.temp_qlpack_file = self.temp_file_path.parent / "qlpack.yml"
- with self.temp_qlpack_file.open("w") as f:
- f.write("name: temp\n")
- f.write("version: 1.0.0\n")
- f.write("libraryPathDependencies: codeql/java-all\n")
-
- return self
-
- def execute(self, query_string: str, column_names: List[str]) -> DataFrame:
- """Writes the query to the temporary file and executes it against the specified CodeQL database.
-
- Args:
- query_string (str): The CodeQL query string to be executed.
- column_names (List[str]): The list of column names for the CSV the CodeQL produces when we execute the query.
-
- Returns:
- dict: A dictionary containing the resulting DataFrame.
-
- Raises:
- RuntimeError: If the context manager is not entered using the 'with' statement.
- CodeQLQueryExecutionException: If there is an error executing the query.
- """
- if not self.temp_file_path:
- raise RuntimeError("Context manager not entered. Use 'with' statement.")
-
- # Write the query to the temp file so we can execute it.
- self.temp_file_path.write_text(query_string)
-
- # Construct and execute the CodeQL CLI command asking for a JSON output.
- codeql_query_cmd = shlex.split(f"codeql query run {self.temp_file_path} --database={self.database_path} --output={self.temp_bqrs_file_path}", posix=False)
-
- call = subprocess.Popen(codeql_query_cmd, stdout=None, stderr=None)
- _, err = call.communicate()
- if call.returncode != 0:
- raise CodeQLQueryExecutionException(f"Error executing query: {err.stderr}")
-
- # Convert the bqrs file to a CSV file
- bqrs2csv_command = shlex.split(f"codeql bqrs decode --format=csv --output={self.csv_output_file} {self.temp_bqrs_file_path}", posix=False)
-
- # Read the CSV file content and cast it to a DataFrame
-
- call = subprocess.Popen(bqrs2csv_command, stdout=None, stderr=None)
- _, err = call.communicate()
- if call.returncode != 0:
- raise CodeQLQueryExecutionException(f"Error executing query: {err.stderr}")
- else:
- return pd.read_csv(
- self.csv_output_file,
- header=None,
- names=column_names,
- skiprows=[0],
- )
-
- def __exit__(self, exc_type, exc_val, exc_tb):
- """
- Clean up resources used by the CodeQL analysis.
-
- Deletes the temporary files created during the analysis, including the temporary file path,
- the CSV output file, and the temporary QL pack file.
- """
- if self.temp_file_path and self.temp_file_path.exists():
- self.temp_file_path.unlink()
-
- if self.csv_output_file and self.csv_output_file.exists():
- self.csv_output_file.unlink()
-
- if self.temp_qlpack_file and self.temp_qlpack_file.exists():
- self.temp_qlpack_file.unlink()
diff --git a/cldk/analysis/java/codeql/codeql.py b/cldk/analysis/java/codeql/codeql.py
deleted file mode 100644
index 352df2e..0000000
--- a/cldk/analysis/java/codeql/codeql.py
+++ /dev/null
@@ -1,258 +0,0 @@
-################################################################################
-# Copyright IBM Corporation 2024
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-################################################################################
-
-"""
-CodeQL module
-"""
-
-from pathlib import Path
-import shlex
-import subprocess
-
-from pandas import DataFrame
-from cldk.models.java import JApplication
-from cldk.analysis.java.codeql.backend import CodeQLQueryRunner
-from tempfile import TemporaryDirectory
-import atexit
-import signal
-
-from cldk.utils.exceptions import CodeQLDatabaseBuildException
-import networkx as nx
-from typing import Union
-
-
-class JCodeQL:
- """A class for building the application view of a Java application using CodeQL.
-
- Parameters
- ----------
- project_dir : str or Path
- The path to the root of the Java project.
- codeql_db : str or Path or None
- The path to the CodeQL database. If None, a temporary directory is created to store the database.
-
- Attributes
- ----------
- db_path : Path
- The path to the CodeQL database.
-
- Methods
- -------
- _init_codeql_db(project_dir, codeql_db)
- Initializes the CodeQL database.
- _build_application_view()
- Builds the application view of the java application.
- _build_call_graph()
- Builds the call graph of the application.
- get_application_view()
- Returns the application view of the java application.
- get_class_hierarchy()
- Returns the class hierarchy of the java application.
- get_call_graph()
- Returns the call graph of the java application.
- get_all_methods()
- Returns all the methods of the java application.
- get_all_classes()
- Returns all the classes of the java application.
- """
-
- def __init__(self, project_dir: Union[str, Path], codeql_db: Union[str, Path, None]) -> None:
- self.db_path = self._init_codeql_db(project_dir, codeql_db)
-
- @staticmethod
- def _init_codeql_db(project_dir: Union[str, Path], codeql_db: Union[str, Path, None]) -> Path:
- """Should initialize the CodeQL database.
-
- Parameters
- ----------
- project_dir : str or Path
- The path to the root of the Java project.
- codeql_db : str or Path or None
- The path to the CodeQL database. If None, a temporary directory is created to store the database.
-
- Returns
- -------
- Path
- The path to the CodeQL database.
-
- Raises
- ------
- CodeQLDatabaseBuildException
- If there is an error building the CodeQL database.
- """
-
- # Cast to Path if the project_dir is a string.
- project_dir = Path(project_dir) if isinstance(project_dir, str) else project_dir
-
- # Create a codeql database. Use a temporary directory if the user doesn't specify
- if codeql_db is None:
- db_path: TemporaryDirectory = TemporaryDirectory(delete=False, ignore_cleanup_errors=True)
- codeql_db = db_path.name
- # Since the user is not providing the codeql database path, we'll destroy the database at exit.
- # TODO: this may be a potential gotcha. Is there a better solution here?
- # TODO (BACKWARD COMPATIBILITY ISSUE): Only works on 3.12.
- # If necessary, use shutil to handle this differently in 3.11 and below.
- atexit.register(lambda: db_path.cleanup())
- # Also register the cleanup function for SIGINT and SIGTERM
- signal.signal(signal.SIGINT, lambda *args, **kwargs: db_path.cleanup())
- signal.signal(signal.SIGTERM, lambda *args, **kwargs: db_path.cleanup())
-
- codeql_db_create_cmd = shlex.split(f"codeql database create {codeql_db} --source-root={project_dir} --language=java --overwrite", posix=False)
- call = subprocess.Popen(
- codeql_db_create_cmd,
- stdout=subprocess.DEVNULL,
- stderr=subprocess.PIPE,
- )
- _, error = call.communicate()
- if call.returncode != 0:
- raise CodeQLDatabaseBuildException(f"Error building CodeQL database: {error.decode()}")
- return Path(codeql_db)
-
- def _build_application_view(self) -> JApplication:
- """
- Builds the application view of the java application.
-
- Returns
- -------
- JApplication
- The JApplication object representing the application view.
- """
- application: JApplication = JApplication()
-
- # Lets build the class hierarchy tree first and store that information in the application object.
- query = []
-
- # Add import
- query += ["import java"]
-
- # List classes and their superclasses (ignoring non-application classes and anonymous classes)
- query += [
- "from Class cls",
- "where cls.fromSource() and not cls.isAnonymous()",
- "select cls, cls.getASupertype().getQualifiedName()",
- ]
-
- # Execute the query using the CodeQLQueryRunner context manager
- with CodeQLQueryRunner(self.db_path) as codeql_query:
- class_superclass_pairs: DataFrame = codeql_query.execute(
- query_string="\n".join(query),
- column_names=["class", "superclass"],
- )
-
- application.cha = self.__process_class_hierarchy_pairs_to_tree(class_superclass_pairs)
- return application
-
- @staticmethod
- def __process_class_hierarchy_pairs_to_tree(
- query_result: DataFrame,
- ) -> nx.DiGraph:
- """
- Processes the query result into a directed graph representing the class hierarchy of the application.
-
- Parameters
- ----------
- query_result : DataFrame
- The result of the class hierarchy query.
-
- Returns
- -------
- nx.DiGraph
- A directed graph representing the class hierarchy of the application.
- """
- return nx.from_pandas_edgelist(query_result, "class", "superclass", create_using=nx.DiGraph())
-
- def _build_call_graph(self) -> nx.DiGraph:
- """Builds the call graph of the application.
-
- Returns
- -------
- nx.DiGraph
- A directed graph representing the call graph of the application.
- """
- query = []
-
- # Add import
- query += ["import java"]
-
- # Add Call edges between caller and callee and filter to only capture application methods.
- query += [
- "from Method caller, Method callee",
- "where",
- "caller.fromSource() and",
- "callee.fromSource() and",
- "caller.calls(callee)",
- "select",
- ]
-
- # Caller metadata
- query += [
- "caller.getFile().getAbsolutePath(),",
- '"[" + caller.getBody().getLocation().getStartLine() + ", " + caller.getBody().getLocation().getEndLine() + "]", //Caller body slice indices',
- "caller.getQualifiedName(), // Caller's fullsignature",
- "caller.getAModifier(), // caller's method modifier",
- "caller.paramsString(), // caller's method parameter types",
- "caller.getReturnType().toString(), // Caller's return type",
- "caller.getDeclaringType().getQualifiedName(), // Caller's class",
- "caller.getDeclaringType().getAModifier(), // Caller's class modifier",
- ]
-
- # Callee metadata
- query += [
- "callee.getFile().getAbsolutePath(),",
- '"[" + callee.getBody().getLocation().getStartLine() + ", " + callee.getBody().getLocation().getEndLine() + "]", //Caller body slice indices',
- "callee.getQualifiedName(), // Caller's fullsignature",
- "callee.getAModifier(), // callee's method modifier",
- "callee.paramsString(), // callee's method parameter types",
- "callee.getReturnType().toString(), // Caller's return type",
- "callee.getDeclaringType().getQualifiedName(), // Caller's class",
- "callee.getDeclaringType().getAModifier() // Caller's class modifier",
- ]
-
- query_string = "\n".join(query)
-
- # Execute the query using the CodeQLQueryRunner context manager
- with CodeQLQueryRunner(self.db_path) as query:
- query_result: DataFrame = query.execute(
- query_string,
- column_names=[
- # Caller Columns
- "caller_file",
- "caller_body_slice_index",
- "caller_signature",
- "caller_modifier",
- "caller_params",
- "caller_return_type",
- "caller_class_signature",
- "caller_class_modifier",
- # Callee Columns
- "callee_file",
- "callee_body_slice_index",
- "callee_signature",
- "callee_modifier",
- "callee_params",
- "callee_return_type",
- "callee_class_signature",
- "callee_class_modifier",
- ],
- )
-
- # Process the query results into JMethod instances
- callgraph: nx.DiGraph = self.__process_call_edges_to_callgraph(query_result)
- return callable
-
- @staticmethod
- def __process_call_edges_to_callgraph(query_result: DataFrame) -> nx.DiGraph:
- pass
diff --git a/cldk/analysis/java/java_analysis.py b/cldk/analysis/java/java_analysis.py
index a95557f..54b9679 100644
--- a/cldk/analysis/java/java_analysis.py
+++ b/cldk/analysis/java/java_analysis.py
@@ -19,19 +19,17 @@
"""
from pathlib import Path
-from typing import Any, Dict, List, Tuple, Set, Union
+from typing import Dict, List, Tuple, Set, Union
import networkx as nx
from tree_sitter import Tree
-from cldk.analysis import SymbolTable, CallGraph, AnalysisLevel
-from cldk.analysis.java.treesitter import JavaSitter
+from cldk.analysis import SymbolTable, CallGraph
+from cldk.analysis.commons.treesitter import TreesitterJava
from cldk.models.java import JCallable
from cldk.models.java import JApplication
from cldk.models.java.models import JCRUDOperation, JCompilationUnit, JMethodDetail, JType, JField
from cldk.analysis.java.codeanalyzer import JCodeanalyzer
-from cldk.analysis.java.codeql import JCodeQL
-from cldk.utils.analysis_engine import AnalysisEngine
class JavaAnalysis(SymbolTable, CallGraph):
@@ -40,7 +38,6 @@ def __init__(
self,
project_dir: str | Path | None,
source_code: str | None,
- analysis_backend: str,
analysis_backend_path: str | None,
analysis_json_path: str | Path | None,
analysis_level: str,
@@ -53,7 +50,6 @@ def __init__(
Args:
project_dir (str | Path | None): The directory path of the project.
source_code (str | None): Java file for single source file analysis.
- analysis_backend (str): The analysis_backend used for analysis. Currently 'codeql' and 'codeanalyzer' are supported.
analysis_backend_path (str | None): The path to the analysis_backend, defaults to None and in the case of codeql, it is assumed that the cli is installed and available in the PATH. In the case of codeanalyzer the codeanalyzer.jar is downloaded from the lastest release.
analysis_json_path (str | Path | None): The path save the to the analysis database (analysis.json), defaults to None. If None, the analysis database is not persisted.
analysis_level (str): Analysis level (symbol-table, call-graph)
@@ -75,24 +71,19 @@ def __init__(
self.analysis_backend_path = analysis_backend_path
self.eager_analysis = eager_analysis
self.use_graalvm_binary = use_graalvm_binary
- self.analysis_backend = analysis_backend
self.target_files = target_files
+ self.treesitter_java: TreesitterJava = TreesitterJava()
# Initialize the analysis analysis_backend
- if analysis_backend.lower() == "codeql":
- self.analysis_backend: JCodeQL = JCodeQL(self.project_dir, self.analysis_json_path)
- elif analysis_backend.lower() == "codeanalyzer":
- self.backend: JCodeanalyzer = JCodeanalyzer(
- project_dir=self.project_dir,
- source_code=self.source_code,
- eager_analysis=self.eager_analysis,
- analysis_level=self.analysis_level,
- analysis_json_path=self.analysis_json_path,
- use_graalvm_binary=self.use_graalvm_binary,
- analysis_backend_path=self.analysis_backend_path,
- target_files=self.target_files,
- )
- else:
- raise NotImplementedError(f"Support for {analysis_backend} has not been implemented yet.")
+ self.backend: JCodeanalyzer = JCodeanalyzer(
+ project_dir=self.project_dir,
+ source_code=self.source_code,
+ eager_analysis=self.eager_analysis,
+ analysis_level=self.analysis_level,
+ analysis_json_path=self.analysis_json_path,
+ use_graalvm_binary=self.use_graalvm_binary,
+ analysis_backend_path=self.analysis_backend_path,
+ target_files=self.target_files,
+ )
def get_imports(self) -> List[str]:
"""Should return all the imports in the source code.
@@ -159,15 +150,7 @@ def get_compilation_units(self) -> List[JCompilationUnit]:
Returns:
List[JCompilationUnit]: Compilation units of the Java code.
"""
-
- # TODO: This code is broken:
- # JCodeanalyzer does not have a get_compilation_units() method
- # Commenting out until implemented
-
- # if self.analysis_backend in [AnalysisEngine.CODEQL, AnalysisEngine.TREESITTER]:
- # raise NotImplementedError("Support for this functionality has not been implemented yet.")
- # return self.backend.get_compilation_units()
- raise NotImplementedError("Support for this functionality has not been implemented yet.")
+ return self.backend.get_compilation_units()
def get_class_hierarchy(self) -> nx.DiGraph:
"""Should return class hierarchy of the java code.
@@ -179,8 +162,6 @@ def get_class_hierarchy(self) -> nx.DiGraph:
nx.DiGraph: The class hierarchy of the Java code.
"""
- if self.backend in [AnalysisEngine.CODEQL, AnalysisEngine.TREESITTER]:
- raise NotImplementedError("Support for this functionality has not been implemented yet.")
raise NotImplementedError("Class hierarchy is not implemented yet.")
def is_parsable(self, source_code: str) -> bool:
@@ -192,7 +173,7 @@ def is_parsable(self, source_code: str) -> bool:
Returns:
True if the code is parsable, False otherwise
"""
- return JavaSitter().is_parsable(source_code)
+ return self.treesitter_java.is_parsable(source_code)
def get_raw_ast(self, source_code: str) -> Tree:
"""
@@ -203,7 +184,7 @@ def get_raw_ast(self, source_code: str) -> Tree:
Returns:
Tree: the raw AST
"""
- return JavaSitter().get_raw_ast(source_code)
+ return self.treesitter_java.get_raw_ast(source_code)
def get_call_graph(self) -> nx.DiGraph:
"""Should return the call graph of the Java code.
@@ -267,26 +248,22 @@ def get_methods(self) -> Dict[str, Dict[str, JCallable]]:
"""Should return all methods in the Java code.
Raises:
- NotImplementedError: Raised when current AnalysisEngine does not support this function.
+ NotImplementedError: Raised when we do not support this function.
Returns:
Dict[str, Dict[str, JCallable]]: Dictionary of dictionaries of all methods in the Java code with qualified class name as key and dictionary of methods in that class.
"""
- if self.analysis_backend in [AnalysisEngine.CODEQL, AnalysisEngine.TREESITTER]:
- raise NotImplementedError("Support for this functionality has not been implemented yet.")
return self.backend.get_all_methods_in_application()
def get_classes(self) -> Dict[str, JType]:
"""Should return all classes in the Java code.
Raises:
- NotImplementedError: Raised when current AnalysisEngine does not support this function.
+ NotImplementedError: Raised when we do not support this function.
Returns:
Dict[str, JType]: A dictionary of all classes in the Java code, with qualified class names as keys.
"""
- if self.analysis_backend in [AnalysisEngine.CODEQL, AnalysisEngine.TREESITTER]:
- raise NotImplementedError("Support for this functionality has not been implemented yet.")
return self.backend.get_all_classes()
def get_classes_by_criteria(self, inclusions=None, exclusions=None) -> Dict[str, JType]:
@@ -297,15 +274,11 @@ def get_classes_by_criteria(self, inclusions=None, exclusions=None) -> Dict[str,
exclusions (List, optional): exclusion criteria for the classes. Defaults to None.
Raises:
- NotImplementedError: Raised when current AnalysisEngine does not support this function.
+ NotImplementedError: Raised when we do not support this function.
Returns:
Dict[str, JType]: A dict of all classes in the Java code, with qualified class names as keys
"""
-
- if self.analysis_backend in [AnalysisEngine.CODEQL, AnalysisEngine.TREESITTER]:
- raise NotImplementedError("Support for this functionality has not been implemented yet.")
-
if exclusions is None:
exclusions = []
if inclusions is None:
@@ -332,14 +305,12 @@ def get_class(self, qualified_class_name: str) -> JType:
qualified_class_name (str): The qualified name of the class.
Raises:
- NotImplementedError: Raised when current AnalysisEngine does not support this function.
+ NotImplementedError: Raised when we do not support this function.
Returns:
JType: Class object for the given qualified class name.
"""
- if self.analysis_backend in [AnalysisEngine.CODEQL, AnalysisEngine.TREESITTER]:
- raise NotImplementedError("Support for this functionality has not been implemented yet.")
return self.backend.get_class(qualified_class_name)
def get_method(self, qualified_class_name: str, qualified_method_name: str) -> JCallable:
@@ -350,13 +321,11 @@ def get_method(self, qualified_class_name: str, qualified_method_name: str) -> J
qualified_method_name (str): The qualified name of the method.
Raises:
- NotImplementedError: Raised when current AnalysisEngine does not support this function.
+ NotImplementedError: Raised when we do not support this function.
Returns:
JCallable: A method for the given qualified method name.
"""
- if self.analysis_backend in [AnalysisEngine.CODEQL, AnalysisEngine.TREESITTER]:
- raise NotImplementedError("Support for this functionality has not been implemented yet.")
return self.backend.get_method(qualified_class_name, qualified_method_name)
def get_java_file(self, qualified_class_name: str) -> str:
@@ -366,13 +335,11 @@ def get_java_file(self, qualified_class_name: str) -> str:
qualified_class_name (str): The qualified name of the class.
Raises:
- NotImplementedError: Raised when current AnalysisEngine does not support this function.
+ NotImplementedError: Raised when we do not support this function.
Returns:
str: Java file name containing the given qualified class.
"""
- if self.analysis_backend in [AnalysisEngine.CODEQL, AnalysisEngine.TREESITTER]:
- raise NotImplementedError("Support for this functionality has not been implemented yet.")
return self.backend.get_java_file(qualified_class_name)
def get_java_compilation_unit(self, file_path: str) -> JCompilationUnit:
@@ -382,13 +349,11 @@ def get_java_compilation_unit(self, file_path: str) -> JCompilationUnit:
file_path (str): Absolute path to Java source file
Raises:
- NotImplementedError: Raised when current AnalysisEngine does not support this function.
+ NotImplementedError: Raised when we do not support this function.
Returns:
JCompilationUnit: Compilation unit object for Java source file
"""
- if self.analysis_backend in [AnalysisEngine.CODEQL, AnalysisEngine.TREESITTER]:
- raise NotImplementedError("Support for this functionality has not been implemented yet.")
return self.backend.get_java_compilation_unit(file_path)
def get_methods_in_class(self, qualified_class_name) -> Dict[str, JCallable]:
@@ -398,13 +363,11 @@ def get_methods_in_class(self, qualified_class_name) -> Dict[str, JCallable]:
qualified_class_name (str): qualified class name
Raises:
- NotImplementedError: Raised when current AnalysisEngine does not support this function.
+ NotImplementedError: Raised when we do not support this function.
Returns:
Dict[str, JCallable]: A dictionary of all constructors of the given class.
"""
- if self.analysis_backend in [AnalysisEngine.CODEQL, AnalysisEngine.TREESITTER]:
- raise NotImplementedError("Support for this functionality has not been implemented yet.")
return self.backend.get_all_methods_in_class(qualified_class_name)
def get_constructors(self, qualified_class_name) -> Dict[str, JCallable]:
@@ -414,13 +377,11 @@ def get_constructors(self, qualified_class_name) -> Dict[str, JCallable]:
qualified_class_name (str): qualified class name
Raises:
- NotImplementedError: Raised when current AnalysisEngine does not support this function.
+ NotImplementedError: Raised when we do not support this function.
Returns:
Dict[str, JCallable]: A dictionary of all constructors of the given class.
"""
- if self.analysis_backend in [AnalysisEngine.CODEQL, AnalysisEngine.TREESITTER]:
- raise NotImplementedError("Support for this functionality has not been implemented yet.")
return self.backend.get_all_constructors(qualified_class_name)
def get_fields(self, qualified_class_name) -> List[JField]:
@@ -430,13 +391,11 @@ def get_fields(self, qualified_class_name) -> List[JField]:
qualified_class_name (str): qualified class name
Raises:
- NotImplementedError: Raised when current AnalysisEngine does not support this function.
+ NotImplementedError: Raised when we do not support this function.
Returns:
List[JField]: A list of all fields of the given class.
"""
- if self.analysis_backend in [AnalysisEngine.CODEQL, AnalysisEngine.TREESITTER]:
- raise NotImplementedError("Support for this functionality has not been implemented yet.")
return self.backend.get_all_fields(qualified_class_name)
def get_nested_classes(self, qualified_class_name) -> List[JType]:
@@ -446,13 +405,11 @@ def get_nested_classes(self, qualified_class_name) -> List[JType]:
qualified_class_name (str): qualified class name
Raises:
- NotImplementedError: Raised when current AnalysisEngine does not support this function.
+ NotImplementedError: Raised when we do not support this function.
Returns:
List[JType]: A list of nested classes for the given class.
"""
- if self.analysis_backend in [AnalysisEngine.CODEQL, AnalysisEngine.TREESITTER]:
- raise NotImplementedError("Support for this functionality has not been implemented yet.")
return self.backend.get_all_nested_classes(qualified_class_name)
def get_sub_classes(self, qualified_class_name) -> Dict[str, JType]:
@@ -472,13 +429,11 @@ def get_extended_classes(self, qualified_class_name) -> List[str]:
qualified_class_name (str): The qualified name of the class.
Raises:
- NotImplementedError: Raised when current AnalysisEngine does not support this function.
+ NotImplementedError: Raised when we do not support this function.
Returns:
List[str]: A list of extended classes for the given class.
"""
- if self.analysis_backend in [AnalysisEngine.CODEQL, AnalysisEngine.TREESITTER]:
- raise NotImplementedError("Support for this functionality has not been implemented yet.")
return self.backend.get_extended_classes(qualified_class_name)
def get_implemented_interfaces(self, qualified_class_name: str) -> List[str]:
@@ -488,13 +443,11 @@ def get_implemented_interfaces(self, qualified_class_name: str) -> List[str]:
qualified_class_name (str): The qualified name of the class.
Raises:
- NotImplementedError: Raised when current AnalysisEngine does not support this function.
+ NotImplementedError: Raised when we do not support this function.
Returns:
List[str]: A list of implemented interfaces for the given class.
"""
- if self.analysis_backend in [AnalysisEngine.CODEQL, AnalysisEngine.TREESITTER]:
- raise NotImplementedError("Support for this functionality has not been implemented yet.")
return self.backend.get_implemented_interfaces(qualified_class_name)
def __get_class_call_graph_using_symbol_table(self, qualified_class_name: str, method_signature: str | None = None) -> (List)[Tuple[JMethodDetail, JMethodDetail]]:
@@ -505,13 +458,11 @@ def __get_class_call_graph_using_symbol_table(self, qualified_class_name: str, m
method_signature (str | None, optional): The signature of the method in the class.. Defaults to None.
Raises:
- NotImplementedError: Raised when current AnalysisEngine does not support this function.
+ NotImplementedError: Raised when we do not support this function.
Returns:
List[Tuple[JMethodDetail, JMethodDetail]]: An edge list of the call graph for the given class and method.
"""
- if self.analysis_backend in [AnalysisEngine.CODEQL, AnalysisEngine.TREESITTER]:
- raise NotImplementedError("Support for this functionality has not been implemented yet.")
return self.backend.get_class_call_graph_using_symbol_table(qualified_class_name, method_signature)
def get_class_call_graph(self, qualified_class_name: str, method_signature: str | None = None, using_symbol_table: bool = False) -> List[Tuple[JMethodDetail, JMethodDetail]]:
@@ -523,48 +474,42 @@ def get_class_call_graph(self, qualified_class_name: str, method_signature: str
using_symbol_table (bool, optional): Generate call graph using symbol table. Defaults to False.
Raises:
- NotImplementedError: Raised when current AnalysisEngine does not support this function.
+ NotImplementedError: Raised when we do not support this function.
Returns:
List[Tuple[JMethodDetail, JMethodDetail]]: An edge list of the call graph for the given class and method.
"""
if using_symbol_table:
return self.__get_class_call_graph_using_symbol_table(qualified_class_name=qualified_class_name, method_signature=method_signature)
- if self.analysis_backend in [AnalysisEngine.CODEQL, AnalysisEngine.TREESITTER]:
- raise NotImplementedError("Support for this functionality has not been implemented yet.")
return self.backend.get_class_call_graph(qualified_class_name, method_signature)
def get_entry_point_classes(self) -> Dict[str, JType]:
"""Should return a dictionary of all entry point classes in the Java code.
Raises:
- NotImplementedError: Raised when current AnalysisEngine does not support this function.
+ NotImplementedError: Raised when we do not support this function.
Returns:
Dict[str, JType]: A dict of all entry point classes in the Java code, with qualified class names as keys
"""
- if self.analysis_backend in [AnalysisEngine.CODEQL, AnalysisEngine.TREESITTER]:
- raise NotImplementedError("Support for this functionality has not been implemented yet.")
return self.backend.get_all_entry_point_classes()
def get_entry_point_methods(self) -> Dict[str, Dict[str, JCallable]]:
"""Should return a dictionary of all entry point methods in the Java code with qualified class name as key and dictionary of methods in that class as value
Raises:
- NotImplementedError: Raised when current AnalysisEngine does not support this function.
+ NotImplementedError: Raised when we do not support this function.
Returns:
Dict[str, Dict[str, JCallable]]: A dictionary of dictionaries of entry point methods in the Java code.
"""
- if self.analysis_backend in [AnalysisEngine.CODEQL, AnalysisEngine.TREESITTER]:
- raise NotImplementedError("Support for this functionality has not been implemented yet.")
return self.backend.get_all_entry_point_methods()
def remove_all_comments(self) -> str:
"""Remove all comments from the source code.
Raises:
- NotImplementedError: Raised when current AnalysisEngine does not support this function.
+ NotImplementedError: Raised when we do not support this function.
Returns:
str: The source code with all comments removed.
@@ -578,7 +523,7 @@ def get_methods_with_annotations(self, annotations: List[str]) -> Dict[str, List
annotations (List[str]): List of annotation strings.
Raises:
- NotImplementedError: Raised when current AnalysisEngine does not support this function.
+ NotImplementedError: Raised when we do not support this function.
Returns:
Dict[str, List[Dict]]: Dictionary with annotations as keys and a list of dictionaries containing method names and bodies, as values.
@@ -587,21 +532,16 @@ def get_methods_with_annotations(self, annotations: List[str]) -> Dict[str, List
raise NotImplementedError("Support for this functionality has not been implemented yet.")
def get_test_methods(self) -> Dict[str, str]:
- """Should return a dictionary of method names and method bodies.
-
- Args:
- source_class_code (str): String containing code for a java class.
+ """Should return a dictionary of method names and method bodies
Raises:
- NotImplementedError: Raised when current AnalysisEngine does not support this function.
+ NotImplementedError: Raised when we do not support this function.
Returns:
Dict[str, str]: Dictionary of method names and method bodies.
"""
- if self.analysis_backend in [AnalysisEngine.CODEQL, AnalysisEngine.CODEANALYZER]:
- raise NotImplementedError("Support for this functionality has not been implemented yet.")
- return self.backend.get_test_methods(self.source_code)
+ return self.treesitter_java.get_test_methods(source_class_code=self.source_code)
def get_calling_lines(self, target_method_name: str) -> List[int]:
"""Should return a list of line numbers in source method block where target method is called.
@@ -610,7 +550,7 @@ def get_calling_lines(self, target_method_name: str) -> List[int]:
target_method_name (str): target method name.
Raises:
- NotImplementedError: Raised when current AnalysisEngine does not support this function.
+ NotImplementedError: Raised when we do not support this function.
Returns:
List[int]: List of line numbers within in source method code block.
diff --git a/cldk/analysis/java/treesitter/__init__.py b/cldk/analysis/java/treesitter/__init__.py
deleted file mode 100644
index 8026f75..0000000
--- a/cldk/analysis/java/treesitter/__init__.py
+++ /dev/null
@@ -1,23 +0,0 @@
-################################################################################
-# Copyright IBM Corporation 2024
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-################################################################################
-
-"""
-Treesitter package
-"""
-
-from cldk.analysis.java.treesitter.java_sitter import JavaSitter
-
-__all__ = ["JavaSitter"]
diff --git a/cldk/analysis/python/python_analysis.py b/cldk/analysis/python/python_analysis.py
index 2c9f460..7a0697f 100644
--- a/cldk/analysis/python/python_analysis.py
+++ b/cldk/analysis/python/python_analysis.py
@@ -22,7 +22,7 @@
from typing import List
from cldk.analysis import SymbolTable
-from cldk.analysis.python.treesitter import PythonSitter
+from cldk.analysis.commons.treesitter import TreesitterPython
from cldk.models.python.models import PyMethod, PyImport, PyModule, PyClass
@@ -31,7 +31,6 @@ class PythonAnalysis(SymbolTable):
def __init__(
self,
- analysis_backend: str,
eager_analysis: bool,
project_dir: str | Path | None,
source_code: str | None,
@@ -45,16 +44,7 @@ def __init__(
self.analysis_backend_path = analysis_backend_path
self.eager_analysis = eager_analysis
self.use_graalvm_binary = use_graalvm_binary
-
- # Initialize the analysis analysis_backend
- if analysis_backend.lower() == "codeql":
- raise NotImplementedError("Support for {analysis_backend} has not been implemented yet.")
- elif analysis_backend.lower() == "codeanalyzer":
- raise NotImplementedError("Support for {analysis_backend} has not been implemented yet.")
- elif analysis_backend.lower() == "treesitter":
- self.analysis_backend: PythonSitter = PythonSitter()
- else:
- raise NotImplementedError("Support for {analysis_backend} has not been implemented yet.")
+ self.analysis_backend: TreesitterPython = TreesitterPython()
def get_methods(self) -> List[PyMethod]:
"""
@@ -96,7 +86,7 @@ def is_parsable(self, source_code: str) -> bool:
Returns:
True if the code is parsable, False otherwise
"""
- return PythonSitter().is_parsable(source_code)
+ return TreesitterPython().is_parsable(source_code)
def get_raw_ast(self, source_code: str) -> str:
"""
@@ -107,7 +97,7 @@ def get_raw_ast(self, source_code: str) -> str:
Returns:
Tree: the raw AST
"""
- return PythonSitter().get_raw_ast(source_code)
+ return TreesitterPython().get_raw_ast(source_code)
def get_imports(self) -> List[PyImport]:
"""
diff --git a/cldk/core.py b/cldk/core.py
index b3c7f81..a97fd6d 100644
--- a/cldk/core.py
+++ b/cldk/core.py
@@ -26,7 +26,7 @@
from cldk.analysis import AnalysisLevel
from cldk.analysis.c import CAnalysis
from cldk.analysis.java import JavaAnalysis
-from cldk.analysis.java.treesitter import JavaSitter
+from cldk.analysis.commons.treesitter import TreesitterJava
from cldk.utils.exceptions import CldkInitializationException
from cldk.utils.sanitization.java import TreesitterSanitizer
@@ -56,7 +56,6 @@ def analysis(
project_path: str | Path | None = None,
source_code: str | None = None,
eager: bool = False,
- analysis_backend: str | None = "codeanalyzer",
analysis_level: str = AnalysisLevel.symbol_table,
target_files: List[str] | None = None,
analysis_backend_path: str | None = None,
@@ -64,7 +63,7 @@ def analysis(
use_graalvm_binary: bool = False,
) -> JavaAnalysis:
"""
- Initialize the preprocessor based on the specified language and analysis_backend.
+ Initialize the preprocessor based on the specified language.
Parameters
----------
@@ -73,18 +72,11 @@ def analysis(
source_code : str, optional
The source code of the project, defaults to None. If None, it is assumed that the whole project is being
analyzed.
- analysis_backend : str, optional
- The analysis_backend used for analysis, defaults to "codeql".
analysis_backend_path : str, optional
- The path to the analysis_backend, defaults to None and in the case of codeql, it is assumed that the cli is
- installed and available in the PATH. In the case of codeanalyzer the codeanalyzer.jar is downloaded from the
- lastest release.
+ The path to the analysis backend, defaults to None where it assumes the default backend path.
analysis_json_path : str or Path, optional
The path save the to the analysis database (analysis.json), defaults to None. If None, the analysis database
is not persisted.
- use_graalvm_binary : bool, optional
- A flag indicating whether to use the GraalVM binary for SDG analysis, defaults to False. If False,
- the default Java binary is used and one needs to have Java 17 or higher installed.
eager : bool, optional
A flag indicating whether to perform eager analysis, defaults to False. If True, the analysis is performed
eagerly. That is, the analysis.json file is created during analysis every time even if it already exists.
@@ -121,7 +113,6 @@ def analysis(
return JavaAnalysis(
project_dir=project_path,
source_code=source_code,
- analysis_backend=analysis_backend,
analysis_level=analysis_level,
analysis_backend_path=analysis_backend_path,
analysis_json_path=analysis_json_path,
@@ -145,7 +136,7 @@ def treesitter_parser(self):
"""
if self.language == "java":
- return JavaSitter()
+ return TreesitterJava()
else:
raise NotImplementedError(f"Treesitter parser for {self.language} is not implemented yet.")
diff --git a/cldk/models/treesitter/__init__.py b/cldk/models/treesitter/__init__.py
index c4abf4f..1c23178 100644
--- a/cldk/models/treesitter/__init__.py
+++ b/cldk/models/treesitter/__init__.py
@@ -18,6 +18,6 @@
Treesitter package
"""
-from .models import Captures
+from ...analysis.commons.treesitter.models import Captures
__all__ = ["Captures"]
diff --git a/cldk/utils/analysis_engine.py b/cldk/utils/analysis_engine.py
deleted file mode 100644
index 8f93f52..0000000
--- a/cldk/utils/analysis_engine.py
+++ /dev/null
@@ -1,25 +0,0 @@
-################################################################################
-# Copyright IBM Corporation 2024
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-################################################################################
-
-"""
-Analysis Engine module
-"""
-
-
-class AnalysisEngine:
- TREESITTER: str = "treesitter"
- CODEQL: str = "codeql"
- CODEANALYZER: str = "codeanalyzer"
diff --git a/cldk/utils/sanitization/java/treesitter_sanitizer.py b/cldk/utils/sanitization/java/treesitter_sanitizer.py
index dfca9af..85de9b3 100644
--- a/cldk/utils/sanitization/java/treesitter_sanitizer.py
+++ b/cldk/utils/sanitization/java/treesitter_sanitizer.py
@@ -22,8 +22,8 @@
from copy import deepcopy
from typing import Dict, List, Set
-from cldk.analysis.java.treesitter import JavaSitter
-from cldk.models.treesitter import Captures
+from cldk.analysis.commons.treesitter import TreesitterJava
+from cldk.analysis.commons.treesitter.models import Captures
log = logging.getLogger(__name__)
@@ -33,7 +33,7 @@ class TreesitterSanitizer:
def __init__(self, source_code):
self.source_code = source_code
self.sanitized_code = deepcopy(self.source_code)
- self.__javasitter = JavaSitter()
+ self.__javasitter = TreesitterJava()
def keep_only_focal_method_and_its_callees(self, focal_method: str) -> str:
"""Remove all methods except the focal method and its callees.
diff --git a/cldk/utils/sanitization/java/treesitter_utils.py b/cldk/utils/sanitization/java/treesitter_utils.py
index 4156004..570e1a4 100644
--- a/cldk/utils/sanitization/java/treesitter_utils.py
+++ b/cldk/utils/sanitization/java/treesitter_utils.py
@@ -22,10 +22,10 @@
from copy import deepcopy
from typing import Dict, List, Any, LiteralString
-from cldk.analysis.java.treesitter import JavaSitter
-from cldk.models.treesitter import Captures
+from cldk.analysis.commons.treesitter.treesitter_java import TreesitterJava
+from cldk.analysis.commons.treesitter.models import Captures
-java_sitter = JavaSitter()
+java_sitter = TreesitterJava()
def _replace_in_source(
diff --git a/docs/images/cldk-dark.png b/docs/images/cldk-dark.png
new file mode 100644
index 0000000000000000000000000000000000000000..fa4492a5545bff0ba583f5d0dfa42ee452d20263
GIT binary patch
literal 356097
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%0Aen(jCQo
z?dZww&)4#!IQZ^4Qko2}u!7y|*LnW^*{aDw(R1cB@$uE7D^s3ZLCPpjDwnSi>lUJO
z^my>hLh6{e@2>C5W!1#UEj<4`WtVMl?{|H5Ol$l?y>-FQJ>saJsY|_U=NaQ`6=*N*k^GC;pX9h7sv=AVyr@%iEL5qNQm(++yy+Za?G2g#2+e@mFI9OJrY(I
z)>uLR5>y4r@i(Zuti494k`+8QM6YYUEpV@2`1CTQdMD5dMtdbqyy@XJ#ErXt`$7Ca
z_H-zZQsF^W5|edk0&21d^*=4Vs48KCWVHerb*jx&j9JDp=25=lQV)m*~O!>JbxX)O&RWi#~
z)zicS_u5PzpP$r&W>4>q%M>6f4p4j1h+>+kb2TCd8#q4QEM7TP1(dtONiOznPojJ*
zkPoW;ieGwc8Q5`Gl-KGtdhrl!{E$+Gnp6_e|HdO$I?BhDSp=@T;L
z9XM=ylVFp(!UM1%Xx0Dr5}ZO=mY*>pIF-rJyypX^U4hKUiZ_)$J${&am+`ws0gBe;
zW>HSX(p(_}1=HHN%nT-+7_%2Lr!aOqBkO;r&O6=3K6_7lxbjs@6%<;f6vkI|Zm#(H-4eFyWmh@5{eFP-nMi#9w$pg$`
z(%w8v+m&w}V4BSTXPVk4Ou%%`FPS$aUv*^279R)mIyek%8v6K)DuJhSVR8km32vbt
zC>mo}rw-T#(JWqa{6N^%)D*;$3k-BBXw85c(4fp?LdH=4samA`c&PQQ4n_GR)xHW{
zwf%$BZWYKlv+T*v+!JLC7Yk4v-=8SU$Ho8?!gj=g1NOFuWed5&-Ta>M{|1CA_?t3v
zrMMWgr(s@$(oG)+YHmGjcIT#ckMhqPiO$nLTl|9V(QKKZtg~Q#+hlP2KKr>(fu;|VluFTqmP|P_)s4}$;%0DVH$9^MWRo!$XAVMSU
zOqP$TJ@*WkSHlXuvcd7DryrGv-dK9HlnR@NG;5Iiz-vxiL`c%t=t&zIh{QjcNE6&T
zNULCD?VhZq{;;Q(Kd?W6U0y36T<<#2EyGOj+7y;X9O1wOzDNC*BT6wYHl@lMomshU
zV4GLS{NE~Rn_&Ygk86pHztX3Qx~z59WMnE`&z$ps*JjSd->9}3IkmNvZb>N0wB*{}
z>}X9ICcL$KU30R2z;+X1d!kAd97;AMzj{V0`cL;kQtk$!r93y{C{yo>qy1f91ZC!W
z6bTcb5zq|aEE3R2$imx~r*0tlQMl6R><#N6rEXVT+ChH2ZSI&t51c{59V
zd^7r3_y4zjfYW^He-p})x>-(UxYB?55C`PY;x?@W4KNccU}wj~)YFEpGXp`{0L!B=
z=XW<5OefIoK!mv$`PB!iW=!04whaTDU6;WLB0w?!
zz%GE_dbL*^U1niAjQMEG$cu&kXfR^SpJW;g01_Nkl_o%lgDY9oroDO3#X@}b@E30MRGDpUz2%r08v^!Y
z%IAyrS_Pe674ZLCcrO;}MyQjT!B~i3EKtHo%pL~CBvv;wTF%Z)ovV}0x3K}E6gPG1
zrwLVqKM;;di;n08s}shwZI#XcQR$_1ntQ7bp?jdjh-#tcxIGm>qRqKuKW!pA`M2B%
zp28WhWu$B_;)t%Yz6Uv8A*kaZpW-uANrG6eeLZ?9E`)CG`Up1tUwZuW?kkt%e!fG6
zTNsbK?tt@j4Pk1yV^_*j?=m<5;SHi8Q3ebqt&sNBJw91aJ%0`sWsf5~
zi0Z)ZX|WpQ
zCuwUAfFu@eZVQ*Fvd=uRA$vZIN)#zLRMv;6ktnhxF2avSe{Gqd371{AS
z&z~O;nRnc#Kq3$I+;5fYW#EhyHHCn3Ki!6i`Z4EiaFS)iBM`kzr5L8y4t?6VqpNv4
zBlgHWVv{r#p|~SMCN>U+P=t`Y$LlLMWUfr{7(#*v&nm>kB%)QrmwZUuyZLD|{cF^|
zS7w+&31%$mfxw+^tVVv;%$U@9_P6B8&7CeVwXXEO(qQ-@GBP%E3rpm+qzml+rFajF
z-t~nyQNcrSj5#0Uj#bTdLU0STXsjKp6I{Ig_SEbYCnK#(z#%A)xrERU?lEf|zyF+P
zmSm4S}9ZAtFTf5;lM`CD{#N6(OWKEf>wwv3n1MH7wH*UIn
zAA+rwpyk9-(%XG-Y7$e_Tb_F}r%Bv#B;|-__^kS9u2iD_6AMN<0&D2uD#dVMMtB7!
z6tHYL(es^{0j!EXo3?e|17psN`L{f!`e|-k^@oLJ=NT#yfhw2xEN3}osHeT>?NE0A4~go
z_A#h%hTx0x-Aae%u=aDu7vYR1666E3H}8^k503xMB;MEF6J^No*+@+Kmw4(0PITmV
zAA?M5J!Kf-SA*GDJw}Va0F=QJ*5M#4h2l%XN?rxMeTRzb13jc_Ql`l7r?4CH;@X#`
zEBEiAlbFg6h_MRhKSc#;hA!iQ
zo6-pUao{@|-5PgXu3~c@Ji$oDFD%t7fOKsVmg;e+90AVt_4(nTIHMr0vh#4zkXS)%
zR!Hl!n1$hM?48vOZ`;qJ1PQ_HCmS|B^FS&*RxY94*zz=nhmK_l3M-O8ZGoIW*nQE2
zlQx!F!TsJP>j)TSG*JA#P9XS}u-_os2H1UyOPH0%;Q0Igcns&%=4(-^w1q3ZOG<#d
z^4De(XJYn@h4u7CDK&oBeH%6IWTp=%0PU!?sZkQ*u@&KDU&U+EhgVHY%hb=dhl-=6
zjZm$#J<~EN6fV8Qp;m0f0%@}3z5=N!Sv<4K?xZO67kWFiTIA$LmV=FGMF+K+
z8rM~&uGZlP9xF!J{QznN^p}f$g|WL%XYC@#tAY1NhOizEIO7qbcHfK`7zS45p=DiJ
zdEe=;L1}-$*FK`37;CBD+vd`XQqEarnAf>AkMk-n^4{71(!a9op(idakgsv$&(=j9s4HdLVo_3&O+;e%-%%XbIaeqFOhW!#b4*wU8U!LQ;(hSU
zdsYMTt|GkNane$$cBRu2Hl_HBJ62%YYzsHv+K68LgG+2rRunB=vK8Dqx3ue(4UYUx?->p!*ws
zdjUq^^5rX9?W~qvqam~Sg4PqDol->CBi=>{BDiMNeQuiCEPr9^NS
z5Nn)o15RYBDEWfms=A*Vw!ndpjgdnmhuVOO`kjS760Y6*f_$I(AWl_FP?q!cYE7DC
ze#Oo*I8d#oU@+eaO2~@=098+lAH{#V_BsV9oTQ-XSf4ldi+(n+vTQvf&hbH5s{>Z8
zitf|$rd`|d^~UOfNbRnT=NbC&*p)`WAIv099i@{NGZO)zIt;BV1n=>&yhb=w;13ng
zfkH{FL%%J2;yeQrd4KZCunGTxAQAp?piyqiKYC{ykjjXExUffl9Jr5b
zL}cOERgvCUppQdVxPt;SkeDBXVVXh#pZId@sHm^W?e2gu8{b0%oBc2-fr1>M5*S)~
zp*9V=9LnKaQ==1!Z!yu;;_qIzhKkP6e${WIhfEArTBj0q)|BrxPKrZ{Wq(c!^D^#P
zX=mPb=JJ^Oy
zQch^w({$W>9%<||lXV$yKmFf`wpC{X;PFuJpI|
z@haB|o_Kx$Lto
zQm($aW9kAw<{Cy4ks#mWSf>Q>upf&z3(1&OC7B
z9aZJrK}-V7G?M?EF}d`j{KC^Kagf5JKCo((bUgN7wU
zY)Z!6`Ak$HaMhD`mlVE%17N+ri9ptJmIOxK9ZhHTu=@b&u!7OQa(1rRiDzol_(y8$>f`Y06q6S3VCVZj)Xj0|1m
z<^{p5^n0Y@f_Z(}7RSKD4Mb?((`cImvlS%*ZkVRx35Nvu
z(Ub@tZ>Xuk6Pz;ni@t`V*+wP_GFq}EU@VsMpGfzyymDM&!Y|j+{i}HV^^Z_z<>F?I
zTTj+w-be=~NTt{NLU3ffPnaDrB#AL$bf133-yL|@u6AXLa7gi47=p!Fzkj^qn~JoS
z3%oP9Xzkv;Q;B+!;$HXs$Fpi9IG>DCH!3(zNa8Oj0ZwlD(9*E}e)f>eIT5|vynZ%(
z+FNA5J|Lu;Te>pwQ+~p*TebZF^UWzwRK>8vK_LqAW5hZBz(><1KY}NJaY}#`LqP@F
z@a(AoS-F@yipl?eoV@WplUdUV()4AyOMwh^b!>YG`&-M6%jcHBdbN9ewj^D*M|C2(
z5tUHXxKh<6S)>4m=!>(`jOh^%mS-kM4FE9b%__60>Wv-La$=`g?qLowa3CH?@9a4H
zypPZ!9@wpLxBmn&Fr{+50Qc<
zj%-;Mw>P}Z`ZVzG=BDFHJ;07yEtoLFY1-YtXA0s93lnZZ!{iiZm6Mqs$Kti<*BNq0de@taAQoz&`l!JXkssp(6Q`k$&R5LJWh
z%(|SxTgbn%;4u3
zlygF3WpJ1#{HWIdPFu8%p=M7==pF37G(TpOD6e_@p?JQv0>?hd>*1#C$$o?LGp!n5
zRVPjL8mm@d+b-@nWMm-lU(NfVd}^K3x!&8~jj#c~dbo2Gp@fBDj=DoumI4+pWp)3M
z_XYJVTVs+`kI*_f7FkwZ)y#F>t!643=@0R+@9#-`DPzBDU4jQJ0RJy1ZO~|O->?>j
zjHxM4;0Fvp@ZJ7{UnzyjE5ga^PFZ}G@vb_dikE&uC$FdPPU3SFd5_UfUIWZaMgCbV
zn=4~(JNCk6^;!i)SR#LE_1~_dq~W9I$0wOY0L-&XYcqETvo6D~BWhZ)Iq?lFCo(J{
zoO40>x1w~|Klm&B4LC4NUbBw*lP9DRRd<_^*S{p`o%Jwh8mOQr#G~H%ZSusUA>o9daQJq7u?6@d=}0l2g}YeAU9$zu
zXLemP?0)Fz{)>K-zyR9w61>$rqCqg==iWD#V8~k1pSy+5z+Ip31E%b*tf7o`jrANj
z^E6}lT27669j;G0zzgqfWY@X;kYVex@U_H}C#TY7KZ*pG$;yI>1T65M