diff --git a/pkg/_fe_analyzer_shared/lib/src/flow_analysis/flow_analysis.dart b/pkg/_fe_analyzer_shared/lib/src/flow_analysis/flow_analysis.dart index 0edf67491f7..7d5c28744d7 100644 --- a/pkg/_fe_analyzer_shared/lib/src/flow_analysis/flow_analysis.dart +++ b/pkg/_fe_analyzer_shared/lib/src/flow_analysis/flow_analysis.dart @@ -750,6 +750,7 @@ abstract class FlowAnalysis< required bool isFinal, required bool isLate, required bool isImplicitlyTyped, + bool inheritPromotableProperties = false, }); /// Whether the [variable] is definitely assigned in the current state. @@ -1195,6 +1196,12 @@ abstract class FlowAnalysis< SharedTypeView scrutineeType, ); + /// Call this method just before changing the binding of `this`. + void thisBinding_begin(ExpressionInfo? targetInfo); + + /// Call this method just after the end of a `this` binding. + void thisBinding_end(); + /// Call this method just after visiting the expression `this` (or the /// pseudo-expression `super`, in the case of the analyzer, which represents /// `super.x` as a property get whose target is `super`). @@ -1956,11 +1963,13 @@ class FlowAnalysisDebug< required bool isFinal, required bool isLate, required bool isImplicitlyTyped, + bool inheritPromotableProperties = false, }) { _wrap( 'initialize($variable, $matchedType, $initializerExpressionInfo, ' 'isFinal: $isFinal, isLate: $isLate, ' - 'isImplicitlyTyped: $isImplicitlyTyped)', + 'isImplicitlyTyped: $isImplicitlyTyped, ' + 'inheritPromotableProperties: $inheritPromotableProperties)', () => _wrapped.initialize( variable, matchedType, @@ -1968,6 +1977,7 @@ class FlowAnalysisDebug< isFinal: isFinal, isLate: isLate, isImplicitlyTyped: isImplicitlyTyped, + inheritPromotableProperties: inheritPromotableProperties, ), ); } @@ -2498,6 +2508,19 @@ class FlowAnalysisDebug< ); } + @override + void thisBinding_begin(ExpressionInfo? targetInfo) { + _wrap( + 'thisBinding_begin($targetInfo)', + () => _wrapped.thisBinding_begin(targetInfo), + ); + } + + @override + void thisBinding_end() { + _wrap('thisBinding_end()', () => _wrapped.thisBinding_end()); + } + @override ExpressionInfo thisOrSuper( SharedTypeView staticType, { @@ -5173,8 +5196,10 @@ class _FlowAnalysisImpl< @override late final SsaNode _superSsaNode = new SsaNode(); + final List _thisSsaNodes = [new SsaNode()]; + @override - late final SsaNode _thisSsaNode = new SsaNode(); + SsaNode get _thisSsaNode => _thisSsaNodes.last; @override final List<_Reference> _cascadeTargetStack = []; @@ -5845,6 +5870,7 @@ class _FlowAnalysisImpl< required bool isFinal, required bool isLate, required bool isImplicitlyTyped, + bool inheritPromotableProperties = false, }) { SharedTypeView unpromotedType = operations.variableType(variable); int variableKey = promotionKeyStore.keyForVariable(variable); @@ -5856,6 +5882,7 @@ class _FlowAnalysisImpl< isLate: isLate, isImplicitlyTyped: isImplicitlyTyped, unpromotedType: unpromotedType, + inheritPromotableProperties: inheritPromotableProperties, ); } @@ -6643,6 +6670,24 @@ class _FlowAnalysisImpl< } } + @override + void thisBinding_begin(ExpressionInfo? targetInfo) { + _Reference? expressionReference = _getExpressionReference(targetInfo); + SsaNode ssaNode = + expressionReference?.ssaNode ?? + new SsaNode( + conditionVariableState: targetInfo != null && targetInfo.isNonTrivial + ? targetInfo + : null, + ); + _thisSsaNodes.add(ssaNode); + } + + @override + void thisBinding_end() { + _thisSsaNodes.removeLast(); + } + @override ExpressionInfo thisOrSuper( SharedTypeView staticType, { @@ -7445,6 +7490,7 @@ class _FlowAnalysisImpl< required bool isLate, required bool isImplicitlyTyped, required SharedTypeView unpromotedType, + bool inheritPromotableProperties = false, }) { if (isLate) { // Don't use expression info for late variables, since we don't know when @@ -7458,12 +7504,15 @@ class _FlowAnalysisImpl< // https://github.com/dart-lang/language/issues/1785. expressionInfo = null; } - SsaNode newSsaNode = new SsaNode( - conditionVariableState: - expressionInfo != null && expressionInfo.isNonTrivial - ? expressionInfo - : null, - ); + SsaNode newSsaNode = + inheritPromotableProperties && expressionInfo is _Reference + ? expressionInfo.ssaNode + : new SsaNode( + conditionVariableState: + expressionInfo != null && expressionInfo.isNonTrivial + ? expressionInfo + : null, + ); _current = _current.write( this, null, @@ -7731,16 +7780,23 @@ class _FlowAnalysisImpl< ).restoreConditionVariableState(scrutineeInfo, this, _current); } - TrivialVariableReference _thisOrSuperReference( + _Reference _thisOrSuperReference( SharedTypeView staticType, { required bool isSuper, - }) => new TrivialVariableReference( - promotionKey: promotionKeyStore.thisPromotionKey, - model: _current, - type: staticType, - isThisOrSuper: true, - ssaNode: isSuper ? _superSsaNode : _thisSsaNode, - ); + }) { + SsaNode ssaNode = isSuper ? _superSsaNode : _thisSsaNode; + return new TrivialVariableReference( + promotionKey: promotionKeyStore.thisPromotionKey, + model: _current, + type: staticType, + isThisOrSuper: true, + ssaNode: ssaNode, + ).restoreConditionVariableState( + ssaNode.conditionVariableState, + this, + _current, + ); + } TrivialVariableReference _variableReference( int variableKey, 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 b442da7ee0e..592e526defa 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 @@ -12827,6 +12827,165 @@ main() { checkAssigned(branch1, false), ]); }); + + test('Anonymous method with target having no ExpressionInfo', () { + h.run([ + expr( + 'A', + ).invokeAnonymousMethod([checkReachable(true)], returnType: 'void'), + ]); + }); + + test('Anonymous method has a different notion of this', () { + h.addMember('C', '_field', 'Object', promotable: true); + h.thisType = 'C'; + h.run([ + this_.as_('C'), + this_.property('_field').as_('num'), + checkPromoted(this_.property('_field'), 'num'), + expr('C').invokeAnonymousMethod([ + checkNotPromoted(this_.property('_field')), + this_.property('_field').as_('int'), + checkPromoted(this_.property('_field'), 'int'), + ], returnType: 'void'), + checkPromoted(this_.property('_field'), 'num'), + ]); + }); + + test( + 'Anonymous method with target having ExpressionInfo but not a Reference', + () { + h.addMember('C', '_field', 'Object', promotable: true); + h.thisType = 'C'; + h.run([ + this_.as_('C'), + this_.property('_field').as_('num'), + checkPromoted(this_.property('_field'), 'num'), + expr('bool').conditional(expr('C'), expr('C')).invokeAnonymousMethod([ + checkNotPromoted(this_.property('_field')), + this_.property('_field').as_('int'), + checkPromoted(this_.property('_field'), 'int'), + ], returnType: 'void'), + checkPromoted(this_.property('_field'), 'num'), + ]); + }, + ); + + test('Anonymous method promotes this._field ' + 'from localVariable._field and vice versa', () { + var x = Var('x'); + h.addMember('A', '_field', 'Object', promotable: true); + h.run([ + declare(x, type: 'A', initializer: expr('A')), + x.property('_field').as_('num'), + x.invokeAnonymousMethod([ + checkPromoted(this_.property('_field'), 'num'), + this_.property('_field').as_('int'), + ], returnType: 'void'), + checkPromoted(x.property('_field'), 'int'), + ]); + }); + + test('Anonymous method promotes this._field ' + 'from instanceVariable._field and vice versa', () { + h.addMember('A', '_field', 'Object', promotable: true); + h.addMember('B', '_subField', 'Object', promotable: true); + h.thisType = 'A'; + h.run([ + this_.as_('A'), + this_.property('_field').as_('B'), + this_.property('_field').property('_subField').as_('num'), + this_.property('_field').invokeAnonymousMethod([ + checkPromoted(this_.property('_subField'), 'num'), + this_.property('_subField').as_('int'), + ], returnType: 'void'), + checkPromoted(this_.property('_field').property('_subField'), 'int'), + ]); + }); + + test('Parameterized anonymous method has the same notion of this', () { + h.addMember('C', '_field', 'Object', promotable: true); + h.thisType = 'C'; + h.run([ + this_.as_('C'), + this_.property('_field').as_('num'), + checkPromoted(this_.property('_field'), 'num'), + expr('C').invokeAnonymousMethod(isParameterless: false, [ + checkPromoted(this_.property('_field'), 'num'), + this_.property('_field').as_('int'), + checkPromoted(this_.property('_field'), 'int'), + ], returnType: 'void'), + checkPromoted(this_.property('_field'), 'int'), + ]); + }); + + test('Parameterized anonymous method with target having ExpressionInfo ' + 'but not a Reference', () { + h.addMember('C', '_field', 'Object', promotable: true); + h.thisType = 'C'; + h.run([ + this_.as_('C'), + this_.property('_field').as_('num'), + checkPromoted(this_.property('_field'), 'num'), + expr('bool') + .conditional(expr('C'), expr('C')) + .invokeAnonymousMethod(isParameterless: false, [ + checkPromoted(this_.property('_field'), 'num'), + this_.property('_field').as_('int'), + checkPromoted(this_.property('_field'), 'int'), + ], returnType: 'void'), + checkPromoted(this_.property('_field'), 'int'), + ]); + }); + + test('Parameterized anonymous method parameter inherits promotion', () { + var x = Var('x'); + var p = Var('p'); + h.addMember('A', '_field', 'Object', promotable: true); + h.thisType = 'A'; + h.run([ + declare(x, type: 'A', initializer: expr('A')), + x.property('_field').as_('num'), + x.invokeAnonymousMethod(isParameterless: false, parameter: p, [ + checkPromoted(p.property('_field'), 'num'), + p.property('_field').as_('int'), + ], returnType: 'void'), + checkPromoted(x.property('_field'), 'int'), + ]); + }); + + test('Parameterized anonymous method promotes parameter._field ' + 'from instanceVariable._field and vice versa', () { + var p = Var('p'); + h.addMember('A', '_field', 'B', promotable: true); + h.addMember('B', '_subField', 'Object', promotable: true); + h.thisType = 'A'; + h.run([ + this_.as_('A'), + this_.property('_field').as_('B'), + this_.property('_field').property('_subField').as_('num'), + this_ + .property('_field') + .invokeAnonymousMethod(isParameterless: false, parameter: p, [ + checkPromoted(p.property('_subField'), 'num'), + p.property('_subField').as_('int'), + ], returnType: 'void'), + checkPromoted(this_.property('_field').property('_subField'), 'int'), + ]); + }); + + test('Anonymous method this serves as condition variable', () { + var x = Var('x'); + h.run([ + declare(x, type: 'int?', initializer: expr('int?')), + x.eq(nullLiteral).not.invokeAnonymousMethod(isParameterless: true, [ + this_.conditional( + checkPromoted(x, 'int'), + expr('bool'), + ), + ], returnType: 'bool'), + ]); + }); }); } diff --git a/pkg/_fe_analyzer_shared/test/mini_ast.dart b/pkg/_fe_analyzer_shared/test/mini_ast.dart index 9a0d0c6fd98..26c3c13e958 100644 --- a/pkg/_fe_analyzer_shared/test/mini_ast.dart +++ b/pkg/_fe_analyzer_shared/test/mini_ast.dart @@ -2453,17 +2453,26 @@ class InvokeAnonymousMethod extends Expression { final bool isNullAware; + final bool isParameterless; + + final Var? parameter; + InvokeAnonymousMethod._( this.target, this.body, { required this.returnType, required this.isNullAware, + required this.isParameterless, + this.parameter, required super.location, }); @override void preVisit(PreVisitor visitor) { target.preVisit(visitor); + if (parameter != null) { + visitor._assignedVariables.declare(parameter!); + } body.preVisit(visitor); } @@ -2484,10 +2493,40 @@ class InvokeAnonymousMethod extends Expression { if (isNullAware) { targetResult = h.typeAnalyzer.createNullAwareGuard(target, targetResult); } + var targetInfo = targetResult.flowAnalysisInfo; + var previousThisType = h._thisType; + if (isParameterless) { + h.flow.thisBinding_begin(targetInfo); + h._thisType = targetResult.type.unwrapTypeView(); + } h.flow.anonymousBlockBody_begin(); + if (parameter != null) { + bool isImplicitlyTyped = parameter!._type == null; + if (parameter!._type == null) { + parameter!._type = targetResult.type.unwrapTypeView(); + } + h.flow.declare( + parameter!, + SharedTypeView(parameter!.type), + initialized: false, + ); + h.flow.initialize( + parameter!, + targetResult.type, + targetInfo, + isFinal: false, + isLate: false, + isImplicitlyTyped: isImplicitlyTyped, + inheritPromotableProperties: true, + ); + } // Analyze the block, and generate its IR. body.visit(h); h.flow.anonymousBlockBody_end(); + if (isParameterless) { + h._thisType = previousThisType; + h.flow.thisBinding_end(); + } // Form the IR for the anonymous method invocation. h.irBuilder.apply( 'anonymous-method', @@ -4971,6 +5010,8 @@ mixin ProtoExpression List body, { required String returnType, bool isNullAware = false, + bool isParameterless = true, + Var? parameter, }) { var location = computeLocation(); return new InvokeAnonymousMethod._( @@ -4978,6 +5019,8 @@ mixin ProtoExpression Block._(body, location: location), returnType: Type(returnType), isNullAware: isNullAware, + isParameterless: isParameterless, + parameter: parameter, location: location, ); } @@ -7129,6 +7172,7 @@ class _MiniAstTypeAnalyzer SharedTypeView(thisType), isSuper: false, ); + flow.storeExpressionInfo(node, flowAnalysisInfo); return new ExpressionTypeAnalysisResult( type: SharedTypeView(thisType), flowAnalysisInfo: flowAnalysisInfo, @@ -7151,6 +7195,9 @@ class _MiniAstTypeAnalyzer SharedTypeView(memberType), ); var promotedType = wrappedPromotedType?.unwrapTypeView(); + if (flowAnalysisInfo != null) { + flow.storeExpressionInfo(node, flowAnalysisInfo); + } return new ExpressionTypeAnalysisResult( type: SharedTypeView(promotedType ?? memberType), flowAnalysisInfo: flowAnalysisInfo, @@ -7249,6 +7296,7 @@ class _MiniAstTypeAnalyzer ) { var (promotedType, flowAnalysisInfo) = flow.variableRead(variable); callback?.call(promotedType?.unwrapTypeView()); + flow.storeExpressionInfo(node, flowAnalysisInfo); return new ExpressionTypeAnalysisResult( type: promotedType ?? SharedTypeView(variable.type), flowAnalysisInfo: flowAnalysisInfo, @@ -7843,6 +7891,9 @@ class _MiniAstTypeAnalyzer member, SharedTypeView(memberType), ); + if (propertyGetNode != null && flowAnalysisInfo != null) { + flow.storeExpressionInfo(propertyGetNode, flowAnalysisInfo); + } return ExpressionTypeAnalysisResult( type: wrappedPromotedType ?? SharedTypeView(memberType), flowAnalysisInfo: flowAnalysisInfo, diff --git a/pkg/analyzer/lib/src/generated/resolver.dart b/pkg/analyzer/lib/src/generated/resolver.dart index a922fb0206d..c331922079e 100644 --- a/pkg/analyzer/lib/src/generated/resolver.dart +++ b/pkg/analyzer/lib/src/generated/resolver.dart @@ -1904,6 +1904,7 @@ class ResolverVisitor extends ThrowingAstVisitor node.visitChildren(this); var returnType = _finishFunctionBodyInference(); flowAnalysis.flow?.anonymousBlockBody_end(); + return returnType; } finally { _bodyContext = oldBodyContext; @@ -1916,11 +1917,13 @@ class ResolverVisitor extends ThrowingAstVisitor TypeImpl? imposedType, }) { checkUnreachableNode(node); + analyzeExpression( node.expression, SharedTypeSchemaView(imposedType ?? UnknownInferredType.instance), ); popRewrite(); + return node.expression.staticType ?? typeProvider.dynamicType; } @@ -1975,11 +1978,33 @@ class ResolverVisitor extends ThrowingAstVisitor for (var parameter in parameters.parameters) { var element = parameter.declaredFragment?.element; if (element != null) { - flow.declare( - element, - SharedTypeView(element.type), - initialized: true, - ); + if (parameter == parameters.parameters.first) { + flow.declare( + element, + SharedTypeView(element.type), + initialized: false, + ); + flow.initialize( + element, + SharedTypeView(element.type), + target != null + ? flowAnalysis.flow?.getExpressionInfo(target) + : null, + isFinal: false, + isLate: false, + isImplicitlyTyped: parameter.type == null, + inheritPromotableProperties: true, + ); + } else { + // An error will occur because there are multiple parameters, but + // those extra parameters should still allow for meaningful analysis + // in the body of the anonymous method. + flow.declare( + element, + SharedTypeView(element.type), + initialized: true, + ); + } } } } @@ -1988,9 +2013,15 @@ class ResolverVisitor extends ThrowingAstVisitor if (parameters == null) { var oldThisType = _thisType; _thisType = parameterType; + var target = node.target; + var targetInfo = target != null + ? flowAnalysis.flow?.getExpressionInfo(target) + : null; + flowAnalysis.flow?.thisBinding_begin(targetInfo); try { returnedType = node.body.resolve(this, contextType); } finally { + flowAnalysis.flow?.thisBinding_end(); _thisType = oldThisType; } } else { diff --git a/pkg/front_end/lib/src/type_inference/inference_visitor.dart b/pkg/front_end/lib/src/type_inference/inference_visitor.dart index eb85e71d815..341d6681894 100644 --- a/pkg/front_end/lib/src/type_inference/inference_visitor.dart +++ b/pkg/front_end/lib/src/type_inference/inference_visitor.dart @@ -11868,7 +11868,16 @@ class InferenceVisitorImpl extends InferenceVisitorBase flowAnalysis.declare( node.variable, new SharedTypeView(node.variable.type), - initialized: true, + initialized: false, + ); + flowAnalysis.initialize( + node.variable, + new SharedTypeView(node.variable.type), + flowAnalysis.getExpressionInfo(node.variable.initializer!), + isFinal: false, + isLate: false, + isImplicitlyTyped: node.isImplicitlyTyped, + inheritPromotableProperties: true, ); if (node.isNullAware) { flow.nullAwareAccess_rightBegin( @@ -11878,11 +11887,20 @@ class InferenceVisitorImpl extends InferenceVisitorBase ); } + bool isParameterless = node.variable.isSynthesized; + if (isParameterless) { + flow.thisBinding_begin( + flowAnalysis.getExpressionInfo(node.variable.initializer!), + ); + } ExpressionInferenceResult bodyResult = inferExpression( node.body, typeContext, isVoidAllowed: true, ); + if (isParameterless) { + flow.thisBinding_end(); + } if (node.isNullAware) { flow.nullAwareAccess_end(); diff --git a/tests/language/anonymous_methods/block/condition_variable_test.dart b/tests/language/anonymous_methods/block/condition_variable_test.dart new file mode 100644 index 00000000000..53ee64c54e6 --- /dev/null +++ b/tests/language/anonymous_methods/block/condition_variable_test.dart @@ -0,0 +1,17 @@ +// 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. + +// SharedOptions=--enable-experiment=anonymous-methods + +import 'package:expect/expect.dart'; + +void main() { + final int? x = 2; + (x != null).{ + Expect.isTrue(this ? x.isEven : false); + }; + (x != null).(b) { + Expect.isTrue(b ? x.isEven : false); + }; +} diff --git a/tests/language/anonymous_methods/expression/condition_variable_test.dart b/tests/language/anonymous_methods/expression/condition_variable_test.dart new file mode 100644 index 00000000000..ddbc1dc421f --- /dev/null +++ b/tests/language/anonymous_methods/expression/condition_variable_test.dart @@ -0,0 +1,13 @@ +// 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. + +// SharedOptions=--enable-experiment=anonymous-methods + +import 'package:expect/expect.dart'; + +void main() { + final int? x = 2; + Expect.isTrue((x != null).=> this ? x.isEven : false); + Expect.isTrue((x != null).(b) => b ? x.isEven : false); +} diff --git a/tests/language/anonymous_methods/expression/extension_member_test.dart b/tests/language/anonymous_methods/expression/extension_member_test.dart index 14385990a7e..014da992eab 100644 --- a/tests/language/anonymous_methods/expression/extension_member_test.dart +++ b/tests/language/anonymous_methods/expression/extension_member_test.dart @@ -6,7 +6,6 @@ // SharedOptions=--enable-experiment=anonymous-methods import 'package:expect/expect.dart'; -import '../../static_type_helper.dart'; extension on int { int get g1 => this + '$this'.=> this.length; diff --git a/tests/language/anonymous_methods/expression/this_access_test.dart b/tests/language/anonymous_methods/expression/this_access_test.dart index 1a552356b9b..b9e32883ff1 100644 --- a/tests/language/anonymous_methods/expression/this_access_test.dart +++ b/tests/language/anonymous_methods/expression/this_access_test.dart @@ -6,7 +6,6 @@ // SharedOptions=--enable-experiment=anonymous-methods import 'package:expect/expect.dart'; -import '../../static_type_helper.dart'; class A { final int x = 'first'.=> this.length + length;