#![allow(clippy::pub_use, clippy::exhaustive_enums)]
use std::cmp::Ordering;
use std::iter::Once;
use std::str::FromStr;
use foldhash::HashMap;
use foldhash::HashMapExt;
use regex::Regex;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
use strum::VariantNames;
use mago_database::GlobSettings;
use mago_database::file::FileId;
use mago_database::matcher::ExclusionMatcher;
use mago_span::Span;
use mago_text_edit::TextEdit;
mod formatter;
mod internal;
pub mod baseline;
pub mod color;
pub mod error;
pub mod output;
pub mod reporter;
pub use color::ColorChoice;
pub use formatter::ReportingFormat;
pub use output::ReportingTarget;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum IgnoreEntry {
Code(String),
Scoped {
code: String,
#[serde(rename = "in", deserialize_with = "one_or_many")]
paths: Vec<String>,
},
Pattern {
pattern: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
code: Option<String>,
#[serde(rename = "in", default, skip_serializing_if = "Option::is_none", deserialize_with = "opt_one_or_many")]
paths: Option<Vec<String>>,
},
}
fn one_or_many<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum OneOrMany {
One(String),
Many(Vec<String>),
}
match OneOrMany::deserialize(deserializer)? {
OneOrMany::One(s) => Ok(vec![s]),
OneOrMany::Many(v) => Ok(v),
}
}
fn opt_one_or_many<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(Some(one_or_many(deserializer)?))
}
#[derive(Debug, Default)]
pub struct CompiledIgnoreSet {
entries: Vec<CompiledIgnoreEntry>,
}
#[derive(Debug)]
enum CompiledIgnoreEntry {
Code(String),
Scoped { code: String, matcher: ExclusionMatcher<String> },
Pattern { regex: Regex, code: Option<String>, matcher: Option<ExclusionMatcher<String>> },
}
#[derive(Debug, PartialEq, Eq, Ord, Copy, Clone, Hash, PartialOrd, Deserialize, Serialize)]
pub enum AnnotationKind {
Primary,
Secondary,
}
#[derive(Debug, PartialEq, Eq, Ord, Clone, Hash, PartialOrd, Deserialize, Serialize)]
pub struct Annotation {
pub message: Option<String>,
pub kind: AnnotationKind,
pub span: Span,
}
#[derive(
Debug, PartialEq, Eq, Ord, Copy, Clone, Hash, PartialOrd, Deserialize, Serialize, Display, VariantNames, JsonSchema,
)]
#[strum(serialize_all = "lowercase")]
pub enum Level {
#[serde(alias = "note")]
Note,
#[serde(alias = "help")]
Help,
#[serde(alias = "warning", alias = "warn")]
Warning,
#[serde(alias = "error", alias = "err")]
Error,
}
impl FromStr for Level {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"note" => Ok(Self::Note),
"help" => Ok(Self::Help),
"warning" => Ok(Self::Warning),
"error" => Ok(Self::Error),
_ => Err(()),
}
}
}
type IssueEdits = Vec<TextEdit>;
type IssueEditBatches = Vec<(Option<String>, IssueEdits)>;
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct Issue {
pub level: Level,
pub code: Option<String>,
pub message: String,
pub notes: Vec<String>,
pub help: Option<String>,
pub link: Option<String>,
pub annotations: Vec<Annotation>,
pub edits: HashMap<FileId, IssueEdits>,
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct IssueCollection {
issues: Vec<Issue>,
}
impl AnnotationKind {
#[inline]
#[must_use]
pub const fn is_primary(&self) -> bool {
matches!(self, AnnotationKind::Primary)
}
#[inline]
#[must_use]
pub const fn is_secondary(&self) -> bool {
matches!(self, AnnotationKind::Secondary)
}
}
impl CompiledIgnoreSet {
#[must_use]
pub fn compile(entries: &[IgnoreEntry], glob: GlobSettings) -> Self {
let mut compiled = Vec::with_capacity(entries.len());
for entry in entries {
match entry {
IgnoreEntry::Code(code) => compiled.push(CompiledIgnoreEntry::Code(code.clone())),
IgnoreEntry::Scoped { code, paths } => match ExclusionMatcher::compile(paths.iter().cloned(), glob) {
Ok(matcher) => compiled.push(CompiledIgnoreEntry::Scoped { code: code.clone(), matcher }),
Err(err) => {
tracing::error!("Failed to compile ignore patterns for `{code}`: {err}. Entry will be skipped.")
}
},
IgnoreEntry::Pattern { pattern, code, paths } => {
let regex = match Regex::new(pattern) {
Ok(regex) => regex,
Err(err) => {
tracing::error!(
"Failed to compile ignore regex `{pattern}`: {err}. Entry will be skipped."
);
continue;
}
};
let matcher = match paths {
Some(paths) => match ExclusionMatcher::compile(paths.iter().cloned(), glob) {
Ok(matcher) => Some(matcher),
Err(err) => {
tracing::error!(
"Failed to compile ignore paths for regex `{pattern}`: {err}. Entry will be skipped."
);
continue;
}
},
None => None,
};
compiled.push(CompiledIgnoreEntry::Pattern { regex, code: code.clone(), matcher });
}
}
}
Self { entries: compiled }
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
}
impl Annotation {
#[must_use]
pub fn new(kind: AnnotationKind, span: Span) -> Self {
Self { message: None, kind, span }
}
#[must_use]
pub fn primary(span: Span) -> Self {
Self::new(AnnotationKind::Primary, span)
}
#[must_use]
pub fn secondary(span: Span) -> Self {
Self::new(AnnotationKind::Secondary, span)
}
#[must_use]
pub fn with_message(mut self, message: impl Into<String>) -> Self {
self.message = Some(message.into());
self
}
#[must_use]
pub fn is_primary(&self) -> bool {
self.kind == AnnotationKind::Primary
}
}
impl Level {
#[must_use]
pub fn downgrade(&self) -> Self {
match self {
Level::Error => Level::Warning,
Level::Warning => Level::Help,
Level::Help | Level::Note => Level::Note,
}
}
}
impl Issue {
pub fn new(level: Level, message: impl Into<String>) -> Self {
Self {
level,
code: None,
message: message.into(),
annotations: Vec::new(),
notes: Vec::new(),
help: None,
link: None,
edits: HashMap::default(),
}
}
pub fn error(message: impl Into<String>) -> Self {
Self::new(Level::Error, message)
}
pub fn warning(message: impl Into<String>) -> Self {
Self::new(Level::Warning, message)
}
pub fn help(message: impl Into<String>) -> Self {
Self::new(Level::Help, message)
}
pub fn note(message: impl Into<String>) -> Self {
Self::new(Level::Note, message)
}
#[must_use]
pub fn with_code(mut self, code: impl Into<String>) -> Self {
self.code = Some(code.into());
self
}
#[must_use]
pub fn with_annotation(mut self, annotation: Annotation) -> Self {
self.annotations.push(annotation);
self
}
#[must_use]
pub fn with_annotations(mut self, annotation: impl IntoIterator<Item = Annotation>) -> Self {
self.annotations.extend(annotation);
self
}
#[must_use]
pub fn primary_annotation(&self) -> Option<&Annotation> {
self.annotations.iter().filter(|annotation| annotation.is_primary()).min_by_key(|annotation| annotation.span)
}
#[must_use]
pub fn primary_span(&self) -> Option<Span> {
self.primary_annotation().map(|annotation| annotation.span)
}
#[must_use]
pub fn with_note(mut self, note: impl Into<String>) -> Self {
self.notes.push(note.into());
self
}
#[must_use]
pub fn with_help(mut self, help: impl Into<String>) -> Self {
self.help = Some(help.into());
self
}
#[must_use]
pub fn with_link(mut self, link: impl Into<String>) -> Self {
self.link = Some(link.into());
self
}
#[must_use]
pub fn with_edit(mut self, file_id: FileId, edit: TextEdit) -> Self {
self.edits.entry(file_id).or_default().push(edit);
self
}
#[must_use]
pub fn with_file_edits(mut self, file_id: FileId, edits: IssueEdits) -> Self {
if !edits.is_empty() {
self.edits.entry(file_id).or_default().extend(edits);
}
self
}
#[must_use]
pub fn take_edits(&mut self) -> HashMap<FileId, IssueEdits> {
std::mem::replace(&mut self.edits, HashMap::with_capacity(0))
}
}
impl IssueCollection {
#[must_use]
pub fn new() -> Self {
Self { issues: Vec::new() }
}
pub fn from(issues: impl IntoIterator<Item = Issue>) -> Self {
Self { issues: issues.into_iter().collect() }
}
pub fn push(&mut self, issue: Issue) {
self.issues.push(issue);
}
pub fn extend(&mut self, issues: impl IntoIterator<Item = Issue>) {
self.issues.extend(issues);
}
pub fn reserve(&mut self, additional: usize) {
self.issues.reserve(additional);
}
pub fn shrink_to_fit(&mut self) {
self.issues.shrink_to_fit();
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.issues.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.issues.len()
}
#[must_use]
pub fn with_maximum_level(self, level: Level) -> Self {
Self { issues: self.issues.into_iter().filter(|issue| issue.level <= level).collect() }
}
#[must_use]
pub fn with_minimum_level(self, level: Level) -> Self {
Self { issues: self.issues.into_iter().filter(|issue| issue.level >= level).collect() }
}
#[must_use]
pub fn has_minimum_level(&self, level: Level) -> bool {
self.issues.iter().any(|issue| issue.level >= level)
}
#[must_use]
pub fn get_level_count(&self, level: Level) -> usize {
self.issues.iter().filter(|issue| issue.level == level).count()
}
#[must_use]
pub fn get_highest_level(&self) -> Option<Level> {
self.issues.iter().map(|issue| issue.level).max()
}
#[must_use]
pub fn get_lowest_level(&self) -> Option<Level> {
self.issues.iter().map(|issue| issue.level).min()
}
pub fn filter_out_ignored<F>(&mut self, set: &CompiledIgnoreSet, resolve_file_name: F)
where
F: Fn(FileId) -> Option<String>,
{
if set.is_empty() {
return;
}
self.issues.retain(|issue| {
let mut cached_path: Option<Option<String>> = None;
let mut resolve_path = |issue: &Issue| -> Option<String> {
cached_path
.get_or_insert_with(|| issue.primary_span().and_then(|span| resolve_file_name(span.file_id)))
.clone()
};
for entry in &set.entries {
match entry {
CompiledIgnoreEntry::Code(ignored_code) => {
if let Some(code) = &issue.code
&& ignored_code == code
{
return false;
}
}
CompiledIgnoreEntry::Scoped { code: ignored_code, matcher } => {
let Some(code) = &issue.code else {
continue;
};
if ignored_code != code {
continue;
}
if let Some(name) = resolve_path(issue)
&& matcher.is_match(&name)
{
return false;
}
}
CompiledIgnoreEntry::Pattern { regex, code: ignored_code, matcher } => {
if let Some(ignored_code) = ignored_code {
let Some(code) = &issue.code else {
continue;
};
if ignored_code != code {
continue;
}
}
if let Some(matcher) = matcher {
let Some(name) = resolve_path(issue) else {
continue;
};
if !matcher.is_match(&name) {
continue;
}
}
if issue_text_matches(issue, regex) {
return false;
}
}
}
}
true
});
}
pub fn filter_retain_codes(&mut self, retain_codes: &[String]) {
self.issues.retain(|issue| if let Some(code) = &issue.code { retain_codes.contains(code) } else { false });
}
pub fn take_edits(&mut self) -> impl Iterator<Item = (FileId, IssueEdits)> + '_ {
self.issues.iter_mut().flat_map(|issue| issue.take_edits().into_iter())
}
#[must_use]
pub fn with_edits(self) -> Self {
Self { issues: self.issues.into_iter().filter(|issue| !issue.edits.is_empty()).collect() }
}
#[must_use]
pub fn sorted(self) -> Self {
let mut issues = self.issues;
issues.sort_by(|a, b| match a.level.cmp(&b.level) {
Ordering::Greater => Ordering::Greater,
Ordering::Less => Ordering::Less,
Ordering::Equal => match a.code.as_deref().cmp(&b.code.as_deref()) {
Ordering::Less => Ordering::Less,
Ordering::Greater => Ordering::Greater,
Ordering::Equal => {
let a_span = a.primary_span();
let b_span = b.primary_span();
match (a_span, b_span) {
(Some(a_span), Some(b_span)) => a_span.cmp(&b_span),
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(None, None) => Ordering::Equal,
}
}
},
});
Self { issues }
}
pub fn iter(&self) -> impl Iterator<Item = &Issue> {
self.issues.iter()
}
#[must_use]
pub fn to_edit_batches(self) -> HashMap<FileId, IssueEditBatches> {
let mut result: HashMap<FileId, Vec<(Option<String>, IssueEdits)>> = HashMap::default();
for issue in self.issues.into_iter().filter(|issue| !issue.edits.is_empty()) {
let code = issue.code;
for (file_id, edit_list) in issue.edits {
result.entry(file_id).or_default().push((code.clone(), edit_list));
}
}
result
}
}
fn issue_text_matches(issue: &Issue, regex: &Regex) -> bool {
if regex.is_match(&issue.message) {
return true;
}
if issue
.annotations
.iter()
.any(|annotation| annotation.message.as_ref().is_some_and(|message| regex.is_match(message)))
{
return true;
}
if issue.notes.iter().any(|note| regex.is_match(note)) {
return true;
}
issue.help.as_ref().is_some_and(|help| regex.is_match(help))
}
impl IntoIterator for IssueCollection {
type Item = Issue;
type IntoIter = std::vec::IntoIter<Issue>;
fn into_iter(self) -> Self::IntoIter {
self.issues.into_iter()
}
}
impl<'collection> IntoIterator for &'collection IssueCollection {
type Item = &'collection Issue;
type IntoIter = std::slice::Iter<'collection, Issue>;
fn into_iter(self) -> Self::IntoIter {
self.issues.iter()
}
}
impl Default for IssueCollection {
fn default() -> Self {
Self::new()
}
}
impl IntoIterator for Issue {
type Item = Issue;
type IntoIter = Once<Issue>;
fn into_iter(self) -> Self::IntoIter {
std::iter::once(self)
}
}
impl FromIterator<Issue> for IssueCollection {
fn from_iter<T>(iter: T) -> Self
where
T: IntoIterator<Item = Issue>,
{
Self { issues: iter.into_iter().collect() }
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::*;
#[test]
pub fn test_highest_collection_level() {
let mut collection = IssueCollection::from(vec![]);
assert_eq!(collection.get_highest_level(), None);
collection.push(Issue::note("note"));
assert_eq!(collection.get_highest_level(), Some(Level::Note));
collection.push(Issue::help("help"));
assert_eq!(collection.get_highest_level(), Some(Level::Help));
collection.push(Issue::warning("warning"));
assert_eq!(collection.get_highest_level(), Some(Level::Warning));
collection.push(Issue::error("error"));
assert_eq!(collection.get_highest_level(), Some(Level::Error));
}
#[test]
pub fn test_level_downgrade() {
assert_eq!(Level::Error.downgrade(), Level::Warning);
assert_eq!(Level::Warning.downgrade(), Level::Help);
assert_eq!(Level::Help.downgrade(), Level::Note);
assert_eq!(Level::Note.downgrade(), Level::Note);
}
#[test]
pub fn test_issue_collection_with_maximum_level() {
let mut collection = IssueCollection::from(vec![
Issue::error("error"),
Issue::warning("warning"),
Issue::help("help"),
Issue::note("note"),
]);
collection = collection.with_maximum_level(Level::Warning);
assert_eq!(collection.len(), 3);
assert_eq!(
collection.iter().map(|issue| issue.level).collect::<Vec<_>>(),
vec![Level::Warning, Level::Help, Level::Note]
);
}
#[test]
pub fn test_issue_collection_with_minimum_level() {
let mut collection = IssueCollection::from(vec![
Issue::error("error"),
Issue::warning("warning"),
Issue::help("help"),
Issue::note("note"),
]);
collection = collection.with_minimum_level(Level::Warning);
assert_eq!(collection.len(), 2);
assert_eq!(collection.iter().map(|issue| issue.level).collect::<Vec<_>>(), vec![Level::Error, Level::Warning,]);
}
#[test]
pub fn test_issue_collection_has_minimum_level() {
let mut collection = IssueCollection::from(vec![]);
assert!(!collection.has_minimum_level(Level::Error));
assert!(!collection.has_minimum_level(Level::Warning));
assert!(!collection.has_minimum_level(Level::Help));
assert!(!collection.has_minimum_level(Level::Note));
collection.push(Issue::note("note"));
assert!(!collection.has_minimum_level(Level::Error));
assert!(!collection.has_minimum_level(Level::Warning));
assert!(!collection.has_minimum_level(Level::Help));
assert!(collection.has_minimum_level(Level::Note));
collection.push(Issue::help("help"));
assert!(!collection.has_minimum_level(Level::Error));
assert!(!collection.has_minimum_level(Level::Warning));
assert!(collection.has_minimum_level(Level::Help));
assert!(collection.has_minimum_level(Level::Note));
collection.push(Issue::warning("warning"));
assert!(!collection.has_minimum_level(Level::Error));
assert!(collection.has_minimum_level(Level::Warning));
assert!(collection.has_minimum_level(Level::Help));
assert!(collection.has_minimum_level(Level::Note));
collection.push(Issue::error("error"));
assert!(collection.has_minimum_level(Level::Error));
assert!(collection.has_minimum_level(Level::Warning));
assert!(collection.has_minimum_level(Level::Help));
assert!(collection.has_minimum_level(Level::Note));
}
#[test]
pub fn test_issue_collection_level_count() {
let mut collection = IssueCollection::from(vec![]);
assert_eq!(collection.get_level_count(Level::Error), 0);
assert_eq!(collection.get_level_count(Level::Warning), 0);
assert_eq!(collection.get_level_count(Level::Help), 0);
assert_eq!(collection.get_level_count(Level::Note), 0);
collection.push(Issue::error("error"));
assert_eq!(collection.get_level_count(Level::Error), 1);
assert_eq!(collection.get_level_count(Level::Warning), 0);
assert_eq!(collection.get_level_count(Level::Help), 0);
assert_eq!(collection.get_level_count(Level::Note), 0);
collection.push(Issue::warning("warning"));
assert_eq!(collection.get_level_count(Level::Error), 1);
assert_eq!(collection.get_level_count(Level::Warning), 1);
assert_eq!(collection.get_level_count(Level::Help), 0);
assert_eq!(collection.get_level_count(Level::Note), 0);
collection.push(Issue::help("help"));
assert_eq!(collection.get_level_count(Level::Error), 1);
assert_eq!(collection.get_level_count(Level::Warning), 1);
assert_eq!(collection.get_level_count(Level::Help), 1);
assert_eq!(collection.get_level_count(Level::Note), 0);
collection.push(Issue::note("note"));
assert_eq!(collection.get_level_count(Level::Error), 1);
assert_eq!(collection.get_level_count(Level::Warning), 1);
assert_eq!(collection.get_level_count(Level::Help), 1);
assert_eq!(collection.get_level_count(Level::Note), 1);
}
#[test]
pub fn test_primary_span_is_deterministic() {
let file = FileId::zero();
let span_later = Span::new(file, 20u32.into(), 25u32.into());
let span_earlier = Span::new(file, 5u32.into(), 10u32.into());
let issue = Issue::error("x")
.with_annotation(Annotation::primary(span_later))
.with_annotation(Annotation::primary(span_earlier));
assert_eq!(issue.primary_span(), Some(span_earlier));
}
fn ignore_fixture() -> (IssueCollection, HashMap<FileId, &'static [u8]>) {
let file_id = |name: &[u8]| FileId::new(name);
let paths: [&[u8]; 4] =
[b"src/App.php", b"tests/Unit/FooTest.php", b"modules/auth/views/login.tpl", b"types/user/form.tpl"];
let mut mapping = HashMap::new();
let issues: Vec<Issue> = paths
.iter()
.map(|p| {
let id = file_id(p);
mapping.insert(id, *p);
Issue::error("oops").with_code("invalid-global").with_annotation(Annotation::primary(Span::new(
id,
0u32.into(),
1u32.into(),
)))
})
.collect();
(IssueCollection::from(issues), mapping)
}
fn resolve<'mapping>(
mapping: &'mapping HashMap<FileId, &'static [u8]>,
) -> impl Fn(FileId) -> Option<String> + 'mapping {
move |id| mapping.get(&id).map(|s| String::from_utf8_lossy(s).into_owned())
}
fn remaining_paths(collection: &IssueCollection, mapping: &HashMap<FileId, &'static [u8]>) -> Vec<String> {
collection
.iter()
.filter_map(|issue| issue.primary_span().and_then(|s| mapping.get(&s.file_id)).copied())
.map(|bytes| String::from_utf8_lossy(bytes).into_owned())
.collect()
}
#[test]
pub fn test_filter_out_ignored_with_plain_prefix() {
let (mut collection, mapping) = ignore_fixture();
let entries =
vec![IgnoreEntry::Scoped { code: "invalid-global".to_string(), paths: vec!["tests/".to_string()] }];
let set = CompiledIgnoreSet::compile(&entries, GlobSettings::default());
collection.filter_out_ignored(&set, resolve(&mapping));
assert_eq!(
remaining_paths(&collection, &mapping),
vec![
"src/App.php".to_string(),
"modules/auth/views/login.tpl".to_string(),
"types/user/form.tpl".to_string(),
]
);
}
#[test]
pub fn test_filter_out_ignored_with_glob_pattern() {
let (mut collection, mapping) = ignore_fixture();
let entries = vec![IgnoreEntry::Scoped {
code: "invalid-global".to_string(),
paths: vec!["modules/*/*/*.tpl".to_string(), "types/*/*.tpl".to_string()],
}];
let set = CompiledIgnoreSet::compile(&entries, GlobSettings::default());
collection.filter_out_ignored(&set, resolve(&mapping));
assert_eq!(
remaining_paths(&collection, &mapping),
vec!["src/App.php".to_string(), "tests/Unit/FooTest.php".to_string()]
);
}
#[test]
pub fn test_filter_out_ignored_mixes_plain_and_glob() {
let (mut collection, mapping) = ignore_fixture();
let entries = vec![IgnoreEntry::Scoped {
code: "invalid-global".to_string(),
paths: vec!["tests/".to_string(), "modules/*/*/*.tpl".to_string(), "types/*/*.tpl".to_string()],
}];
let set = CompiledIgnoreSet::compile(&entries, GlobSettings::default());
collection.filter_out_ignored(&set, resolve(&mapping));
assert_eq!(remaining_paths(&collection, &mapping), vec!["src/App.php".to_string()]);
}
#[test]
pub fn test_filter_out_ignored_respects_code_scope() {
let (mut collection, mapping) = ignore_fixture();
let entries = vec![IgnoreEntry::Scoped { code: "different-code".to_string(), paths: vec!["**/*".to_string()] }];
let set = CompiledIgnoreSet::compile(&entries, GlobSettings::default());
collection.filter_out_ignored(&set, resolve(&mapping));
assert_eq!(collection.len(), 4);
}
fn pattern_fixture() -> (IssueCollection, HashMap<FileId, &'static [u8]>) {
let paths: [&[u8]; 3] = [b"src/App.php", b"src/Bridge/Symfony.php", b"tests/Unit/FooTest.php"];
let mut mapping = HashMap::new();
let mut issues: Vec<Issue> = Vec::new();
let id0 = FileId::new(blake3::hash(paths[0]).as_bytes());
mapping.insert(id0, paths[0]);
issues.push(
Issue::error("Saw type `mixed` in Symfony bridge.")
.with_code("mixed-assignment")
.with_annotation(Annotation::primary(Span::new(id0, 0u32.into(), 1u32.into()))),
);
let id1 = FileId::new(blake3::hash(paths[1]).as_bytes());
mapping.insert(id1, paths[1]);
issues.push(
Issue::error("Could not infer a precise return type.")
.with_code("mixed-assignment")
.with_note("Originates from Symfony vendor stubs.")
.with_annotation(Annotation::primary(Span::new(id1, 0u32.into(), 1u32.into()))),
);
let id2 = FileId::new(blake3::hash(paths[2]).as_bytes());
mapping.insert(id2, paths[2]);
issues.push(
Issue::error("Unused variable.")
.with_code("unused-variable")
.with_annotation(Annotation::primary(Span::new(id2, 0u32.into(), 1u32.into()))),
);
(IssueCollection::from(issues), mapping)
}
#[test]
pub fn test_pattern_matches_title_and_note() {
let (mut collection, mapping) = pattern_fixture();
let entries = vec![IgnoreEntry::Pattern {
pattern: "Symfony".to_string(),
code: Some("mixed-assignment".to_string()),
paths: None,
}];
let set = CompiledIgnoreSet::compile(&entries, GlobSettings::default());
collection.filter_out_ignored(&set, resolve(&mapping));
assert_eq!(remaining_paths(&collection, &mapping), vec!["tests/Unit/FooTest.php".to_string()]);
}
#[test]
pub fn test_pattern_without_code_matches_across_codes() {
let (mut collection, mapping) = pattern_fixture();
let entries = vec![IgnoreEntry::Pattern { pattern: "Symfony".to_string(), code: None, paths: None }];
let set = CompiledIgnoreSet::compile(&entries, GlobSettings::default());
collection.filter_out_ignored(&set, resolve(&mapping));
assert_eq!(remaining_paths(&collection, &mapping), vec!["tests/Unit/FooTest.php".to_string()]);
}
#[test]
pub fn test_pattern_with_path_scope() {
let (mut collection, mapping) = pattern_fixture();
let entries = vec![IgnoreEntry::Pattern {
pattern: "Symfony".to_string(),
code: None,
paths: Some(vec!["src/Bridge/".to_string()]),
}];
let set = CompiledIgnoreSet::compile(&entries, GlobSettings::default());
collection.filter_out_ignored(&set, resolve(&mapping));
assert_eq!(
remaining_paths(&collection, &mapping),
vec!["src/App.php".to_string(), "tests/Unit/FooTest.php".to_string()]
);
}
#[test]
pub fn test_pattern_case_insensitive_with_flag() {
let (mut collection, mapping) = pattern_fixture();
let entries = vec![IgnoreEntry::Pattern { pattern: "(?i)symfony".to_string(), code: None, paths: None }];
let set = CompiledIgnoreSet::compile(&entries, GlobSettings::default());
collection.filter_out_ignored(&set, resolve(&mapping));
assert_eq!(remaining_paths(&collection, &mapping), vec!["tests/Unit/FooTest.php".to_string()]);
}
#[test]
pub fn test_pattern_invalid_regex_is_skipped() {
let (mut collection, mapping) = pattern_fixture();
let entries = vec![
IgnoreEntry::Pattern { pattern: "[unterminated".to_string(), code: None, paths: None },
IgnoreEntry::Code("unused-variable".to_string()),
];
let set = CompiledIgnoreSet::compile(&entries, GlobSettings::default());
assert_eq!(set.len(), 1);
collection.filter_out_ignored(&set, resolve(&mapping));
assert_eq!(
remaining_paths(&collection, &mapping),
vec!["src/App.php".to_string(), "src/Bridge/Symfony.php".to_string()]
);
}
#[test]
pub fn test_pattern_matches_help_message() {
let id = FileId::new(blake3::hash(b"src/foo.php").as_bytes());
let mut mapping: HashMap<FileId, &'static [u8]> = HashMap::new();
mapping.insert(id, &b"src/foo.php"[..]);
let mut collection = IssueCollection::from(vec![
Issue::error("Title.")
.with_code("some-code")
.with_help("Consider migrating off legacy Symfony bridge.")
.with_annotation(Annotation::primary(Span::new(id, 0u32.into(), 1u32.into()))),
]);
let entries = vec![IgnoreEntry::Pattern { pattern: "Symfony".to_string(), code: None, paths: None }];
let set = CompiledIgnoreSet::compile(&entries, GlobSettings::default());
collection.filter_out_ignored(&set, resolve(&mapping));
assert!(collection.is_empty());
}
}