From 1d48e6ec9660de9b4c0abc8f4f3544878f65002b Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Wed, 31 Jan 2024 15:45:55 -0800 Subject: [PATCH 01/50] fixes --- nullaway/build.gradle | 2 +- .../uber/nullaway/LibModelInfoExtractor.java | 220 ++++++++++++++++++ 2 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 nullaway/src/main/java/com/uber/nullaway/LibModelInfoExtractor.java diff --git a/nullaway/build.gradle b/nullaway/build.gradle index 15327aed88..31b0481559 100644 --- a/nullaway/build.gradle +++ b/nullaway/build.gradle @@ -39,7 +39,7 @@ dependencies { compileOnly deps.build.errorProneCheckApi implementation deps.build.checkerDataflow implementation deps.build.guava - + implementation 'com.github.javaparser:javaparser-core:3.25.8' testImplementation project(":annotations") testImplementation deps.test.junit4 testImplementation(deps.build.errorProneTestHelpers) { diff --git a/nullaway/src/main/java/com/uber/nullaway/LibModelInfoExtractor.java b/nullaway/src/main/java/com/uber/nullaway/LibModelInfoExtractor.java new file mode 100644 index 0000000000..556eb187cf --- /dev/null +++ b/nullaway/src/main/java/com/uber/nullaway/LibModelInfoExtractor.java @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2019 Uber Technologies, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.uber.nullaway; + +import com.github.javaparser.ParseResult; +import com.github.javaparser.ParserConfiguration.LanguageLevel; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.NodeList; +import com.github.javaparser.ast.PackageDeclaration; +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.body.ConstructorDeclaration; +import com.github.javaparser.ast.body.EnumDeclaration; +import com.github.javaparser.ast.body.FieldDeclaration; +import com.github.javaparser.ast.body.InitializerDeclaration; +import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.ast.expr.AnnotationExpr; +import com.github.javaparser.ast.expr.NormalAnnotationExpr; +import com.github.javaparser.ast.type.ClassOrInterfaceType; +import com.github.javaparser.ast.type.TypeParameter; +import com.github.javaparser.ast.visitor.ModifierVisitor; +import com.github.javaparser.utils.CollectionStrategy; +import com.github.javaparser.utils.ParserCollectionStrategy; +import com.github.javaparser.utils.ProjectRoot; +import com.github.javaparser.utils.SourceRoot; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayDeque; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class LibModelInfoExtractor { + + public static void main(String[] args) { + for (String arg : args) { + processSourceFile(arg); + } + } + + public static void processSourceFile(String file) { + Path root = dirnameToPath(file); + MinimizerCallback mc = new MinimizerCallback(); + CollectionStrategy strategy = new ParserCollectionStrategy(); + // Required to include directories that contain a module-info.java, which don't parse by + // default. + strategy.getParserConfiguration().setLanguageLevel(LanguageLevel.JAVA_17); + ProjectRoot projectRoot = strategy.collect(root); + + projectRoot + .getSourceRoots() + .forEach( + sourceRoot -> { + try { + sourceRoot.parse("", mc); + } catch (IOException e) { + System.err.println("IOException: " + e); + } + }); + } + + public static Path dirnameToPath(String dir) { + File f = new File(dir); + if (!f.exists()) { + System.err.printf("Directory %s (%s) does not exist.%n", dir, f); + System.exit(1); + } + if (!f.isDirectory()) { + System.err.printf("Not a directory: %s (%s).%n", dir, f); + System.exit(1); + } + String absoluteDir = f.getAbsolutePath(); + if (absoluteDir.endsWith("/.")) { + absoluteDir = absoluteDir.substring(0, absoluteDir.length() - 2); + } + return Paths.get(absoluteDir); + } + + /** Callback to process each Java file; see class documentation for details. */ + private static class MinimizerCallback implements SourceRoot.Callback { + + /** The visitor instance. */ + private final CompilationUnitVisitor mv; + + /** Create a MinimizerCallback instance. */ + public MinimizerCallback() { + this.mv = new CompilationUnitVisitor(); + } + + @Override + public Result process(Path localPath, Path absolutePath, ParseResult result) { + Result res = Result.SAVE; + Optional opt = result.getResult(); + if (opt.isPresent()) { + CompilationUnit cu = opt.get(); + visitAll(cu); + } + return res; + } + + @SuppressWarnings("unused") + public void visitAll(Node rootNode) { + if (rootNode == null) { + return; + } + ArrayDeque stack = new ArrayDeque<>(); + stack.addFirst(rootNode); + while (!stack.isEmpty()) { + Node current = stack.removeFirst(); + current.accept(mv, null); + List children = current.getChildNodes(); + if (children != null) { + for (Node child : children) { + stack.addFirst(child); + } + } + } + } + } + + private static class CompilationUnitVisitor extends ModifierVisitor { + + private String packageName = ""; + + @Override + public PackageDeclaration visit(PackageDeclaration n, Void arg) { + this.packageName = n.getNameAsString(); + System.out.println("Package name: " + packageName); + return n; + } + + @Override + public ClassOrInterfaceDeclaration visit(ClassOrInterfaceDeclaration cid, Void arg) { + String classOrInterfaceName = packageName + "." + cid.getNameAsString(); + Map nullableTypeBoundsMap = new HashMap<>(); + Map> classOrInterfaceAnnotationsMap = new HashMap<>(); + List paramList = cid.getTypeParameters(); + // Finding Nullable upper bounds for generic type parameters. + for (int i = 0; i < paramList.size(); i++) { + boolean hasNullableUpperBound = false; + NodeList upperBoundList = paramList.get(i).getTypeBound(); + for (ClassOrInterfaceType upperBound : upperBoundList) { + if (upperBound.isAnnotationPresent("Nullable")) { + hasNullableUpperBound = true; + break; + } + } + if (hasNullableUpperBound) { + nullableTypeBoundsMap.put(i, classOrInterfaceName); + } + } + // Finding All the annotations on the class or interface. + classOrInterfaceAnnotationsMap.put(classOrInterfaceName, cid.getAnnotations()); + System.out.println("Fully qualified class name: " + classOrInterfaceName); + nullableTypeBoundsMap.forEach( + (p, a) -> System.out.println("Nullable Index: " + p + "\tClass: " + a)); + classOrInterfaceAnnotationsMap.forEach( + (c, a) -> System.out.println("Class: " + c + "\tAnnotations: " + a)); + return cid; + } + + @Override + public EnumDeclaration visit(EnumDeclaration ed, Void arg) { + return ed; + } + + @Override + public ConstructorDeclaration visit(ConstructorDeclaration cd, Void arg) { + return cd; + } + + @Override + public MethodDeclaration visit(MethodDeclaration md, Void arg) { + String methodName = md.getNameAsString(); + Map> methodAnnotationsMap = new HashMap<>(); + methodAnnotationsMap.put(methodName, md.getAnnotations()); + methodAnnotationsMap.forEach( + (m, a) -> System.out.println("Method: " + m + "\tAnnotations: " + a)); + return md; + } + + @Override + public FieldDeclaration visit(FieldDeclaration fd, Void arg) { + return fd; + } + + @Override + public InitializerDeclaration visit(InitializerDeclaration id, Void arg) { + return id; + } + + @Override + public NormalAnnotationExpr visit(NormalAnnotationExpr nae, Void arg) { + return nae; + } + } +} From b0631cbe197110ecf76bbdece23d241f434edf7a Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Tue, 6 Feb 2024 19:14:13 -0800 Subject: [PATCH 02/50] stubx file writer logic --- gradle/dependencies.gradle | 1 + nullaway/build.gradle | 2 +- .../uber/nullaway/LibModelInfoExtractor.java | 82 +++++++++++- .../nullaway/MethodAnnotationsRecord.java | 26 ++++ .../com/uber/nullaway/StubxFileWriter.java | 122 ++++++++++++++++++ 5 files changed, 227 insertions(+), 6 deletions(-) create mode 100644 nullaway/src/main/java/com/uber/nullaway/MethodAnnotationsRecord.java create mode 100644 nullaway/src/main/java/com/uber/nullaway/StubxFileWriter.java diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index bada5eedf1..a7e5b66984 100755 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -75,6 +75,7 @@ def build = [ errorProneTestHelpersOld: "com.google.errorprone:error_prone_test_helpers:${oldestErrorProneVersion}", checkerDataflow : "org.checkerframework:dataflow-nullaway:${versions.checkerFramework}", guava : "com.google.guava:guava:30.1-jre", + javaparser : "com.github.javaparser:javaparser-core:3.25.8", javaxValidation : "javax.validation:validation-api:2.0.1.Final", jspecify : "org.jspecify:jspecify:0.3.0", jsr305Annotations : "com.google.code.findbugs:jsr305:3.0.2", diff --git a/nullaway/build.gradle b/nullaway/build.gradle index 31b0481559..a9e5ee6a93 100644 --- a/nullaway/build.gradle +++ b/nullaway/build.gradle @@ -39,7 +39,7 @@ dependencies { compileOnly deps.build.errorProneCheckApi implementation deps.build.checkerDataflow implementation deps.build.guava - implementation 'com.github.javaparser:javaparser-core:3.25.8' + implementation deps.build.javaparser testImplementation project(":annotations") testImplementation deps.test.junit4 testImplementation(deps.build.errorProneTestHelpers) { diff --git a/nullaway/src/main/java/com/uber/nullaway/LibModelInfoExtractor.java b/nullaway/src/main/java/com/uber/nullaway/LibModelInfoExtractor.java index 556eb187cf..6d1e058d17 100644 --- a/nullaway/src/main/java/com/uber/nullaway/LibModelInfoExtractor.java +++ b/nullaway/src/main/java/com/uber/nullaway/LibModelInfoExtractor.java @@ -43,22 +43,34 @@ import com.github.javaparser.utils.ParserCollectionStrategy; import com.github.javaparser.utils.ProjectRoot; import com.github.javaparser.utils.SourceRoot; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import java.io.DataOutputStream; import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.attribute.FileTime; import java.util.ArrayDeque; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; public class LibModelInfoExtractor { + private static Map methodRecords = new LinkedHashMap<>(); + public static void main(String[] args) { - for (String arg : args) { - processSourceFile(arg); - } + // input directory + processSourceFile(args[0]); + // output directory + writeToAstubx(args[1]); } public static void processSourceFile(String file) { @@ -82,6 +94,40 @@ public static void processSourceFile(String file) { }); } + public static void writeToAstubx(String outputPath) { + Map importedAnnotations = + ImmutableMap.builder() + .put("Nonnull", "javax.annotation.Nonnull") + .put("Nullable", "javax.annotation.Nullable") + .build(); + ZipOutputStream zos; + try { + zos = new ZipOutputStream(new FileOutputStream(outputPath)); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + if (!methodRecords.isEmpty()) { + ZipEntry entry = new ZipEntry("META-INF/nullaway/libmodels.astubx"); + // Set the modification/creation time to 0 to ensure that this jars always have the same + // checksum + entry.setTime(0); + entry.setCreationTime(FileTime.fromMillis(0)); + try { + zos.putNextEntry(entry); + StubxFileWriter.write( + new DataOutputStream(zos), + importedAnnotations, + new HashMap<>(), + new HashMap<>(), + methodRecords); + zos.closeEntry(); + zos.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + public static Path dirnameToPath(String dir) { File f = new File(dir); if (!f.exists()) { @@ -173,7 +219,9 @@ public ClassOrInterfaceDeclaration visit(ClassOrInterfaceDeclaration cid, Void a } } // Finding All the annotations on the class or interface. - classOrInterfaceAnnotationsMap.put(classOrInterfaceName, cid.getAnnotations()); + if (cid.getAnnotations().isNonEmpty()) { + classOrInterfaceAnnotationsMap.put(classOrInterfaceName, cid.getAnnotations()); + } System.out.println("Fully qualified class name: " + classOrInterfaceName); nullableTypeBoundsMap.forEach( (p, a) -> System.out.println("Nullable Index: " + p + "\tClass: " + a)); @@ -195,10 +243,34 @@ public ConstructorDeclaration visit(ConstructorDeclaration cd, Void arg) { @Override public MethodDeclaration visit(MethodDeclaration md, Void arg) { String methodName = md.getNameAsString(); + Optional parentClassNode = md.getParentNode(); + String parentClassName = + parentClassNode.isPresent() + ? ((ClassOrInterfaceDeclaration) parentClassNode.get()).getNameAsString() + : ""; + String methodSignature = md.getSignature().toString(); Map> methodAnnotationsMap = new HashMap<>(); - methodAnnotationsMap.put(methodName, md.getAnnotations()); + Map nonNullReturnMethods = new HashMap<>(); + boolean isNullableAnnotationPresent = false; + if (md.getAnnotations().isNonEmpty()) { + methodAnnotationsMap.put(methodName, md.getAnnotations()); + } methodAnnotationsMap.forEach( (m, a) -> System.out.println("Method: " + m + "\tAnnotations: " + a)); + for (AnnotationExpr annotation : md.getAnnotations()) { + if (annotation.getNameAsString().equalsIgnoreCase("@Nullable")) { + isNullableAnnotationPresent = true; + break; + } + } + if (!isNullableAnnotationPresent) { + nonNullReturnMethods.put(packageName + "." + parentClassName, methodSignature); + methodRecords.put( + packageName + "." + parentClassName + ":" + methodName, + new MethodAnnotationsRecord(ImmutableSet.of("Nonnull"), ImmutableMap.of())); + } + nonNullReturnMethods.forEach( + (c, s) -> System.out.println("Enclosing Class: " + c + "\tMethod Signature: " + s)); return md; } diff --git a/nullaway/src/main/java/com/uber/nullaway/MethodAnnotationsRecord.java b/nullaway/src/main/java/com/uber/nullaway/MethodAnnotationsRecord.java new file mode 100644 index 0000000000..199fd89925 --- /dev/null +++ b/nullaway/src/main/java/com/uber/nullaway/MethodAnnotationsRecord.java @@ -0,0 +1,26 @@ +package com.uber.nullaway; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +/** A record describing the annotations associated with a java method and its arguments. */ +final class MethodAnnotationsRecord { + private final ImmutableSet methodAnnotations; + // 0 means receiver + private final ImmutableMap> argumentAnnotations; + + MethodAnnotationsRecord( + ImmutableSet methodAnnotations, + ImmutableMap> argumentAnnotations) { + this.methodAnnotations = methodAnnotations; + this.argumentAnnotations = argumentAnnotations; + } + + ImmutableSet getMethodAnnotations() { + return methodAnnotations; + } + + ImmutableMap> getArgumentAnnotations() { + return argumentAnnotations; + } +} diff --git a/nullaway/src/main/java/com/uber/nullaway/StubxFileWriter.java b/nullaway/src/main/java/com/uber/nullaway/StubxFileWriter.java new file mode 100644 index 0000000000..682b0ce89e --- /dev/null +++ b/nullaway/src/main/java/com/uber/nullaway/StubxFileWriter.java @@ -0,0 +1,122 @@ +package com.uber.nullaway; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** Simple writer for the astubx format. */ +final class StubxFileWriter { + /** + * The file magic number for version 0 .astubx files. It should be the first four bytes of any + * compatible .astubx file. + */ + private static final int VERSION_0_FILE_MAGIC_NUMBER = 691458791; + + /** + * This method writes the provided list of annotations to a DataOutputStream in the astubx format. + * + * @param out Output stream. + * @param importedAnnotations Mapping of 'custom annotations' to their 'definition classes'. + * @param packageAnnotations Map of 'package names' to their 'list of package-level annotations'. + * @param typeAnnotations Map of 'type names' to their 'list of type annotations'. + * @param methodRecords Map of 'method signatures' to their 'method annotations record'. Method + * annotations record consists of return value annotations and argument annotations. {@link + * MethodAnnotationsRecord} + * @exception IOException On output error. + */ + static void write( + DataOutputStream out, + Map importedAnnotations, + Map> packageAnnotations, + Map> typeAnnotations, + Map methodRecords) + throws IOException { + // File format version/magic number + out.writeInt(VERSION_0_FILE_MAGIC_NUMBER); + // Followed by the number of string dictionary entries + int numStringEntries = 0; + Map encodingDictionary = new LinkedHashMap<>(); + List strings = new ArrayList(); + List> keysets = + ImmutableList.of( + importedAnnotations.values(), + packageAnnotations.keySet(), + typeAnnotations.keySet(), + methodRecords.keySet()); + for (Collection keyset : keysets) { + for (String key : keyset) { + assert !encodingDictionary.containsKey(key); + strings.add(key); + encodingDictionary.put(key, numStringEntries); + ++numStringEntries; + } + } + out.writeInt(numStringEntries); + // Followed by the entries themselves + for (String s : strings) { + out.writeUTF(s); + } + // Followed by the number of encoded package annotation records + int packageAnnotationSize = 0; + for (Map.Entry> entry : packageAnnotations.entrySet()) { + packageAnnotationSize += entry.getValue().size(); + } + out.writeInt(packageAnnotationSize); + // Followed by those records as pairs of ints pointing into the dictionary + for (Map.Entry> entry : packageAnnotations.entrySet()) { + for (String annot : entry.getValue()) { + out.writeInt(encodingDictionary.get(entry.getKey())); + out.writeInt(encodingDictionary.get(importedAnnotations.get(annot))); + } + } + // Followed by the number of encoded type annotation records + int typeAnnotationSize = 0; + for (Map.Entry> entry : typeAnnotations.entrySet()) { + typeAnnotationSize += entry.getValue().size(); + } + out.writeInt(typeAnnotationSize); + // Followed by those records as pairs of ints pointing into the dictionary + for (Map.Entry> entry : typeAnnotations.entrySet()) { + for (String annot : entry.getValue()) { + out.writeInt(encodingDictionary.get(entry.getKey())); + out.writeInt(encodingDictionary.get(importedAnnotations.get(annot))); + } + } + // Followed by the number of encoded method return/declaration annotation records + int methodAnnotationSize = 0; + int methodArgumentRecordsSize = 0; + for (Map.Entry entry : methodRecords.entrySet()) { + methodAnnotationSize += entry.getValue().getMethodAnnotations().size(); + methodArgumentRecordsSize += entry.getValue().getArgumentAnnotations().size(); + } + out.writeInt(methodAnnotationSize); + // Followed by those records as pairs of ints pointing into the dictionary + for (Map.Entry entry : methodRecords.entrySet()) { + for (String annot : entry.getValue().getMethodAnnotations()) { + out.writeInt(encodingDictionary.get(entry.getKey())); + out.writeInt(encodingDictionary.get(importedAnnotations.get(annot))); + } + } + // Followed by the number of encoded method argument annotation records + out.writeInt(methodArgumentRecordsSize); + // Followed by those records as a triplet of ints ( 0 and 2 point in the dictionary, 1 is the + // argument position) + for (Map.Entry entry : methodRecords.entrySet()) { + for (Map.Entry> argEntry : + entry.getValue().getArgumentAnnotations().entrySet()) { + for (String annot : argEntry.getValue()) { + out.writeInt(encodingDictionary.get(entry.getKey())); + out.writeInt(argEntry.getKey()); + out.writeInt(encodingDictionary.get(importedAnnotations.get(annot))); + } + } + } + } +} From e5353d131c51283e823198e929ef1a7ee4c8b490 Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Fri, 9 Feb 2024 09:15:00 -0800 Subject: [PATCH 03/50] changing method returns to nullable --- .../java/com/uber/nullaway/LibModelInfoExtractor.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/nullaway/src/main/java/com/uber/nullaway/LibModelInfoExtractor.java b/nullaway/src/main/java/com/uber/nullaway/LibModelInfoExtractor.java index 6d1e058d17..32f691c009 100644 --- a/nullaway/src/main/java/com/uber/nullaway/LibModelInfoExtractor.java +++ b/nullaway/src/main/java/com/uber/nullaway/LibModelInfoExtractor.java @@ -167,7 +167,6 @@ public Result process(Path localPath, Path absolutePath, ParseResult> methodAnnotationsMap = new HashMap<>(); - Map nonNullReturnMethods = new HashMap<>(); + Map nullableReturnMethods = new HashMap<>(); boolean isNullableAnnotationPresent = false; if (md.getAnnotations().isNonEmpty()) { methodAnnotationsMap.put(methodName, md.getAnnotations()); @@ -263,13 +262,13 @@ public MethodDeclaration visit(MethodDeclaration md, Void arg) { break; } } - if (!isNullableAnnotationPresent) { - nonNullReturnMethods.put(packageName + "." + parentClassName, methodSignature); + if (isNullableAnnotationPresent) { + nullableReturnMethods.put(packageName + "." + parentClassName, methodSignature); methodRecords.put( packageName + "." + parentClassName + ":" + methodName, - new MethodAnnotationsRecord(ImmutableSet.of("Nonnull"), ImmutableMap.of())); + new MethodAnnotationsRecord(ImmutableSet.of("Nullable"), ImmutableMap.of())); } - nonNullReturnMethods.forEach( + nullableReturnMethods.forEach( (c, s) -> System.out.println("Enclosing Class: " + c + "\tMethod Signature: " + s)); return md; } From ea7d117ad5cc5f92d696a37b04466e36c1b22034 Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Tue, 13 Feb 2024 17:03:35 -0800 Subject: [PATCH 04/50] Adding safe cast for EnumDeclaration as parent for method --- .../com/uber/nullaway/LibModelInfoExtractor.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/nullaway/src/main/java/com/uber/nullaway/LibModelInfoExtractor.java b/nullaway/src/main/java/com/uber/nullaway/LibModelInfoExtractor.java index 32f691c009..888d2ada23 100644 --- a/nullaway/src/main/java/com/uber/nullaway/LibModelInfoExtractor.java +++ b/nullaway/src/main/java/com/uber/nullaway/LibModelInfoExtractor.java @@ -243,10 +243,14 @@ public ConstructorDeclaration visit(ConstructorDeclaration cd, Void arg) { public MethodDeclaration visit(MethodDeclaration md, Void arg) { String methodName = md.getNameAsString(); Optional parentClassNode = md.getParentNode(); - String parentClassName = - parentClassNode.isPresent() - ? ((ClassOrInterfaceDeclaration) parentClassNode.get()).getNameAsString() - : ""; + String parentClassName = ""; + if (parentClassNode.isPresent()) { + if (parentClassNode.get() instanceof ClassOrInterfaceDeclaration) { + parentClassName = ((ClassOrInterfaceDeclaration) parentClassNode.get()).getNameAsString(); + } else if (parentClassNode.get() instanceof EnumDeclaration) { + parentClassName = ((EnumDeclaration) parentClassNode.get()).getNameAsString(); + } + } String methodSignature = md.getSignature().toString(); Map> methodAnnotationsMap = new HashMap<>(); Map nullableReturnMethods = new HashMap<>(); From e9a06dc26abc4573e728b00fb3282be67fe73c15 Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Sat, 17 Feb 2024 18:49:37 -0800 Subject: [PATCH 05/50] updating logic for method declaration --- .../uber/nullaway/LibModelInfoExtractor.java | 53 +++++++++++-------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/nullaway/src/main/java/com/uber/nullaway/LibModelInfoExtractor.java b/nullaway/src/main/java/com/uber/nullaway/LibModelInfoExtractor.java index 888d2ada23..6b69da6f08 100644 --- a/nullaway/src/main/java/com/uber/nullaway/LibModelInfoExtractor.java +++ b/nullaway/src/main/java/com/uber/nullaway/LibModelInfoExtractor.java @@ -36,6 +36,7 @@ import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.expr.AnnotationExpr; import com.github.javaparser.ast.expr.NormalAnnotationExpr; +import com.github.javaparser.ast.type.ArrayType; import com.github.javaparser.ast.type.ClassOrInterfaceType; import com.github.javaparser.ast.type.TypeParameter; import com.github.javaparser.ast.visitor.ModifierVisitor; @@ -47,9 +48,8 @@ import com.google.common.collect.ImmutableSet; import java.io.DataOutputStream; import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.FileTime; @@ -60,7 +60,6 @@ import java.util.Map; import java.util.Optional; import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; public class LibModelInfoExtractor { @@ -100,28 +99,27 @@ public static void writeToAstubx(String outputPath) { .put("Nonnull", "javax.annotation.Nonnull") .put("Nullable", "javax.annotation.Nullable") .build(); - ZipOutputStream zos; + // ZipOutputStream zos; + DataOutputStream dos; try { - zos = new ZipOutputStream(new FileOutputStream(outputPath)); - } catch (FileNotFoundException e) { + // zos = new ZipOutputStream(new FileOutputStream(outputPath)); + dos = new DataOutputStream(Files.newOutputStream(Paths.get(outputPath))); + } catch (IOException e) { throw new RuntimeException(e); } if (!methodRecords.isEmpty()) { - ZipEntry entry = new ZipEntry("META-INF/nullaway/libmodels.astubx"); + ZipEntry entry = new ZipEntry("/Users/abhijitkulkarni/libmodels.astubx"); // Set the modification/creation time to 0 to ensure that this jars always have the same // checksum entry.setTime(0); entry.setCreationTime(FileTime.fromMillis(0)); try { - zos.putNextEntry(entry); + // zos.putNextEntry(entry); StubxFileWriter.write( - new DataOutputStream(zos), - importedAnnotations, - new HashMap<>(), - new HashMap<>(), - methodRecords); - zos.closeEntry(); - zos.close(); + dos, importedAnnotations, new HashMap<>(), new HashMap<>(), methodRecords); + // zos.closeEntry(); + // zos.close(); + dos.close(); } catch (IOException e) { throw new RuntimeException(e); } @@ -193,14 +191,16 @@ private static class CompilationUnitVisitor extends ModifierVisitor { @Override public PackageDeclaration visit(PackageDeclaration n, Void arg) { this.packageName = n.getNameAsString(); - System.out.println("Package name: " + packageName); + // System.out.println("Package name: " + packageName); return n; } @Override public ClassOrInterfaceDeclaration visit(ClassOrInterfaceDeclaration cid, Void arg) { String classOrInterfaceName = packageName + "." + cid.getNameAsString(); + @SuppressWarnings("all") Map nullableTypeBoundsMap = new HashMap<>(); + @SuppressWarnings("all") Map> classOrInterfaceAnnotationsMap = new HashMap<>(); List paramList = cid.getTypeParameters(); // Finding Nullable upper bounds for generic type parameters. @@ -221,11 +221,11 @@ public ClassOrInterfaceDeclaration visit(ClassOrInterfaceDeclaration cid, Void a if (cid.getAnnotations().isNonEmpty()) { classOrInterfaceAnnotationsMap.put(classOrInterfaceName, cid.getAnnotations()); } - System.out.println("Fully qualified class name: " + classOrInterfaceName); + /*System.out.println("Fully qualified class name: " + classOrInterfaceName); nullableTypeBoundsMap.forEach( (p, a) -> System.out.println("Nullable Index: " + p + "\tClass: " + a)); classOrInterfaceAnnotationsMap.forEach( - (c, a) -> System.out.println("Class: " + c + "\tAnnotations: " + a)); + (c, a) -> System.out.println("Class: " + c + "\tAnnotations: " + a));*/ return cid; } @@ -252,24 +252,33 @@ public MethodDeclaration visit(MethodDeclaration md, Void arg) { } } String methodSignature = md.getSignature().toString(); + @SuppressWarnings("all") Map> methodAnnotationsMap = new HashMap<>(); Map nullableReturnMethods = new HashMap<>(); boolean isNullableAnnotationPresent = false; if (md.getAnnotations().isNonEmpty()) { methodAnnotationsMap.put(methodName, md.getAnnotations()); } - methodAnnotationsMap.forEach( - (m, a) -> System.out.println("Method: " + m + "\tAnnotations: " + a)); + /*methodAnnotationsMap.forEach( + (m, a) -> System.out.println("Method: " + m + "\tAnnotations: " + a));*/ for (AnnotationExpr annotation : md.getAnnotations()) { - if (annotation.getNameAsString().equalsIgnoreCase("@Nullable")) { + if (annotation.getNameAsString().equalsIgnoreCase("Nullable")) { isNullableAnnotationPresent = true; break; } } + String methodReturnType = ""; + if (md.getType() instanceof ClassOrInterfaceType) { + methodReturnType = md.getType().getChildNodes().get(0).toString(); + } else if (md.getType() instanceof ArrayType) { + methodReturnType = "Array"; + } else { + methodReturnType = md.getType().toString(); + } if (isNullableAnnotationPresent) { nullableReturnMethods.put(packageName + "." + parentClassName, methodSignature); methodRecords.put( - packageName + "." + parentClassName + ":" + methodName, + packageName + "." + parentClassName + ":" + methodReturnType + " " + methodSignature, new MethodAnnotationsRecord(ImmutableSet.of("Nullable"), ImmutableMap.of())); } nullableReturnMethods.forEach( From 19b020b80d52eb09eab177d6d611983add9fd2b4 Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Tue, 20 Feb 2024 19:05:23 -0800 Subject: [PATCH 06/50] moving lib model to separate module --- lib-model-consume/build.gradle | 33 +++++++++++++ .../uber/nullaway/LibModelInfoExtractor.java | 0 .../nullaway/MethodAnnotationsRecord.java | 0 .../com/uber/nullaway/StubxFileWriter.java | 0 .../uber/nullaway/provider/TestProvider.java | 14 ++++++ .../ConsumeLibModelIntegrationTest.java | 49 +++++++++++++++++++ nullaway/build.gradle | 1 - settings.gradle | 1 + 8 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 lib-model-consume/build.gradle rename {nullaway => lib-model-consume}/src/main/java/com/uber/nullaway/LibModelInfoExtractor.java (100%) rename {nullaway => lib-model-consume}/src/main/java/com/uber/nullaway/MethodAnnotationsRecord.java (100%) rename {nullaway => lib-model-consume}/src/main/java/com/uber/nullaway/StubxFileWriter.java (100%) create mode 100644 lib-model-consume/src/main/java/com/uber/nullaway/provider/TestProvider.java create mode 100644 lib-model-consume/src/test/java/com/uber/nullaway/libmodel/ConsumeLibModelIntegrationTest.java diff --git a/lib-model-consume/build.gradle b/lib-model-consume/build.gradle new file mode 100644 index 0000000000..7438626396 --- /dev/null +++ b/lib-model-consume/build.gradle @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2017. Uber Technologies + * + * 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. + */ +plugins { + id "java-library" + id "nullaway.java-test-conventions" +} + +dependencies { + testImplementation deps.test.junit4 + testImplementation(deps.build.errorProneTestHelpers) { + exclude group: "junit", module: "junit" + } + testImplementation project(":nullaway") + testImplementation project(":lib-model-consume") + implementation deps.build.guava + implementation deps.build.javaparser + compileOnly deps.apt.autoService + annotationProcessor deps.apt.autoService + compileOnly project(":nullaway") +} diff --git a/nullaway/src/main/java/com/uber/nullaway/LibModelInfoExtractor.java b/lib-model-consume/src/main/java/com/uber/nullaway/LibModelInfoExtractor.java similarity index 100% rename from nullaway/src/main/java/com/uber/nullaway/LibModelInfoExtractor.java rename to lib-model-consume/src/main/java/com/uber/nullaway/LibModelInfoExtractor.java diff --git a/nullaway/src/main/java/com/uber/nullaway/MethodAnnotationsRecord.java b/lib-model-consume/src/main/java/com/uber/nullaway/MethodAnnotationsRecord.java similarity index 100% rename from nullaway/src/main/java/com/uber/nullaway/MethodAnnotationsRecord.java rename to lib-model-consume/src/main/java/com/uber/nullaway/MethodAnnotationsRecord.java diff --git a/nullaway/src/main/java/com/uber/nullaway/StubxFileWriter.java b/lib-model-consume/src/main/java/com/uber/nullaway/StubxFileWriter.java similarity index 100% rename from nullaway/src/main/java/com/uber/nullaway/StubxFileWriter.java rename to lib-model-consume/src/main/java/com/uber/nullaway/StubxFileWriter.java diff --git a/lib-model-consume/src/main/java/com/uber/nullaway/provider/TestProvider.java b/lib-model-consume/src/main/java/com/uber/nullaway/provider/TestProvider.java new file mode 100644 index 0000000000..f1ed9059ed --- /dev/null +++ b/lib-model-consume/src/main/java/com/uber/nullaway/provider/TestProvider.java @@ -0,0 +1,14 @@ +package com.uber.nullaway.provider; + +import com.google.auto.service.AutoService; +import com.uber.nullaway.jarinfer.JarInferStubxProvider; +import java.util.Collections; +import java.util.List; + +@AutoService(JarInferStubxProvider.class) +public class TestProvider implements JarInferStubxProvider { + @Override + public List pathsToStubxFiles() { + return Collections.singletonList("libmodels.astubx"); + } +} diff --git a/lib-model-consume/src/test/java/com/uber/nullaway/libmodel/ConsumeLibModelIntegrationTest.java b/lib-model-consume/src/test/java/com/uber/nullaway/libmodel/ConsumeLibModelIntegrationTest.java new file mode 100644 index 0000000000..7323d4f7ad --- /dev/null +++ b/lib-model-consume/src/test/java/com/uber/nullaway/libmodel/ConsumeLibModelIntegrationTest.java @@ -0,0 +1,49 @@ +package com.uber.nullaway.libmodel; + +import com.google.errorprone.CompilationTestHelper; +import com.uber.nullaway.NullAway; +import java.util.Arrays; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class ConsumeLibModelIntegrationTest { + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private CompilationTestHelper compilationHelper; + + @Before + public void setup() { + compilationHelper = CompilationTestHelper.newInstance(NullAway.class, getClass()); + } + + @Test + public void jarinferNullableReturnsTestForLibModel() { + compilationHelper + .setArgs( + Arrays.asList( + "-d", + temporaryFolder.getRoot().getAbsolutePath(), + "-XepOpt:NullAway:AnnotatedPackages=com.uber", + "-XepOpt:NullAway:JarInferEnabled=true", + "-XepOpt:NullAway:JarInferUseReturnAnnotations=true")) + .addSourceLines( + "Test.java", + "package com.uber;", + "import javax.annotation.Nullable;", + "import java.util.Vector;", + "class Test {", + " static Vector vector = new Vector<>();", + " static void test(Object[] value){", + " }", + " static void test1() {", + " vector.add(1);", + " // BUG: Diagnostic contains: passing @Nullable parameter", + " test(vector.toArray());", + " }", + "}") + .doTest(); + } +} diff --git a/nullaway/build.gradle b/nullaway/build.gradle index a9e5ee6a93..dd994920b2 100644 --- a/nullaway/build.gradle +++ b/nullaway/build.gradle @@ -39,7 +39,6 @@ dependencies { compileOnly deps.build.errorProneCheckApi implementation deps.build.checkerDataflow implementation deps.build.guava - implementation deps.build.javaparser testImplementation project(":annotations") testImplementation deps.test.junit4 testImplementation(deps.build.errorProneTestHelpers) { diff --git a/settings.gradle b/settings.gradle index bfafe10aff..0440952f6a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -27,3 +27,4 @@ include ':jdk-recent-unit-tests' include ':code-coverage-report' include ':sample-app' include ':jar-infer:test-android-lib-jarinfer' +include ':lib-model-consume' From 31f569cba30fceac8177c36c0953ab364b488a5e Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Tue, 20 Feb 2024 21:38:00 -0800 Subject: [PATCH 07/50] moving code --- .../com/uber/nullaway/{ => libmodel}/LibModelInfoExtractor.java | 2 +- .../uber/nullaway/{ => libmodel}/MethodAnnotationsRecord.java | 2 +- .../java/com/uber/nullaway/{ => libmodel}/StubxFileWriter.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename lib-model-consume/src/main/java/com/uber/nullaway/{ => libmodel}/LibModelInfoExtractor.java (99%) rename lib-model-consume/src/main/java/com/uber/nullaway/{ => libmodel}/MethodAnnotationsRecord.java (95%) rename lib-model-consume/src/main/java/com/uber/nullaway/{ => libmodel}/StubxFileWriter.java (99%) diff --git a/lib-model-consume/src/main/java/com/uber/nullaway/LibModelInfoExtractor.java b/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java similarity index 99% rename from lib-model-consume/src/main/java/com/uber/nullaway/LibModelInfoExtractor.java rename to lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java index 6b69da6f08..1313327a78 100644 --- a/lib-model-consume/src/main/java/com/uber/nullaway/LibModelInfoExtractor.java +++ b/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java @@ -20,7 +20,7 @@ * THE SOFTWARE. */ -package com.uber.nullaway; +package com.uber.nullaway.libmodel; import com.github.javaparser.ParseResult; import com.github.javaparser.ParserConfiguration.LanguageLevel; diff --git a/lib-model-consume/src/main/java/com/uber/nullaway/MethodAnnotationsRecord.java b/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/MethodAnnotationsRecord.java similarity index 95% rename from lib-model-consume/src/main/java/com/uber/nullaway/MethodAnnotationsRecord.java rename to lib-model-consume/src/main/java/com/uber/nullaway/libmodel/MethodAnnotationsRecord.java index 199fd89925..3923a81754 100644 --- a/lib-model-consume/src/main/java/com/uber/nullaway/MethodAnnotationsRecord.java +++ b/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/MethodAnnotationsRecord.java @@ -1,4 +1,4 @@ -package com.uber.nullaway; +package com.uber.nullaway.libmodel; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; diff --git a/lib-model-consume/src/main/java/com/uber/nullaway/StubxFileWriter.java b/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/StubxFileWriter.java similarity index 99% rename from lib-model-consume/src/main/java/com/uber/nullaway/StubxFileWriter.java rename to lib-model-consume/src/main/java/com/uber/nullaway/libmodel/StubxFileWriter.java index 682b0ce89e..0d7f4f3441 100644 --- a/lib-model-consume/src/main/java/com/uber/nullaway/StubxFileWriter.java +++ b/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/StubxFileWriter.java @@ -1,4 +1,4 @@ -package com.uber.nullaway; +package com.uber.nullaway.libmodel; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; From a26f856e324ca4a92831db0df45d3c4096909f14 Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Sat, 24 Feb 2024 15:08:48 -0800 Subject: [PATCH 08/50] infrastructure code for lib model --- build.gradle | 2 +- .../lib-model-consume}/build.gradle | 11 +++- .../libmodel/LibModelInfoExtractor.java | 12 ++-- .../libmodel/MethodAnnotationsRecord.java | 0 .../nullaway/libmodel/StubxFileWriter.java | 0 .../ConsumeLibModelIntegrationTest.java | 0 lib-model/lib-model-test/build.gradle | 58 +++++++++++++++++++ .../libmodel}/provider/TestProvider.java | 2 +- settings.gradle | 3 +- 9 files changed, 78 insertions(+), 10 deletions(-) rename {lib-model-consume => lib-model/lib-model-consume}/build.gradle (79%) rename {lib-model-consume => lib-model/lib-model-consume}/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java (97%) rename {lib-model-consume => lib-model/lib-model-consume}/src/main/java/com/uber/nullaway/libmodel/MethodAnnotationsRecord.java (100%) rename {lib-model-consume => lib-model/lib-model-consume}/src/main/java/com/uber/nullaway/libmodel/StubxFileWriter.java (100%) rename {lib-model-consume => lib-model/lib-model-consume}/src/test/java/com/uber/nullaway/libmodel/ConsumeLibModelIntegrationTest.java (100%) create mode 100644 lib-model/lib-model-test/build.gradle rename {lib-model-consume/src/main/java/com/uber/nullaway => lib-model/lib-model-test/src/main/java/com/uber/nullaway/libmodel}/provider/TestProvider.java (89%) diff --git a/build.gradle b/build.gradle index 5ccd8b0ed6..0c715a2a09 100644 --- a/build.gradle +++ b/build.gradle @@ -97,7 +97,7 @@ subprojects { project -> // For some reason, spotless complains when applied to the jar-infer folder itself, even // though there is no top-level :jar-infer project - if (project.name != "jar-infer") { + if (project.name != "jar-infer"&&project.name != "lib-model") { project.apply plugin: "com.diffplug.spotless" spotless { java { diff --git a/lib-model-consume/build.gradle b/lib-model/lib-model-consume/build.gradle similarity index 79% rename from lib-model-consume/build.gradle rename to lib-model/lib-model-consume/build.gradle index 7438626396..c6bc61495b 100644 --- a/lib-model-consume/build.gradle +++ b/lib-model/lib-model-consume/build.gradle @@ -18,13 +18,22 @@ plugins { id "nullaway.java-test-conventions" } +jar{ + manifest { + attributes('Main-Class':'com.uber.nullaway.libmodel.LibModelInfoExtractor') + } + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } +} + dependencies { testImplementation deps.test.junit4 testImplementation(deps.build.errorProneTestHelpers) { exclude group: "junit", module: "junit" } testImplementation project(":nullaway") - testImplementation project(":lib-model-consume") + testImplementation project(":lib-model:lib-model-test") implementation deps.build.guava implementation deps.build.javaparser compileOnly deps.apt.autoService diff --git a/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java b/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java similarity index 97% rename from lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java rename to lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java index 1313327a78..90d02e8e1c 100644 --- a/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java +++ b/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java @@ -99,26 +99,26 @@ public static void writeToAstubx(String outputPath) { .put("Nonnull", "javax.annotation.Nonnull") .put("Nullable", "javax.annotation.Nullable") .build(); - // ZipOutputStream zos; + // ZipOutputStream zos; DataOutputStream dos; try { - // zos = new ZipOutputStream(new FileOutputStream(outputPath)); + // zos = new ZipOutputStream(Files.newOutputStream(Paths.get(outputPath))); dos = new DataOutputStream(Files.newOutputStream(Paths.get(outputPath))); } catch (IOException e) { throw new RuntimeException(e); } if (!methodRecords.isEmpty()) { - ZipEntry entry = new ZipEntry("/Users/abhijitkulkarni/libmodels.astubx"); + ZipEntry entry = new ZipEntry("META-INF/nullaway/libmodels.astubx"); // Set the modification/creation time to 0 to ensure that this jars always have the same // checksum entry.setTime(0); entry.setCreationTime(FileTime.fromMillis(0)); try { - // zos.putNextEntry(entry); + // zos.putNextEntry(entry); StubxFileWriter.write( dos, importedAnnotations, new HashMap<>(), new HashMap<>(), methodRecords); - // zos.closeEntry(); - // zos.close(); + // zos.closeEntry(); + // zos.close(); dos.close(); } catch (IOException e) { throw new RuntimeException(e); diff --git a/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/MethodAnnotationsRecord.java b/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/MethodAnnotationsRecord.java similarity index 100% rename from lib-model-consume/src/main/java/com/uber/nullaway/libmodel/MethodAnnotationsRecord.java rename to lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/MethodAnnotationsRecord.java diff --git a/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/StubxFileWriter.java b/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/StubxFileWriter.java similarity index 100% rename from lib-model-consume/src/main/java/com/uber/nullaway/libmodel/StubxFileWriter.java rename to lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/StubxFileWriter.java diff --git a/lib-model-consume/src/test/java/com/uber/nullaway/libmodel/ConsumeLibModelIntegrationTest.java b/lib-model/lib-model-consume/src/test/java/com/uber/nullaway/libmodel/ConsumeLibModelIntegrationTest.java similarity index 100% rename from lib-model-consume/src/test/java/com/uber/nullaway/libmodel/ConsumeLibModelIntegrationTest.java rename to lib-model/lib-model-consume/src/test/java/com/uber/nullaway/libmodel/ConsumeLibModelIntegrationTest.java diff --git a/lib-model/lib-model-test/build.gradle b/lib-model/lib-model-test/build.gradle new file mode 100644 index 0000000000..4e8a907137 --- /dev/null +++ b/lib-model/lib-model-test/build.gradle @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2017. Uber Technologies + * + * 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. + */ + +plugins { + id "java-library" +} + +evaluationDependsOn(":lib-model:lib-model-consume") + +def jdkPath = "/Users/abhijitkulkarni/jspecify_jdk/jdk/src" +def astubxPath = "com/uber/nullaway/libmodel/provider/libmodels.astubx" + +jar { + manifest { + attributes( + 'Created-By' : "Gradle ${gradle.gradleVersion}", + 'Build-Jdk' : "${System.properties['java.version']} (${System.properties['java.vendor']} ${System.properties['java.vm.version']})", + 'Build-OS' : "${System.properties['os.name']} ${System.properties['os.arch']} ${System.properties['os.version']}" + ) + } +} + +jar.doLast { + javaexec { + classpath = files("${rootProject.projectDir}/lib-model/lib-model-consume/build/libs/lib-model-consume.jar") + main = "com.uber.nullaway.libmodel.LibModelInfoExtractor" + args = [ + jdkPath, + "${jar.destinationDirectory.get()}/${astubxPath}" + ] + } + exec { + workingDir "./build/libs" + commandLine "jar", "uf", "lib-model-test.jar", astubxPath + } +} + +dependencies { + compileOnly deps.apt.autoService + annotationProcessor deps.apt.autoService + compileOnly project(":nullaway") + implementation deps.build.jsr305Annotations +} + +jar.dependsOn ":lib-model:lib-model-consume:assemble" diff --git a/lib-model-consume/src/main/java/com/uber/nullaway/provider/TestProvider.java b/lib-model/lib-model-test/src/main/java/com/uber/nullaway/libmodel/provider/TestProvider.java similarity index 89% rename from lib-model-consume/src/main/java/com/uber/nullaway/provider/TestProvider.java rename to lib-model/lib-model-test/src/main/java/com/uber/nullaway/libmodel/provider/TestProvider.java index f1ed9059ed..9b4c5e7e32 100644 --- a/lib-model-consume/src/main/java/com/uber/nullaway/provider/TestProvider.java +++ b/lib-model/lib-model-test/src/main/java/com/uber/nullaway/libmodel/provider/TestProvider.java @@ -1,4 +1,4 @@ -package com.uber.nullaway.provider; +package com.uber.nullaway.libmodel.provider; import com.google.auto.service.AutoService; import com.uber.nullaway.jarinfer.JarInferStubxProvider; diff --git a/settings.gradle b/settings.gradle index 0440952f6a..9251e83003 100644 --- a/settings.gradle +++ b/settings.gradle @@ -27,4 +27,5 @@ include ':jdk-recent-unit-tests' include ':code-coverage-report' include ':sample-app' include ':jar-infer:test-android-lib-jarinfer' -include ':lib-model-consume' +include ':lib-model:lib-model-consume' +include ':lib-model:lib-model-test' From ee56362545b42b81a1ae3608a5d12983b6558960 Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Sun, 25 Feb 2024 18:22:26 -0800 Subject: [PATCH 09/50] adding sample_jdk source files in resources --- ...Test.java => LibModelIntegrationTest.java} | 2 +- lib-model/lib-model-test/build.gradle | 2 +- .../share/classes/java/util/Vector.java | 1492 +++++++++++++++++ .../classes/java/util/function/Function.java | 103 ++ 4 files changed, 1597 insertions(+), 2 deletions(-) rename lib-model/lib-model-consume/src/test/java/com/uber/nullaway/libmodel/{ConsumeLibModelIntegrationTest.java => LibModelIntegrationTest.java} (97%) create mode 100644 lib-model/lib-model-test/src/main/resources/sample_jspecify_jdk/src/java.base/share/classes/java/util/Vector.java create mode 100644 lib-model/lib-model-test/src/main/resources/sample_jspecify_jdk/src/java.base/share/classes/java/util/function/Function.java diff --git a/lib-model/lib-model-consume/src/test/java/com/uber/nullaway/libmodel/ConsumeLibModelIntegrationTest.java b/lib-model/lib-model-consume/src/test/java/com/uber/nullaway/libmodel/LibModelIntegrationTest.java similarity index 97% rename from lib-model/lib-model-consume/src/test/java/com/uber/nullaway/libmodel/ConsumeLibModelIntegrationTest.java rename to lib-model/lib-model-consume/src/test/java/com/uber/nullaway/libmodel/LibModelIntegrationTest.java index 7323d4f7ad..94b657dbeb 100644 --- a/lib-model/lib-model-consume/src/test/java/com/uber/nullaway/libmodel/ConsumeLibModelIntegrationTest.java +++ b/lib-model/lib-model-consume/src/test/java/com/uber/nullaway/libmodel/LibModelIntegrationTest.java @@ -8,7 +8,7 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; -public class ConsumeLibModelIntegrationTest { +public class LibModelIntegrationTest { @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); diff --git a/lib-model/lib-model-test/build.gradle b/lib-model/lib-model-test/build.gradle index 4e8a907137..3ead03b11c 100644 --- a/lib-model/lib-model-test/build.gradle +++ b/lib-model/lib-model-test/build.gradle @@ -20,7 +20,7 @@ plugins { evaluationDependsOn(":lib-model:lib-model-consume") -def jdkPath = "/Users/abhijitkulkarni/jspecify_jdk/jdk/src" +def jdkPath = "${rootProject.projectDir}/lib-model/lib-model-test/src/main/resources/sample_jspecify_jdk/src" def astubxPath = "com/uber/nullaway/libmodel/provider/libmodels.astubx" jar { diff --git a/lib-model/lib-model-test/src/main/resources/sample_jspecify_jdk/src/java.base/share/classes/java/util/Vector.java b/lib-model/lib-model-test/src/main/resources/sample_jspecify_jdk/src/java.base/share/classes/java/util/Vector.java new file mode 100644 index 0000000000..3e1e5bf34e --- /dev/null +++ b/lib-model/lib-model-test/src/main/resources/sample_jspecify_jdk/src/java.base/share/classes/java/util/Vector.java @@ -0,0 +1,1492 @@ +/* + * Copyright (c) 1994, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package java.util; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.StreamCorruptedException; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; + +/** + * The {@code Vector} class implements a growable array of + * objects. Like an array, it contains components that can be + * accessed using an integer index. However, the size of a + * {@code Vector} can grow or shrink as needed to accommodate + * adding and removing items after the {@code Vector} has been created. + * + *

Each vector tries to optimize storage management by maintaining a + * {@code capacity} and a {@code capacityIncrement}. The + * {@code capacity} is always at least as large as the vector + * size; it is usually larger because as components are added to the + * vector, the vector's storage increases in chunks the size of + * {@code capacityIncrement}. An application can increase the + * capacity of a vector before inserting a large number of + * components; this reduces the amount of incremental reallocation. + * + *

+ * The iterators returned by this class's {@link #iterator() iterator} and + * {@link #listIterator(int) listIterator} methods are fail-fast: + * if the vector is structurally modified at any time after the iterator is + * created, in any way except through the iterator's own + * {@link ListIterator#remove() remove} or + * {@link ListIterator#add(Object) add} methods, the iterator will throw a + * {@link ConcurrentModificationException}. Thus, in the face of + * concurrent modification, the iterator fails quickly and cleanly, rather + * than risking arbitrary, non-deterministic behavior at an undetermined + * time in the future. The {@link Enumeration Enumerations} returned by + * the {@link #elements() elements} method are not fail-fast; if the + * Vector is structurally modified at any time after the enumeration is + * created then the results of enumerating are undefined. + * + *

Note that the fail-fast behavior of an iterator cannot be guaranteed + * as it is, generally speaking, impossible to make any hard guarantees in the + * presence of unsynchronized concurrent modification. Fail-fast iterators + * throw {@code ConcurrentModificationException} on a best-effort basis. + * Therefore, it would be wrong to write a program that depended on this + * exception for its correctness: the fail-fast behavior of iterators + * should be used only to detect bugs. + * + *

As of the Java 2 platform v1.2, this class was retrofitted to + * implement the {@link List} interface, making it a member of the + * + * Java Collections Framework. Unlike the new collection + * implementations, {@code Vector} is synchronized. If a thread-safe + * implementation is not needed, it is recommended to use {@link + * ArrayList} in place of {@code Vector}. + * + * @param Type of component elements + * + * @author Lee Boynton + * @author Jonathan Payne + * @see Collection + * @see LinkedList + * @since 1.0 + */ +@NullMarked +public class Vector extends AbstractList implements List, RandomAccess, Cloneable, java.io.Serializable { + + /** + * The array buffer into which the components of the vector are + * stored. The capacity of the vector is the length of this array buffer, + * and is at least large enough to contain all the vector's elements. + * + *

Any array elements following the last element in the Vector are null. + * + * @serial + */ + protected Object[] elementData; + + /** + * The number of valid components in this {@code Vector} object. + * Components {@code elementData[0]} through + * {@code elementData[elementCount-1]} are the actual items. + * + * @serial + */ + protected int elementCount; + + /** + * The amount by which the capacity of the vector is automatically + * incremented when its size becomes greater than its capacity. If + * the capacity increment is less than or equal to zero, the capacity + * of the vector is doubled each time it needs to grow. + * + * @serial + */ + protected int capacityIncrement; + + /** + * use serialVersionUID from JDK 1.0.2 for interoperability + */ + private static final long serialVersionUID = -2767605614048989439L; + + /** + * Constructs an empty vector with the specified initial capacity and + * capacity increment. + * + * @param initialCapacity the initial capacity of the vector + * @param capacityIncrement the amount by which the capacity is + * increased when the vector overflows + * @throws IllegalArgumentException if the specified initial capacity + * is negative + */ + public Vector(int initialCapacity, int capacityIncrement) { + super(); + if (initialCapacity < 0) + throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity); + this.elementData = new Object[initialCapacity]; + this.capacityIncrement = capacityIncrement; + } + + /** + * Constructs an empty vector with the specified initial capacity and + * with its capacity increment equal to zero. + * + * @param initialCapacity the initial capacity of the vector + * @throws IllegalArgumentException if the specified initial capacity + * is negative + */ + public Vector(int initialCapacity) { + this(initialCapacity, 0); + } + + /** + * Constructs an empty vector so that its internal data array + * has size {@code 10} and its standard capacity increment is + * zero. + */ + public Vector() { + this(10); + } + + /** + * Constructs a vector containing the elements of the specified + * collection, in the order they are returned by the collection's + * iterator. + * + * @param c the collection whose elements are to be placed into this + * vector + * @throws NullPointerException if the specified collection is null + * @since 1.2 + */ + public Vector(Collection c) { + elementData = c.toArray(); + elementCount = elementData.length; + // defend against c.toArray (incorrectly) not returning Object[] + // (see e.g. https://bugs.openjdk.java.net/browse/JDK-6260652) + if (elementData.getClass() != Object[].class) + elementData = Arrays.copyOf(elementData, elementCount, Object[].class); + } + + /** + * Copies the components of this vector into the specified array. + * The item at index {@code k} in this vector is copied into + * component {@code k} of {@code anArray}. + * + * @param anArray the array into which the components get copied + * @throws NullPointerException if the given array is null + * @throws IndexOutOfBoundsException if the specified array is not + * large enough to hold all the components of this vector + * @throws ArrayStoreException if a component of this vector is not of + * a runtime type that can be stored in the specified array + * @see #toArray(Object[]) + */ + public synchronized void copyInto(@Nullable Object[] anArray) { + System.arraycopy(elementData, 0, anArray, 0, elementCount); + } + + /** + * Trims the capacity of this vector to be the vector's current + * size. If the capacity of this vector is larger than its current + * size, then the capacity is changed to equal the size by replacing + * its internal data array, kept in the field {@code elementData}, + * with a smaller one. An application can use this operation to + * minimize the storage of a vector. + */ + public synchronized void trimToSize() { + modCount++; + int oldCapacity = elementData.length; + if (elementCount < oldCapacity) { + elementData = Arrays.copyOf(elementData, elementCount); + } + } + + /** + * Increases the capacity of this vector, if necessary, to ensure + * that it can hold at least the number of components specified by + * the minimum capacity argument. + * + *

If the current capacity of this vector is less than + * {@code minCapacity}, then its capacity is increased by replacing its + * internal data array, kept in the field {@code elementData}, with a + * larger one. The size of the new data array will be the old size plus + * {@code capacityIncrement}, unless the value of + * {@code capacityIncrement} is less than or equal to zero, in which case + * the new capacity will be twice the old capacity; but if this new size + * is still smaller than {@code minCapacity}, then the new capacity will + * be {@code minCapacity}. + * + * @param minCapacity the desired minimum capacity + */ + public synchronized void ensureCapacity(int minCapacity) { + if (minCapacity > 0) { + modCount++; + if (minCapacity > elementData.length) + grow(minCapacity); + } + } + + /** + * The maximum size of array to allocate (unless necessary). + * Some VMs reserve some header words in an array. + * Attempts to allocate larger arrays may result in + * OutOfMemoryError: Requested array size exceeds VM limit + */ + private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; + + /** + * Increases the capacity to ensure that it can hold at least the + * number of elements specified by the minimum capacity argument. + * + * @param minCapacity the desired minimum capacity + * @throws OutOfMemoryError if minCapacity is less than zero + */ + private Object[] grow(int minCapacity) { + return elementData = Arrays.copyOf(elementData, newCapacity(minCapacity)); + } + + private Object[] grow() { + return grow(elementCount + 1); + } + + /** + * Returns a capacity at least as large as the given minimum capacity. + * Will not return a capacity greater than MAX_ARRAY_SIZE unless + * the given minimum capacity is greater than MAX_ARRAY_SIZE. + * + * @param minCapacity the desired minimum capacity + * @throws OutOfMemoryError if minCapacity is less than zero + */ + private int newCapacity(int minCapacity) { + // overflow-conscious code + int oldCapacity = elementData.length; + int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity); + if (newCapacity - minCapacity <= 0) { + if (// overflow + minCapacity < 0) + throw new OutOfMemoryError(); + return minCapacity; + } + return (newCapacity - MAX_ARRAY_SIZE <= 0) ? newCapacity : hugeCapacity(minCapacity); + } + + private static int hugeCapacity(int minCapacity) { + if (// overflow + minCapacity < 0) + throw new OutOfMemoryError(); + return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; + } + + /** + * Sets the size of this vector. If the new size is greater than the + * current size, new {@code null} items are added to the end of + * the vector. If the new size is less than the current size, all + * components at index {@code newSize} and greater are discarded. + * + * @param newSize the new size of this vector + * @throws ArrayIndexOutOfBoundsException if the new size is negative + */ + public synchronized void setSize(int newSize) { + modCount++; + if (newSize > elementData.length) + grow(newSize); + final Object[] es = elementData; + for (int to = elementCount, i = newSize; i < to; i++) es[i] = null; + elementCount = newSize; + } + + /** + * Returns the current capacity of this vector. + * + * @return the current capacity (the length of its internal + * data array, kept in the field {@code elementData} + * of this vector) + */ + public synchronized int capacity() { + return elementData.length; + } + + /** + * Returns the number of components in this vector. + * + * @return the number of components in this vector + */ + public synchronized int size() { + return elementCount; + } + + /** + * Tests if this vector has no components. + * + * @return {@code true} if and only if this vector has + * no components, that is, its size is zero; + * {@code false} otherwise. + */ + public synchronized boolean isEmpty() { + return elementCount == 0; + } + + /** + * Returns an enumeration of the components of this vector. The + * returned {@code Enumeration} object will generate all items in + * this vector. The first item generated is the item at index {@code 0}, + * then the item at index {@code 1}, and so on. If the vector is + * structurally modified while enumerating over the elements then the + * results of enumerating are undefined. + * + * @return an enumeration of the components of this vector + * @see Iterator + */ + public Enumeration elements() { + return new Enumeration() { + + int count = 0; + + public boolean hasMoreElements() { + return count < elementCount; + } + + public E nextElement() { + synchronized (Vector.this) { + if (count < elementCount) { + return elementData(count++); + } + } + throw new NoSuchElementException("Vector Enumeration"); + } + }; + } + + /** + * Returns {@code true} if this vector contains the specified element. + * More formally, returns {@code true} if and only if this vector + * contains at least one element {@code e} such that + * {@code Objects.equals(o, e)}. + * + * @param o element whose presence in this vector is to be tested + * @return {@code true} if this vector contains the specified element + */ + public boolean contains(@Nullable Object o) { + return indexOf(o, 0) >= 0; + } + + /** + * Returns the index of the first occurrence of the specified element + * in this vector, or -1 if this vector does not contain the element. + * More formally, returns the lowest index {@code i} such that + * {@code Objects.equals(o, get(i))}, + * or -1 if there is no such index. + * + * @param o element to search for + * @return the index of the first occurrence of the specified element in + * this vector, or -1 if this vector does not contain the element + */ + public int indexOf(@Nullable Object o) { + return indexOf(o, 0); + } + + /** + * Returns the index of the first occurrence of the specified element in + * this vector, searching forwards from {@code index}, or returns -1 if + * the element is not found. + * More formally, returns the lowest index {@code i} such that + * {@code (i >= index && Objects.equals(o, get(i)))}, + * or -1 if there is no such index. + * + * @param o element to search for + * @param index index to start searching from + * @return the index of the first occurrence of the element in + * this vector at position {@code index} or later in the vector; + * {@code -1} if the element is not found. + * @throws IndexOutOfBoundsException if the specified index is negative + * @see Object#equals(Object) + */ + public synchronized int indexOf(@Nullable Object o, int index) { + if (o == null) { + for (int i = index; i < elementCount; i++) if (elementData[i] == null) + return i; + } else { + for (int i = index; i < elementCount; i++) if (o.equals(elementData[i])) + return i; + } + return -1; + } + + /** + * Returns the index of the last occurrence of the specified element + * in this vector, or -1 if this vector does not contain the element. + * More formally, returns the highest index {@code i} such that + * {@code Objects.equals(o, get(i))}, + * or -1 if there is no such index. + * + * @param o element to search for + * @return the index of the last occurrence of the specified element in + * this vector, or -1 if this vector does not contain the element + */ + public synchronized int lastIndexOf(@Nullable Object o) { + return lastIndexOf(o, elementCount - 1); + } + + /** + * Returns the index of the last occurrence of the specified element in + * this vector, searching backwards from {@code index}, or returns -1 if + * the element is not found. + * More formally, returns the highest index {@code i} such that + * {@code (i <= index && Objects.equals(o, get(i)))}, + * or -1 if there is no such index. + * + * @param o element to search for + * @param index index to start searching backwards from + * @return the index of the last occurrence of the element at position + * less than or equal to {@code index} in this vector; + * -1 if the element is not found. + * @throws IndexOutOfBoundsException if the specified index is greater + * than or equal to the current size of this vector + */ + public synchronized int lastIndexOf(@Nullable Object o, int index) { + if (index >= elementCount) + throw new IndexOutOfBoundsException(index + " >= " + elementCount); + if (o == null) { + for (int i = index; i >= 0; i--) if (elementData[i] == null) + return i; + } else { + for (int i = index; i >= 0; i--) if (o.equals(elementData[i])) + return i; + } + return -1; + } + + /** + * Returns the component at the specified index. + * + *

This method is identical in functionality to the {@link #get(int)} + * method (which is part of the {@link List} interface). + * + * @param index an index into this vector + * @return the component at the specified index + * @throws ArrayIndexOutOfBoundsException if the index is out of range + * ({@code index < 0 || index >= size()}) + */ + public synchronized E elementAt(int index) { + if (index >= elementCount) { + throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount); + } + return elementData(index); + } + + /** + * Returns the first component (the item at index {@code 0}) of + * this vector. + * + * @return the first component of this vector + * @throws NoSuchElementException if this vector has no components + */ + public synchronized E firstElement() { + if (elementCount == 0) { + throw new NoSuchElementException(); + } + return elementData(0); + } + + /** + * Returns the last component of the vector. + * + * @return the last component of the vector, i.e., the component at index + * {@code size() - 1} + * @throws NoSuchElementException if this vector is empty + */ + public synchronized E lastElement() { + if (elementCount == 0) { + throw new NoSuchElementException(); + } + return elementData(elementCount - 1); + } + + /** + * Sets the component at the specified {@code index} of this + * vector to be the specified object. The previous component at that + * position is discarded. + * + *

The index must be a value greater than or equal to {@code 0} + * and less than the current size of the vector. + * + *

This method is identical in functionality to the + * {@link #set(int, Object) set(int, E)} + * method (which is part of the {@link List} interface). Note that the + * {@code set} method reverses the order of the parameters, to more closely + * match array usage. Note also that the {@code set} method returns the + * old value that was stored at the specified position. + * + * @param obj what the component is to be set to + * @param index the specified index + * @throws ArrayIndexOutOfBoundsException if the index is out of range + * ({@code index < 0 || index >= size()}) + */ + public synchronized void setElementAt(E obj, int index) { + if (index >= elementCount) { + throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount); + } + elementData[index] = obj; + } + + /** + * Deletes the component at the specified index. Each component in + * this vector with an index greater or equal to the specified + * {@code index} is shifted downward to have an index one + * smaller than the value it had previously. The size of this vector + * is decreased by {@code 1}. + * + *

The index must be a value greater than or equal to {@code 0} + * and less than the current size of the vector. + * + *

This method is identical in functionality to the {@link #remove(int)} + * method (which is part of the {@link List} interface). Note that the + * {@code remove} method returns the old value that was stored at the + * specified position. + * + * @param index the index of the object to remove + * @throws ArrayIndexOutOfBoundsException if the index is out of range + * ({@code index < 0 || index >= size()}) + */ + public synchronized void removeElementAt(int index) { + if (index >= elementCount) { + throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount); + } else if (index < 0) { + throw new ArrayIndexOutOfBoundsException(index); + } + int j = elementCount - index - 1; + if (j > 0) { + System.arraycopy(elementData, index + 1, elementData, index, j); + } + modCount++; + elementCount--; + elementData[elementCount] = null; + /* to let gc do its work */ + } + + /** + * Inserts the specified object as a component in this vector at the + * specified {@code index}. Each component in this vector with + * an index greater or equal to the specified {@code index} is + * shifted upward to have an index one greater than the value it had + * previously. + * + *

The index must be a value greater than or equal to {@code 0} + * and less than or equal to the current size of the vector. (If the + * index is equal to the current size of the vector, the new element + * is appended to the Vector.) + * + *

This method is identical in functionality to the + * {@link #add(int, Object) add(int, E)} + * method (which is part of the {@link List} interface). Note that the + * {@code add} method reverses the order of the parameters, to more closely + * match array usage. + * + * @param obj the component to insert + * @param index where to insert the new component + * @throws ArrayIndexOutOfBoundsException if the index is out of range + * ({@code index < 0 || index > size()}) + */ + public synchronized void insertElementAt(E obj, int index) { + if (index > elementCount) { + throw new ArrayIndexOutOfBoundsException(index + " > " + elementCount); + } + modCount++; + final int s = elementCount; + Object[] elementData = this.elementData; + if (s == elementData.length) + elementData = grow(); + System.arraycopy(elementData, index, elementData, index + 1, s - index); + elementData[index] = obj; + elementCount = s + 1; + } + + /** + * Adds the specified component to the end of this vector, + * increasing its size by one. The capacity of this vector is + * increased if its size becomes greater than its capacity. + * + *

This method is identical in functionality to the + * {@link #add(Object) add(E)} + * method (which is part of the {@link List} interface). + * + * @param obj the component to be added + */ + public synchronized void addElement(E obj) { + modCount++; + add(obj, elementData, elementCount); + } + + /** + * Removes the first (lowest-indexed) occurrence of the argument + * from this vector. If the object is found in this vector, each + * component in the vector with an index greater or equal to the + * object's index is shifted downward to have an index one smaller + * than the value it had previously. + * + *

This method is identical in functionality to the + * {@link #remove(Object)} method (which is part of the + * {@link List} interface). + * + * @param obj the component to be removed + * @return {@code true} if the argument was a component of this + * vector; {@code false} otherwise. + */ + public synchronized boolean removeElement(Object obj) { + modCount++; + int i = indexOf(obj); + if (i >= 0) { + removeElementAt(i); + return true; + } + return false; + } + + /** + * Removes all components from this vector and sets its size to zero. + * + *

This method is identical in functionality to the {@link #clear} + * method (which is part of the {@link List} interface). + */ + public synchronized void removeAllElements() { + final Object[] es = elementData; + for (int to = elementCount, i = elementCount = 0; i < to; i++) es[i] = null; + modCount++; + } + + /** + * Returns a clone of this vector. The copy will contain a + * reference to a clone of the internal data array, not a reference + * to the original internal data array of this {@code Vector} object. + * + * @return a clone of this vector + */ + public synchronized Object clone() { + try { + @SuppressWarnings("unchecked") + Vector v = (Vector) super.clone(); + v.elementData = Arrays.copyOf(elementData, elementCount); + v.modCount = 0; + return v; + } catch (CloneNotSupportedException e) { + // this shouldn't happen, since we are Cloneable + throw new InternalError(e); + } + } + + /** + * Returns an array containing all of the elements in this Vector + * in the correct order. + * + * @since 1.2 + */ + @Nullable + public synchronized Object[] toArray() { + return Arrays.copyOf(elementData, elementCount); + } + + /** + * Returns an array containing all of the elements in this Vector in the + * correct order; the runtime type of the returned array is that of the + * specified array. If the Vector fits in the specified array, it is + * returned therein. Otherwise, a new array is allocated with the runtime + * type of the specified array and the size of this Vector. + * + *

If the Vector fits in the specified array with room to spare + * (i.e., the array has more elements than the Vector), + * the element in the array immediately following the end of the + * Vector is set to null. (This is useful in determining the length + * of the Vector only if the caller knows that the Vector + * does not contain any null elements.) + * + * @param type of array elements. The same type as {@code } or a + * supertype of {@code }. + * @param a the array into which the elements of the Vector are to + * be stored, if it is big enough; otherwise, a new array of the + * same runtime type is allocated for this purpose. + * @return an array containing the elements of the Vector + * @throws ArrayStoreException if the runtime type of a, {@code }, is not + * a supertype of the runtime type, {@code }, of every element in this + * Vector + * @throws NullPointerException if the given array is null + * @since 1.2 + */ + @SuppressWarnings("unchecked") + public synchronized T[] toArray(T[] a) { + if (a.length < elementCount) + return (T[]) Arrays.copyOf(elementData, elementCount, a.getClass()); + System.arraycopy(elementData, 0, a, 0, elementCount); + if (a.length > elementCount) + a[elementCount] = null; + return a; + } + + // Positional Access Operations + @SuppressWarnings("unchecked") + E elementData(int index) { + return (E) elementData[index]; + } + + @SuppressWarnings("unchecked") + static E elementAt(Object[] es, int index) { + return (E) es[index]; + } + + /** + * Returns the element at the specified position in this Vector. + * + * @param index index of the element to return + * @return object at the specified index + * @throws ArrayIndexOutOfBoundsException if the index is out of range + * ({@code index < 0 || index >= size()}) + * @since 1.2 + */ + public synchronized E get(int index) { + if (index >= elementCount) + throw new ArrayIndexOutOfBoundsException(index); + return elementData(index); + } + + /** + * Replaces the element at the specified position in this Vector with the + * specified element. + * + * @param index index of the element to replace + * @param element element to be stored at the specified position + * @return the element previously at the specified position + * @throws ArrayIndexOutOfBoundsException if the index is out of range + * ({@code index < 0 || index >= size()}) + * @since 1.2 + */ + public synchronized E set(int index, E element) { + if (index >= elementCount) + throw new ArrayIndexOutOfBoundsException(index); + E oldValue = elementData(index); + elementData[index] = element; + return oldValue; + } + + /** + * This helper method split out from add(E) to keep method + * bytecode size under 35 (the -XX:MaxInlineSize default value), + * which helps when add(E) is called in a C1-compiled loop. + */ + private void add(E e, Object[] elementData, int s) { + if (s == elementData.length) + elementData = grow(); + elementData[s] = e; + elementCount = s + 1; + } + + /** + * Appends the specified element to the end of this Vector. + * + * @param e element to be appended to this Vector + * @return {@code true} (as specified by {@link Collection#add}) + * @since 1.2 + */ + public synchronized boolean add(E e) { + modCount++; + add(e, elementData, elementCount); + return true; + } + + /** + * Removes the first occurrence of the specified element in this Vector + * If the Vector does not contain the element, it is unchanged. More + * formally, removes the element with the lowest index i such that + * {@code Objects.equals(o, get(i))} (if such + * an element exists). + * + * @param o element to be removed from this Vector, if present + * @return true if the Vector contained the specified element + * @since 1.2 + */ + public boolean remove(@Nullable Object o) { + return removeElement(o); + } + + /** + * Inserts the specified element at the specified position in this Vector. + * Shifts the element currently at that position (if any) and any + * subsequent elements to the right (adds one to their indices). + * + * @param index index at which the specified element is to be inserted + * @param element element to be inserted + * @throws ArrayIndexOutOfBoundsException if the index is out of range + * ({@code index < 0 || index > size()}) + * @since 1.2 + */ + public void add(int index, E element) { + insertElementAt(element, index); + } + + /** + * Removes the element at the specified position in this Vector. + * Shifts any subsequent elements to the left (subtracts one from their + * indices). Returns the element that was removed from the Vector. + * + * @param index the index of the element to be removed + * @return element that was removed + * @throws ArrayIndexOutOfBoundsException if the index is out of range + * ({@code index < 0 || index >= size()}) + * @since 1.2 + */ + public synchronized E remove(int index) { + modCount++; + if (index >= elementCount) + throw new ArrayIndexOutOfBoundsException(index); + E oldValue = elementData(index); + int numMoved = elementCount - index - 1; + if (numMoved > 0) + System.arraycopy(elementData, index + 1, elementData, index, numMoved); + // Let gc do its work + elementData[--elementCount] = null; + return oldValue; + } + + /** + * Removes all of the elements from this Vector. The Vector will + * be empty after this call returns (unless it throws an exception). + * + * @since 1.2 + */ + public void clear() { + removeAllElements(); + } + + // Bulk Operations + /** + * Returns true if this Vector contains all of the elements in the + * specified Collection. + * + * @param c a collection whose elements will be tested for containment + * in this Vector + * @return true if this Vector contains all of the elements in the + * specified collection + * @throws NullPointerException if the specified collection is null + */ + public synchronized boolean containsAll(Collection c) { + return super.containsAll(c); + } + + /** + * Appends all of the elements in the specified Collection to the end of + * this Vector, in the order that they are returned by the specified + * Collection's Iterator. The behavior of this operation is undefined if + * the specified Collection is modified while the operation is in progress. + * (This implies that the behavior of this call is undefined if the + * specified Collection is this Vector, and this Vector is nonempty.) + * + * @param c elements to be inserted into this Vector + * @return {@code true} if this Vector changed as a result of the call + * @throws NullPointerException if the specified collection is null + * @since 1.2 + */ + public boolean addAll(Collection c) { + Object[] a = c.toArray(); + modCount++; + int numNew = a.length; + if (numNew == 0) + return false; + synchronized (this) { + Object[] elementData = this.elementData; + final int s = elementCount; + if (numNew > elementData.length - s) + elementData = grow(s + numNew); + System.arraycopy(a, 0, elementData, s, numNew); + elementCount = s + numNew; + return true; + } + } + + /** + * Removes from this Vector all of its elements that are contained in the + * specified Collection. + * + * @param c a collection of elements to be removed from the Vector + * @return true if this Vector changed as a result of the call + * @throws ClassCastException if the types of one or more elements + * in this vector are incompatible with the specified + * collection + * (optional) + * @throws NullPointerException if this vector contains one or more null + * elements and the specified collection does not support null + * elements + * (optional), + * or if the specified collection is null + * @since 1.2 + */ + public boolean removeAll(Collection c) { + Objects.requireNonNull(c); + return bulkRemove(e -> c.contains(e)); + } + + /** + * Retains only the elements in this Vector that are contained in the + * specified Collection. In other words, removes from this Vector all + * of its elements that are not contained in the specified Collection. + * + * @param c a collection of elements to be retained in this Vector + * (all other elements are removed) + * @return true if this Vector changed as a result of the call + * @throws ClassCastException if the types of one or more elements + * in this vector are incompatible with the specified + * collection + * (optional) + * @throws NullPointerException if this vector contains one or more null + * elements and the specified collection does not support null + * elements + * (optional), + * or if the specified collection is null + * @since 1.2 + */ + public boolean retainAll(Collection c) { + Objects.requireNonNull(c); + return bulkRemove(e -> !c.contains(e)); + } + + /** + * @throws NullPointerException {@inheritDoc} + */ + @SuppressWarnings({ "unchecked" }) + @Override + public boolean removeIf(Predicate filter) { + Objects.requireNonNull(filter); + return bulkRemove(filter); + } + + // A tiny bit set implementation + private static long[] nBits(int n) { + return new long[((n - 1) >> 6) + 1]; + } + + private static void setBit(long[] bits, int i) { + bits[i >> 6] |= 1L << i; + } + + private static boolean isClear(long[] bits, int i) { + return (bits[i >> 6] & (1L << i)) == 0; + } + + private synchronized boolean bulkRemove(Predicate filter) { + int expectedModCount = modCount; + final Object[] es = elementData; + final int end = elementCount; + int i; + // Optimize for initial run of survivors + for (i = 0; i < end && !filter.test(elementAt(es, i)); i++) ; + // Tolerate predicates that reentrantly access the collection for + // read (but writers still get CME), so traverse once to find + // elements to delete, a second pass to physically expunge. + if (i < end) { + final int beg = i; + final long[] deathRow = nBits(end - beg); + // set bit 0 + deathRow[0] = 1L; + for (i = beg + 1; i < end; i++) if (filter.test(elementAt(es, i))) + setBit(deathRow, i - beg); + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + modCount++; + int w = beg; + for (i = beg; i < end; i++) if (isClear(deathRow, i - beg)) + es[w++] = es[i]; + for (i = elementCount = w; i < end; i++) es[i] = null; + return true; + } else { + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + return false; + } + } + + /** + * Inserts all of the elements in the specified Collection into this + * Vector at the specified position. Shifts the element currently at + * that position (if any) and any subsequent elements to the right + * (increases their indices). The new elements will appear in the Vector + * in the order that they are returned by the specified Collection's + * iterator. + * + * @param index index at which to insert the first element from the + * specified collection + * @param c elements to be inserted into this Vector + * @return {@code true} if this Vector changed as a result of the call + * @throws ArrayIndexOutOfBoundsException if the index is out of range + * ({@code index < 0 || index > size()}) + * @throws NullPointerException if the specified collection is null + * @since 1.2 + */ + public synchronized boolean addAll(int index, Collection c) { + if (index < 0 || index > elementCount) + throw new ArrayIndexOutOfBoundsException(index); + Object[] a = c.toArray(); + modCount++; + int numNew = a.length; + if (numNew == 0) + return false; + Object[] elementData = this.elementData; + final int s = elementCount; + if (numNew > elementData.length - s) + elementData = grow(s + numNew); + int numMoved = s - index; + if (numMoved > 0) + System.arraycopy(elementData, index, elementData, index + numNew, numMoved); + System.arraycopy(a, 0, elementData, index, numNew); + elementCount = s + numNew; + return true; + } + + /** + * Compares the specified Object with this Vector for equality. Returns + * true if and only if the specified Object is also a List, both Lists + * have the same size, and all corresponding pairs of elements in the two + * Lists are equal. (Two elements {@code e1} and + * {@code e2} are equal if {@code Objects.equals(e1, e2)}.) + * In other words, two Lists are defined to be + * equal if they contain the same elements in the same order. + * + * @param o the Object to be compared for equality with this Vector + * @return true if the specified Object is equal to this Vector + */ + public synchronized boolean equals(@Nullable Object o) { + return super.equals(o); + } + + /** + * Returns the hash code value for this Vector. + */ + public synchronized int hashCode() { + return super.hashCode(); + } + + /** + * Returns a string representation of this Vector, containing + * the String representation of each element. + */ + public synchronized String toString() { + return super.toString(); + } + + /** + * Returns a view of the portion of this List between fromIndex, + * inclusive, and toIndex, exclusive. (If fromIndex and toIndex are + * equal, the returned List is empty.) The returned List is backed by this + * List, so changes in the returned List are reflected in this List, and + * vice-versa. The returned List supports all of the optional List + * operations supported by this List. + * + *

This method eliminates the need for explicit range operations (of + * the sort that commonly exist for arrays). Any operation that expects + * a List can be used as a range operation by operating on a subList view + * instead of a whole List. For example, the following idiom + * removes a range of elements from a List: + *

+     *      list.subList(from, to).clear();
+     * 
+ * Similar idioms may be constructed for indexOf and lastIndexOf, + * and all of the algorithms in the Collections class can be applied to + * a subList. + * + *

The semantics of the List returned by this method become undefined if + * the backing list (i.e., this List) is structurally modified in + * any way other than via the returned List. (Structural modifications are + * those that change the size of the List, or otherwise perturb it in such + * a fashion that iterations in progress may yield incorrect results.) + * + * @param fromIndex low endpoint (inclusive) of the subList + * @param toIndex high endpoint (exclusive) of the subList + * @return a view of the specified range within this List + * @throws IndexOutOfBoundsException if an endpoint index value is out of range + * {@code (fromIndex < 0 || toIndex > size)} + * @throws IllegalArgumentException if the endpoint indices are out of order + * {@code (fromIndex > toIndex)} + */ + public synchronized List subList(int fromIndex, int toIndex) { + return Collections.synchronizedList(super.subList(fromIndex, toIndex), this); + } + + /** + * Removes from this list all of the elements whose index is between + * {@code fromIndex}, inclusive, and {@code toIndex}, exclusive. + * Shifts any succeeding elements to the left (reduces their index). + * This call shortens the list by {@code (toIndex - fromIndex)} elements. + * (If {@code toIndex==fromIndex}, this operation has no effect.) + */ + protected synchronized void removeRange(int fromIndex, int toIndex) { + modCount++; + shiftTailOverGap(elementData, fromIndex, toIndex); + } + + /** + * Erases the gap from lo to hi, by sliding down following elements. + */ + private void shiftTailOverGap(Object[] es, int lo, int hi) { + System.arraycopy(es, hi, es, lo, elementCount - hi); + for (int to = elementCount, i = (elementCount -= hi - lo); i < to; i++) es[i] = null; + } + + /** + * Loads a {@code Vector} instance from a stream + * (that is, deserializes it). + * This method performs checks to ensure the consistency + * of the fields. + * + * @param in the stream + * @throws java.io.IOException if an I/O error occurs + * @throws ClassNotFoundException if the stream contains data + * of a non-existing class + */ + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + ObjectInputStream.GetField gfields = in.readFields(); + int count = gfields.get("elementCount", 0); + Object[] data = (Object[]) gfields.get("elementData", null); + if (count < 0 || data == null || count > data.length) { + throw new StreamCorruptedException("Inconsistent vector internals"); + } + elementCount = count; + elementData = data.clone(); + } + + /** + * Saves the state of the {@code Vector} instance to a stream + * (that is, serializes it). + * This method performs synchronization to ensure the consistency + * of the serialized data. + * + * @param s the stream + * @throws java.io.IOException if an I/O error occurs + */ + private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException { + final java.io.ObjectOutputStream.PutField fields = s.putFields(); + final Object[] data; + synchronized (this) { + fields.put("capacityIncrement", capacityIncrement); + fields.put("elementCount", elementCount); + data = elementData.clone(); + } + fields.put("elementData", data); + s.writeFields(); + } + + /** + * Returns a list iterator over the elements in this list (in proper + * sequence), starting at the specified position in the list. + * The specified index indicates the first element that would be + * returned by an initial call to {@link ListIterator#next next}. + * An initial call to {@link ListIterator#previous previous} would + * return the element with the specified index minus one. + * + *

The returned list iterator is fail-fast. + * + * @throws IndexOutOfBoundsException {@inheritDoc} + */ + public synchronized ListIterator listIterator(int index) { + if (index < 0 || index > elementCount) + throw new IndexOutOfBoundsException("Index: " + index); + return new ListItr(index); + } + + /** + * Returns a list iterator over the elements in this list (in proper + * sequence). + * + *

The returned list iterator is fail-fast. + * + * @see #listIterator(int) + */ + public synchronized ListIterator listIterator() { + return new ListItr(0); + } + + /** + * Returns an iterator over the elements in this list in proper sequence. + * + *

The returned iterator is fail-fast. + * + * @return an iterator over the elements in this list in proper sequence + */ + public synchronized Iterator iterator() { + return new Itr(); + } + + /** + * An optimized version of AbstractList.Itr + */ + private class Itr implements Iterator { + + // index of next element to return + int cursor; + + // index of last element returned; -1 if no such + int lastRet = -1; + + int expectedModCount = modCount; + + public boolean hasNext() { + // Racy but within spec, since modifications are checked + // within or after synchronization in next/previous + return cursor != elementCount; + } + + public E next() { + synchronized (Vector.this) { + checkForComodification(); + int i = cursor; + if (i >= elementCount) + throw new NoSuchElementException(); + cursor = i + 1; + return elementData(lastRet = i); + } + } + + public void remove() { + if (lastRet == -1) + throw new IllegalStateException(); + synchronized (Vector.this) { + checkForComodification(); + Vector.this.remove(lastRet); + expectedModCount = modCount; + } + cursor = lastRet; + lastRet = -1; + } + + @Override + public void forEachRemaining(Consumer action) { + Objects.requireNonNull(action); + synchronized (Vector.this) { + final int size = elementCount; + int i = cursor; + if (i >= size) { + return; + } + final Object[] es = elementData; + if (i >= es.length) + throw new ConcurrentModificationException(); + while (i < size && modCount == expectedModCount) action.accept(elementAt(es, i++)); + // update once at end of iteration to reduce heap write traffic + cursor = i; + lastRet = i - 1; + checkForComodification(); + } + } + + final void checkForComodification() { + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + } + } + + /** + * An optimized version of AbstractList.ListItr + */ + final class ListItr extends Itr implements ListIterator { + + ListItr(int index) { + super(); + cursor = index; + } + + public boolean hasPrevious() { + return cursor != 0; + } + + public int nextIndex() { + return cursor; + } + + public int previousIndex() { + return cursor - 1; + } + + public E previous() { + synchronized (Vector.this) { + checkForComodification(); + int i = cursor - 1; + if (i < 0) + throw new NoSuchElementException(); + cursor = i; + return elementData(lastRet = i); + } + } + + public void set(E e) { + if (lastRet == -1) + throw new IllegalStateException(); + synchronized (Vector.this) { + checkForComodification(); + Vector.this.set(lastRet, e); + } + } + + public void add(E e) { + int i = cursor; + synchronized (Vector.this) { + checkForComodification(); + Vector.this.add(i, e); + expectedModCount = modCount; + } + cursor = i + 1; + lastRet = -1; + } + } + + /** + * @throws NullPointerException {@inheritDoc} + */ + @Override + public synchronized void forEach(Consumer action) { + Objects.requireNonNull(action); + final int expectedModCount = modCount; + final Object[] es = elementData; + final int size = elementCount; + for (int i = 0; modCount == expectedModCount && i < size; i++) action.accept(elementAt(es, i)); + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + } + + /** + * @throws NullPointerException {@inheritDoc} + */ + @SuppressWarnings({ "unchecked" }) + @Override + public synchronized void replaceAll(UnaryOperator operator) { + Objects.requireNonNull(operator); + final int expectedModCount = modCount; + final Object[] es = elementData; + final int size = elementCount; + for (int i = 0; modCount == expectedModCount && i < size; i++) es[i] = operator.apply(elementAt(es, i)); + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + modCount++; + } + + @SuppressWarnings("unchecked") + @Override + public synchronized void sort(@Nullable Comparator c) { + final int expectedModCount = modCount; + Arrays.sort((E[]) elementData, 0, elementCount, c); + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + modCount++; + } + + /** + * Creates a late-binding + * and fail-fast {@link Spliterator} over the elements in this + * list. + * + *

The {@code Spliterator} reports {@link Spliterator#SIZED}, + * {@link Spliterator#SUBSIZED}, and {@link Spliterator#ORDERED}. + * Overriding implementations should document the reporting of additional + * characteristic values. + * + * @return a {@code Spliterator} over the elements in this list + * @since 1.8 + */ + @Override + public Spliterator spliterator() { + return new VectorSpliterator(null, 0, -1, 0); + } + + /** + * Similar to ArrayList Spliterator + */ + final class VectorSpliterator implements Spliterator { + + private Object[] array; + + // current index, modified on advance/split + private int index; + + // -1 until used; then one past last index + private int fence; + + // initialized when fence set + private int expectedModCount; + + /** + * Creates new spliterator covering the given range. + */ + VectorSpliterator(Object[] array, int origin, int fence, int expectedModCount) { + this.array = array; + this.index = origin; + this.fence = fence; + this.expectedModCount = expectedModCount; + } + + private int getFence() { + // initialize on first use + int hi; + if ((hi = fence) < 0) { + synchronized (Vector.this) { + array = elementData; + expectedModCount = modCount; + hi = fence = elementCount; + } + } + return hi; + } + + public Spliterator trySplit() { + int hi = getFence(), lo = index, mid = (lo + hi) >>> 1; + return (lo >= mid) ? null : new VectorSpliterator(array, lo, index = mid, expectedModCount); + } + + @SuppressWarnings("unchecked") + public boolean tryAdvance(Consumer action) { + Objects.requireNonNull(action); + int i; + if (getFence() > (i = index)) { + index = i + 1; + action.accept((E) array[i]); + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + return true; + } + return false; + } + + @SuppressWarnings("unchecked") + public void forEachRemaining(Consumer action) { + Objects.requireNonNull(action); + final int hi = getFence(); + final Object[] a = array; + int i; + for (i = index, index = hi; i < hi; i++) action.accept((E) a[i]); + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + } + + public long estimateSize() { + return getFence() - index; + } + + public int characteristics() { + return Spliterator.ORDERED | Spliterator.SIZED | Spliterator.SUBSIZED; + } + } + + void checkInvariants() { + // assert elementCount >= 0; + // assert elementCount == elementData.length || elementData[elementCount] == null; + } +} diff --git a/lib-model/lib-model-test/src/main/resources/sample_jspecify_jdk/src/java.base/share/classes/java/util/function/Function.java b/lib-model/lib-model-test/src/main/resources/sample_jspecify_jdk/src/java.base/share/classes/java/util/function/Function.java new file mode 100644 index 0000000000..d895ffc905 --- /dev/null +++ b/lib-model/lib-model-test/src/main/resources/sample_jspecify_jdk/src/java.base/share/classes/java/util/function/Function.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package java.util.function; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import java.util.Objects; + +/** + * Represents a function that accepts one argument and produces a result. + * + *

This is a functional interface + * whose functional method is {@link #apply(Object)}. + * + * @param the type of the input to the function + * @param the type of the result of the function + * + * @since 1.8 + */ +@NullMarked +@FunctionalInterface +public interface Function { + + /** + * Applies this function to the given argument. + * + * @param t the function argument + * @return the function result + */ + R apply(T t); + + /** + * Returns a composed function that first applies the {@code before} + * function to its input, and then applies this function to the result. + * If evaluation of either function throws an exception, it is relayed to + * the caller of the composed function. + * + * @param the type of input to the {@code before} function, and to the + * composed function + * @param before the function to apply before this function is applied + * @return a composed function that first applies the {@code before} + * function and then applies this function + * @throws NullPointerException if before is null + * + * @see #andThen(Function) + */ + default Function compose(Function before) { + Objects.requireNonNull(before); + return (V v) -> apply(before.apply(v)); + } + + /** + * Returns a composed function that first applies this function to + * its input, and then applies the {@code after} function to the result. + * If evaluation of either function throws an exception, it is relayed to + * the caller of the composed function. + * + * @param the type of output of the {@code after} function, and of the + * composed function + * @param after the function to apply after this function is applied + * @return a composed function that first applies this function and then + * applies the {@code after} function + * @throws NullPointerException if after is null + * + * @see #compose(Function) + */ + default Function andThen(Function after) { + Objects.requireNonNull(after); + return (T t) -> after.apply(apply(t)); + } + + /** + * Returns a function that always returns its input argument. + * + * @param the type of the input and output objects to the function + * @return a function that always returns its input argument + */ + static Function identity() { + return t -> t; + } +} From 32f0990f082ba3ac0f392ef25a4aa934a173d83a Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Sun, 25 Feb 2024 22:27:13 -0800 Subject: [PATCH 10/50] adding logic to create directories if they don't exist to avoid NoSuchFileException --- .../libmodel/LibModelInfoExtractor.java | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java b/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java index 90d02e8e1c..7d17db92aa 100644 --- a/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java +++ b/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java @@ -52,14 +52,12 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.nio.file.attribute.FileTime; import java.util.ArrayDeque; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.zip.ZipEntry; public class LibModelInfoExtractor { @@ -72,6 +70,12 @@ public static void main(String[] args) { writeToAstubx(args[1]); } + @SuppressWarnings("unused") + public static Map runLibModelProcess(String[] args) { + main(args); + return methodRecords; + } + public static void processSourceFile(String file) { Path root = dirnameToPath(file); MinimizerCallback mc = new MinimizerCallback(); @@ -99,26 +103,18 @@ public static void writeToAstubx(String outputPath) { .put("Nonnull", "javax.annotation.Nonnull") .put("Nullable", "javax.annotation.Nullable") .build(); - // ZipOutputStream zos; DataOutputStream dos; try { - // zos = new ZipOutputStream(Files.newOutputStream(Paths.get(outputPath))); - dos = new DataOutputStream(Files.newOutputStream(Paths.get(outputPath))); + Path opPath = Paths.get(outputPath); + Files.createDirectories(opPath.getParent()); + dos = new DataOutputStream(Files.newOutputStream(opPath)); } catch (IOException e) { throw new RuntimeException(e); } if (!methodRecords.isEmpty()) { - ZipEntry entry = new ZipEntry("META-INF/nullaway/libmodels.astubx"); - // Set the modification/creation time to 0 to ensure that this jars always have the same - // checksum - entry.setTime(0); - entry.setCreationTime(FileTime.fromMillis(0)); try { - // zos.putNextEntry(entry); StubxFileWriter.write( dos, importedAnnotations, new HashMap<>(), new HashMap<>(), methodRecords); - // zos.closeEntry(); - // zos.close(); dos.close(); } catch (IOException e) { throw new RuntimeException(e); From 3dedc77bbd3e45d30cd22dd1ace5390e13ede0db Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Mon, 26 Feb 2024 16:11:01 -0800 Subject: [PATCH 11/50] changing logic for ArrayTypes and adding Scanner findInLine test case for nullable returns --- .../libmodel/LibModelInfoExtractor.java | 5 +- .../libmodel/LibModelIntegrationTest.java | 30 +- .../share/classes/java/util/Scanner.java | 3018 +++++++++++++++++ 3 files changed, 3050 insertions(+), 3 deletions(-) create mode 100644 lib-model/lib-model-test/src/main/resources/sample_jspecify_jdk/src/java.base/share/classes/java/util/Scanner.java diff --git a/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java b/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java index 7d17db92aa..c644d3b35b 100644 --- a/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java +++ b/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java @@ -267,7 +267,10 @@ public MethodDeclaration visit(MethodDeclaration md, Void arg) { if (md.getType() instanceof ClassOrInterfaceType) { methodReturnType = md.getType().getChildNodes().get(0).toString(); } else if (md.getType() instanceof ArrayType) { - methodReturnType = "Array"; + // methodReturnType = "Array"; + // TODO: Figure out the right way to handle Array types + // For now we don't consider it as Nullable + isNullableAnnotationPresent = false; } else { methodReturnType = md.getType().toString(); } diff --git a/lib-model/lib-model-consume/src/test/java/com/uber/nullaway/libmodel/LibModelIntegrationTest.java b/lib-model/lib-model-consume/src/test/java/com/uber/nullaway/libmodel/LibModelIntegrationTest.java index 94b657dbeb..9b0ec1d30f 100644 --- a/lib-model/lib-model-consume/src/test/java/com/uber/nullaway/libmodel/LibModelIntegrationTest.java +++ b/lib-model/lib-model-consume/src/test/java/com/uber/nullaway/libmodel/LibModelIntegrationTest.java @@ -20,7 +20,7 @@ public void setup() { } @Test - public void jarinferNullableReturnsTestForLibModel() { + public void libModelArrayTypeNullableReturnsTest() { compilationHelper .setArgs( Arrays.asList( @@ -40,10 +40,36 @@ public void jarinferNullableReturnsTestForLibModel() { " }", " static void test1() {", " vector.add(1);", - " // BUG: Diagnostic contains: passing @Nullable parameter", " test(vector.toArray());", " }", "}") .doTest(); } + + @Test + public void libModelNullableReturnsTest() { + compilationHelper + .setArgs( + Arrays.asList( + "-d", + temporaryFolder.getRoot().getAbsolutePath(), + "-XepOpt:NullAway:AnnotatedPackages=com.uber", + "-XepOpt:NullAway:JarInferEnabled=true", + "-XepOpt:NullAway:JarInferUseReturnAnnotations=true")) + .addSourceLines( + "Test.java", + "package com.uber;", + "import javax.annotation.Nullable;", + "import java.util.Scanner;", + "class Test {", + " static Scanner scanner = new Scanner(\"nullaway test\");", + " static void test(String value){", + " }", + " static void test1() {", + " // BUG: Diagnostic contains: passing @Nullable parameter 'scanner.findInLine(\"world\")'", + " test(scanner.findInLine(\"world\"));", + " }", + "}") + .doTest(); + } } diff --git a/lib-model/lib-model-test/src/main/resources/sample_jspecify_jdk/src/java.base/share/classes/java/util/Scanner.java b/lib-model/lib-model-test/src/main/resources/sample_jspecify_jdk/src/java.base/share/classes/java/util/Scanner.java new file mode 100644 index 0000000000..e694f72621 --- /dev/null +++ b/lib-model/lib-model-test/src/main/resources/sample_jspecify_jdk/src/java.base/share/classes/java/util/Scanner.java @@ -0,0 +1,3018 @@ +/* + * Copyright (c) 2003, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package java.util; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import java.io.*; +import java.math.*; +import java.nio.*; +import java.nio.channels.*; +import java.nio.charset.*; +import java.nio.file.Path; +import java.nio.file.Files; +import java.text.*; +import java.text.spi.NumberFormatProvider; +import java.util.function.Consumer; +import java.util.regex.*; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +import sun.util.locale.provider.LocaleProviderAdapter; +import sun.util.locale.provider.ResourceBundleBasedAdapter; + +/** + * A simple text scanner which can parse primitive types and strings using + * regular expressions. + * + *

A {@code Scanner} breaks its input into tokens using a + * delimiter pattern, which by default matches whitespace. The resulting + * tokens may then be converted into values of different types using the + * various {@code next} methods. + * + *

For example, this code allows a user to read a number from + * {@code System.in}: + *

{@code
+ *     Scanner sc = new Scanner(System.in);
+ *     int i = sc.nextInt();
+ * }
+ * + *

As another example, this code allows {@code long} types to be + * assigned from entries in a file {@code myNumbers}: + *

{@code
+ *      Scanner sc = new Scanner(new File("myNumbers"));
+ *      while (sc.hasNextLong()) {
+ *          long aLong = sc.nextLong();
+ *      }
+ * }
+ * + *

The scanner can also use delimiters other than whitespace. This + * example reads several items in from a string: + *

{@code
+ *     String input = "1 fish 2 fish red fish blue fish";
+ *     Scanner s = new Scanner(input).useDelimiter("\\s*fish\\s*");
+ *     System.out.println(s.nextInt());
+ *     System.out.println(s.nextInt());
+ *     System.out.println(s.next());
+ *     System.out.println(s.next());
+ *     s.close();
+ * }
+ *

+ * prints the following output: + *

{@code
+ *     1
+ *     2
+ *     red
+ *     blue
+ * }
+ * + *

The same output can be generated with this code, which uses a regular + * expression to parse all four tokens at once: + *

{@code
+ *     String input = "1 fish 2 fish red fish blue fish";
+ *     Scanner s = new Scanner(input);
+ *     s.findInLine("(\\d+) fish (\\d+) fish (\\w+) fish (\\w+)");
+ *     MatchResult result = s.match();
+ *     for (int i=1; i<=result.groupCount(); i++)
+ *         System.out.println(result.group(i));
+ *     s.close();
+ * }
+ * + *

The default whitespace delimiter used + * by a scanner is as recognized by {@link Character#isWhitespace(char) + * Character.isWhitespace()}. The {@link #reset reset()} + * method will reset the value of the scanner's delimiter to the default + * whitespace delimiter regardless of whether it was previously changed. + * + *

A scanning operation may block waiting for input. + * + *

The {@link #next} and {@link #hasNext} methods and their + * companion methods (such as {@link #nextInt} and + * {@link #hasNextInt}) first skip any input that matches the delimiter + * pattern, and then attempt to return the next token. Both {@code hasNext()} + * and {@code next()} methods may block waiting for further input. Whether a + * {@code hasNext()} method blocks has no connection to whether or not its + * associated {@code next()} method will block. The {@link #tokens} method + * may also block waiting for input. + * + *

The {@link #findInLine findInLine()}, + * {@link #findWithinHorizon findWithinHorizon()}, + * {@link #skip skip()}, and {@link #findAll findAll()} + * methods operate independently of the delimiter pattern. These methods will + * attempt to match the specified pattern with no regard to delimiters in the + * input and thus can be used in special circumstances where delimiters are + * not relevant. These methods may block waiting for more input. + * + *

When a scanner throws an {@link InputMismatchException}, the scanner + * will not pass the token that caused the exception, so that it may be + * retrieved or skipped via some other method. + * + *

Depending upon the type of delimiting pattern, empty tokens may be + * returned. For example, the pattern {@code "\\s+"} will return no empty + * tokens since it matches multiple instances of the delimiter. The delimiting + * pattern {@code "\\s"} could return empty tokens since it only passes one + * space at a time. + * + *

A scanner can read text from any object which implements the {@link + * java.lang.Readable} interface. If an invocation of the underlying + * readable's {@link java.lang.Readable#read read()} method throws an {@link + * java.io.IOException} then the scanner assumes that the end of the input + * has been reached. The most recent {@code IOException} thrown by the + * underlying readable can be retrieved via the {@link #ioException} method. + * + *

When a {@code Scanner} is closed, it will close its input source + * if the source implements the {@link java.io.Closeable} interface. + * + *

A {@code Scanner} is not safe for multithreaded use without + * external synchronization. + * + *

Unless otherwise mentioned, passing a {@code null} parameter into + * any method of a {@code Scanner} will cause a + * {@code NullPointerException} to be thrown. + * + *

A scanner will default to interpreting numbers as decimal unless a + * different radix has been set by using the {@link #useRadix} method. The + * {@link #reset} method will reset the value of the scanner's radix to + * {@code 10} regardless of whether it was previously changed. + * + *

Localized numbers

+ * + *

An instance of this class is capable of scanning numbers in the standard + * formats as well as in the formats of the scanner's locale. A scanner's + * initial locale is the value returned by the {@link + * java.util.Locale#getDefault(Locale.Category) + * Locale.getDefault(Locale.Category.FORMAT)} method; it may be changed via the {@link + * #useLocale useLocale()} method. The {@link #reset} method will reset the value of the + * scanner's locale to the initial locale regardless of whether it was + * previously changed. + * + *

The localized formats are defined in terms of the following parameters, + * which for a particular locale are taken from that locale's {@link + * java.text.DecimalFormat DecimalFormat} object, {@code df}, and its and + * {@link java.text.DecimalFormatSymbols DecimalFormatSymbols} object, + * {@code dfs}. + * + *

+ *
LocalGroupSeparator   + *
The character used to separate thousands groups, + * i.e., {@code dfs.}{@link + * java.text.DecimalFormatSymbols#getGroupingSeparator + * getGroupingSeparator()} + *
LocalDecimalSeparator   + *
The character used for the decimal point, + * i.e., {@code dfs.}{@link + * java.text.DecimalFormatSymbols#getDecimalSeparator + * getDecimalSeparator()} + *
LocalPositivePrefix   + *
The string that appears before a positive number (may + * be empty), i.e., {@code df.}{@link + * java.text.DecimalFormat#getPositivePrefix + * getPositivePrefix()} + *
LocalPositiveSuffix   + *
The string that appears after a positive number (may be + * empty), i.e., {@code df.}{@link + * java.text.DecimalFormat#getPositiveSuffix + * getPositiveSuffix()} + *
LocalNegativePrefix   + *
The string that appears before a negative number (may + * be empty), i.e., {@code df.}{@link + * java.text.DecimalFormat#getNegativePrefix + * getNegativePrefix()} + *
LocalNegativeSuffix   + *
The string that appears after a negative number (may be + * empty), i.e., {@code df.}{@link + * java.text.DecimalFormat#getNegativeSuffix + * getNegativeSuffix()} + *
LocalNaN   + *
The string that represents not-a-number for + * floating-point values, + * i.e., {@code dfs.}{@link + * java.text.DecimalFormatSymbols#getNaN + * getNaN()} + *
LocalInfinity   + *
The string that represents infinity for floating-point + * values, i.e., {@code dfs.}{@link + * java.text.DecimalFormatSymbols#getInfinity + * getInfinity()} + *
+ * + *

Number syntax

+ * + *

The strings that can be parsed as numbers by an instance of this class + * are specified in terms of the following regular-expression grammar, where + * Rmax is the highest digit in the radix being used (for example, Rmax is 9 in base 10). + * + *

+ *
NonAsciiDigit: + *
A non-ASCII character c for which + * {@link java.lang.Character#isDigit Character.isDigit}{@code (c)} + * returns true + * + *
Non0Digit: + *
{@code [1-}Rmax{@code ] | }NonASCIIDigit + * + *
Digit: + *
{@code [0-}Rmax{@code ] | }NonASCIIDigit + * + *
GroupedNumeral: + *
Non0Digit + * Digit{@code ? + * }Digit{@code ?} + *
    LocalGroupSeparator + * Digit + * Digit + * Digit{@code )+ )} + * + *
Numeral: + *
{@code ( ( }Digit{@code + ) + * | }GroupedNumeral{@code )} + * + *
Integer: + *
{@code ( [-+]? ( }Numeral{@code + * ) )} + *
{@code | }LocalPositivePrefix Numeral + * LocalPositiveSuffix + *
{@code | }LocalNegativePrefix Numeral + * LocalNegativeSuffix + * + *
DecimalNumeral: + *
Numeral + *
{@code | }Numeral + * LocalDecimalSeparator + * Digit{@code *} + *
{@code | }LocalDecimalSeparator + * Digit{@code +} + * + *
Exponent: + *
{@code ( [eE] [+-]? }Digit{@code + )} + * + *
Decimal: + *
{@code ( [-+]? }DecimalNumeral + * Exponent{@code ? )} + *
{@code | }LocalPositivePrefix + * DecimalNumeral + * LocalPositiveSuffix + * Exponent{@code ?} + *
{@code | }LocalNegativePrefix + * DecimalNumeral + * LocalNegativeSuffix + * Exponent{@code ?} + * + *
HexFloat: + *
{@code [-+]? 0[xX][0-9a-fA-F]*\.[0-9a-fA-F]+ + * ([pP][-+]?[0-9]+)?} + * + *
NonNumber: + *
{@code NaN + * | }LocalNan{@code + * | Infinity + * | }LocalInfinity + * + *
SignedNonNumber: + *
{@code ( [-+]? }NonNumber{@code )} + *
{@code | }LocalPositivePrefix + * NonNumber + * LocalPositiveSuffix + *
{@code | }LocalNegativePrefix + * NonNumber + * LocalNegativeSuffix + * + *
Float: + *
Decimal + * {@code | }HexFloat + * {@code | }SignedNonNumber + * + *
+ *

Whitespace is not significant in the above regular expressions. + * + * @since 1.5 + */ +@NullMarked +public final class Scanner implements Iterator, Closeable { + + // Internal buffer used to hold input + private CharBuffer buf; + + // Size of internal character buffer + // change to 1024; + private static final int BUFFER_SIZE = 1024; + + // The index into the buffer currently held by the Scanner + private int position; + + // Internal matcher used for finding delimiters + private Matcher matcher; + + // Pattern used to delimit tokens + private Pattern delimPattern; + + // Pattern found in last hasNext operation + private Pattern hasNextPattern; + + // Position after last hasNext operation + private int hasNextPosition; + + // Result after last hasNext operation + private String hasNextResult; + + // The input source + private Readable source; + + // Boolean is true if source is done + private boolean sourceClosed = false; + + // Boolean indicating more input is required + private boolean needInput = false; + + // Boolean indicating if a delim has been skipped this operation + private boolean skipped = false; + + // A store of a position that the scanner may fall back to + private int savedScannerPosition = -1; + + // A cache of the last primitive type scanned + private Object typeCache = null; + + // Boolean indicating if a match result is available + private boolean matchValid = false; + + // Boolean indicating if this scanner has been closed + private boolean closed = false; + + // The current radix used by this scanner + private int radix = 10; + + // The default radix for this scanner + private int defaultRadix = 10; + + // The locale used by this scanner + private Locale locale = null; + + // A cache of the last few recently used Patterns + private PatternLRUCache patternCache = new PatternLRUCache(7); + + // A holder of the last IOException encountered + private IOException lastException; + + // Number of times this scanner's state has been modified. + // Generally incremented on most public APIs and checked + // within spliterator implementations. + int modCount; + + // A pattern for java whitespace + private static Pattern WHITESPACE_PATTERN = Pattern.compile("\\p{javaWhitespace}+"); + + // A pattern for any token + private static Pattern FIND_ANY_PATTERN = Pattern.compile("(?s).*"); + + // A pattern for non-ASCII digits + private static Pattern NON_ASCII_DIGIT = Pattern.compile("[\\p{javaDigit}&&[^0-9]]"); + + // Fields and methods to support scanning primitive types + /** + * Locale dependent values used to scan numbers + */ + private String groupSeparator = "\\,"; + + private String decimalSeparator = "\\."; + + private String nanString = "NaN"; + + private String infinityString = "Infinity"; + + private String positivePrefix = ""; + + private String negativePrefix = "\\-"; + + private String positiveSuffix = ""; + + private String negativeSuffix = ""; + + /** + * Fields and an accessor method to match booleans + */ + private static volatile Pattern boolPattern; + + private static final String BOOLEAN_PATTERN = "true|false"; + + private static Pattern boolPattern() { + Pattern bp = boolPattern; + if (bp == null) + boolPattern = bp = Pattern.compile(BOOLEAN_PATTERN, Pattern.CASE_INSENSITIVE); + return bp; + } + + /** + * Fields and methods to match bytes, shorts, ints, and longs + */ + private Pattern integerPattern; + + private String digits = "0123456789abcdefghijklmnopqrstuvwxyz"; + + private String non0Digit = "[\\p{javaDigit}&&[^0]]"; + + private int SIMPLE_GROUP_INDEX = 5; + + private String buildIntegerPatternString() { + String radixDigits = digits.substring(0, radix); + // \\p{javaDigit} is not guaranteed to be appropriate + // here but what can we do? The final authority will be + // whatever parse method is invoked, so ultimately the + // Scanner will do the right thing + String digit = "((?i)[" + radixDigits + "]|\\p{javaDigit})"; + String groupedNumeral = "(" + non0Digit + digit + "?" + digit + "?(" + groupSeparator + digit + digit + digit + ")+)"; + // digit++ is the possessive form which is necessary for reducing + // backtracking that would otherwise cause unacceptable performance + String numeral = "((" + digit + "++)|" + groupedNumeral + ")"; + String javaStyleInteger = "([-+]?(" + numeral + "))"; + String negativeInteger = negativePrefix + numeral + negativeSuffix; + String positiveInteger = positivePrefix + numeral + positiveSuffix; + return "(" + javaStyleInteger + ")|(" + positiveInteger + ")|(" + negativeInteger + ")"; + } + + private Pattern integerPattern() { + if (integerPattern == null) { + integerPattern = patternCache.forName(buildIntegerPatternString()); + } + return integerPattern; + } + + /** + * Fields and an accessor method to match line separators + */ + private static volatile Pattern separatorPattern; + + private static volatile Pattern linePattern; + + private static final String LINE_SEPARATOR_PATTERN = "\r\n|[\n\r\u2028\u2029\u0085]"; + + private static final String LINE_PATTERN = ".*(" + LINE_SEPARATOR_PATTERN + ")|.+$"; + + private static Pattern separatorPattern() { + Pattern sp = separatorPattern; + if (sp == null) + separatorPattern = sp = Pattern.compile(LINE_SEPARATOR_PATTERN); + return sp; + } + + private static Pattern linePattern() { + Pattern lp = linePattern; + if (lp == null) + linePattern = lp = Pattern.compile(LINE_PATTERN); + return lp; + } + + /** + * Fields and methods to match floats and doubles + */ + private Pattern floatPattern; + + private Pattern decimalPattern; + + private void buildFloatAndDecimalPattern() { + // \\p{javaDigit} may not be perfect, see above + String digit = "([0-9]|(\\p{javaDigit}))"; + String exponent = "([eE][+-]?" + digit + "+)?"; + String groupedNumeral = "(" + non0Digit + digit + "?" + digit + "?(" + groupSeparator + digit + digit + digit + ")+)"; + // Once again digit++ is used for performance, as above + String numeral = "((" + digit + "++)|" + groupedNumeral + ")"; + String decimalNumeral = "(" + numeral + "|" + numeral + decimalSeparator + digit + "*+|" + decimalSeparator + digit + "++)"; + String nonNumber = "(NaN|" + nanString + "|Infinity|" + infinityString + ")"; + String positiveFloat = "(" + positivePrefix + decimalNumeral + positiveSuffix + exponent + ")"; + String negativeFloat = "(" + negativePrefix + decimalNumeral + negativeSuffix + exponent + ")"; + String decimal = "(([-+]?" + decimalNumeral + exponent + ")|" + positiveFloat + "|" + negativeFloat + ")"; + String hexFloat = "[-+]?0[xX][0-9a-fA-F]*\\.[0-9a-fA-F]+([pP][-+]?[0-9]+)?"; + String positiveNonNumber = "(" + positivePrefix + nonNumber + positiveSuffix + ")"; + String negativeNonNumber = "(" + negativePrefix + nonNumber + negativeSuffix + ")"; + String signedNonNumber = "(([-+]?" + nonNumber + ")|" + positiveNonNumber + "|" + negativeNonNumber + ")"; + floatPattern = Pattern.compile(decimal + "|" + hexFloat + "|" + signedNonNumber); + decimalPattern = Pattern.compile(decimal); + } + + private Pattern floatPattern() { + if (floatPattern == null) { + buildFloatAndDecimalPattern(); + } + return floatPattern; + } + + private Pattern decimalPattern() { + if (decimalPattern == null) { + buildFloatAndDecimalPattern(); + } + return decimalPattern; + } + + // Constructors + /** + * Constructs a {@code Scanner} that returns values scanned + * from the specified source delimited by the specified pattern. + * + * @param source A character source implementing the Readable interface + * @param pattern A delimiting pattern + */ + private Scanner(Readable source, Pattern pattern) { + assert source != null : "source should not be null"; + assert pattern != null : "pattern should not be null"; + this.source = source; + delimPattern = pattern; + buf = CharBuffer.allocate(BUFFER_SIZE); + buf.limit(0); + matcher = delimPattern.matcher(buf); + matcher.useTransparentBounds(true); + matcher.useAnchoringBounds(false); + useLocale(Locale.getDefault(Locale.Category.FORMAT)); + } + + /** + * Constructs a new {@code Scanner} that produces values scanned + * from the specified source. + * + * @param source A character source implementing the {@link Readable} + * interface + */ + public Scanner(Readable source) { + this(Objects.requireNonNull(source, "source"), WHITESPACE_PATTERN); + } + + /** + * Constructs a new {@code Scanner} that produces values scanned + * from the specified input stream. Bytes from the stream are converted + * into characters using the underlying platform's + * {@linkplain java.nio.charset.Charset#defaultCharset() default charset}. + * + * @param source An input stream to be scanned + */ + public Scanner(InputStream source) { + this(new InputStreamReader(source), WHITESPACE_PATTERN); + } + + /** + * Constructs a new {@code Scanner} that produces values scanned + * from the specified input stream. Bytes from the stream are converted + * into characters using the specified charset. + * + * @param source An input stream to be scanned + * @param charsetName The encoding type used to convert bytes from the + * stream into characters to be scanned + * @throws IllegalArgumentException if the specified character set + * does not exist + */ + public Scanner(InputStream source, String charsetName) { + this(source, toCharset(charsetName)); + } + + /** + * Constructs a new {@code Scanner} that produces values scanned + * from the specified input stream. Bytes from the stream are converted + * into characters using the specified charset. + * + * @param source an input stream to be scanned + * @param charset the charset used to convert bytes from the file + * into characters to be scanned + * @since 10 + */ + public Scanner(InputStream source, Charset charset) { + this(makeReadable(Objects.requireNonNull(source, "source"), charset), WHITESPACE_PATTERN); + } + + /** + * Returns a charset object for the given charset name. + * @throws NullPointerException is csn is null + * @throws IllegalArgumentException if the charset is not supported + */ + private static Charset toCharset(String csn) { + Objects.requireNonNull(csn, "charsetName"); + try { + return Charset.forName(csn); + } catch (IllegalCharsetNameException | UnsupportedCharsetException e) { + // IllegalArgumentException should be thrown + throw new IllegalArgumentException(e); + } + } + + /* + * This method is added so that null-check on charset can be performed before + * creating InputStream as an existing test required it. + */ + private static Readable makeReadable(Path source, Charset charset) throws IOException { + Objects.requireNonNull(charset, "charset"); + return makeReadable(Files.newInputStream(source), charset); + } + + private static Readable makeReadable(InputStream source, Charset charset) { + Objects.requireNonNull(charset, "charset"); + return new InputStreamReader(source, charset); + } + + /** + * Constructs a new {@code Scanner} that produces values scanned + * from the specified file. Bytes from the file are converted into + * characters using the underlying platform's + * {@linkplain java.nio.charset.Charset#defaultCharset() default charset}. + * + * @param source A file to be scanned + * @throws FileNotFoundException if source is not found + */ + public Scanner(File source) throws FileNotFoundException { + this((ReadableByteChannel) (new FileInputStream(source).getChannel())); + } + + /** + * Constructs a new {@code Scanner} that produces values scanned + * from the specified file. Bytes from the file are converted into + * characters using the specified charset. + * + * @param source A file to be scanned + * @param charsetName The encoding type used to convert bytes from the file + * into characters to be scanned + * @throws FileNotFoundException if source is not found + * @throws IllegalArgumentException if the specified encoding is + * not found + */ + public Scanner(File source, String charsetName) throws FileNotFoundException { + this(Objects.requireNonNull(source), toDecoder(charsetName)); + } + + /** + * Constructs a new {@code Scanner} that produces values scanned + * from the specified file. Bytes from the file are converted into + * characters using the specified charset. + * + * @param source A file to be scanned + * @param charset The charset used to convert bytes from the file + * into characters to be scanned + * @throws IOException + * if an I/O error occurs opening the source + * @since 10 + */ + public Scanner(File source, Charset charset) throws IOException { + this(Objects.requireNonNull(source), charset.newDecoder()); + } + + private Scanner(File source, CharsetDecoder dec) throws FileNotFoundException { + this(makeReadable((ReadableByteChannel) (new FileInputStream(source).getChannel()), dec)); + } + + private static CharsetDecoder toDecoder(String charsetName) { + Objects.requireNonNull(charsetName, "charsetName"); + try { + return Charset.forName(charsetName).newDecoder(); + } catch (IllegalCharsetNameException | UnsupportedCharsetException unused) { + throw new IllegalArgumentException(charsetName); + } + } + + private static Readable makeReadable(ReadableByteChannel source, CharsetDecoder dec) { + return Channels.newReader(source, dec, -1); + } + + private static Readable makeReadable(ReadableByteChannel source, Charset charset) { + Objects.requireNonNull(charset, "charset"); + return Channels.newReader(source, charset); + } + + /** + * Constructs a new {@code Scanner} that produces values scanned + * from the specified file. Bytes from the file are converted into + * characters using the underlying platform's + * {@linkplain java.nio.charset.Charset#defaultCharset() default charset}. + * + * @param source + * the path to the file to be scanned + * @throws IOException + * if an I/O error occurs opening source + * + * @since 1.7 + */ + public Scanner(Path source) throws IOException { + this(Files.newInputStream(source)); + } + + /** + * Constructs a new {@code Scanner} that produces values scanned + * from the specified file. Bytes from the file are converted into + * characters using the specified charset. + * + * @param source + * the path to the file to be scanned + * @param charsetName + * The encoding type used to convert bytes from the file + * into characters to be scanned + * @throws IOException + * if an I/O error occurs opening source + * @throws IllegalArgumentException + * if the specified encoding is not found + * @since 1.7 + */ + public Scanner(Path source, String charsetName) throws IOException { + this(Objects.requireNonNull(source), toCharset(charsetName)); + } + + /** + * Constructs a new {@code Scanner} that produces values scanned + * from the specified file. Bytes from the file are converted into + * characters using the specified charset. + * + * @param source + * the path to the file to be scanned + * @param charset + * the charset used to convert bytes from the file + * into characters to be scanned + * @throws IOException + * if an I/O error occurs opening the source + * @since 10 + */ + public Scanner(Path source, Charset charset) throws IOException { + this(makeReadable(source, charset)); + } + + /** + * Constructs a new {@code Scanner} that produces values scanned + * from the specified string. + * + * @param source A string to scan + */ + public Scanner(String source) { + this(new StringReader(source), WHITESPACE_PATTERN); + } + + /** + * Constructs a new {@code Scanner} that produces values scanned + * from the specified channel. Bytes from the source are converted into + * characters using the underlying platform's + * {@linkplain java.nio.charset.Charset#defaultCharset() default charset}. + * + * @param source A channel to scan + */ + public Scanner(ReadableByteChannel source) { + this(makeReadable(Objects.requireNonNull(source, "source")), WHITESPACE_PATTERN); + } + + private static Readable makeReadable(ReadableByteChannel source) { + return makeReadable(source, Charset.defaultCharset().newDecoder()); + } + + /** + * Constructs a new {@code Scanner} that produces values scanned + * from the specified channel. Bytes from the source are converted into + * characters using the specified charset. + * + * @param source A channel to scan + * @param charsetName The encoding type used to convert bytes from the + * channel into characters to be scanned + * @throws IllegalArgumentException if the specified character set + * does not exist + */ + public Scanner(ReadableByteChannel source, String charsetName) { + this(makeReadable(Objects.requireNonNull(source, "source"), toDecoder(charsetName)), WHITESPACE_PATTERN); + } + + /** + * Constructs a new {@code Scanner} that produces values scanned + * from the specified channel. Bytes from the source are converted into + * characters using the specified charset. + * + * @param source a channel to scan + * @param charset the encoding type used to convert bytes from the + * channel into characters to be scanned + * @since 10 + */ + public Scanner(ReadableByteChannel source, Charset charset) { + this(makeReadable(Objects.requireNonNull(source, "source"), charset), WHITESPACE_PATTERN); + } + + // Private primitives used to support scanning + private void saveState() { + savedScannerPosition = position; + } + + private void revertState() { + this.position = savedScannerPosition; + savedScannerPosition = -1; + skipped = false; + } + + private boolean revertState(boolean b) { + this.position = savedScannerPosition; + savedScannerPosition = -1; + skipped = false; + return b; + } + + private void cacheResult() { + hasNextResult = matcher.group(); + hasNextPosition = matcher.end(); + hasNextPattern = matcher.pattern(); + } + + private void cacheResult(String result) { + hasNextResult = result; + hasNextPosition = matcher.end(); + hasNextPattern = matcher.pattern(); + } + + // Clears both regular cache and type cache + private void clearCaches() { + hasNextPattern = null; + typeCache = null; + } + + // Also clears both the regular cache and the type cache + private String getCachedResult() { + position = hasNextPosition; + hasNextPattern = null; + typeCache = null; + return hasNextResult; + } + + // Also clears both the regular cache and the type cache + private void useTypeCache() { + if (closed) + throw new IllegalStateException("Scanner closed"); + position = hasNextPosition; + hasNextPattern = null; + typeCache = null; + } + + // Tries to read more input. May block. + private void readInput() { + if (buf.limit() == buf.capacity()) + makeSpace(); + // Prepare to receive data + int p = buf.position(); + buf.position(buf.limit()); + buf.limit(buf.capacity()); + int n = 0; + try { + n = source.read(buf); + } catch (IOException ioe) { + lastException = ioe; + n = -1; + } + if (n == -1) { + sourceClosed = true; + needInput = false; + } + if (n > 0) + needInput = false; + // Restore current position and limit for reading + buf.limit(buf.position()); + buf.position(p); + } + + // After this method is called there will either be an exception + // or else there will be space in the buffer + private boolean makeSpace() { + clearCaches(); + int offset = savedScannerPosition == -1 ? position : savedScannerPosition; + buf.position(offset); + // Gain space by compacting buffer + if (offset > 0) { + buf.compact(); + translateSavedIndexes(offset); + position -= offset; + buf.flip(); + return true; + } + // Gain space by growing buffer + int newSize = buf.capacity() * 2; + CharBuffer newBuf = CharBuffer.allocate(newSize); + newBuf.put(buf); + newBuf.flip(); + translateSavedIndexes(offset); + position -= offset; + buf = newBuf; + matcher.reset(buf); + return true; + } + + // When a buffer compaction/reallocation occurs the saved indexes must + // be modified appropriately + private void translateSavedIndexes(int offset) { + if (savedScannerPosition != -1) + savedScannerPosition -= offset; + } + + // If we are at the end of input then NoSuchElement; + // If there is still input left then InputMismatch + private void throwFor() { + skipped = false; + if ((sourceClosed) && (position == buf.limit())) + throw new NoSuchElementException(); + else + throw new InputMismatchException(); + } + + // Returns true if a complete token or partial token is in the buffer. + // It is not necessary to find a complete token since a partial token + // means that there will be another token with or without more input. + private boolean hasTokenInBuffer() { + matchValid = false; + matcher.usePattern(delimPattern); + matcher.region(position, buf.limit()); + // Skip delims first + if (matcher.lookingAt()) { + if (matcher.hitEnd() && !sourceClosed) { + // more input might change the match of delims, in which + // might change whether or not if there is token left in + // buffer (don't update the "position" in this case) + needInput = true; + return false; + } + position = matcher.end(); + } + // If we are sitting at the end, no more tokens in buffer + if (position == buf.limit()) + return false; + return true; + } + + /* + * Returns a "complete token" that matches the specified pattern + * + * A token is complete if surrounded by delims; a partial token + * is prefixed by delims but not postfixed by them + * + * The position is advanced to the end of that complete token + * + * Pattern == null means accept any token at all + * + * Triple return: + * 1. valid string means it was found + * 2. null with needInput=false means we won't ever find it + * 3. null with needInput=true means try again after readInput + */ + private String getCompleteTokenInBuffer(Pattern pattern) { + matchValid = false; + // Skip delims first + matcher.usePattern(delimPattern); + if (!skipped) { + // Enforcing only one skip of leading delims + matcher.region(position, buf.limit()); + if (matcher.lookingAt()) { + // If more input could extend the delimiters then we must wait + // for more input + if (matcher.hitEnd() && !sourceClosed) { + needInput = true; + return null; + } + // The delims were whole and the matcher should skip them + skipped = true; + position = matcher.end(); + } + } + // If we are sitting at the end, no more tokens in buffer + if (position == buf.limit()) { + if (sourceClosed) + return null; + needInput = true; + return null; + } + // Must look for next delims. Simply attempting to match the + // pattern at this point may find a match but it might not be + // the first longest match because of missing input, or it might + // match a partial token instead of the whole thing. + // Then look for next delims + matcher.region(position, buf.limit()); + boolean foundNextDelim = matcher.find(); + if (foundNextDelim && (matcher.end() == position)) { + // Zero length delimiter match; we should find the next one + // using the automatic advance past a zero length match; + // Otherwise we have just found the same one we just skipped + foundNextDelim = matcher.find(); + } + if (foundNextDelim) { + // In the rare case that more input could cause the match + // to be lost and there is more input coming we must wait + // for more input. Note that hitting the end is okay as long + // as the match cannot go away. It is the beginning of the + // next delims we want to be sure about, we don't care if + // they potentially extend further. + if (matcher.requireEnd() && !sourceClosed) { + needInput = true; + return null; + } + int tokenEnd = matcher.start(); + // There is a complete token. + if (pattern == null) { + // Must continue with match to provide valid MatchResult + pattern = FIND_ANY_PATTERN; + } + // Attempt to match against the desired pattern + matcher.usePattern(pattern); + matcher.region(position, tokenEnd); + if (matcher.matches()) { + String s = matcher.group(); + position = matcher.end(); + return s; + } else { + // Complete token but it does not match + return null; + } + } + // If we can't find the next delims but no more input is coming, + // then we can treat the remainder as a whole token + if (sourceClosed) { + if (pattern == null) { + // Must continue with match to provide valid MatchResult + pattern = FIND_ANY_PATTERN; + } + // Last token; Match the pattern here or throw + matcher.usePattern(pattern); + matcher.region(position, buf.limit()); + if (matcher.matches()) { + String s = matcher.group(); + position = matcher.end(); + return s; + } + // Last piece does not match + return null; + } + // There is a partial token in the buffer; must read more + // to complete it + needInput = true; + return null; + } + + // Finds the specified pattern in the buffer up to horizon. + // Returns true if the specified input pattern was matched, + // and leaves the matcher field with the current match state. + private boolean findPatternInBuffer(Pattern pattern, int horizon) { + matchValid = false; + matcher.usePattern(pattern); + int bufferLimit = buf.limit(); + int horizonLimit = -1; + int searchLimit = bufferLimit; + if (horizon > 0) { + horizonLimit = position + horizon; + if (horizonLimit < bufferLimit) + searchLimit = horizonLimit; + } + matcher.region(position, searchLimit); + if (matcher.find()) { + if (matcher.hitEnd() && (!sourceClosed)) { + // The match may be longer if didn't hit horizon or real end + if (searchLimit != horizonLimit) { + // Hit an artificial end; try to extend the match + needInput = true; + return false; + } + // The match could go away depending on what is next + if ((searchLimit == horizonLimit) && matcher.requireEnd()) { + // Rare case: we hit the end of input and it happens + // that it is at the horizon and the end of input is + // required for the match. + needInput = true; + return false; + } + } + // Did not hit end, or hit real end, or hit horizon + position = matcher.end(); + return true; + } + if (sourceClosed) + return false; + // If there is no specified horizon, or if we have not searched + // to the specified horizon yet, get more input + if ((horizon == 0) || (searchLimit != horizonLimit)) + needInput = true; + return false; + } + + // Attempts to match a pattern anchored at the current position. + // Returns true if the specified input pattern was matched, + // and leaves the matcher field with the current match state. + private boolean matchPatternInBuffer(Pattern pattern) { + matchValid = false; + matcher.usePattern(pattern); + matcher.region(position, buf.limit()); + if (matcher.lookingAt()) { + if (matcher.hitEnd() && (!sourceClosed)) { + // Get more input and try again + needInput = true; + return false; + } + position = matcher.end(); + return true; + } + if (sourceClosed) + return false; + // Read more to find pattern + needInput = true; + return false; + } + + // Throws if the scanner is closed + private void ensureOpen() { + if (closed) + throw new IllegalStateException("Scanner closed"); + } + + // Public methods + /** + * Closes this scanner. + * + *

If this scanner has not yet been closed then if its underlying + * {@linkplain java.lang.Readable readable} also implements the {@link + * java.io.Closeable} interface then the readable's {@code close} method + * will be invoked. If this scanner is already closed then invoking this + * method will have no effect. + * + *

Attempting to perform search operations after a scanner has + * been closed will result in an {@link IllegalStateException}. + */ + public void close() { + if (closed) + return; + if (source instanceof Closeable) { + try { + ((Closeable) source).close(); + } catch (IOException ioe) { + lastException = ioe; + } + } + sourceClosed = true; + source = null; + closed = true; + } + + /** + * Returns the {@code IOException} last thrown by this + * {@code Scanner}'s underlying {@code Readable}. This method + * returns {@code null} if no such exception exists. + * + * @return the last exception thrown by this scanner's readable + */ + @Nullable + public IOException ioException() { + return lastException; + } + + /** + * Returns the {@code Pattern} this {@code Scanner} is currently + * using to match delimiters. + * + * @return this scanner's delimiting pattern. + */ + public Pattern delimiter() { + return delimPattern; + } + + /** + * Sets this scanner's delimiting pattern to the specified pattern. + * + * @param pattern A delimiting pattern + * @return this scanner + */ + public Scanner useDelimiter(Pattern pattern) { + modCount++; + delimPattern = pattern; + return this; + } + + /** + * Sets this scanner's delimiting pattern to a pattern constructed from + * the specified {@code String}. + * + *

An invocation of this method of the form + * {@code useDelimiter(pattern)} behaves in exactly the same way as the + * invocation {@code useDelimiter(Pattern.compile(pattern))}. + * + *

Invoking the {@link #reset} method will set the scanner's delimiter + * to the default. + * + * @param pattern A string specifying a delimiting pattern + * @return this scanner + */ + public Scanner useDelimiter(String pattern) { + modCount++; + delimPattern = patternCache.forName(pattern); + return this; + } + + /** + * Returns this scanner's locale. + * + *

A scanner's locale affects many elements of its default + * primitive matching regular expressions; see + * localized numbers above. + * + * @return this scanner's locale + */ + public Locale locale() { + return this.locale; + } + + /** + * Sets this scanner's locale to the specified locale. + * + *

A scanner's locale affects many elements of its default + * primitive matching regular expressions; see + * localized numbers above. + * + *

Invoking the {@link #reset} method will set the scanner's locale to + * the initial locale. + * + * @param locale A string specifying the locale to use + * @return this scanner + */ + public Scanner useLocale(Locale locale) { + if (locale.equals(this.locale)) + return this; + modCount++; + this.locale = locale; + DecimalFormat df = null; + NumberFormat nf = NumberFormat.getNumberInstance(locale); + DecimalFormatSymbols dfs = DecimalFormatSymbols.getInstance(locale); + if (nf instanceof DecimalFormat) { + df = (DecimalFormat) nf; + } else { + // In case where NumberFormat.getNumberInstance() returns + // other instance (non DecimalFormat) based on the provider + // used and java.text.spi.NumberFormatProvider implementations, + // DecimalFormat constructor is used to obtain the instance + LocaleProviderAdapter adapter = LocaleProviderAdapter.getAdapter(NumberFormatProvider.class, locale); + if (!(adapter instanceof ResourceBundleBasedAdapter)) { + adapter = LocaleProviderAdapter.getResourceBundleBased(); + } + String[] all = adapter.getLocaleResources(locale).getNumberPatterns(); + df = new DecimalFormat(all[0], dfs); + } + // These must be literalized to avoid collision with regex + // metacharacters such as dot or parenthesis + groupSeparator = "\\" + dfs.getGroupingSeparator(); + decimalSeparator = "\\" + dfs.getDecimalSeparator(); + // Quoting the nonzero length locale-specific things + // to avoid potential conflict with metacharacters + nanString = "\\Q" + dfs.getNaN() + "\\E"; + infinityString = "\\Q" + dfs.getInfinity() + "\\E"; + positivePrefix = df.getPositivePrefix(); + if (positivePrefix.length() > 0) + positivePrefix = "\\Q" + positivePrefix + "\\E"; + negativePrefix = df.getNegativePrefix(); + if (negativePrefix.length() > 0) + negativePrefix = "\\Q" + negativePrefix + "\\E"; + positiveSuffix = df.getPositiveSuffix(); + if (positiveSuffix.length() > 0) + positiveSuffix = "\\Q" + positiveSuffix + "\\E"; + negativeSuffix = df.getNegativeSuffix(); + if (negativeSuffix.length() > 0) + negativeSuffix = "\\Q" + negativeSuffix + "\\E"; + // Force rebuilding and recompilation of locale dependent + // primitive patterns + integerPattern = null; + floatPattern = null; + return this; + } + + /** + * Returns this scanner's default radix. + * + *

A scanner's radix affects elements of its default + * number matching regular expressions; see + * localized numbers above. + * + * @return the default radix of this scanner + */ + public int radix() { + return this.defaultRadix; + } + + /** + * Sets this scanner's default radix to the specified radix. + * + *

A scanner's radix affects elements of its default + * number matching regular expressions; see + * localized numbers above. + * + *

If the radix is less than {@link Character#MIN_RADIX Character.MIN_RADIX} + * or greater than {@link Character#MAX_RADIX Character.MAX_RADIX}, then an + * {@code IllegalArgumentException} is thrown. + * + *

Invoking the {@link #reset} method will set the scanner's radix to + * {@code 10}. + * + * @param radix The radix to use when scanning numbers + * @return this scanner + * @throws IllegalArgumentException if radix is out of range + */ + public Scanner useRadix(int radix) { + if ((radix < Character.MIN_RADIX) || (radix > Character.MAX_RADIX)) + throw new IllegalArgumentException("radix:" + radix); + if (this.defaultRadix == radix) + return this; + modCount++; + this.defaultRadix = radix; + // Force rebuilding and recompilation of radix dependent patterns + integerPattern = null; + return this; + } + + // The next operation should occur in the specified radix but + // the default is left untouched. + private void setRadix(int radix) { + if ((radix < Character.MIN_RADIX) || (radix > Character.MAX_RADIX)) + throw new IllegalArgumentException("radix:" + radix); + if (this.radix != radix) { + // Force rebuilding and recompilation of radix dependent patterns + integerPattern = null; + this.radix = radix; + } + } + + /** + * Returns the match result of the last scanning operation performed + * by this scanner. This method throws {@code IllegalStateException} + * if no match has been performed, or if the last match was + * not successful. + * + *

The various {@code next} methods of {@code Scanner} + * make a match result available if they complete without throwing an + * exception. For instance, after an invocation of the {@link #nextInt} + * method that returned an int, this method returns a + * {@code MatchResult} for the search of the + * Integer regular expression + * defined above. Similarly the {@link #findInLine findInLine()}, + * {@link #findWithinHorizon findWithinHorizon()}, and {@link #skip skip()} + * methods will make a match available if they succeed. + * + * @return a match result for the last match operation + * @throws IllegalStateException If no match result is available + */ + public MatchResult match() { + if (!matchValid) + throw new IllegalStateException("No match result available"); + return matcher.toMatchResult(); + } + + /** + *

Returns the string representation of this {@code Scanner}. The + * string representation of a {@code Scanner} contains information + * that may be useful for debugging. The exact format is unspecified. + * + * @return The string representation of this scanner + */ + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("java.util.Scanner"); + sb.append("[delimiters=" + delimPattern + "]"); + sb.append("[position=" + position + "]"); + sb.append("[match valid=" + matchValid + "]"); + sb.append("[need input=" + needInput + "]"); + sb.append("[source closed=" + sourceClosed + "]"); + sb.append("[skipped=" + skipped + "]"); + sb.append("[group separator=" + groupSeparator + "]"); + sb.append("[decimal separator=" + decimalSeparator + "]"); + sb.append("[positive prefix=" + positivePrefix + "]"); + sb.append("[negative prefix=" + negativePrefix + "]"); + sb.append("[positive suffix=" + positiveSuffix + "]"); + sb.append("[negative suffix=" + negativeSuffix + "]"); + sb.append("[NaN string=" + nanString + "]"); + sb.append("[infinity string=" + infinityString + "]"); + return sb.toString(); + } + + /** + * Returns true if this scanner has another token in its input. + * This method may block while waiting for input to scan. + * The scanner does not advance past any input. + * + * @return true if and only if this scanner has another token + * @throws IllegalStateException if this scanner is closed + * @see java.util.Iterator + */ + public boolean hasNext() { + ensureOpen(); + saveState(); + modCount++; + while (!sourceClosed) { + if (hasTokenInBuffer()) { + return revertState(true); + } + readInput(); + } + boolean result = hasTokenInBuffer(); + return revertState(result); + } + + /** + * Finds and returns the next complete token from this scanner. + * A complete token is preceded and followed by input that matches + * the delimiter pattern. This method may block while waiting for input + * to scan, even if a previous invocation of {@link #hasNext} returned + * {@code true}. + * + * @return the next token + * @throws NoSuchElementException if no more tokens are available + * @throws IllegalStateException if this scanner is closed + * @see java.util.Iterator + */ + public String next() { + ensureOpen(); + clearCaches(); + modCount++; + while (true) { + String token = getCompleteTokenInBuffer(null); + if (token != null) { + matchValid = true; + skipped = false; + return token; + } + if (needInput) + readInput(); + else + throwFor(); + } + } + + /** + * The remove operation is not supported by this implementation of + * {@code Iterator}. + * + * @throws UnsupportedOperationException if this method is invoked. + * @see java.util.Iterator + */ + public void remove() { + throw new UnsupportedOperationException(); + } + + /** + * Returns true if the next token matches the pattern constructed from the + * specified string. The scanner does not advance past any input. + * + *

An invocation of this method of the form {@code hasNext(pattern)} + * behaves in exactly the same way as the invocation + * {@code hasNext(Pattern.compile(pattern))}. + * + * @param pattern a string specifying the pattern to scan + * @return true if and only if this scanner has another token matching + * the specified pattern + * @throws IllegalStateException if this scanner is closed + */ + public boolean hasNext(String pattern) { + return hasNext(patternCache.forName(pattern)); + } + + /** + * Returns the next token if it matches the pattern constructed from the + * specified string. If the match is successful, the scanner advances + * past the input that matched the pattern. + * + *

An invocation of this method of the form {@code next(pattern)} + * behaves in exactly the same way as the invocation + * {@code next(Pattern.compile(pattern))}. + * + * @param pattern a string specifying the pattern to scan + * @return the next token + * @throws NoSuchElementException if no such tokens are available + * @throws IllegalStateException if this scanner is closed + */ + public String next(String pattern) { + return next(patternCache.forName(pattern)); + } + + /** + * Returns true if the next complete token matches the specified pattern. + * A complete token is prefixed and postfixed by input that matches + * the delimiter pattern. This method may block while waiting for input. + * The scanner does not advance past any input. + * + * @param pattern the pattern to scan for + * @return true if and only if this scanner has another token matching + * the specified pattern + * @throws IllegalStateException if this scanner is closed + */ + public boolean hasNext(Pattern pattern) { + ensureOpen(); + if (pattern == null) + throw new NullPointerException(); + hasNextPattern = null; + saveState(); + modCount++; + while (true) { + if (getCompleteTokenInBuffer(pattern) != null) { + matchValid = true; + cacheResult(); + return revertState(true); + } + if (needInput) + readInput(); + else + return revertState(false); + } + } + + /** + * Returns the next token if it matches the specified pattern. This + * method may block while waiting for input to scan, even if a previous + * invocation of {@link #hasNext(Pattern)} returned {@code true}. + * If the match is successful, the scanner advances past the input that + * matched the pattern. + * + * @param pattern the pattern to scan for + * @return the next token + * @throws NoSuchElementException if no more tokens are available + * @throws IllegalStateException if this scanner is closed + */ + public String next(Pattern pattern) { + ensureOpen(); + if (pattern == null) + throw new NullPointerException(); + modCount++; + // Did we already find this pattern? + if (hasNextPattern == pattern) + return getCachedResult(); + clearCaches(); + // Search for the pattern + while (true) { + String token = getCompleteTokenInBuffer(pattern); + if (token != null) { + matchValid = true; + skipped = false; + return token; + } + if (needInput) + readInput(); + else + throwFor(); + } + } + + /** + * Returns true if there is another line in the input of this scanner. + * This method may block while waiting for input. The scanner does not + * advance past any input. + * + * @return true if and only if this scanner has another line of input + * @throws IllegalStateException if this scanner is closed + */ + public boolean hasNextLine() { + saveState(); + modCount++; + String result = findWithinHorizon(linePattern(), 0); + if (result != null) { + MatchResult mr = this.match(); + String lineSep = mr.group(1); + if (lineSep != null) { + result = result.substring(0, result.length() - lineSep.length()); + cacheResult(result); + } else { + cacheResult(); + } + } + revertState(); + return (result != null); + } + + /** + * Advances this scanner past the current line and returns the input + * that was skipped. + * + * This method returns the rest of the current line, excluding any line + * separator at the end. The position is set to the beginning of the next + * line. + * + *

Since this method continues to search through the input looking + * for a line separator, it may buffer all of the input searching for + * the line to skip if no line separators are present. + * + * @return the line that was skipped + * @throws NoSuchElementException if no line was found + * @throws IllegalStateException if this scanner is closed + */ + public String nextLine() { + modCount++; + if (hasNextPattern == linePattern()) + return getCachedResult(); + clearCaches(); + String result = findWithinHorizon(linePattern, 0); + if (result == null) + throw new NoSuchElementException("No line found"); + MatchResult mr = this.match(); + String lineSep = mr.group(1); + if (lineSep != null) + result = result.substring(0, result.length() - lineSep.length()); + if (result == null) + throw new NoSuchElementException(); + else + return result; + } + + // Public methods that ignore delimiters + /** + * Attempts to find the next occurrence of a pattern constructed from the + * specified string, ignoring delimiters. + * + *

An invocation of this method of the form {@code findInLine(pattern)} + * behaves in exactly the same way as the invocation + * {@code findInLine(Pattern.compile(pattern))}. + * + * @param pattern a string specifying the pattern to search for + * @return the text that matched the specified pattern + * @throws IllegalStateException if this scanner is closed + */ + @Nullable + public String findInLine(String pattern) { + return findInLine(patternCache.forName(pattern)); + } + + /** + * Attempts to find the next occurrence of the specified pattern ignoring + * delimiters. If the pattern is found before the next line separator, the + * scanner advances past the input that matched and returns the string that + * matched the pattern. + * If no such pattern is detected in the input up to the next line + * separator, then {@code null} is returned and the scanner's + * position is unchanged. This method may block waiting for input that + * matches the pattern. + * + *

Since this method continues to search through the input looking + * for the specified pattern, it may buffer all of the input searching for + * the desired token if no line separators are present. + * + * @param pattern the pattern to scan for + * @return the text that matched the specified pattern + * @throws IllegalStateException if this scanner is closed + */ + @Nullable + public String findInLine(Pattern pattern) { + ensureOpen(); + if (pattern == null) + throw new NullPointerException(); + clearCaches(); + modCount++; + // Expand buffer to include the next newline or end of input + int endPosition = 0; + saveState(); + while (true) { + if (findPatternInBuffer(separatorPattern(), 0)) { + endPosition = matcher.start(); + // up to next newline + break; + } + if (needInput) { + readInput(); + } else { + endPosition = buf.limit(); + // up to end of input + break; + } + } + revertState(); + int horizonForLine = endPosition - position; + // If there is nothing between the current pos and the next + // newline simply return null, invoking findWithinHorizon + // with "horizon=0" will scan beyond the line bound. + if (horizonForLine == 0) + return null; + // Search for the pattern + return findWithinHorizon(pattern, horizonForLine); + } + + /** + * Attempts to find the next occurrence of a pattern constructed from the + * specified string, ignoring delimiters. + * + *

An invocation of this method of the form + * {@code findWithinHorizon(pattern)} behaves in exactly the same way as + * the invocation + * {@code findWithinHorizon(Pattern.compile(pattern), horizon)}. + * + * @param pattern a string specifying the pattern to search for + * @param horizon the search horizon + * @return the text that matched the specified pattern + * @throws IllegalStateException if this scanner is closed + * @throws IllegalArgumentException if horizon is negative + */ + @Nullable + public String findWithinHorizon(String pattern, int horizon) { + return findWithinHorizon(patternCache.forName(pattern), horizon); + } + + /** + * Attempts to find the next occurrence of the specified pattern. + * + *

This method searches through the input up to the specified + * search horizon, ignoring delimiters. If the pattern is found the + * scanner advances past the input that matched and returns the string + * that matched the pattern. If no such pattern is detected then the + * null is returned and the scanner's position remains unchanged. This + * method may block waiting for input that matches the pattern. + * + *

A scanner will never search more than {@code horizon} code + * points beyond its current position. Note that a match may be clipped + * by the horizon; that is, an arbitrary match result may have been + * different if the horizon had been larger. The scanner treats the + * horizon as a transparent, non-anchoring bound (see {@link + * Matcher#useTransparentBounds} and {@link Matcher#useAnchoringBounds}). + * + *

If horizon is {@code 0}, then the horizon is ignored and + * this method continues to search through the input looking for the + * specified pattern without bound. In this case it may buffer all of + * the input searching for the pattern. + * + *

If horizon is negative, then an IllegalArgumentException is + * thrown. + * + * @param pattern the pattern to scan for + * @param horizon the search horizon + * @return the text that matched the specified pattern + * @throws IllegalStateException if this scanner is closed + * @throws IllegalArgumentException if horizon is negative + */ + @Nullable + public String findWithinHorizon(Pattern pattern, int horizon) { + ensureOpen(); + if (pattern == null) + throw new NullPointerException(); + if (horizon < 0) + throw new IllegalArgumentException("horizon < 0"); + clearCaches(); + modCount++; + // Search for the pattern + while (true) { + if (findPatternInBuffer(pattern, horizon)) { + matchValid = true; + return matcher.group(); + } + if (needInput) + readInput(); + else + // up to end of input + break; + } + return null; + } + + /** + * Skips input that matches the specified pattern, ignoring delimiters. + * This method will skip input if an anchored match of the specified + * pattern succeeds. + * + *

If a match to the specified pattern is not found at the + * current position, then no input is skipped and a + * {@code NoSuchElementException} is thrown. + * + *

Since this method seeks to match the specified pattern starting at + * the scanner's current position, patterns that can match a lot of + * input (".*", for example) may cause the scanner to buffer a large + * amount of input. + * + *

Note that it is possible to skip something without risking a + * {@code NoSuchElementException} by using a pattern that can + * match nothing, e.g., {@code sc.skip("[ \t]*")}. + * + * @param pattern a string specifying the pattern to skip over + * @return this scanner + * @throws NoSuchElementException if the specified pattern is not found + * @throws IllegalStateException if this scanner is closed + */ + public Scanner skip(Pattern pattern) { + ensureOpen(); + if (pattern == null) + throw new NullPointerException(); + clearCaches(); + modCount++; + // Search for the pattern + while (true) { + if (matchPatternInBuffer(pattern)) { + matchValid = true; + position = matcher.end(); + return this; + } + if (needInput) + readInput(); + else + throw new NoSuchElementException(); + } + } + + /** + * Skips input that matches a pattern constructed from the specified + * string. + * + *

An invocation of this method of the form {@code skip(pattern)} + * behaves in exactly the same way as the invocation + * {@code skip(Pattern.compile(pattern))}. + * + * @param pattern a string specifying the pattern to skip over + * @return this scanner + * @throws IllegalStateException if this scanner is closed + */ + public Scanner skip(String pattern) { + return skip(patternCache.forName(pattern)); + } + + // Convenience methods for scanning primitives + /** + * Returns true if the next token in this scanner's input can be + * interpreted as a boolean value using a case insensitive pattern + * created from the string "true|false". The scanner does not + * advance past the input that matched. + * + * @return true if and only if this scanner's next token is a valid + * boolean value + * @throws IllegalStateException if this scanner is closed + */ + public boolean hasNextBoolean() { + return hasNext(boolPattern()); + } + + /** + * Scans the next token of the input into a boolean value and returns + * that value. This method will throw {@code InputMismatchException} + * if the next token cannot be translated into a valid boolean value. + * If the match is successful, the scanner advances past the input that + * matched. + * + * @return the boolean scanned from the input + * @throws InputMismatchException if the next token is not a valid boolean + * @throws NoSuchElementException if input is exhausted + * @throws IllegalStateException if this scanner is closed + */ + public boolean nextBoolean() { + clearCaches(); + return Boolean.parseBoolean(next(boolPattern())); + } + + /** + * Returns true if the next token in this scanner's input can be + * interpreted as a byte value in the default radix using the + * {@link #nextByte} method. The scanner does not advance past any input. + * + * @return true if and only if this scanner's next token is a valid + * byte value + * @throws IllegalStateException if this scanner is closed + */ + public boolean hasNextByte() { + return hasNextByte(defaultRadix); + } + + /** + * Returns true if the next token in this scanner's input can be + * interpreted as a byte value in the specified radix using the + * {@link #nextByte} method. The scanner does not advance past any input. + * + *

If the radix is less than {@link Character#MIN_RADIX Character.MIN_RADIX} + * or greater than {@link Character#MAX_RADIX Character.MAX_RADIX}, then an + * {@code IllegalArgumentException} is thrown. + * + * @param radix the radix used to interpret the token as a byte value + * @return true if and only if this scanner's next token is a valid + * byte value + * @throws IllegalStateException if this scanner is closed + * @throws IllegalArgumentException if the radix is out of range + */ + public boolean hasNextByte(int radix) { + setRadix(radix); + boolean result = hasNext(integerPattern()); + if (result) { + // Cache it + try { + String s = (matcher.group(SIMPLE_GROUP_INDEX) == null) ? processIntegerToken(hasNextResult) : hasNextResult; + typeCache = Byte.parseByte(s, radix); + } catch (NumberFormatException nfe) { + result = false; + } + } + return result; + } + + /** + * Scans the next token of the input as a {@code byte}. + * + *

An invocation of this method of the form + * {@code nextByte()} behaves in exactly the same way as the + * invocation {@code nextByte(radix)}, where {@code radix} + * is the default radix of this scanner. + * + * @return the {@code byte} scanned from the input + * @throws InputMismatchException + * if the next token does not match the Integer + * regular expression, or is out of range + * @throws NoSuchElementException if input is exhausted + * @throws IllegalStateException if this scanner is closed + */ + public byte nextByte() { + return nextByte(defaultRadix); + } + + /** + * Scans the next token of the input as a {@code byte}. + * This method will throw {@code InputMismatchException} + * if the next token cannot be translated into a valid byte value as + * described below. If the translation is successful, the scanner advances + * past the input that matched. + * + *

If the next token matches the Integer regular expression defined + * above then the token is converted into a {@code byte} value as if by + * removing all locale specific prefixes, group separators, and locale + * specific suffixes, then mapping non-ASCII digits into ASCII + * digits via {@link Character#digit Character.digit}, prepending a + * negative sign (-) if the locale specific negative prefixes and suffixes + * were present, and passing the resulting string to + * {@link Byte#parseByte(String, int) Byte.parseByte} with the + * specified radix. + * + *

If the radix is less than {@link Character#MIN_RADIX Character.MIN_RADIX} + * or greater than {@link Character#MAX_RADIX Character.MAX_RADIX}, then an + * {@code IllegalArgumentException} is thrown. + * + * @param radix the radix used to interpret the token as a byte value + * @return the {@code byte} scanned from the input + * @throws InputMismatchException + * if the next token does not match the Integer + * regular expression, or is out of range + * @throws NoSuchElementException if input is exhausted + * @throws IllegalStateException if this scanner is closed + * @throws IllegalArgumentException if the radix is out of range + */ + public byte nextByte(int radix) { + // Check cached result + if ((typeCache != null) && (typeCache instanceof Byte) && this.radix == radix) { + byte val = ((Byte) typeCache).byteValue(); + useTypeCache(); + return val; + } + setRadix(radix); + clearCaches(); + // Search for next byte + try { + String s = next(integerPattern()); + if (matcher.group(SIMPLE_GROUP_INDEX) == null) + s = processIntegerToken(s); + return Byte.parseByte(s, radix); + } catch (NumberFormatException nfe) { + // don't skip bad token + position = matcher.start(); + throw new InputMismatchException(nfe.getMessage()); + } + } + + /** + * Returns true if the next token in this scanner's input can be + * interpreted as a short value in the default radix using the + * {@link #nextShort} method. The scanner does not advance past any input. + * + * @return true if and only if this scanner's next token is a valid + * short value in the default radix + * @throws IllegalStateException if this scanner is closed + */ + public boolean hasNextShort() { + return hasNextShort(defaultRadix); + } + + /** + * Returns true if the next token in this scanner's input can be + * interpreted as a short value in the specified radix using the + * {@link #nextShort} method. The scanner does not advance past any input. + * + *

If the radix is less than {@link Character#MIN_RADIX Character.MIN_RADIX} + * or greater than {@link Character#MAX_RADIX Character.MAX_RADIX}, then an + * {@code IllegalArgumentException} is thrown. + * + * @param radix the radix used to interpret the token as a short value + * @return true if and only if this scanner's next token is a valid + * short value in the specified radix + * @throws IllegalStateException if this scanner is closed + * @throws IllegalArgumentException if the radix is out of range + */ + public boolean hasNextShort(int radix) { + setRadix(radix); + boolean result = hasNext(integerPattern()); + if (result) { + // Cache it + try { + String s = (matcher.group(SIMPLE_GROUP_INDEX) == null) ? processIntegerToken(hasNextResult) : hasNextResult; + typeCache = Short.parseShort(s, radix); + } catch (NumberFormatException nfe) { + result = false; + } + } + return result; + } + + /** + * Scans the next token of the input as a {@code short}. + * + *

An invocation of this method of the form + * {@code nextShort()} behaves in exactly the same way as the + * invocation {@link #nextShort(int) nextShort(radix)}, where {@code radix} + * is the default radix of this scanner. + * + * @return the {@code short} scanned from the input + * @throws InputMismatchException + * if the next token does not match the Integer + * regular expression, or is out of range + * @throws NoSuchElementException if input is exhausted + * @throws IllegalStateException if this scanner is closed + */ + public short nextShort() { + return nextShort(defaultRadix); + } + + /** + * Scans the next token of the input as a {@code short}. + * This method will throw {@code InputMismatchException} + * if the next token cannot be translated into a valid short value as + * described below. If the translation is successful, the scanner advances + * past the input that matched. + * + *

If the next token matches the Integer regular expression defined + * above then the token is converted into a {@code short} value as if by + * removing all locale specific prefixes, group separators, and locale + * specific suffixes, then mapping non-ASCII digits into ASCII + * digits via {@link Character#digit Character.digit}, prepending a + * negative sign (-) if the locale specific negative prefixes and suffixes + * were present, and passing the resulting string to + * {@link Short#parseShort(String, int) Short.parseShort} with the + * specified radix. + * + *

If the radix is less than {@link Character#MIN_RADIX Character.MIN_RADIX} + * or greater than {@link Character#MAX_RADIX Character.MAX_RADIX}, then an + * {@code IllegalArgumentException} is thrown. + * + * @param radix the radix used to interpret the token as a short value + * @return the {@code short} scanned from the input + * @throws InputMismatchException + * if the next token does not match the Integer + * regular expression, or is out of range + * @throws NoSuchElementException if input is exhausted + * @throws IllegalStateException if this scanner is closed + * @throws IllegalArgumentException if the radix is out of range + */ + public short nextShort(int radix) { + // Check cached result + if ((typeCache != null) && (typeCache instanceof Short) && this.radix == radix) { + short val = ((Short) typeCache).shortValue(); + useTypeCache(); + return val; + } + setRadix(radix); + clearCaches(); + // Search for next short + try { + String s = next(integerPattern()); + if (matcher.group(SIMPLE_GROUP_INDEX) == null) + s = processIntegerToken(s); + return Short.parseShort(s, radix); + } catch (NumberFormatException nfe) { + // don't skip bad token + position = matcher.start(); + throw new InputMismatchException(nfe.getMessage()); + } + } + + /** + * Returns true if the next token in this scanner's input can be + * interpreted as an int value in the default radix using the + * {@link #nextInt} method. The scanner does not advance past any input. + * + * @return true if and only if this scanner's next token is a valid + * int value + * @throws IllegalStateException if this scanner is closed + */ + public boolean hasNextInt() { + return hasNextInt(defaultRadix); + } + + /** + * Returns true if the next token in this scanner's input can be + * interpreted as an int value in the specified radix using the + * {@link #nextInt} method. The scanner does not advance past any input. + * + *

If the radix is less than {@link Character#MIN_RADIX Character.MIN_RADIX} + * or greater than {@link Character#MAX_RADIX Character.MAX_RADIX}, then an + * {@code IllegalArgumentException} is thrown. + * + * @param radix the radix used to interpret the token as an int value + * @return true if and only if this scanner's next token is a valid + * int value + * @throws IllegalStateException if this scanner is closed + * @throws IllegalArgumentException if the radix is out of range + */ + public boolean hasNextInt(int radix) { + setRadix(radix); + boolean result = hasNext(integerPattern()); + if (result) { + // Cache it + try { + String s = (matcher.group(SIMPLE_GROUP_INDEX) == null) ? processIntegerToken(hasNextResult) : hasNextResult; + typeCache = Integer.parseInt(s, radix); + } catch (NumberFormatException nfe) { + result = false; + } + } + return result; + } + + /** + * The integer token must be stripped of prefixes, group separators, + * and suffixes, non ascii digits must be converted into ascii digits + * before parse will accept it. + */ + private String processIntegerToken(String token) { + String result = token.replaceAll("" + groupSeparator, ""); + boolean isNegative = false; + int preLen = negativePrefix.length(); + if ((preLen > 0) && result.startsWith(negativePrefix)) { + isNegative = true; + result = result.substring(preLen); + } + int sufLen = negativeSuffix.length(); + if ((sufLen > 0) && result.endsWith(negativeSuffix)) { + isNegative = true; + result = result.substring(result.length() - sufLen, result.length()); + } + if (isNegative) + result = "-" + result; + return result; + } + + /** + * Scans the next token of the input as an {@code int}. + * + *

An invocation of this method of the form + * {@code nextInt()} behaves in exactly the same way as the + * invocation {@code nextInt(radix)}, where {@code radix} + * is the default radix of this scanner. + * + * @return the {@code int} scanned from the input + * @throws InputMismatchException + * if the next token does not match the Integer + * regular expression, or is out of range + * @throws NoSuchElementException if input is exhausted + * @throws IllegalStateException if this scanner is closed + */ + public int nextInt() { + return nextInt(defaultRadix); + } + + /** + * Scans the next token of the input as an {@code int}. + * This method will throw {@code InputMismatchException} + * if the next token cannot be translated into a valid int value as + * described below. If the translation is successful, the scanner advances + * past the input that matched. + * + *

If the next token matches the Integer regular expression defined + * above then the token is converted into an {@code int} value as if by + * removing all locale specific prefixes, group separators, and locale + * specific suffixes, then mapping non-ASCII digits into ASCII + * digits via {@link Character#digit Character.digit}, prepending a + * negative sign (-) if the locale specific negative prefixes and suffixes + * were present, and passing the resulting string to + * {@link Integer#parseInt(String, int) Integer.parseInt} with the + * specified radix. + * + *

If the radix is less than {@link Character#MIN_RADIX Character.MIN_RADIX} + * or greater than {@link Character#MAX_RADIX Character.MAX_RADIX}, then an + * {@code IllegalArgumentException} is thrown. + * + * @param radix the radix used to interpret the token as an int value + * @return the {@code int} scanned from the input + * @throws InputMismatchException + * if the next token does not match the Integer + * regular expression, or is out of range + * @throws NoSuchElementException if input is exhausted + * @throws IllegalStateException if this scanner is closed + * @throws IllegalArgumentException if the radix is out of range + */ + public int nextInt(int radix) { + // Check cached result + if ((typeCache != null) && (typeCache instanceof Integer) && this.radix == radix) { + int val = ((Integer) typeCache).intValue(); + useTypeCache(); + return val; + } + setRadix(radix); + clearCaches(); + // Search for next int + try { + String s = next(integerPattern()); + if (matcher.group(SIMPLE_GROUP_INDEX) == null) + s = processIntegerToken(s); + return Integer.parseInt(s, radix); + } catch (NumberFormatException nfe) { + // don't skip bad token + position = matcher.start(); + throw new InputMismatchException(nfe.getMessage()); + } + } + + /** + * Returns true if the next token in this scanner's input can be + * interpreted as a long value in the default radix using the + * {@link #nextLong} method. The scanner does not advance past any input. + * + * @return true if and only if this scanner's next token is a valid + * long value + * @throws IllegalStateException if this scanner is closed + */ + public boolean hasNextLong() { + return hasNextLong(defaultRadix); + } + + /** + * Returns true if the next token in this scanner's input can be + * interpreted as a long value in the specified radix using the + * {@link #nextLong} method. The scanner does not advance past any input. + * + *

If the radix is less than {@link Character#MIN_RADIX Character.MIN_RADIX} + * or greater than {@link Character#MAX_RADIX Character.MAX_RADIX}, then an + * {@code IllegalArgumentException} is thrown. + * + * @param radix the radix used to interpret the token as a long value + * @return true if and only if this scanner's next token is a valid + * long value + * @throws IllegalStateException if this scanner is closed + * @throws IllegalArgumentException if the radix is out of range + */ + public boolean hasNextLong(int radix) { + setRadix(radix); + boolean result = hasNext(integerPattern()); + if (result) { + // Cache it + try { + String s = (matcher.group(SIMPLE_GROUP_INDEX) == null) ? processIntegerToken(hasNextResult) : hasNextResult; + typeCache = Long.parseLong(s, radix); + } catch (NumberFormatException nfe) { + result = false; + } + } + return result; + } + + /** + * Scans the next token of the input as a {@code long}. + * + *

An invocation of this method of the form + * {@code nextLong()} behaves in exactly the same way as the + * invocation {@code nextLong(radix)}, where {@code radix} + * is the default radix of this scanner. + * + * @return the {@code long} scanned from the input + * @throws InputMismatchException + * if the next token does not match the Integer + * regular expression, or is out of range + * @throws NoSuchElementException if input is exhausted + * @throws IllegalStateException if this scanner is closed + */ + public long nextLong() { + return nextLong(defaultRadix); + } + + /** + * Scans the next token of the input as a {@code long}. + * This method will throw {@code InputMismatchException} + * if the next token cannot be translated into a valid long value as + * described below. If the translation is successful, the scanner advances + * past the input that matched. + * + *

If the next token matches the Integer regular expression defined + * above then the token is converted into a {@code long} value as if by + * removing all locale specific prefixes, group separators, and locale + * specific suffixes, then mapping non-ASCII digits into ASCII + * digits via {@link Character#digit Character.digit}, prepending a + * negative sign (-) if the locale specific negative prefixes and suffixes + * were present, and passing the resulting string to + * {@link Long#parseLong(String, int) Long.parseLong} with the + * specified radix. + * + *

If the radix is less than {@link Character#MIN_RADIX Character.MIN_RADIX} + * or greater than {@link Character#MAX_RADIX Character.MAX_RADIX}, then an + * {@code IllegalArgumentException} is thrown. + * + * @param radix the radix used to interpret the token as an int value + * @return the {@code long} scanned from the input + * @throws InputMismatchException + * if the next token does not match the Integer + * regular expression, or is out of range + * @throws NoSuchElementException if input is exhausted + * @throws IllegalStateException if this scanner is closed + * @throws IllegalArgumentException if the radix is out of range + */ + public long nextLong(int radix) { + // Check cached result + if ((typeCache != null) && (typeCache instanceof Long) && this.radix == radix) { + long val = ((Long) typeCache).longValue(); + useTypeCache(); + return val; + } + setRadix(radix); + clearCaches(); + try { + String s = next(integerPattern()); + if (matcher.group(SIMPLE_GROUP_INDEX) == null) + s = processIntegerToken(s); + return Long.parseLong(s, radix); + } catch (NumberFormatException nfe) { + // don't skip bad token + position = matcher.start(); + throw new InputMismatchException(nfe.getMessage()); + } + } + + /** + * The float token must be stripped of prefixes, group separators, + * and suffixes, non ascii digits must be converted into ascii digits + * before parseFloat will accept it. + * + * If there are non-ascii digits in the token these digits must + * be processed before the token is passed to parseFloat. + */ + private String processFloatToken(String token) { + String result = token.replaceAll(groupSeparator, ""); + if (!decimalSeparator.equals("\\.")) + result = result.replaceAll(decimalSeparator, "."); + boolean isNegative = false; + int preLen = negativePrefix.length(); + if ((preLen > 0) && result.startsWith(negativePrefix)) { + isNegative = true; + result = result.substring(preLen); + } + int sufLen = negativeSuffix.length(); + if ((sufLen > 0) && result.endsWith(negativeSuffix)) { + isNegative = true; + result = result.substring(result.length() - sufLen, result.length()); + } + if (result.equals(nanString)) + result = "NaN"; + if (result.equals(infinityString)) + result = "Infinity"; + if (isNegative) + result = "-" + result; + // Translate non-ASCII digits + Matcher m = NON_ASCII_DIGIT.matcher(result); + if (m.find()) { + StringBuilder inASCII = new StringBuilder(); + for (int i = 0; i < result.length(); i++) { + char nextChar = result.charAt(i); + if (Character.isDigit(nextChar)) { + int d = Character.digit(nextChar, 10); + if (d != -1) + inASCII.append(d); + else + inASCII.append(nextChar); + } else { + inASCII.append(nextChar); + } + } + result = inASCII.toString(); + } + return result; + } + + /** + * Returns true if the next token in this scanner's input can be + * interpreted as a float value using the {@link #nextFloat} + * method. The scanner does not advance past any input. + * + * @return true if and only if this scanner's next token is a valid + * float value + * @throws IllegalStateException if this scanner is closed + */ + public boolean hasNextFloat() { + setRadix(10); + boolean result = hasNext(floatPattern()); + if (result) { + // Cache it + try { + String s = processFloatToken(hasNextResult); + typeCache = Float.valueOf(Float.parseFloat(s)); + } catch (NumberFormatException nfe) { + result = false; + } + } + return result; + } + + /** + * Scans the next token of the input as a {@code float}. + * This method will throw {@code InputMismatchException} + * if the next token cannot be translated into a valid float value as + * described below. If the translation is successful, the scanner advances + * past the input that matched. + * + *

If the next token matches the Float regular expression defined above + * then the token is converted into a {@code float} value as if by + * removing all locale specific prefixes, group separators, and locale + * specific suffixes, then mapping non-ASCII digits into ASCII + * digits via {@link Character#digit Character.digit}, prepending a + * negative sign (-) if the locale specific negative prefixes and suffixes + * were present, and passing the resulting string to + * {@link Float#parseFloat Float.parseFloat}. If the token matches + * the localized NaN or infinity strings, then either "Nan" or "Infinity" + * is passed to {@link Float#parseFloat(String) Float.parseFloat} as + * appropriate. + * + * @return the {@code float} scanned from the input + * @throws InputMismatchException + * if the next token does not match the Float + * regular expression, or is out of range + * @throws NoSuchElementException if input is exhausted + * @throws IllegalStateException if this scanner is closed + */ + public float nextFloat() { + // Check cached result + if ((typeCache != null) && (typeCache instanceof Float)) { + float val = ((Float) typeCache).floatValue(); + useTypeCache(); + return val; + } + setRadix(10); + clearCaches(); + try { + return Float.parseFloat(processFloatToken(next(floatPattern()))); + } catch (NumberFormatException nfe) { + // don't skip bad token + position = matcher.start(); + throw new InputMismatchException(nfe.getMessage()); + } + } + + /** + * Returns true if the next token in this scanner's input can be + * interpreted as a double value using the {@link #nextDouble} + * method. The scanner does not advance past any input. + * + * @return true if and only if this scanner's next token is a valid + * double value + * @throws IllegalStateException if this scanner is closed + */ + public boolean hasNextDouble() { + setRadix(10); + boolean result = hasNext(floatPattern()); + if (result) { + // Cache it + try { + String s = processFloatToken(hasNextResult); + typeCache = Double.valueOf(Double.parseDouble(s)); + } catch (NumberFormatException nfe) { + result = false; + } + } + return result; + } + + /** + * Scans the next token of the input as a {@code double}. + * This method will throw {@code InputMismatchException} + * if the next token cannot be translated into a valid double value. + * If the translation is successful, the scanner advances past the input + * that matched. + * + *

If the next token matches the Float regular expression defined above + * then the token is converted into a {@code double} value as if by + * removing all locale specific prefixes, group separators, and locale + * specific suffixes, then mapping non-ASCII digits into ASCII + * digits via {@link Character#digit Character.digit}, prepending a + * negative sign (-) if the locale specific negative prefixes and suffixes + * were present, and passing the resulting string to + * {@link Double#parseDouble Double.parseDouble}. If the token matches + * the localized NaN or infinity strings, then either "Nan" or "Infinity" + * is passed to {@link Double#parseDouble(String) Double.parseDouble} as + * appropriate. + * + * @return the {@code double} scanned from the input + * @throws InputMismatchException + * if the next token does not match the Float + * regular expression, or is out of range + * @throws NoSuchElementException if the input is exhausted + * @throws IllegalStateException if this scanner is closed + */ + public double nextDouble() { + // Check cached result + if ((typeCache != null) && (typeCache instanceof Double)) { + double val = ((Double) typeCache).doubleValue(); + useTypeCache(); + return val; + } + setRadix(10); + clearCaches(); + // Search for next float + try { + return Double.parseDouble(processFloatToken(next(floatPattern()))); + } catch (NumberFormatException nfe) { + // don't skip bad token + position = matcher.start(); + throw new InputMismatchException(nfe.getMessage()); + } + } + + // Convenience methods for scanning multi precision numbers + /** + * Returns true if the next token in this scanner's input can be + * interpreted as a {@code BigInteger} in the default radix using the + * {@link #nextBigInteger} method. The scanner does not advance past any + * input. + * + * @return true if and only if this scanner's next token is a valid + * {@code BigInteger} + * @throws IllegalStateException if this scanner is closed + */ + public boolean hasNextBigInteger() { + return hasNextBigInteger(defaultRadix); + } + + /** + * Returns true if the next token in this scanner's input can be + * interpreted as a {@code BigInteger} in the specified radix using + * the {@link #nextBigInteger} method. The scanner does not advance past + * any input. + * + *

If the radix is less than {@link Character#MIN_RADIX Character.MIN_RADIX} + * or greater than {@link Character#MAX_RADIX Character.MAX_RADIX}, then an + * {@code IllegalArgumentException} is thrown. + * + * @param radix the radix used to interpret the token as an integer + * @return true if and only if this scanner's next token is a valid + * {@code BigInteger} + * @throws IllegalStateException if this scanner is closed + * @throws IllegalArgumentException if the radix is out of range + */ + public boolean hasNextBigInteger(int radix) { + setRadix(radix); + boolean result = hasNext(integerPattern()); + if (result) { + // Cache it + try { + String s = (matcher.group(SIMPLE_GROUP_INDEX) == null) ? processIntegerToken(hasNextResult) : hasNextResult; + typeCache = new BigInteger(s, radix); + } catch (NumberFormatException nfe) { + result = false; + } + } + return result; + } + + /** + * Scans the next token of the input as a {@link java.math.BigInteger + * BigInteger}. + * + *

An invocation of this method of the form + * {@code nextBigInteger()} behaves in exactly the same way as the + * invocation {@code nextBigInteger(radix)}, where {@code radix} + * is the default radix of this scanner. + * + * @return the {@code BigInteger} scanned from the input + * @throws InputMismatchException + * if the next token does not match the Integer + * regular expression, or is out of range + * @throws NoSuchElementException if the input is exhausted + * @throws IllegalStateException if this scanner is closed + */ + public BigInteger nextBigInteger() { + return nextBigInteger(defaultRadix); + } + + /** + * Scans the next token of the input as a {@link java.math.BigInteger + * BigInteger}. + * + *

If the next token matches the Integer regular expression defined + * above then the token is converted into a {@code BigInteger} value as if + * by removing all group separators, mapping non-ASCII digits into ASCII + * digits via the {@link Character#digit Character.digit}, and passing the + * resulting string to the {@link + * java.math.BigInteger#BigInteger(java.lang.String) + * BigInteger(String, int)} constructor with the specified radix. + * + *

If the radix is less than {@link Character#MIN_RADIX Character.MIN_RADIX} + * or greater than {@link Character#MAX_RADIX Character.MAX_RADIX}, then an + * {@code IllegalArgumentException} is thrown. + * + * @param radix the radix used to interpret the token + * @return the {@code BigInteger} scanned from the input + * @throws InputMismatchException + * if the next token does not match the Integer + * regular expression, or is out of range + * @throws NoSuchElementException if the input is exhausted + * @throws IllegalStateException if this scanner is closed + * @throws IllegalArgumentException if the radix is out of range + */ + public BigInteger nextBigInteger(int radix) { + // Check cached result + if ((typeCache != null) && (typeCache instanceof BigInteger) && this.radix == radix) { + BigInteger val = (BigInteger) typeCache; + useTypeCache(); + return val; + } + setRadix(radix); + clearCaches(); + // Search for next int + try { + String s = next(integerPattern()); + if (matcher.group(SIMPLE_GROUP_INDEX) == null) + s = processIntegerToken(s); + return new BigInteger(s, radix); + } catch (NumberFormatException nfe) { + // don't skip bad token + position = matcher.start(); + throw new InputMismatchException(nfe.getMessage()); + } + } + + /** + * Returns true if the next token in this scanner's input can be + * interpreted as a {@code BigDecimal} using the + * {@link #nextBigDecimal} method. The scanner does not advance past any + * input. + * + * @return true if and only if this scanner's next token is a valid + * {@code BigDecimal} + * @throws IllegalStateException if this scanner is closed + */ + public boolean hasNextBigDecimal() { + setRadix(10); + boolean result = hasNext(decimalPattern()); + if (result) { + // Cache it + try { + String s = processFloatToken(hasNextResult); + typeCache = new BigDecimal(s); + } catch (NumberFormatException nfe) { + result = false; + } + } + return result; + } + + /** + * Scans the next token of the input as a {@link java.math.BigDecimal + * BigDecimal}. + * + *

If the next token matches the Decimal regular expression defined + * above then the token is converted into a {@code BigDecimal} value as if + * by removing all group separators, mapping non-ASCII digits into ASCII + * digits via the {@link Character#digit Character.digit}, and passing the + * resulting string to the {@link + * java.math.BigDecimal#BigDecimal(java.lang.String) BigDecimal(String)} + * constructor. + * + * @return the {@code BigDecimal} scanned from the input + * @throws InputMismatchException + * if the next token does not match the Decimal + * regular expression, or is out of range + * @throws NoSuchElementException if the input is exhausted + * @throws IllegalStateException if this scanner is closed + */ + public BigDecimal nextBigDecimal() { + // Check cached result + if ((typeCache != null) && (typeCache instanceof BigDecimal)) { + BigDecimal val = (BigDecimal) typeCache; + useTypeCache(); + return val; + } + setRadix(10); + clearCaches(); + // Search for next float + try { + String s = processFloatToken(next(decimalPattern())); + return new BigDecimal(s); + } catch (NumberFormatException nfe) { + // don't skip bad token + position = matcher.start(); + throw new InputMismatchException(nfe.getMessage()); + } + } + + /** + * Resets this scanner. + * + *

Resetting a scanner discards all of its explicit state + * information which may have been changed by invocations of + * {@link #useDelimiter useDelimiter()}, + * {@link #useLocale useLocale()}, or + * {@link #useRadix useRadix()}. + * + *

An invocation of this method of the form + * {@code scanner.reset()} behaves in exactly the same way as the + * invocation + * + *

{@code
+     *   scanner.useDelimiter("\\p{javaWhitespace}+")
+     *          .useLocale(Locale.getDefault(Locale.Category.FORMAT))
+     *          .useRadix(10);
+     * }
+ * + * @return this scanner + * + * @since 1.6 + */ + public Scanner reset() { + delimPattern = WHITESPACE_PATTERN; + useLocale(Locale.getDefault(Locale.Category.FORMAT)); + useRadix(10); + clearCaches(); + modCount++; + return this; + } + + /** + * Returns a stream of delimiter-separated tokens from this scanner. The + * stream contains the same tokens that would be returned, starting from + * this scanner's current state, by calling the {@link #next} method + * repeatedly until the {@link #hasNext} method returns false. + * + *

The resulting stream is sequential and ordered. All stream elements are + * non-null. + * + *

Scanning starts upon initiation of the terminal stream operation, using the + * current state of this scanner. Subsequent calls to any methods on this scanner + * other than {@link #close} and {@link #ioException} may return undefined results + * or may cause undefined effects on the returned stream. The returned stream's source + * {@code Spliterator} is fail-fast and will, on a best-effort basis, throw a + * {@link java.util.ConcurrentModificationException} if any such calls are detected + * during stream pipeline execution. + * + *

After stream pipeline execution completes, this scanner is left in an indeterminate + * state and cannot be reused. + * + *

If this scanner contains a resource that must be released, this scanner + * should be closed, either by calling its {@link #close} method, or by + * closing the returned stream. Closing the stream will close the underlying scanner. + * {@code IllegalStateException} is thrown if the scanner has been closed when this + * method is called, or if this scanner is closed during stream pipeline execution. + * + *

This method might block waiting for more input. + * + * @apiNote + * For example, the following code will create a list of + * comma-delimited tokens from a string: + * + *

{@code
+     * List result = new Scanner("abc,def,,ghi")
+     *     .useDelimiter(",")
+     *     .tokens()
+     *     .collect(Collectors.toList());
+     * }
+ * + *

The resulting list would contain {@code "abc"}, {@code "def"}, + * the empty string, and {@code "ghi"}. + * + * @return a sequential stream of token strings + * @throws IllegalStateException if this scanner is closed + * @since 9 + */ + public Stream tokens() { + ensureOpen(); + Stream stream = StreamSupport.stream(new TokenSpliterator(), false); + return stream.onClose(this::close); + } + + class TokenSpliterator extends Spliterators.AbstractSpliterator { + + int expectedCount = -1; + + TokenSpliterator() { + super(Long.MAX_VALUE, Spliterator.IMMUTABLE | Spliterator.NONNULL | Spliterator.ORDERED); + } + + @Override + public boolean tryAdvance(Consumer cons) { + if (expectedCount >= 0 && expectedCount != modCount) { + throw new ConcurrentModificationException(); + } + if (hasNext()) { + String token = next(); + expectedCount = modCount; + cons.accept(token); + if (expectedCount != modCount) { + throw new ConcurrentModificationException(); + } + return true; + } else { + expectedCount = modCount; + return false; + } + } + } + + /** + * Returns a stream of match results from this scanner. The stream + * contains the same results in the same order that would be returned by + * calling {@code findWithinHorizon(pattern, 0)} and then {@link #match} + * successively as long as {@link #findWithinHorizon findWithinHorizon()} + * finds matches. + * + *

The resulting stream is sequential and ordered. All stream elements are + * non-null. + * + *

Scanning starts upon initiation of the terminal stream operation, using the + * current state of this scanner. Subsequent calls to any methods on this scanner + * other than {@link #close} and {@link #ioException} may return undefined results + * or may cause undefined effects on the returned stream. The returned stream's source + * {@code Spliterator} is fail-fast and will, on a best-effort basis, throw a + * {@link java.util.ConcurrentModificationException} if any such calls are detected + * during stream pipeline execution. + * + *

After stream pipeline execution completes, this scanner is left in an indeterminate + * state and cannot be reused. + * + *

If this scanner contains a resource that must be released, this scanner + * should be closed, either by calling its {@link #close} method, or by + * closing the returned stream. Closing the stream will close the underlying scanner. + * {@code IllegalStateException} is thrown if the scanner has been closed when this + * method is called, or if this scanner is closed during stream pipeline execution. + * + *

As with the {@link #findWithinHorizon findWithinHorizon()} methods, this method + * might block waiting for additional input, and it might buffer an unbounded amount of + * input searching for a match. + * + * @apiNote + * For example, the following code will read a file and return a list + * of all sequences of characters consisting of seven or more Latin capital + * letters: + * + *

{@code
+     * try (Scanner sc = new Scanner(Path.of("input.txt"))) {
+     *     Pattern pat = Pattern.compile("[A-Z]{7,}");
+     *     List capWords = sc.findAll(pat)
+     *                               .map(MatchResult::group)
+     *                               .collect(Collectors.toList());
+     * }
+     * }
+ * + * @param pattern the pattern to be matched + * @return a sequential stream of match results + * @throws NullPointerException if pattern is null + * @throws IllegalStateException if this scanner is closed + * @since 9 + */ + public Stream findAll(Pattern pattern) { + Objects.requireNonNull(pattern); + ensureOpen(); + Stream stream = StreamSupport.stream(new FindSpliterator(pattern), false); + return stream.onClose(this::close); + } + + /** + * Returns a stream of match results that match the provided pattern string. + * The effect is equivalent to the following code: + * + *
{@code
+     *     scanner.findAll(Pattern.compile(patString))
+     * }
+ * + * @param patString the pattern string + * @return a sequential stream of match results + * @throws NullPointerException if patString is null + * @throws IllegalStateException if this scanner is closed + * @throws PatternSyntaxException if the regular expression's syntax is invalid + * @since 9 + * @see java.util.regex.Pattern + */ + public Stream findAll(String patString) { + Objects.requireNonNull(patString); + ensureOpen(); + return findAll(patternCache.forName(patString)); + } + + class FindSpliterator extends Spliterators.AbstractSpliterator { + + final Pattern pattern; + + int expectedCount = -1; + + // true if we need to auto-advance + private boolean advance = false; + + FindSpliterator(Pattern pattern) { + super(Long.MAX_VALUE, Spliterator.IMMUTABLE | Spliterator.NONNULL | Spliterator.ORDERED); + this.pattern = pattern; + } + + @Override + public boolean tryAdvance(Consumer cons) { + ensureOpen(); + if (expectedCount >= 0) { + if (expectedCount != modCount) { + throw new ConcurrentModificationException(); + } + } else { + // init + matchValid = false; + matcher.usePattern(pattern); + expectedCount = modCount; + } + while (true) { + // assert expectedCount == modCount + if (nextInBuffer()) { + // doesn't increment modCount + cons.accept(matcher.toMatchResult()); + if (expectedCount != modCount) { + throw new ConcurrentModificationException(); + } + return true; + } + if (needInput) + // doesn't increment modCount + readInput(); + else + // reached end of input + return false; + } + } + + // reimplementation of findPatternInBuffer with auto-advance on zero-length matches + private boolean nextInBuffer() { + if (advance) { + if (position + 1 > buf.limit()) { + if (!sourceClosed) + needInput = true; + return false; + } + position++; + advance = false; + } + matcher.region(position, buf.limit()); + if (matcher.find() && (!matcher.hitEnd() || sourceClosed)) { + // Did not hit end, or hit real end + position = matcher.end(); + advance = matcher.start() == position; + return true; + } + if (!sourceClosed) + needInput = true; + return false; + } + } + + /** + * Small LRU cache of Patterns. + */ + private static class PatternLRUCache { + + private Pattern[] oa = null; + + private final int size; + + PatternLRUCache(int size) { + this.size = size; + } + + boolean hasName(Pattern p, String s) { + return p.pattern().equals(s); + } + + void moveToFront(Object[] oa, int i) { + Object ob = oa[i]; + for (int j = i; j > 0; j--) oa[j] = oa[j - 1]; + oa[0] = ob; + } + + Pattern forName(String name) { + if (oa == null) { + Pattern[] temp = new Pattern[size]; + oa = temp; + } else { + for (int i = 0; i < oa.length; i++) { + Pattern ob = oa[i]; + if (ob == null) + continue; + if (hasName(ob, name)) { + if (i > 0) + moveToFront(oa, i); + return ob; + } + } + } + // Create a new object + Pattern ob = Pattern.compile(name); + oa[oa.length - 1] = ob; + moveToFront(oa, oa.length - 1); + return ob; + } + } +} From 07586ae52c12f2a82f2132db639797dbdf67f647 Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Tue, 27 Feb 2024 14:43:23 -0800 Subject: [PATCH 12/50] replacing with synthetic test case files --- .../libmodel/LibModelIntegrationTest.java | 35 +- lib-model/lib-model-test/build.gradle | 2 +- .../nullaway/libmodel/AnnotationExample.java | 13 + .../nullaway/libmodel/AnnotationExample.java | 17 + .../share/classes/java/util/Scanner.java | 3018 ----------------- .../share/classes/java/util/Vector.java | 1492 -------- .../classes/java/util/function/Function.java | 103 - 7 files changed, 35 insertions(+), 4645 deletions(-) create mode 100644 lib-model/lib-model-test/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java create mode 100644 lib-model/lib-model-test/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java delete mode 100644 lib-model/lib-model-test/src/main/resources/sample_jspecify_jdk/src/java.base/share/classes/java/util/Scanner.java delete mode 100644 lib-model/lib-model-test/src/main/resources/sample_jspecify_jdk/src/java.base/share/classes/java/util/Vector.java delete mode 100644 lib-model/lib-model-test/src/main/resources/sample_jspecify_jdk/src/java.base/share/classes/java/util/function/Function.java diff --git a/lib-model/lib-model-consume/src/test/java/com/uber/nullaway/libmodel/LibModelIntegrationTest.java b/lib-model/lib-model-consume/src/test/java/com/uber/nullaway/libmodel/LibModelIntegrationTest.java index 9b0ec1d30f..d03b8c76a0 100644 --- a/lib-model/lib-model-consume/src/test/java/com/uber/nullaway/libmodel/LibModelIntegrationTest.java +++ b/lib-model/lib-model-consume/src/test/java/com/uber/nullaway/libmodel/LibModelIntegrationTest.java @@ -19,33 +19,6 @@ public void setup() { compilationHelper = CompilationTestHelper.newInstance(NullAway.class, getClass()); } - @Test - public void libModelArrayTypeNullableReturnsTest() { - compilationHelper - .setArgs( - Arrays.asList( - "-d", - temporaryFolder.getRoot().getAbsolutePath(), - "-XepOpt:NullAway:AnnotatedPackages=com.uber", - "-XepOpt:NullAway:JarInferEnabled=true", - "-XepOpt:NullAway:JarInferUseReturnAnnotations=true")) - .addSourceLines( - "Test.java", - "package com.uber;", - "import javax.annotation.Nullable;", - "import java.util.Vector;", - "class Test {", - " static Vector vector = new Vector<>();", - " static void test(Object[] value){", - " }", - " static void test1() {", - " vector.add(1);", - " test(vector.toArray());", - " }", - "}") - .doTest(); - } - @Test public void libModelNullableReturnsTest() { compilationHelper @@ -60,14 +33,14 @@ public void libModelNullableReturnsTest() { "Test.java", "package com.uber;", "import javax.annotation.Nullable;", - "import java.util.Scanner;", + "import com.uber.nullaway.libmodel.AnnotationExample;", "class Test {", - " static Scanner scanner = new Scanner(\"nullaway test\");", + " static AnnotationExample annotationExample = new AnnotationExample();", " static void test(String value){", " }", " static void test1() {", - " // BUG: Diagnostic contains: passing @Nullable parameter 'scanner.findInLine(\"world\")'", - " test(scanner.findInLine(\"world\"));", + " // BUG: Diagnostic contains: passing @Nullable parameter 'annotationExample.makeUpperCase(\"nullaway\")'", + " test(annotationExample.makeUpperCase(\"nullaway\"));", " }", "}") .doTest(); diff --git a/lib-model/lib-model-test/build.gradle b/lib-model/lib-model-test/build.gradle index 3ead03b11c..8697115a75 100644 --- a/lib-model/lib-model-test/build.gradle +++ b/lib-model/lib-model-test/build.gradle @@ -20,7 +20,7 @@ plugins { evaluationDependsOn(":lib-model:lib-model-consume") -def jdkPath = "${rootProject.projectDir}/lib-model/lib-model-test/src/main/resources/sample_jspecify_jdk/src" +def jdkPath = "${rootProject.projectDir}/lib-model/lib-model-test/src/main/resources/sample_annotated/src" def astubxPath = "com/uber/nullaway/libmodel/provider/libmodels.astubx" jar { diff --git a/lib-model/lib-model-test/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java b/lib-model/lib-model-test/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java new file mode 100644 index 0000000000..d7eef946d6 --- /dev/null +++ b/lib-model/lib-model-test/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java @@ -0,0 +1,13 @@ +package com.uber.nullaway.libmodel; + +import java.util.Locale; + +public class AnnotationExample { + public String makeUpperCase(String inputString) { + if (inputString == null || inputString.isEmpty()) { + return null; + } else { + return inputString.toUpperCase(Locale.ROOT); + } + } +} diff --git a/lib-model/lib-model-test/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java b/lib-model/lib-model-test/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java new file mode 100644 index 0000000000..62507c8ea8 --- /dev/null +++ b/lib-model/lib-model-test/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java @@ -0,0 +1,17 @@ +package com.uber.nullaway.libmodel; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +public class AnnotationExample { + + @Nullable + public String makeUpperCase(String inputString) { + if (inputString == null || inputString.isEmpty()) { + return null; + } else { + return inputString.toUpperCase(); + } + } +} diff --git a/lib-model/lib-model-test/src/main/resources/sample_jspecify_jdk/src/java.base/share/classes/java/util/Scanner.java b/lib-model/lib-model-test/src/main/resources/sample_jspecify_jdk/src/java.base/share/classes/java/util/Scanner.java deleted file mode 100644 index e694f72621..0000000000 --- a/lib-model/lib-model-test/src/main/resources/sample_jspecify_jdk/src/java.base/share/classes/java/util/Scanner.java +++ /dev/null @@ -1,3018 +0,0 @@ -/* - * Copyright (c) 2003, 2018, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ -package java.util; - -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; -import java.io.*; -import java.math.*; -import java.nio.*; -import java.nio.channels.*; -import java.nio.charset.*; -import java.nio.file.Path; -import java.nio.file.Files; -import java.text.*; -import java.text.spi.NumberFormatProvider; -import java.util.function.Consumer; -import java.util.regex.*; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; -import sun.util.locale.provider.LocaleProviderAdapter; -import sun.util.locale.provider.ResourceBundleBasedAdapter; - -/** - * A simple text scanner which can parse primitive types and strings using - * regular expressions. - * - *

A {@code Scanner} breaks its input into tokens using a - * delimiter pattern, which by default matches whitespace. The resulting - * tokens may then be converted into values of different types using the - * various {@code next} methods. - * - *

For example, this code allows a user to read a number from - * {@code System.in}: - *

{@code
- *     Scanner sc = new Scanner(System.in);
- *     int i = sc.nextInt();
- * }
- * - *

As another example, this code allows {@code long} types to be - * assigned from entries in a file {@code myNumbers}: - *

{@code
- *      Scanner sc = new Scanner(new File("myNumbers"));
- *      while (sc.hasNextLong()) {
- *          long aLong = sc.nextLong();
- *      }
- * }
- * - *

The scanner can also use delimiters other than whitespace. This - * example reads several items in from a string: - *

{@code
- *     String input = "1 fish 2 fish red fish blue fish";
- *     Scanner s = new Scanner(input).useDelimiter("\\s*fish\\s*");
- *     System.out.println(s.nextInt());
- *     System.out.println(s.nextInt());
- *     System.out.println(s.next());
- *     System.out.println(s.next());
- *     s.close();
- * }
- *

- * prints the following output: - *

{@code
- *     1
- *     2
- *     red
- *     blue
- * }
- * - *

The same output can be generated with this code, which uses a regular - * expression to parse all four tokens at once: - *

{@code
- *     String input = "1 fish 2 fish red fish blue fish";
- *     Scanner s = new Scanner(input);
- *     s.findInLine("(\\d+) fish (\\d+) fish (\\w+) fish (\\w+)");
- *     MatchResult result = s.match();
- *     for (int i=1; i<=result.groupCount(); i++)
- *         System.out.println(result.group(i));
- *     s.close();
- * }
- * - *

The default whitespace delimiter used - * by a scanner is as recognized by {@link Character#isWhitespace(char) - * Character.isWhitespace()}. The {@link #reset reset()} - * method will reset the value of the scanner's delimiter to the default - * whitespace delimiter regardless of whether it was previously changed. - * - *

A scanning operation may block waiting for input. - * - *

The {@link #next} and {@link #hasNext} methods and their - * companion methods (such as {@link #nextInt} and - * {@link #hasNextInt}) first skip any input that matches the delimiter - * pattern, and then attempt to return the next token. Both {@code hasNext()} - * and {@code next()} methods may block waiting for further input. Whether a - * {@code hasNext()} method blocks has no connection to whether or not its - * associated {@code next()} method will block. The {@link #tokens} method - * may also block waiting for input. - * - *

The {@link #findInLine findInLine()}, - * {@link #findWithinHorizon findWithinHorizon()}, - * {@link #skip skip()}, and {@link #findAll findAll()} - * methods operate independently of the delimiter pattern. These methods will - * attempt to match the specified pattern with no regard to delimiters in the - * input and thus can be used in special circumstances where delimiters are - * not relevant. These methods may block waiting for more input. - * - *

When a scanner throws an {@link InputMismatchException}, the scanner - * will not pass the token that caused the exception, so that it may be - * retrieved or skipped via some other method. - * - *

Depending upon the type of delimiting pattern, empty tokens may be - * returned. For example, the pattern {@code "\\s+"} will return no empty - * tokens since it matches multiple instances of the delimiter. The delimiting - * pattern {@code "\\s"} could return empty tokens since it only passes one - * space at a time. - * - *

A scanner can read text from any object which implements the {@link - * java.lang.Readable} interface. If an invocation of the underlying - * readable's {@link java.lang.Readable#read read()} method throws an {@link - * java.io.IOException} then the scanner assumes that the end of the input - * has been reached. The most recent {@code IOException} thrown by the - * underlying readable can be retrieved via the {@link #ioException} method. - * - *

When a {@code Scanner} is closed, it will close its input source - * if the source implements the {@link java.io.Closeable} interface. - * - *

A {@code Scanner} is not safe for multithreaded use without - * external synchronization. - * - *

Unless otherwise mentioned, passing a {@code null} parameter into - * any method of a {@code Scanner} will cause a - * {@code NullPointerException} to be thrown. - * - *

A scanner will default to interpreting numbers as decimal unless a - * different radix has been set by using the {@link #useRadix} method. The - * {@link #reset} method will reset the value of the scanner's radix to - * {@code 10} regardless of whether it was previously changed. - * - *

Localized numbers

- * - *

An instance of this class is capable of scanning numbers in the standard - * formats as well as in the formats of the scanner's locale. A scanner's - * initial locale is the value returned by the {@link - * java.util.Locale#getDefault(Locale.Category) - * Locale.getDefault(Locale.Category.FORMAT)} method; it may be changed via the {@link - * #useLocale useLocale()} method. The {@link #reset} method will reset the value of the - * scanner's locale to the initial locale regardless of whether it was - * previously changed. - * - *

The localized formats are defined in terms of the following parameters, - * which for a particular locale are taken from that locale's {@link - * java.text.DecimalFormat DecimalFormat} object, {@code df}, and its and - * {@link java.text.DecimalFormatSymbols DecimalFormatSymbols} object, - * {@code dfs}. - * - *

- *
LocalGroupSeparator   - *
The character used to separate thousands groups, - * i.e., {@code dfs.}{@link - * java.text.DecimalFormatSymbols#getGroupingSeparator - * getGroupingSeparator()} - *
LocalDecimalSeparator   - *
The character used for the decimal point, - * i.e., {@code dfs.}{@link - * java.text.DecimalFormatSymbols#getDecimalSeparator - * getDecimalSeparator()} - *
LocalPositivePrefix   - *
The string that appears before a positive number (may - * be empty), i.e., {@code df.}{@link - * java.text.DecimalFormat#getPositivePrefix - * getPositivePrefix()} - *
LocalPositiveSuffix   - *
The string that appears after a positive number (may be - * empty), i.e., {@code df.}{@link - * java.text.DecimalFormat#getPositiveSuffix - * getPositiveSuffix()} - *
LocalNegativePrefix   - *
The string that appears before a negative number (may - * be empty), i.e., {@code df.}{@link - * java.text.DecimalFormat#getNegativePrefix - * getNegativePrefix()} - *
LocalNegativeSuffix   - *
The string that appears after a negative number (may be - * empty), i.e., {@code df.}{@link - * java.text.DecimalFormat#getNegativeSuffix - * getNegativeSuffix()} - *
LocalNaN   - *
The string that represents not-a-number for - * floating-point values, - * i.e., {@code dfs.}{@link - * java.text.DecimalFormatSymbols#getNaN - * getNaN()} - *
LocalInfinity   - *
The string that represents infinity for floating-point - * values, i.e., {@code dfs.}{@link - * java.text.DecimalFormatSymbols#getInfinity - * getInfinity()} - *
- * - *

Number syntax

- * - *

The strings that can be parsed as numbers by an instance of this class - * are specified in terms of the following regular-expression grammar, where - * Rmax is the highest digit in the radix being used (for example, Rmax is 9 in base 10). - * - *

- *
NonAsciiDigit: - *
A non-ASCII character c for which - * {@link java.lang.Character#isDigit Character.isDigit}{@code (c)} - * returns true - * - *
Non0Digit: - *
{@code [1-}Rmax{@code ] | }NonASCIIDigit - * - *
Digit: - *
{@code [0-}Rmax{@code ] | }NonASCIIDigit - * - *
GroupedNumeral: - *
Non0Digit - * Digit{@code ? - * }Digit{@code ?} - *
    LocalGroupSeparator - * Digit - * Digit - * Digit{@code )+ )} - * - *
Numeral: - *
{@code ( ( }Digit{@code + ) - * | }GroupedNumeral{@code )} - * - *
Integer: - *
{@code ( [-+]? ( }Numeral{@code - * ) )} - *
{@code | }LocalPositivePrefix Numeral - * LocalPositiveSuffix - *
{@code | }LocalNegativePrefix Numeral - * LocalNegativeSuffix - * - *
DecimalNumeral: - *
Numeral - *
{@code | }Numeral - * LocalDecimalSeparator - * Digit{@code *} - *
{@code | }LocalDecimalSeparator - * Digit{@code +} - * - *
Exponent: - *
{@code ( [eE] [+-]? }Digit{@code + )} - * - *
Decimal: - *
{@code ( [-+]? }DecimalNumeral - * Exponent{@code ? )} - *
{@code | }LocalPositivePrefix - * DecimalNumeral - * LocalPositiveSuffix - * Exponent{@code ?} - *
{@code | }LocalNegativePrefix - * DecimalNumeral - * LocalNegativeSuffix - * Exponent{@code ?} - * - *
HexFloat: - *
{@code [-+]? 0[xX][0-9a-fA-F]*\.[0-9a-fA-F]+ - * ([pP][-+]?[0-9]+)?} - * - *
NonNumber: - *
{@code NaN - * | }LocalNan{@code - * | Infinity - * | }LocalInfinity - * - *
SignedNonNumber: - *
{@code ( [-+]? }NonNumber{@code )} - *
{@code | }LocalPositivePrefix - * NonNumber - * LocalPositiveSuffix - *
{@code | }LocalNegativePrefix - * NonNumber - * LocalNegativeSuffix - * - *
Float: - *
Decimal - * {@code | }HexFloat - * {@code | }SignedNonNumber - * - *
- *

Whitespace is not significant in the above regular expressions. - * - * @since 1.5 - */ -@NullMarked -public final class Scanner implements Iterator, Closeable { - - // Internal buffer used to hold input - private CharBuffer buf; - - // Size of internal character buffer - // change to 1024; - private static final int BUFFER_SIZE = 1024; - - // The index into the buffer currently held by the Scanner - private int position; - - // Internal matcher used for finding delimiters - private Matcher matcher; - - // Pattern used to delimit tokens - private Pattern delimPattern; - - // Pattern found in last hasNext operation - private Pattern hasNextPattern; - - // Position after last hasNext operation - private int hasNextPosition; - - // Result after last hasNext operation - private String hasNextResult; - - // The input source - private Readable source; - - // Boolean is true if source is done - private boolean sourceClosed = false; - - // Boolean indicating more input is required - private boolean needInput = false; - - // Boolean indicating if a delim has been skipped this operation - private boolean skipped = false; - - // A store of a position that the scanner may fall back to - private int savedScannerPosition = -1; - - // A cache of the last primitive type scanned - private Object typeCache = null; - - // Boolean indicating if a match result is available - private boolean matchValid = false; - - // Boolean indicating if this scanner has been closed - private boolean closed = false; - - // The current radix used by this scanner - private int radix = 10; - - // The default radix for this scanner - private int defaultRadix = 10; - - // The locale used by this scanner - private Locale locale = null; - - // A cache of the last few recently used Patterns - private PatternLRUCache patternCache = new PatternLRUCache(7); - - // A holder of the last IOException encountered - private IOException lastException; - - // Number of times this scanner's state has been modified. - // Generally incremented on most public APIs and checked - // within spliterator implementations. - int modCount; - - // A pattern for java whitespace - private static Pattern WHITESPACE_PATTERN = Pattern.compile("\\p{javaWhitespace}+"); - - // A pattern for any token - private static Pattern FIND_ANY_PATTERN = Pattern.compile("(?s).*"); - - // A pattern for non-ASCII digits - private static Pattern NON_ASCII_DIGIT = Pattern.compile("[\\p{javaDigit}&&[^0-9]]"); - - // Fields and methods to support scanning primitive types - /** - * Locale dependent values used to scan numbers - */ - private String groupSeparator = "\\,"; - - private String decimalSeparator = "\\."; - - private String nanString = "NaN"; - - private String infinityString = "Infinity"; - - private String positivePrefix = ""; - - private String negativePrefix = "\\-"; - - private String positiveSuffix = ""; - - private String negativeSuffix = ""; - - /** - * Fields and an accessor method to match booleans - */ - private static volatile Pattern boolPattern; - - private static final String BOOLEAN_PATTERN = "true|false"; - - private static Pattern boolPattern() { - Pattern bp = boolPattern; - if (bp == null) - boolPattern = bp = Pattern.compile(BOOLEAN_PATTERN, Pattern.CASE_INSENSITIVE); - return bp; - } - - /** - * Fields and methods to match bytes, shorts, ints, and longs - */ - private Pattern integerPattern; - - private String digits = "0123456789abcdefghijklmnopqrstuvwxyz"; - - private String non0Digit = "[\\p{javaDigit}&&[^0]]"; - - private int SIMPLE_GROUP_INDEX = 5; - - private String buildIntegerPatternString() { - String radixDigits = digits.substring(0, radix); - // \\p{javaDigit} is not guaranteed to be appropriate - // here but what can we do? The final authority will be - // whatever parse method is invoked, so ultimately the - // Scanner will do the right thing - String digit = "((?i)[" + radixDigits + "]|\\p{javaDigit})"; - String groupedNumeral = "(" + non0Digit + digit + "?" + digit + "?(" + groupSeparator + digit + digit + digit + ")+)"; - // digit++ is the possessive form which is necessary for reducing - // backtracking that would otherwise cause unacceptable performance - String numeral = "((" + digit + "++)|" + groupedNumeral + ")"; - String javaStyleInteger = "([-+]?(" + numeral + "))"; - String negativeInteger = negativePrefix + numeral + negativeSuffix; - String positiveInteger = positivePrefix + numeral + positiveSuffix; - return "(" + javaStyleInteger + ")|(" + positiveInteger + ")|(" + negativeInteger + ")"; - } - - private Pattern integerPattern() { - if (integerPattern == null) { - integerPattern = patternCache.forName(buildIntegerPatternString()); - } - return integerPattern; - } - - /** - * Fields and an accessor method to match line separators - */ - private static volatile Pattern separatorPattern; - - private static volatile Pattern linePattern; - - private static final String LINE_SEPARATOR_PATTERN = "\r\n|[\n\r\u2028\u2029\u0085]"; - - private static final String LINE_PATTERN = ".*(" + LINE_SEPARATOR_PATTERN + ")|.+$"; - - private static Pattern separatorPattern() { - Pattern sp = separatorPattern; - if (sp == null) - separatorPattern = sp = Pattern.compile(LINE_SEPARATOR_PATTERN); - return sp; - } - - private static Pattern linePattern() { - Pattern lp = linePattern; - if (lp == null) - linePattern = lp = Pattern.compile(LINE_PATTERN); - return lp; - } - - /** - * Fields and methods to match floats and doubles - */ - private Pattern floatPattern; - - private Pattern decimalPattern; - - private void buildFloatAndDecimalPattern() { - // \\p{javaDigit} may not be perfect, see above - String digit = "([0-9]|(\\p{javaDigit}))"; - String exponent = "([eE][+-]?" + digit + "+)?"; - String groupedNumeral = "(" + non0Digit + digit + "?" + digit + "?(" + groupSeparator + digit + digit + digit + ")+)"; - // Once again digit++ is used for performance, as above - String numeral = "((" + digit + "++)|" + groupedNumeral + ")"; - String decimalNumeral = "(" + numeral + "|" + numeral + decimalSeparator + digit + "*+|" + decimalSeparator + digit + "++)"; - String nonNumber = "(NaN|" + nanString + "|Infinity|" + infinityString + ")"; - String positiveFloat = "(" + positivePrefix + decimalNumeral + positiveSuffix + exponent + ")"; - String negativeFloat = "(" + negativePrefix + decimalNumeral + negativeSuffix + exponent + ")"; - String decimal = "(([-+]?" + decimalNumeral + exponent + ")|" + positiveFloat + "|" + negativeFloat + ")"; - String hexFloat = "[-+]?0[xX][0-9a-fA-F]*\\.[0-9a-fA-F]+([pP][-+]?[0-9]+)?"; - String positiveNonNumber = "(" + positivePrefix + nonNumber + positiveSuffix + ")"; - String negativeNonNumber = "(" + negativePrefix + nonNumber + negativeSuffix + ")"; - String signedNonNumber = "(([-+]?" + nonNumber + ")|" + positiveNonNumber + "|" + negativeNonNumber + ")"; - floatPattern = Pattern.compile(decimal + "|" + hexFloat + "|" + signedNonNumber); - decimalPattern = Pattern.compile(decimal); - } - - private Pattern floatPattern() { - if (floatPattern == null) { - buildFloatAndDecimalPattern(); - } - return floatPattern; - } - - private Pattern decimalPattern() { - if (decimalPattern == null) { - buildFloatAndDecimalPattern(); - } - return decimalPattern; - } - - // Constructors - /** - * Constructs a {@code Scanner} that returns values scanned - * from the specified source delimited by the specified pattern. - * - * @param source A character source implementing the Readable interface - * @param pattern A delimiting pattern - */ - private Scanner(Readable source, Pattern pattern) { - assert source != null : "source should not be null"; - assert pattern != null : "pattern should not be null"; - this.source = source; - delimPattern = pattern; - buf = CharBuffer.allocate(BUFFER_SIZE); - buf.limit(0); - matcher = delimPattern.matcher(buf); - matcher.useTransparentBounds(true); - matcher.useAnchoringBounds(false); - useLocale(Locale.getDefault(Locale.Category.FORMAT)); - } - - /** - * Constructs a new {@code Scanner} that produces values scanned - * from the specified source. - * - * @param source A character source implementing the {@link Readable} - * interface - */ - public Scanner(Readable source) { - this(Objects.requireNonNull(source, "source"), WHITESPACE_PATTERN); - } - - /** - * Constructs a new {@code Scanner} that produces values scanned - * from the specified input stream. Bytes from the stream are converted - * into characters using the underlying platform's - * {@linkplain java.nio.charset.Charset#defaultCharset() default charset}. - * - * @param source An input stream to be scanned - */ - public Scanner(InputStream source) { - this(new InputStreamReader(source), WHITESPACE_PATTERN); - } - - /** - * Constructs a new {@code Scanner} that produces values scanned - * from the specified input stream. Bytes from the stream are converted - * into characters using the specified charset. - * - * @param source An input stream to be scanned - * @param charsetName The encoding type used to convert bytes from the - * stream into characters to be scanned - * @throws IllegalArgumentException if the specified character set - * does not exist - */ - public Scanner(InputStream source, String charsetName) { - this(source, toCharset(charsetName)); - } - - /** - * Constructs a new {@code Scanner} that produces values scanned - * from the specified input stream. Bytes from the stream are converted - * into characters using the specified charset. - * - * @param source an input stream to be scanned - * @param charset the charset used to convert bytes from the file - * into characters to be scanned - * @since 10 - */ - public Scanner(InputStream source, Charset charset) { - this(makeReadable(Objects.requireNonNull(source, "source"), charset), WHITESPACE_PATTERN); - } - - /** - * Returns a charset object for the given charset name. - * @throws NullPointerException is csn is null - * @throws IllegalArgumentException if the charset is not supported - */ - private static Charset toCharset(String csn) { - Objects.requireNonNull(csn, "charsetName"); - try { - return Charset.forName(csn); - } catch (IllegalCharsetNameException | UnsupportedCharsetException e) { - // IllegalArgumentException should be thrown - throw new IllegalArgumentException(e); - } - } - - /* - * This method is added so that null-check on charset can be performed before - * creating InputStream as an existing test required it. - */ - private static Readable makeReadable(Path source, Charset charset) throws IOException { - Objects.requireNonNull(charset, "charset"); - return makeReadable(Files.newInputStream(source), charset); - } - - private static Readable makeReadable(InputStream source, Charset charset) { - Objects.requireNonNull(charset, "charset"); - return new InputStreamReader(source, charset); - } - - /** - * Constructs a new {@code Scanner} that produces values scanned - * from the specified file. Bytes from the file are converted into - * characters using the underlying platform's - * {@linkplain java.nio.charset.Charset#defaultCharset() default charset}. - * - * @param source A file to be scanned - * @throws FileNotFoundException if source is not found - */ - public Scanner(File source) throws FileNotFoundException { - this((ReadableByteChannel) (new FileInputStream(source).getChannel())); - } - - /** - * Constructs a new {@code Scanner} that produces values scanned - * from the specified file. Bytes from the file are converted into - * characters using the specified charset. - * - * @param source A file to be scanned - * @param charsetName The encoding type used to convert bytes from the file - * into characters to be scanned - * @throws FileNotFoundException if source is not found - * @throws IllegalArgumentException if the specified encoding is - * not found - */ - public Scanner(File source, String charsetName) throws FileNotFoundException { - this(Objects.requireNonNull(source), toDecoder(charsetName)); - } - - /** - * Constructs a new {@code Scanner} that produces values scanned - * from the specified file. Bytes from the file are converted into - * characters using the specified charset. - * - * @param source A file to be scanned - * @param charset The charset used to convert bytes from the file - * into characters to be scanned - * @throws IOException - * if an I/O error occurs opening the source - * @since 10 - */ - public Scanner(File source, Charset charset) throws IOException { - this(Objects.requireNonNull(source), charset.newDecoder()); - } - - private Scanner(File source, CharsetDecoder dec) throws FileNotFoundException { - this(makeReadable((ReadableByteChannel) (new FileInputStream(source).getChannel()), dec)); - } - - private static CharsetDecoder toDecoder(String charsetName) { - Objects.requireNonNull(charsetName, "charsetName"); - try { - return Charset.forName(charsetName).newDecoder(); - } catch (IllegalCharsetNameException | UnsupportedCharsetException unused) { - throw new IllegalArgumentException(charsetName); - } - } - - private static Readable makeReadable(ReadableByteChannel source, CharsetDecoder dec) { - return Channels.newReader(source, dec, -1); - } - - private static Readable makeReadable(ReadableByteChannel source, Charset charset) { - Objects.requireNonNull(charset, "charset"); - return Channels.newReader(source, charset); - } - - /** - * Constructs a new {@code Scanner} that produces values scanned - * from the specified file. Bytes from the file are converted into - * characters using the underlying platform's - * {@linkplain java.nio.charset.Charset#defaultCharset() default charset}. - * - * @param source - * the path to the file to be scanned - * @throws IOException - * if an I/O error occurs opening source - * - * @since 1.7 - */ - public Scanner(Path source) throws IOException { - this(Files.newInputStream(source)); - } - - /** - * Constructs a new {@code Scanner} that produces values scanned - * from the specified file. Bytes from the file are converted into - * characters using the specified charset. - * - * @param source - * the path to the file to be scanned - * @param charsetName - * The encoding type used to convert bytes from the file - * into characters to be scanned - * @throws IOException - * if an I/O error occurs opening source - * @throws IllegalArgumentException - * if the specified encoding is not found - * @since 1.7 - */ - public Scanner(Path source, String charsetName) throws IOException { - this(Objects.requireNonNull(source), toCharset(charsetName)); - } - - /** - * Constructs a new {@code Scanner} that produces values scanned - * from the specified file. Bytes from the file are converted into - * characters using the specified charset. - * - * @param source - * the path to the file to be scanned - * @param charset - * the charset used to convert bytes from the file - * into characters to be scanned - * @throws IOException - * if an I/O error occurs opening the source - * @since 10 - */ - public Scanner(Path source, Charset charset) throws IOException { - this(makeReadable(source, charset)); - } - - /** - * Constructs a new {@code Scanner} that produces values scanned - * from the specified string. - * - * @param source A string to scan - */ - public Scanner(String source) { - this(new StringReader(source), WHITESPACE_PATTERN); - } - - /** - * Constructs a new {@code Scanner} that produces values scanned - * from the specified channel. Bytes from the source are converted into - * characters using the underlying platform's - * {@linkplain java.nio.charset.Charset#defaultCharset() default charset}. - * - * @param source A channel to scan - */ - public Scanner(ReadableByteChannel source) { - this(makeReadable(Objects.requireNonNull(source, "source")), WHITESPACE_PATTERN); - } - - private static Readable makeReadable(ReadableByteChannel source) { - return makeReadable(source, Charset.defaultCharset().newDecoder()); - } - - /** - * Constructs a new {@code Scanner} that produces values scanned - * from the specified channel. Bytes from the source are converted into - * characters using the specified charset. - * - * @param source A channel to scan - * @param charsetName The encoding type used to convert bytes from the - * channel into characters to be scanned - * @throws IllegalArgumentException if the specified character set - * does not exist - */ - public Scanner(ReadableByteChannel source, String charsetName) { - this(makeReadable(Objects.requireNonNull(source, "source"), toDecoder(charsetName)), WHITESPACE_PATTERN); - } - - /** - * Constructs a new {@code Scanner} that produces values scanned - * from the specified channel. Bytes from the source are converted into - * characters using the specified charset. - * - * @param source a channel to scan - * @param charset the encoding type used to convert bytes from the - * channel into characters to be scanned - * @since 10 - */ - public Scanner(ReadableByteChannel source, Charset charset) { - this(makeReadable(Objects.requireNonNull(source, "source"), charset), WHITESPACE_PATTERN); - } - - // Private primitives used to support scanning - private void saveState() { - savedScannerPosition = position; - } - - private void revertState() { - this.position = savedScannerPosition; - savedScannerPosition = -1; - skipped = false; - } - - private boolean revertState(boolean b) { - this.position = savedScannerPosition; - savedScannerPosition = -1; - skipped = false; - return b; - } - - private void cacheResult() { - hasNextResult = matcher.group(); - hasNextPosition = matcher.end(); - hasNextPattern = matcher.pattern(); - } - - private void cacheResult(String result) { - hasNextResult = result; - hasNextPosition = matcher.end(); - hasNextPattern = matcher.pattern(); - } - - // Clears both regular cache and type cache - private void clearCaches() { - hasNextPattern = null; - typeCache = null; - } - - // Also clears both the regular cache and the type cache - private String getCachedResult() { - position = hasNextPosition; - hasNextPattern = null; - typeCache = null; - return hasNextResult; - } - - // Also clears both the regular cache and the type cache - private void useTypeCache() { - if (closed) - throw new IllegalStateException("Scanner closed"); - position = hasNextPosition; - hasNextPattern = null; - typeCache = null; - } - - // Tries to read more input. May block. - private void readInput() { - if (buf.limit() == buf.capacity()) - makeSpace(); - // Prepare to receive data - int p = buf.position(); - buf.position(buf.limit()); - buf.limit(buf.capacity()); - int n = 0; - try { - n = source.read(buf); - } catch (IOException ioe) { - lastException = ioe; - n = -1; - } - if (n == -1) { - sourceClosed = true; - needInput = false; - } - if (n > 0) - needInput = false; - // Restore current position and limit for reading - buf.limit(buf.position()); - buf.position(p); - } - - // After this method is called there will either be an exception - // or else there will be space in the buffer - private boolean makeSpace() { - clearCaches(); - int offset = savedScannerPosition == -1 ? position : savedScannerPosition; - buf.position(offset); - // Gain space by compacting buffer - if (offset > 0) { - buf.compact(); - translateSavedIndexes(offset); - position -= offset; - buf.flip(); - return true; - } - // Gain space by growing buffer - int newSize = buf.capacity() * 2; - CharBuffer newBuf = CharBuffer.allocate(newSize); - newBuf.put(buf); - newBuf.flip(); - translateSavedIndexes(offset); - position -= offset; - buf = newBuf; - matcher.reset(buf); - return true; - } - - // When a buffer compaction/reallocation occurs the saved indexes must - // be modified appropriately - private void translateSavedIndexes(int offset) { - if (savedScannerPosition != -1) - savedScannerPosition -= offset; - } - - // If we are at the end of input then NoSuchElement; - // If there is still input left then InputMismatch - private void throwFor() { - skipped = false; - if ((sourceClosed) && (position == buf.limit())) - throw new NoSuchElementException(); - else - throw new InputMismatchException(); - } - - // Returns true if a complete token or partial token is in the buffer. - // It is not necessary to find a complete token since a partial token - // means that there will be another token with or without more input. - private boolean hasTokenInBuffer() { - matchValid = false; - matcher.usePattern(delimPattern); - matcher.region(position, buf.limit()); - // Skip delims first - if (matcher.lookingAt()) { - if (matcher.hitEnd() && !sourceClosed) { - // more input might change the match of delims, in which - // might change whether or not if there is token left in - // buffer (don't update the "position" in this case) - needInput = true; - return false; - } - position = matcher.end(); - } - // If we are sitting at the end, no more tokens in buffer - if (position == buf.limit()) - return false; - return true; - } - - /* - * Returns a "complete token" that matches the specified pattern - * - * A token is complete if surrounded by delims; a partial token - * is prefixed by delims but not postfixed by them - * - * The position is advanced to the end of that complete token - * - * Pattern == null means accept any token at all - * - * Triple return: - * 1. valid string means it was found - * 2. null with needInput=false means we won't ever find it - * 3. null with needInput=true means try again after readInput - */ - private String getCompleteTokenInBuffer(Pattern pattern) { - matchValid = false; - // Skip delims first - matcher.usePattern(delimPattern); - if (!skipped) { - // Enforcing only one skip of leading delims - matcher.region(position, buf.limit()); - if (matcher.lookingAt()) { - // If more input could extend the delimiters then we must wait - // for more input - if (matcher.hitEnd() && !sourceClosed) { - needInput = true; - return null; - } - // The delims were whole and the matcher should skip them - skipped = true; - position = matcher.end(); - } - } - // If we are sitting at the end, no more tokens in buffer - if (position == buf.limit()) { - if (sourceClosed) - return null; - needInput = true; - return null; - } - // Must look for next delims. Simply attempting to match the - // pattern at this point may find a match but it might not be - // the first longest match because of missing input, or it might - // match a partial token instead of the whole thing. - // Then look for next delims - matcher.region(position, buf.limit()); - boolean foundNextDelim = matcher.find(); - if (foundNextDelim && (matcher.end() == position)) { - // Zero length delimiter match; we should find the next one - // using the automatic advance past a zero length match; - // Otherwise we have just found the same one we just skipped - foundNextDelim = matcher.find(); - } - if (foundNextDelim) { - // In the rare case that more input could cause the match - // to be lost and there is more input coming we must wait - // for more input. Note that hitting the end is okay as long - // as the match cannot go away. It is the beginning of the - // next delims we want to be sure about, we don't care if - // they potentially extend further. - if (matcher.requireEnd() && !sourceClosed) { - needInput = true; - return null; - } - int tokenEnd = matcher.start(); - // There is a complete token. - if (pattern == null) { - // Must continue with match to provide valid MatchResult - pattern = FIND_ANY_PATTERN; - } - // Attempt to match against the desired pattern - matcher.usePattern(pattern); - matcher.region(position, tokenEnd); - if (matcher.matches()) { - String s = matcher.group(); - position = matcher.end(); - return s; - } else { - // Complete token but it does not match - return null; - } - } - // If we can't find the next delims but no more input is coming, - // then we can treat the remainder as a whole token - if (sourceClosed) { - if (pattern == null) { - // Must continue with match to provide valid MatchResult - pattern = FIND_ANY_PATTERN; - } - // Last token; Match the pattern here or throw - matcher.usePattern(pattern); - matcher.region(position, buf.limit()); - if (matcher.matches()) { - String s = matcher.group(); - position = matcher.end(); - return s; - } - // Last piece does not match - return null; - } - // There is a partial token in the buffer; must read more - // to complete it - needInput = true; - return null; - } - - // Finds the specified pattern in the buffer up to horizon. - // Returns true if the specified input pattern was matched, - // and leaves the matcher field with the current match state. - private boolean findPatternInBuffer(Pattern pattern, int horizon) { - matchValid = false; - matcher.usePattern(pattern); - int bufferLimit = buf.limit(); - int horizonLimit = -1; - int searchLimit = bufferLimit; - if (horizon > 0) { - horizonLimit = position + horizon; - if (horizonLimit < bufferLimit) - searchLimit = horizonLimit; - } - matcher.region(position, searchLimit); - if (matcher.find()) { - if (matcher.hitEnd() && (!sourceClosed)) { - // The match may be longer if didn't hit horizon or real end - if (searchLimit != horizonLimit) { - // Hit an artificial end; try to extend the match - needInput = true; - return false; - } - // The match could go away depending on what is next - if ((searchLimit == horizonLimit) && matcher.requireEnd()) { - // Rare case: we hit the end of input and it happens - // that it is at the horizon and the end of input is - // required for the match. - needInput = true; - return false; - } - } - // Did not hit end, or hit real end, or hit horizon - position = matcher.end(); - return true; - } - if (sourceClosed) - return false; - // If there is no specified horizon, or if we have not searched - // to the specified horizon yet, get more input - if ((horizon == 0) || (searchLimit != horizonLimit)) - needInput = true; - return false; - } - - // Attempts to match a pattern anchored at the current position. - // Returns true if the specified input pattern was matched, - // and leaves the matcher field with the current match state. - private boolean matchPatternInBuffer(Pattern pattern) { - matchValid = false; - matcher.usePattern(pattern); - matcher.region(position, buf.limit()); - if (matcher.lookingAt()) { - if (matcher.hitEnd() && (!sourceClosed)) { - // Get more input and try again - needInput = true; - return false; - } - position = matcher.end(); - return true; - } - if (sourceClosed) - return false; - // Read more to find pattern - needInput = true; - return false; - } - - // Throws if the scanner is closed - private void ensureOpen() { - if (closed) - throw new IllegalStateException("Scanner closed"); - } - - // Public methods - /** - * Closes this scanner. - * - *

If this scanner has not yet been closed then if its underlying - * {@linkplain java.lang.Readable readable} also implements the {@link - * java.io.Closeable} interface then the readable's {@code close} method - * will be invoked. If this scanner is already closed then invoking this - * method will have no effect. - * - *

Attempting to perform search operations after a scanner has - * been closed will result in an {@link IllegalStateException}. - */ - public void close() { - if (closed) - return; - if (source instanceof Closeable) { - try { - ((Closeable) source).close(); - } catch (IOException ioe) { - lastException = ioe; - } - } - sourceClosed = true; - source = null; - closed = true; - } - - /** - * Returns the {@code IOException} last thrown by this - * {@code Scanner}'s underlying {@code Readable}. This method - * returns {@code null} if no such exception exists. - * - * @return the last exception thrown by this scanner's readable - */ - @Nullable - public IOException ioException() { - return lastException; - } - - /** - * Returns the {@code Pattern} this {@code Scanner} is currently - * using to match delimiters. - * - * @return this scanner's delimiting pattern. - */ - public Pattern delimiter() { - return delimPattern; - } - - /** - * Sets this scanner's delimiting pattern to the specified pattern. - * - * @param pattern A delimiting pattern - * @return this scanner - */ - public Scanner useDelimiter(Pattern pattern) { - modCount++; - delimPattern = pattern; - return this; - } - - /** - * Sets this scanner's delimiting pattern to a pattern constructed from - * the specified {@code String}. - * - *

An invocation of this method of the form - * {@code useDelimiter(pattern)} behaves in exactly the same way as the - * invocation {@code useDelimiter(Pattern.compile(pattern))}. - * - *

Invoking the {@link #reset} method will set the scanner's delimiter - * to the default. - * - * @param pattern A string specifying a delimiting pattern - * @return this scanner - */ - public Scanner useDelimiter(String pattern) { - modCount++; - delimPattern = patternCache.forName(pattern); - return this; - } - - /** - * Returns this scanner's locale. - * - *

A scanner's locale affects many elements of its default - * primitive matching regular expressions; see - * localized numbers above. - * - * @return this scanner's locale - */ - public Locale locale() { - return this.locale; - } - - /** - * Sets this scanner's locale to the specified locale. - * - *

A scanner's locale affects many elements of its default - * primitive matching regular expressions; see - * localized numbers above. - * - *

Invoking the {@link #reset} method will set the scanner's locale to - * the initial locale. - * - * @param locale A string specifying the locale to use - * @return this scanner - */ - public Scanner useLocale(Locale locale) { - if (locale.equals(this.locale)) - return this; - modCount++; - this.locale = locale; - DecimalFormat df = null; - NumberFormat nf = NumberFormat.getNumberInstance(locale); - DecimalFormatSymbols dfs = DecimalFormatSymbols.getInstance(locale); - if (nf instanceof DecimalFormat) { - df = (DecimalFormat) nf; - } else { - // In case where NumberFormat.getNumberInstance() returns - // other instance (non DecimalFormat) based on the provider - // used and java.text.spi.NumberFormatProvider implementations, - // DecimalFormat constructor is used to obtain the instance - LocaleProviderAdapter adapter = LocaleProviderAdapter.getAdapter(NumberFormatProvider.class, locale); - if (!(adapter instanceof ResourceBundleBasedAdapter)) { - adapter = LocaleProviderAdapter.getResourceBundleBased(); - } - String[] all = adapter.getLocaleResources(locale).getNumberPatterns(); - df = new DecimalFormat(all[0], dfs); - } - // These must be literalized to avoid collision with regex - // metacharacters such as dot or parenthesis - groupSeparator = "\\" + dfs.getGroupingSeparator(); - decimalSeparator = "\\" + dfs.getDecimalSeparator(); - // Quoting the nonzero length locale-specific things - // to avoid potential conflict with metacharacters - nanString = "\\Q" + dfs.getNaN() + "\\E"; - infinityString = "\\Q" + dfs.getInfinity() + "\\E"; - positivePrefix = df.getPositivePrefix(); - if (positivePrefix.length() > 0) - positivePrefix = "\\Q" + positivePrefix + "\\E"; - negativePrefix = df.getNegativePrefix(); - if (negativePrefix.length() > 0) - negativePrefix = "\\Q" + negativePrefix + "\\E"; - positiveSuffix = df.getPositiveSuffix(); - if (positiveSuffix.length() > 0) - positiveSuffix = "\\Q" + positiveSuffix + "\\E"; - negativeSuffix = df.getNegativeSuffix(); - if (negativeSuffix.length() > 0) - negativeSuffix = "\\Q" + negativeSuffix + "\\E"; - // Force rebuilding and recompilation of locale dependent - // primitive patterns - integerPattern = null; - floatPattern = null; - return this; - } - - /** - * Returns this scanner's default radix. - * - *

A scanner's radix affects elements of its default - * number matching regular expressions; see - * localized numbers above. - * - * @return the default radix of this scanner - */ - public int radix() { - return this.defaultRadix; - } - - /** - * Sets this scanner's default radix to the specified radix. - * - *

A scanner's radix affects elements of its default - * number matching regular expressions; see - * localized numbers above. - * - *

If the radix is less than {@link Character#MIN_RADIX Character.MIN_RADIX} - * or greater than {@link Character#MAX_RADIX Character.MAX_RADIX}, then an - * {@code IllegalArgumentException} is thrown. - * - *

Invoking the {@link #reset} method will set the scanner's radix to - * {@code 10}. - * - * @param radix The radix to use when scanning numbers - * @return this scanner - * @throws IllegalArgumentException if radix is out of range - */ - public Scanner useRadix(int radix) { - if ((radix < Character.MIN_RADIX) || (radix > Character.MAX_RADIX)) - throw new IllegalArgumentException("radix:" + radix); - if (this.defaultRadix == radix) - return this; - modCount++; - this.defaultRadix = radix; - // Force rebuilding and recompilation of radix dependent patterns - integerPattern = null; - return this; - } - - // The next operation should occur in the specified radix but - // the default is left untouched. - private void setRadix(int radix) { - if ((radix < Character.MIN_RADIX) || (radix > Character.MAX_RADIX)) - throw new IllegalArgumentException("radix:" + radix); - if (this.radix != radix) { - // Force rebuilding and recompilation of radix dependent patterns - integerPattern = null; - this.radix = radix; - } - } - - /** - * Returns the match result of the last scanning operation performed - * by this scanner. This method throws {@code IllegalStateException} - * if no match has been performed, or if the last match was - * not successful. - * - *

The various {@code next} methods of {@code Scanner} - * make a match result available if they complete without throwing an - * exception. For instance, after an invocation of the {@link #nextInt} - * method that returned an int, this method returns a - * {@code MatchResult} for the search of the - * Integer regular expression - * defined above. Similarly the {@link #findInLine findInLine()}, - * {@link #findWithinHorizon findWithinHorizon()}, and {@link #skip skip()} - * methods will make a match available if they succeed. - * - * @return a match result for the last match operation - * @throws IllegalStateException If no match result is available - */ - public MatchResult match() { - if (!matchValid) - throw new IllegalStateException("No match result available"); - return matcher.toMatchResult(); - } - - /** - *

Returns the string representation of this {@code Scanner}. The - * string representation of a {@code Scanner} contains information - * that may be useful for debugging. The exact format is unspecified. - * - * @return The string representation of this scanner - */ - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("java.util.Scanner"); - sb.append("[delimiters=" + delimPattern + "]"); - sb.append("[position=" + position + "]"); - sb.append("[match valid=" + matchValid + "]"); - sb.append("[need input=" + needInput + "]"); - sb.append("[source closed=" + sourceClosed + "]"); - sb.append("[skipped=" + skipped + "]"); - sb.append("[group separator=" + groupSeparator + "]"); - sb.append("[decimal separator=" + decimalSeparator + "]"); - sb.append("[positive prefix=" + positivePrefix + "]"); - sb.append("[negative prefix=" + negativePrefix + "]"); - sb.append("[positive suffix=" + positiveSuffix + "]"); - sb.append("[negative suffix=" + negativeSuffix + "]"); - sb.append("[NaN string=" + nanString + "]"); - sb.append("[infinity string=" + infinityString + "]"); - return sb.toString(); - } - - /** - * Returns true if this scanner has another token in its input. - * This method may block while waiting for input to scan. - * The scanner does not advance past any input. - * - * @return true if and only if this scanner has another token - * @throws IllegalStateException if this scanner is closed - * @see java.util.Iterator - */ - public boolean hasNext() { - ensureOpen(); - saveState(); - modCount++; - while (!sourceClosed) { - if (hasTokenInBuffer()) { - return revertState(true); - } - readInput(); - } - boolean result = hasTokenInBuffer(); - return revertState(result); - } - - /** - * Finds and returns the next complete token from this scanner. - * A complete token is preceded and followed by input that matches - * the delimiter pattern. This method may block while waiting for input - * to scan, even if a previous invocation of {@link #hasNext} returned - * {@code true}. - * - * @return the next token - * @throws NoSuchElementException if no more tokens are available - * @throws IllegalStateException if this scanner is closed - * @see java.util.Iterator - */ - public String next() { - ensureOpen(); - clearCaches(); - modCount++; - while (true) { - String token = getCompleteTokenInBuffer(null); - if (token != null) { - matchValid = true; - skipped = false; - return token; - } - if (needInput) - readInput(); - else - throwFor(); - } - } - - /** - * The remove operation is not supported by this implementation of - * {@code Iterator}. - * - * @throws UnsupportedOperationException if this method is invoked. - * @see java.util.Iterator - */ - public void remove() { - throw new UnsupportedOperationException(); - } - - /** - * Returns true if the next token matches the pattern constructed from the - * specified string. The scanner does not advance past any input. - * - *

An invocation of this method of the form {@code hasNext(pattern)} - * behaves in exactly the same way as the invocation - * {@code hasNext(Pattern.compile(pattern))}. - * - * @param pattern a string specifying the pattern to scan - * @return true if and only if this scanner has another token matching - * the specified pattern - * @throws IllegalStateException if this scanner is closed - */ - public boolean hasNext(String pattern) { - return hasNext(patternCache.forName(pattern)); - } - - /** - * Returns the next token if it matches the pattern constructed from the - * specified string. If the match is successful, the scanner advances - * past the input that matched the pattern. - * - *

An invocation of this method of the form {@code next(pattern)} - * behaves in exactly the same way as the invocation - * {@code next(Pattern.compile(pattern))}. - * - * @param pattern a string specifying the pattern to scan - * @return the next token - * @throws NoSuchElementException if no such tokens are available - * @throws IllegalStateException if this scanner is closed - */ - public String next(String pattern) { - return next(patternCache.forName(pattern)); - } - - /** - * Returns true if the next complete token matches the specified pattern. - * A complete token is prefixed and postfixed by input that matches - * the delimiter pattern. This method may block while waiting for input. - * The scanner does not advance past any input. - * - * @param pattern the pattern to scan for - * @return true if and only if this scanner has another token matching - * the specified pattern - * @throws IllegalStateException if this scanner is closed - */ - public boolean hasNext(Pattern pattern) { - ensureOpen(); - if (pattern == null) - throw new NullPointerException(); - hasNextPattern = null; - saveState(); - modCount++; - while (true) { - if (getCompleteTokenInBuffer(pattern) != null) { - matchValid = true; - cacheResult(); - return revertState(true); - } - if (needInput) - readInput(); - else - return revertState(false); - } - } - - /** - * Returns the next token if it matches the specified pattern. This - * method may block while waiting for input to scan, even if a previous - * invocation of {@link #hasNext(Pattern)} returned {@code true}. - * If the match is successful, the scanner advances past the input that - * matched the pattern. - * - * @param pattern the pattern to scan for - * @return the next token - * @throws NoSuchElementException if no more tokens are available - * @throws IllegalStateException if this scanner is closed - */ - public String next(Pattern pattern) { - ensureOpen(); - if (pattern == null) - throw new NullPointerException(); - modCount++; - // Did we already find this pattern? - if (hasNextPattern == pattern) - return getCachedResult(); - clearCaches(); - // Search for the pattern - while (true) { - String token = getCompleteTokenInBuffer(pattern); - if (token != null) { - matchValid = true; - skipped = false; - return token; - } - if (needInput) - readInput(); - else - throwFor(); - } - } - - /** - * Returns true if there is another line in the input of this scanner. - * This method may block while waiting for input. The scanner does not - * advance past any input. - * - * @return true if and only if this scanner has another line of input - * @throws IllegalStateException if this scanner is closed - */ - public boolean hasNextLine() { - saveState(); - modCount++; - String result = findWithinHorizon(linePattern(), 0); - if (result != null) { - MatchResult mr = this.match(); - String lineSep = mr.group(1); - if (lineSep != null) { - result = result.substring(0, result.length() - lineSep.length()); - cacheResult(result); - } else { - cacheResult(); - } - } - revertState(); - return (result != null); - } - - /** - * Advances this scanner past the current line and returns the input - * that was skipped. - * - * This method returns the rest of the current line, excluding any line - * separator at the end. The position is set to the beginning of the next - * line. - * - *

Since this method continues to search through the input looking - * for a line separator, it may buffer all of the input searching for - * the line to skip if no line separators are present. - * - * @return the line that was skipped - * @throws NoSuchElementException if no line was found - * @throws IllegalStateException if this scanner is closed - */ - public String nextLine() { - modCount++; - if (hasNextPattern == linePattern()) - return getCachedResult(); - clearCaches(); - String result = findWithinHorizon(linePattern, 0); - if (result == null) - throw new NoSuchElementException("No line found"); - MatchResult mr = this.match(); - String lineSep = mr.group(1); - if (lineSep != null) - result = result.substring(0, result.length() - lineSep.length()); - if (result == null) - throw new NoSuchElementException(); - else - return result; - } - - // Public methods that ignore delimiters - /** - * Attempts to find the next occurrence of a pattern constructed from the - * specified string, ignoring delimiters. - * - *

An invocation of this method of the form {@code findInLine(pattern)} - * behaves in exactly the same way as the invocation - * {@code findInLine(Pattern.compile(pattern))}. - * - * @param pattern a string specifying the pattern to search for - * @return the text that matched the specified pattern - * @throws IllegalStateException if this scanner is closed - */ - @Nullable - public String findInLine(String pattern) { - return findInLine(patternCache.forName(pattern)); - } - - /** - * Attempts to find the next occurrence of the specified pattern ignoring - * delimiters. If the pattern is found before the next line separator, the - * scanner advances past the input that matched and returns the string that - * matched the pattern. - * If no such pattern is detected in the input up to the next line - * separator, then {@code null} is returned and the scanner's - * position is unchanged. This method may block waiting for input that - * matches the pattern. - * - *

Since this method continues to search through the input looking - * for the specified pattern, it may buffer all of the input searching for - * the desired token if no line separators are present. - * - * @param pattern the pattern to scan for - * @return the text that matched the specified pattern - * @throws IllegalStateException if this scanner is closed - */ - @Nullable - public String findInLine(Pattern pattern) { - ensureOpen(); - if (pattern == null) - throw new NullPointerException(); - clearCaches(); - modCount++; - // Expand buffer to include the next newline or end of input - int endPosition = 0; - saveState(); - while (true) { - if (findPatternInBuffer(separatorPattern(), 0)) { - endPosition = matcher.start(); - // up to next newline - break; - } - if (needInput) { - readInput(); - } else { - endPosition = buf.limit(); - // up to end of input - break; - } - } - revertState(); - int horizonForLine = endPosition - position; - // If there is nothing between the current pos and the next - // newline simply return null, invoking findWithinHorizon - // with "horizon=0" will scan beyond the line bound. - if (horizonForLine == 0) - return null; - // Search for the pattern - return findWithinHorizon(pattern, horizonForLine); - } - - /** - * Attempts to find the next occurrence of a pattern constructed from the - * specified string, ignoring delimiters. - * - *

An invocation of this method of the form - * {@code findWithinHorizon(pattern)} behaves in exactly the same way as - * the invocation - * {@code findWithinHorizon(Pattern.compile(pattern), horizon)}. - * - * @param pattern a string specifying the pattern to search for - * @param horizon the search horizon - * @return the text that matched the specified pattern - * @throws IllegalStateException if this scanner is closed - * @throws IllegalArgumentException if horizon is negative - */ - @Nullable - public String findWithinHorizon(String pattern, int horizon) { - return findWithinHorizon(patternCache.forName(pattern), horizon); - } - - /** - * Attempts to find the next occurrence of the specified pattern. - * - *

This method searches through the input up to the specified - * search horizon, ignoring delimiters. If the pattern is found the - * scanner advances past the input that matched and returns the string - * that matched the pattern. If no such pattern is detected then the - * null is returned and the scanner's position remains unchanged. This - * method may block waiting for input that matches the pattern. - * - *

A scanner will never search more than {@code horizon} code - * points beyond its current position. Note that a match may be clipped - * by the horizon; that is, an arbitrary match result may have been - * different if the horizon had been larger. The scanner treats the - * horizon as a transparent, non-anchoring bound (see {@link - * Matcher#useTransparentBounds} and {@link Matcher#useAnchoringBounds}). - * - *

If horizon is {@code 0}, then the horizon is ignored and - * this method continues to search through the input looking for the - * specified pattern without bound. In this case it may buffer all of - * the input searching for the pattern. - * - *

If horizon is negative, then an IllegalArgumentException is - * thrown. - * - * @param pattern the pattern to scan for - * @param horizon the search horizon - * @return the text that matched the specified pattern - * @throws IllegalStateException if this scanner is closed - * @throws IllegalArgumentException if horizon is negative - */ - @Nullable - public String findWithinHorizon(Pattern pattern, int horizon) { - ensureOpen(); - if (pattern == null) - throw new NullPointerException(); - if (horizon < 0) - throw new IllegalArgumentException("horizon < 0"); - clearCaches(); - modCount++; - // Search for the pattern - while (true) { - if (findPatternInBuffer(pattern, horizon)) { - matchValid = true; - return matcher.group(); - } - if (needInput) - readInput(); - else - // up to end of input - break; - } - return null; - } - - /** - * Skips input that matches the specified pattern, ignoring delimiters. - * This method will skip input if an anchored match of the specified - * pattern succeeds. - * - *

If a match to the specified pattern is not found at the - * current position, then no input is skipped and a - * {@code NoSuchElementException} is thrown. - * - *

Since this method seeks to match the specified pattern starting at - * the scanner's current position, patterns that can match a lot of - * input (".*", for example) may cause the scanner to buffer a large - * amount of input. - * - *

Note that it is possible to skip something without risking a - * {@code NoSuchElementException} by using a pattern that can - * match nothing, e.g., {@code sc.skip("[ \t]*")}. - * - * @param pattern a string specifying the pattern to skip over - * @return this scanner - * @throws NoSuchElementException if the specified pattern is not found - * @throws IllegalStateException if this scanner is closed - */ - public Scanner skip(Pattern pattern) { - ensureOpen(); - if (pattern == null) - throw new NullPointerException(); - clearCaches(); - modCount++; - // Search for the pattern - while (true) { - if (matchPatternInBuffer(pattern)) { - matchValid = true; - position = matcher.end(); - return this; - } - if (needInput) - readInput(); - else - throw new NoSuchElementException(); - } - } - - /** - * Skips input that matches a pattern constructed from the specified - * string. - * - *

An invocation of this method of the form {@code skip(pattern)} - * behaves in exactly the same way as the invocation - * {@code skip(Pattern.compile(pattern))}. - * - * @param pattern a string specifying the pattern to skip over - * @return this scanner - * @throws IllegalStateException if this scanner is closed - */ - public Scanner skip(String pattern) { - return skip(patternCache.forName(pattern)); - } - - // Convenience methods for scanning primitives - /** - * Returns true if the next token in this scanner's input can be - * interpreted as a boolean value using a case insensitive pattern - * created from the string "true|false". The scanner does not - * advance past the input that matched. - * - * @return true if and only if this scanner's next token is a valid - * boolean value - * @throws IllegalStateException if this scanner is closed - */ - public boolean hasNextBoolean() { - return hasNext(boolPattern()); - } - - /** - * Scans the next token of the input into a boolean value and returns - * that value. This method will throw {@code InputMismatchException} - * if the next token cannot be translated into a valid boolean value. - * If the match is successful, the scanner advances past the input that - * matched. - * - * @return the boolean scanned from the input - * @throws InputMismatchException if the next token is not a valid boolean - * @throws NoSuchElementException if input is exhausted - * @throws IllegalStateException if this scanner is closed - */ - public boolean nextBoolean() { - clearCaches(); - return Boolean.parseBoolean(next(boolPattern())); - } - - /** - * Returns true if the next token in this scanner's input can be - * interpreted as a byte value in the default radix using the - * {@link #nextByte} method. The scanner does not advance past any input. - * - * @return true if and only if this scanner's next token is a valid - * byte value - * @throws IllegalStateException if this scanner is closed - */ - public boolean hasNextByte() { - return hasNextByte(defaultRadix); - } - - /** - * Returns true if the next token in this scanner's input can be - * interpreted as a byte value in the specified radix using the - * {@link #nextByte} method. The scanner does not advance past any input. - * - *

If the radix is less than {@link Character#MIN_RADIX Character.MIN_RADIX} - * or greater than {@link Character#MAX_RADIX Character.MAX_RADIX}, then an - * {@code IllegalArgumentException} is thrown. - * - * @param radix the radix used to interpret the token as a byte value - * @return true if and only if this scanner's next token is a valid - * byte value - * @throws IllegalStateException if this scanner is closed - * @throws IllegalArgumentException if the radix is out of range - */ - public boolean hasNextByte(int radix) { - setRadix(radix); - boolean result = hasNext(integerPattern()); - if (result) { - // Cache it - try { - String s = (matcher.group(SIMPLE_GROUP_INDEX) == null) ? processIntegerToken(hasNextResult) : hasNextResult; - typeCache = Byte.parseByte(s, radix); - } catch (NumberFormatException nfe) { - result = false; - } - } - return result; - } - - /** - * Scans the next token of the input as a {@code byte}. - * - *

An invocation of this method of the form - * {@code nextByte()} behaves in exactly the same way as the - * invocation {@code nextByte(radix)}, where {@code radix} - * is the default radix of this scanner. - * - * @return the {@code byte} scanned from the input - * @throws InputMismatchException - * if the next token does not match the Integer - * regular expression, or is out of range - * @throws NoSuchElementException if input is exhausted - * @throws IllegalStateException if this scanner is closed - */ - public byte nextByte() { - return nextByte(defaultRadix); - } - - /** - * Scans the next token of the input as a {@code byte}. - * This method will throw {@code InputMismatchException} - * if the next token cannot be translated into a valid byte value as - * described below. If the translation is successful, the scanner advances - * past the input that matched. - * - *

If the next token matches the Integer regular expression defined - * above then the token is converted into a {@code byte} value as if by - * removing all locale specific prefixes, group separators, and locale - * specific suffixes, then mapping non-ASCII digits into ASCII - * digits via {@link Character#digit Character.digit}, prepending a - * negative sign (-) if the locale specific negative prefixes and suffixes - * were present, and passing the resulting string to - * {@link Byte#parseByte(String, int) Byte.parseByte} with the - * specified radix. - * - *

If the radix is less than {@link Character#MIN_RADIX Character.MIN_RADIX} - * or greater than {@link Character#MAX_RADIX Character.MAX_RADIX}, then an - * {@code IllegalArgumentException} is thrown. - * - * @param radix the radix used to interpret the token as a byte value - * @return the {@code byte} scanned from the input - * @throws InputMismatchException - * if the next token does not match the Integer - * regular expression, or is out of range - * @throws NoSuchElementException if input is exhausted - * @throws IllegalStateException if this scanner is closed - * @throws IllegalArgumentException if the radix is out of range - */ - public byte nextByte(int radix) { - // Check cached result - if ((typeCache != null) && (typeCache instanceof Byte) && this.radix == radix) { - byte val = ((Byte) typeCache).byteValue(); - useTypeCache(); - return val; - } - setRadix(radix); - clearCaches(); - // Search for next byte - try { - String s = next(integerPattern()); - if (matcher.group(SIMPLE_GROUP_INDEX) == null) - s = processIntegerToken(s); - return Byte.parseByte(s, radix); - } catch (NumberFormatException nfe) { - // don't skip bad token - position = matcher.start(); - throw new InputMismatchException(nfe.getMessage()); - } - } - - /** - * Returns true if the next token in this scanner's input can be - * interpreted as a short value in the default radix using the - * {@link #nextShort} method. The scanner does not advance past any input. - * - * @return true if and only if this scanner's next token is a valid - * short value in the default radix - * @throws IllegalStateException if this scanner is closed - */ - public boolean hasNextShort() { - return hasNextShort(defaultRadix); - } - - /** - * Returns true if the next token in this scanner's input can be - * interpreted as a short value in the specified radix using the - * {@link #nextShort} method. The scanner does not advance past any input. - * - *

If the radix is less than {@link Character#MIN_RADIX Character.MIN_RADIX} - * or greater than {@link Character#MAX_RADIX Character.MAX_RADIX}, then an - * {@code IllegalArgumentException} is thrown. - * - * @param radix the radix used to interpret the token as a short value - * @return true if and only if this scanner's next token is a valid - * short value in the specified radix - * @throws IllegalStateException if this scanner is closed - * @throws IllegalArgumentException if the radix is out of range - */ - public boolean hasNextShort(int radix) { - setRadix(radix); - boolean result = hasNext(integerPattern()); - if (result) { - // Cache it - try { - String s = (matcher.group(SIMPLE_GROUP_INDEX) == null) ? processIntegerToken(hasNextResult) : hasNextResult; - typeCache = Short.parseShort(s, radix); - } catch (NumberFormatException nfe) { - result = false; - } - } - return result; - } - - /** - * Scans the next token of the input as a {@code short}. - * - *

An invocation of this method of the form - * {@code nextShort()} behaves in exactly the same way as the - * invocation {@link #nextShort(int) nextShort(radix)}, where {@code radix} - * is the default radix of this scanner. - * - * @return the {@code short} scanned from the input - * @throws InputMismatchException - * if the next token does not match the Integer - * regular expression, or is out of range - * @throws NoSuchElementException if input is exhausted - * @throws IllegalStateException if this scanner is closed - */ - public short nextShort() { - return nextShort(defaultRadix); - } - - /** - * Scans the next token of the input as a {@code short}. - * This method will throw {@code InputMismatchException} - * if the next token cannot be translated into a valid short value as - * described below. If the translation is successful, the scanner advances - * past the input that matched. - * - *

If the next token matches the Integer regular expression defined - * above then the token is converted into a {@code short} value as if by - * removing all locale specific prefixes, group separators, and locale - * specific suffixes, then mapping non-ASCII digits into ASCII - * digits via {@link Character#digit Character.digit}, prepending a - * negative sign (-) if the locale specific negative prefixes and suffixes - * were present, and passing the resulting string to - * {@link Short#parseShort(String, int) Short.parseShort} with the - * specified radix. - * - *

If the radix is less than {@link Character#MIN_RADIX Character.MIN_RADIX} - * or greater than {@link Character#MAX_RADIX Character.MAX_RADIX}, then an - * {@code IllegalArgumentException} is thrown. - * - * @param radix the radix used to interpret the token as a short value - * @return the {@code short} scanned from the input - * @throws InputMismatchException - * if the next token does not match the Integer - * regular expression, or is out of range - * @throws NoSuchElementException if input is exhausted - * @throws IllegalStateException if this scanner is closed - * @throws IllegalArgumentException if the radix is out of range - */ - public short nextShort(int radix) { - // Check cached result - if ((typeCache != null) && (typeCache instanceof Short) && this.radix == radix) { - short val = ((Short) typeCache).shortValue(); - useTypeCache(); - return val; - } - setRadix(radix); - clearCaches(); - // Search for next short - try { - String s = next(integerPattern()); - if (matcher.group(SIMPLE_GROUP_INDEX) == null) - s = processIntegerToken(s); - return Short.parseShort(s, radix); - } catch (NumberFormatException nfe) { - // don't skip bad token - position = matcher.start(); - throw new InputMismatchException(nfe.getMessage()); - } - } - - /** - * Returns true if the next token in this scanner's input can be - * interpreted as an int value in the default radix using the - * {@link #nextInt} method. The scanner does not advance past any input. - * - * @return true if and only if this scanner's next token is a valid - * int value - * @throws IllegalStateException if this scanner is closed - */ - public boolean hasNextInt() { - return hasNextInt(defaultRadix); - } - - /** - * Returns true if the next token in this scanner's input can be - * interpreted as an int value in the specified radix using the - * {@link #nextInt} method. The scanner does not advance past any input. - * - *

If the radix is less than {@link Character#MIN_RADIX Character.MIN_RADIX} - * or greater than {@link Character#MAX_RADIX Character.MAX_RADIX}, then an - * {@code IllegalArgumentException} is thrown. - * - * @param radix the radix used to interpret the token as an int value - * @return true if and only if this scanner's next token is a valid - * int value - * @throws IllegalStateException if this scanner is closed - * @throws IllegalArgumentException if the radix is out of range - */ - public boolean hasNextInt(int radix) { - setRadix(radix); - boolean result = hasNext(integerPattern()); - if (result) { - // Cache it - try { - String s = (matcher.group(SIMPLE_GROUP_INDEX) == null) ? processIntegerToken(hasNextResult) : hasNextResult; - typeCache = Integer.parseInt(s, radix); - } catch (NumberFormatException nfe) { - result = false; - } - } - return result; - } - - /** - * The integer token must be stripped of prefixes, group separators, - * and suffixes, non ascii digits must be converted into ascii digits - * before parse will accept it. - */ - private String processIntegerToken(String token) { - String result = token.replaceAll("" + groupSeparator, ""); - boolean isNegative = false; - int preLen = negativePrefix.length(); - if ((preLen > 0) && result.startsWith(negativePrefix)) { - isNegative = true; - result = result.substring(preLen); - } - int sufLen = negativeSuffix.length(); - if ((sufLen > 0) && result.endsWith(negativeSuffix)) { - isNegative = true; - result = result.substring(result.length() - sufLen, result.length()); - } - if (isNegative) - result = "-" + result; - return result; - } - - /** - * Scans the next token of the input as an {@code int}. - * - *

An invocation of this method of the form - * {@code nextInt()} behaves in exactly the same way as the - * invocation {@code nextInt(radix)}, where {@code radix} - * is the default radix of this scanner. - * - * @return the {@code int} scanned from the input - * @throws InputMismatchException - * if the next token does not match the Integer - * regular expression, or is out of range - * @throws NoSuchElementException if input is exhausted - * @throws IllegalStateException if this scanner is closed - */ - public int nextInt() { - return nextInt(defaultRadix); - } - - /** - * Scans the next token of the input as an {@code int}. - * This method will throw {@code InputMismatchException} - * if the next token cannot be translated into a valid int value as - * described below. If the translation is successful, the scanner advances - * past the input that matched. - * - *

If the next token matches the Integer regular expression defined - * above then the token is converted into an {@code int} value as if by - * removing all locale specific prefixes, group separators, and locale - * specific suffixes, then mapping non-ASCII digits into ASCII - * digits via {@link Character#digit Character.digit}, prepending a - * negative sign (-) if the locale specific negative prefixes and suffixes - * were present, and passing the resulting string to - * {@link Integer#parseInt(String, int) Integer.parseInt} with the - * specified radix. - * - *

If the radix is less than {@link Character#MIN_RADIX Character.MIN_RADIX} - * or greater than {@link Character#MAX_RADIX Character.MAX_RADIX}, then an - * {@code IllegalArgumentException} is thrown. - * - * @param radix the radix used to interpret the token as an int value - * @return the {@code int} scanned from the input - * @throws InputMismatchException - * if the next token does not match the Integer - * regular expression, or is out of range - * @throws NoSuchElementException if input is exhausted - * @throws IllegalStateException if this scanner is closed - * @throws IllegalArgumentException if the radix is out of range - */ - public int nextInt(int radix) { - // Check cached result - if ((typeCache != null) && (typeCache instanceof Integer) && this.radix == radix) { - int val = ((Integer) typeCache).intValue(); - useTypeCache(); - return val; - } - setRadix(radix); - clearCaches(); - // Search for next int - try { - String s = next(integerPattern()); - if (matcher.group(SIMPLE_GROUP_INDEX) == null) - s = processIntegerToken(s); - return Integer.parseInt(s, radix); - } catch (NumberFormatException nfe) { - // don't skip bad token - position = matcher.start(); - throw new InputMismatchException(nfe.getMessage()); - } - } - - /** - * Returns true if the next token in this scanner's input can be - * interpreted as a long value in the default radix using the - * {@link #nextLong} method. The scanner does not advance past any input. - * - * @return true if and only if this scanner's next token is a valid - * long value - * @throws IllegalStateException if this scanner is closed - */ - public boolean hasNextLong() { - return hasNextLong(defaultRadix); - } - - /** - * Returns true if the next token in this scanner's input can be - * interpreted as a long value in the specified radix using the - * {@link #nextLong} method. The scanner does not advance past any input. - * - *

If the radix is less than {@link Character#MIN_RADIX Character.MIN_RADIX} - * or greater than {@link Character#MAX_RADIX Character.MAX_RADIX}, then an - * {@code IllegalArgumentException} is thrown. - * - * @param radix the radix used to interpret the token as a long value - * @return true if and only if this scanner's next token is a valid - * long value - * @throws IllegalStateException if this scanner is closed - * @throws IllegalArgumentException if the radix is out of range - */ - public boolean hasNextLong(int radix) { - setRadix(radix); - boolean result = hasNext(integerPattern()); - if (result) { - // Cache it - try { - String s = (matcher.group(SIMPLE_GROUP_INDEX) == null) ? processIntegerToken(hasNextResult) : hasNextResult; - typeCache = Long.parseLong(s, radix); - } catch (NumberFormatException nfe) { - result = false; - } - } - return result; - } - - /** - * Scans the next token of the input as a {@code long}. - * - *

An invocation of this method of the form - * {@code nextLong()} behaves in exactly the same way as the - * invocation {@code nextLong(radix)}, where {@code radix} - * is the default radix of this scanner. - * - * @return the {@code long} scanned from the input - * @throws InputMismatchException - * if the next token does not match the Integer - * regular expression, or is out of range - * @throws NoSuchElementException if input is exhausted - * @throws IllegalStateException if this scanner is closed - */ - public long nextLong() { - return nextLong(defaultRadix); - } - - /** - * Scans the next token of the input as a {@code long}. - * This method will throw {@code InputMismatchException} - * if the next token cannot be translated into a valid long value as - * described below. If the translation is successful, the scanner advances - * past the input that matched. - * - *

If the next token matches the Integer regular expression defined - * above then the token is converted into a {@code long} value as if by - * removing all locale specific prefixes, group separators, and locale - * specific suffixes, then mapping non-ASCII digits into ASCII - * digits via {@link Character#digit Character.digit}, prepending a - * negative sign (-) if the locale specific negative prefixes and suffixes - * were present, and passing the resulting string to - * {@link Long#parseLong(String, int) Long.parseLong} with the - * specified radix. - * - *

If the radix is less than {@link Character#MIN_RADIX Character.MIN_RADIX} - * or greater than {@link Character#MAX_RADIX Character.MAX_RADIX}, then an - * {@code IllegalArgumentException} is thrown. - * - * @param radix the radix used to interpret the token as an int value - * @return the {@code long} scanned from the input - * @throws InputMismatchException - * if the next token does not match the Integer - * regular expression, or is out of range - * @throws NoSuchElementException if input is exhausted - * @throws IllegalStateException if this scanner is closed - * @throws IllegalArgumentException if the radix is out of range - */ - public long nextLong(int radix) { - // Check cached result - if ((typeCache != null) && (typeCache instanceof Long) && this.radix == radix) { - long val = ((Long) typeCache).longValue(); - useTypeCache(); - return val; - } - setRadix(radix); - clearCaches(); - try { - String s = next(integerPattern()); - if (matcher.group(SIMPLE_GROUP_INDEX) == null) - s = processIntegerToken(s); - return Long.parseLong(s, radix); - } catch (NumberFormatException nfe) { - // don't skip bad token - position = matcher.start(); - throw new InputMismatchException(nfe.getMessage()); - } - } - - /** - * The float token must be stripped of prefixes, group separators, - * and suffixes, non ascii digits must be converted into ascii digits - * before parseFloat will accept it. - * - * If there are non-ascii digits in the token these digits must - * be processed before the token is passed to parseFloat. - */ - private String processFloatToken(String token) { - String result = token.replaceAll(groupSeparator, ""); - if (!decimalSeparator.equals("\\.")) - result = result.replaceAll(decimalSeparator, "."); - boolean isNegative = false; - int preLen = negativePrefix.length(); - if ((preLen > 0) && result.startsWith(negativePrefix)) { - isNegative = true; - result = result.substring(preLen); - } - int sufLen = negativeSuffix.length(); - if ((sufLen > 0) && result.endsWith(negativeSuffix)) { - isNegative = true; - result = result.substring(result.length() - sufLen, result.length()); - } - if (result.equals(nanString)) - result = "NaN"; - if (result.equals(infinityString)) - result = "Infinity"; - if (isNegative) - result = "-" + result; - // Translate non-ASCII digits - Matcher m = NON_ASCII_DIGIT.matcher(result); - if (m.find()) { - StringBuilder inASCII = new StringBuilder(); - for (int i = 0; i < result.length(); i++) { - char nextChar = result.charAt(i); - if (Character.isDigit(nextChar)) { - int d = Character.digit(nextChar, 10); - if (d != -1) - inASCII.append(d); - else - inASCII.append(nextChar); - } else { - inASCII.append(nextChar); - } - } - result = inASCII.toString(); - } - return result; - } - - /** - * Returns true if the next token in this scanner's input can be - * interpreted as a float value using the {@link #nextFloat} - * method. The scanner does not advance past any input. - * - * @return true if and only if this scanner's next token is a valid - * float value - * @throws IllegalStateException if this scanner is closed - */ - public boolean hasNextFloat() { - setRadix(10); - boolean result = hasNext(floatPattern()); - if (result) { - // Cache it - try { - String s = processFloatToken(hasNextResult); - typeCache = Float.valueOf(Float.parseFloat(s)); - } catch (NumberFormatException nfe) { - result = false; - } - } - return result; - } - - /** - * Scans the next token of the input as a {@code float}. - * This method will throw {@code InputMismatchException} - * if the next token cannot be translated into a valid float value as - * described below. If the translation is successful, the scanner advances - * past the input that matched. - * - *

If the next token matches the Float regular expression defined above - * then the token is converted into a {@code float} value as if by - * removing all locale specific prefixes, group separators, and locale - * specific suffixes, then mapping non-ASCII digits into ASCII - * digits via {@link Character#digit Character.digit}, prepending a - * negative sign (-) if the locale specific negative prefixes and suffixes - * were present, and passing the resulting string to - * {@link Float#parseFloat Float.parseFloat}. If the token matches - * the localized NaN or infinity strings, then either "Nan" or "Infinity" - * is passed to {@link Float#parseFloat(String) Float.parseFloat} as - * appropriate. - * - * @return the {@code float} scanned from the input - * @throws InputMismatchException - * if the next token does not match the Float - * regular expression, or is out of range - * @throws NoSuchElementException if input is exhausted - * @throws IllegalStateException if this scanner is closed - */ - public float nextFloat() { - // Check cached result - if ((typeCache != null) && (typeCache instanceof Float)) { - float val = ((Float) typeCache).floatValue(); - useTypeCache(); - return val; - } - setRadix(10); - clearCaches(); - try { - return Float.parseFloat(processFloatToken(next(floatPattern()))); - } catch (NumberFormatException nfe) { - // don't skip bad token - position = matcher.start(); - throw new InputMismatchException(nfe.getMessage()); - } - } - - /** - * Returns true if the next token in this scanner's input can be - * interpreted as a double value using the {@link #nextDouble} - * method. The scanner does not advance past any input. - * - * @return true if and only if this scanner's next token is a valid - * double value - * @throws IllegalStateException if this scanner is closed - */ - public boolean hasNextDouble() { - setRadix(10); - boolean result = hasNext(floatPattern()); - if (result) { - // Cache it - try { - String s = processFloatToken(hasNextResult); - typeCache = Double.valueOf(Double.parseDouble(s)); - } catch (NumberFormatException nfe) { - result = false; - } - } - return result; - } - - /** - * Scans the next token of the input as a {@code double}. - * This method will throw {@code InputMismatchException} - * if the next token cannot be translated into a valid double value. - * If the translation is successful, the scanner advances past the input - * that matched. - * - *

If the next token matches the Float regular expression defined above - * then the token is converted into a {@code double} value as if by - * removing all locale specific prefixes, group separators, and locale - * specific suffixes, then mapping non-ASCII digits into ASCII - * digits via {@link Character#digit Character.digit}, prepending a - * negative sign (-) if the locale specific negative prefixes and suffixes - * were present, and passing the resulting string to - * {@link Double#parseDouble Double.parseDouble}. If the token matches - * the localized NaN or infinity strings, then either "Nan" or "Infinity" - * is passed to {@link Double#parseDouble(String) Double.parseDouble} as - * appropriate. - * - * @return the {@code double} scanned from the input - * @throws InputMismatchException - * if the next token does not match the Float - * regular expression, or is out of range - * @throws NoSuchElementException if the input is exhausted - * @throws IllegalStateException if this scanner is closed - */ - public double nextDouble() { - // Check cached result - if ((typeCache != null) && (typeCache instanceof Double)) { - double val = ((Double) typeCache).doubleValue(); - useTypeCache(); - return val; - } - setRadix(10); - clearCaches(); - // Search for next float - try { - return Double.parseDouble(processFloatToken(next(floatPattern()))); - } catch (NumberFormatException nfe) { - // don't skip bad token - position = matcher.start(); - throw new InputMismatchException(nfe.getMessage()); - } - } - - // Convenience methods for scanning multi precision numbers - /** - * Returns true if the next token in this scanner's input can be - * interpreted as a {@code BigInteger} in the default radix using the - * {@link #nextBigInteger} method. The scanner does not advance past any - * input. - * - * @return true if and only if this scanner's next token is a valid - * {@code BigInteger} - * @throws IllegalStateException if this scanner is closed - */ - public boolean hasNextBigInteger() { - return hasNextBigInteger(defaultRadix); - } - - /** - * Returns true if the next token in this scanner's input can be - * interpreted as a {@code BigInteger} in the specified radix using - * the {@link #nextBigInteger} method. The scanner does not advance past - * any input. - * - *

If the radix is less than {@link Character#MIN_RADIX Character.MIN_RADIX} - * or greater than {@link Character#MAX_RADIX Character.MAX_RADIX}, then an - * {@code IllegalArgumentException} is thrown. - * - * @param radix the radix used to interpret the token as an integer - * @return true if and only if this scanner's next token is a valid - * {@code BigInteger} - * @throws IllegalStateException if this scanner is closed - * @throws IllegalArgumentException if the radix is out of range - */ - public boolean hasNextBigInteger(int radix) { - setRadix(radix); - boolean result = hasNext(integerPattern()); - if (result) { - // Cache it - try { - String s = (matcher.group(SIMPLE_GROUP_INDEX) == null) ? processIntegerToken(hasNextResult) : hasNextResult; - typeCache = new BigInteger(s, radix); - } catch (NumberFormatException nfe) { - result = false; - } - } - return result; - } - - /** - * Scans the next token of the input as a {@link java.math.BigInteger - * BigInteger}. - * - *

An invocation of this method of the form - * {@code nextBigInteger()} behaves in exactly the same way as the - * invocation {@code nextBigInteger(radix)}, where {@code radix} - * is the default radix of this scanner. - * - * @return the {@code BigInteger} scanned from the input - * @throws InputMismatchException - * if the next token does not match the Integer - * regular expression, or is out of range - * @throws NoSuchElementException if the input is exhausted - * @throws IllegalStateException if this scanner is closed - */ - public BigInteger nextBigInteger() { - return nextBigInteger(defaultRadix); - } - - /** - * Scans the next token of the input as a {@link java.math.BigInteger - * BigInteger}. - * - *

If the next token matches the Integer regular expression defined - * above then the token is converted into a {@code BigInteger} value as if - * by removing all group separators, mapping non-ASCII digits into ASCII - * digits via the {@link Character#digit Character.digit}, and passing the - * resulting string to the {@link - * java.math.BigInteger#BigInteger(java.lang.String) - * BigInteger(String, int)} constructor with the specified radix. - * - *

If the radix is less than {@link Character#MIN_RADIX Character.MIN_RADIX} - * or greater than {@link Character#MAX_RADIX Character.MAX_RADIX}, then an - * {@code IllegalArgumentException} is thrown. - * - * @param radix the radix used to interpret the token - * @return the {@code BigInteger} scanned from the input - * @throws InputMismatchException - * if the next token does not match the Integer - * regular expression, or is out of range - * @throws NoSuchElementException if the input is exhausted - * @throws IllegalStateException if this scanner is closed - * @throws IllegalArgumentException if the radix is out of range - */ - public BigInteger nextBigInteger(int radix) { - // Check cached result - if ((typeCache != null) && (typeCache instanceof BigInteger) && this.radix == radix) { - BigInteger val = (BigInteger) typeCache; - useTypeCache(); - return val; - } - setRadix(radix); - clearCaches(); - // Search for next int - try { - String s = next(integerPattern()); - if (matcher.group(SIMPLE_GROUP_INDEX) == null) - s = processIntegerToken(s); - return new BigInteger(s, radix); - } catch (NumberFormatException nfe) { - // don't skip bad token - position = matcher.start(); - throw new InputMismatchException(nfe.getMessage()); - } - } - - /** - * Returns true if the next token in this scanner's input can be - * interpreted as a {@code BigDecimal} using the - * {@link #nextBigDecimal} method. The scanner does not advance past any - * input. - * - * @return true if and only if this scanner's next token is a valid - * {@code BigDecimal} - * @throws IllegalStateException if this scanner is closed - */ - public boolean hasNextBigDecimal() { - setRadix(10); - boolean result = hasNext(decimalPattern()); - if (result) { - // Cache it - try { - String s = processFloatToken(hasNextResult); - typeCache = new BigDecimal(s); - } catch (NumberFormatException nfe) { - result = false; - } - } - return result; - } - - /** - * Scans the next token of the input as a {@link java.math.BigDecimal - * BigDecimal}. - * - *

If the next token matches the Decimal regular expression defined - * above then the token is converted into a {@code BigDecimal} value as if - * by removing all group separators, mapping non-ASCII digits into ASCII - * digits via the {@link Character#digit Character.digit}, and passing the - * resulting string to the {@link - * java.math.BigDecimal#BigDecimal(java.lang.String) BigDecimal(String)} - * constructor. - * - * @return the {@code BigDecimal} scanned from the input - * @throws InputMismatchException - * if the next token does not match the Decimal - * regular expression, or is out of range - * @throws NoSuchElementException if the input is exhausted - * @throws IllegalStateException if this scanner is closed - */ - public BigDecimal nextBigDecimal() { - // Check cached result - if ((typeCache != null) && (typeCache instanceof BigDecimal)) { - BigDecimal val = (BigDecimal) typeCache; - useTypeCache(); - return val; - } - setRadix(10); - clearCaches(); - // Search for next float - try { - String s = processFloatToken(next(decimalPattern())); - return new BigDecimal(s); - } catch (NumberFormatException nfe) { - // don't skip bad token - position = matcher.start(); - throw new InputMismatchException(nfe.getMessage()); - } - } - - /** - * Resets this scanner. - * - *

Resetting a scanner discards all of its explicit state - * information which may have been changed by invocations of - * {@link #useDelimiter useDelimiter()}, - * {@link #useLocale useLocale()}, or - * {@link #useRadix useRadix()}. - * - *

An invocation of this method of the form - * {@code scanner.reset()} behaves in exactly the same way as the - * invocation - * - *

{@code
-     *   scanner.useDelimiter("\\p{javaWhitespace}+")
-     *          .useLocale(Locale.getDefault(Locale.Category.FORMAT))
-     *          .useRadix(10);
-     * }
- * - * @return this scanner - * - * @since 1.6 - */ - public Scanner reset() { - delimPattern = WHITESPACE_PATTERN; - useLocale(Locale.getDefault(Locale.Category.FORMAT)); - useRadix(10); - clearCaches(); - modCount++; - return this; - } - - /** - * Returns a stream of delimiter-separated tokens from this scanner. The - * stream contains the same tokens that would be returned, starting from - * this scanner's current state, by calling the {@link #next} method - * repeatedly until the {@link #hasNext} method returns false. - * - *

The resulting stream is sequential and ordered. All stream elements are - * non-null. - * - *

Scanning starts upon initiation of the terminal stream operation, using the - * current state of this scanner. Subsequent calls to any methods on this scanner - * other than {@link #close} and {@link #ioException} may return undefined results - * or may cause undefined effects on the returned stream. The returned stream's source - * {@code Spliterator} is fail-fast and will, on a best-effort basis, throw a - * {@link java.util.ConcurrentModificationException} if any such calls are detected - * during stream pipeline execution. - * - *

After stream pipeline execution completes, this scanner is left in an indeterminate - * state and cannot be reused. - * - *

If this scanner contains a resource that must be released, this scanner - * should be closed, either by calling its {@link #close} method, or by - * closing the returned stream. Closing the stream will close the underlying scanner. - * {@code IllegalStateException} is thrown if the scanner has been closed when this - * method is called, or if this scanner is closed during stream pipeline execution. - * - *

This method might block waiting for more input. - * - * @apiNote - * For example, the following code will create a list of - * comma-delimited tokens from a string: - * - *

{@code
-     * List result = new Scanner("abc,def,,ghi")
-     *     .useDelimiter(",")
-     *     .tokens()
-     *     .collect(Collectors.toList());
-     * }
- * - *

The resulting list would contain {@code "abc"}, {@code "def"}, - * the empty string, and {@code "ghi"}. - * - * @return a sequential stream of token strings - * @throws IllegalStateException if this scanner is closed - * @since 9 - */ - public Stream tokens() { - ensureOpen(); - Stream stream = StreamSupport.stream(new TokenSpliterator(), false); - return stream.onClose(this::close); - } - - class TokenSpliterator extends Spliterators.AbstractSpliterator { - - int expectedCount = -1; - - TokenSpliterator() { - super(Long.MAX_VALUE, Spliterator.IMMUTABLE | Spliterator.NONNULL | Spliterator.ORDERED); - } - - @Override - public boolean tryAdvance(Consumer cons) { - if (expectedCount >= 0 && expectedCount != modCount) { - throw new ConcurrentModificationException(); - } - if (hasNext()) { - String token = next(); - expectedCount = modCount; - cons.accept(token); - if (expectedCount != modCount) { - throw new ConcurrentModificationException(); - } - return true; - } else { - expectedCount = modCount; - return false; - } - } - } - - /** - * Returns a stream of match results from this scanner. The stream - * contains the same results in the same order that would be returned by - * calling {@code findWithinHorizon(pattern, 0)} and then {@link #match} - * successively as long as {@link #findWithinHorizon findWithinHorizon()} - * finds matches. - * - *

The resulting stream is sequential and ordered. All stream elements are - * non-null. - * - *

Scanning starts upon initiation of the terminal stream operation, using the - * current state of this scanner. Subsequent calls to any methods on this scanner - * other than {@link #close} and {@link #ioException} may return undefined results - * or may cause undefined effects on the returned stream. The returned stream's source - * {@code Spliterator} is fail-fast and will, on a best-effort basis, throw a - * {@link java.util.ConcurrentModificationException} if any such calls are detected - * during stream pipeline execution. - * - *

After stream pipeline execution completes, this scanner is left in an indeterminate - * state and cannot be reused. - * - *

If this scanner contains a resource that must be released, this scanner - * should be closed, either by calling its {@link #close} method, or by - * closing the returned stream. Closing the stream will close the underlying scanner. - * {@code IllegalStateException} is thrown if the scanner has been closed when this - * method is called, or if this scanner is closed during stream pipeline execution. - * - *

As with the {@link #findWithinHorizon findWithinHorizon()} methods, this method - * might block waiting for additional input, and it might buffer an unbounded amount of - * input searching for a match. - * - * @apiNote - * For example, the following code will read a file and return a list - * of all sequences of characters consisting of seven or more Latin capital - * letters: - * - *

{@code
-     * try (Scanner sc = new Scanner(Path.of("input.txt"))) {
-     *     Pattern pat = Pattern.compile("[A-Z]{7,}");
-     *     List capWords = sc.findAll(pat)
-     *                               .map(MatchResult::group)
-     *                               .collect(Collectors.toList());
-     * }
-     * }
- * - * @param pattern the pattern to be matched - * @return a sequential stream of match results - * @throws NullPointerException if pattern is null - * @throws IllegalStateException if this scanner is closed - * @since 9 - */ - public Stream findAll(Pattern pattern) { - Objects.requireNonNull(pattern); - ensureOpen(); - Stream stream = StreamSupport.stream(new FindSpliterator(pattern), false); - return stream.onClose(this::close); - } - - /** - * Returns a stream of match results that match the provided pattern string. - * The effect is equivalent to the following code: - * - *
{@code
-     *     scanner.findAll(Pattern.compile(patString))
-     * }
- * - * @param patString the pattern string - * @return a sequential stream of match results - * @throws NullPointerException if patString is null - * @throws IllegalStateException if this scanner is closed - * @throws PatternSyntaxException if the regular expression's syntax is invalid - * @since 9 - * @see java.util.regex.Pattern - */ - public Stream findAll(String patString) { - Objects.requireNonNull(patString); - ensureOpen(); - return findAll(patternCache.forName(patString)); - } - - class FindSpliterator extends Spliterators.AbstractSpliterator { - - final Pattern pattern; - - int expectedCount = -1; - - // true if we need to auto-advance - private boolean advance = false; - - FindSpliterator(Pattern pattern) { - super(Long.MAX_VALUE, Spliterator.IMMUTABLE | Spliterator.NONNULL | Spliterator.ORDERED); - this.pattern = pattern; - } - - @Override - public boolean tryAdvance(Consumer cons) { - ensureOpen(); - if (expectedCount >= 0) { - if (expectedCount != modCount) { - throw new ConcurrentModificationException(); - } - } else { - // init - matchValid = false; - matcher.usePattern(pattern); - expectedCount = modCount; - } - while (true) { - // assert expectedCount == modCount - if (nextInBuffer()) { - // doesn't increment modCount - cons.accept(matcher.toMatchResult()); - if (expectedCount != modCount) { - throw new ConcurrentModificationException(); - } - return true; - } - if (needInput) - // doesn't increment modCount - readInput(); - else - // reached end of input - return false; - } - } - - // reimplementation of findPatternInBuffer with auto-advance on zero-length matches - private boolean nextInBuffer() { - if (advance) { - if (position + 1 > buf.limit()) { - if (!sourceClosed) - needInput = true; - return false; - } - position++; - advance = false; - } - matcher.region(position, buf.limit()); - if (matcher.find() && (!matcher.hitEnd() || sourceClosed)) { - // Did not hit end, or hit real end - position = matcher.end(); - advance = matcher.start() == position; - return true; - } - if (!sourceClosed) - needInput = true; - return false; - } - } - - /** - * Small LRU cache of Patterns. - */ - private static class PatternLRUCache { - - private Pattern[] oa = null; - - private final int size; - - PatternLRUCache(int size) { - this.size = size; - } - - boolean hasName(Pattern p, String s) { - return p.pattern().equals(s); - } - - void moveToFront(Object[] oa, int i) { - Object ob = oa[i]; - for (int j = i; j > 0; j--) oa[j] = oa[j - 1]; - oa[0] = ob; - } - - Pattern forName(String name) { - if (oa == null) { - Pattern[] temp = new Pattern[size]; - oa = temp; - } else { - for (int i = 0; i < oa.length; i++) { - Pattern ob = oa[i]; - if (ob == null) - continue; - if (hasName(ob, name)) { - if (i > 0) - moveToFront(oa, i); - return ob; - } - } - } - // Create a new object - Pattern ob = Pattern.compile(name); - oa[oa.length - 1] = ob; - moveToFront(oa, oa.length - 1); - return ob; - } - } -} diff --git a/lib-model/lib-model-test/src/main/resources/sample_jspecify_jdk/src/java.base/share/classes/java/util/Vector.java b/lib-model/lib-model-test/src/main/resources/sample_jspecify_jdk/src/java.base/share/classes/java/util/Vector.java deleted file mode 100644 index 3e1e5bf34e..0000000000 --- a/lib-model/lib-model-test/src/main/resources/sample_jspecify_jdk/src/java.base/share/classes/java/util/Vector.java +++ /dev/null @@ -1,1492 +0,0 @@ -/* - * Copyright (c) 1994, 2018, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ -package java.util; - -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.StreamCorruptedException; -import java.util.function.Consumer; -import java.util.function.Predicate; -import java.util.function.UnaryOperator; - -/** - * The {@code Vector} class implements a growable array of - * objects. Like an array, it contains components that can be - * accessed using an integer index. However, the size of a - * {@code Vector} can grow or shrink as needed to accommodate - * adding and removing items after the {@code Vector} has been created. - * - *

Each vector tries to optimize storage management by maintaining a - * {@code capacity} and a {@code capacityIncrement}. The - * {@code capacity} is always at least as large as the vector - * size; it is usually larger because as components are added to the - * vector, the vector's storage increases in chunks the size of - * {@code capacityIncrement}. An application can increase the - * capacity of a vector before inserting a large number of - * components; this reduces the amount of incremental reallocation. - * - *

- * The iterators returned by this class's {@link #iterator() iterator} and - * {@link #listIterator(int) listIterator} methods are fail-fast: - * if the vector is structurally modified at any time after the iterator is - * created, in any way except through the iterator's own - * {@link ListIterator#remove() remove} or - * {@link ListIterator#add(Object) add} methods, the iterator will throw a - * {@link ConcurrentModificationException}. Thus, in the face of - * concurrent modification, the iterator fails quickly and cleanly, rather - * than risking arbitrary, non-deterministic behavior at an undetermined - * time in the future. The {@link Enumeration Enumerations} returned by - * the {@link #elements() elements} method are not fail-fast; if the - * Vector is structurally modified at any time after the enumeration is - * created then the results of enumerating are undefined. - * - *

Note that the fail-fast behavior of an iterator cannot be guaranteed - * as it is, generally speaking, impossible to make any hard guarantees in the - * presence of unsynchronized concurrent modification. Fail-fast iterators - * throw {@code ConcurrentModificationException} on a best-effort basis. - * Therefore, it would be wrong to write a program that depended on this - * exception for its correctness: the fail-fast behavior of iterators - * should be used only to detect bugs. - * - *

As of the Java 2 platform v1.2, this class was retrofitted to - * implement the {@link List} interface, making it a member of the - * - * Java Collections Framework. Unlike the new collection - * implementations, {@code Vector} is synchronized. If a thread-safe - * implementation is not needed, it is recommended to use {@link - * ArrayList} in place of {@code Vector}. - * - * @param Type of component elements - * - * @author Lee Boynton - * @author Jonathan Payne - * @see Collection - * @see LinkedList - * @since 1.0 - */ -@NullMarked -public class Vector extends AbstractList implements List, RandomAccess, Cloneable, java.io.Serializable { - - /** - * The array buffer into which the components of the vector are - * stored. The capacity of the vector is the length of this array buffer, - * and is at least large enough to contain all the vector's elements. - * - *

Any array elements following the last element in the Vector are null. - * - * @serial - */ - protected Object[] elementData; - - /** - * The number of valid components in this {@code Vector} object. - * Components {@code elementData[0]} through - * {@code elementData[elementCount-1]} are the actual items. - * - * @serial - */ - protected int elementCount; - - /** - * The amount by which the capacity of the vector is automatically - * incremented when its size becomes greater than its capacity. If - * the capacity increment is less than or equal to zero, the capacity - * of the vector is doubled each time it needs to grow. - * - * @serial - */ - protected int capacityIncrement; - - /** - * use serialVersionUID from JDK 1.0.2 for interoperability - */ - private static final long serialVersionUID = -2767605614048989439L; - - /** - * Constructs an empty vector with the specified initial capacity and - * capacity increment. - * - * @param initialCapacity the initial capacity of the vector - * @param capacityIncrement the amount by which the capacity is - * increased when the vector overflows - * @throws IllegalArgumentException if the specified initial capacity - * is negative - */ - public Vector(int initialCapacity, int capacityIncrement) { - super(); - if (initialCapacity < 0) - throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity); - this.elementData = new Object[initialCapacity]; - this.capacityIncrement = capacityIncrement; - } - - /** - * Constructs an empty vector with the specified initial capacity and - * with its capacity increment equal to zero. - * - * @param initialCapacity the initial capacity of the vector - * @throws IllegalArgumentException if the specified initial capacity - * is negative - */ - public Vector(int initialCapacity) { - this(initialCapacity, 0); - } - - /** - * Constructs an empty vector so that its internal data array - * has size {@code 10} and its standard capacity increment is - * zero. - */ - public Vector() { - this(10); - } - - /** - * Constructs a vector containing the elements of the specified - * collection, in the order they are returned by the collection's - * iterator. - * - * @param c the collection whose elements are to be placed into this - * vector - * @throws NullPointerException if the specified collection is null - * @since 1.2 - */ - public Vector(Collection c) { - elementData = c.toArray(); - elementCount = elementData.length; - // defend against c.toArray (incorrectly) not returning Object[] - // (see e.g. https://bugs.openjdk.java.net/browse/JDK-6260652) - if (elementData.getClass() != Object[].class) - elementData = Arrays.copyOf(elementData, elementCount, Object[].class); - } - - /** - * Copies the components of this vector into the specified array. - * The item at index {@code k} in this vector is copied into - * component {@code k} of {@code anArray}. - * - * @param anArray the array into which the components get copied - * @throws NullPointerException if the given array is null - * @throws IndexOutOfBoundsException if the specified array is not - * large enough to hold all the components of this vector - * @throws ArrayStoreException if a component of this vector is not of - * a runtime type that can be stored in the specified array - * @see #toArray(Object[]) - */ - public synchronized void copyInto(@Nullable Object[] anArray) { - System.arraycopy(elementData, 0, anArray, 0, elementCount); - } - - /** - * Trims the capacity of this vector to be the vector's current - * size. If the capacity of this vector is larger than its current - * size, then the capacity is changed to equal the size by replacing - * its internal data array, kept in the field {@code elementData}, - * with a smaller one. An application can use this operation to - * minimize the storage of a vector. - */ - public synchronized void trimToSize() { - modCount++; - int oldCapacity = elementData.length; - if (elementCount < oldCapacity) { - elementData = Arrays.copyOf(elementData, elementCount); - } - } - - /** - * Increases the capacity of this vector, if necessary, to ensure - * that it can hold at least the number of components specified by - * the minimum capacity argument. - * - *

If the current capacity of this vector is less than - * {@code minCapacity}, then its capacity is increased by replacing its - * internal data array, kept in the field {@code elementData}, with a - * larger one. The size of the new data array will be the old size plus - * {@code capacityIncrement}, unless the value of - * {@code capacityIncrement} is less than or equal to zero, in which case - * the new capacity will be twice the old capacity; but if this new size - * is still smaller than {@code minCapacity}, then the new capacity will - * be {@code minCapacity}. - * - * @param minCapacity the desired minimum capacity - */ - public synchronized void ensureCapacity(int minCapacity) { - if (minCapacity > 0) { - modCount++; - if (minCapacity > elementData.length) - grow(minCapacity); - } - } - - /** - * The maximum size of array to allocate (unless necessary). - * Some VMs reserve some header words in an array. - * Attempts to allocate larger arrays may result in - * OutOfMemoryError: Requested array size exceeds VM limit - */ - private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; - - /** - * Increases the capacity to ensure that it can hold at least the - * number of elements specified by the minimum capacity argument. - * - * @param minCapacity the desired minimum capacity - * @throws OutOfMemoryError if minCapacity is less than zero - */ - private Object[] grow(int minCapacity) { - return elementData = Arrays.copyOf(elementData, newCapacity(minCapacity)); - } - - private Object[] grow() { - return grow(elementCount + 1); - } - - /** - * Returns a capacity at least as large as the given minimum capacity. - * Will not return a capacity greater than MAX_ARRAY_SIZE unless - * the given minimum capacity is greater than MAX_ARRAY_SIZE. - * - * @param minCapacity the desired minimum capacity - * @throws OutOfMemoryError if minCapacity is less than zero - */ - private int newCapacity(int minCapacity) { - // overflow-conscious code - int oldCapacity = elementData.length; - int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity); - if (newCapacity - minCapacity <= 0) { - if (// overflow - minCapacity < 0) - throw new OutOfMemoryError(); - return minCapacity; - } - return (newCapacity - MAX_ARRAY_SIZE <= 0) ? newCapacity : hugeCapacity(minCapacity); - } - - private static int hugeCapacity(int minCapacity) { - if (// overflow - minCapacity < 0) - throw new OutOfMemoryError(); - return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; - } - - /** - * Sets the size of this vector. If the new size is greater than the - * current size, new {@code null} items are added to the end of - * the vector. If the new size is less than the current size, all - * components at index {@code newSize} and greater are discarded. - * - * @param newSize the new size of this vector - * @throws ArrayIndexOutOfBoundsException if the new size is negative - */ - public synchronized void setSize(int newSize) { - modCount++; - if (newSize > elementData.length) - grow(newSize); - final Object[] es = elementData; - for (int to = elementCount, i = newSize; i < to; i++) es[i] = null; - elementCount = newSize; - } - - /** - * Returns the current capacity of this vector. - * - * @return the current capacity (the length of its internal - * data array, kept in the field {@code elementData} - * of this vector) - */ - public synchronized int capacity() { - return elementData.length; - } - - /** - * Returns the number of components in this vector. - * - * @return the number of components in this vector - */ - public synchronized int size() { - return elementCount; - } - - /** - * Tests if this vector has no components. - * - * @return {@code true} if and only if this vector has - * no components, that is, its size is zero; - * {@code false} otherwise. - */ - public synchronized boolean isEmpty() { - return elementCount == 0; - } - - /** - * Returns an enumeration of the components of this vector. The - * returned {@code Enumeration} object will generate all items in - * this vector. The first item generated is the item at index {@code 0}, - * then the item at index {@code 1}, and so on. If the vector is - * structurally modified while enumerating over the elements then the - * results of enumerating are undefined. - * - * @return an enumeration of the components of this vector - * @see Iterator - */ - public Enumeration elements() { - return new Enumeration() { - - int count = 0; - - public boolean hasMoreElements() { - return count < elementCount; - } - - public E nextElement() { - synchronized (Vector.this) { - if (count < elementCount) { - return elementData(count++); - } - } - throw new NoSuchElementException("Vector Enumeration"); - } - }; - } - - /** - * Returns {@code true} if this vector contains the specified element. - * More formally, returns {@code true} if and only if this vector - * contains at least one element {@code e} such that - * {@code Objects.equals(o, e)}. - * - * @param o element whose presence in this vector is to be tested - * @return {@code true} if this vector contains the specified element - */ - public boolean contains(@Nullable Object o) { - return indexOf(o, 0) >= 0; - } - - /** - * Returns the index of the first occurrence of the specified element - * in this vector, or -1 if this vector does not contain the element. - * More formally, returns the lowest index {@code i} such that - * {@code Objects.equals(o, get(i))}, - * or -1 if there is no such index. - * - * @param o element to search for - * @return the index of the first occurrence of the specified element in - * this vector, or -1 if this vector does not contain the element - */ - public int indexOf(@Nullable Object o) { - return indexOf(o, 0); - } - - /** - * Returns the index of the first occurrence of the specified element in - * this vector, searching forwards from {@code index}, or returns -1 if - * the element is not found. - * More formally, returns the lowest index {@code i} such that - * {@code (i >= index && Objects.equals(o, get(i)))}, - * or -1 if there is no such index. - * - * @param o element to search for - * @param index index to start searching from - * @return the index of the first occurrence of the element in - * this vector at position {@code index} or later in the vector; - * {@code -1} if the element is not found. - * @throws IndexOutOfBoundsException if the specified index is negative - * @see Object#equals(Object) - */ - public synchronized int indexOf(@Nullable Object o, int index) { - if (o == null) { - for (int i = index; i < elementCount; i++) if (elementData[i] == null) - return i; - } else { - for (int i = index; i < elementCount; i++) if (o.equals(elementData[i])) - return i; - } - return -1; - } - - /** - * Returns the index of the last occurrence of the specified element - * in this vector, or -1 if this vector does not contain the element. - * More formally, returns the highest index {@code i} such that - * {@code Objects.equals(o, get(i))}, - * or -1 if there is no such index. - * - * @param o element to search for - * @return the index of the last occurrence of the specified element in - * this vector, or -1 if this vector does not contain the element - */ - public synchronized int lastIndexOf(@Nullable Object o) { - return lastIndexOf(o, elementCount - 1); - } - - /** - * Returns the index of the last occurrence of the specified element in - * this vector, searching backwards from {@code index}, or returns -1 if - * the element is not found. - * More formally, returns the highest index {@code i} such that - * {@code (i <= index && Objects.equals(o, get(i)))}, - * or -1 if there is no such index. - * - * @param o element to search for - * @param index index to start searching backwards from - * @return the index of the last occurrence of the element at position - * less than or equal to {@code index} in this vector; - * -1 if the element is not found. - * @throws IndexOutOfBoundsException if the specified index is greater - * than or equal to the current size of this vector - */ - public synchronized int lastIndexOf(@Nullable Object o, int index) { - if (index >= elementCount) - throw new IndexOutOfBoundsException(index + " >= " + elementCount); - if (o == null) { - for (int i = index; i >= 0; i--) if (elementData[i] == null) - return i; - } else { - for (int i = index; i >= 0; i--) if (o.equals(elementData[i])) - return i; - } - return -1; - } - - /** - * Returns the component at the specified index. - * - *

This method is identical in functionality to the {@link #get(int)} - * method (which is part of the {@link List} interface). - * - * @param index an index into this vector - * @return the component at the specified index - * @throws ArrayIndexOutOfBoundsException if the index is out of range - * ({@code index < 0 || index >= size()}) - */ - public synchronized E elementAt(int index) { - if (index >= elementCount) { - throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount); - } - return elementData(index); - } - - /** - * Returns the first component (the item at index {@code 0}) of - * this vector. - * - * @return the first component of this vector - * @throws NoSuchElementException if this vector has no components - */ - public synchronized E firstElement() { - if (elementCount == 0) { - throw new NoSuchElementException(); - } - return elementData(0); - } - - /** - * Returns the last component of the vector. - * - * @return the last component of the vector, i.e., the component at index - * {@code size() - 1} - * @throws NoSuchElementException if this vector is empty - */ - public synchronized E lastElement() { - if (elementCount == 0) { - throw new NoSuchElementException(); - } - return elementData(elementCount - 1); - } - - /** - * Sets the component at the specified {@code index} of this - * vector to be the specified object. The previous component at that - * position is discarded. - * - *

The index must be a value greater than or equal to {@code 0} - * and less than the current size of the vector. - * - *

This method is identical in functionality to the - * {@link #set(int, Object) set(int, E)} - * method (which is part of the {@link List} interface). Note that the - * {@code set} method reverses the order of the parameters, to more closely - * match array usage. Note also that the {@code set} method returns the - * old value that was stored at the specified position. - * - * @param obj what the component is to be set to - * @param index the specified index - * @throws ArrayIndexOutOfBoundsException if the index is out of range - * ({@code index < 0 || index >= size()}) - */ - public synchronized void setElementAt(E obj, int index) { - if (index >= elementCount) { - throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount); - } - elementData[index] = obj; - } - - /** - * Deletes the component at the specified index. Each component in - * this vector with an index greater or equal to the specified - * {@code index} is shifted downward to have an index one - * smaller than the value it had previously. The size of this vector - * is decreased by {@code 1}. - * - *

The index must be a value greater than or equal to {@code 0} - * and less than the current size of the vector. - * - *

This method is identical in functionality to the {@link #remove(int)} - * method (which is part of the {@link List} interface). Note that the - * {@code remove} method returns the old value that was stored at the - * specified position. - * - * @param index the index of the object to remove - * @throws ArrayIndexOutOfBoundsException if the index is out of range - * ({@code index < 0 || index >= size()}) - */ - public synchronized void removeElementAt(int index) { - if (index >= elementCount) { - throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount); - } else if (index < 0) { - throw new ArrayIndexOutOfBoundsException(index); - } - int j = elementCount - index - 1; - if (j > 0) { - System.arraycopy(elementData, index + 1, elementData, index, j); - } - modCount++; - elementCount--; - elementData[elementCount] = null; - /* to let gc do its work */ - } - - /** - * Inserts the specified object as a component in this vector at the - * specified {@code index}. Each component in this vector with - * an index greater or equal to the specified {@code index} is - * shifted upward to have an index one greater than the value it had - * previously. - * - *

The index must be a value greater than or equal to {@code 0} - * and less than or equal to the current size of the vector. (If the - * index is equal to the current size of the vector, the new element - * is appended to the Vector.) - * - *

This method is identical in functionality to the - * {@link #add(int, Object) add(int, E)} - * method (which is part of the {@link List} interface). Note that the - * {@code add} method reverses the order of the parameters, to more closely - * match array usage. - * - * @param obj the component to insert - * @param index where to insert the new component - * @throws ArrayIndexOutOfBoundsException if the index is out of range - * ({@code index < 0 || index > size()}) - */ - public synchronized void insertElementAt(E obj, int index) { - if (index > elementCount) { - throw new ArrayIndexOutOfBoundsException(index + " > " + elementCount); - } - modCount++; - final int s = elementCount; - Object[] elementData = this.elementData; - if (s == elementData.length) - elementData = grow(); - System.arraycopy(elementData, index, elementData, index + 1, s - index); - elementData[index] = obj; - elementCount = s + 1; - } - - /** - * Adds the specified component to the end of this vector, - * increasing its size by one. The capacity of this vector is - * increased if its size becomes greater than its capacity. - * - *

This method is identical in functionality to the - * {@link #add(Object) add(E)} - * method (which is part of the {@link List} interface). - * - * @param obj the component to be added - */ - public synchronized void addElement(E obj) { - modCount++; - add(obj, elementData, elementCount); - } - - /** - * Removes the first (lowest-indexed) occurrence of the argument - * from this vector. If the object is found in this vector, each - * component in the vector with an index greater or equal to the - * object's index is shifted downward to have an index one smaller - * than the value it had previously. - * - *

This method is identical in functionality to the - * {@link #remove(Object)} method (which is part of the - * {@link List} interface). - * - * @param obj the component to be removed - * @return {@code true} if the argument was a component of this - * vector; {@code false} otherwise. - */ - public synchronized boolean removeElement(Object obj) { - modCount++; - int i = indexOf(obj); - if (i >= 0) { - removeElementAt(i); - return true; - } - return false; - } - - /** - * Removes all components from this vector and sets its size to zero. - * - *

This method is identical in functionality to the {@link #clear} - * method (which is part of the {@link List} interface). - */ - public synchronized void removeAllElements() { - final Object[] es = elementData; - for (int to = elementCount, i = elementCount = 0; i < to; i++) es[i] = null; - modCount++; - } - - /** - * Returns a clone of this vector. The copy will contain a - * reference to a clone of the internal data array, not a reference - * to the original internal data array of this {@code Vector} object. - * - * @return a clone of this vector - */ - public synchronized Object clone() { - try { - @SuppressWarnings("unchecked") - Vector v = (Vector) super.clone(); - v.elementData = Arrays.copyOf(elementData, elementCount); - v.modCount = 0; - return v; - } catch (CloneNotSupportedException e) { - // this shouldn't happen, since we are Cloneable - throw new InternalError(e); - } - } - - /** - * Returns an array containing all of the elements in this Vector - * in the correct order. - * - * @since 1.2 - */ - @Nullable - public synchronized Object[] toArray() { - return Arrays.copyOf(elementData, elementCount); - } - - /** - * Returns an array containing all of the elements in this Vector in the - * correct order; the runtime type of the returned array is that of the - * specified array. If the Vector fits in the specified array, it is - * returned therein. Otherwise, a new array is allocated with the runtime - * type of the specified array and the size of this Vector. - * - *

If the Vector fits in the specified array with room to spare - * (i.e., the array has more elements than the Vector), - * the element in the array immediately following the end of the - * Vector is set to null. (This is useful in determining the length - * of the Vector only if the caller knows that the Vector - * does not contain any null elements.) - * - * @param type of array elements. The same type as {@code } or a - * supertype of {@code }. - * @param a the array into which the elements of the Vector are to - * be stored, if it is big enough; otherwise, a new array of the - * same runtime type is allocated for this purpose. - * @return an array containing the elements of the Vector - * @throws ArrayStoreException if the runtime type of a, {@code }, is not - * a supertype of the runtime type, {@code }, of every element in this - * Vector - * @throws NullPointerException if the given array is null - * @since 1.2 - */ - @SuppressWarnings("unchecked") - public synchronized T[] toArray(T[] a) { - if (a.length < elementCount) - return (T[]) Arrays.copyOf(elementData, elementCount, a.getClass()); - System.arraycopy(elementData, 0, a, 0, elementCount); - if (a.length > elementCount) - a[elementCount] = null; - return a; - } - - // Positional Access Operations - @SuppressWarnings("unchecked") - E elementData(int index) { - return (E) elementData[index]; - } - - @SuppressWarnings("unchecked") - static E elementAt(Object[] es, int index) { - return (E) es[index]; - } - - /** - * Returns the element at the specified position in this Vector. - * - * @param index index of the element to return - * @return object at the specified index - * @throws ArrayIndexOutOfBoundsException if the index is out of range - * ({@code index < 0 || index >= size()}) - * @since 1.2 - */ - public synchronized E get(int index) { - if (index >= elementCount) - throw new ArrayIndexOutOfBoundsException(index); - return elementData(index); - } - - /** - * Replaces the element at the specified position in this Vector with the - * specified element. - * - * @param index index of the element to replace - * @param element element to be stored at the specified position - * @return the element previously at the specified position - * @throws ArrayIndexOutOfBoundsException if the index is out of range - * ({@code index < 0 || index >= size()}) - * @since 1.2 - */ - public synchronized E set(int index, E element) { - if (index >= elementCount) - throw new ArrayIndexOutOfBoundsException(index); - E oldValue = elementData(index); - elementData[index] = element; - return oldValue; - } - - /** - * This helper method split out from add(E) to keep method - * bytecode size under 35 (the -XX:MaxInlineSize default value), - * which helps when add(E) is called in a C1-compiled loop. - */ - private void add(E e, Object[] elementData, int s) { - if (s == elementData.length) - elementData = grow(); - elementData[s] = e; - elementCount = s + 1; - } - - /** - * Appends the specified element to the end of this Vector. - * - * @param e element to be appended to this Vector - * @return {@code true} (as specified by {@link Collection#add}) - * @since 1.2 - */ - public synchronized boolean add(E e) { - modCount++; - add(e, elementData, elementCount); - return true; - } - - /** - * Removes the first occurrence of the specified element in this Vector - * If the Vector does not contain the element, it is unchanged. More - * formally, removes the element with the lowest index i such that - * {@code Objects.equals(o, get(i))} (if such - * an element exists). - * - * @param o element to be removed from this Vector, if present - * @return true if the Vector contained the specified element - * @since 1.2 - */ - public boolean remove(@Nullable Object o) { - return removeElement(o); - } - - /** - * Inserts the specified element at the specified position in this Vector. - * Shifts the element currently at that position (if any) and any - * subsequent elements to the right (adds one to their indices). - * - * @param index index at which the specified element is to be inserted - * @param element element to be inserted - * @throws ArrayIndexOutOfBoundsException if the index is out of range - * ({@code index < 0 || index > size()}) - * @since 1.2 - */ - public void add(int index, E element) { - insertElementAt(element, index); - } - - /** - * Removes the element at the specified position in this Vector. - * Shifts any subsequent elements to the left (subtracts one from their - * indices). Returns the element that was removed from the Vector. - * - * @param index the index of the element to be removed - * @return element that was removed - * @throws ArrayIndexOutOfBoundsException if the index is out of range - * ({@code index < 0 || index >= size()}) - * @since 1.2 - */ - public synchronized E remove(int index) { - modCount++; - if (index >= elementCount) - throw new ArrayIndexOutOfBoundsException(index); - E oldValue = elementData(index); - int numMoved = elementCount - index - 1; - if (numMoved > 0) - System.arraycopy(elementData, index + 1, elementData, index, numMoved); - // Let gc do its work - elementData[--elementCount] = null; - return oldValue; - } - - /** - * Removes all of the elements from this Vector. The Vector will - * be empty after this call returns (unless it throws an exception). - * - * @since 1.2 - */ - public void clear() { - removeAllElements(); - } - - // Bulk Operations - /** - * Returns true if this Vector contains all of the elements in the - * specified Collection. - * - * @param c a collection whose elements will be tested for containment - * in this Vector - * @return true if this Vector contains all of the elements in the - * specified collection - * @throws NullPointerException if the specified collection is null - */ - public synchronized boolean containsAll(Collection c) { - return super.containsAll(c); - } - - /** - * Appends all of the elements in the specified Collection to the end of - * this Vector, in the order that they are returned by the specified - * Collection's Iterator. The behavior of this operation is undefined if - * the specified Collection is modified while the operation is in progress. - * (This implies that the behavior of this call is undefined if the - * specified Collection is this Vector, and this Vector is nonempty.) - * - * @param c elements to be inserted into this Vector - * @return {@code true} if this Vector changed as a result of the call - * @throws NullPointerException if the specified collection is null - * @since 1.2 - */ - public boolean addAll(Collection c) { - Object[] a = c.toArray(); - modCount++; - int numNew = a.length; - if (numNew == 0) - return false; - synchronized (this) { - Object[] elementData = this.elementData; - final int s = elementCount; - if (numNew > elementData.length - s) - elementData = grow(s + numNew); - System.arraycopy(a, 0, elementData, s, numNew); - elementCount = s + numNew; - return true; - } - } - - /** - * Removes from this Vector all of its elements that are contained in the - * specified Collection. - * - * @param c a collection of elements to be removed from the Vector - * @return true if this Vector changed as a result of the call - * @throws ClassCastException if the types of one or more elements - * in this vector are incompatible with the specified - * collection - * (optional) - * @throws NullPointerException if this vector contains one or more null - * elements and the specified collection does not support null - * elements - * (optional), - * or if the specified collection is null - * @since 1.2 - */ - public boolean removeAll(Collection c) { - Objects.requireNonNull(c); - return bulkRemove(e -> c.contains(e)); - } - - /** - * Retains only the elements in this Vector that are contained in the - * specified Collection. In other words, removes from this Vector all - * of its elements that are not contained in the specified Collection. - * - * @param c a collection of elements to be retained in this Vector - * (all other elements are removed) - * @return true if this Vector changed as a result of the call - * @throws ClassCastException if the types of one or more elements - * in this vector are incompatible with the specified - * collection - * (optional) - * @throws NullPointerException if this vector contains one or more null - * elements and the specified collection does not support null - * elements - * (optional), - * or if the specified collection is null - * @since 1.2 - */ - public boolean retainAll(Collection c) { - Objects.requireNonNull(c); - return bulkRemove(e -> !c.contains(e)); - } - - /** - * @throws NullPointerException {@inheritDoc} - */ - @SuppressWarnings({ "unchecked" }) - @Override - public boolean removeIf(Predicate filter) { - Objects.requireNonNull(filter); - return bulkRemove(filter); - } - - // A tiny bit set implementation - private static long[] nBits(int n) { - return new long[((n - 1) >> 6) + 1]; - } - - private static void setBit(long[] bits, int i) { - bits[i >> 6] |= 1L << i; - } - - private static boolean isClear(long[] bits, int i) { - return (bits[i >> 6] & (1L << i)) == 0; - } - - private synchronized boolean bulkRemove(Predicate filter) { - int expectedModCount = modCount; - final Object[] es = elementData; - final int end = elementCount; - int i; - // Optimize for initial run of survivors - for (i = 0; i < end && !filter.test(elementAt(es, i)); i++) ; - // Tolerate predicates that reentrantly access the collection for - // read (but writers still get CME), so traverse once to find - // elements to delete, a second pass to physically expunge. - if (i < end) { - final int beg = i; - final long[] deathRow = nBits(end - beg); - // set bit 0 - deathRow[0] = 1L; - for (i = beg + 1; i < end; i++) if (filter.test(elementAt(es, i))) - setBit(deathRow, i - beg); - if (modCount != expectedModCount) - throw new ConcurrentModificationException(); - modCount++; - int w = beg; - for (i = beg; i < end; i++) if (isClear(deathRow, i - beg)) - es[w++] = es[i]; - for (i = elementCount = w; i < end; i++) es[i] = null; - return true; - } else { - if (modCount != expectedModCount) - throw new ConcurrentModificationException(); - return false; - } - } - - /** - * Inserts all of the elements in the specified Collection into this - * Vector at the specified position. Shifts the element currently at - * that position (if any) and any subsequent elements to the right - * (increases their indices). The new elements will appear in the Vector - * in the order that they are returned by the specified Collection's - * iterator. - * - * @param index index at which to insert the first element from the - * specified collection - * @param c elements to be inserted into this Vector - * @return {@code true} if this Vector changed as a result of the call - * @throws ArrayIndexOutOfBoundsException if the index is out of range - * ({@code index < 0 || index > size()}) - * @throws NullPointerException if the specified collection is null - * @since 1.2 - */ - public synchronized boolean addAll(int index, Collection c) { - if (index < 0 || index > elementCount) - throw new ArrayIndexOutOfBoundsException(index); - Object[] a = c.toArray(); - modCount++; - int numNew = a.length; - if (numNew == 0) - return false; - Object[] elementData = this.elementData; - final int s = elementCount; - if (numNew > elementData.length - s) - elementData = grow(s + numNew); - int numMoved = s - index; - if (numMoved > 0) - System.arraycopy(elementData, index, elementData, index + numNew, numMoved); - System.arraycopy(a, 0, elementData, index, numNew); - elementCount = s + numNew; - return true; - } - - /** - * Compares the specified Object with this Vector for equality. Returns - * true if and only if the specified Object is also a List, both Lists - * have the same size, and all corresponding pairs of elements in the two - * Lists are equal. (Two elements {@code e1} and - * {@code e2} are equal if {@code Objects.equals(e1, e2)}.) - * In other words, two Lists are defined to be - * equal if they contain the same elements in the same order. - * - * @param o the Object to be compared for equality with this Vector - * @return true if the specified Object is equal to this Vector - */ - public synchronized boolean equals(@Nullable Object o) { - return super.equals(o); - } - - /** - * Returns the hash code value for this Vector. - */ - public synchronized int hashCode() { - return super.hashCode(); - } - - /** - * Returns a string representation of this Vector, containing - * the String representation of each element. - */ - public synchronized String toString() { - return super.toString(); - } - - /** - * Returns a view of the portion of this List between fromIndex, - * inclusive, and toIndex, exclusive. (If fromIndex and toIndex are - * equal, the returned List is empty.) The returned List is backed by this - * List, so changes in the returned List are reflected in this List, and - * vice-versa. The returned List supports all of the optional List - * operations supported by this List. - * - *

This method eliminates the need for explicit range operations (of - * the sort that commonly exist for arrays). Any operation that expects - * a List can be used as a range operation by operating on a subList view - * instead of a whole List. For example, the following idiom - * removes a range of elements from a List: - *

-     *      list.subList(from, to).clear();
-     * 
- * Similar idioms may be constructed for indexOf and lastIndexOf, - * and all of the algorithms in the Collections class can be applied to - * a subList. - * - *

The semantics of the List returned by this method become undefined if - * the backing list (i.e., this List) is structurally modified in - * any way other than via the returned List. (Structural modifications are - * those that change the size of the List, or otherwise perturb it in such - * a fashion that iterations in progress may yield incorrect results.) - * - * @param fromIndex low endpoint (inclusive) of the subList - * @param toIndex high endpoint (exclusive) of the subList - * @return a view of the specified range within this List - * @throws IndexOutOfBoundsException if an endpoint index value is out of range - * {@code (fromIndex < 0 || toIndex > size)} - * @throws IllegalArgumentException if the endpoint indices are out of order - * {@code (fromIndex > toIndex)} - */ - public synchronized List subList(int fromIndex, int toIndex) { - return Collections.synchronizedList(super.subList(fromIndex, toIndex), this); - } - - /** - * Removes from this list all of the elements whose index is between - * {@code fromIndex}, inclusive, and {@code toIndex}, exclusive. - * Shifts any succeeding elements to the left (reduces their index). - * This call shortens the list by {@code (toIndex - fromIndex)} elements. - * (If {@code toIndex==fromIndex}, this operation has no effect.) - */ - protected synchronized void removeRange(int fromIndex, int toIndex) { - modCount++; - shiftTailOverGap(elementData, fromIndex, toIndex); - } - - /** - * Erases the gap from lo to hi, by sliding down following elements. - */ - private void shiftTailOverGap(Object[] es, int lo, int hi) { - System.arraycopy(es, hi, es, lo, elementCount - hi); - for (int to = elementCount, i = (elementCount -= hi - lo); i < to; i++) es[i] = null; - } - - /** - * Loads a {@code Vector} instance from a stream - * (that is, deserializes it). - * This method performs checks to ensure the consistency - * of the fields. - * - * @param in the stream - * @throws java.io.IOException if an I/O error occurs - * @throws ClassNotFoundException if the stream contains data - * of a non-existing class - */ - private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { - ObjectInputStream.GetField gfields = in.readFields(); - int count = gfields.get("elementCount", 0); - Object[] data = (Object[]) gfields.get("elementData", null); - if (count < 0 || data == null || count > data.length) { - throw new StreamCorruptedException("Inconsistent vector internals"); - } - elementCount = count; - elementData = data.clone(); - } - - /** - * Saves the state of the {@code Vector} instance to a stream - * (that is, serializes it). - * This method performs synchronization to ensure the consistency - * of the serialized data. - * - * @param s the stream - * @throws java.io.IOException if an I/O error occurs - */ - private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException { - final java.io.ObjectOutputStream.PutField fields = s.putFields(); - final Object[] data; - synchronized (this) { - fields.put("capacityIncrement", capacityIncrement); - fields.put("elementCount", elementCount); - data = elementData.clone(); - } - fields.put("elementData", data); - s.writeFields(); - } - - /** - * Returns a list iterator over the elements in this list (in proper - * sequence), starting at the specified position in the list. - * The specified index indicates the first element that would be - * returned by an initial call to {@link ListIterator#next next}. - * An initial call to {@link ListIterator#previous previous} would - * return the element with the specified index minus one. - * - *

The returned list iterator is fail-fast. - * - * @throws IndexOutOfBoundsException {@inheritDoc} - */ - public synchronized ListIterator listIterator(int index) { - if (index < 0 || index > elementCount) - throw new IndexOutOfBoundsException("Index: " + index); - return new ListItr(index); - } - - /** - * Returns a list iterator over the elements in this list (in proper - * sequence). - * - *

The returned list iterator is fail-fast. - * - * @see #listIterator(int) - */ - public synchronized ListIterator listIterator() { - return new ListItr(0); - } - - /** - * Returns an iterator over the elements in this list in proper sequence. - * - *

The returned iterator is fail-fast. - * - * @return an iterator over the elements in this list in proper sequence - */ - public synchronized Iterator iterator() { - return new Itr(); - } - - /** - * An optimized version of AbstractList.Itr - */ - private class Itr implements Iterator { - - // index of next element to return - int cursor; - - // index of last element returned; -1 if no such - int lastRet = -1; - - int expectedModCount = modCount; - - public boolean hasNext() { - // Racy but within spec, since modifications are checked - // within or after synchronization in next/previous - return cursor != elementCount; - } - - public E next() { - synchronized (Vector.this) { - checkForComodification(); - int i = cursor; - if (i >= elementCount) - throw new NoSuchElementException(); - cursor = i + 1; - return elementData(lastRet = i); - } - } - - public void remove() { - if (lastRet == -1) - throw new IllegalStateException(); - synchronized (Vector.this) { - checkForComodification(); - Vector.this.remove(lastRet); - expectedModCount = modCount; - } - cursor = lastRet; - lastRet = -1; - } - - @Override - public void forEachRemaining(Consumer action) { - Objects.requireNonNull(action); - synchronized (Vector.this) { - final int size = elementCount; - int i = cursor; - if (i >= size) { - return; - } - final Object[] es = elementData; - if (i >= es.length) - throw new ConcurrentModificationException(); - while (i < size && modCount == expectedModCount) action.accept(elementAt(es, i++)); - // update once at end of iteration to reduce heap write traffic - cursor = i; - lastRet = i - 1; - checkForComodification(); - } - } - - final void checkForComodification() { - if (modCount != expectedModCount) - throw new ConcurrentModificationException(); - } - } - - /** - * An optimized version of AbstractList.ListItr - */ - final class ListItr extends Itr implements ListIterator { - - ListItr(int index) { - super(); - cursor = index; - } - - public boolean hasPrevious() { - return cursor != 0; - } - - public int nextIndex() { - return cursor; - } - - public int previousIndex() { - return cursor - 1; - } - - public E previous() { - synchronized (Vector.this) { - checkForComodification(); - int i = cursor - 1; - if (i < 0) - throw new NoSuchElementException(); - cursor = i; - return elementData(lastRet = i); - } - } - - public void set(E e) { - if (lastRet == -1) - throw new IllegalStateException(); - synchronized (Vector.this) { - checkForComodification(); - Vector.this.set(lastRet, e); - } - } - - public void add(E e) { - int i = cursor; - synchronized (Vector.this) { - checkForComodification(); - Vector.this.add(i, e); - expectedModCount = modCount; - } - cursor = i + 1; - lastRet = -1; - } - } - - /** - * @throws NullPointerException {@inheritDoc} - */ - @Override - public synchronized void forEach(Consumer action) { - Objects.requireNonNull(action); - final int expectedModCount = modCount; - final Object[] es = elementData; - final int size = elementCount; - for (int i = 0; modCount == expectedModCount && i < size; i++) action.accept(elementAt(es, i)); - if (modCount != expectedModCount) - throw new ConcurrentModificationException(); - } - - /** - * @throws NullPointerException {@inheritDoc} - */ - @SuppressWarnings({ "unchecked" }) - @Override - public synchronized void replaceAll(UnaryOperator operator) { - Objects.requireNonNull(operator); - final int expectedModCount = modCount; - final Object[] es = elementData; - final int size = elementCount; - for (int i = 0; modCount == expectedModCount && i < size; i++) es[i] = operator.apply(elementAt(es, i)); - if (modCount != expectedModCount) - throw new ConcurrentModificationException(); - modCount++; - } - - @SuppressWarnings("unchecked") - @Override - public synchronized void sort(@Nullable Comparator c) { - final int expectedModCount = modCount; - Arrays.sort((E[]) elementData, 0, elementCount, c); - if (modCount != expectedModCount) - throw new ConcurrentModificationException(); - modCount++; - } - - /** - * Creates a late-binding - * and fail-fast {@link Spliterator} over the elements in this - * list. - * - *

The {@code Spliterator} reports {@link Spliterator#SIZED}, - * {@link Spliterator#SUBSIZED}, and {@link Spliterator#ORDERED}. - * Overriding implementations should document the reporting of additional - * characteristic values. - * - * @return a {@code Spliterator} over the elements in this list - * @since 1.8 - */ - @Override - public Spliterator spliterator() { - return new VectorSpliterator(null, 0, -1, 0); - } - - /** - * Similar to ArrayList Spliterator - */ - final class VectorSpliterator implements Spliterator { - - private Object[] array; - - // current index, modified on advance/split - private int index; - - // -1 until used; then one past last index - private int fence; - - // initialized when fence set - private int expectedModCount; - - /** - * Creates new spliterator covering the given range. - */ - VectorSpliterator(Object[] array, int origin, int fence, int expectedModCount) { - this.array = array; - this.index = origin; - this.fence = fence; - this.expectedModCount = expectedModCount; - } - - private int getFence() { - // initialize on first use - int hi; - if ((hi = fence) < 0) { - synchronized (Vector.this) { - array = elementData; - expectedModCount = modCount; - hi = fence = elementCount; - } - } - return hi; - } - - public Spliterator trySplit() { - int hi = getFence(), lo = index, mid = (lo + hi) >>> 1; - return (lo >= mid) ? null : new VectorSpliterator(array, lo, index = mid, expectedModCount); - } - - @SuppressWarnings("unchecked") - public boolean tryAdvance(Consumer action) { - Objects.requireNonNull(action); - int i; - if (getFence() > (i = index)) { - index = i + 1; - action.accept((E) array[i]); - if (modCount != expectedModCount) - throw new ConcurrentModificationException(); - return true; - } - return false; - } - - @SuppressWarnings("unchecked") - public void forEachRemaining(Consumer action) { - Objects.requireNonNull(action); - final int hi = getFence(); - final Object[] a = array; - int i; - for (i = index, index = hi; i < hi; i++) action.accept((E) a[i]); - if (modCount != expectedModCount) - throw new ConcurrentModificationException(); - } - - public long estimateSize() { - return getFence() - index; - } - - public int characteristics() { - return Spliterator.ORDERED | Spliterator.SIZED | Spliterator.SUBSIZED; - } - } - - void checkInvariants() { - // assert elementCount >= 0; - // assert elementCount == elementData.length || elementData[elementCount] == null; - } -} diff --git a/lib-model/lib-model-test/src/main/resources/sample_jspecify_jdk/src/java.base/share/classes/java/util/function/Function.java b/lib-model/lib-model-test/src/main/resources/sample_jspecify_jdk/src/java.base/share/classes/java/util/function/Function.java deleted file mode 100644 index d895ffc905..0000000000 --- a/lib-model/lib-model-test/src/main/resources/sample_jspecify_jdk/src/java.base/share/classes/java/util/function/Function.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ -package java.util.function; - -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; -import java.util.Objects; - -/** - * Represents a function that accepts one argument and produces a result. - * - *

This is a functional interface - * whose functional method is {@link #apply(Object)}. - * - * @param the type of the input to the function - * @param the type of the result of the function - * - * @since 1.8 - */ -@NullMarked -@FunctionalInterface -public interface Function { - - /** - * Applies this function to the given argument. - * - * @param t the function argument - * @return the function result - */ - R apply(T t); - - /** - * Returns a composed function that first applies the {@code before} - * function to its input, and then applies this function to the result. - * If evaluation of either function throws an exception, it is relayed to - * the caller of the composed function. - * - * @param the type of input to the {@code before} function, and to the - * composed function - * @param before the function to apply before this function is applied - * @return a composed function that first applies the {@code before} - * function and then applies this function - * @throws NullPointerException if before is null - * - * @see #andThen(Function) - */ - default Function compose(Function before) { - Objects.requireNonNull(before); - return (V v) -> apply(before.apply(v)); - } - - /** - * Returns a composed function that first applies this function to - * its input, and then applies the {@code after} function to the result. - * If evaluation of either function throws an exception, it is relayed to - * the caller of the composed function. - * - * @param the type of output of the {@code after} function, and of the - * composed function - * @param after the function to apply after this function is applied - * @return a composed function that first applies this function and then - * applies the {@code after} function - * @throws NullPointerException if after is null - * - * @see #compose(Function) - */ - default Function andThen(Function after) { - Objects.requireNonNull(after); - return (T t) -> after.apply(apply(t)); - } - - /** - * Returns a function that always returns its input argument. - * - * @param the type of the input and output objects to the function - * @return a function that always returns its input argument - */ - static Function identity() { - return t -> t; - } -} From 4b7891ea3559a307fded2691076832ea3479466e Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Tue, 27 Feb 2024 14:49:01 -0800 Subject: [PATCH 13/50] unnecessary formatting change --- nullaway/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/nullaway/build.gradle b/nullaway/build.gradle index dd994920b2..15327aed88 100644 --- a/nullaway/build.gradle +++ b/nullaway/build.gradle @@ -39,6 +39,7 @@ dependencies { compileOnly deps.build.errorProneCheckApi implementation deps.build.checkerDataflow implementation deps.build.guava + testImplementation project(":annotations") testImplementation deps.test.junit4 testImplementation(deps.build.errorProneTestHelpers) { From 10c36e352cd1ca61303ee5b50f96a0e17af33fc2 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Wed, 28 Feb 2024 18:53:46 -0800 Subject: [PATCH 14/50] minor cleanup --- build.gradle | 2 +- .../java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 0c715a2a09..0246ab077c 100644 --- a/build.gradle +++ b/build.gradle @@ -97,7 +97,7 @@ subprojects { project -> // For some reason, spotless complains when applied to the jar-infer folder itself, even // though there is no top-level :jar-infer project - if (project.name != "jar-infer"&&project.name != "lib-model") { + if (project.name != "jar-infer" && project.name != "lib-model") { project.apply plugin: "com.diffplug.spotless" spotless { java { diff --git a/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java b/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java index c644d3b35b..ee8939381e 100644 --- a/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java +++ b/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java @@ -139,13 +139,10 @@ public static Path dirnameToPath(String dir) { return Paths.get(absoluteDir); } - /** Callback to process each Java file; see class documentation for details. */ private static class MinimizerCallback implements SourceRoot.Callback { - /** The visitor instance. */ private final CompilationUnitVisitor mv; - /** Create a MinimizerCallback instance. */ public MinimizerCallback() { this.mv = new CompilationUnitVisitor(); } From 36b5a21ed1e68dd84af26f8afed2d280932207c2 Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Wed, 28 Feb 2024 19:35:53 -0800 Subject: [PATCH 15/50] Update lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java Co-authored-by: Manu Sridharan --- .../java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java b/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java index ee8939381e..dfb6f39b26 100644 --- a/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java +++ b/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java @@ -139,7 +139,7 @@ public static Path dirnameToPath(String dir) { return Paths.get(absoluteDir); } - private static class MinimizerCallback implements SourceRoot.Callback { + private static class AnnotationCollectorCallback implements SourceRoot.Callback { private final CompilationUnitVisitor mv; From a6c820414b6af698a9c83dd09b2d1c1c4ad2871f Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Wed, 28 Feb 2024 19:49:32 -0800 Subject: [PATCH 16/50] fixes --- lib-model/lib-model-consume/build.gradle | 2 - .../libmodel/LibModelInfoExtractor.java | 51 ++++++------------- 2 files changed, 16 insertions(+), 37 deletions(-) diff --git a/lib-model/lib-model-consume/build.gradle b/lib-model/lib-model-consume/build.gradle index c6bc61495b..77990d7b1d 100644 --- a/lib-model/lib-model-consume/build.gradle +++ b/lib-model/lib-model-consume/build.gradle @@ -36,7 +36,5 @@ dependencies { testImplementation project(":lib-model:lib-model-test") implementation deps.build.guava implementation deps.build.javaparser - compileOnly deps.apt.autoService - annotationProcessor deps.apt.autoService compileOnly project(":nullaway") } diff --git a/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java b/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java index dfb6f39b26..c51726c03b 100644 --- a/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java +++ b/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java @@ -39,7 +39,7 @@ import com.github.javaparser.ast.type.ArrayType; import com.github.javaparser.ast.type.ClassOrInterfaceType; import com.github.javaparser.ast.type.TypeParameter; -import com.github.javaparser.ast.visitor.ModifierVisitor; +import com.github.javaparser.ast.visitor.VoidVisitorAdapter; import com.github.javaparser.utils.CollectionStrategy; import com.github.javaparser.utils.ParserCollectionStrategy; import com.github.javaparser.utils.ProjectRoot; @@ -65,20 +65,14 @@ public class LibModelInfoExtractor { public static void main(String[] args) { // input directory - processSourceFile(args[0]); + processDirectory(args[0]); // output directory writeToAstubx(args[1]); } - @SuppressWarnings("unused") - public static Map runLibModelProcess(String[] args) { - main(args); - return methodRecords; - } - - public static void processSourceFile(String file) { + public static void processDirectory(String file) { Path root = dirnameToPath(file); - MinimizerCallback mc = new MinimizerCallback(); + AnnotationCollectorCallback mc = new AnnotationCollectorCallback(); CollectionStrategy strategy = new ParserCollectionStrategy(); // Required to include directories that contain a module-info.java, which don't parse by // default. @@ -141,10 +135,10 @@ public static Path dirnameToPath(String dir) { private static class AnnotationCollectorCallback implements SourceRoot.Callback { - private final CompilationUnitVisitor mv; + private final AnnotationCollectionVisitor mv; - public MinimizerCallback() { - this.mv = new CompilationUnitVisitor(); + public AnnotationCollectorCallback() { + this.mv = new AnnotationCollectionVisitor(); } @Override @@ -177,19 +171,18 @@ public void visitAll(Node rootNode) { } } - private static class CompilationUnitVisitor extends ModifierVisitor { + private static class AnnotationCollectionVisitor extends VoidVisitorAdapter { private String packageName = ""; @Override - public PackageDeclaration visit(PackageDeclaration n, Void arg) { + public void visit(PackageDeclaration n, Void arg) { this.packageName = n.getNameAsString(); // System.out.println("Package name: " + packageName); - return n; } @Override - public ClassOrInterfaceDeclaration visit(ClassOrInterfaceDeclaration cid, Void arg) { + public void visit(ClassOrInterfaceDeclaration cid, Void arg) { String classOrInterfaceName = packageName + "." + cid.getNameAsString(); @SuppressWarnings("all") Map nullableTypeBoundsMap = new HashMap<>(); @@ -219,21 +212,16 @@ public ClassOrInterfaceDeclaration visit(ClassOrInterfaceDeclaration cid, Void a (p, a) -> System.out.println("Nullable Index: " + p + "\tClass: " + a)); classOrInterfaceAnnotationsMap.forEach( (c, a) -> System.out.println("Class: " + c + "\tAnnotations: " + a));*/ - return cid; } @Override - public EnumDeclaration visit(EnumDeclaration ed, Void arg) { - return ed; - } + public void visit(EnumDeclaration ed, Void arg) {} @Override - public ConstructorDeclaration visit(ConstructorDeclaration cd, Void arg) { - return cd; - } + public void visit(ConstructorDeclaration cd, Void arg) {} @Override - public MethodDeclaration visit(MethodDeclaration md, Void arg) { + public void visit(MethodDeclaration md, Void arg) { String methodName = md.getNameAsString(); Optional parentClassNode = md.getParentNode(); String parentClassName = ""; @@ -279,22 +267,15 @@ public MethodDeclaration visit(MethodDeclaration md, Void arg) { } nullableReturnMethods.forEach( (c, s) -> System.out.println("Enclosing Class: " + c + "\tMethod Signature: " + s)); - return md; } @Override - public FieldDeclaration visit(FieldDeclaration fd, Void arg) { - return fd; - } + public void visit(FieldDeclaration fd, Void arg) {} @Override - public InitializerDeclaration visit(InitializerDeclaration id, Void arg) { - return id; - } + public void visit(InitializerDeclaration id, Void arg) {} @Override - public NormalAnnotationExpr visit(NormalAnnotationExpr nae, Void arg) { - return nae; - } + public void visit(NormalAnnotationExpr nae, Void arg) {} } } From 0c2cd44331be54a8b64f0aaff2133458d7813c84 Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Thu, 29 Feb 2024 15:21:39 -0800 Subject: [PATCH 17/50] Generating MethodAnnotationsRecord using AutoValue --- lib-model/lib-model-consume/build.gradle | 2 ++ .../libmodel/LibModelInfoExtractor.java | 2 +- .../libmodel/MethodAnnotationsRecord.java | 20 +++++++------------ .../nullaway/libmodel/StubxFileWriter.java | 8 ++++---- 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/lib-model/lib-model-consume/build.gradle b/lib-model/lib-model-consume/build.gradle index 77990d7b1d..d7574b4fcc 100644 --- a/lib-model/lib-model-consume/build.gradle +++ b/lib-model/lib-model-consume/build.gradle @@ -36,5 +36,7 @@ dependencies { testImplementation project(":lib-model:lib-model-test") implementation deps.build.guava implementation deps.build.javaparser + compileOnly deps.apt.autoValueAnnot + annotationProcessor deps.apt.autoValue compileOnly project(":nullaway") } diff --git a/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java b/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java index c51726c03b..1ee7b890ab 100644 --- a/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java +++ b/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java @@ -263,7 +263,7 @@ public void visit(MethodDeclaration md, Void arg) { nullableReturnMethods.put(packageName + "." + parentClassName, methodSignature); methodRecords.put( packageName + "." + parentClassName + ":" + methodReturnType + " " + methodSignature, - new MethodAnnotationsRecord(ImmutableSet.of("Nullable"), ImmutableMap.of())); + MethodAnnotationsRecord.create(ImmutableSet.of("Nullable"), ImmutableMap.of())); } nullableReturnMethods.forEach( (c, s) -> System.out.println("Enclosing Class: " + c + "\tMethod Signature: " + s)); diff --git a/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/MethodAnnotationsRecord.java b/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/MethodAnnotationsRecord.java index 3923a81754..475f1c0ca4 100644 --- a/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/MethodAnnotationsRecord.java +++ b/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/MethodAnnotationsRecord.java @@ -1,26 +1,20 @@ package com.uber.nullaway.libmodel; +import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; /** A record describing the annotations associated with a java method and its arguments. */ -final class MethodAnnotationsRecord { - private final ImmutableSet methodAnnotations; - // 0 means receiver - private final ImmutableMap> argumentAnnotations; +@AutoValue +abstract class MethodAnnotationsRecord { - MethodAnnotationsRecord( + static MethodAnnotationsRecord create( ImmutableSet methodAnnotations, ImmutableMap> argumentAnnotations) { - this.methodAnnotations = methodAnnotations; - this.argumentAnnotations = argumentAnnotations; + return new AutoValue_MethodAnnotationsRecord(methodAnnotations, argumentAnnotations); } - ImmutableSet getMethodAnnotations() { - return methodAnnotations; - } + abstract ImmutableSet methodAnnotations(); - ImmutableMap> getArgumentAnnotations() { - return argumentAnnotations; - } + abstract ImmutableMap> argumentAnnotations(); } diff --git a/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/StubxFileWriter.java b/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/StubxFileWriter.java index 0d7f4f3441..9a0f333a63 100644 --- a/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/StubxFileWriter.java +++ b/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/StubxFileWriter.java @@ -93,13 +93,13 @@ static void write( int methodAnnotationSize = 0; int methodArgumentRecordsSize = 0; for (Map.Entry entry : methodRecords.entrySet()) { - methodAnnotationSize += entry.getValue().getMethodAnnotations().size(); - methodArgumentRecordsSize += entry.getValue().getArgumentAnnotations().size(); + methodAnnotationSize += entry.getValue().methodAnnotations().size(); + methodArgumentRecordsSize += entry.getValue().argumentAnnotations().size(); } out.writeInt(methodAnnotationSize); // Followed by those records as pairs of ints pointing into the dictionary for (Map.Entry entry : methodRecords.entrySet()) { - for (String annot : entry.getValue().getMethodAnnotations()) { + for (String annot : entry.getValue().methodAnnotations()) { out.writeInt(encodingDictionary.get(entry.getKey())); out.writeInt(encodingDictionary.get(importedAnnotations.get(annot))); } @@ -110,7 +110,7 @@ static void write( // argument position) for (Map.Entry entry : methodRecords.entrySet()) { for (Map.Entry> argEntry : - entry.getValue().getArgumentAnnotations().entrySet()) { + entry.getValue().argumentAnnotations().entrySet()) { for (String annot : argEntry.getValue()) { out.writeInt(encodingDictionary.get(entry.getKey())); out.writeInt(argEntry.getKey()); From ac14c998c8bb706ed9cf5295660f09b58960431c Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Thu, 29 Feb 2024 16:02:09 -0800 Subject: [PATCH 18/50] Removing Redundant files for StubxWriter and MethodAnnotationsRecord --- jar-infer/jar-infer-cli/build.gradle | 1 + jar-infer/jar-infer-lib/build.gradle | 1 + .../DefinitelyDerefedParamsDriver.java | 6 +- .../jarinfer/MethodAnnotationsRecord.java | 26 ---- .../libmodel/LibModelInfoExtractor.java | 2 +- .../libmodel/MethodAnnotationsRecord.java | 4 +- .../nullaway/libmodel/StubxFileWriter.java | 122 ------------------ .../uber/nullaway/libmodel}/StubxWriter.java | 14 +- 8 files changed, 16 insertions(+), 160 deletions(-) delete mode 100644 jar-infer/jar-infer-lib/src/main/java/com/uber/nullaway/jarinfer/MethodAnnotationsRecord.java delete mode 100644 lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/StubxFileWriter.java rename {jar-infer/jar-infer-lib/src/main/java/com/uber/nullaway/jarinfer => lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel}/StubxWriter.java (92%) diff --git a/jar-infer/jar-infer-cli/build.gradle b/jar-infer/jar-infer-cli/build.gradle index 3245d662b3..cf28c3b5f0 100644 --- a/jar-infer/jar-infer-cli/build.gradle +++ b/jar-infer/jar-infer-cli/build.gradle @@ -18,6 +18,7 @@ dependencies { implementation deps.build.commonscli implementation deps.build.guava implementation project(":jar-infer:jar-infer-lib") + implementation project(":lib-model:lib-model-consume") testImplementation deps.test.junit4 testImplementation(deps.build.errorProneTestHelpers) { diff --git a/jar-infer/jar-infer-lib/build.gradle b/jar-infer/jar-infer-lib/build.gradle index 8ea4f9b656..552770a079 100644 --- a/jar-infer/jar-infer-lib/build.gradle +++ b/jar-infer/jar-infer-lib/build.gradle @@ -37,6 +37,7 @@ dependencies { api deps.build.guava api deps.build.commonsIO compileOnly deps.build.errorProneCheckApi + implementation project(":lib-model:lib-model-consume") testImplementation deps.test.junit4 testImplementation(deps.build.errorProneTestHelpers) { diff --git a/jar-infer/jar-infer-lib/src/main/java/com/uber/nullaway/jarinfer/DefinitelyDerefedParamsDriver.java b/jar-infer/jar-infer-lib/src/main/java/com/uber/nullaway/jarinfer/DefinitelyDerefedParamsDriver.java index fc852414fb..6616785dbc 100644 --- a/jar-infer/jar-infer-lib/src/main/java/com/uber/nullaway/jarinfer/DefinitelyDerefedParamsDriver.java +++ b/jar-infer/jar-infer-lib/src/main/java/com/uber/nullaway/jarinfer/DefinitelyDerefedParamsDriver.java @@ -43,6 +43,8 @@ import com.ibm.wala.types.TypeReference; import com.ibm.wala.util.collections.Iterator2Iterable; import com.ibm.wala.util.config.FileOfClasses; +import com.uber.nullaway.libmodel.MethodAnnotationsRecord; +import com.uber.nullaway.libmodel.StubxWriter; import java.io.ByteArrayInputStream; import java.io.DataOutputStream; import java.io.File; @@ -437,7 +439,7 @@ private void writeModel(DataOutputStream out) throws IOException { } methodRecords.put( sign, - new MethodAnnotationsRecord( + MethodAnnotationsRecord.create( nullableReturns.contains(sign) ? ImmutableSet.of("Nullable") : ImmutableSet.of(), ImmutableMap.copyOf(argAnnotation))); nullableReturns.remove(sign); @@ -445,7 +447,7 @@ private void writeModel(DataOutputStream out) throws IOException { for (String nullableReturnMethodSign : Iterator2Iterable.make(nullableReturns.iterator())) { methodRecords.put( nullableReturnMethodSign, - new MethodAnnotationsRecord(ImmutableSet.of("Nullable"), ImmutableMap.of())); + MethodAnnotationsRecord.create(ImmutableSet.of("Nullable"), ImmutableMap.of())); } StubxWriter.write(out, importedAnnotations, packageAnnotations, typeAnnotations, methodRecords); } diff --git a/jar-infer/jar-infer-lib/src/main/java/com/uber/nullaway/jarinfer/MethodAnnotationsRecord.java b/jar-infer/jar-infer-lib/src/main/java/com/uber/nullaway/jarinfer/MethodAnnotationsRecord.java deleted file mode 100644 index 5f94b2730c..0000000000 --- a/jar-infer/jar-infer-lib/src/main/java/com/uber/nullaway/jarinfer/MethodAnnotationsRecord.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.uber.nullaway.jarinfer; - -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; - -/** A record describing the annotations associated with a java method and its arguments. */ -final class MethodAnnotationsRecord { - private final ImmutableSet methodAnnotations; - // 0 means receiver - private final ImmutableMap> argumentAnnotations; - - MethodAnnotationsRecord( - ImmutableSet methodAnnotations, - ImmutableMap> argumentAnnotations) { - this.methodAnnotations = methodAnnotations; - this.argumentAnnotations = argumentAnnotations; - } - - ImmutableSet getMethodAnnotations() { - return methodAnnotations; - } - - ImmutableMap> getArgumentAnnotations() { - return argumentAnnotations; - } -} diff --git a/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java b/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java index 1ee7b890ab..0fefc2195f 100644 --- a/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java +++ b/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java @@ -107,7 +107,7 @@ public static void writeToAstubx(String outputPath) { } if (!methodRecords.isEmpty()) { try { - StubxFileWriter.write( + StubxWriter.write( dos, importedAnnotations, new HashMap<>(), new HashMap<>(), methodRecords); dos.close(); } catch (IOException e) { diff --git a/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/MethodAnnotationsRecord.java b/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/MethodAnnotationsRecord.java index 475f1c0ca4..7651b8b88d 100644 --- a/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/MethodAnnotationsRecord.java +++ b/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/MethodAnnotationsRecord.java @@ -6,9 +6,9 @@ /** A record describing the annotations associated with a java method and its arguments. */ @AutoValue -abstract class MethodAnnotationsRecord { +public abstract class MethodAnnotationsRecord { - static MethodAnnotationsRecord create( + public static MethodAnnotationsRecord create( ImmutableSet methodAnnotations, ImmutableMap> argumentAnnotations) { return new AutoValue_MethodAnnotationsRecord(methodAnnotations, argumentAnnotations); diff --git a/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/StubxFileWriter.java b/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/StubxFileWriter.java deleted file mode 100644 index 9a0f333a63..0000000000 --- a/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/StubxFileWriter.java +++ /dev/null @@ -1,122 +0,0 @@ -package com.uber.nullaway.libmodel; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import java.io.DataOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** Simple writer for the astubx format. */ -final class StubxFileWriter { - /** - * The file magic number for version 0 .astubx files. It should be the first four bytes of any - * compatible .astubx file. - */ - private static final int VERSION_0_FILE_MAGIC_NUMBER = 691458791; - - /** - * This method writes the provided list of annotations to a DataOutputStream in the astubx format. - * - * @param out Output stream. - * @param importedAnnotations Mapping of 'custom annotations' to their 'definition classes'. - * @param packageAnnotations Map of 'package names' to their 'list of package-level annotations'. - * @param typeAnnotations Map of 'type names' to their 'list of type annotations'. - * @param methodRecords Map of 'method signatures' to their 'method annotations record'. Method - * annotations record consists of return value annotations and argument annotations. {@link - * MethodAnnotationsRecord} - * @exception IOException On output error. - */ - static void write( - DataOutputStream out, - Map importedAnnotations, - Map> packageAnnotations, - Map> typeAnnotations, - Map methodRecords) - throws IOException { - // File format version/magic number - out.writeInt(VERSION_0_FILE_MAGIC_NUMBER); - // Followed by the number of string dictionary entries - int numStringEntries = 0; - Map encodingDictionary = new LinkedHashMap<>(); - List strings = new ArrayList(); - List> keysets = - ImmutableList.of( - importedAnnotations.values(), - packageAnnotations.keySet(), - typeAnnotations.keySet(), - methodRecords.keySet()); - for (Collection keyset : keysets) { - for (String key : keyset) { - assert !encodingDictionary.containsKey(key); - strings.add(key); - encodingDictionary.put(key, numStringEntries); - ++numStringEntries; - } - } - out.writeInt(numStringEntries); - // Followed by the entries themselves - for (String s : strings) { - out.writeUTF(s); - } - // Followed by the number of encoded package annotation records - int packageAnnotationSize = 0; - for (Map.Entry> entry : packageAnnotations.entrySet()) { - packageAnnotationSize += entry.getValue().size(); - } - out.writeInt(packageAnnotationSize); - // Followed by those records as pairs of ints pointing into the dictionary - for (Map.Entry> entry : packageAnnotations.entrySet()) { - for (String annot : entry.getValue()) { - out.writeInt(encodingDictionary.get(entry.getKey())); - out.writeInt(encodingDictionary.get(importedAnnotations.get(annot))); - } - } - // Followed by the number of encoded type annotation records - int typeAnnotationSize = 0; - for (Map.Entry> entry : typeAnnotations.entrySet()) { - typeAnnotationSize += entry.getValue().size(); - } - out.writeInt(typeAnnotationSize); - // Followed by those records as pairs of ints pointing into the dictionary - for (Map.Entry> entry : typeAnnotations.entrySet()) { - for (String annot : entry.getValue()) { - out.writeInt(encodingDictionary.get(entry.getKey())); - out.writeInt(encodingDictionary.get(importedAnnotations.get(annot))); - } - } - // Followed by the number of encoded method return/declaration annotation records - int methodAnnotationSize = 0; - int methodArgumentRecordsSize = 0; - for (Map.Entry entry : methodRecords.entrySet()) { - methodAnnotationSize += entry.getValue().methodAnnotations().size(); - methodArgumentRecordsSize += entry.getValue().argumentAnnotations().size(); - } - out.writeInt(methodAnnotationSize); - // Followed by those records as pairs of ints pointing into the dictionary - for (Map.Entry entry : methodRecords.entrySet()) { - for (String annot : entry.getValue().methodAnnotations()) { - out.writeInt(encodingDictionary.get(entry.getKey())); - out.writeInt(encodingDictionary.get(importedAnnotations.get(annot))); - } - } - // Followed by the number of encoded method argument annotation records - out.writeInt(methodArgumentRecordsSize); - // Followed by those records as a triplet of ints ( 0 and 2 point in the dictionary, 1 is the - // argument position) - for (Map.Entry entry : methodRecords.entrySet()) { - for (Map.Entry> argEntry : - entry.getValue().argumentAnnotations().entrySet()) { - for (String annot : argEntry.getValue()) { - out.writeInt(encodingDictionary.get(entry.getKey())); - out.writeInt(argEntry.getKey()); - out.writeInt(encodingDictionary.get(importedAnnotations.get(annot))); - } - } - } - } -} diff --git a/jar-infer/jar-infer-lib/src/main/java/com/uber/nullaway/jarinfer/StubxWriter.java b/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/StubxWriter.java similarity index 92% rename from jar-infer/jar-infer-lib/src/main/java/com/uber/nullaway/jarinfer/StubxWriter.java rename to lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/StubxWriter.java index 326097f4c3..9dc6052341 100644 --- a/jar-infer/jar-infer-lib/src/main/java/com/uber/nullaway/jarinfer/StubxWriter.java +++ b/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/StubxWriter.java @@ -1,4 +1,4 @@ -package com.uber.nullaway.jarinfer; +package com.uber.nullaway.libmodel; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -12,7 +12,7 @@ import java.util.Set; /** Simple writer for the astubx format. */ -final class StubxWriter { +public final class StubxWriter { /** * The file magic number for version 0 .astubx files. It should be the first four bytes of any * compatible .astubx file. @@ -31,7 +31,7 @@ final class StubxWriter { * MethodAnnotationsRecord} * @exception IOException On output error. */ - static void write( + public static void write( DataOutputStream out, Map importedAnnotations, Map> packageAnnotations, @@ -93,13 +93,13 @@ static void write( int methodAnnotationSize = 0; int methodArgumentRecordsSize = 0; for (Map.Entry entry : methodRecords.entrySet()) { - methodAnnotationSize += entry.getValue().getMethodAnnotations().size(); - methodArgumentRecordsSize += entry.getValue().getArgumentAnnotations().size(); + methodAnnotationSize += entry.getValue().methodAnnotations().size(); + methodArgumentRecordsSize += entry.getValue().argumentAnnotations().size(); } out.writeInt(methodAnnotationSize); // Followed by those records as pairs of ints pointing into the dictionary for (Map.Entry entry : methodRecords.entrySet()) { - for (String annot : entry.getValue().getMethodAnnotations()) { + for (String annot : entry.getValue().methodAnnotations()) { out.writeInt(encodingDictionary.get(entry.getKey())); out.writeInt(encodingDictionary.get(importedAnnotations.get(annot))); } @@ -110,7 +110,7 @@ static void write( // argument position) for (Map.Entry entry : methodRecords.entrySet()) { for (Map.Entry> argEntry : - entry.getValue().getArgumentAnnotations().entrySet()) { + entry.getValue().argumentAnnotations().entrySet()) { for (String annot : argEntry.getValue()) { out.writeInt(encodingDictionary.get(entry.getKey())); out.writeInt(argEntry.getKey()); From 32e80d564dd491e7b7e98796193a301838a66c49 Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Tue, 5 Mar 2024 14:34:30 -0800 Subject: [PATCH 19/50] making methodRecords local variable and passing as a param to make it thread safe --- .../libmodel/LibModelInfoExtractor.java | 48 +++++++++++-------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java b/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java index 0fefc2195f..47dd5bf2a9 100644 --- a/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java +++ b/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java @@ -61,18 +61,18 @@ public class LibModelInfoExtractor { - private static Map methodRecords = new LinkedHashMap<>(); - public static void main(String[] args) { + Map methodRecords = new LinkedHashMap<>(); + LibModelInfoExtractor libModelInfoExtractor = new LibModelInfoExtractor(); // input directory - processDirectory(args[0]); + libModelInfoExtractor.processDirectory(args[0], methodRecords); // output directory - writeToAstubx(args[1]); + libModelInfoExtractor.writeToAstubx(args[1], methodRecords); } - public static void processDirectory(String file) { + public void processDirectory(String file, Map methodRecords) { Path root = dirnameToPath(file); - AnnotationCollectorCallback mc = new AnnotationCollectorCallback(); + AnnotationCollectorCallback mc = new AnnotationCollectorCallback(methodRecords); CollectionStrategy strategy = new ParserCollectionStrategy(); // Required to include directories that contain a module-info.java, which don't parse by // default. @@ -91,7 +91,7 @@ public static void processDirectory(String file) { }); } - public static void writeToAstubx(String outputPath) { + public void writeToAstubx(String outputPath, Map methodRecords) { Map importedAnnotations = ImmutableMap.builder() .put("Nonnull", "javax.annotation.Nonnull") @@ -116,7 +116,7 @@ public static void writeToAstubx(String outputPath) { } } - public static Path dirnameToPath(String dir) { + public Path dirnameToPath(String dir) { File f = new File(dir); if (!f.exists()) { System.err.printf("Directory %s (%s) does not exist.%n", dir, f); @@ -136,8 +136,10 @@ public static Path dirnameToPath(String dir) { private static class AnnotationCollectorCallback implements SourceRoot.Callback { private final AnnotationCollectionVisitor mv; + private Map methodRecords; - public AnnotationCollectorCallback() { + public AnnotationCollectorCallback(Map methodRecords) { + this.methodRecords = methodRecords; this.mv = new AnnotationCollectionVisitor(); } @@ -160,7 +162,7 @@ public void visitAll(Node rootNode) { stack.addFirst(rootNode); while (!stack.isEmpty()) { Node current = stack.removeFirst(); - current.accept(mv, null); + current.accept(mv, this.methodRecords); List children = current.getChildNodes(); if (children != null) { for (Node child : children) { @@ -171,18 +173,19 @@ public void visitAll(Node rootNode) { } } - private static class AnnotationCollectionVisitor extends VoidVisitorAdapter { + private static class AnnotationCollectionVisitor + extends VoidVisitorAdapter> { private String packageName = ""; @Override - public void visit(PackageDeclaration n, Void arg) { + public void visit(PackageDeclaration n, Map methodRecordsMap) { this.packageName = n.getNameAsString(); - // System.out.println("Package name: " + packageName); } @Override - public void visit(ClassOrInterfaceDeclaration cid, Void arg) { + public void visit( + ClassOrInterfaceDeclaration cid, Map methodRecordsMap) { String classOrInterfaceName = packageName + "." + cid.getNameAsString(); @SuppressWarnings("all") Map nullableTypeBoundsMap = new HashMap<>(); @@ -215,13 +218,14 @@ public void visit(ClassOrInterfaceDeclaration cid, Void arg) { } @Override - public void visit(EnumDeclaration ed, Void arg) {} + public void visit(EnumDeclaration ed, Map methodRecordsMap) {} @Override - public void visit(ConstructorDeclaration cd, Void arg) {} + public void visit( + ConstructorDeclaration cd, Map methodRecordsMap) {} @Override - public void visit(MethodDeclaration md, Void arg) { + public void visit(MethodDeclaration md, Map methodRecordsMap) { String methodName = md.getNameAsString(); Optional parentClassNode = md.getParentNode(); String parentClassName = ""; @@ -261,7 +265,7 @@ public void visit(MethodDeclaration md, Void arg) { } if (isNullableAnnotationPresent) { nullableReturnMethods.put(packageName + "." + parentClassName, methodSignature); - methodRecords.put( + methodRecordsMap.put( packageName + "." + parentClassName + ":" + methodReturnType + " " + methodSignature, MethodAnnotationsRecord.create(ImmutableSet.of("Nullable"), ImmutableMap.of())); } @@ -270,12 +274,14 @@ public void visit(MethodDeclaration md, Void arg) { } @Override - public void visit(FieldDeclaration fd, Void arg) {} + public void visit(FieldDeclaration fd, Map methodRecordsMap) {} @Override - public void visit(InitializerDeclaration id, Void arg) {} + public void visit( + InitializerDeclaration id, Map methodRecordsMap) {} @Override - public void visit(NormalAnnotationExpr nae, Void arg) {} + public void visit( + NormalAnnotationExpr nae, Map methodRecordsMap) {} } } From 888c1b209d57b6997cd6bcb815c3c465ad31071e Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Tue, 5 Mar 2024 15:58:14 -0800 Subject: [PATCH 20/50] separating lib model cli module --- lib-model/lib-model-consume-cli/build.gradle | 40 +++++++++++++++++++ .../libmodel/LibModelInfoExtractor.java | 0 lib-model/lib-model-consume/build.gradle | 11 ----- lib-model/lib-model-test/build.gradle | 4 +- settings.gradle | 1 + 5 files changed, 43 insertions(+), 13 deletions(-) create mode 100644 lib-model/lib-model-consume-cli/build.gradle rename lib-model/{lib-model-consume => lib-model-consume-cli}/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java (100%) diff --git a/lib-model/lib-model-consume-cli/build.gradle b/lib-model/lib-model-consume-cli/build.gradle new file mode 100644 index 0000000000..88de937331 --- /dev/null +++ b/lib-model/lib-model-consume-cli/build.gradle @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2017. Uber Technologies + * + * 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. + */ +plugins { + id "java-library" +} + +jar{ + manifest { + attributes('Main-Class':'com.uber.nullaway.libmodel.LibModelInfoExtractor') + } + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } +} + +dependencies { + testImplementation deps.test.junit4 + testImplementation(deps.build.errorProneTestHelpers) { + exclude group: "junit", module: "junit" + } + testImplementation project(":nullaway") + implementation project(":lib-model:lib-model-consume") + implementation deps.build.guava + implementation deps.build.javaparser + compileOnly project(":nullaway") +} +jar.dependsOn ":lib-model:lib-model-consume:assemble" diff --git a/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java b/lib-model/lib-model-consume-cli/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java similarity index 100% rename from lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java rename to lib-model/lib-model-consume-cli/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java diff --git a/lib-model/lib-model-consume/build.gradle b/lib-model/lib-model-consume/build.gradle index d7574b4fcc..cb002627d9 100644 --- a/lib-model/lib-model-consume/build.gradle +++ b/lib-model/lib-model-consume/build.gradle @@ -18,15 +18,6 @@ plugins { id "nullaway.java-test-conventions" } -jar{ - manifest { - attributes('Main-Class':'com.uber.nullaway.libmodel.LibModelInfoExtractor') - } - from { - configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } - } -} - dependencies { testImplementation deps.test.junit4 testImplementation(deps.build.errorProneTestHelpers) { @@ -35,8 +26,6 @@ dependencies { testImplementation project(":nullaway") testImplementation project(":lib-model:lib-model-test") implementation deps.build.guava - implementation deps.build.javaparser compileOnly deps.apt.autoValueAnnot annotationProcessor deps.apt.autoValue - compileOnly project(":nullaway") } diff --git a/lib-model/lib-model-test/build.gradle b/lib-model/lib-model-test/build.gradle index 8697115a75..eeaea5031a 100644 --- a/lib-model/lib-model-test/build.gradle +++ b/lib-model/lib-model-test/build.gradle @@ -35,7 +35,7 @@ jar { jar.doLast { javaexec { - classpath = files("${rootProject.projectDir}/lib-model/lib-model-consume/build/libs/lib-model-consume.jar") + classpath = files("${rootProject.projectDir}/lib-model/lib-model-consume-cli/build/libs/lib-model-consume-cli.jar") main = "com.uber.nullaway.libmodel.LibModelInfoExtractor" args = [ jdkPath, @@ -55,4 +55,4 @@ dependencies { implementation deps.build.jsr305Annotations } -jar.dependsOn ":lib-model:lib-model-consume:assemble" +jar.dependsOn ":lib-model:lib-model-consume-cli:assemble" diff --git a/settings.gradle b/settings.gradle index ceb6135435..2639860a71 100644 --- a/settings.gradle +++ b/settings.gradle @@ -34,3 +34,4 @@ include ':sample-app' include ':jar-infer:test-android-lib-jarinfer' include ':lib-model:lib-model-consume' include ':lib-model:lib-model-test' +include 'lib-model:lib-model-consume-cli' From fb81869fa1fcfb9547b033cfb67460eacee79677 Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Tue, 5 Mar 2024 16:27:09 -0800 Subject: [PATCH 21/50] removing unnecessary dependencies --- lib-model/lib-model-consume-cli/build.gradle | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib-model/lib-model-consume-cli/build.gradle b/lib-model/lib-model-consume-cli/build.gradle index 88de937331..0f2548d41a 100644 --- a/lib-model/lib-model-consume-cli/build.gradle +++ b/lib-model/lib-model-consume-cli/build.gradle @@ -27,14 +27,8 @@ jar{ } dependencies { - testImplementation deps.test.junit4 - testImplementation(deps.build.errorProneTestHelpers) { - exclude group: "junit", module: "junit" - } - testImplementation project(":nullaway") implementation project(":lib-model:lib-model-consume") implementation deps.build.guava implementation deps.build.javaparser - compileOnly project(":nullaway") } jar.dependsOn ":lib-model:lib-model-consume:assemble" From e889dc3f5dd0e4368cd82f4a5969efa714f06264 Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Wed, 6 Mar 2024 22:39:04 -0800 Subject: [PATCH 22/50] refactoring module names and build files --- build.gradle | 2 +- jar-infer/jar-infer-cli/build.gradle | 2 +- jar-infer/jar-infer-lib/build.gradle | 2 +- .../library-model-generator-cli}/build.gradle | 4 ++-- .../uber/nullaway/libmodel/LibModelInfoExtractor.java | 0 .../build.gradle | 10 +++++----- .../com/uber/nullaway/libmodel/AnnotationExample.java | 10 ++++++++++ .../uber/nullaway/libmodel/provider/TestProvider.java | 0 .../com/uber/nullaway/libmodel/AnnotationExample.java | 10 ++++++++++ .../library-model-generator}/build.gradle | 2 +- .../nullaway/libmodel/MethodAnnotationsRecord.java | 0 .../java/com/uber/nullaway/libmodel/StubxWriter.java | 0 .../nullaway/libmodel/LibModelIntegrationTest.java | 0 settings.gradle | 6 +++--- 14 files changed, 34 insertions(+), 14 deletions(-) rename {lib-model/lib-model-consume-cli => library-model/library-model-generator-cli}/build.gradle (87%) rename {lib-model/lib-model-consume-cli => library-model/library-model-generator-cli}/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java (100%) rename {lib-model/lib-model-test => library-model/library-model-generator-integration-test}/build.gradle (76%) rename {lib-model/lib-model-test => library-model/library-model-generator-integration-test}/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java (52%) rename {lib-model/lib-model-test => library-model/library-model-generator-integration-test}/src/main/java/com/uber/nullaway/libmodel/provider/TestProvider.java (100%) rename {lib-model/lib-model-test => library-model/library-model-generator-integration-test}/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java (56%) rename {lib-model/lib-model-consume => library-model/library-model-generator}/build.gradle (91%) rename {lib-model/lib-model-consume => library-model/library-model-generator}/src/main/java/com/uber/nullaway/libmodel/MethodAnnotationsRecord.java (100%) rename {lib-model/lib-model-consume => library-model/library-model-generator}/src/main/java/com/uber/nullaway/libmodel/StubxWriter.java (100%) rename {lib-model/lib-model-consume => library-model/library-model-generator}/src/test/java/com/uber/nullaway/libmodel/LibModelIntegrationTest.java (100%) diff --git a/build.gradle b/build.gradle index 0246ab077c..0144abcb8e 100644 --- a/build.gradle +++ b/build.gradle @@ -97,7 +97,7 @@ subprojects { project -> // For some reason, spotless complains when applied to the jar-infer folder itself, even // though there is no top-level :jar-infer project - if (project.name != "jar-infer" && project.name != "lib-model") { + if (project.name != "jar-infer" && project.name != "library-model") { project.apply plugin: "com.diffplug.spotless" spotless { java { diff --git a/jar-infer/jar-infer-cli/build.gradle b/jar-infer/jar-infer-cli/build.gradle index cf28c3b5f0..9dafc4dd54 100644 --- a/jar-infer/jar-infer-cli/build.gradle +++ b/jar-infer/jar-infer-cli/build.gradle @@ -18,7 +18,7 @@ dependencies { implementation deps.build.commonscli implementation deps.build.guava implementation project(":jar-infer:jar-infer-lib") - implementation project(":lib-model:lib-model-consume") + implementation project(":library-model:library-model-generator") testImplementation deps.test.junit4 testImplementation(deps.build.errorProneTestHelpers) { diff --git a/jar-infer/jar-infer-lib/build.gradle b/jar-infer/jar-infer-lib/build.gradle index 552770a079..c0f6c348a7 100644 --- a/jar-infer/jar-infer-lib/build.gradle +++ b/jar-infer/jar-infer-lib/build.gradle @@ -37,7 +37,7 @@ dependencies { api deps.build.guava api deps.build.commonsIO compileOnly deps.build.errorProneCheckApi - implementation project(":lib-model:lib-model-consume") + implementation project(":library-model:library-model-generator") testImplementation deps.test.junit4 testImplementation(deps.build.errorProneTestHelpers) { diff --git a/lib-model/lib-model-consume-cli/build.gradle b/library-model/library-model-generator-cli/build.gradle similarity index 87% rename from lib-model/lib-model-consume-cli/build.gradle rename to library-model/library-model-generator-cli/build.gradle index 0f2548d41a..d33016c5bd 100644 --- a/lib-model/lib-model-consume-cli/build.gradle +++ b/library-model/library-model-generator-cli/build.gradle @@ -27,8 +27,8 @@ jar{ } dependencies { - implementation project(":lib-model:lib-model-consume") + implementation project(":library-model:library-model-generator") implementation deps.build.guava implementation deps.build.javaparser } -jar.dependsOn ":lib-model:lib-model-consume:assemble" +jar.dependsOn ":library-model:library-model-generator:assemble" diff --git a/lib-model/lib-model-consume-cli/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java b/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java similarity index 100% rename from lib-model/lib-model-consume-cli/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java rename to library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java diff --git a/lib-model/lib-model-test/build.gradle b/library-model/library-model-generator-integration-test/build.gradle similarity index 76% rename from lib-model/lib-model-test/build.gradle rename to library-model/library-model-generator-integration-test/build.gradle index eeaea5031a..b7f1d1a57c 100644 --- a/lib-model/lib-model-test/build.gradle +++ b/library-model/library-model-generator-integration-test/build.gradle @@ -18,9 +18,9 @@ plugins { id "java-library" } -evaluationDependsOn(":lib-model:lib-model-consume") +evaluationDependsOn(":library-model:library-model-generator") -def jdkPath = "${rootProject.projectDir}/lib-model/lib-model-test/src/main/resources/sample_annotated/src" +def jdkPath = "${rootProject.projectDir}/library-model/library-model-generator-integration-test/src/main/resources/sample_annotated/src" def astubxPath = "com/uber/nullaway/libmodel/provider/libmodels.astubx" jar { @@ -35,7 +35,7 @@ jar { jar.doLast { javaexec { - classpath = files("${rootProject.projectDir}/lib-model/lib-model-consume-cli/build/libs/lib-model-consume-cli.jar") + classpath = files("${rootProject.projectDir}/library-model/library-model-generator-cli/build/libs/library-model-generator-cli.jar") main = "com.uber.nullaway.libmodel.LibModelInfoExtractor" args = [ jdkPath, @@ -44,7 +44,7 @@ jar.doLast { } exec { workingDir "./build/libs" - commandLine "jar", "uf", "lib-model-test.jar", astubxPath + commandLine "jar", "uf", "library-model-generator-integration-test.jar", astubxPath } } @@ -55,4 +55,4 @@ dependencies { implementation deps.build.jsr305Annotations } -jar.dependsOn ":lib-model:lib-model-consume-cli:assemble" +jar.dependsOn ":library-model:library-model-generator-cli:assemble" diff --git a/lib-model/lib-model-test/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java b/library-model/library-model-generator-integration-test/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java similarity index 52% rename from lib-model/lib-model-test/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java rename to library-model/library-model-generator-integration-test/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java index d7eef946d6..43d283368e 100644 --- a/lib-model/lib-model-test/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java +++ b/library-model/library-model-generator-integration-test/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java @@ -10,4 +10,14 @@ public String makeUpperCase(String inputString) { return inputString.toUpperCase(Locale.ROOT); } } + + public String[] makeUpperCaseArray(String inputString) { + if (inputString == null || inputString.isEmpty()) { + return null; + } else { + String[] result = new String[1]; + result[0] = inputString.toUpperCase(Locale.ROOT); + return result; + } + } } diff --git a/lib-model/lib-model-test/src/main/java/com/uber/nullaway/libmodel/provider/TestProvider.java b/library-model/library-model-generator-integration-test/src/main/java/com/uber/nullaway/libmodel/provider/TestProvider.java similarity index 100% rename from lib-model/lib-model-test/src/main/java/com/uber/nullaway/libmodel/provider/TestProvider.java rename to library-model/library-model-generator-integration-test/src/main/java/com/uber/nullaway/libmodel/provider/TestProvider.java diff --git a/lib-model/lib-model-test/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java b/library-model/library-model-generator-integration-test/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java similarity index 56% rename from lib-model/lib-model-test/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java rename to library-model/library-model-generator-integration-test/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java index 62507c8ea8..193ea0c1a2 100644 --- a/lib-model/lib-model-test/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java +++ b/library-model/library-model-generator-integration-test/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java @@ -14,4 +14,14 @@ public String makeUpperCase(String inputString) { return inputString.toUpperCase(); } } + + public String @Nullable [] makeUpperCaseArray(String inputString) { + if (inputString == null || inputString.isEmpty()) { + return null; + } else { + String[] result = new String[1]; + result[0] = inputString.toUpperCase(); + return result; + } + } } diff --git a/lib-model/lib-model-consume/build.gradle b/library-model/library-model-generator/build.gradle similarity index 91% rename from lib-model/lib-model-consume/build.gradle rename to library-model/library-model-generator/build.gradle index cb002627d9..31746e5311 100644 --- a/lib-model/lib-model-consume/build.gradle +++ b/library-model/library-model-generator/build.gradle @@ -24,7 +24,7 @@ dependencies { exclude group: "junit", module: "junit" } testImplementation project(":nullaway") - testImplementation project(":lib-model:lib-model-test") + testImplementation project(":library-model:library-model-generator-integration-test") implementation deps.build.guava compileOnly deps.apt.autoValueAnnot annotationProcessor deps.apt.autoValue diff --git a/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/MethodAnnotationsRecord.java b/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/MethodAnnotationsRecord.java similarity index 100% rename from lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/MethodAnnotationsRecord.java rename to library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/MethodAnnotationsRecord.java diff --git a/lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/StubxWriter.java b/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/StubxWriter.java similarity index 100% rename from lib-model/lib-model-consume/src/main/java/com/uber/nullaway/libmodel/StubxWriter.java rename to library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/StubxWriter.java diff --git a/lib-model/lib-model-consume/src/test/java/com/uber/nullaway/libmodel/LibModelIntegrationTest.java b/library-model/library-model-generator/src/test/java/com/uber/nullaway/libmodel/LibModelIntegrationTest.java similarity index 100% rename from lib-model/lib-model-consume/src/test/java/com/uber/nullaway/libmodel/LibModelIntegrationTest.java rename to library-model/library-model-generator/src/test/java/com/uber/nullaway/libmodel/LibModelIntegrationTest.java diff --git a/settings.gradle b/settings.gradle index 2639860a71..b0364e9a1f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -32,6 +32,6 @@ include ':jdk-recent-unit-tests' include ':code-coverage-report' include ':sample-app' include ':jar-infer:test-android-lib-jarinfer' -include ':lib-model:lib-model-consume' -include ':lib-model:lib-model-test' -include 'lib-model:lib-model-consume-cli' +include ':library-model:library-model-generator' +include ':library-model:library-model-generator-integration-test' +include 'library-model:library-model-generator-cli' From 26efa96a57b7292cc1db8d9dff719e0a7ebdfbab Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Wed, 6 Mar 2024 23:05:19 -0800 Subject: [PATCH 23/50] using shadowJar plugin --- .../library-model-generator-cli/build.gradle | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/library-model/library-model-generator-cli/build.gradle b/library-model/library-model-generator-cli/build.gradle index d33016c5bd..d9e90f087c 100644 --- a/library-model/library-model-generator-cli/build.gradle +++ b/library-model/library-model-generator-cli/build.gradle @@ -15,20 +15,31 @@ */ plugins { id "java-library" + id "com.github.johnrengelman.shadow" } jar{ manifest { attributes('Main-Class':'com.uber.nullaway.libmodel.LibModelInfoExtractor') } - from { - configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } - } + // add this classifier so that the output file for the jar task differs from + // the output file for the shadowJar task (otherwise they overwrite each other's + // outputs, forcing the tasks to always re-run) + archiveClassifier = "nonshadow" +} + +shadowJar { + mergeServiceFiles() + configurations = [ + project.configurations.runtimeClasspath + ] + archiveClassifier = "" } +shadowJar.dependsOn jar +assemble.dependsOn shadowJar dependencies { implementation project(":library-model:library-model-generator") implementation deps.build.guava implementation deps.build.javaparser } -jar.dependsOn ":library-model:library-model-generator:assemble" From cbe7bf583ff3225d26614b9dce88cbf8daeb17b7 Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Wed, 6 Mar 2024 23:06:00 -0800 Subject: [PATCH 24/50] formatting --- library-model/library-model-generator-cli/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library-model/library-model-generator-cli/build.gradle b/library-model/library-model-generator-cli/build.gradle index d9e90f087c..1ae269b187 100644 --- a/library-model/library-model-generator-cli/build.gradle +++ b/library-model/library-model-generator-cli/build.gradle @@ -31,7 +31,7 @@ jar{ shadowJar { mergeServiceFiles() configurations = [ - project.configurations.runtimeClasspath + project.configurations.runtimeClasspath ] archiveClassifier = "" } From 600164c0e18d9b36de6fe9f0dcd9070fcbdc4e2f Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Thu, 7 Mar 2024 00:49:51 -0800 Subject: [PATCH 25/50] refactoring, adding javadocs and clean-up --- .../library-model-generator-cli/build.gradle | 2 +- ...ractor.java => LibraryModelGenerator.java} | 159 ++++++++---------- .../build.gradle | 2 +- 3 files changed, 72 insertions(+), 91 deletions(-) rename library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/{LibModelInfoExtractor.java => LibraryModelGenerator.java} (70%) diff --git a/library-model/library-model-generator-cli/build.gradle b/library-model/library-model-generator-cli/build.gradle index 1ae269b187..e5caa913be 100644 --- a/library-model/library-model-generator-cli/build.gradle +++ b/library-model/library-model-generator-cli/build.gradle @@ -20,7 +20,7 @@ plugins { jar{ manifest { - attributes('Main-Class':'com.uber.nullaway.libmodel.LibModelInfoExtractor') + attributes('Main-Class':'com.uber.nullaway.libmodel.LibraryModelGenerator') } // add this classifier so that the output file for the jar task differs from // the output file for the shadowJar task (otherwise they overwrite each other's diff --git a/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java b/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java similarity index 70% rename from library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java rename to library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java index 47dd5bf2a9..3530de6f69 100644 --- a/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibModelInfoExtractor.java +++ b/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java @@ -29,13 +29,9 @@ import com.github.javaparser.ast.NodeList; import com.github.javaparser.ast.PackageDeclaration; import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; -import com.github.javaparser.ast.body.ConstructorDeclaration; import com.github.javaparser.ast.body.EnumDeclaration; -import com.github.javaparser.ast.body.FieldDeclaration; -import com.github.javaparser.ast.body.InitializerDeclaration; import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.expr.AnnotationExpr; -import com.github.javaparser.ast.expr.NormalAnnotationExpr; import com.github.javaparser.ast.type.ArrayType; import com.github.javaparser.ast.type.ClassOrInterfaceType; import com.github.javaparser.ast.type.TypeParameter; @@ -52,27 +48,50 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.ArrayDeque; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; -public class LibModelInfoExtractor { +/** + * Utilized for generating an astubx file from a directory containing annotated Java source code. + * + *

This class utilizes com.github.javaparser APIs to analyze Java source files within a specified + * directory. It processes the annotated Java source code to generate an astubx file that contains + * the required annotation information to be able to generate library models. + */ +public class LibraryModelGenerator { + /** + * This is the main method of the cli tool. It parses the source files within a specified + * directory, obtains meaningful Nullability annotation information and writes it into an astubx + * file. + * + * @param args Command line arguments for the directory containing source files and the output + * directory. + */ public static void main(String[] args) { - Map methodRecords = new LinkedHashMap<>(); - LibModelInfoExtractor libModelInfoExtractor = new LibModelInfoExtractor(); - // input directory - libModelInfoExtractor.processDirectory(args[0], methodRecords); - // output directory - libModelInfoExtractor.writeToAstubx(args[1], methodRecords); + LibraryModelGenerator libraryModelGenerator = new LibraryModelGenerator(); + libraryModelGenerator.generateAstubxForLibraryModels(args[0], args[1]); + } + + public void generateAstubxForLibraryModels(String inputSourceDirectory, String outputDirectory) { + Map methodRecords = processDirectory(inputSourceDirectory); + writeToAstubx(outputDirectory, methodRecords); } - public void processDirectory(String file, Map methodRecords) { - Path root = dirnameToPath(file); - AnnotationCollectorCallback mc = new AnnotationCollectorCallback(methodRecords); + /** + * Parses the source files within the directory using javaparser. + * + * @param sourceDirectoryRoot Directory containing annotated java source files. + * @return a Map containing the Nullability annotation information from the source files. + */ + private Map processDirectory(String sourceDirectoryRoot) { + Map methodRecords = new LinkedHashMap<>(); + Path root = dirnameToPath(sourceDirectoryRoot); + AnnotationCollectorCallback ac = new AnnotationCollectorCallback(methodRecords); CollectionStrategy strategy = new ParserCollectionStrategy(); // Required to include directories that contain a module-info.java, which don't parse by // default. @@ -84,48 +103,42 @@ public void processDirectory(String file, Map m .forEach( sourceRoot -> { try { - sourceRoot.parse("", mc); + sourceRoot.parse("", ac); } catch (IOException e) { - System.err.println("IOException: " + e); + throw new RuntimeException(e); } }); + return methodRecords; } - public void writeToAstubx(String outputPath, Map methodRecords) { + /** + * Writes the Nullability annotation information into the output directory as an astubx file. + * + * @param outputPath Output Directory. + * @param methodRecords Map containing the collected Nullability annotation information. + */ + private void writeToAstubx( + String outputPath, Map methodRecords) { + if (methodRecords.isEmpty()) { + return; + } Map importedAnnotations = ImmutableMap.builder() .put("Nonnull", "javax.annotation.Nonnull") .put("Nullable", "javax.annotation.Nullable") .build(); - DataOutputStream dos; - try { - Path opPath = Paths.get(outputPath); - Files.createDirectories(opPath.getParent()); - dos = new DataOutputStream(Files.newOutputStream(opPath)); + Path outputPathInstance = Paths.get(outputPath); + try (DataOutputStream dos = new DataOutputStream(Files.newOutputStream(outputPathInstance))) { + Files.createDirectories(outputPathInstance.getParent()); + StubxWriter.write( + dos, importedAnnotations, Collections.emptyMap(), Collections.emptyMap(), methodRecords); } catch (IOException e) { throw new RuntimeException(e); } - if (!methodRecords.isEmpty()) { - try { - StubxWriter.write( - dos, importedAnnotations, new HashMap<>(), new HashMap<>(), methodRecords); - dos.close(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } } public Path dirnameToPath(String dir) { File f = new File(dir); - if (!f.exists()) { - System.err.printf("Directory %s (%s) does not exist.%n", dir, f); - System.exit(1); - } - if (!f.isDirectory()) { - System.err.printf("Not a directory: %s (%s).%n", dir, f); - System.exit(1); - } String absoluteDir = f.getAbsolutePath(); if (absoluteDir.endsWith("/.")) { absoluteDir = absoluteDir.substring(0, absoluteDir.length() - 2); @@ -135,12 +148,10 @@ public Path dirnameToPath(String dir) { private static class AnnotationCollectorCallback implements SourceRoot.Callback { - private final AnnotationCollectionVisitor mv; - private Map methodRecords; + private final AnnotationCollectionVisitor annotationCollectionVisitor; public AnnotationCollectorCallback(Map methodRecords) { - this.methodRecords = methodRecords; - this.mv = new AnnotationCollectionVisitor(); + this.annotationCollectionVisitor = new AnnotationCollectionVisitor(methodRecords); } @Override @@ -149,43 +160,29 @@ public Result process(Path localPath, Path absolutePath, ParseResult opt = result.getResult(); if (opt.isPresent()) { CompilationUnit cu = opt.get(); - visitAll(cu); + cu.accept(annotationCollectionVisitor, null); } return res; } - - public void visitAll(Node rootNode) { - if (rootNode == null) { - return; - } - ArrayDeque stack = new ArrayDeque<>(); - stack.addFirst(rootNode); - while (!stack.isEmpty()) { - Node current = stack.removeFirst(); - current.accept(mv, this.methodRecords); - List children = current.getChildNodes(); - if (children != null) { - for (Node child : children) { - stack.addFirst(child); - } - } - } - } } - private static class AnnotationCollectionVisitor - extends VoidVisitorAdapter> { + private static class AnnotationCollectionVisitor extends VoidVisitorAdapter { private String packageName = ""; + private Map methodRecords; + + public AnnotationCollectionVisitor(Map methodRecords) { + this.methodRecords = methodRecords; + } @Override - public void visit(PackageDeclaration n, Map methodRecordsMap) { - this.packageName = n.getNameAsString(); + public void visit(PackageDeclaration pd, Void arg) { + this.packageName = pd.getNameAsString(); + super.visit(pd, null); } @Override - public void visit( - ClassOrInterfaceDeclaration cid, Map methodRecordsMap) { + public void visit(ClassOrInterfaceDeclaration cid, Void arg) { String classOrInterfaceName = packageName + "." + cid.getNameAsString(); @SuppressWarnings("all") Map nullableTypeBoundsMap = new HashMap<>(); @@ -215,17 +212,11 @@ public void visit( (p, a) -> System.out.println("Nullable Index: " + p + "\tClass: " + a)); classOrInterfaceAnnotationsMap.forEach( (c, a) -> System.out.println("Class: " + c + "\tAnnotations: " + a));*/ + super.visit(cid, null); } @Override - public void visit(EnumDeclaration ed, Map methodRecordsMap) {} - - @Override - public void visit( - ConstructorDeclaration cd, Map methodRecordsMap) {} - - @Override - public void visit(MethodDeclaration md, Map methodRecordsMap) { + public void visit(MethodDeclaration md, Void arg) { String methodName = md.getNameAsString(); Optional parentClassNode = md.getParentNode(); String parentClassName = ""; @@ -265,23 +256,13 @@ public void visit(MethodDeclaration md, Map met } if (isNullableAnnotationPresent) { nullableReturnMethods.put(packageName + "." + parentClassName, methodSignature); - methodRecordsMap.put( + methodRecords.put( packageName + "." + parentClassName + ":" + methodReturnType + " " + methodSignature, MethodAnnotationsRecord.create(ImmutableSet.of("Nullable"), ImmutableMap.of())); } nullableReturnMethods.forEach( (c, s) -> System.out.println("Enclosing Class: " + c + "\tMethod Signature: " + s)); + super.visit(md, null); } - - @Override - public void visit(FieldDeclaration fd, Map methodRecordsMap) {} - - @Override - public void visit( - InitializerDeclaration id, Map methodRecordsMap) {} - - @Override - public void visit( - NormalAnnotationExpr nae, Map methodRecordsMap) {} } } diff --git a/library-model/library-model-generator-integration-test/build.gradle b/library-model/library-model-generator-integration-test/build.gradle index b7f1d1a57c..f618ae7cc2 100644 --- a/library-model/library-model-generator-integration-test/build.gradle +++ b/library-model/library-model-generator-integration-test/build.gradle @@ -36,7 +36,7 @@ jar { jar.doLast { javaexec { classpath = files("${rootProject.projectDir}/library-model/library-model-generator-cli/build/libs/library-model-generator-cli.jar") - main = "com.uber.nullaway.libmodel.LibModelInfoExtractor" + main = "com.uber.nullaway.libmodel.LibraryModelGenerator" args = [ jdkPath, "${jar.destinationDirectory.get()}/${astubxPath}" From d1fed9339a7e76920b5fd023f3cc41b0aad2e053 Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Thu, 7 Mar 2024 00:59:38 -0800 Subject: [PATCH 26/50] create directory fix --- .../libmodel/LibraryModelGenerator.java | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java b/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java index 3530de6f69..4e5a61f3b5 100644 --- a/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java +++ b/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java @@ -123,15 +123,20 @@ private void writeToAstubx( return; } Map importedAnnotations = - ImmutableMap.builder() - .put("Nonnull", "javax.annotation.Nonnull") - .put("Nullable", "javax.annotation.Nullable") - .build(); + ImmutableMap.of( + "Nonnull", "javax.annotation.Nonnull", + "Nullable", "javax.annotation.Nullable"); Path outputPathInstance = Paths.get(outputPath); - try (DataOutputStream dos = new DataOutputStream(Files.newOutputStream(outputPathInstance))) { + try { Files.createDirectories(outputPathInstance.getParent()); - StubxWriter.write( - dos, importedAnnotations, Collections.emptyMap(), Collections.emptyMap(), methodRecords); + try (DataOutputStream dos = new DataOutputStream(Files.newOutputStream(outputPathInstance))) { + StubxWriter.write( + dos, + importedAnnotations, + Collections.emptyMap(), + Collections.emptyMap(), + methodRecords); + } } catch (IOException e) { throw new RuntimeException(e); } From db993b85d6f38ced822e528f4dca582a78e18357 Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Thu, 7 Mar 2024 14:07:28 -0800 Subject: [PATCH 27/50] Handling ArrayType @Nullable return Scenario and some refactoring --- .../libmodel/LibraryModelGenerator.java | 88 ++++++++++--------- .../nullaway/libmodel/AnnotationExample.java | 10 ++- .../nullaway/libmodel/AnnotationExample.java | 10 ++- .../libmodel/LibModelIntegrationTest.java | 27 ++++++ 4 files changed, 85 insertions(+), 50 deletions(-) diff --git a/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java b/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java index 4e5a61f3b5..4ae96587bd 100644 --- a/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java +++ b/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java @@ -83,7 +83,7 @@ public void generateAstubxForLibraryModels(String inputSourceDirectory, String o } /** - * Parses the source files within the directory using javaparser. + * Parses all the source files within the directory using javaparser. * * @param sourceDirectoryRoot Directory containing annotated java source files. * @return a Map containing the Nullability annotation information from the source files. @@ -191,8 +191,6 @@ public void visit(ClassOrInterfaceDeclaration cid, Void arg) { String classOrInterfaceName = packageName + "." + cid.getNameAsString(); @SuppressWarnings("all") Map nullableTypeBoundsMap = new HashMap<>(); - @SuppressWarnings("all") - Map> classOrInterfaceAnnotationsMap = new HashMap<>(); List paramList = cid.getTypeParameters(); // Finding Nullable upper bounds for generic type parameters. for (int i = 0; i < paramList.size(); i++) { @@ -208,21 +206,11 @@ public void visit(ClassOrInterfaceDeclaration cid, Void arg) { nullableTypeBoundsMap.put(i, classOrInterfaceName); } } - // Finding All the annotations on the class or interface. - if (cid.getAnnotations().isNonEmpty()) { - classOrInterfaceAnnotationsMap.put(classOrInterfaceName, cid.getAnnotations()); - } - /*System.out.println("Fully qualified class name: " + classOrInterfaceName); - nullableTypeBoundsMap.forEach( - (p, a) -> System.out.println("Nullable Index: " + p + "\tClass: " + a)); - classOrInterfaceAnnotationsMap.forEach( - (c, a) -> System.out.println("Class: " + c + "\tAnnotations: " + a));*/ super.visit(cid, null); } @Override public void visit(MethodDeclaration md, Void arg) { - String methodName = md.getNameAsString(); Optional parentClassNode = md.getParentNode(); String parentClassName = ""; if (parentClassNode.isPresent()) { @@ -232,42 +220,58 @@ public void visit(MethodDeclaration md, Void arg) { parentClassName = ((EnumDeclaration) parentClassNode.get()).getNameAsString(); } } - String methodSignature = md.getSignature().toString(); - @SuppressWarnings("all") - Map> methodAnnotationsMap = new HashMap<>(); - Map nullableReturnMethods = new HashMap<>(); - boolean isNullableAnnotationPresent = false; - if (md.getAnnotations().isNonEmpty()) { - methodAnnotationsMap.put(methodName, md.getAnnotations()); + if (hasNullableReturn(md)) { + methodRecords.put( + packageName + + "." + + parentClassName + + ":" + + getMethodReturnTypeString(md) + + " " + + md.getSignature().toString(), + MethodAnnotationsRecord.create(ImmutableSet.of("Nullable"), ImmutableMap.of())); } - /*methodAnnotationsMap.forEach( - (m, a) -> System.out.println("Method: " + m + "\tAnnotations: " + a));*/ - for (AnnotationExpr annotation : md.getAnnotations()) { - if (annotation.getNameAsString().equalsIgnoreCase("Nullable")) { - isNullableAnnotationPresent = true; - break; + super.visit(md, null); + } + + /** + * Takes a MethodDeclaration and Tells us whether it can return null. + * + * @param md MethodDeclaration. + * @return Whether the method can return null. + */ + private boolean hasNullableReturn(MethodDeclaration md) { + if (md.getType() instanceof ArrayType) { + for (AnnotationExpr annotation : md.getType().getAnnotations()) { + if (annotation.getNameAsString().equalsIgnoreCase("Nullable")) { + return true; + } + } + } else { + for (AnnotationExpr annotation : md.getAnnotations()) { + if (annotation.getNameAsString().equalsIgnoreCase("Nullable")) { + return true; + } } } - String methodReturnType = ""; + return false; + } + + /** + * Takes a MethodDeclaration and returns the String value for the return type that will be + * written into the astubx file. + * + * @param md MethodDeclaration. + * @return The return type string value to be written into the astubx file. + */ + private String getMethodReturnTypeString(MethodDeclaration md) { if (md.getType() instanceof ClassOrInterfaceType) { - methodReturnType = md.getType().getChildNodes().get(0).toString(); + return md.getType().getChildNodes().get(0).toString(); } else if (md.getType() instanceof ArrayType) { - // methodReturnType = "Array"; - // TODO: Figure out the right way to handle Array types - // For now we don't consider it as Nullable - isNullableAnnotationPresent = false; + return "Array"; } else { - methodReturnType = md.getType().toString(); + return md.getType().toString(); } - if (isNullableAnnotationPresent) { - nullableReturnMethods.put(packageName + "." + parentClassName, methodSignature); - methodRecords.put( - packageName + "." + parentClassName + ":" + methodReturnType + " " + methodSignature, - MethodAnnotationsRecord.create(ImmutableSet.of("Nullable"), ImmutableMap.of())); - } - nullableReturnMethods.forEach( - (c, s) -> System.out.println("Enclosing Class: " + c + "\tMethod Signature: " + s)); - super.visit(md, null); } } } diff --git a/library-model/library-model-generator-integration-test/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java b/library-model/library-model-generator-integration-test/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java index 43d283368e..279c98bc40 100644 --- a/library-model/library-model-generator-integration-test/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java +++ b/library-model/library-model-generator-integration-test/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java @@ -11,12 +11,14 @@ public String makeUpperCase(String inputString) { } } - public String[] makeUpperCaseArray(String inputString) { - if (inputString == null || inputString.isEmpty()) { + public Integer[] generateIntArray(int size) { + if (size <= 0) { return null; } else { - String[] result = new String[1]; - result[0] = inputString.toUpperCase(Locale.ROOT); + Integer[] result = new Integer[size]; + for (int i = 0; i < size; i++) { + result[i] = i + 1; + } return result; } } diff --git a/library-model/library-model-generator-integration-test/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java b/library-model/library-model-generator-integration-test/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java index 193ea0c1a2..8f677fe6ba 100644 --- a/library-model/library-model-generator-integration-test/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java +++ b/library-model/library-model-generator-integration-test/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java @@ -15,12 +15,14 @@ public String makeUpperCase(String inputString) { } } - public String @Nullable [] makeUpperCaseArray(String inputString) { - if (inputString == null || inputString.isEmpty()) { + public Integer @Nullable [] generateIntArray(int size) { + if (size <= 0) { return null; } else { - String[] result = new String[1]; - result[0] = inputString.toUpperCase(); + Integer[] result = new Integer[size]; + for (int i = 0; i < size; i++) { + result[i] = i + 1; + } return result; } } diff --git a/library-model/library-model-generator/src/test/java/com/uber/nullaway/libmodel/LibModelIntegrationTest.java b/library-model/library-model-generator/src/test/java/com/uber/nullaway/libmodel/LibModelIntegrationTest.java index d03b8c76a0..35eea6ad72 100644 --- a/library-model/library-model-generator/src/test/java/com/uber/nullaway/libmodel/LibModelIntegrationTest.java +++ b/library-model/library-model-generator/src/test/java/com/uber/nullaway/libmodel/LibModelIntegrationTest.java @@ -45,4 +45,31 @@ public void libModelNullableReturnsTest() { "}") .doTest(); } + + @Test + public void libModelNullableReturnsArrayTest() { + compilationHelper + .setArgs( + Arrays.asList( + "-d", + temporaryFolder.getRoot().getAbsolutePath(), + "-XepOpt:NullAway:AnnotatedPackages=com.uber", + "-XepOpt:NullAway:JarInferEnabled=true", + "-XepOpt:NullAway:JarInferUseReturnAnnotations=true")) + .addSourceLines( + "Test.java", + "package com.uber;", + "import javax.annotation.Nullable;", + "import com.uber.nullaway.libmodel.AnnotationExample;", + "class Test {", + " static AnnotationExample annotationExample = new AnnotationExample();", + " static void test(Integer[] value){", + " }", + " static void test1() {", + " // BUG: Diagnostic contains: passing @Nullable parameter 'annotationExample.generateIntArray(7)'", + " test(annotationExample.generateIntArray(7));", + " }", + "}") + .doTest(); + } } From 09c0f4c406d30c7a2851d8d76497fab64ad4f708 Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Thu, 7 Mar 2024 14:19:52 -0800 Subject: [PATCH 28/50] javadoc changes --- .../com/uber/nullaway/libmodel/LibraryModelGenerator.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java b/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java index 4ae96587bd..4708d5c965 100644 --- a/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java +++ b/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java @@ -235,10 +235,10 @@ public void visit(MethodDeclaration md, Void arg) { } /** - * Takes a MethodDeclaration and Tells us whether it can return null. + * Determines if a MethodDeclaration can return null. * - * @param md MethodDeclaration. - * @return Whether the method can return null. + * @param md The MethodDeclaration instance. + * @return {@code true} if the method can return null, {@code false} otherwise. */ private boolean hasNullableReturn(MethodDeclaration md) { if (md.getType() instanceof ArrayType) { @@ -261,7 +261,7 @@ private boolean hasNullableReturn(MethodDeclaration md) { * Takes a MethodDeclaration and returns the String value for the return type that will be * written into the astubx file. * - * @param md MethodDeclaration. + * @param md The MethodDeclaration instance. * @return The return type string value to be written into the astubx file. */ private String getMethodReturnTypeString(MethodDeclaration md) { From 161c6acbc611a84f20e2824c014d102e1d867aa3 Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Thu, 7 Mar 2024 17:28:11 -0800 Subject: [PATCH 29/50] Logic to only consider jspecify Nullable annotation --- .../libmodel/LibraryModelGenerator.java | 24 ++++++++++++++++--- .../nullaway/libmodel/AnnotationExample.java | 4 ++++ .../nullaway/libmodel/AnnotationExample.java | 5 ++++ .../libmodel/LibModelIntegrationTest.java | 7 +++--- 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java b/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java index 4708d5c965..1060205663 100644 --- a/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java +++ b/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java @@ -25,6 +25,7 @@ import com.github.javaparser.ParseResult; import com.github.javaparser.ParserConfiguration.LanguageLevel; import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.ImportDeclaration; import com.github.javaparser.ast.Node; import com.github.javaparser.ast.NodeList; import com.github.javaparser.ast.PackageDeclaration; @@ -174,7 +175,10 @@ public Result process(Path localPath, Path absolutePath, ParseResult { private String packageName = ""; + private boolean isJspecifyNullableImportPresent = false; private Map methodRecords; + private static final String ARRAY_RETURN_TYPE_STRING = "Array"; + private static final String JSPECIFY_NULLABLE_IMPORT = "org.jspecify.annotations.Nullable"; public AnnotationCollectionVisitor(Map methodRecords) { this.methodRecords = methodRecords; @@ -186,6 +190,14 @@ public void visit(PackageDeclaration pd, Void arg) { super.visit(pd, null); } + @Override + public void visit(ImportDeclaration id, Void arg) { + if (id.getName().toString().contains(JSPECIFY_NULLABLE_IMPORT)) { + this.isJspecifyNullableImportPresent = true; + } + super.visit(id, null); + } + @Override public void visit(ClassOrInterfaceDeclaration cid, Void arg) { String classOrInterfaceName = packageName + "." + cid.getNameAsString(); @@ -243,13 +255,13 @@ public void visit(MethodDeclaration md, Void arg) { private boolean hasNullableReturn(MethodDeclaration md) { if (md.getType() instanceof ArrayType) { for (AnnotationExpr annotation : md.getType().getAnnotations()) { - if (annotation.getNameAsString().equalsIgnoreCase("Nullable")) { + if (isAnnotationNullable(annotation)) { return true; } } } else { for (AnnotationExpr annotation : md.getAnnotations()) { - if (annotation.getNameAsString().equalsIgnoreCase("Nullable")) { + if (isAnnotationNullable(annotation)) { return true; } } @@ -268,10 +280,16 @@ private String getMethodReturnTypeString(MethodDeclaration md) { if (md.getType() instanceof ClassOrInterfaceType) { return md.getType().getChildNodes().get(0).toString(); } else if (md.getType() instanceof ArrayType) { - return "Array"; + return ARRAY_RETURN_TYPE_STRING; } else { return md.getType().toString(); } } + + private boolean isAnnotationNullable(AnnotationExpr annotation) { + return (annotation.getNameAsString().equalsIgnoreCase("Nullable") + && this.isJspecifyNullableImportPresent) + || annotation.getNameAsString().equalsIgnoreCase(JSPECIFY_NULLABLE_IMPORT); + } } } diff --git a/library-model/library-model-generator-integration-test/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java b/library-model/library-model-generator-integration-test/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java index 279c98bc40..1aabc2fd04 100644 --- a/library-model/library-model-generator-integration-test/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java +++ b/library-model/library-model-generator-integration-test/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java @@ -22,4 +22,8 @@ public Integer[] generateIntArray(int size) { return result; } } + + public String nullReturn() { + return null; + } } diff --git a/library-model/library-model-generator-integration-test/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java b/library-model/library-model-generator-integration-test/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java index 8f677fe6ba..e7ffcfb963 100644 --- a/library-model/library-model-generator-integration-test/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java +++ b/library-model/library-model-generator-integration-test/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java @@ -26,4 +26,9 @@ public String makeUpperCase(String inputString) { return result; } } + + @javax.annotation.Nullable + public String nullReturn() { + return null; + } } diff --git a/library-model/library-model-generator/src/test/java/com/uber/nullaway/libmodel/LibModelIntegrationTest.java b/library-model/library-model-generator/src/test/java/com/uber/nullaway/libmodel/LibModelIntegrationTest.java index 35eea6ad72..9a6f07b70f 100644 --- a/library-model/library-model-generator/src/test/java/com/uber/nullaway/libmodel/LibModelIntegrationTest.java +++ b/library-model/library-model-generator/src/test/java/com/uber/nullaway/libmodel/LibModelIntegrationTest.java @@ -32,16 +32,18 @@ public void libModelNullableReturnsTest() { .addSourceLines( "Test.java", "package com.uber;", - "import javax.annotation.Nullable;", "import com.uber.nullaway.libmodel.AnnotationExample;", "class Test {", " static AnnotationExample annotationExample = new AnnotationExample();", " static void test(String value){", " }", - " static void test1() {", + " static void testPositive() {", " // BUG: Diagnostic contains: passing @Nullable parameter 'annotationExample.makeUpperCase(\"nullaway\")'", " test(annotationExample.makeUpperCase(\"nullaway\"));", " }", + " static void testNegative() {", + " test(annotationExample.nullReturn());", + " }", "}") .doTest(); } @@ -59,7 +61,6 @@ public void libModelNullableReturnsArrayTest() { .addSourceLines( "Test.java", "package com.uber;", - "import javax.annotation.Nullable;", "import com.uber.nullaway.libmodel.AnnotationExample;", "class Test {", " static AnnotationExample annotationExample = new AnnotationExample();", From 27ebcc9e454976f9ffc23bfc802229e3b720cce8 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Fri, 8 Mar 2024 11:09:04 -0800 Subject: [PATCH 30/50] tweak comment --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 0144abcb8e..4b9fef8d4b 100644 --- a/build.gradle +++ b/build.gradle @@ -95,8 +95,8 @@ subprojects { project -> google() } - // For some reason, spotless complains when applied to the jar-infer folder itself, even - // though there is no top-level :jar-infer project + // For some reason, spotless complains when applied to the folders containing projects + // when they do not have a build.gradle file if (project.name != "jar-infer" && project.name != "library-model") { project.apply plugin: "com.diffplug.spotless" spotless { From 5ee2428c483c84e044daea974407f6d7c43b82f6 Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Fri, 8 Mar 2024 15:55:45 -0800 Subject: [PATCH 31/50] Refactoring and documentation --- .../library-model-generator-cli/build.gradle | 4 +- .../libmodel/LibraryModelGeneratorCLI.java | 42 +++++++++++++++++++ .../build.gradle | 8 ++-- .../nullaway/libmodel/AnnotationExample.java | 6 +++ .../nullaway/libmodel/AnnotationExample.java | 2 + .../library-model-generator/build.gradle | 1 + .../libmodel/LibraryModelGenerator.java | 19 +++------ 7 files changed, 61 insertions(+), 21 deletions(-) create mode 100644 library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGeneratorCLI.java rename library-model/{library-model-generator-cli => library-model-generator}/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java (95%) diff --git a/library-model/library-model-generator-cli/build.gradle b/library-model/library-model-generator-cli/build.gradle index e5caa913be..dcc2eaff8e 100644 --- a/library-model/library-model-generator-cli/build.gradle +++ b/library-model/library-model-generator-cli/build.gradle @@ -20,7 +20,7 @@ plugins { jar{ manifest { - attributes('Main-Class':'com.uber.nullaway.libmodel.LibraryModelGenerator') + attributes('Main-Class':'com.uber.nullaway.libmodel.LibraryModelGeneratorCLI') } // add this classifier so that the output file for the jar task differs from // the output file for the shadowJar task (otherwise they overwrite each other's @@ -40,6 +40,4 @@ assemble.dependsOn shadowJar dependencies { implementation project(":library-model:library-model-generator") - implementation deps.build.guava - implementation deps.build.javaparser } diff --git a/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGeneratorCLI.java b/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGeneratorCLI.java new file mode 100644 index 0000000000..c2fa6086b7 --- /dev/null +++ b/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGeneratorCLI.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2019 Uber Technologies, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.uber.nullaway.libmodel; + +/** + * A CLI tool for invoking the process for {@link LibraryModelGenerator} which generates astubx + * file(s) from a directory containing annotated source code to be used as external library models. + */ +public class LibraryModelGeneratorCLI { + /** + * This is the main method of the cli tool. It parses the source files within a specified + * directory, obtains meaningful Nullability annotation information and writes it into an astubx + * file. + * + * @param args Command line arguments for the directory containing source files and the output + * directory. + */ + public static void main(String[] args) { + LibraryModelGenerator libraryModelGenerator = new LibraryModelGenerator(); + libraryModelGenerator.generateAstubxForLibraryModels(args[0], args[1]); + } +} diff --git a/library-model/library-model-generator-integration-test/build.gradle b/library-model/library-model-generator-integration-test/build.gradle index f618ae7cc2..184cb6047c 100644 --- a/library-model/library-model-generator-integration-test/build.gradle +++ b/library-model/library-model-generator-integration-test/build.gradle @@ -18,9 +18,7 @@ plugins { id "java-library" } -evaluationDependsOn(":library-model:library-model-generator") - -def jdkPath = "${rootProject.projectDir}/library-model/library-model-generator-integration-test/src/main/resources/sample_annotated/src" +def testInputsPath = "${rootProject.projectDir}/library-model/library-model-generator-integration-test/src/main/resources/sample_annotated/src" def astubxPath = "com/uber/nullaway/libmodel/provider/libmodels.astubx" jar { @@ -36,9 +34,9 @@ jar { jar.doLast { javaexec { classpath = files("${rootProject.projectDir}/library-model/library-model-generator-cli/build/libs/library-model-generator-cli.jar") - main = "com.uber.nullaway.libmodel.LibraryModelGenerator" + main = "com.uber.nullaway.libmodel.LibraryModelGeneratorCLI" args = [ - jdkPath, + testInputsPath, "${jar.destinationDirectory.get()}/${astubxPath}" ] } diff --git a/library-model/library-model-generator-integration-test/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java b/library-model/library-model-generator-integration-test/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java index 1aabc2fd04..3fe23e73bb 100644 --- a/library-model/library-model-generator-integration-test/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java +++ b/library-model/library-model-generator-integration-test/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java @@ -2,6 +2,12 @@ import java.util.Locale; +/** + * This class has the same name as the class under + * resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java because we use + * this as the unannotated version for our test cases to see if we are appropriately processing the + * annotations as an external library model. + */ public class AnnotationExample { public String makeUpperCase(String inputString) { if (inputString == null || inputString.isEmpty()) { diff --git a/library-model/library-model-generator-integration-test/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java b/library-model/library-model-generator-integration-test/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java index e7ffcfb963..6d7036c103 100644 --- a/library-model/library-model-generator-integration-test/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java +++ b/library-model/library-model-generator-integration-test/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java @@ -27,6 +27,8 @@ public String makeUpperCase(String inputString) { } } + + //This is to test that the Nullable annotation here does not get processed since it is not JSpecify. @javax.annotation.Nullable public String nullReturn() { return null; diff --git a/library-model/library-model-generator/build.gradle b/library-model/library-model-generator/build.gradle index 31746e5311..c36dbf29f2 100644 --- a/library-model/library-model-generator/build.gradle +++ b/library-model/library-model-generator/build.gradle @@ -26,6 +26,7 @@ dependencies { testImplementation project(":nullaway") testImplementation project(":library-model:library-model-generator-integration-test") implementation deps.build.guava + implementation deps.build.javaparser compileOnly deps.apt.autoValueAnnot annotationProcessor deps.apt.autoValue } diff --git a/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java b/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java similarity index 95% rename from library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java rename to library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java index 1060205663..9e7fba95cd 100644 --- a/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java +++ b/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java @@ -65,19 +65,6 @@ */ public class LibraryModelGenerator { - /** - * This is the main method of the cli tool. It parses the source files within a specified - * directory, obtains meaningful Nullability annotation information and writes it into an astubx - * file. - * - * @param args Command line arguments for the directory containing source files and the output - * directory. - */ - public static void main(String[] args) { - LibraryModelGenerator libraryModelGenerator = new LibraryModelGenerator(); - libraryModelGenerator.generateAstubxForLibraryModels(args[0], args[1]); - } - public void generateAstubxForLibraryModels(String inputSourceDirectory, String outputDirectory) { Map methodRecords = processDirectory(inputSourceDirectory); writeToAstubx(outputDirectory, methodRecords); @@ -243,6 +230,8 @@ public void visit(MethodDeclaration md, Void arg) { + md.getSignature().toString(), MethodAnnotationsRecord.create(ImmutableSet.of("Nullable"), ImmutableMap.of())); } + for (int i = 0; i < md.getParameters().size(); i++) {} + super.visit(md, null); } @@ -254,6 +243,9 @@ public void visit(MethodDeclaration md, Void arg) { */ private boolean hasNullableReturn(MethodDeclaration md) { if (md.getType() instanceof ArrayType) { + /* For an Array return type the annotation is on the type when the Array instance is + Nullable(Object @Nullable []) and on the node when the elements inside are + Nullable(@Nullable Object []) */ for (AnnotationExpr annotation : md.getType().getAnnotations()) { if (isAnnotationNullable(annotation)) { return true; @@ -287,6 +279,7 @@ private String getMethodReturnTypeString(MethodDeclaration md) { } private boolean isAnnotationNullable(AnnotationExpr annotation) { + // Since we only want to consider jspecify Nullable annotations. return (annotation.getNameAsString().equalsIgnoreCase("Nullable") && this.isJspecifyNullableImportPresent) || annotation.getNameAsString().equalsIgnoreCase(JSPECIFY_NULLABLE_IMPORT); From 8c442f7e02c12cd1989de3799852f0432ffc650d Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Sat, 9 Mar 2024 13:54:13 -0800 Subject: [PATCH 32/50] removing comment --- .../src/com/uber/nullaway/libmodel/AnnotationExample.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/library-model/library-model-generator-integration-test/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java b/library-model/library-model-generator-integration-test/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java index 6d7036c103..e7ffcfb963 100644 --- a/library-model/library-model-generator-integration-test/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java +++ b/library-model/library-model-generator-integration-test/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java @@ -27,8 +27,6 @@ public String makeUpperCase(String inputString) { } } - - //This is to test that the Nullable annotation here does not get processed since it is not JSpecify. @javax.annotation.Nullable public String nullReturn() { return null; From 609f2e5bb1a8b116edd890edcf6b0349f6082223 Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Sat, 9 Mar 2024 14:09:08 -0800 Subject: [PATCH 33/50] cleanup --- .../java/com/uber/nullaway/libmodel/LibraryModelGenerator.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java b/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java index 9e7fba95cd..26ad50780e 100644 --- a/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java +++ b/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java @@ -230,8 +230,6 @@ public void visit(MethodDeclaration md, Void arg) { + md.getSignature().toString(), MethodAnnotationsRecord.create(ImmutableSet.of("Nullable"), ImmutableMap.of())); } - for (int i = 0; i < md.getParameters().size(); i++) {} - super.visit(md, null); } From fb83aff2eb627ef049903d51176846b943dea340 Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Sun, 10 Mar 2024 16:06:02 -0700 Subject: [PATCH 34/50] Adding NullMarked flag and some clean up --- .../libmodel/LibraryModelGenerator.java | 36 +++++++------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java b/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java index 26ad50780e..deb853999c 100644 --- a/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java +++ b/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java @@ -27,7 +27,6 @@ import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.ImportDeclaration; import com.github.javaparser.ast.Node; -import com.github.javaparser.ast.NodeList; import com.github.javaparser.ast.PackageDeclaration; import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; import com.github.javaparser.ast.body.EnumDeclaration; @@ -35,7 +34,6 @@ import com.github.javaparser.ast.expr.AnnotationExpr; import com.github.javaparser.ast.type.ArrayType; import com.github.javaparser.ast.type.ClassOrInterfaceType; -import com.github.javaparser.ast.type.TypeParameter; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; import com.github.javaparser.utils.CollectionStrategy; import com.github.javaparser.utils.ParserCollectionStrategy; @@ -50,9 +48,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collections; -import java.util.HashMap; import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; import java.util.Optional; @@ -163,8 +159,11 @@ private static class AnnotationCollectionVisitor extends VoidVisitorAdapter methodRecords; private static final String ARRAY_RETURN_TYPE_STRING = "Array"; + private static final String NULL_MARKED = "NullMarked"; + private static final String NULLABLE = "Nullable"; private static final String JSPECIFY_NULLABLE_IMPORT = "org.jspecify.annotations.Nullable"; public AnnotationCollectionVisitor(Map methodRecords) { @@ -187,24 +186,13 @@ public void visit(ImportDeclaration id, Void arg) { @Override public void visit(ClassOrInterfaceDeclaration cid, Void arg) { - String classOrInterfaceName = packageName + "." + cid.getNameAsString(); - @SuppressWarnings("all") - Map nullableTypeBoundsMap = new HashMap<>(); - List paramList = cid.getTypeParameters(); - // Finding Nullable upper bounds for generic type parameters. - for (int i = 0; i < paramList.size(); i++) { - boolean hasNullableUpperBound = false; - NodeList upperBoundList = paramList.get(i).getTypeBound(); - for (ClassOrInterfaceType upperBound : upperBoundList) { - if (upperBound.isAnnotationPresent("Nullable")) { - hasNullableUpperBound = true; - break; - } - } - if (hasNullableUpperBound) { - nullableTypeBoundsMap.put(i, classOrInterfaceName); - } - } + cid.getAnnotations() + .forEach( + a -> { + if (a.getNameAsString().equalsIgnoreCase(NULL_MARKED)) { + this.isNullMarked = true; + } + }); super.visit(cid, null); } @@ -219,7 +207,7 @@ public void visit(MethodDeclaration md, Void arg) { parentClassName = ((EnumDeclaration) parentClassNode.get()).getNameAsString(); } } - if (hasNullableReturn(md)) { + if (this.isNullMarked && hasNullableReturn(md)) { methodRecords.put( packageName + "." @@ -278,7 +266,7 @@ private String getMethodReturnTypeString(MethodDeclaration md) { private boolean isAnnotationNullable(AnnotationExpr annotation) { // Since we only want to consider jspecify Nullable annotations. - return (annotation.getNameAsString().equalsIgnoreCase("Nullable") + return (annotation.getNameAsString().equalsIgnoreCase(NULLABLE) && this.isJspecifyNullableImportPresent) || annotation.getNameAsString().equalsIgnoreCase(JSPECIFY_NULLABLE_IMPORT); } From 629ec5b79f20efc4e62b83ce73da32989a49e840 Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Sun, 10 Mar 2024 16:10:58 -0700 Subject: [PATCH 35/50] adding doc --- .../src/com/uber/nullaway/libmodel/AnnotationExample.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/library-model/library-model-generator-integration-test/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java b/library-model/library-model-generator-integration-test/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java index e7ffcfb963..5bcfcad4ff 100644 --- a/library-model/library-model-generator-integration-test/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java +++ b/library-model/library-model-generator-integration-test/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java @@ -27,6 +27,10 @@ public String makeUpperCase(String inputString) { } } + /** + * This method exists to test that + * we do not process this annotation. + */ @javax.annotation.Nullable public String nullReturn() { return null; From 51905970c99883ee2e064d23386925d5e00f1f55 Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Wed, 13 Mar 2024 11:25:32 -0700 Subject: [PATCH 36/50] refactoring --- ....java => LibraryModelIntegrationTest.java} | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) rename library-model/library-model-generator/src/test/java/com/uber/nullaway/libmodel/{LibModelIntegrationTest.java => LibraryModelIntegrationTest.java} (71%) diff --git a/library-model/library-model-generator/src/test/java/com/uber/nullaway/libmodel/LibModelIntegrationTest.java b/library-model/library-model-generator/src/test/java/com/uber/nullaway/libmodel/LibraryModelIntegrationTest.java similarity index 71% rename from library-model/library-model-generator/src/test/java/com/uber/nullaway/libmodel/LibModelIntegrationTest.java rename to library-model/library-model-generator/src/test/java/com/uber/nullaway/libmodel/LibraryModelIntegrationTest.java index 9a6f07b70f..5a73272e00 100644 --- a/library-model/library-model-generator/src/test/java/com/uber/nullaway/libmodel/LibModelIntegrationTest.java +++ b/library-model/library-model-generator/src/test/java/com/uber/nullaway/libmodel/LibraryModelIntegrationTest.java @@ -8,7 +8,7 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; -public class LibModelIntegrationTest { +public class LibraryModelIntegrationTest { @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @@ -20,7 +20,7 @@ public void setup() { } @Test - public void libModelNullableReturnsTest() { + public void libraryModelNullableReturnsTest() { compilationHelper .setArgs( Arrays.asList( @@ -49,7 +49,7 @@ public void libModelNullableReturnsTest() { } @Test - public void libModelNullableReturnsArrayTest() { + public void libraryModelNullableReturnsArrayTest() { compilationHelper .setArgs( Arrays.asList( @@ -66,11 +66,34 @@ public void libModelNullableReturnsArrayTest() { " static AnnotationExample annotationExample = new AnnotationExample();", " static void test(Integer[] value){", " }", - " static void test1() {", + " static void testPositive() {", " // BUG: Diagnostic contains: passing @Nullable parameter 'annotationExample.generateIntArray(7)'", " test(annotationExample.generateIntArray(7));", " }", "}") .doTest(); } + + @Test + public void libModelNullableParamTest() { + compilationHelper + .setArgs( + Arrays.asList( + "-d", + temporaryFolder.getRoot().getAbsolutePath(), + "-XepOpt:NullAway:AnnotatedPackages=com.uber", + "-XepOpt:NullAway:JarInferEnabled=true", + "-XepOpt:NullAway:JarInferUseReturnAnnotations=true")) + .addSourceLines( + "Test.java", + "package com.uber;", + "import com.uber.nullaway.libmodel.AnnotationExample;", + "class Test {", + " static AnnotationExample annotationExample = new AnnotationExample();", + " static void testNegative() {", + " annotationExample.nullDereference(null);", + " }", + "}") + .doTest(); + } } From fd7d546f7532e0e24fa344587c380bd80885f487 Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Wed, 13 Mar 2024 11:27:23 -0700 Subject: [PATCH 37/50] Removing unused test case --- .../libmodel/LibraryModelIntegrationTest.java | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/library-model/library-model-generator/src/test/java/com/uber/nullaway/libmodel/LibraryModelIntegrationTest.java b/library-model/library-model-generator/src/test/java/com/uber/nullaway/libmodel/LibraryModelIntegrationTest.java index 5a73272e00..2cba401fd9 100644 --- a/library-model/library-model-generator/src/test/java/com/uber/nullaway/libmodel/LibraryModelIntegrationTest.java +++ b/library-model/library-model-generator/src/test/java/com/uber/nullaway/libmodel/LibraryModelIntegrationTest.java @@ -73,27 +73,4 @@ public void libraryModelNullableReturnsArrayTest() { "}") .doTest(); } - - @Test - public void libModelNullableParamTest() { - compilationHelper - .setArgs( - Arrays.asList( - "-d", - temporaryFolder.getRoot().getAbsolutePath(), - "-XepOpt:NullAway:AnnotatedPackages=com.uber", - "-XepOpt:NullAway:JarInferEnabled=true", - "-XepOpt:NullAway:JarInferUseReturnAnnotations=true")) - .addSourceLines( - "Test.java", - "package com.uber;", - "import com.uber.nullaway.libmodel.AnnotationExample;", - "class Test {", - " static AnnotationExample annotationExample = new AnnotationExample();", - " static void testNegative() {", - " annotationExample.nullDereference(null);", - " }", - "}") - .doTest(); - } } From f213d82200d00ee3547c31a94c4d62c98ffd0a81 Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Wed, 13 Mar 2024 14:44:56 -0700 Subject: [PATCH 38/50] Refactoring test setup for library-model-generator --- .../nullaway/libmodel/LibraryModelIntegrationTest.java | 0 library-model/library-model-generator/build.gradle | 7 ------- .../java/com/uber/nullaway/libmodel/AnnotationExample.java | 0 .../com/uber/nullaway/libmodel/provider/TestProvider.java | 0 .../src/com/uber/nullaway/libmodel/AnnotationExample.java | 0 settings.gradle | 3 ++- 6 files changed, 2 insertions(+), 8 deletions(-) rename library-model/{library-model-generator => library-model-generator-integration-test}/src/test/java/com/uber/nullaway/libmodel/LibraryModelIntegrationTest.java (100%) rename library-model/{library-model-generator-integration-test => test-library-model-generator}/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java (100%) rename library-model/{library-model-generator-integration-test => test-library-model-generator}/src/main/java/com/uber/nullaway/libmodel/provider/TestProvider.java (100%) rename library-model/{library-model-generator-integration-test => test-library-model-generator}/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java (100%) diff --git a/library-model/library-model-generator/src/test/java/com/uber/nullaway/libmodel/LibraryModelIntegrationTest.java b/library-model/library-model-generator-integration-test/src/test/java/com/uber/nullaway/libmodel/LibraryModelIntegrationTest.java similarity index 100% rename from library-model/library-model-generator/src/test/java/com/uber/nullaway/libmodel/LibraryModelIntegrationTest.java rename to library-model/library-model-generator-integration-test/src/test/java/com/uber/nullaway/libmodel/LibraryModelIntegrationTest.java diff --git a/library-model/library-model-generator/build.gradle b/library-model/library-model-generator/build.gradle index c36dbf29f2..f662aa6b7c 100644 --- a/library-model/library-model-generator/build.gradle +++ b/library-model/library-model-generator/build.gradle @@ -15,16 +15,9 @@ */ plugins { id "java-library" - id "nullaway.java-test-conventions" } dependencies { - testImplementation deps.test.junit4 - testImplementation(deps.build.errorProneTestHelpers) { - exclude group: "junit", module: "junit" - } - testImplementation project(":nullaway") - testImplementation project(":library-model:library-model-generator-integration-test") implementation deps.build.guava implementation deps.build.javaparser compileOnly deps.apt.autoValueAnnot diff --git a/library-model/library-model-generator-integration-test/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java b/library-model/test-library-model-generator/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java similarity index 100% rename from library-model/library-model-generator-integration-test/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java rename to library-model/test-library-model-generator/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java diff --git a/library-model/library-model-generator-integration-test/src/main/java/com/uber/nullaway/libmodel/provider/TestProvider.java b/library-model/test-library-model-generator/src/main/java/com/uber/nullaway/libmodel/provider/TestProvider.java similarity index 100% rename from library-model/library-model-generator-integration-test/src/main/java/com/uber/nullaway/libmodel/provider/TestProvider.java rename to library-model/test-library-model-generator/src/main/java/com/uber/nullaway/libmodel/provider/TestProvider.java diff --git a/library-model/library-model-generator-integration-test/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java b/library-model/test-library-model-generator/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java similarity index 100% rename from library-model/library-model-generator-integration-test/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java rename to library-model/test-library-model-generator/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java diff --git a/settings.gradle b/settings.gradle index b0364e9a1f..95c4141ddc 100644 --- a/settings.gradle +++ b/settings.gradle @@ -34,4 +34,5 @@ include ':sample-app' include ':jar-infer:test-android-lib-jarinfer' include ':library-model:library-model-generator' include ':library-model:library-model-generator-integration-test' -include 'library-model:library-model-generator-cli' +include ':library-model:library-model-generator-cli' +include ':library-model:test-library-model-generator' From 1137aa93285c7d56dbc6c2de462cd6ded975aefc Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Wed, 13 Mar 2024 14:45:46 -0700 Subject: [PATCH 39/50] Refactoring test setup --- .../build.gradle | 46 ++++----------- .../test-library-model-generator/build.gradle | 56 +++++++++++++++++++ 2 files changed, 67 insertions(+), 35 deletions(-) create mode 100644 library-model/test-library-model-generator/build.gradle diff --git a/library-model/library-model-generator-integration-test/build.gradle b/library-model/library-model-generator-integration-test/build.gradle index 184cb6047c..c425f283f2 100644 --- a/library-model/library-model-generator-integration-test/build.gradle +++ b/library-model/library-model-generator-integration-test/build.gradle @@ -13,44 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - plugins { id "java-library" -} - -def testInputsPath = "${rootProject.projectDir}/library-model/library-model-generator-integration-test/src/main/resources/sample_annotated/src" -def astubxPath = "com/uber/nullaway/libmodel/provider/libmodels.astubx" - -jar { - manifest { - attributes( - 'Created-By' : "Gradle ${gradle.gradleVersion}", - 'Build-Jdk' : "${System.properties['java.version']} (${System.properties['java.vendor']} ${System.properties['java.vm.version']})", - 'Build-OS' : "${System.properties['os.name']} ${System.properties['os.arch']} ${System.properties['os.version']}" - ) - } -} - -jar.doLast { - javaexec { - classpath = files("${rootProject.projectDir}/library-model/library-model-generator-cli/build/libs/library-model-generator-cli.jar") - main = "com.uber.nullaway.libmodel.LibraryModelGeneratorCLI" - args = [ - testInputsPath, - "${jar.destinationDirectory.get()}/${astubxPath}" - ] - } - exec { - workingDir "./build/libs" - commandLine "jar", "uf", "library-model-generator-integration-test.jar", astubxPath - } + id "nullaway.java-test-conventions" } dependencies { - compileOnly deps.apt.autoService - annotationProcessor deps.apt.autoService - compileOnly project(":nullaway") - implementation deps.build.jsr305Annotations + testImplementation project(":nullaway") + testImplementation project(":library-model:test-library-model-generator") + testImplementation deps.test.junit4 + testImplementation(deps.build.errorProneTestHelpers) { + exclude group: "junit", module: "junit" + } + implementation deps.build.guava + implementation deps.build.javaparser + compileOnly deps.apt.autoValueAnnot + annotationProcessor deps.apt.autoValue } - -jar.dependsOn ":library-model:library-model-generator-cli:assemble" diff --git a/library-model/test-library-model-generator/build.gradle b/library-model/test-library-model-generator/build.gradle new file mode 100644 index 0000000000..3a77dd1bd4 --- /dev/null +++ b/library-model/test-library-model-generator/build.gradle @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2017. Uber Technologies + * + * 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. + */ + +plugins { + id "java-library" +} + +def testInputsPath = "${rootProject.projectDir}/library-model/test-library-model-generator/src/main/resources/sample_annotated/src" +def astubxPath = "com/uber/nullaway/libmodel/provider/libmodels.astubx" + +jar { + manifest { + attributes( + 'Created-By' : "Gradle ${gradle.gradleVersion}", + 'Build-Jdk' : "${System.properties['java.version']} (${System.properties['java.vendor']} ${System.properties['java.vm.version']})", + 'Build-OS' : "${System.properties['os.name']} ${System.properties['os.arch']} ${System.properties['os.version']}" + ) + } +} + +jar.doLast { + javaexec { + classpath = files("${rootProject.projectDir}/library-model/library-model-generator-cli/build/libs/library-model-generator-cli.jar") + main = "com.uber.nullaway.libmodel.LibraryModelGeneratorCLI" + args = [ + testInputsPath, + "${jar.destinationDirectory.get()}/${astubxPath}" + ] + } + exec { + workingDir "./build/libs" + commandLine "jar", "uf", "test-library-model-generator.jar", astubxPath + } +} + +dependencies { + compileOnly deps.apt.autoService + annotationProcessor deps.apt.autoService + compileOnly project(":nullaway") + implementation deps.build.jsr305Annotations +} + +jar.dependsOn ":library-model:library-model-generator-cli:assemble" From 41bc90fa8351d62b029c1f459c4e5372bdeb7833 Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Thu, 14 Mar 2024 14:40:15 -0700 Subject: [PATCH 40/50] adding comment for NullMarked functionality --- .../java/com/uber/nullaway/libmodel/LibraryModelGenerator.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java b/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java index deb853999c..b70ceda5f1 100644 --- a/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java +++ b/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java @@ -186,6 +186,8 @@ public void visit(ImportDeclaration id, Void arg) { @Override public void visit(ClassOrInterfaceDeclaration cid, Void arg) { + /*Currently, this logic only supports @NullMarked annotations on top-level classes and + does not handle cases where @NullMarked appears on some nested classes but not others*/ cid.getAnnotations() .forEach( a -> { From 26c4907e9a911c337440b4a3607864757e4c4a4e Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Fri, 22 Mar 2024 03:24:31 -0700 Subject: [PATCH 41/50] adding simple usage message --- .../com/uber/nullaway/libmodel/LibraryModelGeneratorCLI.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGeneratorCLI.java b/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGeneratorCLI.java index c2fa6086b7..f56130106f 100644 --- a/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGeneratorCLI.java +++ b/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGeneratorCLI.java @@ -36,6 +36,11 @@ public class LibraryModelGeneratorCLI { * directory. */ public static void main(String[] args) { + if (args.length != 2) { + System.out.println( + "Incorrect number of command line arguments. Required arguments: "); + return; + } LibraryModelGenerator libraryModelGenerator = new LibraryModelGenerator(); libraryModelGenerator.generateAstubxForLibraryModels(args[0], args[1]); } From dc0618a90076ec5c66d436a409642c000e044290 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sat, 23 Mar 2024 15:43:42 -0700 Subject: [PATCH 42/50] review suggestion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Lázaro Clapp --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 4b9fef8d4b..12f68d6f0a 100644 --- a/build.gradle +++ b/build.gradle @@ -95,7 +95,7 @@ subprojects { project -> google() } - // For some reason, spotless complains when applied to the folders containing projects + // Spotless complains when applied to the folders containing projects // when they do not have a build.gradle file if (project.name != "jar-infer" && project.name != "library-model") { project.apply plugin: "com.diffplug.spotless" From 5fa73da5e690853440cce81decbe58d17fa3c497 Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Sat, 23 Mar 2024 16:55:54 -0700 Subject: [PATCH 43/50] adding condition to consider jspecify annotation in InferredJARModelsHandler.java and updating some comments --- .../uber/nullaway/libmodel/LibraryModelGenerator.java | 11 +++++++---- .../com/uber/nullaway/libmodel/AnnotationExample.java | 2 ++ .../nullaway/handlers/InferredJARModelsHandler.java | 3 ++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java b/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java index b70ceda5f1..f6705ead67 100644 --- a/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java +++ b/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java @@ -108,8 +108,8 @@ private void writeToAstubx( } Map importedAnnotations = ImmutableMap.of( - "Nonnull", "javax.annotation.Nonnull", - "Nullable", "javax.annotation.Nullable"); + "NonNull", "org.jspecify.annotations.NonNull", + "Nullable", "org.jspecify.annotations.Nullable"); Path outputPathInstance = Paths.get(outputPath); try { Files.createDirectories(outputPathInstance.getParent()); @@ -186,8 +186,11 @@ public void visit(ImportDeclaration id, Void arg) { @Override public void visit(ClassOrInterfaceDeclaration cid, Void arg) { - /*Currently, this logic only supports @NullMarked annotations on top-level classes and - does not handle cases where @NullMarked appears on some nested classes but not others*/ + /*This logic assumes an explicit @NullMarked annotation on the top-level class within a + source file, and it's expected that each source file contains only one top-level class. The + logic does not currently handle cases where @NullMarked annotations appear on some nested + classes but not others. It also does not consider annotations within package-info.java or + module-info.java files at this time.*/ cid.getAnnotations() .forEach( a -> { diff --git a/library-model/test-library-model-generator/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java b/library-model/test-library-model-generator/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java index 5bcfcad4ff..89578c5426 100644 --- a/library-model/test-library-model-generator/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java +++ b/library-model/test-library-model-generator/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java @@ -30,6 +30,8 @@ public String makeUpperCase(String inputString) { /** * This method exists to test that * we do not process this annotation. + * Since for the purposes of this tool, + * we are only considering the jspecify annotation. */ @javax.annotation.Nullable public String nullReturn() { diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/InferredJARModelsHandler.java b/nullaway/src/main/java/com/uber/nullaway/handlers/InferredJARModelsHandler.java index bd70059580..495ac46334 100644 --- a/nullaway/src/main/java/com/uber/nullaway/handlers/InferredJARModelsHandler.java +++ b/nullaway/src/main/java/com/uber/nullaway/handlers/InferredJARModelsHandler.java @@ -218,7 +218,8 @@ private boolean isReturnAnnotatedNullable(Symbol.MethodSymbol methodSymbol) { if (methodArgAnnotations != null) { Set methodAnnotations = methodArgAnnotations.get(RETURN); if (methodAnnotations != null) { - if (methodAnnotations.contains("javax.annotation.Nullable")) { + if (methodAnnotations.contains("javax.annotation.Nullable") + || methodAnnotations.contains("org.jspecify.annotations.Nullable")) { LOG(DEBUG, "DEBUG", "Nullable return for method: " + methodSign); return true; } From f390cd1c878e236e7fb2ea8cd0f78bfed465b2b6 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sat, 23 Mar 2024 17:57:05 -0700 Subject: [PATCH 44/50] try to incorporate more code coverage info --- code-coverage-report/build.gradle | 2 ++ library-model/library-model-generator/build.gradle | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/code-coverage-report/build.gradle b/code-coverage-report/build.gradle index 3e5420f298..939178e1fc 100644 --- a/code-coverage-report/build.gradle +++ b/code-coverage-report/build.gradle @@ -80,4 +80,6 @@ dependencies { implementation project(':jar-infer:nullaway-integration-test') implementation project(':guava-recent-unit-tests') implementation project(':jdk-recent-unit-tests') + implementation project(':library-model:library-model-generator') + implementation project(':library-model:library-model-generator-integration-test') } diff --git a/library-model/library-model-generator/build.gradle b/library-model/library-model-generator/build.gradle index f662aa6b7c..0fde5ebda8 100644 --- a/library-model/library-model-generator/build.gradle +++ b/library-model/library-model-generator/build.gradle @@ -14,7 +14,8 @@ * limitations under the License. */ plugins { - id "java-library" + id 'java-library' + id 'nullaway.java-test-conventions' } dependencies { From ad447880ef958440cd0344bfe5b851c23ff56ad6 Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Sat, 23 Mar 2024 18:04:16 -0700 Subject: [PATCH 45/50] Adding a test to check if the functionality is off without the JarInferEnabled and JarInferUseReturnAnnotations flags. --- .../libmodel/LibraryModelIntegrationTest.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/library-model/library-model-generator-integration-test/src/test/java/com/uber/nullaway/libmodel/LibraryModelIntegrationTest.java b/library-model/library-model-generator-integration-test/src/test/java/com/uber/nullaway/libmodel/LibraryModelIntegrationTest.java index 2cba401fd9..de888aa53f 100644 --- a/library-model/library-model-generator-integration-test/src/test/java/com/uber/nullaway/libmodel/LibraryModelIntegrationTest.java +++ b/library-model/library-model-generator-integration-test/src/test/java/com/uber/nullaway/libmodel/LibraryModelIntegrationTest.java @@ -73,4 +73,28 @@ public void libraryModelNullableReturnsArrayTest() { "}") .doTest(); } + + @Test + public void libraryModelWithoutJarInferEnabledTest() { + compilationHelper + .setArgs( + Arrays.asList( + "-d", + temporaryFolder.getRoot().getAbsolutePath(), + "-XepOpt:NullAway:AnnotatedPackages=com.uber")) + .addSourceLines( + "Test.java", + "package com.uber;", + "import com.uber.nullaway.libmodel.AnnotationExample;", + "class Test {", + " static AnnotationExample annotationExample = new AnnotationExample();", + " static void test(String value){", + " }", + " static void testNegative() {", + " // Since the JarInferEnabled and JarInferUseReturnAnnotations flags are not set, we don't get an error here", + " test(annotationExample.makeUpperCase(\"nullaway\"));", + " }", + "}") + .doTest(); + } } From 8f86b36ebb2c839be65795f6e9393b2bb377f6d1 Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Sun, 24 Mar 2024 13:33:37 -0700 Subject: [PATCH 46/50] Handling the scenario for inner class: https://github.com/uber/NullAway/pull/922#discussion_r1531481198 --- .../libmodel/LibraryModelIntegrationTest.java | 26 +++++++++++++++++++ .../libmodel/LibraryModelGenerator.java | 26 +++++-------------- .../nullaway/libmodel/AnnotationExample.java | 7 +++++ .../nullaway/libmodel/AnnotationExample.java | 8 ++++++ 4 files changed, 47 insertions(+), 20 deletions(-) diff --git a/library-model/library-model-generator-integration-test/src/test/java/com/uber/nullaway/libmodel/LibraryModelIntegrationTest.java b/library-model/library-model-generator-integration-test/src/test/java/com/uber/nullaway/libmodel/LibraryModelIntegrationTest.java index de888aa53f..c0149fd78f 100644 --- a/library-model/library-model-generator-integration-test/src/test/java/com/uber/nullaway/libmodel/LibraryModelIntegrationTest.java +++ b/library-model/library-model-generator-integration-test/src/test/java/com/uber/nullaway/libmodel/LibraryModelIntegrationTest.java @@ -97,4 +97,30 @@ public void libraryModelWithoutJarInferEnabledTest() { "}") .doTest(); } + + @Test + public void libraryModelInnerClassNullableReturnsTest() { + compilationHelper + .setArgs( + Arrays.asList( + "-d", + temporaryFolder.getRoot().getAbsolutePath(), + "-XepOpt:NullAway:AnnotatedPackages=com.uber", + "-XepOpt:NullAway:JarInferEnabled=true", + "-XepOpt:NullAway:JarInferUseReturnAnnotations=true")) + .addSourceLines( + "Test.java", + "package com.uber;", + "import com.uber.nullaway.libmodel.AnnotationExample;", + "class Test {", + " static AnnotationExample.InnerExample innerExample = new AnnotationExample.InnerExample();", + " static void test(String value){", + " }", + " static void testPositive() {", + " // BUG: Diagnostic contains: passing @Nullable parameter 'innerExample.returnNull()'", + " test(innerExample.returnNull());", + " }", + "}") + .doTest(); + } } diff --git a/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java b/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java index f6705ead67..cde4bb6bc3 100644 --- a/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java +++ b/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java @@ -26,10 +26,8 @@ import com.github.javaparser.ParserConfiguration.LanguageLevel; import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.ImportDeclaration; -import com.github.javaparser.ast.Node; import com.github.javaparser.ast.PackageDeclaration; import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; -import com.github.javaparser.ast.body.EnumDeclaration; import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.expr.AnnotationExpr; import com.github.javaparser.ast.type.ArrayType; @@ -157,7 +155,7 @@ public Result process(Path localPath, Path absolutePath, ParseResult { - private String packageName = ""; + private StringBuilder parentName; private boolean isJspecifyNullableImportPresent = false; private boolean isNullMarked = false; private Map methodRecords; @@ -172,7 +170,7 @@ public AnnotationCollectionVisitor(Map methodRe @Override public void visit(PackageDeclaration pd, Void arg) { - this.packageName = pd.getNameAsString(); + this.parentName = new StringBuilder(pd.getNameAsString()); super.visit(pd, null); } @@ -191,6 +189,7 @@ public void visit(ClassOrInterfaceDeclaration cid, Void arg) { logic does not currently handle cases where @NullMarked annotations appear on some nested classes but not others. It also does not consider annotations within package-info.java or module-info.java files at this time.*/ + parentName.append(".").append(cid.getNameAsString()); cid.getAnnotations() .forEach( a -> { @@ -199,28 +198,15 @@ public void visit(ClassOrInterfaceDeclaration cid, Void arg) { } }); super.visit(cid, null); + // We reset the variable that constructs the parent name after visiting all the children. + parentName.delete(parentName.lastIndexOf("." + cid.getNameAsString()), parentName.length()); } @Override public void visit(MethodDeclaration md, Void arg) { - Optional parentClassNode = md.getParentNode(); - String parentClassName = ""; - if (parentClassNode.isPresent()) { - if (parentClassNode.get() instanceof ClassOrInterfaceDeclaration) { - parentClassName = ((ClassOrInterfaceDeclaration) parentClassNode.get()).getNameAsString(); - } else if (parentClassNode.get() instanceof EnumDeclaration) { - parentClassName = ((EnumDeclaration) parentClassNode.get()).getNameAsString(); - } - } if (this.isNullMarked && hasNullableReturn(md)) { methodRecords.put( - packageName - + "." - + parentClassName - + ":" - + getMethodReturnTypeString(md) - + " " - + md.getSignature().toString(), + parentName + ":" + getMethodReturnTypeString(md) + " " + md.getSignature().toString(), MethodAnnotationsRecord.create(ImmutableSet.of("Nullable"), ImmutableMap.of())); } super.visit(md, null); diff --git a/library-model/test-library-model-generator/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java b/library-model/test-library-model-generator/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java index 3fe23e73bb..ce8405fe44 100644 --- a/library-model/test-library-model-generator/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java +++ b/library-model/test-library-model-generator/src/main/java/com/uber/nullaway/libmodel/AnnotationExample.java @@ -32,4 +32,11 @@ public Integer[] generateIntArray(int size) { public String nullReturn() { return null; } + + public static class InnerExample { + + public String returnNull() { + return null; + } + } } diff --git a/library-model/test-library-model-generator/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java b/library-model/test-library-model-generator/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java index 89578c5426..f95c54090a 100644 --- a/library-model/test-library-model-generator/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java +++ b/library-model/test-library-model-generator/src/main/resources/sample_annotated/src/com/uber/nullaway/libmodel/AnnotationExample.java @@ -37,4 +37,12 @@ public String makeUpperCase(String inputString) { public String nullReturn() { return null; } + + public static class InnerExample { + + @Nullable + public String returnNull() { + return null; + } + } } From 35b988c98a926e4e8e1a369550bd29c97daa2be5 Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Sun, 24 Mar 2024 13:44:52 -0700 Subject: [PATCH 47/50] minor comment modifications. --- .../com/uber/nullaway/libmodel/LibraryModelGenerator.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java b/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java index cde4bb6bc3..1f6b1a9734 100644 --- a/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java +++ b/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java @@ -188,7 +188,7 @@ public void visit(ClassOrInterfaceDeclaration cid, Void arg) { source file, and it's expected that each source file contains only one top-level class. The logic does not currently handle cases where @NullMarked annotations appear on some nested classes but not others. It also does not consider annotations within package-info.java or - module-info.java files at this time.*/ + module-info.java files.*/ parentName.append(".").append(cid.getNameAsString()); cid.getAnnotations() .forEach( @@ -256,7 +256,7 @@ private String getMethodReturnTypeString(MethodDeclaration md) { } private boolean isAnnotationNullable(AnnotationExpr annotation) { - // Since we only want to consider jspecify Nullable annotations. + // We only consider jspecify Nullable annotations(star imports are not supported). return (annotation.getNameAsString().equalsIgnoreCase(NULLABLE) && this.isJspecifyNullableImportPresent) || annotation.getNameAsString().equalsIgnoreCase(JSPECIFY_NULLABLE_IMPORT); From c1fe36b397f284d254915a9f8cdf40e09b712fdf Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Sun, 24 Mar 2024 13:53:06 -0700 Subject: [PATCH 48/50] Removing redundant main class specification in javaexec; it's already defined in the jar's manifest --- library-model/test-library-model-generator/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/library-model/test-library-model-generator/build.gradle b/library-model/test-library-model-generator/build.gradle index 3a77dd1bd4..eef52e77cf 100644 --- a/library-model/test-library-model-generator/build.gradle +++ b/library-model/test-library-model-generator/build.gradle @@ -34,7 +34,6 @@ jar { jar.doLast { javaexec { classpath = files("${rootProject.projectDir}/library-model/library-model-generator-cli/build/libs/library-model-generator-cli.jar") - main = "com.uber.nullaway.libmodel.LibraryModelGeneratorCLI" args = [ testInputsPath, "${jar.destinationDirectory.get()}/${astubxPath}" From 439ca144041b15a7f4e95fde4239fe8cd86fc222 Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Wed, 27 Mar 2024 19:07:18 -0700 Subject: [PATCH 49/50] changing parentName from StringBuilder to String --- .../com/uber/nullaway/libmodel/LibraryModelGenerator.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java b/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java index 1f6b1a9734..78df3fbd43 100644 --- a/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java +++ b/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java @@ -155,7 +155,7 @@ public Result process(Path localPath, Path absolutePath, ParseResult { - private StringBuilder parentName; + private String parentName = ""; private boolean isJspecifyNullableImportPresent = false; private boolean isNullMarked = false; private Map methodRecords; @@ -170,7 +170,7 @@ public AnnotationCollectionVisitor(Map methodRe @Override public void visit(PackageDeclaration pd, Void arg) { - this.parentName = new StringBuilder(pd.getNameAsString()); + this.parentName = pd.getNameAsString(); super.visit(pd, null); } @@ -189,7 +189,7 @@ public void visit(ClassOrInterfaceDeclaration cid, Void arg) { logic does not currently handle cases where @NullMarked annotations appear on some nested classes but not others. It also does not consider annotations within package-info.java or module-info.java files.*/ - parentName.append(".").append(cid.getNameAsString()); + parentName += "." + cid.getNameAsString(); cid.getAnnotations() .forEach( a -> { @@ -199,7 +199,7 @@ public void visit(ClassOrInterfaceDeclaration cid, Void arg) { }); super.visit(cid, null); // We reset the variable that constructs the parent name after visiting all the children. - parentName.delete(parentName.lastIndexOf("." + cid.getNameAsString()), parentName.length()); + parentName = parentName.substring(0, parentName.lastIndexOf("." + cid.getNameAsString())); } @Override From a072bbdf8792eaeea86cda94148eae5a7833dd1e Mon Sep 17 00:00:00 2001 From: Abhijit Kulkarni Date: Thu, 28 Mar 2024 20:29:03 -0700 Subject: [PATCH 50/50] updating year in copyright header for newly created files --- library-model/library-model-generator-cli/build.gradle | 2 +- .../com/uber/nullaway/libmodel/LibraryModelGeneratorCLI.java | 2 +- .../library-model-generator-integration-test/build.gradle | 2 +- library-model/library-model-generator/build.gradle | 2 +- .../java/com/uber/nullaway/libmodel/LibraryModelGenerator.java | 2 +- library-model/test-library-model-generator/build.gradle | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/library-model/library-model-generator-cli/build.gradle b/library-model/library-model-generator-cli/build.gradle index dcc2eaff8e..94fd8a3742 100644 --- a/library-model/library-model-generator-cli/build.gradle +++ b/library-model/library-model-generator-cli/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017. Uber Technologies + * Copyright (C) 2024. Uber Technologies * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGeneratorCLI.java b/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGeneratorCLI.java index f56130106f..daea085bf4 100644 --- a/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGeneratorCLI.java +++ b/library-model/library-model-generator-cli/src/main/java/com/uber/nullaway/libmodel/LibraryModelGeneratorCLI.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Uber Technologies, Inc. + * Copyright (c) 2024 Uber Technologies, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/library-model/library-model-generator-integration-test/build.gradle b/library-model/library-model-generator-integration-test/build.gradle index c425f283f2..664e2bc4f0 100644 --- a/library-model/library-model-generator-integration-test/build.gradle +++ b/library-model/library-model-generator-integration-test/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017. Uber Technologies + * Copyright (C) 2024. Uber Technologies * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/library-model/library-model-generator/build.gradle b/library-model/library-model-generator/build.gradle index 0fde5ebda8..1d497fccd7 100644 --- a/library-model/library-model-generator/build.gradle +++ b/library-model/library-model-generator/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017. Uber Technologies + * Copyright (C) 2024. Uber Technologies * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java b/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java index 78df3fbd43..306a36611f 100644 --- a/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java +++ b/library-model/library-model-generator/src/main/java/com/uber/nullaway/libmodel/LibraryModelGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Uber Technologies, Inc. + * Copyright (c) 2024 Uber Technologies, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/library-model/test-library-model-generator/build.gradle b/library-model/test-library-model-generator/build.gradle index eef52e77cf..676abca9e7 100644 --- a/library-model/test-library-model-generator/build.gradle +++ b/library-model/test-library-model-generator/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017. Uber Technologies + * Copyright (C) 2024. Uber Technologies * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License.