Skip to main content

codehelion_helper_protocol/
protocol.rs

1//! The wire format core and helpers agree on.
2//!
3//! Everything here is data: no process is spawned and no compiler is linked.
4//! A helper binary depends on this module to speak the protocol and on nothing
5//! else of codehelion, which is what keeps a toolchain dependency from reaching
6//! the analysis crates.
7//!
8//! # Framing
9//!
10//! Messages travel over the helper's standard input and output as
11//! length-prefixed frames: a four-byte big-endian payload length, a one-byte
12//! encoding tag, then the payload. The tag exists so that compiler IR too large
13//! to be worth serializing as text can travel as bytes later without changing
14//! how a frame is found in the stream — only [`Encoding`] gains a variant.
15//!
16//! A frame carries its own length so a reader never has to guess where a
17//! message ends, and never has to trust the sender's promise about total
18//! volume: [`MAX_FRAME_BYTES`] bounds what one frame may ask a reader to
19//! allocate, so a helper that has gone wrong cannot exhaust memory before it is
20//! noticed.
21
22use std::io::{Read, Write};
23
24use serde::{Deserialize, Serialize};
25
26use crate::ir::{CompilerIr, Unavailability, UnitRef};
27
28/// The only protocol revision this build speaks.
29///
30/// The product has not been released, so clients and helpers use the complete
31/// current protocol directly.
32pub const PROTOCOL_VERSION: u32 = 1;
33
34/// Largest payload a single frame may declare.
35///
36/// A reader allocates what the header says before it has seen the body, so
37/// this is the ceiling on what one malformed or hostile frame can cost.
38pub const MAX_FRAME_BYTES: u32 = 64 * 1024 * 1024;
39
40/// How a frame's payload is encoded.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(rename_all = "snake_case")]
43#[repr(u8)]
44pub enum Encoding {
45    /// UTF-8 JSON. The only encoding this revision writes.
46    Json = 0,
47}
48
49impl Encoding {
50    /// The tag byte written into the frame header.
51    #[must_use]
52    pub const fn tag(self) -> u8 {
53        self as u8
54    }
55
56    /// The encoding a tag byte names, or `None` if this build has no such
57    /// encoding — which is how a frame from a newer peer is refused rather
58    /// than misread.
59    #[must_use]
60    pub const fn from_tag(tag: u8) -> Option<Self> {
61        match tag {
62            0 => Some(Self::Json),
63            _ => None,
64        }
65    }
66}
67
68/// Something a helper can be asked for.
69///
70/// A helper reports the subset it can supply during the handshake, and a run
71/// asks for nothing outside that subset. The variants are the information
72/// kinds semantic analysis is built from; a helper that offers fewer is not
73/// broken, it is less capable, and [`Capability::absence`] says what that costs.
74#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
75#[serde(rename_all = "snake_case")]
76pub enum Capability {
77    /// Resolved types for expressions and bindings.
78    Types,
79    /// Which definition each name refers to, and whether it is outside the
80    /// scanned code.
81    NameResolution,
82    /// Resolved call targets.
83    CallTargets,
84    /// A control-flow graph built from the compiler's own.
85    MirCfg,
86    /// Macro expansion with both spelling and expansion locations.
87    MacroExpansion,
88    /// Template or generic instantiation traced to its definition.
89    TemplateInstantiation,
90}
91
92/// What a run does when a helper cannot supply a capability.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum Absence {
95    /// Continue without it, recording what was not available.
96    Degrade,
97    /// Refuse semantic analysis: without this the result would be a weaker
98    /// answer wearing a stronger name.
99    Refuse,
100}
101
102impl Capability {
103    /// Every capability this protocol revision understands.
104    pub const ALL: [Self; 6] = [
105        Self::Types,
106        Self::NameResolution,
107        Self::CallTargets,
108        Self::MirCfg,
109        Self::MacroExpansion,
110        Self::TemplateInstantiation,
111    ];
112
113    /// Stable lowercase identifier, the same spelling this serializes as.
114    #[must_use]
115    pub const fn name(self) -> &'static str {
116        match self {
117            Self::Types => "types",
118            Self::NameResolution => "name_resolution",
119            Self::CallTargets => "call_targets",
120            Self::MirCfg => "mir_cfg",
121            Self::MacroExpansion => "macro_expansion",
122            Self::TemplateInstantiation => "template_instantiation",
123        }
124    }
125
126    /// What its absence costs.
127    ///
128    /// Two are load-bearing, for the same reason stated twice. Semantic mode
129    /// exists to answer with what the compiler knows rather than with what the
130    /// text looks like: without resolved types a run reports syntactic findings
131    /// under a semantic label, and without name resolution it decides which
132    /// names to compare on by guessing from their spelling, which is the
133    /// structural answer wearing the same stronger name.
134    ///
135    /// Everything else refines an answer those two make possible in the first
136    /// place, so missing any of them narrows the result rather than misnaming
137    /// it.
138    #[must_use]
139    pub const fn absence(self) -> Absence {
140        match self {
141            Self::Types | Self::NameResolution => Absence::Refuse,
142            Self::CallTargets
143            | Self::MirCfg
144            | Self::MacroExpansion
145            | Self::TemplateInstantiation => Absence::Degrade,
146        }
147    }
148}
149
150/// A message from core to a helper.
151#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
152pub struct Request {
153    /// The revision this message is written in.
154    pub protocol_version: u32,
155    /// Correlates a response with the request that asked for it.
156    pub id: u64,
157    /// What is being asked.
158    pub body: RequestBody,
159}
160
161/// The askable things.
162#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
163#[serde(tag = "kind", rename_all = "snake_case")]
164pub enum RequestBody {
165    /// Identify yourself and say what you can do.
166    Handshake(ClientIdentity),
167    /// Say what the code in a tree is analyzed under.
168    ///
169    /// Asked before any unit is, because what a run records its answers under
170    /// has to be settled before there are answers to record.
171    DescribeBuild(DescribeBuild),
172    /// Analyze one unit and return what the compiler knows about it.
173    Analyze(Analyze),
174    /// Finish outstanding work and exit.
175    Shutdown,
176}
177
178/// The tree whose build is being asked about.
179#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
180pub struct DescribeBuild {
181    /// A directory inside the project, as this machine spells it.
182    ///
183    /// Not necessarily the project's own root: a scan can be rooted at one
184    /// member of a workspace, and finding the project from there is the
185    /// helper's job because it is the side that knows what a project is.
186    pub root: String,
187}
188
189/// The conditions a tree's code is analyzed under.
190///
191/// What belongs here is what changes the answers rather than what changes the
192/// build: two runs that resolve the same names to the same things are one
193/// variant however differently they were invoked, and two that do not are two
194/// however alike the command line looked.
195///
196/// Empty on both counts when the helper found no project to describe. That is
197/// not the same claim as a project that enables nothing — a described build
198/// always has settings, because the target alone supplies a dozen — so nothing
199/// has to be spelled to tell the two apart.
200#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
201pub struct BuildDescription {
202    /// Enabled features, each qualified by the package that enables it.
203    ///
204    /// Qualified because a feature is declared per package: `serde/derive` and
205    /// `ledger/derive` are unrelated facts, and an unqualified list would let
206    /// one package's selection stand in for another's.
207    pub features: Vec<String>,
208    /// The conditional-compilation settings the code is read under, as the
209    /// compiler spells them.
210    pub cfgs: Vec<String>,
211}
212
213/// One unit to analyze, and what is wanted from it.
214#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
215pub struct Analyze {
216    /// Which unit.
217    pub unit: UnitRef,
218    /// The exact compilation-database entry to use for a C or C++ request.
219    ///
220    /// A source path is not enough: a database may intentionally list it more
221    /// than once under different `-D` settings. This carries the complete
222    /// recorded command identity rather than a database index, because an
223    /// index changes when an unrelated command is inserted or reordered.
224    pub compile_command: Option<CompileCommandSelector>,
225    /// Canonical scan-root boundary for paths a compilation command may read.
226    ///
227    /// Set only for an untrusted scan. Helpers must refuse path-bearing
228    /// compiler arguments that resolve outside this directory; omitting it
229    /// preserves the configured, trusted compilation-database behaviour.
230    pub read_boundary: Option<String>,
231    /// What to spend time on.
232    ///
233    /// Never more than the helper offered at handshake. Asking for less than it
234    /// can do is how a run that needs only types avoids paying for a
235    /// control-flow graph nobody will read.
236    pub want: Vec<Capability>,
237    /// What the helper may run out of the project while answering.
238    ///
239    /// Empty unless somebody said otherwise.
240    pub permitted: Vec<Execution>,
241}
242
243/// One stable, exact selector for an entry in `compile_commands.json`.
244///
245/// The path is the entry's source after resolving it against `directory`; the
246/// command remains one argument per element so quoting cannot alter its
247/// meaning between the scanner and helper. Together they name one database
248/// entry without relying on its position in a generated file.
249#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
250pub struct CompileCommandSelector {
251    /// The translation-unit source path.
252    pub file: String,
253    /// The command's working directory, when the database recorded one.
254    pub directory: Option<String>,
255    /// The recorded compiler invocation, including its compiler and source.
256    pub arguments: Vec<String>,
257}
258
259impl CompileCommandSelector {
260    /// Whether this and `other` name one entry of one compilation database.
261    ///
262    /// Not derived equality, because the two are built by two programs out of
263    /// one database and their paths are two resolvings of one file. Those are
264    /// compared as paths, and past a Windows verbatim prefix that only one of
265    /// the two need have come back carrying — where they are compared as
266    /// strings instead, no entry matches any request and every unit of a C or
267    /// C++ project comes back with no build information.
268    ///
269    /// The arguments are compared exactly: they are the words the database
270    /// recorded, which neither side resolved and neither side may reword.
271    #[must_use]
272    pub fn names_the_same_entry(&self, other: &Self) -> bool {
273        fn one_path(left: &str, right: &str) -> bool {
274            crate::ir::ordinary(std::path::Path::new(left))
275                == crate::ir::ordinary(std::path::Path::new(right))
276        }
277        self.arguments == other.arguments
278            && one_path(&self.file, &other.file)
279            && match (self.directory.as_deref(), other.directory.as_deref()) {
280                (Some(mine), Some(theirs)) => one_path(mine, theirs),
281                (None, None) => true,
282                _ => false,
283            }
284    }
285}
286
287/// Something a helper may be permitted to run out of the project it is
288/// analyzing.
289///
290/// Named per class rather than as one switch, for the reason the tool's own
291/// permissions are: expanding a macro the project's developers already run and
292/// executing a configure step that may reach the network are decisions of
293/// different weight, and a single permission would make agreeing to either mean
294/// agreeing to both.
295#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
296#[serde(rename_all = "kebab-case")]
297pub enum Execution {
298    /// A Cargo build script.
299    BuildScript,
300    /// A procedural macro, expanded by compiling and calling it.
301    ProcMacro,
302    /// A configure step: `CMake`, autotools, or a generator script.
303    Configure,
304    /// A compiler wrapper the project interposes.
305    CompilerWrapper,
306    /// A command that generates source files.
307    GeneratedSource,
308}
309
310impl Execution {
311    /// Stable identifier, the same spelling this serializes as and the same
312    /// one a person types to permit it.
313    #[must_use]
314    pub const fn name(self) -> &'static str {
315        match self {
316            Self::BuildScript => "build-script",
317            Self::ProcMacro => "proc-macro",
318            Self::Configure => "configure",
319            Self::CompilerWrapper => "compiler-wrapper",
320            Self::GeneratedSource => "generated-source",
321        }
322    }
323
324    /// The class a name refers to, or `None` for one this build cannot name.
325    #[must_use]
326    pub fn from_name(name: &str) -> Option<Self> {
327        [
328            Self::BuildScript,
329            Self::ProcMacro,
330            Self::Configure,
331            Self::CompilerWrapper,
332            Self::GeneratedSource,
333        ]
334        .into_iter()
335        .find(|class| class.name() == name)
336    }
337}
338
339/// Who is connecting, and which revisions it can speak.
340#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
341pub struct ClientIdentity {
342    /// Name of the connecting program.
343    pub client: String,
344    /// Its version, for diagnostics rather than for negotiation.
345    pub client_version: String,
346    /// The exact protocol revision it speaks.
347    pub protocol: u32,
348}
349
350/// A message from a helper to core.
351#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
352pub struct Response {
353    /// The revision this message is written in.
354    pub protocol_version: u32,
355    /// The request this answers.
356    pub id: u64,
357    /// The answer.
358    pub body: ResponseBody,
359}
360
361/// The answers.
362#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
363#[serde(tag = "kind", rename_all = "snake_case")]
364pub enum ResponseBody {
365    /// Who the helper is and what it can do.
366    Handshake(Box<HelperIdentity>),
367    /// What the tree's code is analyzed under.
368    Build(Box<BuildDescription>),
369    /// What the compiler knows about the unit.
370    Analyzed(Box<CompilerIr>),
371    /// Nothing can be known about the unit, and why.
372    ///
373    /// Distinct from [`ResponseBody::Failed`]: the helper is working, and this
374    /// unit is one it cannot analyze. A scan carries on and says so.
375    Unavailable {
376        /// Which unit.
377        unit: UnitRef,
378        /// Why it cannot be analyzed.
379        reason: Unavailability,
380    },
381    /// Shutdown acknowledged.
382    Shutdown,
383    /// The request could not be answered.
384    Failed(Failure),
385}
386
387/// What a helper says about itself.
388#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
389pub struct HelperIdentity {
390    /// Helper name, as `doctor` reports it.
391    pub name: String,
392    /// Helper version.
393    pub version: String,
394    /// The exact protocol revision it speaks.
395    pub protocol: u32,
396    /// The toolchains it was built against, as the compiler spells them.
397    ///
398    /// A helper built for one compiler release cannot be trusted against
399    /// another, so this is matched against the project's own toolchain rather
400    /// than assumed compatible.
401    pub toolchains: Vec<String>,
402    /// What it can supply.
403    pub capabilities: Vec<Capability>,
404    /// The classes of execution it will act on when it is permitted them.
405    ///
406    /// Stated so that permitting something this helper would not do can be
407    /// refused rather than accepted and forgotten. A permission that changes
408    /// nothing is worse than one that is turned down: somebody granted it, and
409    /// the thin answer that follows looks like the project's own.
410    pub executes: Vec<Execution>,
411}
412
413/// Why a request could not be answered.
414#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
415pub struct Failure {
416    /// A stable short code for programmatic handling.
417    pub code: String,
418    /// What went wrong, for a person.
419    pub message: String,
420}
421
422/// Something that went wrong reading or writing a frame.
423#[derive(Debug, thiserror::Error)]
424pub enum FrameError {
425    /// The stream ended where a frame was expected.
426    #[error("the stream ended mid-frame")]
427    Truncated,
428    /// The header named a payload larger than [`MAX_FRAME_BYTES`].
429    #[error("a frame declared {declared} bytes, over the {MAX_FRAME_BYTES} ceiling")]
430    TooLarge {
431        /// What the header claimed.
432        declared: u32,
433    },
434    /// The header named an encoding this build does not have.
435    #[error("a frame arrived in unknown encoding {tag}")]
436    UnknownEncoding {
437        /// The tag byte that was read.
438        tag: u8,
439    },
440    /// The payload was not the message it claimed to be.
441    #[error("a frame's payload did not parse: {0}")]
442    Malformed(#[from] serde_json::Error),
443    /// The underlying stream failed.
444    #[error("the stream failed: {0}")]
445    Io(#[from] std::io::Error),
446}
447
448/// Header length: four bytes of payload length plus one encoding tag.
449const HEADER_BYTES: usize = 5;
450
451/// Write `value` as one JSON frame.
452///
453/// # Errors
454///
455/// Fails if the value cannot be serialized or the stream cannot take it.
456pub fn write_frame<W: Write, T: Serialize>(writer: &mut W, value: &T) -> Result<(), FrameError> {
457    let payload = serde_json::to_vec(value)?;
458    let length =
459        u32::try_from(payload.len()).map_err(|_| FrameError::TooLarge { declared: u32::MAX })?;
460    if length > MAX_FRAME_BYTES {
461        return Err(FrameError::TooLarge { declared: length });
462    }
463    let mut header = [0u8; HEADER_BYTES];
464    header[..4].copy_from_slice(&length.to_be_bytes());
465    header[4] = Encoding::Json.tag();
466    writer.write_all(&header)?;
467    writer.write_all(&payload)?;
468    writer.flush()?;
469    Ok(())
470}
471
472/// Read one frame and parse it.
473///
474/// Returns `Ok(None)` when the stream ends cleanly between frames, which is how
475/// a peer that has finished is told apart from one that died mid-message.
476///
477/// # Errors
478///
479/// Fails on a truncated frame, an oversized or unknown-encoding header, a
480/// payload that does not parse, or a stream error.
481pub fn read_frame<R: Read, T: for<'de> Deserialize<'de>>(
482    reader: &mut R,
483) -> Result<Option<T>, FrameError> {
484    let mut header = [0u8; HEADER_BYTES];
485    match read_exact_or_eof(reader, &mut header)? {
486        Read0::Eof => return Ok(None),
487        Read0::Partial => return Err(FrameError::Truncated),
488        Read0::Full => {}
489    }
490    let length = u32::from_be_bytes([header[0], header[1], header[2], header[3]]);
491    if length > MAX_FRAME_BYTES {
492        return Err(FrameError::TooLarge { declared: length });
493    }
494    if Encoding::from_tag(header[4]).is_none() {
495        return Err(FrameError::UnknownEncoding { tag: header[4] });
496    }
497    let mut payload = vec![0u8; length as usize];
498    match read_exact_or_eof(reader, &mut payload)? {
499        Read0::Full => {}
500        Read0::Eof | Read0::Partial => return Err(FrameError::Truncated),
501    }
502    Ok(Some(serde_json::from_slice(&payload)?))
503}
504
505/// How much of a buffer a read managed to fill.
506enum Read0 {
507    /// Nothing at all: the stream ended between frames.
508    Eof,
509    /// Some but not all: the stream ended inside a frame.
510    Partial,
511    /// All of it.
512    Full,
513}
514
515fn read_exact_or_eof<R: Read>(reader: &mut R, buffer: &mut [u8]) -> Result<Read0, std::io::Error> {
516    let mut filled = 0;
517    while filled < buffer.len() {
518        let read = reader.read(&mut buffer[filled..])?;
519        if read == 0 {
520            return Ok(if filled == 0 {
521                Read0::Eof
522            } else {
523                Read0::Partial
524            });
525        }
526        filled += read;
527    }
528    Ok(Read0::Full)
529}
530
531#[cfg(test)]
532#[allow(clippy::expect_used, clippy::unwrap_used)]
533mod tests {
534    use super::*;
535
536    fn identity() -> HelperIdentity {
537        HelperIdentity {
538            name: "mock".into(),
539            version: "0.1.0".into(),
540            protocol: PROTOCOL_VERSION,
541            toolchains: vec!["rustc 1.85.0".into()],
542            capabilities: vec![Capability::Types, Capability::CallTargets],
543            executes: vec![Execution::BuildScript],
544        }
545    }
546
547    #[test]
548    fn a_frame_survives_the_round_trip() {
549        let message = Response {
550            protocol_version: PROTOCOL_VERSION,
551            id: 7,
552            body: ResponseBody::Handshake(Box::new(identity())),
553        };
554        let mut buffer = Vec::new();
555        write_frame(&mut buffer, &message).unwrap();
556        let back: Option<Response> = read_frame(&mut buffer.as_slice()).unwrap();
557        assert_eq!(back, Some(message));
558    }
559
560    #[test]
561    fn frames_are_read_one_at_a_time_from_one_stream() {
562        let mut buffer = Vec::new();
563        for id in 0..3u64 {
564            write_frame(
565                &mut buffer,
566                &Response {
567                    protocol_version: PROTOCOL_VERSION,
568                    id,
569                    body: ResponseBody::Shutdown,
570                },
571            )
572            .unwrap();
573        }
574        let mut stream = buffer.as_slice();
575        for id in 0..3u64 {
576            let message: Response = read_frame(&mut stream).unwrap().unwrap();
577            assert_eq!(message.id, id);
578        }
579        assert!(read_frame::<_, Response>(&mut stream).unwrap().is_none());
580    }
581
582    #[test]
583    fn a_stream_that_ends_between_frames_is_not_an_error() {
584        let empty: &[u8] = &[];
585        assert!(read_frame::<_, Response>(&mut { empty }).unwrap().is_none());
586    }
587
588    #[test]
589    fn a_stream_that_ends_inside_a_frame_is_an_error() {
590        let mut buffer = Vec::new();
591        write_frame(
592            &mut buffer,
593            &Response {
594                protocol_version: PROTOCOL_VERSION,
595                id: 1,
596                body: ResponseBody::Shutdown,
597            },
598        )
599        .unwrap();
600        buffer.truncate(buffer.len() - 1);
601        let error = read_frame::<_, Response>(&mut buffer.as_slice()).unwrap_err();
602        assert!(matches!(error, FrameError::Truncated), "{error:?}");
603    }
604
605    #[test]
606    fn an_oversized_header_is_refused_before_anything_is_allocated() {
607        let mut header = [0u8; HEADER_BYTES];
608        header[..4].copy_from_slice(&(MAX_FRAME_BYTES + 1).to_be_bytes());
609        let error = read_frame::<_, Response>(&mut header.as_slice()).unwrap_err();
610        assert!(matches!(error, FrameError::TooLarge { .. }), "{error:?}");
611    }
612
613    #[test]
614    fn an_encoding_this_build_lacks_is_refused_rather_than_guessed() {
615        let mut buffer = Vec::new();
616        write_frame(
617            &mut buffer,
618            &Response {
619                protocol_version: PROTOCOL_VERSION,
620                id: 1,
621                body: ResponseBody::Shutdown,
622            },
623        )
624        .unwrap();
625        buffer[4] = 9;
626        let error = read_frame::<_, Response>(&mut buffer.as_slice()).unwrap_err();
627        assert!(
628            matches!(error, FrameError::UnknownEncoding { tag: 9 }),
629            "{error:?}"
630        );
631    }
632
633    /// One spelling, kept in one place. A stored capability and a transmitted
634    /// one that disagree would make a database written by this build unreadable
635    /// by it.
636    #[test]
637    fn what_a_capability_is_called_is_what_it_is_sent_as() {
638        for capability in Capability::ALL {
639            let sent = serde_json::to_string(&capability).unwrap();
640            assert_eq!(sent, format!("\"{}\"", capability.name()));
641        }
642    }
643
644    /// The same rule as for capabilities, and for the same reason: what is
645    /// stored, what is typed and what is sent are one spelling.
646    #[test]
647    fn what_an_execution_class_is_called_is_what_it_is_sent_as() {
648        for class in [
649            Execution::BuildScript,
650            Execution::ProcMacro,
651            Execution::Configure,
652            Execution::CompilerWrapper,
653            Execution::GeneratedSource,
654        ] {
655            let sent = serde_json::to_string(&class).unwrap();
656            assert_eq!(sent, format!("\"{}\"", class.name()));
657        }
658    }
659
660    /// A name nobody recognises is not a class. Reading it as the catch-all
661    /// would let a misspelling travel as a permission, and the whole point of
662    /// naming classes is that granting one grants exactly one.
663    #[test]
664    fn a_class_nobody_can_name_is_not_read_as_the_one_with_no_name() {
665        assert_eq!(
666            Execution::from_name("build-script"),
667            Some(Execution::BuildScript)
668        );
669        assert_eq!(Execution::from_name("build-scripts"), None);
670        assert_eq!(Execution::from_name("unknown"), None);
671        assert!(
672            serde_json::from_str::<Vec<Execution>>(r#"["build-script","something-newer"]"#)
673                .is_err()
674        );
675    }
676
677    #[test]
678    fn a_capability_this_build_cannot_name_is_rejected() {
679        assert!(
680            serde_json::from_str::<Vec<Capability>>(r#"["types","overload_resolution"]"#).is_err()
681        );
682    }
683
684    /// The two that decide what a comparison is made of. Everything else
685    /// sharpens a comparison that these make possible at all.
686    #[test]
687    fn what_a_comparison_is_made_of_is_worth_refusing_over() {
688        assert_eq!(Capability::Types.absence(), Absence::Refuse);
689        assert_eq!(Capability::NameResolution.absence(), Absence::Refuse);
690        for capability in [
691            Capability::CallTargets,
692            Capability::MirCfg,
693            Capability::MacroExpansion,
694            Capability::TemplateInstantiation,
695        ] {
696            assert_eq!(capability.absence(), Absence::Degrade, "{capability:?}");
697        }
698    }
699
700    fn selector(file: &str, directory: Option<&str>) -> CompileCommandSelector {
701        CompileCommandSelector {
702            file: file.to_owned(),
703            directory: directory.map(ToOwned::to_owned),
704            arguments: vec!["clang++".into(), "-c".into(), "a.cpp".into()],
705        }
706    }
707
708    /// The scanner and the helper each resolve the database's paths for
709    /// themselves, and one of them coming back with the verbatim form is a
710    /// difference in how the path was written down rather than in which file
711    /// it names.
712    #[test]
713    fn one_entry_resolved_by_two_programs_is_one_entry() {
714        let plain = selector("C:/w/a.cpp", Some("C:/w"));
715        let verbatim = selector(r"\\?\C:/w/a.cpp", Some(r"\\?\C:/w"));
716        assert!(plain.names_the_same_entry(&verbatim));
717        assert!(verbatim.names_the_same_entry(&plain));
718        assert!(plain.names_the_same_entry(&plain));
719    }
720
721    /// A database may list one source more than once under different settings,
722    /// which is the whole reason a selector carries the command.
723    #[test]
724    fn two_commands_over_one_source_are_two_entries() {
725        let mut other = selector("C:/w/a.cpp", Some("C:/w"));
726        other.arguments.push("-DWIDE".into());
727        assert!(!selector("C:/w/a.cpp", Some("C:/w")).names_the_same_entry(&other));
728        assert!(
729            !selector("C:/w/a.cpp", Some("C:/w"))
730                .names_the_same_entry(&selector("C:/w/b.cpp", Some("C:/w")))
731        );
732        assert!(
733            !selector("C:/w/a.cpp", Some("C:/w"))
734                .names_the_same_entry(&selector("C:/w/a.cpp", None))
735        );
736    }
737}