use crate::analyzer::hadolint::types::RuleCode;
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, Default)]
pub struct PragmaState {
pub ignored: HashMap<u32, HashSet<RuleCode>>,
pub global_ignored: HashSet<RuleCode>,
pub shell: Option<String>,
}
impl PragmaState {
pub fn new() -> Self {
Self::default()
}
pub fn is_ignored(&self, code: &RuleCode, line: u32) -> bool {
if self.global_ignored.contains(code) {
return true;
}
if let Some(ignored) = self.ignored.get(&line)
&& ignored.contains(code)
{
return true;
}
if line > 0
&& let Some(ignored) = self.ignored.get(&(line - 1))
&& ignored.contains(code)
{
return true;
}
false
}
}
pub fn parse_pragma(comment: &str) -> Option<Pragma> {
let comment = comment.trim();
let pragma_start = comment.find("hadolint")?;
let pragma_content = &comment[pragma_start + "hadolint".len()..].trim();
if let Some(rest) = pragma_content.strip_prefix("global") {
let rest = rest.trim();
if let Some(codes) = parse_ignore_list(rest) {
return Some(Pragma::GlobalIgnore(codes));
}
}
if let Some(codes) = parse_ignore_list(pragma_content) {
return Some(Pragma::Ignore(codes));
}
if let Some(shell) = pragma_content.strip_prefix("shell=") {
let shell = shell.trim();
return Some(Pragma::Shell(shell.to_string()));
}
None
}
fn parse_ignore_list(s: &str) -> Option<Vec<RuleCode>> {
let s = s.trim();
if !s.starts_with("ignore=") && !s.starts_with("ignore =") {
return None;
}
let eq_pos = s.find('=')?;
let codes_str = &s[eq_pos + 1..].trim();
let codes: Vec<RuleCode> = codes_str
.split(',')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.map(RuleCode::new)
.collect();
if codes.is_empty() { None } else { Some(codes) }
}
#[derive(Debug, Clone)]
pub enum Pragma {
Ignore(Vec<RuleCode>),
GlobalIgnore(Vec<RuleCode>),
Shell(String),
}
pub fn extract_pragmas(
instructions: &[crate::analyzer::hadolint::parser::InstructionPos],
) -> PragmaState {
let mut state = PragmaState::new();
for instr in instructions {
if let crate::analyzer::hadolint::parser::instruction::Instruction::Comment(comment) =
&instr.instruction
&& let Some(pragma) = parse_pragma(comment)
{
match pragma {
Pragma::Ignore(codes) => {
let entry = state.ignored.entry(instr.line_number).or_default();
for code in codes {
entry.insert(code);
}
}
Pragma::GlobalIgnore(codes) => {
for code in codes {
state.global_ignored.insert(code);
}
}
Pragma::Shell(shell) => {
state.shell = Some(shell);
}
}
}
}
state
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_ignore() {
let pragma = parse_pragma("# hadolint ignore=DL3008,DL3009").unwrap();
match pragma {
Pragma::Ignore(codes) => {
assert_eq!(codes.len(), 2);
assert_eq!(codes[0].as_str(), "DL3008");
assert_eq!(codes[1].as_str(), "DL3009");
}
_ => panic!("Expected Ignore pragma"),
}
}
#[test]
fn test_parse_global_ignore() {
let pragma = parse_pragma("# hadolint global ignore=DL3008").unwrap();
match pragma {
Pragma::GlobalIgnore(codes) => {
assert_eq!(codes.len(), 1);
assert_eq!(codes[0].as_str(), "DL3008");
}
_ => panic!("Expected GlobalIgnore pragma"),
}
}
#[test]
fn test_parse_shell() {
let pragma = parse_pragma("# hadolint shell=/bin/bash").unwrap();
match pragma {
Pragma::Shell(shell) => {
assert_eq!(shell, "/bin/bash");
}
_ => panic!("Expected Shell pragma"),
}
}
#[test]
fn test_no_pragma() {
assert!(parse_pragma("# This is a regular comment").is_none());
}
#[test]
fn test_pragma_state_is_ignored() {
let mut state = PragmaState::new();
let mut codes = HashSet::new();
codes.insert(RuleCode::new("DL3008"));
state.ignored.insert(5, codes);
state.global_ignored.insert(RuleCode::new("DL3009"));
assert!(state.is_ignored(&RuleCode::new("DL3008"), 6));
assert!(!state.is_ignored(&RuleCode::new("DL3008"), 10));
assert!(state.is_ignored(&RuleCode::new("DL3009"), 1));
assert!(state.is_ignored(&RuleCode::new("DL3009"), 100));
assert!(!state.is_ignored(&RuleCode::new("DL3010"), 1));
}
}