Files

171 lines
5.4 KiB
Python

# SPDX-License-Identifier: MIT
"""This module is responsible for parsing compilation database entries."""
import json
import os
from collections.abc import Iterable, Iterator
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from clanganalyzer import shell_split
__all__ = ["classify_source", "Compilation", "CompilationDatabase"]
@dataclass(frozen=True)
class CompilationEntry:
"""Represents a compilation database entry.
Attributes:
directory: Working directory for the compilation
file: Source file path (relative to directory)
arguments: List of compiler arguments
"""
directory: str
file: str
arguments: list[str]
@classmethod
def from_db_entry(cls, entry: dict[str, Any]) -> "CompilationEntry":
"""Create CompilationEntry from compilation database entry."""
directory = entry["directory"]
file = entry["file"]
# Handle both "command" (string) and "arguments" (list) formats
if "command" in entry:
arguments = shell_split(entry["command"])
else:
arguments = entry["arguments"]
if not isinstance(arguments, list):
arguments = [arguments]
return cls(directory=directory, file=file, arguments=arguments)
@dataclass
class Compilation:
"""Represents a compilation of a single source file."""
directory: str
source: str
compiler: str
flags: list[str]
@classmethod
def from_entry(cls, entry: CompilationEntry) -> "Compilation":
"""Create compilation from a compilation database entry."""
directory = os.path.normpath(entry.directory)
source = entry.file if os.path.isabs(entry.file) else os.path.join(directory, entry.file)
source = os.path.normpath(source)
# Extract compiler and flags from arguments
compiler, flags = cls._parse_arguments(entry.arguments)
return cls(directory=directory, source=source, compiler=compiler, flags=flags)
@staticmethod
def _parse_arguments(arguments: list[str]) -> tuple[str, list[str]]:
"""Parse compiler arguments to extract compiler type and flags.
Returns:
Tuple of (compiler_type, flags) where compiler_type is "c" or "c++"
"""
if not arguments:
return "c", []
# Determine compiler type from executable name
executable = os.path.basename(arguments[0])
if any(pattern in executable for pattern in ["c++", "cxx", "g++", "clang++"]):
compiler_type = "c++"
else:
compiler_type = "c"
# Return all arguments except the compiler executable as flags
# The analyzer will reconstruct the command as needed
return compiler_type, arguments[1:]
def as_dict(self) -> dict[str, Any]:
"""Convert compilation to dictionary representation."""
return {
"source": self.source,
"directory": self.directory,
"compiler": self.compiler,
"flags": self.flags,
}
class CompilationDatabase:
"""Compilation database loader."""
@staticmethod
def load(filename: str) -> Iterable[Compilation]:
"""Load compilations from compilation database file.
:param filename: Path to compilation database JSON file
:returns: Iterator of Compilation objects
"""
with open(filename) as handle:
entries = json.load(handle)
for entry_data in entries:
entry = CompilationEntry.from_db_entry(entry_data)
yield Compilation.from_entry(entry)
@staticmethod
def file_commonprefix(filename: str) -> Path | None:
"""Create file prefix from a compilation database entries."""
with open(filename) as handle:
return _commonprefix(item["file"] for item in json.load(handle))
def _commonprefix(files: Iterator[str]) -> Path | None:
"""Fixed version of os.path.commonprefix.
:param files: list of file names.
:return: the longest path prefix that is a prefix of all files, or None if no common prefix."""
result = None
for current in files:
if result is not None:
result = os.path.commonprefix([result, current])
else:
result = current
if result is None or result == "":
return None
elif not os.path.isdir(result):
return Path(os.path.dirname(result))
return Path(os.path.abspath(result))
def classify_source(filename: str, c_compiler: bool = True) -> str | None:
"""Classify source file names and returns the presumed language,
based on the file name extension.
:param filename: the source file name
:param c_compiler: indicate that the compiler is a C compiler,
:return: the language from file name extension."""
mapping = {
".c": "c" if c_compiler else "c++",
".i": "c-cpp-output" if c_compiler else "c++-cpp-output",
".ii": "c++-cpp-output",
".m": "objective-c",
".mi": "objective-c-cpp-output",
".mm": "objective-c++",
".mii": "objective-c++-cpp-output",
".C": "c++",
".cc": "c++",
".CC": "c++",
".cp": "c++",
".cpp": "c++",
".cxx": "c++",
".c++": "c++",
".C++": "c++",
".txx": "c++",
}
__, extension = os.path.splitext(os.path.basename(filename))
return mapping.get(extension)