Files
sdk/pkg/dev_compiler/test/sourcemap/ddc_common.dart
T
Mark Zhou dbabe4dea6 [dartdevc] Imports/Exports are now aliased relative to the library root to disambiguate cross-module name conflicts.
Note: Changes in this CL will require some google3 changes (mostly in code related to test entrypoints).

We encountered an issue whereby cross-module programs importing files
with the same name (but separate identities/locations) were being
overridden throughout all of DDK's module builders. For example, a
module importing "x.dart" and "y/x.dart" would export {x: x, x: x$} -
the latter renamed module shadowing the first. DDC handles this by
prepending a formatted module-root-relative identifier string, so
"y/x.dart" becomes something like y__x, and the export becomes {x: x,
y__x: y__x}. However, this results in some code bloat, especially as
paths become deeply nested. More importantly, the $-renaming scheme is
not consistent across modules. Were we to export x$ instead of x, how
would the importing library know whether x or x$ referred to "y/x.dart"?

To have the best of both worlds, we're introducing the concept of a
module alias that exists at the boundary of module imports and exports.
We use this module-root-relative alias to differentiate modules with the
same name but use the shorter $-renaming scheme throughout the generated
code. Relevant points:

1) jsLibraryAlias has been introduced alongside jsLibraryName.
jsLibraryName no longer must be unique - that property having been
transferred to the alias. DDK no longer just outputs pathSegments.last,
instead adopting DDC's scheme of "creating" a special alias from the
module root. jsLibraryName's temporary IDs are renamed using the
existing DDK scheme.

2) Libraries are now associated with NameSpecifiers. The name and asName
fields represent jsName and jsAlias, respectively, for exports
(vice-versa for imports).

3) The test frameworks have been taking advantage of DDK's previous
naming scheme (throwing way the module root, keeping just the name). A
common pattern is to expect DDK's output symbol to be "someexport", then
forcing DDC's output (normally "some__path__someexport") to resemble
that with the deprecated "library-root" flag set to the path of the
output JS file. We resolve this by updating the scaffolding code to look
for the module-root-relative (long) name, which requires a bunch of
path-to-identifier subsitution logic. The "absoluteRoot" bool in the
sourcemap test runner is a way to reenable this DDC-specific deprecated
behavior. The test runner now requires this alias in the html generator.

Fixes https://github.com/dart-lang/sdk/issues/37473

Change-Id: Iaaa82f3350d424af195967b55c87cb32b30a3d85
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/108942
Reviewed-by: Nate Bosch <nbosch@google.com>
Reviewed-by: Bob Nystrom <rnystrom@google.com>
2019-08-01 17:24:59 +00:00

309 lines
9.2 KiB
Dart

