diff --git a/pkg/_fe_analyzer_shared/lib/src/type_inference/type_analyzer.dart b/pkg/_fe_analyzer_shared/lib/src/type_inference/type_analyzer.dart index 59c9a5ddda8..4c7d0987c20 100644 --- a/pkg/_fe_analyzer_shared/lib/src/type_inference/type_analyzer.dart +++ b/pkg/_fe_analyzer_shared/lib/src/type_inference/type_analyzer.dart @@ -2345,6 +2345,18 @@ mixin TypeAnalyzer< /// Queries whether [pattern] is a variable pattern. bool isVariablePattern(Node pattern); + /// Pops the top of the [_dotShorthands] stack when we're finished resolving + /// the dot shorthand head that requires the recent-most context. + void popDotShorthandContext() { + _dotShorthands.removeLast(); + } + + /// Pushes the [context] onto the stack to use when we resolve the dot + /// shorthand head. + void pushDotShorthandContext(SharedTypeSchemaView context) { + _dotShorthands.add(context); + } + /// Returns the type of the property in [receiverType] that corresponds to /// the name of the [field]. If the property cannot be resolved, the client /// should report an error, and return `dynamic` for recovery. diff --git a/pkg/analyzer/lib/src/dart/ast/ast.dart b/pkg/analyzer/lib/src/dart/ast/ast.dart index ef4e6764766..2d32e818ed8 100644 --- a/pkg/analyzer/lib/src/dart/ast/ast.dart +++ b/pkg/analyzer/lib/src/dart/ast/ast.dart @@ -5407,6 +5407,7 @@ abstract final class DotShorthandInvocation extends InvocationExpression { } final class DotShorthandInvocationImpl extends InvocationExpressionImpl + with DotShorthandMixin implements DotShorthandInvocation { @override final Token period; @@ -5466,6 +5467,13 @@ final class DotShorthandInvocationImpl extends InvocationExpressionImpl } } +base mixin DotShorthandMixin on AstNodeImpl { + /// Whether the AST node is a dot shorthand and has a dot shorthand head + /// ([DotShorthandInvocation] or [DotShorthandPropertyAccess]) as its + /// inner-most target. + bool isDotShorthand = false; +} + /// A node that represents a dot shorthand property access of a field or a /// static getter. /// @@ -5483,6 +5491,7 @@ abstract final class DotShorthandPropertyAccess extends Expression { } final class DotShorthandPropertyAccessImpl extends ExpressionImpl + with DotShorthandMixin implements DotShorthandPropertyAccess { @override final Token period; diff --git a/pkg/analyzer/lib/src/dart/resolver/method_invocation_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/method_invocation_resolver.dart index d21fb245a85..6597c4903e8 100644 --- a/pkg/analyzer/lib/src/dart/resolver/method_invocation_resolver.dart +++ b/pkg/analyzer/lib/src/dart/resolver/method_invocation_resolver.dart @@ -56,7 +56,7 @@ class MethodInvocationResolver with ScopeHelpers { final InvocationInferenceHelper _inferenceHelper; /// The invocation being resolved. - MethodInvocationImpl? _invocation; + InvocationExpressionImpl? _invocation; /// The [Name] object of the invocation being resolved by [resolve]. Name? _currentName; @@ -207,6 +207,8 @@ class MethodInvocationResolver with ScopeHelpers { FunctionExpressionInvocationImpl? resolveDotShorthand( DotShorthandInvocationImpl node, List whyNotPromotedArguments) { + _invocation = node; + var contextType = _resolver.getDotShorthandContext().unwrapTypeSchemaView(); // TODO(kallentu): Dot shorthands work - Support other context types if (contextType is InterfaceTypeImpl) { diff --git a/pkg/analyzer/lib/src/fasta/ast_builder.dart b/pkg/analyzer/lib/src/fasta/ast_builder.dart index 19529e63da4..5489537bb4c 100644 --- a/pkg/analyzer/lib/src/fasta/ast_builder.dart +++ b/pkg/analyzer/lib/src/fasta/ast_builder.dart @@ -4066,20 +4066,44 @@ class AstBuilder extends StackListener { ); } - // TODO(kallentu): Handle dot shorthands. + var dotShorthand = pop() as ExpressionImpl; + if (dotShorthand is DotShorthandMixin) { + (dotShorthand as DotShorthandMixin).isDotShorthand = true; + } + // TODO(kallentu): Add this assert once we've applied the DotShorthandMixin + // on all possible expressions that can be a dot shorthand. + // } else { + // assert( + // false, + // "'$dotShorthand' must be a 'DotShorthandMixin' because we " + // "should only call 'handleDotShorthandContext' after parsing " + // "expressions that have a context type we can cache."); + // } + push(dotShorthand); } @override - void handleDotShorthandHead(Token token) { + void handleDotShorthandHead(Token periodToken) { debugEvent("DotShorthandHead"); if (!enabledDotShorthands) { _reportFeatureNotEnabled( feature: ExperimentalFeatures.dot_shorthands, - startToken: token, + startToken: periodToken, ); } - // TODO(kallentu): Handle dot shorthands. + var operand = pop() as ExpressionImpl; + // TODO(kallentu): Handle property access case. + if (operand is MethodInvocationImpl) { + push(DotShorthandInvocationImpl( + period: periodToken, + memberName: operand.methodName, + typeArguments: operand.typeArguments, + argumentList: operand.argumentList, + )); + } else { + push(operand); + } } @override diff --git a/pkg/analyzer/lib/src/generated/element_resolver.dart b/pkg/analyzer/lib/src/generated/element_resolver.dart index 33fe584ce02..8e37daaa98b 100644 --- a/pkg/analyzer/lib/src/generated/element_resolver.dart +++ b/pkg/analyzer/lib/src/generated/element_resolver.dart @@ -163,6 +163,19 @@ class ElementResolver { _resolveAnnotations(node.metadata); } + /// Resolves the dot shorthand invocation, [node]. + /// + /// If [node] is rewritten to be a [FunctionExpressionInvocation] in the + /// process, then returns that new node. Otherwise, returns `null`. + FunctionExpressionInvocationImpl? visitDotShorthandInvocation( + covariant DotShorthandInvocationImpl node, + {List? whyNotPromotedArguments, + required TypeImpl contextType}) { + whyNotPromotedArguments ??= []; + return _methodInvocationResolver.resolveDotShorthand( + node, whyNotPromotedArguments); + } + void visitEnumConstantDeclaration(EnumConstantDeclaration node) { _resolveAnnotations(node.metadata); } diff --git a/pkg/analyzer/lib/src/generated/resolver.dart b/pkg/analyzer/lib/src/generated/resolver.dart index edb8ad6908a..4f51a695b75 100644 --- a/pkg/analyzer/lib/src/generated/resolver.dart +++ b/pkg/analyzer/lib/src/generated/resolver.dart @@ -2278,9 +2278,40 @@ class ResolverVisitor extends ThrowingAstVisitor } @override - void visitDotShorthandInvocation(DotShorthandInvocation node, + void visitDotShorthandInvocation(covariant DotShorthandInvocationImpl node, {TypeImpl contextType = UnknownInferredType.instance}) { - throw UnimplementedError('TODO(kallentu)'); + inferenceLogWriter?.enterExpression(node, contextType); + + // If [isDotShorthand] is set, cache the context type for resolution. + if (node.isDotShorthand) { + pushDotShorthandContext(SharedTypeSchemaView(contextType)); + } + + checkUnreachableNode(node); + var whyNotPromotedArguments = + Function()>[]; + + node.typeArguments?.accept(this); + var functionRewrite = elementResolver.visitDotShorthandInvocation(node, + whyNotPromotedArguments: whyNotPromotedArguments, + contextType: contextType); + // TODO(kallentu): Handle constructors. + if (functionRewrite is FunctionExpressionInvocationImpl) { + _resolveRewrittenFunctionExpressionInvocation( + functionRewrite, whyNotPromotedArguments, + contextType: contextType); + } + var replacement = + insertGenericFunctionInstantiation(node, contextType: contextType); + checkForArgumentTypesNotAssignableInList( + node.argumentList, whyNotPromotedArguments); + _insertImplicitCallReference(replacement, contextType: contextType); + + if (node.isDotShorthand) { + popDotShorthandContext(); + } + + inferenceLogWriter?.exitExpression(node); } @override @@ -4279,6 +4310,8 @@ class ResolverVisitor extends ThrowingAstVisitor name = nameNodeName is PrefixedIdentifier ? nameNodeName.identifier.name : '${nameNodeName.name}.new'; + } else if (nameNode is DotShorthandInvocation) { + name = nameNode.memberName.name; } else { throw UnimplementedError('(${nameNode.runtimeType}) $nameNode'); } diff --git a/pkg/analyzer/lib/src/test_utilities/find_node.dart b/pkg/analyzer/lib/src/test_utilities/find_node.dart index ebfc5a3cf54..3446b0978b3 100644 --- a/pkg/analyzer/lib/src/test_utilities/find_node.dart +++ b/pkg/analyzer/lib/src/test_utilities/find_node.dart @@ -59,6 +59,8 @@ class FindNode { ConstructorFieldInitializer get singleConstructorFieldInitializer => _single(); + DotShorthandInvocation get singleDotShorthandInvocation => _single(); + EnumDeclaration get singleEnumDeclaration => _single(); ExportDirective get singleExportDirective => _single(); @@ -360,6 +362,10 @@ class FindNode { return _node(search, (n) => n is DoStatement); } + DotShorthandInvocation dotShorthandInvocation(String search) { + return _node(search, (n) => n is DotShorthandInvocation); + } + DoubleLiteral doubleLiteral(String search) { return _node(search, (n) => n is DoubleLiteral); } diff --git a/pkg/analyzer/test/src/dart/resolution/dot_shorthand_invocation_test.dart b/pkg/analyzer/test/src/dart/resolution/dot_shorthand_invocation_test.dart new file mode 100644 index 00000000000..7d902bf8c2a --- /dev/null +++ b/pkg/analyzer/test/src/dart/resolution/dot_shorthand_invocation_test.dart @@ -0,0 +1,130 @@ +// Copyright (c) 2025, 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:test_reflective_loader/test_reflective_loader.dart'; + +import 'context_collection_resolution.dart'; +import 'node_text_expectations.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(DotShorthandInvocationResolutionTest); + defineReflectiveTests(UpdateNodeTextExpectations); + }); +} + +@reflectiveTest +class DotShorthandInvocationResolutionTest extends PubPackageResolutionTest { + test_dotShorthand_basic() async { + await assertNoErrorsInCode(r''' +class C { + static C member() => C(1); + int x; + C(this.x); +} + +void main() { + C c = .member(); + print(c); +} +'''); + + var node = findNode.singleDotShorthandInvocation; + assertResolvedNodeText(node, r''' +DotShorthandInvocation + period: . + memberName: SimpleIdentifier + token: member + element: ::@class::C::@method::member#element + staticType: C Function() + argumentList: ArgumentList + leftParenthesis: ( + rightParenthesis: ) + staticInvokeType: C Function() + staticType: C +'''); + } + + test_dotShorthand_basic_generic() async { + await assertNoErrorsInCode(r''' +class C { + static C member(U x) => C(x); + T x; + C(this.x); +} + +void main() { + C c = .member(1); + print(c); +} +'''); + + var node = findNode.singleDotShorthandInvocation; + assertResolvedNodeText(node, r''' +DotShorthandInvocation + period: . + memberName: SimpleIdentifier + token: member + element: ::@class::C::@method::member#element + staticType: C Function(U) + typeArguments: TypeArgumentList + leftBracket: < + arguments + NamedType + name: int + element2: dart:core::@class::int + type: int + rightBracket: > + argumentList: ArgumentList + leftParenthesis: ( + arguments + IntegerLiteral + literal: 1 + correspondingParameter: ParameterMember + baseElement: ::@class::C::@method::member::@parameter::x#element + substitution: {U: int} + staticType: int + rightParenthesis: ) + staticInvokeType: C Function(int) + staticType: C + typeArgumentTypes + int +'''); + } + + test_dotShorthand_basic_parameter() async { + await assertNoErrorsInCode(r''' +class C { + static C member(int x) => C(x); + int x; + C(this.x); +} + +void main() { + C c = .member(1); + print(c); +} +'''); + + var node = findNode.singleDotShorthandInvocation; + assertResolvedNodeText(node, r''' +DotShorthandInvocation + period: . + memberName: SimpleIdentifier + token: member + element: ::@class::C::@method::member#element + staticType: C Function(int) + argumentList: ArgumentList + leftParenthesis: ( + arguments + IntegerLiteral + literal: 1 + correspondingParameter: ::@class::C::@method::member::@parameter::x#element + staticType: int + rightParenthesis: ) + staticInvokeType: C Function(int) + staticType: C +'''); + } +} diff --git a/pkg/analyzer/test/src/dart/resolution/test_all.dart b/pkg/analyzer/test/src/dart/resolution/test_all.dart index 832118f8d91..06a7036dfc3 100644 --- a/pkg/analyzer/test/src/dart/resolution/test_all.dart +++ b/pkg/analyzer/test/src/dart/resolution/test_all.dart @@ -26,6 +26,7 @@ import 'constructor_field_initializer_test.dart' import 'constructor_reference_test.dart' as constructor_reference; import 'constructor_test.dart' as constructor; import 'declared_variable_pattern_test.dart' as declared_variable_pattern; +import 'dot_shorthand_invocation_test.dart' as dot_shorthand_invocation; import 'enum_test.dart' as enum_resolution; import 'extension_method_test.dart' as extension_method; import 'extension_override_test.dart' as extension_override; @@ -139,6 +140,7 @@ main() { constructor_reference.main(); constructor.main(); declared_variable_pattern.main(); + dot_shorthand_invocation.main(); enum_resolution.main(); extension_method.main(); extension_override.main(); diff --git a/pkg/analyzer/test/src/diagnostics/experiment_not_enabled_test.dart b/pkg/analyzer/test/src/diagnostics/experiment_not_enabled_test.dart index c3877d0dbc8..813824b80be 100644 --- a/pkg/analyzer/test/src/diagnostics/experiment_not_enabled_test.dart +++ b/pkg/analyzer/test/src/diagnostics/experiment_not_enabled_test.dart @@ -34,13 +34,13 @@ main() { test_dotShorthands_disabled() async { await assertErrorsInCode(r''' +// @dart = 3.8 void main() { - Object c = .hash; + Object c = .hash(1, 2); print(c); } ''', [ - error(ParserErrorCode.EXPERIMENT_NOT_ENABLED, 27, 1), - error(CompileTimeErrorCode.UNDEFINED_IDENTIFIER, 28, 4), + error(ParserErrorCode.EXPERIMENT_NOT_ENABLED, 42, 1), ]); } diff --git a/pkg/analyzer/test/src/fasta/ast_builder_test.dart b/pkg/analyzer/test/src/fasta/ast_builder_test.dart index d67abd0f493..f0a17711096 100644 --- a/pkg/analyzer/test/src/fasta/ast_builder_test.dart +++ b/pkg/analyzer/test/src/fasta/ast_builder_test.dart @@ -903,6 +903,28 @@ ConstructorDeclaration '''); } + void test_dotShorthand_invocation() { + var parseResult = parseStringWithErrors(r''' +class C {} + +void main() { + C c = .new(); +} +'''); + parseResult.assertNoErrors(); + + var node = parseResult.findNode.dotShorthandInvocation('.new()'); + assertParsedNodeText(node, r''' +DotShorthandInvocation + period: . + memberName: SimpleIdentifier + token: new + argumentList: ArgumentList + leftParenthesis: ( + rightParenthesis: ) +'''); + } + void test_enum_base() { var parseResult = parseStringWithErrors(r''' base enum E { v } diff --git a/pkg/analyzer/test/src/summary/resolved_ast_printer.dart b/pkg/analyzer/test/src/summary/resolved_ast_printer.dart index c06aea34e76..e0fd2653e12 100644 --- a/pkg/analyzer/test/src/summary/resolved_ast_printer.dart +++ b/pkg/analyzer/test/src/summary/resolved_ast_printer.dart @@ -465,6 +465,18 @@ class ResolvedAstPrinter extends ThrowingAstVisitor { }); } + @override + void visitDotShorthandInvocation(DotShorthandInvocation node) { + _sink.writeln('DotShorthandInvocation'); + _sink.withIndent(() { + _writeNamedChildEntities(node); + _writeParameterElement(node); + _writeType('staticInvokeType', node.staticInvokeType); + _writeType('staticType', node.staticType); + _writeTypeList('typeArgumentTypes', node.typeArgumentTypes); + }); + } + @override void visitDottedName(DottedName node) { _sink.writeln('DottedName'); diff --git a/pkg/analyzer_utilities/lib/test/experiments/experiments.dart b/pkg/analyzer_utilities/lib/test/experiments/experiments.dart index 3654a3340af..5cfc870f5e1 100644 --- a/pkg/analyzer_utilities/lib/test/experiments/experiments.dart +++ b/pkg/analyzer_utilities/lib/test/experiments/experiments.dart @@ -16,6 +16,7 @@ import 'package:analyzer/dart/analysis/features.dart'; /// whether a given flag is already included. List experimentsForTests = [ Feature.augmentations.enableString, + Feature.dot_shorthands.enableString, Feature.enhanced_parts.enableString, Feature.macros.enableString, ];