| # Copyright 2018 Google LLC | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # https://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| import glob | |
| import os | |
| import xml.etree.ElementTree as ET | |
| import re | |
| import requests | |
| import yaml | |
| import synthtool as s | |
| import synthtool.gcp as gcp | |
| from synthtool import cache, shell | |
| from synthtool.gcp import common, partials, pregenerated, samples, snippets | |
| from synthtool.log import logger | |
| from pathlib import Path | |
| from typing import Any, Optional, Dict, Iterable, List | |
| JAR_DOWNLOAD_URL = "https://github.com/google/google-java-format/releases/download/google-java-format-{version}/google-java-format-{version}-all-deps.jar" | |
| DEFAULT_FORMAT_VERSION = "1.7" | |
| GOOD_LICENSE = """/* | |
| * Copyright 2020 Google LLC | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * https://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| """ | |
| PROTOBUF_HEADER = "// Generated by the protocol buffer compiler. DO NOT EDIT!" | |
| BAD_LICENSE = """/\\* | |
| \\* Copyright \\d{4} Google LLC | |
| \\* | |
| \\* Licensed under the Apache License, Version 2.0 \\(the "License"\\); you may not use this file except | |
| \\* in compliance with the License. You may obtain a copy of the License at | |
| \\* | |
| \\* http://www.apache.org/licenses/LICENSE-2.0 | |
| \\* | |
| \\* Unless required by applicable law or agreed to in writing, software distributed under the License | |
| \\* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express | |
| \\* or implied. See the License for the specific language governing permissions and limitations under | |
| \\* the License. | |
| \\*/ | |
| """ | |
| DEFAULT_MIN_SUPPORTED_JAVA_VERSION = 8 | |
| def format_code( | |
| path: str, version: str = DEFAULT_FORMAT_VERSION, times: int = 2 | |
| ) -> None: | |
| """ | |
| Runs the google-java-format jar against all .java files found within the | |
| provided path. | |
| """ | |
| jar_name = f"google-java-format-{version}.jar" | |
| jar = cache.get_cache_dir() / jar_name | |
| if not jar.exists(): | |
| _download_formatter(version, jar) | |
| # Find all .java files in path and run the formatter on them | |
| files = list(glob.iglob(os.path.join(path, "**/*.java"), recursive=True)) | |
| # Run the formatter as a jar file | |
| logger.info("Running java formatter on {} files".format(len(files))) | |
| for _ in range(times): | |
| shell.run(["java", "-jar", str(jar), "--replace"] + files) | |
| def _download_formatter(version: str, dest: Path) -> None: | |
| logger.info("Downloading java formatter") | |
| url = JAR_DOWNLOAD_URL.format(version=version) | |
| response = requests.get(url) | |
| response.raise_for_status() | |
| with open(dest, "wb") as fh: | |
| fh.write(response.content) | |
| HEADER_REGEX = re.compile("\\* Copyright \\d{4} Google LLC") | |
| def _file_has_header(path: Path) -> bool: | |
| """Return true if the file already contains a license header.""" | |
| with open(path, "rt") as fp: | |
| for line in fp: | |
| if HEADER_REGEX.search(line): | |
| return True | |
| return False | |
| def _filter_no_header(paths: Iterable[Path]) -> Iterable[Path]: | |
| """Return a subset of files that do not already have a header.""" | |
| for path in paths: | |
| anchor = Path(path.anchor) | |
| remainder = str(path.relative_to(path.anchor)) | |
| for file in anchor.glob(remainder): | |
| if not _file_has_header(file): | |
| yield file | |
| def fix_proto_headers(proto_root: Path) -> None: | |
| """Helper to ensure that generated proto classes have appropriate license headers. | |
| If the file does not already contain a license header, inject one at the top of the file. | |
| Some resource name classes may contain malformed license headers. In those cases, replace | |
| those with our standard license header. | |
| """ | |
| s.replace( | |
| _filter_no_header([proto_root / "src/**/*.java"]), | |
| PROTOBUF_HEADER, | |
| f"{GOOD_LICENSE}{PROTOBUF_HEADER}", | |
| ) | |
| # https://github.com/googleapis/gapic-generator/issues/3074 | |
| s.replace( | |
| [proto_root / "src/**/*Name.java", proto_root / "src/**/*Names.java"], | |
| BAD_LICENSE, | |
| GOOD_LICENSE, | |
| ) | |
| def fix_grpc_headers(grpc_root: Path, package_name: str = "unused") -> None: | |
| """Helper to ensure that generated grpc stub classes have appropriate license headers. | |
| If the file does not already contain a license header, inject one at the top of the file. | |
| """ | |
| s.replace( | |
| _filter_no_header([grpc_root / "src/**/*.java"]), | |
| "^package (.*);", | |
| f"{GOOD_LICENSE}package \\1;", | |
| ) | |
| def latest_maven_version(group_id: str, artifact_id: str) -> Optional[str]: | |
| """Helper function to find the latest released version of a Maven artifact. | |
| Fetches metadata from Maven Central and parses out the latest released | |
| version. | |
| Args: | |
| group_id (str): The groupId of the Maven artifact | |
| artifact_id (str): The artifactId of the Maven artifact | |
| Returns: | |
| The latest version of the artifact as a string or None | |
| """ | |
| group_path = "/".join(group_id.split(".")) | |
| url = ( | |
| f"https://repo1.maven.org/maven2/{group_path}/{artifact_id}/maven-metadata.xml" | |
| ) | |
| response = requests.get(url) | |
| if response.status_code >= 400: | |
| return "0.0.0" | |
| return version_from_maven_metadata(response.text) | |
| def version_from_maven_metadata(metadata: str) -> Optional[str]: | |
| """Helper function to parse the latest released version from the Maven | |
| metadata XML file. | |
| Args: | |
| metadata (str): The XML contents of the Maven metadata file | |
| Returns: | |
| The latest version of the artifact as a string or None | |
| """ | |
| root = ET.fromstring(metadata) | |
| latest = root.find("./versioning/latest") | |
| if latest is not None: | |
| return latest.text | |
| return None | |
| def _common_generation( | |
| service: str, | |
| version: str, | |
| library: Path, | |
| package_pattern: str, | |
| suffix: str = "", | |
| destination_name: str = None, | |
| cloud_api: bool = True, | |
| diregapic: bool = False, | |
| preserve_gapic: bool = False, | |
| ): | |
| """Helper function to execution the common generation cleanup actions. | |
| Fixes headers for protobuf classes and generated gRPC stub services. Copies | |
| code and samples to their final destinations by convention. Runs the code | |
| formatter on the generated code. | |
| Args: | |
| service (str): Name of the service. | |
| version (str): Service API version. | |
| library (Path): Path to the temp directory with the generated library. | |
| package_pattern (str): Package name template for fixing file headers. | |
| suffix (str, optional): Suffix that the generated library folder. The | |
| artman output differs from bazel's output directory. Defaults to "". | |
| destination_name (str, optional): Override the service name for the | |
| destination of the output code. Defaults to the service name. | |
| preserve_gapic (bool, optional): Whether to preserve the gapic directory | |
| prefix. Default False. | |
| """ | |
| if destination_name is None: | |
| destination_name = service | |
| cloud_prefix = "cloud-" if cloud_api else "" | |
| package_name = package_pattern.format(service=service, version=version) | |
| fix_proto_headers( | |
| library / f"proto-google-{cloud_prefix}{service}-{version}{suffix}" | |
| ) | |
| fix_grpc_headers( | |
| library / f"grpc-google-{cloud_prefix}{service}-{version}{suffix}", package_name | |
| ) | |
| if preserve_gapic: | |
| s.copy( | |
| [library / f"gapic-google-{cloud_prefix}{service}-{version}{suffix}/src"], | |
| f"gapic-google-{cloud_prefix}{destination_name}-{version}/src", | |
| required=True, | |
| ) | |
| else: | |
| s.copy( | |
| [library / f"gapic-google-{cloud_prefix}{service}-{version}{suffix}/src"], | |
| f"google-{cloud_prefix}{destination_name}/src", | |
| required=True, | |
| ) | |
| s.copy( | |
| [library / f"grpc-google-{cloud_prefix}{service}-{version}{suffix}/src"], | |
| f"grpc-google-{cloud_prefix}{destination_name}-{version}/src", | |
| # For REST-only clients, like java-compute, gRPC artifact does not exist | |
| required=(not diregapic), | |
| ) | |
| s.copy( | |
| [library / f"proto-google-{cloud_prefix}{service}-{version}{suffix}/src"], | |
| f"proto-google-{cloud_prefix}{destination_name}-{version}/src", | |
| required=True, | |
| ) | |
| if preserve_gapic: | |
| format_code(f"gapic-google-{cloud_prefix}{destination_name}-{version}/src") | |
| else: | |
| format_code(f"google-{cloud_prefix}{destination_name}/src") | |
| format_code(f"grpc-google-{cloud_prefix}{destination_name}-{version}/src") | |
| format_code(f"proto-google-{cloud_prefix}{destination_name}-{version}/src") | |
| def gapic_library( | |
| service: str, | |
| version: str, | |
| config_pattern: str = "/google/cloud/{service}/artman_{service}_{version}.yaml", | |
| package_pattern: str = "com.google.cloud.{service}.{version}", | |
| gapic: gcp.GAPICGenerator = None, | |
| destination_name: str = None, | |
| diregapic: bool = False, | |
| preserve_gapic: bool = False, | |
| **kwargs, | |
| ) -> Path: | |
| """Generate a Java library using the gapic-generator via artman via Docker. | |
| Generates code into a temp directory, fixes missing header fields, and | |
| copies into the expected locations. | |
| Args: | |
| service (str): Name of the service. | |
| version (str): Service API version. | |
| config_pattern (str, optional): Path template to artman config YAML | |
| file. Defaults to "/google/cloud/{service}/artman_{service}_{version}.yaml" | |
| package_pattern (str, optional): Package name template for fixing file | |
| headers. Defaults to "com.google.cloud.{service}.{version}". | |
| gapic (GAPICGenerator, optional): Generator instance. | |
| destination_name (str, optional): Override the service name for the | |
| destination of the output code. Defaults to the service name. | |
| preserve_gapic (bool, optional): Whether to preserve the gapic directory | |
| prefix. Default False. | |
| **kwargs: Additional options for gapic.java_library() | |
| Returns: | |
| The path to the temp directory containing the generated client. | |
| """ | |
| if gapic is None: | |
| gapic = gcp.GAPICGenerator() | |
| library = gapic.java_library( | |
| service=service, | |
| version=version, | |
| config_path=config_pattern.format(service=service, version=version), | |
| artman_output_name="", | |
| include_samples=True, | |
| diregapic=diregapic, | |
| **kwargs, | |
| ) | |
| _common_generation( | |
| service=service, | |
| version=version, | |
| library=library, | |
| package_pattern=package_pattern, | |
| destination_name=destination_name, | |
| diregapic=diregapic, | |
| preserve_gapic=preserve_gapic, | |
| ) | |
| return library | |
| def bazel_library( | |
| service: str, | |
| version: str, | |
| package_pattern: str = "com.google.cloud.{service}.{version}", | |
| gapic: gcp.GAPICBazel = None, | |
| destination_name: str = None, | |
| cloud_api: bool = True, | |
| diregapic: bool = False, | |
| preserve_gapic: bool = False, | |
| **kwargs, | |
| ) -> Path: | |
| """Generate a Java library using the gapic-generator via bazel. | |
| Generates code into a temp directory, fixes missing header fields, and | |
| copies into the expected locations. | |
| Args: | |
| service (str): Name of the service. | |
| version (str): Service API version. | |
| package_pattern (str, optional): Package name template for fixing file | |
| headers. Defaults to "com.google.cloud.{service}.{version}". | |
| gapic (GAPICBazel, optional): Generator instance. | |
| destination_name (str, optional): Override the service name for the | |
| destination of the output code. Defaults to the service name. | |
| preserve_gapic (bool, optional): Whether to preserve the gapic directory | |
| prefix. Default False. | |
| **kwargs: Additional options for gapic.java_library() | |
| Returns: | |
| The path to the temp directory containing the generated client. | |
| """ | |
| if gapic is None: | |
| gapic = gcp.GAPICBazel() | |
| library = gapic.java_library( | |
| service=service, version=version, diregapic=diregapic, **kwargs | |
| ) | |
| _common_generation( | |
| service=service, | |
| version=version, | |
| library=library / f"google-cloud-{service}-{version}-java", | |
| package_pattern=package_pattern, | |
| suffix="-java", | |
| destination_name=destination_name, | |
| cloud_api=cloud_api, | |
| diregapic=diregapic, | |
| preserve_gapic=preserve_gapic, | |
| ) | |
| return library | |
| def pregenerated_library( | |
| path: str, | |
| service: str, | |
| version: str, | |
| destination_name: str = None, | |
| cloud_api: bool = True, | |
| ) -> Path: | |
| """Generate a Java library using the gapic-generator via bazel. | |
| Generates code into a temp directory, fixes missing header fields, and | |
| copies into the expected locations. | |
| Args: | |
| path (str): Path in googleapis-gen to un-versioned generated code. | |
| service (str): Name of the service. | |
| version (str): Service API version. | |
| destination_name (str, optional): Override the service name for the | |
| destination of the output code. Defaults to the service name. | |
| cloud_api (bool, optional): Whether or not this is a cloud API (for naming) | |
| Returns: | |
| The path to the temp directory containing the generated client. | |
| """ | |
| generator = pregenerated.Pregenerated() | |
| library = generator.generate(path) | |
| cloud_prefix = "cloud-" if cloud_api else "" | |
| _common_generation( | |
| service=service, | |
| version=version, | |
| library=library / f"google-{cloud_prefix}{service}-{version}-java", | |
| package_pattern="unused", | |
| suffix="-java", | |
| destination_name=destination_name, | |
| cloud_api=cloud_api, | |
| ) | |
| return library | |
| def _merge_release_please(destination_text: str): | |
| config = yaml.safe_load(destination_text) | |
| if "handleGHRelease" in config: | |
| return destination_text | |
| config["handleGHRelease"] = True | |
| if "branches" in config: | |
| for branch in config["branches"]: | |
| branch["handleGHRelease"] = True | |
| return yaml.dump(config) | |
| def _merge_common_templates( | |
| source_text: str, destination_text: str, file_path: Path | |
| ) -> str: | |
| # keep any existing pom.xml | |
| if file_path.match("pom.xml") or file_path.match("sync-repo-settings.yaml"): | |
| logger.debug(f"existing pom file found ({file_path}) - keeping the existing") | |
| return destination_text | |
| if file_path.match("release-please.yml"): | |
| return _merge_release_please(destination_text) | |
| # by default return the newly generated content | |
| return source_text | |
| def _common_template_metadata() -> Dict[str, Any]: | |
| metadata = {} # type: Dict[str, Any] | |
| repo_metadata = common._load_repo_metadata() | |
| if repo_metadata: | |
| metadata["repo"] = repo_metadata | |
| group_id, artifact_id = repo_metadata["distribution_name"].split(":") | |
| metadata["latest_version"] = latest_maven_version( | |
| group_id=group_id, artifact_id=artifact_id | |
| ) | |
| metadata["latest_bom_version"] = latest_maven_version( | |
| group_id="com.google.cloud", | |
| artifact_id="libraries-bom", | |
| ) | |
| metadata["samples"] = samples.all_samples(["samples/**/src/main/java/**/*.java"]) | |
| metadata["snippets"] = snippets.all_snippets( | |
| ["samples/**/src/main/java/**/*.java", "samples/**/pom.xml"] | |
| ) | |
| if repo_metadata and "min_java_version" in repo_metadata: | |
| metadata["min_java_version"] = repo_metadata["min_java_version"] | |
| else: | |
| metadata["min_java_version"] = DEFAULT_MIN_SUPPORTED_JAVA_VERSION | |
| return metadata | |
| def common_templates( | |
| excludes: List[str] = [], template_path: Optional[Path] = None, **kwargs | |
| ) -> None: | |
| """Generate common templates for a Java Library | |
| Fetches information about the repository from the .repo-metadata.json file, | |
| information about the latest artifact versions and copies the files into | |
| their expected location. | |
| Args: | |
| excludes (List[str], optional): List of template paths to ignore | |
| **kwargs: Additional options for CommonTemplates.java_library() | |
| """ | |
| metadata = _common_template_metadata() | |
| kwargs["metadata"] = metadata | |
| # Generate flat to tell this repository is a split repo that have migrated | |
| # to monorepo. The owlbot.py in the monorepo sets monorepo=True. | |
| monorepo = kwargs.get("monorepo", False) | |
| split_repo = not monorepo | |
| repo_metadata = metadata["repo"] | |
| repo_short = repo_metadata["repo_short"] | |
| # Special libraries that are not GAPIC_AUTO but in the monorepo | |
| special_libs_in_monorepo = [ | |
| "java-translate", | |
| "java-dns", | |
| "java-notification", | |
| "java-resourcemanager", | |
| ] | |
| kwargs["migrated_split_repo"] = split_repo and ( | |
| repo_metadata["library_type"] == "GAPIC_AUTO" | |
| or (repo_short and repo_short in special_libs_in_monorepo) | |
| ) | |
| logger.info( | |
| "monorepo: {}, split_repo: {}, library_type: {}," | |
| " repo_short: {}, migrated_split_repo: {}".format( | |
| monorepo, | |
| split_repo, | |
| repo_metadata["library_type"], | |
| repo_short, | |
| kwargs["migrated_split_repo"], | |
| ) | |
| ) | |
| templates = gcp.CommonTemplates(template_path=template_path).java_library(**kwargs) | |
| # skip README generation on Kokoro (autosynth) | |
| if os.environ.get("KOKORO_ROOT") is not None: | |
| # README.md is now synthesized separately. This prevents synthtool from deleting the | |
| # README as it's no longer generated here. | |
| excludes.append("README.md") | |
| s.copy([templates], excludes=excludes, merge=_merge_common_templates) | |
| def custom_templates(files: List[str], **kwargs) -> None: | |
| """Generate custom template files | |
| Fetches information about the repository from the .repo-metadata.json file, | |
| information about the latest artifact versions and copies the files into | |
| their expected location. | |
| Args: | |
| files (List[str], optional): List of template paths to include | |
| **kwargs: Additional options for CommonTemplates.render() | |
| """ | |
| kwargs["metadata"] = _common_template_metadata() | |
| kwargs["metadata"]["partials"] = partials.load_partials() | |
| for file in files: | |
| template = gcp.CommonTemplates().render(file, **kwargs) | |
| s.copy([template]) | |
| def remove_method(filename: str, signature: str): | |
| """Helper to remove an entire method. | |
| Goes line-by-line to detect the start of the block. Determines | |
| the end of the block by a closing brace at the same indentation | |
| level. This requires the file to be correctly formatted. | |
| Example: consider the following class: | |
| class Example { | |
| public void main(String[] args) { | |
| System.out.println("Hello World"); | |
| } | |
| public String foo() { | |
| return "bar"; | |
| } | |
| } | |
| To remove the `main` method above, use: | |
| remove_method('path/to/file', 'public void main(String[] args)') | |
| Args: | |
| filename (str): Path to source file | |
| signature (str): Full signature of the method to remove. Example: | |
| `public void main(String[] args)`. | |
| """ | |
| lines = [] | |
| leading_regex = None | |
| with open(filename, "r") as fp: | |
| line = fp.readline() | |
| while line: | |
| # for each line, try to find the matching | |
| regex = re.compile("(\\s*)" + re.escape(signature) + ".*") | |
| match = regex.match(line) | |
| if match: | |
| leading_regex = re.compile(match.group(1) + "}") | |
| line = fp.readline() | |
| continue | |
| # not in a ignore block - preserve the line | |
| if not leading_regex: | |
| lines.append(line) | |
| line = fp.readline() | |
| continue | |
| # detect the closing tag based on the leading spaces | |
| match = leading_regex.match(line) | |
| if match: | |
| # block is closed, resume capturing content | |
| leading_regex = None | |
| line = fp.readline() | |
| with open(filename, "w") as fp: | |
| for line in lines: | |
| # print(line) | |
| fp.write(line) | |
| def copy_and_rename_method(filename: str, signature: str, before: str, after: str): | |
| """Helper to make a copy an entire method and rename it. | |
| Goes line-by-line to detect the start of the block. Determines | |
| the end of the block by a closing brace at the same indentation | |
| level. This requires the file to be correctly formatted. | |
| The method is copied over and renamed in the method signature. | |
| The calls to both methods are separate and unaffected. | |
| Example: consider the following class: | |
| class Example { | |
| public void main(String[] args) { | |
| System.out.println("Hello World"); | |
| } | |
| public String foo() { | |
| return "bar"; | |
| } | |
| } | |
| To copy and rename the `main` method above, use: | |
| copy_and_rename_method('path/to/file', 'public void main(String[] args)', | |
| 'main', 'foo1') | |
| Args: | |
| filename (str): Path to source file | |
| signature (str): Full signature of the method to remove. Example: | |
| `public void main(String[] args)`. | |
| before (str): name of the method to be copied | |
| after (str): new name of the copied method | |
| """ | |
| lines = [] | |
| method = [] | |
| leading_regex = None | |
| with open(filename, "r") as fp: | |
| line = fp.readline() | |
| while line: | |
| # for each line, try to find the matching | |
| regex = re.compile("(\\s*)" + re.escape(signature) + ".*") | |
| match = regex.match(line) | |
| if match: | |
| leading_regex = re.compile(match.group(1) + "}") | |
| lines.append(line) | |
| method.append(line.replace(before, after)) | |
| line = fp.readline() | |
| continue | |
| lines.append(line) | |
| # not in a ignore block - preserve the line | |
| if leading_regex: | |
| method.append(line) | |
| else: | |
| line = fp.readline() | |
| continue | |
| # detect the closing tag based on the leading spaces | |
| match = leading_regex.match(line) | |
| if match: | |
| # block is closed, resume capturing content | |
| leading_regex = None | |
| lines.append("\n") | |
| lines.extend(method) | |
| line = fp.readline() | |
| with open(filename, "w") as fp: | |
| for line in lines: | |
| # print(line) | |
| fp.write(line) | |
| def add_javadoc(filename: str, signature: str, javadoc_type: str, content: List[str]): | |
| """Helper to add a javadoc annoatation to a method. | |
| Goes line-by-line to detect the start of the block. | |
| Then finds the existing method comment (if it exists). If the | |
| comment already exists, it will append the javadoc annotation | |
| to the javadoc block. Otherwise, it will create a new javadoc | |
| comment block. | |
| Example: consider the following class: | |
| class Example { | |
| public void main(String[] args) { | |
| System.out.println("Hello World"); | |
| } | |
| public String foo() { | |
| return "bar"; | |
| } | |
| } | |
| To add a javadoc annotation the `main` method above, use: | |
| add_javadoc('path/to/file', 'public void main(String[] args)', | |
| 'deprecated', 'Please use foo instead.') | |
| Args: | |
| filename (str): Path to source file | |
| signature (str): Full signature of the method to remove. Example: | |
| `public void main(String[] args)`. | |
| javadoc_type (str): The type of javadoc annotation. Example: `deprecated`. | |
| content (List[str]): The javadoc lines | |
| """ | |
| lines: List[str] = [] | |
| annotations: List[str] = [] | |
| with open(filename, "r") as fp: | |
| line = fp.readline() | |
| while line: | |
| # for each line, try to find the matching | |
| regex = re.compile("(\\s*)" + re.escape(signature) + ".*") | |
| match = regex.match(line) | |
| if match: | |
| leading_spaces = len(line) - len(line.lstrip()) | |
| indent = leading_spaces * " " | |
| last_line = lines.pop() | |
| while last_line.lstrip() and last_line.lstrip()[0] == "@": | |
| annotations.append(last_line) | |
| last_line = lines.pop() | |
| if last_line.strip() == "*/": | |
| first = True | |
| for content_line in content: | |
| if first: | |
| lines.append( | |
| indent | |
| + " * @" | |
| + javadoc_type | |
| + " " | |
| + content_line | |
| + "\n" | |
| ) | |
| first = False | |
| else: | |
| lines.append(indent + " * " + content_line + "\n") | |
| lines.append(last_line) | |
| else: | |
| lines.append(last_line) | |
| lines.append(indent + "/**\n") | |
| first = True | |
| for content_line in content: | |
| if first: | |
| lines.append( | |
| indent | |
| + " * @" | |
| + javadoc_type | |
| + " " | |
| + content_line | |
| + "\n" | |
| ) | |
| first = False | |
| else: | |
| lines.append(indent + " * " + content_line + "\n") | |
| lines.append(indent + " */\n") | |
| lines.extend(annotations[::-1]) | |
| lines.append(line) | |
| line = fp.readline() | |
| with open(filename, "w") as fp: | |
| for line in lines: | |
| # print(line) | |
| fp.write(line) | |
| def annotate_method(filename: str, signature: str, annotation: str): | |
| """Helper to add an annotation to a method. | |
| Goes line-by-line to detect the start of the block. | |
| Then adds the annotation above the found method signature. | |
| Example: consider the following class: | |
| class Example { | |
| public void main(String[] args) { | |
| System.out.println("Hello World"); | |
| } | |
| public String foo() { | |
| return "bar"; | |
| } | |
| } | |
| To add an annotation the `main` method above, use: | |
| annotate_method('path/to/file', 'public void main(String[] args)', | |
| '@Generated()') | |
| Args: | |
| filename (str): Path to source file | |
| signature (str): Full signature of the method to remove. Example: | |
| `public void main(String[] args)`. | |
| annotation (str): Full annotation. Example: `@Deprecated` | |
| """ | |
| lines: List[str] = [] | |
| with open(filename, "r") as fp: | |
| line = fp.readline() | |
| while line: | |
| # for each line, try to find the matching | |
| regex = re.compile("(\\s*)" + re.escape(signature) + ".*") | |
| match = regex.match(line) | |
| if match: | |
| leading_spaces = len(line) - len(line.lstrip()) | |
| indent = leading_spaces * " " | |
| lines.append(indent + annotation + "\n") | |
| lines.append(line) | |
| line = fp.readline() | |
| with open(filename, "w") as fp: | |
| for line in lines: | |
| # print(line) | |
| fp.write(line) | |
| def deprecate_method(filename: str, signature: str, alternative: str): | |
| """Helper to deprecate a method. | |
| Goes line-by-line to detect the start of the block. | |
| Then adds the deprecation comment before the method signature. | |
| The @Deprecation annotation is also added. | |
| Example: consider the following class: | |
| class Example { | |
| public void main(String[] args) { | |
| System.out.println("Hello World"); | |
| } | |
| public String foo() { | |
| return "bar"; | |
| } | |
| } | |
| To deprecate the `main` method above, use: | |
| deprecate_method('path/to/file', 'public void main(String[] args)', | |
| DEPRECATION_WARNING.format(new_method="foo")) | |
| Args: | |
| filename (str): Path to source file | |
| signature (str): Full signature of the method to remove. Example: | |
| `public void main(String[] args)`. | |
| alternative: DEPRECATION WARNING: multiline javadoc comment with user | |
| specified leading open/close comment tags | |
| """ | |
| add_javadoc(filename, signature, "deprecated", alternative.splitlines()) | |
| annotate_method(filename, signature, "@Deprecated") |