ocomment_core/lib.rs
1//! Byte-oriented scanning and transformation for OComment.
2//!
3//! `ocomment-core` finds the comments in a source file and works out which of
4//! them may be removed. It does no I/O: it takes bytes and hands back what it
5//! found and what it would write. The CLI, the LSP server, and the plugin host
6//! are all built on the calls below.
7//!
8//! # Byte-preserving
9//!
10//! Byte offsets are the canonical coordinate system, and the engine never
11//! decodes the complete input as UTF-8. Every byte outside a removed comment is
12//! copied through untouched, so a BOM, CRLF line endings, a missing final
13//! newline, and source bytes that are not UTF-8 at all all survive a
14//! transformation unchanged. Nothing is reformatted, reindented, or reordered:
15//! the only bytes that move are the ones a comment occupied.
16//!
17//! Three rules hold everywhere in this crate:
18//!
19//! - A [`ByteSpan`] is half-open, `start..end`, counted in bytes.
20//! - The [`Edit`]s of a [`TransformResult`] are sorted by `span.start` and
21//! never overlap, so applying them in order in one pass is enough.
22//! - The same bytes, language, and options always give the same answer. An
23//! independent OCaml implementation is compared against this one byte for
24//! byte.
25//!
26//! # Scanning and transforming
27//!
28//! [`scan`] reports. [`transform_plan`] reports and plans edits without
29//! materializing output, while [`transform`] also gives you the bytes and
30//! source map. [`PreparedScanner`] compiles policy regular expressions once
31//! for a multi-file caller. [`apply_edits`] is the last byte-building step,
32//! exposed on its own for a caller that wants to filter or postpone the edits.
33//!
34//! ```
35//! use ocomment_core::{CommentKind, Language, ScanOptions, scan};
36//!
37//! let report = scan(b"let x = 1; // note\n", Language::Rust, ScanOptions::default());
38//! assert_eq!(report.comments.len(), 1);
39//! assert_eq!(report.comments[0].kind, CommentKind::Line);
40//! assert!(report.comments[0].disposition.is_remove());
41//! ```
42//!
43//! ```
44//! use ocomment_core::{Language, TransformOptions, transform};
45//!
46//! // A BOM, a CRLF ending, and a comment to take out.
47//! let source = "\u{feff}fn main() {} // trailing\r\n".as_bytes();
48//! let result = transform(source, Language::Rust, TransformOptions::default());
49//! assert_eq!(result.output, "\u{feff}fn main() {} \r\n".as_bytes());
50//! ```
51//!
52//! ```
53//! use ocomment_core::{Language, TransformOptions, apply_edits, transform};
54//!
55//! let source = b"let x = 1; // note\nlet y = 2; // and\n";
56//! let result = transform(source, Language::Rust, TransformOptions::default());
57//!
58//! // Sorted and non-overlapping, so one pass applies them.
59//! assert!(
60//! result
61//! .edits
62//! .windows(2)
63//! .all(|pair| pair[0].span.end <= pair[1].span.start)
64//! );
65//! assert_eq!(apply_edits(source, &result.edits), result.output);
66//! ```
67//!
68//! A [`TransformResult`] also carries a [`SourceMap`] between the original
69//! offsets and the new ones, which is what lets an editor keep a cursor, a
70//! diagnostic, or a breakpoint pointing at the right place after a removal.
71//!
72//! # What survives
73//!
74//! Every comment is classified as a [`CommentKind`] first — from its
75//! delimiters, then from its own text and position — and the [`Policy`] then
76//! decides that kind:
77//!
78//! | Kind | [`Policy::Safe`] | [`Policy::Legal`] | [`Policy::All`] |
79//! | --- | --- | --- | --- |
80//! | `line`, `block`, `doc-line`, `doc-block` | remove | remove | remove |
81//! | `license` | remove | keep | remove |
82//! | `directive`, `html-comment`, `optimizer-hint`, `version-comment` | keep | keep | remove |
83//! | `shebang`, `encoding` | keep | keep | keep unless forced |
84//!
85//! The shebang and the encoding declaration are the two a source needs to keep
86//! working, so even [`Policy::All`] leaves them until
87//! [`ScanOptions::force_protected`] says otherwise. The policy is the last word
88//! rather than the first: [`ScanOptions::keep_kinds`],
89//! [`ScanOptions::keep_regex`], [`ScanOptions::remove_kinds`] and
90//! [`ScanOptions::remove_regex`] are all tested before it.
91//!
92//! [`explain_disposition`] answers *why* for one comment, naming the rule that
93//! applied rather than summarising it, so a caller can quote the pattern, kind,
94//! or directive back to a user. [`explain_disposition_with`] takes pattern sets
95//! compiled once for a whole file. [`explain_comment`] answers it for a comment
96//! a scan produced, which is the same answer plus the one rule a comment's own
97//! bytes cannot account for: a YAML block scalar leaning on the comment that
98//! ends it keeps that comment because of where it sits.
99//!
100//! # Scanners this crate does not have
101//!
102//! [`transform_spans`] takes comment spans an external scanner already found
103//! and puts them through the same policy, layout, edit validation, and source
104//! map as a built-in scan, after checking that the spans are non-empty, sorted,
105//! non-overlapping, and inside the source. That is the hand-off point for a
106//! WebAssembly plugin.
107//!
108//! A [`DeclarativeProfile`] is the smaller answer: literal comment and string
109//! delimiters, read in a single byte-oriented pass, with the ambiguities that
110//! would make that pass wrong rejected up front by [`validate_profile`]. It
111//! needs no code, and it cannot express a syntax whose comments depend on more
112//! than delimiters.
113//!
114//! # Editing a live buffer
115//!
116//! [`IncrementalDocument`] rescans only what an edit disturbed and is the path
117//! the LSP server takes. [`IncrementalDocument::apply_changes`] is
118//! transactional: a batch that fails validation leaves the source, the report,
119//! the checkpoints, and the version exactly as they were, so a client that
120//! sends a stale or malformed batch cannot corrupt the document.
121//!
122//! ```
123//! use ocomment_core::{
124//! ByteSpan, DocumentChange, IncrementalDocument, IncrementalError, Language, ScanOptions,
125//! };
126//!
127//! let mut document = IncrementalDocument::new(
128//! b"let x = 1; // note\n".to_vec(),
129//! Language::Rust,
130//! ScanOptions::default(),
131//! 1,
132//! );
133//!
134//! // A span past the end of the document is refused, and nothing moves.
135//! let outside = ByteSpan::new(0, document.source().len() + 1);
136//! assert_eq!(
137//! document.apply_changes(
138//! &[DocumentChange {
139//! span: outside,
140//! replacement: Vec::new(),
141//! }],
142//! 2,
143//! ),
144//! Err(IncrementalError::InvalidSpan),
145//! );
146//! assert_eq!(document.version(), 1);
147//! assert_eq!(document.report().comments.len(), 1);
148//! ```
149
150mod detect;
151mod incremental;
152mod profile;
153mod scanner;
154mod transform;
155mod types;
156
157#[doc(hidden)]
158pub mod lexical_pool;
159
160pub use detect::{Detection, detect_language, shebang_interpreters};
161pub use incremental::{DocumentChange, IncrementalDocument, IncrementalError, PositionEncoding};
162pub use profile::{
163 BlockDelimiter, DeclarativeProfile, LineDelimiter, ProfileError, ProtectedPattern,
164 StringDelimiter, scan_profile, transform_profile, validate_profile,
165};
166pub use scanner::{
167 DispositionPatterns, PreparedScanner, explain_comment, explain_comment_with,
168 explain_disposition, explain_disposition_with, scan,
169};
170pub use transform::{apply_edits, transform, transform_plan, transform_spans};
171pub use types::*;