From 0a64ccdd6bebc77c025c1847b9f07924a1338db2 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Thu, 22 Oct 2026 11:46:33 +1100 Subject: [PATCH] chore(clasp-rs): land the empty SIS Rust scaffold Replace the stub with the libCLImate.Rust skeleton at version 0.0.0: banded Cargo.toml, empty library docs, the versions example, rustfmt, and checker scripts, plus ci.yml. The public API stays empty. - package name remains clasp-rs, and the lib target is clasp_rs; - dev-dependency test_help-rs matches the exemplar; runtime deps stay empty; - CHANGES, NEWS, TODO, and EXAMPLES record the scaffold and future work; --- .cargo/config.toml | 1 - .gitattributes | 68 ++++++++++ .github/workflows/ci.yml | 85 ++++++++++++ .gitignore | 53 +++++++- .vimrc | 66 ++++++++- .vscode/settings.json | 85 +++++++++--- CHANGES.md | 12 ++ Cargo.lock | 35 +++++ Cargo.toml | 52 +++++++- EXAMPLES.md | 9 ++ LICENSE | 2 +- NEWS.md | 9 ++ README.md | 131 ++++++++++++++++-- TODO.md | 29 ++++ examples/versions/main.rs | 3 + .rustfmt.toml => rustfmt.toml | 36 +++-- scripts/check_derives.py | 129 ++++++++++++++++++ scripts/check_doc_76.py | 66 +++++++++ scripts/check_test_names.py | 243 ++++++++++++++++++++++++++++++++++ scripts/fmt | 17 +++ src/lib.rs | 23 +++- 21 files changed, 1093 insertions(+), 61 deletions(-) create mode 100644 .gitattributes create mode 100644 .github/workflows/ci.yml create mode 100644 CHANGES.md create mode 100644 Cargo.lock create mode 100644 EXAMPLES.md create mode 100644 NEWS.md create mode 100644 TODO.md create mode 100644 examples/versions/main.rs rename .rustfmt.toml => rustfmt.toml (72%) create mode 100755 scripts/check_derives.py create mode 100755 scripts/check_doc_76.py create mode 100755 scripts/check_test_names.py create mode 100755 scripts/fmt diff --git a/.cargo/config.toml b/.cargo/config.toml index d9517c1..54d5d51 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,2 +1 @@ # .cargo/config.toml - diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..5a8705b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,68 @@ +# .gitattributes — Rust (GitHub-hosted) +# +# Sources: GitHub Docs (line endings), git-scm gitattributes (diff=rust), +# gitattributes/gitattributes Rust.gitattributes + Common.gitattributes, +# github-linguist overrides. + +# Default: detect text, normalize to LF in the repository +* text=auto + +# --- Source --- +*.rs text eol=lf diff=rust +*.toml text +Cargo.lock text +Cargo.toml text +rust-toolchain text +rust-toolchain.toml text + +# --- Scripts --- +*.bash text eol=lf +*.bat text eol=crlf +*.cmd text eol=crlf +*.ps1 text eol=crlf +*.sh text eol=lf +*.zsh text eol=lf + +# --- Docs / meta --- +*.adoc text +*.markdown text diff=markdown +*.md text diff=markdown +*.txt text +AUTHORS text +CHANGELOG text +CHANGES text +CONTRIBUTING text +COPYING text +LICENSE text +NEWS text +README text +TODO text +.gitattributes text +.gitignore text + +# --- Serialisation --- +*.json text +*.xml text +*.yaml text +*.yml text + +# --- Binary --- +*.a binary +*.dll binary +*.dylib binary +*.exe binary +*.rlib binary +*.so binary + +# --- Archives / images (common) --- +*.gif binary +*.gz binary +*.ico binary +*.jpeg binary +*.jpg binary +*.png binary +*.tar binary +*.zip binary + +# --- GitHub Linguist --- +**/target/** linguist-generated diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3c6a18c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,85 @@ +name: CI + +on: + push: + branches: + - master + - dev + - boilerplate + - idiomatic + - rc1 + - rc2 + - rc3 + pull_request: + +permissions: + contents: read + +defaults: + run: + shell: bash + +env: + CARGO_TERM_COLOR: always + +jobs: + check: + name: Stable checks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + + - uses: Swatinem/rust-cache@v2 + + - name: cargo test (all targets) + run: cargo test --all-targets --locked + + - name: cargo test (no default features) + run: cargo test --no-default-features --locked + + - name: cargo clippy + run: cargo clippy --all-targets --locked -- -D warnings + + - name: cargo doc + run: RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --locked + + - name: Install pinned nightly rustfmt + run: rustup toolchain install nightly-2026-09-10 --profile minimal --component rustfmt + + - name: rustfmt + env: + RUSTFMT_TOOLCHAIN: nightly-2026-09-10 + RUSTUP_TOOLCHAIN: nightly-2026-09-10 + run: ./scripts/fmt --check + + - name: DOC_76 checker + run: python3 scripts/check_doc_76.py + + - name: RUST_TEST_NAMING checker + run: python3 scripts/check_test_names.py + + - name: DERIVE_LAYOUT checker + run: python3 scripts/check_derives.py + + - name: cargo build (examples) + run: cargo build --examples --locked + + - name: cargo publish (dry run) + run: cargo publish --dry-run --locked + + msrv: + name: MSRV (1.74) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@1.74.0 + + - uses: Swatinem/rust-cache@v2 + + - name: cargo check (library) + run: cargo check --lib --locked diff --git a/.gitignore b/.gitignore index 37c2727..6b5e062 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,27 @@ # directories (by name) -/_build/ +/_analyses/ + +/.idea/ + +/scratch/ /target/ # directories (by pattern) +**/__pycache__/ + +**/.mypy_cache/ +**/.pytest_cache/ +**/.ruff_cache/ + # files (by name) .DS_Store - -Cargo.lock +.ruby-version # files (by pattern) @@ -20,7 +29,45 @@ Cargo.lock **/*.rs.bk # These are backup files generated by rustfmt *~ +*.*~ +*.???_obj +*.a +*.app +*.bak +*.class +*.diff +*.dll +*.dylib +*.exe +*.gch +*.gem +*.idb +*.ilk +*.lib +*.log +*.ncb +*.o +*.obj +*.opensdf +*.opt +*.org +*.out +*.pch +*.pdb +*.pyc +*.pyo +*.res +*.sbr +*.scc +*.sdf +*.so +*.suo +*.sw[ponm] *.swp +*.test +*.tlog *.tmp +*.xcuserstate *.zip + diff --git a/.vimrc b/.vimrc index b7392f5..9546145 100644 --- a/.vimrc +++ b/.vimrc @@ -1,6 +1,70 @@ +" Synesis Rust project .vimrc — aligned with .vscode/settings.json (Rust) +set nocompatible +filetype indent plugin on +syntax enable set autoindent +set backspace=indent,eol,start +set hlsearch +set incsearch +set number + +" files.insertFinalNewline +set eol +set fixeol + +" editor.renderWhitespace: all +set list +set listchars=tab:->,trail:-,extends:>,precedes:<,nbsp:+ + +" editor.detectIndentation: false — global defaults (editor.tabSize: 4, insertSpaces: true) +set colorcolumn=76 set expandtab set shiftwidth=4 set softtabstop=4 -set tabstop=4 \ No newline at end of file +set tabstop=4 + +" colorcolumn draws a full-column tint in Vim (not a VS Code-style 1px line). +" Keep it subtle via the ColorColumn highlight group; reapply after colorscheme changes. +if has('termguicolors') + " set termguicolors +endif + +function! s:ConfigureColorColumn() abort + highlight ColorColumn ctermbg=236 guibg=#2a2a2a cterm=NONE gui=NONE +endfunction + +call s:ConfigureColorColumn() +autocmd ColorScheme * call s:ConfigureColorColumn() + +" files.trimTrailingWhitespace +autocmd BufWritePre * %s/\s\+$//e + +augroup sis_rust + autocmd! + + " [bat] + autocmd FileType bat,dosbatch setlocal expandtab tabstop=4 shiftwidth=4 softtabstop=4 colorcolumn=60,76 + + " [c] / [cpp] + autocmd FileType c,cpp setlocal expandtab tabstop=4 shiftwidth=4 softtabstop=4 colorcolumn=60,64,68,72,76 + + " [cmake] + autocmd FileType cmake setlocal noexpandtab tabstop=4 shiftwidth=4 softtabstop=4 + + " [json] / [markdown] / [yaml] / [ruby] + autocmd FileType json,markdown,yaml,ruby setlocal expandtab tabstop=2 shiftwidth=2 softtabstop=2 + + " [python] + autocmd FileType python setlocal expandtab tabstop=4 shiftwidth=4 softtabstop=4 colorcolumn=60,76 + + " [rust] + autocmd FileType rust setlocal expandtab tabstop=4 shiftwidth=4 softtabstop=4 colorcolumn=76 + + " [shellscript] + autocmd FileType sh,bash,zsh setlocal expandtab tabstop=2 shiftwidth=2 softtabstop=2 colorcolumn=60,76 + + " [toml] + autocmd FileType toml setlocal noexpandtab tabstop=2 shiftwidth=2 softtabstop=2 +augroup END + diff --git a/.vscode/settings.json b/.vscode/settings.json index d1956bc..6f3e9c2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,23 +1,64 @@ { - "[json]": { - "editor.tabSize": 2, - }, - "[ruby]": { - "editor.tabSize": 2, - }, - "[rust]": { - "editor.tabSize": 4, - }, - "[toml]": { - "editor.tabSize": 2, - }, - "cmake.configureOnOpen": false, - "editor.detectIndentation": false, - "editor.insertSpaces": true, - "editor.tabSize": 2, - "files.trimTrailingWhitespace": true, - "git.mergeEditor": false, - "rust-analyzer.cargo.noDefaultFeatures": true, - "rust-analyzer.completion.autoimport.enable": false, - "rust-analyzer.showUnlinkedFileNotification": false, - } + "[json]": { + "editor.insertSpaces": true, + "editor.tabSize": 2, + }, + "[markdown]": { + "editor.insertSpaces": true, + "editor.tabSize": 2, + }, + "[python]": { + "editor.insertSpaces": true, + "editor.rulers": [ 60, 76 ], + "editor.tabSize": 4, + }, + "[ruby]": { + "editor.insertSpaces": true, + "editor.rulers": [ 60, 76 ], + "editor.tabSize": 2, + }, + "[rust]": { + "editor.defaultFormatter": "rust-lang.rust-analyzer", + "editor.formatOnSave": true, + "editor.insertSpaces": true, + "editor.rulers": [ 60, 76 ], + "editor.tabSize": 4, + }, + "[shellscript]": { + "editor.insertSpaces": true, + "editor.rulers": [ 60, 76 ], + "editor.tabSize": 2, + }, + "[toml]": { + "editor.insertSpaces": false, + "editor.tabSize": 2, + }, + "cmake.configureOnOpen": false, + "debug.allowBreakpointsEverywhere": true, + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "editor.renderWhitespace": "all", + "editor.rulers": [ 76 ], + "editor.tabSize": 2, + "files.insertFinalNewline": true, + "files.trimTrailingWhitespace": true, + "git.mergeEditor": false, + "rust-analyzer.cargo.buildScripts.enable": true, + "rust-analyzer.cargo.features": [], + "rust-analyzer.cargo.noDefaultFeatures": true, + "rust-analyzer.check.allTargets": true, + "rust-analyzer.check.command": "clippy", + "rust-analyzer.checkOnSave": true, + "rust-analyzer.completion.autoimport.enable": false, + "rust-analyzer.debug.engine": "vadimcn.vscode-lldb", + "rust-analyzer.debug.openDebugPane": true, + "rust-analyzer.procMacro.enable": true, + "rust-analyzer.rustfmt.overrideCommand": [ + "rustup", + "run", + "nightly-2026-09-10", + "rustfmt", + "--unstable-features", + ], + "rust-analyzer.showUnlinkedFileNotification": false, +} diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..02f729f --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,12 @@ +# CLASP.Rust - Changes + + +## 0.0.0 - 22nd October 2026 + +* added the initial **CLASP.Rust** crate and Cargo package metadata; +* added the **versions** example and introductory project documentation; +* added the initial feature, dependency, CI, formatting, and repository configuration; +* documented the MSRV, examples, dependencies, and current empty API surface; + + + diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..1286c8f --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,35 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "base-traits" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "febda1e6bd75ec4a0680596f5d8689bbc391c3213607d286ab711f3b8b091ee1" +dependencies = [ + "bt-rs", +] + +[[package]] +name = "bt-rs" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b33a0644862185265eb4bee9b1679c1ae36f8258c000f3f482210acd95f7d70" + +[[package]] +name = "clasp-rs" +version = "0.0.0" +dependencies = [ + "test_help-rs", +] + +[[package]] +name = "test_help-rs" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbd2a2dbea40fe948ff30aab06e73ecf79ff443ec01d38dc16ed9a28e1a4847" +dependencies = [ + "base-traits", + "bt-rs", +] diff --git a/Cargo.toml b/Cargo.toml index e52c63f..0291344 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,33 +7,81 @@ authors = [ "Matt Wilson ", ] +categories = [ + "command-line-interface", +] description = "Command-Line Argument Sorting and Parsing (for Rust)" +documentation = "https://docs.rs/clasp-rs" edition = "2021" -homepage = "https://github.com/synesissoftware/libpath.Rust" +exclude = [ + ".cargo", + ".cursor", + ".github", + ".vimrc", + ".vscode", + "scripts", + "target", +] +homepage = "https://github.com/synesissoftware/CLASP.Rust" +keywords = [ + "cli", + "command-line", + "options", + "parsing", +] license = "BSD-3-Clause" name = "clasp-rs" readme = "README.md" +repository = "https://github.com/synesissoftware/CLASP.Rust" +rust-version = "1.74" version = "0.0.0" # ########################################################## # Targets +[lib] +name = "clasp_rs" +path = "src/lib.rs" + +[[example]] +name = "versions" +path = "examples/versions/main.rs" + # ########################################################## # Features [features] +default = [ +] + +# General features: +# +# - "_NEVER_TO_BE_ENABLED" - this is a placeholder feature and must NEVER be specified; +# - "null-feature" - a feature that has no effect (and, thus, is useful for simplifying driver scripts); + +_NEVER_TO_BE_ENABLED = [] +null-feature = [] + + +# Crate-specific features: +# + # ########################################################## # Dependencies -[dependencies] +[build-dependencies] +[dependencies] [dev-dependencies] +test_help-rs = { version = "0.2", default-features = false, features = [ +]} + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/EXAMPLES.md b/EXAMPLES.md new file mode 100644 index 0000000..f6abcc1 --- /dev/null +++ b/EXAMPLES.md @@ -0,0 +1,9 @@ +# CLASP.Rust - Examples + + +| Name | Source | Summary | +| ------------ | ------------------------------------- | ---------------------------------- | +| **versions** | [Source](./examples/versions/main.rs) | Prints the crate name and version. | + + + diff --git a/LICENSE b/LICENSE index aaa5310..c6a22de 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ CLASP.Rust - BSD 3-Clause License -Copyright (c) 2019-2024, Matthew Wilson and Synesis Information Systems +Copyright (c) 2019-2026, Matthew Wilson and Synesis Information Systems Copyright (c) 2008-2019, Matthew Wilson and Synesis Software All rights reserved. diff --git a/NEWS.md b/NEWS.md new file mode 100644 index 0000000..e4b4b0b --- /dev/null +++ b/NEWS.md @@ -0,0 +1,9 @@ +# CLASP.Rust - News + + +| Date | News Item | Details | +| ------------------- | ------------------------- | --------------- | +| 22nd October 2026 | [CLASP.Rust 0.0.0](https://github.com/synesissoftware/CLASP.Rust/releases/tag/0.0.0) released | Initial release | + + + diff --git a/README.md b/README.md index 9139760..8dddf11 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,117 @@ # CLASP.Rust -Command-Line Argument Sorting and Parsing, for Rust +Command-Line Argument Sorting and Parsing (for Rust) - -## Introduction - -T.B.C. +![Language](https://img.shields.io/badge/Rust-000000?style=flat&logo=rust&logoColor=white) +[![License](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) +[![Crates.io](https://img.shields.io/crates/v/clasp-rs.svg)](https://crates.io/crates/clasp-rs) +[![GitHub release](https://img.shields.io/github/v/release/synesissoftware/CLASP.Rust.svg)](https://github.com/synesissoftware/CLASP.Rust/releases/latest) +![MSRV](https://img.shields.io/badge/MSRV-1.74-lightgrey) +[![CI](https://github.com/synesissoftware/CLASP.Rust/actions/workflows/ci.yml/badge.svg)](https://github.com/synesissoftware/CLASP.Rust/actions/workflows/ci.yml) +[![docs.rs](https://docs.rs/clasp-rs/badge.svg)](https://docs.rs/clasp-rs) ## Table of Contents - [Introduction](#introduction) +- [Developer Experience](#developer-experience) - [Installation](#installation) - [Components](#components) + - [Constants](#constants) + - [Enumerations](#enumerations) + - [Features](#features) + - [Functions](#functions) + - [Macros](#macros) + - [Structures](#structures) + - [Traits](#traits) - [Examples](#examples) - [Project Information](#project-information) - [Where to get help](#where-to-get-help) - [Contribution guidelines](#contribution-guidelines) + - [Minimum Supported Rust Version (MSRV)](#minimum-supported-rust-version-msrv) - [Dependencies](#dependencies) + - [Efferent (fan-out)](#efferent-fan-out) + - [Build Dependencies](#build-dependencies) + - [Development Dependencies](#development-dependencies) + - [Afferent (fan-in)](#afferent-fan-in) - [Related projects](#related-projects) - [License](#license) +## Introduction + +**CLASP** stands for **C**ommand-**L**ine **A**rgument **S**orting and **P**arsing. **CLASP.Rust** is an unpublished empty scaffold with no supported public API. The reference implementation is the C/C++ [**CLASP**](https://github.com/synesissoftware/CLASP) library. The other implementations are listed under [Related projects](#related-projects). + + +## Developer Experience + +A stable Rust toolchain at **MSRV 1.74** or newer is required. From the crate root: + +```bash +cargo build +cargo test +./scripts/fmt +``` + +**scripts/fmt** runs the pinned nightly **rustfmt** described in **rustfmt.toml**. The stock example is **examples/versions/main.rs**. + + ## Installation -T.B.C. +Reference in **Cargo.toml** in the usual way: + +```toml +clasp-rs = { version = "0.0" } +``` ## Components -T.B.C. +### Constants + +None defined at this time. + + +### Enumerations + +None defined at this time. + + +### Features + +The following crate features are defined: + +* `null-feature` — a feature that has no effect (useful for simplifying driver scripts); + + +### Functions + +None defined at this time. + + +### Macros + +None defined at this time. + + +### Structures + +None defined at this time. + + +### Traits + +None defined at this time. ## Examples -T.B.C. +Examples can be found in `examples/`: +* `examples/versions/main.rs` — prints the crate name and version; -## Project Information -T.B.C. +## Project Information ### Where to get help @@ -51,14 +123,48 @@ T.B.C. Defect reports, feature requests, and pull requests are welcome on https://github.com/synesissoftware/CLASP.Rust. +### Minimum Supported Rust Version (MSRV) + +The declared Minimum Supported Rust Version (MSRV) for **CLASP.Rust** is **1.74**. + +This MSRV guarantee applies to the library crate itself, its runtime dependencies (`[dependencies]`), and its build dependencies (`[build-dependencies]`). Downstream consumers compiling this crate as a dependency are guaranteed that it builds cleanly on the declared MSRV toolchain. + +Development dependencies (`[dev-dependencies]`, such as benchmarking frameworks like **criterion**) may require newer Rust toolchains for local development or performance testing. These dev-dependencies are never fetched or compiled by downstream consumers and do not affect the library's MSRV guarantee. + + ### Dependencies -T.B.C. +#### Efferent (fan-out) + +Libraries upon which **CLASP.Rust** depends: + +* None currently. + + +#### Build Dependencies + +None currently. + + +#### Development Dependencies + +* [**test_help-rs**](https://github.com/synesissoftware/test_help-rs) — test helpers for unit tests; + + +#### Afferent (fan-in) + +None currently. ### Related projects -T.B.C. +* [**CLASP**](https://github.com/synesissoftware/CLASP) — the C/C++ reference implementation; +* [**CLASP.Go**](https://github.com/synesissoftware/CLASP.Go); +* [**CLASP.js**](https://github.com/synesissoftware/CLASP.js); +* [**CLASP.NET**](https://github.com/synesissoftware/CLASP.NET); +* [**CLASP.Python**](https://github.com/synesissoftware/CLASP.Python); +* [**CLASP.Ruby**](https://github.com/synesissoftware/CLASP.Ruby); +* [**libCLImate.Rust**](https://github.com/synesissoftware/libCLImate.Rust); ### License @@ -67,4 +173,3 @@ T.B.C. - diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..6b59983 --- /dev/null +++ b/TODO.md @@ -0,0 +1,29 @@ +# CLASP.Rust - TODO + + +## Table of Contents + +- [Functional improvements](#functional-improvements) +- [Performance improvements](#performance-improvements) +- [Packaging improvements](#packaging-improvements) + + +## Functional improvements + +* [ ] Conformance table against the other **CLASP** implementations; +* [ ] Parser core; +* [ ] Usage and version generation; +* [ ] Wiring into **libCLImate.Rust**; + + +## Performance improvements + +* \ + + +## Packaging improvements + +* \ + + + diff --git a/examples/versions/main.rs b/examples/versions/main.rs new file mode 100644 index 0000000..9886976 --- /dev/null +++ b/examples/versions/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("{} v{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"),); +} diff --git a/.rustfmt.toml b/rustfmt.toml similarity index 72% rename from .rustfmt.toml rename to rustfmt.toml index e006cc6..6ff4572 100644 --- a/.rustfmt.toml +++ b/rustfmt.toml @@ -1,7 +1,16 @@ - -# rustfmt.toml for libpath.Rust +# rustfmt.toml +# +# Synesis Information Systems — gold rustfmt configuration for SIS Rust +# crates. +# +# Requires pinned nightly rustfmt with unstable features. Prefer: +# +# ./scripts/fmt +# +# Default pin (see scripts/fmt): nightly-2026-09-10 # -# configured for cargo-fmt 1.7.0-nightly +# Layout: full option inventory, lexicographic by option name; deprecated +# or unused options remain present but commented. # array_width=60 # deprecated # attr_fn_like_width=70 # deprecated @@ -12,30 +21,29 @@ brace_style="SameLineWhere" # chain_width=60 # deprecated color="Auto" combine_control_expr=false -comment_width=100 +comment_width=76 condense_wildcard_suffixes=false control_brace_style="AlwaysSameLine" disable_all_formatting=false -edition="2018" +edition="2021" empty_item_single_line=false enum_discrim_align_threshold=0 error_on_line_overflow=false error_on_unformatted=false -# fn_args_layout="Vertical" +# fn_args_layout="Vertical" # deprecated alias of fn_params_layout # fn_call_width=60 # deprecated fn_params_layout="Vertical" fn_single_line=false +# force_code_in_doc_comments=true force_explicit_abi=true force_multiline_blocks=true -# force_code_in_doc_comments=true format_macro_bodies=true format_macro_matchers=true format_strings=false group_imports="Preserve" hard_tabs=false -hide_parse_errors=false +# hide_parse_errors=false ignore=[ - ] imports_granularity="Crate" imports_indent="Block" @@ -60,6 +68,8 @@ reorder_modules=true # report_fixme="Always" # report_todo="Never" # required_version="????" +short_array_element_width_threshold=1 +show_parse_errors=true # single_line_if_else_max_width=0 # deprecated skip_children=false space_after_colon=true @@ -67,17 +77,19 @@ space_before_colon=true spaces_around_ranges=false struct_field_align_threshold=20 struct_lit_single_line=false +# struct_lit_trailing_comma="Vertical" # struct_lit_width=0 # deprecated +# struct_trailing_comma="Vertical" # struct_variant_width=0 # deprecated -tab_spaces=4 # Q: do we want to move to 2?? +# style_edition="2021" +tab_spaces=4 trailing_comma="Vertical" trailing_semicolon=true type_punctuation_density="Wide" -# unstable_features=false +unstable_features=true use_field_init_shorthand=true use_small_heuristics="Default" use_try_shorthand=true # version= where_single_line=false wrap_comments=false - diff --git a/scripts/check_derives.py b/scripts/check_derives.py new file mode 100755 index 0000000..5c595ec --- /dev/null +++ b/scripts/check_derives.py @@ -0,0 +1,129 @@ +#! /usr/bin/env python3 +""" +Verify DERIVE_LAYOUT: multi-trait `#[derive(...)]` macros must be split +into separate single-trait lines, ordered alphabetically by trait name, +except tightly coupled groups (Eq/PartialEq, Ord/PartialOrd). +""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + + +COUPLED_TRAIT_GROUPS = [ + ["Eq", "PartialEq"], + ["Ord", "PartialOrd"], +] + +PASS = "\N{WHITE HEAVY CHECK MARK}" # ✅ +FAIL = "\N{CROSS MARK}" # ❌ + + +def lint_file(filepath: Path) -> list[str]: + errors: list[str] = [] + + lines = filepath.read_text(encoding="utf-8").splitlines() + i = 0 + + while i < len(lines): + line = lines[i] + + if not re.match(r"^\s*#\[derive\(", line): + i += 1 + continue + + derive_block: list[tuple[int, str]] = [] + start_line_num = i + 1 + + while i < len(lines) and re.match(r"^\s*#\[derive\(", lines[i]): + derive_block.append((i + 1, lines[i])) + i += 1 + + parsed_lines: list[tuple[int, str, str]] = [] + block_has_error = False + + for line_num, line_str in derive_block: + match = re.search(r"#\[derive\((.*?)\)\]", line_str) + + if not match: + continue + + traits = [ + t.strip() + for t in match.group(1).split(",") + if t.strip() + ] + + if len(traits) > 1: + if traits not in COUPLED_TRAIT_GROUPS: + block_has_error = True + allowed = ", ".join( + f"'{', '.join(group)}'" + for group in COUPLED_TRAIT_GROUPS + ) + errors.append( + f"{filepath}:{line_num}: multi-trait derive " + f"'{line_str.strip()}' is not allowed " + f"(except coupled groups: {allowed})", + ) + elif len(traits) == 0: + block_has_error = True + errors.append( + f"{filepath}:{line_num}: empty derive attribute " + f"'{line_str.strip()}'", + ) + + sort_key = traits[0] if traits else "" + parsed_lines.append((line_num, line_str, sort_key)) + + if not block_has_error and len(parsed_lines) > 1: + sort_keys = [item[2] for item in parsed_lines] + + if sort_keys != sorted(sort_keys): + actual = [item[1].strip() for item in parsed_lines] + expected = [ + item[1].strip() + for item in sorted(parsed_lines, key=lambda x: x[2]) + ] + errors.append( + f"{filepath}:{start_line_num}: derive attributes not " + f"sorted alphabetically\n" + f" actual: {actual}\n" + f" expected: {expected}", + ) + + return errors + + +def main() -> int: + root = Path(__file__).resolve().parents[1] + errors: list[str] = [] + + for directory in ("src", "examples", "benches", "test"): + base = root / directory + + if not base.is_dir(): + continue + + for path in sorted(base.rglob("*.rs")): + if "target" in path.parts: + continue + + errors.extend(lint_file(path)) + + if errors: + print( + f"{FAIL} DERIVE_LAYOUT violations:", + file=sys.stderr, + ) + print("\n".join(f" {FAIL} {error}" for error in errors), file=sys.stderr) + return 1 + + print(f"{PASS} DERIVE_LAYOUT: ok") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/check_doc_76.py b/scripts/check_doc_76.py new file mode 100755 index 0000000..2a10562 --- /dev/null +++ b/scripts/check_doc_76.py @@ -0,0 +1,66 @@ +#! /usr/bin/env python3 +""" +Verify DOC_76: public documentation comment lines are at most 76 characters. + +Code blocks inside doc comments (``` ... ```) are exempt, matching Synesis +Information Systems' internal project standards. +""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + + +DOC_LINE = re.compile(r"^\s*(//!|///)") + +PASS = "\N{WHITE HEAVY CHECK MARK}" # ✅ +FAIL = "\N{CROSS MARK}" # ❌ + + +def iter_doc_violations(path: Path) -> list[str]: + violations: list[str] = [] + in_codeblock = False + + for line_no, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + stripped = line.rstrip() + + if DOC_LINE.match(stripped) and re.search(r"\s*```\s*$", stripped): + in_codeblock = not in_codeblock + continue + + if in_codeblock or not DOC_LINE.match(stripped): + continue + + if len(stripped) > 76: + violations.append( + f"{path}:{line_no} ({len(stripped)} chars): {stripped}" + ) + + return violations + + +def main() -> int: + root = Path(__file__).resolve().parents[1] + errors: list[str] = [] + + for path in sorted(root.rglob("*.rs")): + if "target" in path.parts: + continue + errors.extend(iter_doc_violations(path)) + + if errors: + print( + f"{FAIL} DOC_76 violations (doc comment lines must be <= 76 characters):", + file=sys.stderr, + ) + print("\n".join(f" {FAIL} {error}" for error in errors), file=sys.stderr) + return 1 + + print(f"{PASS} DOC_76: ok") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/check_test_names.py b/scripts/check_test_names.py new file mode 100755 index 0000000..1909373 --- /dev/null +++ b/scripts/check_test_names.py @@ -0,0 +1,243 @@ +#! /usr/bin/env python3 +""" +Verify RUST_TEST_NAMING: test functions and test modules use TEST_ prefix +and SHOUTING_SNAKE_CASE, except words that name a specific Rust construct +(type, function, macro, field, etc.) which must preserve exact case. + +When a construct name is embedded as a SHOUTING_SNAKE_CASE constant (or +PascalCase construct), it may be delimited with an extra underscore on +each side — e.g. HAVING__IGNORE_CASE__1. +""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + + +TEST_ATTR = re.compile(r"^\s*#\[(\w+::)?test(\(\))?\]") +FN_DEF = re.compile(r"^\s*fn\s+(\w+)") +MOD_DEF = re.compile(r"^\s*mod\s+(\w+)") +SNAKE_PART = re.compile(r"^[a-z][a-z0-9]*$") + +PASS = "\N{WHITE HEAVY CHECK MARK}" # ✅ +FAIL = "\N{CROSS MARK}" # ❌ + + +def is_pascal_case_atom(atom: str) -> bool: + return ( + atom[0].isupper() + and any(c.islower() for c in atom) + and atom.isalnum() + ) + + +def atom_violation(atom: str) -> str | None: + if atom.isupper() or atom.isdigit(): + return None + + if atom[0].islower() and all(SNAKE_PART.match(part) for part in atom.split("_")): + return None + + if is_pascal_case_atom(atom): + return None + + return ( + f"segment '{atom}' must be SHOUTING_SNAKE_CASE, a PascalCase construct " + "name, or a Rust snake_case identifier" + ) + + +def parse_padded_construct( + segments: list[str], start: int +) -> tuple[str | None, int, list[str]]: + """Parse __CONSTRUCT__ padding around a shouting or PascalCase atom.""" + violations: list[str] = [] + i = start + + while i < len(segments) and not segments[i]: + i += 1 + if i >= len(segments): + return None, i, ["empty segment padding without construct"] + + seg = segments[i] + atom: str | None = None + + if seg.isupper() or seg.isdigit(): + parts = [seg] + i += 1 + while i < len(segments) and segments[i] and ( + segments[i].isupper() or segments[i].isdigit() + ): + parts.append(segments[i]) + i += 1 + atom = "_".join(parts) + reason = atom_violation(atom) + if reason: + violations.append(reason) + elif seg[0].isupper() and is_pascal_case_atom(seg): + atom = seg + reason = atom_violation(seg) + if reason: + violations.append(reason) + i += 1 + else: + return None, start, ["empty segment padding without construct"] + + while i < len(segments) and not segments[i]: + i += 1 + + return atom, i, violations + + +def parse_name_atoms(rest: str) -> tuple[list[str], list[str]]: + """Split a test name body into atoms; return (atoms, violations).""" + atoms: list[str] = [] + violations: list[str] = [] + segments = rest.split("_") + i = 0 + + while i < len(segments): + seg = segments[i] + if not seg: + start = i + atom, i, viols = parse_padded_construct(segments, i) + violations.extend(viols) + if atom: + atoms.append(atom) + elif not viols: + violations.append(f"empty segment in '{rest}'") + if i == start: + i += 1 + continue + + if seg.isupper() or seg.isdigit(): + reason = atom_violation(seg) + if reason: + violations.append(reason) + else: + atoms.append(seg) + i += 1 + continue + + if seg[0].isupper(): + reason = atom_violation(seg) + if reason: + violations.append(reason) + else: + atoms.append(seg) + i += 1 + continue + + if SNAKE_PART.match(seg): + parts = [seg] + i += 1 + while i < len(segments) and SNAKE_PART.match(segments[i]): + parts.append(segments[i]) + i += 1 + atom = "_".join(parts) + reason = atom_violation(atom) + if reason: + violations.append(reason) + else: + atoms.append(atom) + continue + + violations.append( + f"segment '{seg}' must be SHOUTING_SNAKE_CASE, a PascalCase construct " + "name, or a Rust snake_case identifier" + ) + i += 1 + + return atoms, violations + + +def iter_name_violations(name: str) -> list[str]: + if not name.startswith("TEST_"): + return ["must start with 'TEST_'"] + + rest = name[len("TEST_") :] + if not rest: + return ["must have a name after 'TEST_'"] + + _, violations = parse_name_atoms(rest) + return violations + + +def iter_test_results(path: Path, root: Path) -> list[tuple[bool, str]]: + results: list[tuple[bool, str]] = [] + pending_test = False + display = path.relative_to(root) + + for line_no, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + stripped = line.rstrip() + + if TEST_ATTR.match(stripped): + pending_test = True + continue + + if pending_test and stripped.startswith("#["): + continue + + fn_match = FN_DEF.match(stripped) + if fn_match: + name = fn_match.group(1) + if pending_test: + pending_test = False + reasons = iter_name_violations(name) + label = f"{display}:{line_no}: test function '{name}'" + if reasons: + for reason in reasons: + results.append((False, f"{label}: {reason}")) + else: + results.append((True, label)) + continue + + mod_match = MOD_DEF.match(stripped) + if mod_match: + pending_test = False + name = mod_match.group(1) + if name.startswith("TEST_"): + reasons = iter_name_violations(name) + label = f"{display}:{line_no}: test module '{name}'" + if reasons: + for reason in reasons: + results.append((False, f"{label}: {reason}")) + else: + results.append((True, label)) + continue + + if stripped and not stripped.startswith("#") and stripped.endswith("{"): + pending_test = False + + return results + + +def main() -> int: + root = Path(__file__).resolve().parents[1] + results: list[tuple[bool, str]] = [] + + for path in sorted(root.rglob("*.rs")): + if "target" in path.parts: + continue + results.extend(iter_test_results(path, root)) + + failures = [line for ok, line in results if not ok] + if failures: + print( + f"{FAIL} RUST_TEST_NAMING violations " + "(test functions and modules must use TEST_ + SHOUTING_SNAKE_CASE):", + file=sys.stderr, + ) + for ok, line in results: + mark = PASS if ok else FAIL + print(f" {mark} {line}", file=sys.stderr) + return 1 + + print(f"{PASS} RUST_TEST_NAMING: ok") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/fmt b/scripts/fmt new file mode 100755 index 0000000..932cc02 --- /dev/null +++ b/scripts/fmt @@ -0,0 +1,17 @@ +#! /usr/bin/env bash +# Synesis Information Systems — gold rustfmt driver for SIS Rust crates. +# +# Uses a dated nightly only for this process (see RUSTFMT_TOOLCHAIN). Does +# not rely on rust-toolchain.toml. +set -euo pipefail + +RUSTFMT_TOOLCHAIN="${RUSTFMT_TOOLCHAIN:-nightly-2026-09-10}" +RUSTFMT="$(rustup which --toolchain "${RUSTFMT_TOOLCHAIN}" rustfmt 2>/dev/null || true)" +if [[ -z "${RUSTFMT}" ]]; then + echo "error: ${RUSTFMT_TOOLCHAIN} rustfmt is required (see rustfmt.toml)" >&2 + echo " rustup toolchain install ${RUSTFMT_TOOLCHAIN} --component rustfmt" >&2 + exit 1 +fi + +export RUSTFMT +exec env RUSTUP_TOOLCHAIN="${RUSTFMT_TOOLCHAIN}" cargo fmt --all -- --unstable-features "$@" diff --git a/src/lib.rs b/src/lib.rs index cfd8a5a..c3d9474 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,25 @@ +#![allow(non_snake_case)] +//! **CLASP.Rust** is currently an unpublished scaffold for a future Rust +//! command-line argument sorting and parsing implementation. +//! +//! No supported public API currently exists. Command-line parsing, +//! application helpers, public interfaces, tests, and usage examples belong +//! to future implementation work and are intentionally absent. -pub mod clasp { +// lib.rs : CLASP.Rust -} -#[cfg(test)] -mod tests { +// ///////////////////////////////////////////////////////// +// crate-level feature definitions -} +// ///////////////////////////////////////////////////////// +// crate-level feature discrimination -/* ///////////////////////////// end of file //////////////////////////// */ +// ///////////////////////////////////////////////////////// +// imports + + +// ///////////////////////////// end of file //////////////////////////// //