use super::ast::*;
use super::error::{MakeParseError, SourceLocation};
use std::collections::HashMap;
struct PreprocessingResult {
text: String,
recipe_metadata_map: HashMap<usize, Vec<(usize, String)>>,
}
fn preprocess_line_continuations_with_metadata(input: &str) -> PreprocessingResult {
let mut result = String::new();
let mut recipe_metadata_map = HashMap::new();
let lines: Vec<&str> = input.lines().collect();
let mut i = 0;
let mut preprocessed_line_num = 0;
while i < lines.len() {
let mut line = lines[i].to_string();
let mut breaks: Vec<(usize, String)> = Vec::new();
while line.trim_end().ends_with('\\') && i + 1 < lines.len() {
let break_position = line
.trim_end()
.strip_suffix('\\')
.expect("backslash suffix verified by while condition")
.trim_end()
.len();
line = line
.trim_end()
.strip_suffix('\\')
.expect("backslash suffix verified by while condition")
.trim_end()
.to_string();
i += 1;
let next_line_full = lines[i];
let original_indent = next_line_full
.chars()
.take_while(|c| c.is_whitespace())
.collect::<String>();
let next_line = next_line_full.trim_start();
breaks.push((break_position, original_indent));
line.push(' ');
line.push_str(next_line);
}
if !breaks.is_empty() {
recipe_metadata_map.insert(preprocessed_line_num, breaks);
}
result.push_str(&line);
result.push('\n');
i += 1;
preprocessed_line_num += 1;
}
PreprocessingResult {
text: result,
recipe_metadata_map,
}
}
fn preprocess_line_continuations(input: &str) -> String {
preprocess_line_continuations_with_metadata(input).text
}
fn is_empty_line(line: &str) -> bool {
line.trim().is_empty()
}
fn is_comment_line(line: &str) -> bool {
line.trim_start().starts_with('#')
}
fn is_include_directive(line: &str) -> bool {
line.trim_start().starts_with("include ")
|| line.trim_start().starts_with("-include ")
|| line.trim_start().starts_with("sinclude ")
}
fn is_conditional_directive(line: &str) -> bool {
line.trim_start().starts_with("ifeq ")
|| line.trim_start().starts_with("ifdef ")
|| line.trim_start().starts_with("ifndef ")
|| line.trim_start().starts_with("ifneq ")
}
fn is_define_directive(line: &str) -> bool {
line.trim_start().starts_with("define ")
}
fn is_target_rule(line: &str) -> bool {
line.contains(':') && !line.trim_start().starts_with('\t')
}
fn parse_comment_line(line: &str, line_num: usize) -> MakeItem {
let text = line
.trim_start()
.strip_prefix('#')
.unwrap_or("")
.trim()
.to_string();
MakeItem::Comment {
text,
span: Span::new(0, line.len(), line_num),
}
}
fn try_add_item(
items: &mut Vec<MakeItem>,
result: Result<MakeItem, MakeParseError>,
) -> Result<(), String> {
match result {
Ok(item) => {
items.push(item);
Ok(())
}
Err(e) => Err(e.to_detailed_string()),
}
}
fn try_parse_include(line: &str, line_num: usize) -> Option<Result<MakeItem, MakeParseError>> {
if is_include_directive(line) {
Some(parse_include(line, line_num))
} else {
None
}
}
fn try_parse_variable(line: &str, line_num: usize) -> Option<Result<MakeItem, MakeParseError>> {
if is_variable_assignment(line) {
Some(parse_variable(line, line_num))
} else {
None
}
}
fn try_parse_comment(line: &str, line_num: usize) -> Option<MakeItem> {
if is_comment_line(line) {
Some(parse_comment_line(line, line_num))
} else {
None
}
}
fn should_skip_line(line: &str) -> bool {
is_empty_line(line)
}
fn parse_makefile_items(
lines: &[&str],
metadata_map: &HashMap<usize, Vec<(usize, String)>>,
) -> Result<Vec<MakeItem>, String> {
let mut items = Vec::new();
let mut i = 0;
while i < lines.len() {
let line = lines[i];
let line_num = i + 1;
if should_skip_line(line) {
i += 1;
continue;
}
if let Some(comment) = try_parse_comment(line, line_num) {
items.push(comment);
i += 1;
continue;
}
if let Some(result) = try_parse_include(line, line_num) {
try_add_item(&mut items, result)?;
i += 1;
continue;
}
if is_conditional_directive(line) {
try_add_item(&mut items, parse_conditional(lines, &mut i, metadata_map))?;
continue;
}
if is_define_directive(line) {
try_add_item(&mut items, parse_define_block(lines, &mut i))?;
continue;
}
if let Some(result) = try_parse_variable(line, line_num) {
try_add_item(&mut items, result)?;
i += 1;
continue;
}
if is_target_rule(line) {
try_add_item(&mut items, parse_target_rule(lines, &mut i, metadata_map))?;
continue;
}
i += 1;
}
Ok(items)
}
fn collect_phony_targets(items: &[MakeItem]) -> std::collections::HashSet<String> {
let mut phony_targets = std::collections::HashSet::new();
for item in items {
if let MakeItem::Target {
name,
prerequisites,
..
} = item
{
if name == ".PHONY" {
for prereq in prerequisites {
phony_targets.insert(prereq.clone());
}
}
}
}
phony_targets
}
fn mark_phony_targets(
items: Vec<MakeItem>,
phony_targets: &std::collections::HashSet<String>,
) -> Vec<MakeItem> {
items
.into_iter()
.map(|item| {
if let MakeItem::Target {
name,
prerequisites,
recipe,
phony: _,
recipe_metadata,
span,
} = item
{
MakeItem::Target {
phony: phony_targets.contains(&name),
name,
prerequisites,
recipe,
recipe_metadata,
span,
}
} else {
item
}
})
.collect()
}
pub fn parse_makefile(input: &str) -> Result<MakeAst, String> {
let preprocessing = preprocess_line_continuations_with_metadata(input);
let lines: Vec<&str> = preprocessing.text.lines().collect();
let line_count = lines.len();
let mut items = parse_makefile_items(&lines, &preprocessing.recipe_metadata_map)?;
let phony_targets = collect_phony_targets(&items);
items = mark_phony_targets(items, &phony_targets);
Ok(MakeAst {
items,
metadata: MakeMetadata::with_line_count(line_count),
})
}
include!("parser_is_variable.rs");