1# Copyright 2018 Google LLC 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15import glob 16import os 17import xml.etree.ElementTree as ET 18import re 19import requests 20import yaml 21import synthtool as s 22import synthtool.gcp as gcp 23from synthtool import cache, shell 24from synthtool.gcp import common, partials, pregenerated, samples, snippets 25from synthtool.log import logger 26from pathlib import Path 27from typing import Any, Optional, Dict, Iterable, List 28 29JAR_DOWNLOAD_URL = "https://github.com/google/google-java-format/releases/download/google-java-format-{version}/google-java-format-{version}-all-deps.jar" 30DEFAULT_FORMAT_VERSION = "1.7" 31GOOD_LICENSE = """/* 32 * Copyright 2020 Google LLC 33 * 34 * Licensed under the Apache License, Version 2.0 (the "License"); 35 * you may not use this file except in compliance with the License. 36 * You may obtain a copy of the License at 37 * 38 * https://www.apache.org/licenses/LICENSE-2.0 39 * 40 * Unless required by applicable law or agreed to in writing, software 41 * distributed under the License is distributed on an "AS IS" BASIS, 42 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 43 * See the License for the specific language governing permissions and 44 * limitations under the License. 45 */ 46""" 47PROTOBUF_HEADER = "// Generated by the protocol buffer compiler. DO NOT EDIT!" 48BAD_LICENSE = """/\\* 49 \\* Copyright \\d{4} Google LLC 50 \\* 51 \\* Licensed under the Apache License, Version 2.0 \\(the "License"\\); you may not use this file except 52 \\* in compliance with the License. You may obtain a copy of the License at 53 \\* 54 \\* http://www.apache.org/licenses/LICENSE-2.0 55 \\* 56 \\* Unless required by applicable law or agreed to in writing, software distributed under the License 57 \\* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 58 \\* or implied. See the License for the specific language governing permissions and limitations under 59 \\* the License. 60 \\*/ 61""" 62DEFAULT_MIN_SUPPORTED_JAVA_VERSION = 8 63 64 65def format_code( 66 path: str, version: str = DEFAULT_FORMAT_VERSION, times: int = 2 67) -> None: 68 """ 69 Runs the google-java-format jar against all .java files found within the 70 provided path. 71 """ 72 jar_name = f"google-java-format-{version}.jar" 73 jar = cache.get_cache_dir() / jar_name 74 if not jar.exists(): 75 _download_formatter(version, jar) 76 77 # Find all .java files in path and run the formatter on them 78 files = list(glob.iglob(os.path.join(path, "**/*.java"), recursive=True)) 79 80 # Run the formatter as a jar file 81 logger.info("Running java formatter on {} files".format(len(files))) 82 for _ in range(times): 83 shell.run(["java", "-jar", str(jar), "--replace"] + files) 84 85 86def _download_formatter(version: str, dest: Path) -> None: 87 logger.info("Downloading java formatter") 88 url = JAR_DOWNLOAD_URL.format(version=version) 89 response = requests.get(url) 90 response.raise_for_status() 91 with open(dest, "wb") as fh: 92 fh.write(response.content) 93 94 95HEADER_REGEX = re.compile("\\* Copyright \\d{4} Google LLC") 96 97 98def _file_has_header(path: Path) -> bool: 99 """Return true if the file already contains a license header.""" 100 with open(path, "rt") as fp: 101 for line in fp: 102 if HEADER_REGEX.search(line): 103 return True 104 return False 105 106 107def _filter_no_header(paths: Iterable[Path]) -> Iterable[Path]: 108 """Return a subset of files that do not already have a header.""" 109 for path in paths: 110 anchor = Path(path.anchor) 111 remainder = str(path.relative_to(path.anchor)) 112 for file in anchor.glob(remainder): 113 if not _file_has_header(file): 114 yield file 115 116 117def fix_proto_headers(proto_root: Path) -> None: 118 """Helper to ensure that generated proto classes have appropriate license headers. 119 120 If the file does not already contain a license header, inject one at the top of the file. 121 Some resource name classes may contain malformed license headers. In those cases, replace 122 those with our standard license header. 123 """ 124 s.replace( 125 _filter_no_header([proto_root / "src/**/*.java"]), 126 PROTOBUF_HEADER, 127 f"{GOOD_LICENSE}{PROTOBUF_HEADER}", 128 ) 129 # https://github.com/googleapis/gapic-generator/issues/3074 130 s.replace( 131 [proto_root / "src/**/*Name.java", proto_root / "src/**/*Names.java"], 132 BAD_LICENSE, 133 GOOD_LICENSE, 134 ) 135 136 137def fix_grpc_headers(grpc_root: Path, package_name: str = "unused") -> None: 138 """Helper to ensure that generated grpc stub classes have appropriate license headers. 139 140 If the file does not already contain a license header, inject one at the top of the file. 141 """ 142 s.replace( 143 _filter_no_header([grpc_root / "src/**/*.java"]), 144 "^package (.*);", 145 f"{GOOD_LICENSE}package \\1;", 146 ) 147 148 149def latest_maven_version(group_id: str, artifact_id: str) -> Optional[str]: 150 """Helper function to find the latest released version of a Maven artifact. 151 152 Fetches metadata from Maven Central and parses out the latest released 153 version. 154 155 Args: 156 group_id (str): The groupId of the Maven artifact 157 artifact_id (str): The artifactId of the Maven artifact 158 159 Returns: 160 The latest version of the artifact as a string or None 161 """ 162 group_path = "/".join(group_id.split(".")) 163 url = ( 164 f"https://repo1.maven.org/maven2/{group_path}/{artifact_id}/maven-metadata.xml" 165 ) 166 response = requests.get(url) 167 if response.status_code >= 400: 168 return "0.0.0" 169 170 return version_from_maven_metadata(response.text) 171 172 173def version_from_maven_metadata(metadata: str) -> Optional[str]: 174 """Helper function to parse the latest released version from the Maven 175 metadata XML file. 176 177 Args: 178 metadata (str): The XML contents of the Maven metadata file 179 180 Returns: 181 The latest version of the artifact as a string or None 182 """ 183 root = ET.fromstring(metadata) 184 latest = root.find("./versioning/latest") 185 if latest is not None: 186 return latest.text 187 188 return None 189 190 191def _common_generation( 192 service: str, 193 version: str, 194 library: Path, 195 package_pattern: str, 196 suffix: str = "", 197 destination_name: str = None, 198 cloud_api: bool = True, 199 diregapic: bool = False, 200 preserve_gapic: bool = False, 201): 202 """Helper function to execution the common generation cleanup actions. 203 204 Fixes headers for protobuf classes and generated gRPC stub services. Copies 205 code and samples to their final destinations by convention. Runs the code 206 formatter on the generated code. 207 208 Args: 209 service (str): Name of the service. 210 version (str): Service API version. 211 library (Path): Path to the temp directory with the generated library. 212 package_pattern (str): Package name template for fixing file headers. 213 suffix (str, optional): Suffix that the generated library folder. The 214 artman output differs from bazel's output directory. Defaults to "". 215 destination_name (str, optional): Override the service name for the 216 destination of the output code. Defaults to the service name. 217 preserve_gapic (bool, optional): Whether to preserve the gapic directory 218 prefix. Default False. 219 """ 220 221 if destination_name is None: 222 destination_name = service 223 224 cloud_prefix = "cloud-" if cloud_api else "" 225 package_name = package_pattern.format(service=service, version=version) 226 fix_proto_headers( 227 library / f"proto-google-{cloud_prefix}{service}-{version}{suffix}" 228 ) 229 fix_grpc_headers( 230 library / f"grpc-google-{cloud_prefix}{service}-{version}{suffix}", package_name 231 ) 232 233 if preserve_gapic: 234 s.copy( 235 [library / f"gapic-google-{cloud_prefix}{service}-{version}{suffix}/src"], 236 f"gapic-google-{cloud_prefix}{destination_name}-{version}/src", 237 required=True, 238 ) 239 else: 240 s.copy( 241 [library / f"gapic-google-{cloud_prefix}{service}-{version}{suffix}/src"], 242 f"google-{cloud_prefix}{destination_name}/src", 243 required=True, 244 ) 245 246 s.copy( 247 [library / f"grpc-google-{cloud_prefix}{service}-{version}{suffix}/src"], 248 f"grpc-google-{cloud_prefix}{destination_name}-{version}/src", 249 # For REST-only clients, like java-compute, gRPC artifact does not exist 250 required=(not diregapic), 251 ) 252 s.copy( 253 [library / f"proto-google-{cloud_prefix}{service}-{version}{suffix}/src"], 254 f"proto-google-{cloud_prefix}{destination_name}-{version}/src", 255 required=True, 256 ) 257 258 if preserve_gapic: 259 format_code(f"gapic-google-{cloud_prefix}{destination_name}-{version}/src") 260 else: 261 format_code(f"google-{cloud_prefix}{destination_name}/src") 262 format_code(f"grpc-google-{cloud_prefix}{destination_name}-{version}/src") 263 format_code(f"proto-google-{cloud_prefix}{destination_name}-{version}/src") 264 265 266def gapic_library( 267 service: str, 268 version: str, 269 config_pattern: str = "/google/cloud/{service}/artman_{service}_{version}.yaml", 270 package_pattern: str = "com.google.cloud.{service}.{version}", 271 gapic: gcp.GAPICGenerator = None, 272 destination_name: str = None, 273 diregapic: bool = False, 274 preserve_gapic: bool = False, 275 **kwargs, 276) -> Path: 277 """Generate a Java library using the gapic-generator via artman via Docker. 278 279 Generates code into a temp directory, fixes missing header fields, and 280 copies into the expected locations. 281 282 Args: 283 service (str): Name of the service. 284 version (str): Service API version. 285 config_pattern (str, optional): Path template to artman config YAML 286 file. Defaults to "/google/cloud/{service}/artman_{service}_{version}.yaml" 287 package_pattern (str, optional): Package name template for fixing file 288 headers. Defaults to "com.google.cloud.{service}.{version}". 289 gapic (GAPICGenerator, optional): Generator instance. 290 destination_name (str, optional): Override the service name for the 291 destination of the output code. Defaults to the service name. 292 preserve_gapic (bool, optional): Whether to preserve the gapic directory 293 prefix. Default False. 294 **kwargs: Additional options for gapic.java_library() 295 296 Returns: 297 The path to the temp directory containing the generated client. 298 """ 299 if gapic is None: 300 gapic = gcp.GAPICGenerator() 301 302 library = gapic.java_library( 303 service=service, 304 version=version, 305 config_path=config_pattern.format(service=service, version=version), 306 artman_output_name="", 307 include_samples=True, 308 diregapic=diregapic, 309 **kwargs, 310 ) 311 312 _common_generation( 313 service=service, 314 version=version, 315 library=library, 316 package_pattern=package_pattern, 317 destination_name=destination_name, 318 diregapic=diregapic, 319 preserve_gapic=preserve_gapic, 320 ) 321 322 return library 323 324 325def bazel_library( 326 service: str, 327 version: str, 328 package_pattern: str = "com.google.cloud.{service}.{version}", 329 gapic: gcp.GAPICBazel = None, 330 destination_name: str = None, 331 cloud_api: bool = True, 332 diregapic: bool = False, 333 preserve_gapic: bool = False, 334 **kwargs, 335) -> Path: 336 """Generate a Java library using the gapic-generator via bazel. 337 338 Generates code into a temp directory, fixes missing header fields, and 339 copies into the expected locations. 340 341 Args: 342 service (str): Name of the service. 343 version (str): Service API version. 344 package_pattern (str, optional): Package name template for fixing file 345 headers. Defaults to "com.google.cloud.{service}.{version}". 346 gapic (GAPICBazel, optional): Generator instance. 347 destination_name (str, optional): Override the service name for the 348 destination of the output code. Defaults to the service name. 349 preserve_gapic (bool, optional): Whether to preserve the gapic directory 350 prefix. Default False. 351 **kwargs: Additional options for gapic.java_library() 352 353 Returns: 354 The path to the temp directory containing the generated client. 355 """ 356 if gapic is None: 357 gapic = gcp.GAPICBazel() 358 359 library = gapic.java_library( 360 service=service, version=version, diregapic=diregapic, **kwargs 361 ) 362 363 _common_generation( 364 service=service, 365 version=version, 366 library=library / f"google-cloud-{service}-{version}-java", 367 package_pattern=package_pattern, 368 suffix="-java", 369 destination_name=destination_name, 370 cloud_api=cloud_api, 371 diregapic=diregapic, 372 preserve_gapic=preserve_gapic, 373 ) 374 375 return library 376 377 378def pregenerated_library( 379 path: str, 380 service: str, 381 version: str, 382 destination_name: str = None, 383 cloud_api: bool = True, 384) -> Path: 385 """Generate a Java library using the gapic-generator via bazel. 386 387 Generates code into a temp directory, fixes missing header fields, and 388 copies into the expected locations. 389 390 Args: 391 path (str): Path in googleapis-gen to un-versioned generated code. 392 service (str): Name of the service. 393 version (str): Service API version. 394 destination_name (str, optional): Override the service name for the 395 destination of the output code. Defaults to the service name. 396 cloud_api (bool, optional): Whether or not this is a cloud API (for naming) 397 398 Returns: 399 The path to the temp directory containing the generated client. 400 """ 401 generator = pregenerated.Pregenerated() 402 library = generator.generate(path) 403 404 cloud_prefix = "cloud-" if cloud_api else "" 405 _common_generation( 406 service=service, 407 version=version, 408 library=library / f"google-{cloud_prefix}{service}-{version}-java", 409 package_pattern="unused", 410 suffix="-java", 411 destination_name=destination_name, 412 cloud_api=cloud_api, 413 ) 414 415 return library 416 417 418def _merge_release_please(destination_text: str): 419 config = yaml.safe_load(destination_text) 420 if "handleGHRelease" in config: 421 return destination_text 422 423 config["handleGHRelease"] = True 424 425 if "branches" in config: 426 for branch in config["branches"]: 427 branch["handleGHRelease"] = True 428 return yaml.dump(config) 429 430 431def _merge_common_templates( 432 source_text: str, destination_text: str, file_path: Path 433) -> str: 434 # keep any existing pom.xml 435 if file_path.match("pom.xml") or file_path.match("sync-repo-settings.yaml"): 436 logger.debug(f"existing pom file found ({file_path}) - keeping the existing") 437 return destination_text 438 439 if file_path.match("release-please.yml"): 440 return _merge_release_please(destination_text) 441 442 # by default return the newly generated content 443 return source_text 444 445 446def _common_template_metadata() -> Dict[str, Any]: 447 metadata = {} # type: Dict[str, Any] 448 repo_metadata = common._load_repo_metadata() 449 if repo_metadata: 450 metadata["repo"] = repo_metadata 451 group_id, artifact_id = repo_metadata["distribution_name"].split(":") 452 453 metadata["latest_version"] = latest_maven_version( 454 group_id=group_id, artifact_id=artifact_id 455 ) 456 457 metadata["latest_bom_version"] = latest_maven_version( 458 group_id="com.google.cloud", 459 artifact_id="libraries-bom", 460 ) 461 462 metadata["samples"] = samples.all_samples(["samples/**/src/main/java/**/*.java"]) 463 metadata["snippets"] = snippets.all_snippets( 464 ["samples/**/src/main/java/**/*.java", "samples/**/pom.xml"] 465 ) 466 if repo_metadata and "min_java_version" in repo_metadata: 467 metadata["min_java_version"] = repo_metadata["min_java_version"] 468 else: 469 metadata["min_java_version"] = DEFAULT_MIN_SUPPORTED_JAVA_VERSION 470 471 return metadata 472 473 474def common_templates( 475 excludes: List[str] = [], template_path: Optional[Path] = None, **kwargs 476) -> None: 477 """Generate common templates for a Java Library 478 479 Fetches information about the repository from the .repo-metadata.json file, 480 information about the latest artifact versions and copies the files into 481 their expected location. 482 483 Args: 484 excludes (List[str], optional): List of template paths to ignore 485 **kwargs: Additional options for CommonTemplates.java_library() 486 """ 487 metadata = _common_template_metadata() 488 kwargs["metadata"] = metadata 489 490 # Generate flat to tell this repository is a split repo that have migrated 491 # to monorepo. The owlbot.py in the monorepo sets monorepo=True. 492 monorepo = kwargs.get("monorepo", False) 493 split_repo = not monorepo 494 repo_metadata = metadata["repo"] 495 repo_short = repo_metadata["repo_short"] 496 # Special libraries that are not GAPIC_AUTO but in the monorepo 497 special_libs_in_monorepo = [ 498 "java-translate", 499 "java-dns", 500 "java-notification", 501 "java-resourcemanager", 502 ] 503 kwargs["migrated_split_repo"] = split_repo and ( 504 repo_metadata["library_type"] == "GAPIC_AUTO" 505 or (repo_short and repo_short in special_libs_in_monorepo) 506 ) 507 logger.info( 508 "monorepo: {}, split_repo: {}, library_type: {}," 509 " repo_short: {}, migrated_split_repo: {}".format( 510 monorepo, 511 split_repo, 512 repo_metadata["library_type"], 513 repo_short, 514 kwargs["migrated_split_repo"], 515 ) 516 ) 517 518 templates = gcp.CommonTemplates(template_path=template_path).java_library(**kwargs) 519 520 # skip README generation on Kokoro (autosynth) 521 if os.environ.get("KOKORO_ROOT") is not None: 522 # README.md is now synthesized separately. This prevents synthtool from deleting the 523 # README as it's no longer generated here. 524 excludes.append("README.md") 525 526 s.copy([templates], excludes=excludes, merge=_merge_common_templates) 527 528 529def custom_templates(files: List[str], **kwargs) -> None: 530 """Generate custom template files 531 532 Fetches information about the repository from the .repo-metadata.json file, 533 information about the latest artifact versions and copies the files into 534 their expected location. 535 536 Args: 537 files (List[str], optional): List of template paths to include 538 **kwargs: Additional options for CommonTemplates.render() 539 """ 540 kwargs["metadata"] = _common_template_metadata() 541 kwargs["metadata"]["partials"] = partials.load_partials() 542 for file in files: 543 template = gcp.CommonTemplates().render(file, **kwargs) 544 s.copy([template]) 545 546 547def remove_method(filename: str, signature: str): 548 """Helper to remove an entire method. 549 550 Goes line-by-line to detect the start of the block. Determines 551 the end of the block by a closing brace at the same indentation 552 level. This requires the file to be correctly formatted. 553 554 Example: consider the following class: 555 556 class Example { 557 public void main(String[] args) { 558 System.out.println("Hello World"); 559 } 560 561 public String foo() { 562 return "bar"; 563 } 564 } 565 566 To remove the `main` method above, use: 567 568 remove_method('path/to/file', 'public void main(String[] args)') 569 570 Args: 571 filename (str): Path to source file 572 signature (str): Full signature of the method to remove. Example: 573 `public void main(String[] args)`. 574 """ 575 lines = [] 576 leading_regex = None 577 with open(filename, "r") as fp: 578 line = fp.readline() 579 while line: 580 # for each line, try to find the matching 581 regex = re.compile("(\\s*)" + re.escape(signature) + ".*") 582 match = regex.match(line) 583 if match: 584 leading_regex = re.compile(match.group(1) + "}") 585 line = fp.readline() 586 continue 587 588 # not in a ignore block - preserve the line 589 if not leading_regex: 590 lines.append(line) 591 line = fp.readline() 592 continue 593 594 # detect the closing tag based on the leading spaces 595 match = leading_regex.match(line) 596 if match: 597 # block is closed, resume capturing content 598 leading_regex = None 599 600 line = fp.readline() 601 602 with open(filename, "w") as fp: 603 for line in lines: 604 # print(line) 605 fp.write(line) 606 607 608def copy_and_rename_method(filename: str, signature: str, before: str, after: str): 609 """Helper to make a copy an entire method and rename it. 610 611 Goes line-by-line to detect the start of the block. Determines 612 the end of the block by a closing brace at the same indentation 613 level. This requires the file to be correctly formatted. 614 The method is copied over and renamed in the method signature. 615 The calls to both methods are separate and unaffected. 616 617 Example: consider the following class: 618 619 class Example { 620 public void main(String[] args) { 621 System.out.println("Hello World"); 622 } 623 624 public String foo() { 625 return "bar"; 626 } 627 } 628 629 To copy and rename the `main` method above, use: 630 631 copy_and_rename_method('path/to/file', 'public void main(String[] args)', 632 'main', 'foo1') 633 634 Args: 635 filename (str): Path to source file 636 signature (str): Full signature of the method to remove. Example: 637 `public void main(String[] args)`. 638 before (str): name of the method to be copied 639 after (str): new name of the copied method 640 """ 641 lines = [] 642 method = [] 643 leading_regex = None 644 with open(filename, "r") as fp: 645 line = fp.readline() 646 while line: 647 # for each line, try to find the matching 648 regex = re.compile("(\\s*)" + re.escape(signature) + ".*") 649 match = regex.match(line) 650 if match: 651 leading_regex = re.compile(match.group(1) + "}") 652 lines.append(line) 653 method.append(line.replace(before, after)) 654 line = fp.readline() 655 continue 656 657 lines.append(line) 658 # not in a ignore block - preserve the line 659 if leading_regex: 660 method.append(line) 661 else: 662 line = fp.readline() 663 continue 664 665 # detect the closing tag based on the leading spaces 666 match = leading_regex.match(line) 667 if match: 668 # block is closed, resume capturing content 669 leading_regex = None 670 lines.append("\n") 671 lines.extend(method) 672 673 line = fp.readline() 674 675 with open(filename, "w") as fp: 676 for line in lines: 677 # print(line) 678 fp.write(line) 679 680 681def add_javadoc(filename: str, signature: str, javadoc_type: str, content: List[str]): 682 """Helper to add a javadoc annoatation to a method. 683 684 Goes line-by-line to detect the start of the block. 685 Then finds the existing method comment (if it exists). If the 686 comment already exists, it will append the javadoc annotation 687 to the javadoc block. Otherwise, it will create a new javadoc 688 comment block. 689 690 Example: consider the following class: 691 692 class Example { 693 public void main(String[] args) { 694 System.out.println("Hello World"); 695 } 696 697 public String foo() { 698 return "bar"; 699 } 700 } 701 702 To add a javadoc annotation the `main` method above, use: 703 704 add_javadoc('path/to/file', 'public void main(String[] args)', 705 'deprecated', 'Please use foo instead.') 706 707 Args: 708 filename (str): Path to source file 709 signature (str): Full signature of the method to remove. Example: 710 `public void main(String[] args)`. 711 javadoc_type (str): The type of javadoc annotation. Example: `deprecated`. 712 content (List[str]): The javadoc lines 713 """ 714 lines: List[str] = [] 715 annotations: List[str] = [] 716 with open(filename, "r") as fp: 717 line = fp.readline() 718 while line: 719 # for each line, try to find the matching 720 regex = re.compile("(\\s*)" + re.escape(signature) + ".*") 721 match = regex.match(line) 722 if match: 723 leading_spaces = len(line) - len(line.lstrip()) 724 indent = leading_spaces * " " 725 last_line = lines.pop() 726 while last_line.lstrip() and last_line.lstrip()[0] == "@": 727 annotations.append(last_line) 728 last_line = lines.pop() 729 if last_line.strip() == "*/": 730 first = True 731 for content_line in content: 732 if first: 733 lines.append( 734 indent 735 + " * @" 736 + javadoc_type 737 + " " 738 + content_line 739 + "\n" 740 ) 741 first = False 742 else: 743 lines.append(indent + " * " + content_line + "\n") 744 lines.append(last_line) 745 else: 746 lines.append(last_line) 747 lines.append(indent + "/**\n") 748 first = True 749 for content_line in content: 750 if first: 751 lines.append( 752 indent 753 + " * @" 754 + javadoc_type 755 + " " 756 + content_line 757 + "\n" 758 ) 759 first = False 760 else: 761 lines.append(indent + " * " + content_line + "\n") 762 lines.append(indent + " */\n") 763 lines.extend(annotations[::-1]) 764 lines.append(line) 765 line = fp.readline() 766 767 with open(filename, "w") as fp: 768 for line in lines: 769 # print(line) 770 fp.write(line) 771 772 773def annotate_method(filename: str, signature: str, annotation: str): 774 """Helper to add an annotation to a method. 775 776 Goes line-by-line to detect the start of the block. 777 Then adds the annotation above the found method signature. 778 779 Example: consider the following class: 780 781 class Example { 782 public void main(String[] args) { 783 System.out.println("Hello World"); 784 } 785 786 public String foo() { 787 return "bar"; 788 } 789 } 790 791 To add an annotation the `main` method above, use: 792 793 annotate_method('path/to/file', 'public void main(String[] args)', 794 '@Generated()') 795 796 Args: 797 filename (str): Path to source file 798 signature (str): Full signature of the method to remove. Example: 799 `public void main(String[] args)`. 800 annotation (str): Full annotation. Example: `@Deprecated` 801 """ 802 lines: List[str] = [] 803 with open(filename, "r") as fp: 804 line = fp.readline() 805 while line: 806 # for each line, try to find the matching 807 regex = re.compile("(\\s*)" + re.escape(signature) + ".*") 808 match = regex.match(line) 809 if match: 810 leading_spaces = len(line) - len(line.lstrip()) 811 indent = leading_spaces * " " 812 lines.append(indent + annotation + "\n") 813 lines.append(line) 814 line = fp.readline() 815 816 with open(filename, "w") as fp: 817 for line in lines: 818 # print(line) 819 fp.write(line) 820 821 822def deprecate_method(filename: str, signature: str, alternative: str): 823 """Helper to deprecate a method. 824 825 Goes line-by-line to detect the start of the block. 826 Then adds the deprecation comment before the method signature. 827 The @Deprecation annotation is also added. 828 829 Example: consider the following class: 830 831 class Example { 832 public void main(String[] args) { 833 System.out.println("Hello World"); 834 } 835 836 public String foo() { 837 return "bar"; 838 } 839 } 840 841 To deprecate the `main` method above, use: 842 843 deprecate_method('path/to/file', 'public void main(String[] args)', 844 DEPRECATION_WARNING.format(new_method="foo")) 845 846 Args: 847 filename (str): Path to source file 848 signature (str): Full signature of the method to remove. Example: 849 `public void main(String[] args)`. 850 alternative: DEPRECATION WARNING: multiline javadoc comment with user 851 specified leading open/close comment tags 852 """ 853 add_javadoc(filename, signature, "deprecated", alternative.splitlines()) 854 annotate_method(filename, signature, "@Deprecated") 855