Skip to content

Commit 2cffd50

Browse files
committed
Error if trailing free-form text after responses
1 parent 3472f8d commit 2cffd50

6 files changed

Lines changed: 141 additions & 37 deletions

File tree

src/editor/server.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,10 @@ impl TechniqueLanguageServer {
770770
"Must either be a list (values) or a tablet (labelled pairs), not a mix of the two".to_string(),
771771
DiagnosticSeverity::ERROR,
772772
),
773+
ParsingError::MixedStepContent(_) => (
774+
"A step is described first and answered second, so text can't follow responses".to_string(),
775+
DiagnosticSeverity::ERROR,
776+
),
773777
ParsingError::InvalidInvocation(_) => (
774778
"Invalid procedure Invocation".to_string(),
775779
DiagnosticSeverity::ERROR,

src/language/types.rs

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -649,28 +649,6 @@ pub(crate) fn validate_genus(input: &str, span: Span) -> Option<Genus<'_>> {
649649
}
650650
}
651651

652-
pub fn validate_response(input: &str) -> Option<Response<'_>> {
653-
if input.len() == 0 {
654-
return None;
655-
}
656-
657-
// A response is the quoted value and nothing else. There has to be one,
658-
// and it must not be padded as `'Yes'` and `' Yes '` would differ but
659-
// render identically.
660-
let re = regex!(r"^'([^'\s](?:[^']*[^'\s])?)'$");
661-
let cap = re.captures(input)?;
662-
663-
let value = cap
664-
.get(1)
665-
.unwrap()
666-
.as_str();
667-
668-
Some(Response {
669-
value,
670-
span: Span::default(),
671-
})
672-
}
673-
674652
#[cfg(test)]
675653
mod check {
676654
use super::*;

src/parsing/checks/errors.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,21 @@ making_coffee :
171171
);
172172
}
173173

174+
#[test]
175+
fn invalid_text_after_responses() {
176+
expect_error(
177+
r#"
178+
making_coffee :
179+
180+
1. Do you want coffee?
181+
'Yes' | 'No'
182+
Tell the barista.
183+
"#
184+
.trim_ascii(),
185+
ParsingError::MixedStepContent(Span::new(73, 0)),
186+
);
187+
}
188+
174189
#[test]
175190
fn invalid_multiline_missing_closing() {
176191
expect_error(

src/parsing/checks/parser.rs

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,13 @@ fn response_as_expression() {
288288
Span::default()
289289
))
290290
);
291+
292+
// and the value is held to the same rule as one in an enum
293+
input.initialize("' Monarchy '");
294+
assert_eq!(
295+
input.read_expression(),
296+
Err(ParsingError::InvalidResponse(Span::new(0, 0)))
297+
);
291298
}
292299

293300
#[test]
@@ -2502,8 +2509,12 @@ fn splitting_by() {
25022509
// different split character
25032510
input.initialize("'Yes'|'No'|'Maybe'");
25042511
let result = input.take_split_by('|', |inner| {
2505-
validate_response(inner.source)
2506-
.ok_or(ParsingError::IllegalParserState(Span::new(inner.offset, 0)))
2512+
inner
2513+
.read_enum_response()
2514+
.map(|value| Response {
2515+
value,
2516+
span: Span::default(),
2517+
})
25072518
});
25082519
assert_eq!(
25092520
result,
@@ -2598,6 +2609,51 @@ fn reading_responses() {
25982609
}
25992610
])
26002611
);
2612+
2613+
// A padded or empty value is not a response
2614+
input.initialize("' Yes '");
2615+
let result = input.read_responses();
2616+
assert_eq!(result, Err(ParsingError::InvalidResponse(Span::new(0, 0))));
2617+
2618+
input.initialize("''");
2619+
let result = input.read_responses();
2620+
assert_eq!(result, Err(ParsingError::InvalidResponse(Span::new(0, 0))));
2621+
2622+
// The enum ends with its lines, leaving what follows to the enclosing
2623+
// scope, and a wrapped enum continues onto the next line
2624+
input.initialize("'Yes' | 'No'\n { x ~ y }");
2625+
let result = input.read_responses();
2626+
assert_eq!(
2627+
result,
2628+
Ok(vec![
2629+
Response {
2630+
value: "Yes",
2631+
span: Span::default()
2632+
},
2633+
Response {
2634+
value: "No",
2635+
span: Span::default()
2636+
}
2637+
])
2638+
);
2639+
assert_eq!(input.source, " { x ~ y }");
2640+
2641+
input.initialize("'Yes' |\n 'No'\n { x ~ y }");
2642+
let result = input.read_responses();
2643+
assert_eq!(
2644+
result,
2645+
Ok(vec![
2646+
Response {
2647+
value: "Yes",
2648+
span: Span::default()
2649+
},
2650+
Response {
2651+
value: "No",
2652+
span: Span::default()
2653+
}
2654+
])
2655+
);
2656+
assert_eq!(input.source, " { x ~ y }");
26012657
}
26022658

