use crate::{ceprintln, PatternReader};
use sysexits::{ExitCode, Result};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GraphDescription {
buffer: String,
comment_depth: usize,
count_identifiers: usize,
count_strings: usize,
identifiers: Vec<String>,
line: usize,
pending_token: Option<Tokens>,
position: usize,
string_literals: Vec<String>,
tokens: Vec<Tokens>,
}
impl GraphDescription {
crate::getters!(@fn @ref
identifiers: Vec<String>,
string_literals: Vec<String>,
tokens: Vec<Tokens>
);
fn assume_comment(&mut self) {
self.comment_depth += 1;
self.pending_token = Some(Tokens::Comment);
}
fn assume_identifier(&mut self, character: char) {
self.buffer.push(character);
self.pending_token = Some(Tokens::Identifier(self.count_identifiers));
}
fn assume_line_feed(&mut self) {
self.line += 1;
self.position = 0;
self.pending_token = Some(Tokens::LineFeed(1));
}
pub fn check_for_syntax_issues(&self) -> Result<usize> {
let mut result = 0;
if self.starts_with_obsolete_spaces()? {
result += 1;
}
if self.has_no_trailing_line_feed()? {
result += 1;
}
Ok(result)
}
pub fn check_for_typos(&self) -> Result<usize> {
let mut result = 0;
for token in &self.tokens {
match token {
Tokens::Unexpected {
character,
line,
position,
} => {
result += 1;
ceprintln!(
" Typo "!Green,
"'{character}' in line {line} at position {position}."
);
}
_ => continue,
}
}
Ok(result)
}
pub fn check_line_width(&self, input: &str) -> Result<usize> {
let mut column = 0;
let mut line = 1;
let mut result = 0;
for character in input.chars() {
if character == '\n' {
if column > 80 {
result += 1;
ceprintln!(
" Line "!Yellow,
"{line} is {} characters too long.",
column - 80
);
}
column = 0;
line += 1;
} else {
column += 1;
}
}
Ok(result)
}
fn finalise_pending_token(&mut self, token: Tokens, character: char) {
self.tokens.push(token);
self.pending_token = None;
self.match_character(character);
}
fn has_no_trailing_line_feed(&self) -> Result<bool> {
if matches!(self.tokens.last(), Some(Tokens::LineFeed(_)) | None) {
Ok(false)
} else {
ceprintln!(
"Syntax "!Red,
"rule violation: each source file must be ended by line feeds."
);
Ok(true)
}
}
pub fn main(input: &Option<std::path::PathBuf>) -> Result<()> {
let mut agd = Self::new();
let input = input.read()?.try_into_string()?;
let lines = agd.check_line_width(&input)?;
agd.read(&input)?;
let typos = agd.check_for_typos()?;
let syntax = agd.check_for_syntax_issues()?;
let sum = lines + syntax + typos;
if sum == 0 {
Ok(())
} else {
ceprintln!(
"Failed "!Red,
"due to {sum} issue{} to fix.",
if sum == 1 { "" } else { "s" }
);
Err(ExitCode::DataErr)
}
}
fn match_character(&mut self, character: char) {
match character {
'\n' => self.assume_line_feed(),
' ' => self.pending_token = Some(Tokens::Space(1)),
'"' => {
self.pending_token = Some(Tokens::StringLiteral(self.count_strings));
}
'(' => self.assume_comment(),
'.' => self.tokens.push(Tokens::FullStop),
'A'..='Z' | 'a'..='z' => self.assume_identifier(character),
_ => self.push_unexpected(character),
}
}
#[must_use]
pub const fn new() -> Self {
Self {
buffer: String::new(),
comment_depth: 0,
count_identifiers: 0,
count_strings: 0,
identifiers: Vec::new(),
line: 1,
pending_token: None,
position: 0,
string_literals: Vec::new(),
tokens: Vec::new(),
}
}
fn push_identifier(&mut self) {
let identifier = self.buffer.clone();
self.buffer.clear();
self.pending_token = None;
match identifier.as_str() {
"Abbreviate" => self.tokens.push(Tokens::Abbreviate),
"Connect" => self.tokens.push(Tokens::Connect),
"Declare" => self.tokens.push(Tokens::Declare),
"and" => self.tokens.push(Tokens::And),
"by" => self.tokens.push(Tokens::By),
_ => {
self.identifiers.push(identifier);
self.tokens.push(Tokens::Identifier(self.count_identifiers));
self.count_identifiers += 1;
}
}
}
fn push_string(&mut self) {
self.string_literals.push(self.buffer.clone());
self.tokens.push(Tokens::StringLiteral(self.count_strings));
self.buffer.clear();
self.pending_token = None;
self.count_strings += 1;
}
fn push_unexpected(&mut self, character: char) {
self.tokens.push(Tokens::Unexpected {
character,
line: self.line,
position: self.position,
});
}
pub fn read(&mut self, s: &str) -> Result<()> {
for character in s.chars() {
self.position += 1;
match self.pending_token {
Some(token) => match token {
Tokens::Comment => match character {
'(' => {
self.comment_depth += 1;
}
')' => {
self.comment_depth -= 1;
if self.comment_depth == 0 {
self.tokens.push(token);
self.pending_token = None;
}
}
_ => continue,
},
Tokens::Identifier(_) => {
if matches!(
character,
'0'..='9'
| 'A'..='Z'
| 'a'..='z'
| '_'
| '-'
) {
self.buffer.push(character);
} else {
self.push_identifier();
self.match_character(character);
}
}
Tokens::LineFeed(n) => {
if character == '\n' {
self.line += 1;
self.position = 0;
self.pending_token = Some(Tokens::LineFeed(n + 1));
} else {
self.finalise_pending_token(token, character);
}
}
Tokens::Space(n) => {
if character == ' ' {
self.pending_token = Some(Tokens::Space(n + 1));
} else {
self.finalise_pending_token(token, character);
}
}
Tokens::StringLiteral(_) => {
if character == '"' {
self.push_string();
} else {
self.buffer.push(character);
}
}
_ => unreachable!(),
},
None => self.match_character(character),
}
}
if self.pending_token.is_none() {
Ok(())
} else {
match self.pending_token {
Some(token) => {
if matches!(token, Tokens::LineFeed(_) | Tokens::Space(_)) {
self.tokens.push(token);
self.pending_token = None;
Ok(())
} else {
eprintln!("This source file is not ready for review, yet.");
Err(ExitCode::DataErr)
}
}
None => unreachable!(),
}
}
}
fn starts_with_obsolete_spaces(&self) -> Result<bool> {
if matches!(self.tokens.first(), Some(Tokens::Space(_))) {
ceprintln!(
"Syntax "!Red,
"rule violation: the source file starts with obsolete spaces."
);
Ok(true)
} else {
Ok(false)
}
}
}
impl Default for GraphDescription {
fn default() -> Self {
Self::new()
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Tokens {
Abbreviate,
And,
By,
Comment,
Connect,
Declare,
FullStop,
Identifier(usize),
LineFeed(usize),
Space(usize),
StringLiteral(usize),
Unexpected {
character: char,
line: usize,
position: usize,
},
}