From a81eb1f327abed4dda2f5d3cad4ade1cbbf1f6fc Mon Sep 17 00:00:00 2001 From: Nicole Lee Date: Fri, 24 Jul 2026 17:47:13 +0000 Subject: [PATCH 1/3] feat: add JSpecify migration and nullmarked automation script --- jspecify_migration.py | 79 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 jspecify_migration.py diff --git a/jspecify_migration.py b/jspecify_migration.py new file mode 100644 index 000000000000..816a50ef5f98 --- /dev/null +++ b/jspecify_migration.py @@ -0,0 +1,79 @@ +import os +import re + +def migrate_file(file_path): + with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + + original_content = content + + # 1. Add JSpecify NullMarked import if not present + package_match = re.search(r"^(package\s+[\w\.]+;)", content, re.MULTILINE) + if package_match and "org.jspecify.annotations.NullMarked" not in content and "@NullMarked" not in content: + package_line = package_match.group(1) + content = content.replace(package_line, package_line + "\nimport org.jspecify.annotations.NullMarked;") + + # 2. Add @NullMarked before class/interface/enum declaration + class_match = re.search(r"\n(public\s+)?(abstract\s+)?(final\s+)?(class|interface|enum)\s+\w+", content) + if class_match: + class_start = class_match.start() + prefix = content[:class_start] + lines = prefix.split("\n") + + # Find the correct insertion point (before other class-level annotations) + insert_idx = len(lines) + for i in range(len(lines) - 1, -1, -1): + line = lines[i].strip() + if line.startswith("@") and not line.startswith("@Override") and not line.startswith("@Test"): + insert_idx = i + elif line == "" or line.startswith("//") or line.startswith("/*") or line.startswith("*"): + break + else: + break + lines.insert(insert_idx, "@NullMarked") + content = "\n".join(lines) + content[class_start:] + elif "package-info.java" in file_path: + content = content.replace(package_line, "@NullMarked\n" + package_line) + + # 3. Replace legacy javax Nullable import + content = content.replace("import javax.annotation.Nullable;", "import org.jspecify.annotations.Nullable;") + + # 4. Reposition @Nullable from declaration to type-use position + # (immediately before the type name, after class modifiers) + patterns = [ + (r"@Nullable\s+public\s+abstract\s+final\s+(\w+)", r"public abstract final @Nullable \1"), + (r"@Nullable\s+public\s+abstract\s+(\w+)", r"public abstract @Nullable \1"), + (r"@Nullable\s+public\s+static\s+final\s+(\w+)", r"public static final @Nullable \1"), + (r"@Nullable\s+public\s+static\s+(\w+)", r"public static @Nullable \1"), + (r"@Nullable\s+public\s+final\s+(\w+)", r"public final @Nullable \1"), + (r"@Nullable\s+public\s+(\w+)", r"public @Nullable \1"), + (r"@Nullable\s+private\s+static\s+final\s+(\w+)", r"private static final @Nullable \1"), + (r"@Nullable\s+private\s+static\s+(\w+)", r"private static @Nullable \1"), + (r"@Nullable\s+private\s+final\s+(\w+)", r"private final @Nullable \1"), + (r"@Nullable\s+private\s+(\w+)", r"private @Nullable \1"), + (r"@Nullable\s+protected\s+static\s+final\s+(\w+)", r"protected static final @Nullable \1"), + (r"@Nullable\s+protected\s+static\s+(\w+)", r"protected static @Nullable \1"), + (r"@Nullable\s+protected\s+final\s+(\w+)", r"protected final @Nullable \1"), + (r"@Nullable\s+protected\s+(\w+)", r"protected @Nullable \1"), + ] + + for pattern, replacement in patterns: + content = re.sub(pattern, replacement, content) + + # Save changes if modified + if content != original_content: + with open(file_path, "w", encoding="utf-8") as f: + f.write(content) + +def walk_and_migrate(directory): + for root, _, files in os.walk(directory): + for file in files: + if file.endswith(".java"): + migrate_file(os.path.join(root, file)) + +if __name__ == "__main__": + import sys + if len(sys.argv) > 1: + walk_and_migrate(sys.argv[1]) + else: + print("Usage: python jspecify_migration.py ") From a349256b911674278a733560ff30aac42426b40c Mon Sep 17 00:00:00 2001 From: Nicole Lee Date: Fri, 24 Jul 2026 17:47:13 +0000 Subject: [PATCH 2/3] feat: add JSpecify migration and nullmarked automation script --- jspecify_migration.py | 77 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 jspecify_migration.py diff --git a/jspecify_migration.py b/jspecify_migration.py new file mode 100644 index 000000000000..a0ef5e250491 --- /dev/null +++ b/jspecify_migration.py @@ -0,0 +1,77 @@ +import os +import re + +def migrate_file(file_path): + with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + + original_content = content + + # 1. Add JSpecify NullMarked import if not present + package_match = re.search(r"^(package\s+[\w\.]+;)", content, re.MULTILINE) + if package_match and "org.jspecify.annotations.NullMarked" not in content and "@NullMarked" not in content: + package_line = package_match.group(1) + content = content.replace(package_line, package_line + "\nimport org.jspecify.annotations.NullMarked;") + + # 2. Add @NullMarked before class/interface/enum declaration + class_match = re.search(r"\n(public\s+)?(abstract\s+)?(final\s+)?(class|interface|enum)\s+\w+", content) + if class_match: + class_start = class_match.start() + prefix = content[:class_start] + lines = prefix.split("\n") + + # Find the correct insertion point (before other class-level annotations) + insert_idx = len(lines) + for i in range(len(lines) - 1, -1, -1): + line = lines[i].strip() + if line.startswith("@") and not line.startswith("@Override") and not line.startswith("@Test"): + insert_idx = i + elif line == "" or line.startswith("//") or line.startswith("/*") or line.startswith("*"): + break + else: + break + lines.insert(insert_idx, "@NullMarked") + content = "\n".join(lines) + content[class_start:] + + # 3. Replace legacy javax Nullable import + content = content.replace("import javax.annotation.Nullable;", "import org.jspecify.annotations.Nullable;") + + # 4. Reposition @Nullable from declaration to type-use position + # (immediately before the type name, after class modifiers) + patterns = [ + (r"@Nullable\s+public\s+abstract\s+final\s+(\w+)", r"public abstract final @Nullable \1"), + (r"@Nullable\s+public\s+abstract\s+(\w+)", r"public abstract @Nullable \1"), + (r"@Nullable\s+public\s+static\s+final\s+(\w+)", r"public static final @Nullable \1"), + (r"@Nullable\s+public\s+static\s+(\w+)", r"public static @Nullable \1"), + (r"@Nullable\s+public\s+final\s+(\w+)", r"public final @Nullable \1"), + (r"@Nullable\s+public\s+(\w+)", r"public @Nullable \1"), + (r"@Nullable\s+private\s+static\s+final\s+(\w+)", r"private static final @Nullable \1"), + (r"@Nullable\s+private\s+static\s+(\w+)", r"private static @Nullable \1"), + (r"@Nullable\s+private\s+final\s+(\w+)", r"private final @Nullable \1"), + (r"@Nullable\s+private\s+(\w+)", r"private @Nullable \1"), + (r"@Nullable\s+protected\s+static\s+final\s+(\w+)", r"protected static final @Nullable \1"), + (r"@Nullable\s+protected\s+static\s+(\w+)", r"protected static @Nullable \1"), + (r"@Nullable\s+protected\s+final\s+(\w+)", r"protected final @Nullable \1"), + (r"@Nullable\s+protected\s+(\w+)", r"protected @Nullable \1"), + ] + + for pattern, replacement in patterns: + content = re.sub(pattern, replacement, content) + + # Save changes if modified + if content != original_content: + with open(file_path, "w", encoding="utf-8") as f: + f.write(content) + +def walk_and_migrate(directory): + for root, _, files in os.walk(directory): + for file in files: + if file.endswith(".java") and file != "package-info.java": + migrate_file(os.path.join(root, file)) + +if __name__ == "__main__": + import sys + if len(sys.argv) > 1: + walk_and_migrate(sys.argv[1]) + else: + print("Usage: python jspecify_migration.py ") From 74ec191a1d02d986ce221ecb30910baeae163529 Mon Sep 17 00:00:00 2001 From: Nicole Lee Date: Thu, 30 Jul 2026 15:29:58 +0000 Subject: [PATCH 3/3] updated the script to include removal of both javax nullable and nonnull and applied gemini suggestions --- jspecify_migration.py | 73 +++++++++++++++++++------------------------ 1 file changed, 33 insertions(+), 40 deletions(-) diff --git a/jspecify_migration.py b/jspecify_migration.py index ac9c4016488f..c1bd93eef18a 100644 --- a/jspecify_migration.py +++ b/jspecify_migration.py @@ -8,56 +8,49 @@ def migrate_file(file_path): original_content = content # 1. Add JSpecify NullMarked import if not present - package_match = re.search(r"^(package\s+[\w\.]+;)", content, re.MULTILINE) + package_match = re.search(r"^\s*package\s+[\w\.]+\s*;", content, re.MULTILINE) if package_match and "org.jspecify.annotations.NullMarked" not in content and "@NullMarked" not in content: - package_line = package_match.group(1) - content = content.replace(package_line, package_line + "\nimport org.jspecify.annotations.NullMarked;") + end_pos = package_match.end() + content = content[:end_pos] + "\nimport org.jspecify.annotations.NullMarked;" + content[end_pos:] # 2. Add @NullMarked before class/interface/enum declaration - class_match = re.search(r"\n(public\s+)?(abstract\s+)?(final\s+)?(class|interface|enum)\s+\w+", content) + class_match = re.search( + r"(\n|^)\s*(?:(?:public|protected|private|abstract|final|static|sealed|non-sealed)\s+)*(?:class|interface|enum|record)\s+\w+", + content + ) if class_match: class_start = class_match.start() - prefix = content[:class_start] - lines = prefix.split("\n") - - # Find the correct insertion point (before other class-level annotations) - insert_idx = len(lines) - for i in range(len(lines) - 1, -1, -1): - line = lines[i].strip() - if line.startswith("@") and not line.startswith("@Override") and not line.startswith("@Test"): - insert_idx = i - elif line == "" or line.startswith("//") or line.startswith("/*") or line.startswith("*"): - break - else: - break - lines.insert(insert_idx, "@NullMarked") - content = "\n".join(lines) + content[class_start:] - + if class_start == 0: + content = "@NullMarked\n" + content + else: + prefix = content[:class_start] + lines = prefix.split("\n") + + # Find the correct insertion point (before other class-level annotations) + insert_idx = len(lines) + for i in range(len(lines) - 1, -1, -1): + line = lines[i].strip() + if line.startswith("@") and not line.startswith("@Override") and not line.startswith("@Test"): + insert_idx = i + elif line == "" or line.startswith("//") or line.startswith("/*") or line.startswith("*"): + break + else: + break + lines.insert(insert_idx, "@NullMarked") + content = "\n".join(lines) + content[class_start:] # 3. Replace legacy javax Nullable import content = content.replace("import javax.annotation.Nullable;", "import org.jspecify.annotations.Nullable;") - # 4. Reposition @Nullable from declaration to type-use position - # (immediately before the type name, after class modifiers) - patterns = [ - (r"@Nullable\s+public\s+abstract\s+final\s+(\w+)", r"public abstract final @Nullable \1"), - (r"@Nullable\s+public\s+abstract\s+(\w+)", r"public abstract @Nullable \1"), - (r"@Nullable\s+public\s+static\s+final\s+(\w+)", r"public static final @Nullable \1"), - (r"@Nullable\s+public\s+static\s+(\w+)", r"public static @Nullable \1"), - (r"@Nullable\s+public\s+final\s+(\w+)", r"public final @Nullable \1"), - (r"@Nullable\s+public\s+(\w+)", r"public @Nullable \1"), - (r"@Nullable\s+private\s+static\s+final\s+(\w+)", r"private static final @Nullable \1"), - (r"@Nullable\s+private\s+static\s+(\w+)", r"private static @Nullable \1"), - (r"@Nullable\s+private\s+final\s+(\w+)", r"private final @Nullable \1"), - (r"@Nullable\s+private\s+(\w+)", r"private @Nullable \1"), - (r"@Nullable\s+protected\s+static\s+final\s+(\w+)", r"protected static final @Nullable \1"), - (r"@Nullable\s+protected\s+static\s+(\w+)", r"protected static @Nullable \1"), - (r"@Nullable\s+protected\s+final\s+(\w+)", r"protected final @Nullable \1"), - (r"@Nullable\s+protected\s+(\w+)", r"protected @Nullable \1"), - ] + # 4. Remove legacy javax Nonnull import and annotations + content = re.sub(r"import\s+javax\.annotation\.Nonnull;\s*\r?\n", "", content) + content = re.sub(r"^[ \t]*@(javax\.annotation\.)?Nonnull[ \t]*\r?\n", "", content, flags=re.MULTILINE) + content = re.sub(r"\b@(javax\.annotation\.)?Nonnull\s+", "", content) - for pattern, replacement in patterns: - content = re.sub(pattern, replacement, content) + # 5. Reposition @Nullable to be after all modifiers (type-use position) + # This single regex handles any combination of modifiers (including package-private, final parameters, etc.) + modifier_pattern = r"@Nullable\s+((?:(?:public|private|protected|static|final|abstract|synchronized|volatile|transient|native|strictfp|sealed|non-sealed)\s+)+)" + content = re.sub(modifier_pattern, r"\1@Nullable ", content) # Save changes if modified if content != original_content: