Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve compile command handling #82

Merged
merged 7 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions codebasin/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# Copyright (C) 2019-2024 Intel Corporation
# SPDX-License-Identifier: BSD-3-Clause
import shlex
import warnings

import codebasin.source
import codebasin.walkers

warnings.warn(
Expand All @@ -11,3 +13,113 @@
+ "a future release of Code Base Investigator.",
DeprecationWarning,
)


class CompileCommand:
"""
A single compile command from a compilation database.

Attributes
----------
filename: string
The name of the source file compiled by this command.

directory: string, optional
The working directory for this command.

arguments: list[string], optional
The `argv` for this command, including the executable as `argv[0]`.

output: string, optional
The name of the file produced by this command, or None if not
specified.
"""

def __init__(
self,
filename,
directory=None,
arguments=None,
command=None,
output=None,
):
"""
Raises
------
ValueError
If both arguments and command are None.
"""
self._filename = filename
self._directory = directory
if arguments is None and command is None:
raise ValueError("CompileCommand requires arguments or command.")
self._arguments = arguments
self._command = command
self._output = output

@property
def directory(self):
return self._directory

@property
def filename(self):
return self._filename

@property
def arguments(self):
if self._arguments is None:
return shlex.split(self._command)
else:
return self._arguments

@property
def output(self):
return self._output

def __str__(self):
if self._command is None:
return " ".join(self._arguments)
else:
return self._command

def is_supported(self):
"""
Returns
-------
bool
True if the command can be emulated and False otherwise.
Commands that are not supported will not impact analysis.
"""
# Commands must be non-empty in order to do something.
# Commands must operate on source files.
if len(self.arguments) > 0 and codebasin.source.is_source_file(
self.filename,
):
return True

return False

@classmethod
def from_json(cls, instance: dict):
"""
Parameters
----------
instance: dict
A JSON object representing a single compile command.

Returns
-------
CompileCommand
A CompileCommand corresponding to the JSON object.
"""
directory = instance.get("directory", None)
arguments = instance.get("arguments", None)
command = instance.get("command", None)
output = instance.get("output", None)
return cls(
instance["file"],
directory=directory,
arguments=arguments,
command=command,
output=output,
)
26 changes: 12 additions & 14 deletions codebasin/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,11 @@
import logging
import os
import re
import shlex
import warnings

import yaml

from codebasin import util
from codebasin import CompileCommand, util

log = logging.getLogger("codebasin")

Expand Down Expand Up @@ -418,11 +417,10 @@ def load_database(dbpath, rootdir):

configuration = []
for e in db:
# Database may not have tokenized arguments
if "command" in e:
argv = shlex.split(e["command"])
elif "arguments" in e:
argv = e["arguments"]
command = CompileCommand.from_json(e)
if not command.is_supported():
continue
argv = command.arguments

# Extract defines, include paths and include files
# from command-line arguments
Expand All @@ -444,19 +442,19 @@ def load_database(dbpath, rootdir):
# - relative to a directory
# - as an absolute path
filedir = rootdir
if "directory" in e:
if os.path.isabs(e["directory"]):
filedir = e["directory"]
if command.directory is not None:
if os.path.isabs(command.directory):
filedir = command.directory
else:
filedir = os.path.realpath(
rootdir,
os.path.join(e["directory"]),
os.path.join(command.directory),
)

if os.path.isabs(e["file"]):
path = os.path.realpath(e["file"])
if os.path.isabs(command.filename):
path = os.path.realpath(command.filename)
laserkelvin marked this conversation as resolved.
Show resolved Hide resolved
else:
path = os.path.realpath(os.path.join(filedir, e["file"]))
path = os.path.realpath(os.path.join(filedir, command.filename))

# Compilation database may contain files that don't
# exist without running make
Expand Down
63 changes: 63 additions & 0 deletions codebasin/source.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Copyright (C) 2019-2024 Intel Corporation
# SPDX-License-Identifier: BSD-3-Clause

import os
from pathlib import Path
from typing import Union


def is_source_file(filename: Union[str, os.PathLike]) -> bool:
"""
Parameters
----------
filename: Union[str, os.Pathlike]
The filename of a potential source file.

Returns
-------
bool
True if the file ends in a recognized extension and False otherwise.
Only files that can be parsed correctly have recognized extensions.

Raises
------
TypeError
If filename is not a string or Path.
"""
if not (isinstance(filename, str) or isinstance(filename, Path)):
raise TypeError("filename must be a string or Path")

