use std::{
collections::HashSet,
path::{Path, PathBuf},
};
use anyhow::{bail, Ok, Result};
use clap::Parser;
use forc_tracing::{println_action_green, println_action_yellow, println_yellow_bold};
use forc_util::{format_diagnostic, fs_locking::is_file_dirty};
use itertools::Itertools;
use sway_ast::Module;
use sway_core::{
language::lexed::{LexedModule, LexedProgram},
Engines,
};
use sway_error::formatting::*;
use sway_features::{ExperimentalFeatures, Feature};
use sway_types::{SourceEngine, Span};
use swayfmt::Formatter;
use crate::{
cli::{
self,
shared::{
compile_package, create_migration_diagnostic, detailed_migration_guide_msg,
max_feature_name_len, PROJECT_IS_COMPATIBLE,
},
},
get_migration_steps_or_return, instructive_error,
migrations::{
ContinueMigrationProcess, DryRun, InteractionResponse, MigrationStep, MigrationStepKind,
MigrationSteps, Occurrence, ProgramInfo,
},
};
forc_util::cli_examples! {
crate::cli::Opt {
[ Migrate the project in the current path => "forc migrate run"]
[ Migrate the project located in another path => "forc migrate run --path {path}" ]
[ Migrate the project offline without downloading any dependencies => "forc migrate run --offline" ]
}
}
#[derive(Debug, Parser)]
pub(crate) struct Command {
#[clap(flatten)]
pub run: cli::shared::Compile,
}
struct ModifiedModules<'a> {
source_engine: &'a SourceEngine,
modified_modules_paths: HashSet<PathBuf>,
}
impl<'a> ModifiedModules<'a> {
fn new(source_engine: &'a SourceEngine, occurrences_spans: &[Span]) -> Self {
Self {
source_engine,
modified_modules_paths: occurrences_spans
.iter()
.filter_map(|span| span.source_id().copied())
.filter(|source_id| !source_engine.is_source_id_autogenerated(source_id))
.map(|source_id| source_engine.get_path(&source_id))
.collect(),
}
}
fn get_path_if_modified(&self, module: &Module) -> Option<PathBuf> {
module.source_id().and_then(|source_id| {
let path = self.source_engine.get_path(&source_id);
if self.modified_modules_paths.contains(&path) {
Some(path)
} else {
None
}
})
}
fn get_dirty_modified_modules_paths(&self) -> Vec<&PathBuf> {
self.modified_modules_paths
.iter()
.filter(|path| is_file_dirty(path))
.collect()
}
}
pub(crate) fn exec(command: Command) -> Result<()> {
let migration_steps = get_migration_steps_or_return!();
let engines = Engines::default();
let build_instructions = command.run;
let experimental = build_instructions.experimental_features()?;
let mut program_info = compile_package(&engines, &build_instructions)?;
print_migrating_action(migration_steps);
let max_len = max_feature_name_len(migration_steps);
let last_migration_feature = migration_steps
.last()
.expect(
"`get_migration_steps_or_return!` guarantees that the `migration_steps` are not empty",
)
.0;
let mut current_feature_migration_has_code_changes = false;
let mut num_of_postponed_steps = 0;
for (feature, migration_steps) in migration_steps.iter() {
for migration_step in migration_steps.iter() {
match migration_step.kind {
MigrationStepKind::Instruction(instruction) => {
let occurrences = instruction(&program_info)?;
print_instruction_result(
&engines,
max_len,
feature,
migration_step,
&occurrences,
);
if !occurrences.is_empty() {
println_yellow_bold("If you've already reviewed the above points, you can ignore this info.");
}
}
MigrationStepKind::CodeModification(
modification,
manual_migration_actions,
continue_migration_process,
) => {
let occurrences = modification(&mut program_info.as_mut(), DryRun::No)?;
output_modified_modules(
&build_instructions.manifest_dir()?,
&program_info,
&occurrences,
experimental,
)?;
let stop_migration_process = print_modification_result(
max_len,
feature,
migration_step,
manual_migration_actions,
continue_migration_process,
&occurrences,
InteractionResponse::None,
&mut current_feature_migration_has_code_changes,
);
if stop_migration_process == StopMigrationProcess::Yes {
return Ok(());
}
}
MigrationStepKind::Interaction(
instruction,
interaction,
manual_migration_actions,
continue_migration_process,
) => {
let instruction_occurrences_spans = instruction(&program_info)?;
print_instruction_result(
&engines,
max_len,
feature,
migration_step,
&instruction_occurrences_spans,
);
if !instruction_occurrences_spans.is_empty() {
let (interaction_response, interaction_occurrences_spans) =
interaction(&mut program_info.as_mut())?;
if interaction_response == InteractionResponse::PostponeStep {
num_of_postponed_steps += 1;
}
output_modified_modules(
&build_instructions.manifest_dir()?,
&program_info,
&interaction_occurrences_spans,
experimental,
)?;
let stop_migration_process = print_modification_result(
max_len,
feature,
migration_step,
manual_migration_actions,
continue_migration_process,
&interaction_occurrences_spans,
interaction_response,
&mut current_feature_migration_has_code_changes,
);
if stop_migration_process == StopMigrationProcess::Yes {
return Ok(());
}
}
}
};
}
if current_feature_migration_has_code_changes {
if *feature == last_migration_feature {
print_migration_finished_action(num_of_postponed_steps);
} else {
print_continue_migration_action("Review the changed code");
}
return Ok(());
}
}
print_migration_finished_action(num_of_postponed_steps);
Ok(())
}
#[derive(PartialEq, Eq)]
enum StopMigrationProcess {
Yes,
No,
}
#[allow(clippy::too_many_arguments)]
fn print_modification_result(
max_len: usize,
feature: &Feature,
migration_step: &MigrationStep,
manual_migration_actions: &[&str],
continue_migration_process: ContinueMigrationProcess,
occurrences: &[Occurrence],
interaction_response: InteractionResponse,
current_feature_migration_has_code_changes: &mut bool,
) -> StopMigrationProcess {
if occurrences.is_empty() {
if interaction_response == InteractionResponse::PostponeStep {
print_postponed_action(max_len, feature, migration_step);
} else {
print_checked_action(max_len, feature, migration_step);
}
StopMigrationProcess::No
} else {
print_changing_code_action(max_len, feature, migration_step);
println!(
"Source code successfully changed ({} change{}).",
occurrences.len(),
plural_s(occurrences.len())
);
match continue_migration_process {
ContinueMigrationProcess::Never => {
print_continue_migration_action("Review the changed code");
StopMigrationProcess::Yes
}
ContinueMigrationProcess::IfNoManualMigrationActionsNeeded => {
if !migration_step.has_manual_actions() {
*current_feature_migration_has_code_changes = true;
StopMigrationProcess::No
} else {
println!();
println!("You still need to manually:");
manual_migration_actions
.iter()
.for_each(|help| println!("- {help}"));
println!();
println!("{}", detailed_migration_guide_msg(feature));
print_continue_migration_action("Do the above manual changes");
StopMigrationProcess::Yes
}
}
}
}
}
fn print_instruction_result(
engines: &Engines,
max_len: usize,
feature: &Feature,
migration_step: &MigrationStep,
occurrences: &[Occurrence],
) {
if occurrences.is_empty() {
print_checked_action(max_len, feature, migration_step);
} else {
print_review_action(max_len, feature, migration_step);
if let Some(diagnostic) =
create_migration_diagnostic(engines.se(), feature, migration_step, occurrences)
{
format_diagnostic(&diagnostic);
}
}
}
fn output_modified_modules(
manifest_dir: &Path,
program_info: &ProgramInfo,
occurrences: &[Occurrence],
experimental: ExperimentalFeatures,
) -> Result<()> {
if occurrences.is_empty() {
return Ok(());
}
let modified_modules = ModifiedModules::new(
program_info.engines.se(),
&occurrences
.iter()
.map(|o| o.span.clone())
.collect::<Vec<_>>(),
);
check_that_modified_modules_are_not_dirty(&modified_modules)?;
output_changed_lexed_program(
manifest_dir,
&modified_modules,
&program_info.lexed_program,
experimental,
)?;
Ok(())
}
fn check_that_modified_modules_are_not_dirty(modified_modules: &ModifiedModules) -> Result<()> {
let dirty_modules = modified_modules.get_dirty_modified_modules_paths();
if !dirty_modules.is_empty() {
bail!(instructive_error("Files cannot be changed, because they are open in an editor and contain unsaved changes.",
&[
"The below files are open in an editor and contain unsaved changes:".to_string(),
]
.into_iter()
.chain(dirty_modules.iter().map(|file| format!(" - {}", file.display())))
.chain(vec!["Please save the open files before running the migrations.".to_string()])
.collect::<Vec<_>>()
));
}
Ok(())
}
fn output_changed_lexed_program(
manifest_dir: &Path,
modified_modules: &ModifiedModules,
lexed_program: &LexedProgram,
experimental: ExperimentalFeatures,
) -> Result<()> {
fn output_modules_rec(
manifest_dir: &Path,
modified_modules: &ModifiedModules,
lexed_module: &LexedModule,
experimental: ExperimentalFeatures,
) -> Result<()> {
if let Some(path) = modified_modules.get_path_if_modified(&lexed_module.tree.value) {
let mut formatter = Formatter::from_dir(manifest_dir, experimental)?;
let code = formatter.format_module(&lexed_module.tree.clone())?;
std::fs::write(path, code)?;
}
for (_, lexed_submodule) in lexed_module.submodules.iter() {
output_modules_rec(
manifest_dir,
modified_modules,
&lexed_submodule.module,
experimental,
)?;
}
Ok(())
}
output_modules_rec(
manifest_dir,
modified_modules,
&lexed_program.root,
experimental,
)
}
fn print_migrating_action(migration_steps: MigrationSteps) {
println_action_green(
"Migrating",
&format!(
"Breaking change feature{} {}",
plural_s(migration_steps.len()),
sequence_to_str(
&migration_steps
.iter()
.map(|(feature, _)| feature.name())
.collect_vec(),
Enclosing::None,
4
),
),
);
}
fn print_changing_code_action(max_len: usize, feature: &Feature, migration_step: &MigrationStep) {
println_action_yellow(
"Changing",
&full_migration_step_title(max_len, feature, migration_step),
);
}
fn print_checked_action(max_len: usize, feature: &Feature, migration_step: &MigrationStep) {
println_action_green(
"Checked",
&full_migration_step_title(max_len, feature, migration_step),
);
}
fn print_review_action(max_len: usize, feature: &Feature, migration_step: &MigrationStep) {
println_action_yellow(
"Review",
&full_migration_step_title(max_len, feature, migration_step),
);
}
fn print_postponed_action(max_len: usize, feature: &Feature, migration_step: &MigrationStep) {
println_action_yellow(
"Postponed",
&full_migration_step_title(max_len, feature, migration_step),
);
}
fn print_migration_finished_action(num_of_postponed_steps: usize) {
if num_of_postponed_steps > 0 {
println_action_green(
"Finished",
&format!(
"Run `forc migrate` at a later point to resolve {} postponed migration step{}",
num_to_str(num_of_postponed_steps),
plural_s(num_of_postponed_steps),
),
)
} else {
println_action_green("Finished", PROJECT_IS_COMPATIBLE);
}
}
fn print_continue_migration_action(txt: &str) {
println_action_yellow(
"Continue",
&format!("{txt} and re-run `forc migrate` to finish the migration process"),
);
}
fn full_migration_step_title(
max_len: usize,
feature: &Feature,
migration_step: &MigrationStep,
) -> String {
let feature_name_len = max_len + 2;
format!(
"{:<feature_name_len$} {}",
format!("[{}]", feature.name()),
migration_step.title
)
}