blob: 62e2455e9f420cc9a527a2b6e5b3a8938a6b76dd [file] [log] [blame]
// Copyright (c) 2020, 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:convert';
import 'package:async/async.dart';
import 'package:dwds/src/debugging/metadata/module_metadata.dart';
import 'package:dwds/src/readers/asset_reader.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart' as p;
/// A provider of metadata in which data is collected through DDC outputs.
class MetadataProvider {
final AssetReader _assetReader;
final _logger = Logger('MetadataProvider');
final String entrypoint;
bool _soundNullSafety;
final List<String> _libraries = [];
final Map<String, String> _scriptToModule = {};
final Map<String, String> _moduleToSourceMap = {};
final Map<String, String> _modulePathToModule = {};
final Map<String, String> _moduleToModulePath = {};
final Map<String, List<String>> _scripts = {};
final _metadataMemoizer = AsyncMemoizer();
/// Implicitly imported libraries in any DDC component.
///
/// Currently dart_sdk module does not come with the metadata.
/// To allow evaluation of expressions that use libraries and
/// types from the SDK (such as a dart Type object), add the
/// metadata for dart_sdk manually.
///
/// TODO: Generate sdk module metadata to be consumed by debugger.
/// Issue: https://github.com/dart-lang/sdk/issues/45477
List<String> get sdkLibraries => const [
'dart:_runtime',
'dart:_debugger',
'dart:_foreign_helper',
'dart:_interceptors',
'dart:_internal',
'dart:_isolate_helper',
'dart:_js_helper',
'dart:_js_primitives',
'dart:_metadata',
'dart:_native_typed_data',
'dart:_rti',
'dart:async',
'dart:collection',
'dart:convert',
'dart:core',
'dart:developer',
'dart:io',
'dart:isolate',
'dart:js',
'dart:js_util',
'dart:math',
'dart:typed_data',
'dart:indexed_db',
'dart:html',
'dart:html_common',
'dart:svg',
'dart:web_audio',
'dart:web_gl',
'dart:ui',
];
MetadataProvider(this.entrypoint, this._assetReader)
: _soundNullSafety = false;
/// A sound null safety mode for the whole app.
///
/// All libraries have to agree on null safety mode.
Future<bool> get soundNullSafety async {
await _initialize();
return _soundNullSafety;
}
/// A list of all libraries in the Dart application.
///
/// Example:
///
/// [
/// dart:web_gl,
/// dart:math,
/// org-dartlang-app:///web/main.dart
/// ]
///
Future<List<String>> get libraries async {
await _initialize();
return _libraries;
}
/// A map of library uri to dart scripts.
///
/// Example:
///
/// {
/// org-dartlang-app:///web/main.dart :
/// { web/main.dart }
/// }
///
Future<Map<String, List<String>>> get scripts async {
await _initialize();
return _scripts;
}
/// A map of script to containing module.
///
/// Example:
///
/// {
/// org-dartlang-app:///web/main.dart :
/// web/main
/// }
///
Future<Map<String, String>> get scriptToModule async {
await _initialize();
return _scriptToModule;
}
/// A map of module name to source map path.
///
/// Example:
///
/// {
/// org-dartlang-app:///web/main.dart :
/// web/main.ddc.js.map
/// }
///
///
Future<Map<String, String>> get moduleToSourceMap async {
await _initialize();
return _moduleToSourceMap;
}
/// A map of module path to module name
///
/// Example:
///
/// {
/// web/main.ddc.js :
/// web/main
/// }
///
Future<Map<String, String>> get modulePathToModule async {
await _initialize();
return _modulePathToModule;
}
/// A map of module to module path
///
/// Example:
///
/// {
/// web/main
/// web/main.ddc.js :
/// }
///
Future<Map<String, String>> get moduleToModulePath async {
await _initialize();
return _moduleToModulePath;
}
/// A list of module ids
///
/// Example:
///
/// [
/// web/main,
/// web/foo/bar
/// ]
///
Future<List<String>> get modules async {
await _initialize();
return _moduleToModulePath.keys.toList();
}
Future<void> _initialize() async {
await _metadataMemoizer.runOnce(() async {
var hasSoundNullSafety = true;
var hasUnsoundNullSafety = true;
// The merged metadata resides next to the entrypoint.
// Assume that <name>.bootstrap.js has <name>.ddc_merged_metadata
if (entrypoint.endsWith('.bootstrap.js')) {
_logger.info('Loading debug metadata...');
final serverPath =
entrypoint.replaceAll('.bootstrap.js', '.ddc_merged_metadata');
final merged = await _assetReader.metadataContents(serverPath);
if (merged != null) {
_addSdkMetadata();
for (var contents in merged.split('\n')) {
try {
if (contents.isEmpty ||
contents.startsWith('// intentionally empty:')) continue;
final moduleJson = json.decode(contents);
final metadata =
ModuleMetadata.fromJson(moduleJson as Map<String, dynamic>);
_addMetadata(metadata);
hasUnsoundNullSafety &= !metadata.soundNullSafety;
hasSoundNullSafety &= metadata.soundNullSafety;
_logger
.fine('Loaded debug metadata for module: ${metadata.name}');
} catch (e) {
_logger.warning('Failed to read metadata: $e');
rethrow;
}
}
if (!hasSoundNullSafety && !hasUnsoundNullSafety) {
throw Exception('Metadata contains modules with mixed null safety');
}
_soundNullSafety = hasSoundNullSafety;
}
_logger.info('Loaded debug metadata '
'(${_soundNullSafety ? "sound" : "weak"} null safety)');
}
});
}
void _addMetadata(ModuleMetadata metadata) {
final modulePath = stripLeadingSlashes(metadata.moduleUri);
final sourceMapPath = stripLeadingSlashes(metadata.sourceMapUri);
_moduleToSourceMap[metadata.name] = sourceMapPath;
_modulePathToModule[modulePath] = metadata.name;
_moduleToModulePath[metadata.name] = modulePath;
for (var library in metadata.libraries.values) {
if (library.importUri.startsWith('file:/')) {
throw AbsoluteImportUriException(library.importUri);
}
_libraries.add(library.importUri);
_scripts[library.importUri] = [];
_scriptToModule[library.importUri] = metadata.name;
for (var path in library.partUris) {
// Parts in metadata are relative to the library Uri directory.
final partPath = p.url.join(p.dirname(library.importUri), path);
_scripts[library.importUri]!.add(partPath);
_scriptToModule[partPath] = metadata.name;
}
}
}
void _addSdkMetadata() {
final moduleName = 'dart_sdk';
for (var lib in sdkLibraries) {
_libraries.add(lib);
_scripts[lib] = [];
_scriptToModule[lib] = moduleName;
}
}
}
class AbsoluteImportUriException implements Exception {
final String importUri;
AbsoluteImportUriException(this.importUri);
@override
String toString() => "AbsoluteImportUriError: '$importUri'";
}