Skip to content

Commit

Permalink
Make codemod available in oss compiler
Browse files Browse the repository at this point in the history
Reviewed By: captbaritone

Differential Revision: D64006449

fbshipit-source-id: 078ba9dc814cfda39356ea9e1142fe41d40c24e9
  • Loading branch information
gordyf authored and facebook-github-bot committed Oct 9, 2024
1 parent 6e4c120 commit bddf8a4
Show file tree
Hide file tree
Showing 8 changed files with 267 additions and 0 deletions.
1 change: 1 addition & 0 deletions compiler/crates/relay-bin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
3 changes: 3 additions & 0 deletions compiler/crates/relay-bin/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
}
45 changes: 45 additions & 0 deletions compiler/crates/relay-bin/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String>,

/// 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<PathBuf>,

/// The name of the codemod to run
#[clap(long, short, arg_enum)]
codemod: AvailableCodemod,
}

#[derive(Parser)]
#[clap(
rename_all = "camel_case",
Expand Down Expand Up @@ -129,6 +153,7 @@ enum Commands {
Compiler(CompileCommand),
Lsp(LspCommand),
ConfigJsonSchema(ConfigJsonSchemaCommand),
Codemod(CodemodCommand),
}

#[derive(ArgEnum, Clone, Copy)]
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -256,6 +282,25 @@ fn set_project_flag(config: &mut Config, projects: Vec<String>) -> 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);

Expand Down
18 changes: 18 additions & 0 deletions compiler/crates/relay-codemod/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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" }
126 changes: 126 additions & 0 deletions compiler/crates/relay-codemod/src/codemod.rs
Original file line number Diff line number Diff line change
@@ -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<Program>,
config: Arc<Config>,
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<CodeActionOrCommand>) -> 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<String> = 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<TextEdit>) -> 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(())
}
15 changes: 15 additions & 0 deletions compiler/crates/relay-codemod/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
57 changes: 57 additions & 0 deletions compiler/crates/relay-compiler/src/get_programs.rs
Original file line number Diff line number Diff line change
@@ -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<TPerfLogger: PerfLogger + 'static>(
mut config: Config,
perf_logger: Arc<TPerfLogger>,
) -> (Arc<Programs>, CompilerState, Arc<Config>) {
let raw_programs: Arc<Mutex<Vec<Arc<Programs>>>> = 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))
}
2 changes: 2 additions & 0 deletions compiler/crates/relay-compiler/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit bddf8a4

Please sign in to comment.