Skip to content

Commit

Permalink
Resolve non-imported symbols in comments [dart-lang#1153]
Browse files Browse the repository at this point in the history
@Hixie noticed, that sometimes, a library, like the Flutter rendering
library, wants to refer to another library, like the Flutter widgets
library, in the documentation, but doesn't want to introduce a
dependency in the code. Currently, there’s no mechanisms in dartdoc,
which allows that.

This commit adds that. You can use either a short name (e.g. [icon]) or
a fully qualified name (like, [material.Icon.icon]), in the HTML docs,
it always will be shown as a short name though (is that a desired
behavior?).  Dartdoc will go over all the libraries, classes, methods,
properties, constants of the package that is being documented, and will
try to resolve the symbol. If it’s successful, it will add the link to
it, same way as when the symbol is in scope.

Some outstanding questions:

* What to do in case of ambiguity here? Show a warning, crash with an
  error message, ignore?
* Do we want to show short name in case a fully qualified name is
  provided? I saw in the comments in the ticket
  (dart-lang#1153 (comment))
  that @Hixie would want that.
* Anything else?

Testing: Unit tests, of course, also tried to generate Flutter docs with
that, and observed that the links for previously non-resolved symbols
work.
  • Loading branch information
astashov committed Dec 17, 2016
1 parent 1c911bc commit b386283
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 19 deletions.
74 changes: 62 additions & 12 deletions lib/src/markdown_processor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ const List<String> _oneLinerSkipTags = const ["code", "pre"];

final List<md.InlineSyntax> _markdown_syntaxes = [new _InlineCodeSyntax()];

class MatchingLinkResult {
final ModelElement element;
final String label;
MatchingLinkResult(this.element, this.label);
}

// TODO: this is in the wrong place
NodeList<CommentReference> _getCommentRefs(ModelElement modelElement) {
if (modelElement == null) return null;
Expand Down Expand Up @@ -67,10 +73,10 @@ NodeList<CommentReference> _getCommentRefs(ModelElement modelElement) {
}

/// Returns null if element is a parameter.
ModelElement _getMatchingLinkElement(
MatchingLinkResult _getMatchingLinkElement(
String codeRef, ModelElement element, List<CommentReference> commentRefs,
{bool isConstructor: false}) {
if (commentRefs == null) return null;
if (commentRefs == null) return new MatchingLinkResult(null, null);

Element refElement;
bool isEnum = false;
Expand All @@ -87,7 +93,9 @@ ModelElement _getMatchingLinkElement(
}

// Did not find an element in scope
if (refElement == null) return null;
if (refElement == null) {
return _findRefElementInLibrary(codeRef, element, commentRefs);
}

if (refElement is PropertyAccessorElement) {
// yay we found an accessor that wraps a const, but we really
Expand All @@ -99,7 +107,7 @@ ModelElement _getMatchingLinkElement(
}
}

if (refElement is ParameterElement) return null;
if (refElement is ParameterElement) return new MatchingLinkResult(null, null);

// bug! this can fail to find the right library name if the element's name
// we're looking for is the same as a name that comes in from an imported
Expand All @@ -114,37 +122,79 @@ ModelElement _getMatchingLinkElement(
// Is there a way to pull this from a registry of known elements?
// Seems like we're creating too many objects this way.
if (isEnum) {
return new EnumField(refElement, refLibrary);
return new MatchingLinkResult(new EnumField(refElement, refLibrary), null);
}
return new ModelElement.from(refElement, refLibrary);
return new MatchingLinkResult(new ModelElement.from(refElement, refLibrary), null);
}
return new MatchingLinkResult(null, null);
}

MatchingLinkResult _findRefElementInLibrary(String codeRef, ModelElement element, List<CommentReference> commentRefs) {
final package = element.library.package;
final Map<String, ModelElement> result = {};
package.libraries.forEach((library) {
final List<ModelElement> modelElements = []
..addAll(library.allClasses)
..addAll(library.constants)
..addAll(library.enums)
..addAll(library.functions)
..addAll(library.properties)
..addAll(library.typedefs);

library.allClasses.forEach((c) {
modelElements.addAll(c.allInstanceMethods);
modelElements.addAll(c.allInstanceProperties);
modelElements.addAll(c.allOperators);
modelElements.addAll(c.staticMethods);
modelElements.addAll(c.staticProperties);
});

modelElements.forEach((modelElement) {
if (codeRef == modelElement.name || codeRef == modelElement.fullyQualifiedName) {
result[modelElement.fullyQualifiedName] = modelElement;
}
});
});

if (result.isEmpty) {
return new MatchingLinkResult(null, null);
} else if (result.length == 1) {
return new MatchingLinkResult(result.values.first, result.values.first.name);
} else {
if (_emitWarning) {
print("Abiguous reference '${codeRef}' in '${element.fullyQualifiedName}', " +
"we found it in the following elements: ${result.keys.map((k) => "'${k}'").join(", ")}");
}
return new MatchingLinkResult(null, null);
}
return null;
}

String _linkDocReference(String reference, ModelElement element,
NodeList<CommentReference> commentRefs) {
ModelElement linkedElement;
// support for [new Constructor] and [new Class.namedCtr]
var refs = reference.split(' ');
MatchingLinkResult result;
if (refs.length == 2 && refs.first == 'new') {
linkedElement = _getMatchingLinkElement(refs[1], element, commentRefs,
result = _getMatchingLinkElement(refs[1], element, commentRefs,
isConstructor: true);
} else {
linkedElement = _getMatchingLinkElement(reference, element, commentRefs);
result = _getMatchingLinkElement(reference, element, commentRefs);
}
final linkedElement = result.element;
final label = result.label ?? reference;
if (linkedElement != null) {
var classContent = '';
if (linkedElement.isDeprecated) {
classContent = 'class="deprecated" ';
}
// this would be linkedElement.linkedName, but link bodies are slightly
// different for doc references. sigh.
return '<a ${classContent}href="${linkedElement.href}">$reference</a>';
return '<a ${classContent}href="${linkedElement.href}">$label</a>';
} else {
if (_emitWarning) {
print(" warning: unresolved doc reference '$reference' (in $element)");
}
return '<code>${HTML_ESCAPE.convert(reference)}</code>';
return '<code>${HTML_ESCAPE.convert(label)}</code>';
}
}

Expand Down
4 changes: 2 additions & 2 deletions test/model_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ void main() {
test(
'link to a name in another library in this package, but is not imported into this library, is codeified',
() {
expect(docsAsHtml, contains('<code>doesStuff</code>'));
expect(docsAsHtml, contains('<a href="anonymous_library/doesStuff.html">doesStuff</a>'));
});

test(
Expand Down Expand Up @@ -427,7 +427,7 @@ void main() {
expect(resolved, isNotNull);
expect(resolved,
contains('<a href="two_exports/BaseClass-class.html">BaseClass</a>'));
expect(resolved, contains('linking over to <code>Apple</code>.'));
expect(resolved, contains('linking over to <a href="ex/Apple-class.html">Apple</a>.'));
});

test('references to class and constructors', () {
Expand Down
2 changes: 1 addition & 1 deletion testing/test_package/lib/src/extending.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ int topLevelVariable = 1;
///
/// Also check out [topLevelVariable].
///
/// This should not work: linking over to [Apple].
/// This also should work: linking over to [Apple].
class ExtendingClass extends BaseClass {}
4 changes: 2 additions & 2 deletions testing/test_package_docs/ex/ShapeType-class.html
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,8 @@ <h5><a href="ex/ex-library.html">ex</a></h5>

<section class="desc markdown">
<p>Foo bar.</p><ol start="3"><li>All references should be hyperlinks. <a href="ex/MyError-class.html">MyError</a> and
<code>ShapeType</code> and <a href="ex/MyError-class.html">MyError</a> and <code>MyException</code> and
<a href="ex/MyError-class.html">MyError</a> and <code>MyException</code> and
<a href="ex/ShapeType-class.html">ShapeType</a> and <a href="ex/MyError-class.html">MyError</a> and <a href="ex/MyException-class.html">MyException</a> and
<a href="ex/MyError-class.html">MyError</a> and <a href="ex/MyException-class.html">MyException</a> and
<code>List&lt;int&gt;</code> foo bar.</li></ol>
</section>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ <h5><a href="fake/BaseForDocComments-class.html">BaseForDocComments</a></h5>
<p>Reference to a top-level const in another library <a href="ex/incorrectDocReferenceFromEx-constant.html">incorrectDocReferenceFromEx</a></p>
<p>Reference to prefixed-name from another lib <a href="css/theOnlyThingInTheLibrary.html">css.theOnlyThingInTheLibrary</a> xx</p>
<p>Reference to a name that exists in this package, but is not imported
in this library <code>doesStuff</code> xx</p>
in this library <a href="anonymous_library/doesStuff.html">doesStuff</a> xx</p>
<p>Reference to a name of a class from an import of a library that exported
the name <a href="two_exports/BaseClass-class.html">BaseClass</a> xx</p>
</section>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ <h5><a href="two_exports/two_exports-library.html">two_exports</a></h5>
<section class="desc markdown">
<p>Extending class extends <a href="two_exports/BaseClass-class.html">BaseClass</a>.</p>
<p>Also check out <a href="two_exports/topLevelVariable.html">topLevelVariable</a>.</p>
<p>This should not work: linking over to <code>Apple</code>.</p>
<p>This also should work: linking over to <a href="ex/Apple-class.html">Apple</a>.</p>
</section>

<section>
Expand Down

0 comments on commit b386283

Please sign in to comment.