diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-11 19:45:17 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-11 19:45:17 +0900 |
| commit | 24cc697a9cd0dcac854359d65b8265f02f483b72 (patch) | |
| tree | c9693dbf3136d840157609161a3a5695828e853b | |
| parent | 2aceeb116150b6d6e6d3f371c2af509902ceafea (diff) | |
| download | php-mozart-24cc697a9cd0dcac854359d65b8265f02f483b72.tar.gz php-mozart-24cc697a9cd0dcac854359d65b8265f02f483b72.tar.zst php-mozart-24cc697a9cd0dcac854359d65b8265f02f483b72.zip | |
chore(lint): add Ruby linter scripts and apply rules
Adds scripts/lint with linters for mod.rs naming, contiguous use blocks,
use-as aliasing, sorted Cargo dependencies, std::collections maps, and
workspace dependency requirements. Renames mod.rs files, reorders use
statements, drops unnecessary import aliases, and sorts Cargo.toml
entries to satisfy the new rules.
29 files changed, 384 insertions, 50 deletions
diff --git a/crates/mozart-console-macros/src/codegen.rs b/crates/mozart-console-macros/src/codegen.rs index 8601e07..c6c0f83 100644 --- a/crates/mozart-console-macros/src/codegen.rs +++ b/crates/mozart-console-macros/src/codegen.rs @@ -1,10 +1,9 @@ +use crate::parser::Segment; use proc_macro2::TokenStream; use quote::quote; use syn::Expr; use syn::punctuated::Punctuated; -use crate::parser::Segment; - /// Returns true if the string contains any format placeholders (`{}`, `{name}`, `{0}`, `{:<10}`, etc.) /// but not escaped braces `{{` or `}}`. fn has_placeholders(s: &str) -> bool { diff --git a/crates/mozart-core/src/config.rs b/crates/mozart-core/src/config.rs index 58d1d17..1fbbb41 100644 --- a/crates/mozart-core/src/config.rs +++ b/crates/mozart-core/src/config.rs @@ -5,12 +5,11 @@ //! known properties. Unknown properties are captured in the `extra` map so //! that round-tripping through serde is lossless. +use crate::composer::composer_home; use indexmap::IndexMap; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; -use crate::composer::composer_home; - /// Parse a size string like "300MiB", "1GB", "512k", or a plain integer string /// into a byte count. Mirrors Composer's `Config::get('cache-files-maxsize')`. fn parse_size_bytes(s: &str) -> Option<u64> { diff --git a/crates/mozart-core/src/config_source.rs b/crates/mozart-core/src/config_source.rs index 984007a..42d2d6f 100644 --- a/crates/mozart-core/src/config_source.rs +++ b/crates/mozart-core/src/config_source.rs @@ -1,6 +1,5 @@ -use std::path::{Path, PathBuf}; - use anyhow::anyhow; +use std::path::{Path, PathBuf}; pub struct JsonConfigSource { path: PathBuf, diff --git a/crates/mozart-core/src/config_validator.rs b/crates/mozart-core/src/config_validator.rs index dbed651..85cc538 100644 --- a/crates/mozart-core/src/config_validator.rs +++ b/crates/mozart-core/src/config_validator.rs @@ -5,13 +5,11 @@ //! Composer's: `ValidateCommand` and `DiagnoseCommand` each `new //! ConfigValidator(...)`; neither depends on the other. +use crate::validation; +use regex::Regex; use std::collections::HashSet; use std::sync::LazyLock; -use regex::Regex; - -use crate::validation as v; - static DEPRECATED_GPL_OR_LATER_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)^[AL]?GPL-[123](\.[01])?\+$").unwrap()); @@ -113,7 +111,7 @@ fn check_name(obj: &serde_json::Map<String, serde_json::Value>, result: &mut Val if name.chars().any(|c| c.is_ascii_uppercase()) { let suggested = name .split('/') - .map(v::sanitize_package_name_component) + .map(validation::sanitize_package_name_component) .collect::<Vec<_>>() .join("/"); result.publish_errors.push(format!( @@ -122,7 +120,7 @@ fn check_name(obj: &serde_json::Map<String, serde_json::Value>, result: &mut Val )); } - if !name.is_empty() && !v::validate_package_name(name) && !name.contains('/') { + if !name.is_empty() && !validation::validate_package_name(name) && !name.contains('/') { result.errors.push(format!( "The name \"{name}\" is invalid, it should be in the format \"vendor/package\"." )); @@ -224,11 +222,11 @@ fn check_license(obj: &serde_json::Map<String, serde_json::Value>, result: &mut continue; } let to_validate = license.replace("proprietary", "MIT"); - if v::validate_license(&to_validate) { + if validation::validate_license(&to_validate) { continue; } let quoted = serde_json::to_string(license).unwrap_or_else(|_| format!("\"{license}\"")); - if v::validate_license(to_validate.trim()) { + if validation::validate_license(to_validate.trim()) { result.warnings.push(format!( "License {quoted} must not contain extra spaces, make sure to trim it." )); @@ -461,7 +459,7 @@ fn check_minimum_stability( result: &mut ValidationResult, ) { if let Some(stability) = obj.get("minimum-stability").and_then(|v| v.as_str()) - && !v::validate_stability(stability) + && !validation::validate_stability(stability) { result.errors.push(format!( "The minimum-stability \"{stability}\" is invalid. \ diff --git a/crates/mozart-core/src/installer/mod.rs b/crates/mozart-core/src/installer.rs index 8572627..8572627 100644 --- a/crates/mozart-core/src/installer/mod.rs +++ b/crates/mozart-core/src/installer.rs diff --git a/crates/mozart-core/src/package/archiver/archive_manager.rs b/crates/mozart-core/src/package/archiver/archive_manager.rs index b4f8e27..14497b2 100644 --- a/crates/mozart-core/src/package/archiver/archive_manager.rs +++ b/crates/mozart-core/src/package/archiver/archive_manager.rs @@ -1,9 +1,8 @@ -use crate::downloader::DownloadManager; - use super::{ ArchiveFormat, collect_archivable_files, create_archive, generate_archive_filename, parse_composer_excludes, parse_gitattributes, parse_gitignore_pattern, self_exclusion_patterns, }; +use crate::downloader::DownloadManager; use std::path::{Path, PathBuf}; /// A package to be archived. diff --git a/crates/mozart-core/src/repository/downloader.rs b/crates/mozart-core/src/repository/downloader.rs index f2e33a7..711a678 100644 --- a/crates/mozart-core/src/repository/downloader.rs +++ b/crates/mozart-core/src/repository/downloader.rs @@ -350,7 +350,7 @@ pub async fn install_package( #[cfg(test)] mod tests { use super::*; - use std::io::Write as IoWrite; + use std::io::Write as _; use tempfile::tempdir; /// Build a minimal zip archive in memory. diff --git a/crates/mozart-core/src/repository/installer_executor/mod.rs b/crates/mozart-core/src/repository/installer_executor.rs index f67c612..1cb26d2 100644 --- a/crates/mozart-core/src/repository/installer_executor/mod.rs +++ b/crates/mozart-core/src/repository/installer_executor.rs @@ -13,10 +13,9 @@ //! Composer's `(string) $operation` byte-for-byte without the executor //! having to also reproduce console formatting. -use std::path::PathBuf; - use super::installed::InstalledPackageEntry; use super::lockfile::{LockAlias, LockedPackage}; +use std::path::PathBuf; pub mod filesystem; pub mod trace_recorder; diff --git a/crates/mozart-core/src/repository/installer_executor/trace_recorder.rs b/crates/mozart-core/src/repository/installer_executor/trace_recorder.rs index b60a869..5dd39b0 100644 --- a/crates/mozart-core/src/repository/installer_executor/trace_recorder.rs +++ b/crates/mozart-core/src/repository/installer_executor/trace_recorder.rs @@ -14,12 +14,11 @@ //! - Update (downgrade direction): `Downgrading <name> (<oldVersion> => <newVersion>)` //! - Uninstall: `Removing <name> (<version>)` -use mozart_semver::Version; - use super::{ ExecuteContext, InstallerExecutor, PackageOperation, format_full_pretty_alias, format_full_pretty_version, }; +use mozart_semver::Version; /// Recording-only executor. Construct with [`TraceRecorderExecutor::new`], /// then read [`TraceRecorderExecutor::trace`] after the run completes. diff --git a/crates/mozart-core/src/repository/path_repository.rs b/crates/mozart-core/src/repository/path_repository.rs index 0cff012..2353809 100644 --- a/crates/mozart-core/src/repository/path_repository.rs +++ b/crates/mozart-core/src/repository/path_repository.rs @@ -19,11 +19,10 @@ //! consumers comparing references against Composer-produced lockfiles see //! byte-identical values. -use std::path::{Path, PathBuf}; - use crate::package::RawRepository; -use mozart_php_serialize::{Value as PhpValue, serialize as php_serialize}; +use mozart_php_serialize::{Value, serialize}; use sha1::{Digest as _, Sha1}; +use std::path::{Path, PathBuf}; /// Translate path repos in `repositories` into synthetic `type: package` /// entries. Non-path entries are returned unchanged in original order. @@ -123,11 +122,11 @@ fn resolve_path(url: &str, base_dir: &Path) -> PathBuf { /// flag is the only option Composer's auto-detection populates when the user /// supplied no `options` block. fn compute_path_reference(json_bytes: &[u8], is_relative: bool) -> String { - let options = PhpValue::Array(vec![( - PhpValue::String("relative".to_string()), - PhpValue::Bool(is_relative), + let options = Value::Array(vec![( + Value::String("relative".to_string()), + Value::Bool(is_relative), )]); - let serialized = php_serialize(&options); + let serialized = serialize(&options); let mut hasher = Sha1::new(); hasher.update(json_bytes); hasher.update(serialized.as_bytes()); diff --git a/crates/mozart-core/src/repository/repository/mod.rs b/crates/mozart-core/src/repository/repository.rs index 4afff54..ece0c5f 100644 --- a/crates/mozart-core/src/repository/repository/mod.rs +++ b/crates/mozart-core/src/repository/repository.rs @@ -10,10 +10,9 @@ //! the live Packagist HTTP repo, [`inline_package_repo`] for `type: package` //! entries embedded in `composer.json`, and [`vcs_repo`] for VCS repositories. -use std::collections::BTreeMap; - use super::advisory::{MatchedAdvisory, PackageInfo}; use super::packagist::{PackagistVersion, SearchResult}; +use std::collections::BTreeMap; pub mod inline_package_repo; pub mod packagist_repo; diff --git a/crates/mozart-core/src/vcs/process.rs b/crates/mozart-core/src/vcs/process.rs index 8ccc11d..7538d55 100644 --- a/crates/mozart-core/src/vcs/process.rs +++ b/crates/mozart-core/src/vcs/process.rs @@ -1,10 +1,9 @@ +use anyhow::{Result, bail}; use indexmap::IndexMap; use std::path::Path; use std::process::Command; use std::time::{Duration, Instant}; -use anyhow::{Result, bail}; - /// Output from a process execution. #[derive(Debug, Clone)] pub struct ProcessOutput { diff --git a/crates/mozart-core/src/vcs/util/mod.rs b/crates/mozart-core/src/vcs/util.rs index b2c35fc..b2c35fc 100644 --- a/crates/mozart-core/src/vcs/util/mod.rs +++ b/crates/mozart-core/src/vcs/util.rs diff --git a/crates/mozart-test-harness/src/pool_builder_parser.rs b/crates/mozart-test-harness/src/pool_builder_parser.rs index 2876c25..a8f7326 100644 --- a/crates/mozart-test-harness/src/pool_builder_parser.rs +++ b/crates/mozart-test-harness/src/pool_builder_parser.rs @@ -4,12 +4,11 @@ //! Section bodies are stored as raw strings (typically JSON); the runner is //! responsible for interpreting them. +use crate::parser::split_sections; use anyhow::{Context as _, Result, bail}; use std::fs; use std::path::Path; -use crate::parser::split_sections; - const VALID_SECTIONS: &[&str] = &[ "TEST", "ROOT", diff --git a/crates/mozart-test-harness/src/runner.rs b/crates/mozart-test-harness/src/runner.rs index bb26255..fa5c360 100644 --- a/crates/mozart-test-harness/src/runner.rs +++ b/crates/mozart-test-harness/src/runner.rs @@ -1,10 +1,9 @@ +use crate::parser::ParsedTest; use anyhow::{Context as _, Result}; use std::path::Path; use std::process::Command; use tempfile::TempDir; -use crate::parser::ParsedTest; - /// Outcome of running a parsed `.test` against the `mozart` binary. /// /// The temp directory is kept alive in this struct so callers can inspect diff --git a/crates/mozart/Cargo.toml b/crates/mozart/Cargo.toml index b8b3c06..4fef18a 100644 --- a/crates/mozart/Cargo.toml +++ b/crates/mozart/Cargo.toml @@ -22,8 +22,8 @@ sha1.workspace = true tempfile.workspace = true terminal_size.workspace = true tokio.workspace = true -tracing-subscriber.workspace = true tracing.workspace = true +tracing-subscriber.workspace = true url.workspace = true [dev-dependencies] diff --git a/crates/mozart/src/commands/audit.rs b/crates/mozart/src/commands/audit.rs index 5193b06..8b28770 100644 --- a/crates/mozart/src/commands/audit.rs +++ b/crates/mozart/src/commands/audit.rs @@ -1,5 +1,3 @@ -use std::path::Path; - use crate::composer::Composer; use clap::Args; use indexmap::IndexMap; @@ -8,6 +6,7 @@ use mozart_core::console::IoInterface; use mozart_core::repository::advisory::{AuditOptions, Auditor, PackageInfo}; use mozart_core::repository::cache::{Cache, build_cache_config}; use mozart_core::repository::repository::RepositorySet; +use std::path::Path; #[derive(Args)] pub struct AuditArgs { diff --git a/crates/mozart/src/commands/base_config.rs b/crates/mozart/src/commands/base_config.rs index c10e7e7..ed05184 100644 --- a/crates/mozart/src/commands/base_config.rs +++ b/crates/mozart/src/commands/base_config.rs @@ -1,7 +1,6 @@ -use std::path::PathBuf; - use mozart_core::composer::composer_home; use mozart_core::config_source::JsonConfigSource; +use std::path::PathBuf; /// Mirrors Composer's `BaseConfigCommand`: resolves the target config file path /// and enforces the `--file` ↔ `--global` mutual exclusivity. diff --git a/crates/mozart/src/commands/create_project.rs b/crates/mozart/src/commands/create_project.rs index 49354d1..fd2edfa 100644 --- a/crates/mozart/src/commands/create_project.rs +++ b/crates/mozart/src/commands/create_project.rs @@ -1,3 +1,4 @@ +use crate::factory::create_download_manager; use clap::Args; use indexmap::IndexMap; use mozart_core::console::IoInterface; @@ -12,8 +13,6 @@ use mozart_core::repository::version; use mozart_core::validation; use std::path::{Path, PathBuf}; -use crate::factory::create_download_manager; - #[derive(Args)] pub struct CreateProjectArgs { /// Package name to install diff --git a/crates/mozart/src/commands/repository.rs b/crates/mozart/src/commands/repository.rs index 6616352..fe94eac 100644 --- a/crates/mozart/src/commands/repository.rs +++ b/crates/mozart/src/commands/repository.rs @@ -1,11 +1,10 @@ +use super::base_config::BaseConfigContext; +use super::config_helpers::{normalize_repositories, render_value}; use anyhow::anyhow; use clap::Args; use mozart_core::console::IoInterface; use mozart_core::console_writeln; -use super::base_config::BaseConfigContext; -use super::config_helpers::{normalize_repositories, render_value}; - #[derive(Args)] pub struct RepositoryArgs { /// Action (list, add, remove, set-url, get-url, enable, disable) diff --git a/crates/mozart/tests/installer.rs b/crates/mozart/tests/installer.rs index fbeba6d..c96e3b4 100644 --- a/crates/mozart/tests/installer.rs +++ b/crates/mozart/tests/installer.rs @@ -8,9 +8,6 @@ //! EXPECT-LOCK + EXPECT-INSTALLED — the same load-bearing assertions //! Composer's PHPUnit suite uses. -use std::path::{Path, PathBuf}; -use std::sync::{Arc, Mutex}; - use clap::Parser; use mozart::commands::{Cli, Commands, install, update}; use mozart_core::console::{Console, IoInterface}; @@ -18,6 +15,8 @@ use mozart_core::exit_code::MozartError; use mozart_core::repository::installer_executor::TraceRecorderExecutor; use mozart_core::repository::repository::RepositorySet; use mozart_test_harness::{ParsedTest, parse_test_file}; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; use tempfile::TempDir; fn fixtures_dir() -> PathBuf { diff --git a/scripts/lint b/scripts/lint new file mode 100755 index 0000000..59bdcf3 --- /dev/null +++ b/scripts/lint @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby + +require 'pathname' + +LINTERS = [ + :cargo_workspace_dependencies, + :contiguous_use_block, + # TODO: re-enable this linter + # :no_decorative_section_comment, + :no_mod_rs, + # TODO: re-enable this linter + # :no_std_collections_maps, + :no_use_as_alias, + :sorted_dependencies, +] + +root_dir = Pathname.new(__dir__).join('..').expand_path + +results = LINTERS.map do |linter| + require_relative "linters/#{linter}" + puts "===== #{linter} =====" + ok = send(linter, root_dir) + puts "Passed." if ok + puts + ok +end +exit(results.all? ? 0 : 1) diff --git a/scripts/linters/cargo_workspace_dependencies.rb b/scripts/linters/cargo_workspace_dependencies.rb new file mode 100644 index 0000000..85305c9 --- /dev/null +++ b/scripts/linters/cargo_workspace_dependencies.rb @@ -0,0 +1,48 @@ +def cargo_workspace_dependencies(root_dir) + pattern = root_dir.join('crates', '*', 'Cargo.toml').to_s + errors = Dir.glob(pattern).sort.flat_map do |path| + relative = Pathname.new(path).relative_path_from(root_dir).to_s + find_non_workspace_deps(path, relative) + end + + return true if errors.empty? + + puts 'Found `[dependencies]` / `[dev-dependencies]` entries that do not use `workspace = true`.' + puts 'In a crate `Cargo.toml`, only `name.workspace = true` or `name = { workspace = true, ... }` is allowed:' + errors.each do |err| + puts " #{err}" + end + false +end + +def find_non_workspace_deps(path, relative) + errors = [] + current_section = nil + + File.read(path).each_line.with_index do |raw_line, idx| + stripped = raw_line.chomp.strip + + if stripped =~ /\A\[([^\]]+)\]\z/ + current_section = $1 + next + end + + next unless %w[dependencies dev-dependencies build-dependencies].include?(current_section) + next if stripped.empty? || stripped.start_with?('#') + + if stripped =~ /\A([A-Za-z0-9_-]+)\.workspace\s*=\s*true\b/ + next + elsif stripped =~ /\A([A-Za-z0-9_-]+)\s*=\s*\{(.+)\}\s*\z/ + name = $1 + inner = $2 + next if inner =~ /\bworkspace\s*=\s*true\b/ + + errors << "#{relative}:#{idx + 1}: `#{name}` does not use `workspace = true`" + elsif stripped =~ /\A([A-Za-z0-9_-]+)\s*=/ + name = $1 + errors << "#{relative}:#{idx + 1}: `#{name}` does not use `workspace = true`" + end + end + + errors +end diff --git a/scripts/linters/contiguous_use_block.rb b/scripts/linters/contiguous_use_block.rb new file mode 100644 index 0000000..bce9e32 --- /dev/null +++ b/scripts/linters/contiguous_use_block.rb @@ -0,0 +1,80 @@ +def contiguous_use_block(root_dir) + pattern = root_dir.join('crates', '**', '*.rs').to_s + errors = Dir.glob(pattern).sort.flat_map do |path| + relative = Pathname.new(path).relative_path_from(root_dir).to_s + find_split_use_block(path, relative) + end + + return true if errors.empty? + + puts 'Found blank lines splitting the leading `use` block into sections.' + puts 'All `use` statements at the top of the file must be contiguous (no blank lines between them):' + errors.each do |err| + puts " #{err}" + end + false +end + +USE_START_RE = /\A(?:pub(?:\([^)]*\))?\s+)?use\b/ + +def find_split_use_block(path, relative) + lines = File.readlines(path) + errors = [] + + i = skip_preamble(lines) + return [] if i.nil? + + loop do + i = consume_use_statement(lines, i) + break if i >= lines.length + + blanks = [] + j = i + while j < lines.length + stripped = lines[j].strip + if stripped.empty? + blanks << j + j += 1 + elsif stripped.start_with?('//') || stripped.start_with?('#[') + j += 1 + else + break + end + end + + if j < lines.length && lines[j].strip =~ USE_START_RE + blanks.each do |bi| + errors << "#{relative}:#{bi + 1}: blank line splits the leading `use` block" + end + i = j + else + break + end + end + + errors +end + +def skip_preamble(lines) + lines.each_with_index do |raw, idx| + stripped = raw.strip + return idx if stripped =~ USE_START_RE + next if stripped.empty? || stripped.start_with?('//') || stripped.start_with?('#![') || stripped.start_with?('#[') + + return nil + end + nil +end + +def consume_use_statement(lines, start_idx) + brace_depth = 0 + i = start_idx + while i < lines.length + line = lines[i] + brace_depth += line.count('{') - line.count('}') + done = brace_depth <= 0 && line.rstrip.end_with?(';') + i += 1 + return i if done + end + i +end diff --git a/scripts/linters/no_decorative_section_comment.rb b/scripts/linters/no_decorative_section_comment.rb new file mode 100644 index 0000000..8f26fad --- /dev/null +++ b/scripts/linters/no_decorative_section_comment.rb @@ -0,0 +1,34 @@ +def no_decorative_section_comment(root_dir) + pattern = root_dir.join('crates', '**', '*.rs').to_s + errors = Dir.glob(pattern).sort.flat_map do |path| + relative = Pathname.new(path).relative_path_from(root_dir).to_s + find_decorative_comments(path, relative) + end + + return true if errors.empty? + + puts 'Found decorative section comments (4+ consecutive `=`, `-`, or Unicode box-drawing characters).' + puts 'These section dividers are unnecessarily noisy — remove them:' + errors.each do |err| + puts " #{err}" + end + false +end + +DECORATIVE_RUN_RE = /[-=]{4,}|[─-╿]{4,}/ + +def find_decorative_comments(path, relative) + errors = [] + + File.readlines(path).each_with_index do |raw, idx| + stripped = raw.lstrip + next unless stripped.start_with?('//') + next if stripped.start_with?('///') || stripped.start_with?('//!') + + next unless stripped.match?(DECORATIVE_RUN_RE) + + errors << "#{relative}:#{idx + 1}: decorative section comment" + end + + errors +end diff --git a/scripts/linters/no_mod_rs.rb b/scripts/linters/no_mod_rs.rb new file mode 100755 index 0000000..8fef9f0 --- /dev/null +++ b/scripts/linters/no_mod_rs.rb @@ -0,0 +1,14 @@ +def no_mod_rs(root_dir) + pattern = root_dir.join('crates', '*', 'src', '**', 'mod.rs').to_s + errors = Dir.glob(pattern).sort.map do |path| + Pathname.new(path).relative_path_from(root_dir).to_s + end + + return true if errors.empty? + + puts 'Found `mod.rs` file(s). Use `src/<submodule>.rs` instead of `<submodule>/mod.rs`:' + errors.each do |path| + puts " #{path}" + end + false +end diff --git a/scripts/linters/no_std_collections_maps.rb b/scripts/linters/no_std_collections_maps.rb new file mode 100644 index 0000000..b80ef07 --- /dev/null +++ b/scripts/linters/no_std_collections_maps.rb @@ -0,0 +1,48 @@ +def no_std_collections_maps(root_dir) + pattern = root_dir.join('crates', '**', '*.rs').to_s + errors = Dir.glob(pattern).sort.flat_map do |path| + relative = Pathname.new(path).relative_path_from(root_dir).to_s + find_std_map_usages(path, relative) + end + + return true if errors.empty? + + puts 'Found uses of `std::collections::{HashMap, HashSet, BTreeMap, BTreeSet}`.' + puts 'Use `indexmap::IndexMap` / `indexmap::IndexSet` instead:' + errors.each do |err| + puts " #{err}" + end + false +end + +BANNED_MAP_NAMES = %w[HashMap HashSet BTreeMap BTreeSet].freeze + +def find_std_map_usages(path, relative) + errors = [] + + File.readlines(path).each_with_index do |raw, idx| + code = raw.split('//', 2).first || raw + + code.scan(/\bstd::collections::(HashMap|HashSet|BTreeMap|BTreeSet)\b/) do |m| + errors << "#{relative}:#{idx + 1}: use of `std::collections::#{m[0]}` (use `indexmap::#{indexmap_replacement(m[0])}` instead)" + end + + code.scan(/\bstd::collections::\{([^}]*)\}/) do |m| + m[0].split(',').each do |entry| + name = entry.strip.split(/\s+as\s+/).first + next unless BANNED_MAP_NAMES.include?(name) + + errors << "#{relative}:#{idx + 1}: import of `std::collections::#{name}` (use `indexmap::#{indexmap_replacement(name)}` instead)" + end + end + end + + errors.uniq +end + +def indexmap_replacement(name) + case name + when 'HashMap', 'BTreeMap' then 'IndexMap' + when 'HashSet', 'BTreeSet' then 'IndexSet' + end +end diff --git a/scripts/linters/no_use_as_alias.rb b/scripts/linters/no_use_as_alias.rb new file mode 100644 index 0000000..adb2e67 --- /dev/null +++ b/scripts/linters/no_use_as_alias.rb @@ -0,0 +1,51 @@ +def no_use_as_alias(root_dir) + pattern = root_dir.join('crates', '**', '*.rs').to_s + errors = Dir.glob(pattern).sort.flat_map do |path| + relative = Pathname.new(path).relative_path_from(root_dir).to_s + find_use_aliases(path, relative) + end + + return true if errors.empty? + + puts 'Found `use ... as Name` aliases.' + puts 'Renaming imports is forbidden; only unnamed imports `as _` (e.g. `use std::io::Write as _;`) are allowed:' + errors.each do |err| + puts " #{err}" + end + false +end + +USE_ALIAS_START_RE = /\A(?:pub(?:\([^)]*\))?\s+)?use\b/ + +def find_use_aliases(path, relative) + errors = [] + in_use = false + brace_depth = 0 + + File.readlines(path).each_with_index do |raw, idx| + code = raw.split('//', 2).first || raw + stripped = code.strip + + unless in_use + next unless stripped =~ USE_ALIAS_START_RE + + in_use = true + brace_depth = 0 + end + + code.scan(/\bas\s+([A-Za-z_][A-Za-z0-9_]*)/) do |m| + name = m[0] + next if name == '_' + + errors << "#{relative}:#{idx + 1}: `as #{name}` aliasing in `use` statement" + end + + brace_depth += code.count('{') - code.count('}') + if brace_depth <= 0 && code.rstrip.end_with?(';') + in_use = false + brace_depth = 0 + end + end + + errors +end diff --git a/scripts/linters/sorted_dependencies.rb b/scripts/linters/sorted_dependencies.rb new file mode 100644 index 0000000..16ec894 --- /dev/null +++ b/scripts/linters/sorted_dependencies.rb @@ -0,0 +1,50 @@ +def sorted_dependencies(root_dir) + pattern = root_dir.join('crates', '*', 'Cargo.toml').to_s + errors = Dir.glob(pattern).sort.flat_map do |path| + relative = Pathname.new(path).relative_path_from(root_dir).to_s + sections = parse_dep_sections(File.read(path)) + + %w[dependencies dev-dependencies].filter_map do |section| + deps = sections[section] + next if deps.nil? || deps.empty? + + expected = sort_dep_names(deps) + next if deps == expected + + { path: relative, section: section, actual: deps, expected: expected } + end + end + + return true if errors.empty? + + puts 'Found unsorted `[dependencies]` / `[dev-dependencies]` in Cargo.toml.' + puts 'Entries must be alphabetical, with `mozart-*` crates listed before others:' + errors.each do |err| + puts " #{err[:path]} [#{err[:section]}]" + puts " actual: #{err[:actual].join(', ')}" + puts " expected: #{err[:expected].join(', ')}" + end + false +end + +def parse_dep_sections(content) + sections = {} + current = nil + + content.each_line do |line| + stripped = line.chomp + if stripped =~ /\A\s*\[([^\]]+)\]\s*\z/ + current = $1 + sections[current] ||= [] + elsif current && stripped =~ /\A([A-Za-z0-9_-]+)\s*[.=]/ + sections[current] << $1 + end + end + + sections +end + +def sort_dep_names(deps) + mozart, other = deps.partition { |d| d.start_with?('mozart-') } + mozart.sort + other.sort +end |