// Copyright (c) 2017, 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.
library dev_compiler.test.sourcemap.ddc_common;
import 'dart:io';
import 'dart:mirrors' show currentMirrorSystem;
import 'package:front_end/src/api_unstable/ddc.dart' as fe;
import 'package:front_end/src/testing/annotated_code_helper.dart';
import 'package:path/path.dart' as p;
import 'package:sourcemap_testing/src/stacktrace_helper.dart';
import 'package:sourcemap_testing/src/stepping_helper.dart';
import 'package:testing/testing.dart';
import 'common.dart';
abstract class CompilerRunner {
Future<Null> run(Uri inputFile, Uri outputFile, Uri outWrapperPath);
}
abstract class WithCompilerState {
fe.InitializedCompilerState compilerState;
}
class Compile extends Step<Data, Data, ChainContext> {
final CompilerRunner runner;
const Compile(this.runner);
@override
String get name => "compile";
@override
Future<Result<Data>> run(Data data, ChainContext context) async {
var dartScriptAbsolute = File.fromUri(data.uri).absolute;
var inputFile = dartScriptAbsolute.path;
data.outDir = await Directory.systemTemp.createTemp("ddc_step_test");
data.code = AnnotatedCode.fromText(
File(inputFile).readAsStringSync(), commentStart, commentEnd);
var outDirUri = data.outDir.uri;
var testFile = outDirUri.resolve("test.dart");
File.fromUri(testFile).writeAsStringSync(data.code.sourceCode);
var outputFilename = "js.js";
var outputFile = outDirUri.resolve(outputFilename);
var outWrapperPath = outDirUri.resolve("wrapper.js");
await runner.run(testFile, outputFile, outWrapperPath);
return pass(data);
}
}
class TestStackTrace extends Step<Data, Data, ChainContext> {
final CompilerRunner runner;
final String marker;
final List<String> knownMarkers;
const TestStackTrace(this.runner, this.marker, this.knownMarkers);
@override
String get name => "TestStackTrace";
@override
Future<Result<Data>> run(Data data, ChainContext context) async {
data.outDir = await Directory.systemTemp.createTemp("stacktrace-test");
String code = await File.fromUri(data.uri).readAsString();
Test test = processTestCode(code, knownMarkers);
await testStackTrace(test, marker, _compile,
jsPreambles: _getPreambles,
useJsMethodNamesOnAbsence: true,
jsNameConverter: _convertName,
forcedTmpDir: data.outDir,
verbose: true);
return pass(data);
}
Future<bool> _compile(String input, String output) async {
var outWrapperPath = _getWrapperPathFromDirectoryFile(Uri.file(input));
await runner.run(Uri.file(input), Uri.file(output), outWrapperPath);
return true;
}
List<String> _getPreambles(String input, String output) {
return [
'--module',
_getWrapperPathFromDirectoryFile(Uri.file(input)).toFilePath(),
'--'
];
}
Uri _getWrapperPathFromDirectoryFile(Uri input) {
return input.resolve("wrapper.js");
}
String _convertName(String name) {
if (name == null) return null;
// Hack for DDC naming scheme.
String result = name;
if (result.startsWith("new ")) result = result.substring(4);
if (result.startsWith("Object.")) result = result.substring(7);
String inputName =
INPUT_FILE_NAME.substring(0, INPUT_FILE_NAME.indexOf(".") + 1);
if (result.startsWith(inputName)) {
result = result.substring(inputName.length);
}
return result;
}
}
Directory _cachedDdcDir;
Directory getDdcDir() {
Directory search() {
Directory dir = File.fromUri(Platform.script).parent;
Uri dirUrl = dir.uri;
if (dirUrl.pathSegments.contains("dev_compiler")) {
for (int i = dirUrl.pathSegments.length - 2; i >= 0; --i) {
// Directory uri ends in empty string
if (dirUrl.pathSegments[i] == "dev_compiler") break;
dir = dir.parent;
}
return dir;
}
throw "Cannot find DDC directory.";
}
return _cachedDdcDir ??= search();
}
String getWrapperContent(
Uri jsSdkPath, String inputFileNameNoExt, String outputFilename) {
assert(jsSdkPath.isAbsolute);
return """
import { dart, _isolate_helper } from '${uriPathForwardSlashed(jsSdkPath)}';
import { $inputFileNameNoExt } from '$outputFilename';
let global = new Function('return this;')();
$d8Preambles
// d8 does not seem to print the `.stack` property like
// node.js and browsers do, so include that.
Error.prototype.toString = function() {
// Note: on d8, the stack property includes the error message too.
return this.stack;
};
global.scheduleImmediate = function(callback) {
// Ensure unhandled promise rejections get printed.
Promise.resolve(null).then(callback).catch(e => console.error(e));
};
let main = $inputFileNameNoExt.main;
try {
dartMainRunner(main, []);
} catch(e) {
console.error(e);
}
""";
}
void createHtmlWrapper(File sdkJsFile, Uri outputFile, String jsContent,
String outputFilename, Uri outDir) {
// For debugging via HTML, Chrome and ./pkg/test_runner/bin/http_server.dart.
var sdkFile = File(p.relative(sdkJsFile.path, from: sdkRoot.path));
String jsRootDart = "/root_dart/${sdkFile.uri}";
File.fromUri(outputFile.resolve("$outputFilename.html.js")).writeAsStringSync(
jsContent.replaceFirst("from 'dart_sdk.js'", "from '$jsRootDart'"));
File.fromUri(outputFile.resolve("$outputFilename.html.html"))
.writeAsStringSync(getWrapperHtmlContent(
jsRootDart, "/root_build/$outputFilename.html.js"));
print("You should now be able to run\n\n"
"dart ${sdkRoot.path}/pkg/test_runner/bin/http_server.dart -p 39550 "
"--network 127.0.0.1 "
"--build-directory=${outDir.toFilePath()}"
"\n\nand go to\n\n"
"http://127.0.0.1:39550/root_build/$outputFilename.html.html"
"\n\nto step through via the browser.");
}
String getWrapperHtmlContent(String jsRootDart, String outFileRootBuild) {
return """
<!DOCTYPE html>
<html>
<head>
<title>ddc test</title>
<script type="module">
import { dart, _isolate_helper } from '$jsRootDart';
import { test } from '$outFileRootBuild';
let main = test.main;
main();
</script>
</head>
<body>
<h1>ddc test</h1>
</body>
</html>
""";
}
Uri selfUri = currentMirrorSystem()
.findLibrary(#dev_compiler.test.sourcemap.ddc_common)
.uri;
String d8Preambles = File.fromUri(selfUri.resolve(
'../../../../sdk/lib/_internal/js_dev_runtime/private/preambles/d8.js'))
.readAsStringSync();
/// Transforms a path to a valid JS identifier.
///
/// This logic must be synchronized with [pathToJSIdentifier] in DDC at:
/// pkg/dev_compiler/lib/src/compiler/module_builder.dart
String pathToJSIdentifier(String path) {
path = p.normalize(path);
if (path.startsWith('/') || path.startsWith('\\')) {
path = path.substring(1, path.length);
}
return _toJSIdentifier(path
.replaceAll('\\', '__')
.replaceAll('/', '__')
.replaceAll('..', '__')
.replaceAll('-', '_'));
}
/// Escape [name] to make it into a valid identifier.
String _toJSIdentifier(String name) {
if (name.isEmpty) return r'$';
// Escape any invalid characters
StringBuffer buffer;
for (var i = 0; i < name.length; i++) {
var ch = name[i];
var needsEscape = ch == r'$' || _invalidCharInIdentifier.hasMatch(ch);
if (needsEscape && buffer == null) {
buffer = StringBuffer(name.substring(0, i));
}
if (buffer != null) {
buffer.write(needsEscape ? '\$${ch.codeUnits.join("")}' : ch);
}
}
var result = buffer != null ? '$buffer' : name;
// Ensure the identifier first character is not numeric and that the whole
// identifier is not a keyword.
if (result.startsWith(RegExp('[0-9]')) || _invalidVariableName(result)) {
return '\$$result';
}
return result;
}
// Invalid characters for identifiers, which would need to be escaped.
final _invalidCharInIdentifier = RegExp(r'[^A-Za-z_$0-9]');
bool _invalidVariableName(String keyword) {
switch (keyword) {
// http://www.ecma-international.org/ecma-262/6.0/#sec-future-reserved-words
case "await":
case "break":
case "case":
case "catch":
case "class":
case "const":
case "continue":
case "debugger":
case "default":
case "delete":
case "do":
case "else":
case "enum":
case "export":
case "extends":
case "finally":
case "for":
case "function":
case "if":
case "import":
case "in":
case "instanceof":
case "let":
case "new":
case "return":
case "super":
case "switch":
case "this":
case "throw":
case "try":
case "typeof":
case "var":
case "void":
case "while":
case "with":
case "arguments":
case "eval":
// http://www.ecma-international.org/ecma-262/6.0/#sec-future-reserved-words
// http://www.ecma-international.org/ecma-262/6.0/#sec-identifiers-static-semantics-early-errors
case "implements":
case "interface":
case "package":
case "private":
case "protected":
case "public":
case "static":
case "yield":
return true;
}
return false;
}