use std::fmt::{Display, Formatter};
use std::hash::{Hash, Hasher};
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::string::ToString;
use anyhow::{Context, Result, bail};
use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
use log::debug;
use pep440_rs::{VersionSpecifier, VersionSpecifiers};
use ruff_db::diagnostic::DiagnosticFormat;
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use strum_macros::EnumIter;
use ruff_cache::{CacheKey, CacheKeyHasher};
use ruff_macros::CacheKey;
use ruff_python_ast::{self as ast, PySourceType, SourceType};
use crate::Applicability;
use crate::preview::is_warn_on_unknown_selectors_enabled;
use crate::registry::RuleSet;
use crate::rule_selector::UnresolvedRuleSelector;
use crate::{display_settings, fs};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, EnumIter)]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[serde(rename_all = "lowercase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum PythonVersion {
Py37,
Py38,
Py39,
Py310,
Py311,
Py312,
Py313,
Py314,
Py315,
}
impl Default for PythonVersion {
fn default() -> Self {
Self::try_from(ast::PythonVersion::default()).unwrap()
}
}
impl TryFrom<ast::PythonVersion> for PythonVersion {
type Error = String;
fn try_from(value: ast::PythonVersion) -> Result<Self, Self::Error> {
match value {
ast::PythonVersion::PY37 => Ok(Self::Py37),
ast::PythonVersion::PY38 => Ok(Self::Py38),
ast::PythonVersion::PY39 => Ok(Self::Py39),
ast::PythonVersion::PY310 => Ok(Self::Py310),
ast::PythonVersion::PY311 => Ok(Self::Py311),
ast::PythonVersion::PY312 => Ok(Self::Py312),
ast::PythonVersion::PY313 => Ok(Self::Py313),
ast::PythonVersion::PY314 => Ok(Self::Py314),
ast::PythonVersion::PY315 => Ok(Self::Py315),
_ => Err(format!("unrecognized python version {value}")),
}
}
}
impl From<PythonVersion> for ast::PythonVersion {
fn from(value: PythonVersion) -> Self {
let (major, minor) = value.as_tuple();
Self { major, minor }
}
}
impl From<PythonVersion> for pep440_rs::Version {
fn from(version: PythonVersion) -> Self {
let (major, minor) = version.as_tuple();
Self::new([u64::from(major), u64::from(minor)])
}
}
impl PythonVersion {
pub const fn as_tuple(&self) -> (u8, u8) {
match self {
Self::Py37 => (3, 7),
Self::Py38 => (3, 8),
Self::Py39 => (3, 9),
Self::Py310 => (3, 10),
Self::Py311 => (3, 11),
Self::Py312 => (3, 12),
Self::Py313 => (3, 13),
Self::Py314 => (3, 14),
Self::Py315 => (3, 15),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, CacheKey, is_macro::Is)]
pub enum PreviewMode {
#[default]
Disabled,
Enabled,
}
impl From<bool> for PreviewMode {
fn from(version: bool) -> Self {
if version {
PreviewMode::Enabled
} else {
PreviewMode::Disabled
}
}
}
impl Display for PreviewMode {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Disabled => write!(f, "disabled"),
Self::Enabled => write!(f, "enabled"),
}
}
}
#[derive(Debug, Copy, Clone, CacheKey, Default, PartialEq, Eq, is_macro::Is)]
pub enum UnsafeFixes {
#[default]
Hint,
Disabled,
Enabled,
}
impl Display for UnsafeFixes {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Hint => "hint",
Self::Disabled => "disabled",
Self::Enabled => "enabled",
}
)
}
}
impl From<bool> for UnsafeFixes {
fn from(value: bool) -> Self {
if value {
UnsafeFixes::Enabled
} else {
UnsafeFixes::Disabled
}
}
}
impl UnsafeFixes {
pub fn required_applicability(&self) -> Applicability {
match self {
Self::Enabled => Applicability::Unsafe,
Self::Disabled | Self::Hint => Applicability::Safe,
}
}
}
#[derive(Debug, Clone, CacheKey, PartialEq, PartialOrd, Eq, Ord)]
pub struct GlobPath {
path: PathBuf,
}
impl GlobPath {
pub fn normalize(path: impl AsRef<Path>, root: impl AsRef<Path>) -> Self {
let root = root.as_ref().to_string_lossy();
let escaped = globset::escape(&root);
let absolute = fs::normalize_path_to(path, escaped);
Self { path: absolute }
}
}
impl Deref for GlobPath {
type Target = PathBuf;
fn deref(&self) -> &Self::Target {
&self.path
}
}
#[derive(Debug, Clone, CacheKey, PartialEq, PartialOrd, Eq, Ord)]
pub enum FilePattern {
Builtin(&'static str),
Config(String),
User(String, GlobPath),
}
impl FilePattern {
pub fn add_to(self, builder: &mut GlobSetBuilder) -> Result<()> {
match self {
FilePattern::Builtin(pattern) => {
builder.add(Glob::from_str(pattern)?);
}
FilePattern::Config(pattern) => {
builder.add(Glob::new(&pattern)?);
}
FilePattern::User(pattern, absolute) => {
builder.add(Glob::new(&absolute.to_string_lossy())?);
if !pattern.contains(std::path::MAIN_SEPARATOR) {
builder.add(Glob::new(&pattern)?);
}
}
}
Ok(())
}
}
impl Display for FilePattern {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{:?}",
match self {
Self::Builtin(pattern) => pattern,
Self::User(pattern, _) | Self::Config(pattern) => pattern.as_str(),
}
)
}
}
impl FromStr for FilePattern {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::User(
s.to_string(),
GlobPath::normalize(s, fs::get_cwd()),
))
}
}
#[derive(Debug, Clone, Default)]
pub struct FilePatternSet {
set: GlobSet,
cache_key: u64,
#[expect(clippy::used_underscore_binding)]
_set_internals: Vec<FilePattern>,
}
impl FilePatternSet {
#[expect(clippy::used_underscore_binding)]
pub fn try_from_iter<I>(patterns: I) -> Result<Self, anyhow::Error>
where
I: IntoIterator<Item = FilePattern>,
{
let mut builder = GlobSetBuilder::new();
let mut hasher = CacheKeyHasher::new();
let mut _set_internals = vec![];
for pattern in patterns {
_set_internals.push(pattern.clone());
pattern.cache_key(&mut hasher);
pattern.add_to(&mut builder)?;
}
let set = builder.build()?;
Ok(FilePatternSet {
set,
cache_key: hasher.finish(),
_set_internals,
})
}
}
impl Display for FilePatternSet {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if self._set_internals.is_empty() {
write!(f, "[]")?;
} else {
writeln!(f, "[")?;
for pattern in &self._set_internals {
writeln!(f, "\t{pattern},")?;
}
write!(f, "]")?;
}
Ok(())
}
}
impl Deref for FilePatternSet {
type Target = GlobSet;
fn deref(&self) -> &Self::Target {
&self.set
}
}
impl CacheKey for FilePatternSet {
fn cache_key(&self, state: &mut CacheKeyHasher) {
state.write_usize(self.set.len());
state.write_u64(self.cache_key);
}
}
#[derive(Debug, Clone)]
pub struct PerFile<T> {
basename: String,
absolute: GlobPath,
negated: bool,
data: T,
}
impl<T> PerFile<T> {
fn new(mut pattern: String, project_root: Option<&Path>, data: T) -> Self {
let negated = pattern.starts_with('!');
if negated {
pattern.drain(..1);
}
let project_root = project_root.unwrap_or(fs::get_cwd());
Self {
absolute: GlobPath::normalize(&pattern, project_root),
basename: pattern,
negated,
data,
}
}
}
#[derive(Debug, Clone)]
pub struct PerFileIgnore(PerFile<Vec<UnresolvedRuleSelector>>);
impl PerFileIgnore {
pub fn new(
pattern: String,
selectors: Vec<UnresolvedRuleSelector>,
project_root: Option<&Path>,
) -> Self {
Self(PerFile::new(pattern, project_root, selectors))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PatternPrefixPair {
pub pattern: String,
pub prefix: UnresolvedRuleSelector,
}
impl PatternPrefixPair {
const EXPECTED_PATTERN: &'static str = "<FilePattern>:<RuleCode> pattern";
}
impl FromStr for PatternPrefixPair {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (pattern_str, code_string) = {
let tokens = s.split(':').collect::<Vec<_>>();
if tokens.len() != 2 {
bail!("Expected {}", Self::EXPECTED_PATTERN);
}
(tokens[0].trim(), tokens[1].trim())
};
let pattern = pattern_str.into();
let prefix = UnresolvedRuleSelector::cli(code_string);
Ok(Self { pattern, prefix })
}
}
#[derive(
Clone,
Copy,
Debug,
PartialOrd,
Ord,
PartialEq,
Eq,
Default,
Serialize,
Deserialize,
CacheKey,
EnumIter,
)]
#[serde(rename_all = "lowercase")]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum Language {
#[default]
Python,
Pyi,
Ipynb,
Markdown,
}
impl FromStr for Language {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"python" => Ok(Self::Python),
"pyi" => Ok(Self::Pyi),
"ipynb" => Ok(Self::Ipynb),
"md" => Ok(Self::Markdown),
_ => {
bail!("Unrecognized language: `{s}`. Expected one of `python`, `pyi`, or `ipynb`.")
}
}
}
}
impl From<Language> for SourceType {
fn from(value: Language) -> Self {
match value {
Language::Python => Self::Python(PySourceType::Python),
Language::Ipynb => Self::Python(PySourceType::Ipynb),
Language::Pyi => Self::Python(PySourceType::Stub),
Language::Markdown => Self::Markdown,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ExtensionPair {
pub extension: String,
pub language: Language,
}
impl ExtensionPair {
const EXPECTED_PATTERN: &'static str = "<Extension>:<LanguageCode> pattern";
}
impl FromStr for ExtensionPair {
type Err = anyhow::Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let (extension_str, language_str) = {
let tokens = s.split(':').collect::<Vec<_>>();
if tokens.len() != 2 {
bail!("Expected {}", Self::EXPECTED_PATTERN);
}
(tokens[0].trim(), tokens[1].trim())
};
let extension = extension_str.into();
let language = Language::from_str(language_str)?;
Ok(Self {
extension,
language,
})
}
}
impl From<ExtensionPair> for (String, Language) {
fn from(value: ExtensionPair) -> Self {
(value.extension, value.language)
}
}
#[derive(Debug, Clone, Default, CacheKey)]
pub struct ExtensionMapping(FxHashMap<String, Language>);
impl ExtensionMapping {
pub fn extensions(&self) -> impl Iterator<Item = &String> {
self.0.keys()
}
fn get(&self, path: &Path) -> Option<Language> {
let ext = path.extension()?.to_str()?;
self.0.get(ext).copied()
}
fn get_extension(&self, ext: &str) -> Option<Language> {
self.0.get(ext).copied()
}
pub fn get_source_type(&self, path: &Path) -> SourceType {
match self.get(path) {
None => SourceType::from(path),
Some(language) => SourceType::from(language),
}
}
pub fn get_source_type_by_extension(&self, ext: &str) -> SourceType {
match self.get_extension(ext) {
None => SourceType::from_extension(ext),
Some(language) => SourceType::from(language),
}
}
}
impl From<FxHashMap<String, Language>> for ExtensionMapping {
fn from(value: FxHashMap<String, Language>) -> Self {
Self(value)
}
}
impl FromIterator<ExtensionPair> for ExtensionMapping {
fn from_iter<T: IntoIterator<Item = ExtensionPair>>(iter: T) -> Self {
Self(
iter.into_iter()
.map(|pair| (pair.extension, pair.language))
.collect(),
)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Debug, Hash, Default)]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum OutputFormat {
Concise,
#[default]
Full,
Json,
JsonLines,
Junit,
Grouped,
Github,
Gitlab,
Pylint,
Rdjson,
Azure,
Sarif,
}
impl OutputFormat {
pub const fn is_human_readable(&self) -> bool {
matches!(
self,
OutputFormat::Full | OutputFormat::Concise | OutputFormat::Grouped
)
}
}
impl Display for OutputFormat {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Concise => write!(f, "concise"),
Self::Full => write!(f, "full"),
Self::Json => write!(f, "json"),
Self::JsonLines => write!(f, "json_lines"),
Self::Junit => write!(f, "junit"),
Self::Grouped => write!(f, "grouped"),
Self::Github => write!(f, "github"),
Self::Gitlab => write!(f, "gitlab"),
Self::Pylint => write!(f, "pylint"),
Self::Rdjson => write!(f, "rdjson"),
Self::Azure => write!(f, "azure"),
Self::Sarif => write!(f, "sarif"),
}
}
}
pub enum RuffOutputFormat {
Grouped,
Sarif,
}
impl TryFrom<OutputFormat> for DiagnosticFormat {
type Error = RuffOutputFormat;
fn try_from(format: OutputFormat) -> std::result::Result<Self, Self::Error> {
match format {
OutputFormat::Concise => Ok(DiagnosticFormat::Concise),
OutputFormat::Full => Ok(DiagnosticFormat::Full),
OutputFormat::Json => Ok(DiagnosticFormat::Json),
OutputFormat::JsonLines => Ok(DiagnosticFormat::JsonLines),
OutputFormat::Junit => Ok(DiagnosticFormat::Junit),
OutputFormat::Gitlab => Ok(DiagnosticFormat::Gitlab),
OutputFormat::Pylint => Ok(DiagnosticFormat::Pylint),
OutputFormat::Rdjson => Ok(DiagnosticFormat::Rdjson),
OutputFormat::Azure => Ok(DiagnosticFormat::Azure),
OutputFormat::Github => Ok(DiagnosticFormat::Github),
OutputFormat::Grouped => Err(RuffOutputFormat::Grouped),
OutputFormat::Sarif => Err(RuffOutputFormat::Sarif),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)]
#[serde(try_from = "String")]
pub struct RequiredVersion(VersionSpecifiers);
impl TryFrom<String> for RequiredVersion {
type Error = pep440_rs::VersionSpecifiersParseError;
fn try_from(value: String) -> Result<Self, Self::Error> {
value.parse()
}
}
impl FromStr for RequiredVersion {
type Err = pep440_rs::VersionSpecifiersParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
if let Ok(version) = pep440_rs::Version::from_str(value) {
Ok(Self(VersionSpecifiers::from(
VersionSpecifier::equals_version(version),
)))
} else {
Ok(Self(VersionSpecifiers::from_str(value)?))
}
}
}
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for RequiredVersion {
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("RequiredVersion")
}
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
<String as schemars::JsonSchema>::json_schema(generator)
}
}
impl RequiredVersion {
pub fn contains(&self, version: &pep440_rs::Version) -> bool {
self.0.contains(version)
}
}
impl Display for RequiredVersion {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.0, f)
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, CacheKey)]
pub enum IdentifierPattern {
Literal(String),
Glob(Box<glob::Pattern>),
}
impl IdentifierPattern {
pub fn new(pattern: &str) -> Result<Self, glob::PatternError> {
if pattern.contains(['?', '*', '[']) {
Ok(Self::Glob(Box::new(glob::Pattern::new(pattern)?)))
} else {
Ok(Self::Literal(pattern.to_string()))
}
}
pub(crate) fn matches(&self, candidate: &str) -> bool {
match self {
Self::Literal(literal) => literal == candidate,
Self::Glob(pattern) => pattern.matches(candidate),
}
}
pub(crate) fn as_str(&self) -> &str {
match self {
Self::Literal(literal) => literal,
Self::Glob(pattern) => pattern.as_str(),
}
}
}
impl Display for IdentifierPattern {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for IdentifierPattern {
type Err = glob::PatternError;
fn from_str(pattern: &str) -> Result<Self, Self::Err> {
Self::new(pattern)
}
}
#[derive(Debug, Clone)]
pub struct CompiledPerFile<T> {
absolute_matcher: GlobMatcher,
basename_matcher: GlobMatcher,
negated: bool,
data: T,
}
impl<T> CompiledPerFile<T> {
fn new(
absolute_matcher: GlobMatcher,
basename_matcher: GlobMatcher,
negated: bool,
data: T,
) -> Self {
Self {
absolute_matcher,
basename_matcher,
negated,
data,
}
}
}
impl<T> CacheKey for CompiledPerFile<T>
where
T: CacheKey,
{
fn cache_key(&self, state: &mut CacheKeyHasher) {
self.absolute_matcher.cache_key(state);
self.basename_matcher.cache_key(state);
self.negated.cache_key(state);
self.data.cache_key(state);
}
}
impl<T> Display for CompiledPerFile<T>
where
T: Display,
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
display_settings! {
formatter = f,
fields = [
self.absolute_matcher | globmatcher,
self.basename_matcher | globmatcher,
self.negated,
self.data,
]
}
Ok(())
}
}
#[derive(Debug, Clone, Default)]
pub struct CompiledPerFileList<T> {
inner: Vec<CompiledPerFile<T>>,
}
impl<T> CacheKey for CompiledPerFileList<T>
where
T: CacheKey,
{
fn cache_key(&self, state: &mut CacheKeyHasher) {
self.inner.cache_key(state);
}
}
impl<T> CompiledPerFileList<T> {
fn resolve(per_file_items: impl IntoIterator<Item = PerFile<T>>) -> Result<Self> {
let inner: Result<Vec<_>> = per_file_items
.into_iter()
.map(|per_file_ignore| {
let absolute_matcher = Glob::new(&per_file_ignore.absolute.to_string_lossy())
.with_context(|| format!("invalid glob {:?}", per_file_ignore.absolute))?
.compile_matcher();
let basename_matcher = Glob::new(&per_file_ignore.basename)
.with_context(|| format!("invalid glob {:?}", per_file_ignore.basename))?
.compile_matcher();
Ok(CompiledPerFile::new(
absolute_matcher,
basename_matcher,
per_file_ignore.negated,
per_file_ignore.data,
))
})
.collect();
Ok(Self { inner: inner? })
}
pub(crate) fn is_empty(&self) -> bool {
self.inner.is_empty()
}
}
impl<T: std::fmt::Debug> CompiledPerFileList<T> {
pub(crate) fn iter_matches<'a, 'p>(
&'a self,
path: &'p Path,
debug_label: &'static str,
) -> impl Iterator<Item = &'p T>
where
'a: 'p,
{
let file_name = path.file_name().expect("Unable to parse filename");
self.inner.iter().filter_map(move |entry| {
if entry.basename_matcher.is_match(file_name) {
if entry.negated {
None
} else {
debug!(
"{} for {:?} due to basename match on {:?}: {:?}",
debug_label,
path,
entry.basename_matcher.glob().regex(),
entry.data
);
Some(&entry.data)
}
} else if entry.absolute_matcher.is_match(path) {
if entry.negated {
None
} else {
debug!(
"{} for {:?} due to absolute match on {:?}: {:?}",
debug_label,
path,
entry.absolute_matcher.glob().regex(),
entry.data
);
Some(&entry.data)
}
} else if entry.negated {
debug!(
"{} for {:?} due to negated pattern matching neither {:?} nor {:?}: {:?}",
debug_label,
path,
entry.basename_matcher.glob().regex(),
entry.absolute_matcher.glob().regex(),
entry.data
);
Some(&entry.data)
} else {
None
}
})
}
}
impl<T> Display for CompiledPerFileList<T>
where
T: Display,
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if self.inner.is_empty() {
write!(f, "{{}}")?;
} else {
writeln!(f, "{{")?;
for value in &self.inner {
writeln!(f, "\t{value}")?;
}
write!(f, "}}")?;
}
Ok(())
}
}
#[derive(Debug, Clone, CacheKey, Default)]
pub struct CompiledPerFileIgnoreList(CompiledPerFileList<RuleSet>);
impl CompiledPerFileIgnoreList {
pub fn resolve(per_file_ignores: Vec<PerFileIgnore>, preview: PreviewMode) -> Result<Self> {
let mut resolution_error = None;
let list = CompiledPerFileList::resolve(per_file_ignores.into_iter().map(|ignore| {
let PerFile {
basename,
absolute,
negated,
data: selectors,
} = ignore.0;
let data: RuleSet = selectors
.iter()
.filter_map(|selector| match selector.resolve(preview) {
Ok(selector) => Some(selector),
Err(mut err) => {
err = err.with_setting("per-file-ignores");
if is_warn_on_unknown_selectors_enabled(preview) {
err.log_warning();
} else {
resolution_error.get_or_insert(err);
}
None
}
})
.flat_map(|selector| selector.all_rules())
.collect();
PerFile {
basename,
absolute,
negated,
data,
}
}))?;
if let Some(error) = resolution_error {
return Err(error.into());
}
Ok(Self(list))
}
}
impl Deref for CompiledPerFileIgnoreList {
type Target = CompiledPerFileList<RuleSet>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Display for CompiledPerFileIgnoreList {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
#[derive(Debug, Clone)]
pub struct PerFileTargetVersion(PerFile<ast::PythonVersion>);
impl PerFileTargetVersion {
pub fn new(pattern: String, version: ast::PythonVersion, project_root: Option<&Path>) -> Self {
Self(PerFile::new(pattern, project_root, version))
}
}
#[derive(CacheKey, Clone, Debug, Default)]
pub struct CompiledPerFileTargetVersionList(CompiledPerFileList<ast::PythonVersion>);
impl CompiledPerFileTargetVersionList {
pub fn resolve(per_file_versions: Vec<PerFileTargetVersion>) -> Result<Self> {
Ok(Self(CompiledPerFileList::resolve(
per_file_versions.into_iter().map(|version| version.0),
)?))
}
pub fn is_match(&self, path: &Path) -> Option<ast::PythonVersion> {
self.0
.iter_matches(path, "Setting Python version")
.next()
.copied()
}
}
impl Display for CompiledPerFileTargetVersionList {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
#[cfg(test)]
mod tests {
use super::IdentifierPattern;
#[test]
fn default_python_version_works() {
super::PythonVersion::default();
}
#[test]
fn identifier_pattern_matches_literals_exactly() {
let pattern = IdentifierPattern::new("package.module").unwrap();
assert!(pattern.matches("package.module"));
assert!(!pattern.matches("package"));
assert!(!pattern.matches("package.module.extra"));
}
#[test]
fn identifier_pattern_preserves_glob_matching() {
let pattern = IdentifierPattern::new("package.*").unwrap();
assert!(pattern.matches("package.module"));
assert!(!pattern.matches("other.module"));
}
#[test]
fn identifier_pattern_rejects_invalid_globs() {
assert!(IdentifierPattern::new("package[").is_err());
}
}