Skip to content

Commit 14096fe

Browse files
authored
Merge pull request #82 from Pennycook/compile-command
Improve compile command handling
2 parents 4a369a7 + cef6ec5 commit 14096fe

File tree

7 files changed

+297
-14
lines changed

7 files changed

+297
-14
lines changed

codebasin/__init__.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# Copyright (C) 2019-2024 Intel Corporation
22
# SPDX-License-Identifier: BSD-3-Clause
3+
import shlex
34
import warnings
45

6+
import codebasin.source
57
import codebasin.walkers
68

79
warnings.warn(
@@ -11,3 +13,113 @@
1113
+ "a future release of Code Base Investigator.",
1214
DeprecationWarning,
1315
)
16+
17+
18+
class CompileCommand:
19+
"""
20+
A single compile command from a compilation database.
21+
22+
Attributes
23+
----------
24+
filename: string
25+
The name of the source file compiled by this command.
26+
27+
directory: string, optional
28+
The working directory for this command.
29+
30+
arguments: list[string], optional
31+
The `argv` for this command, including the executable as `argv[0]`.
32+
33+
output: string, optional
34+
The name of the file produced by this command, or None if not
35+
specified.
36+
"""
37+
38+
def __init__(
39+
self,
40+
filename,
41+
directory=None,
42+
arguments=None,
43+
command=None,
44+
output=None,
45+
):
46+
"""
47+
Raises
48+
------
49+
ValueError
50+
If both arguments and command are None.
51+
"""
52+
self._filename = filename
53+
self._directory = directory
54+
if arguments is None and command is None:
55+
raise ValueError("CompileCommand requires arguments or command.")
56+
self._arguments = arguments
57+
self._command = command
58+
self._output = output
59+
60+
@property
61+
def directory(self):
62+
return self._directory
63+
64+
@property
65+
def filename(self):
66+
return self._filename
67+
68+
@property
69+
def arguments(self):
70+
if self._arguments is None:
71+
return shlex.split(self._command)
72+
else:
73+
return self._arguments
74+
75+
@property
76+
def output(self):
77+
return self._output
78+
79+
def __str__(self):
80+
if self._command is None:
81+
return " ".join(self._arguments)
82+
else:
83+
return self._command
84+
85+
def is_supported(self):
86+
"""
87+
Returns
88+
-------
89+
bool
90+
True if the command can be emulated and False otherwise.
91+
Commands that are not supported will not impact analysis.
92+
"""
93+
# Commands must be non-empty in order to do something.
94+
# Commands must operate on source files.
95+
if len(self.arguments) > 0 and codebasin.source.is_source_file(
96+
self.filename,
97+
):
98+
return True
99+
100+
return False
101+
102+
@classmethod
103+
def from_json(cls, instance: dict):
104+
"""
105+
Parameters
106+
----------
107+
instance: dict
108+
A JSON object representing a single compile command.
109+
110+
Returns
111+
-------
112+
CompileCommand
113+
A CompileCommand corresponding to the JSON object.
114+
"""
115+
directory = instance.get("directory", None)
116+
arguments = instance.get("arguments", None)
117+
command = instance.get("command", None)
118+
output = instance.get("output", None)
119+
return cls(
120+
instance["file"],
121+
directory=directory,
122+
arguments=arguments,
123+
command=command,
124+
output=output,
125+
)

codebasin/config.py

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,11 @@
1111
import logging
1212
import os
1313
import re
14-
import shlex
1514
import warnings
1615

1716
import yaml
1817

19-
from codebasin import util
18+
from codebasin import CompileCommand, util
2019

2120
log = logging.getLogger("codebasin")
2221

@@ -418,11 +417,10 @@ def load_database(dbpath, rootdir):
418417

419418
configuration = []
420419
for e in db:
421-
# Database may not have tokenized arguments
422-
if "command" in e:
423-
argv = shlex.split(e["command"])
424-
elif "arguments" in e:
425-
argv = e["arguments"]
420+
command = CompileCommand.from_json(e)
421+
if not command.is_supported():
422+
continue
423+
argv = command.arguments
426424

427425
# Extract defines, include paths and include files
428426
# from command-line arguments
@@ -444,19 +442,19 @@ def load_database(dbpath, rootdir):
444442
# - relative to a directory
445443
# - as an absolute path
446444
filedir = rootdir
447-
if "directory" in e:
448-
if os.path.isabs(e["directory"]):
449-
filedir = e["directory"]
445+
if command.directory is not None:
446+
if os.path.isabs(command.directory):
447+
filedir = command.directory
450448
else:
451449
filedir = os.path.realpath(
452450
rootdir,
453-
os.path.join(e["directory"]),
451+
os.path.join(command.directory),
454452
)
455453

456-
if os.path.isabs(e["file"]):
457-
path = os.path.realpath(e["file"])
454+
if os.path.isabs(command.filename):
455+
path = os.path.realpath(command.filename)
458456
else:
459-
path = os.path.realpath(os.path.join(filedir, e["file"]))
457+
path = os.path.realpath(os.path.join(filedir, command.filename))
460458

461459
# Compilation database may contain files that don't
462460
# exist without running make

