From d0278ea7b804a787a1851dd82333d60b79b2064c Mon Sep 17 00:00:00 2001 From: Paul Berry Date: Sat, 21 Feb 2026 10:59:44 -0800 Subject: [PATCH] [flow analysis] Test post increment/decrement demotion. Adds a language test and a flow analysis unit test to cover a flow analysis behavior of post-increment and post-decrement operators that wasn't previously covered. The tests verify that the expressions `x++` and `x--` demote `x` in the same way that `x = x + 1` and `x = x - 1` would. This demotion is only user-visible if the type of `x` is a user-defined type. In the process of writing these tests, I noticed that the "mini-AST" implementation of post-increment (which is used solely for flow analysis unit testing) was not correct; it presumed that the type read from the target, the type written to it, and the type of the whole expression were all the same. This is not correct; the type written to the target is determined by the return type of the `+` operator. I've fixed this as part of this CL so that the unit test properly exercises flow analysis. I will follow this up with some refactoring of how flow analysis handles post increment/decrement operations. Landing the test first allows us to be confident that the refactor won't change the tested behavior. Change-Id: I6a6a6964417b48db0c1681c06d7418bd79e96357 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/482342 Commit-Queue: Paul Berry Reviewed-by: Konstantin Shcheglov --- .../flow_analysis/flow_analysis_test.dart | 18 ++++++ pkg/_fe_analyzer_shared/test/mini_ast.dart | 16 ++++- .../postfix_increment_decrement_test.dart | 60 +++++++++++++++++++ 3 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 tests/language/nnbd/flow_analysis/postfix_increment_decrement_test.dart diff --git a/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart b/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart index 2a766398d9e..03b125a0dc8 100644 --- a/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart +++ b/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart @@ -2090,6 +2090,24 @@ main() { ]); }); + test('postIncDec() demotes to the written type', () { + // If `x` has type B, but is promoted to subtype C and then D, and D + // has a `+` operator that returns C, then after `x++`, `x` should be + // demoted to C. + var x = Var('x'); + h.addSuperInterfaces('B', (_) => [Type('Object')]); + h.addSuperInterfaces('C', (_) => [Type('B'), Type('Object')]); + h.addSuperInterfaces('D', (_) => [Type('C'), Type('B'), Type('Object')]); + h.addMember('D', '+', 'C Function(int)'); + h.run([ + declare(x, initializer: expr('B')), + x.as_('C'), + x.as_('D'), + x.postIncDec(), + checkPromoted(x, 'C'), + ]); + }); + test('switchExpression throw in scrutinee makes all cases unreachable', () { h.run([ switchExpr(throw_(expr('C')), [ diff --git a/pkg/_fe_analyzer_shared/test/mini_ast.dart b/pkg/_fe_analyzer_shared/test/mini_ast.dart index ae018a85b66..e59117410f2 100644 --- a/pkg/_fe_analyzer_shared/test/mini_ast.dart +++ b/pkg/_fe_analyzer_shared/test/mini_ast.dart @@ -1790,6 +1790,7 @@ class Harness { 'int.>=': Type('bool Function(num)'), 'int.abs': Type('int Function()'), 'int.isEven': Type('bool'), + 'num.+': Type('num Function(num)'), 'num.sign': Type('num'), 'Object.toString': Type('String Function()'), }; @@ -4368,12 +4369,21 @@ class PostIncDec extends Expression { @override ExpressionTypeAnalysisResult visit(Harness h, SharedTypeSchemaView schema) { - Type type = h.typeAnalyzer + Type operandType = h.typeAnalyzer .analyzeExpression(lhs, h.operations.unknownType) .type .unwrapTypeView(); - lhs._visitPostIncDec(h, this, type); - return new ExpressionTypeAnalysisResult(type: SharedTypeView(type)); + var member = h.getMember(operandType, '+'); + if (member == null) { + fail('No + operator found for $operandType'); + } + var memberType = member._type; + if (memberType is! FunctionType) { + fail('Expected function type for + operator, got $memberType'); + } + var writtenType = memberType.returnType; + lhs._visitPostIncDec(h, this, writtenType); + return new ExpressionTypeAnalysisResult(type: SharedTypeView(operandType)); } } diff --git a/tests/language/nnbd/flow_analysis/postfix_increment_decrement_test.dart b/tests/language/nnbd/flow_analysis/postfix_increment_decrement_test.dart new file mode 100644 index 00000000000..60fc2fcca10 --- /dev/null +++ b/tests/language/nnbd/flow_analysis/postfix_increment_decrement_test.dart @@ -0,0 +1,60 @@ +// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:expect/static_type_helper.dart'; + +// This test checks that `x++` and `x--` demote `x` to the type returned by the +// `+` or `-` operator defined on the previously-promoted type of `x`. +// +// It further verifies that when `x` is `int`, `x++` and `x--` do not demote to +// `num`, because of the special type inference rule that adding or subtracting +// two ints yields an int. + +increment_int(num x) { + if (x is int) { + x++; + x.expectStaticType>(); + } +} + +decrement_int(num x) { + if (x is int) { + x--; + x.expectStaticType>(); + } +} + +increment_userDefinedType(B x) { + if (x is C) { + if (x is D) { + x++; + x.expectStaticType>(); + } + } +} + +decrement_userDefinedType(B x) { + if (x is C) { + if (x is D) { + x++; + x.expectStaticType>(); + } + } +} + +class B {} + +class C extends B {} + +class D extends C { + C operator +(int i) => this; + C operator -(int i) => this; +} + +main() { + increment_int(0); + decrement_int(0); + increment_userDefinedType(D()); + decrement_userDefinedType(D()); +}