|
| 1 | +use std::collections::BTreeSet; |
| 2 | + |
| 3 | +use crate::{SyntaxFileObservation, SyntaxFileState, SyntaxPrecheckOutcome, SyntaxPrecheckStatus}; |
| 4 | + |
| 5 | +/// 按选定文件范围聚合语法初检;枚举中断、取消、未知版本和局部失败不能成为 clean。 |
| 6 | +/// 参数为每个已选文件的唯一观察、范围是否完成及取消状态;返回初检状态或输入契约错误。 |
| 7 | +pub fn assess_syntax_precheck( |
| 8 | + files: &[SyntaxFileObservation], |
| 9 | + scope_complete: bool, |
| 10 | + cancelled: bool, |
| 11 | +) -> Result<SyntaxPrecheckOutcome, &'static str> { |
| 12 | + let mut paths = BTreeSet::new(); |
| 13 | + let mut outcome = SyntaxPrecheckOutcome { |
| 14 | + status: SyntaxPrecheckStatus::NotRun, |
| 15 | + scope_complete, |
| 16 | + cancelled, |
| 17 | + selected_files: files.len(), |
| 18 | + checked_files: 0, |
| 19 | + incomplete_files: 0, |
| 20 | + unsupported_files: 0, |
| 21 | + unqualified_files: 0, |
| 22 | + truncated_files: 0, |
| 23 | + suspected_recoveries: 0, |
| 24 | + }; |
| 25 | + for file in files { |
| 26 | + if !valid_relative_path(&file.path) || !paths.insert(file.path.as_str()) { |
| 27 | + return Err("syntax_scope_invalid_or_duplicate_path"); |
| 28 | + } |
| 29 | + match &file.state { |
| 30 | + SyntaxFileState::Checked { |
| 31 | + recoveries, |
| 32 | + grammar_qualified, |
| 33 | + truncated, |
| 34 | + } => { |
| 35 | + outcome.checked_files += 1; |
| 36 | + outcome.suspected_recoveries = outcome |
| 37 | + .suspected_recoveries |
| 38 | + .checked_add(*recoveries) |
| 39 | + .ok_or("syntax_recovery_count_overflow")?; |
| 40 | + if !grammar_qualified { |
| 41 | + outcome.unqualified_files += 1; |
| 42 | + } |
| 43 | + if *truncated { |
| 44 | + outcome.truncated_files += 1; |
| 45 | + } |
| 46 | + } |
| 47 | + SyntaxFileState::Incomplete { reason } => { |
| 48 | + if reason.is_empty() { |
| 49 | + return Err("syntax_incomplete_reason_missing"); |
| 50 | + } |
| 51 | + outcome.incomplete_files += 1; |
| 52 | + } |
| 53 | + SyntaxFileState::Unsupported { reason } => { |
| 54 | + if reason.is_empty() { |
| 55 | + return Err("syntax_unsupported_reason_missing"); |
| 56 | + } |
| 57 | + outcome.unsupported_files += 1; |
| 58 | + } |
| 59 | + } |
| 60 | + } |
| 61 | + outcome.status = if !scope_complete || cancelled { |
| 62 | + SyntaxPrecheckStatus::Incomplete |
| 63 | + } else if files.is_empty() { |
| 64 | + SyntaxPrecheckStatus::NotRun |
| 65 | + } else if outcome.unsupported_files == files.len() { |
| 66 | + SyntaxPrecheckStatus::Unsupported |
| 67 | + } else if outcome.incomplete_files > 0 |
| 68 | + || outcome.unsupported_files > 0 |
| 69 | + || outcome.unqualified_files > 0 |
| 70 | + || outcome.truncated_files > 0 |
| 71 | + { |
| 72 | + SyntaxPrecheckStatus::Incomplete |
| 73 | + } else if outcome.suspected_recoveries > 0 { |
| 74 | + SyntaxPrecheckStatus::SuspectedIssue |
| 75 | + } else { |
| 76 | + SyntaxPrecheckStatus::Clean |
| 77 | + }; |
| 78 | + Ok(outcome) |
| 79 | +} |
| 80 | + |
| 81 | +fn valid_relative_path(path: &str) -> bool { |
| 82 | + !path.is_empty() |
| 83 | + && !path.starts_with('/') |
| 84 | + && !path.contains('\\') |
| 85 | + && !path.chars().any(char::is_control) |
| 86 | + && path |
| 87 | + .split('/') |
| 88 | + .all(|part| !part.is_empty() && part != "." && part != "..") |
| 89 | +} |
0 commit comments