diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c5c0681..c2db3a1 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,13 +1,7 @@ -# Human review is required for what defines "passing" and for what publishes: -# CI and release workflows, public contracts, upstream pins, and agent instructions. -# Rules stay narrow so review is rare. Ordinary code, tests, and docs -# stay unowned, so agents may review and merge them. /.github/CODEOWNERS @Teakowa /.github/workflows/ @Teakowa -# The protocol spec is the public contract /docs/spec/ @Teakowa -# Toolchain and release /rust-toolchain.toml @Teakowa /release-please-config.json @Teakowa diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 460e92e..bd055ee 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,14 +1,10 @@ -# Dependabot version updates for language-provider-protocol. -# Docs: https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 updates: - # Rust workspace: the root manifest governs both conformance crates. - package-ecosystem: cargo directory: / schedule: interval: monthly - # CI workflows and their action pins. - package-ecosystem: github-actions directory: / schedule: diff --git a/Cargo.toml b/Cargo.toml index 94ebdc0..a804108 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,25 +14,17 @@ serde = "1" serde_json = "1" [workspace.lints.rust] -# Unsafe operations inside an unsafe function still need an explicit block so -# the unsafe surface and its invariants remain visible at the operation site. unsafe_op_in_unsafe_fn = "deny" -# Public items in a private module are not part of a usable crate API. unreachable_pub = "deny" [workspace.lints.clippy] -# These groups cover bug-prone findings without enabling broad style or -# restriction policies; CI promotes the remaining warning-level checks. correctness = { level = "deny", priority = -1 } suspicious = { level = "deny", priority = -1 } perf = { level = "warn", priority = -1 } -# These development/progress macros must not reach protocol code. dbg_macro = "deny" todo = "deny" unimplemented = "deny" -# Reduce dependency artifact volume while retaining line-table diagnostics for -# WrightKit code. See wrightkit/.github docs/rust-build-artifacts.md. [profile.dev] debug = 1 diff --git a/conformance/common/src/lib.rs b/conformance/common/src/lib.rs index 8a687da..49d32c3 100644 --- a/conformance/common/src/lib.rs +++ b/conformance/common/src/lib.rs @@ -1,9 +1,3 @@ -//! Helpers shared by the conformance runner and the reference mock provider. -//! -//! The runner expands `${PROJECT_URI}` in fixtures and the mock provider -//! resolves `entry` URIs back to paths; both sides must agree on the file-URI -//! codec byte for byte. - use std::path::{Path, PathBuf}; /// `file:` URI for an absolute path, percent-encoding bytes outside the diff --git a/conformance/mock-provider/src/main.rs b/conformance/mock-provider/src/main.rs index 2954ed5..f4018f9 100644 --- a/conformance/mock-provider/src/main.rs +++ b/conformance/mock-provider/src/main.rs @@ -1,10 +1,3 @@ -//! `lpp-mock-provider`: the reference LPP provider for `x-demo-lang`. -//! -//! Layout: `server` owns session state and dispatch (the method table lives -//! next to the handlers in `methods`), `project` resolves `entry` targets -//! into document sets, `wire` holds the request param shapes, `rpc` builds -//! response envelopes, and `puzzle` is the x-demo-lang implementation. - mod methods; mod project; mod puzzle; @@ -64,8 +57,6 @@ fn write_response(out: &mut impl Write, response: &Value) -> io::Result<()> { out.flush() } -/// `--protocol-version ` pins the supported version; `--without a,b,c` -/// disables capabilities (used by the capability-negotiation fixtures). fn parse_args() -> (String, HashSet<&'static str>) { let mut version = DEFAULT_PROTOCOL_VERSION.to_string(); let mut disabled = HashSet::new(); diff --git a/conformance/mock-provider/src/methods.rs b/conformance/mock-provider/src/methods.rs index 8851e7f..c4d9702 100644 --- a/conformance/mock-provider/src/methods.rs +++ b/conformance/mock-provider/src/methods.rs @@ -1,11 +1,3 @@ -//! `lpp/*` method handlers and the `METHODS` dispatch table. -//! -//! Handlers share three pieces of plumbing: `documents_for_request` resolves -//! the document set (inline `documents` or a loaded `entry` project), -//! `parsed_set` checks and parses it in URI order, and `symbol_at_position` -//! resolves the document/position/symbol preamble that definition, -//! references, and rename share. - use std::collections::HashMap; use serde_json::{Value, json}; @@ -25,15 +17,12 @@ use crate::wire::{ RenameParams, ResolveIdsParams, ValidateEditsParams, }; -/// A dispatchable `lpp/*` method: wire name, gating capability, handler. pub(crate) struct Method { pub name: &'static str, pub capability: &'static str, pub handler: fn(&Server, Value) -> Result, } -/// Every `lpp/*` request method. Dispatch, capability gating, and the -/// `Method not found` fallback all route through this table. pub(crate) const METHODS: &[Method] = &[ Method { name: "lpp/check", @@ -259,9 +248,6 @@ impl Server { let edits: Vec = sorted_entries(¶ms.documents) .into_iter() .map(|(key, doc)| { - // §15.2: every reference in every received document must be - // covered, so a document that did not parse cleanly makes the - // rename unsafe — refuse instead of emitting partial edits. let Some(puzzle) = parse_document(&doc.text).into_puzzle() else { return Err(HandlerError::lpp( "invalidDocument", @@ -387,15 +373,10 @@ impl Server { } } -/// The most ids (calls plus enum members, duplicates counted) one -/// `lpp/resolveIds` request may carry. const MAX_RESOLVE_IDS: usize = 1024; -/// The provider-side bound applied when a request omits `limit`. const DEFAULT_LOOKUP_LIMIT: u32 = 20; -/// Reads `acceptedArtifactFormats` from `lpp/compile` params. Valid only -/// in LPP 1.4-or-later sessions, as a non-empty array of strings. fn accepted_artifact_formats( server: &Server, params: &Value, @@ -415,8 +396,6 @@ fn accepted_artifact_formats( fn select_artifact_format(accepted: Option>) -> Result<&'static str, HandlerError> { match accepted { None => Ok(ARTIFACT_FORMAT), - // The client's list is in preference order: the first accepted format - // we support wins. Some(accepted) => accepted .iter() .find_map(|a| { @@ -446,14 +425,12 @@ fn sha256_hex(text: &str) -> String { }) } -/// Document-set entries in key order. fn sorted_entries(map: &HashMap) -> Vec<(&String, &Document)> { let mut entries: Vec<(&String, &Document)> = map.iter().collect(); entries.sort_by(|a, b| a.0.cmp(b.0)); entries } -/// Documents in document-set key order. fn sorted_docs(map: &HashMap) -> Vec<&Document> { sorted_entries(map) .into_iter() @@ -461,7 +438,6 @@ fn sorted_docs(map: &HashMap) -> Vec<&Document> { .collect() } -/// Check and parse every document in document-set key order. fn parsed_set( map: &HashMap, ) -> Result, HandlerError> { @@ -495,8 +471,6 @@ fn check_document(doc: &Document) -> Result<(), HandlerError> { Ok(()) } -/// `{ uri, version, : }` — the per-document view shape shared by -/// check, compile, and symbols. fn document_view(doc: &Document, key: &str, value: Value) -> Value { let mut view = json!({ "uri": doc.uri, "version": doc.version }); view[key] = value; @@ -507,8 +481,6 @@ fn location_json(uri: &str, range: Range) -> Value { json!({ "uri": uri, "range": range }) } -/// The shared definition/references/rename preamble: document checks, then -/// the symbol under `position`, or the method's `noSymbolAtPosition` refusal. fn symbol_at_position( doc: &Document, position: Position, diff --git a/conformance/mock-provider/src/project.rs b/conformance/mock-provider/src/project.rs index 4bddbf4..45eaaca 100644 --- a/conformance/mock-provider/src/project.rs +++ b/conformance/mock-provider/src/project.rs @@ -1,7 +1,3 @@ -//! Project loading: resolving an `entry` target (LPP 1.1 file, LPP 1.2 -//! directory) into the in-memory document set that `lpp/check` and -//! `lpp/compile` consume. - use std::collections::HashMap; use std::path::PathBuf; @@ -12,9 +8,6 @@ use crate::rpc::HandlerError; use crate::server::{LANGUAGE_ID, Server}; use crate::wire::{DocsParams, Document, ProjectEntry}; -/// Resolve the document set for a `lpp/check`/`lpp/compile`-style request: -/// either the inline `documents` map or a project `entry` to load. -/// Returns the set and the canonical entry URI when loaded from a project. pub(crate) fn documents_for_request( server: &Server, params: DocsParams, @@ -50,15 +43,12 @@ enum Target { Directory, } -/// Project loading tied to one request's `ProjectEntry`. The shared error -/// constructors live here so call sites name only the reason and message. struct Loader<'a> { server: &'a Server, entry: &'a ProjectEntry, } impl Loader<'_> { - /// `invalidEntry`: the request's entry object failed validation. fn invalid(&self, reason: &str, message: impl Into) -> HandlerError { HandlerError::lpp( "invalidEntry", @@ -67,8 +57,6 @@ impl Loader<'_> { ) } - /// `projectLoadFailed`: filesystem resolution failed. `uri` is the - /// concrete file URI when one is known. fn fail(&self, reason: &str, uri: Option<&str>, message: impl Into) -> HandlerError { let mut details = json!({ "entryUri": self.entry.uri, "reason": reason }); if let Some(uri) = uri { @@ -171,9 +159,6 @@ impl Loader<'_> { ) })?; - // The project is the `.xdl` files in the selected entry's directory. - // A failed directory entry fails the load rather than silently - // dropping a required source (spec section 8.1). let entries: Vec<_> = std::fs::read_dir(&root) .map_err(|error| { self.fail( @@ -197,8 +182,6 @@ impl Loader<'_> { if path.extension().is_none_or(|ext| ext != "xdl") { continue; } - // A directory named `*.xdl` is not a source file; symlinks - // still resolve (or fail) through canonicalization below. let is_dir = entry .file_type() .map_err(|error| { @@ -219,8 +202,6 @@ impl Loader<'_> { let mut documents = HashMap::new(); for path in paths { - // The failing file's URI is knowable before canonicalization; - // `details.uri` should name it (spec section 8.1). let unresolved_uri = path_to_file_uri(&path); let path = std::fs::canonicalize(&path).map_err(|error| { self.fail( diff --git a/conformance/mock-provider/src/puzzle.rs b/conformance/mock-provider/src/puzzle.rs index 41dee30..5ffb6b0 100644 --- a/conformance/mock-provider/src/puzzle.rs +++ b/conformance/mock-provider/src/puzzle.rs @@ -1,8 +1,3 @@ -//! The `x-demo-lang` puzzle language: a document declares one equation puzzle -//! (target, start, named arithmetic ops, and a solution applying ops in -//! sequence). The language is deliberately unlike OPY and DEL so the mock -//! provider cannot accidentally satisfy two-language assumptions in LPP. - use serde::Serialize; mod artifact; @@ -133,7 +128,6 @@ pub(crate) struct ParseOutput { } impl ParseOutput { - /// The puzzle when the document parsed without error diagnostics. pub(crate) fn into_puzzle(self) -> Option { match self.puzzle { Some(puzzle) if !self.has_errors() => Some(puzzle), diff --git a/conformance/mock-provider/src/puzzle/artifact.rs b/conformance/mock-provider/src/puzzle/artifact.rs index ade6065..21469c0 100644 --- a/conformance/mock-provider/src/puzzle/artifact.rs +++ b/conformance/mock-provider/src/puzzle/artifact.rs @@ -1,7 +1,3 @@ -//! Artifact formats produced by `lpp/compile` and consumed by -//! `lpp/reconstruct`. Artifact payloads are opaque to LPP; only the mock -//! provider interprets them. - use serde::Deserialize; use serde_json::{Value, json}; use std::fmt::Write; @@ -9,7 +5,6 @@ use std::fmt::Write; use super::Puzzle; pub(crate) const ARTIFACT_FORMAT: &str = "x-demo/puzzle-eval-v1"; -/// Compile-only format: `lpp/reconstruct` does not support it. pub(crate) const SUMMARY_ARTIFACT_FORMAT: &str = "x-demo/puzzle-summary-v1"; pub(crate) const COMPILE_ARTIFACT_FORMATS: [&str; 2] = [ARTIFACT_FORMAT, SUMMARY_ARTIFACT_FORMAT]; @@ -48,16 +43,12 @@ struct SheetOp { arg: i64, } -/// The `x-demo-lang` identifier grammar: `[A-Za-z_][A-Za-z0-9_]*`. fn is_ident(name: &str) -> bool { let mut chars = name.chars(); matches!(chars.next(), Some(c) if c.is_ascii_alphabetic() || c == '_') && chars.all(|c| c.is_ascii_alphanumeric() || c == '_') } -/// Canonical source text regenerated from a puzzle evaluation sheet. -/// Names are emitted verbatim, so every one must be a valid identifier or -/// the output would not parse (or would carry injected syntax). pub(crate) fn reconstruct_source(content: &str) -> Option { let sheet: Sheet = serde_json::from_str(content).ok()?; if !is_ident(&sheet.name) diff --git a/conformance/mock-provider/src/puzzle/catalog.rs b/conformance/mock-provider/src/puzzle/catalog.rs index be23797..8da76c3 100644 --- a/conformance/mock-provider/src/puzzle/catalog.rs +++ b/conformance/mock-provider/src/puzzle/catalog.rs @@ -1,14 +1,5 @@ -//! The `x-demo-lang` name vocabulary served by `lpp/lookup`. -//! -//! The catalog is fixed for a provider version: the section keywords -//! (`puzzle`, `ops`, `solution`), the four arithmetic operators and their -//! enum domain, and the `target`/`start` settings. List order is the -//! language's canonical order and is also the tie-break order for ranked -//! results. - use serde_json::{Value, json}; -/// A lookup scope: the full catalog or the children of one `within` selector. pub(crate) enum LookupScope { All, Callable(String), @@ -16,9 +7,6 @@ pub(crate) enum LookupScope { Settings(String), } -/// Ranked `entries` for an `lpp/lookup` request. Returns `None` when `scope` -/// names a `within` selector the language does not know; the caller turns -/// that into a `lookup.unknownWithin` refusal. pub(crate) fn lookup( query: &str, kind: Option<&str>, @@ -39,8 +27,6 @@ pub(crate) fn lookup( )) } -/// Keep `kind` matches, drop non-matches for `query`, order by match tier -/// then catalog order (the sort is stable), and cap at `limit` entries. fn rank(entries: Vec, query: &str, kind: Option<&str>, limit: usize) -> Vec { let mut scored: Vec<(u8, Value)> = entries .into_iter() @@ -52,9 +38,6 @@ fn rank(entries: Vec, query: &str, kind: Option<&str>, limit: usize) -> V scored.into_iter().map(|(_, e)| e).collect() } -/// Match tier: 0 exact, 1 prefix, 2 substring; `None` = no match. An empty -/// query matches everything at tier 0, so an unconstrained result keeps the -/// scope's canonical order. fn score(entry: &Value, query: &str) -> Option { if query.is_empty() { return Some(0); @@ -101,8 +84,6 @@ fn keyword(name: &str) -> Value { }) } -/// An arithmetic operator: `x ` inside an op declaration, so it -/// is callable on an `integer` receiver with one required `arg` parameter. fn operator(name: &str, symbol: &str) -> Value { json!({ "identity": format!("x-demo:operator/{name}"), @@ -118,7 +99,6 @@ fn operator(name: &str, symbol: &str) -> Value { }) } -/// The enum domain of arithmetic operators op bodies choose from. fn operator_domain() -> Value { json!({ "identity": "x-demo:enum/operator", @@ -132,7 +112,6 @@ fn operator_domain() -> Value { }) } -/// A required `name = ` assignment in the puzzle body. fn setting(name: &str) -> Value { json!({ "identity": format!("x-demo:setting/{name}"), @@ -143,8 +122,6 @@ fn setting(name: &str) -> Value { }) } -/// One entry per parameter of the callable at `identity`, in call order. -/// `None` when `identity` names no catalog callable. fn callable_params(identity: &str) -> Option> { let parameters = catalog().into_iter().find(|e| e["identity"] == identity)?["callable"]["parameters"] @@ -171,8 +148,6 @@ fn callable_params(identity: &str) -> Option> { ) } -/// The member spellings of the enum domain at `identity`, in domain order. -/// `None` when `identity` names no entry that defines an enum domain. fn enum_members(identity: &str) -> Option> { let members = catalog() .into_iter() @@ -195,8 +170,6 @@ fn enum_members(identity: &str) -> Option> { ) } -/// Entries directly below a settings path prefix. `x-demo-lang` settings are -/// a flat namespace, so only the empty prefix names a scope. fn settings_children(prefix: &str) -> Option> { if !prefix.is_empty() { return None; @@ -209,7 +182,6 @@ fn settings_children(prefix: &str) -> Option> { ) } -/// The display name of the catalog entry with `spelling`, else the spelling. fn display_name(spelling: &str) -> String { catalog() .into_iter() diff --git a/conformance/mock-provider/src/puzzle/edits.rs b/conformance/mock-provider/src/puzzle/edits.rs index 84766be..f2b6a9d 100644 --- a/conformance/mock-provider/src/puzzle/edits.rs +++ b/conformance/mock-provider/src/puzzle/edits.rs @@ -1,6 +1,3 @@ -//! Normative edit application rules for `lpp/validateEdits` (spec 16.3): -//! bounds checks, overlap detection, application, and re-parsing. - use super::parse::parse_document; use super::text::{Range, SourceText}; @@ -25,7 +22,6 @@ impl EditValidation { pub(crate) fn validate_edits(text: &str, edits: &[(Range, String)]) -> EditValidation { let src = SourceText::new(text); - // Bounds and ordering of each edit range. let mut resolved = Vec::with_capacity(edits.len()); for (i, (range, _)) in edits.iter().enumerate() { match (src.byte_of(range.start), src.byte_of(range.end)) { @@ -34,8 +30,6 @@ pub(crate) fn validate_edits(text: &str, edits: &[(Range, String)]) -> EditValid } } - // Overlap detection in document order; the failing index refers to the - // original request order. let mut order: Vec = (0..edits.len()).collect(); order.sort_by_key(|&i| resolved[i].0); for pair in order.windows(2) { @@ -44,7 +38,6 @@ pub(crate) fn validate_edits(text: &str, edits: &[(Range, String)]) -> EditValid } } - // Apply in document order. let mut result = String::with_capacity(text.len()); let mut cursor = 0; for &i in &order { diff --git a/conformance/mock-provider/src/puzzle/lex.rs b/conformance/mock-provider/src/puzzle/lex.rs index 573c7f0..90f4846 100644 --- a/conformance/mock-provider/src/puzzle/lex.rs +++ b/conformance/mock-provider/src/puzzle/lex.rs @@ -1,9 +1,3 @@ -//! Tokenizer for `x-demo-lang`. -//! -//! `name: x => x * 2` inside an `ops { }` block declares an op; a `solution = -//! [ a, b ]` list applies ops in sequence. Whitespace separates tokens; only -//! newlines are significant. - use super::text::SourceText; use super::{OpKind, Range}; @@ -18,21 +12,17 @@ pub(super) enum TokenKind { Colon, Comma, Eq, - Arrow, // => + Arrow, Star, Plus, Minus, Slash, Newline, - /// A character outside the token grammar. Carries the char so diagnostics - /// can name it; the parser never accepts it as an identifier. Unknown(char), - /// An integer literal outside the i64 range. BadNumber, } impl TokenKind { - /// The arithmetic operator an op body applies to `x`, if this token is one. pub(super) fn as_op(&self) -> Option { Some(match self { TokenKind::Star => OpKind::Mul, @@ -47,7 +37,6 @@ impl TokenKind { #[derive(Debug, Clone)] pub(super) struct Token { pub kind: TokenKind, - /// LPP range covering the token's byte offsets. pub range: Range, } @@ -113,8 +102,6 @@ pub(super) fn tokenize(src: &SourceText<'_>, text: &str) -> Vec { kind.clone() } None => { - // Consume the whole character so token ranges stay on - // UTF-8 boundaries; `SourceText` positions slice `text`. let ch = text[i..].chars().next().expect("in-bounds byte"); i += ch.len_utf8(); TokenKind::Unknown(ch) @@ -135,12 +122,9 @@ fn read_number(text: &str, i: usize) -> (Option, usize) { while end < bytes.len() && bytes[end].is_ascii_digit() { end += 1; } - // Parse the signed slice so -9223372036854775808 (i64::MIN) stays - // representable; out-of-range literals surface as `None`. (text[i..end].parse::().ok(), end) } -/// Human-readable token description for parse diagnostics. pub(super) fn describe(tok: &Token) -> String { match &tok.kind { TokenKind::Ident(name) => format!("'{name}'"), diff --git a/conformance/mock-provider/src/puzzle/parse.rs b/conformance/mock-provider/src/puzzle/parse.rs index 46010f9..fc749d9 100644 --- a/conformance/mock-provider/src/puzzle/parse.rs +++ b/conformance/mock-provider/src/puzzle/parse.rs @@ -1,8 +1,3 @@ -//! Recursive-descent parser for `x-demo-lang`. -//! -//! Recovery is line-oriented: an unexpected token reports one diagnostic and -//! skips to the next newline, so one malformed line does not cascade. - use std::collections::HashSet; use super::lex::{Token, TokenKind, describe, tokenize}; @@ -17,9 +12,6 @@ pub(crate) fn parse_document(text: &str) -> ParseOutput { diagnostics: Vec::new(), }; let puzzle = parse_top(&mut parser); - // Spec section 9.2: diagnostics are sorted by range.start. Diagnostics - // are pushed in analysis phases (body errors, then missing-section at the - // puzzle name, then semantic checks), so a stable sort is required. parser .diagnostics .sort_by_key(|d| (d.range.start.line, d.range.start.character)); @@ -62,7 +54,6 @@ impl Parser { } } - /// Report an unexpected end of input just past the last token. fn eof_error(&mut self, what: &str) { let at = self .tokens @@ -72,19 +63,16 @@ impl Parser { self.error_at(at, format!("unexpected end of input: expected {what}")); } - /// Report a syntax error at `range` without consuming input. fn error_at(&mut self, range: Range, message: impl Into) { self.diagnostics .push(diagnostic(range, "error", "x-demo/syntax", message.into())); } - /// Report a syntax error at `tok` and recover to the next line. fn error(&mut self, tok: &Token, message: impl Into) { self.error_at(tok.range, message); self.recover_line(); } - /// "unexpected {tok} in {context}" — idents print the bare word. fn unexpected(&mut self, tok: &Token, context: &str) { match &tok.kind { TokenKind::Ident(word) => { @@ -94,8 +82,6 @@ impl Parser { } } - /// Consume an identifier, or report an error and recover. Returns `None` - /// silently at end of input. fn expect_ident(&mut self) -> Option<(String, Token)> { let tok = self.peek().cloned()?; let TokenKind::Ident(name) = &tok.kind else { @@ -110,8 +96,6 @@ impl Parser { Some((name, tok)) } - /// Consume a number, or report an error and recover. Returns `None` - /// silently at end of input. fn expect_number(&mut self, what: &str) -> Option { let tok = self.peek().cloned()?; match tok.kind { @@ -130,8 +114,6 @@ impl Parser { } } - /// Consume `kind`, or report an error and recover. Returns `false` - /// silently at end of input. fn expect_punct(&mut self, kind: TokenKind, what: &str) -> bool { match self.peek().cloned() { Some(tok) if tok.kind == kind => { @@ -146,9 +128,6 @@ impl Parser { } } - /// Consume the required `x` parameter name in an op declaration, or report - /// an error and recover. `expect_ident` has already recovered to the next - /// line on a wrong token; at end of input there is nothing to recover. fn expect_x(&mut self, what: &str) -> bool { match self.expect_ident() { Some((name, _)) if name == "x" => true, @@ -354,7 +333,6 @@ fn parse_ops(parser: &mut Parser, puzzle: &mut Puzzle) -> bool { } } -/// `name: x => x ` fn parse_op(parser: &mut Parser, puzzle: &mut Puzzle, name_tok: &Token) { let TokenKind::Ident(name) = &name_tok.kind else { return; diff --git a/conformance/mock-provider/src/puzzle/spellings.rs b/conformance/mock-provider/src/puzzle/spellings.rs index 150f359..3638561 100644 --- a/conformance/mock-provider/src/puzzle/spellings.rs +++ b/conformance/mock-provider/src/puzzle/spellings.rs @@ -1,11 +1,3 @@ -//! The canonical-id to `x-demo-lang` spelling table served by -//! `lpp/resolveIds`. -//! -//! The ids are the Workshop ids the protocol fixtures use. The demo language -//! maps only a few of them, with spellings unlike OverPy's, so a client cannot -//! pass by assuming spelling equals id. Every other id is unmapped and is -//! omitted from the result. - use std::collections::BTreeMap; use serde_json::{Map, Value, json}; @@ -14,8 +6,6 @@ const CALLS: &[(&str, &str)] = &[("applyImpulse", "shove"), ("setStatusEffect", const ENUM_MEMBERS: &[(&str, &str, &str)] = &[("Status", "STUNNED", "stunned")]; -/// The result for the requested ids: only mapped ids, with `enums` grouped by -/// domain and a domain omitted when none of its members map. pub(crate) fn resolve_ids(calls: &[String], enums: &BTreeMap>) -> Value { let calls: Map = calls .iter() diff --git a/conformance/mock-provider/src/puzzle/symbols.rs b/conformance/mock-provider/src/puzzle/symbols.rs index 514ae0c..f8a58cf 100644 --- a/conformance/mock-provider/src/puzzle/symbols.rs +++ b/conformance/mock-provider/src/puzzle/symbols.rs @@ -1,16 +1,8 @@ -//! Symbol semantics for `x-demo-lang`: the queries behind `lpp/symbols`, -//! `lpp/definition`, `lpp/references`, and `lpp/rename`. -//! -//! Each query answers a language question with a typed result; the handlers -//! in `methods.rs` shape those results into wire envelopes and refusals. - use serde::Serialize; use super::Puzzle; use super::text::{Range, SourceText}; -/// `kind` values for `lpp/symbols` entries: the language's declaration -/// vocabulary. #[derive(Debug, Clone, Copy, Serialize)] #[serde(rename_all = "camelCase")] enum SymbolKind { @@ -18,7 +10,6 @@ enum SymbolKind { Op, } -/// A symbol declared in a document, as listed by `lpp/symbols`. #[derive(Debug, Serialize)] pub(crate) struct DocumentSymbol { name: String, @@ -26,8 +17,6 @@ pub(crate) struct DocumentSymbol { range: Range, } -/// The symbol a position resolves to: the puzzle's name, or an op via its -/// declaration or a solution reference. #[derive(Debug, Clone)] pub(crate) enum Symbol { Puzzle { name: String }, @@ -35,8 +24,6 @@ pub(crate) enum Symbol { } impl Puzzle { - /// The symbol under `byte`: the puzzle name, an op declaration, or a - /// solution reference, in that order. pub(crate) fn symbol_at(&self, src: &SourceText<'_>, byte: usize) -> Option { if src.contains_byte(self.name_range, byte) { return Some(Symbol::Puzzle { @@ -51,8 +38,6 @@ impl Puzzle { .map(|(_, name)| Symbol::Op { name: name.clone() }) } - /// The symbols this puzzle declares, in declaration order: the puzzle - /// name, then each op. pub(crate) fn document_symbols(&self) -> Vec { let mut symbols = vec![DocumentSymbol { name: self.name.clone(), @@ -67,8 +52,6 @@ impl Puzzle { symbols } - /// The declaration range `symbol` resolves to: its own range for the - /// puzzle name, the op's declaration range for an op. pub(crate) fn declaration_range(&self, symbol: &Symbol) -> Option { match symbol { Symbol::Puzzle { .. } => Some(self.name_range), @@ -76,8 +59,6 @@ impl Puzzle { } } - /// Ranges where `name` occurs as an op: its declaration (first, when - /// `include_declaration`), then solution references in document order. pub(crate) fn occurrences(&self, name: &str, include_declaration: bool) -> Vec { let mut ranges = Vec::new(); if include_declaration { @@ -94,11 +75,6 @@ impl Puzzle { ranges } - /// Ranges where `symbol` occurs, resolved by symbol kind: a puzzle name - /// is only ever its own declaration (puzzle names are never referenced - /// elsewhere); an op is its declaration then references in document - /// order. Looking an op up by name is wrong for a puzzle symbol that - /// happens to share the op's name. pub(crate) fn symbol_occurrences( &self, symbol: &Symbol, @@ -113,8 +89,6 @@ impl Puzzle { } } - /// Whether renaming `symbol` to `new_name` would collide with an - /// existing op. Only op renames can collide. pub(crate) fn rename_collides(&self, symbol: &Symbol, new_name: &str) -> bool { let Symbol::Op { name } = symbol else { return false; @@ -122,9 +96,6 @@ impl Puzzle { self.op(name).is_some() && name.as_str() != new_name && self.op(new_name).is_some() } - /// The ranges a rename of `symbol` edits in this puzzle. A puzzle-name - /// rename only edits the document holding the position; an op rename - /// edits the op's declaration and every solution reference to it. pub(crate) fn rename_ranges(&self, symbol: &Symbol, is_position_doc: bool) -> Vec { match symbol { Symbol::Puzzle { name } => { @@ -135,9 +106,6 @@ impl Puzzle { } } Symbol::Op { name } => { - // §15.2 requires edits sorted by range.start; occurrences - // lists the declaration first (a §14.2 carve-out for - // references), which misorders when `solution` precedes `ops`. let mut ranges = self.occurrences(name, true); ranges.sort_by_key(|r| (r.start.line, r.start.character)); ranges @@ -146,7 +114,6 @@ impl Puzzle { } } -/// `x-demo-lang` identifier: `[A-Za-z_][A-Za-z0-9_]*`. pub(crate) fn is_valid_identifier(name: &str) -> bool { let mut chars = name.chars(); let Some(first) = chars.next() else { diff --git a/conformance/mock-provider/src/puzzle/tests.rs b/conformance/mock-provider/src/puzzle/tests.rs index 009256a..e244031 100644 --- a/conformance/mock-provider/src/puzzle/tests.rs +++ b/conformance/mock-provider/src/puzzle/tests.rs @@ -54,9 +54,6 @@ fn reports_duplicate_and_unresolved() { #[test] fn malformed_op_declaration_does_not_consume_the_next_line() { - // Recovery is line-oriented: a bad op parameter reports one diagnostic - // and skips to the next newline; the following op still parses, so the - // solution reference to it resolves. const SRC: &str = "puzzle t {\n target = 2\n start = 0\n ops {\n bad: 5 => x + 1\n later: x => x * 2\n }\n solution = [ later ]\n}"; let out = parse_document(SRC); let codes: Vec<&str> = out.diagnostics.iter().map(|d| d.code.as_str()).collect(); @@ -67,7 +64,6 @@ fn malformed_op_declaration_does_not_consume_the_next_line() { #[test] fn missing_op_punctuation_reports_once_without_swallowing_the_next_op() { - // `b` is missing ':' — the error must not cascade into `c`'s line. const SRC: &str = "puzzle p {\n target = 1\n start = 0\n ops {\n b x => x + 1\n c: x => x + 2\n }\n solution = [ b, c ]\n}"; let out = parse_document(SRC); let codes: Vec<&str> = out.diagnostics.iter().map(|d| d.code.as_str()).collect(); @@ -79,9 +75,6 @@ fn missing_op_punctuation_reports_once_without_swallowing_the_next_op() { #[test] fn unknown_bytes_are_syntax_errors_not_identifiers() { - // '@', '$', and multi-byte characters are not part of the token grammar: - // they must surface as syntax errors, never parse as identifiers, and - // must not panic the diagnostic path. for src in [ "puzzle @ {\n target = 1\n start = 0\n ops {}\n solution = []\n}", "puzzle p {\n target = 1\n start = 0\n ops {\n $: x => x + 1\n }\n solution = [ $ ]\n}", @@ -97,9 +90,6 @@ fn unknown_bytes_are_syntax_errors_not_identifiers() { #[test] fn truncated_document_reports_an_error_instead_of_no_puzzle() { - // A document ending right after `puzzle` or `puzzle ` must produce - // an error diagnostic, not a silent `puzzle: None` — `lpp/compile` - // assumes no errors implies a puzzle. for src in ["puzzle", "puzzle p", "puzzle p "] { let out = parse_document(src); assert!(out.puzzle.is_none(), "unexpectedly parsed {src:?}"); @@ -112,8 +102,6 @@ fn truncated_document_reports_an_error_instead_of_no_puzzle() { #[test] fn out_of_range_integer_literal_is_an_error() { - // An integer literal outside the i64 range must be diagnosed, not - // silently coerced to 0 and compiled into the artifact. let out = parse_document( "puzzle p {\n target = 99999999999999999999\n start = 0\n ops {}\n solution = []\n}", ); @@ -122,7 +110,6 @@ fn out_of_range_integer_literal_is_an_error() { "expected an error for an out-of-range literal" ); - // i64::MIN is a valid literal. let out = parse_document( "puzzle p {\n target = -9223372036854775808\n start = 0\n ops {}\n solution = []\n}", ); @@ -134,8 +121,6 @@ fn out_of_range_integer_literal_is_an_error() { #[test] fn diagnostics_are_sorted_by_range_start() { - // A syntax error late in the body plus a missing-section diagnostic at - // the puzzle name must still come out ordered by range.start (§9.2). const SRC: &str = "puzzle p {\n start = 1\n ops {\n d: x => x + 1\n }\n solution = [ d ]\n ???\n}"; let out = parse_document(SRC); @@ -173,12 +158,10 @@ fn warns_when_target_not_reached() { #[test] fn utf16_positions_are_exact() { - // "puzzle αβ {": UTF-16 units: p=0..e=5, space=6, α=7, β=8, space=9, - // {=10. Bytes: α=7-8, β=9-10 (2 bytes each). let src = SourceText::new("puzzle αβ {\n target = 40\n}"); - assert_eq!(src.byte_of(pos(0, 7)), Some(7)); // α - assert_eq!(src.byte_of(pos(0, 8)), Some(9)); // β - assert_eq!(src.byte_of(pos(0, 9)), Some(11)); // space after β + assert_eq!(src.byte_of(pos(0, 7)), Some(7)); + assert_eq!(src.byte_of(pos(0, 8)), Some(9)); + assert_eq!(src.byte_of(pos(0, 9)), Some(11)); assert_eq!(src.position_of(9), pos(0, 8)); assert_eq!(src.position_of(11), pos(0, 9)); assert!(src.byte_of(pos(0, 11)).is_some()); @@ -187,8 +170,6 @@ fn utf16_positions_are_exact() { #[test] fn trailing_newline_phantom_line_is_a_valid_position() { - // A document ending in '\n' has a phantom empty last line; a position - // there is valid (spec §6.4) and must not invert the line's byte range. let src = SourceText::new("a\n"); assert_eq!(src.line_text(0), "a"); assert_eq!(src.line_text(1), ""); @@ -198,7 +179,6 @@ fn trailing_newline_phantom_line_is_a_valid_position() { assert_eq!(src.line_text(1), ""); assert_eq!(src.byte_of(pos(1, 0)), Some(3)); - // An empty line between two newlines stays empty, not inverted. let src = SourceText::new("a\n\nb"); assert_eq!(src.line_text(1), ""); assert_eq!(src.byte_of(pos(1, 0)), Some(2)); @@ -206,13 +186,10 @@ fn trailing_newline_phantom_line_is_a_valid_position() { #[test] fn crlf_terminator_does_not_count_toward_character() { - // "\r\n" is the line terminator, not content: positions at its bytes map - // to end-of-line, never past the visible line length. let src = SourceText::new("aaaaaaaaaa\r\nb"); - assert_eq!(src.position_of(10), pos(0, 10)); // '\r' byte -> end of line 0 - assert_eq!(src.position_of(11), pos(0, 10)); // '\n' byte -> end of line 0 + assert_eq!(src.position_of(10), pos(0, 10)); + assert_eq!(src.position_of(11), pos(0, 10)); assert_eq!(src.position_of(12), pos(1, 0)); - // A lone '\r' mid-line is content. let src = SourceText::new("a\rb\nc"); assert_eq!(src.position_of(3), pos(0, 3)); } @@ -229,9 +206,6 @@ fn compile_reconstruct_round_trip() { #[test] fn reconstruct_rejects_names_the_parser_would_not_accept() { - // Names are written into generated source verbatim; anything outside - // the identifier grammar would emit text our own parser rejects (or - // inject extra syntax). for bad in ["has space", "a\nb", "", "x-y", "9lives"] { for content in [ serde_json::json!({"name": bad, "target": 1, "start": 0, "ops": [], "solution": [], "value": 0}), @@ -263,8 +237,6 @@ fn validate_edits_detects_overlap_in_original_order() { #[test] fn validate_edits_detects_overlap_when_unsorted() { - // The failing index refers to the request's edit array order, not the - // document-sorted order. let edits = vec![edit(7, 18, 7, 23, "b"), edit(7, 15, 7, 21, "a")]; match validate_edits(CLEAN, &edits) { EditValidation::Invalid { @@ -293,8 +265,6 @@ fn validate_edits_accepts_rename_edits() { #[test] fn lookup_ranks_tiers_then_catalog_order() { - // "a": prefix match on "add" and "arithmetic operator" (tier 1); substring - // on "subtract", "target", "start" (tier 2); ties keep catalog order. let entries = lookup("a", None, &LookupScope::All, 20).expect("all scope"); let ids: Vec<&str> = entries .iter() @@ -314,7 +284,6 @@ fn lookup_ranks_tiers_then_catalog_order() { #[test] fn lookup_within_scopes_children() { - // Enum scope lists member spellings in domain order. let members = lookup( "", None, @@ -328,7 +297,6 @@ fn lookup_within_scopes_children() { .collect(); assert_eq!(spellings, ["+", "-", "*", "/"]); - // Callable scope lists the callable's parameters in call order. let params = lookup( "", None, @@ -343,8 +311,6 @@ fn lookup_within_scopes_children() { #[test] fn lookup_unknown_within_is_none() { - // A non-callable identity, an unknown identity, and a leaf settings path - // each name no scope. for scope in [ LookupScope::Callable("x-demo:keyword/ops".into()), LookupScope::Enum("x-demo:setting/target".into()), @@ -354,14 +320,12 @@ fn lookup_unknown_within_is_none() { } } -/// Parses `text` into a puzzle and returns it with its source text. fn puzzle_of(text: &str) -> (SourceText<'_>, Puzzle) { let src = SourceText::new(text); let puzzle = parse_document(text).puzzle.expect("parsed"); (src, puzzle) } -/// The symbol under `line:character`, resolved the way the wire handlers do. fn symbol_at(src: &SourceText<'_>, puzzle: &Puzzle, line: u32, character: u32) -> Option { puzzle.symbol_at(src, src.byte_of(pos(line, character)).expect("in document")) } @@ -388,7 +352,6 @@ fn symbol_at_resolves_puzzle_op_declaration_and_reference() { Symbol::Puzzle { name } => assert_eq!(name, "clean"), Symbol::Op { .. } => panic!("expected puzzle symbol"), } - // An op declaration and each solution reference resolve to the same op. for (line, character) in [(4, 6), (7, 16), (7, 25)] { match symbol_at(&src, &puzzle, line, character).expect("op") { Symbol::Op { name } => assert_eq!(name, "double"), @@ -400,7 +363,6 @@ fn symbol_at_resolves_puzzle_op_declaration_and_reference() { Symbol::Puzzle { .. } => panic!("expected op symbol"), } - // Keywords, settings, and punctuation are not symbols. for (line, character) in [(0, 3), (1, 4), (7, 3), (7, 11)] { assert!(symbol_at(&src, &puzzle, line, character).is_none()); } @@ -411,9 +373,7 @@ fn declaration_range_points_at_the_declaration() { let (src, puzzle) = puzzle_of(CLEAN); let at = |line, character| symbol_at(&src, &puzzle, line, character).expect("symbol"); - // The puzzle name is its own declaration. assert_eq!(puzzle.declaration_range(&at(0, 9)), Some(rng(0, 7, 0, 12))); - // An op declaration and its solution references share one declaration. for (line, character) in [(4, 6), (7, 16), (7, 25)] { assert_eq!( puzzle.declaration_range(&at(line, character)), @@ -433,17 +393,12 @@ fn occurrences_list_declaration_first_then_references() { puzzle.occurrences("double", false), vec![rng(7, 15, 7, 21), rng(7, 23, 7, 29)] ); - // A declared-but-unreferenced op lists only its declaration. assert_eq!(puzzle.occurrences("plus1", true), vec![rng(5, 4, 5, 9)]); - // Unknown names have no occurrences. assert!(puzzle.occurrences("triple", true).is_empty()); } #[test] fn rename_ranges_are_sorted_when_solution_precedes_ops() { - // `solution` may precede `ops` in a document; rename edits must still be - // sorted by range.start (§15.2) even though occurrences lists the - // declaration first. const SRC: &str = "puzzle p {\n target = 2\n start = 1\n solution = [ d ]\n ops {\n d: x => x + 1\n }\n}"; let (src, puzzle) = puzzle_of(SRC); let symbol = symbol_at(&src, &puzzle, 5, 4).expect("op d"); @@ -455,8 +410,6 @@ fn rename_ranges_are_sorted_when_solution_precedes_ops() { #[test] fn puzzle_symbol_occurrences_do_not_include_a_same_named_op() { - // Position on the puzzle name `a` while an op `a` exists: the puzzle - // symbol's occurrences are its own declaration only — never the op's. const SRC: &str = "puzzle a {\n target = 2\n start = 1\n ops {\n a: x => x + 1\n }\n solution = [ a ]\n}"; let (src, puzzle) = puzzle_of(SRC); let symbol = symbol_at(&src, &puzzle, 0, 7).expect("puzzle name"); @@ -472,7 +425,6 @@ fn puzzle_symbol_occurrences_do_not_include_a_same_named_op() { fn rename_ranges_for_op_cover_declaration_and_references() { let (src, puzzle) = puzzle_of(CLEAN); let symbol = symbol_at(&src, &puzzle, 7, 16).expect("symbol"); - // Op renames edit every document that mentions the op, positioned or not. for is_position_doc in [true, false] { assert_eq!( puzzle.rename_ranges(&symbol, is_position_doc), @@ -496,10 +448,8 @@ fn rename_collides_only_with_another_op() { let puzzle_name = symbol_at(&src, &puzzle, 0, 9).expect("puzzle name"); assert!(puzzle.rename_collides(&op, "plus1")); - // Renaming to the current name is not a collision. assert!(!puzzle.rename_collides(&op, "double")); assert!(!puzzle.rename_collides(&op, "triple")); - // Puzzle-name renames never collide with ops. assert!(!puzzle.rename_collides(&puzzle_name, "plus1")); } diff --git a/conformance/mock-provider/src/puzzle/text.rs b/conformance/mock-provider/src/puzzle/text.rs index 8cf0881..d0802e6 100644 --- a/conformance/mock-provider/src/puzzle/text.rs +++ b/conformance/mock-provider/src/puzzle/text.rs @@ -1,9 +1,5 @@ -//! Source text with LPP position mapping: 0-based lines and UTF-16 code units -//! within a line. - use serde::{Deserialize, Serialize}; -/// LPP position: 0-based line, UTF-16 code units within the line. #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] pub(crate) struct Position { pub line: u32, @@ -11,15 +7,12 @@ pub(crate) struct Position { } impl Position { - /// 0:0 — the origin, also used as the zero-length range position for - /// diagnostics before any token exists. pub(crate) const ORIGIN: Self = Position { line: 0, character: 0, }; } -/// Half-open range (start inclusive, end exclusive). #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] pub(crate) struct Range { pub start: Position, @@ -27,7 +20,6 @@ pub(crate) struct Range { } impl Range { - /// Zero-length range at `pos`. pub(crate) fn point(pos: Position) -> Self { Range { start: pos, @@ -52,7 +44,6 @@ impl<'a> SourceText<'a> { Self { text, line_starts } } - /// Byte range of a line, excluding a trailing `"\n"` or `"\r\n"`. fn line_byte_range(&self, line: usize) -> (usize, usize) { let start = self.line_starts[line]; let mut end = self @@ -60,9 +51,6 @@ impl<'a> SourceText<'a> { .get(line + 1) .copied() .unwrap_or(self.text.len()); - // The terminator counts only when it sits inside this line's span — - // a phantom final line after a trailing newline has an empty span - // and must not strip the previous line's terminator. if end > start && self.text.as_bytes()[end - 1] == b'\n' { end -= 1; if end > start && self.text.as_bytes()[end - 1] == b'\r' { @@ -77,9 +65,6 @@ impl<'a> SourceText<'a> { &self.text[start..end] } - /// Byte offset of a position, or `None` when the position is outside the - /// document or inside a supplementary-plane character (a position at the - /// end of a line is valid). pub(crate) fn byte_of(&self, pos: Position) -> Option { let start = *self.line_starts.get(pos.line as usize)?; let line_text = self.line_text(pos.line as usize); @@ -98,15 +83,12 @@ impl<'a> SourceText<'a> { (units == pos.character).then(|| start + byte) } - /// LPP position of a byte offset. pub(crate) fn position_of(&self, byte: usize) -> Position { let byte = byte.min(self.text.len()); let line = match self.line_starts.binary_search(&byte) { Ok(i) => i, Err(i) => i - 1, }; - // Positions count content only: a byte inside the line terminator - // ("\r\n" or "\n") maps to the end of the line. let (start, end) = self.line_byte_range(line); let units = self.text[start..byte.min(end)] .chars() @@ -118,7 +100,6 @@ impl<'a> SourceText<'a> { } } - /// LPP range covering a token's byte offsets. pub(crate) fn range_of(&self, start: usize, end: usize) -> Range { Range { start: self.position_of(start), @@ -126,7 +107,6 @@ impl<'a> SourceText<'a> { } } - /// True when `pos` is a valid position inside `range`. pub(crate) fn contains_byte(&self, range: Range, byte: usize) -> bool { match (self.byte_of(range.start), self.byte_of(range.end)) { (Some(start), Some(end)) => start <= byte && byte < end, diff --git a/conformance/mock-provider/src/rpc.rs b/conformance/mock-provider/src/rpc.rs index 09ecfab..5ad5ab9 100644 --- a/conformance/mock-provider/src/rpc.rs +++ b/conformance/mock-provider/src/rpc.rs @@ -1,27 +1,18 @@ -//! JSON-RPC 2.0 response envelopes and the handler error channel. -//! -//! LPP errors travel in `error.data.lpp = { kind, details }` with code -//! `-32000`; standard JSON-RPC errors carry only `code` and `message`. - use serde::de::DeserializeOwned; use serde_json::{Value, json}; -/// Errors a method handler can raise: either an LPP-typed error -/// (`kind`/`details` under `error.data.lpp`) or a standard JSON-RPC error. pub(crate) enum HandlerError { Lpp(&'static str, Value, String), Std(i64, &'static str), } impl HandlerError { - /// An LPP `refusal` error carrying `refusalCode` in its details. pub(crate) fn refusal(code: &'static str, details: Value, message: impl Into) -> Self { let mut details = details; details["refusalCode"] = json!(code); Self::Lpp("refusal", details, message.into()) } - /// An LPP-typed error with `kind`/`details`. pub(crate) fn lpp(kind: &'static str, details: Value, message: impl Into) -> Self { Self::Lpp(kind, details, message.into()) } diff --git a/conformance/mock-provider/src/server.rs b/conformance/mock-provider/src/server.rs index 95c5ea6..9a115af 100644 --- a/conformance/mock-provider/src/server.rs +++ b/conformance/mock-provider/src/server.rs @@ -1,8 +1,3 @@ -//! Session state and JSON-RPC method dispatch. -//! -//! `METHODS` (defined next to the handlers in `methods.rs`) is the single -//! table routing a method name to its capability gate and handler. - use std::collections::HashSet; use serde_json::{Value, json}; @@ -11,7 +6,6 @@ use crate::methods::METHODS; use crate::rpc::{lpp_error, ok, parse_params, std_error}; use crate::wire::InitParams; -/// Protocol versions accepted by `--protocol-version`. pub(crate) const PROTOCOL_VERSIONS: [&str; 7] = ["1.0", "1.1", "1.2", "1.3", "1.4", "1.5", "1.6"]; pub(crate) const DEFAULT_PROTOCOL_VERSION: &str = PROTOCOL_VERSIONS[0]; pub(crate) const LANGUAGE_ID: &str = "x-demo-lang"; @@ -19,8 +13,6 @@ pub(crate) const LANGUAGE_ID: &str = "x-demo-lang"; const SERVER_NAME: &str = "lpp-mock-provider"; const LANGUAGE_EXTENSIONS: [&str; 1] = ["xdl"]; -/// Capability names as they appear on the wire, each with the minimum -/// protocol minor version that advertises it. const CAPABILITIES: &[(&str, u32)] = &[ ("check", 0), ("compile", 0), @@ -44,7 +36,6 @@ fn invalid_request() -> Value { std_error(Value::Null, -32600, "Invalid Request") } -/// `invalidRequest`/`notInitialized` for any method called pre-`initialize`. fn not_initialized(id: &Value) -> Value { lpp_error( id.clone(), @@ -54,7 +45,6 @@ fn not_initialized(id: &Value) -> Value { ) } -/// Mark `name` disabled for `--without`. Returns false for unknown names. pub(crate) fn disable_capability(disabled: &mut HashSet<&'static str>, name: &str) -> bool { match CAPABILITIES.iter().find(|(n, _)| *n == name) { Some(&(n, _)) => { @@ -84,13 +74,10 @@ impl Server { } } - /// True when the negotiated protocol version is 1.`minor` or later. pub(crate) fn since(&self, minor: u32) -> bool { self.version_minor.is_some_and(|v| v >= minor) } - /// True when `capability` exists at the negotiated protocol version and - /// is not disabled via `--without`. Unknown names are never available. pub(crate) fn capability(&self, name: &str) -> bool { let Some(&(_, min)) = CAPABILITIES.iter().find(|(n, _)| *n == name) else { return false; @@ -108,14 +95,12 @@ impl Server { .into() } - /// Handle one newline-delimited message and return the response envelope. pub(crate) fn handle_message(&mut self, line: &str) -> Value { let parsed: Value = match serde_json::from_str(line) { Ok(value) => value, Err(_) => return std_error(Value::Null, -32700, "Parse error"), }; let Some(object) = parsed.as_object() else { - // Batches are arrays; both fail as Invalid Request. return invalid_request(); }; if object.get("jsonrpc").and_then(Value::as_str) != Some("2.0") { @@ -126,11 +111,7 @@ impl Server { }; let id = match object.get("id") { Some(id @ Value::String(_)) => id.clone(), - // Request ids are integers or strings (spec section 4); a - // fractional number is not an integer. Some(id @ Value::Number(_)) if id.is_i64() || id.is_u64() => id.clone(), - // No notifications in LPP v1: a message without an id (or with a - // null id) is a protocol violation. None | Some(Value::Null) => { return lpp_error( Value::Null, @@ -139,11 +120,8 @@ impl Server { "invalid request: LPP v1 defines no notifications", ); } - // Any other id shape (bool, float, array, object) is not a valid - // JSON-RPC request. Some(_) => return invalid_request(), }; - // `params` is a REQUIRED object in every LPP v1 request. let params = match object.get("params") { Some(params @ Value::Object(_)) => params.clone(), _ => return std_error(id.clone(), -32602, "Invalid params"), diff --git a/conformance/mock-provider/src/wire.rs b/conformance/mock-provider/src/wire.rs index bb09ef3..ebfb2a8 100644 --- a/conformance/mock-provider/src/wire.rs +++ b/conformance/mock-provider/src/wire.rs @@ -1,6 +1,3 @@ -//! Wire parameter shapes for the LPP v1 methods the mock provider serves. -//! Field names follow the spec's camelCase JSON keys via serde. - use std::collections::{BTreeMap, HashMap}; use serde::{Deserialize, Serialize}; @@ -40,7 +37,6 @@ pub(crate) struct ProjectEntry { pub kind: Option, } -/// `documents`-vs-`entry` params shared by `lpp/check` and `lpp/compile`. #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct DocsParams { @@ -48,7 +44,6 @@ pub(crate) struct DocsParams { pub entry: Option, #[allow(dead_code)] pub project_root: Option, - /// Validated as a string; the demo language has no locale-dependent text. #[allow(dead_code)] pub locale: Option, } @@ -92,8 +87,6 @@ pub(crate) struct RenameParams { pub project_root: Option, } -/// `within` selector for `lpp/lookup`: names the scope whose children the -/// result lists. `kind` is validated against the closed set in the handler. #[derive(Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub(crate) struct LookupWithin { @@ -107,17 +100,12 @@ pub(crate) struct LookupParams { pub language_id: String, pub query: Option, pub kind: Option, - /// Echoed in `lookup.unknownWithin` refusal details. pub within: Option, - /// Accepted for forward compatibility; the demo language only serves its - /// default locale. #[allow(dead_code)] pub locale: Option, pub limit: Option, } -/// `lpp/resolveIds` params; element types are enforced by deserialization -/// and the total bound by the handler. #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct ResolveIdsParams { diff --git a/conformance/mock-provider/tests/stdio.rs b/conformance/mock-provider/tests/stdio.rs index 73bf348..f54d7ae 100644 --- a/conformance/mock-provider/tests/stdio.rs +++ b/conformance/mock-provider/tests/stdio.rs @@ -1,6 +1,3 @@ -//! Stdio-framing regressions: malformed bytes on the wire must not crash the -//! provider, and EOF must end the session cleanly. - use std::io::Write; use std::process::{Command, Stdio}; @@ -8,13 +5,10 @@ fn provider() -> Command { Command::new(env!("CARGO_BIN_EXE_lpp-mock-provider")) } -/// Feed `input` to a fresh provider, then close stdin and collect the -/// response messages. fn run_session(input: &[u8]) -> Vec { run_session_args(&[], input) } -/// `run_session` with extra provider command-line arguments. fn run_session_args(args: &[&str], input: &[u8]) -> Vec { let mut child = provider() .args(args) @@ -50,7 +44,6 @@ fn non_utf8_line_gets_parse_error_and_session_continues() { { let mut stdin = child.stdin.take().expect("stdin"); stdin.write_all(b"\xff\n").expect("write invalid bytes"); - // A well-formed request after the bad line: the session must survive. stdin .write_all( br#"{"jsonrpc":"2.0","id":1,"method":"lpp/initialize","params":{"protocolVersion":"1.0"}}"# @@ -75,12 +68,9 @@ fn non_utf8_line_gets_parse_error_and_session_continues() { #[test] fn request_shape_strictness() { - // Spec section 4: `id` is an integer or string; `params` is REQUIRED. let requests = [ - // Fractional and boolean ids are not integer/string ids. r#"{"jsonrpc":"2.0","id":1.5,"method":"lpp/initialize","params":{"protocolVersion":"1.0"}}"#, r#"{"jsonrpc":"2.0","id":true,"method":"lpp/initialize","params":{"protocolVersion":"1.0"}}"#, - // Missing `params`, and a non-object `params`. r#"{"jsonrpc":"2.0","id":3,"method":"lpp/initialize"}"#, r#"{"jsonrpc":"2.0","id":4,"method":"lpp/initialize","params":[]}"#, ]; @@ -93,10 +83,6 @@ fn request_shape_strictness() { #[test] fn rename_refuses_when_a_document_has_errors() { - // A document whose parse produced error diagnostics cannot be renamed - // atomically — spec section 15.2 requires covering every reference in - // every received document — so the provider must refuse rather than - // emit partial edits. let clean = "puzzle c {\n target = 2\n start = 1\n ops {\n a: x => x + 1\n }\n solution = [ a ]\n}"; let dup = "puzzle p {\n target = 1\n start = 0\n ops {\n a: x => x + 1\n a: x => x + 2\n }\n solution = [ a ]\n}"; let request = format!( @@ -112,8 +98,6 @@ fn rename_refuses_when_a_document_has_errors() { ); } -/// Scratch directory under the repo's ignored `target/` for project-loader -/// tests. Removed at test start and end so reruns stay clean. fn project_dir(name: &str) -> std::path::PathBuf { let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) .join("../../target/project-tests") @@ -133,7 +117,6 @@ fn check_directory(root: &std::path::Path) -> serde_json::Value { r#"{{"jsonrpc":"2.0","id":2,"method":"lpp/check","params":{{"entry":{{"uri":"file://{}","languageId":"x-demo-lang","version":1,"kind":"directory"}}}}}}"#, root.display() ); - // Directory entries need LPP 1.2+, so pin the provider's version. let responses = run_session_args( &["--protocol-version", "1.6"], format!("{init}\n{request}\n").as_bytes(), @@ -143,8 +126,6 @@ fn check_directory(root: &std::path::Path) -> serde_json::Value { #[test] fn project_loader_ignores_xdl_directories() { - // A directory named `*.xdl` is not a source file; loading must skip it - // rather than fail the whole request trying to read it. let root = project_dir("xdl-dir"); std::fs::write(root.join("entry.xdl"), VALID).unwrap(); std::fs::create_dir(root.join("junk.xdl")).unwrap(); @@ -159,8 +140,6 @@ fn project_loader_ignores_xdl_directories() { #[cfg(unix)] #[test] fn project_load_failure_names_the_broken_source_uri() { - // A dangling symlink canonicalizes to nothing: the failure's - // `details.uri` must name the file that could not be loaded. let root = project_dir("dangling"); std::fs::write(root.join("entry.xdl"), VALID).unwrap(); std::os::unix::fs::symlink("missing-target.xdl", root.join("broken.xdl")).unwrap(); diff --git a/conformance/runner/src/main.rs b/conformance/runner/src/main.rs index 031252b..3a01594 100644 --- a/conformance/runner/src/main.rs +++ b/conformance/runner/src/main.rs @@ -1,10 +1,3 @@ -//! `lpp-conformance-runner`: replays LPP v1 fixtures against a provider -//! binary. `scenario` owns the fixture schema and structural validation, -//! `project_fixture` owns the materialized `projectFiles` tree and -//! `${PROJECT_URI}` substitution, `matcher` owns the expected-response -//! comparison including provider-owned leaves, and `session` owns the spawned -//! provider exchange. Fixtures must stay deterministic and self-contained. - mod matcher; mod project_fixture; mod scenario; @@ -141,7 +134,6 @@ fn parse_args() -> Args { fn arg_value(iter: &mut impl Iterator, flag: &str) -> String { match iter.next() { - // A missing value must not silently swallow the next flag. Some(value) if !value.starts_with("--") => value, _ => usage_error(&format!("{flag} requires a value")), } diff --git a/conformance/runner/src/matcher.rs b/conformance/runner/src/matcher.rs index 387afb4..82e3a4c 100644 --- a/conformance/runner/src/matcher.rs +++ b/conformance/runner/src/matcher.rs @@ -1,25 +1,5 @@ -//! Expected-vs-actual response comparison. -//! -//! The fixture DSL marks leaves the spec declares provider-supplied as -//! `{ "$provider": "" }`. A marked leaf is asserted by its contract -//! shape instead of a literal value, so providers other than the reference -//! mock can satisfy protocol-scope scenarios. Unmarked leaves keep verbatim -//! JSON equality. -//! -//! The kinds mirror spec-declared provider-supplied positions: -//! -//! * `boolean` — a `capabilities` value; the key set in the fixture stays the -//! normative required-field assertion. -//! * `languageList` — the `languages` array: non-empty, each entry exactly -//! `{ "id": , "extensions": [] }`. -//! * `nonEmptyString` — provider prose or identity: `serverInfo` fields, -//! error `message`, `refusalCode`, provider-defined `reason`. -//! * `protocolVersions` — `supportedProtocolVersions`: a non-empty array of -//! `MAJOR.MINOR` strings. - use serde_json::Value; -/// The provider-leaf kinds the fixture DSL accepts. const KINDS: &[&str] = &[ "boolean", "languageList", @@ -27,11 +7,8 @@ const KINDS: &[&str] = &[ "protocolVersions", ]; -/// The `capabilities` object inside an `lpp/initialize` result. const CAPABILITIES_PATH: &str = "$.result.capabilities"; -/// Capability ids in introduction order. Per §7.3 the set is closed for each -/// LPP minor version: ids are added only through a new protocol version. const CAPABILITY_IDS: &[&str] = &[ "check", "compile", @@ -47,7 +24,6 @@ const CAPABILITY_IDS: &[&str] = &[ "resolveIds", ]; -/// The capability ids a negotiated protocol version declares. fn declared_capability_ids(root: &Value) -> Option<&'static [&'static str]> { let count = match root.pointer("/result/protocolVersion")?.as_str()? { "1.0" => 8, @@ -60,16 +36,10 @@ fn declared_capability_ids(root: &Value) -> Option<&'static [&'static str]> { Some(&CAPABILITY_IDS[..count]) } -/// The `data.lpp.kind` of an expected error response. fn expected_error_kind(root: &Value) -> Option<&str> { root.pointer("/error/data/lpp/kind")?.as_str() } -/// Whether `{ "$provider": kind }` may appear at `path` inside an expected -/// response, per the §20.1 whitelist of spec-declared provider-supplied -/// positions. `root` is the whole `expectResponse`, so positions whose -/// legality depends on sibling fields (`data.lpp.kind`, the negotiated -/// `protocolVersion`) can be checked. fn marker_position_legal(path: &str, kind: &str, root: &Value) -> bool { match path { "$.result.serverInfo.name" | "$.result.serverInfo.version" => kind == "nonEmptyString", @@ -82,8 +52,6 @@ fn marker_position_legal(path: &str, kind: &str, root: &Value) -> bool { "$.error.data.lpp.details.refusalCode" => { kind == "nonEmptyString" && expected_error_kind(root) == Some("refusal") } - // `reason` is provider-defined only for the kinds whose details the - // spec does not enumerate; `invalidRequest.reason` is a closed set. "$.error.data.lpp.details.reason" => { kind == "nonEmptyString" && matches!( @@ -109,7 +77,6 @@ fn marker_position_legal(path: &str, kind: &str, root: &Value) -> bool { } } -/// `Some(kind)` when `expected` is a well-formed provider leaf marker. fn marker_kind(expected: &Value) -> Option<&str> { expected .as_object() @@ -119,11 +86,6 @@ fn marker_kind(expected: &Value) -> Option<&str> { .filter(|kind| KINDS.contains(kind)) } -/// Validates every `$provider` marker inside a fixture value. Markers are -/// legal only in expected responses, so requests are checked with -/// `allow_markers: false`. A marker's legality also depends on where it sits: -/// `value` doubles as the response root so marker paths can be checked -/// against the §20.1 whitelist. pub(crate) fn validate_leaves(value: &Value, allow_markers: bool) -> Result<(), String> { validate_at(value, value, "$", allow_markers) } @@ -162,10 +124,6 @@ fn validate_at(root: &Value, value: &Value, path: &str, allow_markers: bool) -> } } -/// A `capabilities` object that carries markers must mark the full declared -/// capability set for the negotiated version: every value a `boolean` marker, -/// every key a declared id, and no declared id missing. Verbatim capability -/// maps (adapter-specific `providerArgs` scenarios) are unaffected. fn validate_capability_object(root: &Value, value: &Value) -> Result<(), String> { let Some(object) = value.as_object() else { return Ok(()); @@ -196,8 +154,6 @@ fn validate_capability_object(root: &Value, value: &Value) -> Result<(), String> Ok(()) } -/// Recursive expected-vs-actual comparison honoring provider-leaf markers. -/// On mismatch returns the failing path and what differed. pub(crate) fn matches(expected: &Value, actual: &Value) -> Result<(), String> { match_at(expected, actual, "$") } @@ -208,11 +164,6 @@ fn match_at(expected: &Value, actual: &Value, path: &str) -> Result<(), String> } match (expected, actual) { (Value::Object(expected), Value::Object(actual)) => { - // §7.3: a provider may advertise any capability subset — an absent - // id means not offered, equivalent to `false`. The exemption only - // applies to a fully marker-checked capabilities object; a - // verbatim map (a `providerArgs` scenario asserting an explicit - // negotiated map, false keys included) keeps exact key matching. let subset_mode = path == CAPABILITIES_PATH && expected.values().all(|value| marker_kind(value).is_some()); for (key, expected_value) in expected { @@ -266,9 +217,6 @@ fn check_kind(kind: &str, actual: &Value) -> Result<(), String> { } } -/// `languages`: a non-empty array of `{ id, extensions }` entries, where `id` -/// is a non-empty string and `extensions` a (possibly empty) array of file -/// extensions written without a leading dot and in lowercase (§7.2). fn is_language_list(actual: &Value) -> bool { let Some(entries) = actual.as_array() else { return false; @@ -295,7 +243,6 @@ fn is_extension(ext: &str) -> bool { !ext.is_empty() && !ext.starts_with('.') && !ext.chars().any(char::is_uppercase) } -/// `supportedProtocolVersions`: a non-empty array of `MAJOR.MINOR` strings. fn is_version_list(actual: &Value) -> bool { let Some(versions) = actual.as_array() else { return false; @@ -356,7 +303,6 @@ mod tests { ("languageList", json!([])), ("languageList", json!([{ "id": "opy" }])), ("languageList", json!([{ "id": "", "extensions": [] }])), - // §7.2: extensions are lowercase, without a leading dot. ( "languageList", json!([{ "id": "opy", "extensions": [".OPY"] }]), @@ -404,7 +350,6 @@ mod tests { #[test] fn capabilities_may_advertise_a_subset() { let expected = json!({"result": {"capabilities": {"check": {"$provider": "boolean"}, "lookup": {"$provider": "boolean"}}}}); - // Per §7.3 absent means not advertised; advertised values are checked. assert!(matches(&expected, &json!({"result": {"capabilities": {}}})).is_ok()); assert!( matches( @@ -432,8 +377,6 @@ mod tests { #[test] fn verbatim_capability_map_keeps_exact_keys() { - // A `providerArgs` scenario asserting `reconstruct: false` must not - // pass when the provider omits `reconstruct` entirely. let expected = json!({"result": {"capabilities": {"check": true, "reconstruct": false}}}); assert!( matches( @@ -480,17 +423,14 @@ mod tests { #[test] fn validate_leaves_enforces_marker_form_and_position() { assert!(validate_leaves(&init_result(), true).is_ok()); - // Marker form. assert!(validate_leaves(&json!({"result": {"protocolVersion": "1.5", "capabilities": {"check": {"$provider": "unknown"}}}}), true).is_err()); assert!(validate_leaves(&json!({"$provider": "boolean", "x": 1}), true).is_err()); assert!(validate_leaves(&json!({"$provider": "boolean"}), false).is_err()); - // Plain objects without the key are never markers. assert!(validate_leaves(&json!({"provider": "boolean"}), false).is_ok()); } #[test] fn markers_reject_contract_owned_positions() { - // Marker on the root or a contract-owned leaf. for expected in [ json!({"$provider": "nonEmptyString"}), json!({"result": {"protocolVersion": {"$provider": "nonEmptyString"}}}), @@ -516,20 +456,16 @@ mod tests { #[test] fn details_markers_follow_the_error_kind() { - // `reason` is provider-defined under `invalidDocument` ... let expected = json!({"error": {"data": {"lpp": { "kind": "invalidDocument", "details": {"reason": {"$provider": "nonEmptyString"}} }}}}); assert!(validate_leaves(&expected, true).is_ok()); - // ... but a closed set under `invalidRequest`. let expected = json!({"error": {"data": {"lpp": { "kind": "invalidRequest", "details": {"reason": {"$provider": "nonEmptyString"}} }}}}); assert!(validate_leaves(&expected, true).is_err()); - // `refusalCode` only under `refusal`; `supportedProtocolVersions` only - // under `protocolVersionMismatch`. let expected = json!({"error": {"data": {"lpp": { "kind": "invalidRequest", "details": {"refusalCode": {"$provider": "nonEmptyString"}} @@ -545,24 +481,20 @@ mod tests { #[test] fn marked_capabilities_cover_the_declared_set() { let mut expected = init_result(); - // A declared id left unmarked weakens the closed-set check. expected["result"]["capabilities"] .as_object_mut() .unwrap() .remove("lookup"); assert!(validate_leaves(&expected, true).is_err()); - // An undeclared id is not a provider-supplied position. let mut expected = init_result(); expected["result"]["capabilities"] .as_object_mut() .unwrap() .insert("custom".into(), json!({"$provider": "boolean"})); assert!(validate_leaves(&expected, true).is_err()); - // Markers cannot mix with verbatim capability values. let mut expected = init_result(); expected["result"]["capabilities"]["check"] = json!(true); assert!(validate_leaves(&expected, true).is_err()); - // The declared set follows the negotiated version. let mut expected = init_result(); expected["result"]["protocolVersion"] = json!("1.0"); for key in ["projectLoading", "sourceIdentity", "lookup"] { diff --git a/conformance/runner/src/project_fixture.rs b/conformance/runner/src/project_fixture.rs index 9bba047..8f51c35 100644 --- a/conformance/runner/src/project_fixture.rs +++ b/conformance/runner/src/project_fixture.rs @@ -1,8 +1,3 @@ -//! Session-runtime project fixtures: materializing a scenario's -//! `projectFiles` into an isolated directory and substituting its URI for -//! `${PROJECT_URI}` in request and expected-response JSON. Consumed only by -//! `session`; the schema-side rules live in `scenario`. - use std::path::PathBuf; use lpp_conformance_common::path_to_file_uri; @@ -10,7 +5,6 @@ use serde_json::Value; use crate::scenario::{Scenario, validate_project_path}; -/// A materialized `projectFiles` tree; removed when dropped. pub(crate) struct ProjectFixture { directory: PathBuf, pub uri: String, @@ -22,8 +16,6 @@ impl Drop for ProjectFixture { } } -/// Write `projectFiles` into an isolated directory under `target/`. -/// `Ok(None)` when the scenario has no project files. pub(crate) fn materialize_project( scenario: &Scenario, scenario_index: usize, @@ -38,7 +30,6 @@ pub(crate) fn materialize_project( std::fs::create_dir(&directory) .map_err(|error| format!("cannot create project fixture directory: {error}"))?; - // From here on the fixture owns the directory; any error cleans it up. let mut fixture = ProjectFixture { directory, uri: String::new(), @@ -60,7 +51,6 @@ pub(crate) fn materialize_project( Ok(Some(fixture)) } -/// Recursively replace `${PROJECT_URI}` in fixture JSON. pub(crate) fn substitute_project_uri(value: &Value, project: Option<&ProjectFixture>) -> Value { match value { Value::String(text) => { diff --git a/conformance/runner/src/scenario.rs b/conformance/runner/src/scenario.rs index 9af605a..f792ac9 100644 --- a/conformance/runner/src/scenario.rs +++ b/conformance/runner/src/scenario.rs @@ -1,6 +1,3 @@ -//! Scenario files: the fixture schema (`*.json` under `fixtures/v1/`) and the -//! structural validation the runner performs before replaying anything. - use std::collections::HashMap; use std::path::Path; @@ -16,8 +13,6 @@ pub(crate) struct Scenario { #[allow(dead_code)] pub description: Option, pub scope: String, - /// Wraps `steps` in the standard initialize/shutdown handshake for the - /// given protocol version, spliced from `sessions/.json`. #[serde(default)] pub session: Option, #[serde(default)] @@ -32,13 +27,10 @@ pub(crate) struct Scenario { #[derive(Deserialize)] #[serde(try_from = "StepWire")] pub(crate) struct Step { - /// The one request form the step sends. pub request: StepRequest, pub expect_response: Value, } -/// A step's outbound payload: either a JSON-RPC `request` object or a -/// `rawLine` sent verbatim — never both, never neither. pub(crate) enum StepRequest { Request(Value), RawLine(String), @@ -70,9 +62,6 @@ impl TryFrom for Step { } } -/// The literal handshake a `session` fixture inherits: the exact -/// initialize/shutdown steps live in `sessions/.json` next to the -/// fixtures, keeping expected responses as data rather than runner logic. #[derive(Deserialize)] struct SessionTemplate { initialize: Step, @@ -84,8 +73,6 @@ pub(crate) fn read_scenario(path: &Path) -> Result { let mut scenario: Scenario = serde_json::from_str(&text).map_err(|e| format!("fixture is not valid JSON: {e}"))?; if let Some(version) = &scenario.session { - // The handshake comes from the template; stepping it twice is a - // session-behavior test and must stay explicit. for (i, step) in scenario.steps.iter().enumerate() { let method = match &step.request { StepRequest::Request(request) => request.get("method").and_then(Value::as_str), @@ -110,9 +97,6 @@ pub(crate) fn read_scenario(path: &Path) -> Result { Ok(scenario) } -/// Wire-envelope defaults so fixtures state only what varies: `jsonrpc` is -/// `"2.0"` on both sides, and an absent `expectResponse.id` expects the -/// request's id (null for `rawLine` steps). fn apply_envelope_defaults(steps: &mut [Step]) { for step in steps { if let StepRequest::Request(request) = &mut step.request { @@ -177,10 +161,6 @@ fn validate_scenario(scenario: &Scenario) -> Result<(), String> { } for (i, step) in scenario.steps.iter().enumerate() { let response_id = step.expect_response.get("id"); - // Normal requests: the response id echoes the request id, unless the - // scenario deliberately sends an id-less message (notification, - // batch), in which case the response id is null. Raw lines carry no - // id, so the expected response id must be null. let id_ok = match &step.request { StepRequest::Request(request) => match (request.get("id"), response_id) { (Some(request_id), Some(response_id)) => request_id == response_id, @@ -258,8 +238,6 @@ fn contains_project_uri(value: &Value) -> bool { mod tests { use super::*; - /// The schema rejects a step carrying both request forms — all shipped - /// fixtures are valid, so only these unit tests exercise the rule. #[test] fn step_rejects_request_and_raw_line_together() { let step = json!({ diff --git a/conformance/runner/src/session.rs b/conformance/runner/src/session.rs index 67fa92d..0e9e836 100644 --- a/conformance/runner/src/session.rs +++ b/conformance/runner/src/session.rs @@ -1,7 +1,3 @@ -//! Replaying one scenario against a spawned provider process: request lines -//! go to stdin, response lines are compared verbatim after JSON parsing, and -//! the process must exit with the expected status once stdin closes. - use std::io::{BufRead, BufReader, Write}; use std::process::{Child, Command, Stdio}; use std::sync::mpsc; @@ -66,8 +62,6 @@ pub(crate) fn run_scenario( )); } Err(mpsc::RecvTimeoutError::Disconnected) => { - // stdout is closed, so no response can ever arrive; reap the - // provider immediately instead of waiting out the timeout. let status = kill(&mut child); return Err(format!( "step {i}: provider closed stdout without responding (exit status {status:?})" @@ -111,9 +105,6 @@ pub(crate) fn run_scenario( "expected exit code {expected_exit}, got {actual_exit}" )); } - // Output the provider emitted past the last step must surface. Give the - // reader a short grace period for already-buffered lines; a descendant - // can keep the pipe open, so the wait stays bounded. const DRAIN_GRACE: Duration = Duration::from_millis(500); let _ = done_rx.recv_timeout(DRAIN_GRACE); let extra: Vec = response_rx.try_iter().collect(); @@ -136,8 +127,6 @@ fn step_request_line(step: &Step, project: Option<&ProjectFixture>) -> String { serde_json::to_string(&substitute_project_uri(request, project)) .expect("request serializes") } - // `${PROJECT_URI}` inside a raw line substitutes literally, matching - // the request-object path. StepRequest::RawLine(raw) => raw.replace( "${PROJECT_URI}", project.map_or("", |project| project.uri.as_str()), @@ -145,8 +134,6 @@ fn step_request_line(step: &Step, project: Option<&ProjectFixture>) -> String { } } -/// Poll for exit until `timeout`, then kill and reap. Returns the exit -/// status, or a default when the child cannot be reaped. fn wait_exit(child: &mut Child, timeout: Duration) -> std::process::ExitStatus { let deadline = Instant::now() + timeout; loop { @@ -162,7 +149,6 @@ fn wait_exit(child: &mut Child, timeout: Duration) -> std::process::ExitStatus { } } -/// Kill the provider and report its final exit status. fn kill(child: &mut Child) -> std::process::ExitStatus { let _ = child.kill(); child.wait().unwrap_or_default() @@ -175,8 +161,6 @@ mod tests { use std::fs; use std::path::{Path, PathBuf}; - /// Scratch dir for fake-provider scripts under the repo's ignored - /// `target/`; removed at test start and end so reruns stay clean. fn scratch(name: &str) -> PathBuf { let dir = Path::new(env!("CARGO_MANIFEST_DIR")) .join("../../target/runner-tests") @@ -210,8 +194,6 @@ mod tests { } } - /// Run the script as `/bin/sh