From 1322f7002b67141c8adb748019669ce5bbdf6ce3 Mon Sep 17 00:00:00 2001 From: Aosen Xiong Date: Sat, 4 Jul 2026 17:37:11 -0400 Subject: [PATCH 01/13] Use adapted bounds for method type arguments --- .../common/basetype/BaseTypeVisitor.java | 3 +- .../framework/type/AnnotatedTypeFactory.java | 20 ++++++ .../MethodTypeVariableBounds.java | 72 +++++++++++++++++++ 3 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 framework/tests/viewpointtest/MethodTypeVariableBounds.java diff --git a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java index ac667319c2f1..482e6cda0f5f 100644 --- a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java +++ b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java @@ -2236,8 +2236,7 @@ public Void visitMethodInvocation(MethodInvocationTree tree, Void p) { List typeargs = mType.typeArgs; List paramBounds = - CollectionsPlume.mapList( - AnnotatedTypeVariable::getBounds, invokedMethod.getTypeVariables()); + atypeFactory.methodTypeVariablesFromUse(tree, invokedMethod); ExecutableElement method = invokedMethod.getElement(); CharSequence methodName = ElementUtils.getSimpleDescription(method); diff --git a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java index 350c59635874..ea38aa9b15e9 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java @@ -2398,6 +2398,26 @@ public List typeVariablesFromUse( return res; } + /** + * Returns the method type parameter bounds adapted to the viewpoint of a method invocation. + * + * @param tree a method invocation + * @param invokedMethod the type of the invoked method + * @return the adapted method type parameter bounds + */ + public List methodTypeVariablesFromUse( + MethodInvocationTree tree, AnnotatedExecutableType invokedMethod) { + List bounds = + CollectionsPlume.mapList( + AnnotatedTypeVariable::getBounds, invokedMethod.getTypeVariables()); + + AnnotatedTypeMirror receiverType = getReceiverType(tree); + if (viewpointAdapter != null && receiverType != null) { + viewpointAdapter.viewpointAdaptTypeParameterBounds(receiverType, bounds); + } + return bounds; + } + /** * Creates and returns an AnnotatedNullType qualified with {@code annotations}. * diff --git a/framework/tests/viewpointtest/MethodTypeVariableBounds.java b/framework/tests/viewpointtest/MethodTypeVariableBounds.java new file mode 100644 index 000000000000..e7fe4c948242 --- /dev/null +++ b/framework/tests/viewpointtest/MethodTypeVariableBounds.java @@ -0,0 +1,72 @@ +import viewpointtest.quals.*; + +public class MethodTypeVariableBounds { + static class Methods { + void noArg() {} + + void withArg(T t) {} + } + + void topReceiver( + @Top Methods methods, + @Top Object top, + @A Object a, + @B Object b, + @Bottom Object bottom) { + // @Top viewpoint-adapts @ReceiverDependentQual to @Lost, so only @Bottom is within the + // adapted method type parameter bound. + // :: error: (type.argument.type.incompatible) + methods.noArg(); + + // :: error: (type.argument.type.incompatible) + methods.<@Top Object>withArg(top); + + // :: error: (type.argument.type.incompatible) + methods.<@A Object>withArg(a); + + // :: error: (type.argument.type.incompatible) + methods.<@B Object>withArg(b); + + methods.<@Bottom Object>withArg(bottom); + + // :: error: (type.arguments.not.inferred) + methods.withArg(top); + + // :: error: (type.arguments.not.inferred) + methods.withArg(a); + + // :: error: (type.arguments.not.inferred) + methods.withArg(b); + + methods.withArg(bottom); + } + + void aReceiver( + @A Methods methods, @Top Object top, @A Object a, @B Object b, @Bottom Object bottom) { + // @A viewpoint-adapts @ReceiverDependentQual to @A, so @A and @Bottom are within the + // adapted method type parameter bound. + // :: error: (type.argument.type.incompatible) + methods.noArg(); + + // :: error: (type.argument.type.incompatible) + methods.<@Top Object>withArg(top); + + methods.<@A Object>withArg(a); + + // :: error: (type.argument.type.incompatible) + methods.<@B Object>withArg(b); + + methods.<@Bottom Object>withArg(bottom); + + // :: error: (type.arguments.not.inferred) + methods.withArg(top); + + // :: error: (type.arguments.not.inferred) + methods.withArg(a); + + // :: error: (type.arguments.not.inferred) + methods.withArg(b); + + methods.withArg(bottom); + } +} From f7cbccc3fd3b3098daefb1aa54d929f877c91e2d Mon Sep 17 00:00:00 2001 From: Aosen Xiong Date: Sat, 4 Jul 2026 18:12:51 -0400 Subject: [PATCH 02/13] Reuse method receiver handling for type-variable bounds --- .../common/basetype/BaseTypeVisitor.java | 2 +- .../framework/type/AnnotatedTypeFactory.java | 44 +++++++++++++------ 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java index 482e6cda0f5f..94e06aa0b048 100644 --- a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java +++ b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java @@ -2236,7 +2236,7 @@ public Void visitMethodInvocation(MethodInvocationTree tree, Void p) { List typeargs = mType.typeArgs; List paramBounds = - atypeFactory.methodTypeVariablesFromUse(tree, invokedMethod); + atypeFactory.methodTypeVariableBoundsFromUse(tree, invokedMethod); ExecutableElement method = invokedMethod.getElement(); CharSequence methodName = ElementUtils.getSimpleDescription(method); diff --git a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java index 5ed12f9be703..b42ef74287a3 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java @@ -2399,25 +2399,51 @@ public List typeVariablesFromUse( } /** - * Returns the method type parameter bounds adapted to the viewpoint of a method invocation. + * Returns the method type-variable bounds adapted to the viewpoint of a method invocation. * * @param tree a method invocation * @param invokedMethod the type of the invoked method * @return the adapted method type parameter bounds */ - public List methodTypeVariablesFromUse( + public List methodTypeVariableBoundsFromUse( MethodInvocationTree tree, AnnotatedExecutableType invokedMethod) { List bounds = CollectionsPlume.mapList( AnnotatedTypeVariable::getBounds, invokedMethod.getTypeVariables()); - AnnotatedTypeMirror receiverType = getReceiverType(tree); + AnnotatedTypeMirror receiverType = getMethodReceiverType(tree); if (viewpointAdapter != null && receiverType != null) { viewpointAdapter.viewpointAdaptTypeParameterBounds(receiverType, bounds); } return bounds; } + /** + * Returns the receiver type used to viewpoint-adapt a method invocation. + * + * @param tree a method invocation tree + * @return the receiver type, or null if the invocation has no receiver + */ + private @Nullable AnnotatedTypeMirror getMethodReceiverType(MethodInvocationTree tree) { + ExecutableElement methodElt = TreeUtils.elementFromUse(tree); + if (ElementUtils.isStatic(methodElt)) { + return null; + } + + AnnotatedTypeMirror receiverType = getReceiverType(tree); + if (receiverType == null + && (TreeUtils.isSuperConstructorCall(tree) + || TreeUtils.isThisConstructorCall(tree))) { + // super() and this() calls don't have a receiver, but they should be view-point adapted + // as if "this" is the receiver. + receiverType = getSelfType(tree); + } + if (receiverType != null && receiverType.getKind() == TypeKind.DECLARED) { + receiverType = applyCaptureConversion(receiverType); + } + return receiverType; + } + /** * Creates and returns an AnnotatedNullType qualified with {@code annotations}. * @@ -2753,17 +2779,7 @@ public ParameterizedExecutableType methodFromUseWithoutTypeArgInference( protected ParameterizedExecutableType methodFromUse( MethodInvocationTree tree, boolean inferTypeArgs) { ExecutableElement methodElt = TreeUtils.elementFromUse(tree); - AnnotatedTypeMirror receiverType = getReceiverType(tree); - if (receiverType == null - && (TreeUtils.isSuperConstructorCall(tree) - || TreeUtils.isThisConstructorCall(tree))) { - // super() and this() calls don't have a receiver, but they should be view-point adapted - // as if "this" is the receiver. - receiverType = getSelfType(tree); - } - if (receiverType != null && receiverType.getKind() == TypeKind.DECLARED) { - receiverType = applyCaptureConversion(receiverType); - } + AnnotatedTypeMirror receiverType = getMethodReceiverType(tree); ParameterizedExecutableType result = methodFromUse(tree, methodElt, receiverType, inferTypeArgs); From df99a830cfad82127a0a417294b43e364171d433 Mon Sep 17 00:00:00 2001 From: Aosen Xiong Date: Mon, 6 Jul 2026 11:37:22 -0400 Subject: [PATCH 03/13] Fix doclint warnings --- .../org/checkerframework/common/basetype/BaseTypeVisitor.java | 1 + .../checkerframework/framework/type/AnnotatedTypeFactory.java | 2 ++ 2 files changed, 3 insertions(+) diff --git a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java index 94e06aa0b048..fe661aa5f62c 100644 --- a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java +++ b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java @@ -194,6 +194,7 @@ * @see "JLS $4" * @see TypeHierarchy#isSubtype * @see AnnotatedTypeFactory + * @param the type of the annotated type factory */ public class BaseTypeVisitor> extends SourceVisitor { diff --git a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java index 1ddad79b22c3..bafa650197bd 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java @@ -1279,6 +1279,8 @@ public final TypeHierarchy getTypeHierarchy() { /** * TypeVariableSubstitutor provides a method to replace type parameters with their arguments. + * + * @return type variable substitutor to replace type parameters with their arguments */ protected TypeVariableSubstitutor createTypeVariableSubstitutor() { return new TypeVariableSubstitutor(); From b02a8e7e50e61fadf90b0bf0b55511efce06965d Mon Sep 17 00:00:00 2001 From: Aosen Xiong Date: Thu, 16 Jul 2026 12:04:49 -0400 Subject: [PATCH 04/13] Install adapted executable type variables for viewpoint adaptation AnnotatedTypeCopierWithReplacement skips executable type parameters, so adapted method/constructor type-variable bounds were discarded and inference still saw unadapted declaration bounds. Install the adapted declarations, drop the redundant post-check re-adaptation helper, and add constructor coverage. --- .../common/basetype/BaseTypeVisitor.java | 3 +- .../type/AbstractViewpointAdapter.java | 30 +++++-- .../framework/type/AnnotatedTypeFactory.java | 20 ----- .../ConstructorTypeVariableBounds.java | 79 +++++++++++++++++++ .../MethodTypeVariableBounds.java | 6 +- 5 files changed, 106 insertions(+), 32 deletions(-) create mode 100644 framework/tests/viewpointtest/ConstructorTypeVariableBounds.java diff --git a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java index fe661aa5f62c..5cb1b69148ee 100644 --- a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java +++ b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java @@ -2237,7 +2237,8 @@ public Void visitMethodInvocation(MethodInvocationTree tree, Void p) { List typeargs = mType.typeArgs; List paramBounds = - atypeFactory.methodTypeVariableBoundsFromUse(tree, invokedMethod); + CollectionsPlume.mapList( + AnnotatedTypeVariable::getBounds, invokedMethod.getTypeVariables()); ExecutableElement method = invokedMethod.getElement(); CharSequence methodName = ElementUtils.getSimpleDescription(method); diff --git a/framework/src/main/java/org/checkerframework/framework/type/AbstractViewpointAdapter.java b/framework/src/main/java/org/checkerframework/framework/type/AbstractViewpointAdapter.java index 253f6d1ce7a0..1bfd778fa4cd 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AbstractViewpointAdapter.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AbstractViewpointAdapter.java @@ -120,9 +120,15 @@ public void viewpointAdaptConstructor( AnnotatedTypeMirror p = combineTypeWithType(receiverType, parameterType); mappings.put(parameterType, p); } - for (AnnotatedTypeMirror typeVariable : typeVariables) { - AnnotatedTypeMirror tv = combineTypeWithType(receiverType, typeVariable); - mappings.put(typeVariable, tv); + // Adapt type-variable declarations separately. AnnotatedTypeCopierWithReplacement does not + // replace executable type parameters (see its visitTypeVariable), so install the adapted + // declarations explicitly below. + List adaptedTypeVariables = new ArrayList<>(typeVariables.size()); + for (AnnotatedTypeVariable typeVariable : typeVariables) { + AnnotatedTypeVariable adapted = + (AnnotatedTypeVariable) combineTypeWithType(receiverType, typeVariable); + mappings.put(typeVariable, adapted); + adaptedTypeVariables.add(adapted); } AnnotatedTypeMirror cr = combineTypeWithType(receiverType, constructorReturn); mappings.put(constructorReturn, cr); @@ -133,7 +139,7 @@ public void viewpointAdaptConstructor( unsubstitutedConstructorType, mappings); constructorType.setParameterTypes(unsubstitutedConstructorType.getParameterTypes()); - constructorType.setTypeVariables(unsubstitutedConstructorType.getTypeVariables()); + constructorType.setTypeVariables(adaptedTypeVariables); constructorType.setReturnType(unsubstitutedConstructorType.getReturnType()); } @@ -163,9 +169,17 @@ public void viewpointAdaptMethod( mappings.put(parameterType, p); } + // Adapt type-variable declarations separately. AnnotatedTypeCopierWithReplacement does not + // replace executable type parameters (see its visitTypeVariable), so install the adapted + // declarations explicitly below. Without this, inference and checkTypeArguments would see + // the unadapted declaration bounds (e.g. @ReceiverDependentQual instead of the + // receiver-adapted qualifier). + List adaptedTypeVariables = new ArrayList<>(typeVariables.size()); for (AnnotatedTypeVariable typeVariable : typeVariables) { - AnnotatedTypeMirror tv = combineTypeWithType(receiverType, typeVariable); - mappings.put(typeVariable, tv); + AnnotatedTypeVariable adapted = + (AnnotatedTypeVariable) combineTypeWithType(receiverType, typeVariable); + mappings.put(typeVariable, adapted); + adaptedTypeVariables.add(adapted); } if (returnType.getKind() != TypeKind.VOID) { @@ -184,11 +198,11 @@ public void viewpointAdaptMethod( unsubstitutedMethodType, mappings); // Because we can't viewpoint adapt asMemberOf result, we adapt the declared method first, - // and sets the corresponding parts to asMemberOf result + // and set the corresponding parts on the asMemberOf result. methodType.setReturnType(unsubstitutedMethodType.getReturnType()); methodType.setReceiverType(unsubstitutedMethodType.getReceiverType()); methodType.setParameterTypes(unsubstitutedMethodType.getParameterTypes()); - methodType.setTypeVariables(unsubstitutedMethodType.getTypeVariables()); + methodType.setTypeVariables(adaptedTypeVariables); } /** diff --git a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java index bafa650197bd..2c3ee51dc35a 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java @@ -2426,26 +2426,6 @@ public List typeVariablesFromUse( return res; } - /** - * Returns the method type-variable bounds adapted to the viewpoint of a method invocation. - * - * @param tree a method invocation - * @param invokedMethod the type of the invoked method - * @return the adapted method type parameter bounds - */ - public List methodTypeVariableBoundsFromUse( - MethodInvocationTree tree, AnnotatedExecutableType invokedMethod) { - List bounds = - CollectionsPlume.mapList( - AnnotatedTypeVariable::getBounds, invokedMethod.getTypeVariables()); - - AnnotatedTypeMirror receiverType = getMethodReceiverType(tree); - if (viewpointAdapter != null && receiverType != null) { - viewpointAdapter.viewpointAdaptTypeParameterBounds(receiverType, bounds); - } - return bounds; - } - /** * Returns the receiver type used to viewpoint-adapt a method invocation. * diff --git a/framework/tests/viewpointtest/ConstructorTypeVariableBounds.java b/framework/tests/viewpointtest/ConstructorTypeVariableBounds.java new file mode 100644 index 000000000000..18e405fb8290 --- /dev/null +++ b/framework/tests/viewpointtest/ConstructorTypeVariableBounds.java @@ -0,0 +1,79 @@ +import viewpointtest.quals.*; + +public class ConstructorTypeVariableBounds { + static class C { + // No-arg generic constructor: type argument is unused, so inference instantiates T to + // the adapted upper bound. + @SuppressWarnings({"inconsistent.constructor.type", "super.invocation.invalid"}) + C() {} + + @SuppressWarnings({"inconsistent.constructor.type", "super.invocation.invalid"}) + C(T t) {} + } + + void topViewpoint( + @Top Object top, @A Object a, @B Object b, @Bottom Object bottom) { + // Constructed type @Top adapts @ReceiverDependentQual to @Lost. Creating @Top is also + // forbidden by the viewpoint test checker. + // :: error: (new.class.type.invalid) :: error: (type.argument.type.incompatible) + new @Top C(); + + // :: error: (new.class.type.invalid) :: error: (type.argument.type.incompatible) + new <@Top Object> @Top C(top); + + // :: error: (new.class.type.invalid) :: error: (type.argument.type.incompatible) + new <@A Object> @Top C(a); + + // :: error: (new.class.type.invalid) :: error: (type.argument.type.incompatible) + new <@B Object> @Top C(b); + + // :: error: (new.class.type.invalid) + new <@Bottom Object> @Top C(bottom); + + // :: error: (new.class.type.invalid) :: error: (type.arguments.not.inferred) + new @Top C(top); + + // :: error: (new.class.type.invalid) :: error: (type.arguments.not.inferred) + new @Top C(a); + + // :: error: (new.class.type.invalid) :: error: (type.arguments.not.inferred) + new @Top C(b); + + // :: error: (new.class.type.invalid) + new @Top C(bottom); + } + + void aViewpoint( + @Top Object top, @A Object a, @B Object b, @Bottom Object bottom) { + // Constructed type @A adapts @ReceiverDependentQual to @A, so @A and @Bottom are within + // the adapted constructor type parameter bound. Inference instantiates T to @A for the + // no-arg constructor. + // :: warning: (cast.unsafe.constructor.invocation) + new @A C(); + + // :: error: (type.argument.type.incompatible) :: warning: (cast.unsafe.constructor.invocation) + new <@Top Object> @A C(top); + + // :: warning: (cast.unsafe.constructor.invocation) + new <@A Object> @A C(a); + + // :: error: (type.argument.type.incompatible) :: warning: (cast.unsafe.constructor.invocation) + new <@B Object> @A C(b); + + // :: warning: (cast.unsafe.constructor.invocation) + new <@Bottom Object> @A C(bottom); + + // :: error: (type.arguments.not.inferred) + new @A C(top); + + // Inference succeeds: argument @A is within the adapted bound @A. + // :: warning: (cast.unsafe.constructor.invocation) + new @A C(a); + + // :: error: (type.arguments.not.inferred) + new @A C(b); + + // :: warning: (cast.unsafe.constructor.invocation) + new @A C(bottom); + } +} diff --git a/framework/tests/viewpointtest/MethodTypeVariableBounds.java b/framework/tests/viewpointtest/MethodTypeVariableBounds.java index e7fe4c948242..26693dc0fa34 100644 --- a/framework/tests/viewpointtest/MethodTypeVariableBounds.java +++ b/framework/tests/viewpointtest/MethodTypeVariableBounds.java @@ -44,8 +44,8 @@ void topReceiver( void aReceiver( @A Methods methods, @Top Object top, @A Object a, @B Object b, @Bottom Object bottom) { // @A viewpoint-adapts @ReceiverDependentQual to @A, so @A and @Bottom are within the - // adapted method type parameter bound. - // :: error: (type.argument.type.incompatible) + // adapted method type parameter bound. Inference instantiates T to the adapted upper + // bound @A, which is a valid type argument. methods.noArg(); // :: error: (type.argument.type.incompatible) @@ -61,7 +61,7 @@ void aReceiver( // :: error: (type.arguments.not.inferred) methods.withArg(top); - // :: error: (type.arguments.not.inferred) + // Inference succeeds: argument @A is within the adapted bound @A. methods.withArg(a); // :: error: (type.arguments.not.inferred) From c23bd13b05d41d49bf27d738a6f751d6186ffd06 Mon Sep 17 00:00:00 2001 From: Aosen Xiong Date: Thu, 16 Jul 2026 12:09:34 -0400 Subject: [PATCH 05/13] Leave AnnotatedTypeFactory unchanged --- .../framework/type/AnnotatedTypeFactory.java | 40 +++++-------------- 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java index d41f3536d36c..84349678b40c 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java @@ -1279,8 +1279,6 @@ public final TypeHierarchy getTypeHierarchy() { /** * TypeVariableSubstitutor provides a method to replace type parameters with their arguments. - * - * @return type variable substitutor to replace type parameters with their arguments */ protected TypeVariableSubstitutor createTypeVariableSubstitutor() { return new TypeVariableSubstitutor(); @@ -2448,32 +2446,6 @@ public List typeVariablesFromUse( return res; } - /** - * Returns the receiver type used to viewpoint-adapt a method invocation. - * - * @param tree a method invocation tree - * @return the receiver type, or null if the invocation has no receiver - */ - private @Nullable AnnotatedTypeMirror getMethodReceiverType(MethodInvocationTree tree) { - ExecutableElement methodElt = TreeUtils.elementFromUse(tree); - if (ElementUtils.isStatic(methodElt)) { - return null; - } - - AnnotatedTypeMirror receiverType = getReceiverType(tree); - if (receiverType == null - && (TreeUtils.isSuperConstructorCall(tree) - || TreeUtils.isThisConstructorCall(tree))) { - // super() and this() calls don't have a receiver, but they should be view-point adapted - // as if "this" is the receiver. - receiverType = getSelfType(tree); - } - if (receiverType != null && receiverType.getKind() == TypeKind.DECLARED) { - receiverType = applyCaptureConversion(receiverType); - } - return receiverType; - } - /** * Creates and returns an AnnotatedNullType qualified with {@code annotations}. * @@ -2809,7 +2781,17 @@ public ParameterizedExecutableType methodFromUseWithoutTypeArgInference( protected ParameterizedExecutableType methodFromUse( MethodInvocationTree tree, boolean inferTypeArgs) { ExecutableElement methodElt = TreeUtils.elementFromUse(tree); - AnnotatedTypeMirror receiverType = getMethodReceiverType(tree); + AnnotatedTypeMirror receiverType = getReceiverType(tree); + if (receiverType == null + && (TreeUtils.isSuperConstructorCall(tree) + || TreeUtils.isThisConstructorCall(tree))) { + // super() and this() calls don't have a receiver, but they should be view-point adapted + // as if "this" is the receiver. + receiverType = getSelfType(tree); + } + if (receiverType != null && receiverType.getKind() == TypeKind.DECLARED) { + receiverType = applyCaptureConversion(receiverType); + } ParameterizedExecutableType result = methodFromUse(tree, methodElt, receiverType, inferTypeArgs); From 46e83c896bdb53c2a6686b08a4f23c0ad97fddfb Mon Sep 17 00:00:00 2001 From: Aosen Xiong Date: Thu, 16 Jul 2026 12:10:59 -0400 Subject: [PATCH 06/13] Leave BaseTypeVisitor unchanged --- .../org/checkerframework/common/basetype/BaseTypeVisitor.java | 1 - 1 file changed, 1 deletion(-) diff --git a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java index 27519f3a4ef4..39b95fd78aae 100644 --- a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java +++ b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java @@ -194,7 +194,6 @@ * @see "JLS $4" * @see TypeHierarchy#isSubtype * @see AnnotatedTypeFactory - * @param the type of the annotated type factory */ public class BaseTypeVisitor> extends SourceVisitor { From ef1442da125b9717b34861d2d0a9be4c2c0461dc Mon Sep 17 00:00:00 2001 From: Aosen Xiong Date: Thu, 16 Jul 2026 12:45:56 -0400 Subject: [PATCH 07/13] Format ConstructorTypeVariableBounds with Spotless --- .../ConstructorTypeVariableBounds.java | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/framework/tests/viewpointtest/ConstructorTypeVariableBounds.java b/framework/tests/viewpointtest/ConstructorTypeVariableBounds.java index 18e405fb8290..1328c4f347a3 100644 --- a/framework/tests/viewpointtest/ConstructorTypeVariableBounds.java +++ b/framework/tests/viewpointtest/ConstructorTypeVariableBounds.java @@ -11,24 +11,23 @@ static class C { C(T t) {} } - void topViewpoint( - @Top Object top, @A Object a, @B Object b, @Bottom Object bottom) { + void topViewpoint(@Top Object top, @A Object a, @B Object b, @Bottom Object bottom) { // Constructed type @Top adapts @ReceiverDependentQual to @Lost. Creating @Top is also // forbidden by the viewpoint test checker. // :: error: (new.class.type.invalid) :: error: (type.argument.type.incompatible) new @Top C(); // :: error: (new.class.type.invalid) :: error: (type.argument.type.incompatible) - new <@Top Object> @Top C(top); + new <@Top Object>@Top C(top); // :: error: (new.class.type.invalid) :: error: (type.argument.type.incompatible) - new <@A Object> @Top C(a); + new <@A Object>@Top C(a); // :: error: (new.class.type.invalid) :: error: (type.argument.type.incompatible) - new <@B Object> @Top C(b); + new <@B Object>@Top C(b); // :: error: (new.class.type.invalid) - new <@Bottom Object> @Top C(bottom); + new <@Bottom Object>@Top C(bottom); // :: error: (new.class.type.invalid) :: error: (type.arguments.not.inferred) new @Top C(top); @@ -43,25 +42,26 @@ void topViewpoint( new @Top C(bottom); } - void aViewpoint( - @Top Object top, @A Object a, @B Object b, @Bottom Object bottom) { + void aViewpoint(@Top Object top, @A Object a, @B Object b, @Bottom Object bottom) { // Constructed type @A adapts @ReceiverDependentQual to @A, so @A and @Bottom are within // the adapted constructor type parameter bound. Inference instantiates T to @A for the // no-arg constructor. // :: warning: (cast.unsafe.constructor.invocation) new @A C(); - // :: error: (type.argument.type.incompatible) :: warning: (cast.unsafe.constructor.invocation) - new <@Top Object> @A C(top); + // :: error: (type.argument.type.incompatible) :: warning: + // (cast.unsafe.constructor.invocation) + new <@Top Object>@A C(top); // :: warning: (cast.unsafe.constructor.invocation) - new <@A Object> @A C(a); + new <@A Object>@A C(a); - // :: error: (type.argument.type.incompatible) :: warning: (cast.unsafe.constructor.invocation) - new <@B Object> @A C(b); + // :: error: (type.argument.type.incompatible) :: warning: + // (cast.unsafe.constructor.invocation) + new <@B Object>@A C(b); // :: warning: (cast.unsafe.constructor.invocation) - new <@Bottom Object> @A C(bottom); + new <@Bottom Object>@A C(bottom); // :: error: (type.arguments.not.inferred) new @A C(top); From 16f7c80392d3226818ffaf2c498bbf07da1bf3f6 Mon Sep 17 00:00:00 2001 From: Aosen Xiong Date: Fri, 24 Jul 2026 11:33:05 -0400 Subject: [PATCH 08/13] Adapt executable type variable bounds via AnnotatedTypeCopierWithReplacement --- .../type/AbstractViewpointAdapter.java | 65 ++++++++++++------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/framework/type/AbstractViewpointAdapter.java b/framework/src/main/java/org/checkerframework/framework/type/AbstractViewpointAdapter.java index 1bfd778fa4cd..be5e5d90dd65 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AbstractViewpointAdapter.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AbstractViewpointAdapter.java @@ -106,40 +106,50 @@ public void viewpointAdaptConstructor( AnnotatedTypeMirror receiverType, ExecutableElement constructorElt, AnnotatedExecutableType constructorType) { - // constructorType's typevar are not substituted when calling viewpointAdaptConstructor + // 1. Make a copy of constructorType before type variables are substituted. AnnotatedExecutableType unsubstitutedConstructorType = constructorType.deepCopy(); - // For constructors, we adapt parameter types, return type and type parameters + // 2. Viewpoint-adapt constructor parameter types, type variable bounds, and return type. List parameterTypes = unsubstitutedConstructorType.getParameterTypes(); List typeVariables = unsubstitutedConstructorType.getTypeVariables(); AnnotatedTypeMirror constructorReturn = unsubstitutedConstructorType.getReturnType(); IdentityHashMap mappings = new IdentityHashMap<>(); + + // 2a. Adapt parameter types. for (AnnotatedTypeMirror parameterType : parameterTypes) { AnnotatedTypeMirror p = combineTypeWithType(receiverType, parameterType); mappings.put(parameterType, p); } - // Adapt type-variable declarations separately. AnnotatedTypeCopierWithReplacement does not - // replace executable type parameters (see its visitTypeVariable), so install the adapted - // declarations explicitly below. - List adaptedTypeVariables = new ArrayList<>(typeVariables.size()); + + // 2b. Adapt upper and lower bounds of constructor type variables. for (AnnotatedTypeVariable typeVariable : typeVariables) { - AnnotatedTypeVariable adapted = - (AnnotatedTypeVariable) combineTypeWithType(receiverType, typeVariable); - mappings.put(typeVariable, adapted); - adaptedTypeVariables.add(adapted); + if (typeVariable.getUpperBoundField() != null) { + AnnotatedTypeMirror adaptedUpper = + combineTypeWithType(receiverType, typeVariable.getUpperBound()); + mappings.put(typeVariable.getUpperBoundField(), adaptedUpper); + } + if (typeVariable.getLowerBoundField() != null) { + AnnotatedTypeMirror adaptedLower = + combineTypeWithType(receiverType, typeVariable.getLowerBound()); + mappings.put(typeVariable.getLowerBoundField(), adaptedLower); + } } + + // 2c. Adapt constructor return type. AnnotatedTypeMirror cr = combineTypeWithType(receiverType, constructorReturn); mappings.put(constructorReturn, cr); + // 3. Replace components using AnnotatedTypeCopierWithReplacement. unsubstitutedConstructorType = (AnnotatedExecutableType) AnnotatedTypeCopierWithReplacement.replace( unsubstitutedConstructorType, mappings); + // 4. Update target constructor type in place with adapted components. constructorType.setParameterTypes(unsubstitutedConstructorType.getParameterTypes()); - constructorType.setTypeVariables(adaptedTypeVariables); + constructorType.setTypeVariables(unsubstitutedConstructorType.getTypeVariables()); constructorType.setReturnType(unsubstitutedConstructorType.getReturnType()); } @@ -148,14 +158,15 @@ public void viewpointAdaptMethod( AnnotatedTypeMirror receiverType, ExecutableElement methodElt, AnnotatedExecutableType methodType) { + // 1. Check whether the method should be viewpoint-adapted (e.g. skip static methods). if (!shouldAdaptMethod(methodElt)) { return; } - // methodType's typevar are not substituted when calling viewpointAdaptMethod + // 2. Make a copy of methodType before type variables are substituted. AnnotatedExecutableType unsubstitutedMethodType = methodType.deepCopy(); - // For methods, we additionally adapt method receiver compared to constructors + // 3. Viewpoint-adapt parameter types, type variable bounds, return type, and receiver. List parameterTypes = unsubstitutedMethodType.getParameterTypes(); List typeVariables = unsubstitutedMethodType.getTypeVariables(); AnnotatedTypeMirror returnType = unsubstitutedMethodType.getReturnType(); @@ -164,45 +175,51 @@ public void viewpointAdaptMethod( IdentityHashMap mappings = new IdentityHashMap<>(); + // 3a. Adapt parameter types. for (AnnotatedTypeMirror parameterType : parameterTypes) { AnnotatedTypeMirror p = combineTypeWithType(receiverType, parameterType); mappings.put(parameterType, p); } - // Adapt type-variable declarations separately. AnnotatedTypeCopierWithReplacement does not - // replace executable type parameters (see its visitTypeVariable), so install the adapted - // declarations explicitly below. Without this, inference and checkTypeArguments would see - // the unadapted declaration bounds (e.g. @ReceiverDependentQual instead of the - // receiver-adapted qualifier). - List adaptedTypeVariables = new ArrayList<>(typeVariables.size()); + // 3b. Adapt upper and lower bounds of method type variables. for (AnnotatedTypeVariable typeVariable : typeVariables) { - AnnotatedTypeVariable adapted = - (AnnotatedTypeVariable) combineTypeWithType(receiverType, typeVariable); - mappings.put(typeVariable, adapted); - adaptedTypeVariables.add(adapted); + if (typeVariable.getUpperBoundField() != null) { + AnnotatedTypeMirror adaptedUpper = + combineTypeWithType(receiverType, typeVariable.getUpperBound()); + mappings.put(typeVariable.getUpperBoundField(), adaptedUpper); + } + if (typeVariable.getLowerBoundField() != null) { + AnnotatedTypeMirror adaptedLower = + combineTypeWithType(receiverType, typeVariable.getLowerBound()); + mappings.put(typeVariable.getLowerBoundField(), adaptedLower); + } } + // 3c. Adapt non-void return type. if (returnType.getKind() != TypeKind.VOID) { AnnotatedTypeMirror r = combineTypeWithType(receiverType, returnType); mappings.put(returnType, r); } + // 3d. Adapt method receiver type. if (methodReceiver != null) { AnnotatedTypeMirror mr = combineTypeWithType(receiverType, methodReceiver); mappings.put(methodReceiver, mr); } + // 4. Replace components using AnnotatedTypeCopierWithReplacement. unsubstitutedMethodType = (AnnotatedExecutableType) AnnotatedTypeCopierWithReplacement.replace( unsubstitutedMethodType, mappings); + // 5. Update target method type in place with adapted components. // Because we can't viewpoint adapt asMemberOf result, we adapt the declared method first, // and set the corresponding parts on the asMemberOf result. methodType.setReturnType(unsubstitutedMethodType.getReturnType()); methodType.setReceiverType(unsubstitutedMethodType.getReceiverType()); methodType.setParameterTypes(unsubstitutedMethodType.getParameterTypes()); - methodType.setTypeVariables(adaptedTypeVariables); + methodType.setTypeVariables(unsubstitutedMethodType.getTypeVariables()); } /** From a4e94e15be7a04fa23461cfd6e484bc3da6ca8f3 Mon Sep 17 00:00:00 2001 From: Werner Dietl Date: Fri, 24 Jul 2026 17:24:39 -0400 Subject: [PATCH 09/13] Pin ruff version in GitHub Actions setup and fix ruff 0.16.0 lint findings (#1886) Co-authored-by: Claude Fable 5 --- .../cf-performance/gen-sized-program.py | 6 +- .github/actions/setup-misc/action.yml | 9 +- .github/renovate.json | 7 + docs/developer/release/release_build.py | 163 +++++++--------- docs/developer/release/release_errors.py | 14 ++ docs/developer/release/release_push.py | 173 ++++++++--------- docs/developer/release/release_utils.py | 181 ++++++++---------- docs/developer/release/release_vars.py | 12 +- docs/developer/release/sanity_checks.py | 72 ++++--- 9 files changed, 296 insertions(+), 341 deletions(-) create mode 100644 docs/developer/release/release_errors.py diff --git a/.claude/skills/cf-performance/gen-sized-program.py b/.claude/skills/cf-performance/gen-sized-program.py index ae36655d42e2..57c7b549687c 100755 --- a/.claude/skills/cf-performance/gen-sized-program.py +++ b/.claude/skills/cf-performance/gen-sized-program.py @@ -68,8 +68,10 @@ def vararg(n: int) -> str: f" List xs{i} = Arrays.asList(s{i}, s{i}, s{i});", f" List ys{i} = Arrays.asList(s{i});", f' String f{i} = String.format("%s %s", s{i}, s{i});', - f" java.lang.reflect.Method mm{i} =" - f' Big.class.getMethod("m{i}", String.class);', + ( + f" java.lang.reflect.Method mm{i} =" + f' Big.class.getMethod("m{i}", String.class);' + ), " }", ] lines.append("}") diff --git a/.github/actions/setup-misc/action.yml b/.github/actions/setup-misc/action.yml index 35ec67702347..978c023778bd 100644 --- a/.github/actions/setup-misc/action.yml +++ b/.github/actions/setup-misc/action.yml @@ -67,4 +67,11 @@ runs: shell: bash # PEP 668: Ubuntu 24.04's system Python is externally-managed. # `--break-system-packages` is the documented escape hatch for CI. - run: pip install --break-system-packages black flake8 html5validator + # + # ruff is pinned: the Makefile's `install-ruff` target (`if ! command -v + # ruff; then pipx install ruff; fi`) only installs its own (unpinned, + # always-latest) copy when ruff isn't already on PATH, so installing a + # fixed version here makes that fallback a no-op and keeps `make + # style-check` results reproducible across CI runs. Bump this pin + # deliberately, not as a side effect of an unrelated PR. + run: pip install --break-system-packages black flake8 html5validator ruff==0.16.0 diff --git a/.github/renovate.json b/.github/renovate.json index 1353c9fcbfb9..5baa8bf8b2b5 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -41,6 +41,13 @@ "depNameTemplate": "openjdk/jdk", "versioningTemplate": "loose", "extractVersionTemplate": "^jdk-{{{major}}}\\+(?\\d+)$" + }, + { + "description": "Update the pinned ruff version in the misc job's GitHub Actions setup", + "fileMatch": ["^\\.github/actions/setup-misc/action\\.yml$"], + "matchStrings": ["ruff==(?[\\d.]+)"], + "datasourceTemplate": "pypi", + "depNameTemplate": "ruff" } ] } diff --git a/docs/developer/release/release_build.py b/docs/developer/release/release_build.py index f90bb47412c2..bf152c0176cd 100755 --- a/docs/developer/release/release_build.py +++ b/docs/developer/release/release_build.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# encoding: utf-8 """ release_build.py @@ -10,50 +9,52 @@ # See README-release-process.html for more information -from release_vars import ANNO_FILE_UTILITIES -from release_vars import ANNO_TOOLS -from release_vars import BUILD_REPOS -from release_vars import CF_VERSION -from release_vars import CHECKER_FRAMEWORK -from release_vars import CHECKER_FRAMEWORK_RELEASE -from release_vars import CHECKLINK -from release_vars import CHECKLINK_REPO -from release_vars import DEV_SITE_DIR -from release_vars import INTERM_REPOS -from release_vars import INTERM_TO_BUILD_REPOS -from release_vars import LIVE_SITE_URL -from release_vars import LIVE_TO_INTERM_REPOS -from release_vars import PLUME_BIB -from release_vars import PLUME_BIB_REPO -from release_vars import PLUME_SCRIPTS -from release_vars import PLUME_SCRIPTS_REPO -from release_vars import RELEASE_BUILD_COMPLETED_FLAG_FILE -from release_vars import TOOLS - -from release_vars import execute - -from release_utils import check_repos -from release_utils import check_tools -from release_utils import clone_from_scratch_or_update -from release_utils import commit_tag_and_push -from release_utils import continue_or_exit -from release_utils import create_empty_file -from release_utils import current_distribution_by_website -from release_utils import delete_if_exists -from release_utils import delete_path_if_exists -from release_utils import ensure_group_access -from release_utils import increment_version -from release_utils import os -from release_utils import print_step -from release_utils import prompt_to_continue -from release_utils import prompt_w_default -from release_utils import prompt_yes_no -from release_utils import has_command_line_option -from release_utils import set_umask - -from distutils.dir_util import copy_tree import datetime import sys +from distutils.dir_util import copy_tree + +from release_utils import ( + check_repos, + check_tools, + clone_from_scratch_or_update, + commit_tag_and_push, + continue_or_exit, + create_empty_file, + current_distribution_by_website, + delete_if_exists, + delete_path_if_exists, + ensure_group_access, + has_command_line_option, + increment_version, + os, + print_step, + prompt_to_continue, + prompt_w_default, + prompt_yes_no, + set_umask, +) +from release_vars import ( + ANNO_FILE_UTILITIES, + ANNO_TOOLS, + BUILD_REPOS, + CF_VERSION, + CHECKER_FRAMEWORK, + CHECKER_FRAMEWORK_RELEASE, + CHECKLINK, + CHECKLINK_REPO, + DEV_SITE_DIR, + INTERM_REPOS, + INTERM_TO_BUILD_REPOS, + LIVE_SITE_URL, + LIVE_TO_INTERM_REPOS, + PLUME_BIB, + PLUME_BIB_REPO, + PLUME_SCRIPTS, + PLUME_SCRIPTS_REPO, + RELEASE_BUILD_COMPLETED_FLAG_FILE, + TOOLS, + execute, +) # Turned on by the --debug command-line option. debug = False @@ -151,7 +152,7 @@ def create_dev_website_release_version_dir(project_name, version): interm_dir = os.path.join(DEV_SITE_DIR, project_name, "releases", version) delete_path_if_exists(interm_dir) - execute("mkdir -p %s" % interm_dir, True, False) + execute(f"mkdir -p {interm_dir}", True, False) return interm_dir @@ -199,7 +200,9 @@ def update_project_dev_website(project_name, release_version): def get_current_date(): "Return today's date in a string format similar to: 02 May 2016" - return datetime.date.today().strftime("%d %b %Y") + # Use the releaser's local calendar date (not UTC): astimezone() makes the + # datetime timezone-aware without changing which local date it names. + return datetime.datetime.now().astimezone().date().strftime("%d %b %Y") def build_annotation_tools_release(version, afu_interm_dir): @@ -210,20 +213,11 @@ def build_annotation_tools_release(version, afu_interm_dir): date = get_current_date() buildfile = os.path.join(ANNO_FILE_UTILITIES, "build.xml") - ant_cmd = ( - 'ant %s -buildfile %s -e update-versions -Drelease.ver="%s" -Drelease.date="%s"' - % (ant_debug, buildfile, version, date) - ) + ant_cmd = f'ant {ant_debug} -buildfile {buildfile} -e update-versions -Drelease.ver="{version}" -Drelease.date="{date}"' execute(ant_cmd) # Deploy to intermediate site - gradle_cmd = ( - "./gradlew releaseBuildWithoutTest -Pafu.version=%s -Pdeploy-dir=%s" - % ( - version, - afu_interm_dir, - ) - ) + gradle_cmd = f"./gradlew releaseBuildWithoutTest -Pafu.version={version} -Pdeploy-dir={afu_interm_dir}" execute(gradle_cmd, True, False, ANNO_FILE_UTILITIES) update_project_dev_website("annotation-file-utilities", version) @@ -246,24 +240,12 @@ def build_checker_framework_release( execute("./gradlew assemble -Prelease=true", True, False, ANNO_FILE_UTILITIES) # update versions - ant_props = ( - '-Dchecker=%s -Dold.release.ver=%s -Drelease.ver=%s -Dafu.version=%s -Dafu.properties=%s -Dafu.release.date="%s"' - # python `black` styling can't decide where to break the line. - % ( - checker_dir, - old_cf_version, - version, - version, - afu_build_properties, - afu_release_date, - ) - ) + ant_props = f'-Dchecker={checker_dir} -Dold.release.ver={old_cf_version} -Drelease.ver={version} -Dafu.version={version} -Dafu.properties={afu_build_properties} -Dafu.release.date="{afu_release_date}"' # IMPORTANT: The release.xml in the directory where the Checker Framework is # being built is used. Not the release.xml in the directory you ran # release_build.py from. - ant_cmd = "ant %s -f release.xml %s update-checker-framework-versions " % ( - ant_debug, - ant_props, + ant_cmd = ( + f"ant {ant_debug} -f release.xml {ant_props} update-checker-framework-versions " ) execute(ant_cmd, True, False, CHECKER_FRAMEWORK_RELEASE) # Update version numbers in the manual and API documentation, @@ -275,9 +257,7 @@ def build_checker_framework_release( # Check that updating versions didn't overlook anything. print("Here are occurrences of the old version number, " + old_cf_version + ":") - grep_cmd = ( - "grep -n -r --exclude-dir=build --exclude-dir=.git -F %s" % old_cf_version - ) + grep_cmd = f"grep -n -r --exclude-dir=build --exclude-dir=.git -F {old_cf_version}" execute(grep_cmd, False, False, CHECKER_FRAMEWORK) continue_or_exit( 'If any occurrence is not acceptable, then stop the release, update target "update-checker-framework-versions" in file release.xml, and start over.' @@ -299,36 +279,30 @@ def build_checker_framework_release( checker_tutorial_dir = os.path.join(CHECKER_FRAMEWORK, "docs", "tutorial") execute("make", True, False, checker_tutorial_dir) - cfZipName = "checker-framework-%s.zip" % version + cfZipName = f"checker-framework-{version}.zip" # Create checker-framework-X.Y.Z.zip and put it in checker_framework_interm_dir - ant_props = "-Dchecker=%s -Ddest.dir=%s -Dfile.name=%s -Dversion=%s" % ( - checker_dir, - checker_framework_interm_dir, - cfZipName, - version, - ) + ant_props = f"-Dchecker={checker_dir} -Ddest.dir={checker_framework_interm_dir} -Dfile.name={cfZipName} -Dversion={version}" # IMPORTANT: The release.xml in the directory where the Checker Framework # is being built is used. Not the release.xml in the directory you ran # release_build.py from. - ant_cmd = "ant %s -f release.xml %s zip-checker-framework " % (ant_debug, ant_props) + ant_cmd = f"ant {ant_debug} -f release.xml {ant_props} zip-checker-framework " execute(ant_cmd, True, False, CHECKER_FRAMEWORK_RELEASE) - ant_props = "-Dchecker=%s -Ddest.dir=%s -Dfile.name=%s -Dversion=%s" % ( + ant_props = "-Dchecker={} -Ddest.dir={} -Dfile.name={} -Dversion={}".format( checker_dir, checker_framework_interm_dir, "mvn-examples.zip", version, ) # IMPORTANT: The release.xml in the directory where the Checker Framework is being built is used. Not the release.xml in the directory you ran release_build.py from. - ant_cmd = "ant %s -f release.xml %s zip-maven-examples " % (ant_debug, ant_props) + ant_cmd = f"ant {ant_debug} -f release.xml {ant_props} zip-maven-examples " execute(ant_cmd, True, False, CHECKER_FRAMEWORK_RELEASE) # copy the remaining checker-framework website files to checker_framework_interm_dir ant_props = ( # Adding a comment to maybe help black formatting - "-Dchecker=%s -Ddest.dir=%s -Dmanual.name=%s -Ddataflow.manual.name=%s -Dchecker.webpage=%s" - % ( + "-Dchecker={} -Ddest.dir={} -Dmanual.name={} -Ddataflow.manual.name={} -Dchecker.webpage={}".format( checker_dir, checker_framework_interm_dir, "checker-framework-manual", @@ -338,9 +312,8 @@ def build_checker_framework_release( ) # IMPORTANT: The release.xml in the directory where the Checker Framework is being built is used. Not the release.xml in the directory you ran release_build.py from. - ant_cmd = "ant %s -f release.xml %s checker-framework-website-docs " % ( - ant_debug, - ant_props, + ant_cmd = ( + f"ant {ant_debug} -f release.xml {ant_props} checker-framework-website-docs " ) execute(ant_cmd, True, False, CHECKER_FRAMEWORK_RELEASE) @@ -352,8 +325,6 @@ def build_checker_framework_release( update_project_dev_website("checker-framework", version) - return - def commit_to_interm_projects(cf_version): """Commit the changes for each project from its build repo to its @@ -438,11 +409,9 @@ def main(argv): if old_cf_version == cf_version: print( - ( - "It is *strongly discouraged* to not update the release version numbers for the Checker Framework " - + "even if no changes were made to these in a month. This would break so much " - + "in the release scripts that they would become unusable. Update the version number in checker-framework/build.gradle\n" - ) + "It is *strongly discouraged* to not update the release version numbers for the Checker Framework " + + "even if no changes were made to these in a month. This would break so much " + + "in the release scripts that they would become unusable. Update the version number in checker-framework/build.gradle\n" ) prompt_to_continue() @@ -479,7 +448,7 @@ def main(argv): # Not "cp -p" because that does not work across filesystems whereas rsync does CFLOGO = os.path.join(CHECKER_FRAMEWORK, "docs", "logo", "Logo", "CFLogo.png") - execute("rsync --times %s %s" % (CFLOGO, checker_framework_interm_dir)) + execute(f"rsync --times {CFLOGO} {checker_framework_interm_dir}") # Each project has a set of files that are updated for release. Usually these updates include new # release date and version information. All changed files are committed and pushed to the intermediate diff --git a/docs/developer/release/release_errors.py b/docs/developer/release/release_errors.py new file mode 100644 index 000000000000..4e46c86b54be --- /dev/null +++ b/docs/developer/release/release_errors.py @@ -0,0 +1,14 @@ +""" +release_errors.py + +Defines the exception type shared by the release scripts. Kept in its own +module, rather than in release_vars.py or release_utils.py, because +release_utils.py already imports from release_vars.py (execute); either of +those two importing this exception back from the other would be a circular +import. +""" + + +class ReleaseError(Exception): + """Raised for any failure in the release scripts (release_vars.py, + release_utils.py, release_build.py, release_push.py, sanity_checks.py).""" diff --git a/docs/developer/release/release_push.py b/docs/developer/release/release_push.py index a221c9ab8b9b..e02a2478ad81 100755 --- a/docs/developer/release/release_push.py +++ b/docs/developer/release/release_push.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# encoding: utf-8 """ release_push.py @@ -11,48 +10,50 @@ # See README-release-process.html for more information import os +import sys from os.path import expanduser -from release_vars import AFU_LIVE_RELEASES_DIR -from release_vars import ANNO_FILE_UTILITIES -from release_vars import CF_VERSION -from release_vars import CHECKER_FRAMEWORK -from release_vars import CHECKER_LIVE_RELEASES_DIR -from release_vars import CHECKER_LIVE_API_DIR -from release_vars import CHECKLINK -from release_vars import DEV_SITE_DIR -from release_vars import DEV_SITE_URL -from release_vars import INTERM_ANNO_REPO -from release_vars import INTERM_CHECKER_REPO -from release_vars import LIVE_SITE_DIR -from release_vars import LIVE_SITE_URL -from release_vars import RELEASE_BUILD_COMPLETED_FLAG_FILE -from release_vars import SANITY_DIR -from release_vars import SCRIPTS_DIR -from release_vars import TMP_DIR - -from release_vars import execute - -from release_utils import continue_or_exit -from release_utils import current_distribution_by_website -from release_utils import delete_if_exists -from release_utils import delete_path -from release_utils import delete_path_if_exists -from release_utils import ensure_group_access -from release_utils import get_announcement_email -from release_utils import print_step -from release_utils import prompt_to_continue -from release_utils import prompt_yes_no -from release_utils import push_changes_prompt_if_fail -from release_utils import has_command_line_option -from release_utils import read_first_line -from release_utils import set_umask -from release_utils import subprocess -from release_utils import version_number_to_array +from release_errors import ReleaseError +from release_utils import ( + continue_or_exit, + current_distribution_by_website, + delete_if_exists, + delete_path, + delete_path_if_exists, + ensure_group_access, + get_announcement_email, + has_command_line_option, + print_step, + prompt_to_continue, + prompt_yes_no, + push_changes_prompt_if_fail, + read_first_line, + set_umask, + subprocess, + version_number_to_array, +) +from release_vars import ( + AFU_LIVE_RELEASES_DIR, + ANNO_FILE_UTILITIES, + CF_VERSION, + CHECKER_FRAMEWORK, + CHECKER_LIVE_API_DIR, + CHECKER_LIVE_RELEASES_DIR, + CHECKLINK, + DEV_SITE_DIR, + DEV_SITE_URL, + INTERM_ANNO_REPO, + INTERM_CHECKER_REPO, + LIVE_SITE_DIR, + LIVE_SITE_URL, + RELEASE_BUILD_COMPLETED_FLAG_FILE, + SANITY_DIR, + SCRIPTS_DIR, + TMP_DIR, + execute, +) from sanity_checks import javac_sanity_check, maven_sanity_check -import sys - def check_release_version(previous_release, new_release): """Ensure that the given new release version is greater than the given @@ -60,7 +61,7 @@ def check_release_version(previous_release, new_release): if version_number_to_array(previous_release) >= version_number_to_array( new_release ): - raise Exception( + raise ReleaseError( "Previous release version (" + previous_release + ") should be less than " @@ -82,15 +83,12 @@ def copy_release_dir(path_to_dev_releases, path_to_live_releases, release_versio delete_path(dest_location) if os.path.exists(dest_location): - raise Exception("Destination location exists: " + dest_location) + raise ReleaseError("Destination location exists: " + dest_location) # The / at the end of the source location is necessary so that # rsync copies the files in the source directory to the destination directory # rather than a subdirectory of the destination directory. - cmd = "rsync --no-group --omit-dir-times --recursive --links --quiet %s/ %s" % ( - source_location, - dest_location, - ) + cmd = f"rsync --no-group --omit-dir-times --recursive --links --quiet {source_location}/ {dest_location}" execute(cmd) return dest_location @@ -103,7 +101,7 @@ def promote_release(path_to_releases, release_version): from_dir = os.path.join(path_to_releases, release_version) to_dir = os.path.join(path_to_releases, "..") # Trailing slash is crucial. - cmd = "rsync -aJ --no-group --omit-dir-times %s/ %s" % (from_dir, to_dir) + cmd = f"rsync -aJ --no-group --omit-dir-times {from_dir}/ {to_dir}" execute(cmd) @@ -111,7 +109,9 @@ def copy_htaccess(): "Copy the .htaccess file from the dev site to the live site." LIVE_HTACCESS = os.path.join(LIVE_SITE_DIR, ".htaccess") execute( - "rsync --times %s %s" % (os.path.join(DEV_SITE_DIR, ".htaccess"), LIVE_HTACCESS) + "rsync --times {} {}".format( + os.path.join(DEV_SITE_DIR, ".htaccess"), LIVE_HTACCESS + ) ) ensure_group_access(LIVE_HTACCESS) @@ -149,8 +149,7 @@ def stage_maven_artifacts_in_maven_central(new_cf_version): "/projects/swlab1/checker-framework/hosting-info/release-private.password" ) execute( - "./gradlew publish -Prelease=true --no-parallel -Psigning.gnupg.keyName=checker-framework-dev@googlegroups.com -Psigning.gnupg.passphrase=%s" - % gnupgPassphrase, + f"./gradlew publish -Prelease=true --no-parallel -Psigning.gnupg.keyName=checker-framework-dev@googlegroups.com -Psigning.gnupg.passphrase={gnupgPassphrase}", working_dir=CHECKER_FRAMEWORK, ) @@ -172,30 +171,22 @@ def run_link_checker(site, output, additional_param=""): cmd = ["sh", check_links_script, additional_param, site] env = {"CHECKLINK": CHECKLINK} - out_file = open(output, "w+") - print( - ( - "Executing: " - + " ".join("%s=%r" % (key2, val2) for (key2, val2) in list(env.items())) - + " " - + " ".join(cmd) - ) + "Executing: " + + " ".join(f"{key2}={val2!r}" for (key2, val2) in list(env.items())) + + " " + + " ".join(cmd) ) - process = subprocess.Popen(cmd, env=env, stdout=out_file, stderr=out_file) - process.communicate() - process.wait() - out_file.close() + with open(output, "w+") as out_file: + process = subprocess.Popen(cmd, env=env, stdout=out_file, stderr=out_file) + process.communicate() + process.wait() if process.returncode != 0: - msg = "Non-zero return code (%s; see output in %s) while executing %s" % ( - process.returncode, - output, - cmd, - ) + msg = f"Non-zero return code ({process.returncode}; see output in {output}) while executing {cmd}" print(msg + "\n") if not prompt_yes_no("Continue despite link checker results?", True): - raise Exception(msg) + raise ReleaseError(msg) return output @@ -239,20 +230,21 @@ def check_all_links( print("\t" + afuCheck + "\n") if not is_checkerCheck_empty: print("\t" + checkerCheck + "\n") - if errors_reported: - if not prompt_yes_no("Continue despite link checker results?", True): - release_option = "" - if not test_mode: - release_option = " release" - raise Exception( - "The link checker reported errors. Please fix them by committing changes to the mainline\n" - + "repository and pushing them to GitHub, then updating the development and live sites by\n" - + "running\n" - + " python3 release_build.py all\n" - + " python3 release_push" - + release_option - + "\n" - ) + if errors_reported and not prompt_yes_no( + "Continue despite link checker results?", True + ): + release_option = "" + if not test_mode: + release_option = " release" + raise ReleaseError( + "The link checker reported errors. Please fix them by committing changes to the mainline\n" + + "repository and pushing them to GitHub, then updating the development and live sites by\n" + + "running\n" + + " python3 release_build.py all\n" + + " python3 release_push" + + release_option + + "\n" + ) def push_interm_to_release_repos(): @@ -268,23 +260,21 @@ def validate_args(argv): criteria issued in print_usage.""" if len(argv) > 3: print_usage() - raise Exception("Invalid arguments. " + ",".join(argv)) + raise ReleaseError("Invalid arguments. " + ",".join(argv)) for i in range(1, len(argv)): if argv[i] != "release": print_usage() - raise Exception("Invalid arguments. " + ",".join(argv)) + raise ReleaseError("Invalid arguments. " + ",".join(argv)) def print_usage(): """Print instructions on how to use this script, and in particular how to set test or release mode.""" print( - ( - "Usage: python3 release_build.py [release]\n" - + 'If the "release" argument is ' - + "NOT specified then the script will execute all steps that checking and prompting " - + "steps but will NOT actually perform a release. This is for testing the script." - ) + "Usage: python3 release_build.py [release]\n" + + 'If the "release" argument is ' + + "NOT specified then the script will execute all steps that checking and prompting " + + "steps but will NOT actually perform a release. This is for testing the script." ) @@ -306,7 +296,7 @@ def main(argv): m2_settings = expanduser("~") + "/.m2/settings.xml" if not os.path.exists(m2_settings): - raise Exception("File does not exist: " + m2_settings) + raise ReleaseError("File does not exist: " + m2_settings) if test_mode: msg = ( @@ -346,8 +336,7 @@ def main(argv): check_release_version(current_cf_version, new_cf_version) print( - "Checker Framework and AFU: current-version=%s new-version=%s" - % (current_cf_version, new_cf_version) + f"Checker Framework and AFU: current-version={current_cf_version} new-version={new_cf_version}" ) # Runs the link the checker on all websites at: diff --git a/docs/developer/release/release_utils.py b/docs/developer/release/release_utils.py index 84c17a7c7291..58388fe75ad1 100755 --- a/docs/developer/release/release_utils.py +++ b/docs/developer/release/release_utils.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# encoding: utf-8 """ releaseutils.py @@ -10,14 +9,16 @@ Copyright (c) 2012 University of Washington """ -import urllib.request -import urllib.error -import urllib.parse -import re -import subprocess import os import os.path +import re import shutil +import subprocess +import urllib.error +import urllib.parse +import urllib.request + +from release_errors import ReleaseError from release_vars import execute # ========================================================================================= @@ -41,23 +42,20 @@ def execute_write_to_file( command_args, output_file_path, halt_if_fail=True, working_dir=None ): """Execute the given command, capturing the output to the given file.""" - print("Executing: %s" % (command_args)) + print(f"Executing: {command_args}") import shlex args = shlex.split(command_args) if isinstance(command_args, str) else command_args - output_file = open(output_file_path, "w+") - process = subprocess.Popen( - args, stdout=output_file, stderr=output_file, cwd=working_dir - ) - process.communicate() - process.wait() - output_file.close() + with open(output_file_path, "w+") as output_file: + process = subprocess.Popen( + args, stdout=output_file, stderr=output_file, cwd=working_dir + ) + process.communicate() + process.wait() if process.returncode != 0 and halt_if_fail: - raise Exception( - "Error %s while executing %s" % (process.returncode, command_args) - ) + raise ReleaseError(f"Error {process.returncode} while executing {command_args}") def check_command(command): @@ -65,8 +63,8 @@ def check_command(command): is installed and on the PATH.""" p = execute(["which", command], False) if p: - raise AssertionError("command not found: %s" % command) - print("") + raise AssertionError(f"command not found: {command}") + print() def prompt_yes_no(msg, default=False): @@ -78,9 +76,7 @@ def prompt_yes_no(msg, default=False): result = prompt_w_default(msg, default_str, "^(Yes|yes|No|no)$") - if result == "yes" or result == "Yes": - return True - return False + return bool(result == "yes" or result == "Yes") def prompt_yn(msg): @@ -105,7 +101,7 @@ def prompt_w_default(msg, default, valid_regex=None): If default is None, requires an answer.""" answer = None while answer is None: - answer = input(msg + " (%s): " % default) + answer = input(msg + f" ({default}): ") if answer is None or answer == "": answer = default @@ -129,14 +125,12 @@ def check_tools(tools): print("\nChecking to make sure the following programs are installed:") print(", ".join(tools)) print( - ( - "Note: If you are NOT working in the Release Docker image then you " - + "likely need to change the variables that are set in release.py\n" - + 'Search for "Set environment variables".' - ) + "Note: If you are NOT working in the Release Docker image then you " + + "likely need to change the variables that are set in release.py\n" + + 'Search for "Set environment variables".' ) list(map(check_command, tools)) - print("") + print() def continue_or_exit(msg): @@ -145,7 +139,7 @@ def continue_or_exit(msg): msg + " Continue ('no' will exit the script)?", "yes", "^(Yes|yes|No|no)$" ) if continue_script == "no" or continue_script == "No": - raise Exception("User elected NOT to continue at prompt: " + msg) + raise ReleaseError("User elected NOT to continue at prompt: " + msg) # ========================================================================================= @@ -193,7 +187,7 @@ def current_distribution_by_website(site): Reads the checker framework version from the checker framework website and returns the version of the current release. """ - print("Looking up checker-framework-version from %s\n" % site) + print(f"Looking up checker-framework-version from {site}\n") ver_re = re.compile( r"checker-framework-(.*)\.zip" ) @@ -210,9 +204,7 @@ def git_bare_repo_exists_at_path( repo_root, ): # Bare git repos have no .git directory but they have a refs directory "Returns whether a bare git repository exists at the given filesystem path." - if os.path.isdir(repo_root + "/refs"): - return True - return False + return bool(os.path.isdir(repo_root + "/refs")) def git_repo_exists_at_path(repo_root): @@ -227,9 +219,9 @@ def push_changes_prompt_if_fail(repo_root): if they would like to try again. Loop until pushing changes succeeds or the user answers opts to not try again.""" while True: - cmd = "(cd %s && git push --tags)" % repo_root + cmd = f"(cd {repo_root} && git push --tags)" result = os.system(cmd) - cmd = "(cd %s && git push origin master)" % repo_root + cmd = f"(cd {repo_root} && git push origin master)" result = os.system(cmd) if result == 0: break @@ -271,8 +263,8 @@ def commit_tag_and_push(version, path, tag_prefix): push these changes.""" # Do nothing (instead of erring) if there is nothing to commit. if execute("git diff-index --quiet HEAD", False, False, working_dir=path) != 0: - execute('git commit -a -m "new release %s"' % (version), working_dir=path) - execute("git tag %s%s" % (tag_prefix, version), working_dir=path) + execute(f'git commit -a -m "new release {version}"', working_dir=path) + execute(f"git tag {tag_prefix}{version}", working_dir=path) push_changes(path) @@ -309,7 +301,7 @@ def clone(src_repo, dst_repo, bareflag): flags = "" if bareflag: flags = "--bare" - execute("git clone --quiet %s %s %s" % (flags, src_repo, dst_repo)) + execute(f"git clone --quiet {flags} {src_repo} {dst_repo}") def is_repo_cleaned_and_updated(repo): @@ -342,27 +334,21 @@ def is_repo_cleaned_and_updated(repo): def check_repos(repos, fail_on_error, is_intermediate_repo_list): """Fail if the repository is not clean and up to date.""" for repo in repos: - if git_repo_exists_at_path(repo): - if not is_repo_cleaned_and_updated(repo): - if is_intermediate_repo_list: - print( - ( - "\nWARNING: Intermediate repository " - + repo - + " is not up to date with respect to the live repository.\n" - + "A separate warning will not be issued for a build repository that is cloned off of the intermediate repository." - ) - ) - if fail_on_error: - raise Exception("repo %s is not cleaned and updated!" % repo) - else: - if not prompt_yn( - "%s is not clean and up to date! Continue (answering 'n' will exit the script)?" - % repo - ): - raise Exception( - "%s is not clean and up to date! Halting!" % repo - ) + if git_repo_exists_at_path(repo) and not is_repo_cleaned_and_updated(repo): + if is_intermediate_repo_list: + print( + "\nWARNING: Intermediate repository " + + repo + + " is not up to date with respect to the live repository.\n" + + "A separate warning will not be issued for a build repository that is cloned off of the intermediate repository." + ) + if fail_on_error: + raise ReleaseError(f"repo {repo} is not cleaned and updated!") + else: + if not prompt_yn( + f"{repo} is not clean and up to date! Continue (answering 'n' will exit the script)?" + ): + raise ReleaseError(f"{repo} is not clean and up to date! Halting!") def get_tag_line(lines, revision, tag_prefixes): @@ -397,12 +383,12 @@ def get_commit_for_tag(revision, repo_file_path, tag_prefixes): commit = lines[0] if commit is None: - msg = "Could not find revision %s in repo %s using tags %s " % ( + msg = "Could not find revision {} in repo {} using tags {} ".format( revision, repo_file_path, ",".join(tag_prefixes), ) - raise Exception(msg) + raise ReleaseError(msg) return commit @@ -415,7 +401,7 @@ def wget_file(source_url, destination_dir): """Download a file from the source URL to the given destination directory. Useful since download_binary does not seem to work on source files.""" print("DEST DIR: " + destination_dir) - execute("wget %s" % source_url, True, False, destination_dir) + execute(f"wget {source_url}", True, False, destination_dir) def download_binary(source_url, destination): @@ -425,31 +411,28 @@ def download_binary(source_url, destination): content_length = http_response.headers["content-length"] if content_length is None: - raise Exception("No content-length when downloading: " + source_url) + raise ReleaseError("No content-length when downloading: " + source_url) - dest_file = open(destination, "wb") - dest_file.write(http_response.read()) - dest_file.close() + with open(destination, "wb") as dest_file: + dest_file.write(http_response.read()) def read_first_line(file_path): "Return the first line in the given file. Assumes the file exists." - infile = open(file_path, "r") - first_line = infile.readline() - infile.close() - return first_line + with open(file_path, "r") as infile: + return infile.readline() def ensure_group_access(path): "Give group access to all files and directories under the specified path" # Errs for any file not owned by this user. # But, the point is to set group writeability of any *new* files. - execute("chmod -f -R g+rw %s" % path, halt_if_fail=False) + execute(f"chmod -f -R g+rw {path}", halt_if_fail=False) def ensure_user_access(path): "Give the user access to all files and directories under the specified path" - execute("chmod -f -R u+rwx %s" % path, halt_if_fail=True) + execute(f"chmod -f -R u+rwx {path}", halt_if_fail=True) def set_umask(): @@ -485,18 +468,17 @@ def are_in_file(file_path, strs_to_find): """Returns true if every string in the given strs_to_find array is found in at least one line in the given file. In particular, returns true if strs_to_find is empty. Note that the strs_to_find parameter is mutated.""" - infile = open(file_path) - - for line in infile: - if len(strs_to_find) == 0: - return True - - index = 0 - while index < len(strs_to_find): - if strs_to_find[index] in line: - del strs_to_find[index] - else: - index = index + 1 + with open(file_path) as infile: + for line in infile: + if len(strs_to_find) == 0: + return True + + index = 0 + while index < len(strs_to_find): + if strs_to_find[index] in line: + del strs_to_find[index] + else: + index = index + 1 return len(strs_to_find) == 0 @@ -509,22 +491,18 @@ def insert_before_line(to_insert, file_path, line): with open(file_path) as infile: content = infile.readlines() - output = open(file_path, "w") - for i in range(0, mid_line): - output.write(content[i]) - - output.write(to_insert) + with open(file_path, "w") as output: + output.writelines(content[i] for i in range(mid_line)) - for i in range(mid_line, len(content)): - output.write(content[i]) + output.write(to_insert) - output.close() + output.writelines(content[i] for i in range(mid_line, len(content))) def create_empty_file(file_path): "Creates an empty file with the given filename." - dest_file = open(file_path, "wb") - dest_file.close() + with open(file_path, "wb"): + pass # ========================================================================================= @@ -537,7 +515,7 @@ def print_step(step): print(step) dashStr = "" - for dummy in range(0, len(step)): + for dummy in range(len(step)): dashStr += "-" print(dashStr) @@ -545,9 +523,9 @@ def print_step(step): def get_announcement_email(version): """Return the template for the e-mail announcing a new release of the Checker Framework.""" - return """ + return f""" To: checker-framework-discuss@googlegroups.com -Subject: Release %s of the Checker Framework +Subject: Release {version} of the Checker Framework We have released a new version of the Checker Framework. The Checker Framework lets you create and/or run pluggable type checkers, in order to detect and prevent bugs in your code. @@ -555,13 +533,10 @@ def get_announcement_email(version): You can find documentation and download links at: http://eisop.github.io/ -Changes for Checker Framework version %s: +Changes for Checker Framework version {version}: <> -""" % ( - version, - version, - ) +""" # ========================================================================================= diff --git a/docs/developer/release/release_vars.py b/docs/developer/release/release_vars.py index 1596e3cc952f..c1b31e3c9c43 100755 --- a/docs/developer/release/release_vars.py +++ b/docs/developer/release/release_vars.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# encoding: utf-8 """ release_vars.py @@ -14,9 +13,10 @@ import os import pwd -import subprocess import shlex +import subprocess +from release_errors import ReleaseError # --------------------------------------------------------------------------------- # The only methods that should go here are methods that help define global release @@ -42,9 +42,9 @@ def execute(command_args, halt_if_fail=True, capture_output=False, working_dir=N """ if working_dir is not None: - print("Executing in %s: %s" % (working_dir, command_args)) + print(f"Executing in {working_dir}: {command_args}") else: - print("Executing: %s" % (command_args)) + print(f"Executing: {command_args}") args = shlex.split(command_args) if isinstance(command_args, str) else command_args if capture_output: @@ -56,8 +56,8 @@ def execute(command_args, halt_if_fail=True, capture_output=False, working_dir=N else: result = subprocess.call(args, cwd=working_dir) if halt_if_fail and result: - raise Exception( - "Error %s while executing %s in %s" % (result, args, working_dir) + raise ReleaseError( + f"Error {result} while executing {args} in {working_dir}" ) return result diff --git a/docs/developer/release/sanity_checks.py b/docs/developer/release/sanity_checks.py index ad49597eb58e..7e2a93f33a1c 100755 --- a/docs/developer/release/sanity_checks.py +++ b/docs/developer/release/sanity_checks.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# encoding: utf-8 """ releaseutils.py @@ -12,21 +11,24 @@ import zipfile -from release_vars import CHECKER_FRAMEWORK -from release_vars import CHECKER_FRAMEWORK_RELEASE -from release_vars import SANITY_DIR - -from release_vars import execute - -from release_utils import are_in_file -from release_utils import delete -from release_utils import delete_path -from release_utils import download_binary -from release_utils import ensure_user_access -from release_utils import execute_write_to_file -from release_utils import insert_before_line -from release_utils import os -from release_utils import wget_file +from release_errors import ReleaseError +from release_utils import ( + are_in_file, + delete, + delete_path, + download_binary, + ensure_user_access, + execute_write_to_file, + insert_before_line, + os, + wget_file, +) +from release_vars import ( + CHECKER_FRAMEWORK, + CHECKER_FRAMEWORK_RELEASE, + SANITY_DIR, + execute, +) def javac_sanity_check(checker_framework_website, release_version): @@ -51,12 +53,10 @@ def javac_sanity_check(checker_framework_website, release_version): execute("mkdir -p " + javac_sanity_dir) javac_sanity_zip = os.path.join( - javac_sanity_dir, "checker-framework-%s.zip" % release_version + javac_sanity_dir, f"checker-framework-{release_version}.zip" ) - print( - "Attempting to download %s to %s" % (new_checkers_release_zip, javac_sanity_zip) - ) + print(f"Attempting to download {new_checkers_release_zip} to {javac_sanity_zip}") download_binary(new_checkers_release_zip, javac_sanity_zip) nullness_example_url = "https://raw.githubusercontent.com/eisop/checker-framework/master/docs/examples/NullnessExampleWithWarnings.java" @@ -135,10 +135,7 @@ def maven_sanity_check(sub_sanity_dir_name, repo_url, release_version): output_log = os.path.join(maven_example_dir, "output.log") ant_release_script = os.path.join(CHECKER_FRAMEWORK_RELEASE, "release.xml") - get_example_dir_cmd = ( - "ant -f %s update-and-copy-maven-example -Dchecker=%s -Dversion=%s -Ddest.dir=%s" - % (ant_release_script, checker_dir, release_version, maven_sanity_dir) - ) + get_example_dir_cmd = f"ant -f {ant_release_script} update-and-copy-maven-example -Dchecker={checker_dir} -Dversion={release_version} -Ddest.dir={maven_sanity_dir}" execute(get_example_dir_cmd) path_to_artifacts = os.path.join( @@ -146,12 +143,10 @@ def maven_sanity_check(sub_sanity_dir_name, repo_url, release_version): ) if repo_url != "": print( - ( - "This script will now delete your Maven Checker Framework artifacts.\n" - + "See README-release-process.html#Maven-Plugin dependencies. These artifacts " - + "will need to be re-downloaded the next time you need them. This will be " - + "done automatically by Maven next time you use the plugin." - ) + "This script will now delete your Maven Checker Framework artifacts.\n" + + "See README-release-process.html#Maven-Plugin dependencies. These artifacts " + + "will need to be re-downloaded the next time you need them. This will be " + + "done automatically by Maven next time you use the plugin." ) if os.path.isdir(path_to_artifacts): @@ -174,7 +169,7 @@ def check_results(title, output_log, expected_errors): found_errors = are_in_file(output_log, expected_errors) if not found_errors: - raise Exception( + raise ReleaseError( title + " did not work!\n" + "File: " @@ -184,32 +179,29 @@ def check_results(title, output_log, expected_errors): + ", ".join(expected_errors) ) else: - print("%s check: passed!\n" % title) + print(f"{title} check: passed!\n") def add_repo_information(pom, repo_url): """Adds development maven repo to pom file so that the artifacts used are the development artifacts""" - to_insert = """ + to_insert = f""" checker-framework-repo - %s + {repo_url} checker-framework-repo - %s + {repo_url} - """ % ( - repo_url, - repo_url, - ) + """ - result_str = execute('grep -nm 1 "" %s' % pom, True, True).decode() + result_str = execute(f'grep -nm 1 "" {pom}', True, True).decode() line_no_str = result_str.split(":")[0] line_no = int(line_no_str) print(" LINE_NO: " + line_no_str) From 2328a58609c0ddcdd6284028e98e38bafa22c2a2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2026 22:23:12 +0000 Subject: [PATCH 10/13] Update dependency openjdk/jdk to v32 (#1885) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc03de028e2c..44347100ca3c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ env: USE_BAZEL_VERSION: "9.2.0" # JDK 27 early-access build; bump when a new EA build is published. JDK_EA_MAJOR: "27" - JDK_EA_BUILD: "31" + JDK_EA_BUILD: "32" jobs: # Basic sanity tests on the primary JDK. From 89cf608ebbe9bb2e147eef6bef787a6ae58b9990 Mon Sep 17 00:00:00 2001 From: Aosen Xiong Date: Mon, 27 Jul 2026 21:22:20 -0400 Subject: [PATCH 11/13] Expand executable type-variable bound tests --- .../ConstructorTypeVariableBounds.java | 138 ++++++++++++++++-- .../MethodTypeVariableBounds.java | 107 ++++++++++++++ 2 files changed, 236 insertions(+), 9 deletions(-) diff --git a/framework/tests/viewpointtest/ConstructorTypeVariableBounds.java b/framework/tests/viewpointtest/ConstructorTypeVariableBounds.java index 1328c4f347a3..7ce3c7512e6f 100644 --- a/framework/tests/viewpointtest/ConstructorTypeVariableBounds.java +++ b/framework/tests/viewpointtest/ConstructorTypeVariableBounds.java @@ -11,6 +11,24 @@ static class C { C(T t) {} } + static class LowerBoundC { + // The @ReceiverDependentQual annotation on T is its explicit lower bound. The upper bound + // is the implicit Object bound. + @SuppressWarnings({"inconsistent.constructor.type", "super.invocation.invalid"}) + <@ReceiverDependentQual T> LowerBoundC() {} + + @SuppressWarnings({"inconsistent.constructor.type", "super.invocation.invalid"}) + <@ReceiverDependentQual T> LowerBoundC(T t) {} + } + + static class LowerAndUpperBoundC { + @SuppressWarnings({"inconsistent.constructor.type", "super.invocation.invalid"}) + <@ReceiverDependentQual T extends @ReceiverDependentQual Object> LowerAndUpperBoundC() {} + + @SuppressWarnings({"inconsistent.constructor.type", "super.invocation.invalid"}) + <@ReceiverDependentQual T extends @ReceiverDependentQual Object> LowerAndUpperBoundC(T t) {} + } + void topViewpoint(@Top Object top, @A Object a, @B Object b, @Bottom Object bottom) { // Constructed type @Top adapts @ReceiverDependentQual to @Lost. Creating @Top is also // forbidden by the viewpoint test checker. @@ -40,40 +58,142 @@ void topViewpoint(@Top Object top, @A Object a, @B Object b, @Bottom Object bott // :: error: (new.class.type.invalid) new @Top C(bottom); + + // The lower bound @ReceiverDependentQual viewpoint-adapts to @Lost. Explicit type + // arguments must be supertypes of that lower bound, so only @Top is valid. + // :: error: (new.class.type.invalid) + new @Top LowerBoundC(); + + // :: error: (new.class.type.invalid) + new <@Top Object>@Top LowerBoundC(top); + + // :: error: (new.class.type.invalid) :: error: (type.argument.type.incompatible) + new <@A Object>@Top LowerBoundC(a); + + // :: error: (new.class.type.invalid) :: error: (type.argument.type.incompatible) + new <@B Object>@Top LowerBoundC(b); + + // :: error: (new.class.type.invalid) :: error: (type.argument.type.incompatible) + new <@Bottom Object>@Top LowerBoundC(bottom); + + // Inference can choose @Top, which is above both the adapted lower bound and the argument. + // :: error: (new.class.type.invalid) + new @Top LowerBoundC(top); + + // :: error: (new.class.type.invalid) + new @Top LowerBoundC(a); + + // :: error: (new.class.type.invalid) + new @Top LowerBoundC(b); + + // :: error: (new.class.type.invalid) :: error: (type.arguments.not.inferred) + new @Top LowerBoundC(bottom); + + // Both bounds viewpoint-adapt to @Lost. Because @Lost is non-reflexive, no type argument + // can be both above the lower bound and below the upper bound. + // :: error: (new.class.type.invalid) :: error: (type.arguments.not.inferred) + new @Top LowerAndUpperBoundC(); + + // :: error: (new.class.type.invalid) :: error: (type.arguments.not.inferred) + new <@Top Object>@Top LowerAndUpperBoundC(top); + + // :: error: (new.class.type.invalid) :: error: (type.arguments.not.inferred) + new <@A Object>@Top LowerAndUpperBoundC(a); + + // :: error: (new.class.type.invalid) :: error: (type.arguments.not.inferred) + new <@B Object>@Top LowerAndUpperBoundC(b); + + // :: error: (new.class.type.invalid) :: error: (type.arguments.not.inferred) + new <@Bottom Object>@Top LowerAndUpperBoundC(bottom); + + // :: error: (new.class.type.invalid) :: error: (type.arguments.not.inferred) + new @Top LowerAndUpperBoundC(top); + + // :: error: (new.class.type.invalid) :: error: (type.arguments.not.inferred) + new @Top LowerAndUpperBoundC(a); + + // :: error: (new.class.type.invalid) :: error: (type.arguments.not.inferred) + new @Top LowerAndUpperBoundC(b); + + // :: error: (new.class.type.invalid) :: error: (type.arguments.not.inferred) + new @Top LowerAndUpperBoundC(bottom); } + @SuppressWarnings("cast.unsafe.constructor.invocation") void aViewpoint(@Top Object top, @A Object a, @B Object b, @Bottom Object bottom) { // Constructed type @A adapts @ReceiverDependentQual to @A, so @A and @Bottom are within // the adapted constructor type parameter bound. Inference instantiates T to @A for the // no-arg constructor. - // :: warning: (cast.unsafe.constructor.invocation) new @A C(); - // :: error: (type.argument.type.incompatible) :: warning: - // (cast.unsafe.constructor.invocation) + // :: error: (type.argument.type.incompatible) new <@Top Object>@A C(top); - // :: warning: (cast.unsafe.constructor.invocation) new <@A Object>@A C(a); - // :: error: (type.argument.type.incompatible) :: warning: - // (cast.unsafe.constructor.invocation) + // :: error: (type.argument.type.incompatible) new <@B Object>@A C(b); - // :: warning: (cast.unsafe.constructor.invocation) new <@Bottom Object>@A C(bottom); // :: error: (type.arguments.not.inferred) new @A C(top); // Inference succeeds: argument @A is within the adapted bound @A. - // :: warning: (cast.unsafe.constructor.invocation) new @A C(a); // :: error: (type.arguments.not.inferred) new @A C(b); - // :: warning: (cast.unsafe.constructor.invocation) new @A C(bottom); + + // The lower bound @ReceiverDependentQual viewpoint-adapts to @A. Explicit type arguments + // must be supertypes of @A, so @Top and @A are valid. + new @A LowerBoundC(); + + new <@Top Object>@A LowerBoundC(top); + + new <@A Object>@A LowerBoundC(a); + + // :: error: (type.argument.type.incompatible) + new <@B Object>@A LowerBoundC(b); + + // :: error: (type.argument.type.incompatible) + new <@Bottom Object>@A LowerBoundC(bottom); + + // Inference chooses a type argument that is above both @A and the invocation argument. + new @A LowerBoundC(top); + + new @A LowerBoundC(a); + + new @A LowerBoundC(b); + + new @A LowerBoundC(bottom); + + // Both bounds viewpoint-adapt to @A, so an explicit type argument must be exactly @A. + new @A LowerAndUpperBoundC(); + + // :: error: (type.argument.type.incompatible) + new <@Top Object>@A LowerAndUpperBoundC(top); + + new <@A Object>@A LowerAndUpperBoundC(a); + + // :: error: (type.argument.type.incompatible) + new <@B Object>@A LowerAndUpperBoundC(b); + + // :: error: (type.argument.type.incompatible) + new <@Bottom Object>@A LowerAndUpperBoundC(bottom); + + // :: error: (type.arguments.not.inferred) + new @A LowerAndUpperBoundC(top); + + // Inference chooses T = @A. + new @A LowerAndUpperBoundC(a); + + // :: error: (type.arguments.not.inferred) + new @A LowerAndUpperBoundC(b); + + // Inference chooses T = @A, which accepts the @Bottom argument. + new @A LowerAndUpperBoundC(bottom); } } diff --git a/framework/tests/viewpointtest/MethodTypeVariableBounds.java b/framework/tests/viewpointtest/MethodTypeVariableBounds.java index 26693dc0fa34..4035b643983c 100644 --- a/framework/tests/viewpointtest/MethodTypeVariableBounds.java +++ b/framework/tests/viewpointtest/MethodTypeVariableBounds.java @@ -5,6 +5,18 @@ static class Methods { void noArg() {} void withArg(T t) {} + + // The @ReceiverDependentQual annotation on T is its explicit lower bound. The upper bound + // is the implicit Object bound. + <@ReceiverDependentQual T> void lowerNoArg() {} + + <@ReceiverDependentQual T> void lowerWithArg(T t) {} + + <@ReceiverDependentQual T extends @ReceiverDependentQual Object> + void lowerAndUpperNoArg() {} + + <@ReceiverDependentQual T extends @ReceiverDependentQual Object> void lowerAndUpperWithArg( + T t) {} } void topReceiver( @@ -39,6 +51,57 @@ void topReceiver( methods.withArg(b); methods.withArg(bottom); + + // The lower bound @ReceiverDependentQual viewpoint-adapts to @Lost. Explicit type + // arguments must be supertypes of that lower bound, so only @Top is valid. + methods.lowerNoArg(); + methods.<@Top Object>lowerWithArg(top); + + // :: error: (type.argument.type.incompatible) + methods.<@A Object>lowerWithArg(a); + + // :: error: (type.argument.type.incompatible) + methods.<@B Object>lowerWithArg(b); + + // :: error: (type.argument.type.incompatible) + methods.<@Bottom Object>lowerWithArg(bottom); + + // Inference can choose @Top, which is above both the adapted lower bound and the argument. + methods.lowerWithArg(top); + methods.lowerWithArg(a); + methods.lowerWithArg(b); + + // :: error: (type.arguments.not.inferred) + methods.lowerWithArg(bottom); + + // Both bounds viewpoint-adapt to @Lost. Because @Lost is non-reflexive, no type argument + // can be both above the lower bound and below the upper bound. + // :: error: (type.arguments.not.inferred) + methods.lowerAndUpperNoArg(); + + // :: error: (type.argument.type.incompatible) + methods.<@Top Object>lowerAndUpperWithArg(top); + + // :: error: (type.argument.type.incompatible) + methods.<@A Object>lowerAndUpperWithArg(a); + + // :: error: (type.argument.type.incompatible) + methods.<@B Object>lowerAndUpperWithArg(b); + + // :: error: (type.argument.type.incompatible) + methods.<@Bottom Object>lowerAndUpperWithArg(bottom); + + // :: error: (type.arguments.not.inferred) + methods.lowerAndUpperWithArg(top); + + // :: error: (type.arguments.not.inferred) + methods.lowerAndUpperWithArg(a); + + // :: error: (type.arguments.not.inferred) + methods.lowerAndUpperWithArg(b); + + // :: error: (type.arguments.not.inferred) + methods.lowerAndUpperWithArg(bottom); } void aReceiver( @@ -68,5 +131,49 @@ void aReceiver( methods.withArg(b); methods.withArg(bottom); + + // The lower bound @ReceiverDependentQual viewpoint-adapts to @A. Explicit type arguments + // must be supertypes of @A, so @Top and @A are valid. + methods.lowerNoArg(); + methods.<@Top Object>lowerWithArg(top); + methods.<@A Object>lowerWithArg(a); + + // :: error: (type.argument.type.incompatible) + methods.<@B Object>lowerWithArg(b); + + // :: error: (type.argument.type.incompatible) + methods.<@Bottom Object>lowerWithArg(bottom); + + // Inference chooses a type argument that is above both @A and the invocation argument. + methods.lowerWithArg(top); + methods.lowerWithArg(a); + methods.lowerWithArg(b); + methods.lowerWithArg(bottom); + + // Both bounds viewpoint-adapt to @A, so an explicit type argument must be exactly @A. + methods.lowerAndUpperNoArg(); + + // :: error: (type.argument.type.incompatible) + methods.<@Top Object>lowerAndUpperWithArg(top); + + methods.<@A Object>lowerAndUpperWithArg(a); + + // :: error: (type.argument.type.incompatible) + methods.<@B Object>lowerAndUpperWithArg(b); + + // :: error: (type.argument.type.incompatible) + methods.<@Bottom Object>lowerAndUpperWithArg(bottom); + + // :: error: (type.arguments.not.inferred) + methods.lowerAndUpperWithArg(top); + + // Inference chooses T = @A. + methods.lowerAndUpperWithArg(a); + + // :: error: (type.arguments.not.inferred) + methods.lowerAndUpperWithArg(b); + + // Inference chooses T = @A, which accepts the @Bottom argument. + methods.lowerAndUpperWithArg(bottom); } } From f7329538d14b7027773f9df1cb8b0cae829701ae Mon Sep 17 00:00:00 2001 From: Aosen Xiong Date: Mon, 27 Jul 2026 21:50:17 -0400 Subject: [PATCH 12/13] Trigger CI From d3ce2de134f7621781e6ce9657df3933e9721bc6 Mon Sep 17 00:00:00 2001 From: Werner Dietl Date: Wed, 5 Aug 2026 04:51:19 -0400 Subject: [PATCH 13/13] Simplify executable type variable bounds adaptation loops --- .../type/AbstractViewpointAdapter.java | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/framework/type/AbstractViewpointAdapter.java b/framework/src/main/java/org/checkerframework/framework/type/AbstractViewpointAdapter.java index be5e5d90dd65..1660c09d6683 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AbstractViewpointAdapter.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AbstractViewpointAdapter.java @@ -125,16 +125,13 @@ public void viewpointAdaptConstructor( // 2b. Adapt upper and lower bounds of constructor type variables. for (AnnotatedTypeVariable typeVariable : typeVariables) { - if (typeVariable.getUpperBoundField() != null) { - AnnotatedTypeMirror adaptedUpper = - combineTypeWithType(receiverType, typeVariable.getUpperBound()); - mappings.put(typeVariable.getUpperBoundField(), adaptedUpper); - } - if (typeVariable.getLowerBoundField() != null) { - AnnotatedTypeMirror adaptedLower = - combineTypeWithType(receiverType, typeVariable.getLowerBound()); - mappings.put(typeVariable.getLowerBoundField(), adaptedLower); - } + AnnotatedTypeMirror adaptedUpper = + combineTypeWithType(receiverType, typeVariable.getUpperBound()); + mappings.put(typeVariable.getUpperBound(), adaptedUpper); + + AnnotatedTypeMirror adaptedLower = + combineTypeWithType(receiverType, typeVariable.getLowerBound()); + mappings.put(typeVariable.getLowerBound(), adaptedLower); } // 2c. Adapt constructor return type. @@ -183,16 +180,13 @@ public void viewpointAdaptMethod( // 3b. Adapt upper and lower bounds of method type variables. for (AnnotatedTypeVariable typeVariable : typeVariables) { - if (typeVariable.getUpperBoundField() != null) { - AnnotatedTypeMirror adaptedUpper = - combineTypeWithType(receiverType, typeVariable.getUpperBound()); - mappings.put(typeVariable.getUpperBoundField(), adaptedUpper); - } - if (typeVariable.getLowerBoundField() != null) { - AnnotatedTypeMirror adaptedLower = - combineTypeWithType(receiverType, typeVariable.getLowerBound()); - mappings.put(typeVariable.getLowerBoundField(), adaptedLower); - } + AnnotatedTypeMirror adaptedUpper = + combineTypeWithType(receiverType, typeVariable.getUpperBound()); + mappings.put(typeVariable.getUpperBound(), adaptedUpper); + + AnnotatedTypeMirror adaptedLower = + combineTypeWithType(receiverType, typeVariable.getLowerBound()); + mappings.put(typeVariable.getLowerBound(), adaptedLower); } // 3c. Adapt non-void return type.