Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/editor/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,13 @@ impl TechniqueLanguageServer {
ParsingError::InvalidHeader(_) => {
("Invalid header line".to_string(), DiagnosticSeverity::ERROR)
}
ParsingError::InvalidVersion(_) => {
("Invalid version string".to_string(), DiagnosticSeverity::ERROR)
}
ParsingError::InsufficientVersion(_, version) => (
format!("Requires technique {}", version),
DiagnosticSeverity::ERROR,
),
ParsingError::InvalidIdentifier(_, id) => (
format!("Invalid identifier '{}'", id),
DiagnosticSeverity::ERROR,
Expand Down
8 changes: 7 additions & 1 deletion src/formatting/formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,13 @@ impl<'i> Formatter<'i> {

fn format_header(&mut self, metadata: &'i Metadata) {
self.switch_syntax(Syntax::Header);
self.append_str("% technique v1\n");
self.append_str("% technique ");
self.append_str(
&metadata
.version
.to_string(),
);
self.append_char('\n');

if let Some(license) = metadata.license {
self.append_str("! ");
Expand Down
89 changes: 84 additions & 5 deletions src/language/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,88 @@ pub struct Document<'i> {
pub body: Option<Technique<'i>>,
}

#[derive(Copy, Clone, Eq, Debug, PartialEq, PartialOrd, Ord)]
pub struct Version {
pub major: u32,
pub minor: Option<u32>,
pub patch: Option<u32>,
}

impl Version {
pub const fn new(major: u32, minor: Option<u32>, patch: Option<u32>) -> Self {
Version {
major,
minor,
patch,
}
}

pub fn compiler() -> Self {
Version {
major: env!("CARGO_PKG_VERSION_MAJOR")
.parse()
.unwrap(),
minor: Some(
env!("CARGO_PKG_VERSION_MINOR")
.parse()
.unwrap(),
),
patch: Some(
env!("CARGO_PKG_VERSION_PATCH")
.parse()
.unwrap(),
),
}
}

pub fn supported_by(&self, compiler: &Version) -> bool {
// Transitional: the entire existing corpus declares v1 while the
// compiler is still at 0.x. Remove when the tooling reaches 1.0.
if compiler.major == 0
&& self.major == 1
&& self
.minor
.is_none()
{
return true;
}

if self.major != compiler.major {
return false;
}

(
self.minor
.unwrap_or(0),
self.patch
.unwrap_or(0),
) <= (
compiler
.minor
.unwrap_or(0),
compiler
.patch
.unwrap_or(0),
)
}
}

impl std::fmt::Display for Version {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "v{}", self.major)?;
if let Some(minor) = self.minor {
write!(f, ".{}", minor)?;
}
if let Some(patch) = self.patch {
write!(f, ".{}", patch)?;
}
Ok(())
}
}

#[derive(Eq, Debug)]
pub struct Metadata<'i> {
pub version: u8,
pub version: Version,
pub license: Option<&'i str>,
pub copyright: Option<&'i str>,
pub domain: Option<&'i str>,
Expand All @@ -43,7 +122,7 @@ impl PartialEq for Metadata<'_> {
impl Default for Metadata<'_> {
fn default() -> Self {
Metadata {
version: 1,
version: Version::new(1, None, None),
license: None,
copyright: None,
domain: None,
Expand Down Expand Up @@ -840,7 +919,7 @@ mod check {

fn maker<'i>() -> Metadata<'i> {
let t1 = Metadata {
version: 1,
version: Version::new(1, None, None),
license: None,
copyright: None,
domain: None,
Expand All @@ -853,7 +932,7 @@ mod check {
#[test]
fn ast_construction() {
let t1 = Metadata {
version: 1,
version: Version::new(1, None, None),
license: None,
copyright: None,
domain: None,
Expand All @@ -863,7 +942,7 @@ mod check {
assert_eq!(Metadata::default(), t1);

let t2 = Metadata {
version: 1,
version: Version::new(1, None, None),
license: Some("MIT"),
copyright: Some("ACME, Inc"),
domain: Some("checklist"),
Expand Down
78 changes: 70 additions & 8 deletions src/parsing/checks/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ fn magic_line() {
assert!(is_magic_line(input.source));

let result = input.read_magic_line();
assert_eq!(result, Ok(1));
assert_eq!(result, Ok(Version::new(1, None, None)));

input.initialize("%technique v1");
assert!(is_magic_line(input.source));

let result = input.read_magic_line();
assert_eq!(result, Ok(1));
assert_eq!(result, Ok(Version::new(1, None, None)));

input.initialize("%techniquev1");
assert!(is_magic_line(input.source));
Expand All @@ -23,6 +23,69 @@ fn magic_line() {
assert!(result.is_err());
}

#[test]
fn magic_line_semantic_version() {
let mut input = Parser::new();

// v1 stands until the compiler reaches 2.0
input.initialize("% technique v1");
let result = input.read_magic_line();
assert_eq!(result, Ok(Version::new(1, None, None)));

// v0 was the original language version and is not accepted
input.initialize("% technique v0");
let result = input.read_magic_line();
assert_eq!(result, Err(ParsingError::InvalidVersion(Span::new(12, 2))));

// a malformed version is distinguished from one that is merely too new
input.initialize("% technique v1.0.0.0");
let result = input.read_magic_line();
assert_eq!(result, Err(ParsingError::InvalidVersion(Span::new(12, 8))));

input.initialize("% technique vX");
let result = input.read_magic_line();
assert_eq!(result, Err(ParsingError::InvalidVersion(Span::new(12, 2))));

// the version is required
input.initialize("% technique");
let result = input.read_magic_line();
assert_eq!(result, Err(ParsingError::InvalidVersion(Span::new(11, 0))));

input.initialize("% technique v9999.9");
let result = input.read_magic_line();
assert_eq!(
result,
Err(ParsingError::InsufficientVersion(
Span::new(12, 7),
Version::new(9999, Some(9), None)
))
);
}

#[test]
fn version_supported_by_compiler() {
// a document asking for v1 is met by any 1.x compiler
assert!(Version::new(1, None, None).supported_by(&Version::new(1, Some(90), Some(6))));
assert!(Version::new(1, Some(3), None).supported_by(&Version::new(1, Some(90), Some(6))));
assert!(Version::new(1, Some(3), Some(1)).supported_by(&Version::new(1, Some(3), Some(1))));

// but not by an older 1.x, nor by the next major
assert!(!Version::new(1, Some(9), None).supported_by(&Version::new(1, Some(3), Some(0))));
assert!(!Version::new(1, Some(3), Some(2)).supported_by(&Version::new(1, Some(3), Some(1))));
assert!(!Version::new(1, None, None).supported_by(&Version::new(2, Some(0), Some(0))));
assert!(!Version::new(2, None, None).supported_by(&Version::new(1, Some(90), Some(6))));

// 0.x behaves the same way, so an earlier release is met by a later one
assert!(Version::new(0, Some(6), Some(6)).supported_by(&Version::new(0, Some(7), Some(0))));
assert!(Version::new(0, Some(7), None).supported_by(&Version::new(0, Some(7), Some(0))));
assert!(!Version::new(0, Some(7), Some(1)).supported_by(&Version::new(0, Some(7), Some(0))));

// while the compiler is at 0.x a bare v1 is admitted, but only bare
assert!(Version::new(1, None, None).supported_by(&Version::new(0, Some(7), Some(0))));
assert!(!Version::new(1, Some(0), None).supported_by(&Version::new(0, Some(7), Some(0))));
assert!(!Version::new(2, None, None).supported_by(&Version::new(0, Some(7), Some(0))));
}

#[test]
fn magic_line_wrong_keyword_error_position() {
// Test that error position points to the first character of the wrong keyword
Expand All @@ -41,18 +104,17 @@ fn magic_line_wrong_keyword_error_position() {
#[test]
fn magic_line_wrong_version_error_position() {
// Test that error position points to the version number after "v" in wrong version strings
assert_eq!(analyze_magic_line("% technique v0"), 13); // Points to "0" in "v0"
assert_eq!(analyze_magic_line("% technique v2"), 14); // Points to "2" in "v2" with extra space
assert_eq!(analyze_magic_line("% technique\tv0"), 13); // Points to "0" in "v0" with tab
assert_eq!(analyze_magic_line("% technique vX"), 15); // Points to "X" in "vX" with multiple spaces
assert_eq!(analyze_magic_line("% technique v99"), 13); // Points to "9" in "v99"
assert_eq!(analyze_magic_line("% technique v0.5"), 15); // Points to "0" in "v0.5" with multiple spaces
assert_eq!(analyze_magic_line("% technique\tvX"), 13); // Points to "X" in "vX" with tab
assert_eq!(analyze_magic_line("% technique v1.0.0.0"), 13); // Points to "1" when there are too many components

// Test edge case where there's no "v" at all - should point to where version should start
assert_eq!(analyze_magic_line("% technique 1.0"), 12); // Points to "1" when there's no "v"
assert_eq!(analyze_magic_line("% technique v1.0"), 14); // Points to "." when there is a "v1" but it has minor version
assert_eq!(analyze_magic_line("% technique 2"), 13); // Points to "2" when there's no "v" with extra space
assert_eq!(analyze_magic_line("% technique beta"), 12); // Points to "b" in "beta" when there's no "v"

// Test where the version is missing entirely
assert_eq!(analyze_magic_line("% technique"), 11); // Points past the keyword
}

#[test]
Expand Down
4 changes: 2 additions & 2 deletions src/parsing/checks/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ fn technique_header() {
assert_eq!(
metadata,
Ok(Metadata {
version: 1,
version: Version::new(1, None, None),
license: None,
copyright: None,
domain: None,
Expand All @@ -37,7 +37,7 @@ fn technique_header() {
assert_eq!(
metadata,
Ok(Metadata {
version: 1,
version: Version::new(1, None, None),
license: Some("MIT"),
copyright: Some("ACME, Inc"),
domain: Some("checklist"),
Expand Down
Loading