1 package host.exp.exponent.tools; 2 3 import com.github.javaparser.JavaParser; 4 import com.github.javaparser.ParseException; 5 import com.github.javaparser.ast.CompilationUnit; 6 import com.github.javaparser.ast.Modifier; 7 import com.github.javaparser.ast.Node; 8 import com.github.javaparser.ast.NodeList; 9 import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; 10 import com.github.javaparser.ast.body.ConstructorDeclaration; 11 import com.github.javaparser.ast.body.FieldDeclaration; 12 import com.github.javaparser.ast.body.MethodDeclaration; 13 import com.github.javaparser.ast.body.Parameter; 14 import com.github.javaparser.ast.comments.LineComment; 15 import com.github.javaparser.ast.expr.Expression; 16 import com.github.javaparser.ast.expr.MethodCallExpr; 17 import com.github.javaparser.ast.expr.SimpleName; 18 import com.github.javaparser.ast.stmt.BlockStmt; 19 import com.github.javaparser.ast.stmt.CatchClause; 20 import com.github.javaparser.ast.stmt.EmptyStmt; 21 import com.github.javaparser.ast.stmt.LabeledStmt; 22 import com.github.javaparser.ast.stmt.Statement; 23 import com.github.javaparser.ast.stmt.TryStmt; 24 import com.github.javaparser.ast.type.ReferenceType; 25 import com.github.javaparser.ast.type.UnionType; 26 import com.github.javaparser.ast.visitor.GenericVisitor; 27 import com.github.javaparser.ast.visitor.ModifierVisitor; 28 import com.github.javaparser.ast.visitor.VoidVisitor; 29 30 import org.apache.commons.io.FileUtils; 31 import org.json.JSONObject; 32 33 import java.io.File; 34 import java.io.FileInputStream; 35 import java.io.FileOutputStream; 36 import java.io.IOException; 37 import java.io.OutputStream; 38 import java.util.ArrayList; 39 import java.util.Arrays; 40 import java.util.EnumSet; 41 import java.util.HashMap; 42 import java.util.List; 43 import java.util.Map; 44 45 46 public class ReactAndroidCodeTransformer { 47 48 private static final String REACT_ANDROID_DEST_ROOT = "react-native-lab/react-native/packages/react-native/ReactAndroid"; 49 private static final String SOURCE_PATH = "src/main/java/com/facebook/react/"; 50 51 private static abstract class MethodVisitor { visit(final String name, final MethodDeclaration n)52 abstract Node visit(final String name, final MethodDeclaration n); modifySource(final String source)53 String modifySource(final String source) { 54 return source; 55 }; 56 } 57 58 private static final Map<String, MethodVisitor> FILES_TO_MODIFY = new HashMap<>(); 59 getCallMethodReflectionBlock(String className, String methodNameAndTypes, String targetAndValues)60 private static String getCallMethodReflectionBlock(String className, String methodNameAndTypes, String targetAndValues) { 61 return getCallMethodReflectionBlock(className, methodNameAndTypes, targetAndValues, "", ""); 62 } 63 getCallMethodReflectionBlock(String className, String methodNameAndTypes, String targetAndValues, String returnValue, String defaultReturnValue)64 private static String getCallMethodReflectionBlock(String className, String methodNameAndTypes, String targetAndValues, String returnValue, String defaultReturnValue) { 65 return "{\n" + 66 " try {\n" + 67 " " + returnValue + "Class.forName(\"" + className + "\").getMethod(" + methodNameAndTypes + ").invoke(" + targetAndValues + ");\n" + 68 " } catch (Exception expoHandleErrorException) {\n" + 69 " expoHandleErrorException.printStackTrace();\n" + defaultReturnValue + 70 " }\n" + 71 "}"; 72 } 73 getHandleErrorBlockString(String title, String details, String exceptionId, String isFatal)74 private static String getHandleErrorBlockString(String title, String details, String exceptionId, String isFatal) { 75 return getCallMethodReflectionBlock("host.exp.exponent.ReactNativeStaticHelpers", "\"handleReactNativeError\", String.class, Object.class, Integer.class, Boolean.class", "null, " + title + ", " + details + ", " + exceptionId + ", " + isFatal); 76 } 77 getHandleErrorBlock(String title, String details, String exceptionId, String isFatal)78 private static BlockStmt getHandleErrorBlock(String title, String details, String exceptionId, String isFatal) { 79 return JavaParser.parseBlock(getHandleErrorBlockString(title, details, exceptionId, isFatal)); 80 } 81 getCatchClause(String title, String details, String exceptionId, String isFatal)82 private static CatchClause getCatchClause(String title, String details, String exceptionId, String isFatal) { 83 ReferenceType t = JavaParser.parseClassOrInterfaceType("RuntimeException"); 84 SimpleName v = new SimpleName("expoException"); 85 BlockStmt catchBlock = getHandleErrorBlock(title, details, exceptionId, isFatal); 86 return getCatchClause(Arrays.asList(t), v, catchBlock); 87 } 88 getCatchClause()89 private static CatchClause getCatchClause() { 90 ReferenceType t = JavaParser.parseClassOrInterfaceType("Throwable"); 91 SimpleName v = new SimpleName("expoException"); 92 return getCatchClause(Arrays.asList(t), v, new BlockStmt()); 93 } 94 getCatchClause( List<ReferenceType> exceptionTypes, SimpleName exceptionId, BlockStmt catchBlock)95 private static CatchClause getCatchClause( 96 List<ReferenceType> exceptionTypes, 97 SimpleName exceptionId, 98 BlockStmt catchBlock) { 99 UnionType type = new UnionType(NodeList.nodeList(exceptionTypes)); 100 Parameter exceptionParam = new Parameter(type, exceptionId); 101 return new CatchClause(exceptionParam, catchBlock); 102 } 103 getTryCatch(Statement statement, String title, String details, String exceptionId, String isFatal)104 private static TryStmt getTryCatch(Statement statement, String title, String details, String exceptionId, String isFatal) { 105 TryStmt tryStatement = new TryStmt(); 106 BlockStmt tryBlockStatement = new BlockStmt(NodeList.nodeList(statement)); 107 tryStatement.setTryBlock(tryBlockStatement); 108 tryStatement.setCatchClauses(NodeList.nodeList(getCatchClause(title, details, exceptionId, isFatal))); 109 return tryStatement; 110 } 111 getTryCatch(Statement statement)112 private static TryStmt getTryCatch(Statement statement) { 113 TryStmt tryStatement = new TryStmt(); 114 BlockStmt tryBlockStatement = new BlockStmt(NodeList.nodeList(statement)); 115 tryStatement.setTryBlock(tryBlockStatement); 116 tryStatement.setCatchClauses(NodeList.nodeList(getCatchClause())); 117 return tryStatement; 118 } 119 addBeforeEndOfClass(final String source, final String add)120 private static String addBeforeEndOfClass(final String source, final String add) { 121 int endOfClass = source.lastIndexOf("}"); 122 return source.substring(0, endOfClass) + "\n" + add + "\n" + source.substring(endOfClass); 123 } 124 125 static { 126 FILES_TO_MODIFY.put("devsupport/DevServerHelper.java", new MethodVisitor() { 127 128 @Override 129 public Node visit(String methodName, MethodDeclaration n) { 130 switch (methodName) { 131 case "createBundleURL": 132 // In RN 0.54 this method is overloaded; skip the convenience version 133 NodeList<Parameter> params = n.getParameters(); 134 if (params.size() == 2 && params.get(0).getNameAsString().equals("mainModuleID") && 135 params.get(1).getNameAsString().equals("type")) { 136 return n; 137 } 138 139 BlockStmt stmt = JavaParser.parseBlock(getCallMethodReflectionBlock( 140 "host.exp.exponent.ReactNativeStaticHelpers", 141 "\"getBundleUrlForActivityId\", int.class, String.class, String.class, String.class, boolean.class, boolean.class", 142 "null, mSettings.exponentActivityId, host, mainModuleID, type.typeID(), getDevMode(), getJSMinifyMode()", 143 "return (String) ", 144 "return null;")); 145 n.setBody(stmt); 146 n.getModifiers().remove(Modifier.STATIC); 147 return n; 148 } 149 150 return n; 151 } 152 }); 153 FILES_TO_MODIFY.put("modules/network/OkHttpClientProvider.java", new MethodVisitor() { 154 155 @Override 156 public Node visit(String methodName, MethodDeclaration n) { 157 switch (methodName) { 158 case "createClient": 159 BlockStmt stmt = JavaParser.parseBlock(getCallMethodReflectionBlock( 160 "host.exp.exponent.ReactNativeStaticHelpers", 161 "\"getOkHttpClient\", Class.class", 162 "null, OkHttpClientProvider.class", 163 "return (OkHttpClient) ", 164 "return null;")); 165 n.setBody(stmt); 166 return n; 167 } 168 169 return n; 170 } 171 }); 172 FILES_TO_MODIFY.put("devsupport/DevSupportManagerBase.java", new MethodVisitor() { 173 174 @Override 175 public Node visit(String methodName, MethodDeclaration n) { 176 switch (methodName) { 177 case "handleReloadJS": 178 // Catch error if "draw over other apps" not enabled 179 return handleReloadJS(n); 180 case "handleException": 181 // Handle any uncaught error in original method 182 return handleException(n); 183 case "hasUpToDateJSBundleInCache": 184 // Use this to always force a refresh in debug mode. 185 return hasUpToDateJSBundleInCache(n); 186 case "showDevOptionsDialog": 187 return showDevOptionsDialog(n); 188 case "getExponentActivityId": 189 n.setBody(JavaParser.parseBlock("{return mDevServerHelper.mSettings.exponentActivityId;}")); 190 return n; 191 } 192 193 return n; 194 } 195 }); 196 FILES_TO_MODIFY.put("devsupport/BridgeDevSupportManager.java", null); 197 198 FILES_TO_MODIFY.put("modules/core/ExceptionsManagerModule.java", new MethodVisitor() { 199 200 @Override 201 public Node visit(String methodName, MethodDeclaration n) { 202 // In dev mode call the original methods. Otherwise open Expo error screen 203 switch (methodName) { 204 case "reportFatalException": 205 return exceptionsManagerModuleHandleException(n, "message", "stack", "(int) idDouble", "true"); 206 case "reportSoftException": 207 return exceptionsManagerModuleHandleException(n, "message", "stack", "(int) idDouble", "false"); 208 case "updateExceptionMessage": 209 return exceptionsManagerModuleHandleException(n, "title", "details", "(int) exceptionIdDouble", "false"); 210 } 211 212 return n; 213 } 214 }); 215 FILES_TO_MODIFY.put("modules/dialog/DialogModule.java", new MethodVisitor() { 216 217 @Override 218 public Node visit(String methodName, MethodDeclaration n) { 219 switch (methodName) { 220 case "onHostResume": 221 return wrapInTryCatch(n); 222 } 223 224 return n; 225 } 226 }); 227 FILES_TO_MODIFY.put("modules/network/NetworkingModule.java", null); 228 FILES_TO_MODIFY.put("modules/systeminfo/AndroidInfoHelpers.java", null); 229 FILES_TO_MODIFY.put("uimanager/NativeViewHierarchyManager.java", new MethodVisitor() { 230 231 @Override 232 public Node visit(String methodName, MethodDeclaration n) { 233 switch (methodName) { 234 case "updateProperties": 235 return wrapInTryCatch(n); 236 } 237 238 return n; 239 } 240 }); 241 FILES_TO_MODIFY.put("bridge/DefaultJSExceptionHandler.java", new MethodVisitor() { 242 243 @Override 244 public Node visit(String methodName, MethodDeclaration n) { 245 switch (methodName) { 246 case "handleException": 247 // Catch any uncaught exceptions 248 return wrapInTryCatchAndHandleError(n); 249 } 250 251 return n; 252 } 253 }); 254 FILES_TO_MODIFY.put("devsupport/DevInternalSettings.java", new MethodVisitor() { 255 256 @Override 257 public Node visit(String methodName, MethodDeclaration n) { 258 switch (methodName) { 259 case "isReloadOnJSChangeEnabled": 260 BlockStmt blockStmt = JavaParser.parseBlock("{return false;}"); 261 blockStmt.addOrphanComment(new LineComment(" NOTE(brentvatne): This is not possible to enable/disable so we should always disable it for")); 262 blockStmt.addOrphanComment(new LineComment(" now. I managed to get into a state where fast refresh wouldn't work because live reload")); 263 blockStmt.addOrphanComment(new LineComment(" would kick in every time and there was no way to turn it off from the dev menu.")); 264 blockStmt.addOrphanComment(new LineComment(" return mPreferences.getBoolean(PREFS_RELOAD_ON_JS_CHANGE_KEY, false);")); 265 n.setBody(blockStmt); 266 return n; 267 case "setReloadOnJSChangeEnabled": 268 BlockStmt emptyBlockStmt = JavaParser.parseBlock("{}"); 269 emptyBlockStmt.addOrphanComment(new LineComment(" NOTE(brentvatne): We don't need to do anything here because this option is always false")); 270 emptyBlockStmt.addOrphanComment(new LineComment(" mPreferences.edit().putBoolean(PREFS_RELOAD_ON_JS_CHANGE_KEY, enabled).apply();")); 271 n.setBody(emptyBlockStmt); 272 return n; 273 } 274 275 return n; 276 } 277 278 @Override 279 String modifySource(String source) { 280 return addBeforeEndOfClass(source, "public int exponentActivityId = -1;"); 281 } 282 }); 283 } 284 main(final String[] args)285 public static void main(final String[] args) throws IOException { 286 String executionPath = ReactAndroidCodeTransformer.class.getProtectionDomain().getCodeSource().getLocation().getPath(); 287 String projectRoot = new File(executionPath + "../../../../../../").getCanonicalPath() + '/'; 288 289 String sdkVersion; 290 try { 291 sdkVersion = args[0]; 292 } catch (Exception e) { 293 throw new IllegalArgumentException("Invalid args passed in, expected one argument -- SDK version."); 294 } 295 296 // Update maven publish information 297 replaceInFile(new File(projectRoot + REACT_ANDROID_DEST_ROOT + "/build.gradle"), 298 "def AAR_OUTPUT_URL = \"file://${projectDir}/../android\"", 299 "def AAR_OUTPUT_URL = \"file:${System.env.HOME}/.m2/repository\""); 300 301 replaceInFile(new File(projectRoot + REACT_ANDROID_DEST_ROOT + "/build.gradle"), 302 "group = GROUP", 303 "group = 'com.facebook.react'"); 304 305 // This version also gets updated in android-tasks.js 306 replaceInFile(new File(projectRoot + REACT_ANDROID_DEST_ROOT + "/build.gradle"), 307 "version = VERSION_NAME", 308 "version = '" + sdkVersion + "'"); 309 310 // RN uses a weird directory structure for soloader to build with Buck. Change this so that Android Studio doesn't complain. 311 replaceInFile(new File(projectRoot + REACT_ANDROID_DEST_ROOT + "/build.gradle"), 312 "'src/main/libraries/soloader'", 313 "'src/main/libraries/soloader/java'"); 314 315 // Actually modify the files 316 String path = projectRoot + REACT_ANDROID_DEST_ROOT + '/' + SOURCE_PATH; 317 for (String fileName : FILES_TO_MODIFY.keySet()) { 318 try { 319 updateFile(path + fileName, FILES_TO_MODIFY.get(fileName)); 320 } catch (ParseException e) { 321 e.printStackTrace(); 322 } 323 } 324 } 325 replaceInFile(final File file, final String searchString, final String replaceString)326 private static void replaceInFile(final File file, final String searchString, final String replaceString) { 327 try { 328 String content = FileUtils.readFileToString(file, "UTF-8"); 329 content = content.replace(searchString, replaceString); 330 FileUtils.writeStringToFile(file, content, "UTF-8"); 331 } catch (IOException e) { 332 throw new RuntimeException("Generating file failed", e); 333 } 334 } 335 updateFile(final String path, final MethodVisitor methodVisitor)336 private static void updateFile(final String path, final MethodVisitor methodVisitor) throws IOException, ParseException { 337 FileInputStream in = new FileInputStream(path); 338 CompilationUnit cu = JavaParser.parse(in); 339 in.close(); 340 341 new ChangerVisitor(methodVisitor).visit(cu, null); 342 343 try (OutputStream out = new FileOutputStream(path)) { 344 if (methodVisitor != null) { 345 out.write(methodVisitor.modifySource(cu.toString()).getBytes()); 346 } else { 347 out.write(cu.toString().getBytes()); 348 } 349 } 350 } 351 352 private static class ChangerVisitor extends ModifierVisitor<Void> { 353 354 MethodVisitor mMethodVisitor; 355 ChangerVisitor(MethodVisitor methodVisitor)356 ChangerVisitor(MethodVisitor methodVisitor) { 357 mMethodVisitor = methodVisitor; 358 } 359 360 @Override visit(final ClassOrInterfaceDeclaration n, final Void arg)361 public Node visit(final ClassOrInterfaceDeclaration n, final Void arg) { 362 super.visit(n, arg); 363 364 // Remove all final modifiers 365 n.getModifiers().remove(Modifier.FINAL); 366 367 return n; 368 } 369 370 @Override visit(final ConstructorDeclaration n, final Void arg)371 public Node visit(final ConstructorDeclaration n, final Void arg) { 372 String name = n.getName().toString(); 373 switch (name) { 374 case "NetworkingModule": 375 return networkingModuleConstructor(n); 376 case "BridgeDevSupportManager": 377 return bridgeDevSupportManagerConstructor(n); 378 } 379 380 return n; 381 } 382 383 @Override visit(final FieldDeclaration n, final Void arg)384 public Node visit(final FieldDeclaration n, final Void arg) { 385 super.visit(n, arg); 386 387 // Remove all final modifiers from static fields 388 EnumSet<Modifier> modifiers = n.getModifiers(); 389 if (modifiers.contains(Modifier.STATIC) && !n.toString().contains("String NAME")) { 390 modifiers.remove(Modifier.FINAL); 391 } 392 393 modifiers.remove(Modifier.PRIVATE); 394 modifiers.remove(Modifier.PROTECTED); 395 modifiers.add(Modifier.PUBLIC); 396 397 n.setModifiers(modifiers); 398 return n; 399 } 400 401 @Override visit(final MethodDeclaration n, final Void arg)402 public Node visit(final MethodDeclaration n, final Void arg) { 403 super.visit(n, arg); 404 405 String methodName = n.getName().toString(); 406 if (mMethodVisitor != null) { 407 return mMethodVisitor.visit(methodName, n); 408 } 409 410 return n; 411 } 412 } 413 414 private interface StatementMapper { map(Statement statement)415 Statement map(Statement statement); 416 } 417 mapNode(final Node node, final StatementMapper mapper)418 private static Node mapNode(final Node node, final StatementMapper mapper) { 419 if (node instanceof BlockStmt) { 420 return mapBlockStatement((BlockStmt) node, mapper); 421 } else if (node.getChildNodes().size() > 0) { 422 List<Node> childNodes = new ArrayList<>(node.getChildNodes()); 423 for (Node child : childNodes) { 424 child.setParentNode(null); 425 mapNode(child, mapper).setParentNode(node); 426 } 427 428 if (node instanceof Statement) { 429 return mapper.map((Statement) node); 430 } else { 431 return node; 432 } 433 } else if (node instanceof Statement) { 434 return mapper.map((Statement) node); 435 } else { 436 return node; 437 } 438 } 439 mapBlockStatement(final BlockStmt body, final StatementMapper mapper)440 private static BlockStmt mapBlockStatement(final BlockStmt body, final StatementMapper mapper) { 441 NodeList<Statement> newStatements = new NodeList<>(); 442 for (Statement statement : body.getStatements()) { 443 newStatements.add((Statement) mapNode(statement, mapper)); 444 } 445 body.setStatements(newStatements); 446 return body; 447 } 448 mapBlockStatement(final MethodDeclaration n, final StatementMapper mapper)449 private static Node mapBlockStatement(final MethodDeclaration n, final StatementMapper mapper) { 450 n.getBody().ifPresent(body -> { 451 body = mapBlockStatement(body, mapper); 452 n.setBody(body); 453 }); 454 455 return n; 456 } 457 mapBlockStatement(final ConstructorDeclaration n, final StatementMapper mapper)458 private static Node mapBlockStatement(final ConstructorDeclaration n, final StatementMapper mapper) { 459 BlockStmt body = n.getBody(); 460 body = mapBlockStatement(body, mapper); 461 n.setBody(body); 462 463 return n; 464 } 465 handleReloadJS(final MethodDeclaration n)466 private static Node handleReloadJS(final MethodDeclaration n) { 467 return mapBlockStatement(n, new StatementMapper() { 468 @Override 469 public Statement map(Statement statement) { 470 if (!statement.toString().contains("progressDialog.show();")) { 471 return statement; 472 } 473 474 return getTryCatch(statement, "\"Must allow Expo to draw over other apps in dev mode.\"", "null", "-1", "true"); 475 } 476 }); 477 } 478 479 private static Node handleException(final MethodDeclaration n) { 480 return mapBlockStatement(n, new StatementMapper() { 481 @Override 482 public Statement map(Statement statement) { 483 if (!statement.toString().startsWith("if (mIsDevSupportEnabled) {")) { 484 return statement; 485 } 486 487 return getTryCatch(statement, "expoException.getMessage()", "null", "-1", "true"); 488 } 489 }); 490 } 491 492 private static Node exceptionsManagerModuleHandleException(final MethodDeclaration n, final String errorMessageName, final String errorDetailsName, final String errorIdName, final String isFatal) { 493 String source = 494 "{\n" + 495 "if (mDevSupportManager.getDevSupportEnabled()) {\n" + 496 n.getBody().get().toString() + "\n" + 497 "} else {\n" + 498 getHandleErrorBlockString(errorMessageName, errorDetailsName, errorIdName, isFatal) + "\n" + 499 "}\n" + 500 "}\n"; 501 502 BlockStmt blockStmt = JavaParser.parseBlock(source); 503 n.setBody(blockStmt); 504 return n; 505 } 506 507 private static Node hasUpToDateJSBundleInCache(final MethodDeclaration n) { 508 BlockStmt blockStmt = JavaParser.parseBlock("{\nreturn false;\n}"); 509 n.setBody(blockStmt); 510 return n; 511 } 512 513 private static Node showDevOptionsDialog(final MethodDeclaration n) { 514 return mapBlockStatement(n, new StatementMapper() { 515 @Override 516 public Statement map(Statement statement) { 517 if (statement instanceof LabeledStmt) { 518 LabeledStmt labeledStmt = (LabeledStmt) statement; 519 if ("expo_transformer_remove".equals(labeledStmt.getLabel().getIdentifier())) { 520 Statement emptyStatement = new EmptyStmt(); 521 emptyStatement.setLineComment(" code removed by ReactAndroidCodeTransformer"); 522 return emptyStatement; 523 } 524 } 525 526 return statement; 527 } 528 }); 529 } 530 531 // Remove stetho. Otherwise a stetho interceptor gets added each time a new NetworkingModule 532 // is created. 533 private static Node networkingModuleConstructor(final ConstructorDeclaration n) { 534 return mapBlockStatement(n, new StatementMapper() { 535 @Override 536 public Statement map(Statement statement) { 537 if (!statement.toString().equals("mClient.networkInterceptors().add(new StethoInterceptor());")) { 538 return statement; 539 } 540 541 return new EmptyStmt(); 542 } 543 }); 544 } 545 546 // Remove some custom dev options. unlike `showDevOptionsDialog`, this happens in constructor. 547 private static Node bridgeDevSupportManagerConstructor(final ConstructorDeclaration n) { 548 return mapBlockStatement(n, new StatementMapper() { 549 @Override 550 public Statement map(Statement statement) { 551 if (statement instanceof LabeledStmt) { 552 LabeledStmt labeledStmt = (LabeledStmt) statement; 553 if ("expo_transformer_remove".equals(labeledStmt.getLabel().getIdentifier())) { 554 Statement emptyStatement = new EmptyStmt(); 555 emptyStatement.setLineComment(" code removed by ReactAndroidCodeTransformer"); 556 return emptyStatement; 557 } 558 } 559 560 return statement; 561 } 562 }); 563 } 564 565 private static Node wrapInTryCatch(final MethodDeclaration n) { 566 n.getBody().ifPresent(body -> { 567 Statement tryCatch = getTryCatch(body); 568 NodeList<Statement> statements = NodeList.nodeList(tryCatch); 569 body = new BlockStmt(statements); 570 n.setBody(body); 571 }); 572 573 return n; 574 } 575 576 private static Node wrapInTryCatchAndHandleError(final MethodDeclaration n) { 577 n.getBody().ifPresent(body -> { 578 Statement tryCatch = getTryCatch(body, "expoException.getMessage()", "null", "-1", "true"); 579 NodeList<Statement> statements = NodeList.nodeList(tryCatch); 580 body = new BlockStmt(statements); 581 n.setBody(body); 582 }); 583 584 return n; 585 } 586 } 587