formatter.rs 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. // SPDX-License-Identifier: Apache-2.0
  2. use solang_forge_fmt::{format_to, parse, solang_ext::AstEq, FormatterConfig};
  3. use itertools::Itertools;
  4. use std::{fs, path::PathBuf};
  5. use tracing_subscriber::{EnvFilter, FmtSubscriber};
  6. fn tracing() {
  7. let subscriber = FmtSubscriber::builder()
  8. .with_env_filter(EnvFilter::from_default_env())
  9. .with_test_writer()
  10. .finish();
  11. let _ = tracing::subscriber::set_global_default(subscriber);
  12. }
  13. fn test_directory(base_name: &str, test_config: TestConfig) {
  14. tracing();
  15. let mut original = None;
  16. let tests = fs::read_dir(
  17. PathBuf::from(env!("CARGO_MANIFEST_DIR"))
  18. .join("testdata")
  19. .join(base_name),
  20. )
  21. .unwrap()
  22. .filter_map(|path| {
  23. let path = path.unwrap().path();
  24. let source = fs::read_to_string(&path).unwrap();
  25. if let Some(filename) = path.file_name().and_then(|name| name.to_str()) {
  26. if filename == "original.sol" {
  27. original = Some(source);
  28. } else if filename
  29. .strip_suffix("fmt.sol")
  30. .map(|filename| filename.strip_suffix('.'))
  31. .is_some()
  32. {
  33. // The majority of the tests were written with the assumption
  34. // that the default value for max line length is `80`.
  35. // Preserve that to avoid rewriting test logic.
  36. let default_config = FormatterConfig {
  37. line_length: 80,
  38. ..Default::default()
  39. };
  40. let mut config = toml::Value::try_from(default_config).unwrap();
  41. let config_table = config.as_table_mut().unwrap();
  42. let mut lines = source.split('\n').peekable();
  43. let mut line_num = 1;
  44. while let Some(line) = lines.peek() {
  45. let entry = line
  46. .strip_prefix("//")
  47. .and_then(|line| line.trim().strip_prefix("config:"))
  48. .map(str::trim);
  49. let entry = if let Some(entry) = entry {
  50. entry
  51. } else {
  52. break;
  53. };
  54. let values = match toml::from_str::<toml::Value>(entry) {
  55. Ok(toml::Value::Table(table)) => table,
  56. _ => panic!("Invalid config item in {filename} at {line_num}"),
  57. };
  58. config_table.extend(values);
  59. line_num += 1;
  60. lines.next();
  61. }
  62. let config = config
  63. .try_into()
  64. .unwrap_or_else(|err| panic!("Invalid config for {filename}: {err}"));
  65. return Some((filename.to_string(), config, lines.join("\n")));
  66. }
  67. }
  68. None
  69. })
  70. .collect::<Vec<_>>();
  71. for (filename, config, formatted) in tests {
  72. test_formatter(
  73. &filename,
  74. config,
  75. original.as_ref().expect("original.sol not found"),
  76. &formatted,
  77. test_config,
  78. );
  79. }
  80. }
  81. fn assert_eof(content: &str) {
  82. assert!(content.ends_with('\n') && !content.ends_with("\n\n"));
  83. }
  84. fn test_formatter(
  85. filename: &str,
  86. config: FormatterConfig,
  87. source: &str,
  88. expected_source: &str,
  89. test_config: TestConfig,
  90. ) {
  91. #[derive(Eq)]
  92. struct PrettyString(String);
  93. impl PartialEq for PrettyString {
  94. fn eq(&self, other: &Self) -> bool {
  95. self.0.lines().eq(other.0.lines())
  96. }
  97. }
  98. impl std::fmt::Debug for PrettyString {
  99. fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  100. f.write_str(&self.0)
  101. }
  102. }
  103. assert_eof(expected_source);
  104. let source_parsed = parse(source).unwrap();
  105. let expected_parsed = parse(expected_source).unwrap();
  106. if !test_config.skip_compare_ast_eq && !source_parsed.pt.ast_eq(&expected_parsed.pt) {
  107. similar_asserts::assert_eq!(
  108. source_parsed.pt,
  109. expected_parsed.pt,
  110. "(formatted Parse Tree == expected Parse Tree) in {}",
  111. filename
  112. );
  113. }
  114. let expected = PrettyString(expected_source.to_string());
  115. let mut source_formatted = String::new();
  116. format_to(&mut source_formatted, source_parsed, config.clone()).unwrap();
  117. assert_eof(&source_formatted);
  118. let source_formatted = PrettyString(source_formatted);
  119. similar_asserts::assert_eq!(
  120. source_formatted,
  121. expected,
  122. "(formatted == expected) in {}",
  123. filename
  124. );
  125. let mut expected_formatted = String::new();
  126. format_to(&mut expected_formatted, expected_parsed, config).unwrap();
  127. assert_eof(&expected_formatted);
  128. let expected_formatted = PrettyString(expected_formatted);
  129. similar_asserts::assert_eq!(
  130. expected_formatted,
  131. expected,
  132. "(formatted == expected) in {}",
  133. filename
  134. );
  135. }
  136. #[derive(Clone, Copy, Default)]
  137. struct TestConfig {
  138. /// Whether to compare the formatted source code AST with the original AST
  139. skip_compare_ast_eq: bool,
  140. }
  141. impl TestConfig {
  142. fn skip_compare_ast_eq() -> Self {
  143. Self {
  144. skip_compare_ast_eq: true,
  145. }
  146. }
  147. }
  148. macro_rules! test_dir {
  149. ($dir:ident $(,)?) => {
  150. test_dir!($dir, Default::default());
  151. };
  152. ($dir:ident, $config:expr $(,)?) => {
  153. #[allow(non_snake_case)]
  154. #[test]
  155. fn $dir() {
  156. test_directory(stringify!($dir), $config);
  157. }
  158. };
  159. }
  160. macro_rules! test_directories {
  161. ($($dir:ident),+ $(,)?) => {$(
  162. test_dir!($dir);
  163. )+};
  164. }
  165. test_directories! {
  166. ConstructorDefinition,
  167. ConstructorModifierStyle,
  168. ContractDefinition,
  169. DocComments,
  170. EnumDefinition,
  171. ErrorDefinition,
  172. EventDefinition,
  173. FunctionDefinition,
  174. FunctionDefinitionWithFunctionReturns,
  175. FunctionType,
  176. ImportDirective,
  177. ModifierDefinition,
  178. StatementBlock,
  179. StructDefinition,
  180. TypeDefinition,
  181. UsingDirective,
  182. VariableDefinition,
  183. OperatorExpressions,
  184. WhileStatement,
  185. DoWhileStatement,
  186. ForStatement,
  187. IfStatement,
  188. IfStatement2,
  189. VariableAssignment,
  190. FunctionCallArgsStatement,
  191. RevertStatement,
  192. RevertNamedArgsStatement,
  193. ReturnStatement,
  194. TryStatement,
  195. ConditionalOperatorExpression,
  196. NamedFunctionCallExpression,
  197. ArrayExpressions,
  198. UnitExpression,
  199. ThisExpression,
  200. SimpleComments,
  201. LiteralExpression,
  202. Yul,
  203. YulStrings,
  204. IntTypes,
  205. InlineDisable,
  206. NumberLiteralUnderscore,
  207. HexUnderscore,
  208. FunctionCall,
  209. TrailingComma,
  210. PragmaDirective,
  211. Annotation,
  212. MappingType,
  213. EmitStatement,
  214. Repros,
  215. BlockComments,
  216. BlockCommentsFunction,
  217. EnumVariants,
  218. }
  219. test_dir!(SortedImports, TestConfig::skip_compare_ast_eq());