Skip to content

Commit 664c464

Browse files
committed
semantic value conversion has its own type
1 parent 8aeaa75 commit 664c464

File tree

4 files changed

+331
-254
lines changed

4 files changed

+331
-254
lines changed

bear/src/output/writers.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,22 +54,22 @@ impl IteratorWriter<semantic::Command> for SemanticOutputWriter {
5454

5555
/// The type represents a converter that formats `semantic::Command` instances into `Entry` objects.
5656
pub(super) struct ConverterClangOutputWriter<T: IteratorWriter<Entry>> {
57-
format: config::EntryFormat,
57+
converter: semantic::clang::CommandConverter,
5858
writer: T,
5959
}
6060

6161
impl<T: IteratorWriter<Entry>> ConverterClangOutputWriter<T> {
6262
pub(super) fn new(writer: T, format: &config::EntryFormat) -> Self {
6363
Self {
64-
format: format.clone(),
64+
converter: semantic::clang::CommandConverter::new(format.clone()),
6565
writer,
6666
}
6767
}
6868
}
6969

7070
impl<T: IteratorWriter<Entry>> IteratorWriter<semantic::Command> for ConverterClangOutputWriter<T> {
7171
fn write(self, semantics: impl Iterator<Item = semantic::Command>) -> Result<(), WriterError> {
72-
let entries = semantics.flat_map(|semantic| semantic.to_entries(&self.format));
72+
let entries = semantics.flat_map(|semantic| self.converter.to_entries(&semantic));
7373
self.writer.write(entries)
7474
}
7575
}
Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
3+
//! Command to compilation database entry conversion functionality.
4+
//!
5+
//! This module provides the [`CommandConverter`] which is responsible for converting
6+
//! semantic [`Command`] instances into clang compilation database [`Entry`] objects.
7+
//! The converter encapsulates format configuration and conversion logic, providing
8+
//! a clean separation between domain objects and output formatting.
9+
//!
10+
//! The conversion process handles:
11+
//! - Extracting source files from compiler command arguments
12+
//! - Building properly formatted command lines for each source file
13+
//! - Computing output files based on command arguments
14+
//! - Applying format configuration (array vs string commands, output field inclusion)
15+
//!
16+
//! # Example
17+
//!
18+
//! ```rust
19+
//! use bear::semantic::clang::converter::CommandConverter;
20+
//! use bear::config::EntryFormat;
21+
//!
22+
//! let config = EntryFormat::default();
23+
//! let converter = CommandConverter::new(config);
24+
//!
25+
//! // The converter can be used to convert semantic Command instances
26+
//! // into compilation database entries based on the configured format
27+
//! ```
28+
29+
use super::Entry;
30+
use crate::config;
31+
use crate::semantic::{ArgumentGroup, ArgumentKind, Command, CompilerCommand};
32+
33+
/// Converts commands into compilation database entries.
34+
///
35+
/// This converter takes format configuration during construction and uses it
36+
/// to convert commands into appropriately formatted entries.
37+
pub struct CommandConverter {
38+
format: config::EntryFormat,
39+
}
40+
41+
impl CommandConverter {
42+
/// Creates a new CommandConverter with the specified format configuration.
43+
pub fn new(format: config::EntryFormat) -> Self {
44+
Self { format }
45+
}
46+
47+
/// Converts the command into compilation database entries.
48+
pub fn to_entries(&self, command: &Command) -> Vec<Entry> {
49+
match command {
50+
Command::Compiler(cmd) => self.convert_compiler_command(cmd),
51+
Command::Ignored(_) => vec![],
52+
}
53+
}
54+
55+
/// Converts a compiler command into compilation database entries.
56+
fn convert_compiler_command(&self, cmd: &CompilerCommand) -> Vec<Entry> {
57+
// Find all source files in the arguments
58+
let source_files = Self::find_arguments_by_kind(cmd, ArgumentKind::Source)
59+
.flat_map(ArgumentGroup::as_file)
60+
.collect::<Vec<String>>();
61+
62+
// If no source files found, return empty vector
63+
if source_files.is_empty() {
64+
return vec![];
65+
}
66+
67+
// Find output file if present
68+
let output_file = if self.format.keep_output_field {
69+
Self::compute_output_file(cmd)
70+
} else {
71+
None
72+
};
73+
74+
// Create one entry per source file
75+
source_files
76+
.into_iter()
77+
.map(|source_file| {
78+
let command_args = Self::build_command_args_for_source(cmd, &source_file);
79+
80+
Entry::new(
81+
source_file,
82+
command_args,
83+
&cmd.working_dir,
84+
output_file.as_ref(),
85+
self.format.command_field_as_array,
86+
)
87+
})
88+
.collect()
89+
}
90+
91+
/// Builds command arguments for a specific source file.
92+
///
93+
/// This method constructs the command arguments list that includes the executable,
94+
/// all non-source arguments, and the specific source file.
95+
/// It ensures that the source file is placed in the correct position relative to output arguments.
96+
fn build_command_args_for_source(cmd: &CompilerCommand, source_file: &str) -> Vec<String> {
97+
// Start with the executable
98+
let mut command_args = vec![cmd.executable.to_string_lossy().to_string()];
99+
100+
// Process arguments in the correct order for compilation database
101+
let mut source_added = false;
102+
103+
// Add all non-source arguments, while handling source file placement
104+
for arg in &cmd.arguments {
105+
if matches!(arg.kind, ArgumentKind::Source) {
106+
continue;
107+
}
108+
109+
// If we encounter output arguments and haven't added the source yet,
110+
// add the source first, then the output args
111+
if matches!(arg.kind, ArgumentKind::Output) && !source_added {
112+
command_args.push(source_file.to_string());
113+
source_added = true;
114+
}
115+
116+
command_args.extend(arg.args.iter().cloned());
117+
}
118+
119+
// If we haven't added the source yet, add it at the end
120+
if !source_added {
121+
command_args.push(source_file.to_string());
122+
}
123+
124+
command_args
125+
}
126+
127+
/// Returns arguments of a specific kind from the command.
128+
///
129+
/// This method filters arguments by their kind and returns their values as strings.
130+
fn find_arguments_by_kind(
131+
cmd: &CompilerCommand,
132+
kind: ArgumentKind,
133+
) -> impl Iterator<Item = &ArgumentGroup> {
134+
cmd.arguments.iter().filter(move |arg| arg.kind == kind)
135+
}
136+
137+
/// Computes the output file path from the command arguments.
138+
///
139+
/// This method examines the output arguments (typically "-o filename")
140+
/// and returns the filename as a PathBuf.
141+
fn compute_output_file(cmd: &CompilerCommand) -> Option<String> {
142+
// Find output arguments and convert to a file path
143+
Self::find_arguments_by_kind(cmd, ArgumentKind::Output)
144+
.nth(0)
145+
.and_then(|arg_group| arg_group.as_file())
146+
}
147+
}
148+
149+
#[cfg(test)]
150+
mod tests {
151+
use super::*;
152+
use crate::config::EntryFormat;
153+
use crate::semantic::{ArgumentKind, Command, CompilerCommand, CompilerPass};
154+
155+
#[test]
156+
fn test_compiler_command_to_entries_single_source() {
157+
let command = Command::Compiler(CompilerCommand::from_strings(
158+
"/home/user",
159+
"/usr/bin/gcc",
160+
vec![
161+
(
162+
ArgumentKind::Other(Some(CompilerPass::Compiling)),
163+
vec!["-c"],
164+
),
165+
(ArgumentKind::Other(None), vec!["-Wall"]),
166+
(ArgumentKind::Source, vec!["main.c"]),
167+
(ArgumentKind::Output, vec!["-o", "main.o"]),
168+
],
169+
));
170+
171+
let converter = CommandConverter::new(EntryFormat::default());
172+
let entries = converter.to_entries(&command);
173+
174+
let expected = vec![Entry::from_arguments_str(
175+
"main.c",
176+
vec!["/usr/bin/gcc", "-c", "-Wall", "main.c", "-o", "main.o"],
177+
"/home/user",
178+
Some("main.o"),
179+
)];
180+
assert_eq!(entries, expected);
181+
}
182+
183+
#[test]
184+
fn test_compiler_command_to_entries_multiple_sources() {
185+
let command = Command::Compiler(CompilerCommand::from_strings(
186+
"/home/user",
187+
"/usr/bin/g++",
188+
vec![
189+
(
190+
ArgumentKind::Other(Some(CompilerPass::Compiling)),
191+
vec!["-c"],
192+
),
193+
(ArgumentKind::Source, vec!["file1.cpp"]),
194+
(ArgumentKind::Source, vec!["file2.cpp"]),
195+
],
196+
));
197+
198+
let converter = CommandConverter::new(EntryFormat::default());
199+
let result = converter.to_entries(&command);
200+
201+
let expected = vec![
202+
Entry::from_arguments_str(
203+
"file1.cpp",
204+
vec!["/usr/bin/g++", "-c", "file1.cpp"],
205+
"/home/user",
206+
None,
207+
),
208+
Entry::from_arguments_str(
209+
"file2.cpp",
210+
vec!["/usr/bin/g++", "-c", "file2.cpp"],
211+
"/home/user",
212+
None,
213+
),
214+
];
215+
assert_eq!(result, expected);
216+
}
217+
218+
#[test]
219+
fn test_compiler_command_to_entries_no_sources() {
220+
let command = Command::Compiler(CompilerCommand::from_strings(
221+
"/home/user",
222+
"gcc",
223+
vec![(
224+
ArgumentKind::Other(Some(CompilerPass::Info)),
225+
vec!["--version"],
226+
)],
227+
));
228+
229+
let converter = CommandConverter::new(EntryFormat::default());
230+
let result = converter.to_entries(&command);
231+
232+
let expected: Vec<Entry> = vec![];
233+
assert_eq!(result, expected);
234+
}
235+
236+
#[test]
237+
fn test_to_entries_command_field_as_string() {
238+
let command = Command::Compiler(CompilerCommand::from_strings(
239+
"/home/user",
240+
"/usr/bin/gcc",
241+
vec![
242+
(
243+
ArgumentKind::Other(Some(CompilerPass::Compiling)),
244+
vec!["-c"],
245+
),
246+
(ArgumentKind::Source, vec!["main.c"]),
247+
(ArgumentKind::Output, vec!["-o", "main.o"]),
248+
],
249+
));
250+
let config = EntryFormat {
251+
keep_output_field: true,
252+
command_field_as_array: false,
253+
};
254+
let converter = CommandConverter::new(config);
255+
let entries = converter.to_entries(&command);
256+
257+
let expected = vec![Entry::from_command_str(
258+
"main.c",
259+
"/usr/bin/gcc -c main.c -o main.o",
260+
"/home/user",
261+
Some("main.o"),
262+
)];
263+
assert_eq!(entries, expected);
264+
}
265+
266+
#[test]
267+
fn test_to_entries_without_output_field() {
268+
let command = Command::Compiler(CompilerCommand::from_strings(
269+
"/home/user",
270+
"/usr/bin/gcc",
271+
vec![
272+
(
273+
ArgumentKind::Other(Some(CompilerPass::Compiling)),
274+
vec!["-c"],
275+
),
276+
(ArgumentKind::Source, vec!["main.c"]),
277+
(ArgumentKind::Output, vec!["-o", "main.o"]),
278+
],
279+
));
280+
let config = EntryFormat {
281+
command_field_as_array: true,
282+
keep_output_field: false,
283+
};
284+
let sut = CommandConverter::new(config);
285+
let result = sut.to_entries(&command);
286+
287+
let expected = vec![Entry::from_arguments_str(
288+
"main.c",
289+
vec!["/usr/bin/gcc", "-c", "main.c", "-o", "main.o"],
290+
"/home/user",
291+
None,
292+
)];
293+
assert_eq!(result, expected);
294+
}
295+
296+
#[test]
297+
fn test_command_converter_public_api() {
298+
// Test that CommandConverter can be used as a public API
299+
let config = EntryFormat {
300+
command_field_as_array: true,
301+
keep_output_field: false,
302+
};
303+
let converter = CommandConverter::new(config);
304+
305+
let compiler_cmd = CompilerCommand::from_strings(
306+
"/home/user",
307+
"/usr/bin/gcc",
308+
vec![
309+
(
310+
ArgumentKind::Other(Some(CompilerPass::Compiling)),
311+
vec!["-c"],
312+
),
313+
(ArgumentKind::Source, vec!["test.c"]),
314+
],
315+
);
316+
let command = Command::Compiler(compiler_cmd);
317+
318+
let entries = converter.to_entries(&command);
319+
320+
assert_eq!(entries.len(), 1);
321+
// Verify the entry is valid using the public API
322+
assert!(entries[0].validate().is_ok());
323+
}
324+
}

bear/src/semantic/clang/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
//! as an array. The definition of the JSON compilation database files is done in the
1212
//! LLVM project [documentation](https://clang.llvm.org/docs/JSONCompilationDatabase.html).
1313
14+
pub mod converter;
1415
mod filter;
1516

1617
use serde::{Deserialize, Serialize};
@@ -19,6 +20,7 @@ use std::path;
1920
use thiserror::Error;
2021

2122
// Re-export types for easier access
23+
pub use converter::CommandConverter;
2224
pub use filter::DuplicateEntryFilter;
2325

2426
/// Represents an entry of the compilation database.

0 commit comments

Comments
 (0)