[frontend_server/DDC] Expression compilation for JavaScript can pass scriptUri

This allows using the new scope finder and facilitates expression
compilation with and in extension types.

For now the script uri is optional (and only passable in the new json
input via package:frontend_server), and only if the script uri is passed
we'll use the new scope finder.

Flutter etc should be updated to pass the new data.

Change-Id: I36eed1ea76a825e63e4c5b9ea60daf18aee39f3d
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/342400
Reviewed-by: Johnni Winther <johnniwinther@google.com>
Commit-Queue: Jens Johansen <jensj@google.com>
Reviewed-by: Anna Gringauze <annagrin@google.com>
This commit is contained in:
Jens Johansen 2024-01-10 10:12:22 +00:00 committed by Commit Queue
parent bcfab01e87
commit 39d68052a9
12 changed files with 611 additions and 41 deletions

View file

@ -54,8 +54,12 @@ class ExpressionCompiler {
) : onDiagnostic = _options.onDiagnostic!,
_context = _compiler.context;
/// Compiles [expression] in [libraryUri] at [line]:[column] to JavaScript
/// in [moduleName].
/// Compiles [expression] in library [libraryUri] and file [scriptUri]
/// at [line]:[column] to JavaScript in [moduleName].
///
/// [libraryUri] and [scriptUri] can be the same, but if for instance
/// evaluating expressions in a part file the [libraryUri] will be the uri of
/// the "part of" file whereas [scriptUri] will be the uri of the part.
///
/// [line] and [column] are 1-based.
///
@ -68,14 +72,20 @@ class ExpressionCompiler {
/// [jsFrameValues] is a map from js variable name to its primitive value
/// or another variable name, for example
/// { 'x': '1', 'y': 'y', 'o': 'null' }
Future<String?> compileExpressionToJs(String libraryUri, int line, int column,
Map<String, String> jsScope, String expression) async {
Future<String?> compileExpressionToJs(
String libraryUri,
String? scriptUri,
int line,
int column,
Map<String, String> jsScope,
String expression) async {
try {
// 1. find dart scope where debugger is paused
_log('Compiling expression \n$expression');
var dartScope = _findScopeAt(Uri.parse(libraryUri), line, column);
var dartScope = _findScopeAt(Uri.parse(libraryUri),
scriptUri == null ? null : Uri.parse(scriptUri), line, column);
if (dartScope == null) {
_log('Scope not found at $libraryUri:$line:$column');
return null;
@ -91,8 +101,8 @@ class ExpressionCompiler {
// remove undefined js variables (this allows us to get a reference error
// from chrome on evaluation)
dartScope.definitions
.removeWhere((variable, type) => !jsScope.containsKey(variable));
dartScope.definitions.removeWhere((variable, type) =>
!jsScope.containsKey(_dartNameToJsName(variable)));
dartScope.typeParameters
.removeWhere((parameter) => !jsScope.containsKey(parameter.name));
@ -101,7 +111,8 @@ class ExpressionCompiler {
// captured variables optimized away in chrome)
var localJsScope = [
...dartScope.typeParameters.map((parameter) => jsScope[parameter.name]),
...dartScope.definitions.keys.map((variable) => jsScope[variable])
...dartScope.definitions.keys
.map((variable) => jsScope[_dartNameToJsName(variable)])
];
_log('Performed scope substitutions for expression');
@ -150,7 +161,14 @@ class ExpressionCompiler {
}
}
DartScope? _findScopeAt(Uri libraryUri, int line, int column) {
String? _dartNameToJsName(String? dartName) {
if (dartName == null) return dartName;
if (isExtensionThisName(dartName)) return r'$this';
return dartName;
}
DartScope? _findScopeAt(
Uri libraryUri, Uri? scriptFileUri, int line, int column) {
if (line < 0) {
onDiagnostic(_createInternalError(
libraryUri, line, column, 'Invalid source location'));
@ -164,6 +182,15 @@ class ExpressionCompiler {
return null;
}
// TODO(jensj): Eventually make the scriptUri required and always use this,
// but for now use the old mechanism when no script is provided.
if (scriptFileUri != null) {
final offset = _component.getOffset(library.fileUri, line, column);
final scope2 =
DartScopeBuilder2.findScopeFromOffset(library, scriptFileUri, offset);
return scope2;
}
var scope = DartScopeBuilder.findScope(_component, library, line, column);
if (scope == null) {
onDiagnostic(_createInternalError(
@ -184,12 +211,20 @@ class ExpressionCompiler {
/// [scope] current dart scope information.
/// [expression] expression to compile in given [scope].
Future<String?> _compileExpression(DartScope scope, String expression) async {
var methodName = scope.member?.name.text;
var member = scope.member;
if (member != null) {
if (member.isExtensionMember || member.isExtensionTypeMember) {
methodName = extractQualifiedNameFromExtensionMethodName(methodName);
}
}
var procedure = await _compiler.compileExpression(
expression,
scope.definitions,
scope.typeParameters,
debugProcedureName,
scope.library.importUri,
methodName: methodName,
className: scope.cls?.name,
isStatic: scope.isStatic);

View file

@ -371,6 +371,7 @@ class ExpressionCompilerWorker {
var compiledProcedure = await expressionCompiler.compileExpressionToJs(
request.libraryUri,
request.scriptUri,
request.line,
request.column,
request.jsScope,
@ -708,6 +709,7 @@ class CompileExpressionRequest {
final Map<String, String> jsModules;
final Map<String, String> jsScope;
final String libraryUri;
final String? scriptUri;
final int line;
final String moduleName;
@ -717,6 +719,7 @@ class CompileExpressionRequest {
required this.jsModules,
required this.jsScope,
required this.libraryUri,
required this.scriptUri,
required this.line,
required this.moduleName,
});
@ -729,6 +732,7 @@ class CompileExpressionRequest {
jsModules: Map<String, String>.from(json['jsModules'] as Map),
jsScope: Map<String, String>.from(json['jsScope'] as Map),
libraryUri: json['libraryUri'] as String,
scriptUri: json['scriptUri'] as String?,
moduleName: json['moduleName'] as String,
);
}

View file

@ -11,6 +11,7 @@ import 'package:async/async.dart';
import 'package:browser_launcher/browser_launcher.dart' as browser;
import 'package:dev_compiler/src/compiler/module_builder.dart';
import 'package:path/path.dart' as p;
import 'package:source_maps/source_maps.dart';
import 'package:test/test.dart';
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'
as wip;
@ -28,11 +29,13 @@ class ExpressionEvaluationTestDriver {
late TestExpressionCompiler compiler;
late Uri htmlBootstrapper;
late Uri input;
Uri? inputPart;
late Uri output;
late Uri packagesFile;
String? preemptiveBp;
late SetupCompilerOptions setup;
late String source;
String? partSource;
late Directory testDir;
late String dartSdkPath;
@ -102,6 +105,7 @@ class ExpressionEvaluationTestDriver {
SetupCompilerOptions setup,
String source, {
Map<String, bool> experiments = const {},
String? partSource,
}) async {
// Perform setup sanity checks.
var summaryPath = setup.options.sdkSummary!.toFilePath();
@ -110,6 +114,7 @@ class ExpressionEvaluationTestDriver {
}
this.setup = setup;
this.source = source;
this.partSource = partSource;
testDir = chromeDir.createTempSync('ddc_eval_test');
var scriptPath = Platform.script.normalizePath().toFilePath();
var ddcPath = p.dirname(p.dirname(p.dirname(scriptPath)));
@ -118,6 +123,14 @@ class ExpressionEvaluationTestDriver {
File(input.toFilePath())
..createSync()
..writeAsStringSync(source);
if (partSource != null) {
inputPart = testDir.uri.resolve('part.dart');
File(inputPart!.toFilePath())
..createSync()
..writeAsStringSync(partSource);
} else {
inputPart = null;
}
packagesFile = testDir.uri.resolve('package_config.json');
File(packagesFile.toFilePath())
@ -369,7 +382,8 @@ class ExpressionEvaluationTestDriver {
// Breakpoint at the first WIP location mapped from its Dart line.
var dartLine = _findBreakpointLine(breakpointId);
var location = await _jsLocationFromDartLine(script, dartLine);
var location =
await _jsLocationFromDartLine(script, dartLine.value, dartLine.key);
var bp = await debugger.setBreakpoint(location);
final pauseQueue = StreamQueue(pauseController.stream);
@ -434,12 +448,10 @@ class ExpressionEvaluationTestDriver {
required String breakpointId,
required String expression,
}) async {
var dartLine = _findBreakpointLine(breakpointId);
return await _onBreakpoint(breakpointId, onPause: (event) async {
var result = await _evaluateDartExpressionInFrame(
event,
expression,
dartLine,
);
return await stringifyRemoteObject(result);
});
@ -527,12 +539,10 @@ class ExpressionEvaluationTestDriver {
assert(expectedError == null || expectedResult == null,
'Cannot expect both an error and result.');
var dartLine = _findBreakpointLine(breakpointId);
return await _onBreakpoint(breakpointId, onPause: (event) async {
var evalResult = await _evaluateDartExpressionInFrame(
event,
expression,
dartLine,
);
var error = evalResult.json['error'];
@ -620,15 +630,60 @@ class ExpressionEvaluationTestDriver {
}
Future<TestCompilationResult> _compileDartExpressionInFrame(
wip.WipCallFrame frame, String expression, int dartLine) async {
wip.WipCallFrame frame, String expression) async {
// Retrieve the call frame and its scope variables.
var scope = await _collectScopeVariables(frame);
var searchLine = frame.location.lineNumber;
var searchColumn = frame.location.columnNumber;
var inputSourceUrl = input.pathSegments.last;
var inputPartSourceUrl = inputPart?.pathSegments.last;
// package:dwds - which I think is what actually provides line and column
// when debugging e.g. via flutter - basically finds the closest point
// before or on the line/column, so we do the same here.
// If there is no javascript column we pick the smallest column value on
// that line.
TargetEntry? best;
for (var lineEntry in compiler.sourceMap.lines) {
if (lineEntry.line != searchLine) continue;
for (var entry in lineEntry.entries) {
if (entry.sourceUrlId != null) {
var sourceMapUrl = compiler.sourceMap.urls[entry.sourceUrlId!];
if (sourceMapUrl == inputSourceUrl ||
sourceMapUrl == inputPartSourceUrl) {
if (best == null) {
best = entry;
} else if (searchColumn != null &&
entry.column > best.column &&
entry.column <= searchColumn) {
best = entry;
} else if (searchColumn == null && entry.column < best.column) {
best = entry;
}
}
}
}
}
if (best == null || best.sourceLine == null || best.sourceColumn == null) {
throw StateError('Unable to find the matching dart line and column '
' for where the javascript paused.');
}
final bestUrl = compiler.sourceMap.urls[best.sourceUrlId!];
var scriptUrl = input;
if (bestUrl == inputPartSourceUrl) {
scriptUrl = inputPart!;
}
// Convert from 0-indexed to 1-indexed.
var dartLine = best.sourceLine! + 1;
var dartColumn = best.sourceColumn! + 1;
// Perform an incremental compile.
return await compiler.compileExpression(
input: input,
libraryUri: input,
scriptUri: scriptUrl,
line: dartLine,
column: 1,
column: dartColumn,
scope: scope,
expression: expression,
);
@ -638,7 +693,7 @@ class ExpressionEvaluationTestDriver {
String expression) async {
// Perform an incremental compile.
return await compiler.compileExpression(
input: input,
libraryUri: input,
line: 1,
column: 1,
scope: {},
@ -648,15 +703,13 @@ class ExpressionEvaluationTestDriver {
Future<wip.RemoteObject> _evaluateDartExpressionInFrame(
wip.DebuggerPausedEvent event,
String expression,
int dartLine, {
String expression, {
bool returnByValue = false,
}) async {
var frame = event.getCallFrames().first;
var result = await _compileDartExpressionInFrame(
frame,
expression,
dartLine,
);
if (!result.isSuccess) {
@ -782,29 +835,48 @@ class ExpressionEvaluationTestDriver {
return matches(RegExp(unindented, multiLine: true));
}
/// Finds the line number in [source] matching [breakpointId].
/// Finds the first line number in [source] or [partSource] matching
/// [breakpointId].
///
/// A breakpoint ID is found by looking for a line that ends with a comment
/// of exactly this form: `// Breakpoint: <id>`.
///
/// Throws if it can't find the matching line.
/// Throws if it can't find a matching line.
///
/// The returned map entry is the uri (key) and the 1-indexed line number of
/// the comment (value).
/// Note that we often put the comment on the line *before* where we actually
/// want the breakpoint, and that the value can thus be seen as being that
/// line but then being 0-indexed.
///
/// Adapted from webdev/blob/master/dwds/test/fixtures/context.dart.
int _findBreakpointLine(String breakpointId) {
var lines = LineSplitter.split(source).toList();
var lineNumber =
lines.indexWhere((l) => l.endsWith('// Breakpoint: $breakpointId'));
if (lineNumber == -1) {
throw StateError(
'Unable to find breakpoint in $input with id: $breakpointId');
MapEntry<Uri, int> _findBreakpointLine(String breakpointId) {
var lineNumber = _findBreakpointLineImpl(breakpointId, source);
if (lineNumber >= 0) {
return MapEntry(input, lineNumber + 1);
}
return lineNumber + 1;
if (partSource != null) {
lineNumber = _findBreakpointLineImpl(breakpointId, partSource!);
if (lineNumber >= 0) {
return MapEntry(inputPart!, lineNumber + 1);
}
}
throw StateError(
'Unable to find breakpoint in $input with id: $breakpointId');
}
/// Finds the 0-indexed line number in [source] for the given breakpoint id.
static int _findBreakpointLineImpl(String breakpointId, String source) {
var lines = LineSplitter.split(source).toList();
return lines.indexWhere((l) => l.endsWith('// Breakpoint: $breakpointId'));
}
/// Finds the corresponding JS WipLocation for a given line in Dart.
/// The input [dartLine] is 1-indexed, but really refers to the following line
/// meaning that it talks about the following line in a 0-indexed manner.
Future<wip.WipLocation> _jsLocationFromDartLine(
wip.WipScript script, int dartLine) async {
var inputSourceUrl = input.pathSegments.last;
wip.WipScript script, int dartLine, Uri lineIn) async {
var inputSourceUrl = lineIn.pathSegments.last;
for (var lineEntry in compiler.sourceMap.lines) {
for (var entry in lineEntry.entries) {
if (entry.sourceUrlId != null &&

View file

@ -60,7 +60,7 @@ class ExpressionCompilerTestDriver {
required String expression,
}) async {
return compiler.compileExpression(
input: input,
libraryUri: input,
line: line,
column: 1,
scope: scope,

View file

@ -739,4 +739,247 @@ void runSharedTests(
);
});
});
group('extension type expression compilations |', () {
var source = r'''
//@dart=3.3
void main() {
Foo f = new Foo(42);
Baz b = new Baz(new Bar(42));
print(f);
print(b);
// Breakpoint: BP1
print(f.value);
print(b.value);
f.printValue();
f.printThis();
b.printThis();
}
class Bar {
final int i;
Bar(this.i);
String toString() => "Bar[$i]";
}
extension type Foo(int value) {
void printValue() {
// Breakpoint: BP2
print("This foos value is '$value'");
}
String printThis() {
var foo = value;
// Breakpoint: BP3
print("This foos this value is '$this'");
return "I printed '$value'!";
}
}
extension type Baz(Bar value) {
String printThis() {
var foo = value;
// Breakpoint: BP4
print("This Baz' this value is '$this'");
return "I printed '$value'!";
}
}
''';
setUpAll(() async {
await driver.initSource(setup, source);
});
tearDownAll(() async {
await driver.cleanupTest();
});
test('value on extension type (int)', () async {
var result = await driver.evaluateDartExpressionInFrame(
breakpointId: 'BP1',
expression: 'f.value',
);
expect(result, '42');
});
test('value on extension type (custom class)', () async {
var result = await driver.evaluateDartExpressionInFrame(
breakpointId: 'BP1',
expression: 'b.value.toString()',
);
expect(result, 'Bar[42]');
});
test('method on extension type (int)', () async {
var result = await driver.evaluateDartExpressionInFrame(
breakpointId: 'BP1',
expression: 'f.printThis()',
);
expect(result, "I printed '42'!");
});
test('method on extension type (custom class)', () async {
var result = await driver.evaluateDartExpressionInFrame(
breakpointId: 'BP1',
expression: 'b.printThis()',
);
expect(result, "I printed 'Bar[42]'!");
});
test('inside extension type method (int) (1)', () async {
var result = await driver.evaluateDartExpressionInFrame(
breakpointId: 'BP2',
expression: 'printThis()',
);
expect(result, "I printed '42'!");
});
test('inside extension type method (int) (2)', () async {
var result = await driver.evaluateDartExpressionInFrame(
breakpointId: 'BP3',
expression: 'foo + value',
);
expect(result, '84');
});
test('inside extension type method (custom class) (1)', () async {
var result = await driver.evaluateDartExpressionInFrame(
breakpointId: 'BP4',
expression: 'printThis()',
);
expect(result, "I printed 'Bar[42]'!");
});
test('inside extension type method (custom class) (2)', () async {
var result = await driver.evaluateDartExpressionInFrame(
breakpointId: 'BP4',
expression: 'foo.i + value.i',
);
expect(result, '84');
});
});
group('extensions expression compilations |', () {
var source = r'''
void main() {
int i = 42;
Bar b = new Bar(42);
print(i);
print(b);
// Breakpoint: BP1
i.printThis();
b.printThis();
}
class Bar {
final int i;
Bar(this.i);
String toString() => "Bar[$i]";
}
extension Foo on int {
String printThis() {
var value = this;
// Breakpoint: BP2
print("This foos this value is '$this'");
return "I printed '$value'!";
}
}
extension Baz on Bar {
String printThis() {
var value = this;
// Breakpoint: BP3
print("This Bars this value is '$this'");
return "I printed '$value'!";
}
}''';
setUpAll(() async {
await driver.initSource(setup, source);
});
tearDownAll(() async {
await driver.cleanupTest();
});
test('call function on extension (int)', () async {
var result = await driver.evaluateDartExpressionInFrame(
breakpointId: 'BP1',
expression: 'i.printThis()',
);
expect(result, "I printed '42'!");
});
test('call function on extension (custom class)', () async {
var result = await driver.evaluateDartExpressionInFrame(
breakpointId: 'BP1',
expression: 'b.printThis()',
);
expect(result, "I printed 'Bar[42]'!");
});
test('inside extension method (int) (1)', () async {
var result = await driver.evaluateDartExpressionInFrame(
breakpointId: 'BP2',
expression: 'printThis()',
);
expect(result, "I printed '42'!");
});
test('inside extension type method (int) (2)', () async {
var result = await driver.evaluateDartExpressionInFrame(
breakpointId: 'BP2',
expression: 'this + value',
);
expect(result, '84');
});
test('inside extension method (custom class) (1)', () async {
var result = await driver.evaluateDartExpressionInFrame(
breakpointId: 'BP3',
expression: 'printThis()',
);
expect(result, "I printed 'Bar[42]'!");
});
test('inside extension type method (custom class) (2)', () async {
var result = await driver.evaluateDartExpressionInFrame(
breakpointId: 'BP3',
expression: 'this.i + value.i',
);
expect(result, '84');
});
});
group('part files expression compilations |', () {
// WARNING: The (main) source and the part source have been constructred
// so that the same offset (71) is valid on both, and both have an 'x'
// variable, where one is a String and the other is an int. The 4 dots after
// 'padding' for instance is not a mistake.
var source = r'''
part 'part.dart';
void main() {
String x = "foo";
// padding....
foo();
print(x);
}''';
var partSource = r'''
part of 'test.dart';
void foo() {
int x = 42;
// Breakpoint: BP1
print(x);
}''';
setUpAll(() async {
await driver.initSource(setup, source, partSource: partSource);
});
tearDownAll(() async {
await driver.cleanupTest();
});
test('can evaluate in part file', () async {
var result = await driver.evaluateDartExpressionInFrame(
breakpointId: 'BP1',
expression: 'x + 1',
);
expect(result, '43');
});
});
}

View file

@ -123,8 +123,10 @@ class TestExpressionCompiler {
setup, component, compiler, code.metadata, sourceMap);
}
// Line and column are 1-based.
Future<TestCompilationResult> compileExpression(
{required Uri input,
{required Uri libraryUri,
Uri? scriptUri,
required int line,
required int column,
required Map<String, String> scope,
@ -132,9 +134,14 @@ class TestExpressionCompiler {
// clear previous errors
setup.errors.clear();
var libraryUri = metadataForLibraryUri(input);
var libraryMetadata = metadataForLibraryUri(libraryUri);
var jsExpression = await compiler.compileExpressionToJs(
libraryUri.importUri, line, column, scope, expression);
libraryMetadata.importUri,
scriptUri?.toString(),
line,
column,
scope,
expression);
if (setup.errors.isNotEmpty) {
jsExpression = setup.errors.toString().replaceAll(
RegExp(

View file

@ -809,3 +809,11 @@ String extractJoinedIntermediateName(String name) {
String createJoinedIntermediateName(String variableName, int index) {
return '$variableName$joinedIntermediateInfix$index';
}
/// This turns Foo|bar into Foo.bar.
///
/// This only works for normal methods and operators, but for getters and
/// setters.
String? extractQualifiedNameFromExtensionMethodName(String? methodName) {
return methodName?.replaceFirst('|', '.');
}

View file

@ -123,6 +123,20 @@ class NameScheme {
}
}
static String createProcedureNameForTesting(
{required ContainerName? containerName,
required ContainerType containerType,
required bool isStatic,
required ProcedureKind kind,
required String name}) {
return _createProcedureName(
containerName: containerName,
containerType: containerType,
isStatic: isStatic,
kind: kind,
name: name);
}
static String _createProcedureName(
{required ContainerName? containerName,
required ContainerType containerType,

View file

@ -0,0 +1,53 @@
// Copyright (c) 2023, 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:front_end/src/api_prototype/lowering_predicates.dart';
import 'package:front_end/src/fasta/source/name_scheme.dart';
import 'package:kernel/ast.dart';
void main() {
testExtractQualifiedNameFromExtensionMethodName();
}
void testExtractQualifiedNameFromExtensionMethodName() {
// Doesn't crash on null and returns null in that case.
expect(extractQualifiedNameFromExtensionMethodName(null), null);
// When given data it actually extracts what we want.
for (ContainerType containerType in [
ContainerType.ExtensionType,
ContainerType.Extension
]) {
for (bool isStatic in [true, false]) {
String encodedName = NameScheme.createProcedureNameForTesting(
containerName: new TesterContainerName("Foo"),
containerType: containerType,
isStatic: isStatic,
kind: ProcedureKind.Method,
name: "bar");
String extracted =
extractQualifiedNameFromExtensionMethodName(encodedName)!;
expectDifferent(encodedName, extracted);
expect(extracted, "Foo.bar");
}
}
}
class TesterContainerName extends ContainerName {
@override
final String name;
TesterContainerName(this.name);
@override
void attachMemberName(MemberName name) {}
}
void expect(Object? actual, Object? expect) {
if (expect != actual) throw "Expected $expect got $actual";
}
void expectDifferent(Object? actual, Object? expectNot) {
if (expectNot == actual) throw "Expected not $expectNot got $actual";
}

View file

@ -334,8 +334,14 @@ abstract class CompilerInterface {
String? scriptUri,
bool isStatic);
/// Compiles [expression] in [libraryUri] at [line]:[column] to JavaScript
/// in [moduleName].
/// Compiles [expression] in library [libraryUri] and file [scriptUri]
/// at [line]:[column] to JavaScript in [moduleName].
///
/// [libraryUri] and [scriptUri] can be the same, but if for instance
/// evaluating expressions in a part file the [libraryUri] will be the uri of
/// the "part of" file whereas [scriptUri] will be the uri of the part.
///
/// [line] and [column] are 1-based.
///
/// Values listed in [jsFrameValues] are substituted for their names in the
/// [expression].
@ -354,6 +360,7 @@ abstract class CompilerInterface {
/// { 'dart':'dart_sdk', 'main': '/packages/hello_world_main.dart' }
Future<void> compileExpressionToJs(
String libraryUri,
String? scriptUri,
int line,
int column,
Map<String, String> jsModules,
@ -1022,6 +1029,7 @@ class FrontendCompiler implements CompilerInterface {
@override
Future<void> compileExpressionToJs(
String libraryUri,
String? scriptUri,
int line,
int column,
Map<String, String> jsModules,
@ -1064,7 +1072,7 @@ class FrontendCompiler implements CompilerInterface {
);
final String? procedure = await expressionCompiler.compileExpressionToJs(
libraryUri, line, column, jsFrameValues, expression);
libraryUri, scriptUri, line, column, jsFrameValues, expression);
final String result = errors.isNotEmpty ? errors[0] : procedure!;
@ -1497,6 +1505,7 @@ StreamSubscription<String> listenAndCompile(CompilerInterface compiler,
compileExpressionToJsRequest.expression = string;
await compiler.compileExpressionToJs(
compileExpressionToJsRequest.libraryUri,
null /* not supported here - use json! */,
compileExpressionToJsRequest.line,
compileExpressionToJsRequest.column,
compileExpressionToJsRequest.jsModules,
@ -1634,6 +1643,7 @@ Future<void> processJsonInput(
} else if (type == "COMPILE_EXPRESSION_JS") {
String expression = getValue<String>("expression") ?? "";
String libraryUri = getValue<String>("libraryUri") ?? "";
String? scriptUri = getValue<String?>("scriptUri");
int line = getValue<int>("line") ?? -1;
int column = getValue<int>("column") ?? -1;
Map<String, String> jsModules = getMap("jsModules") ?? {};
@ -1650,6 +1660,7 @@ Future<void> processJsonInput(
await compiler.compileExpressionToJs(
libraryUri,
scriptUri,
line,
column,
jsModules,

View file

@ -739,6 +739,107 @@ extension type Foo(int value) {
frontendServer.close();
});
// TODO(jensj): This is the javascript version of the above.
// It should share code.
test('compile expression extension types to JavaScript',
skip: !useJsonForCommunication, () async {
File file = new File('${tempDir.path}/foo.dart')..createSync();
String data = r"""
//@dart=3.3
void main() {
Foo f = new Foo(42);
print(f);
print(f.value);
f.printValue();
f.printThis();
}
extension type Foo(int value) {
void printValue() {
print("This foos value is '$value'");
}
void printThis() {
print("This foos this value is '$this'");
}
}""";
file.writeAsStringSync(data);
File packageConfig =
new File('${tempDir.path}/.dart_tool/package_config.json')
..createSync(recursive: true)
..writeAsStringSync('''
{
"configVersion": 2,
"packages": [
{
"name": "hello",
"rootUri": "../",
"packageUri": "./"
}
]
}
''');
String library = 'package:hello/foo.dart';
String module = 'packages/hello/foo.dart';
File dillFile = new File('${tempDir.path}/foo.dart.dill');
File sourceFile = new File('${dillFile.path}.sources');
File manifestFile = new File('${dillFile.path}.json');
File sourceMapsFile = new File('${dillFile.path}.map');
expect(dillFile.existsSync(), equals(false));
final List<String> args = <String>[
'--sdk-root=${sdkRoot.toFilePath()}',
'--incremental',
'--platform=${ddcPlatformKernel.path}',
'--output-dill=${dillFile.path}',
'--target=dartdevc',
'--packages=${packageConfig.path}',
];
final FrontendServer frontendServer = new FrontendServer();
Future<int> result = frontendServer.open(args);
frontendServer.compile(file.path);
int count = 0;
frontendServer.listen((Result compiledResult) {
CompilationResult result =
new CompilationResult.parse(compiledResult.status);
if (count == 0) {
// First request is to 'compile', which results in full JavaScript
expect(result.errorsCount, equals(0));
expect(sourceFile.existsSync(), equals(true));
expect(manifestFile.existsSync(), equals(true));
expect(sourceMapsFile.existsSync(), equals(true));
expect(result.filename, dillFile.path);
frontendServer.accept();
frontendServer.compileExpressionToJs('f.value', library, 5, 3, module,
scriptUri: file.uri, jsFrameValues: {"f": "42"});
count += 1;
} else if (count == 1) {
expect(result.errorsCount, equals(0));
File outputFile = new File(result.filename);
expect(outputFile.existsSync(), equals(true));
expect(outputFile.lengthSync(), isPositive);
frontendServer.compileExpressionToJs(
'this.value', library, 11, 5, module,
scriptUri: file.uri, jsFrameValues: {r"$this": "42"});
count += 1;
} else if (count == 2) {
expect(result.errorsCount, equals(0));
File outputFile = new File(result.filename);
expect(outputFile.existsSync(), equals(true));
expect(outputFile.lengthSync(), isPositive);
frontendServer.quit();
}
});
expect(await result, 0);
expect(count, 2);
frontendServer.close();
});
test('mixed compile expression commands with non-web target', () async {
File file = new File('${tempDir.path}/foo.dart')..createSync();
file.writeAsStringSync("main() {}\n");
@ -3323,7 +3424,9 @@ class FrontendServer {
// TODO(johnniwinther): Use (required) named arguments.
void compileExpressionToJs(String expression, String libraryUri, int line,
int column, String moduleName,
{String boundaryKey = 'abc'}) {
{Uri? scriptUri,
Map<String, String>? jsFrameValues,
String boundaryKey = 'abc'}) {
if (useJsonForCommunication) {
outputParser.expectSources = false;
inputStreamController.add('JSON_INPUT\n'.codeUnits);
@ -3332,6 +3435,8 @@ class FrontendServer {
"data": {
"expression": expression,
"libraryUri": libraryUri,
if (scriptUri != null) "scriptUri": scriptUri.toString(),
if (jsFrameValues != null) "jsFrameValues": jsFrameValues,
"line": line,
"column": column,
"moduleName": moduleName,

View file

@ -556,6 +556,11 @@ class DartScopeBuilder2 extends VisitorDefault<void> with VisitorVoidMixin {
static DartScope findScopeFromOffsetAndClass(
Library library, Uri scriptUri, Class? cls, int offset) {
List<DartScope2> scopes = _raw(library, scriptUri, cls, offset);
return _findScopePick(scopes, library, cls, offset);
}
static DartScope _findScopePick(
List<DartScope2> scopes, Library library, Class? cls, int offset) {
DartScope2 scope;
if (scopes.length == 0) {
// This shouldn't happen.
@ -586,6 +591,12 @@ class DartScopeBuilder2 extends VisitorDefault<void> with VisitorVoidMixin {
scope.typeParameters);
}
static DartScope findScopeFromOffset(
Library library, Uri scriptUri, int offset) {
List<DartScope2> scopes = _rawNoClass(library, scriptUri, offset);
return _findScopePick(scopes, library, null, offset);
}
static List<DartScope2> _filterAll(
List<DartScope2> rawScopes, Library library, int offset) {
List<DartScope2> firstFilteredScopes =
@ -1155,6 +1166,13 @@ class DartScopeBuilder2 extends VisitorDefault<void> with VisitorVoidMixin {
return builder.findScopes;
}
static List<DartScope2> _rawNoClass(
Library library, Uri scriptUri, int offset) {
DartScopeBuilder2 builder = DartScopeBuilder2._(library, scriptUri, offset);
builder.visitLibrary(library);
return builder.findScopes;
}
static List<DartScope2> findScopeFromOffsetAndClassRawForTesting(
Library library, Uri scriptUri, Class? cls, int offset) =>
_raw(library, scriptUri, cls, offset);