From bddf8a499ffcd3473409cb9b827305e0e09cd731 Mon Sep 17 00:00:00 2001 From: Gordy French Date: Tue, 8 Oct 2024 18:03:18 -0700 Subject: [PATCH] Make codemod available in oss compiler Reviewed By: captbaritone Differential Revision: D64006449 fbshipit-source-id: 078ba9dc814cfda39356ea9e1142fe41d40c24e9 --- compiler/crates/relay-bin/Cargo.toml | 1 + compiler/crates/relay-bin/src/errors.rs | 3 + compiler/crates/relay-bin/src/main.rs | 45 +++++++ compiler/crates/relay-codemod/Cargo.toml | 18 +++ compiler/crates/relay-codemod/src/codemod.rs | 126 ++++++++++++++++++ compiler/crates/relay-codemod/src/lib.rs | 15 +++ .../crates/relay-compiler/src/get_programs.rs | 57 ++++++++ compiler/crates/relay-compiler/src/lib.rs | 2 + 8 files changed, 267 insertions(+) create mode 100644 compiler/crates/relay-codemod/Cargo.toml create mode 100644 compiler/crates/relay-codemod/src/codemod.rs create mode 100644 compiler/crates/relay-codemod/src/lib.rs create mode 100644 compiler/crates/relay-compiler/src/get_programs.rs diff --git a/compiler/crates/relay-bin/Cargo.toml b/compiler/crates/relay-bin/Cargo.toml index e0b5d65d3854a..a60253d6b2373 100644 --- a/compiler/crates/relay-bin/Cargo.toml +++ b/compiler/crates/relay-bin/Cargo.toml @@ -13,6 +13,7 @@ clap = { version = "3.2.25", features = ["derive", "env", "regex", "unicode", "w common = { path = "../common" } intern = { path = "../intern" } log = { version = "0.4.22", features = ["kv_unstable"] } +relay-codemod = { path = "../relay-codemod" } relay-compiler = { path = "../relay-compiler" } relay-lsp = { path = "../relay-lsp" } schema = { path = "../schema" } diff --git a/compiler/crates/relay-bin/src/errors.rs b/compiler/crates/relay-bin/src/errors.rs index cf60623f253a0..7aa20884f1710 100644 --- a/compiler/crates/relay-bin/src/errors.rs +++ b/compiler/crates/relay-bin/src/errors.rs @@ -20,4 +20,7 @@ pub enum Error { #[error("Unable to run relay compiler. Error details: \n{details}")] CompilerError { details: String }, + + #[error("Unable to run relay codemod. Error details: \n{details}")] + CodemodError { details: String }, } diff --git a/compiler/crates/relay-bin/src/main.rs b/compiler/crates/relay-bin/src/main.rs index db18f966969cc..88404facb8031 100644 --- a/compiler/crates/relay-bin/src/main.rs +++ b/compiler/crates/relay-bin/src/main.rs @@ -17,12 +17,15 @@ use common::ConsoleLogger; use intern::string_key::Intern; use log::error; use log::info; +use relay_codemod::run_codemod; +use relay_codemod::AvailableCodemod; use relay_compiler::build_project::artifact_writer::ArtifactValidationWriter; use relay_compiler::build_project::generate_extra_artifacts::default_generate_extra_artifacts_fn; use relay_compiler::compiler::Compiler; use relay_compiler::config::Config; use relay_compiler::config::ConfigFile; use relay_compiler::errors::Error as CompilerError; +use relay_compiler::get_programs; use relay_compiler::FileSourceKind; use relay_compiler::LocalPersister; use relay_compiler::OperationPersister; @@ -62,6 +65,27 @@ struct Opt { compile: CompileCommand, } +#[derive(Parser)] +#[clap( + rename_all = "camel_case", + about = "Apply codemod (verification with auto-applied fixes)" +)] +struct CodemodCommand { + /// Compile only this project. You can pass this argument multiple times. + /// to compile multiple projects. If excluded, all projects will be compiled. + #[clap(name = "project", long, short)] + projects: Vec, + + /// Compile using this config file. If not provided, searches for a config in + /// package.json under the `relay` key or `relay.config.json` files among other up + /// from the current working directory. + config: Option, + + /// The name of the codemod to run + #[clap(long, short, arg_enum)] + codemod: AvailableCodemod, +} + #[derive(Parser)] #[clap( rename_all = "camel_case", @@ -129,6 +153,7 @@ enum Commands { Compiler(CompileCommand), Lsp(LspCommand), ConfigJsonSchema(ConfigJsonSchemaCommand), + Codemod(CodemodCommand), } #[derive(ArgEnum, Clone, Copy)] @@ -189,6 +214,7 @@ async fn main() { println!("{}", ConfigFile::json_schema()); Ok(()) } + Commands::Codemod(command) => handle_codemod_command(command).await, }; if let Err(err) = result { @@ -256,6 +282,25 @@ fn set_project_flag(config: &mut Config, projects: Vec) -> Result<(), Er Ok(()) } +async fn handle_codemod_command(command: CodemodCommand) -> Result<(), Error> { + let mut config = get_config(command.config)?; + set_project_flag(&mut config, command.projects)?; + let (programs, _, config) = get_programs(config, Arc::new(ConsoleLogger)).await; + + match run_codemod( + Arc::clone(&programs.source), + Arc::clone(&config), + command.codemod, + ) + .await + { + Ok(_) => Ok(()), + Err(e) => Err(Error::CodemodError { + details: format!("{:?}", e), + }), + } +} + async fn handle_compiler_command(command: CompileCommand) -> Result<(), Error> { configure_logger(command.output, TerminalMode::Mixed); diff --git a/compiler/crates/relay-codemod/Cargo.toml b/compiler/crates/relay-codemod/Cargo.toml new file mode 100644 index 0000000000000..9dbd14027b33b --- /dev/null +++ b/compiler/crates/relay-codemod/Cargo.toml @@ -0,0 +1,18 @@ +# @generated by autocargo from //relay/oss/crates/relay-codemod:relay-codemod + +[package] +name = "relay-codemod" +version = "0.0.0" +authors = ["Facebook"] +edition = "2021" +repository = "https://github.com/facebook/relay" +license = "MIT" + +[dependencies] +clap = { version = "3.2.25", features = ["derive", "env", "regex", "unicode", "wrap_help"] } +graphql-ir = { path = "../graphql-ir" } +log = { version = "0.4.22", features = ["kv_unstable"] } +lsp-types = "0.94.1" +relay-compiler = { path = "../relay-compiler" } +relay-lsp = { path = "../relay-lsp" } +relay-transforms = { path = "../relay-transforms" } diff --git a/compiler/crates/relay-codemod/src/codemod.rs b/compiler/crates/relay-codemod/src/codemod.rs new file mode 100644 index 0000000000000..619097d9d81db --- /dev/null +++ b/compiler/crates/relay-codemod/src/codemod.rs @@ -0,0 +1,126 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +use std::fs; +use std::sync::Arc; + +use clap::ValueEnum; +use graphql_ir::Program; +use log::info; +use lsp_types::CodeActionOrCommand; +use lsp_types::TextEdit; +use lsp_types::Url; +use relay_compiler::config::Config; +use relay_transforms::fragment_alias_directive; +use relay_transforms::validate_unused_variables; + +#[derive(ValueEnum, Debug, Clone)] +pub enum AvailableCodemod { + /// Removes fields that are unused in the GraphQL response + RemoveUnusedVariables, + + /// Marks unaliased conditional fragment spreads as @dangerously_unaliased_fixme + MarkDangerousConditionalFragmentSpreads, +} + +pub async fn run_codemod( + program: Arc, + config: Arc, + codemod: AvailableCodemod, +) -> Result<(), std::io::Error> { + let diagnostics = match &codemod { + AvailableCodemod::RemoveUnusedVariables => match validate_unused_variables(&program) { + Ok(_) => vec![], + Err(e) => e, + }, + AvailableCodemod::MarkDangerousConditionalFragmentSpreads => { + match fragment_alias_directive(&program, true) { + Ok(_) => vec![], + Err(e) => e, + } + } + }; + let actions = relay_lsp::diagnostics_to_code_actions(config, &diagnostics); + + info!( + "Codemod {:?} ran and found {} changes to make.", + codemod, + actions.len() + ); + apply_actions(actions)?; + Ok(()) +} + +fn apply_actions(actions: Vec) -> Result<(), std::io::Error> { + let mut collected_changes = std::collections::HashMap::new(); + + // Collect all the changes into a map of file-to-list-of-changes + for action in actions { + if let CodeActionOrCommand::CodeAction(code_action) = action { + if let Some(changes) = code_action.edit.unwrap().changes { + for (file, changes) in changes { + collected_changes + .entry(file) + .or_insert_with(Vec::new) + .extend(changes); + } + } + } + } + + for (file, mut changes) in collected_changes { + sort_changes(&file, &mut changes)?; + + // Read file into memory and apply changes + let file_contents: String = fs::read_to_string(file.path())?; + let mut lines: Vec = file_contents.lines().map(|s| s.to_string()).collect(); + for change in &changes { + let line = change.range.start.line as usize; + let mut new_line = String::new(); + new_line.push_str(&lines[line][..change.range.start.character as usize]); + new_line.push_str(&change.new_text); + new_line.push_str(&lines[line][change.range.end.character as usize..]); + lines[line] = new_line; + } + + // Write file back out + let new_file_contents = lines.join("\n"); + fs::write(file.path(), new_file_contents)?; + + info!("Applied {} changes to {}", changes.len(), file.path()); + } + Ok(()) +} + +fn sort_changes(url: &Url, changes: &mut Vec) -> Result<(), std::io::Error> { + // Now we have all the changes for this file. Sort them by position within the file, end of file first + // This way the changes are applied in reverse order, so we don't have to worry about altering the positions of the remaining changes + changes.sort_by(|a, b| b.range.start.cmp(&a.range.start)); + + // Verify none of the changes overlap + let mut prev_change: Option<&TextEdit> = None; + for change in changes { + if let Some(prev_change) = prev_change { + if change.range.end.line > prev_change.range.start.line + || (change.range.end.line == prev_change.range.start.line + && change.range.end.character > prev_change.range.start.character) + { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!( + "Codemod produced changes that overlap: File {}, changes: {:?} vs {:?}", + url.path(), + change, + prev_change + ), + )); + } + } + prev_change = Some(change); + } + Ok(()) +} diff --git a/compiler/crates/relay-codemod/src/lib.rs b/compiler/crates/relay-codemod/src/lib.rs new file mode 100644 index 0000000000000..db29f49cdb183 --- /dev/null +++ b/compiler/crates/relay-codemod/src/lib.rs @@ -0,0 +1,15 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#![deny(warnings)] +#![deny(rust_2018_idioms)] +#![deny(clippy::all)] + +mod codemod; + +pub use crate::codemod::run_codemod; +pub use crate::codemod::AvailableCodemod; diff --git a/compiler/crates/relay-compiler/src/get_programs.rs b/compiler/crates/relay-compiler/src/get_programs.rs new file mode 100644 index 0000000000000..f838959a0eaff --- /dev/null +++ b/compiler/crates/relay-compiler/src/get_programs.rs @@ -0,0 +1,57 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +use std::sync::Arc; +use std::sync::Mutex; + +use common::PerfLogger; +use relay_transforms::Programs; + +use crate::compiler::Compiler; +use crate::compiler_state::CompilerState; +use crate::config::Config; +use crate::NoopArtifactWriter; + +pub async fn get_programs( + mut config: Config, + perf_logger: Arc, +) -> (Arc, CompilerState, Arc) { + let raw_programs: Arc>>> = Arc::new(Mutex::new(vec![])); + let raw_programs_cloned = raw_programs.clone(); + + config.compile_everything = true; + config.generate_virtual_id_file_name = None; + config.artifact_writer = Box::new(NoopArtifactWriter); + config.generate_extra_artifacts = Some(Box::new( + move |_config, _project_config, _schema, programs, _artifacts| { + raw_programs_cloned + .lock() + .unwrap() + .push(Arc::new(programs.clone())); + vec![] + }, + )); + let config = Arc::new(config); + + let compiler = Compiler::new(Arc::clone(&config), Arc::clone(&perf_logger)); + let compiler_state = match compiler.compile().await { + Ok(compiler_state) => compiler_state, + Err(e) => { + eprintln!("{}", e); + std::process::exit(1); + } + }; + let programs = { + let guard = raw_programs.lock().unwrap(); + if guard.len() < 1 { + eprintln!("Failed to extract program from compiler state"); + std::process::exit(1); + } + guard[0].clone() + }; + (Arc::clone(&programs), compiler_state, Arc::clone(&config)) +} diff --git a/compiler/crates/relay-compiler/src/lib.rs b/compiler/crates/relay-compiler/src/lib.rs index 88d23692e49eb..2aabce08e8173 100644 --- a/compiler/crates/relay-compiler/src/lib.rs +++ b/compiler/crates/relay-compiler/src/lib.rs @@ -18,6 +18,7 @@ pub mod config; mod docblocks; pub mod errors; pub mod file_source; +mod get_programs; mod graphql_asts; mod operation_persister; mod red_to_green; @@ -67,6 +68,7 @@ pub use file_source::FileSourceSubscriptionNextChange; pub use file_source::FsSourceReader; pub use file_source::SourceControlUpdateStatus; pub use file_source::SourceReader; +pub use get_programs::get_programs; pub use graphql_asts::GraphQLAsts; pub use operation_persister::LocalPersister; pub use operation_persister::RemotePersister;