Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
0xLucqs committed Aug 21, 2024
1 parent b9a1161 commit d11acfa
Show file tree
Hide file tree
Showing 9 changed files with 486 additions and 79 deletions.
260 changes: 212 additions & 48 deletions Cargo.lock

Large diffs are not rendered by default.

28 changes: 16 additions & 12 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,19 @@ license = "Apache-2.0"
license-file = "LICENSE"

[workspace.dependencies]
cairo-lang-compiler = { git = "https://github.com/0xLucqs/cairo", branch = "lucas/multiline_diag" }
cairo-lang-parser = { git = "https://github.com/0xLucqs/cairo", branch = "lucas/multiline_diag" }
cairo-lang-utils = { git = "https://github.com/0xLucqs/cairo", branch = "lucas/multiline_diag" }
cairo-lang-semantic = { git = "https://github.com/0xLucqs/cairo", branch = "lucas/multiline_diag" }
cairo-lang-filesystem = { git = "https://github.com/0xLucqs/cairo", branch = "lucas/multiline_diag" }
cairo-lang-diagnostics = { git = "https://github.com/0xLucqs/cairo", branch = "lucas/multiline_diag" }
cairo-lang-test-plugin = { git = "https://github.com/0xLucqs/cairo", branch = "lucas/multiline_diag" }
cairo-lang-lowering = { git = "https://github.com/0xLucqs/cairo", branch = "lucas/multiline_diag" }
cairo-lang-syntax = { git = "https://github.com/0xLucqs/cairo", branch = "lucas/multiline_diag" }
cairo-lang-defs = { git = "https://github.com/0xLucqs/cairo", branch = "lucas/multiline_diag" }
cairo-lang-test-utils = { git = "https://github.com/0xLucqs/cairo", branch = "lucas/multiline_diag" }
cairo-lang-language-server = { git = "https://github.com/0xLucqs/cairo", branch = "lucas/multiline_diag" }
cairo-lang-compiler = { path = "../cairo/crates/cairo-lang-compiler" }
cairo-lang-parser = { path = "../cairo/crates/cairo-lang-parser" }
cairo-lang-utils = { path = "../cairo/crates/cairo-lang-utils" }
cairo-lang-semantic = { path = "../cairo/crates/cairo-lang-semantic" }
cairo-lang-filesystem = { path = "../cairo/crates/cairo-lang-filesystem" }
cairo-lang-diagnostics = { path = "../cairo/crates/cairo-lang-diagnostics" }
cairo-lang-test-plugin = { path = "../cairo/crates/cairo-lang-test-plugin" }
cairo-lang-lowering = { path = "../cairo/crates/cairo-lang-lowering" }
cairo-lang-syntax = { path = "../cairo/crates/cairo-lang-syntax" }
cairo-lang-defs = { path = "../cairo/crates/cairo-lang-defs" }
cairo-lang-starknet = { path = "../cairo/crates/cairo-lang-starknet" }
cairo-lang-test-utils = { path = "../cairo/crates/cairo-lang-test-utils" }
cairo-lang-language-server = { path = "../cairo/crates/cairo-lang-language-server" }
salsa = "0.16.1"
indoc = "2"
test-case = "3.0"
Expand All @@ -30,3 +31,6 @@ ctor = "0.2.8"
paste = "1.0.15"
itertools = "0.13.0"
log = "0.4.22"
clap = "4.5.16"
anyhow = "1.0.86"
smol_str = "0.2.2"
13 changes: 12 additions & 1 deletion crates/cairo-lint-cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
[package]
name = "cairo-lint"
name = "scarb-cairo-lint"
version.workspace = true
edition.workspace = true
repository.workspace = true
license-file.workspace = true

[[bin]]
name = "scarb-cairo-lint"
path = "src/main.rs"

[dependencies]
cairo-lang-compiler.workspace = true
cairo-lang-parser.workspace = true
Expand All @@ -18,3 +22,10 @@ cairo-lang-syntax.workspace = true
cairo-lang-defs.workspace = true
cairo-lang-language-server.workspace = true
salsa.workspace = true
clap = { workspace = true, features = ["derive"] }
scarb-ui = "0.1.5"
anyhow.workspace = true
scarb-metadata = "1.12.0"
cairo-lint-core = { path = "../cairo-lint-core" }
# scarb = { git = "https://github.com/software-mansion/scarb", branch = "main" }
smol_str.workspace = true
85 changes: 85 additions & 0 deletions crates/cairo-lint-cli/src/helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use std::path::PathBuf;

