diff --git a/.DS_Store b/.DS_Store index 8220253..6463e0e 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e14af8..499e025 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -159,7 +159,6 @@ jobs: cargo build --bin andromeda-compile --release --target ${{ matrix.rust-target }} --manifest-path ./crates/cli/Cargo.toml cargo build --bin andromeda-fmt --release --target ${{ matrix.rust-target }} --manifest-path ./crates/cli/Cargo.toml cargo build --bin andromeda-lint --release --target ${{ matrix.rust-target }} --manifest-path ./crates/cli/Cargo.toml - cargo build --bin andromeda-check --release --target ${{ matrix.rust-target }} --manifest-path ./crates/cli/Cargo.toml cargo build --bin andromeda-bundle --release --target ${{ matrix.rust-target }} --manifest-path ./crates/cli/Cargo.toml env: CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc @@ -188,7 +187,7 @@ jobs: fi # Prepare satellite binaries (only if they exist) - for satellite in run compile fmt lint check bundle; do + for satellite in run compile fmt lint bundle; do if [ -f "andromeda-${satellite}.exe" ]; then # Windows binaries cp "andromeda-${satellite}.exe" "andromeda-${satellite}-${{ matrix.rust-target }}.exe" @@ -222,7 +221,6 @@ jobs: target/${{ matrix.rust-target }}/release/andromeda-compile-${{ matrix.rust-target }}* target/${{ matrix.rust-target }}/release/andromeda-fmt-${{ matrix.rust-target }}* target/${{ matrix.rust-target }}/release/andromeda-lint-${{ matrix.rust-target }}* - target/${{ matrix.rust-target }}/release/andromeda-check-${{ matrix.rust-target }}* target/${{ matrix.rust-target }}/release/andromeda-bundle-${{ matrix.rust-target }}* release: diff --git a/Cargo.lock b/Cargo.lock index a6ecdbc..dc2a8c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -115,7 +115,7 @@ dependencies = [ [[package]] name = "andromeda" -version = "0.1.9" +version = "0.1.10" dependencies = [ "andromeda-core", "andromeda-runtime", @@ -167,7 +167,7 @@ dependencies = [ [[package]] name = "andromeda-core" -version = "0.1.9" +version = "0.1.10" dependencies = [ "anyhow", "anymap", @@ -191,7 +191,7 @@ dependencies = [ [[package]] name = "andromeda-runtime" -version = "0.1.9" +version = "0.1.10" dependencies = [ "andromeda-core", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 12809c0..b803f05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ authors = ["the Andromeda team"] edition = "2024" license = "Mozilla Public License 2.0" repository = "https://github.com/tryandromeda/andromeda" -version = "0.1.9" +version = "0.1.10" [workspace.dependencies] andromeda-core = { path = "crates/core" } diff --git a/README.md b/README.md index 753cea9..ba2969f 100644 --- a/README.md +++ b/README.md @@ -429,7 +429,6 @@ keyboard-driven Breakout clone exercising the full window + canvas input/render - **andromeda-compile** - Compile JS/TS to executables - **andromeda-fmt** - Format code - **andromeda-lint** - Lint code for quality issues -- **andromeda-check** - Type-check TypeScript - **andromeda-bundle** - Bundle and minify code ## Crates diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 32895ff..d941331 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -53,10 +53,6 @@ path = "src/bin/satellite_fmt.rs" name = "andromeda-lint" path = "src/bin/satellite_lint.rs" -[[bin]] -name = "andromeda-check" -path = "src/bin/satellite_check.rs" - [[bin]] name = "andromeda-bundle" path = "src/bin/satellite_bundle.rs" diff --git a/crates/cli/src/bin/installer.rs b/crates/cli/src/bin/installer.rs index c5a51c9..fbcc371 100644 --- a/crates/cli/src/bin/installer.rs +++ b/crates/cli/src/bin/installer.rs @@ -34,16 +34,15 @@ enum InstallerCommand { /// - compile: Compile JS/TS into standalone executables /// - fmt: Format JavaScript/TypeScript files /// - lint: Lint JavaScript/TypeScript files - /// - check: Type-check TypeScript files /// - bundle: Bundle and minify JS/TS files /// - all: Install all satellites /// /// Examples: /// andromeda-installer satellite run - /// andromeda-installer satellite fmt lint check + /// andromeda-installer satellite fmt lint /// andromeda-installer satellite all --force Satellite { - /// Satellites to install (run, compile, fmt, lint, check, bundle, or 'all') + /// Satellites to install (run, compile, fmt, lint, bundle, or 'all') #[arg(required = true)] satellites: Vec, @@ -93,7 +92,7 @@ struct GitHubAsset { const REPO_OWNER: &str = "tryandromeda"; const REPO_NAME: &str = "andromeda"; -const AVAILABLE_SATELLITES: &[&str] = &["run", "compile", "fmt", "lint", "check", "bundle"]; +const AVAILABLE_SATELLITES: &[&str] = &["run", "compile", "fmt", "lint", "bundle"]; fn main() -> CliResult<()> { let cli = Cli::parse(); @@ -563,7 +562,7 @@ fn print_satellite_header() { println!("══════════════════════════════════════════"); println!(); println!("Satellites are specialized, lightweight binaries for specific tasks."); - println!("Available: run, compile, fmt, lint, check, bundle"); + println!("Available: run, compile, fmt, lint, bundle"); println!(); } diff --git a/crates/cli/src/bin/satellite_check.rs b/crates/cli/src/bin/satellite_check.rs deleted file mode 100644 index fdc6462..0000000 --- a/crates/cli/src/bin/satellite_check.rs +++ /dev/null @@ -1,35 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -// If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. - -#![allow(clippy::result_large_err)] - -/// Andromeda Satellite - Check -/// -/// A minimal executable focused solely on type-checking TypeScript files. -/// Designed for container instances where only type-checking capability is needed. -use andromeda::{CliError, CliResult}; -use clap::Parser as ClapParser; -use std::path::PathBuf; - -#[derive(Debug, ClapParser)] -#[command(name = "andromeda-check")] -#[command(about = "Andromeda Satellite - Type-check TypeScript files")] -#[command(version = env!("CARGO_PKG_VERSION"))] -struct Cli { - /// The file(s) or directory(ies) to type-check - #[arg(required = false)] - paths: Vec, -} - -fn main() -> CliResult<()> { - andromeda::error::init_error_reporting(); - - let cli = Cli::parse(); - - let config = andromeda::config::ConfigManager::load_or_default(None); - - andromeda::check::check_files_with_config(&cli.paths, Some(config)) - .map_err(|e| CliError::runtime_error_simple(format!("{e}")))?; - - Ok(()) -} diff --git a/crates/cli/src/bundle.rs b/crates/cli/src/bundle.rs index cb9f30b..c30457b 100644 --- a/crates/cli/src/bundle.rs +++ b/crates/cli/src/bundle.rs @@ -1,5 +1,6 @@ -// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -// If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. use oxc_allocator::Allocator; use oxc_mangler::MangleOptions; diff --git a/crates/cli/src/check.rs b/crates/cli/src/check.rs deleted file mode 100644 index 6e727f6..0000000 --- a/crates/cli/src/check.rs +++ /dev/null @@ -1,581 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -#![allow(unused_assignments)] - -use crate::config::AndromedaConfig; -use crate::error::CliResult; -use crate::helper::find_formattable_files; -use console::Style; -use miette as oxc_miette; -use miette::{Diagnostic, NamedSource, SourceSpan}; -use oxc_allocator::Allocator; -use oxc_parser::Parser; -use oxc_semantic::SemanticBuilder; -use oxc_span::SourceType; -use serde::Serialize; -use std::collections::HashSet; -use std::fs; -use std::path::{Path, PathBuf}; - -/// Type checking error types with rich diagnostic information. -#[derive(Diagnostic, Debug, Clone)] -#[non_exhaustive] -pub enum TypeCheckError { - /// Unknown identifier (could not be resolved against any scope or ambient declaration) - #[diagnostic(code(andromeda::check::unknown_identifier))] - UnknownIdentifier { - name: String, - #[label("Cannot find name '{name}'")] - span: SourceSpan, - #[source_code] - source_code: NamedSource, - }, - /// Parse error - #[diagnostic(code(andromeda::check::parse_error))] - ParseError { - message: String, - #[label("Syntax error: {message}")] - span: SourceSpan, - #[source_code] - source_code: NamedSource, - }, - /// Semantic error (anything from oxc_semantic that isn't a name resolution issue) - #[diagnostic(code(andromeda::check::semantic_error))] - SemanticError { - message: String, - #[label("Semantic error: {message}")] - span: SourceSpan, - #[source_code] - source_code: NamedSource, - }, - /// Unused variable - #[diagnostic(code(andromeda::check::unused_variable))] - UnusedVariable { - name: String, - #[label("Variable '{name}' is declared but never used")] - span: SourceSpan, - #[source_code] - source_code: NamedSource, - }, -} - -impl TypeCheckError { - /// The stable diagnostic code for serialised output. - pub fn code(&self) -> &'static str { - match self { - TypeCheckError::UnknownIdentifier { .. } => "andromeda::check::unknown_identifier", - TypeCheckError::ParseError { .. } => "andromeda::check::parse_error", - TypeCheckError::SemanticError { .. } => "andromeda::check::semantic_error", - TypeCheckError::UnusedVariable { .. } => "andromeda::check::unused_variable", - } - } - - pub fn span(&self) -> SourceSpan { - match self { - TypeCheckError::UnknownIdentifier { span, .. } => *span, - TypeCheckError::ParseError { span, .. } => *span, - TypeCheckError::SemanticError { span, .. } => *span, - TypeCheckError::UnusedVariable { span, .. } => *span, - } - } -} - -impl std::fmt::Display for TypeCheckError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - TypeCheckError::UnknownIdentifier { name, .. } => { - write!(f, "Cannot find name '{name}'") - } - TypeCheckError::ParseError { message, .. } => { - write!(f, "Parse error: {message}") - } - TypeCheckError::SemanticError { message, .. } => { - write!(f, "Semantic error: {message}") - } - TypeCheckError::UnusedVariable { name, .. } => { - write!(f, "Variable '{name}' is declared but never used") - } - } - } -} - -impl std::error::Error for TypeCheckError {} - -/// JSON-friendly view of a single diagnostic, for `--json` output. -#[derive(Debug, Serialize)] -pub struct JsonDiagnostic { - pub path: String, - pub code: &'static str, - pub message: String, - pub line: usize, - pub column: usize, - pub offset: usize, - pub length: usize, -} - -impl JsonDiagnostic { - fn from_error(path: &Path, source: &str, err: &TypeCheckError) -> Self { - let span = err.span(); - let offset = span.offset(); - let length = span.len(); - let (line, column) = offset_to_line_column(source, offset); - Self { - path: path.display().to_string(), - code: err.code(), - message: err.to_string(), - line, - column, - offset, - length, - } - } -} - -fn offset_to_line_column(source: &str, offset: usize) -> (usize, usize) { - let mut line = 1usize; - let mut col = 1usize; - let mut byte = 0usize; - for ch in source.chars() { - if byte >= offset { - break; - } - if ch == '\n' { - line += 1; - col = 1; - } else { - col += 1; - } - byte += ch.len_utf8(); - } - (line, col) -} - -/// Names that are always considered globally available, so they should not trigger `UnknownIdentifier` -const BUILTIN_GLOBALS: &[&str] = &[ - "globalThis", - "undefined", - "NaN", - "Infinity", - "this", - "arguments", - "eval", - "Object", - "Function", - "Array", - "String", - "Boolean", - "Number", - "BigInt", - "Symbol", - "Date", - "RegExp", - "Error", - "TypeError", - "RangeError", - "SyntaxError", - "ReferenceError", - "EvalError", - "URIError", - "AggregateError", - "Promise", - "Proxy", - "Reflect", - "JSON", - "Math", - "Map", - "Set", - "WeakMap", - "WeakSet", - "WeakRef", - "FinalizationRegistry", - "Iterator", - "AsyncIterator", - "ArrayBuffer", - "SharedArrayBuffer", - "DataView", - "Atomics", - "Int8Array", - "Uint8Array", - "Uint8ClampedArray", - "Int16Array", - "Uint16Array", - "Int32Array", - "Uint32Array", - "Float16Array", - "Float32Array", - "Float64Array", - "BigInt64Array", - "BigUint64Array", - "parseInt", - "parseFloat", - "isNaN", - "isFinite", - "encodeURI", - "encodeURIComponent", - "decodeURI", - "decodeURIComponent", - "console", - "self", - "performance", - "setTimeout", - "setInterval", - "clearTimeout", - "clearInterval", - "queueMicrotask", - "structuredClone", - "fetch", - "Request", - "Response", - "Headers", - "URL", - "URLPattern", - "URLSearchParams", - "TextEncoder", - "TextEncoderStream", - "TextDecoder", - "TextDecoderStream", - "ReadableStream", - "WritableStream", - "TransformStream", - "ByteLengthQueuingStrategy", - "CountQueuingStrategy", - "Blob", - "File", - "FormData", - "AbortController", - "AbortSignal", - "Event", - "EventTarget", - "CustomEvent", - "MessageEvent", - "ErrorEvent", - "CloseEvent", - "BroadcastChannel", - "MessageChannel", - "MessagePort", - "crypto", - "Crypto", - "SubtleCrypto", - "CryptoKey", - "atob", - "btoa", - "alert", - "confirm", - "prompt", - "localStorage", - "sessionStorage", - "Storage", - "WebSocket", - "Worker", - "DOMException", - "Navigator", - "navigator", - "clientInformation", - "Image", - "ImageData", - "ImageBitmap", - "OffscreenCanvas", - "Path2D", - "CanvasGradient", - "CanvasPattern", - "CanvasRenderingContext2D", - "DOMMatrix", - "DOMMatrixReadOnly", - "TextMetrics", - "createImageBitmap", - "Database", - "DatabaseSync", - "StatementSync", - "sqlite", - "Cron", - "Window", - "createWindow", - "Andromeda", - "__andromeda__", - "assert", - "assertEquals", - "assertNotEquals", - "assertThrows", - "constants", - "exports", -]; - -fn known_globals() -> &'static HashSet<&'static str> { - use std::sync::OnceLock; - static GLOBALS: OnceLock> = OnceLock::new(); - GLOBALS.get_or_init(|| BUILTIN_GLOBALS.iter().copied().collect()) -} - -fn is_known_global(name: &str) -> bool { - known_globals().contains(name) -} - -/// Type check file content directly -#[allow(clippy::result_large_err)] -pub fn check_file_content_with_config( - path: &PathBuf, - content: &str, - _config_override: Option, -) -> CliResult> { - let allocator = Allocator::default(); - let source_type = SourceType::from_path(path).unwrap_or_default(); - - if !source_type.is_typescript() { - return Ok(Vec::new()); - } - - let is_declaration = source_type.is_typescript_definition(); - - let ret = Parser::new(&allocator, content, source_type).parse(); - let program = &ret.program; - let mut type_errors: Vec = Vec::new(); - - let source_name = path.display().to_string(); - let named_source = NamedSource::new(source_name, content.to_string()); - - for error in &ret.errors { - if let Some(labels) = &error.labels - && let Some(label) = labels.first() - { - let span = SourceSpan::new(label.offset().into(), label.len()); - type_errors.push(TypeCheckError::ParseError { - message: error.to_string(), - span, - source_code: named_source.clone(), - }); - } - } - - let semantic_ret = SemanticBuilder::new() - .with_check_syntax_error(true) - .with_cfg(true) - .build(program); - - let semantic = &semantic_ret.semantic; - let scoping = semantic.scoping(); - - let mut unresolved_spans: HashSet<(u32, u32)> = HashSet::new(); - - for reference_id_list in scoping.root_unresolved_references_ids() { - for reference_id in reference_id_list { - let reference = scoping.get_reference(reference_id); - - if reference.flags().is_type_only() { - continue; - } - - let name = semantic.reference_name(reference); - - if is_known_global(name) { - continue; - } - - let ref_span = semantic.reference_span(reference); - let key = (ref_span.start, ref_span.end); - if !unresolved_spans.insert(key) { - continue; - } - - let span = SourceSpan::new((ref_span.start as usize).into(), ref_span.size() as usize); - - type_errors.push(TypeCheckError::UnknownIdentifier { - name: name.to_string(), - span, - source_code: named_source.clone(), - }); - } - } - - for error in &semantic_ret.errors { - let Some(labels) = &error.labels else { - continue; - }; - let Some(label) = labels.first() else { - continue; - }; - - let start = label.offset() as u32; - let end = start + label.len() as u32; - if unresolved_spans.contains(&(start, end)) { - continue; - } - - let span = SourceSpan::new(label.offset().into(), label.len()); - type_errors.push(TypeCheckError::SemanticError { - message: error.to_string(), - span, - source_code: named_source.clone(), - }); - } - - if !is_declaration { - for symbol_id in scoping.symbol_ids() { - if !scoping.symbol_is_unused(symbol_id) { - continue; - } - let name = scoping.symbol_name(symbol_id); - if name.starts_with('_') { - continue; - } - let symbol_span = scoping.symbol_span(symbol_id); - let span = SourceSpan::new( - (symbol_span.start as usize).into(), - symbol_span.size() as usize, - ); - type_errors.push(TypeCheckError::UnusedVariable { - name: name.to_string(), - span, - source_code: named_source.clone(), - }); - } - } - - Ok(type_errors) -} - -/// Output format for `andromeda check`. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum CheckOutputFormat { - Pretty, - Json, - Quiet, -} - -/// Type check multiple files -#[allow(clippy::result_large_err, dead_code)] -#[hotpath::measure] -pub fn check_files_with_config( - paths: &[PathBuf], - config_override: Option, -) -> CliResult<()> { - check_files_with_options(paths, config_override, CheckOutputFormat::Pretty) -} - -#[allow(clippy::result_large_err)] -pub fn check_files_with_options( - paths: &[PathBuf], - config_override: Option, - format: CheckOutputFormat, -) -> CliResult<()> { - let files_to_check: Vec = if paths.is_empty() { - find_formattable_files(&[PathBuf::from(".")]) - .unwrap_or_default() - .into_iter() - .filter(|path| { - let source_type = SourceType::from_path(path).unwrap_or_default(); - source_type.is_typescript() - }) - .collect() - } else { - find_formattable_files(paths) - .unwrap_or_default() - .into_iter() - .filter(|path| { - let source_type = SourceType::from_path(path).unwrap_or_default(); - source_type.is_typescript() || source_type.is_javascript() - }) - .collect() - }; - - if files_to_check.is_empty() { - if format == CheckOutputFormat::Pretty { - let warning = Style::new().yellow().apply_to("Warning"); - eprintln!("{warning} No matching files found."); - } - return Ok(()); - } - - let mut total_errors = 0usize; - let mut files_with_read_errors = 0usize; - - for path in &files_to_check { - if format == CheckOutputFormat::Pretty { - let label = Style::new().green().apply_to("Check"); - eprintln!("{label} {}", path.display()); - } - - let content = match fs::read_to_string(path) { - Ok(c) => c, - Err(e) => { - files_with_read_errors += 1; - if format == CheckOutputFormat::Pretty { - let prefix = Style::new().red().bold().apply_to("error"); - eprintln!("{prefix}: Failed to read {}: {e}", path.display()); - } - continue; - } - }; - - match check_file_content_with_config(path, &content, config_override.clone()) { - Ok(type_errors) => { - total_errors += type_errors.len(); - emit_results(path, &content, &type_errors, format); - } - Err(e) => { - total_errors += 1; - if format == CheckOutputFormat::Pretty { - let prefix = Style::new().red().bold().apply_to("error"); - eprintln!("{prefix}: Failed to type-check {}: {e}", path.display()); - } - } - } - } - - if format == CheckOutputFormat::Pretty && total_errors > 0 { - eprintln!(); - let plural = if total_errors == 1 { "error" } else { "errors" }; - eprintln!("Found {total_errors} {plural}."); - } - - if total_errors > 0 || files_with_read_errors > 0 { - std::process::exit(1); - } - - Ok(()) -} - -/// Maximum number of type errors to display per file before truncating. -const MAX_DISPLAY_ERRORS: usize = 20; - -fn emit_results( - path: &Path, - content: &str, - type_errors: &[TypeCheckError], - format: CheckOutputFormat, -) { - match format { - CheckOutputFormat::Pretty => display_type_check_results(type_errors), - CheckOutputFormat::Json => emit_json_results(path, content, type_errors), - CheckOutputFormat::Quiet => {} - } -} - -fn emit_json_results(path: &Path, content: &str, type_errors: &[TypeCheckError]) { - for err in type_errors { - let diag = JsonDiagnostic::from_error(path, content, err); - if let Ok(line) = serde_json::to_string(&diag) { - println!("{line}"); - } - } -} - -/// Display type check diagnostics. Silent when there are none. -fn display_type_check_results(type_errors: &[TypeCheckError]) { - if type_errors.is_empty() { - return; - } - - let errors_to_show = type_errors.len().min(MAX_DISPLAY_ERRORS); - - for error in type_errors.iter().take(errors_to_show) { - let report = oxc_miette::Report::new(error.clone()); - eprintln!("{report:?}"); - } - - let remaining = type_errors.len() - errors_to_show; - if remaining > 0 { - let plural = if remaining == 1 { "" } else { "s" }; - eprintln!("... {remaining} more diagnostic{plural} not shown."); - } -} diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index d68b051..1a703d3 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -8,7 +8,6 @@ //! including running, compiling, formatting, linting, and bundling JavaScript/TypeScript code. pub mod bundle; -pub mod check; pub mod compile; pub mod config; pub mod error; diff --git a/crates/cli/src/lint.rs b/crates/cli/src/lint.rs index 7aa2174..538343e 100644 --- a/crates/cli/src/lint.rs +++ b/crates/cli/src/lint.rs @@ -1,68 +1,11 @@ // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. // If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. -//! # Andromeda Linter -//! -//! A comprehensive JavaScript/TypeScript linter with 27+ rules inspired by ESLint, Deno, and oxc_linter. -//! -//! ## Implemented Rules -//! -//! ### Code Quality Rules -//! - **no-empty** - Disallow empty statements -//! - **no-var** - Require let or const instead of var -//! - **no-unused-vars** - Disallow unused variables -//! - **prefer-const** - Require const declarations for variables that are never reassigned -//! - **camelcase** - Enforce camelCase naming convention -//! - **no-eval** - Disallow use of eval() -//! -//! ### Error Prevention Rules -//! - **no-debugger** - Disallow debugger statements -//! - **no-console** - Disallow console statements -//! - **no-unreachable** - Disallow unreachable code after return, throw, break, or continue -//! - **no-duplicate-case** - Disallow duplicate case labels in switch statements -//! - **no-constant-condition** - Disallow constant expressions in conditions -//! - **no-dupe-keys** - Disallow duplicate keys in object literals -//! - **no-const-assign** - Disallow reassigning const variables -//! - **no-func-assign** - Disallow reassigning function declarations -//! - **no-ex-assign** - Disallow reassigning exception parameters in catch clauses -//! -//! ### Best Practices Rules -//! - **eqeqeq** - Require === and !== instead of == and != -//! - **no-compare-neg-zero** - Disallow comparing against -0 -//! - **no-cond-assign** - Disallow assignment operators in conditional expressions -//! - **use-isnan** - Require calls to isNaN() when checking for NaN -//! - **no-fallthrough** - Disallow fallthrough of case statements -//! - **no-unsafe-negation** - Disallow negating the left operand of relational operators -//! - **no-boolean-literal-for-arguments** - Disallow boolean literals as arguments -//! -//! ### TypeScript Rules -//! - **no-explicit-any** - Disallow the any type -//! -//! ### Async/Await Rules -//! - **require-await** - Disallow async functions which have no await expression -//! - **no-async-promise-executor** - Disallow async functions as Promise executors -//! -//! ### Advanced Rules -//! - **no-sparse-arrays** - Disallow sparse array literals -//! - **no-unsafe-finally** - Disallow control flow statements in finally blocks -//! -//! ## Usage -//! -//! Rules can be configured in `.andromeda.toml`: -//! ```toml -//! [lint] -//! rules = ["no-var", "no-debugger", "eqeqeq"] -//! disabled_rules = ["no-console"] -//! max_warnings = 10 -//! ``` - #![allow(unused_assignments)] use crate::config::{AndromedaConfig, ConfigManager, LintConfig}; use crate::error::{CliError, CliResult}; -use console::Style; use miette as oxc_miette; -use owo_colors::OwoColorize; use oxc_allocator::Allocator; use oxc_ast::ast::{Expression, Statement}; use oxc_miette::{Diagnostic, NamedSource, SourceSpan}; @@ -72,21 +15,19 @@ use oxc_semantic::SymbolFlags; use oxc_span::{GetSpan, SourceType}; use std::collections::HashSet; use std::fs; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; -/// Comprehensive lint error types based on Deno's rule set with enhanced diagnostics +/// Comprehensive lint error types using rules from oxc_linter (hopefully can use oxc_linter if they publish it). #[derive(Diagnostic, Debug, Clone)] pub enum LintError { /// Empty statement found (no-empty) #[diagnostic( code(andromeda::lint::no_empty), - help( - "🔍 Remove unnecessary semicolons that create empty statements.\n💡 Empty statements can make code harder to read and may indicate errors." - ), - url("https://docs.deno.com/lint/rules/no-empty") + help("Remove unnecessary semicolons that create empty statements."), + url("https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-empty") )] NoEmpty { - #[label("Empty statement found here")] + #[label("empty statement")] span: SourceSpan, #[source_code] source_code: NamedSource, @@ -95,10 +36,8 @@ pub enum LintError { /// Usage of 'var' keyword (no-var) #[diagnostic( code(andromeda::lint::no_var), - help( - "🔍 Replace 'var' with 'let' or 'const' for better scoping.\n💡 'var' has function-level scoping which can lead to unexpected behavior.\n📖 Use 'let' for variables that will be reassigned, 'const' for constants." - ), - url("https://docs.deno.com/lint/rules/no-var") + help("Use 'let' for variables that will be reassigned, or 'const' for constants."), + url("https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-var") )] NoVar { #[label("'var' keyword used here")] @@ -111,13 +50,11 @@ pub enum LintError { /// Unused variable (no-unused-vars) #[diagnostic( code(andromeda::lint::no_unused_vars), - help( - "🔍 Remove the unused variable or prefix it with '_' if intentionally unused.\n💡 Unused variables can indicate dead code or typos in variable names.\n🧹 Removing unused variables helps keep code clean and maintainable." - ), - url("https://docs.deno.com/lint/rules/no-unused-vars") + help("Remove the unused variable, or prefix it with '_' if intentionally unused."), + url("https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-unused-vars") )] NoUnusedVars { - #[label("Unused variable '{variable_name}'")] + #[label("unused variable '{variable_name}'")] span: SourceSpan, #[source_code] source_code: NamedSource, @@ -127,13 +64,11 @@ pub enum LintError { /// Variable could be const (prefer-const) #[diagnostic( code(andromeda::lint::prefer_const), - help( - "🔍 Use 'const' instead of 'let' for variables that are never reassigned.\n💡 'const' prevents accidental reassignment and makes intent clearer.\n📖 Save 'let' for variables that will be modified." - ), - url("https://docs.deno.com/lint/rules/prefer-const") + help("Use 'const' for variables that are never reassigned."), + url("https://oxc.rs/docs/guide/usage/linter/rules/eslint/prefer-const") )] PreferConst { - #[label("Variable '{variable_name}' is never reassigned, use 'const'")] + #[label("'{variable_name}' is never reassigned, use 'const'")] span: SourceSpan, #[source_code] source_code: NamedSource, @@ -143,13 +78,11 @@ pub enum LintError { /// Console.log usage (no-console) #[diagnostic( code(andromeda::lint::no_console), - help( - "🔍 Remove console statements from production code.\n💡 Console statements should not be left in production code.\n📖 Use proper logging or remove console statements." - ), - url("https://docs.deno.com/lint/rules/no-console") + help("Remove console statements from production code or use proper logging."), + url("https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-console") )] NoConsole { - #[label("Console statement found here")] + #[label("console statement")] span: SourceSpan, #[source_code] source_code: NamedSource, @@ -159,13 +92,11 @@ pub enum LintError { /// Debugger statement (no-debugger) #[diagnostic( code(andromeda::lint::no_debugger), - help( - "🔍 Remove debugger statements from production code.\n💡 Debugger statements should not be left in production code.\n🚨 This can cause applications to stop in production." - ), - url("https://docs.deno.com/lint/rules/no-debugger") + help("Remove debugger statements from production code."), + url("https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-debugger") )] NoDebugger { - #[label("Debugger statement found here")] + #[label("debugger statement")] span: SourceSpan, #[source_code] source_code: NamedSource, @@ -174,13 +105,11 @@ pub enum LintError { /// Explicit any type (no-explicit-any) #[diagnostic( code(andromeda::lint::no_explicit_any), - help( - "🔍 Use specific types instead of 'any'.\n💡 The 'any' type defeats the purpose of TypeScript.\n📖 Consider using specific types, union types, or generic constraints." - ), - url("https://docs.deno.com/lint/rules/no-explicit-any") + help("Use specific types, union types, or generic constraints instead of 'any'."), + url("https://oxc.rs/docs/guide/usage/linter/rules/typescript/no-explicit-any") )] NoExplicitAny { - #[label("Explicit 'any' type used here")] + #[label("explicit 'any' type")] span: SourceSpan, #[source_code] source_code: NamedSource, @@ -189,13 +118,11 @@ pub enum LintError { /// Missing await in async function (require-await) #[diagnostic( code(andromeda::lint::require_await), - help( - "🔍 Add await keyword or remove async from function.\n💡 Async functions should contain await expressions.\n📖 Functions without await don't need to be async." - ), - url("https://docs.deno.com/lint/rules/require-await") + help("Add an await expression, or remove the 'async' keyword."), + url("https://oxc.rs/docs/guide/usage/linter/rules/eslint/require-await") )] RequireAwait { - #[label("Async function without await")] + #[label("async function without await")] span: SourceSpan, #[source_code] source_code: NamedSource, @@ -205,13 +132,11 @@ pub enum LintError { /// Use of eval (no-eval) #[diagnostic( code(andromeda::lint::no_eval), - help( - "🔍 Avoid using eval() as it's a security risk.\n💡 eval() can execute arbitrary code and is a security vulnerability.\n🚨 Consider alternative approaches for dynamic code execution." - ), - url("https://docs.deno.com/lint/rules/no-eval") + help("eval() executes arbitrary code; use a safer alternative."), + url("https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-eval") )] NoEval { - #[label("eval() usage found here")] + #[label("eval() usage")] span: SourceSpan, #[source_code] source_code: NamedSource, @@ -220,61 +145,41 @@ pub enum LintError { /// Loose equality comparison (eqeqeq) #[diagnostic( code(andromeda::lint::eqeqeq), - help( - "🔍 Use strict equality (=== or !==) instead of loose equality (== or !=).\n💡 Strict equality prevents type coercion bugs.\n📖 Use === and !== for safer comparisons." - ), - url("https://docs.deno.com/lint/rules/eqeqeq") + help("Use strict equality (=== or !==) to avoid type coercion bugs."), + url("https://oxc.rs/docs/guide/usage/linter/rules/eslint/eqeqeq") )] Eqeqeq { - #[label("Use strict equality (=== or !==) instead")] + #[label("use strict equality (=== or !==) instead")] span: SourceSpan, #[source_code] source_code: NamedSource, operator: String, }, - /// Camelcase naming convention (camelcase) + /// Camelcase naming convention (camelcase). + /// + /// oxc does not implement stylistic rules, so the documentation link points at the upstream Deno lint reference. #[diagnostic( code(andromeda::lint::camelcase), - help( - "🔍 Use camelCase naming convention.\n💡 Consistent naming improves code readability.\n📖 Use camelCase for variables, functions, and methods." - ), + help("Use camelCase for variables, functions, and methods."), url("https://docs.deno.com/lint/rules/camelcase") )] Camelcase { - #[label("Identifier '{name}' is not in camelCase")] + #[label("identifier '{name}' is not in camelCase")] span: SourceSpan, #[source_code] source_code: NamedSource, name: String, }, - /// Boolean literal as argument (no-boolean-literal-for-arguments) - #[diagnostic( - code(andromeda::lint::no_boolean_literal_for_arguments), - help( - "🔍 Avoid passing boolean literals as arguments.\n💡 Boolean arguments make code harder to understand.\n📖 Consider using named objects or enums instead." - ), - url("https://docs.deno.com/lint/rules/no-boolean-literal-for-arguments") - )] - NoBooleanLiteralForArguments { - #[label("Boolean literal passed as argument")] - span: SourceSpan, - #[source_code] - source_code: NamedSource, - value: bool, - }, - /// Unreachable code (no-unreachable) #[diagnostic( code(andromeda::lint::no_unreachable), - help( - "🔍 Remove unreachable code after return, throw, break, or continue.\n💡 Code after these statements will never execute.\n🧹 This usually indicates a logical error or dead code." - ), - url("https://eslint.org/docs/latest/rules/no-unreachable") + help("Remove unreachable code after return, throw, break, or continue."), + url("https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-unreachable") )] NoUnreachable { - #[label("Unreachable code detected")] + #[label("unreachable code")] span: SourceSpan, #[source_code] source_code: NamedSource, @@ -283,13 +188,11 @@ pub enum LintError { /// Duplicate case label (no-duplicate-case) #[diagnostic( code(andromeda::lint::no_duplicate_case), - help( - "🔍 Remove duplicate case labels in switch statements.\n💡 Duplicate cases will never be reached.\n🐛 This is likely a copy-paste error." - ), - url("https://eslint.org/docs/latest/rules/no-duplicate-case") + help("Remove or rename the duplicate case label."), + url("https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-duplicate-case") )] NoDuplicateCase { - #[label("Duplicate case label")] + #[label("duplicate case label")] span: SourceSpan, #[source_code] source_code: NamedSource, @@ -298,13 +201,11 @@ pub enum LintError { /// Constant condition (no-constant-condition) #[diagnostic( code(andromeda::lint::no_constant_condition), - help( - "🔍 Avoid using constant expressions in conditions.\n💡 Constant conditions make branches unreachable.\n📖 Use meaningful boolean expressions instead." - ), - url("https://eslint.org/docs/latest/rules/no-constant-condition") + help("Use a meaningful boolean expression instead of a constant condition."), + url("https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-constant-condition") )] NoConstantCondition { - #[label("Constant condition detected")] + #[label("constant condition")] span: SourceSpan, #[source_code] source_code: NamedSource, @@ -313,13 +214,11 @@ pub enum LintError { /// Duplicate keys in object literals (no-dupe-keys) #[diagnostic( code(andromeda::lint::no_dupe_keys), - help( - "🔍 Remove duplicate keys in object literals.\n💡 Later keys overwrite earlier ones silently.\n🐛 This often indicates a typo or logical error." - ), - url("https://eslint.org/docs/latest/rules/no-dupe-keys") + help("Remove the duplicate key; later keys silently overwrite earlier ones."), + url("https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-dupe-keys") )] NoDupeKeys { - #[label("Duplicate key '{key}'")] + #[label("duplicate key '{key}'")] span: SourceSpan, #[source_code] source_code: NamedSource, @@ -329,13 +228,11 @@ pub enum LintError { /// Comparing against -0 (no-compare-neg-zero) #[diagnostic( code(andromeda::lint::no_compare_neg_zero), - help( - "🔍 Use Object.is(x, -0) to check for negative zero.\n💡 Regular equality doesn't distinguish between 0 and -0.\n📖 This can lead to unexpected behavior in some cases." - ), - url("https://eslint.org/docs/latest/rules/no-compare-neg-zero") + help("Use Object.is(x, -0) to detect negative zero."), + url("https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-compare-neg-zero") )] NoCompareNegZero { - #[label("Comparing against -0")] + #[label("comparing against -0")] span: SourceSpan, #[source_code] source_code: NamedSource, @@ -344,13 +241,11 @@ pub enum LintError { /// Assignment in conditional (no-cond-assign) #[diagnostic( code(andromeda::lint::no_cond_assign), - help( - "🔍 Avoid assignments in conditional expressions.\n💡 This is often a typo where == was intended instead of =.\n📖 If intentional, wrap the assignment in parentheses." - ), - url("https://eslint.org/docs/latest/rules/no-cond-assign") + help("Use '==' or wrap the assignment in extra parentheses if intentional."), + url("https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-cond-assign") )] NoCondAssign { - #[label("Assignment in conditional expression")] + #[label("assignment in conditional expression")] span: SourceSpan, #[source_code] source_code: NamedSource, @@ -359,13 +254,11 @@ pub enum LintError { /// Const reassignment (no-const-assign) #[diagnostic( code(andromeda::lint::no_const_assign), - help( - "🔍 Cannot reassign const variable '{variable_name}'.\n💡 Const variables cannot be reassigned after declaration.\n🐛 Use 'let' if you need to reassign the variable." - ), - url("https://eslint.org/docs/latest/rules/no-const-assign") + help("Use 'let' if the variable needs to be reassigned."), + url("https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-const-assign") )] NoConstAssign { - #[label("Reassignment to const variable '{variable_name}'")] + #[label("reassignment to const variable '{variable_name}'")] span: SourceSpan, #[source_code] source_code: NamedSource, @@ -375,13 +268,11 @@ pub enum LintError { /// Use isNaN for NaN checks (use-isnan) #[diagnostic( code(andromeda::lint::use_isnan), - help( - "🔍 Use Number.isNaN() or isNaN() to check for NaN.\n💡 NaN is never equal to itself, so comparisons will always be false.\n📖 Use isNaN(x) or Number.isNaN(x) instead." - ), - url("https://eslint.org/docs/latest/rules/use-isnan") + help("Use Number.isNaN(x) or isNaN(x); NaN is never equal to itself."), + url("https://oxc.rs/docs/guide/usage/linter/rules/eslint/use-isnan") )] UseIsNan { - #[label("Use isNaN() instead of comparing to NaN")] + #[label("use isNaN() instead of comparing to NaN")] span: SourceSpan, #[source_code] source_code: NamedSource, @@ -390,13 +281,11 @@ pub enum LintError { /// Missing break in switch case (no-fallthrough) #[diagnostic( code(andromeda::lint::no_fallthrough), - help( - "🔍 Add break, return, or throw at the end of this case.\n💡 Fallthrough cases can lead to unexpected behavior.\n📖 Add a comment '// fallthrough' if intentional." - ), - url("https://eslint.org/docs/latest/rules/no-fallthrough") + help("Add break, return, or throw; or add a '// fallthrough' comment if intentional."), + url("https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-fallthrough") )] NoFallthrough { - #[label("Case falls through without break/return/throw")] + #[label("case falls through without break/return/throw")] span: SourceSpan, #[source_code] source_code: NamedSource, @@ -405,13 +294,11 @@ pub enum LintError { /// Function reassignment (no-func-assign) #[diagnostic( code(andromeda::lint::no_func_assign), - help( - "🔍 Avoid reassigning function declarations.\n💡 Reassigning functions can lead to confusing code.\n🐛 This may indicate a logical error." - ), - url("https://eslint.org/docs/latest/rules/no-func-assign") + help("Avoid reassigning function declarations."), + url("https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-func-assign") )] NoFuncAssign { - #[label("Reassignment to function '{function_name}'")] + #[label("reassignment to function '{function_name}'")] span: SourceSpan, #[source_code] source_code: NamedSource, @@ -421,13 +308,11 @@ pub enum LintError { /// Unsafe negation (no-unsafe-negation) #[diagnostic( code(andromeda::lint::no_unsafe_negation), - help( - "🔍 Use parentheses to clarify negation intent.\n💡 Negating the left operand of relational operators is often a mistake.\n📖 Did you mean !(a in b) instead of !a in b?" - ), - url("https://eslint.org/docs/latest/rules/no-unsafe-negation") + help("Did you mean !(a in b) instead of !a in b? Use parentheses to clarify intent."), + url("https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-unsafe-negation") )] NoUnsafeNegation { - #[label("Unsafe negation of left operand")] + #[label("unsafe negation of left operand")] span: SourceSpan, #[source_code] source_code: NamedSource, @@ -436,13 +321,11 @@ pub enum LintError { /// Sparse arrays (no-sparse-arrays) #[diagnostic( code(andromeda::lint::no_sparse_arrays), - help( - "🔍 Remove extra commas in array literals.\n💡 Sparse arrays have undefined 'holes' which can cause bugs.\n📖 Use explicit undefined values if needed." - ), - url("https://eslint.org/docs/latest/rules/no-sparse-arrays") + help("Remove the extra comma, or use an explicit 'undefined' value."), + url("https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-sparse-arrays") )] NoSparseArrays { - #[label("Sparse array detected")] + #[label("sparse array")] span: SourceSpan, #[source_code] source_code: NamedSource, @@ -451,13 +334,11 @@ pub enum LintError { /// Exception parameter reassignment (no-ex-assign) #[diagnostic( code(andromeda::lint::no_ex_assign), - help( - "🔍 Avoid reassigning exception parameters in catch clauses.\n💡 This can lead to confusing code and lost error information.\n📖 Use a different variable if you need to modify the value." - ), - url("https://eslint.org/docs/latest/rules/no-ex-assign") + help("Use a different variable if you need to modify the value."), + url("https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-ex-assign") )] NoExAssign { - #[label("Reassignment to exception parameter")] + #[label("reassignment to exception parameter")] span: SourceSpan, #[source_code] source_code: NamedSource, @@ -466,13 +347,11 @@ pub enum LintError { /// Async Promise executor (no-async-promise-executor) #[diagnostic( code(andromeda::lint::no_async_promise_executor), - help( - "🔍 Don't use async functions as Promise executors.\n💡 Async executors can hide errors and lead to unhandled rejections.\n📖 Use regular functions and return promises explicitly." - ), - url("https://eslint.org/docs/latest/rules/no-async-promise-executor") + help("Use a regular function and return a promise explicitly."), + url("https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-async-promise-executor") )] NoAsyncPromiseExecutor { - #[label("Async function used as Promise executor")] + #[label("async function used as Promise executor")] span: SourceSpan, #[source_code] source_code: NamedSource, @@ -481,13 +360,11 @@ pub enum LintError { /// Unsafe finally (no-unsafe-finally) #[diagnostic( code(andromeda::lint::no_unsafe_finally), - help( - "🔍 Avoid return, throw, break, or continue in finally blocks.\n💡 Control flow statements in finally can override earlier returns/throws.\n🐛 This can mask errors and lead to unexpected behavior." - ), - url("https://eslint.org/docs/latest/rules/no-unsafe-finally") + help("Avoid return, throw, break, or continue inside a finally block."), + url("https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-unsafe-finally") )] NoUnsafeFinally { - #[label("Unsafe control flow in finally block")] + #[label("unsafe control flow in finally block")] span: SourceSpan, #[source_code] source_code: NamedSource, @@ -522,9 +399,6 @@ impl std::fmt::Display for LintError { LintError::Camelcase { name, .. } => { write!(f, "Identifier '{name}' is not in camelCase") } - LintError::NoBooleanLiteralForArguments { value, .. } => { - write!(f, "Boolean literal '{value}' passed as argument") - } LintError::NoUnreachable { .. } => write!(f, "Unreachable code detected"), LintError::NoDuplicateCase { .. } => write!(f, "Duplicate case label in switch"), LintError::NoConstantCondition { .. } => write!(f, "Constant condition in expression"), @@ -553,9 +427,6 @@ impl std::fmt::Display for LintError { impl std::error::Error for LintError {} /// Helper function to check if a lint rule should be applied -/// Rules are enabled if: -/// 1. They are explicitly in the `rules` list, OR -/// 2. They are in the default enabled rules list AND not in the `disabled_rules` list fn is_rule_enabled(rule_name: &str, lint_config: &LintConfig) -> bool { // If disabled_rules contains the rule, it's disabled if lint_config.disabled_rules.contains(&rule_name.to_string()) { @@ -576,7 +447,6 @@ fn is_rule_enabled(rule_name: &str, lint_config: &LintConfig) -> bool { "no-unused-vars", // "camelcase", // "no-console" - "no-boolean-literal-for-arguments", "no-explicit-any", "require-await", "no-eval", @@ -677,23 +547,6 @@ fn check_expression_for_issues( }); } - // Check for boolean literals as arguments (no-boolean-literal-for-arguments) - if is_rule_enabled("no-boolean-literal-for-arguments", lint_config) { - for arg in &call.arguments { - if let Some(Expression::BooleanLiteral(bool_lit)) = arg.as_expression() { - let span = SourceSpan::new( - (bool_lit.span.start as usize).into(), - bool_lit.span.size() as usize, - ); - lint_errors.push(LintError::NoBooleanLiteralForArguments { - span, - source_code: named_source.clone(), - value: bool_lit.value, - }); - } - } - } - // Recursively check arguments for arg in &call.arguments { if let Some(expr) = arg.as_expression() { @@ -1126,7 +979,7 @@ fn check_unreachable_code( } } -/// Check for duplicate case labels in switch statements +/// Check for duplicate case labels in switch statements. fn check_duplicate_cases( switch_stmt: &oxc_ast::ast::SwitchStatement, named_source: &NamedSource, @@ -1137,27 +990,27 @@ fn check_duplicate_cases( return; } - use std::collections::HashSet; - let mut seen_cases = HashSet::new(); + use oxc_span::ContentEq; - for case in &switch_stmt.cases { - if let Some(test) = &case.test { - let case_str = format!("{:?}", test); - if !seen_cases.insert(case_str) { - let span = SourceSpan::new( - (test.span().start as usize).into(), - test.span().size() as usize, - ); - lint_errors.push(LintError::NoDuplicateCase { - span, - source_code: named_source.clone(), - }); - } + let mut previous_tests: Vec<&Expression<'_>> = Vec::new(); + for test in switch_stmt.cases.iter().filter_map(|c| c.test.as_ref()) { + let test = test.without_parentheses(); + if previous_tests.iter().any(|prev| prev.content_eq(test)) { + let span = SourceSpan::new( + (test.span().start as usize).into(), + test.span().size() as usize, + ); + lint_errors.push(LintError::NoDuplicateCase { + span, + source_code: named_source.clone(), + }); + } else { + previous_tests.push(test); } } } -/// Check for constant conditions +/// Check for constant conditions. fn check_constant_condition( test_expr: &Expression, named_source: &NamedSource, @@ -1168,24 +1021,59 @@ fn check_constant_condition( return; } - match test_expr { + if is_constant_expression(test_expr) { + let span = SourceSpan::new( + (test_expr.span().start as usize).into(), + test_expr.span().size() as usize, + ); + lint_errors.push(LintError::NoConstantCondition { + span, + source_code: named_source.clone(), + }); + } +} + +/// Returns `true` when an expression always evaluates to a value with a statically-known truthiness +fn is_constant_expression(expr: &Expression) -> bool { + use oxc_ast::ast::{LogicalOperator, UnaryOperator}; + + match expr { Expression::BooleanLiteral(_) | Expression::NumericLiteral(_) - | Expression::StringLiteral(_) => { - let span = SourceSpan::new( - (test_expr.span().start as usize).into(), - test_expr.span().size() as usize, - ); - lint_errors.push(LintError::NoConstantCondition { - span, - source_code: named_source.clone(), - }); + | Expression::BigIntLiteral(_) + | Expression::StringLiteral(_) + | Expression::NullLiteral(_) + | Expression::RegExpLiteral(_) + | Expression::ObjectExpression(_) + | Expression::ArrayExpression(_) + | Expression::FunctionExpression(_) + | Expression::ArrowFunctionExpression(_) + | Expression::ClassExpression(_) => true, + Expression::ParenthesizedExpression(p) => is_constant_expression(&p.expression), + Expression::UnaryExpression(u) => match u.operator { + // `typeof x` is not constant — it depends on the binding's existence. + UnaryOperator::Typeof => false, + _ => is_constant_expression(&u.argument), + }, + Expression::LogicalExpression(l) => match l.operator { + LogicalOperator::And | LogicalOperator::Or | LogicalOperator::Coalesce => { + is_constant_expression(&l.left) && is_constant_expression(&l.right) + } + }, + Expression::ConditionalExpression(c) => { + is_constant_expression(&c.test) + && is_constant_expression(&c.consequent) + && is_constant_expression(&c.alternate) } - _ => {} + Expression::SequenceExpression(s) => { + s.expressions.last().is_some_and(is_constant_expression) + } + Expression::TemplateLiteral(t) => t.expressions.iter().all(is_constant_expression), + _ => false, } } -/// Check for duplicate keys in object literals +/// Check for duplicate keys in object literals. fn check_dupe_keys( obj_expr: &oxc_ast::ast::ObjectExpression, named_source: &NamedSource, @@ -1196,25 +1084,43 @@ fn check_dupe_keys( return; } + use oxc_ast::ast::{ObjectProperty, ObjectPropertyKind, PropertyKind}; use std::collections::HashMap; - let mut seen_keys: HashMap = HashMap::new(); + + fn is_proto_setter_property(prop: &ObjectProperty<'_>, name: &str) -> bool { + name == "__proto__" + && prop.kind == PropertyKind::Init + && !prop.computed + && !prop.shorthand + && !prop.method + } + + let mut seen_keys: HashMap = HashMap::new(); for prop in &obj_expr.properties { - if let oxc_ast::ast::ObjectPropertyKind::ObjectProperty(obj_prop) = prop - && let oxc_ast::ast::PropertyKey::StaticIdentifier(ident) = &obj_prop.key + let ObjectPropertyKind::ObjectProperty(obj_prop) = prop else { + continue; + }; + let Some(name) = obj_prop.key.static_name() else { + continue; + }; + if is_proto_setter_property(obj_prop, &name) { + continue; + } + + let key_name = name.into_owned(); + if let Some(prev_kind) = seen_keys.insert(key_name.clone(), obj_prop.kind) + && (prev_kind == PropertyKind::Init + || obj_prop.kind == PropertyKind::Init + || prev_kind == obj_prop.kind) { - let key_name = ident.name.to_string(); - if seen_keys.insert(key_name.clone(), ident.span).is_some() { - let span = SourceSpan::new( - (ident.span.start as usize).into(), - ident.span.size() as usize, - ); - lint_errors.push(LintError::NoDupeKeys { - span, - source_code: named_source.clone(), - key: key_name, - }); - } + let key_span = obj_prop.key.span(); + let span = SourceSpan::new((key_span.start as usize).into(), key_span.size() as usize); + lint_errors.push(LintError::NoDupeKeys { + span, + source_code: named_source.clone(), + key: key_name, + }); } } } @@ -1252,7 +1158,10 @@ fn check_compare_neg_zero( } } -/// Check for NaN comparisons +/// Check for NaN comparisons. +/// +/// Mirrors oxc_linter's `eslint/use-isnan` identifier detection: both the +/// global `NaN` binding and the `Number.NaN` member access are recognized. fn check_nan_comparison( bin_expr: &oxc_ast::ast::BinaryExpression, named_source: &NamedSource, @@ -1263,15 +1172,7 @@ fn check_nan_comparison( return; } - let is_nan = |expr: &Expression| -> bool { - if let Expression::Identifier(ident) = expr { - ident.name == "NaN" - } else { - false - } - }; - - if is_nan(&bin_expr.left) || is_nan(&bin_expr.right) { + if is_nan_identifier(&bin_expr.left) || is_nan_identifier(&bin_expr.right) { let span = SourceSpan::new( (bin_expr.span.start as usize).into(), bin_expr.span.size() as usize, @@ -1283,6 +1184,12 @@ fn check_nan_comparison( } } +/// Returns `true` if `expr` resolves to the `NaN` value at runtime — either the bare `NaN` global or `Number.NaN`. +fn is_nan_identifier(expr: &Expression) -> bool { + let expr = expr.get_inner_expression(); + expr.is_specific_id("NaN") || expr.is_specific_member_access("Number", "NaN") +} + /// Check for sparse arrays fn check_sparse_arrays( array_expr: &oxc_ast::ast::ArrayExpression, @@ -1800,30 +1707,103 @@ fn check_for_reassignments(stmt: &Statement, reassigned_variables: &mut HashSet< Statement::ExpressionStatement(expr_stmt) => { check_expression_for_reassignments(&expr_stmt.expression, reassigned_variables); } + Statement::VariableDeclaration(decl) => { + for declarator in &decl.declarations { + if let Some(init) = &declarator.init { + check_expression_for_reassignments(init, reassigned_variables); + } + } + } + Statement::ReturnStatement(ret) => { + if let Some(arg) = &ret.argument { + check_expression_for_reassignments(arg, reassigned_variables); + } + } + Statement::ThrowStatement(throw) => { + check_expression_for_reassignments(&throw.argument, reassigned_variables); + } Statement::BlockStatement(block) => { for stmt in &block.body { check_for_reassignments(stmt, reassigned_variables); } } Statement::IfStatement(if_stmt) => { + check_expression_for_reassignments(&if_stmt.test, reassigned_variables); check_for_reassignments(&if_stmt.consequent, reassigned_variables); if let Some(alternate) = &if_stmt.alternate { check_for_reassignments(alternate, reassigned_variables); } } Statement::SwitchStatement(switch_stmt) => { + check_expression_for_reassignments(&switch_stmt.discriminant, reassigned_variables); for case in &switch_stmt.cases { + if let Some(test) = &case.test { + check_expression_for_reassignments(test, reassigned_variables); + } for stmt in &case.consequent { check_for_reassignments(stmt, reassigned_variables); } } } Statement::ForStatement(for_stmt) => { + if let Some(oxc_ast::ast::ForStatementInit::VariableDeclaration(decl)) = &for_stmt.init + { + for declarator in &decl.declarations { + if let Some(init) = &declarator.init { + check_expression_for_reassignments(init, reassigned_variables); + } + } + } else if let Some(init) = &for_stmt.init + && let Some(expr) = init.as_expression() + { + check_expression_for_reassignments(expr, reassigned_variables); + } + if let Some(test) = &for_stmt.test { + check_expression_for_reassignments(test, reassigned_variables); + } + if let Some(update) = &for_stmt.update { + check_expression_for_reassignments(update, reassigned_variables); + } check_for_reassignments(&for_stmt.body, reassigned_variables); } + Statement::ForInStatement(for_in) => { + check_expression_for_reassignments(&for_in.right, reassigned_variables); + check_for_reassignments(&for_in.body, reassigned_variables); + } + Statement::ForOfStatement(for_of) => { + check_expression_for_reassignments(&for_of.right, reassigned_variables); + check_for_reassignments(&for_of.body, reassigned_variables); + } Statement::WhileStatement(while_stmt) => { + check_expression_for_reassignments(&while_stmt.test, reassigned_variables); check_for_reassignments(&while_stmt.body, reassigned_variables); } + Statement::DoWhileStatement(do_while) => { + check_expression_for_reassignments(&do_while.test, reassigned_variables); + check_for_reassignments(&do_while.body, reassigned_variables); + } + Statement::TryStatement(try_stmt) => { + for stmt in &try_stmt.block.body { + check_for_reassignments(stmt, reassigned_variables); + } + if let Some(handler) = &try_stmt.handler { + for stmt in &handler.body.body { + check_for_reassignments(stmt, reassigned_variables); + } + } + if let Some(finalizer) = &try_stmt.finalizer { + for stmt in &finalizer.body { + check_for_reassignments(stmt, reassigned_variables); + } + } + } + Statement::LabeledStatement(labeled) => { + check_for_reassignments(&labeled.body, reassigned_variables); + } + Statement::WithStatement(with) => { + check_expression_for_reassignments(&with.object, reassigned_variables); + check_for_reassignments(&with.body, reassigned_variables); + } Statement::FunctionDeclaration(func) => { if let Some(body) = &func.body { for stmt in &body.statements { @@ -1847,6 +1827,7 @@ fn check_expression_for_reassignments( if let AssignmentTarget::AssignmentTargetIdentifier(id) = &assign.left { reassigned_variables.insert(id.name.to_string()); } + check_expression_for_reassignments(&assign.right, reassigned_variables); } Expression::UpdateExpression(update) => { if let oxc_ast::ast::SimpleAssignmentTarget::AssignmentTargetIdentifier(id) = @@ -1856,12 +1837,124 @@ fn check_expression_for_reassignments( } } Expression::CallExpression(call) => { + check_expression_for_reassignments(&call.callee, reassigned_variables); for arg in &call.arguments { if let Some(expr) = arg.as_expression() { check_expression_for_reassignments(expr, reassigned_variables); } } } + Expression::NewExpression(new_expr) => { + check_expression_for_reassignments(&new_expr.callee, reassigned_variables); + for arg in &new_expr.arguments { + if let Some(expr) = arg.as_expression() { + check_expression_for_reassignments(expr, reassigned_variables); + } + } + } + Expression::ArrowFunctionExpression(arrow) => { + for stmt in &arrow.body.statements { + check_for_reassignments(stmt, reassigned_variables); + } + } + Expression::FunctionExpression(func) => { + if let Some(body) = &func.body { + for stmt in &body.statements { + check_for_reassignments(stmt, reassigned_variables); + } + } + } + Expression::ParenthesizedExpression(paren) => { + check_expression_for_reassignments(&paren.expression, reassigned_variables); + } + Expression::ConditionalExpression(cond) => { + check_expression_for_reassignments(&cond.test, reassigned_variables); + check_expression_for_reassignments(&cond.consequent, reassigned_variables); + check_expression_for_reassignments(&cond.alternate, reassigned_variables); + } + Expression::BinaryExpression(bin) => { + check_expression_for_reassignments(&bin.left, reassigned_variables); + check_expression_for_reassignments(&bin.right, reassigned_variables); + } + Expression::LogicalExpression(logical) => { + check_expression_for_reassignments(&logical.left, reassigned_variables); + check_expression_for_reassignments(&logical.right, reassigned_variables); + } + Expression::UnaryExpression(unary) => { + check_expression_for_reassignments(&unary.argument, reassigned_variables); + } + Expression::AwaitExpression(await_expr) => { + check_expression_for_reassignments(&await_expr.argument, reassigned_variables); + } + Expression::YieldExpression(yield_expr) => { + if let Some(arg) = &yield_expr.argument { + check_expression_for_reassignments(arg, reassigned_variables); + } + } + Expression::SequenceExpression(seq) => { + for expr in &seq.expressions { + check_expression_for_reassignments(expr, reassigned_variables); + } + } + Expression::ArrayExpression(arr) => { + for element in &arr.elements { + if let Some(expr) = element.as_expression() { + check_expression_for_reassignments(expr, reassigned_variables); + } + } + } + Expression::ObjectExpression(obj) => { + for prop in &obj.properties { + if let oxc_ast::ast::ObjectPropertyKind::ObjectProperty(p) = prop { + check_expression_for_reassignments(&p.value, reassigned_variables); + } + } + } + Expression::TemplateLiteral(tpl) => { + for expr in &tpl.expressions { + check_expression_for_reassignments(expr, reassigned_variables); + } + } + Expression::TaggedTemplateExpression(tagged) => { + check_expression_for_reassignments(&tagged.tag, reassigned_variables); + for expr in &tagged.quasi.expressions { + check_expression_for_reassignments(expr, reassigned_variables); + } + } + Expression::ChainExpression(chain) => { + use oxc_ast::ast::ChainElement; + match &chain.expression { + ChainElement::CallExpression(call) => { + check_expression_for_reassignments(&call.callee, reassigned_variables); + for arg in &call.arguments { + if let Some(expr) = arg.as_expression() { + check_expression_for_reassignments(expr, reassigned_variables); + } + } + } + ChainElement::StaticMemberExpression(member) => { + check_expression_for_reassignments(&member.object, reassigned_variables); + } + ChainElement::ComputedMemberExpression(member) => { + check_expression_for_reassignments(&member.object, reassigned_variables); + check_expression_for_reassignments(&member.expression, reassigned_variables); + } + ChainElement::PrivateFieldExpression(member) => { + check_expression_for_reassignments(&member.object, reassigned_variables); + } + _ => {} + } + } + Expression::StaticMemberExpression(member) => { + check_expression_for_reassignments(&member.object, reassigned_variables); + } + Expression::ComputedMemberExpression(member) => { + check_expression_for_reassignments(&member.object, reassigned_variables); + check_expression_for_reassignments(&member.expression, reassigned_variables); + } + Expression::PrivateFieldExpression(member) => { + check_expression_for_reassignments(&member.object, reassigned_variables); + } _ => {} } } @@ -1972,26 +2065,21 @@ fn report_prefer_const_violations( } } -/// Lint a single JS/TS file with configuration +/// Lint a single JS/TS file with configuration. Returns the number of issues displayed. #[allow(clippy::result_large_err)] #[hotpath::measure] pub fn lint_file_with_config( path: &PathBuf, config_override: Option, -) -> CliResult<()> { +) -> CliResult { let content = fs::read_to_string(path).map_err(|e| CliError::file_read_error(path.clone(), e))?; - // Load configuration let config = config_override.unwrap_or_else(|| ConfigManager::load_or_default(None)); - match lint_file_content_with_config(path, &content, Some(config.clone())) { - Ok(lint_errors) => { - display_lint_results_with_config(path, &lint_errors, Some(&config)); - Ok(()) - } - Err(e) => Err(e), - } + let lint_errors = lint_file_content_with_config(path, &content, Some(config.clone()))?; + display_lint_results_with_config(&lint_errors, Some(&config)); + Ok(lint_errors.len()) } /// Lint file content directly with configuration (useful for LSP) @@ -2198,77 +2286,32 @@ pub fn lint_file_content_with_config( Ok(lint_errors) } -/// Display lint results to the console with configuration +/// Display lint diagnostics. Silent when there are none. fn display_lint_results_with_config( - path: &Path, lint_errors: &[LintError], config_override: Option<&AndromedaConfig>, ) { - if !lint_errors.is_empty() { - // Load configuration to check max_warnings - let default_config = ConfigManager::load_or_default(None); - let config = config_override.unwrap_or(&default_config); - let max_warnings = config.lint.max_warnings.unwrap_or(0); - - // Limit displayed errors if max_warnings is set - let errors_to_show = if max_warnings > 0 && lint_errors.len() > max_warnings as usize { - &lint_errors[..max_warnings as usize] - } else { - lint_errors - }; - - let truncated_msg = if errors_to_show.len() < lint_errors.len() { - format!(", showing first {}", errors_to_show.len()) - } else { - String::new() - }; + if lint_errors.is_empty() { + return; + } - println!(); - println!( - "{} {} ({} issue{} found{})", - "🔍".bright_yellow(), - "Lint Issues".bright_yellow().bold(), - lint_errors.len(), - if lint_errors.len() == 1 { "" } else { "s" }, - truncated_msg.bright_yellow() - ); - println!("{}", "─".repeat(60).yellow()); - - for (i, error) in errors_to_show.iter().enumerate() { - if errors_to_show.len() > 1 { - println!(); - println!( - "{} Issue {} of {}:", - "📍".cyan(), - (i + 1).to_string().bright_cyan(), - errors_to_show.len().to_string().bright_cyan() - ); - println!("{}", "─".repeat(30).cyan()); - } - println!("{:?}", oxc_miette::Report::new(error.clone())); - } - - if errors_to_show.len() < lint_errors.len() { - println!(); - println!( - "{} {} more issue{} not shown (limited by max_warnings setting)", - "⚠️".bright_yellow(), - (lint_errors.len() - errors_to_show.len()) - .to_string() - .bright_yellow(), - if lint_errors.len() - errors_to_show.len() == 1 { - "" - } else { - "s" - } - ); - } + let default_config = ConfigManager::load_or_default(None); + let config = config_override.unwrap_or(&default_config); + let max_warnings = config.lint.max_warnings.unwrap_or(0); - println!(); + let errors_to_show = if max_warnings > 0 && lint_errors.len() > max_warnings as usize { + &lint_errors[..max_warnings as usize] } else { - let ok = Style::new().green().bold().apply_to("✔"); - let file = Style::new().cyan().apply_to(path.display()); - let msg = Style::new().white().dim().apply_to("No lint issues found."); - println!("{ok} {file}: {msg}"); + lint_errors + }; + + for error in errors_to_show { + eprintln!("{:?}", oxc_miette::Report::new(error.clone())); + } + + let remaining = lint_errors.len() - errors_to_show.len(); + if remaining > 0 { + let plural = if remaining == 1 { "" } else { "s" }; + eprintln!("... {remaining} more diagnostic{plural} not shown (max_warnings limit)."); } } diff --git a/crates/cli/src/lsp/backend.rs b/crates/cli/src/lsp/backend.rs index 4d4e455..9578225 100644 --- a/crates/cli/src/lsp/backend.rs +++ b/crates/cli/src/lsp/backend.rs @@ -1,5 +1,6 @@ -// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -// If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. use crate::config::ConfigManager; use crate::lint::lint_file_content_with_config; @@ -25,81 +26,71 @@ fn get_canvas_context_completions() -> Vec { "fillStyle", CompletionItemKind::PROPERTY, "fillStyle: string | CanvasGradient", - "🎨 Sets or returns the color, gradient, or pattern used to fill the drawing.", + "Sets or returns the color, gradient, or pattern used to fill the drawing.", "fillStyle = ${1:color}", - Some("🎨"), ), create_completion_item( "strokeStyle", CompletionItemKind::PROPERTY, "strokeStyle: string", - "🖌️ Sets or returns the color, gradient, or pattern used for strokes.", + "Sets or returns the color, gradient, or pattern used for strokes.", "strokeStyle = ${1:color}", - Some("🖌️"), ), create_completion_item( "fillRect", CompletionItemKind::FUNCTION, "fillRect(x: number, y: number, width: number, height: number): void", - "🔳 Draws a filled rectangle.", + "Draws a filled rectangle.", "fillRect(${1:x}, ${2:y}, ${3:width}, ${4:height})", - Some("🔳"), ), create_completion_item( "strokeRect", CompletionItemKind::FUNCTION, "strokeRect(x: number, y: number, width: number, height: number): void", - "▭ Draws a rectangle outline.", + "Draws a rectangle outline.", "strokeRect(${1:x}, ${2:y}, ${3:width}, ${4:height})", - Some("▭"), ), create_completion_item( "beginPath", CompletionItemKind::FUNCTION, "beginPath(): void", - "🎯 Begins a new path.", + "Begins a new path.", "beginPath()", - Some("🎯"), ), create_completion_item( "moveTo", CompletionItemKind::FUNCTION, "moveTo(x: number, y: number): void", - "📍 Moves the path to the specified point.", + "Moves the path to the specified point.", "moveTo(${1:x}, ${2:y})", - Some("📍"), ), create_completion_item( "lineTo", CompletionItemKind::FUNCTION, "lineTo(x: number, y: number): void", - "📏 Adds a line to the path.", + "Adds a line to the path.", "lineTo(${1:x}, ${2:y})", - Some("📏"), ), create_completion_item( "arc", CompletionItemKind::FUNCTION, "arc(x: number, y: number, radius: number, startAngle: number, endAngle: number): void", - "⭕ Adds an arc to the path.", + "Adds an arc to the path.", "arc(${1:x}, ${2:y}, ${3:radius}, ${4:startAngle}, ${5:endAngle})", - Some("⭕"), ), create_completion_item( "fill", CompletionItemKind::FUNCTION, "fill(): void", - "🎨 Fills the current path.", + "Fills the current path.", "fill()", - Some("🎨"), ), create_completion_item( "stroke", CompletionItemKind::FUNCTION, "stroke(): void", - "🖌️ Strokes the current path.", + "Strokes the current path.", "stroke()", - Some("🖌️"), ), ] } @@ -111,49 +102,43 @@ fn get_crypto_subtle_completions() -> Vec { "digest", CompletionItemKind::FUNCTION, "digest(algorithm: AlgorithmIdentifier, data: BufferSource): Promise", - "🔐 Computes a digest of the given data.", + "Computes a digest of the given data.", "digest(${1:'SHA-256'}, ${2:data})", - Some("🔐"), ), create_completion_item( "encrypt", CompletionItemKind::FUNCTION, "encrypt(algorithm: AlgorithmIdentifier, key: CryptoKey, data: BufferSource): Promise", - "🔒 Encrypts data using the specified algorithm and key.", + "Encrypts data using the specified algorithm and key.", "encrypt(${1:algorithm}, ${2:key}, ${3:data})", - Some("🔒"), ), create_completion_item( "decrypt", CompletionItemKind::FUNCTION, "decrypt(algorithm: AlgorithmIdentifier, key: CryptoKey, data: BufferSource): Promise", - "🔓 Decrypts data using the specified algorithm and key.", + "Decrypts data using the specified algorithm and key.", "decrypt(${1:algorithm}, ${2:key}, ${3:data})", - Some("🔓"), ), create_completion_item( "generateKey", CompletionItemKind::FUNCTION, "generateKey(algorithm: AlgorithmIdentifier, extractable: boolean, keyUsages: KeyUsage[]): Promise", - "🔑 Generates a key or key pair.", + "Generates a key or key pair.", "generateKey(${1:algorithm}, ${2:extractable}, ${3:keyUsages})", - Some("🔑"), ), create_completion_item( "sign", CompletionItemKind::FUNCTION, "sign(algorithm: AlgorithmIdentifier, key: CryptoKey, data: BufferSource): Promise", - "✍️ Signs data using the specified algorithm and key.", + "Signs data using the specified algorithm and key.", "sign(${1:algorithm}, ${2:key}, ${3:data})", - Some("✍️"), ), create_completion_item( "verify", CompletionItemKind::FUNCTION, "verify(algorithm: AlgorithmIdentifier, key: CryptoKey, signature: BufferSource, data: BufferSource): Promise", - "✅ Verifies a signature.", + "Verifies a signature.", "verify(${1:algorithm}, ${2:key}, ${3:signature}, ${4:data})", - Some("✅"), ), ] } @@ -165,41 +150,36 @@ fn get_performance_completions() -> Vec { "now", CompletionItemKind::FUNCTION, "now(): number", - "⏱️ Returns a high-resolution timestamp in milliseconds.", + "Returns a high-resolution timestamp in milliseconds.", "now()", - Some("⏱️"), ), create_completion_item( "mark", CompletionItemKind::FUNCTION, "mark(name: string, options?: PerformanceMarkOptions): PerformanceMark", - "📍 Creates a named timestamp in the performance timeline.", + "Creates a named timestamp in the performance timeline.", "mark(${1:name})", - Some("📍"), ), create_completion_item( "measure", CompletionItemKind::FUNCTION, "measure(name: string, startOrOptions?: string | PerformanceMeasureOptions, endMark?: string): PerformanceMeasure", - "📏 Creates a named measurement between two marks.", + "Creates a named measurement between two marks.", "measure(${1:name}, ${2:start}, ${3:end})", - Some("📏"), ), create_completion_item( "clearMarks", CompletionItemKind::FUNCTION, "clearMarks(name?: string): void", - "🗑️ Removes performance marks from the timeline.", + "Removes performance marks from the timeline.", "clearMarks(${1:name})", - Some("🗑️"), ), create_completion_item( "clearMeasures", CompletionItemKind::FUNCTION, "clearMeasures(name?: string): void", - "🗑️ Removes performance measures from the timeline.", + "Removes performance measures from the timeline.", "clearMeasures(${1:name})", - Some("🗑️"), ), ] } @@ -211,49 +191,43 @@ fn get_storage_completions() -> Vec { "getItem", CompletionItemKind::FUNCTION, "getItem(key: string): string | null", - "🔍 Returns the value for the specified key.", + "Returns the value for the specified key.", "getItem(${1:key})", - Some("🔍"), ), create_completion_item( "setItem", CompletionItemKind::FUNCTION, "setItem(key: string, value: string): void", - "💾 Sets the value for the specified key.", + "Sets the value for the specified key.", "setItem(${1:key}, ${2:value})", - Some("💾"), ), create_completion_item( "removeItem", CompletionItemKind::FUNCTION, "removeItem(key: string): void", - "🗑️ Removes the item with the specified key.", + "Removes the item with the specified key.", "removeItem(${1:key})", - Some("🗑️"), ), create_completion_item( "clear", CompletionItemKind::FUNCTION, "clear(): void", - "🗑️ Removes all items from storage.", + "Removes all items from storage.", "clear()", - Some("🗑️"), ), create_completion_item( "key", CompletionItemKind::FUNCTION, "key(index: number): string | null", - "🔑 Returns the key at the specified index.", + "Returns the key at the specified index.", "key(${1:index})", - Some("🔑"), ), create_completion_item( "length", CompletionItemKind::PROPERTY, "length: number", - "📊 The number of items in storage.", + "The number of items in storage.", "length", - Some("📊"), ), ] } @@ -265,73 +239,47 @@ fn get_database_completions() -> Vec { "prepare", CompletionItemKind::FUNCTION, "prepare(sql: string): StatementSync", - "🗄️ Prepares a SQL statement for execution.", + "Prepares a SQL statement for execution.", "prepare(${1:sql})", - Some("🗄️"), ), create_completion_item( "exec", CompletionItemKind::FUNCTION, "exec(sql: string): void", - "⚡ Executes SQL statements without returning results.", + "Executes SQL statements without returning results.", "exec(${1:sql})", - Some("⚡"), ), create_completion_item( "close", CompletionItemKind::FUNCTION, "close(): void", - "🔒 Closes the database connection.", + "Closes the database connection.", "close()", - Some("🔒"), ), create_completion_item( "all", CompletionItemKind::FUNCTION, "all(...params: any[]): unknown[]", - "📋 Executes the statement and returns all results.", + "Executes the statement and returns all results.", "all(${1:params})", - Some("📋"), ), create_completion_item( "get", CompletionItemKind::FUNCTION, "get(...params: any[]): unknown", - "🎯 Executes the statement and returns the first result.", + "Executes the statement and returns the first result.", "get(${1:params})", - Some("🎯"), ), create_completion_item( "run", CompletionItemKind::FUNCTION, "run(...params: any[]): StatementResultingChanges", - "🏃 Executes the statement and returns change information.", + "Executes the statement and returns change information.", "run(${1:params})", - Some("🏃"), ), ] } -/// Get context-specific completions based on what the user is typing -#[allow(dead_code)] -fn get_context_specific_completions(text_before_cursor: &str) -> Option> { - if text_before_cursor.contains("ctx.") || text_before_cursor.contains("context.") { - Some(get_canvas_context_completions()) - } else if text_before_cursor.contains("crypto.subtle.") { - Some(get_crypto_subtle_completions()) - } else if text_before_cursor.contains("performance.") { - Some(get_performance_completions()) - } else if text_before_cursor.contains("localStorage.") - || text_before_cursor.contains("sessionStorage.") - { - Some(get_storage_completions()) - } else if text_before_cursor.contains("db.") { - Some(get_database_completions()) - } else { - None - } -} - /// Document tracker for maintaining document state #[derive(Debug, Clone)] pub struct DocumentInfo { diff --git a/crates/cli/src/lsp/capabilities.rs b/crates/cli/src/lsp/capabilities.rs index c012716..5df55f0 100644 --- a/crates/cli/src/lsp/capabilities.rs +++ b/crates/cli/src/lsp/capabilities.rs @@ -1,5 +1,6 @@ -// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -// If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. use tower_lsp::lsp_types::*; diff --git a/crates/cli/src/lsp/completions.rs b/crates/cli/src/lsp/completions.rs index d57ab20..fc2391f 100644 --- a/crates/cli/src/lsp/completions.rs +++ b/crates/cli/src/lsp/completions.rs @@ -1,5 +1,6 @@ -// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -// If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. use std::collections::HashMap; use tower_lsp::lsp_types::*; @@ -61,7 +62,6 @@ impl AndromedaCompletionProvider { "readTextFileSync(path: string): string", "Reads a text file from the file system synchronously.", "readTextFileSync(${1:path})", - Some("🗎"), ), create_completion_item( "writeTextFileSync", @@ -69,7 +69,6 @@ impl AndromedaCompletionProvider { "writeTextFileSync(path: string, data: string): void", "Writes a text file to the file system synchronously.", "writeTextFileSync(${1:path}, ${2:data})", - Some("💾"), ), create_completion_item( "readFileSync", @@ -77,7 +76,6 @@ impl AndromedaCompletionProvider { "readFileSync(path: string): Uint8Array", "Reads a binary file from the file system synchronously.", "readFileSync(${1:path})", - Some("🗎"), ), create_completion_item( "writeFileSync", @@ -85,7 +83,6 @@ impl AndromedaCompletionProvider { "writeFileSync(path: string, data: Uint8Array): void", "Writes binary data to a file synchronously.", "writeFileSync(${1:path}, ${2:data})", - Some("💾"), ), create_completion_item( "existsSync", @@ -93,7 +90,6 @@ impl AndromedaCompletionProvider { "existsSync(path: string): boolean", "Checks if a file or directory exists synchronously.", "existsSync(${1:path})", - Some("🔍"), ), create_completion_item( "removeSync", @@ -101,7 +97,6 @@ impl AndromedaCompletionProvider { "removeSync(path: string): void", "Removes a file from the file system synchronously.", "removeSync(${1:path})", - Some("🗑️"), ), create_completion_item( "mkdirSync", @@ -109,7 +104,6 @@ impl AndromedaCompletionProvider { "mkdirSync(path: string): void", "Creates a directory synchronously.", "mkdirSync(${1:path})", - Some("📁"), ), // Environment operations create_completion_item( @@ -118,7 +112,6 @@ impl AndromedaCompletionProvider { "env: { get(key: string): string; set(key: string, value: string): void; ... }", "Environment variable operations.", "env", - Some("🌍"), ), // Process operations create_completion_item( @@ -127,7 +120,6 @@ impl AndromedaCompletionProvider { "args: string[]", "Command-line arguments passed to the program.", "args", - Some("📋"), ), create_completion_item( "exit", @@ -135,7 +127,6 @@ impl AndromedaCompletionProvider { "exit(code?: number): void", "Exits the program with an optional exit code.", "exit(${1:0})", - Some("🚪"), ), create_completion_item( "sleep", @@ -143,7 +134,6 @@ impl AndromedaCompletionProvider { "sleep(duration: number): Promise", "Returns a Promise that resolves after the specified duration in milliseconds.", "sleep(${1:1000})", - Some("⏱️"), ), // I/O operations create_completion_item( @@ -152,7 +142,6 @@ impl AndromedaCompletionProvider { "stdin: { readLine(): string }", "Standard input operations.", "stdin", - Some("⌨️"), ), create_completion_item( "stdout", @@ -160,7 +149,6 @@ impl AndromedaCompletionProvider { "stdout: { write(message: string): void }", "Standard output operations.", "stdout", - Some("🖥️"), ), ]; @@ -177,7 +165,6 @@ impl AndromedaCompletionProvider { "console: Console", "Console API for logging and debugging.", "console", - Some("🖥️"), ), // Fetch API create_completion_item( @@ -186,7 +173,6 @@ impl AndromedaCompletionProvider { "fetch(input: RequestInfo, init?: RequestInit): Promise", "Fetch API for making HTTP requests.", "fetch(${1:url})", - Some("🌐"), ), // Text encoding/decoding create_completion_item( @@ -195,7 +181,6 @@ impl AndromedaCompletionProvider { "TextEncoder: new() => TextEncoder", "Encodes strings to UTF-8 bytes.", "new TextEncoder()", - Some("🔤"), ), create_completion_item( "TextDecoder", @@ -203,7 +188,6 @@ impl AndromedaCompletionProvider { "TextDecoder: new(label?: string, options?: TextDecoderOptions) => TextDecoder", "Decodes bytes to strings.", "new TextDecoder(${1:'utf-8'})", - Some("🔤"), ), // URL API create_completion_item( @@ -212,7 +196,6 @@ impl AndromedaCompletionProvider { "URL: new(url: string, base?: string) => URL", "URL parsing and manipulation.", "new URL(${1:url})", - Some("🔗"), ), create_completion_item( "URLSearchParams", @@ -220,7 +203,6 @@ impl AndromedaCompletionProvider { "URLSearchParams: new(init?: string | string[][] | Record) => URLSearchParams", "URL search parameters manipulation.", "new URLSearchParams(${1:params})", - Some("🔍"), ), // Structured clone create_completion_item( @@ -229,7 +211,6 @@ impl AndromedaCompletionProvider { "structuredClone(value: T, options?: StructuredSerializeOptions): T", "Creates a deep clone using the structured clone algorithm.", "structuredClone(${1:value})", - Some("📋"), ), // Navigator create_completion_item( @@ -238,7 +219,6 @@ impl AndromedaCompletionProvider { "navigator: Navigator", "Navigator API with user agent and platform information.", "navigator", - Some("🧭"), ), // Timers create_completion_item( @@ -247,7 +227,6 @@ impl AndromedaCompletionProvider { "setTimeout(callback: () => void, delay?: number): number", "Executes a function after a delay.", "setTimeout(${1:callback}, ${2:delay})", - Some("⏰"), ), create_completion_item( "setInterval", @@ -255,7 +234,6 @@ impl AndromedaCompletionProvider { "setInterval(callback: () => void, delay?: number): number", "Repeatedly executes a function at intervals.", "setInterval(${1:callback}, ${2:delay})", - Some("🔄"), ), create_completion_item( "clearTimeout", @@ -263,7 +241,6 @@ impl AndromedaCompletionProvider { "clearTimeout(id: number): void", "Cancels a timeout.", "clearTimeout(${1:id})", - Some("❌"), ), create_completion_item( "clearInterval", @@ -271,7 +248,6 @@ impl AndromedaCompletionProvider { "clearInterval(id: number): void", "Cancels an interval.", "clearInterval(${1:id})", - Some("❌"), ), create_completion_item( "queueMicrotask", @@ -279,7 +255,6 @@ impl AndromedaCompletionProvider { "queueMicrotask(callback: () => void): void", "Queues a microtask for execution.", "queueMicrotask(${1:callback})", - Some("⚡"), ), ]; @@ -295,7 +270,6 @@ impl AndromedaCompletionProvider { "OffscreenCanvas: new(width: number, height: number) => OffscreenCanvas", "GPU-accelerated off-screen canvas for graphics rendering.", "new OffscreenCanvas(${1:width}, ${2:height})", - Some("🎨"), ), create_completion_item( "CanvasRenderingContext2D", @@ -303,7 +277,6 @@ impl AndromedaCompletionProvider { "CanvasRenderingContext2D", "2D rendering context for canvas operations.", "CanvasRenderingContext2D", - Some("🖌️"), ), create_completion_item( "createImageBitmap", @@ -311,7 +284,6 @@ impl AndromedaCompletionProvider { "createImageBitmap(path: string): Promise", "Creates an ImageBitmap from a file path or URL.", "createImageBitmap(${1:path})", - Some("🖼️"), ), ]; @@ -327,7 +299,6 @@ impl AndromedaCompletionProvider { "crypto: Crypto", "Web Crypto API for cryptographic operations.", "crypto", - Some("🔐"), ), // crypto.subtle methods would be added here create_completion_item( @@ -336,7 +307,6 @@ impl AndromedaCompletionProvider { "crypto.randomUUID(): string", "Generates a cryptographically secure random UUID.", "crypto.randomUUID()", - Some("🎲"), ), create_completion_item( "getRandomValues", @@ -344,7 +314,6 @@ impl AndromedaCompletionProvider { "crypto.getRandomValues(array: T): T", "Fills a typed array with cryptographically secure random values.", "crypto.getRandomValues(${1:array})", - Some("🎲"), ), ]; @@ -360,7 +329,6 @@ impl AndromedaCompletionProvider { "Database: new(filename: string, options?: DatabaseSyncOptions) => DatabaseSync", "SQLite database connection.", "new Database(${1:filename})", - Some("🗄️"), ), create_completion_item( "DatabaseSync", @@ -368,7 +336,6 @@ impl AndromedaCompletionProvider { "DatabaseSync: SQLite database class", "Synchronous SQLite database operations.", "DatabaseSync", - Some("🗄️"), ), ]; @@ -384,7 +351,6 @@ impl AndromedaCompletionProvider { "localStorage: Storage", "Local storage for persistent data.", "localStorage", - Some("💾"), ), create_completion_item( "sessionStorage", @@ -392,7 +358,6 @@ impl AndromedaCompletionProvider { "sessionStorage: Storage", "Session storage for temporary data.", "sessionStorage", - Some("🗃️"), ), ]; @@ -407,7 +372,6 @@ impl AndromedaCompletionProvider { "performance: AndromedaPerformance", "High-resolution time measurements and performance monitoring.", "performance", - Some("⚡"), )]; self.api_completions @@ -506,7 +470,6 @@ pub fn create_completion_item( detail: &str, documentation: &str, insert_text: &str, - icon: Option<&str>, ) -> CompletionItem { let mut item = CompletionItem { label: label.to_string(), @@ -514,11 +477,7 @@ pub fn create_completion_item( detail: Some(detail.to_string()), documentation: Some(Documentation::MarkupContent(MarkupContent { kind: MarkupKind::Markdown, - value: if let Some(icon) = icon { - format!("{icon} {documentation}") - } else { - documentation.to_string() - }, + value: documentation.to_string(), })), insert_text: Some(insert_text.to_string()), insert_text_format: Some(InsertTextFormat::SNIPPET), @@ -550,7 +509,6 @@ pub fn get_env_completions() -> Vec { "get(key: string): string", "Gets the value of an environment variable.", "get(${1:key})", - Some("🔑"), ), create_completion_item( "set", @@ -558,7 +516,6 @@ pub fn get_env_completions() -> Vec { "set(key: string, value: string): void", "Sets the value of an environment variable.", "set(${1:key}, ${2:value})", - Some("✏️"), ), create_completion_item( "remove", @@ -566,7 +523,6 @@ pub fn get_env_completions() -> Vec { "remove(key: string): void", "Removes an environment variable.", "remove(${1:key})", - Some("🗑️"), ), create_completion_item( "keys", @@ -574,7 +530,6 @@ pub fn get_env_completions() -> Vec { "keys(): string[]", "Returns all environment variable keys.", "keys()", - Some("🗂️"), ), ] } @@ -588,7 +543,6 @@ pub fn get_console_completions() -> Vec { "log(...data: any[]): void", "Logs messages to the console.", "log(${1:message})", - Some("📝"), ), create_completion_item( "error", @@ -596,7 +550,6 @@ pub fn get_console_completions() -> Vec { "error(...data: any[]): void", "Logs error messages to the console.", "error(${1:message})", - Some("❌"), ), create_completion_item( "warn", @@ -604,7 +557,6 @@ pub fn get_console_completions() -> Vec { "warn(...data: any[]): void", "Logs warning messages to the console.", "warn(${1:message})", - Some("⚠️"), ), create_completion_item( "info", @@ -612,7 +564,6 @@ pub fn get_console_completions() -> Vec { "info(...data: any[]): void", "Logs info messages to the console.", "info(${1:message})", - Some("ℹ️"), ), create_completion_item( "debug", @@ -620,7 +571,6 @@ pub fn get_console_completions() -> Vec { "debug(...data: any[]): void", "Logs debug messages to the console.", "debug(${1:message})", - Some("🐛"), ), create_completion_item( "table", @@ -628,7 +578,6 @@ pub fn get_console_completions() -> Vec { "table(data: any): void", "Displays data in a table format.", "table(${1:data})", - Some("📋"), ), create_completion_item( "time", @@ -636,7 +585,6 @@ pub fn get_console_completions() -> Vec { "time(label?: string): void", "Starts a timer for performance measurement.", "time(${1:label})", - Some("⏱️"), ), create_completion_item( "timeEnd", @@ -644,7 +592,6 @@ pub fn get_console_completions() -> Vec { "timeEnd(label?: string): void", "Ends a timer and logs the elapsed time.", "timeEnd(${1:label})", - Some("⏹️"), ), ] } diff --git a/crates/cli/src/lsp/diagnostic_converter.rs b/crates/cli/src/lsp/diagnostic_converter.rs index d8b20bb..0ccc42c 100644 --- a/crates/cli/src/lsp/diagnostic_converter.rs +++ b/crates/cli/src/lsp/diagnostic_converter.rs @@ -1,5 +1,6 @@ -// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -// If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. use crate::lint::LintError; use miette as oxc_miette; @@ -97,14 +98,6 @@ pub fn lint_error_to_diagnostic(lint_error: &LintError, source_code: &str) -> Di )), Some("andromeda".to_string()), ), - LintError::NoBooleanLiteralForArguments { value, .. } => ( - format!("Boolean literal '{value}' passed as argument"), - DiagnosticSeverity::INFORMATION, - Some(NumberOrString::String( - "andromeda::lint::no-boolean-literal-for-arguments".to_string(), - )), - Some("andromeda".to_string()), - ), LintError::NoUnreachable { .. } => ( "Unreachable code detected".to_string(), DiagnosticSeverity::WARNING, @@ -257,7 +250,6 @@ fn get_lint_error_span(lint_error: &LintError) -> SourceSpan { LintError::NoEval { span, .. } => *span, LintError::Eqeqeq { span, .. } => *span, LintError::Camelcase { span, .. } => *span, - LintError::NoBooleanLiteralForArguments { span, .. } => *span, LintError::NoUnreachable { span, .. } => *span, LintError::NoDuplicateCase { span, .. } => *span, LintError::NoConstantCondition { span, .. } => *span, diff --git a/crates/cli/src/lsp/mod.rs b/crates/cli/src/lsp/mod.rs index e8d279e..eae3a36 100644 --- a/crates/cli/src/lsp/mod.rs +++ b/crates/cli/src/lsp/mod.rs @@ -1,5 +1,6 @@ -// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -// If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. pub mod backend; pub mod capabilities; diff --git a/crates/cli/src/lsp/options.rs b/crates/cli/src/lsp/options.rs index cc62ec4..99fe0ad 100644 --- a/crates/cli/src/lsp/options.rs +++ b/crates/cli/src/lsp/options.rs @@ -1,5 +1,6 @@ -// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -// If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. use serde::{Deserialize, Serialize}; use tower_lsp::lsp_types::Url; diff --git a/crates/cli/src/lsp/server.rs b/crates/cli/src/lsp/server.rs index 24dcf33..3ba9e2c 100644 --- a/crates/cli/src/lsp/server.rs +++ b/crates/cli/src/lsp/server.rs @@ -1,5 +1,6 @@ -// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. -// If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. use super::backend::AndromedaBackend; use tower_lsp::{LspService, Server}; diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 9fc2896..b774394 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -27,8 +27,6 @@ mod helper; use helper::{find_formattable_files_for_format, find_formattable_files_for_lint}; mod lint; use lint::lint_file_with_config; -mod check; -use check::{CheckOutputFormat, check_files_with_options}; mod config; mod lsp; mod task; @@ -142,30 +140,13 @@ enum Command { }, /// Lint JavaScript/TypeScript files + #[command(visible_alias = "check")] Lint { /// The file(s) or directory(ies) to lint #[arg(required = false)] paths: Vec, }, - /// Type-check TypeScript files - Check { - /// The file(s) or directory(ies) to type-check - #[arg(required = false)] - paths: Vec, - - /// Emit one JSON object per diagnostic (ndjson) on stdout. Progress and - /// summary lines are still written to stderr. Mutually exclusive with - /// --quiet. - #[arg(long, conflicts_with = "quiet")] - json: bool, - - /// Suppress all output; rely on the exit code to signal status. Mutually - /// exclusive with --json. - #[arg(long)] - quiet: bool, - }, - /// Start Language Server Protocol (LSP) server Lsp, @@ -351,7 +332,7 @@ fn run_main() -> CliResult<()> { } else { String::new() }; - println!("✅ Successfully created the output binary at {out:?}{config_str}"); + println!("Created output binary at {out:?}{config_str}"); Ok(()) } Command::Repl { @@ -365,59 +346,39 @@ fn run_main() -> CliResult<()> { .map_err(|e| error::CliError::repl_error(format!("REPL failed: {e}"), Some(e))) } Command::Fmt { paths } => { - // Load configuration let config = ConfigManager::load_or_default(None); let files_to_format = find_formattable_files_for_format(&paths, &config.format)?; if files_to_format.is_empty() { - let warning = Style::new().yellow().bold().apply_to("⚠️"); - let msg = Style::new() - .yellow() - .apply_to("No formattable files found."); - println!("{warning} {msg}"); + let warning = Style::new().yellow().apply_to("Warning"); + eprintln!("{warning} No matching files found."); return Ok(()); } - let count = Style::new().cyan().apply_to(files_to_format.len()); - println!("Found {count} file(s) to format"); - println!("{}", Style::new().dim().apply_to("─".repeat(40))); - let mut already_formatted_count = 0; let mut formatted_count = 0; for path in &files_to_format { match format_file(path)? { - FormatResult::Changed => formatted_count += 1, + FormatResult::Changed => { + let label = Style::new().green().apply_to("Format"); + eprintln!("{label} {}", path.display()); + formatted_count += 1; + } FormatResult::AlreadyFormatted => already_formatted_count += 1, } } - println!(); - let success = Style::new().green().bold().apply_to("✅"); - let complete_msg = Style::new().green().bold().apply_to("Formatting complete"); - println!("{success} {complete_msg}:"); - + let total = formatted_count + already_formatted_count; if formatted_count > 0 { - let formatted_icon = Style::new().green().apply_to("📄"); - let formatted_num = Style::new().green().bold().apply_to(formatted_count); - let formatted_text = if formatted_count == 1 { - "file" - } else { - "files" - }; - println!(" {formatted_icon} {formatted_num} {formatted_text} formatted"); - } - - if already_formatted_count > 0 { - let already_icon = Style::new().cyan().apply_to("✨"); - let already_num = Style::new().cyan().bold().apply_to(already_formatted_count); - let already_text = if already_formatted_count == 1 { - "file" - } else { - "files" - }; - let already_msg = Style::new().cyan().apply_to("already formatted"); - println!(" {already_icon} {already_num} {already_text} {already_msg}"); + let plural = if formatted_count == 1 { "" } else { "s" }; + eprintln!(); + eprintln!( + "Formatted {formatted_count} file{plural} ({already_formatted_count} unchanged of {total})." + ); + } else { + let plural = if total == 1 { "" } else { "s" }; + eprintln!("Checked {total} file{plural}."); } Ok(()) @@ -449,50 +410,43 @@ fn run_main() -> CliResult<()> { None, ) })?; - println!("✅ Successfully bundled and minified to {output:?}"); + println!("Bundled and minified to {output:?}"); Ok(()) } Command::Lint { paths } => { - // Load configuration let config = ConfigManager::load_or_default(None); let files_to_lint = find_formattable_files_for_lint(&paths, &config.lint)?; if files_to_lint.is_empty() { - println!("No lintable files found."); + let warning = console::Style::new().yellow().apply_to("Warning"); + eprintln!("{warning} No matching files found."); return Ok(()); } - println!("Found {} file(s) to lint:", files_to_lint.len()); - let mut had_issues = false; + + let mut total_issues = 0usize; + let mut had_read_errors = false; for path in &files_to_lint { - if let Err(e) = lint_file_with_config(path, Some(config.clone())) { - print_error(e); - had_issues = true; + let label = console::Style::new().green().apply_to("Lint"); + eprintln!("{label} {}", path.display()); + match lint_file_with_config(path, Some(config.clone())) { + Ok(count) => total_issues += count, + Err(e) => { + print_error(e); + had_read_errors = true; + } } } - if had_issues { - Err(error::CliError::runtime_error( - "Linting completed with errors".to_string(), - None, - None, - None, - None, - )) - } else { - Ok(()) - } - } - Command::Check { paths, json, quiet } => { - let config = ConfigManager::load_or_default(None); - let format = if json { - CheckOutputFormat::Json - } else if quiet { - CheckOutputFormat::Quiet - } else { - CheckOutputFormat::Pretty - }; + if total_issues > 0 { + eprintln!(); + let plural = if total_issues == 1 { "" } else { "s" }; + eprintln!("Found {total_issues} issue{plural}."); + } - check_files_with_options(&paths, Some(config), format) + if total_issues > 0 || had_read_errors { + std::process::exit(1); + } + Ok(()) } Command::Lsp => { run_lsp_server().map_err(|e| { @@ -605,7 +559,7 @@ fn handle_config_command(action: ConfigAction) -> CliResult<()> { ) })?; - println!("✅ Created config file: {}", config_path.display()); + println!("Created config file: {}", config_path.display()); Ok(()) } ConfigAction::Show { file } => { @@ -655,7 +609,7 @@ fn handle_config_command(action: ConfigAction) -> CliResult<()> { ) })?; - println!("✅ Configuration is valid!"); + println!("Configuration is valid."); Ok(()) } } diff --git a/crates/cli/src/repl.rs b/crates/cli/src/repl.rs index 194b4e6..d4c4a09 100644 --- a/crates/cli/src/repl.rs +++ b/crates/cli/src/repl.rs @@ -101,7 +101,7 @@ pub fn run_repl_with_config( } }; if script_evaluation(agent, script.unbind(), gc.reborrow()).is_err() { - eprintln!("⚠️ Warning: Error loading builtin module"); + eprintln!("Warning: Error loading builtin module"); handle_runtime_error_with_message("Script evaluation failed".to_string()); } } @@ -170,13 +170,13 @@ pub fn run_repl_with_config( } "gc" => { let gc_style = Style::new().yellow(); - println!("{}", gc_style.apply_to("🗑️ Running garbage collection...")); + println!("{}", gc_style.apply_to("Running garbage collection...")); agent.gc(); println!( "{}", Style::new() .green() - .apply_to("✅ Garbage collection completed") + .apply_to("✓ Garbage collection completed") ); continue; } @@ -298,7 +298,7 @@ fn initialize_global_object(agent: &mut Agent, global_object: Object, mut gc: Gc .unbind() .is_err() { - eprintln!("⚠️ Warning: Error loading extension {}", specifier); + eprintln!("Warning: Error loading extension {}", specifier); handle_runtime_error_with_message("Module evaluation failed".to_string()); } } diff --git a/crates/cli/src/run.rs b/crates/cli/src/run.rs index 7d98aec..7ea9eae 100644 --- a/crates/cli/src/run.rs +++ b/crates/cli/src/run.rs @@ -120,7 +120,7 @@ pub fn run_with_config( match runtime_output.result { Ok(result) => { if effective_verbose { - println!("✅ Execution completed successfully: {result:?}"); + println!("Execution completed successfully: {result:?}"); } Ok(()) } @@ -239,11 +239,7 @@ fn print_error_with_llm_suggestion( // Then try to get and print an LLM suggestion if initialized if is_llm_initialized() { eprintln!(); - eprintln!( - "{} {}", - "🤖".bright_cyan(), - "Fetching AI suggestion...".dimmed() - ); + eprintln!("{}", "Fetching AI suggestion...".dimmed()); let mut context = ErrorContext::new(error_message); @@ -273,8 +269,7 @@ fn print_error_with_llm_suggestion( eprintln!(); eprintln!( - "{} {} {}", - "💡".bright_yellow(), + "{} {}", "AI Suggestion".bright_yellow().bold(), format!("(via {})", suggestion.provider_name).dimmed() ); diff --git a/crates/cli/src/upgrade.rs b/crates/cli/src/upgrade.rs index 14a1435..2c1d536 100644 --- a/crates/cli/src/upgrade.rs +++ b/crates/cli/src/upgrade.rs @@ -43,7 +43,7 @@ struct PlatformInfo { /// Run the upgrade process pub fn run_upgrade(force: bool, target_version: Option, dry_run: bool) -> Result<()> { - println!("🚀 Andromeda Upgrade Tool"); + println!("Andromeda Upgrade Tool"); println!("Current version: {CURRENT_VERSION}"); println!(); @@ -59,12 +59,12 @@ pub fn run_upgrade(force: bool, target_version: Option, dry_run: bool) - println!("Latest available version: {}", release.tag_name); if !force && release.tag_name == CURRENT_VERSION { - println!("✅ You are already running the latest version!"); + println!("You are already running the latest version!"); return Ok(()); } if release.tag_name == CURRENT_VERSION && !force { - println!("ℹ️ You are already on version {CURRENT_VERSION}. Use --force to reinstall."); + println!("You are already on version {CURRENT_VERSION}. Use --force to reinstall."); return Ok(()); } @@ -83,7 +83,7 @@ pub fn run_upgrade(force: bool, target_version: Option, dry_run: bool) - if dry_run { println!( - "🔍 Dry run mode - would upgrade from {} to {}", + "Dry run mode - would upgrade from {} to {}", CURRENT_VERSION, release.tag_name ); println!("Would download: {}", asset.browser_download_url); @@ -107,13 +107,13 @@ pub fn run_upgrade(force: bool, target_version: Option, dry_run: bool) - } } - println!("⬇️ Downloading {}...", asset.name); + println!("Downloading {}...", asset.name); let new_binary = download_asset(&asset.browser_download_url)?; - println!("🔄 Installing new version..."); + println!("Installing new version..."); install_binary(&new_binary, &platform)?; - println!("✅ Successfully upgraded to version {}!", release.tag_name); + println!("Successfully upgraded to version {}!", release.tag_name); println!("Run 'andromeda --version' to verify the new version."); Ok(()) @@ -169,7 +169,7 @@ fn get_latest_release() -> Result { Ok(release) } Err(_) => { - println!("ℹ️ No stable release found, checking for pre-releases..."); + println!("No stable release found, checking for pre-releases..."); get_most_recent_release() } } @@ -288,7 +288,7 @@ echo Upgrade completed successfully! .spawn() .context("Failed to start upgrade process")?; - println!("⚠️ The upgrade will complete after this process exits."); + println!("The upgrade will complete after this process exits."); Ok(()) }