Skip to content

Commit f75a803

Browse files
alexmarkovCommit Queue
authored and
Commit Queue
committed
[dynamic modules] Fix dynamic interface validation of extension types
Instead of validating the erasure of an extension type, validate that extension type declaration and type arguments are specified as callable. TEST=pkg/dynamic_modules/test/data/extension_type2 Fixes b/411433443 Change-Id: I063f0a622abd0d75594c4095573b29a7888f5afb Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/423882 Reviewed-by: Sigmund Cherem <[email protected]> Reviewed-by: Johnni Winther <[email protected]> Commit-Queue: Alexander Markov <[email protected]>
1 parent a303304 commit f75a803

17 files changed

+390
-33
lines changed

pkg/_fe_analyzer_shared/lib/src/messages/codes_generated.dart

+37
Original file line numberDiff line numberDiff line change
@@ -5666,6 +5666,43 @@ const MessageCode messageExtensionTypeRepresentationTypeBottom =
56665666
problemMessage: r"""The representation type can't be a bottom type.""",
56675667
);
56685668

5669+
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
5670+
const Template<Message Function(String name)>
5671+
templateExtensionTypeShouldBeListedAsCallableInDynamicInterface =
5672+
const Template<Message Function(String name)>(
5673+
"ExtensionTypeShouldBeListedAsCallableInDynamicInterface",
5674+
problemMessageTemplate:
5675+
r"""Cannot use extension type '#name' in a dynamic module.""",
5676+
correctionMessageTemplate:
5677+
r"""Try removing the reference to extension type '#name' or update the dynamic interface to list extension type '#name' as callable.""",
5678+
withArguments:
5679+
_withArgumentsExtensionTypeShouldBeListedAsCallableInDynamicInterface,
5680+
);
5681+
5682+
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
5683+
const Code<Message Function(String name)>
5684+
codeExtensionTypeShouldBeListedAsCallableInDynamicInterface =
5685+
const Code<Message Function(String name)>(
5686+
"ExtensionTypeShouldBeListedAsCallableInDynamicInterface",
5687+
);
5688+
5689+
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
5690+
Message _withArgumentsExtensionTypeShouldBeListedAsCallableInDynamicInterface(
5691+
String name) {
5692+
if (name.isEmpty) throw 'No name provided';
5693+
name = demangleMixinApplicationName(name);
5694+
return new Message(
5695+
codeExtensionTypeShouldBeListedAsCallableInDynamicInterface,
5696+
problemMessage:
5697+
"""Cannot use extension type '${name}' in a dynamic module.""",
5698+
correctionMessage:
5699+
"""Try removing the reference to extension type '${name}' or update the dynamic interface to list extension type '${name}' as callable.""",
5700+
arguments: {
5701+
'name': name,
5702+
},
5703+
);
5704+
}
5705+
56695706
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
56705707
const Code<Null> codeExtensionTypeWith = messageExtensionTypeWith;
56715708

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
# for details. All rights reserved. Use of this source code is governed by a
3+
# BSD-style license that can be found in the LICENSE file.
4+
5+
callable:
6+
- library: 'shared/shared.dart'
7+
# TODO(sigmund): This should be included by default
8+
- library: 'dart:core'
9+
class: 'Object'
10+
- library: 'dart:core'
11+
class: 'int'
12+
- library: 'dart:core'
13+
class: 'String'
14+
- library: 'dart:core'
15+
class: 'pragma'
16+
member: '_'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import '../../common/testing.dart' as helper;
6+
import 'package:expect/expect.dart';
7+
8+
import 'shared/shared.dart'; // ignore: unused_import
9+
10+
void main() async {
11+
final result = (await helper.load('entry1.dart')) as int;
12+
Expect.equals(1, result);
13+
helper.done();
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import '../shared/shared.dart';
6+
7+
@pragma('dyn-module:entry-point')
8+
Object? dynamicModuleEntrypoint() => method(value);
9+
10+
int method(B b) => b.foo();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'shared_a.dart';
6+
7+
B get value => B(A());
8+
9+
extension type B(A _internal) {
10+
int foo() => 1;
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
class A {}

pkg/front_end/lib/src/kernel/dynamic_module_validator.dart

+90-16
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@ import 'package:kernel/core_types.dart' show CoreTypes;
88
import 'package:kernel/library_index.dart' show LibraryIndex;
99
import 'package:yaml/yaml.dart';
1010
import '../source/source_loader.dart' show SourceLoader;
11+
import '../api_prototype/lowering_predicates.dart'
12+
show extractQualifiedNameFromExtensionMethodName;
1113

1214
import '../codes/cfe_codes.dart'
1315
show
1416
messageDynamicCallsAreNotAllowedInDynamicModule,
1517
noLength,
1618
templateConstructorShouldBeListedAsCallableInDynamicInterface,
1719
templateMemberShouldBeListedAsCallableInDynamicInterface,
20+
templateExtensionTypeShouldBeListedAsCallableInDynamicInterface,
1821
templateClassShouldBeListedAsCallableInDynamicInterface,
1922
templateClassShouldBeListedAsExtendableInDynamicInterface,
2023
templateMemberShouldBeListedAsCanBeOverriddenInDynamicInterface;
@@ -214,13 +217,19 @@ class DynamicInterfaceLanguageImplPragmas {
214217

215218
bool isCallable(TreeNode node) => switch (node) {
216219
Member() => isPlatformLibrary(node.enclosingLibrary) &&
217-
// Coverage-ignore(suite): Not run.
218220
(isAnnotatedWith(node, callablePragmaName) ||
219221
(!node.name.isPrivate &&
220222
node.enclosingClass != null &&
221223
isAnnotatedWith(node.enclosingClass!, callablePragmaName))),
222224
Class() => isPlatformLibrary(node.enclosingLibrary) &&
223225
isAnnotatedWith(node, callablePragmaName),
226+
ExtensionTypeDeclaration() =>
227+
isPlatformLibrary(node.enclosingLibrary) &&
228+
// Coverage-ignore(suite): Not run.
229+
isAnnotatedWith(node, callablePragmaName),
230+
// Coverage-ignore(suite): Not run.
231+
Extension() => isPlatformLibrary(node.enclosingLibrary) &&
232+
isAnnotatedWith(node, callablePragmaName),
224233
_ => // Coverage-ignore(suite): Not run.
225234
throw 'Unexpected node ${node.runtimeType} $node'
226235
};
@@ -230,7 +239,6 @@ class DynamicInterfaceLanguageImplPragmas {
230239
if (annotation case ConstantExpression(:var constant)) {
231240
if (constant case InstanceConstant(:var classNode, :var fieldValues)
232241
when classNode == coreTypes.pragmaClass) {
233-
// Coverage-ignore-block(suite): Not run.
234242
if (fieldValues[coreTypes.pragmaName.fieldReference]
235243
case StringConstant(:var value) when value == pragmaName) {
236244
return true;
@@ -254,19 +262,69 @@ class _DynamicModuleValidator extends RecursiveVisitor {
254262

255263
_DynamicModuleValidator(this.spec, this.languageImplPragmas,
256264
this.moduleLibraries, this.hierarchy, this.loader) {
257-
_addLibraryExports(spec.callable);
258-
_addLibraryExports(spec.extendable);
259-
_addLibraryExports(spec.canBeOverridden);
265+
_expandNodes(spec.callable);
266+
_expandNodes(spec.extendable);
267+
_expandNodes(spec.canBeOverridden);
260268
}
261269

262-
void _addLibraryExports(Set<TreeNode> nodes) {
263-
Set<TreeNode> exports = {};
270+
// Add nodes which do not have direct relation to its logical "parent" node.
271+
void _expandNodes(Set<TreeNode> nodes) {
272+
Set<TreeNode> extraNodes = {};
264273
for (TreeNode node in nodes) {
265-
if (node is Library) {
266-
exports.addAll(node.additionalExports.map((ref) => ref.node!));
267-
}
274+
_expandNode(node, extraNodes);
275+
}
276+
nodes.addAll(extraNodes);
277+
}
278+
279+
// Add re-exports of Library and members of ExtensionTypeDeclaration and
280+
// Extension. These nodes do not have direct relations to their "parents".
281+
void _expandNode(TreeNode node, Set<TreeNode> extraNodes) {
282+
switch (node) {
283+
case Library():
284+
for (Reference ref in node.additionalExports) {
285+
TreeNode node = ref.node!;
286+
extraNodes.add(node);
287+
_expandNode(node, extraNodes);
288+
}
289+
for (ExtensionTypeDeclaration e in node.extensionTypeDeclarations) {
290+
// Coverage-ignore-block(suite): Not run.
291+
if (e.name[0] != '_') {
292+
_expandNode(e, extraNodes);
293+
}
294+
}
295+
for (Extension e in node.extensions) {
296+
// Coverage-ignore-block(suite): Not run.
297+
if (e.name[0] != '_') {
298+
_expandNode(e, extraNodes);
299+
}
300+
}
301+
case ExtensionTypeDeclaration():
302+
for (ExtensionTypeMemberDescriptor md in node.memberDescriptors) {
303+
TreeNode? member = md.memberReference?.node;
304+
if (member != null) {
305+
extraNodes.add(member);
306+
}
307+
TreeNode? tearOff = md.tearOffReference?.node;
308+
if (tearOff != null) {
309+
extraNodes.add(tearOff);
310+
}
311+
}
312+
case Extension():
313+
for (ExtensionMemberDescriptor md in node.memberDescriptors) {
314+
TreeNode? member = md.memberReference?.node;
315+
if (member != null) {
316+
extraNodes.add(member);
317+
}
318+
TreeNode? tearOff = md
319+
.tearOffReference
320+
// Coverage-ignore(suite): Not run.
321+
?.node;
322+
if (tearOff != null) {
323+
// Coverage-ignore-block(suite): Not run.
324+
extraNodes.add(tearOff);
325+
}
326+
}
268327
}
269-
nodes.addAll(exports);
270328
}
271329

272330
@override
@@ -479,9 +537,9 @@ class _DynamicModuleValidator extends RecursiveVisitor {
479537
}
480538

481539
@override
482-
// Coverage-ignore(suite): Not run.
483540
void visitExtensionType(ExtensionType node) {
484-
node.extensionTypeErasure.accept(this);
541+
_verifyCallable(node.extensionTypeDeclaration, _enclosingTreeNode!);
542+
super.visitExtensionType(node);
485543
}
486544

487545
@override
@@ -570,9 +628,15 @@ class _DynamicModuleValidator extends RecursiveVisitor {
570628
node.location!.file);
571629
case Member():
572630
final Class? cls = target.enclosingClass;
573-
final String name = (cls != null)
574-
? '${cls.name}.${target.name.text}'
575-
: target.name.text;
631+
String name;
632+
if (cls != null) {
633+
name = '${cls.name}.${target.name.text}';
634+
} else {
635+
name = target.name.text;
636+
if (target.isExtensionMember || target.isExtensionTypeMember) {
637+
name = extractQualifiedNameFromExtensionMethodName(name)!;
638+
}
639+
}
576640
loader.addProblem(
577641
templateMemberShouldBeListedAsCallableInDynamicInterface
578642
.withArguments(name),
@@ -586,6 +650,13 @@ class _DynamicModuleValidator extends RecursiveVisitor {
586650
node.fileOffset,
587651
noLength,
588652
node.location!.file);
653+
case ExtensionTypeDeclaration():
654+
loader.addProblem(
655+
templateExtensionTypeShouldBeListedAsCallableInDynamicInterface
656+
.withArguments(target.name),
657+
node.fileOffset,
658+
noLength,
659+
node.location!.file);
589660
// Coverage-ignore(suite): Not run.
590661
case _:
591662
throw 'Unexpected node ${node.runtimeType} $node';
@@ -659,6 +730,7 @@ class _DynamicModuleValidator extends RecursiveVisitor {
659730
Library _enclosingLibrary(TreeNode node) => switch (node) {
660731
Member() => node.enclosingLibrary,
661732
Class() => node.enclosingLibrary,
733+
ExtensionTypeDeclaration() => node.enclosingLibrary,
662734
// Coverage-ignore(suite): Not run.
663735
Library() => node,
664736
_ => // Coverage-ignore(suite): Not run.
@@ -672,6 +744,8 @@ class _DynamicModuleValidator extends RecursiveVisitor {
672744
!node.name.isPrivate && _isSpecified(node.parent!, specified),
673745
Class() =>
674746
node.name[0] != '_' && _isSpecified(node.enclosingLibrary, specified),
747+
ExtensionTypeDeclaration() =>
748+
node.name[0] != '_' && _isSpecified(node.enclosingLibrary, specified),
675749
Library() => false,
676750
_ => // Coverage-ignore(suite): Not run.
677751
throw 'Unexpected node ${node.runtimeType} $node'

pkg/front_end/messages.status

+2
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,8 @@ ExtensionTypeImplementsDeferred/part_wrapped_script: Fail # Uses imports
334334
ExtensionTypeImplementsDeferred/script: Fail # Uses imports
335335
ExtensionTypePrimaryConstructorFunctionFormalParameterSyntax/analyzerCode: Fail
336336
ExtensionTypePrimaryConstructorWithInitializingFormal/analyzerCode: Fail
337+
ExtensionTypeShouldBeListedAsCallableInDynamicInterface/analyzerCode: Fail
338+
ExtensionTypeShouldBeListedAsCallableInDynamicInterface/example: Fail
337339
ExternalConstructorWithBody/part_wrapped_script1: Fail
338340
ExternalConstructorWithBody/script1: Fail
339341
ExternalConstructorWithFieldInitializers/example: Fail

pkg/front_end/messages.yaml

+4
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,10 @@ MemberShouldBeListedAsCallableInDynamicInterface:
312312
problemMessage: "Cannot invoke member '#name' from a dynamic module."
313313
correctionMessage: "Try removing the call or update the dynamic interface to list member '#name' as callable."
314314

315+
ExtensionTypeShouldBeListedAsCallableInDynamicInterface:
316+
problemMessage: "Cannot use extension type '#name' in a dynamic module."
317+
correctionMessage: "Try removing the reference to extension type '#name' or update the dynamic interface to list extension type '#name' as callable."
318+
315319
ClassShouldBeListedAsCallableInDynamicInterface:
316320
problemMessage: "Cannot use class '#name' in a dynamic module."
317321
correctionMessage: "Try removing the reference to class '#name' or update the dynamic interface to list class '#name' as callable."

pkg/front_end/test/coverage_suite_expected.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -720,7 +720,7 @@ const Map<String, ({int hitCount, int missCount})> _expect = {
720720
),
721721
// 100.0%.
722722
"package:front_end/src/kernel/dynamic_module_validator.dart": (
723-
hitCount: 358,
723+
hitCount: 416,
724724
missCount: 0,
725725
),
726726
// 100.0%.

pkg/front_end/testcases/general/dynamic_modules/main.dart

+8
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,18 @@ void test() {
125125
// Allowed - target of redirecting factory is callable.
126126
print(C9());
127127

128+
// Not allowed.
129+
print(ExtType1);
130+
print(ExtType1(42));
131+
print(42.isPositive);
132+
128133
// Allowed - re-exported through main_lib2.dart
129134
print(Lib3Class());
130135
lib3Method();
131136
lib3Field = 42;
137+
print(Lib3ExtType);
138+
print(Lib3ExtType(42));
139+
print(42.lib3IsPositive);
132140
}
133141

134142
void main() {}

0 commit comments

Comments
 (0)