codebasin/source.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Copyright (C) 2019-2024 Intel Corporation
2+
# SPDX-License-Identifier: BSD-3-Clause
3+
4+
import os
5+
from pathlib import Path
6+
from typing import Union
7+
8+
9+
def is_source_file(filename: Union[str, os.PathLike]) -> bool:
10+
"""
11+
Parameters
12+
----------
13+
filename: Union[str, os.Pathlike]
14+
The filename of a potential source file.
15+
16+
Returns
17+
-------
18+
bool
19+
True if the file ends in a recognized extension and False otherwise.
20+
Only files that can be parsed correctly have recognized extensions.
21+
22+
Raises
23+
------
24+
TypeError
25+
If filename is not a string or Path.
26+
"""
27+
if not (isinstance(filename, str) or isinstance(filename, Path)):
28+
raise TypeError("filename must be a string or Path")
29+
30+
extension = Path(filename).suffix
31+
supported_extensions = [
32+
".f90",
33+
".F90",
34+
".f",
35+
".ftn",
36+
".fpp",
37+
".F",
38+
".FOR",
39+
".FTN",
40+
".FPP",
41+
".c",
42+
".h",
43+
".c++",
44+
".cxx",
45+
".cpp",
46+
".cc",
47+
".hpp",
48+
".hxx",
49+
".h++",
50+
".hh",
51+
".inc",
52+
".inl",
53+
".tcc",
54+
".icc",
55+
".ipp",
56+
".cu",
57+
".cuh",
58+
".cl",
59+
".s",
60+
".S",
61+
".asm",
62+
]
63+
return extension in supported_extensions

tests/compile-command/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Copyright (C) 2019-2024 Intel Corporation
2+
# SPDX-License-Identifier: BSD-3-Clause
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Copyright (C) 2019-2024 Intel Corporation
2+
# SPDX-License-Identifier: BSD-3-Clause
3+
4+
import unittest
5+
6+
from codebasin import CompileCommand
7+
8+
9+
class TestCompileCommand(unittest.TestCase):
10+
"""
11+
Test CompileCommand class.
12+
"""
13+
14+
def test_commands_and_arguments(self):
15+
"""Check commands and arguments are not both None"""
16+
17+
with self.assertRaises(ValueError):
18+
CompileCommand("file.cpp", command=None, arguments=None)
19+
20+
with self.assertRaises(ValueError):
21+
instance = {
22+
"file": "file.cpp",
23+
}
24+
CompileCommand.from_json(instance)
25+
26+
def test_command_to_arguments(self):
27+
"""Check commands convert to arguments"""
28+
command = CompileCommand("file.cpp", command="c++ file.cpp")
29+
self.assertEqual(command.arguments, ["c++", "file.cpp"])
30+
31+
instance = {
32+
"file": "file.cpp",
33+
"command": "c++ file.cpp",
34+
}
35+
command = CompileCommand.from_json(instance)
36+
self.assertEqual(command.arguments, ["c++", "file.cpp"])
37+
38+
def test_arguments_to_command(self):
39+
"""Check arguments convert to command"""
40+
command = CompileCommand("file.cpp", arguments=["c++", "file.cpp"])
41+
self.assertEqual(str(command), "c++ file.cpp")
42+
43+
instance = {
44+
"file": "file.cpp",
45+
"arguments": [
46+
"c++",
47+
"file.cpp",
48+
],
49+
}
50+
command = CompileCommand.from_json(instance)
51+
self.assertEqual(str(command), "c++ file.cpp")
52+
53+
def test_empty_command(self):
54+
"""Check empty commands are not supported"""
55+
command = CompileCommand("file.cpp", command="")
56+
self.assertFalse(command.is_supported())
57+
58+
def test_link_command(self):
59+
"""Check link commands are not supported"""
60+
command = CompileCommand("file.o", command="c++ -o a.out file.o")
61+
self.assertFalse(command.is_supported())
62+
63+
def test_valid_command(self):
64+
"""Check valid commands are supported"""
65+
command = CompileCommand("file.cpp", command="c++ file.cpp")
66+
self.assertTrue(command.is_supported())
67+
68+
69+
if __name__ == "__main__":
70+
unittest.main()

tests/source/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Copyright (C) 2019-2024 Intel Corporation
2+
# SPDX-License-Identifier: BSD-3-Clause

tests/source/test_source.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Copyright (C) 2019-2024 Intel Corporation
2+
# SPDX-License-Identifier: BSD-3-Clause
3+
4+
import unittest
5+
from pathlib import Path
6+
7+
import codebasin.source as source
8+
9+
10+
class TestSource(unittest.TestCase):
11+
"""
12+
Test functionality in the source module.
13+
"""
14+
15+
def test_is_source_file_string(self):
16+
"""Check source file identification for string filenames"""
17+
self.assertTrue(source.is_source_file("file.cpp"))
18+
self.assertTrue(source.is_source_file("/path/to/file.cpp"))
19+
self.assertFalse(source.is_source_file("file.o"))
20+
self.assertFalse(source.is_source_file("/path/to/file.o"))
21+
22+
def test_is_source_file_path(self):
23+
"""Check source file identification for Path filenames"""
24+
self.assertTrue(source.is_source_file(Path("file.cpp")))
25+
self.assertTrue(source.is_source_file(Path("/path/to/file.cpp")))
26+
self.assertFalse(source.is_source_file(Path("file.o")))
27+
self.assertFalse(source.is_source_file(Path("/path/to/file.o")))
28+
29+
def test_is_source_types(self):
30+
"""Check type validation for is_source"""
31+
with self.assertRaises(TypeError):
32+
source.is_source_file(1)
33+
34+
35+
if __name__ == "__main__":
36+
unittest.main()

0 commit comments

Comments
 (0)