From a259f4f316d3539c78b2b236f8fa3a8a042bc35a Mon Sep 17 00:00:00 2001 From: harehare Date: Thu, 15 Oct 2026 12:45:00 +0900 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=93=9D=20docs:=20deny=20missing=5Fdoc?= =?UTF-8?q?s=20and=20document=20all=20public=20items?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enable `missing_docs = "deny"` through workspace lints for every crate under crates/ and add the doc comments it requires. Clap ValueEnum variants in mq-run keep `#[allow(missing_docs)]` so `--help` output is unchanged. --- Cargo.toml | 3 + crates/mq-bench/Cargo.toml | 3 + crates/mq-bench/src/lib.rs | 2 + crates/mq-bench/src/main.rs | 2 + crates/mq-bench/src/runner.rs | 7 + crates/mq-check/Cargo.toml | 3 + crates/mq-check/benches/inputs.rs | 3 + crates/mq-check/src/kind_set.rs | 8 + crates/mq-check/src/lib.rs | 49 ++++ crates/mq-check/src/main.rs | 2 + crates/mq-crawler/Cargo.toml | 3 + crates/mq-crawler/src/crawler.rs | 24 ++ crates/mq-crawler/src/http_client.rs | 14 +- crates/mq-crawler/src/lib.rs | 3 + crates/mq-crawler/src/main.rs | 2 + crates/mq-crawler/src/robots.rs | 4 +- crates/mq-dap/Cargo.toml | 3 + crates/mq-dap/src/adapter.rs | 1 + crates/mq-dap/src/error.rs | 8 + crates/mq-dap/src/handler.rs | 2 + crates/mq-dap/src/lib.rs | 7 + crates/mq-dap/src/log.rs | 2 + crates/mq-dap/src/protocol.rs | 20 +- crates/mq-dap/src/server.rs | 1 + crates/mq-ffi/Cargo.toml | 3 + crates/mq-ffi/build.rs | 2 + crates/mq-ffi/mq.h | 15 + crates/mq-ffi/src/lib.rs | 5 + crates/mq-formatter/Cargo.toml | 3 + crates/mq-formatter/benches/benchmark.rs | 2 + crates/mq-formatter/src/formatter.rs | 11 + crates/mq-formatter/src/main.rs | 2 + .../mq-formatter/tests/integration_tests.rs | 2 + crates/mq-help/Cargo.toml | 3 + crates/mq-help/src/catalog.rs | 16 ++ crates/mq-help/src/docs.rs | 7 + crates/mq-help/src/lib.rs | 2 + crates/mq-help/src/reference.rs | 9 + crates/mq-hir/Cargo.toml | 3 + crates/mq-hir/src/error.rs | 29 +- crates/mq-hir/src/find.rs | 5 + crates/mq-hir/src/hir.rs | 8 + crates/mq-hir/src/hir/query.rs | 5 + crates/mq-hir/src/reference.rs | 1 + crates/mq-hir/src/resolve.rs | 1 + crates/mq-hir/src/scope.rs | 21 +- crates/mq-hir/src/source.rs | 12 +- crates/mq-hir/src/symbol.rs | 68 ++++- crates/mq-lang/Cargo.toml | 3 + crates/mq-lang/examples/extract_js_code.rs | 2 + crates/mq-lang/examples/extract_list.rs | 2 + crates/mq-lang/examples/extract_list1.rs | 2 + crates/mq-lang/examples/extract_table.rs | 2 + crates/mq-lang/examples/fibonacci.rs | 2 + crates/mq-lang/examples/markdown_toc.rs | 2 + crates/mq-lang/examples/snake_to_camel.rs | 2 + crates/mq-lang/src/ast.rs | 1 + crates/mq-lang/src/ast/node.rs | 63 ++++- crates/mq-lang/src/ast/parser.rs | 3 + crates/mq-lang/src/cst/node.rs | 202 ++++++++++++++ crates/mq-lang/src/cst/parser.rs | 10 + crates/mq-lang/src/diagnostic.rs | 3 + crates/mq-lang/src/engine.rs | 2 + crates/mq-lang/src/engine/debugger.rs | 4 + crates/mq-lang/src/error.rs | 1 + crates/mq-lang/src/io.rs | 7 + crates/mq-lang/src/io/error.rs | 2 + crates/mq-lang/src/io/mem.rs | 8 + crates/mq-lang/src/io/sandboxed.rs | 1 + crates/mq-lang/src/lexer.rs | 3 + crates/mq-lang/src/lexer/token.rs | 97 ++++++- crates/mq-lang/src/lib.rs | 8 + crates/mq-lang/src/module.rs | 24 ++ crates/mq-lang/src/module/error.rs | 5 + .../mq-lang/src/module/resolver/lockfile.rs | 5 +- crates/mq-lang/src/mqc.rs | 36 ++- crates/mq-lang/src/range.rs | 4 + crates/mq-lang/src/runtime/debugger.rs | 10 + crates/mq-lang/src/runtime/host.rs | 1 + crates/mq-lang/src/runtime/host/adapter.rs | 1 + crates/mq-lang/src/runtime/runtime_value.rs | 2 + crates/mq-lang/src/tarn/disasm.rs | 8 + crates/mq-lang/tests/integration_tests.rs | 2 + crates/mq-lint/Cargo.toml | 3 + crates/mq-lint/src/config.rs | 2 + crates/mq-lint/src/fix.rs | 5 + crates/mq-lint/src/lib.rs | 9 + crates/mq-lint/src/main.rs | 2 + crates/mq-lint/src/message.rs | 106 +++++++ crates/mq-lint/src/rules.rs | 6 + crates/mq-lint/src/rules/complexity.rs | 6 + .../rules/complexity/complex_interpolation.rs | 1 + .../src/rules/complexity/deeply_nested.rs | 1 + .../src/rules/complexity/function_too_long.rs | 1 + .../rules/complexity/too_many_match_arms.rs | 1 + .../src/rules/complexity/too_many_params.rs | 1 + crates/mq-lint/src/rules/correctness.rs | 13 + .../correctness/always_true_condition.rs | 1 + .../correctness/deprecated_function_call.rs | 1 + .../src/rules/correctness/duplicate_import.rs | 1 + .../rules/correctness/duplicate_match_arm.rs | 1 + .../src/rules/correctness/infinite_loop.rs | 1 + .../rules/correctness/missing_else_in_expr.rs | 1 + .../src/rules/correctness/shadow_variable.rs | 1 + .../src/rules/correctness/unreachable_code.rs | 1 + .../src/rules/correctness/unused_function.rs | 1 + .../src/rules/correctness/unused_import.rs | 1 + .../src/rules/correctness/unused_parameter.rs | 1 + .../src/rules/correctness/unused_variable.rs | 1 + crates/mq-lint/src/rules/module.rs | 3 + .../module/ambiguous_qualified_access.rs | 1 + .../src/rules/module/missing_module_doc.rs | 1 + crates/mq-lint/src/rules/security.rs | 2 + .../security/dangerous_capability_call.rs | 1 + crates/mq-lint/src/rules/selector.rs | 4 + .../rules/selector/inefficient_selector.rs | 1 + .../src/rules/selector/missing_depth_guard.rs | 1 + .../rules/selector/selector_always_empty.rs | 1 + crates/mq-lint/src/rules/style.rs | 12 + .../src/rules/style/boolean_comparison.rs | 1 + .../src/rules/style/constant_string_concat.rs | 1 + .../src/rules/style/naming_convention.rs | 1 + .../src/rules/style/negated_condition.rs | 1 + .../src/rules/style/prefer_coalesce.rs | 1 + .../src/rules/style/prefer_let_over_var.rs | 1 + .../src/rules/style/prefer_pipe_style.rs | 1 + .../rules/style/prefer_specific_heading.rs | 1 + .../rules/style/redundant_boolean_literal.rs | 1 + .../mq-lint/src/rules/style/redundant_try.rs | 1 + .../rules/style/unnecessary_interpolation.rs | 1 + crates/mq-lsp/Cargo.toml | 3 + crates/mq-lsp/src/error.rs | 4 + crates/mq-lsp/src/lib.rs | 16 ++ crates/mq-lsp/src/main.rs | 18 ++ crates/mq-lsp/src/semantic_tokens.rs | 2 + crates/mq-lsp/src/server.rs | 2 + crates/mq-macros/Cargo.toml | 3 + crates/mq-macros/src/lib.rs | 2 + crates/mq-markdown/Cargo.toml | 3 + crates/mq-markdown/benches/benchmark.rs | 2 + crates/mq-markdown/examples/html_convert.rs | 2 + crates/mq-markdown/src/html_to_markdown.rs | 1 + .../src/html_to_markdown/options.rs | 4 + crates/mq-markdown/src/markdown.rs | 12 + crates/mq-markdown/src/node.rs | 262 ++++++++++++++++++ crates/mq-markdown/src/node/attr_value.rs | 6 + crates/mq-markdown/src/node/render.rs | 1 + crates/mq-markdown/src/node_kind.rs | 47 ++++ crates/mq-markdown/src/parser/error.rs | 47 +++- .../tests/html_to_markdown_tests.rs | 2 + crates/mq-repl/Cargo.toml | 3 + crates/mq-repl/src/repl.rs | 3 + crates/mq-run/Cargo.toml | 3 + crates/mq-run/src/bin/dbg.rs | 2 + crates/mq-run/src/cli.rs | 13 +- crates/mq-run/src/debugger.rs | 24 ++ crates/mq-run/src/lib.rs | 2 + crates/mq-run/src/main.rs | 2 + crates/mq-run/tests/integration_tests.rs | 4 + crates/mq-run/tests/mqc_tests.rs | 2 + crates/mq-test/Cargo.toml | 3 + crates/mq-test/src/coverage.rs | 1 + crates/mq-test/src/lib.rs | 2 + crates/mq-test/src/main.rs | 2 + crates/mq-wasm/Cargo.toml | 3 + crates/mq-wasm/src/lib.rs | 1 + crates/mq-wasm/src/script.rs | 41 +++ crates/mq-web-api/Cargo.toml | 3 + crates/mq-web-api/src/api.rs | 63 +++++ crates/mq-web-api/src/auth.rs | 13 + crates/mq-web-api/src/cleanup.rs | 5 + crates/mq-web-api/src/config.rs | 18 ++ crates/mq-web-api/src/handlers.rs | 26 ++ crates/mq-web-api/src/lib.rs | 9 + crates/mq-web-api/src/main.rs | 1 + crates/mq-web-api/src/middleware.rs | 2 + crates/mq-web-api/src/middleware/auth.rs | 6 + .../mq-web-api/src/middleware/rate_limit.rs | 1 + crates/mq-web-api/src/problem.rs | 5 + crates/mq-web-api/src/query_cache.rs | 9 + crates/mq-web-api/src/rate_limiter.rs | 25 +- crates/mq-web-api/src/routes.rs | 1 + crates/mq-web-api/src/server.rs | 1 + 183 files changed, 1862 insertions(+), 24 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2bc1eb9e2..7d6eec8d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,9 @@ members = [ ] resolver = "3" +[workspace.lints.rust] +missing_docs = "deny" + [workspace.package] authors = ["Takahiro Sato "] edition = "2024" diff --git a/crates/mq-bench/Cargo.toml b/crates/mq-bench/Cargo.toml index bd3b3e4d1..e1d75954a 100644 --- a/crates/mq-bench/Cargo.toml +++ b/crates/mq-bench/Cargo.toml @@ -11,6 +11,9 @@ readme = "README.md" repository.workspace = true version.workspace = true +[lints] +workspace = true + [dependencies] clap = {workspace = true, features = ["derive"]} glob = {workspace = true} diff --git a/crates/mq-bench/src/lib.rs b/crates/mq-bench/src/lib.rs index e36ce949b..bcb77e0e3 100644 --- a/crates/mq-bench/src/lib.rs +++ b/crates/mq-bench/src/lib.rs @@ -1,3 +1,5 @@ +//! Runner for `bench_` functions in `.mq` files. + mod runner; pub use runner::{BenchRecord, BenchRunner, OutputFormat}; diff --git a/crates/mq-bench/src/main.rs b/crates/mq-bench/src/main.rs index 17089bb20..a531a6463 100644 --- a/crates/mq-bench/src/main.rs +++ b/crates/mq-bench/src/main.rs @@ -1,3 +1,5 @@ +//! Command-line runner for `bench_` functions in `.mq` files. + mod runner; use clap::Parser; diff --git a/crates/mq-bench/src/runner.rs b/crates/mq-bench/src/runner.rs index 437ee33d2..600e0e947 100644 --- a/crates/mq-bench/src/runner.rs +++ b/crates/mq-bench/src/runner.rs @@ -22,12 +22,19 @@ struct DiscoveredBench { /// Timing summary for a single bench function. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BenchRecord { + /// Source file of the benchmark. pub file: String, + /// Benchmark name. pub name: String, + /// Number of timed iterations. pub iterations: usize, + /// Mean time per iteration in nanoseconds. pub mean_ns: u128, + /// Median time per iteration in nanoseconds. pub median_ns: u128, + /// Fastest iteration in nanoseconds. pub min_ns: u128, + /// Slowest iteration in nanoseconds. pub max_ns: u128, } diff --git a/crates/mq-check/Cargo.toml b/crates/mq-check/Cargo.toml index c7adf4cd2..86e124909 100644 --- a/crates/mq-check/Cargo.toml +++ b/crates/mq-check/Cargo.toml @@ -24,6 +24,9 @@ pkg-fmt = "bin" pkg-url = "{ repo }/releases/download/v{ version }/{ bin }-{ target }{ binary-ext }" disabled-strategies = ["quick-install"] +[lints] +workspace = true + [dependencies] clap = {workspace = true, features = ["derive"], optional = true} colored = {workspace = true, optional = true} diff --git a/crates/mq-check/benches/inputs.rs b/crates/mq-check/benches/inputs.rs index 66afe45ea..ecfe6810b 100644 --- a/crates/mq-check/benches/inputs.rs +++ b/crates/mq-check/benches/inputs.rs @@ -1,9 +1,12 @@ +//! Type checker benchmarks. + //! Benchmark inputs shared by `benchmark.rs` and `examples/phase_profile.rs`. /// Named inputs: a tiny program, real-world code of builtin.mq size, and synthetic code with /// `N` functions sharing parameter names (the quadratic case in name resolution and calls). pub const INPUTS: &[&str] = &["small", "builtin", "synthetic_200", "synthetic_1000", "synthetic_4000"]; +/// Returns the source text for the named benchmark input. pub fn input(name: &str) -> String { match name { "small" => "def f(x): x + 1; | f(2)".to_string(), diff --git a/crates/mq-check/src/kind_set.rs b/crates/mq-check/src/kind_set.rs index 7acf855ce..9a67abf40 100644 --- a/crates/mq-check/src/kind_set.rs +++ b/crates/mq-check/src/kind_set.rs @@ -37,26 +37,32 @@ impl KindSet { .fold(Self::EMPTY, |set, kind| set.union(Self::of(kind))) } + /// Whether `kind` is in the set. pub const fn contains(self, kind: NodeKind) -> bool { self.0 & Self::bit(kind) != 0 } + /// Whether the environment has no entries. pub const fn is_empty(self) -> bool { self.0 == 0 } + /// Whether every kind is in the set. pub const fn is_all(self) -> bool { self.0 == Self::ALL.0 } + /// Number of entries. pub const fn len(self) -> usize { self.0.count_ones() as usize } + /// The kinds in either set. pub const fn union(self, other: Self) -> Self { Self(self.0 | other.0) } + /// The kinds in both sets. pub const fn intersect(self, other: Self) -> Self { Self(self.0 & other.0) } @@ -66,10 +72,12 @@ impl KindSet { Self(self.0 & !other.0) } + /// Whether every kind of `self` is in `other`. pub const fn is_subset_of(self, other: Self) -> bool { self.0 & !other.0 == 0 } + /// Whether the sets share any kind. pub const fn intersects(self, other: Self) -> bool { self.0 & other.0 != 0 } diff --git a/crates/mq-check/src/lib.rs b/crates/mq-check/src/lib.rs index a389276b8..066b6191b 100644 --- a/crates/mq-check/src/lib.rs +++ b/crates/mq-check/src/lib.rs @@ -52,22 +52,27 @@ impl TypeEnv { Self(FxHashMap::with_capacity_and_hasher(capacity, Default::default())) } + /// Associates `scheme` with `symbol_id`, replacing any previous entry. pub fn insert(&mut self, symbol_id: SymbolId, scheme: TypeScheme) { self.0.insert(symbol_id, scheme); } + /// The scheme for `symbol_id`, if any. pub fn get(&self, symbol_id: &SymbolId) -> Option<&TypeScheme> { self.0.get(symbol_id) } + /// All symbol-to-scheme entries. pub fn get_all(&self) -> &FxHashMap { &self.0 } + /// Number of entries. pub fn len(&self) -> usize { self.0.len() } + /// Whether the environment has no entries. pub fn is_empty(&self) -> bool { self.0.is_empty() } @@ -89,76 +94,111 @@ pub enum TypeError { #[error("Type mismatch: expected {expected}, found {found}")] #[diagnostic(code(typechecker::type_mismatch))] #[allow(dead_code)] + /// Expected and found types differ. Mismatch { + /// The expected type. expected: String, + /// The type that was found. found: String, + /// Source range of the error, if known. location: Option, #[help] + /// Extra hint shown as help. context: Option, }, #[error("Cannot unify types: {left} and {right}")] #[diagnostic(code(typechecker::unification_error))] #[allow(dead_code)] + /// Two types cannot be unified. UnificationError { + /// Left-hand type. left: String, + /// Right-hand type. right: String, + /// Source range of the error, if known. location: Option, #[help] + /// Extra hint shown as help. context: Option, }, #[error("Occurs check failed: type variable {var} occurs in {ty}")] #[diagnostic(code(typechecker::occurs_check))] #[allow(dead_code)] + /// A type variable occurs inside the type it is bound to. OccursCheck { + /// The type variable. var: String, + /// The type containing the variable. ty: String, + /// Source range of the error, if known. location: Option, }, #[error("Undefined symbol: {name}")] #[diagnostic(code(typechecker::undefined_symbol))] #[allow(dead_code)] + /// A symbol is used but never defined. UndefinedSymbol { + /// Name of the undefined symbol. name: String, + /// Source range of the error, if known. location: Option, }, #[error("Wrong number of arguments: expected {expected}, found {found}")] #[diagnostic(code(typechecker::wrong_arity))] #[allow(dead_code)] + /// A call passes the wrong number of arguments. WrongArity { + /// The expected type. expected: usize, + /// The type that was found. found: usize, + /// Source range of the error, if known. location: Option, #[help] + /// Extra hint shown as help. context: Option, }, #[error("Undefined field `{field}` in record type {record_ty}")] #[diagnostic(code(typechecker::undefined_field))] #[allow(dead_code)] + /// A record type has no such field. UndefinedField { + /// Name of the missing field. field: String, + /// The record type that lacks the field. record_ty: String, + /// Source range of the error, if known. location: Option, }, #[error("Attribute `{attr}` does not exist on {node_ty}")] #[diagnostic(code(typechecker::undefined_attribute))] #[allow(dead_code)] + /// A node type has no such attribute. UndefinedAttribute { + /// Name of the missing attribute. attr: String, + /// The node type that lacks the attribute. node_ty: String, + /// Source range of the error, if known. location: Option, }, #[error("Heterogeneous array: elements have mixed types [{types}]")] #[diagnostic(code(typechecker::heterogeneous_array))] #[allow(dead_code)] + /// Array elements have mixed types. HeterogeneousArray { + /// Comma-separated element types. types: String, + /// Source range of the error, if known. location: Option, }, #[error("Type variable not found: {0}")] #[diagnostic(code(typechecker::type_var_not_found))] + /// A type variable id has no binding. TypeVarNotFound(String), #[error("Internal error: {0}")] #[diagnostic(code(typechecker::internal_error))] + /// An unexpected internal failure. Internal(String), /// Emitted when an operation receives a nullable argument and may silently /// propagate `none` instead of producing the expected concrete type. @@ -166,10 +206,14 @@ pub enum TypeError { #[diagnostic(code(typechecker::nullable_propagation))] #[allow(dead_code)] NullablePropagation { + /// The operation being checked. op: String, + /// The argument that may be `none`. nullable_arg: String, + /// Source range of the error, if known. location: Option, #[help] + /// Extra hint shown as help. context: Option, }, /// Emitted when a branch is statically determined to be unreachable based on @@ -178,7 +222,9 @@ pub enum TypeError { #[diagnostic(code(typechecker::unreachable_code))] #[allow(dead_code)] UnreachableCode { + /// Why the branch is unreachable. reason: String, + /// Source range of the error, if known. location: Option, }, /// Emitted when a match expression does not cover all possible values of the matched type. @@ -186,9 +232,12 @@ pub enum TypeError { #[diagnostic(code(typechecker::non_exhaustive_patterns))] #[allow(dead_code)] NonExhaustiveMatch { + /// Description of the uncovered case. missing: String, + /// Source range of the error, if known. location: Option, #[help] + /// Extra hint shown as help. context: Option, }, } diff --git a/crates/mq-check/src/main.rs b/crates/mq-check/src/main.rs index 3ba94632b..6285fc0cb 100644 --- a/crates/mq-check/src/main.rs +++ b/crates/mq-check/src/main.rs @@ -1,3 +1,5 @@ +//! Command-line type checker for mq programs. + mod format; use std::io::{self, BufWriter, Read, Write}; diff --git a/crates/mq-crawler/Cargo.toml b/crates/mq-crawler/Cargo.toml index dadfc0319..29cab8280 100644 --- a/crates/mq-crawler/Cargo.toml +++ b/crates/mq-crawler/Cargo.toml @@ -11,6 +11,9 @@ readme = "README.md" repository.workspace = true version.workspace = true +[lints] +workspace = true + [dependencies] base64 = {workspace = true} chromiumoxide = {workspace = true} diff --git a/crates/mq-crawler/src/crawler.rs b/crates/mq-crawler/src/crawler.rs index 5e1e18746..e227fb009 100644 --- a/crates/mq-crawler/src/crawler.rs +++ b/crates/mq-crawler/src/crawler.rs @@ -18,34 +18,53 @@ use tokio::time::sleep; use url::Url; #[derive(Clone, Debug, Default)] +/// Format of the crawl statistics written to stderr. pub enum OutputFormat { #[default] + /// Human-readable text. Text, + /// JSON. Json, } #[derive(Debug, Default)] +/// Counters and timing collected during a crawl. pub struct CrawlResult { + /// When the crawl started. pub start_time: Option, + /// When the crawl finished. pub end_time: Option, + /// Pages fetched and processed successfully. pub pages_crawled: usize, + /// Pages skipped because robots.txt disallows them. pub pages_skipped_robots: usize, + /// Pages that failed to fetch or process. pub pages_failed: usize, + /// Links found across all pages. pub links_discovered: usize, + /// Total pages visited. pub total_pages_visited: usize, } #[derive(Debug, Default, Serialize, Deserialize)] +/// Serializable summary of a [`CrawlResult`]. pub struct CrawlResultStats { + /// Pages fetched and processed successfully. pub pages_crawled: usize, + /// Pages skipped because robots.txt disallows them. pub pages_skipped_robots: usize, + /// Pages that failed to fetch or process. pub pages_failed: usize, + /// Links found across all pages. pub links_discovered: usize, + /// Total pages visited. pub total_pages_visited: usize, + /// Crawl duration in seconds. pub duration_secs: Option, } impl CrawlResult { + /// Elapsed time, if the crawl has started and finished. pub fn duration(&self) -> Option { if let (Some(start), Some(end)) = (self.start_time, self.end_time) { Some(end.duration_since(start)) @@ -54,6 +73,7 @@ impl CrawlResult { } } + /// Converts to the serializable statistics form. pub fn to_stats(&self) -> CrawlResultStats { CrawlResultStats { pages_crawled: self.pages_crawled, @@ -65,6 +85,7 @@ impl CrawlResult { } } + /// Writes the statistics to stderr in `format`. pub fn write_stats_to_stderr(&self, format: &OutputFormat) { let stderr = io::stderr(); let mut handle = stderr.lock(); @@ -164,6 +185,7 @@ fn extract_links(html_content: &str, base_url: &Url) -> Vec { } #[derive(Debug, Clone)] +/// Crawls pages from a start URL and applies an mq query to each page. pub struct Crawler { allowed_domains: Option>, checkpoint_interval_pages: usize, @@ -190,6 +212,7 @@ pub struct Crawler { impl Crawler { #[allow(clippy::too_many_arguments)] + /// Creates a crawler for `start_url`. pub async fn new( http_client: HttpClient, start_url: Url, @@ -288,6 +311,7 @@ impl Crawler { Ok(arc_robots) } + /// Runs the crawl to completion. pub async fn run(&mut self) -> Result<(), String> { // Record start time { diff --git a/crates/mq-crawler/src/http_client.rs b/crates/mq-crawler/src/http_client.rs index e1f106072..6760000c6 100644 --- a/crates/mq-crawler/src/http_client.rs +++ b/crates/mq-crawler/src/http_client.rs @@ -56,9 +56,17 @@ impl RetryConfig { #[derive(Debug, Clone)] pub enum AuthConfig { /// `Authorization: Basic `. - Basic { username: String, password: Option }, + Basic { + /// User name. + username: String, + /// Password, if any. + password: Option, + }, /// `Authorization: Bearer `. - Bearer { token: String }, + Bearer { + /// The bearer token. + token: String, + }, } /// Transient (worth retrying) vs. fatal failures. @@ -98,7 +106,9 @@ pub struct ChromiumWaitConfig { /// Enum for different HTTP client implementations #[derive(Debug, Clone)] pub enum HttpClient { + /// Plain HTTP client with retry settings. Reqwest(ReqwestClient, RetryConfig), + /// WebDriver-driven browser with retry settings. Fantoccini(fantoccini::Client, RetryConfig), /// Headless Chrome via CDP. /// `new_page()` waits for the `load` event, which covers synchronous JS diff --git a/crates/mq-crawler/src/lib.rs b/crates/mq-crawler/src/lib.rs index 32c530060..c22fb316c 100644 --- a/crates/mq-crawler/src/lib.rs +++ b/crates/mq-crawler/src/lib.rs @@ -43,7 +43,10 @@ //! - Converts HTML pages to markdown //! - Tracks statistics about the crawl //! +/// The crawler and its results. pub mod crawler; +/// HTTP client implementations. pub mod http_client; +/// robots.txt handling. pub mod robots; pub mod sitemap; diff --git a/crates/mq-crawler/src/main.rs b/crates/mq-crawler/src/main.rs index 16a42907a..d1be841df 100644 --- a/crates/mq-crawler/src/main.rs +++ b/crates/mq-crawler/src/main.rs @@ -1,3 +1,5 @@ +//! Command-line web crawler that converts pages to Markdown. + use clap::Parser; use fantoccini::wd::TimeoutConfiguration; use mq_crawler::crawler::Crawler; diff --git a/crates/mq-crawler/src/robots.rs b/crates/mq-crawler/src/robots.rs index 557f230b6..8571b7e09 100644 --- a/crates/mq-crawler/src/robots.rs +++ b/crates/mq-crawler/src/robots.rs @@ -3,8 +3,8 @@ use robots_txt::{Robots, matcher::SimpleMatcher}; use std::sync::OnceLock; use url::Url; -// A simple wrapper for robots.txt handling. -// It stores the text content and caches the parsed Robots structure. +/// Wrapper for robots.txt handling. +/// Stores the text content and caches the parsed Robots structure. #[derive(Debug)] pub struct RobotsTxt { robots_text: Option, diff --git a/crates/mq-dap/Cargo.toml b/crates/mq-dap/Cargo.toml index 8329d7a2d..ef871bbb3 100644 --- a/crates/mq-dap/Cargo.toml +++ b/crates/mq-dap/Cargo.toml @@ -11,6 +11,9 @@ readme = "README.md" repository.workspace = true version.workspace = true +[lints] +workspace = true + [dependencies] crossbeam-channel = {workspace = true} dap = {workspace = true} diff --git a/crates/mq-dap/src/adapter.rs b/crates/mq-dap/src/adapter.rs index 460daaaa6..0c6f9e27b 100644 --- a/crates/mq-dap/src/adapter.rs +++ b/crates/mq-dap/src/adapter.rs @@ -73,6 +73,7 @@ impl MqAdapter { } } + /// Receiver for messages from the debugger handler, if running. pub fn debugger_message_rx(&self) -> &Option> { &self.debugger_message_rx } diff --git a/crates/mq-dap/src/error.rs b/crates/mq-dap/src/error.rs index 6fd5954a5..46483f356 100644 --- a/crates/mq-dap/src/error.rs +++ b/crates/mq-dap/src/error.rs @@ -4,19 +4,27 @@ use dap::prelude::Command; use thiserror::Error; #[derive(Error, Debug)] +/// Errors raised by the DAP adapter. pub enum MqAdapterError { #[error("Unhandled command: {0:?}")] + /// The adapter has no handler for the command. UnhandledCommand(Box), #[error("Protocol error: {0}")] + /// A DAP message violated the protocol. ProtocolError(Cow<'static, str>), #[error("Failed to deserialize launch arguments: {0}")] + /// Launch arguments could not be parsed. LaunchArgumentsError(serde_json::Error), #[error("Missing launch arguments")] + /// The launch request carried no arguments. MissingLaunchArguments, #[error("File I/O error: {0}")] + /// Reading a file failed. FileError(String), #[error("Query execution error: {0}")] + /// The query failed to compile or run. QueryError(String), #[error("Evaluation error: {0}")] + /// Evaluating an expression failed. EvaluationError(Cow<'static, str>), } diff --git a/crates/mq-dap/src/handler.rs b/crates/mq-dap/src/handler.rs index 6f53570c0..93f26a943 100644 --- a/crates/mq-dap/src/handler.rs +++ b/crates/mq-dap/src/handler.rs @@ -27,6 +27,7 @@ impl std::fmt::Debug for DapDebuggerHandler { } impl DapDebuggerHandler { + /// Creates a handler that reports stops over `message_tx`. pub fn new(message_tx: Sender) -> Self { Self { message_tx, @@ -47,6 +48,7 @@ pub struct DapHandlerWrapper { } impl DapHandlerWrapper { + /// Wraps `handler` with the command channel and shared pause and exception flags. pub fn new( handler: DapDebuggerHandler, command_rx: Receiver, diff --git a/crates/mq-dap/src/lib.rs b/crates/mq-dap/src/lib.rs index 3a724bb21..6c23b7302 100644 --- a/crates/mq-dap/src/lib.rs +++ b/crates/mq-dap/src/lib.rs @@ -47,12 +47,19 @@ //! - `handler`: Request and event handlers //! - `server`: DAP server implementation +/// DAP adapter state and request dispatch. pub mod adapter; +/// Error types. pub mod error; +/// Debug execution engine. pub mod executor; +/// Debugger handlers bridging the engine and the server. pub mod handler; +/// Log output routed to the debug console. pub mod log; +/// DAP message types. pub mod protocol; +/// DAP server entry point. pub mod server; pub use server::start; diff --git a/crates/mq-dap/src/log.rs b/crates/mq-dap/src/log.rs index cd0cd1a46..dc01ca4fc 100644 --- a/crates/mq-dap/src/log.rs +++ b/crates/mq-dap/src/log.rs @@ -4,11 +4,13 @@ use crossbeam_channel::{Receiver, Sender}; use tracing_subscriber::fmt::MakeWriter; #[derive(Clone)] +/// Writer that forwards tracing output to a channel for the debug console. pub struct DebugConsoleWriter { sender: Option>, } impl DebugConsoleWriter { + /// Creates a writer and the receiver of the lines it captures. pub fn new() -> (Self, Receiver) { let (tx, rx) = crossbeam_channel::unbounded(); let writer = Self { sender: Some(tx) }; diff --git a/crates/mq-dap/src/protocol.rs b/crates/mq-dap/src/protocol.rs index 1f447b65b..0c311b681 100644 --- a/crates/mq-dap/src/protocol.rs +++ b/crates/mq-dap/src/protocol.rs @@ -5,30 +5,46 @@ use serde::{Deserialize, Serialize}; pub enum DebuggerMessage { /// A breakpoint was hit, should send stopped event BreakpointHit { + /// Id of the debugged thread. thread_id: i64, + /// Line number of the stop. line: usize, + /// The breakpoint that was hit. breakpoint: mq_lang::Breakpoint, + /// Debugger context at the stop. context: mq_lang::DebugContext, }, /// A step operation completed, should send stopped event StepCompleted { + /// Id of the debugged thread. thread_id: i64, + /// Line number of the stop. line: usize, + /// Debugger context at the stop. context: mq_lang::DebugContext, }, /// Program was paused by user request, should send stopped event Paused { + /// Id of the debugged thread. thread_id: i64, + /// Line number of the stop. line: usize, + /// Debugger context at the stop. context: mq_lang::DebugContext, }, /// A logpoint breakpoint fired; should send an output event without stopping execution. - LogPoint { message: String }, + LogPoint { + /// The interpolated log message. + message: String, + }, /// An uncaught error was raised while the "Uncaught Exceptions" filter is enabled, /// should send a stopped event with reason `exception`. ExceptionPaused { + /// Id of the debugged thread. thread_id: i64, + /// Message text. message: String, + /// Debugger context at the stop. context: mq_lang::DebugContext, }, /// Program has terminated @@ -56,6 +72,8 @@ pub enum DapCommand { #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct LaunchArgs { + /// Path of the mq query file to debug. pub query_file: String, + /// Path of the input file, if any. pub input_file: Option, } diff --git a/crates/mq-dap/src/server.rs b/crates/mq-dap/src/server.rs index 8c6a51a4b..02c7d3688 100644 --- a/crates/mq-dap/src/server.rs +++ b/crates/mq-dap/src/server.rs @@ -9,6 +9,7 @@ use crate::log::DebugConsoleWriter; type DynResult = miette::Result>; +/// Starts the DAP server on stdio. pub fn start() -> DynResult<()> { let (debug_writer, log_rx) = DebugConsoleWriter::new(); diff --git a/crates/mq-ffi/Cargo.toml b/crates/mq-ffi/Cargo.toml index f6ce5222a..2be2b7204 100644 --- a/crates/mq-ffi/Cargo.toml +++ b/crates/mq-ffi/Cargo.toml @@ -12,6 +12,9 @@ readme = "README.md" repository.workspace = true version.workspace = true +[lints] +workspace = true + [dependencies] libc = {workspace = true} mq-lang = {workspace = true} diff --git a/crates/mq-ffi/build.rs b/crates/mq-ffi/build.rs index 4711a418c..6d05a57b2 100644 --- a/crates/mq-ffi/build.rs +++ b/crates/mq-ffi/build.rs @@ -1,3 +1,5 @@ +//! Generates the C header. + use std::env; use std::path::Path; diff --git a/crates/mq-ffi/mq.h b/crates/mq-ffi/mq.h index 389f0b8ae..00188cad4 100644 --- a/crates/mq-ffi/mq.h +++ b/crates/mq-ffi/mq.h @@ -6,11 +6,26 @@ #include #include +/** + * Opaque handle to an mq engine. + */ typedef void mq_context_t; +/** + * Result of a query: `values` on success, `error_msg` on failure. + */ typedef struct mq_result_t { + /** + * Array of `values_len` result strings. + */ char **values; + /** + * Number of entries in `values`. + */ uintptr_t values_len; + /** + * Error message, or null on success. + */ char *error_msg; } mq_result_t; diff --git a/crates/mq-ffi/src/lib.rs b/crates/mq-ffi/src/lib.rs index 77939ac65..a2e804313 100644 --- a/crates/mq-ffi/src/lib.rs +++ b/crates/mq-ffi/src/lib.rs @@ -76,12 +76,17 @@ use std::os::raw::c_char; use std::path::PathBuf; use std::ptr; +/// Opaque handle to an mq engine. pub type MqContext = c_void; #[repr(C)] +/// Result of a query: `values` on success, `error_msg` on failure. pub struct MqResult { + /// Array of `values_len` result strings. pub values: *mut *mut c_char, + /// Number of entries in `values`. pub values_len: usize, + /// Error message, or null on success. pub error_msg: *mut c_char, } diff --git a/crates/mq-formatter/Cargo.toml b/crates/mq-formatter/Cargo.toml index 1f3ed16ba..69e88b49e 100644 --- a/crates/mq-formatter/Cargo.toml +++ b/crates/mq-formatter/Cargo.toml @@ -15,6 +15,9 @@ version.workspace = true cli = ["dep:clap", "dep:glob", "dep:miette", "miette/fancy-no-backtrace"] default = ["cli"] +[lints] +workspace = true + [dependencies] clap = {workspace = true, features = ["derive"], optional = true} glob = {version = "0.3.3", optional = true} diff --git a/crates/mq-formatter/benches/benchmark.rs b/crates/mq-formatter/benches/benchmark.rs index 4296e1c8b..f44ed124d 100644 --- a/crates/mq-formatter/benches/benchmark.rs +++ b/crates/mq-formatter/benches/benchmark.rs @@ -1,3 +1,5 @@ +//! Formatter benchmarks. + use mq_formatter::{Formatter, FormatterConfig}; fn main() { diff --git a/crates/mq-formatter/src/formatter.rs b/crates/mq-formatter/src/formatter.rs index dbf4a43fb..e98429c65 100644 --- a/crates/mq-formatter/src/formatter.rs +++ b/crates/mq-formatter/src/formatter.rs @@ -10,6 +10,7 @@ const NEW_LINE: &str = "\r\n"; const NEW_LINE: &str = "\n"; #[derive(Clone, Debug, Default)] +/// Formats mq source code according to a [`FormatterConfig`]. pub struct Formatter { config: FormatterConfig, output: String, @@ -59,11 +60,17 @@ enum SortPriority { } #[derive(Clone, Debug)] +/// Formatting options. pub struct FormatterConfig { + /// Number of spaces per indent level. pub indent_width: usize, + /// Sort `import`/`include` statements. pub sort_imports: bool, + /// Sort top-level function definitions. pub sort_functions: bool, + /// Sort dict fields. pub sort_fields: bool, + /// Maximum line width before wrapping, if any. pub max_width: Option, } @@ -111,6 +118,7 @@ pub fn needs_pipe(node: &CstNode) -> bool { } impl Formatter { + /// Creates a formatter; `None` uses [`FormatterConfig::default`]. pub fn new(config: Option) -> Self { Self { config: config.unwrap_or_default(), @@ -120,6 +128,7 @@ impl Formatter { } } + /// Formats `code`, returning the CST error reporter if it fails to parse. pub fn format(&mut self, code: &str) -> Result { if code.is_empty() { return Ok(String::new()); @@ -1688,6 +1697,7 @@ impl Formatter { } #[inline(always)] + /// Indent level of the last line written to the output. pub fn current_line_indent(&self) -> usize { // Find the last newline position let start = self.output.rfind('\n').map_or(0, |pos| pos + 1); @@ -1720,6 +1730,7 @@ impl Formatter { } #[inline(always)] + /// Whether the last output line ends with a pipe. pub fn is_last_line_pipe(&self) -> bool { let output = self.output.trim_end_matches('\n'); // Find the last newline position diff --git a/crates/mq-formatter/src/main.rs b/crates/mq-formatter/src/main.rs index 4a3838d8e..387480646 100644 --- a/crates/mq-formatter/src/main.rs +++ b/crates/mq-formatter/src/main.rs @@ -1,3 +1,5 @@ +//! Command-line formatter for mq source files. + use clap::Parser; use glob::glob; use miette::IntoDiagnostic; diff --git a/crates/mq-formatter/tests/integration_tests.rs b/crates/mq-formatter/tests/integration_tests.rs index 8f4ce1e4d..f5c4b6f41 100644 --- a/crates/mq-formatter/tests/integration_tests.rs +++ b/crates/mq-formatter/tests/integration_tests.rs @@ -1,3 +1,5 @@ +//! Formatter integration tests. + use assert_cmd::cargo; #[test] diff --git a/crates/mq-help/Cargo.toml b/crates/mq-help/Cargo.toml index c3bd20a51..f3a0348ad 100644 --- a/crates/mq-help/Cargo.toml +++ b/crates/mq-help/Cargo.toml @@ -16,6 +16,9 @@ version.workspace = true catalog = ["dep:colored", "dep:mq-lang", "dep:serde"] default = ["catalog"] +[lints] +workspace = true + [dependencies] colored = {workspace = true, optional = true} mq-lang = {workspace = true, features = ["cst", "http"], optional = true} diff --git a/crates/mq-help/src/catalog.rs b/crates/mq-help/src/catalog.rs index 65cf3b2d6..1d16534a9 100644 --- a/crates/mq-help/src/catalog.rs +++ b/crates/mq-help/src/catalog.rs @@ -10,15 +10,19 @@ use crate::reference; /// A single documented parameter, as shown by `mq help`. #[derive(Debug, Clone, Serialize)] pub struct HelpParam { + /// Parameter name. pub name: String, #[serde(rename = "type")] + /// Parameter type name. pub type_name: String, } /// A runnable example paired with its verified expected output. #[derive(Debug, Clone, Serialize)] pub struct HelpExample { + /// Example source. pub code: String, + /// Expected output of `code`. pub expected: String, } @@ -28,13 +32,21 @@ pub struct HelpExample { /// `/selectors/{name}` endpoints, so CLI and Web API can't drift apart. #[derive(Debug, Clone, Serialize)] pub struct HelpEntry { + /// Function or selector name. pub name: String, + /// Entry kind (e.g. "function", "selector"). pub kind: &'static str, + /// Parameters in declaration order. pub params: Vec, + /// Return type name. pub returns: String, + /// Description text. pub description: String, + /// Verified examples. pub examples: Vec, + /// Cargo feature required to use the item, if any. pub capability: Option, + /// Standard module that defines the item, if any. pub related_module: Option, /// Other names that resolve to this entry (e.g. `.p` for `.text`). #[serde(skip_serializing_if = "Vec::is_empty")] @@ -151,9 +163,13 @@ pub fn all_names() -> Vec { /// A standard module's header doc (if any) plus the functions it defines. #[derive(Debug, Clone, Serialize)] pub struct HelpModule { + /// Module name. pub name: String, + /// Header description. pub description: String, + /// Header examples. pub examples: Vec, + /// Functions defined by the module. pub functions: Vec, } diff --git a/crates/mq-help/src/docs.rs b/crates/mq-help/src/docs.rs index b4093f438..e0c5e31ce 100644 --- a/crates/mq-help/src/docs.rs +++ b/crates/mq-help/src/docs.rs @@ -10,7 +10,9 @@ pub use table::BUILTIN_DOC; /// (see `doc_examples` tests), so examples cannot silently rot. #[derive(Clone, Debug)] pub struct BuiltinExample { + /// Source code of the example. pub code: &'static str, + /// Expected output. pub expected: &'static str, } @@ -28,16 +30,21 @@ pub enum DocKind { /// Documentation for a native builtin function or selector. #[derive(Clone, Debug)] pub struct BuiltinDoc { + /// Function or selector name. pub name: &'static str, /// Other names that select the same thing (e.g. `.p` for `.text`). Only selectors have them. pub aliases: &'static [&'static str], + /// What the entry describes. pub kind: DocKind, + /// Description text. pub description: &'static str, + /// Parameter names. pub params: &'static [&'static str], /// Parallel to `params`; a type name (e.g. "string", "number") or "dynamic" per param. pub param_types: &'static [&'static str], /// Type name of the returned value (e.g. "array", "bool", "dynamic"). pub returns: &'static str, + /// Examples with verified output. pub examples: &'static [BuiltinExample], /// Cargo feature flag required to use this item, if any (e.g. "file-io"). pub capability: Option<&'static str>, diff --git a/crates/mq-help/src/lib.rs b/crates/mq-help/src/lib.rs index 8420d2fa9..714c7ee13 100644 --- a/crates/mq-help/src/lib.rs +++ b/crates/mq-help/src/lib.rs @@ -14,9 +14,11 @@ //! suggestions, and human-readable rendering. #[cfg(feature = "catalog")] +/// Documentation extracted from mq sources and native builtins. pub mod catalog; pub mod docs; #[cfg(feature = "catalog")] +/// Native builtin and selector reference tables. pub mod reference; #[cfg(feature = "catalog")] diff --git a/crates/mq-help/src/reference.rs b/crates/mq-help/src/reference.rs index 99312df54..5529df8fd 100644 --- a/crates/mq-help/src/reference.rs +++ b/crates/mq-help/src/reference.rs @@ -5,7 +5,9 @@ use crate::docs::BUILTIN_DOC; /// A single verified example extracted from an `Example:` code fence in a doc comment. #[derive(Debug, Clone, PartialEq, Eq)] pub struct MqExample { + /// Example source. pub code: String, + /// Expected output of `code`. pub expected: String, } @@ -13,10 +15,15 @@ pub struct MqExample { /// extracted from its CST and leading `#` doc-comment. #[derive(Debug, Clone, PartialEq, Eq)] pub struct MqFnDoc { + /// Function name. pub name: String, + /// Parameter names. pub params: Vec, + /// Description text. pub description: String, + /// Return type name. pub returns: Option, + /// Verified examples. pub examples: Vec, } @@ -24,7 +31,9 @@ pub struct MqFnDoc { /// standard-module source file (see [`extract_module_doc`]). #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct ModuleDoc { + /// Module description. pub description: String, + /// Verified examples. pub examples: Vec, } diff --git a/crates/mq-hir/Cargo.toml b/crates/mq-hir/Cargo.toml index 2c121e1ea..b2c1c687f 100644 --- a/crates/mq-hir/Cargo.toml +++ b/crates/mq-hir/Cargo.toml @@ -11,6 +11,9 @@ readme = "../../README.md" repository.workspace = true version.workspace = true +[lints] +workspace = true + [dependencies] itertools = {workspace = true} mq-help = {workspace = true} diff --git a/crates/mq-hir/src/error.rs b/crates/mq-hir/src/error.rs index 2108d81de..e77b31f7d 100644 --- a/crates/mq-hir/src/error.rs +++ b/crates/mq-hir/src/error.rs @@ -3,6 +3,7 @@ use thiserror::Error; use crate::{Hir, ScopeId, ScopeKind, Symbol, SymbolKind}; +/// Error found while building or resolving the HIR. #[derive(Debug, Error)] pub enum HirError { #[error( @@ -12,23 +13,42 @@ pub enum HirError { .map(|name| format!("{symbol}. A name with a similar spelling exists: `{name}`.")) .unwrap_or_else(|| symbol.to_string()) )] + /// A call or reference that matches no declaration. UnresolvedSymbol { + /// The unresolved symbol. symbol: Symbol, + /// A similarly spelled declared name, if any. similar_name: Option, }, + /// An `include` of a module that cannot be loaded. #[error("Included module not found: {module_name}")] - ModuleNotFound { symbol: Symbol, module_name: SmolStr }, + ModuleNotFound { + /// The `include` symbol. + symbol: Symbol, + /// The module name that failed to resolve. + module_name: SmolStr, + }, + /// `yield` used outside a function. #[error("`yield` outside a function")] - YieldOutsideFunction { symbol: Symbol }, + YieldOutsideFunction { + /// The offending `yield` keyword symbol. + symbol: Symbol, + }, } +/// Non-fatal diagnostic found in the HIR. #[derive(Debug, Error)] pub enum HirWarning { + /// Code that follows a `halt()` call. #[error("Unreachable code after halt() function call")] - UnreachableCode { symbol: Symbol }, + UnreachableCode { + /// A symbol that follows a `halt()` call in the same parent. + symbol: Symbol, + }, } impl Hir { + /// Returns unresolved symbols, missing included modules and misplaced `yield`s. pub fn errors(&self) -> Vec { self.symbols .iter() @@ -70,6 +90,7 @@ impl Hir { .collect::>() } + /// Returns warnings such as code that follows a `halt()` call. pub fn warnings(&self) -> Vec { let mut warnings = Vec::new(); @@ -114,6 +135,7 @@ impl Hir { warnings } + /// Returns each error message with its source range. pub fn error_ranges(&self) -> Vec<(String, mq_lang::Range)> { self.errors() .iter() @@ -164,6 +186,7 @@ impl Hir { } } + /// Returns each warning message with its source range. pub fn warning_ranges(&self) -> Vec<(String, mq_lang::Range)> { self.warnings() .iter() diff --git a/crates/mq-hir/src/find.rs b/crates/mq-hir/src/find.rs index c4de83ad4..0af6d0ad4 100644 --- a/crates/mq-hir/src/find.rs +++ b/crates/mq-hir/src/find.rs @@ -8,6 +8,7 @@ use crate::{ }; impl Hir { + /// Finds the symbol at `position` in the given source. pub fn find_symbol_in_position( &self, source_id: SourceId, @@ -38,6 +39,7 @@ impl Hir { }) } + /// Finds the innermost scope containing `position` in the given source. pub fn find_scope_in_position(&self, source_id: SourceId, position: mq_lang::Position) -> Option<(ScopeId, Scope)> { self.sources.get(source_id)?; @@ -52,6 +54,7 @@ impl Hir { .map(|(scope_id, _)| (scope_id, self.scopes[scope_id].clone())) } + /// Returns the symbols declared in `scope_id` and its parent scopes. pub fn find_symbols_in_scope(&self, scope_id: ScopeId) -> Vec> { self.symbol_ids_in_scope(scope_id) .into_iter() @@ -116,6 +119,7 @@ impl Hir { symbol_ids } + /// Returns all symbols that belong to the given source. pub fn find_symbols_in_source(&self, source_id: SourceId) -> Vec> { self.symbols .iter() @@ -131,6 +135,7 @@ impl Hir { .collect::>() } + /// Returns the module scope of the given source. pub fn find_scope_by_source(&self, source_id: &SourceId) -> ScopeId { self.source_scopes[source_id] } diff --git a/crates/mq-hir/src/hir.rs b/crates/mq-hir/src/hir.rs index 89373822c..3a3fb2836 100644 --- a/crates/mq-hir/src/hir.rs +++ b/crates/mq-hir/src/hir.rs @@ -13,8 +13,10 @@ use crate::{ mod lower; mod query; +/// High-level intermediate representation of mq sources: their symbols, scopes and references. #[derive(Debug)] pub struct Hir { + /// Builtin module state. pub builtin: Builtin, pub(crate) module_loader: mq_lang::ModuleLoader, pub(crate) scopes: SlotMap, @@ -79,6 +81,7 @@ impl Hir { } } + /// Registers an empty source and its module scope. pub fn add_new_source(&mut self, url: Option) -> (SourceId, ScopeId) { let source_id = self.add_source(Source::new(url)); let scope_id = self.add_scope(Scope::new( @@ -91,6 +94,7 @@ impl Hir { (source_id, scope_id) } + /// Adds `code` to an existing source and scope. pub fn add_line_of_code(&mut self, source_id: SourceId, scope_id: ScopeId, code: &str) { let (nodes, _) = mq_lang::parse_recovery(code); @@ -101,6 +105,7 @@ impl Hir { }); } + /// Parses `code` and adds it as a source, returning its ids. pub fn add_code(&mut self, url: Option, code: &str) -> (SourceId, ScopeId) { let (nodes, _) = mq_lang::parse_recovery(code); @@ -126,6 +131,7 @@ impl Hir { self.resolve(); } + /// Loads the builtin module into the HIR once, unless builtins are disabled. pub fn add_builtin(&mut self) { if self.builtin.loaded || self.builtin.disabled { return; @@ -174,6 +180,7 @@ impl Hir { } } + /// Adds an already parsed source, replacing the contents of any source with the same URL. pub fn add_nodes(&mut self, url: Url, nodes: &[mq_lang::Shared]) -> (SourceId, ScopeId) { self.add_builtin(); @@ -200,6 +207,7 @@ impl Hir { (source_id, scope_id) } + /// Returns the source registered under `url`. pub fn source_by_url(&self, url: &Url) -> Option { self.sources .iter() diff --git a/crates/mq-hir/src/hir/query.rs b/crates/mq-hir/src/hir/query.rs index 9af38c28e..2b6c579b3 100644 --- a/crates/mq-hir/src/hir/query.rs +++ b/crates/mq-hir/src/hir/query.rs @@ -65,6 +65,7 @@ impl Hir { .collect() } + /// Returns the documentation of builtin functions. #[inline(always)] pub fn builtins(&self) -> impl Iterator { self.builtin.docs.functions() @@ -77,20 +78,24 @@ impl Hir { self.bracket_key_counts.get(&call).copied().unwrap_or(0) } + /// Returns the symbol with the given id. pub fn symbol(&self, symbol_id: SymbolId) -> Option<&Symbol> { self.symbols.get(symbol_id) } + /// Iterates over all symbols. #[inline(always)] pub fn symbols(&self) -> impl Iterator { self.symbols.iter() } + /// Returns the scope with the given id. #[inline(always)] pub fn scope(&self, scope_id: ScopeId) -> Option<&Scope> { self.scopes.get(scope_id) } + /// Iterates over all scopes. #[inline(always)] pub fn scopes(&self) -> impl Iterator { self.scopes.iter() diff --git a/crates/mq-hir/src/reference.rs b/crates/mq-hir/src/reference.rs index 7c34e65d8..bb4362be8 100644 --- a/crates/mq-hir/src/reference.rs +++ b/crates/mq-hir/src/reference.rs @@ -1,6 +1,7 @@ use crate::{Hir, Symbol, SymbolId}; impl Hir { + /// Returns the symbols that reference `symbol_id`. #[inline(always)] pub fn references(&self, symbol_id: SymbolId) -> Vec<(SymbolId, Symbol)> { self.references diff --git a/crates/mq-hir/src/resolve.rs b/crates/mq-hir/src/resolve.rs index 7313cdfbe..896fb8f5a 100644 --- a/crates/mq-hir/src/resolve.rs +++ b/crates/mq-hir/src/resolve.rs @@ -55,6 +55,7 @@ impl Hir { } } + /// Returns the declaration that the reference symbol resolves to. #[inline(always)] pub fn resolve_reference_symbol(&self, ref_symbol_id: SymbolId) -> Option { self.references.get(&ref_symbol_id).copied() diff --git a/crates/mq-hir/src/scope.rs b/crates/mq-hir/src/scope.rs index 5dc63500f..ada522223 100644 --- a/crates/mq-hir/src/scope.rs +++ b/crates/mq-hir/src/scope.rs @@ -2,23 +2,38 @@ use crate::SourceInfo; use crate::source::SourceId; use crate::symbol::SymbolId; -slotmap::new_key_type! { pub struct ScopeId; } +slotmap::new_key_type! { + /// Identifier of a [`Scope`]. + pub struct ScopeId; +} +/// A lexical scope of symbols. #[derive(Debug, Clone)] pub struct Scope { + /// Where the scope appears in the source. pub source: SourceInfo, + /// What opened the scope. pub kind: ScopeKind, + /// The enclosing scope. pub parent_id: Option, + /// Directly nested scopes. pub children: Vec, } +/// The construct that opened a [`Scope`]. #[derive(Clone, Debug, PartialEq)] pub enum ScopeKind { + /// A source file or inline module. Module(SourceId), + /// A function body. Function(SymbolId), + /// A `let` binding. Let(SymbolId), + /// A block body. Block(SymbolId), + /// A loop body. Loop(SymbolId), + /// A match arm. MatchArm(SymbolId), /// A parameter's default-value expression. Runs before the function body starts, so it is /// not itself "inside" the enclosing `Function` scope (see `is_outside_function`). @@ -26,6 +41,7 @@ pub enum ScopeKind { } impl Scope { + /// Creates a scope with no children. pub fn new(source: SourceInfo, kind: ScopeKind, parent_id: Option) -> Self { Self { source, @@ -35,15 +51,18 @@ impl Scope { } } + /// Registers a nested scope. pub fn add_child(&mut self, child_id: ScopeId) { self.children.push(child_id); } + /// Whether this is a module scope. #[inline(always)] pub fn is_module(&self) -> bool { matches!(self.kind, ScopeKind::Module(_)) } + /// Returns the symbol that opened the scope, or `None` for a module. pub fn symbol_id(&self) -> Option { match self.kind { ScopeKind::Function(symbol_id) => Some(symbol_id), diff --git a/crates/mq-hir/src/source.rs b/crates/mq-hir/src/source.rs index e2cddb29d..a012485b4 100644 --- a/crates/mq-hir/src/source.rs +++ b/crates/mq-hir/src/source.rs @@ -1,25 +1,35 @@ use url::Url; -slotmap::new_key_type! { pub struct SourceId; } +slotmap::new_key_type! { + /// Identifier of a [`Source`]. + pub struct SourceId; +} +/// A source file registered in the HIR. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Source { + /// Location of the source, if it has one. pub url: Option, } impl Source { + /// Creates a source with the given URL. pub fn new(url: Option) -> Self { Self { url } } } +/// Location of a symbol or scope. #[derive(Debug, Clone, PartialEq, Hash)] pub struct SourceInfo { + /// The source the item belongs to. pub source_id: Option, + /// Range of the item within the source. pub text_range: Option, } impl SourceInfo { + /// Creates a source location. pub fn new(source_id: Option, text_range: Option) -> Self { Self { source_id, text_range } } diff --git a/crates/mq-hir/src/symbol.rs b/crates/mq-hir/src/symbol.rs index ea5bbec56..0c26586c8 100644 --- a/crates/mq-hir/src/symbol.rs +++ b/crates/mq-hir/src/symbol.rs @@ -5,13 +5,19 @@ use crate::{SourceId, scope::ScopeId}; use itertools::Itertools; use smol_str::SmolStr; -slotmap::new_key_type! { pub struct SymbolId; } +slotmap::new_key_type! { + /// Identifier of a [`Symbol`]. + pub struct SymbolId; +} /// Represents metadata about a function parameter #[derive(Debug, Clone, PartialEq)] pub struct ParamInfo { + /// Parameter name. pub name: SmolStr, + /// Whether the parameter has a default value. pub has_default: bool, + /// Whether the parameter is variadic (`*name`). pub is_variadic: bool, } @@ -36,15 +42,23 @@ impl std::fmt::Display for ParamInfo { } type Params = Vec; +/// Doc comment text with its source range. pub type Doc = (mq_lang::Range, String); +/// A named or structural element of the program, such as a function, variable or call. #[derive(Debug, Clone, PartialEq)] pub struct Symbol { + /// Doc comments attached to the symbol. pub doc: Vec, + /// What the symbol is. pub kind: SymbolKind, + /// Name or literal text of the symbol, if any. pub value: Option, + /// Scope the symbol belongs to. pub scope: ScopeId, + /// Where the symbol appears. pub source: SourceInfo, + /// Enclosing symbol, if any. pub parent: Option, /// Monotonically increasing counter set at insertion time. /// Parent symbols always receive a lower number than their children because @@ -52,45 +66,73 @@ pub struct Symbol { pub(crate) insertion_order: u32, } +/// The syntactic or semantic category of a [`Symbol`]. #[derive(Debug, Clone, PartialEq)] pub enum SymbolKind { + /// An argument passed in a call. Argument, + /// An array literal. Array, + /// An assignment. Assign, + /// A binary operator expression. BinaryOp, + /// A block expression. Block, + /// A boolean literal. Boolean, + /// A bytes literal. Bytes, + /// A call to a named function. Call, + /// A call whose callee is an expression. CallDynamic, /// Represents a destructuring binding (`let [a, b] = expr`). /// Owns PatternVariable children and the initializer expression, /// mirroring the structure of `Variable` for simple let bindings. DestructuringBinding, + /// A dict literal. Dict, + /// An `elif` branch. Elif, + /// An `else` branch. Else, + /// A `foreach` loop. Foreach, + /// A function definition with its parameters. Function(Params), + /// A bare identifier, such as a module or import alias name. Ident, + /// An `if` expression. If, + /// An `include` of the given source. Include(SourceId), /// A template string literal (`s"...${expr}..."`). InterpolatedString, + /// A keyword token. Keyword, + /// A `loop` expression. Loop, + /// A `match` expression. Match, + /// A match arm. MatchArm { + /// Whether the arm has an `if` guard. has_guard: bool, }, + /// The `None` literal. None, + /// A number literal. Number, + /// A function parameter. Parameter, /// `is_dict` is `true` for dict patterns (`{a, b}` or `{x: y}`), /// `false` for array patterns (`[a, b]`) or match arm patterns. /// `is_or` is `true` for alternatives (`p1 || p2`), whose children are the alternatives. Pattern { + /// `true` for dict patterns. is_dict: bool, + /// `true` for alternatives. is_or: bool, }, /// A variable introduced by a pattern binding. @@ -98,67 +140,91 @@ pub enum SymbolKind { /// `is_rest` is `true` for the rest element in an array pattern (`..rest`), /// which has type `Array(elem_ty)` rather than `elem_ty`. PatternVariable { + /// `true` for the rest element of an array pattern. is_rest: bool, }, + /// A reference to a variable or function. Ref, + /// A selector such as `.h1`. Selector(mq_lang::Selector), /// A `...expr` spread element inside an array or dict literal. Spread, + /// A string literal. String, + /// A symbol literal (`:name`). Symbol, + /// A unary operator expression. UnaryOp, + /// An `unless` expression. Unless, + /// An `until` loop. Until, + /// A `let` or `var` binding. Variable, + /// A `while` loop. While, + /// A `try` expression. Try, + /// The `catch` branch of a `try`. Catch, + /// An `import` of the given source. Import(SourceId), + /// A module definition for the given source. Module(SourceId), + /// A qualified access such as `module::name`. QualifiedAccess, } impl Symbol { + /// Whether the symbol is a function definition. #[inline(always)] pub fn is_function(&self) -> bool { matches!(self.kind, SymbolKind::Function(_)) } + /// Whether the symbol is a variable binding. #[inline(always)] pub fn is_variable(&self) -> bool { matches!(self.kind, SymbolKind::Variable | SymbolKind::DestructuringBinding) } + /// Whether the symbol is a module definition. #[inline(always)] pub fn is_module(&self) -> bool { matches!(self.kind, SymbolKind::Module(_)) } + /// Whether the symbol is an import. #[inline(always)] pub fn is_import(&self) -> bool { matches!(self.kind, SymbolKind::Import(_)) } + /// Whether the symbol is a function parameter. #[inline(always)] pub fn is_parameter(&self) -> bool { matches!(self.kind, SymbolKind::Parameter) } + /// Whether the symbol is a call argument. #[inline(always)] pub fn is_argument(&self) -> bool { matches!(self.kind, SymbolKind::Argument) } + /// Whether the symbol is a pattern variable. #[inline(always)] pub fn is_pattern_variable(&self) -> bool { matches!(self.kind, SymbolKind::PatternVariable { .. }) } + /// Whether the symbol is a bare identifier. #[inline(always)] pub fn is_ident(&self) -> bool { matches!(self.kind, SymbolKind::Ident) } + /// Whether the symbol is a function whose name starts with `_`. #[inline(always)] pub fn is_internal_function(&self) -> bool { if matches!(self.kind, SymbolKind::Function(_)) { diff --git a/crates/mq-lang/Cargo.toml b/crates/mq-lang/Cargo.toml index 7c4f5ca3b..ac13b0ac6 100644 --- a/crates/mq-lang/Cargo.toml +++ b/crates/mq-lang/Cargo.toml @@ -11,6 +11,9 @@ readme = "../../README.md" repository.workspace = true version.workspace = true +[lints] +workspace = true + [dependencies] mq-macros = {workspace = true} base64 = {workspace = true} diff --git a/crates/mq-lang/examples/extract_js_code.rs b/crates/mq-lang/examples/extract_js_code.rs index 839054aef..52645f737 100644 --- a/crates/mq-lang/examples/extract_js_code.rs +++ b/crates/mq-lang/examples/extract_js_code.rs @@ -1,3 +1,5 @@ +//! Extracts JavaScript code blocks from Markdown. + fn main() { let markdown_content = " # Example diff --git a/crates/mq-lang/examples/extract_list.rs b/crates/mq-lang/examples/extract_list.rs index 275d288cf..660613cb7 100644 --- a/crates/mq-lang/examples/extract_list.rs +++ b/crates/mq-lang/examples/extract_list.rs @@ -1,3 +1,5 @@ +//! Extracts list items from Markdown. + fn main() { let markdown_content = " - item 1 diff --git a/crates/mq-lang/examples/extract_list1.rs b/crates/mq-lang/examples/extract_list1.rs index 85abde80f..d7a6bdc1c 100644 --- a/crates/mq-lang/examples/extract_list1.rs +++ b/crates/mq-lang/examples/extract_list1.rs @@ -1,3 +1,5 @@ +//! Extracts list items from Markdown, variant. + fn main() { let markdown_content = " - item 1 diff --git a/crates/mq-lang/examples/extract_table.rs b/crates/mq-lang/examples/extract_table.rs index 05f34694b..00c688519 100644 --- a/crates/mq-lang/examples/extract_table.rs +++ b/crates/mq-lang/examples/extract_table.rs @@ -1,3 +1,5 @@ +//! Extracts table content from Markdown. + fn main() { let markdown_content = " # Product List diff --git a/crates/mq-lang/examples/fibonacci.rs b/crates/mq-lang/examples/fibonacci.rs index 6d8500e67..b9eb91c4b 100644 --- a/crates/mq-lang/examples/fibonacci.rs +++ b/crates/mq-lang/examples/fibonacci.rs @@ -1,3 +1,5 @@ +//! Computes Fibonacci numbers with a recursive mq function. + fn main() { let mut engine = mq_lang::DefaultEngine::default(); let code = " diff --git a/crates/mq-lang/examples/markdown_toc.rs b/crates/mq-lang/examples/markdown_toc.rs index 0f86d7ca3..942a235a2 100644 --- a/crates/mq-lang/examples/markdown_toc.rs +++ b/crates/mq-lang/examples/markdown_toc.rs @@ -1,3 +1,5 @@ +//! Builds a table of contents from Markdown headings. + fn main() { let markdown_content = " # [header1](https://example.com) diff --git a/crates/mq-lang/examples/snake_to_camel.rs b/crates/mq-lang/examples/snake_to_camel.rs index e7f2b5238..3247ad478 100644 --- a/crates/mq-lang/examples/snake_to_camel.rs +++ b/crates/mq-lang/examples/snake_to_camel.rs @@ -1,3 +1,5 @@ +//! Converts snake_case text to camelCase using the builtin module. + fn main() { let mut engine = mq_lang::DefaultEngine::default(); engine.load_builtin_module(); diff --git a/crates/mq-lang/src/ast.rs b/crates/mq-lang/src/ast.rs index df7d016db..1fa5c9c93 100644 --- a/crates/mq-lang/src/ast.rs +++ b/crates/mq-lang/src/ast.rs @@ -7,6 +7,7 @@ pub mod constants; pub mod node; pub mod parser; +/// A sequence of top-level or block-level nodes. pub type Program = Vec>; pub type TokenId = ArenaId; diff --git a/crates/mq-lang/src/ast/node.rs b/crates/mq-lang/src/ast/node.rs index ba0135624..b60d32e7e 100644 --- a/crates/mq-lang/src/ast/node.rs +++ b/crates/mq-lang/src/ast/node.rs @@ -53,20 +53,24 @@ impl Param { } } +/// Function parameters. pub type Params = Vec; pub type Args = SmallVec<[Shared; 4]>; pub type Cond = (Option>, Shared); pub type Branches = Vec; pub type MatchArms = Vec; +/// An AST node: an expression plus the id of the token it was parsed from. #[derive(PartialEq, PartialOrd, Debug, Clone)] #[cfg_attr(feature = "ast-json", derive(Serialize, Deserialize))] pub struct Node { + /// Id of the node's main token in the token arena. #[cfg_attr( feature = "ast-json", serde(skip_serializing, skip_deserializing, default = "default_token_id") )] pub token_id: TokenId, + /// The expression this node holds. pub expr: Expr, } @@ -76,16 +80,19 @@ fn default_token_id() -> TokenId { } impl Node { + /// Serializes the node to pretty-printed JSON. #[cfg(feature = "ast-json")] pub fn to_json(&self) -> Result { serde_json::to_string_pretty(self) } + /// Deserializes a node from JSON; token ids are not restored. #[cfg(feature = "ast-json")] pub fn from_json(json_str: &str) -> Result { serde_json::from_str(json_str) } + /// Source range of the node, computed from its token and children. pub fn range(&self, arena: Shared>) -> Range { match &self.expr { Expr::Block(program) @@ -203,15 +210,19 @@ impl Node { } } + /// Whether this node is the `nodes` keyword. pub fn is_nodes(&self) -> bool { matches!(&self.expr, Expr::Nodes) } } +/// An identifier paired with the token it was parsed from. #[cfg_attr(feature = "ast-json", derive(Serialize, Deserialize))] #[derive(PartialEq, Debug, Eq, Clone)] pub struct IdentWithToken { + /// The identifier name. pub name: Ident, + /// Id of the identifier token, if known. #[cfg_attr(feature = "ast-json", serde(skip_serializing, skip_deserializing, default))] pub token: Option, } @@ -235,10 +246,12 @@ impl PartialOrd for IdentWithToken { } impl IdentWithToken { + /// Creates an identifier with no token. pub fn new(name: &str) -> Self { Self::new_with_token(name, None) } + /// Creates an identifier bound to `token`. pub fn new_with_token(name: &str, token: Option) -> Self { Self { name: name.into(), @@ -262,16 +275,25 @@ pub enum StringSegment { Self_, } +/// A pattern used by `match`, `let` and `var`. #[cfg_attr(feature = "ast-json", derive(Serialize, Deserialize))] #[derive(PartialEq, PartialOrd, Debug, Clone)] pub enum Pattern { + /// A literal value. Literal(Literal), + /// Binds the matched value to a name. Ident(IdentWithToken), + /// `_`, matches anything. Wildcard, + /// `[p1, p2]`, matches an array element-wise. Array(Vec), + /// Array pattern with a rest binding: the patterns before `..` and the rest binding. ArrayRest(Vec, IdentWithToken), // patterns before .., rest binding + /// `{key: pattern}`, matches a dict by key. Dict(Vec<(IdentWithToken, Pattern)>), - Type(Ident), // :string, :number, etc. + /// A type pattern such as `:string` or `:number`. + Type(Ident), // :string, :number, etc. + /// `p1 || p2`, matches if any alternative matches. Or(Vec), // p1 || p2 || p3 } @@ -283,14 +305,21 @@ pub struct MatchArm { pub body: Shared, } +/// A literal value. #[cfg_attr(feature = "ast-json", derive(Serialize, Deserialize))] #[derive(PartialEq, PartialOrd, Debug, Clone)] pub enum Literal { + /// A string literal. String(String), + /// A bytes literal. Bytes(Vec), + /// A number literal. Number(Number), + /// A `:name` symbol. Symbol(Ident), + /// A boolean literal. Bool(bool), + /// The `None` literal. None, } @@ -395,11 +424,15 @@ impl Display for UnaryOp { } } +/// An expression in the AST. #[cfg_attr(feature = "ast-json", derive(Serialize, Deserialize))] #[derive(PartialEq, PartialOrd, Debug, Clone)] pub enum Expr { + /// `expr as name`, binding the value of the expression. As(IdentWithToken, Shared), + /// A `do ... end` block or pipeline group. Block(Program), + /// A call to a named function. Call(IdentWithToken, Args), /// `lhs op rhs`. Never desugared into `Call`, so it compiles to a direct VM opcode /// regardless of name resolution/shadowing. @@ -413,46 +446,74 @@ pub enum Expr { /// A `{...}` dict literal. Each entry is a nested `Array` of `[key, value]`, /// except spread entries (`...expr`), which remain a nested `Call` on `SPREAD`. Dict(Args), + /// A call whose callee is an arbitrary expression. CallDynamic(Shared, Args), + /// A named function definition. Def(IdentWithToken, Params, Program), + /// An anonymous function. Fn(Params, Program), + /// An immutable binding. Let(Pattern, Shared), + /// An unbounded `loop`. Loop(Program), + /// A mutable binding. Var(Pattern, Shared), + /// Assignment to an existing variable. Assign(IdentWithToken, Shared), /// `ident[index] = value`: sets one element of the variable's array or dict. Never desugared /// into `Assign(ident, set(ident, ..))`, so the VM can update the variable in place. IndexAssign(IdentWithToken, Shared, Shared), + /// Short-circuiting `&&` over its operands. And(Vec>), + /// Short-circuiting `||` over its operands. Or(Vec>), + /// A literal value. Literal(Literal), + /// A variable or function reference. Ident(IdentWithToken), + /// A string with `${...}` segments. InterpolatedString(Vec), + /// A Markdown selector such as `.h1`. Selector(Selector), /// A selector with runtime-evaluated arguments for filtered matching. /// /// Supports `.h(1..2)`, `.h(1, 2)`, `.code("rust")`, etc. SelectorCall(Selector, Box), + /// A `while` loop. While(Shared, Program), + /// An `until` loop. Until(Shared, Program), + /// A `foreach` loop over a collection. Foreach(IdentWithToken, Shared, Program), + /// Condition and body pairs; the final pair has no condition for `else`. If(Branches), + /// Like `If`, with the first condition negated. Unless(Branches), + /// A `match` on a value with its arms. Match(Shared, MatchArms), + /// An `include` of another file. Include(Literal), /// `import "path"` or `import "path" as alias`; the alias rebinds the module name. Import(Literal, Option), + /// An inline module definition. Module(IdentWithToken, Program), + /// `module::name` access: the module path and the accessed target. QualifiedAccess(Vec, AccessTarget), + /// The implicit `self` value. Self_, + /// The `nodes` keyword. Nodes, + /// A parenthesized expression. Paren(Shared), /// `try catch: ` or `try catch(): `. /// When present, `binder` is bound to a dict describing the failure (currently `{"message": ...}`) /// while evaluating the catch expression. Try(Shared, Option, Shared), + /// `break`, with an optional value. Break(Option>), + /// `continue`. Continue, + /// `yield`, with an optional value. Yield(Option>), } diff --git a/crates/mq-lang/src/ast/parser.rs b/crates/mq-lang/src/ast/parser.rs index c1ebb7392..af3515ef5 100644 --- a/crates/mq-lang/src/ast/parser.rs +++ b/crates/mq-lang/src/ast/parser.rs @@ -41,6 +41,7 @@ struct RetainedToken { node_owned: bool, } +/// Parses a token stream into an AST, allocating tokens in the arena. pub struct Parser<'a, 'alloc> { tokens: Peekable>, token_base_address: usize, @@ -51,6 +52,7 @@ pub struct Parser<'a, 'alloc> { } impl<'a, 'alloc> Parser<'a, 'alloc> { + /// Creates a parser over `tokens` for the module `module_id`. pub fn new( tokens: core::slice::Iter<'a, Token>, token_arena: &'alloc mut Arena, @@ -67,6 +69,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { } } + /// Parses the whole input into a program. pub fn parse(&mut self) -> Result { self.parse_program(ProgramKind::Root) } diff --git a/crates/mq-lang/src/cst/node.rs b/crates/mq-lang/src/cst/node.rs index 45eced555..f05fcf706 100644 --- a/crates/mq-lang/src/cst/node.rs +++ b/crates/mq-lang/src/cst/node.rs @@ -18,11 +18,16 @@ pub type TriviaList = SmallVec<[Trivia; 1]>; /// `elif` clauses attached to an `if`. pub type Elifs = SmallVec<[Shared; 2]>; +/// Whitespace, newlines and comments attached to a node. #[derive(Debug, Clone, PartialEq)] pub enum Trivia { + /// A run of spaces. Whitespace(Shared), + /// A line break. NewLine, + /// A run of tabs. Tab(Shared), + /// A `#` comment. Comment(Shared), } @@ -38,22 +43,27 @@ impl Display for Trivia { } impl Trivia { + /// Returns `true` for a whitespace trivia. pub fn is_whitespace(&self) -> bool { matches!(self, Trivia::Whitespace(_)) } + /// Returns `true` for a newline trivia. pub fn is_new_line(&self) -> bool { matches!(self, Trivia::NewLine) } + /// Returns `true` for a tab trivia. pub fn is_tab(&self) -> bool { matches!(self, Trivia::Tab(_)) } + /// Returns `true` for a comment trivia. pub fn is_comment(&self) -> bool { matches!(self, Trivia::Comment(_)) } + /// Returns the comment text, or `None` for other trivia. pub fn comment(&self) -> Option<&str> { match self { Trivia::Comment(token) => match &token.kind { @@ -64,6 +74,7 @@ impl Trivia { } } + /// Source range of the trivia; newlines have a default range. pub fn range(&self) -> Range { match self { Trivia::Comment(token) => token.range, @@ -74,42 +85,63 @@ impl Trivia { } } +/// A concrete syntax tree node with its token and surrounding trivia. #[derive(Debug, Clone, PartialEq, Default)] pub struct Node { + /// The kind of this node. pub kind: NodeKind, + /// The main token of this node, if it has one. pub token: Option>, + /// Trivia before the token. pub leading_trivia: TriviaList, + /// Trivia after the token. pub trailing_trivia: TriviaList, } +/// The kind of a CST node together with its structural children. #[derive(Debug, Clone, PartialEq, Default)] pub enum NodeKind { + /// An array literal. Array { + /// Child nodes in source order, including delimiter tokens. items: ArgList, }, + /// An `as` binding. As { + /// The expression. expr: Shared, + /// The name node. name: Shared, }, /// `lhs = rhs` or a compound assignment (`+=`, `-=`, ...); `op` identifies which. Assign { + /// The operator. op: BinaryOp, + /// The left-hand side. lhs: Shared, + /// The right-hand side. rhs: Shared, }, + /// A binary operation. BinaryOp { + /// The operator. op: BinaryOp, + /// The left-hand side. lhs: Shared, + /// The right-hand side. rhs: Shared, }, /// A `do...end` block, an implicit pipeline (`foo(a | b)`), or a selector /// descendant-chain grouping; all share the same flat `program` shape. Block { + /// Body nodes in source order. program: ArgList, }, /// `break` or `break: value`. Break { + /// The `:` token. colon: Option>, + /// The optional value. value: Option>, }, /// A function call (`ident(args)`) or a bracket/slice access desugared to the @@ -117,236 +149,389 @@ pub enum NodeKind { /// `args` is the flat, token-interleaved list `parse_args`/`parse_bracket_access` /// produce, in source order. Call { + /// Argument or condition nodes in source order, including delimiter tokens. args: ArgList, }, /// A call whose callee is an arbitrary expression, e.g. `(f)(x)` or `a[0](x)`. CallDynamic { + /// The expression that evaluates to the function to call. callee: Shared, + /// Argument or condition nodes in source order, including delimiter tokens. args: ArgList, }, + /// A `continue` expression. Continue, + /// A function definition. Def { + /// The name node. name: Shared, + /// Parameter nodes in source order, including delimiter tokens. params: Option, + /// The `:` or `do` token that opens the body. colon_or_do: Option>, + /// Body nodes in source order. program: ArgList, }, + /// A dict literal. Dict { + /// Dict entries in source order, including delimiter tokens. entries: ArgList, }, + /// A `key: value` dict entry. DictEntry { + /// The key node. key: Shared, + /// The `:` token. colon: Shared, + /// The optional value. value: Shared, }, + /// A `do` keyword. Do, + /// An `end` keyword. End, + /// An `elif` clause. Elif { + /// Argument or condition nodes in source order, including delimiter tokens. args: ArgList, + /// The `:` token. colon: Option>, + /// The body evaluated when the condition holds. then_branch: Shared, }, + /// An `else` clause. Else { + /// The `:` token. colon: Option>, + /// The body evaluated when the condition holds. then_branch: Shared, }, + /// An environment variable reference. Env, + /// End of input. #[default] Eof, /// Source the parser could not make sense of. `children` holds, in source order, the /// tokens (and any nodes) consumed during recovery, so the text round-trips losslessly. Error { + /// Nodes consumed during error recovery, in source order. children: ArgList, }, + /// An anonymous function. Fn { + /// Parameter nodes in source order, including delimiter tokens. params: ArgList, + /// The `:` or `do` token that opens the body. colon_or_do: Option>, + /// Body nodes in source order. program: ArgList, }, + /// A `foreach` loop. Foreach { + /// Argument or condition nodes in source order, including delimiter tokens. args: ArgList, + /// The `:` or `do` token that opens the body. colon_or_do: Option>, + /// Body nodes in source order. program: ArgList, }, + /// A parenthesized expression. Group { + /// The opening `(`. lparen: Shared, + /// The expression. expr: Shared, + /// The closing `)`. rparen: Shared, }, /// An identifier; `attr` is a trailing attribute selector (`x.attr`). Ident { + /// Optional trailing attribute selector. attr: Option>, }, + /// An `if` expression. If { + /// Argument or condition nodes in source order, including delimiter tokens. args: ArgList, + /// The `:` token. colon: Option>, + /// The body evaluated when the condition holds. then_branch: Shared, + /// The `elif` branches. elifs: Elifs, + /// The `else` branch, if any. else_branch: Option>, }, /// `import "path"` or `import "path" as alias`. Import { + /// The module path string. path: Shared, + /// The `as` token. as_token: Option>, + /// The alias name. alias: Option>, }, + /// An `include` statement. Include { + /// The module path string. path: Shared, }, + /// An interpolated string literal. InterpolatedString, + /// A `let` binding. Let { + /// The left-hand side. lhs: Shared, + /// The `=` token. eq_token: Shared, + /// The right-hand side. rhs: Shared, }, + /// A `loop` expression. Loop { + /// The `:` or `do` token that opens the body. colon_or_do: Option>, + /// Body nodes in source order. program: ArgList, }, + /// A `var` binding. Var { + /// The left-hand side. lhs: Shared, + /// The `=` token. eq_token: Shared, + /// The right-hand side. rhs: Shared, }, + /// A literal value. Literal, /// A zero-width placeholder for a required token that is absent /// (e.g. the `)` of an unclosed call). `expected` describes the token. Missing { + /// Description of the missing token. expected: &'static str, /// Where the token would have been, so the node has a zero-width range. position: Position, }, + /// A `match` expression. Match { + /// Argument or condition nodes in source order, including delimiter tokens. args: ArgList, + /// The `:` or `do` token that opens the body. colon_or_do: Option>, + /// Match arms in source order. arms: ArgList, + /// The closing `end` token, if any. end_token: Option>, }, /// `| pattern [if (guard)]: body`. `guard_args` is the flat, token-interleaved /// `(...)` list `parse_args` produces (present iff `if_token` is). MatchArm { + /// The leading `|` token. pipe: Shared, + /// The pattern node. pattern: Shared, + /// The `if` token of the guard, if any. if_token: Option>, + /// The parenthesized guard expression, if any. guard_args: Option, + /// The `:` token. colon: Shared, + /// The body node. body: Shared, }, + /// A `module` definition. Module { + /// The name node. name: Shared, + /// The `:` or `do` token that opens the body. colon_or_do: Option>, + /// Body nodes in source order. program: ArgList, }, + /// The `nodes` keyword. Nodes, /// `p1 || p2 || ...`; `items` interleaves the patterns with their `||` tokens. OrPattern { + /// Child nodes in source order, including delimiter tokens. items: ArgList, }, /// A `def`/`fn`/`catch` parameter; `token` is its name. Param { + /// The leading `*` of a variadic parameter. asterisk: Option>, + /// The `=` token. eq_token: Option>, + /// The default value expression. default: Option>, }, /// Wildcard/literal/ident/type/array/dict pattern (one tag, several grammars, /// like `Call`). Leaf patterns carry no items; array/dict patterns hold the /// flat, token-interleaved `[...]`/`{...}` list. Pattern { + /// Child nodes in source order, including delimiter tokens. items: ArgList, }, /// `a::b::c` or `a::b(args)`; `items` interleaves idents, `::` tokens and any call args. QualifiedAccess { + /// Child nodes in source order, including delimiter tokens. items: ArgList, }, /// A selector. `items` holds bracket tokens for `.[n]` or a trailing attribute selector. Selector { + /// Child nodes in source order, including delimiter tokens. items: ArgList, }, /// A `:name` symbol literal. Symbol { + /// The `:` token. colon: Shared, + /// The name node. name: Shared, }, + /// A selector call with arguments. SelectorCall { + /// Argument or condition nodes in source order, including delimiter tokens. args: ArgList, }, + /// The `self` keyword, optionally with an attribute selector. Self_ { + /// Optional trailing attribute selector. attr: Option>, }, + /// A `self` attribute access. SelfAttr, + /// A spread (`...expr`). Spread { + /// The operand. operand: Shared, }, + /// A bare token such as a delimiter or operator. Token, + /// A `try` expression. Try { + /// The `:` or `do` token that opens the body. colon_or_do: Option>, + /// The body node. body: Shared, + /// The `catch` clause, if any. catch: Option>, }, /// `params` is the optional `(e)` error binder. Catch { + /// Parameter nodes in source order, including delimiter tokens. params: Option, + /// The `:` or `do` token that opens the body. colon_or_do: Option>, + /// The body node. body: Shared, }, + /// An `unless` expression. Unless { + /// Argument or condition nodes in source order, including delimiter tokens. args: ArgList, + /// The `:` token. colon: Option>, + /// The body evaluated when the condition holds. then_branch: Shared, }, + /// An `until` loop. Until { + /// Argument or condition nodes in source order, including delimiter tokens. args: ArgList, + /// The `:` or `do` token that opens the body. colon_or_do: Option>, + /// Body nodes in source order. program: ArgList, }, + /// A unary operation. UnaryOp { + /// The operator. op: UnaryOp, + /// The operand. operand: Shared, }, + /// A `while` loop. While { + /// Argument or condition nodes in source order, including delimiter tokens. args: ArgList, + /// The `:` or `do` token that opens the body. colon_or_do: Option>, + /// Body nodes in source order. program: ArgList, }, + /// A `yield` expression. Yield { + /// The `:` token. colon: Option>, + /// The optional value. value: Option>, }, } +/// A binary operation. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum BinaryOp { + /// `&&` And, + /// `convert` (`@`) Convert, + /// `=` Assign, + /// `??` Coalesce, + /// `/` Division, + /// `/=` DivisionEqual, + /// `//=` DoubleDivisionEqual, + /// `|=` PipeEqual, + /// `==` Equal, + /// `>` Gt, + /// `>=` Gte, + /// `<` Lt, + /// `<=` Lte, + /// `-` Minus, + /// `-=` MinusEqual, + /// `%` Modulo, + /// `%=` ModuloEqual, + /// `*` Multiplication, + /// `*=` MultiplicationEqual, + /// `!=` NotEqual, + /// `||` Or, + /// `+` Plus, + /// `+=` PlusEqual, + /// `..` RangeOp, + /// `=~` RegexMatch, + /// `!~` NotRegexMatch, + /// `<<` LeftShift, + /// `>>` RightShift, } impl BinaryOp { + /// Whether the operator assigns to its left-hand side. pub fn is_assignment(&self) -> bool { matches!( self, @@ -362,9 +547,12 @@ impl BinaryOp { } } +/// A unary operation. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum UnaryOp { + /// `!` Not, + /// `-` prefix Negate, } @@ -378,6 +566,7 @@ impl Display for Node { } impl Node { + /// Creates a synthetic `|` node, optionally preceded by a newline. pub fn new_pipe(with_new_line: bool) -> Self { Node { kind: NodeKind::Token, @@ -399,10 +588,12 @@ impl Node { } } + /// Whether a newline precedes this node. pub fn has_new_line(&self) -> bool { self.leading_trivia.contains(&Trivia::NewLine) } + /// Range of the main token; a missing node has a zero-width range at its position. pub fn range(&self) -> Range { if let NodeKind::Missing { position, .. } = self.kind { return Range { @@ -596,6 +787,7 @@ impl Node { } } + /// The name node. pub fn name(&self) -> Option { self.token.as_ref().map(|token| match &token.kind { TokenKind::Ident(name) | TokenKind::Selector(name) => name.clone(), @@ -603,10 +795,12 @@ impl Node { }) } + /// Whether this is a bare token node. pub fn is_token(&self) -> bool { matches!(self.kind, NodeKind::Token) } + /// Whether this is the end-of-input node. pub fn is_eof(&self) -> bool { matches!(self.kind, NodeKind::Eof) } @@ -621,20 +815,24 @@ impl Node { self.is_error() || self.children().any(|child| child.has_error()) } + /// Whether this is an anonymous function. pub fn is_fn(&self) -> bool { matches!(self.kind, NodeKind::Fn { .. }) } + /// Whether this is a function definition. pub fn is_def(&self) -> bool { matches!(self.kind, NodeKind::Def { .. }) } + /// Whether this node is a `|` token. pub fn is_pipe(&self) -> bool { self.token .as_ref() .is_some_and(|token| matches!(token.kind, TokenKind::Pipe)) } + /// Comments in the leading trivia with their ranges. pub fn comments(&self) -> Vec { self.leading_trivia .iter() @@ -647,10 +845,12 @@ impl Node { self.children().filter(|child| !child.is_token()) } + /// Like [`non_token_children`](Self::non_token_children), collected. pub fn children_without_token(&self) -> SmallVec<[Shared; 4]> { self.children().filter(|child| !child.is_token()).cloned().collect() } + /// Splits the children into header nodes (name, params, condition) and body nodes, dropping tokens. pub fn split_cond_and_program(&self) -> (Vec>, Vec>) { match &self.kind { NodeKind::Def { @@ -721,6 +921,7 @@ impl Node { ) } + /// The left and right operands of a binary or assignment node. pub fn binary_op(&self) -> Option<(&Shared, &Shared)> { match &self.kind { NodeKind::BinaryOp { lhs, rhs, .. } | NodeKind::Assign { lhs, rhs, .. } => Some((lhs, rhs)), @@ -728,6 +929,7 @@ impl Node { } } + /// The operand of a unary operation node. pub fn unary_op(&self) -> Option> { match &self.kind { NodeKind::UnaryOp { operand, .. } => Some(Shared::clone(operand)), diff --git a/crates/mq-lang/src/cst/parser.rs b/crates/mq-lang/src/cst/parser.rs index 70814cf0a..4c959eb8e 100644 --- a/crates/mq-lang/src/cst/parser.rs +++ b/crates/mq-lang/src/cst/parser.rs @@ -15,6 +15,7 @@ use super::{ use itertools::Itertools; use thiserror::Error; +/// Collects parse errors, capped at a maximum count. #[derive(Debug, Clone, PartialEq, Error)] pub struct ErrorReporter { errors: BTreeSet, @@ -37,6 +38,7 @@ impl Default for ErrorReporter { } impl ErrorReporter { + /// Creates a reporter that keeps at most `max_errors` errors. pub fn new(max_errors: usize) -> Self { Self { errors: BTreeSet::new(), @@ -44,6 +46,7 @@ impl ErrorReporter { } } + /// Creates a reporter pre-filled with `errors`. pub fn with_error(errors: Vec, max_errors: usize) -> Self { let mut h = BTreeSet::new(); h.extend(errors); @@ -51,12 +54,14 @@ impl ErrorReporter { Self { errors: h, max_errors } } + /// Records `error` unless the limit is reached. pub fn report(&mut self, error: ParseError) { if self.errors.len() < self.max_errors { self.errors.insert(error); } } + /// Returns the collected errors as diagnostics. pub fn to_vec(&self) -> Vec { self.errors .iter() @@ -74,6 +79,7 @@ impl ErrorReporter { .collect() } + /// Whether any error was reported. pub fn has_errors(&self) -> bool { !self.errors.is_empty() } @@ -97,6 +103,7 @@ impl ErrorReporter { .collect::>() } + /// Returns each error message with its source range in `text`. pub fn error_ranges(&self, text: &str) -> Vec<(String, Range)> { self.to_vec() .iter() @@ -125,6 +132,7 @@ enum ProgramKind { Block, } +/// Error-recovering parser that builds a lossless CST from tokens. pub struct Parser<'a> { tokens: &'a [Shared], pos: usize, @@ -132,6 +140,7 @@ pub struct Parser<'a> { } impl<'a> Parser<'a> { + /// Creates a parser over `tokens`. pub fn new(tokens: &'a [Shared]) -> Self { Self { tokens, @@ -168,6 +177,7 @@ impl<'a> Parser<'a> { self.advance().ok_or(ParseError::UnexpectedEOFDetected) } + /// Parses the tokens into top-level nodes and the errors found along the way. pub fn parse(&mut self) -> (Vec>, ErrorReporter) { let nodes = self.parse_program(ProgramKind::Root, false); (nodes, std::mem::take(&mut self.errors)) diff --git a/crates/mq-lang/src/diagnostic.rs b/crates/mq-lang/src/diagnostic.rs index d60a45664..3f85bf28c 100644 --- a/crates/mq-lang/src/diagnostic.rs +++ b/crates/mq-lang/src/diagnostic.rs @@ -10,8 +10,11 @@ use crate::Range; /// hint strings explaining how to fix the issue. #[derive(Debug, Clone, PartialEq)] pub struct Diagnostic { + /// The error message. pub message: String, + /// Where the problem is, if known. pub range: Option, + /// Suggestions for fixing the problem. pub hints: Vec, } diff --git a/crates/mq-lang/src/engine.rs b/crates/mq-lang/src/engine.rs index eaaf719f0..727598872 100644 --- a/crates/mq-lang/src/engine.rs +++ b/crates/mq-lang/src/engine.rs @@ -100,6 +100,7 @@ impl Default for Engine> { // `let engine: Engine = Engine::new(resolver);` (this only fixes the *value* passed at // construction; use `set_io` afterwards to install it). impl Engine> { + /// Creates an engine with the given module resolver and a sandboxed native `Io` that denies all access by default. pub fn new(module_resolver: T) -> Self { let token_arena = create_default_token_arena(); let module_loader = ModuleLoader::new(module_resolver); @@ -551,6 +552,7 @@ impl Engine { .map_err(|e| Box::new(error::Error::from_error("", e.into(), module_loader.clone()))) } + /// The mq-lang crate version. pub const fn version() -> &'static str { env!("CARGO_PKG_VERSION") } diff --git a/crates/mq-lang/src/engine/debugger.rs b/crates/mq-lang/src/engine/debugger.rs index 958446f73..12b4d8ce8 100644 --- a/crates/mq-lang/src/engine/debugger.rs +++ b/crates/mq-lang/src/engine/debugger.rs @@ -54,14 +54,17 @@ impl Engine { Shared::clone(&self.vm.debugger) } + /// Installs the handler called on breakpoints and steps. pub fn set_debugger_handler(&mut self, handler: Box) { self.vm.debugger_handler = Shared::new(SharedCell::new(handler)); } + /// The arena holding all tokens parsed by this engine. pub fn token_arena(&self) -> Shared>> { Shared::clone(&self.token_arena) } + /// Name of the module with the given id. pub fn get_module_name(&self, module_id: ModuleId) -> Cow<'static, str> { self.vm.module_loader.module_name(module_id) } @@ -74,6 +77,7 @@ impl Engine { .map_err(|e| Box::new(error::Error::from_error("", e.into(), module_loader.clone()))) } + /// Source code of the module with the given id. pub fn get_source_code_for_debug(&self, module_id: ModuleId) -> Result> { let module_loader = &self.vm.module_loader; let source_code = module_loader.get_source_code_for_debug(module_id); diff --git a/crates/mq-lang/src/error.rs b/crates/mq-lang/src/error.rs index 3462563cc..8c4d078e6 100644 --- a/crates/mq-lang/src/error.rs +++ b/crates/mq-lang/src/error.rs @@ -81,6 +81,7 @@ impl Error { } } + /// Builds an error, resolving module source code through `module_loader` for diagnostics. #[cold] pub fn from_error( top_level_source_code: impl Into, diff --git a/crates/mq-lang/src/io.rs b/crates/mq-lang/src/io.rs index de06fd7e9..ec5e25fd5 100644 --- a/crates/mq-lang/src/io.rs +++ b/crates/mq-lang/src/io.rs @@ -112,7 +112,9 @@ pub struct FileMetadata { /// choose to (see [`SandboxedIo`] for a decorator that does); `Io` itself is /// purely a capability abstraction, not a policy. pub trait Io: std::fmt::Debug + IoSyncBound + 'static { + /// Reads the file at `path` as UTF-8 text. fn read_to_string(&self, path: &Path) -> Result; + /// Reads the file at `path` as bytes. fn read_bytes(&self, path: &Path) -> Result, IoError>; /// Opens `path` for incremental reading, backing `open_file`. The default reads the whole @@ -121,7 +123,9 @@ pub trait Io: std::fmt::Debug + IoSyncBound + 'static { Ok(Box::new(std::io::Cursor::new(self.read_bytes(path)?))) } + /// Writes `content` to `path`, replacing the file. fn write(&self, path: &Path, content: &[u8]) -> Result<(), IoError>; + /// Whether `path` exists. fn exists(&self, path: &Path) -> Result; /// Size of the file at `path`, in bytes. @@ -136,6 +140,7 @@ pub trait Io: std::fmt::Debug + IoSyncBound + 'static { /// Falls back to the original path unchanged if canonicalization fails. fn canonicalize(&self, path: &Path) -> PathBuf; + /// Value of the environment variable `name`. fn env_var(&self, name: &str) -> Result; /// All accessible environment-variable pairs, backing the `env()` builtin. @@ -184,7 +189,9 @@ pub trait Io: std::fmt::Debug + IoSyncBound + 'static { .collect() } + /// The user home directory, if known. fn home_dir(&self) -> Option; + /// The current working directory, if known. fn current_dir(&self) -> Option; /// Runs `command` with `args` as a child process — never through a shell, so shell diff --git a/crates/mq-lang/src/io/error.rs b/crates/mq-lang/src/io/error.rs index 66f0af542..1ebe2df80 100644 --- a/crates/mq-lang/src/io/error.rs +++ b/crates/mq-lang/src/io/error.rs @@ -8,6 +8,7 @@ use thiserror::Error; #[derive(Debug, Clone, PartialEq, Error)] #[non_exhaustive] pub enum IoError { + /// The named resource does not exist. #[error("{0} not found")] NotFound(Cow<'static, str>), /// The operation was refused by the concrete `Io` implementation (e.g. @@ -15,6 +16,7 @@ pub enum IoError { /// disabled). #[error("{0}")] PermissionDenied(Cow<'static, str>), + /// Any other failure. #[error("{0}")] Other(Cow<'static, str>), } diff --git a/crates/mq-lang/src/io/mem.rs b/crates/mq-lang/src/io/mem.rs index 264a80375..0468b9124 100644 --- a/crates/mq-lang/src/io/mem.rs +++ b/crates/mq-lang/src/io/mem.rs @@ -79,32 +79,38 @@ impl MemIo { self.open_readers.load(Ordering::SeqCst) } + /// Adds a file at `path`. pub fn with_file(self, path: impl Into, content: impl Into>) -> Self { self.files.lock().unwrap().insert(path.into(), content.into()); self } + /// Sets an environment variable. pub fn with_env(self, key: impl Into, value: impl Into) -> Self { self.env.lock().unwrap().insert(key.into(), value.into()); self } + /// Sets the body returned by `fetch` and `http_request` for `url`. pub fn with_fetch_response(self, url: impl Into, body: impl Into) -> Self { self.fetch_responses.lock().unwrap().insert(url.into(), body.into()); self } + /// Sets the stdout returned for running `command` with `args`. pub fn with_command_response(self, command: impl Into, args: &[String], output: impl Into) -> Self { let key = command_key(&command.into(), args); self.command_responses.lock().unwrap().insert(key, output.into()); self } + /// Sets the home directory. pub fn with_home(mut self, path: impl Into) -> Self { self.home = Some(path.into()); self } + /// Sets the current directory. pub fn with_cwd(mut self, path: impl Into) -> Self { self.cwd = Some(path.into()); self @@ -125,6 +131,7 @@ impl MemIo { this } + /// Lines written to stdout so far, if console capture is on. pub fn stdout_lines(&self) -> Vec { self.console .as_ref() @@ -132,6 +139,7 @@ impl MemIo { .unwrap_or_default() } + /// Lines written to stderr so far, if console capture is on. pub fn stderr_lines(&self) -> Vec { self.console .as_ref() diff --git a/crates/mq-lang/src/io/sandboxed.rs b/crates/mq-lang/src/io/sandboxed.rs index df25bbe18..e8d90fa40 100644 --- a/crates/mq-lang/src/io/sandboxed.rs +++ b/crates/mq-lang/src/io/sandboxed.rs @@ -39,6 +39,7 @@ impl Default for SandboxedIo { } impl SandboxedIo { + /// Wraps `inner` with every permission denied. pub fn new(inner: Inner) -> Self { Self { inner, diff --git a/crates/mq-lang/src/lexer.rs b/crates/mq-lang/src/lexer.rs index 8a8797de7..2ccfa7dea 100644 --- a/crates/mq-lang/src/lexer.rs +++ b/crates/mq-lang/src/lexer.rs @@ -8,9 +8,12 @@ use crate::module::ModuleId; use crate::number::Number; use crate::range::{Position, Range}; +/// Options for the lexer. #[derive(Debug, Clone, Default)] pub struct Options { + /// Emit `Unknown` tokens instead of failing on unrecognized text. pub ignore_errors: bool, + /// Emit whitespace, tab and newline tokens. pub include_spaces: bool, } diff --git a/crates/mq-lang/src/lexer/token.rs b/crates/mq-lang/src/lexer/token.rs index 5d4246b77..5c6accd74 100644 --- a/crates/mq-lang/src/lexer/token.rs +++ b/crates/mq-lang/src/lexer/token.rs @@ -7,10 +7,13 @@ use crate::{ArenaId, module::ModuleId, number::Number, range::Range}; #[cfg(feature = "ast-json")] use serde::{Deserialize, Serialize}; +/// A piece of an interpolated string. #[cfg_attr(feature = "ast-json", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialOrd, PartialEq, Ord, Eq)] pub enum StringSegment { + /// Literal text and its range. Text(String, Range), + /// The source of a `${...}` expression and its range. Expr(SmolStr, Range), } @@ -27,11 +30,15 @@ fn default_module_id() -> ModuleId { ArenaId::new(0) } +/// A lexed token with its source range. #[cfg_attr(feature = "ast-json", derive(Serialize, Deserialize))] #[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone)] pub struct Token { + /// Source range of the token. pub range: Range, + /// The kind of the token. pub kind: TokenKind, + /// The module the token came from. #[cfg_attr( feature = "ast-json", serde(skip_serializing, skip_deserializing, default = "default_module_id") @@ -39,96 +46,179 @@ pub struct Token { pub module_id: ModuleId, } -#[cfg_attr(feature = "ast-json", derive(Serialize, Deserialize))] -#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone)] /// Represents the kind of a token in the mq language. /// /// TokenKind variants are sorted alphabetically for maintainability. +#[cfg_attr(feature = "ast-json", derive(Serialize, Deserialize))] +#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone)] pub enum TokenKind { + /// `&&` And, + /// `->` Arrow, + /// `as` As, + /// `@` Convert, + /// `*` Asterisk, + /// `true` or `false`. BoolLiteral(bool), + /// A bytes literal. BytesLiteral(Vec), + /// `break` Break, + /// `catch` Catch, + /// `??` Coalesce, + /// `:` Colon, + /// `::` DoubleColon, + /// `//=` DoubleSlashEqual, + /// `...` DotDotDot, + /// `,` Comma, + /// A comment, without the leading `#`. Comment(String), + /// `continue` Continue, + /// `def` Def, + /// `do` Do, + /// `elif` Elif, + /// `else` Else, + /// `end` End, + /// An environment variable reference such as `$HOME`. Env(SmolStr), + /// End of input. Eof, + /// `=` Equal, + /// `==` EqEq, + /// `fn` Fn, + /// `foreach` Foreach, + /// `>` Gt, + /// `>=` Gte, + /// An identifier. Ident(SmolStr), + /// `if` If, + /// `include` Include, + /// A string with `${...}` segments. InterpolatedString(Vec), + /// `import` Import, + /// `{` LBrace, + /// `[` LBracket, + /// `let` Let, + /// `<<` LeftShift, + /// `loop` Loop, + /// `<` Lt, + /// `<=` Lte, + /// `match` Match, + /// `module` Module, + /// `-` Minus, + /// `-=` MinusEqual, + /// `!=` NeEq, + /// A line break. NewLine, + /// `nodes` Nodes, + /// `None` None, + /// `!` Not, + /// A number literal. NumberLiteral(Number), + /// `||` Or, + /// `%` Percent, + /// `%=` PercentEqual, + /// `|` Pipe, + /// `|=` PipeEqual, + /// `+` Plus, + /// `+=` PlusEqual, + /// `?` Question, + /// `}` RBrace, + /// `..` DoubleDot, + /// `]` RBracket, + /// `>>` RightShift, + /// `)` RParen, + /// A selector such as `.h1`. Selector(SmolStr), + /// `self` Self_, + /// `;` SemiColon, + /// `/` Slash, + /// `/=` SlashEqual, + /// A string literal. StringLiteral(String), + /// `*=` StarEqual, + /// A run of tabs; the field is the count. Tab(usize), + /// `=~` TildeEqual, + /// `!~` NotTildeEqual, + /// `try` Try, /// Text the lexer could not tokenize; only produced when `ignore_errors` is set. Unknown(String), + /// `unless` Unless, + /// `until` Until, + /// A run of spaces; the field is the count. Whitespace(usize), + /// `while` While, + /// `(` LParen, + /// `var` Var, + /// `yield` Yield, } @@ -165,6 +255,7 @@ impl TokenKind { } impl Token { + /// Creates a token of `kind` with a default range and module id. pub fn new(kind: TokenKind) -> Self { Self { kind, @@ -173,11 +264,13 @@ impl Token { } } + /// Whether this is the end-of-input token. #[inline(always)] pub fn is_eof(&self) -> bool { matches!(self.kind, TokenKind::Eof) } + /// Whether this is a selector token. #[inline(always)] pub fn is_selector(&self) -> bool { matches!(self.kind, TokenKind::Selector(_)) diff --git a/crates/mq-lang/src/lib.rs b/crates/mq-lang/src/lib.rs index 0b3462bd9..b7abdccfe 100644 --- a/crates/mq-lang/src/lib.rs +++ b/crates/mq-lang/src/lib.rs @@ -105,7 +105,9 @@ pub use selector::{AttrKind, SELECTOR_NAMES, Selector}; #[cfg(feature = "debug-trace")] pub use tarn::{BytecodeChunk, BytecodeDump, BytecodeInstruction, BytecodeLocation, BytecodePhase}; +/// An [`Engine`] using the default module resolver. pub type DefaultEngine = Engine; +/// A [`ModuleLoader`] using the default module resolver. pub type DefaultModuleLoader = ModuleLoader; pub use suggest::{suggest_name, suggest_selector}; @@ -133,17 +135,20 @@ pub use runtime::debugger::{ use crate::ast::TokenId; +/// The result of evaluating a query. pub type MqResult = Result>; /// Type alias for reference-counted pointer, switches between Shared and Arc depending on "sync" feature. #[cfg(not(feature = "sync"))] pub type Shared = Rc; +/// Reference-counted pointer: `Rc`, or `Arc` with the `sync` feature. #[cfg(feature = "sync")] pub type Shared = Arc; /// Type alias for interior mutability, switches between SharedCell and RwLock depending on "sync" feature. #[cfg(not(feature = "sync"))] pub type SharedCell = RefCell; +/// Interior-mutability cell: `RefCell`, or `RwLock` with the `sync` feature. #[cfg(feature = "sync")] pub type SharedCell = RwLock; @@ -183,6 +188,7 @@ pub fn parse_recovery(code: &str) -> (Vec>, CstErrorReporter) { CstParser::new(&token_vec).parse() } +/// Parses `code` into an AST program, allocating tokens in `token_arena`. pub fn parse(code: &str, token_arena: TokenArena) -> Result> { parse_in_module(code, token_arena, Module::TOP_LEVEL_MODULE_ID) .map_err(|e| Box::new(error::Error::from_error(code, e, DefaultModuleLoader::default()))) @@ -221,12 +227,14 @@ pub fn parse_mdx_input_with(input: &str, options: mq_markdown::ParseOptions) -> Ok(mdx.nodes.into_iter().map(RuntimeValue::from).collect()) } +/// Parses an HTML string, converting it to Markdown nodes. #[cfg(feature = "html-to-markdown")] pub fn parse_html_input(input: &str) -> miette::Result> { let html = mq_markdown::Markdown::from_html_str(input)?; Ok(html.nodes.into_iter().map(RuntimeValue::from).collect()) } +/// Parses an HTML string with the given conversion options, converting it to Markdown nodes. #[cfg(feature = "html-to-markdown")] pub fn parse_html_input_with_options( input: &str, diff --git a/crates/mq-lang/src/module.rs b/crates/mq-lang/src/module.rs index 43a7330bc..454d9a3ec 100644 --- a/crates/mq-lang/src/module.rs +++ b/crates/mq-lang/src/module.rs @@ -27,6 +27,7 @@ thread_local! { static BUILTIN_CACHE: RefCell> = const { RefCell::new(None) }; } +/// Id of a module name in the module-name arena. pub type ModuleId = ArenaId; type ModuleName = SmolStr; @@ -64,6 +65,7 @@ fn get_module_name(name: &str) -> Cow<'static, str> { } } +/// Loads, caches and tracks the source of modules through a [`ModuleResolver`]. #[derive(Debug, Clone)] pub struct ModuleLoader { /// Ids of the modules this loader has loaded. @@ -101,20 +103,29 @@ pub(crate) struct ResolvedModule<'a> { pub(crate) source: &'a str, } +/// A loaded module and its top-level definitions. #[derive(Debug, Clone, PartialEq)] pub struct Module { + /// The module name. pub name: String, + /// `def` nodes in the module. pub functions: Program, + /// Nested `module` nodes. pub modules: Program, + /// `let` and `var` nodes in the module. pub vars: Program, } impl Module { + /// Name of the builtin module. pub const BUILTIN_MODULE: &str = "builtin"; + /// Name of the top-level (query) module. pub const TOP_LEVEL_MODULE: &str = "top-level"; + /// Id of the top-level module. pub const TOP_LEVEL_MODULE_ID: ArenaId = ArenaId::new(0); } +/// Source of the modules bundled with mq, by name. pub static STANDARD_MODULES: LazyLock = LazyLock::new(|| { let mut map = FxHashMap::default(); @@ -153,9 +164,11 @@ pub static STANDARD_MODULES: LazyLock = LazyLock::new(|| { map }); +/// Source of the builtin module. pub const BUILTIN_FILE: &str = include_str!("../builtin.mq"); impl ModuleLoader { + /// Creates a loader that resolves modules with `resolver`. pub fn new(resolver: T) -> Self { let mut loaded_modules = FxHashSet::default(); loaded_modules.insert(Module::TOP_LEVEL_MODULE_ID); @@ -258,6 +271,7 @@ impl ModuleLoader { } } + /// Resolves `module_name` to the path it would be loaded from. pub fn get_module_path(&self, module_name: &str) -> Result { self.resolver.get_path(module_name) } @@ -267,19 +281,23 @@ impl ModuleLoader { self.resolver.get_path(&self.module_key(module_id)) } + /// Sets the top-level source returned for debugging. #[cfg(feature = "debugger")] pub fn set_source_code(&mut self, source_code: String) { self.source_code = Some(source_code); } + /// The resolver search paths. pub fn search_paths(&self) -> Vec { self.resolver.search_paths() } + /// Replaces the resolver search paths. pub fn set_search_paths(&mut self, paths: Vec) { self.resolver.set_search_paths(paths); } + /// Parses `code` and registers it as module `module_name`. pub fn load(&mut self, module_name: &str, code: &str, token_arena: TokenArena) -> Result { self.load_keyed(module_name, module_name, code, token_arena) } @@ -303,6 +321,7 @@ impl ModuleLoader { self.register_loaded(module_id, module_name, &program) } + /// Registers an already parsed program as module `module_name`. pub fn load_from_ast(&mut self, module_name: &str, program: &mut Program) -> Result { if self.is_loaded(module_name) { return Err(ModuleError::AlreadyLoaded(Cow::Owned(module_name.to_string()))); @@ -377,10 +396,12 @@ impl ModuleLoader { self.register_loaded(module_id, self.resolver.canonical_name(module_path), &program) } + /// The short name a module path is registered under. pub fn canonical_name<'a>(&self, module_path: &'a str) -> &'a str { self.resolver.canonical_name(module_path) } + /// Resolves, parses and registers the module at `module_path`. pub fn load_from_file(&mut self, module_path: &str, token_arena: TokenArena) -> Result { // Check before resolving to avoid unnecessary I/O (disk read or network fetch) // when the same module is imported more than once. Modules are keyed by the full @@ -439,6 +460,7 @@ impl ModuleLoader { } } + /// Returns the source code of `module_name` from the resolver. pub fn resolve(&self, module_name: &str) -> Result { #[cfg(feature = "http-import")] if self.http_depth > 0 @@ -547,6 +569,7 @@ impl ModuleLoader { Ok(module) } + /// Source code of the module, using the stored top-level source for the top-level module. #[cfg(feature = "debugger")] pub fn get_source_code_for_debug(&self, module_id: ModuleId) -> Result { #[cfg(feature = "mqc")] @@ -566,6 +589,7 @@ impl ModuleLoader { } } + /// Source code of the module, using `source_code` for the top-level module. #[cold] pub fn get_source_code(&self, module_id: ModuleId, source_code: String) -> Result { #[cfg(feature = "mqc")] diff --git a/crates/mq-lang/src/module/error.rs b/crates/mq-lang/src/module/error.rs index e2559a2c3..fc6d08a77 100644 --- a/crates/mq-lang/src/module/error.rs +++ b/crates/mq-lang/src/module/error.rs @@ -8,14 +8,19 @@ use std::borrow::Cow; #[derive(Debug, PartialEq, Error)] #[non_exhaustive] pub enum ModuleError { + /// The module was already loaded. #[error("Module `{0}` is already loaded")] AlreadyLoaded(Cow<'static, str>), + /// The module could not be found. #[error("Module `{0}` not found")] NotFound(Cow<'static, str>), + /// Reading the module failed. #[error("IO error: {0}")] IOError(Cow<'static, str>), + /// The module has a syntax error. #[error(transparent)] SyntaxError(#[from] SyntaxError), + /// The module declaration is malformed. #[error("Invalid module, expected IDENT or BINDING")] InvalidModule, /// HTTP imports are only permitted at the top level; modules may not fetch remote dependencies. diff --git a/crates/mq-lang/src/module/resolver/lockfile.rs b/crates/mq-lang/src/module/resolver/lockfile.rs index 0fd59b3e6..3ade7227c 100644 --- a/crates/mq-lang/src/module/resolver/lockfile.rs +++ b/crates/mq-lang/src/module/resolver/lockfile.rs @@ -30,7 +30,10 @@ pub enum LockCheck { /// The existing entry's hash matches the freshly fetched content. Match, /// The existing entry's hash does not match; the remote content has drifted. - Mismatch { locked: String }, + Mismatch { + /// The hash recorded in the lock file. + locked: String, + }, } /// A parsed `mq.lock` file: maps a fetched module URL to the SHA-256 hex digest of the diff --git a/crates/mq-lang/src/mqc.rs b/crates/mq-lang/src/mqc.rs index cf5404e21..9636f769b 100644 --- a/crates/mq-lang/src/mqc.rs +++ b/crates/mq-lang/src/mqc.rs @@ -58,49 +58,76 @@ const RECOMPILE_HELP: &str = "Recompile the program from its source with this ve #[derive(Debug, thiserror::Error, miette::Diagnostic)] #[non_exhaustive] pub enum MqcError { + /// The input is not a `.mqc` file. #[error("not an mq bytecode file")] #[diagnostic(code(mq::mqc::not_mqc))] NotMqc, + /// The file structure is invalid. #[error("malformed .mqc file: {0}")] #[diagnostic(code(mq::mqc::malformed), help("{}", RECOMPILE_HELP))] Malformed(Cow<'static, str>), + /// The contents do not match the stored checksum. #[error(".mqc checksum mismatch; the file is corrupted")] #[diagnostic(code(mq::mqc::checksum), help("{}", RECOMPILE_HELP))] ChecksumMismatch, + /// The file exceeds the size limit. #[error(".mqc file is {size} bytes; the limit is {limit}")] #[diagnostic(code(mq::mqc::too_large))] - TooLarge { size: u64, limit: u64 }, + TooLarge { + /// Actual size in bytes. + size: u64, + /// Maximum size in bytes. + limit: u64, + }, + /// The container format version is not supported. #[error("unsupported .mqc container version {0}")] #[diagnostic(code(mq::mqc::version), help("{}", RECOMPILE_HELP))] UnsupportedContainerVersion(u16), + /// A section uses an unsupported version. #[error("unsupported version {version} of the {tag} section")] #[diagnostic(code(mq::mqc::version), help("{}", RECOMPILE_HELP))] - UnsupportedSectionVersion { tag: String, version: u16 }, + UnsupportedSectionVersion { + /// Section tag. + tag: String, + /// Version found in the file. + version: u16, + }, + /// The file has a required section this build does not know. #[error("unknown required section {0}")] #[diagnostic(code(mq::mqc::section), help("{}", RECOMPILE_HELP))] UnknownRequiredSection(String), + /// A section appears more than once. #[error("duplicate section {0}")] #[diagnostic(code(mq::mqc::section))] DuplicateSection(String), + /// A required section is absent. #[error("missing required section {0}")] #[diagnostic(code(mq::mqc::section))] MissingSection(&'static str), + /// The file was compiled for a different VM version or ABI. #[error( "compiled by mq {found_version} (VM ABI {found_abi}), but this is mq {expected_version} (VM ABI {expected_abi})" )] #[diagnostic(code(mq::mqc::abi), help("{}", RECOMPILE_HELP))] IncompatibleVm { + /// mq version that compiled the file. found_version: String, + /// VM ABI of the compiling build. found_abi: u32, + /// mq version of this build. expected_version: String, + /// VM ABI of this build. expected_abi: u32, }, + /// The program uses builtins this build lacks. #[error("requires functions this build does not provide: {}", .0.join(", "))] #[diagnostic(code(mq::mqc::features), help("Run it with an mq build that has the same features."))] MissingBuiltins(Vec), + /// A value cannot be stored in a `.mqc` file. #[error("cannot save {0} in a .mqc file")] #[diagnostic(code(mq::mqc::unsupported_value))] UnsupportedValue(String), + /// An environment variable would be baked in at compile time. #[error("${0} would be read at compile time and saved in the program")] #[diagnostic( code(mq::mqc::env), @@ -109,9 +136,11 @@ pub enum MqcError { ) )] EnvironmentAtCompileTime(String), + /// The bytecode failed validation. #[error("invalid bytecode: {0}")] #[diagnostic(code(mq::mqc::invalid_bytecode), help("{}", RECOMPILE_HELP))] InvalidBytecode(String), + /// A module-level `let` read a name that is only known at run time. #[error("\"{name}\" is not defined when module-level `let`s are computed")] #[diagnostic( code(mq::mqc::module_let_runtime_value), @@ -120,10 +149,13 @@ pub enum MqcError { ) )] ModuleLevelNotDefined { + /// The undefined name. name: String, + /// The underlying error. #[source] source: Box, }, + /// Compiling the source failed. #[error(transparent)] #[diagnostic(transparent)] Compile(Box), diff --git a/crates/mq-lang/src/range.rs b/crates/mq-lang/src/range.rs index 1ce1a2368..9c8c10463 100644 --- a/crates/mq-lang/src/range.rs +++ b/crates/mq-lang/src/range.rs @@ -5,7 +5,9 @@ use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "ast-json", derive(Serialize, Deserialize))] #[derive(Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Hash)] pub struct Position { + /// One-based line number. pub line: u32, + /// One-based column number. pub column: usize, } @@ -26,7 +28,9 @@ impl Position { #[cfg_attr(feature = "ast-json", derive(Serialize, Deserialize))] #[derive(Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Default, Hash)] pub struct Range { + /// Start of the range. pub start: Position, + /// End of the range. pub end: Position, } diff --git a/crates/mq-lang/src/runtime/debugger.rs b/crates/mq-lang/src/runtime/debugger.rs index 8054b7598..e34962bc2 100644 --- a/crates/mq-lang/src/runtime/debugger.rs +++ b/crates/mq-lang/src/runtime/debugger.rs @@ -7,6 +7,7 @@ use crate::{ModuleId, Range, Shared, SharedCell}; use std::{collections::HashSet, fmt::Debug}; +/// How execution proceeds after a stop. #[derive(Debug, Clone, Copy, Eq, PartialEq, Default, Hash)] pub enum DebuggerCommand { /// Continue normal execution. @@ -48,9 +49,12 @@ pub struct Breakpoint { pub log_message: Option, } +/// A named piece of source code. #[derive(Debug, Clone, Default)] pub struct Source { + /// File name, if any. pub name: Option, + /// The source text. pub code: String, } @@ -359,14 +363,17 @@ impl Debugger { id } + /// Pushes a call node onto the call stack. pub fn push_call_stack(&mut self, node: Shared) { self.call_stack.push(node); } + /// Pops the innermost call node. pub fn pop_call_stack(&mut self) { self.call_stack.pop(); } + /// A copy of the current call stack, outermost first. pub fn current_call_stack(&self) -> Vec> { self.call_stack.clone() } @@ -434,6 +441,7 @@ impl Debugger { self.find_active_breakpoint(line, column, &context.source) } + /// Applies `next_action` and sets the command it implies. pub fn next(&mut self, next_action: DebuggerAction) { self.handle_debugger_action(next_action.clone()); self.current_command = next_action.clone().into(); @@ -630,8 +638,10 @@ impl From for DebuggerCommand { } } +/// Receives debugger events during execution. pub trait DebuggerHandler: std::fmt::Debug + Send + Sync { // Called when a breakpoint is hit. + /// Called when a breakpoint is hit. fn on_breakpoint_hit(&self, _breakpoint: &Breakpoint, _context: &DebugContext) -> DebuggerAction { // Default behavior: continue execution DebuggerAction::Continue diff --git a/crates/mq-lang/src/runtime/host.rs b/crates/mq-lang/src/runtime/host.rs index 9b045faa8..6bb20aecf 100644 --- a/crates/mq-lang/src/runtime/host.rs +++ b/crates/mq-lang/src/runtime/host.rs @@ -88,6 +88,7 @@ pub(crate) fn panic_message(payload: &(dyn std::any::Any + Send)) -> String { /// (which back [`Shared`] depending on the `sync` feature), so an explicit method keeps /// invocation through `Shared` unambiguous. pub trait HostFunction: HostFunctionSyncBound + 'static { + /// Invokes the function with evaluated arguments. fn call(&self, args: &[RuntimeValue]) -> HostFnResult; } diff --git a/crates/mq-lang/src/runtime/host/adapter.rs b/crates/mq-lang/src/runtime/host/adapter.rs index c5634bb83..b2218c6d2 100644 --- a/crates/mq-lang/src/runtime/host/adapter.rs +++ b/crates/mq-lang/src/runtime/host/adapter.rs @@ -119,6 +119,7 @@ impl ValueAdapter for Option { /// each typed arity below so a single closure can only ever match one implementation — hosts /// never need to name `Marker` or this trait themselves. pub trait IntoHostFunction { + /// Boxes the closure as a host function. fn into_host_fn(self) -> Shared; } diff --git a/crates/mq-lang/src/runtime/runtime_value.rs b/crates/mq-lang/src/runtime/runtime_value.rs index b9f5873d0..d5e7ed336 100644 --- a/crates/mq-lang/src/runtime/runtime_value.rs +++ b/crates/mq-lang/src/runtime/runtime_value.rs @@ -819,6 +819,7 @@ impl RuntimeValue { } } + /// Converts the value to a JSON value; unrepresentable values become null. pub fn to_json_value(self) -> serde_json::Value { use base64::Engine; match self { @@ -858,6 +859,7 @@ impl RuntimeValue { } } + /// Converts the value to a CBOR value; unrepresentable values become null. pub fn to_cbor_value(self) -> ciborium::Value { match self { RuntimeValue::None => ciborium::Value::Null, diff --git a/crates/mq-lang/src/tarn/disasm.rs b/crates/mq-lang/src/tarn/disasm.rs index 1b8ca1522..cad8f6c22 100644 --- a/crates/mq-lang/src/tarn/disasm.rs +++ b/crates/mq-lang/src/tarn/disasm.rs @@ -20,6 +20,7 @@ pub struct BytecodeDump { /// The bytecode of one phase. #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct BytecodePhase { + /// Phase name. pub name: String, /// Chunk 0 is the entry point. pub chunks: Vec, @@ -28,6 +29,7 @@ pub struct BytecodePhase { /// A function body or the entry point. #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct BytecodeChunk { + /// Position of the chunk in its phase. pub index: usize, /// Includes the implicit `self` slot. pub local_count: usize, @@ -35,6 +37,7 @@ pub struct BytecodeChunk { pub locals: Vec, /// Upvalue names by index. pub upvalues: Vec, + /// Instructions in execution order. pub instructions: Vec, /// Rendered as text and truncated when long. pub constants: Vec, @@ -43,18 +46,23 @@ pub struct BytecodeChunk { /// One VM instruction. #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct BytecodeInstruction { + /// Instruction index within the chunk. pub pc: usize, + /// Opcode name. pub opcode: String, /// Empty when the opcode has none. #[serde(skip_serializing_if = "String::is_empty")] pub operands: String, + /// Source position the instruction came from, if known. pub location: Option, } /// A one-based source position. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] pub struct BytecodeLocation { + /// One-based line. pub line: usize, + /// One-based column. pub column: usize, } diff --git a/crates/mq-lang/tests/integration_tests.rs b/crates/mq-lang/tests/integration_tests.rs index 25bc620b6..45db72632 100644 --- a/crates/mq-lang/tests/integration_tests.rs +++ b/crates/mq-lang/tests/integration_tests.rs @@ -1,3 +1,5 @@ +//! Integration tests for the mq-lang engine. + use mq_lang::{DefaultEngine, DictMap, Engine, Ident, MqResult, RuntimeValue, Shared}; use rstest::{fixture, rstest}; diff --git a/crates/mq-lint/Cargo.toml b/crates/mq-lint/Cargo.toml index 9301fac66..12378bfec 100644 --- a/crates/mq-lint/Cargo.toml +++ b/crates/mq-lint/Cargo.toml @@ -24,6 +24,9 @@ pkg-fmt = "bin" pkg-url = "{ repo }/releases/download/v{ version }/{ bin }-{ target }{ binary-ext }" disabled-strategies = ["quick-install"] +[lints] +workspace = true + [dependencies] clap = {workspace = true, features = ["derive"], optional = true} colored = {workspace = true, optional = true} diff --git a/crates/mq-lint/src/config.rs b/crates/mq-lint/src/config.rs index e39e07302..90f47565b 100644 --- a/crates/mq-lint/src/config.rs +++ b/crates/mq-lint/src/config.rs @@ -7,6 +7,7 @@ use crate::RuleId; /// Per-rule enable/disable flag. #[derive(Debug, Clone)] pub struct RuleConfig { + /// Whether the rule runs. pub enabled: bool, } @@ -48,6 +49,7 @@ impl Default for ComplexityThresholds { pub struct LintConfig { /// Per-rule overrides. Rules not listed here use their default enabled state. pub rules: HashMap, + /// Thresholds for the complexity rules. pub complexity: ComplexityThresholds, } diff --git a/crates/mq-lint/src/fix.rs b/crates/mq-lint/src/fix.rs index 72d2644e8..36e971e97 100644 --- a/crates/mq-lint/src/fix.rs +++ b/crates/mq-lint/src/fix.rs @@ -29,9 +29,12 @@ impl Core { /// resolved core text + `suffix`. #[derive(Debug, Clone, PartialEq)] pub struct Fix { + /// Source range to replace. pub range: mq_lang::Range, core: Core, + /// Text inserted before the resolved core text. pub prefix: String, + /// Text inserted after the resolved core text. pub suffix: String, } @@ -68,11 +71,13 @@ impl Fix { } } + /// Sets the text inserted before the replacement. pub fn with_prefix(mut self, prefix: impl Into) -> Self { self.prefix = prefix.into(); self } + /// Sets the text inserted after the replacement. pub fn with_suffix(mut self, suffix: impl Into) -> Self { self.suffix = suffix.into(); self diff --git a/crates/mq-lint/src/lib.rs b/crates/mq-lint/src/lib.rs index 8744338ac..c80476a27 100644 --- a/crates/mq-lint/src/lib.rs +++ b/crates/mq-lint/src/lib.rs @@ -58,6 +58,7 @@ impl std::fmt::Display for Severity { pub struct Diagnostic { /// The message data that identifies the rule and renders its text. pub kind: LintMessage, + /// How serious the finding is. pub severity: Severity, /// Source location of the finding, if available. pub range: Option, @@ -66,6 +67,7 @@ pub struct Diagnostic { } impl Diagnostic { + /// Creates a diagnostic without a range or fix. pub fn new(kind: LintMessage, severity: Severity) -> Self { Self { kind, @@ -75,11 +77,13 @@ impl Diagnostic { } } + /// Sets the source range. pub fn with_range(mut self, range: mq_lang::Range) -> Self { self.range = Some(range); self } + /// Attaches a suggested fix. pub fn with_fix(mut self, fix: Fix) -> Self { self.fix = Some(fix); self @@ -103,12 +107,16 @@ impl Diagnostic { /// Context passed to each [`LintRule`] during analysis. pub struct LintContext<'a> { + /// The HIR of the analyzed program. pub hir: &'a Hir, + /// The source being linted. pub source_id: SourceId, + /// Linter configuration. pub config: &'a LintConfig, } impl<'a> LintContext<'a> { + /// Creates a context for linting `source_id`. pub fn new(hir: &'a Hir, source_id: SourceId, config: &'a LintConfig) -> Self { Self { hir, source_id, config } } @@ -174,6 +182,7 @@ impl Linter { } } + /// Runs every enabled rule and collects their diagnostics. pub fn run(&self, ctx: &LintContext<'_>) -> Vec { self.rules .iter() diff --git a/crates/mq-lint/src/main.rs b/crates/mq-lint/src/main.rs index 3c6078db0..151bc3a44 100644 --- a/crates/mq-lint/src/main.rs +++ b/crates/mq-lint/src/main.rs @@ -1,3 +1,5 @@ +//! Command-line interface of the mq linter. + mod format; use std::io::{self, BufWriter, Read, Write}; diff --git a/crates/mq-lint/src/message.rs b/crates/mq-lint/src/message.rs index 932c960a4..6b41d95fa 100644 --- a/crates/mq-lint/src/message.rs +++ b/crates/mq-lint/src/message.rs @@ -13,39 +13,73 @@ use std::str::FromStr; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] #[non_exhaustive] pub enum RuleId { + /// A variable that is never used. UnusedVariable, + /// A function that is never called. UnusedFunction, + /// An imported module that is never used. UnusedImport, + /// Code after a `break` or `continue`. UnreachableCode, + /// A loop without a `break`. InfiniteLoop, + /// A call to a deprecated function. DeprecatedFunctionCall, + /// A match arm with a pattern already matched earlier. DuplicateMatchArm, + /// A module imported more than once. DuplicateImport, + /// A variable that shadows one from an outer scope. ShadowVariable, + /// An `if` expression without an `else` branch. MissingElseInExpr, + /// An `if` condition that is a constant. AlwaysTrueCondition, + /// A function body longer than the configured line limit. FunctionTooLong, + /// A function with more parameters than the configured limit. TooManyParams, + /// Nesting deeper than the configured limit. DeeplyNested, + /// A match with more arms than the configured limit. TooManyMatchArms, + /// An interpolated string with more expressions than the configured limit. ComplexInterpolation, + /// A `..` followed by a specific selector. InefficientSelector, + /// A recursive `..` selector without a depth limit. MissingDepthGuard, + /// Two selectors piped together that can never both match. SelectorAlwaysEmpty, + /// A module without a doc comment. MissingModuleDoc, + /// A function name also defined in another module. AmbiguousQualifiedAccess, + /// A `var` that is never reassigned. PreferLetOverVar, + /// A nested call that reads better as a pipe. PreferPipeStyle, + /// An `if`/`else` null check expressible with `??`. PreferCoalesce, + /// The generic `.h` selector used instead of `.h1` to `.h6`. PreferSpecificHeading, + /// `try ... catch: none`, which is what `?` does. RedundantTry, + /// A name that is not `snake_case`. NamingConvention, + /// A comparison with a boolean literal. BooleanComparison, + /// An `if`/`else` that only returns boolean literals. RedundantBooleanLiteral, + /// An interpolated string containing a single expression. UnnecessaryInterpolation, + /// A function parameter that is never used. UnusedParameter, + /// Concatenation of string literals. ConstantStringConcat, + /// An `if`/`else` with a negated condition. NegatedCondition, + /// A call to a capability-gated function. DangerousCapabilityCall, } @@ -152,100 +186,172 @@ impl FromStr for RuleId { #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] pub enum LintMessage { + /// A variable that is never used. UnusedVariable { + /// Name of the offending item. name: String, }, + /// A function that is never called. UnusedFunction { + /// Name of the offending item. name: String, }, + /// An imported module that is never used. UnusedImport { + /// Name of the offending item. name: String, }, + /// Code after a `break` or `continue`. UnreachableCode { + /// The `break` or `continue` keyword. keyword: String, }, + /// A loop without a `break`. InfiniteLoop, + /// A match arm with a pattern already matched earlier. DuplicateMatchArm { + /// The duplicated pattern text. pattern: String, }, + /// A module imported more than once. DuplicateImport { + /// Name of the offending item. name: String, }, + /// A call to a deprecated function. DeprecatedFunctionCall { + /// Name of the offending item. name: String, }, + /// A variable that shadows one from an outer scope. ShadowVariable { + /// Name of the offending item. name: String, }, + /// An `if` expression without an `else` branch. MissingElseInExpr, + /// An `if` condition that is a constant. AlwaysTrueCondition { + /// The constant condition value. value: String, }, + /// A function body longer than the configured line limit. FunctionTooLong { + /// Name of the offending item. name: String, + /// Actual line count. line_count: usize, + /// Configured limit. max_lines: usize, }, + /// A function with more parameters than the configured limit. TooManyParams { + /// Name of the offending item. name: String, + /// Actual parameter count. count: usize, + /// Configured limit. max: usize, }, + /// Nesting deeper than the configured limit. DeeplyNested { + /// Actual nesting depth. depth: usize, + /// Configured limit. max_depth: usize, }, + /// A match with more arms than the configured limit. TooManyMatchArms { + /// Actual arm count. arm_count: usize, + /// Configured limit. max_arms: usize, }, + /// An interpolated string with more expressions than the configured limit. ComplexInterpolation { + /// Actual expression count. expr_count: usize, + /// Configured limit. max_exprs: usize, }, + /// A `..` followed by a specific selector. InefficientSelector, + /// A recursive `..` selector without a depth limit. MissingDepthGuard, + /// Two selectors piped together that can never both match. SelectorAlwaysEmpty { + /// The first selector. first: String, + /// The second selector. second: String, }, + /// A module without a doc comment. MissingModuleDoc { + /// Name of the offending item. name: String, }, + /// A function name also defined in another module. AmbiguousQualifiedAccess { + /// The ambiguous function name. fn_name: String, + /// The module the call is in. this_module: String, + /// The other module defining the function. other_module: String, }, + /// A `var` that is never reassigned. PreferLetOverVar { + /// Name of the offending item. name: String, }, + /// A nested call that reads better as a pipe. PreferPipeStyle { + /// Name of the outer call. outer_name: String, + /// Name of the inner call. inner_name: String, }, + /// An `if`/`else` null check expressible with `??`. PreferCoalesce, + /// The generic `.h` selector used instead of `.h1` to `.h6`. PreferSpecificHeading, + /// `try ... catch: none`, which is what `?` does. RedundantTry, + /// A name that is not `snake_case`. NamingConvention { + /// Name of the offending item. name: String, + /// The suggested snake_case name. suggested: String, }, + /// A comparison with a boolean literal. BooleanComparison { + /// The comparison operator. op: String, + /// The boolean literal compared against. bool_val: String, }, + /// An `if`/`else` that only returns boolean literals. RedundantBooleanLiteral { + /// The literal returned by the `then` branch. then_val: String, }, + /// An interpolated string containing a single expression. UnnecessaryInterpolation, + /// A function parameter that is never used. UnusedParameter { + /// Name of the offending item. name: String, }, + /// Concatenation of string literals. ConstantStringConcat, + /// An `if`/`else` with a negated condition. NegatedCondition, + /// A call to a capability-gated function. DangerousCapabilityCall { + /// Name of the offending item. name: String, + /// The CLI flag that enables the function. flag: String, }, } diff --git a/crates/mq-lint/src/rules.rs b/crates/mq-lint/src/rules.rs index 7dae71094..a399733a8 100644 --- a/crates/mq-lint/src/rules.rs +++ b/crates/mq-lint/src/rules.rs @@ -1,10 +1,16 @@ //! All lint rules, organized by category. +/// The `complexity` rules. pub mod complexity; +/// The `correctness` rules. pub mod correctness; +/// The `module` rules. pub mod module; +/// The `security` rules. pub mod security; +/// The `selector` rules. pub mod selector; +/// The `style` rules. pub mod style; use crate::LintRule; diff --git a/crates/mq-lint/src/rules/complexity.rs b/crates/mq-lint/src/rules/complexity.rs index ce3039b4c..87cc370d8 100644 --- a/crates/mq-lint/src/rules/complexity.rs +++ b/crates/mq-lint/src/rules/complexity.rs @@ -1,11 +1,17 @@ +/// The `complex_interpolation` rule. pub mod complex_interpolation; +/// The `deeply_nested` rule. pub mod deeply_nested; +/// The `function_too_long` rule. pub mod function_too_long; +/// The `too_many_match_arms` rule. pub mod too_many_match_arms; +/// The `too_many_params` rule. pub mod too_many_params; use crate::LintRule; +/// Returns the rules of this category. pub fn all() -> Vec> { vec![ Box::new(function_too_long::FunctionTooLong), diff --git a/crates/mq-lint/src/rules/complexity/complex_interpolation.rs b/crates/mq-lint/src/rules/complexity/complex_interpolation.rs index 9ebeeb96e..f55b72d25 100644 --- a/crates/mq-lint/src/rules/complexity/complex_interpolation.rs +++ b/crates/mq-lint/src/rules/complexity/complex_interpolation.rs @@ -1,6 +1,7 @@ use crate::{Diagnostic, LintContext, LintMessage, LintRule, RuleId, Severity}; use mq_hir::SymbolKind; +/// Flags: an interpolated string with more expressions than the configured limit. pub struct ComplexInterpolation; impl LintRule for ComplexInterpolation { diff --git a/crates/mq-lint/src/rules/complexity/deeply_nested.rs b/crates/mq-lint/src/rules/complexity/deeply_nested.rs index 36ea682f7..bc9990e7a 100644 --- a/crates/mq-lint/src/rules/complexity/deeply_nested.rs +++ b/crates/mq-lint/src/rules/complexity/deeply_nested.rs @@ -1,6 +1,7 @@ use crate::{Diagnostic, LintContext, LintMessage, LintRule, RuleId, Severity}; use mq_hir::{ScopeId, ScopeKind}; +/// Flags: nesting deeper than the configured limit. pub struct DeeplyNested; impl LintRule for DeeplyNested { diff --git a/crates/mq-lint/src/rules/complexity/function_too_long.rs b/crates/mq-lint/src/rules/complexity/function_too_long.rs index 084840670..e27630a70 100644 --- a/crates/mq-lint/src/rules/complexity/function_too_long.rs +++ b/crates/mq-lint/src/rules/complexity/function_too_long.rs @@ -1,5 +1,6 @@ use crate::{Diagnostic, LintContext, LintMessage, LintRule, RuleId, Severity}; +/// Flags: a function body longer than the configured line limit. pub struct FunctionTooLong; impl LintRule for FunctionTooLong { diff --git a/crates/mq-lint/src/rules/complexity/too_many_match_arms.rs b/crates/mq-lint/src/rules/complexity/too_many_match_arms.rs index 33931eda8..995731346 100644 --- a/crates/mq-lint/src/rules/complexity/too_many_match_arms.rs +++ b/crates/mq-lint/src/rules/complexity/too_many_match_arms.rs @@ -1,6 +1,7 @@ use crate::{Diagnostic, LintContext, LintMessage, LintRule, RuleId, Severity}; use mq_hir::SymbolKind; +/// Flags: a match with more arms than the configured limit. pub struct TooManyMatchArms; impl LintRule for TooManyMatchArms { diff --git a/crates/mq-lint/src/rules/complexity/too_many_params.rs b/crates/mq-lint/src/rules/complexity/too_many_params.rs index 026d44491..80bf4a484 100644 --- a/crates/mq-lint/src/rules/complexity/too_many_params.rs +++ b/crates/mq-lint/src/rules/complexity/too_many_params.rs @@ -1,6 +1,7 @@ use crate::{Diagnostic, LintContext, LintMessage, LintRule, RuleId, Severity}; use mq_hir::SymbolKind; +/// Flags: a function with more parameters than the configured limit. pub struct TooManyParams; impl LintRule for TooManyParams { diff --git a/crates/mq-lint/src/rules/correctness.rs b/crates/mq-lint/src/rules/correctness.rs index 2dbed32c7..5f564ec1c 100644 --- a/crates/mq-lint/src/rules/correctness.rs +++ b/crates/mq-lint/src/rules/correctness.rs @@ -1,18 +1,31 @@ +/// The `always_true_condition` rule. pub mod always_true_condition; +/// The `deprecated_function_call` rule. pub mod deprecated_function_call; +/// The `duplicate_import` rule. pub mod duplicate_import; +/// The `duplicate_match_arm` rule. pub mod duplicate_match_arm; +/// The `infinite_loop` rule. pub mod infinite_loop; +/// The `missing_else_in_expr` rule. pub mod missing_else_in_expr; +/// The `shadow_variable` rule. pub mod shadow_variable; +/// The `unreachable_code` rule. pub mod unreachable_code; +/// The `unused_function` rule. pub mod unused_function; +/// The `unused_import` rule. pub mod unused_import; +/// The `unused_parameter` rule. pub mod unused_parameter; +/// The `unused_variable` rule. pub mod unused_variable; use crate::LintRule; +/// Returns the rules of this category. pub fn all() -> Vec> { vec![ Box::new(unused_variable::UnusedVariable), diff --git a/crates/mq-lint/src/rules/correctness/always_true_condition.rs b/crates/mq-lint/src/rules/correctness/always_true_condition.rs index d067fbc25..2a4034991 100644 --- a/crates/mq-lint/src/rules/correctness/always_true_condition.rs +++ b/crates/mq-lint/src/rules/correctness/always_true_condition.rs @@ -1,6 +1,7 @@ use crate::{Diagnostic, LintContext, LintMessage, LintRule, RuleId, Severity}; use mq_hir::SymbolKind; +/// Flags: an `if` condition that is a constant. pub struct AlwaysTrueCondition; impl LintRule for AlwaysTrueCondition { diff --git a/crates/mq-lint/src/rules/correctness/deprecated_function_call.rs b/crates/mq-lint/src/rules/correctness/deprecated_function_call.rs index aeca8cf87..da0f06909 100644 --- a/crates/mq-lint/src/rules/correctness/deprecated_function_call.rs +++ b/crates/mq-lint/src/rules/correctness/deprecated_function_call.rs @@ -2,6 +2,7 @@ use crate::{Diagnostic, LintContext, LintMessage, LintRule, RuleId, Severity}; use mq_hir::SymbolKind; use rustc_hash::FxHashSet; +/// Flags: a call to a deprecated function. pub struct DeprecatedFunctionCall; impl LintRule for DeprecatedFunctionCall { diff --git a/crates/mq-lint/src/rules/correctness/duplicate_import.rs b/crates/mq-lint/src/rules/correctness/duplicate_import.rs index 4d60e51e7..23d3da935 100644 --- a/crates/mq-lint/src/rules/correctness/duplicate_import.rs +++ b/crates/mq-lint/src/rules/correctness/duplicate_import.rs @@ -1,6 +1,7 @@ use crate::{Diagnostic, LintContext, LintMessage, LintRule, RuleId, Severity}; use rustc_hash::FxHashMap; +/// Flags: a module imported more than once. pub struct DuplicateImport; impl LintRule for DuplicateImport { diff --git a/crates/mq-lint/src/rules/correctness/duplicate_match_arm.rs b/crates/mq-lint/src/rules/correctness/duplicate_match_arm.rs index dcafffe58..1d06a4ec4 100644 --- a/crates/mq-lint/src/rules/correctness/duplicate_match_arm.rs +++ b/crates/mq-lint/src/rules/correctness/duplicate_match_arm.rs @@ -3,6 +3,7 @@ use rustc_hash::FxHashSet; use crate::{Diagnostic, LintContext, LintMessage, LintRule, RuleId, Severity}; use mq_hir::SymbolKind; +/// Flags: a match arm with a pattern already matched earlier. pub struct DuplicateMatchArm; impl LintRule for DuplicateMatchArm { diff --git a/crates/mq-lint/src/rules/correctness/infinite_loop.rs b/crates/mq-lint/src/rules/correctness/infinite_loop.rs index e2d053d5c..e542d4774 100644 --- a/crates/mq-lint/src/rules/correctness/infinite_loop.rs +++ b/crates/mq-lint/src/rules/correctness/infinite_loop.rs @@ -2,6 +2,7 @@ use crate::{Diagnostic, LintContext, LintMessage, LintRule, RuleId, Severity}; use mq_hir::SymbolId; use mq_hir::SymbolKind; +/// Flags: a loop without a `break`. pub struct InfiniteLoop; impl LintRule for InfiniteLoop { diff --git a/crates/mq-lint/src/rules/correctness/missing_else_in_expr.rs b/crates/mq-lint/src/rules/correctness/missing_else_in_expr.rs index c373e7e41..2e037dae4 100644 --- a/crates/mq-lint/src/rules/correctness/missing_else_in_expr.rs +++ b/crates/mq-lint/src/rules/correctness/missing_else_in_expr.rs @@ -1,6 +1,7 @@ use crate::{Diagnostic, LintContext, LintMessage, LintRule, RuleId, Severity}; use mq_hir::SymbolKind; +/// Flags: an `if` expression without an `else` branch. pub struct MissingElseInExpr; impl LintRule for MissingElseInExpr { diff --git a/crates/mq-lint/src/rules/correctness/shadow_variable.rs b/crates/mq-lint/src/rules/correctness/shadow_variable.rs index 45356f891..67a1a2cb4 100644 --- a/crates/mq-lint/src/rules/correctness/shadow_variable.rs +++ b/crates/mq-lint/src/rules/correctness/shadow_variable.rs @@ -1,6 +1,7 @@ use crate::{Diagnostic, LintContext, LintMessage, LintRule, RuleId, Severity}; use mq_hir::{ScopeId, SymbolKind}; +/// Flags: a variable that shadows one from an outer scope. pub struct ShadowVariable; impl LintRule for ShadowVariable { diff --git a/crates/mq-lint/src/rules/correctness/unreachable_code.rs b/crates/mq-lint/src/rules/correctness/unreachable_code.rs index 57ac6526a..b04865e69 100644 --- a/crates/mq-lint/src/rules/correctness/unreachable_code.rs +++ b/crates/mq-lint/src/rules/correctness/unreachable_code.rs @@ -1,6 +1,7 @@ use crate::{Diagnostic, LintContext, LintMessage, LintRule, RuleId, Severity}; use mq_hir::SymbolKind; +/// Flags: code after a `break` or `continue`. pub struct UnreachableCode; impl LintRule for UnreachableCode { diff --git a/crates/mq-lint/src/rules/correctness/unused_function.rs b/crates/mq-lint/src/rules/correctness/unused_function.rs index 48f40e257..269135cb4 100644 --- a/crates/mq-lint/src/rules/correctness/unused_function.rs +++ b/crates/mq-lint/src/rules/correctness/unused_function.rs @@ -3,6 +3,7 @@ use rustc_hash::FxHashSet; use crate::{Diagnostic, LintContext, LintMessage, LintRule, RuleId, Severity}; use mq_hir::SymbolKind; +/// Flags: a function that is never called. pub struct UnusedFunction; impl LintRule for UnusedFunction { diff --git a/crates/mq-lint/src/rules/correctness/unused_import.rs b/crates/mq-lint/src/rules/correctness/unused_import.rs index 70627b6f9..2f9415e6e 100644 --- a/crates/mq-lint/src/rules/correctness/unused_import.rs +++ b/crates/mq-lint/src/rules/correctness/unused_import.rs @@ -1,6 +1,7 @@ use crate::{Diagnostic, LintContext, LintMessage, LintRule, RuleId, Severity}; use mq_hir::SymbolKind; +/// Flags: an imported module that is never used. pub struct UnusedImport; impl LintRule for UnusedImport { diff --git a/crates/mq-lint/src/rules/correctness/unused_parameter.rs b/crates/mq-lint/src/rules/correctness/unused_parameter.rs index 5bc48a6fe..045a05c16 100644 --- a/crates/mq-lint/src/rules/correctness/unused_parameter.rs +++ b/crates/mq-lint/src/rules/correctness/unused_parameter.rs @@ -3,6 +3,7 @@ use rustc_hash::FxHashSet; use crate::{Diagnostic, Fix, LintContext, LintMessage, LintRule, RuleId, Severity}; use mq_hir::{ScopeId, ScopeKind, SymbolId, SymbolKind}; +/// Flags: a function parameter that is never used. pub struct UnusedParameter; fn is_in_scope_subtree(ctx: &LintContext<'_>, scope_id: ScopeId, target_scope_id: ScopeId) -> bool { diff --git a/crates/mq-lint/src/rules/correctness/unused_variable.rs b/crates/mq-lint/src/rules/correctness/unused_variable.rs index 4d06223d5..19db7526c 100644 --- a/crates/mq-lint/src/rules/correctness/unused_variable.rs +++ b/crates/mq-lint/src/rules/correctness/unused_variable.rs @@ -3,6 +3,7 @@ use rustc_hash::FxHashSet; use crate::{Diagnostic, Fix, LintContext, LintMessage, LintRule, RuleId, Severity}; use mq_hir::SymbolKind; +/// Flags: a variable that is never used. pub struct UnusedVariable; /// True if `sym` is a `${name}` interpolation embed rather than an actual `let`/`var` declaration diff --git a/crates/mq-lint/src/rules/module.rs b/crates/mq-lint/src/rules/module.rs index 71d8b1381..98c7586d4 100644 --- a/crates/mq-lint/src/rules/module.rs +++ b/crates/mq-lint/src/rules/module.rs @@ -1,8 +1,11 @@ +/// The `ambiguous_qualified_access` rule. pub mod ambiguous_qualified_access; +/// The `missing_module_doc` rule. pub mod missing_module_doc; use crate::LintRule; +/// Returns the rules of this category. pub fn all() -> Vec> { vec![ Box::new(missing_module_doc::MissingModuleDoc), diff --git a/crates/mq-lint/src/rules/module/ambiguous_qualified_access.rs b/crates/mq-lint/src/rules/module/ambiguous_qualified_access.rs index fda85df19..4b0d53925 100644 --- a/crates/mq-lint/src/rules/module/ambiguous_qualified_access.rs +++ b/crates/mq-lint/src/rules/module/ambiguous_qualified_access.rs @@ -3,6 +3,7 @@ use rustc_hash::FxHashMap; use crate::{Diagnostic, LintContext, LintMessage, LintRule, RuleId, Severity}; use mq_hir::SymbolKind; +/// Flags: a function name also defined in another module. pub struct AmbiguousQualifiedAccess; impl LintRule for AmbiguousQualifiedAccess { diff --git a/crates/mq-lint/src/rules/module/missing_module_doc.rs b/crates/mq-lint/src/rules/module/missing_module_doc.rs index 925dfba02..518f885aa 100644 --- a/crates/mq-lint/src/rules/module/missing_module_doc.rs +++ b/crates/mq-lint/src/rules/module/missing_module_doc.rs @@ -1,6 +1,7 @@ use crate::{Diagnostic, LintContext, LintMessage, LintRule, RuleId, Severity}; use mq_hir::SymbolKind; +/// Flags: a module without a doc comment. pub struct MissingModuleDoc; impl LintRule for MissingModuleDoc { diff --git a/crates/mq-lint/src/rules/security.rs b/crates/mq-lint/src/rules/security.rs index 19fe37bab..6e4707771 100644 --- a/crates/mq-lint/src/rules/security.rs +++ b/crates/mq-lint/src/rules/security.rs @@ -1,7 +1,9 @@ +/// The `dangerous_capability_call` rule. pub mod dangerous_capability_call; use crate::LintRule; +/// Returns the rules of this category. pub fn all() -> Vec> { vec![Box::new(dangerous_capability_call::DangerousCapabilityCall)] } diff --git a/crates/mq-lint/src/rules/security/dangerous_capability_call.rs b/crates/mq-lint/src/rules/security/dangerous_capability_call.rs index 6c6c4c3fd..e0fccdb57 100644 --- a/crates/mq-lint/src/rules/security/dangerous_capability_call.rs +++ b/crates/mq-lint/src/rules/security/dangerous_capability_call.rs @@ -18,6 +18,7 @@ fn capability_flag(name: &str) -> Option<&'static str> { } } +/// Flags: a call to a capability-gated function. pub struct DangerousCapabilityCall; impl LintRule for DangerousCapabilityCall { diff --git a/crates/mq-lint/src/rules/selector.rs b/crates/mq-lint/src/rules/selector.rs index c28b0b1ed..812194463 100644 --- a/crates/mq-lint/src/rules/selector.rs +++ b/crates/mq-lint/src/rules/selector.rs @@ -1,9 +1,13 @@ +/// The `inefficient_selector` rule. pub mod inefficient_selector; +/// The `missing_depth_guard` rule. pub mod missing_depth_guard; +/// The `selector_always_empty` rule. pub mod selector_always_empty; use crate::LintRule; +/// Returns the rules of this category. pub fn all() -> Vec> { vec![ Box::new(inefficient_selector::InefficientSelector), diff --git a/crates/mq-lint/src/rules/selector/inefficient_selector.rs b/crates/mq-lint/src/rules/selector/inefficient_selector.rs index fb80d1ebb..60e2e93e9 100644 --- a/crates/mq-lint/src/rules/selector/inefficient_selector.rs +++ b/crates/mq-lint/src/rules/selector/inefficient_selector.rs @@ -1,6 +1,7 @@ use crate::{Diagnostic, LintContext, LintMessage, LintRule, RuleId, Severity}; use mq_hir::SymbolKind; +/// Flags: a `..` followed by a specific selector. pub struct InefficientSelector; impl LintRule for InefficientSelector { diff --git a/crates/mq-lint/src/rules/selector/missing_depth_guard.rs b/crates/mq-lint/src/rules/selector/missing_depth_guard.rs index 5393bc78d..33ae93f58 100644 --- a/crates/mq-lint/src/rules/selector/missing_depth_guard.rs +++ b/crates/mq-lint/src/rules/selector/missing_depth_guard.rs @@ -1,6 +1,7 @@ use crate::{Diagnostic, LintContext, LintMessage, LintRule, RuleId, Severity}; use mq_hir::SymbolKind; +/// Flags: a recursive `..` selector without a depth limit. pub struct MissingDepthGuard; impl LintRule for MissingDepthGuard { diff --git a/crates/mq-lint/src/rules/selector/selector_always_empty.rs b/crates/mq-lint/src/rules/selector/selector_always_empty.rs index 13338db23..16df4e172 100644 --- a/crates/mq-lint/src/rules/selector/selector_always_empty.rs +++ b/crates/mq-lint/src/rules/selector/selector_always_empty.rs @@ -3,6 +3,7 @@ use mq_lang::Selector; use crate::{Diagnostic, LintContext, LintMessage, LintRule, RuleId, Severity}; +/// Flags: two selectors piped together that can never both match. pub struct SelectorAlwaysEmpty; /// Conservative: only flags same-variant value mismatches (plus `.todo`/`.done`) diff --git a/crates/mq-lint/src/rules/style.rs b/crates/mq-lint/src/rules/style.rs index 05b5e1178..c2ed9df4f 100644 --- a/crates/mq-lint/src/rules/style.rs +++ b/crates/mq-lint/src/rules/style.rs @@ -1,17 +1,29 @@ +/// The `boolean_comparison` rule. pub mod boolean_comparison; +/// The `constant_string_concat` rule. pub mod constant_string_concat; +/// The `naming_convention` rule. pub mod naming_convention; +/// The `negated_condition` rule. pub mod negated_condition; +/// The `prefer_coalesce` rule. pub mod prefer_coalesce; +/// The `prefer_let_over_var` rule. pub mod prefer_let_over_var; +/// The `prefer_pipe_style` rule. pub mod prefer_pipe_style; +/// The `prefer_specific_heading` rule. pub mod prefer_specific_heading; +/// The `redundant_boolean_literal` rule. pub mod redundant_boolean_literal; +/// The `redundant_try` rule. pub mod redundant_try; +/// The `unnecessary_interpolation` rule. pub mod unnecessary_interpolation; use crate::LintRule; +/// Returns the rules of this category. pub fn all() -> Vec> { vec![ Box::new(prefer_let_over_var::PreferLetOverVar), diff --git a/crates/mq-lint/src/rules/style/boolean_comparison.rs b/crates/mq-lint/src/rules/style/boolean_comparison.rs index e229fff9f..511c9d6b8 100644 --- a/crates/mq-lint/src/rules/style/boolean_comparison.rs +++ b/crates/mq-lint/src/rules/style/boolean_comparison.rs @@ -1,6 +1,7 @@ use crate::{Diagnostic, Fix, LintContext, LintMessage, LintRule, RuleId, Severity}; use mq_hir::SymbolKind; +/// Flags: a comparison with a boolean literal. pub struct BooleanComparison; impl LintRule for BooleanComparison { diff --git a/crates/mq-lint/src/rules/style/constant_string_concat.rs b/crates/mq-lint/src/rules/style/constant_string_concat.rs index 485795a64..7828dd578 100644 --- a/crates/mq-lint/src/rules/style/constant_string_concat.rs +++ b/crates/mq-lint/src/rules/style/constant_string_concat.rs @@ -1,6 +1,7 @@ use crate::{Diagnostic, Fix, LintContext, LintMessage, LintRule, RuleId, Severity}; use mq_hir::SymbolKind; +/// Flags: concatenation of string literals. pub struct ConstantStringConcat; impl LintRule for ConstantStringConcat { diff --git a/crates/mq-lint/src/rules/style/naming_convention.rs b/crates/mq-lint/src/rules/style/naming_convention.rs index a59870547..3a64fee91 100644 --- a/crates/mq-lint/src/rules/style/naming_convention.rs +++ b/crates/mq-lint/src/rules/style/naming_convention.rs @@ -1,6 +1,7 @@ use crate::{Diagnostic, LintContext, LintMessage, LintRule, RuleId, Severity}; use mq_hir::SymbolKind; +/// Flags: a name that is not `snake_case`. pub struct NamingConvention; impl LintRule for NamingConvention { diff --git a/crates/mq-lint/src/rules/style/negated_condition.rs b/crates/mq-lint/src/rules/style/negated_condition.rs index 9a89b904f..7d4c7daed 100644 --- a/crates/mq-lint/src/rules/style/negated_condition.rs +++ b/crates/mq-lint/src/rules/style/negated_condition.rs @@ -3,6 +3,7 @@ use std::collections::HashSet; use crate::{Diagnostic, LintContext, LintMessage, LintRule, RuleId, Severity}; use mq_hir::{SymbolId, SymbolKind}; +/// Flags: an `if`/`else` with a negated condition. pub struct NegatedCondition; impl LintRule for NegatedCondition { diff --git a/crates/mq-lint/src/rules/style/prefer_coalesce.rs b/crates/mq-lint/src/rules/style/prefer_coalesce.rs index c1dffdd02..7fb5fdf7d 100644 --- a/crates/mq-lint/src/rules/style/prefer_coalesce.rs +++ b/crates/mq-lint/src/rules/style/prefer_coalesce.rs @@ -3,6 +3,7 @@ use mq_hir::{Symbol, SymbolId, SymbolKind}; use crate::fix::Core; use crate::{Diagnostic, Fix, LintContext, LintMessage, LintRule, RuleId, Severity}; +/// Flags: an `if`/`else` null check expressible with `??`. pub struct PreferCoalesce; /// True if `id` has no children (e.g. a bare `Ref`/`Selector`, not a call). diff --git a/crates/mq-lint/src/rules/style/prefer_let_over_var.rs b/crates/mq-lint/src/rules/style/prefer_let_over_var.rs index 286215297..aac5ea4af 100644 --- a/crates/mq-lint/src/rules/style/prefer_let_over_var.rs +++ b/crates/mq-lint/src/rules/style/prefer_let_over_var.rs @@ -4,6 +4,7 @@ use crate::{Diagnostic, Fix, LintContext, LintMessage, LintRule, RuleId, Severit use mq_hir::{SymbolId, SymbolKind}; use mq_lang::Range; +/// Flags: a `var` that is never reassigned. pub struct PreferLetOverVar; impl LintRule for PreferLetOverVar { diff --git a/crates/mq-lint/src/rules/style/prefer_pipe_style.rs b/crates/mq-lint/src/rules/style/prefer_pipe_style.rs index 858441c1c..fa55e06c9 100644 --- a/crates/mq-lint/src/rules/style/prefer_pipe_style.rs +++ b/crates/mq-lint/src/rules/style/prefer_pipe_style.rs @@ -2,6 +2,7 @@ use mq_hir::SymbolKind; use crate::{Diagnostic, LintContext, LintMessage, LintRule, RuleId, Severity}; +/// Flags: a nested call that reads better as a pipe. pub struct PreferPipeStyle; impl LintRule for PreferPipeStyle { diff --git a/crates/mq-lint/src/rules/style/prefer_specific_heading.rs b/crates/mq-lint/src/rules/style/prefer_specific_heading.rs index 019e7b6e1..7107a17c6 100644 --- a/crates/mq-lint/src/rules/style/prefer_specific_heading.rs +++ b/crates/mq-lint/src/rules/style/prefer_specific_heading.rs @@ -1,6 +1,7 @@ use crate::{Diagnostic, LintContext, LintMessage, LintRule, RuleId, Severity}; use mq_hir::SymbolKind; +/// Flags: the generic `.h` selector used instead of `.h1` to `.h6`. pub struct PreferSpecificHeading; impl LintRule for PreferSpecificHeading { diff --git a/crates/mq-lint/src/rules/style/redundant_boolean_literal.rs b/crates/mq-lint/src/rules/style/redundant_boolean_literal.rs index ae11bf68c..6a3ed9bcd 100644 --- a/crates/mq-lint/src/rules/style/redundant_boolean_literal.rs +++ b/crates/mq-lint/src/rules/style/redundant_boolean_literal.rs @@ -1,6 +1,7 @@ use crate::{Diagnostic, Fix, LintContext, LintMessage, LintRule, RuleId, Severity}; use mq_hir::{SymbolId, SymbolKind}; +/// Flags: an `if`/`else` that only returns boolean literals. pub struct RedundantBooleanLiteral; impl LintRule for RedundantBooleanLiteral { diff --git a/crates/mq-lint/src/rules/style/redundant_try.rs b/crates/mq-lint/src/rules/style/redundant_try.rs index 7789165d0..6966b0fe1 100644 --- a/crates/mq-lint/src/rules/style/redundant_try.rs +++ b/crates/mq-lint/src/rules/style/redundant_try.rs @@ -2,6 +2,7 @@ use mq_hir::SymbolKind; use crate::{Diagnostic, Fix, LintContext, LintMessage, LintRule, RuleId, Severity}; +/// Flags: `try ... catch: none`, which is what `?` does. pub struct RedundantTry; impl LintRule for RedundantTry { diff --git a/crates/mq-lint/src/rules/style/unnecessary_interpolation.rs b/crates/mq-lint/src/rules/style/unnecessary_interpolation.rs index 6e6458551..22f7c3baf 100644 --- a/crates/mq-lint/src/rules/style/unnecessary_interpolation.rs +++ b/crates/mq-lint/src/rules/style/unnecessary_interpolation.rs @@ -2,6 +2,7 @@ use crate::{Diagnostic, Fix, LintContext, LintMessage, LintRule, RuleId, Severit use mq_hir::SymbolKind; use rustc_hash::FxHashMap; +/// Flags: an interpolated string containing a single expression. pub struct UnnecessaryInterpolation; impl LintRule for UnnecessaryInterpolation { diff --git a/crates/mq-lsp/Cargo.toml b/crates/mq-lsp/Cargo.toml index 228bec822..e8922f2d9 100644 --- a/crates/mq-lsp/Cargo.toml +++ b/crates/mq-lsp/Cargo.toml @@ -11,6 +11,9 @@ readme = "README.md" repository.workspace = true version.workspace = true +[lints] +workspace = true + [dependencies] bimap = {workspace = true} clap = {workspace = true, features = ["derive"]} diff --git a/crates/mq-lsp/src/error.rs b/crates/mq-lsp/src/error.rs index c6e52c02a..e36b71bd3 100644 --- a/crates/mq-lsp/src/error.rs +++ b/crates/mq-lsp/src/error.rs @@ -2,9 +2,13 @@ use tower_lsp_server::ls_types; use tower_lsp_server::ls_types::NumberOrString; #[derive(Debug, Clone)] +/// A diagnostic produced by the language server. pub enum LspError { + /// A syntax error. SyntaxError(mq_lang::Diagnostic), + /// A type checking error. TypeError(mq_check::TypeError), + /// A lint warning. LintWarning(mq_lint::Diagnostic), } diff --git a/crates/mq-lsp/src/lib.rs b/crates/mq-lsp/src/lib.rs index d628f19f0..c99abc3b9 100644 --- a/crates/mq-lsp/src/lib.rs +++ b/crates/mq-lsp/src/lib.rs @@ -19,22 +19,38 @@ //! # Usage //! //! To use this LSP server, you need to integrate it with an LSP client. The server reads from stdin and writes to stdout, making it compatible with various editors and IDEs that support LSP. +/// Server capability advertisement. pub mod capabilities; +/// Quick fixes and refactoring code actions. pub mod code_action; +/// Completion items. pub mod completions; +/// Document symbols. pub mod document_symbol; +/// Error types. pub mod error; +/// Command execution. pub mod execute_command; +/// Folding ranges. pub mod folding_range; +/// Go to definition. pub mod goto_definition; +/// Hover information. pub mod hover; +/// Inlay hints. pub mod inlay_hints; pub mod refactor; +/// Find references. pub mod references; +/// Rename symbols. pub mod rename; +/// Semantic token highlighting. pub mod semantic_tokens; +/// The LSP server. pub mod server; +/// Signature help. pub mod signature_help; +/// Workspace symbols. pub mod workspace_symbol; pub use server::start; diff --git a/crates/mq-lsp/src/main.rs b/crates/mq-lsp/src/main.rs index 179f8212f..db24d5c85 100644 --- a/crates/mq-lsp/src/main.rs +++ b/crates/mq-lsp/src/main.rs @@ -1,25 +1,43 @@ +//! Language server binary. + use std::path::PathBuf; use clap::Parser; use crate::server::LspConfig; +/// Server capability advertisement. pub mod capabilities; +/// Quick fixes and refactoring code actions. pub mod code_action; +/// Completion items. pub mod completions; +/// Document symbols. pub mod document_symbol; +/// Error types. pub mod error; +/// Command execution. pub mod execute_command; +/// Folding ranges. pub mod folding_range; +/// Go to definition. pub mod goto_definition; +/// Hover information. pub mod hover; +/// Inlay hints. pub mod inlay_hints; pub mod refactor; +/// Find references. pub mod references; +/// Rename symbols. pub mod rename; +/// Semantic token highlighting. pub mod semantic_tokens; +/// The LSP server. pub mod server; +/// Signature help. pub mod signature_help; +/// Workspace symbols. pub mod workspace_symbol; #[derive(Parser, Debug)] diff --git a/crates/mq-lsp/src/semantic_tokens.rs b/crates/mq-lsp/src/semantic_tokens.rs index 5658d458f..ca852217c 100644 --- a/crates/mq-lsp/src/semantic_tokens.rs +++ b/crates/mq-lsp/src/semantic_tokens.rs @@ -136,6 +136,7 @@ fn token_modifier(token_modifier: ls_types::SemanticTokenModifier) -> u32 { TOKEN_MODIFIER.iter().position(|t| t == &token_modifier).unwrap() as u32 } +/// Token types advertised to clients; the index is the token type id. pub const TOKEN_TYPE: &[ls_types::SemanticTokenType] = &[ SemanticTokenType::COMMENT, SemanticTokenType::FUNCTION, @@ -151,6 +152,7 @@ pub const TOKEN_TYPE: &[ls_types::SemanticTokenType] = &[ SemanticTokenType::VARIABLE, ]; +/// Token modifiers advertised to clients; the index is the modifier bit. pub const TOKEN_MODIFIER: &[ls_types::SemanticTokenModifier] = &[ SemanticTokenModifier::DEFINITION, SemanticTokenModifier::DEFAULT_LIBRARY, diff --git a/crates/mq-lsp/src/server.rs b/crates/mq-lsp/src/server.rs index 62b6db0a8..e543e4e31 100644 --- a/crates/mq-lsp/src/server.rs +++ b/crates/mq-lsp/src/server.rs @@ -632,6 +632,7 @@ impl Backend { } #[derive(Debug, Clone, Default)] +/// Language server settings. pub struct LspConfig { module_paths: Vec, enable_type_checking: bool, @@ -668,6 +669,7 @@ impl LspConfig { } } +/// Runs the language server on stdio. pub async fn start(config: LspConfig) { let stdin = tokio::io::stdin(); let stdout = tokio::io::stdout(); diff --git a/crates/mq-macros/Cargo.toml b/crates/mq-macros/Cargo.toml index b7e4a58c7..e19e99425 100644 --- a/crates/mq-macros/Cargo.toml +++ b/crates/mq-macros/Cargo.toml @@ -11,6 +11,9 @@ repository.workspace = true [lib] proc-macro = true +[lints] +workspace = true + [dependencies] proc-macro2 = "1" quote = "1" diff --git a/crates/mq-macros/src/lib.rs b/crates/mq-macros/src/lib.rs index 957d6b46e..fce47089a 100644 --- a/crates/mq-macros/src/lib.rs +++ b/crates/mq-macros/src/lib.rs @@ -1,3 +1,5 @@ +//! Procedural macros for registering native mq builtins. + use proc_macro::TokenStream; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::quote; diff --git a/crates/mq-markdown/Cargo.toml b/crates/mq-markdown/Cargo.toml index b00ee8c68..a3fccf2ce 100644 --- a/crates/mq-markdown/Cargo.toml +++ b/crates/mq-markdown/Cargo.toml @@ -11,6 +11,9 @@ readme = "../../README.md" repository.workspace = true version.workspace = true +[lints] +workspace = true + [dependencies] ego-tree = {workspace = true, optional = true} itertools = {workspace = true} diff --git a/crates/mq-markdown/benches/benchmark.rs b/crates/mq-markdown/benches/benchmark.rs index a1429328a..4b830b583 100644 --- a/crates/mq-markdown/benches/benchmark.rs +++ b/crates/mq-markdown/benches/benchmark.rs @@ -1,3 +1,5 @@ +//! Markdown benchmarks. + fn main() { divan::main(); } diff --git a/crates/mq-markdown/examples/html_convert.rs b/crates/mq-markdown/examples/html_convert.rs index 98e465c79..49ddf10c0 100644 --- a/crates/mq-markdown/examples/html_convert.rs +++ b/crates/mq-markdown/examples/html_convert.rs @@ -1,3 +1,5 @@ +//! Converts an HTML file to Markdown. + use mq_markdown::{ConversionOptions, convert_html_to_markdown}; use std::env; use std::fs; diff --git a/crates/mq-markdown/src/html_to_markdown.rs b/crates/mq-markdown/src/html_to_markdown.rs index c12b52f5a..1acf4736d 100644 --- a/crates/mq-markdown/src/html_to_markdown.rs +++ b/crates/mq-markdown/src/html_to_markdown.rs @@ -241,6 +241,7 @@ fn extract_front_matter_from_head_ref(html: &Html) -> Option miette::Result { if html_input.trim().is_empty() { return Ok("".to_string()); diff --git a/crates/mq-markdown/src/html_to_markdown/options.rs b/crates/mq-markdown/src/html_to_markdown/options.rs index a293cdd6c..621ec1532 100644 --- a/crates/mq-markdown/src/html_to_markdown/options.rs +++ b/crates/mq-markdown/src/html_to_markdown/options.rs @@ -1,7 +1,11 @@ +/// Options for converting HTML to Markdown. #[derive(Debug, Clone, Default)] pub struct ConversionOptions { + /// Emit `