// Copyright (c) 2012, 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. /** * A script to assist in documenting the difference between the dart:html API * and the old DOM API. */ library html_diff; import 'dart:async'; import 'dart:io'; import 'lib/metadata.dart'; // TODO(rnystrom): Use "package:" URL (#4968). import '../../sdk/lib/_internal/compiler/implementation/mirrors/dart2js_mirror.dart'; import '../../sdk/lib/_internal/compiler/implementation/mirrors/mirrors.dart'; import '../../sdk/lib/_internal/compiler/implementation/mirrors/mirrors_util.dart'; import '../../sdk/lib/_internal/compiler/implementation/source_file_provider.dart'; import '../../sdk/lib/_internal/dartdoc/lib/dartdoc.dart'; import '../../sdk/lib/html/html_common/metadata.dart'; // TODO(amouravski): There is currently magic that looks at dart:* libraries // rather than the declared library names. This changed due to recent syntax // changes. We should only need to look at the library 'html'. final List HTML_LIBRARY_URIS = [ new Uri(scheme: 'dart', path: 'html'), new Uri(scheme: 'dart', path: 'indexed_db'), new Uri(scheme: 'dart', path: 'svg'), new Uri(scheme: 'dart', path: 'web_audio')]; /** * A class for computing a many-to-many mapping between the types and * members in `dart:html` and the MDN DOM types. This mapping is * based on two indicators: * * 1. Auto-detected wrappers. Most `dart:html` types correspond * straightforwardly to a single `@DomName` type, and * have the same name. In addition, most `dart:html` methods * just call a single `@DomName` method. This class * detects these simple correspondences automatically. * * 2. Manual annotations. When it's not clear which * `@DomName` items a given `dart:html` item * corresponds to, the `dart:html` item can be annotated in the * documentation comments using the `@DomName` annotation. * * The `@DomName` annotations for types and members are of the form * `@DomName NAME(, NAME)*`, where the `NAME`s refer to the * `@DomName` types/members that correspond to the * annotated `dart:html` type/member. `NAME`s on member annotations * can refer to either fully-qualified member names (e.g. * `Document.createElement`) or unqualified member names * (e.g. `createElement`). Unqualified member names are assumed to * refer to members of one of the corresponding `@DomName` * types. */ class HtmlDiff { /** * A map from `dart:html` members to the corresponding fully qualified * `@DomName` member(s). */ final Map> htmlToDom; /** A map from `dart:html` types to corresponding `@DomName` types. */ final Map> htmlTypesToDom; /** If true, then print warning messages. */ final bool _printWarnings; static LibraryMirror dom; HtmlDiff({bool printWarnings: false}) : _printWarnings = printWarnings, htmlToDom = new Map>(), htmlTypesToDom = new Map>(); void warn(String s) { if (_printWarnings) { print('Warning: $s'); } } /** * Computes the `@DomName` to `dart:html` mapping, and * places it in [htmlToDom] and [htmlTypesToDom]. Before this is run, dart2js * should be initialized (via [parseOptions] and [initializeWorld]) and * [HtmlDiff.initialize] should be called. */ Future run(Uri libraryRoot) { var result = new Completer(); var provider = new SourceFileProvider(); var handler = new FormattingDiagnosticHandler(provider); Future analysis = analyze( HTML_LIBRARY_URIS, libraryRoot, null, provider.readStringFromUri, handler.diagnosticHandler); analysis.then((MirrorSystem mirrors) { for (var libraryUri in HTML_LIBRARY_URIS) { var library = mirrors.libraries[libraryUri]; if (library == null) { warn('Could not find $libraryUri'); result.complete(false); } for (ClassMirror type in library.classes.values) { final domTypes = htmlToDomTypes(type); if (domTypes.isEmpty) continue; htmlTypesToDom.putIfAbsent(type.qualifiedName, () => new Set()).addAll(domTypes); type.members.forEach( (_, m) => _addMemberDiff(m, domTypes, library.simpleName)); } } result.complete(true); }); return result.future; } /** * Records the `@DomName` to `dart:html` mapping for * [htmlMember] (from `dart:html`). [domTypes] are the * `@DomName` type values that correspond to [htmlMember]'s * defining type. */ void _addMemberDiff(MemberMirror htmlMember, List domTypes, String libraryName) { var domMembers = htmlToDomMembers(htmlMember, domTypes); if (htmlMember == null && !domMembers.isEmpty) { warn('$libraryName member ' '${htmlMember.owner.simpleName}.' '${htmlMember.simpleName} has no corresponding ' '$libraryName member.'); } if (htmlMember == null) return; if (!domMembers.isEmpty) { htmlToDom[htmlMember.qualifiedName] = domMembers; } } /** * Returns the `@DomName` type values that correspond to * [htmlType] from `dart:html`. This can be the empty list if no * correspondence is found. */ List htmlToDomTypes(ClassMirror htmlType) { if (htmlType.simpleName == null) return []; final domNameMetadata = findMetadata(htmlType.metadata, 'DomName'); if (domNameMetadata != null) { var domNames = []; var names = domNameMetadata.getField('name'); for (var s in names.reflectee.split(',')) { domNames.add(s.trim()); } if (domNames.length == 1 && domNames[0] == 'none') return []; return domNames; } return []; } /** * Returns the `@DomName` member values that correspond to * [htmlMember] from `dart:html`. This can be the empty set if no * correspondence is found. [domTypes] are the * `@DomName` type values that correspond to [htmlMember]'s * defining type. */ Set htmlToDomMembers(MemberMirror htmlMember, List domTypes) { if (htmlMember.isPrivate) return new Set(); final domNameMetadata = findMetadata(htmlMember.metadata, 'DomName'); if (domNameMetadata != null) { var domNames = []; var names = domNameMetadata.getField('name'); for (var s in names.reflectee.split(',')) { domNames.add(s.trim()); } if (domNames.length == 1 && domNames[0] == 'none') return new Set(); final members = new Set(); domNames.forEach((name) { var nameMembers = _membersFromName(name, domTypes); if (nameMembers.isEmpty) { if (name.contains('.')) { warn('no member $name'); } else { final options = []; for (var t in domTypes) { options.add('$t.$name'); } options.join(' or '); warn('no member $options'); } } members.addAll(nameMembers); }); return members; } return new Set(); } /** * Returns the `@DomName` strings that are indicated by * [name]. [name] can be either an unqualified member name * (e.g. `createElement`), in which case it's treated as the name of * a member of one of [defaultTypes], or a fully-qualified member * name (e.g. `Document.createElement`), in which case it's treated as a * member of the @DomName element (`Document` in this case). */ Set _membersFromName(String name, List defaultTypes) { if (!name.contains('.', 0)) { if (defaultTypes.isEmpty) { warn('no default type for $name'); return new Set(); } final members = new Set(); defaultTypes.forEach((t) { members.add('$t.$name'); }); return members; } if (name.split('.').length != 2) { warn('invalid member name ${name}'); return new Set(); } return new Set.from([name]); } }