From 2f6be4423b60b906c741e6fbd388ad2009c2e1dd Mon Sep 17 00:00:00 2001 From: Teakowa Date: Wed, 21 Oct 2026 11:31:09 +0800 Subject: [PATCH] chore: remove redundant comments Delete narration, banners, and restating doc comments. Keep doc comments on public API, comments about upstream or client behavior we cannot change, SAFETY comments, and source attribution. No code logic changes; rustfmt reflow only. Refs #98 --- .github/CODEOWNERS | 6 -- .github/dependabot.yml | 4 -- Cargo.toml | 8 --- conformance/common/src/lib.rs | 6 -- conformance/mock-provider/src/main.rs | 9 --- conformance/mock-provider/src/methods.rs | 28 -------- conformance/mock-provider/src/project.rs | 19 ------ conformance/mock-provider/src/puzzle.rs | 6 -- .../mock-provider/src/puzzle/artifact.rs | 9 --- .../mock-provider/src/puzzle/catalog.rs | 28 -------- conformance/mock-provider/src/puzzle/edits.rs | 7 -- conformance/mock-provider/src/puzzle/lex.rs | 18 +---- conformance/mock-provider/src/puzzle/parse.rs | 22 ------ .../mock-provider/src/puzzle/spellings.rs | 10 --- .../mock-provider/src/puzzle/symbols.rs | 33 --------- conformance/mock-provider/src/puzzle/tests.rs | 60 ++-------------- conformance/mock-provider/src/puzzle/text.rs | 20 ------ conformance/mock-provider/src/rpc.rs | 9 --- conformance/mock-provider/src/server.rs | 22 ------ conformance/mock-provider/src/wire.rs | 12 ---- conformance/mock-provider/tests/stdio.rs | 21 ------ conformance/runner/src/main.rs | 8 --- conformance/runner/src/matcher.rs | 68 ------------------- conformance/runner/src/project_fixture.rs | 10 --- conformance/runner/src/scenario.rs | 22 ------ conformance/runner/src/session.rs | 24 ------- conformance/runner/tests/cli.rs | 7 -- 27 files changed, 6 insertions(+), 490 deletions(-) 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