compiler/crates/relay-codegen/src/printer.rs (543 lines of code) (raw):

/* * 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 crate::ast::{Ast, AstBuilder, AstKey, ObjectEntry, Primitive, QueryID, RequestParameters}; use crate::build_ast::{ build_fragment, build_operation, build_provided_variables, build_request, build_request_params, build_request_params_ast_key, }; use crate::constants::CODEGEN_CONSTANTS; use crate::indentation::print_indentation; use crate::top_level_statements::{TopLevelStatement, TopLevelStatements}; use crate::utils::escape; use crate::{object, CodegenBuilder, CodegenVariant, JsModuleFormat}; use graphql_ir::{FragmentDefinition, OperationDefinition}; use relay_config::ProjectConfig; use schema::SDLSchema; use fnv::{FnvBuildHasher, FnvHashSet}; use indexmap::IndexMap; use intern::string_key::StringKey; use std::fmt::{Result as FmtResult, Write}; pub fn print_operation( schema: &SDLSchema, operation: &OperationDefinition, project_config: &ProjectConfig, top_level_statements: &mut TopLevelStatements, ) -> String { Printer::without_dedupe(project_config).print_operation(schema, operation, top_level_statements) } pub fn print_fragment( schema: &SDLSchema, fragment: &FragmentDefinition, project_config: &ProjectConfig, top_level_statements: &mut TopLevelStatements, ) -> String { Printer::without_dedupe(project_config).print_fragment(schema, fragment, top_level_statements) } pub fn print_request( schema: &SDLSchema, operation: &OperationDefinition, fragment: &FragmentDefinition, request_parameters: RequestParameters<'_>, project_config: &ProjectConfig, top_level_statements: &mut TopLevelStatements, ) -> String { Printer::without_dedupe(project_config).print_request( schema, operation, fragment, request_parameters, top_level_statements, ) } pub fn print_request_params( schema: &SDLSchema, operation: &OperationDefinition, query_id: &Option<QueryID>, project_config: &ProjectConfig, top_level_statements: &mut TopLevelStatements, ) -> String { let mut request_parameters = build_request_params(operation); request_parameters.id = query_id; let mut builder = AstBuilder::default(); let request_parameters_ast_key = build_request_params_ast_key( schema, request_parameters, &mut builder, operation, top_level_statements, ); let printer = JSONPrinter::new(&builder, project_config, top_level_statements); printer.print(request_parameters_ast_key, false) } pub struct Printer<'p> { project_config: &'p ProjectConfig, builder: AstBuilder, dedupe: bool, } impl<'p> Printer<'p> { pub fn with_dedupe(project_config: &'p ProjectConfig) -> Self { Self { project_config, builder: Default::default(), dedupe: true, } } pub fn without_dedupe(project_config: &'p ProjectConfig) -> Self { Self { project_config, builder: Default::default(), dedupe: false, } } pub fn print_provided_variables( &mut self, schema: &SDLSchema, operation: &OperationDefinition, top_level_statements: &mut TopLevelStatements, ) -> Option<String> { let key = build_provided_variables(schema, &mut self.builder, operation)?; let printer = JSONPrinter::new(&self.builder, self.project_config, top_level_statements); Some(printer.print(key, self.dedupe)) } pub fn print_updatable_query( &mut self, schema: &SDLSchema, fragment: &FragmentDefinition, ) -> String { let mut fragment_builder = CodegenBuilder::new(schema, CodegenVariant::Reader, &mut self.builder); let fragment = Primitive::Key(fragment_builder.build_fragment(fragment, true)); let key = self.builder.intern(Ast::Object(object! { fragment: fragment, kind: Primitive::String(CODEGEN_CONSTANTS.updatable_query), })); let mut top_level_statements = Default::default(); let printer = JSONPrinter::new( &self.builder, self.project_config, &mut top_level_statements, ); printer.print(key, self.dedupe) } pub fn print_request( &mut self, schema: &SDLSchema, operation: &OperationDefinition, fragment: &FragmentDefinition, request_parameters: RequestParameters<'_>, top_level_statements: &mut TopLevelStatements, ) -> String { let request_parameters = build_request_params_ast_key( schema, request_parameters, &mut self.builder, operation, top_level_statements, ); let key = build_request( schema, &mut self.builder, operation, fragment, request_parameters, ); let printer = JSONPrinter::new(&self.builder, self.project_config, top_level_statements); printer.print(key, self.dedupe) } pub fn print_operation( &mut self, schema: &SDLSchema, operation: &OperationDefinition, top_level_statements: &mut TopLevelStatements, ) -> String { let key = build_operation(schema, &mut self.builder, operation); let printer = JSONPrinter::new(&self.builder, self.project_config, top_level_statements); printer.print(key, self.dedupe) } pub fn print_fragment( &mut self, schema: &SDLSchema, fragment: &FragmentDefinition, top_level_statements: &mut TopLevelStatements, ) -> String { let key = build_fragment(schema, &mut self.builder, fragment); let printer = JSONPrinter::new(&self.builder, self.project_config, top_level_statements); printer.print(key, self.dedupe) } pub fn print_request_params( &mut self, schema: &SDLSchema, request_parameters: RequestParameters<'_>, operation: &OperationDefinition, top_level_statements: &mut TopLevelStatements, ) -> String { let key = build_request_params_ast_key( schema, request_parameters, &mut self.builder, operation, top_level_statements, ); let printer = JSONPrinter::new(&self.builder, self.project_config, top_level_statements); printer.print(key, self.dedupe) } } type VariableDefinitions = IndexMap<AstKey, String, FnvBuildHasher>; pub struct JSONPrinter<'b> { variable_definitions: VariableDefinitions, duplicates: FnvHashSet<AstKey>, builder: &'b AstBuilder, eager_es_modules: bool, js_module_format: JsModuleFormat, top_level_statements: &'b mut TopLevelStatements, skip_printing_nulls: bool, } impl<'b> JSONPrinter<'b> { pub fn new( builder: &'b AstBuilder, project_config: &ProjectConfig, top_level_statements: &'b mut TopLevelStatements, ) -> Self { Self { variable_definitions: Default::default(), top_level_statements, duplicates: Default::default(), builder, js_module_format: project_config.js_module_format, eager_es_modules: project_config.typegen_config.eager_es_modules, skip_printing_nulls: project_config .feature_flags .skip_printing_nulls .is_fully_enabled(), } } pub fn print(mut self, root_key: AstKey, dedupe: bool) -> String { if dedupe { let mut visited = Default::default(); self.collect_value_duplicates(&mut visited, root_key); } let mut result = String::new(); self.print_ast(&mut result, root_key, 0, false); if self.variable_definitions.is_empty() { result } else { let mut with_variables = String::new(); with_variables.push_str("(function(){\nvar "); let last = self.variable_definitions.len() - 1; for (i, (_, value)) in self.variable_definitions.drain(..).enumerate() { writeln!( &mut with_variables, "v{} = {}{}", i, value, if i == last { ";" } else { "," } ) .unwrap(); } write!(&mut with_variables, "return {};\n}})()", result).unwrap(); with_variables } } /// We don't dedupe in an already deduped AST unless the duplicate /// also appears on a subtree that's not a duplicate /// Input: /// [ /// [{a: 1}, {b:2}], /// [{a: 1}, {b:2}], /// {b: 2} /// ] /// Output: /// v0 = {b: 2}; /// v1 = [{a: 1}, v0]; fn collect_value_duplicates(&mut self, visited: &mut FnvHashSet<AstKey>, key: AstKey) { match self.builder.lookup(key) { Ast::Array(array) => { if array.is_empty() { return; } if !visited.insert(key) { self.duplicates.insert(key); return; } for val in array { if let Primitive::Key(key) = val { self.collect_value_duplicates(visited, *key); } } } Ast::Object(object) => { if object.is_empty() { return; } if !visited.insert(key) { self.duplicates.insert(key); return; } for entry in object { if let Primitive::Key(key) = entry.value { self.collect_value_duplicates(visited, key); } } } } } fn print_ast(&mut self, f: &mut String, key: AstKey, indent: usize, is_dedupe_var: bool) { // Only use variable references at depth beyond the top level. if indent > 0 && self.duplicates.contains(&key) { let v = if self.variable_definitions.contains_key(&key) { self.variable_definitions.get_full(&key).unwrap().0 } else { let mut variable = String::new(); self.print_ast(&mut variable, key, 0, true); let v = self.variable_definitions.len(); self.variable_definitions.insert(key, variable); v }; return write!(f, "(v{}/*: any*/)", v).unwrap(); } let ast = self.builder.lookup(key); match ast { Ast::Object(object) => { if object.is_empty() { f.push_str("{}"); } else { let next_indent = indent + 1; f.push('{'); for ObjectEntry { key, value } in object { match value { Primitive::SkippableNull if self.skip_printing_nulls => continue, _ => {} } f.push('\n'); print_indentation(f, next_indent); write!(f, "\"{}\": ", key).unwrap(); self.print_primitive(f, value, next_indent, is_dedupe_var) .unwrap(); f.push(','); } f.pop(); f.push('\n'); print_indentation(f, indent); f.push('}'); } } Ast::Array(array) => { if array.is_empty() { if is_dedupe_var { // Empty arrays can only have one inferred flow type and then conflict if // used in different places, this is unsound if we would write to them but // this whole module is based on the idea of a read only JSON tree. f.push_str("([]/*: any*/)"); } else { f.push_str("[]"); } } else { f.push('['); let next_indent = indent + 1; for value in array { match value { Primitive::SkippableNull if self.skip_printing_nulls => continue, _ => {} } f.push('\n'); print_indentation(f, next_indent); self.print_primitive(f, value, next_indent, is_dedupe_var) .unwrap(); f.push(','); } f.pop(); f.push('\n'); print_indentation(f, indent); f.push(']'); } } } } fn print_primitive( &mut self, f: &mut String, primitive: &Primitive, indent: usize, is_dedupe_var: bool, ) -> FmtResult { match primitive { Primitive::Null | Primitive::SkippableNull => write!(f, "null"), Primitive::Bool(b) => write!(f, "{}", if *b { "true" } else { "false" }), Primitive::RawString(str) => { f.push('\"'); escape(str, f); f.push('\"'); Ok(()) } Primitive::String(key) => write!(f, "\"{}\"", key), Primitive::Float(value) => write!(f, "{}", value.as_float()), Primitive::Int(value) => write!(f, "{}", value), Primitive::Variable(variable_name) => write!(f, "{}", variable_name), Primitive::Key(key) => { self.print_ast(f, *key, indent, is_dedupe_var); Ok(()) } Primitive::StorageKey(field_name, key) => { write_static_storage_key(f, self.builder, *field_name, *key) } Primitive::GraphQLModuleDependency(key) => match self.js_module_format { JsModuleFormat::CommonJS => self.write_js_dependency( f, format!("{}_graphql", key), format!("./{}.graphql", key), ), JsModuleFormat::Haste => self.write_js_dependency( f, format!("{}_graphql", key), format!("{}.graphql", key), ), }, Primitive::JSModuleDependency(key) => match self.js_module_format { JsModuleFormat::CommonJS => { self.write_js_dependency(f, key.to_string(), format!("./{}", key)) } JsModuleFormat::Haste => { self.write_js_dependency(f, key.to_string(), key.to_string()) } }, } } fn write_js_dependency(&mut self, f: &mut String, name: String, path: String) -> FmtResult { if self.eager_es_modules { let write_result = write!(f, "{}", name); self.top_level_statements.insert( name.clone(), TopLevelStatement::ImportStatement { name, path }, ); write_result } else { write!(f, "require('{}')", path) } } } fn write_static_storage_key( f: &mut String, builder: &AstBuilder, field_name: StringKey, args_key: AstKey, ) -> FmtResult { write!(f, "\"{}(", field_name)?; let args = builder.lookup(args_key).assert_array(); for arg_key in args { let arg = builder.lookup(arg_key.assert_key()).assert_object(); let name = &arg .iter() .find(|ObjectEntry { key, value: _ }| *key == CODEGEN_CONSTANTS.name) .expect("Expected `name` to exist") .value; let name = name.assert_string(); write!(f, "{}:", name)?; write_argument_value(f, builder, arg)?; f.push(','); } f.pop(); // args won't be empty f.push_str(")\""); Ok(()) } fn write_argument_value(f: &mut String, builder: &AstBuilder, arg: &[ObjectEntry]) -> FmtResult { let key = &arg .iter() .find(|entry| entry.key == CODEGEN_CONSTANTS.kind) .expect("Expected `kind` to exist") .value; let key = key.assert_string(); // match doesn't allow `CODEGEN_CONSTANTS.<>` on the match arm, falling back to if statements if key == CODEGEN_CONSTANTS.literal { let literal = &arg .iter() .find(|entry| entry.key == CODEGEN_CONSTANTS.value) .expect("Expected `name` to exist") .value; write_constant_value(f, builder, literal)?; } else if key == CODEGEN_CONSTANTS.list_value { let items = &arg .iter() .find(|entry| entry.key == CODEGEN_CONSTANTS.items) .expect("Expected `items` to exist") .value; let array = builder.lookup(items.assert_key()).assert_array(); f.push('['); let mut after_first = false; for key_or_null in array { match key_or_null { Primitive::Null => {} Primitive::Key(key) => { if after_first { f.push(','); } else { after_first = true; } let object = builder.lookup(*key).assert_object(); write_argument_value(f, builder, object)?; } _ => panic!("Expected an object key or null"), } } f.push(']'); } else { // We filtered out Variables, here it should only be ObjectValue let fields = &arg .iter() .find(|entry| entry.key == CODEGEN_CONSTANTS.fields) .expect("Expected `fields` to exist") .value; let fields = builder.lookup(fields.assert_key()).assert_array(); f.push('{'); for field in fields { let field = builder.lookup(field.assert_key()).assert_object(); let name = &field .iter() .find(|entry| entry.key == CODEGEN_CONSTANTS.name) .expect("Expected `name` to exist") .value; let name = name.assert_string(); write!(f, "\\\"{}\\\":", name)?; write_argument_value(f, builder, field)?; f.push(','); } if !fields.is_empty() { f.pop(); } f.push('}'); } Ok(()) } fn write_constant_value(f: &mut String, builder: &AstBuilder, value: &Primitive) -> FmtResult { match value { Primitive::Bool(b) => write!(f, "{}", if *b { "true" } else { "false" }), Primitive::String(key) => write!(f, "\\\"{}\\\"", key), Primitive::Float(value) => write!(f, "{}", value.as_float()), Primitive::Int(value) => write!(f, "{}", value), Primitive::Variable(variable_name) => write!(f, "{}", variable_name), Primitive::Key(key) => { let ast = builder.lookup(*key); match ast { Ast::Array(arr) => { f.push('['); for value in arr { write_constant_value(f, builder, value)?; f.push(','); } if !arr.is_empty() { f.pop(); } f.push(']'); Ok(()) } Ast::Object(obj) => { f.push('{'); for ObjectEntry { key: name, value } in obj { write!(f, "\\\"{}\\\":", name)?; write_constant_value(f, builder, value)?; f.push(','); } if !obj.is_empty() { f.pop(); } f.push('}'); Ok(()) } } } Primitive::Null | Primitive::SkippableNull => { f.push_str("null"); Ok(()) } Primitive::StorageKey(_, _) => panic!("Unexpected StorageKey"), Primitive::RawString(_) => panic!("Unexpected RawString"), Primitive::GraphQLModuleDependency(_) => panic!("Unexpected GraphQLModuleDependency"), Primitive::JSModuleDependency(_) => panic!("Unexpected JSModuleDependency"), } }