[CFE] Fix coverage destroyed by (wrong) offsets on extension method tearoffs
Say you have a class method like this:
```
class Bar {
/*offset a*/ void /*offset b*/ qux() {
/*offset c*/ print("hello");
}
}
```
Lets also say that this method is run.
This will mark offset a and offset c as hits.
Offset b does not exist in coverage terms.
Now say you have an extension mehod like this:
```
extension Foo on Bar {
/*offset a*/ void /*offset b*/ baz() {
/*offset c*/ print("hello");
}
}
```
Lets also say that this method is also executed.
This will mark offset a and offset c as hits.
Offset b does not exist in coverage terms for this method.
Because extension methods are special though we create a special tearoff for
it. Lets say we didn't execute that one.
The tearoff method - before this CL - had offset b on positions that caused
the position to exist in coverage terms, and as the method wasn't executed
this would make it a miss.
This CL fixes the issue by setting the offsets on the tearoff that before
introduced offset b to offset a instead.
Change-Id: I3a5339135f3d76327624b35f04cc14afccaf487a
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/404563
Commit-Queue: Jens Johansen <jensj@google.com>
Reviewed-by: Slava Egorov <vegorov@google.com>
Reviewed-by: Johnni Winther <johnniwinther@google.com>
diff --git a/pkg/front_end/lib/src/fragment/method.dart b/pkg/front_end/lib/src/fragment/method.dart
index a6c822d..dbe8e64 100644
--- a/pkg/front_end/lib/src/fragment/method.dart
+++ b/pkg/front_end/lib/src/fragment/method.dart
@@ -750,6 +750,7 @@
Procedure procedure, NameScheme nameScheme, Reference? tearOffReference) {
_extensionTearOffParameterMap = {};
+ int fileStartOffset = _fragment.startOffset;
int fileOffset = _fragment.nameOffset;
int fileEndOffset = _fragment.endOffset;
@@ -837,7 +838,10 @@
procedure,
new Arguments(closurePositionalArguments,
types: typeArguments, named: closureNamedArguments))
- ..fileOffset = fileOffset)
+ // We need to use the fileStartOffset on the StaticInvocation to
+ // avoid a possible "fake coverage miss" on the name of the
+ // extension method.
+ ..fileOffset = fileStartOffset)
..fileOffset = fileOffset;
FunctionExpression closure = new FunctionExpression(
@@ -850,7 +854,10 @@
returnType: closureReturnType)
..fileOffset = fileOffset
..fileEndOffset = fileEndOffset)
- ..fileOffset = fileOffset;
+ // We need to use the fileStartOffset on the FunctionExpression to
+ // avoid a possible "fake coverage miss" on the name of the
+ // extension method.
+ ..fileOffset = fileStartOffset;
FunctionNode function = new FunctionNode(
new ReturnStatement(closure)..fileOffset = fileOffset,
diff --git a/pkg/front_end/test/coverage_suite_expected.dart b/pkg/front_end/test/coverage_suite_expected.dart
index 7ea66756..0dca096 100644
--- a/pkg/front_end/test/coverage_suite_expected.dart
+++ b/pkg/front_end/test/coverage_suite_expected.dart
@@ -148,10 +148,10 @@
hitCount: 97,
missCount: 0,
),
- // 99.88331388564761%.
+ // 100.0%.
"package:front_end/src/base/incremental_compiler.dart": (
hitCount: 856,
- missCount: 1,
+ missCount: 0,
),
// 100.0%.
"package:front_end/src/base/incremental_serializer.dart": (
@@ -183,10 +183,10 @@
hitCount: 31,
missCount: 0,
),
- // 99.26470588235294%.
+ // 100.0%.
"package:front_end/src/base/modifiers.dart": (
hitCount: 135,
- missCount: 1,
+ missCount: 0,
),
// 100.0%.
"package:front_end/src/base/name_space.dart": (
@@ -208,10 +208,10 @@
hitCount: 260,
missCount: 0,
),
- // 99.38837920489296%.
+ // 100.0%.
"package:front_end/src/base/scope.dart": (
hitCount: 650,
- missCount: 4,
+ missCount: 0,
),
// 100.0%.
"package:front_end/src/base/ticker.dart": (
@@ -535,7 +535,7 @@
),
// 100.0%.
"package:front_end/src/fragment/method.dart": (
- hitCount: 771,
+ hitCount: 773,
missCount: 0,
),
// 100.0%.
@@ -920,7 +920,7 @@
),
// 100.0%.
"package:front_end/src/source/source_constructor_builder.dart": (
- hitCount: 1085,
+ hitCount: 1084,
missCount: 0,
),
// 100.0%.
@@ -941,13 +941,13 @@
),
// 100.0%.
"package:front_end/src/source/source_factory_builder.dart": (
- hitCount: 717,
+ hitCount: 715,
missCount: 0,
),
- // 97.91666666666666%.
+ // 100.0%.
"package:front_end/src/source/source_function_builder.dart": (
hitCount: 47,
- missCount: 1,
+ missCount: 0,
),
// 100.0%.
"package:front_end/src/source/source_library_builder.dart": (
@@ -991,7 +991,7 @@
),
// 100.0%.
"package:front_end/src/source/type_parameter_scope_builder.dart": (
- hitCount: 1445,
+ hitCount: 1446,
missCount: 0,
),
// 100.0%.
@@ -1109,10 +1109,10 @@
hitCount: 20,
missCount: 0,
),
- // 92.0%.
+ // 100.0%.
"package:front_end/src/util/local_stack.dart": (
hitCount: 23,
- missCount: 2,
+ missCount: 0,
),
// 100.0%.
"package:front_end/src/util/parser_ast.dart": (
diff --git a/pkg/front_end/testcases/incremental/flutter_widget_factory.yaml.world.1.expect b/pkg/front_end/testcases/incremental/flutter_widget_factory.yaml.world.1.expect
index 6cdd1fb..9453eca 100644
--- a/pkg/front_end/testcases/incremental/flutter_widget_factory.yaml.world.1.expect
+++ b/pkg/front_end/testcases/incremental/flutter_widget_factory.yaml.world.1.expect
@@ -121,18 +121,18 @@
import "org-dartlang-test:///foo.dart";
- static field foo::Foo newFoo = foo::Foo::•($creationLocationd_0dea112b090073317d4: #C37);
- static field () → foo::Foo newFooFunction = #C38;
+ static field foo::Foo newFoo = foo::Foo::•($creationLocationd_0dea112b090073317d4: #C38);
+ static field () → foo::Foo newFooFunction = #C39;
static field foo::Foo newFooFunctionCall = main::newFooFunction(){() → foo::Foo};
- static field foo::Foo extensionFoo = foo::FooExtension|foo(null, $creationLocationd_0dea112b090073317d4: #C41);
+ static field foo::Foo extensionFoo = foo::FooExtension|foo(null, $creationLocationd_0dea112b090073317d4: #C42);
static field () → foo::Foo extensionFooFunction = foo::FooExtension|get#foo(null);
static field foo::Foo extensionFooFunctionCall = main::extensionFooFunction(){() → foo::Foo};
- static field foo::Foo extensionBar = foo::FooExtension|bar(null, $creationLocationd_0dea112b090073317d4: #C42);
+ static field foo::Foo extensionBar = foo::FooExtension|bar(null, $creationLocationd_0dea112b090073317d4: #C43);
static field foo::Foo extensionBaz = foo::FooExtension|baz(null);
- static field foo::Foo extensionBoz = foo::FooExtension|boz(null, $creationLocationd_0dea112b090073317d4: #C43);
- static field foo::Foo extensionConstFoo = foo::FooExtension|constFoo(null, $creationLocationd_0dea112b090073317d4: #C45);
- static field foo::Foo extensionGetterFoo = foo::FooExtension|get#getterFoo(null, $creationLocationd_0dea112b090073317d4: #C49);
- static field () → Null extensionSetterFoo = () → Null => let final has-declared-initializer Null #t1 = null in let final void #t2 = foo::FooExtension|set#setterFoo(null, #t1, $creationLocationd_0dea112b090073317d4: #C53) in #t1;
+ static field foo::Foo extensionBoz = foo::FooExtension|boz(null, $creationLocationd_0dea112b090073317d4: #C44);
+ static field foo::Foo extensionConstFoo = foo::FooExtension|constFoo(null, $creationLocationd_0dea112b090073317d4: #C46);
+ static field foo::Foo extensionGetterFoo = foo::FooExtension|get#getterFoo(null, $creationLocationd_0dea112b090073317d4: #C50);
+ static field () → Null extensionSetterFoo = () → Null => let final has-declared-initializer Null #t1 = null in let final void #t2 = foo::FooExtension|set#setterFoo(null, #t1, $creationLocationd_0dea112b090073317d4: #C54) in #t1;
static field foo::Foo extensionOperatorFoo = foo::FooExtension|unary-(null, $creationLocationd_0dea112b090073317d4: #C57);
static field foo::Foo extensionStaticFoo = foo::FooExtension|staticFoo();
}
@@ -150,50 +150,50 @@
#C11 = 10.0
#C12 = 9.0
#C13 = wid::_Location {file:#C3, line:#C11, column:#C12, name:#C6, parameterLocations:#C1}
- #C14 = 15.0
- #C15 = 7.0
+ #C14 = 14.0
+ #C15 = 3.0
#C16 = "FooExtension|foo"
#C17 = wid::_Location {file:#C3, line:#C14, column:#C15, name:#C16, parameterLocations:#C1}
- #C18 = 18.0
+ #C18 = 17.0
#C19 = "FooExtension|bar"
#C20 = wid::_Location {file:#C3, line:#C18, column:#C15, name:#C19, parameterLocations:#C1}
#C21 = 21.0
#C22 = 20.0
#C23 = wid::_Location {file:#C3, line:#C21, column:#C22, name:#C6, parameterLocations:#C1}
- #C24 = 24.0
+ #C24 = 23.0
#C25 = "FooExtension|boz"
#C26 = wid::_Location {file:#C3, line:#C24, column:#C15, name:#C25, parameterLocations:#C1}
#C27 = 27.0
#C28 = wid::_Location {file:#C3, line:#C27, column:#C27, name:#C6, parameterLocations:#C1}
#C29 = foo::Foo {_location:#C28}
#C30 = "FooExtension|constFoo"
- #C31 = wid::_Location {file:#C3, line:#C27, column:#C15, name:#C30, parameterLocations:#C1}
+ #C31 = wid::_Location {file:#C3, line:#C5, column:#C15, name:#C30, parameterLocations:#C1}
#C32 = 39.0
#C33 = 33.0
#C34 = wid::_Location {file:#C3, line:#C32, column:#C33, name:#C6, parameterLocations:#C1}
#C35 = "org-dartlang-test:///main.dart"
#C36 = 2.0
- #C37 = wid::_Location {file:#C35, line:#C36, column:#C18, name:#C6, parameterLocations:#C1}
- #C38 = static-tearoff foo::Foo::_#new#tearOff
- #C39 = 5.0
- #C40 = 25.0
- #C41 = wid::_Location {file:#C35, line:#C39, column:#C40, name:#C16, parameterLocations:#C1}
- #C42 = wid::_Location {file:#C35, line:#C4, column:#C40, name:#C19, parameterLocations:#C1}
- #C43 = wid::_Location {file:#C35, line:#C11, column:#C40, name:#C25, parameterLocations:#C1}
- #C44 = 30.0
- #C45 = wid::_Location {file:#C35, line:#C9, column:#C44, name:#C30, parameterLocations:#C1}
- #C46 = 12.0
- #C47 = 31.0
- #C48 = "FooExtension|get#getterFoo"
- #C49 = wid::_Location {file:#C35, line:#C46, column:#C47, name:#C48, parameterLocations:#C1}
- #C50 = 13.0
- #C51 = 37.0
- #C52 = "FooExtension|set#setterFoo"
- #C53 = wid::_Location {file:#C35, line:#C50, column:#C51, name:#C52, parameterLocations:#C1}
- #C54 = 14.0
+ #C37 = 18.0
+ #C38 = wid::_Location {file:#C35, line:#C36, column:#C37, name:#C6, parameterLocations:#C1}
+ #C39 = static-tearoff foo::Foo::_#new#tearOff
+ #C40 = 5.0
+ #C41 = 25.0
+ #C42 = wid::_Location {file:#C35, line:#C40, column:#C41, name:#C16, parameterLocations:#C1}
+ #C43 = wid::_Location {file:#C35, line:#C4, column:#C41, name:#C19, parameterLocations:#C1}
+ #C44 = wid::_Location {file:#C35, line:#C11, column:#C41, name:#C25, parameterLocations:#C1}
+ #C45 = 30.0
+ #C46 = wid::_Location {file:#C35, line:#C9, column:#C45, name:#C30, parameterLocations:#C1}
+ #C47 = 12.0
+ #C48 = 31.0
+ #C49 = "FooExtension|get#getterFoo"
+ #C50 = wid::_Location {file:#C35, line:#C47, column:#C48, name:#C49, parameterLocations:#C1}
+ #C51 = 13.0
+ #C52 = 37.0
+ #C53 = "FooExtension|set#setterFoo"
+ #C54 = wid::_Location {file:#C35, line:#C51, column:#C52, name:#C53, parameterLocations:#C1}
#C55 = 28.0
#C56 = "FooExtension|unary-"
- #C57 = wid::_Location {file:#C35, line:#C54, column:#C55, name:#C56, parameterLocations:#C1}
+ #C57 = wid::_Location {file:#C35, line:#C14, column:#C55, name:#C56, parameterLocations:#C1}
}
diff --git a/pkg/front_end/testcases/incremental/flutter_widget_factory.yaml.world.2.expect b/pkg/front_end/testcases/incremental/flutter_widget_factory.yaml.world.2.expect
index 054f50d..57ed023 100644
--- a/pkg/front_end/testcases/incremental/flutter_widget_factory.yaml.world.2.expect
+++ b/pkg/front_end/testcases/incremental/flutter_widget_factory.yaml.world.2.expect
@@ -133,7 +133,7 @@
static field foo::Foo extensionBoz = foo::FooExtension|boz(null, $creationLocationd_0dea112b090073317d4: #C46);
static field foo::Foo extensionGetterFoo = foo::FooExtension|get#getterFoo(null, $creationLocationd_0dea112b090073317d4: #C50);
static field () → Null extensionSetterFoo = () → Null => let final has-declared-initializer Null #t1 = null in let final void #t2 = foo::FooExtension|set#setterFoo(null, #t1, $creationLocationd_0dea112b090073317d4: #C54) in #t1;
- static field foo::Foo extensionOperatorFoo = foo::FooExtension|unary-(null, $creationLocationd_0dea112b090073317d4: #C58);
+ static field foo::Foo extensionOperatorFoo = foo::FooExtension|unary-(null, $creationLocationd_0dea112b090073317d4: #C57);
static field foo::Foo extensionStaticFoo = foo::FooExtension|staticFoo();
}
constants {
@@ -150,24 +150,24 @@
#C11 = 10.0
#C12 = 9.0
#C13 = wid::_Location {file:#C3, line:#C11, column:#C12, name:#C6, parameterLocations:#C1}
- #C14 = 15.0
- #C15 = 7.0
+ #C14 = 14.0
+ #C15 = 3.0
#C16 = "FooExtension|foo"
#C17 = wid::_Location {file:#C3, line:#C14, column:#C15, name:#C16, parameterLocations:#C1}
- #C18 = 18.0
+ #C18 = 17.0
#C19 = "FooExtension|bar"
#C20 = wid::_Location {file:#C3, line:#C18, column:#C15, name:#C19, parameterLocations:#C1}
#C21 = 21.0
#C22 = 20.0
#C23 = wid::_Location {file:#C3, line:#C21, column:#C22, name:#C6, parameterLocations:#C1}
- #C24 = 24.0
+ #C24 = 23.0
#C25 = "FooExtension|boz"
#C26 = wid::_Location {file:#C3, line:#C24, column:#C15, name:#C25, parameterLocations:#C1}
#C27 = 27.0
#C28 = wid::_Location {file:#C3, line:#C27, column:#C27, name:#C6, parameterLocations:#C1}
#C29 = foo::Foo {_location:#C28}
#C30 = "FooExtension|constFoo"
- #C31 = wid::_Location {file:#C3, line:#C27, column:#C15, name:#C30, parameterLocations:#C1}
+ #C31 = wid::_Location {file:#C3, line:#C5, column:#C15, name:#C30, parameterLocations:#C1}
#C32 = 39.0
#C33 = 33.0
#C34 = wid::_Location {file:#C3, line:#C32, column:#C33, name:#C6, parameterLocations:#C1}
@@ -175,8 +175,8 @@
#C36 = 2.0
#C37 = 30.0
#C38 = wid::_Location {file:#C35, line:#C36, column:#C37, name:#C30, parameterLocations:#C1}
- #C39 = 3.0
- #C40 = wid::_Location {file:#C35, line:#C39, column:#C18, name:#C6, parameterLocations:#C1}
+ #C39 = 18.0
+ #C40 = wid::_Location {file:#C35, line:#C15, column:#C39, name:#C6, parameterLocations:#C1}
#C41 = static-tearoff foo::Foo::_#new#tearOff
#C42 = 6.0
#C43 = 25.0
@@ -191,10 +191,9 @@
#C52 = 37.0
#C53 = "FooExtension|set#setterFoo"
#C54 = wid::_Location {file:#C35, line:#C51, column:#C52, name:#C53, parameterLocations:#C1}
- #C55 = 14.0
- #C56 = 28.0
- #C57 = "FooExtension|unary-"
- #C58 = wid::_Location {file:#C35, line:#C55, column:#C56, name:#C57, parameterLocations:#C1}
+ #C55 = 28.0
+ #C56 = "FooExtension|unary-"
+ #C57 = wid::_Location {file:#C35, line:#C14, column:#C55, name:#C56, parameterLocations:#C1}
}
diff --git a/pkg/pkg.status b/pkg/pkg.status
index 9542b63..cc9a9fc8 100644
--- a/pkg/pkg.status
+++ b/pkg/pkg.status
@@ -125,6 +125,7 @@
vm_service/test/coverage_closure_call_after_optimization_test: SkipByDesign # Debugger and coverage are disabled in AOT mode.
vm_service/test/coverage_closure_call_test: SkipByDesign # Debugger and coverage are disabled in AOT mode.
vm_service/test/coverage_const_field_async_closure_test: SkipByDesign # Debugger is disabled in AOT mode.
+vm_service/test/coverage_extension_methods_test: SkipByDesign # Debugger and coverage are disabled in AOT mode.
vm_service/test/coverage_instance_call_after_optimization_test: SkipByDesign # Debugger and coverage are disabled in AOT mode.
vm_service/test/coverage_leaf_function_test: SkipByDesign # Debugger is disabled in AOT mode.
vm_service/test/coverage_optimized_function_test: SkipByDesign # Debugger is disabled in AOT mode.
diff --git a/pkg/vm_service/test/coverage_extension_methods_test.dart b/pkg/vm_service/test/coverage_extension_methods_test.dart
new file mode 100644
index 0000000..a545e05
--- /dev/null
+++ b/pkg/vm_service/test/coverage_extension_methods_test.dart
@@ -0,0 +1,107 @@
+// 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 'dart:developer';
+
+import 'package:test/test.dart';
+import 'package:vm_service/vm_service.dart';
+
+import 'common/service_test_common.dart';
+import 'common/test_helper.dart';
+
+// AUTOGENERATED START
+//
+// Update these constants by running:
+//
+// dart pkg/vm_service/test/update_line_numbers.dart pkg/vm_service/test/coverage_extension_methods_test.dart
+//
+const OFFSET_GET_FROM = 0782;
+const OFFSET_BAZ_START = 0830;
+const OFFSET_PRINT = 0866;
+const OFFSET_BAZ_END = 0905;
+const OFFSET_GET_TO = 0928;
+// AUTOGENERATED END
+
+class Bar {}
+
+/* OFFSET_GET_FROM */ extension Foo on Bar {
+ /* OFFSET_BAZ_START */ void baz() {
+ /* OFFSET_PRINT */ print('hello'); /* OFFSET_BAZ_END */
+ }
+} /* OFFSET_GET_TO */
+
+void testFunction() {
+ debugger();
+ final bar = Bar();
+ bar.baz();
+ debugger();
+}
+
+Future<({Set<int> hits, Set<int> misses})> verifyLocationAndGetHitsAndMisses(
+ VmService service,
+ IsolateRef isolateRef,
+) async {
+ final isolateId = isolateRef.id!;
+ final stack = await service.getStack(isolateId);
+
+ // Make sure we are in the right place.
+ final frames = stack.frames!;
+ expect(frames.length, greaterThanOrEqualTo(1));
+ expect(frames[0].function!.name, 'testFunction');
+
+ final location = frames[0].function!.location!;
+ final report = await service.getSourceReport(
+ isolateId,
+ [SourceReportKind.kCoverage],
+ scriptId: location.script!.id!,
+ tokenPos: OFFSET_GET_FROM,
+ endTokenPos: OFFSET_GET_TO,
+ forceCompile: true,
+ );
+
+ final scripts = report.scripts!;
+ final ranges = report.ranges!;
+ final hits = <int>{};
+ final misses = <int>{};
+ // We have more ranges because there are several methods:
+ // The actual method, a tearoff and a closure inside the tearoff.
+ for (final range in ranges) {
+ hits.addAll(range.coverage!.hits!);
+ misses.addAll(range.coverage!.misses!);
+ expect(range.startPos, OFFSET_BAZ_START);
+ expect(range.endPos, OFFSET_BAZ_END);
+ expect(range.scriptIndex, 0);
+ }
+ misses.removeAll(hits);
+ expect(
+ scripts[0].uri,
+ endsWith('coverage_extension_methods_test.dart'),
+ );
+ return (hits: hits, misses: misses);
+}
+
+var tests = <IsolateTest>[
+ hasStoppedAtBreakpoint,
+ (VmService service, IsolateRef isolateRef) async {
+ final (:hits, :misses) =
+ await verifyLocationAndGetHitsAndMisses(service, isolateRef);
+ expect(hits, isEmpty);
+ expect(misses, {OFFSET_BAZ_START, OFFSET_PRINT});
+ },
+ resumeIsolate,
+ hasStoppedAtBreakpoint,
+ (VmService service, IsolateRef isolateRef) async {
+ final (:hits, :misses) =
+ await verifyLocationAndGetHitsAndMisses(service, isolateRef);
+ expect(hits, {OFFSET_BAZ_START, OFFSET_PRINT});
+ expect(misses, isEmpty);
+ },
+];
+
+void main(List<String> args) => runIsolateTests(
+ args,
+ tests,
+ 'coverage_extension_methods_test.dart',
+ testeeConcurrent: testFunction,
+ );