use crate::core::{diagnostic::Diagnostic, lines_inclusive::LinesInclusiveExt, position::Position};
use unicode_segmentation::UnicodeSegmentation;
use crate::{
config::Config, document::Document, language::Language, line_break::LineBreakPlanner,
spacing_checker::SpacingChecker,
};
pub(crate) fn check_one_file_with_language(
config: &Config,
document: &Document,
language: Language,
) -> Result<Vec<Diagnostic>, anyhow::Error> {
if crate::parser::grammar_for(language) != document.grammar {
anyhow::bail!("selected language does not match the document grammar")
}
let mut diagnostics = Vec::new();
let breaker = LineBreakPlanner::builder()
.ambiguous_width(config.ambiguous_width)
.max_width(config.max_width)
.build()?;
for (line_index, line) in document.content.lines_inclusive().enumerate() {
if let Some(diagnostic) = check_line_length(&breaker, document, line_index as u32, line) {
diagnostics.push(diagnostic);
}
}
let spacing_checker = SpacingChecker::new(config, document, language);
diagnostics.extend(spacing_checker.check()?);
Ok(diagnostics)
}
#[cfg(test)]
pub(crate) fn check_one_file(
config: &Config,
document: &Document,
) -> Result<Vec<Diagnostic>, anyhow::Error> {
let language = match document.grammar {
crate::parser::Grammar::Markdown => Language::Markdown,
crate::parser::Grammar::Json => Language::Json,
crate::parser::Grammar::MarkdownInline => {
anyhow::bail!("cannot check a document with the inline Markdown grammar")
}
};
check_one_file_with_language(config, document, language)
}
fn check_line_length(
breaker: &LineBreakPlanner,
document: &Document,
line_index: u32,
line: &str,
) -> Option<Diagnostic> {
let overflow_pos = breaker.first_overflow(line)?;
let (precedings, followings) = line.split_at(overflow_pos);
let column_index = precedings.encode_utf16().fold(0u32, |acc, _| acc + 1);
let start = Position::new(line_index, column_index);
let next_char_len = followings
.graphemes(true)
.next()
.map(|s| s.encode_utf16().fold(0u32, |acc, _| acc + 1))
.unwrap_or(0u32);
let end = Position::new(line_index, column_index + next_char_len);
Some(Diagnostic::new(
document.filename.as_deref(),
start,
end,
"W001".to_string(),
format!("Line length exceeds {} characters", breaker.max_width()),
))
}
#[cfg(test)]
mod tests {
use crate::parser::Grammar;
use super::*;
use crate::config::SpacingRule;
#[test]
fn check_one_file_checks_unparsed_markdown_documents() {
let mut config = Config::default();
config.spacing.alphabets = SpacingRule::Require;
let document = Document::new("漢A", Grammar::Markdown, None::<String>);
let diagnostics = check_one_file(&config, &document).expect("failed to check document");
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].code, "W002");
}
#[test]
fn check_one_file_reports_spacing_columns_from_line_start() {
let mut config = Config::default();
config.spacing.digits = SpacingRule::Require;
let document = Document::new("# 漢1\n", Grammar::Markdown, Some("t.md"));
let diagnostics = check_one_file(&config, &document).expect("failed to check document");
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].code, "W002");
assert_eq!(diagnostics[0].start, Position::new(0, 3));
assert_eq!(diagnostics[0].end, Position::new(0, 4));
}
#[test]
fn check_one_file_reports_spacing_columns_for_a_later_inline_node() {
let mut config = Config::default();
config.spacing.digits = SpacingRule::Require;
let document = Document::new("# 見出し\n\n# 漢1\n", Grammar::Markdown, Some("t.md"));
let diagnostics = check_one_file(&config, &document).expect("failed to check document");
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].code, "W002");
assert_eq!(diagnostics[0].start, Position::new(2, 3));
assert_eq!(diagnostics[0].end, Position::new(2, 4));
}
#[test]
fn check_one_file_reports_prohibited_spacing_without_panicking() {
let mut config = Config::default();
config.spacing.alphabets = SpacingRule::Prohibit;
let document = Document::new("# 漢 A\n", Grammar::Markdown, Some("t.md"));
let diagnostics = check_one_file(&config, &document).expect("failed to check document");
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].code, "W002");
assert_eq!(diagnostics[0].start, Position::new(0, 3));
assert_eq!(diagnostics[0].end, Position::new(0, 4));
}
#[test]
fn check_one_file_reports_spacing_in_each_kind_of_visible_inline_prose() {
let mut config = Config::default();
config.spacing.alphabets = SpacingRule::Require;
for (source, start) in [
("*漢A*", 2),
("~~A漢~~", 3),
("[漢A](destination)", 2),
("", 3),
] {
let document = Document::new(source, Grammar::Markdown, Some("t.md"));
let diagnostics = check_one_file(&config, &document).expect("failed to check document");
assert_eq!(
diagnostics.len(),
1,
"unexpected diagnostics for {source:?}"
);
assert_eq!(diagnostics[0].start, Position::new(0, start));
assert_eq!(diagnostics[0].end, Position::new(0, start + 1));
}
}
#[test]
fn check_one_file_reports_deletion_span_for_the_entire_ascii_space_run() {
let mut config = Config::default();
config.spacing.alphabets = SpacingRule::Prohibit;
let document = Document::new("漢 A", Grammar::Markdown, Some("t.md"));
let diagnostics = check_one_file(&config, &document).expect("failed to check document");
assert_eq!(diagnostics[0].start, Position::new(0, 1));
assert_eq!(diagnostics[0].end, Position::new(0, 3));
}
#[test]
fn check_one_file_excludes_non_prose_and_unsafe_inline_constructs() {
let mut config = Config::default();
config.spacing.alphabets = SpacingRule::Require;
let sources = [
"`漢A`",
"```\n漢A\n```",
"[text](漢A \"漢A\")",
"[text][漢A]",
"<https://example.test/漢A>",
"<foo@example.test>",
"<a href=\"漢A\">",
"&漢",
"\\*漢",
"[漢A](broken",
"`漢A",
];
for source in sources {
let source_with_prose = format!("{source}\n\n漢A");
let document = Document::new(&source_with_prose, Grammar::Markdown, Some("t.md"));
let diagnostics = check_one_file(&config, &document)
.expect("failed to check document")
.into_iter()
.filter(|diagnostic| diagnostic.code == "W002")
.collect::<Vec<_>>();
assert_eq!(
diagnostics.len(),
1,
"unexpected diagnostics for {source:?}"
);
let prose_line = source.matches('\n').count() as u32 + 2;
assert_eq!(diagnostics[0].start, Position::new(prose_line, 1));
assert_eq!(diagnostics[0].end, Position::new(prose_line, 2));
}
}
#[test]
fn check_one_file_does_not_report_spacing_for_json_documents() {
let mut config = Config::default();
config.spacing.alphabets = SpacingRule::Require;
let document = Document::new("{\"value\":\"漢A\"}", Grammar::Json, Some("t.json"));
let diagnostics = check_one_file(&config, &document).expect("failed to check document");
assert!(
diagnostics
.iter()
.all(|diagnostic| diagnostic.code != "W002")
);
}
#[test]
fn w002_reports_missing_spacing_in_markdown_prose() {
let mut config = Config::default();
config.spacing.alphabets = SpacingRule::Require;
let source = "漢A and `漢A`";
let document = Document::new(source, Grammar::Markdown, Some("t.md"));
let diagnostics = check_one_file(&config, &document).unwrap();
let formatted = crate::format::Formatter::new(&config)
.unwrap()
.format(Some(crate::language::Language::Markdown), source)
.unwrap();
assert_eq!(formatted, "漢 A and `漢A`");
assert_eq!(
diagnostics
.iter()
.filter(|item| item.code == "W002")
.count(),
1
);
}
#[test]
fn w002_does_not_report_spacing_in_json_strings() {
let mut config = Config::default();
config.spacing.alphabets = SpacingRule::Require;
let source = r#"{"value":"漢A"}"#;
let document = Document::new(source, Grammar::Json, Some("t.json"));
let diagnostics = check_one_file(&config, &document).unwrap();
let formatted = crate::format::Formatter::new(&config)
.unwrap()
.format(Some(crate::language::Language::Json), source)
.unwrap();
assert_eq!(formatted, source);
assert!(diagnostics.iter().all(|item| item.code != "W002"));
}
#[test]
fn w002_does_not_report_spacing_in_malformed_json() {
let mut config = Config::default();
config.spacing.alphabets = SpacingRule::Require;
let source = r#"{"value":"漢A""#;
let document = Document::new(source, Grammar::Json, Some("t.json"));
let diagnostics = check_one_file(&config, &document).unwrap();
let formatted = crate::format::Formatter::new(&config)
.unwrap()
.format(Some(crate::language::Language::Json), source)
.unwrap();
assert_eq!(formatted, source);
assert!(diagnostics.iter().all(|item| item.code != "W002"));
}
#[test]
fn w001_reports_overlong_indivisible_source_regardless_of_language_policy() {
let config = Config {
max_width: 5,
..Config::default()
};
for (source, grammar, filename) in [
("`very-long-code-span`", Grammar::Markdown, "code.md"),
(
"<https://example.test/very-long-url>",
Grammar::Markdown,
"url.md",
),
(r#""very long JSON string""#, Grammar::Json, "value.json"),
] {
let document = Document::new(source, grammar, Some(filename));
let diagnostics = check_one_file(&config, &document).unwrap();
assert_eq!(
diagnostics
.iter()
.filter(|item| item.code == "W001")
.count(),
1,
"did not report physical overflow for {source:?}"
);
}
}
}