diff options
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 |
