-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
486 additions
and
79 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |