| // 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:coreimpl'); |
| #import('dart:io'); |
| |
| // TODO(rnystrom): Use "package:" URL (#4968). |
| #import('../../pkg/dartdoc/lib/dartdoc.dart'); |
| #import('../../pkg/dartdoc/lib/mirrors.dart'); |
| #import('../../pkg/dartdoc/lib/mirrors_util.dart'); |
| |
| const HTML_LIBRARY_NAME = 'dart:html'; |
| |
| /** |
| * 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<String, Set<String>> htmlToDom; |
| |
| /** A map from `dart:html` types to corresponding `@domName` types. */ |
| final Map<String, Set<String>> htmlTypesToDom; |
| |
| final CommentMap comments; |
| |
| /** If true, then print warning messages. */ |
| final bool _printWarnings; |
| |
| static Compilation _compilation; |
| static MirrorSystem _mirrors; |
| static LibraryMirror dom; |
| |
| /** |
| * Perform static initialization of [world]. This should be run before |
| * calling [HtmlDiff.run]. |
| */ |
| static void initialize(Path libDir) { |
| _compilation = new Compilation.library(<Path>[new Path(HTML_LIBRARY_NAME)], |
| libDir); |
| _mirrors = _compilation.mirrors; |
| } |
| |
| HtmlDiff([bool printWarnings = false]) : |
| _printWarnings = printWarnings, |
| htmlToDom = new Map<String, Set<String>>(), |
| htmlTypesToDom = new Map<String, Set<String>>(), |
| comments = new CommentMap(); |
| |
| 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. |
| */ |
| void run() { |
| LibraryMirror htmlLib = _mirrors.libraries[HTML_LIBRARY_NAME]; |
| if (htmlLib === null) { |
| warn('Could not find $HTML_LIBRARY_NAME'); |
| return; |
| } |
| for (InterfaceMirror htmlType in htmlLib.types.getValues()) { |
| final domTypes = htmlToDomTypes(htmlType); |
| if (domTypes.isEmpty()) continue; |
| |
| htmlTypesToDom.putIfAbsent(htmlType.qualifiedName, |
| () => new Set()).addAll(domTypes); |
| |
| htmlType.declaredMembers.forEach( |
| (_, m) => _addMemberDiff(m, domTypes)); |
| } |
| } |
| |
| /** |
| * 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<String> domTypes) { |
| var domMembers = htmlToDomMembers(htmlMember, domTypes); |
| if (htmlMember == null && !domMembers.isEmpty()) { |
| warn('$HTML_LIBRARY_NAME member ' |
| '${htmlMember.surroundingDeclaration.simpleName}.' |
| '${htmlMember.simpleName} has no corresponding ' |
| '$HTML_LIBRARY_NAME 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<String> htmlToDomTypes(InterfaceMirror htmlType) { |
| if (htmlType.simpleName == null) return []; |
| final tags = _getTags(comments.find(htmlType.location)); |
| if (tags.containsKey('domName')) { |
| var domNames = <String>[]; |
| for (var s in tags['domName'].split(',')) { |
| domNames.add(s.trim()); |
| } |
| if (domNames.length == 1 && domNames[0] == 'none') return <String>[]; |
| return domNames; |
| } |
| return <String>[]; |
| } |
| |
| /** |
| * 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<String> htmlToDomMembers(MemberMirror htmlMember, List<String> domTypes) { |
| if (htmlMember.isPrivate) return new Set(); |
| final tags = _getTags(comments.find(htmlMember.location)); |
| if (tags.containsKey('domName')) { |
| var domNames = <String>[]; |
| for (var s in tags['domName'].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 = <String>[]; |
| for (var t in domTypes) { |
| options.add('$t.$name'); |
| } |
| Strings.join(options, ' 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<String> _membersFromName(String name, List<String> defaultTypes) { |
| if (!name.contains('.', 0)) { |
| if (defaultTypes.isEmpty()) { |
| warn('no default type for $name'); |
| return new Set(); |
| } |
| final members = new Set<String>(); |
| 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]); |
| } |
| |
| /** |
| * Extracts a [Map] from tag names to values from [comment], which is parsed |
| * from a Dart source file via dartdoc. Tags are of the form `@NAME VALUE`, |
| * where `NAME` is alphabetic and `VALUE` can contain any character other than |
| * `;`. Multiple tags can be separated by semicolons. |
| * |
| * At time of writing, the only tag that's used is `@domName`. |
| */ |
| Map<String, String> _getTags(String comment) { |
| if (comment == null) return const <String, String>{}; |
| final re = const RegExp("@([a-zA-Z]+) ([^;]+)(?:;|\$)"); |
| final tags = <String, String>{}; |
| for (var m in re.allMatches(comment.trim())) { |
| tags[m[1]] = m[2]; |
| } |
| return tags; |
| } |
| } |