26032659
#[test]

src/parsing/parser.rs

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ pub enum ParsingError {
4545
InvalidSection(Span),
4646
MixedSectionContent(Span),
4747
MixedBracketContent(Span),
48+
MixedStepContent(Span),
4849
InvalidInvocation(Span),
4950
InvalidFunction(Span),
5051
InvalidTuple(Span),
@@ -83,6 +84,7 @@ impl ParsingError {
8384
| ParsingError::InvalidSection(span)
8485
| ParsingError::MixedSectionContent(span)
8586
| ParsingError::MixedBracketContent(span)
87+
| ParsingError::MixedStepContent(span)
8688
| ParsingError::InvalidInvocation(span)
8789
| ParsingError::InvalidFunction(span)
8890
| ParsingError::InvalidTuple(span)
@@ -1568,8 +1570,7 @@ impl<'i> Parser<'i> {
15681570
let span = self.span_since(start);
15691571
Ok(Expression::String(parts, span))
15701572
} else if is_enum_response(content) {
1571-
let value =
1572-
self.take_block_chars("a response literal", '\'', '\'', |inner| Ok(inner.source))?;
1573+
let value = self.read_enum_response()?;
15731574
let span = self.span_since(start);
15741575
Ok(Expression::Response(value, span))
15751576
} else if is_invocation(content) {
@@ -2613,19 +2614,48 @@ impl<'i> Parser<'i> {
26132614
)
26142615
}
26152616

2617+
/// Parse a single response literal like 'Yes'
2618+
fn read_enum_response(&mut self) -> Result<&'i str, ParsingError> {
2619+
let start = self.offset;
2620+
let value =
2621+
self.take_block_chars("a response literal", '\'', '\'', |inner| Ok(inner.source))?;
2622+
2623+
// There has to be a value, and it must not be padded as `'Yes'` and
2624+
// `' Yes '` would differ but render identically.
2625+
if value.is_empty() || value != value.trim_ascii() {
2626+
return Err(ParsingError::InvalidResponse(Span::new(start, 0)));
2627+
}
2628+
2629+
Ok(value)
2630+
}
2631+
26162632
/// Parse enum responses like 'Yes' | 'No' | 'Not Applicable'
26172633
fn read_responses(&mut self) -> Result<Vec<Response<'i>>, ParsingError> {
2618-
self.take_split_by('|', |inner| {
2619-
let mut resp = validate_response(inner.source)
2620-
.ok_or(ParsingError::InvalidResponse(Span::new(inner.offset, 0)))?;
2621-
resp.span = Span::new(
2622-
inner.offset,
2623-
inner
2624-
.source
2625-
.len(),
2626-
);
2627-
Ok(resp)
2628-
})
2634+
// The block is the run of lines beginning with a response literal, so
2635+
// that whatever follows the enum is left for the enclosing scope.
2636+
self.take_block_lines(
2637+
is_enum_response,
2638+
|line| !is_enum_response(line),
2639+
|outer| {
2640+
outer.take_split_by('|', |inner| {
2641+
let span = Span::new(
2642+
inner.offset,
2643+
inner
2644+
.source
2645+
.len(),
2646+
);
2647+
let value = inner.read_enum_response()?;
2648+
2649+
// a response is the literal and nothing else
2650+
inner.trim_whitespace();
2651+
if !inner.is_finished() {
2652+
return Err(ParsingError::InvalidResponse(Span::new(span.offset, 0)));
2653+
}
2654+
2655+
Ok(Response { value, span })
2656+
})
2657+
},
2658+
)
26292659
}
26302660

26312661
fn parse_multiline_content(&mut self) -> Result<(Option<&'i str>, Vec<&'i str>), ParsingError> {
@@ -2844,6 +2874,10 @@ impl<'i> Parser<'i> {
28442874
span: self.span_since(responses_start),
28452875
});
28462876
} else {
2877+
// an enum answers the step it is in, so text cannot follow it
2878+
if let Some(Scope::ResponseBlock { .. }) = scopes.last() {
2879+
return Err(ParsingError::MixedStepContent(Span::new(self.offset, 0)));
2880+
}
28472881
break;
28482882
}
28492883
}

src/problem/messages.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,23 @@ An empty List is written `[]`. An empty Tablet is written `[=]`.
515515
.trim_ascii()
516516
.to_string(),
517517
),
518+
ParsingError::MixedStepContent(_) => (
519+
"Step mixes description and responses".to_string(),
520+
r#"
521+
A step is described first and answered second, so the responses enumerating
522+
the choices valid for its result come last; free-form text cannot follow
523+
them:
524+
525+
1. Do you want coffee?
526+
'Yes' | 'No'
527+
Tell the barista.
528+
529+
The trailing sentence needs to be above the responses, or in a substep of its
530+
own.
531+
"#
532+
.trim_ascii()
533+
.to_string(),
534+
),
518535
ParsingError::InvalidInvocation(_) => {
519536
let examples = vec![
520537
Invocation {

0 commit comments

Comments
 (0)