use anyhow::{anyhow, Result};
use cairo_lang_compiler::project::{AllCratesConfig, ProjectConfig, ProjectConfigContent};
use cairo_lang_filesystem::cfg::{Cfg as CompilerCfg, CfgSet};
use cairo_lang_filesystem::db::{CrateSettings, Edition, ExperimentalFeaturesConfig};
use cairo_lang_filesystem::ids::Directory;
use cairo_lang_utils::ordered_hash_map::OrderedHashMap;
use scarb_metadata::{Cfg as ScarbCfg, CompilationUnitMetadata, PackageId};
use smol_str::{SmolStr, ToSmolStr};

/// Different targets for cairo.
pub mod targets {
/// [lib]
pub const LIB: &str = "lib";
/// #[cfg(test)]
pub const TEST: &str = "test";
/// Starknet smart contracts
pub const STARKNET_CONTRACT: &str = "starknet-contract";
/// All the targets
pub const TARGETS: [&str; 3] = [LIB, TEST, STARKNET_CONTRACT];
}

/// Converts [`&[ScarbCfg]`] to a [`CfgSet`]
pub fn to_cairo_cfg(cfgs: &[ScarbCfg]) -> CfgSet {
let mut cfg_set = CfgSet::new();
cfgs.iter().for_each(|cfg| match cfg {
ScarbCfg::KV(key, value) => {
cfg_set.insert(CompilerCfg { key: key.to_smolstr(), value: Some(value.to_smolstr()) });
}
ScarbCfg::Name(name) => {
cfg_set.insert(CompilerCfg { key: name.to_smolstr(), value: None });
}
});
cfg_set
}

/// Convert a string to a compiler [`Edition`]. If the edition is unknown it'll return an error.
pub fn to_cairo_edition(edition: &str) -> Result<Edition> {
match edition {
"2023_01" => Ok(Edition::V2023_01),
"2023_10" => Ok(Edition::V2023_10),
"2023_11" => Ok(Edition::V2023_11),
"2024_07" => Ok(Edition::V2024_07),
_ => Err(anyhow!("Unknown edition {}", edition)),
}
}

/// Gets a bunch of informations related to the project from several objects. Mostly a copy pasta of
/// https://github.com/software-mansion/scarb/blob/fb34a0ce85e0a46e15f58abd3fbaaf1d3c4bf012/scarb/src/compiler/helpers.rs#L17-L62
/// but with metadata objects
pub fn build_project_config(
compilation_unit: &CompilationUnitMetadata,
corelib_id: &PackageId,
corelib: PathBuf,
package_path: PathBuf,
edition: Edition,
) -> Result<ProjectConfig> {
let crate_roots = compilation_unit
.components
.iter()
.filter(|component| &component.package != corelib_id)
.map(|component| (component.name.to_smolstr(), component.source_root().into()))
.collect();
let crates_config: OrderedHashMap<SmolStr, CrateSettings> = compilation_unit
.components
.iter()
.map(|component| {
let cfg_set = component.cfg.as_ref().map(|cfgs| to_cairo_cfg(&cfgs));
(
component.name.to_smolstr(),
CrateSettings {
edition,
cfg_set,
experimental_features: ExperimentalFeaturesConfig { negative_impls: false, coupons: false },
},
)
})
.collect();
let crates_config = AllCratesConfig { override_map: crates_config, ..Default::default() };
let content = ProjectConfigContent { crate_roots, crates_config };

let project_config = ProjectConfig { base_path: package_path, corelib: Some(Directory::Real(corelib)), content };
Ok(project_config)
}
148 changes: 147 additions & 1 deletion crates/cairo-lint-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1 +1,147 @@
fn main() {}
pub mod helpers;

use std::cmp::Reverse;
use std::collections::HashMap;
use std::path::PathBuf;

use anyhow::{anyhow, Result};
use cairo_lang_compiler::project::{update_crate_root, update_crate_roots_from_project_config};
use cairo_lang_defs::db::DefsGroup;
use cairo_lang_diagnostics::DiagnosticEntry;
use cairo_lang_filesystem::db::{init_dev_corelib, FilesGroup, FilesGroupEx, CORELIB_CRATE_NAME};
use cairo_lang_filesystem::ids::{CrateLongId, FileId};
use cairo_lang_semantic::db::SemanticGroup;
use cairo_lang_utils::{Upcast, UpcastMut};
use cairo_lint_core::db::AnalysisDatabase;
use cairo_lint_core::fix::{fix_semantic_diagnostic, Fix};
use clap::Parser;
use helpers::*;
use scarb_metadata::MetadataCommand;
use scarb_ui::args::{PackagesFilter, VerbositySpec};
use scarb_ui::components::Status;
use scarb_ui::{OutputFormat, Ui};
use smol_str::SmolStr;

