1#![forbid(unsafe_code)]
11
12use std::ops::Range;
13
14use zincite_lint::{
15 EditPart, ModelContext, SourceLocation, SourceSnapshot, TextEdit, prepare_text_edits,
16};
17use zincite_syntax::{
18 ByteParsedFile, Diagnostic, FileMode, NodeKind, ParsedFile, SyntaxElement, SyntaxNode,
19 TokenKind, byte_line_column, parse_bytes_with_mode,
20};
21
22pub mod cli;
23mod comprehension;
24mod data;
25mod inspection;
26mod language;
27mod pipeline;
28mod program;
29pub use program::{
30 CaptureMatches, Program, ProgramEmission, ProgramFunction, ProgramLimits, ProgramNative,
31 ProgramResult, ProgramValue, ReportValue,
32};
33pub use zirql::{ConstructedValue, ParameterDeclaration, ParameterInputs, ParameterType};
34mod reduction;
35mod semantic;
36mod structured;
37pub use inspection::{JsonResult, NodeSelection, SelectedNode};
38pub use reduction::{ReductionResult, UnresolvedDependency};
39pub use semantic::{
40 SemanticEntity, SemanticInstantiation, SemanticResult, SemanticStream, SemanticSubject,
41 SemanticType,
42};
43pub use structured::{
44 ArrayIndexing, LiteralEntry, LiteralField, LiteralKind, LiteralLabel, LiteralRow,
45 LiteralSelection, LiteralValue,
46};
47
48use language::{Stage, StageKind};
49
50#[derive(Clone, Copy, Debug)]
58pub struct Limits {
59 pub nesting: usize,
60 pub work: usize,
61 pub collection: usize,
62}
63
64impl Default for Limits {
65 fn default() -> Self {
66 Self {
67 nesting: 64,
68 work: 1_000_000,
69 collection: 100_000,
70 }
71 }
72}
73
74#[derive(Clone, Debug, PartialEq, Eq)]
76pub enum ErrorLocation {
77 Query,
78 Input,
79 Model(SourceLocation),
80}
81
82#[derive(Clone, Debug, PartialEq, Eq)]
83pub struct QueryError {
84 pub location: ErrorLocation,
85 pub range: Range<usize>,
86 pub message: String,
87}
88
89impl QueryError {
90 fn query(range: Range<usize>, message: impl Into<String>) -> Self {
91 Self {
92 location: ErrorLocation::Query,
93 range,
94 message: message.into(),
95 }
96 }
97
98 fn input(range: Range<usize>, message: impl Into<String>) -> Self {
99 Self {
100 location: ErrorLocation::Input,
101 range,
102 message: message.into(),
103 }
104 }
105}
106
107#[derive(Debug)]
108pub struct InputError {
109 pub diagnostics: Vec<Diagnostic>,
111}
112
113#[derive(Debug)]
115pub struct Input {
116 bytes: Vec<u8>,
117 parsed: ByteParsedFile,
118 syntax_offset: usize,
119 mode: FileMode,
120 file: String,
121}
122
123impl Input {
124 pub fn parse(bytes: Vec<u8>, mode: FileMode) -> Result<Self, InputError> {
125 Self::parse_named(bytes, mode, "<input>")
126 }
127
128 pub fn parse_named(
130 bytes: Vec<u8>,
131 mode: FileMode,
132 file: impl Into<String>,
133 ) -> Result<Self, InputError> {
134 let syntax_offset = if bytes.starts_with(b"\xef\xbb\xbf") {
135 3
136 } else {
137 0
138 };
139 let parsed =
140 parse_bytes_with_mode(bytes[syntax_offset..].to_vec(), mode).map_err(|diagnostic| {
141 InputError {
142 diagnostics: vec![shift_diagnostic(diagnostic, syntax_offset)],
143 }
144 })?;
145 if !parsed.analysis_file().diagnostics().is_empty() {
146 return Err(InputError {
147 diagnostics: parsed
148 .analysis_file()
149 .diagnostics()
150 .iter()
151 .cloned()
152 .map(|diagnostic| shift_diagnostic(diagnostic, syntax_offset))
153 .collect(),
154 });
155 }
156 Ok(Self {
157 bytes,
158 parsed,
159 syntax_offset,
160 mode,
161 file: file.into(),
162 })
163 }
164
165 pub fn file(&self) -> &str {
166 &self.file
167 }
168
169 pub fn source_bytes(&self) -> &[u8] {
170 &self.bytes
171 }
172
173 pub fn syntax(&self) -> &ParsedFile {
176 self.parsed.analysis_file()
177 }
178
179 pub fn syntax_offset(&self) -> usize {
180 self.syntax_offset
181 }
182
183 pub fn token_range(&self, index: usize) -> Range<usize> {
184 self.original_range(self.syntax().tokens()[index].range.clone())
185 }
186
187 pub fn token_bytes(&self, index: usize) -> &[u8] {
188 &self.bytes[self.token_range(index)]
189 }
190
191 pub fn line_column(&self, offset: usize) -> (usize, usize) {
192 byte_line_column(&self.bytes, offset)
193 }
194
195 fn original_range(&self, range: Range<usize>) -> Range<usize> {
196 range.start + self.syntax_offset..range.end + self.syntax_offset
197 }
198}
199
200fn shift_diagnostic(mut diagnostic: Diagnostic, offset: usize) -> Diagnostic {
201 diagnostic.range = diagnostic.range.start + offset..diagnostic.range.end + offset;
202 diagnostic
203}
204
205#[derive(Clone, Copy, Debug, PartialEq, Eq)]
207pub enum ItemKind {
208 Assignment,
209 Declaration,
210 Enum,
211 TypeAlias,
212 Function,
213 Predicate,
214 Test,
215 Annotation,
216 Constraint,
217 Include,
218 Output,
219 Solve,
220}
221
222impl ItemKind {
223 pub fn as_str(self) -> &'static str {
224 match self {
225 Self::Assignment => "assignment",
226 Self::Declaration => "declaration",
227 Self::Enum => "enum",
228 Self::TypeAlias => "type_alias",
229 Self::Function => "function",
230 Self::Predicate => "predicate",
231 Self::Test => "test",
232 Self::Annotation => "annotation",
233 Self::Constraint => "constraint",
234 Self::Include => "include",
235 Self::Output => "output",
236 Self::Solve => "solve",
237 }
238 }
239
240 fn parse(value: &str) -> Option<Self> {
241 Some(match value {
242 "assignment" => Self::Assignment,
243 "declaration" => Self::Declaration,
244 "enum" => Self::Enum,
245 "type_alias" => Self::TypeAlias,
246 "function" => Self::Function,
247 "predicate" => Self::Predicate,
248 "test" => Self::Test,
249 "annotation" => Self::Annotation,
250 "constraint" => Self::Constraint,
251 "include" => Self::Include,
252 "output" => Self::Output,
253 "solve" => Self::Solve,
254 _ => return None,
255 })
256 }
257
258 fn from_node(kind: NodeKind) -> Self {
259 match kind {
260 NodeKind::Assignment => Self::Assignment,
261 NodeKind::Declaration => Self::Declaration,
262 NodeKind::EnumDeclaration => Self::Enum,
263 NodeKind::TypeAlias => Self::TypeAlias,
264 NodeKind::FunctionDeclaration => Self::Function,
265 NodeKind::PredicateDeclaration => Self::Predicate,
266 NodeKind::TestDeclaration => Self::Test,
267 NodeKind::AnnotationDeclaration => Self::Annotation,
268 NodeKind::Constraint => Self::Constraint,
269 NodeKind::Include => Self::Include,
270 NodeKind::Output => Self::Output,
271 NodeKind::Solve | NodeKind::SolveMinimize | NodeKind::SolveMaximize => Self::Solve,
272 _ => unreachable!("validated input contains only supported root items"),
273 }
274 }
275
276 fn is_named(self) -> bool {
277 matches!(
278 self,
279 Self::Assignment
280 | Self::Declaration
281 | Self::Enum
282 | Self::TypeAlias
283 | Self::Function
284 | Self::Predicate
285 | Self::Test
286 | Self::Annotation
287 )
288 }
289}
290
291#[derive(Clone, Debug)]
294pub struct SelectedItem<'a> {
295 pub node: &'a SyntaxNode,
296 pub kind: ItemKind,
297 pub name: Option<&'a str>,
299 pub range: Range<usize>,
300 pub source_range: Range<usize>,
302}
303
304#[derive(Debug)]
305pub struct Selection<'a> {
306 input: &'a Input,
307 items: Vec<SelectedItem<'a>>,
308 document: bool,
309}
310
311impl<'a> Selection<'a> {
312 pub fn input(&self) -> &'a Input {
313 self.input
314 }
315
316 pub fn items(&self) -> &[SelectedItem<'a>] {
317 &self.items
318 }
319
320 pub fn is_document(&self) -> bool {
322 self.document
323 }
324}
325
326#[derive(Debug)]
327pub enum QueryResult<'a> {
328 Selection(Selection<'a>),
329 Count(usize),
330 Nodes(NodeSelection<'a>),
331 Strings(Vec<String>),
332 Literals(LiteralSelection<'a>),
333 Tally(Vec<(String, usize)>),
334 Json(JsonResult<'a>),
335 Document(Vec<u8>),
337 Reduction(ReductionResult),
339 Semantic(SemanticResult<'a>),
341}
342
343impl QueryResult<'_> {
344 pub fn render(&self) -> Vec<u8> {
347 match self {
348 Self::Nodes(selection) => selection
349 .nodes()
350 .iter()
351 .flat_map(|node| &selection.input().bytes[node.range.clone()])
352 .copied()
353 .collect(),
354 Self::Literals(selection) => selection
355 .values()
356 .iter()
357 .flat_map(|value| value.spelling)
358 .copied()
359 .collect(),
360 Self::Strings(values) => values
361 .iter()
362 .flat_map(|value| value.bytes().chain(std::iter::once(b'\n')))
363 .collect(),
364 Self::Tally(values) => {
365 let mut bytes = serde_json::to_vec(&inspection::tally_json(values))
366 .expect("string-key counts serialize");
367 bytes.push(b'\n');
368 bytes
369 }
370 Self::Json(result) => result.render(),
371 Self::Count(count) => format!("{count}\n").into_bytes(),
372 Self::Document(bytes) => bytes.clone(),
373 Self::Reduction(result) => result.candidate().source_bytes().to_vec(),
374 Self::Semantic(result) => result.render(),
375 Self::Selection(selection) if selection.document => selection.input.bytes.clone(),
376 Self::Selection(selection) => {
377 let mut bytes = Vec::new();
378 for item in &selection.items {
379 bytes.extend_from_slice(&selection.input.bytes[item.source_range.clone()]);
380 }
381 bytes
382 }
383 }
384 }
385}
386
387#[derive(Clone, Debug)]
389pub struct Query {
390 stages: Vec<Stage>,
391 range: Range<usize>,
392 nesting_limit: usize,
393}
394
395impl Query {
396 pub fn parse(expression: &str, limits: Limits) -> Result<Self, QueryError> {
397 language::parse(expression, limits)
398 }
399
400 pub fn is_editing(&self) -> bool {
401 self.stages.iter().any(|stage| {
402 matches!(
403 stage.kind,
404 StageKind::SetValue(_)
405 | StageKind::Remove
406 | StageKind::FilterElements(_)
407 | StageKind::ReduceEnum(_, _)
408 )
409 })
410 }
411
412 pub fn requires_model_facts(&self) -> bool {
413 self.stages.iter().any(|stage| {
414 matches!(stage.kind, StageKind::ReduceEnum(_, _)) || semantic::requests_facts(stage)
415 })
416 }
417
418 pub fn evaluate<'a>(
419 &self,
420 input: &'a Input,
421 limits: Limits,
422 ) -> Result<QueryResult<'a>, QueryError> {
423 self.evaluate_with_model(input, None, limits)
424 }
425
426 pub fn evaluate_with_facts<'a>(
429 &self,
430 input: &'a Input,
431 facts: std::rc::Rc<zincite_lint::ModelFacts<'a>>,
432 limits: Limits,
433 ) -> Result<QueryResult<'a>, QueryError> {
434 let model = facts.model();
435 pipeline::evaluate(self, input, Some(facts), Some(model), limits)
436 }
437
438 pub fn evaluate_with_model<'a>(
441 &self,
442 input: &'a Input,
443 model: Option<&'a ModelContext>,
444 limits: Limits,
445 ) -> Result<QueryResult<'a>, QueryError> {
446 let facts = if self.stages.iter().any(semantic::requests_facts) {
447 model.map(|model| std::rc::Rc::new(zincite_lint::ModelFacts::new(model)))
448 } else {
449 None
450 };
451 pipeline::evaluate(self, input, facts, model, limits)
452 }
453}
454
455fn evaluate_stage<'a>(
456 input: &'a Input,
457 model: Option<&'a ModelContext>,
458 result: QueryResult<'a>,
459 stage: &Stage,
460 work: &mut Work,
461 limits: Limits,
462) -> Result<QueryResult<'a>, QueryError> {
463 Ok(match &stage.kind {
464 StageKind::Items => {
465 inspection::require_nodes(&result, &stage.range)?;
466 QueryResult::Selection(Selection {
467 input,
468 items: collect_items(input, limits.collection, work, &stage.range, true)?,
469 document: true,
470 })
471 }
472 StageKind::Filter(_)
473 | StageKind::SharedFilter(_)
474 | StageKind::Head(_)
475 | StageKind::Tail(_)
476 | StageKind::Reverse
477 | StageKind::Check { .. }
478 | StageKind::Count
479 | StageKind::Unique
480 | StageKind::Tally => unreachable!("the core evaluates common stages"),
481 StageKind::Values => structured::values(result, stage, work, limits)?,
482 StageKind::Fields | StageKind::Elements | StageKind::Keys => {
483 structured::project(result, stage, work, limits)?
484 }
485 StageKind::FilterElements(comparison) => QueryResult::Document(
486 structured::transform_elements(input, result, comparison, &stage.range, work, limits)?,
487 ),
488 StageKind::ReduceEnum(name, retention) => {
489 if !matches!(result, QueryResult::Selection(_)) {
490 return Err(inspection::type_error(
491 &stage.range,
492 "reduce_enum requires an item selection",
493 ));
494 }
495 QueryResult::Reduction(reduction::reduce(
496 input,
497 model,
498 name,
499 retention,
500 &stage.range,
501 work,
502 limits,
503 )?)
504 }
505 StageKind::SetValue(_) | StageKind::Remove => {
506 let QueryResult::Selection(selection) = result else {
507 return Err(inspection::type_error(
508 &stage.range,
509 "transformations require an item selection",
510 ));
511 };
512 QueryResult::Document(transform_assignments(
513 input,
514 &selection.items,
515 stage,
516 work,
517 limits.collection,
518 )?)
519 }
520 StageKind::Emit => {
521 if !matches!(
522 result,
523 QueryResult::Selection(_)
524 | QueryResult::Nodes(_)
525 | QueryResult::Strings(_)
526 | QueryResult::Tally(_)
527 | QueryResult::Literals(_)
528 ) {
529 return Err(inspection::type_error(
530 &stage.range,
531 "emit requires a node, literal or string stream or tally",
532 ));
533 }
534 result
535 }
536 StageKind::EmitDocument => result,
537 StageKind::Expressions | StageKind::Children | StageKind::Subtree | StageKind::Range(_) => {
538 inspection::navigate(result, stage, work, limits.collection)?
539 }
540 StageKind::Names | StageKind::CallNames | StageKind::AnnotationNames | StageKind::Text => {
541 inspection::project(result, stage, work, limits.collection)?
542 }
543 StageKind::Json => inspection::json(result, work, &stage.range)?,
544 StageKind::References
545 | StageKind::Declarations
546 | StageKind::Uses
547 | StageKind::TransitiveReferences(_)
548 | StageKind::ReferencedDeclarations
549 | StageKind::Types
550 | StageKind::Instantiations => {
551 return Err(inspection::type_error(
552 &stage.range,
553 "stage requires semantic inspection",
554 ));
555 }
556 })
557}
558
559fn transform_assignments(
562 input: &Input,
563 items: &[SelectedItem<'_>],
564 stage: &Stage,
565 work: &mut Work,
566 collection_limit: usize,
567) -> Result<Vec<u8>, QueryError> {
568 if input.mode != FileMode::Data {
569 return Err(QueryError::query(
570 stage.range.clone(),
571 "assignment transformations require data-file mode (.dzn)",
572 ));
573 }
574 let replacement = match &stage.kind {
575 StageKind::SetValue(value) => {
576 work.spend(value.len(), &stage.range)?;
577 validate_replacement(value, &stage.range)?;
578 Some(value)
579 }
580 StageKind::Remove => None,
581 _ => unreachable!("only transformation stages construct a candidate"),
582 };
583 let ordinary_items = if replacement.is_none() {
586 collect_items(input, collection_limit, work, &stage.range, false)?
587 } else {
588 Vec::new()
589 };
590 let mut edits = Vec::with_capacity(items.len());
591 for item in items {
592 work.visit(&stage.range)?;
593 if item.kind != ItemKind::Assignment {
594 return Err(QueryError::input(
595 item.range.clone(),
596 "selected item is not an assignment",
597 ));
598 }
599 let edit = if let Some(value) = replacement {
600 work.spend(value.len(), &stage.range)?;
601 let rhs = assignment_rhs(item.node).ok_or_else(|| {
602 QueryError::input(
603 item.range.clone(),
604 "assignment has no single right-hand side",
605 )
606 })?;
607 let range = input.original_range(rhs.range());
608 let first = input
609 .syntax()
610 .tokens()
611 .partition_point(|token| token.range.end + input.syntax_offset <= range.start);
612 for (index, token) in input.syntax().tokens().iter().enumerate().skip(first) {
613 if token.range.start + input.syntax_offset >= range.end {
614 break;
615 }
616 work.visit(&stage.range)?;
617 if matches!(token.kind, TokenKind::LineComment | TokenKind::BlockComment) {
618 return Err(QueryError::input(
619 input.token_range(index),
620 "cannot replace a right-hand side with internal comments without choosing their new placement",
621 ));
622 }
623 }
624 TextEdit {
625 range,
626 replacement: vec![EditPart::Text(value.clone())],
627 }
628 } else {
629 let ordinary = ordinary_items
630 .binary_search_by_key(&item.range.start, |item| item.range.start)
631 .unwrap();
632 removal_edit(
633 input,
634 item,
635 &ordinary_items[ordinary].source_range,
636 work,
637 &stage.range,
638 )?
639 };
640 edits.push(edit);
641 }
642 let removed = if replacement.is_none() { items } else { &[] };
643 finish_candidate(input, &edits, removed, work, &stage.range).map(|candidate| candidate.bytes)
644}
645
646fn finish_candidate(
647 input: &Input,
648 edits: &[TextEdit],
649 removed: &[SelectedItem<'_>],
650 work: &mut Work,
651 range: &Range<usize>,
652) -> Result<Input, QueryError> {
653 validate_document_directives(input, removed, work, range)?;
654 let snapshot = SourceSnapshot::new("<query>.dzn", input.source_bytes());
655 let candidate = prepare_text_edits(&snapshot, input.source_bytes(), edits)
656 .map_err(|error| QueryError::query(range.clone(), error.to_string()))?;
657 let candidate_input =
658 Input::parse_named(candidate, FileMode::Data, input.file()).map_err(|error| {
659 QueryError::query(
660 range.clone(),
661 format!(
662 "invalid data edit candidate: {}",
663 error.diagnostics[0].message
664 ),
665 )
666 })?;
667 validate_document_directives(&candidate_input, &[], work, range).map_err(|error| {
668 QueryError::query(
669 range.clone(),
670 format!("invalid data edit candidate: {}", error.message),
671 )
672 })?;
673 Ok(candidate_input)
674}
675
676fn removal_edit(
677 input: &Input,
678 item: &SelectedItem<'_>,
679 ordinary_range: &Range<usize>,
680 work: &mut Work,
681 query_range: &Range<usize>,
682) -> Result<TextEdit, QueryError> {
683 let range = item.source_range.clone();
684 let mut deleted = Vec::new();
685 let tokens = input.syntax().tokens();
686 let first =
687 tokens.partition_point(|token| token.range.end + input.syntax_offset <= range.start);
688 for (index, token) in tokens.iter().enumerate().skip(first) {
689 if token.range.start + input.syntax_offset >= range.end {
690 break;
691 }
692 work.visit(query_range)?;
693 if !matches!(token.kind, TokenKind::LineComment | TokenKind::BlockComment)
694 || directive(input.token_bytes(index)).is_none()
695 {
696 continue;
697 }
698 let marker = input.token_range(index);
699 let start = line_indent_start(&input.bytes, marker.start, range.start);
700 let end = tokens
701 .get(index + 1)
702 .filter(|token| token.kind == TokenKind::Whitespace)
703 .map_or(marker.end, |token| token.range.end + input.syntax_offset)
704 .min(range.end);
705 deleted.push(start..end);
706 }
707 deleted.push(ordinary_range.start.max(range.start)..ordinary_range.end.min(range.end));
708 deleted.sort_by_key(|range| (range.start, range.end));
709 let mut replacement = Vec::new();
710 let mut cursor = range.start;
711 for deleted in deleted {
712 if cursor < deleted.start {
713 replacement.push(EditPart::Original(cursor..deleted.start));
714 }
715 cursor = cursor.max(deleted.end);
716 }
717 if cursor < range.end {
718 replacement.push(EditPart::Original(cursor..range.end));
719 }
720 Ok(TextEdit { range, replacement })
721}
722
723fn assignment_rhs(node: &SyntaxNode) -> Option<&SyntaxNode> {
724 let mut children = node.child_nodes();
725 let rhs = children.next()?;
726 children.next().is_none().then_some(rhs)
727}
728
729fn validate_replacement(value: &str, range: &Range<usize>) -> Result<(), QueryError> {
730 let source = format!("zincite_value = {value};");
731 let input = Input::parse(source.into_bytes(), FileMode::Data).map_err(|error| {
732 QueryError::query(
733 range.clone(),
734 format!(
735 "invalid replacement expression: {}",
736 error.diagnostics[0].message
737 ),
738 )
739 })?;
740 if !input
743 .syntax()
744 .tokens()
745 .last()
746 .is_some_and(|token| token.kind == TokenKind::Semicolon)
747 {
748 return Err(QueryError::query(
749 range.clone(),
750 "invalid replacement expression: trailing comment would hide the assignment separator",
751 ));
752 }
753 let mut items = input.syntax().tree().child_nodes();
754 if !items
755 .next()
756 .is_some_and(|item| assignment_rhs(item).is_some())
757 || items.next().is_some()
758 {
759 return Err(QueryError::query(
760 range.clone(),
761 "replacement must contain exactly one expression",
762 ));
763 }
764 Ok(())
765}
766
767fn validate_document_directives(
768 input: &Input,
769 removed: &[SelectedItem<'_>],
770 work: &mut Work,
771 query_range: &Range<usize>,
772) -> Result<(), QueryError> {
773 let mut open = None;
774 let mut removal = 0;
775 let mut root_items = input.syntax().tree().child_nodes().peekable();
776 for (index, token) in input.syntax().tokens().iter().enumerate() {
777 work.visit(query_range)?;
778 if !matches!(token.kind, TokenKind::LineComment | TokenKind::BlockComment) {
779 continue;
780 }
781 let range = input.token_range(index);
782 while removed
783 .get(removal)
784 .is_some_and(|item| item.source_range.end <= range.start)
785 {
786 removal += 1;
787 }
788 let deleted = removed.get(removal).is_some_and(|item| {
789 item.source_range.start <= range.start && range.end <= item.source_range.end
790 });
791 while root_items
792 .peek()
793 .is_some_and(|node| node.range().end + input.syntax_offset <= range.start)
794 {
795 root_items.next();
796 }
797 let marker = directive(input.token_bytes(index));
798 let between_items = root_items
799 .peek()
800 .is_none_or(|node| node.range().start + input.syntax_offset >= range.end);
801 let standalone = standalone(&input.bytes, range.start, input.syntax_offset);
802 if matches!(marker, Some(Directive::Next))
803 && standalone
804 && between_items
805 && let Some(target) = root_items.peek()
806 {
807 let target = target.range().start + input.syntax_offset;
808 let target_deleted = removed
809 .binary_search_by_key(&target, |item| item.range.start)
810 .is_ok();
811 if deleted != target_deleted {
812 return Err(QueryError::input(
813 range,
814 "transformation would separate a next-item directive from its target",
815 ));
816 }
817 }
818 if deleted {
819 continue;
820 }
821 if matches!(marker, Some(Directive::Off | Directive::On)) {
822 if !standalone || !between_items {
823 return Err(QueryError::input(
824 range,
825 "paired formatter directives must be standalone comments between complete items",
826 ));
827 }
828 match marker {
829 Some(Directive::Off) if open.is_none() => open = Some(range),
830 Some(Directive::On) if open.is_some() => open = None,
831 _ => {
832 return Err(QueryError::input(
833 range,
834 "transformation would unbalance paired formatter directives",
835 ));
836 }
837 }
838 }
839 }
840 if let Some(range) = open {
841 return Err(QueryError::input(
842 range,
843 "transformation would unbalance paired formatter directives",
844 ));
845 }
846 Ok(())
847}
848
849struct Work<'a> {
851 budget: &'a mut zirql::Budget,
852}
853
854impl Work<'_> {
855 fn produce(&mut self, units: usize, range: &Range<usize>) -> Result<(), QueryError> {
856 self.budget
857 .charge_results(units, zirql::Location::Query(range.clone()))
858 .map_err(pipeline::core_error)
859 }
860
861 fn visit(&mut self, range: &Range<usize>) -> Result<(), QueryError> {
862 self.spend(1, range)
863 }
864
865 fn spend(&mut self, amount: usize, range: &Range<usize>) -> Result<(), QueryError> {
866 self.budget
867 .charge(amount, zirql::Location::Query(range.clone()))
868 .map_err(pipeline::core_error)
869 }
870}
871
872fn identifier_identity(written: &str) -> &str {
873 written
874 .strip_prefix('\'')
875 .and_then(|name| name.strip_suffix('\''))
876 .unwrap_or(written)
877}
878
879fn collect_items<'a>(
880 input: &'a Input,
881 collection_limit: usize,
882 work: &mut Work,
883 range: &Range<usize>,
884 include_directives: bool,
885) -> Result<Vec<SelectedItem<'a>>, QueryError> {
886 let mut items = Vec::new();
887 for node in input.syntax().tree().child_nodes() {
888 work.visit(range)?;
889 if items.len() >= collection_limit {
890 return Err(QueryError::query(
891 range.clone(),
892 "query collection limit exceeded",
893 ));
894 }
895 let kind = ItemKind::from_node(node.kind());
896 let name = kind.is_named().then(|| {
897 node.children().iter().find_map(|child| {
898 let SyntaxElement::Token(index) = child else {
899 return None;
900 };
901 let token = &input.syntax().tokens()[*index];
902 matches!(
903 token.kind,
904 TokenKind::Identifier | TokenKind::QuotedIdentifier
905 )
906 .then(|| identifier_identity(&input.syntax().source()[token.range.clone()]))
907 })
908 });
909 work.produce(1, range)?;
910 let range = input.original_range(node.range());
911 items.push(SelectedItem {
912 node,
913 kind,
914 name: name.flatten(),
915 source_range: range.clone(),
916 range,
917 });
918 }
919 attach_comments(input, &mut items, include_directives);
920 Ok(items)
921}
922
923fn attach_comments(input: &Input, items: &mut [SelectedItem<'_>], include_directives: bool) {
924 let tokens = input.syntax().tokens();
925 for next in 0..=items.len() {
926 let gap_start = if next == 0 {
927 input.syntax_offset
928 } else {
929 items[next - 1].range.end
930 };
931 let gap_end = items
932 .get(next)
933 .map_or(input.bytes.len(), |item| item.range.start);
934 let first =
935 tokens.partition_point(|token| token.range.end + input.syntax_offset <= gap_start);
936 let last =
937 tokens.partition_point(|token| token.range.start + input.syntax_offset < gap_end);
938 let gap_tokens = &tokens[first..last];
939 let mut boundary = gap_start;
940 if next > 0 {
941 let mut end = gap_start;
943 for token in gap_tokens {
944 let range = input.original_range(token.range.clone());
945 let bytes = &input.bytes[range.clone()];
946 if token.kind == TokenKind::Whitespace {
947 if let Some(length) = first_line_end(bytes) {
948 end = range.start + length;
949 break;
950 }
951 end = range.end;
952 } else {
953 end = range.end;
954 }
955 }
956 items[next - 1].source_range.end = end;
957 boundary = end;
958 }
959 if next < items.len() {
960 let mut start = gap_end;
961 for token in gap_tokens.iter().rev() {
962 let range = input.original_range(token.range.clone());
963 if range.end <= boundary {
964 break;
965 }
966 let bytes = &input.bytes[range.clone()];
967 if token.kind == TokenKind::Whitespace {
968 start = line_indent_start(&input.bytes, start, boundary);
970 if line_breaks(bytes) >= 2 {
971 break;
972 }
973 } else if standalone(&input.bytes, range.start, boundary) {
974 start = line_indent_start(&input.bytes, range.start, boundary);
975 } else {
976 break;
977 }
978 }
979 items[next].source_range.start = start.max(boundary);
980 }
981 if !include_directives {
982 continue;
983 }
984 for token in gap_tokens {
987 let range = input.original_range(token.range.clone());
988 if range.start < boundary || !standalone(&input.bytes, range.start, boundary) {
989 continue;
990 }
991 match directive(&input.bytes[range.clone()]) {
992 Some(Directive::Next | Directive::Off) if next < items.len() => {
993 items[next].source_range.start = items[next]
994 .source_range
995 .start
996 .min(line_indent_start(&input.bytes, range.start, boundary));
997 }
998 Some(Directive::On) if next > 0 => {
999 let end = range.end
1000 + first_line_end(&input.bytes[range.end..gap_end])
1001 .unwrap_or(gap_end - range.end);
1002 items[next - 1].source_range.end = items[next - 1].source_range.end.max(end);
1003 if next < items.len() {
1004 items[next].source_range.start = items[next].source_range.start.max(end);
1005 }
1006 }
1007 _ => {}
1008 }
1009 }
1010 }
1011}
1012
1013fn first_line_end(bytes: &[u8]) -> Option<usize> {
1014 bytes
1015 .iter()
1016 .position(|byte| matches!(byte, b'\r' | b'\n'))
1017 .map(|index| {
1018 index + 1 + usize::from(bytes[index] == b'\r' && bytes.get(index + 1) == Some(&b'\n'))
1019 })
1020}
1021
1022fn line_breaks(bytes: &[u8]) -> usize {
1023 bytes
1024 .iter()
1025 .enumerate()
1026 .filter(|(index, byte)| {
1027 **byte == b'\r' || (**byte == b'\n' && (*index == 0 || bytes[*index - 1] != b'\r'))
1028 })
1029 .count()
1030}
1031
1032fn line_indent_start(bytes: &[u8], offset: usize, boundary: usize) -> usize {
1033 let mut start = offset;
1034 while start > boundary && matches!(bytes[start - 1], b' ' | b'\t') {
1035 start -= 1;
1036 }
1037 start
1038}
1039
1040fn standalone(bytes: &[u8], offset: usize, boundary: usize) -> bool {
1041 let start = line_indent_start(bytes, offset, boundary);
1042 start == boundary || matches!(bytes[start - 1], b'\r' | b'\n')
1043}
1044
1045#[derive(Clone, Copy)]
1046enum Directive {
1047 Next,
1048 Off,
1049 On,
1050}
1051
1052fn directive(bytes: &[u8]) -> Option<Directive> {
1053 let text = std::str::from_utf8(bytes).ok()?.trim();
1054 if let Some(command) = text.strip_prefix("% zincite-fmt:").map(str::trim) {
1055 match command {
1056 "skip" => Some(Directive::Next),
1057 "off" => Some(Directive::Off),
1058 "on" => Some(Directive::On),
1059 _ => None,
1060 }
1061 } else if text
1062 .strip_prefix("% zincite-lint:")
1063 .map(str::trim)
1064 .is_some_and(|command| command == "ignore" || command.starts_with("ignore "))
1065 {
1066 Some(Directive::Next)
1067 } else {
1068 None
1069 }
1070}
1071
1072fn validate_fragment_directives(
1073 input: &Input,
1074 items: &[SelectedItem<'_>],
1075 work: &mut Work<'_>,
1076 query_range: &Range<usize>,
1077) -> Result<(), QueryError> {
1078 let search_work = items
1080 .len()
1081 .checked_mul(items.len().max(1).ilog2() as usize + 1)
1082 .and_then(|units| units.checked_mul(2))
1083 .ok_or_else(|| {
1084 QueryError::query(query_range.clone(), "fragment validation size overflow")
1085 })?;
1086 work.spend(search_work, query_range)?;
1087 work.produce(items.len(), query_range)?;
1088 let mut source_order: Vec<_> = items.iter().collect();
1089 source_order.sort_unstable_by_key(|item| item.source_range.start);
1090 for pair in source_order.windows(2) {
1091 work.visit(query_range)?;
1092 if pair[0].source_range.end > pair[1].source_range.start {
1093 return Err(QueryError::input(
1094 pair[1].source_range.clone(),
1095 "fragment selection would overlap directive attachments",
1096 ));
1097 }
1098 }
1099 let mut selected = 0;
1100 let mut root_items = input.syntax().tree().child_nodes().peekable();
1101 for (index, token) in input.syntax().tokens().iter().enumerate() {
1102 work.visit(query_range)?;
1103 if !matches!(token.kind, TokenKind::LineComment | TokenKind::BlockComment) {
1104 continue;
1105 }
1106 let range = input.token_range(index);
1107 work.spend(range.len(), query_range)?;
1108 while root_items
1109 .peek()
1110 .is_some_and(|node| node.range().end + input.syntax_offset <= range.start)
1111 {
1112 work.visit(query_range)?;
1113 root_items.next();
1114 }
1115 while source_order
1116 .get(selected)
1117 .is_some_and(|item| item.source_range.end <= range.start)
1118 {
1119 work.visit(query_range)?;
1120 selected += 1;
1121 }
1122 let retained = source_order.get(selected).filter(|item| {
1123 item.source_range.start <= range.start && range.end <= item.source_range.end
1124 });
1125 if matches!(directive(input.token_bytes(index)), Some(Directive::Next))
1126 && standalone(&input.bytes, range.start, input.syntax_offset)
1127 && root_items
1128 .peek()
1129 .is_some_and(|node| node.range().start + input.syntax_offset >= range.end)
1130 {
1131 let target = root_items.peek().unwrap().range().start + input.syntax_offset;
1132 work.spend(items.len().max(1).ilog2() as usize + 1, query_range)?;
1133 let target_item = source_order
1134 .binary_search_by_key(&target, |item| item.range.start)
1135 .ok();
1136 if retained.is_some() != target_item.is_some()
1137 || target_item.is_some_and(|index| retained.is_some() && index != selected)
1138 {
1139 return Err(QueryError::input(
1140 range,
1141 "fragment selection would separate a next-item directive from its target",
1142 ));
1143 }
1144 }
1145 }
1146 let mut open = None;
1147 let tokens = input.syntax().tokens();
1148 for item in items {
1149 work.spend(tokens.len().max(1).ilog2() as usize + 1, query_range)?;
1150 let first = tokens.partition_point(|token| {
1151 token.range.end + input.syntax_offset <= item.source_range.start
1152 });
1153 for (index, token) in tokens.iter().enumerate().skip(first) {
1154 work.visit(query_range)?;
1155 let range = input.token_range(index);
1156 if range.start >= item.source_range.end {
1157 break;
1158 }
1159 if !matches!(token.kind, TokenKind::LineComment | TokenKind::BlockComment) {
1160 continue;
1161 }
1162 work.spend(range.len(), query_range)?;
1163 match directive(input.token_bytes(index)) {
1164 Some(Directive::Off) if open.is_none() => open = Some(range),
1165 Some(Directive::On) if open.is_some() => open = None,
1166 Some(Directive::Off | Directive::On) => {
1167 return Err(QueryError::input(
1168 range,
1169 "fragment selection would unbalance paired formatter directives",
1170 ));
1171 }
1172 _ => {}
1173 }
1174 }
1175 }
1176 if let Some(range) = open {
1177 return Err(QueryError::input(
1178 range,
1179 "fragment selection would unbalance paired formatter directives",
1180 ));
1181 }
1182 Ok(())
1183}