extension = Path(filename).suffix
supported_extensions = [
".f90",
".F90",
laserkelvin marked this conversation as resolved.
Show resolved Hide resolved
".f",
".ftn",
".fpp",
".F",
".FOR",
".FTN",
".FPP",
".c",
".h",
".c++",
".cxx",
".cpp",
".cc",
".hpp",
".hxx",
".h++",
".hh",
".inc",
".inl",
".tcc",
".icc",
".ipp",
".cu",
".cuh",
".cl",
".s",
".S",
".asm",
]
return extension in supported_extensions
2 changes: 2 additions & 0 deletions tests/compile-command/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Copyright (C) 2019-2024 Intel Corporation
# SPDX-License-Identifier: BSD-3-Clause
70 changes: 70 additions & 0 deletions tests/compile-command/test_compile_command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Copyright (C) 2019-2024 Intel Corporation
# SPDX-License-Identifier: BSD-3-Clause

import unittest

from codebasin import CompileCommand


class TestCompileCommand(unittest.TestCase):
"""
Test CompileCommand class.
"""

def test_commands_and_arguments(self):
"""Check commands and arguments are not both None"""

with self.assertRaises(ValueError):
CompileCommand("file.cpp", command=None, arguments=None)

with self.assertRaises(ValueError):
instance = {
"file": "file.cpp",
}
CompileCommand.from_json(instance)

def test_command_to_arguments(self):
"""Check commands convert to arguments"""
command = CompileCommand("file.cpp", command="c++ file.cpp")
self.assertEqual(command.arguments, ["c++", "file.cpp"])

instance = {
"file": "file.cpp",
"command": "c++ file.cpp",
}
command = CompileCommand.from_json(instance)
self.assertEqual(command.arguments, ["c++", "file.cpp"])

def test_arguments_to_command(self):
"""Check arguments convert to command"""
command = CompileCommand("file.cpp", arguments=["c++", "file.cpp"])
self.assertEqual(str(command), "c++ file.cpp")

instance = {
"file": "file.cpp",
"arguments": [
"c++",
"file.cpp",
],
}
command = CompileCommand.from_json(instance)
self.assertEqual(str(command), "c++ file.cpp")

def test_empty_command(self):
"""Check empty commands are not supported"""
command = CompileCommand("file.cpp", command="")
self.assertFalse(command.is_supported())

def test_link_command(self):
"""Check link commands are not supported"""
command = CompileCommand("file.o", command="c++ -o a.out file.o")
self.assertFalse(command.is_supported())

def test_valid_command(self):
"""Check valid commands are supported"""
command = CompileCommand("file.cpp", command="c++ file.cpp")
self.assertTrue(command.is_supported())


if __name__ == "__main__":
unittest.main()
2 changes: 2 additions & 0 deletions tests/source/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Copyright (C) 2019-2024 Intel Corporation
# SPDX-License-Identifier: BSD-3-Clause
36 changes: 36 additions & 0 deletions tests/source/test_source.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Copyright (C) 2019-2024 Intel Corporation
# SPDX-License-Identifier: BSD-3-Clause

import unittest
from pathlib import Path

import codebasin.source as source


class TestSource(unittest.TestCase):
"""
Test functionality in the source module.
"""

def test_is_source_file_string(self):
"""Check source file identification for string filenames"""
self.assertTrue(source.is_source_file("file.cpp"))
self.assertTrue(source.is_source_file("/path/to/file.cpp"))
self.assertFalse(source.is_source_file("file.o"))
self.assertFalse(source.is_source_file("/path/to/file.o"))

def test_is_source_file_path(self):
"""Check source file identification for Path filenames"""
self.assertTrue(source.is_source_file(Path("file.cpp")))
self.assertTrue(source.is_source_file(Path("/path/to/file.cpp")))
self.assertFalse(source.is_source_file(Path("file.o")))
self.assertFalse(source.is_source_file(Path("/path/to/file.o")))

def test_is_source_types(self):
"""Check type validation for is_source"""
with self.assertRaises(TypeError):
source.is_source_file(1)


if __name__ == "__main__":
unittest.main()
Loading