#[derive(Parser, Debug)]
struct Args {
/// Name of the package.
#[command(flatten)]
packages_filter: PackagesFilter,
/// Path to the file or project to analyze
path: Option<String>,
/// Logging verbosity.
#[command(flatten)]
pub verbose: VerbositySpec,
/// Comma separated list of target names to compile.
#[arg(long, value_delimiter = ',', env = "SCARB_TARGET_NAMES")]
pub target_names: Vec<String>,
/// Should lint the tests.
#[arg(short, long, default_value_t = false)]
pub test: bool,
/// Should fix the lint when it can.
#[arg(short, long, default_value_t = false)]
pub fix: bool,
}

fn main() -> Result<()> {
let args: Args = Args::parse();
let ui = Ui::new(args.verbose.clone().into(), OutputFormat::Text);
if let Err(err) = main_inner(&ui, args) {
ui.anyhow(&err);
std::process::exit(1);
}
Ok(())
}

fn main_inner(ui: &Ui, args: Args) -> Result<()> {
// Get the scarb project metadata
let metadata = MetadataCommand::new().inherit_stderr().exec()?;
// Get the corelib package metadata
let corelib = metadata
.packages
.iter()
.find(|package| package.name == CORELIB_CRATE_NAME)
.ok_or(anyhow!("Corelib not found"))?;
// Corelib package id
let corelib_id = &corelib.id;
// Corelib path
let corelib = Into::<PathBuf>::into(corelib.manifest_path.parent().as_ref().unwrap()).join("src");
// Remove the compilation units that are not requested by the user. If none is specified will lint
// them all. The test target is a special case and will never be linted unless specified with the
// `--test` flag
let compilation_units = metadata.compilation_units.into_iter().filter(|compilation_unit| {
(args.target_names.is_empty() && compilation_unit.target.kind != targets::TEST)
|| (args.target_names.contains(&compilation_unit.target.kind))
|| (args.test && compilation_unit.target.kind == targets::TEST)
});
// Let's lint everything requested
for compilation_unit in compilation_units {
// Get the current package metadata
let package = metadata.packages.iter().find(|package| package.id == compilation_unit.package).unwrap();
// Print that we're checking this package.
ui.print(Status::new("Checking", &package.name));
// Create our db
let mut db = AnalysisDatabase::new();
// Add the targets of this package
db.use_cfg(&to_cairo_cfg(&compilation_unit.cfg));
// Setup the corelib
init_dev_corelib(db.upcast_mut(), corelib.clone());
// Convert the package edition to a cairo edition. If not specified or not known it will return an
// error.
let edition = to_cairo_edition(
&package.edition.as_ref().ok_or(anyhow!("No edition found for package {}", package.name))?,
)?;
// Get the package path.
let package_path = package.root.clone().into();
// Build the config for this package.
let config = build_project_config(&compilation_unit, &corelib_id, corelib.clone(), package_path, edition)?;
update_crate_roots_from_project_config(&mut db, &config);
if let Some(corelib) = &config.corelib {
update_crate_root(&mut db, &config, CORELIB_CRATE_NAME.into(), corelib.clone());
}
let crate_id =
Upcast::<dyn FilesGroup>::upcast(&db).intern_crate(CrateLongId::Real(SmolStr::new(&package.name)));
// Get all the diagnostics
let mut diags = Vec::new();
for module_id in &*db.crate_modules(crate_id) {
diags.push(db.module_semantic_diagnostics(*module_id).unwrap());
}
let formatted_diags = diags.iter().map(|diag| diag.format(db.upcast())).collect::<String>().trim().to_string();
ui.print(formatted_diags);
if args.fix {
let mut fixes = Vec::with_capacity(diags.len());
for diag in diags.iter().flat_map(|diags| diags.get_all()) {
if let Some((fix_node, fix)) = fix_semantic_diagnostic(&db, &diag) {
let location = diag.location(db.upcast());
fixes.push(Fix { span: location.span, file_path: location.file_id, suggestion: fix });
}
}
fixes.sort_by_key(|fix| Reverse(fix.span.start));
let mut fixable_diagnostics = Vec::with_capacity(fixes.len());
if fixes.len() <= 1 {
fixable_diagnostics = fixes;
} else {
for i in 0..fixes.len() - 1 {
let first = fixes[i].span;
let second = fixes[i + 1].span;
if second.end <= first.start {
fixable_diagnostics.push(fixes[i].clone());
}
}
}

let mut files: HashMap<FileId, String> = HashMap::default();
fixable_diagnostics.into_iter().for_each(|fix| {
let mut file =
files.entry(fix.file_path).or_insert(db.file_content(fix.file_path).unwrap().to_string());
(*file).replace_range(fix.span.to_str_range(), &fix.suggestion);
});
for (file_id, file) in files {
println!("Fixing {}", file_id.full_path(db.upcast()));
std::fs::write(file_id.full_path(db.upcast()), file).unwrap()
}
}
}

Ok(())
}
1 change: 1 addition & 0 deletions crates/cairo-lint-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ cairo-lang-test-plugin.workspace = true
cairo-lang-lowering.workspace = true
cairo-lang-syntax.workspace = true
cairo-lang-defs.workspace = true
cairo-lang-starknet.workspace = true
cairo-lang-language-server.workspace = true
salsa.workspace = true
log.workspace = true
Expand Down
22 changes: 8 additions & 14 deletions crates/cairo-lint-core/src/db.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use cairo_lang_defs::db::{DefsDatabase, DefsGroup};
use cairo_lang_filesystem::cfg::{Cfg, CfgSet};
use cairo_lang_filesystem::db::{init_files_group, AsFilesGroupMut, FilesDatabase, FilesGroup};
use cairo_lang_lowering::db::{init_lowering_group, LoweringDatabase, LoweringGroup};
use cairo_lang_lowering::utils::InliningStrategy;
use cairo_lang_parser::db::{ParserDatabase, ParserGroup};
use cairo_lang_semantic::db::{SemanticDatabase, SemanticGroup};
use cairo_lang_semantic::inline_macros::get_default_plugin_suite;
use cairo_lang_semantic::plugin::PluginSuite;
use cairo_lang_starknet::starknet_plugin_suite;
use cairo_lang_syntax::node::db::{SyntaxDatabase, SyntaxGroup};
use cairo_lang_test_plugin::test_plugin_suite;
use cairo_lang_utils::Upcast;
Expand All @@ -22,24 +22,18 @@ impl AnalysisDatabase {
pub fn new() -> Self {
let mut db = Self { storage: Default::default() };

let plugin_suite = [get_default_plugin_suite(), test_plugin_suite(), cairo_lint_plugin_suite()]
.into_iter()
.fold(PluginSuite::default(), |mut acc, suite| {
acc.add(suite);
acc
});
let plugin_suite =
[get_default_plugin_suite(), test_plugin_suite(), cairo_lint_plugin_suite(), starknet_plugin_suite()]
.into_iter()
.fold(PluginSuite::default(), |mut acc, suite| {
acc.add(suite);
acc
});
db.apply_plugin_suite(plugin_suite);
init_files_group(&mut db);
init_lowering_group(&mut db, InliningStrategy::Default);

db.set_cfg_set(Self::initial_cfg_set().into());

db
}
/// Returns the [`CfgSet`] that should be assumed in the initial database state.
fn initial_cfg_set() -> CfgSet {
CfgSet::from_iter([Cfg::name("test"), Cfg::kv("target", "test")])
}
/// Shortcut for settings compiler plugins from a [`PluginSuite`].
fn apply_plugin_suite(&mut self, plugin_suite: PluginSuite) {
self.set_macro_plugins(plugin_suite.plugins);
Expand Down
4 changes: 3 additions & 1 deletion crates/cairo-lint-core/src/fix.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use cairo_lang_defs::ids::UseId;
use cairo_lang_defs::plugin::PluginDiagnostic;
use cairo_lang_filesystem::ids::FileId;
use cairo_lang_filesystem::span::TextSpan;
use cairo_lang_semantic::diagnostic::SemanticDiagnosticKind;
use cairo_lang_semantic::SemanticDiagnostic;
Expand All @@ -16,9 +17,10 @@ use crate::plugin::{diagnostic_kind_from_message, CairoLintKind};

/// Represents a fix for a diagnostic, containing the span of code to be replaced
/// and the suggested replacement.
#[derive(Default)]
#[derive(Clone)]
pub struct Fix {
pub span: TextSpan,
pub file_path: FileId,
pub suggestion: String,
}

Expand Down
4 changes: 2 additions & 2 deletions rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[toolchain]
channel = "nightly"
components = ["rustfmt", "clippy"]
channel = "nightly-2024-08-19"
components = ["rustfmt", "clippy", "rust-analyzer"]
profile = "minimal"

0 comments on commit d11acfa

Please sign in to comment.