// 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. // @dart = 2.7 import 'dart:io' hide Link; import 'package:_fe_analyzer_shared/src/testing/features.dart'; import 'package:async_helper/async_helper.dart'; import 'package:compiler/src/closure.dart'; import 'package:compiler/src/common.dart'; import 'package:compiler/src/compiler.dart'; import 'package:compiler/src/deferred_load.dart'; import 'package:compiler/src/elements/entities.dart'; import 'package:compiler/src/ir/util.dart'; import 'package:compiler/src/js_model/element_map.dart'; import 'package:compiler/src/js_model/js_world.dart'; import 'package:compiler/src/js_emitter/startup_emitter/fragment_merger.dart'; import 'package:compiler/src/kernel/kernel_strategy.dart'; import 'package:expect/expect.dart'; import '../equivalence/id_equivalence.dart'; import '../equivalence/id_equivalence_helper.dart'; import 'package:compiler/src/constants/values.dart'; import 'package:kernel/ast.dart' as ir; /// Add in options to pass to the compiler like /// `Flags.disableTypeInference` or `Flags.disableInlining` const List compilerOptions = const []; /// Compute the [OutputUnit]s for all source files involved in the test, and /// ensure that the compiler is correctly calculating what is used and what is /// not. We expect all test entry points to be in the `data` directory and any /// or all supporting libraries to be in the `libs` folder, starting with the /// same name as the original file in `data`. main(List args) { asyncTest(() async { Directory dataDir = Directory.fromUri(Platform.script.resolve('data')); await checkTests(dataDir, const OutputUnitDataComputer(), options: compilerOptions, args: args, setUpFunction: () { importPrefixes.clear(); }, testedConfigs: allSpecConfigs + [twoDeferredFragmentConfig, threeDeferredFragmentConfig]); }); } // For ease of testing and making our tests easier to read, we impose an // artificial constraint of requiring every deferred import use a different // named prefix per test. We enforce this constraint here by checking that no // prefix name responds to two different libraries. Map importPrefixes = {}; String importPrefixString(OutputUnit unit) { StringBuffer sb = StringBuffer(); bool first = true; for (ImportEntity import in unit.imports) { if (!first) sb.write(', '); sb.write('${import.name}'); first = false; Expect.isTrue(import.isDeferred); if (importPrefixes.containsKey(import.name)) { var existing = importPrefixes[import.name]; var current = import.enclosingLibraryUri; Expect.equals( existing, current, '\n Duplicate prefix \'${import.name}\' used in both:\n' ' - $existing and\n' ' - $current.\n' ' We require using unique prefixes on these tests to make ' 'the expectations more readable.'); } importPrefixes[import.name] = import.enclosingLibraryUri; } return sb.toString(); } /// Create a consistent string representation of [OutputUnit]s for both /// KImportEntities and ImportElements. String outputUnitString(OutputUnit unit) { if (unit == null) return 'none'; String sb = importPrefixString(unit); return '${unit.name}{$sb}'; } Map> buildPreFragmentMap( Map> fragmentsToLoad, List preDeferredFragments) { Map fragmentMap = {}; for (var preFragment in preDeferredFragments) { fragmentMap[preFragment.finalizedFragment] = preFragment; } Map> preFragmentMap = {}; fragmentsToLoad.forEach((loadId, fragments) { List preFragments = []; for (var fragment in fragments) { preFragments.add(fragmentMap[fragment]); } preFragmentMap[loadId] = preFragments.toList(); }); return preFragmentMap; } class Tags { static const String cls = 'class_unit'; static const String member = 'member_unit'; static const String closure = 'closure_unit'; static const String constants = 'constants'; static const String type = 'type_unit'; // The below tags appear in a single block comment in the main file. // To keep them appearing in sequential order we prefix characters. static const String preFragments = 'a_pre_fragments'; static const String finalizedFragments = 'b_finalized_fragments'; static const String steps = 'c_steps'; } class OutputUnitDataComputer extends DataComputer { const OutputUnitDataComputer(); /// OutputData for [member] as a kernel based element. /// /// At this point the compiler has already been run, so it is holding the /// relevant OutputUnits, we just need to extract that information from it. We /// fill [actualMap] with the data computed about what the resulting OutputUnit /// is. @override void computeMemberData(Compiler compiler, MemberEntity member, Map> actualMap, {bool verbose: false}) { JsClosedWorld closedWorld = compiler.backendClosedWorldForTesting; JsToElementMap elementMap = closedWorld.elementMap; MemberDefinition definition = elementMap.getMemberDefinition(member); OutputUnitIrComputer(compiler.reporter, actualMap, elementMap, closedWorld.outputUnitData, closedWorld.closureDataLookup) .run(definition.node); } @override void computeClassData(Compiler compiler, ClassEntity cls, Map> actualMap, {bool verbose: false}) { JsClosedWorld closedWorld = compiler.backendClosedWorldForTesting; JsToElementMap elementMap = closedWorld.elementMap; ClassDefinition definition = elementMap.getClassDefinition(cls); OutputUnitIrComputer(compiler.reporter, actualMap, elementMap, closedWorld.outputUnitData, closedWorld.closureDataLookup) .computeForClass(definition.node); } @override void computeLibraryData(Compiler compiler, LibraryEntity library, Map> actualMap, {bool verbose}) { KernelFrontendStrategy frontendStrategy = compiler.frontendStrategy; ir.Library node = frontendStrategy.elementMap.getLibraryNode(library); List preDeferredFragments = compiler .backendStrategy.emitterTask.emitter.preDeferredFragmentsForTesting; Map> fragmentsToLoad = compiler.backendStrategy.emitterTask.emitter.finalizedFragmentsToLoad; Set omittedOutputUnits = compiler.backendStrategy.emitterTask.emitter.omittedOutputUnits; PreFragmentsIrComputer(compiler.reporter, actualMap, preDeferredFragments, fragmentsToLoad, omittedOutputUnits) .computeForLibrary(node); } @override DataInterpreter get dataValidator => const FeaturesDataInterpreter(); } class PreFragmentsIrComputer extends IrDataExtractor { final List _preDeferredFragments; final Map> _fragmentsToLoad; final Set _omittedOutputUnits; PreFragmentsIrComputer( DiagnosticReporter reporter, Map> actualMap, this._preDeferredFragments, this._fragmentsToLoad, this._omittedOutputUnits) : super(reporter, actualMap); @override Features computeLibraryValue(Id id, ir.Library library) { var name = '${library.importUri.pathSegments.last}'; Features features = new Features(); if (!name.startsWith('main')) return features; // First build a list of pre fragments and their dependencies. int index = 1; Map finalizedFragmentIndices = {}; Map preFragmentIndices = {}; Map reversePreFragmentIndices = {}; Map reverseFinalizedFragmentIndices = {}; for (var preFragment in _preDeferredFragments) { if (!preFragmentIndices.containsKey(preFragment)) { var finalizedFragment = preFragment.finalizedFragment; preFragmentIndices[preFragment] = index; finalizedFragmentIndices[finalizedFragment] = index; reversePreFragmentIndices[index] = preFragment; reverseFinalizedFragmentIndices[index] = finalizedFragment; index++; } } for (int i = 1; i < index; i++) { var preFragment = reversePreFragmentIndices[i]; List needs = []; List supplied = []; List usedBy = []; for (var dependent in preFragment.successors) { if (preFragmentIndices.containsKey(dependent)) { usedBy.add('p${preFragmentIndices[dependent]}'); } } for (var dependency in preFragment.predecessors) { if (preFragmentIndices.containsKey(dependency)) { needs.add('p${preFragmentIndices[dependency]}'); } } for (var emittedOutputUnit in preFragment.emittedOutputUnits) { supplied.add(emittedOutputUnit.outputUnit); } var suppliedString = '[${supplied.map(outputUnitString).join(', ')}]'; features.addElement(Tags.preFragments, 'p$i: {units: $suppliedString, usedBy: $usedBy, needs: $needs}'); } // Now dump finalized fragments and load ids. for (int i = 1; i < index; i++) { var finalizedFragment = reverseFinalizedFragmentIndices[i]; List supplied = []; for (var codeFragment in finalizedFragment.codeFragments) { List outputUnitStrings = []; for (var outputUnit in codeFragment.outputUnits) { if (!_omittedOutputUnits.contains(outputUnit)) { outputUnitStrings.add(outputUnitString(outputUnit)); } } if (outputUnitStrings.isNotEmpty) { supplied.add(outputUnitStrings.join('+')); } } if (supplied.isNotEmpty) { var suppliedString = '[${supplied.join(', ')}]'; features.addElement(Tags.finalizedFragments, 'f$i: $suppliedString'); } } _fragmentsToLoad.forEach((loadId, finalizedFragments) { List finalizedFragmentNeeds = []; for (var finalizedFragment in finalizedFragments) { assert(finalizedFragmentIndices.containsKey(finalizedFragment)); finalizedFragmentNeeds .add('f${finalizedFragmentIndices[finalizedFragment]}'); } features.addElement( Tags.steps, '$loadId=(${finalizedFragmentNeeds.join(', ')})'); }); return features; } } class OutputUnitIrComputer extends IrDataExtractor { final JsToElementMap _elementMap; final OutputUnitData _data; final ClosureData _closureDataLookup; Set _constants = {}; OutputUnitIrComputer( DiagnosticReporter reporter, Map> actualMap, this._elementMap, this._data, this._closureDataLookup) : super(reporter, actualMap); Features getMemberValue( String tag, MemberEntity member, Set constants) { Features features = Features(); features.add(tag, value: outputUnitString(_data.outputUnitForMemberForTesting(member))); for (var constant in constants) { features.addElement(Tags.constants, constant); } return features; } @override Features computeClassValue(Id id, ir.Class node) { var cls = _elementMap.getClass(node); Features features = Features(); features.add(Tags.cls, value: outputUnitString(_data.outputUnitForClassForTesting(cls))); features.add(Tags.type, value: outputUnitString(_data.outputUnitForClassTypeForTesting(cls))); return features; } @override Features computeMemberValue(Id id, ir.Member node) { if (node is ir.Field && node.isConst) { ir.Expression initializer = node.initializer; ConstantValue constant = _elementMap.getConstantValue(node, initializer); if (!constant.isPrimitive) { SourceSpan span = computeSourceSpanFromTreeNode(initializer); if (initializer is ir.ConstructorInvocation) { // Adjust the source-span to match the AST-based location. The kernel FE // skips the "const" keyword for the expression offset and any prefix in // front of the constructor. The "-6" is an approximation assuming that // there is just a single space after "const" and no prefix. // TODO(sigmund): offsets should be fixed in the FE instead. span = SourceSpan(span.uri, span.begin - 6, span.end - 6); } _registerValue( NodeId(span.begin, IdKind.node), Features.fromMap({ Tags.member: outputUnitString( _data.outputUnitForConstantForTesting(constant)) }), node, span, actualMap, reporter); } } Features features = getMemberValue(Tags.member, _elementMap.getMember(node), _constants); _constants = {}; return features; } @override visitConstantExpression(ir.ConstantExpression node) { ConstantValue constant = _elementMap.getConstantValue(null, node); if (!constant.isPrimitive) { _constants.add('${constant.toStructuredText(_elementMap.types)}=' '${outputUnitString(_data.outputUnitForConstant(constant))}'); } return super.visitConstantExpression(node); } @override Features computeNodeValue(Id id, ir.TreeNode node) { if (node is ir.FunctionExpression || node is ir.FunctionDeclaration) { ClosureRepresentationInfo info = _closureDataLookup.getClosureInfo(node); return getMemberValue(Tags.closure, info.callMethod, const {}); } return null; } } /// Set [actualMap] to hold a key of [id] with the computed data [value] /// corresponding to [object] at location [sourceSpan]. We also perform error /// checking to ensure that the same [id] isn't added twice. void _registerValue(Id id, T value, Object object, SourceSpan sourceSpan, Map> actualMap, CompilerDiagnosticReporter reporter) { if (actualMap.containsKey(id)) { ActualData existingData = actualMap[id]; reportHere(reporter, sourceSpan, "Duplicate id ${id}, value=$value, object=$object"); reportHere( reporter, sourceSpan, "Duplicate id ${id}, value=${existingData.value}, " "object=${existingData.object}"); Expect.fail("Duplicate id $id."); } if (value != null) { actualMap[id] = ActualData(id, value, sourceSpan.uri, sourceSpan.begin, object); } }