use std::ops::Range;
use std::sync::OnceLock;
use chumsky::Parser;
use semver::{Version, VersionReq};
use crate::error::{Error, Span};
pub(crate) const SIMC_STR: &str = "simc";
#[derive(Clone, Debug, Eq, PartialEq)]
enum DirectiveScan<'a> {
Found { range: &'a str, span: Range<usize> },
Malformed { span: Range<usize> },
Absent,
}
pub struct SimcDirective;
impl SimcDirective {
pub fn current_version() -> &'static str {
env!("CARGO_PKG_VERSION")
}
pub(crate) fn prescan(content: &str, file_id: usize) -> Result<usize, (Error, Span)> {
match Self::scan(content) {
DirectiveScan::Absent => Ok(0),
DirectiveScan::Malformed { span } => {
Err((Error::MalformedSimcDirective, Span::new(file_id, span)))
}
DirectiveScan::Found { range, span } => {
Self::validate(range, Span::new(file_id, span.clone()))?;
Ok(span.end)
}
}
}
pub fn requirement_of(content: &str) -> Result<Option<VersionRequirement>, String> {
match Self::scan(content) {
DirectiveScan::Found { range, .. } => VersionRequirement::parse(range.trim()).map(Some),
DirectiveScan::Malformed { .. } => Err(Error::MalformedSimcDirective.to_string()),
DirectiveScan::Absent => Ok(None),
}
}
pub fn missing_warning(content: &str) -> Option<String> {
matches!(Self::scan(content), DirectiveScan::Absent).then(|| {
let base_version = Self::current_version()
.split('-')
.next()
.expect("split yields at least one part");
format!(
"no compiler version directive at the top of the file; consider adding `{SIMC_STR} \"{base_version}\";`",
)
})
}
fn scan(content: &str) -> DirectiveScan<'_> {
let offset = |rest: &str| content.len() - rest.len();
let start = Self::skip_trivia(content);
let Some(rest) = content[start..].strip_prefix(SIMC_STR) else {
return DirectiveScan::Absent;
};
let Some(rest) = rest.trim_start_matches([' ', '\t']).strip_prefix('"') else {
return DirectiveScan::Absent;
};
let Some(quote) = rest
.find(['"', '\n'])
.filter(|&i| rest[i..].starts_with('"'))
else {
let line_end = rest.find('\n').unwrap_or(rest.len());
return DirectiveScan::Malformed {
span: start..offset(rest) + line_end,
};
};
let (range, rest) = (&rest[..quote], &rest[quote + 1..]);
let rest = rest.trim_start_matches([' ', '\t']);
let Some(rest) = rest.strip_prefix(';') else {
return DirectiveScan::Malformed {
span: start..offset(rest),
};
};
DirectiveScan::Found {
range,
span: start..offset(rest),
}
}
fn validate(required: &str, span: Span) -> Result<(), (Error, Span)> {
let required = required.trim();
let req = VersionRequirement::parse(required)
.map_err(|e| (Error::InvalidSimcVersionSyntax { err: e }, span))?;
if !req.matches(Self::current_semver()) {
let err = Error::SimcVersionMismatch {
required: required.to_string(),
current: Self::current_version().to_string(),
};
return Err((err, span));
}
Ok(())
}
fn current_semver() -> &'static Version {
static CURRENT: OnceLock<Version> = OnceLock::new();
CURRENT.get_or_init(|| {
Version::parse(Self::current_version()).expect("CARGO_PKG_VERSION is valid semver")
})
}
fn skip_trivia(content: &str) -> usize {
crate::lexer::trivia()
.to_slice()
.lazy()
.parse(content)
.into_output()
.map_or(0, str::len)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VersionRequirement {
req: VersionReq,
}
impl VersionRequirement {
pub fn parse(s: &str) -> Result<Self, String> {
let req = VersionReq::parse(s).map_err(|e| e.to_string())?;
if req.comparators.iter().any(|c| !c.pre.is_empty()) {
return Err(
"pre-release identifiers are not allowed in version requirements".to_string(),
);
}
Ok(VersionRequirement { req })
}
pub fn req(&self) -> &VersionReq {
&self.req
}
#[allow(rustdoc::private_intra_doc_links)]
pub fn matches(&self, version: &Version) -> bool {
self.req.matches(&Self::effective_version(version))
}
fn effective_version(version: &Version) -> Version {
Version {
pre: semver::Prerelease::EMPTY,
..version.clone()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::driver::MAIN_MODULE;
#[test]
fn scan_directive_cases() {
let found = |content: &str, range: &str, directive: &str| match SimcDirective::scan(content)
{
DirectiveScan::Found { range: r, span } => {
assert_eq!(r, range, "wrong range in {content:?}");
assert_eq!(&content[span], directive, "wrong span in {content:?}");
}
other => panic!("expected Found in {content:?}, got {other:?}"),
};
let malformed = |content: &str, broken: &str| match SimcDirective::scan(content) {
DirectiveScan::Malformed { span } => {
assert_eq!(&content[span], broken, "wrong span in {content:?}");
}
other => panic!("expected Malformed in {content:?}, got {other:?}"),
};
found(
"simc \">=0.6.0\";\nfn main() {}",
">=0.6.0",
"simc \">=0.6.0\";",
);
found("simc \"*\" ;rest", "*", "simc \"*\" ;");
found(
"// note\n/* outer /* inner */ outer */\n\nsimc \"1.0\";",
"1.0",
"simc \"1.0\";",
);
malformed("simc \"1.0\"\nfn f() {}", "simc \"1.0\"");
malformed("simc \"1.0\nfn f() {}", "simc \"1.0");
malformed("simc \"1.0\"", "simc \"1.0\"");
assert_eq!(SimcDirective::scan("simc"), DirectiveScan::Absent);
assert_eq!(
SimcDirective::scan("simcfoo \"1.0\";"),
DirectiveScan::Absent
);
assert_eq!(SimcDirective::scan("fn main() {}"), DirectiveScan::Absent);
assert_eq!(
SimcDirective::scan("fn f() {}\nsimc \"1.0\";"),
DirectiveScan::Absent
);
assert_eq!(
SimcDirective::scan("/* unterminated\nsimc \"1.0\";"),
DirectiveScan::Absent
);
}
#[test]
fn prescan_validates_and_returns_offset() {
let src = "// c\nsimc \"*\";\nfn main() {}";
let DirectiveScan::Found { span, .. } = SimcDirective::scan(src) else {
panic!("expected a directive in the test source");
};
let start = SimcDirective::prescan(src, MAIN_MODULE).unwrap();
assert_eq!(
start, span.end,
"lexing must start right after the directive"
);
assert_eq!(
SimcDirective::prescan("fn main() {}", MAIN_MODULE).unwrap(),
0
);
assert!(matches!(
SimcDirective::prescan("simc \"1.0\"\nfn main() {}", MAIN_MODULE)
.unwrap_err()
.0,
Error::MalformedSimcDirective
));
assert!(matches!(
SimcDirective::prescan("simc \">99.0.0\";", MAIN_MODULE)
.unwrap_err()
.0,
Error::SimcVersionMismatch { .. }
));
}
#[test]
fn matches_respects_operators_and_prerelease() {
let cur = Version::parse("0.6.0-rc.0").unwrap();
let accepted = ["*", "0.6.0", "^0.6.0", "~0.6.0", ">=0.6.0", ">0.1.0"];
let rejected = [
"=0.5.0",
">99.0.0",
"<0.0.1",
"<0.6.0", ">=0.7.0, =0.6.0", ];
for req in accepted {
let req = VersionRequirement::parse(req).unwrap();
assert!(req.matches(&cur), "`{req:?}` should match {cur}");
}
for req in rejected {
let parsed = VersionRequirement::parse(req).unwrap();
assert!(!parsed.matches(&cur), "`{req}` should not match {cur}");
}
}
#[test]
fn prerelease_ranges_rejected() {
for req in ["=0.6.0-rc.0", "^0.6.0-rc.0", ">=0.1.0-alpha.1"] {
assert!(
VersionRequirement::parse(req).is_err(),
"`{req}` must be rejected"
);
}
assert!(matches!(
SimcDirective::validate("=0.6.0-rc.0", Span::new(MAIN_MODULE, 0..1))
.unwrap_err()
.0,
Error::InvalidSimcVersionSyntax { .. }
));
}
#[test]
fn validate_reports_bad_and_incompatible() {
let span = Span::new(MAIN_MODULE, 0..1);
assert!(matches!(
SimcDirective::validate("not-a-version", span)
.unwrap_err()
.0,
Error::InvalidSimcVersionSyntax { .. }
));
assert!(matches!(
SimcDirective::validate(">=99.0.0", span).unwrap_err().0,
Error::SimcVersionMismatch { .. }
));
assert!(SimcDirective::validate("*", span).is_ok());
}
#[test]
fn missing_warning_and_requirement_of() {
assert!(SimcDirective::missing_warning("fn main() {}").is_some());
assert!(SimcDirective::missing_warning("simc \"*\";\nfn main() {}").is_none());
assert!(SimcDirective::missing_warning("// note\nsimc \"*\";\nfn main() {}").is_none());
assert!(SimcDirective::missing_warning("simc \"*\"\nfn main() {}").is_none());
assert_eq!(
SimcDirective::requirement_of("simc \">=0.1.0\";\nfn main() {}")
.unwrap()
.map(|r| r.req().clone()),
Some(VersionReq::parse(">=0.1.0").unwrap())
);
assert_eq!(SimcDirective::requirement_of("fn main() {}"), Ok(None));
assert!(SimcDirective::requirement_of("simc \"*\"\nfn main() {}").is_err());
assert!(SimcDirective::requirement_of("simc \"not-a-version\";").is_err());
}
}