-
Notifications
You must be signed in to change notification settings - Fork 128
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix #438 (Structure suggestions for link resolution warnings as fixits) #455
Conversation
8ff15f0
to
025d425
Compare
Thanks for this PR. I will review it early next week. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking like a great start.
For a developer being presented with one of these diagnostics it would be more helpful if more information was moved to the suggestions instead of the error message. (The suggestions were originally added to the error message because there wasn't any more structure.)
If a developer got a collision error message with all solutions in the error message they'd have to either assume that the order of in the error message is the same as the order of the replacements or they'd have to compare the replacements (if their IDE displays that information) against the error message to know which replacement to pick to get the expected symbol. Inspecting the replacements could be somewhat cumbersome if the link is long since the full link is replaced. It would be easier to inspect the replacements if only the modified path component was replaced (or if collision error messages were insertions instead of replacements).
Like you've identified in the Error.replacements(for:)
comment; "No similar pages. Available children: ..." is not a very helpful replacement. Maybe it would be better presented as a note on the diagnostic but it's probably fine to keep that in the error message.
In the future it would be helpful to offer a replacement for PathHierarchy.Error.nonSymbolMatchForSymbolLink
that replaces ``Link``
with <doc:Link>
.
It's a smart way to avoid the breaking change by adding the candidates to the unresolved topic reference but in order to achieve some of the error structure described above—now or in the future—I think we'll want to add more information that will outgrow UnresolvedTopicReference
.
I think we'll want to consolidate the various error information into one place. Over time this may grow to include: the error message, suggestion messages, text and subranges for replacements, notes, etc.
It's not necessarily wrong to have this information on the unresolved reference but I feel that the distinction between not-yet-resolved and failed-to-resolve is better modeled by TopicReferenceResolutionResult
(not-yet-resolved reference as input and either successfully-resolved or failed-to-resolved reference as output).
It was shortsighted of me to only have an error message on TopicReferenceResolutionResult
but I think it'd be worth a breaking change if we could create well structured diagnostics and had a type that we could improve and extend without breaking changes in the future. One such example would be to replace the error message with a struct that wraps all the error information.
At a minimal I think this should include the error message and a separate message and replacement for each suggestion.
It would be a nice improvement if this also included a subrange for each suggestion. That way .partialResult
error could replace only one path component and .lookupCollision
could insert at the end of a certain path component. It would also be an improvement if this could support messages for notes. These improvements could be added later by someone else but it's important that, going forward, adding more error information like this isn't another breaking change.
025d425
to
79f7219
Compare
@d-ronnqvist thanks for taking the time to review this. Your reviews are always well thought-out and provide so much detail, they really are the best! I agree with all your comments. I just wanted to offer this minimally intrusive solution (also as a basis for discussion) before investing too much time into a solution that could be fundamentally different from what you imagined. Please note that I'd like to add a couple more tests, but the implementation is ready for another review. I'd be particularly interested on what public interface you'd like to see on |
Apologies, I won’t have time to look at this until sometime next week. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks really good.
I left a few minor comments but nothing that's blocking this PR (and some that would be better to address separately)
let innerRange = fromSymbolLink | ||
// ``my/reference`` | ||
? SourceLocation(line: range.lowerBound.line, column: range.lowerBound.column+2, source: range.lowerBound.source)..<SourceLocation(line: range.upperBound.line, column: range.upperBound.column-2, source: range.upperBound.source) | ||
// <doc:my/reference> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's also possible that the link could have been written with the []()
syntax, for example: [](doc:my/reference)
, [doc:my/reference](doc:my/reference)
, or [custom title](doc:my/reference)
.
DocC doesn't have a good way to know what how the original link was written—and also doesn't support custom titles (see https://github.com/apple/swift-docc/issues/271)—so I feel that this is fine for the replacement text. In the future when we have more information we can improve the replacement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point! I created #470 and added a FIXME.
@@ -314,24 +312,14 @@ class PathHierarchyTests: XCTestCase { | |||
(symbolID: "s:14MixedFramework29CollisionsWithEscapedKeywordsC4inityyF", disambiguation: "method"), | |||
(symbolID: "s:14MixedFramework29CollisionsWithEscapedKeywordsC4inityyFZ", disambiguation: "type.method"), | |||
]) | |||
try assertPathRaisesErrorMessage("/MixedFramework/CollisionsWithEscapedKeywords/init()", in: tree, context: context, expectedErrorMessage: """ | |||
Reference is ambiguous after '/MixedFramework/CollisionsWithEscapedKeywords': \ | |||
Append '-init' to refer to 'init()'. \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice to have test assertions for potential notes and solutions. This doesn't have to be done in this PR.
It may make the call site hard to read if all the expected values were passed as arguments. One alternative could be to have a trailing closure argument that passes the error struct in so that the call site can make the individual assertions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a trailing closure argument as you suggested and use it to check the semantic information contained in the solutions that was previously part of the error message. I don't check every detail such as the location of replacements, as that is not really the focus of this test.
I defined some local helper extensions and structures, which IMO provide for pretty clean test code:
try assertPathRaisesErrorMessage("/MixedFramework/CollisionsWithEscapedKeywords/init()", in: tree, context: context, expectedErrorMessage: "Reference is ambiguous after '/MixedFramework/CollisionsWithEscapedKeywords'.") { error in
XCTAssertEqual(error.solutions, [
.init(summary: "Insert disambiguation suffix for 'func `init`()'", replacements: ["-method"]),
.init(summary: "Insert disambiguation suffix for 'init()'", replacements: ["-init"]),
.init(summary: "Insert disambiguation suffix for 'static func `init`()'", replacements: ["-type.method"]),
])
}
return TopicReferenceResolutionError("No local documentation matches this reference.") | ||
case .nonSymbolMatchForSymbolLink: | ||
return TopicReferenceResolutionError("Symbol links can only resolve symbols.", solutions: [ | ||
Solution(summary: "Use a '<doc:>' style reference.", replacements: [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice solution and replacement
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! I think its a good idea to have this error category but, unfortunately, often times the resolution algorithm already aborts with a different error during the symbol-restricted lookup, even though it could resolve it correctly if the correct reference syntax had been used. This causes some very misleading error to be emitted (e.g. "no top-level symbol with name MyArticle"), because the nonSymbolMatchForSymbolLink
error only gets thrown if the symbol-restricted lookup succeeds without result. It would be great to have a better analysis of the root-cause there some day, but that's definitely out of scope for this PR.
@@ -222,7 +222,12 @@ final class PathHierarchyBasedLinkResolver { | |||
if let resolvedFallbackReference = fallbackResolver.resolve(unresolvedReference, in: parent, fromSymbolLink: isCurrentlyResolvingSymbolLink, context: context) { | |||
return .success(resolvedFallbackReference) | |||
} else { | |||
return .failure(unresolvedReference, errorMessage: error.errorMessage(context: context)) | |||
var originalReferenceString = unresolvedReference.path | |||
if let fragment = unresolvedReference.topicURL.components.fragment { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm mostly curious why this is needed. Is there a bug where the unresolved reference drops the fragment? If so, please add a FIXME comment that mentions the issue and gives context to why the originalReferenceString
is needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need the originalReferenceString
in order to calculate the proper ranges for replacements.
The only information I have on the valid prefix of the reference based solely on the PathHierarchy.Error
is the partialResult
. This doesn't give me information on which ancestor the reference is relative to, thus I don't know the length of the original string. Whether or not the reference includes a fragment doesn't really matter.
// we store the base as an `Error` so that | ||
// we can potentailly pass through a `DescribedError` | ||
// conformance in the future | ||
private let base: Error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the message and note was stored directly instead of storing the base error then this struct can conform to DescribedError
. It also makes it easier to conform to Hashable
(although I think Solution
and Replacement
would need to explicitly declare conformance as well).
The functionality of accepting any error as input can still be achieved with an initializer that takes an error argument and reads the localizedDescription
and recoverySuggestion
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added all four properties of the DescribedError
protocol as stored constants to the error so I don't loose information when passing though an error. Hashable
conformance is better now 👍
79f7219
to
16a0833
Compare
If you update this PR (either merge or rebase) I can merge it for you. I can also do it for you if you prefer. |
19eb060
to
f54a17c
Compare
Thanks David for reminding me, I was a little occupied with other things last week. I updated the PR and ran |
@swift-ci please test |
- create TopicReferenceResolutionError with minimal public interface - replace TopicReferenceResolutionResult.failure's errorMessage with TopicReferenceResolutionError - add support for non-symbol reference syntax - add Solution for PathHierarchy.Error.nonSymbolMatchForSymbolLink - add meaningful information to Solution.summary - scope Replacements as small as possible (on disambiguation, path segment, or reference delimiters) - add note listing all available candidates even if near-miss suggestions are available - add Solutions if no near-miss is available, but there are only three candidates or less - make offsetWithRange mutating - remove XCTAssertElements test helper
- add DescribedError conformance TopicReferenceResolutionError - improve Hashable implementation of TopicReferenceResolutionError by storing properties demanded by DescribedError instead of raw Error - conform Replacement and Solution to Hashable to aid Hashable implementation of TopicReferenceResolutionError - add fixme regarding support for regular markdown link-style references when creating solutions (swiftlang#470) - add basic semantic information about possible solutions back into PathHierarchyTests - remove debug print in SymbolTests
…rningsInDocComment() + improve improve documentation about indices in source locations
f54a17c
to
6911594
Compare
@swift-ci please test |
I've been holding off on merging this for a few days while I've been investigating how these diagnostics present in IDEs and on the command line. Because of limitations with how diagnostics are output, the information about which disambiguation refers to which symbol never gets presented. I opened #478 about this. I think that my plan is to merge this PR sometime in the next few days and immediately follow it up with another PR that restores the presentation of this information by including the suggested solution messages in the general diagnostic message. That way we get all the infrastructure benefits of have structured diagnostics values in the code while maintaining an equivalent presentation to what we have today. Further follow up PRs at a later time can work to introduce new output formats for tools and for people that better utilize these structured diagnostics values. |
@swift-ci please test |
Bug/issue #, if applicable: #438
Summary
This PR adds fixits to the warnings generated by reference resolution for near-miss and collision scenarios.
The fixits are always generated so that they replace the whole reference, i.e. the replacement area includes the whole reference link, plus the surrounding backticks.
Fixits only fix the first faulty segment in a reference. They do not alter the valid prefix and keep the unchecked suffix in place.
For example:
/MyKit/MyClas
->/MyKit/MyClass
MyClas
->MyClass
MyClas/validMember
->MyClass/validMember
MyClas/invalidMember
->MyClass/invalidMember
->MyClass/validMember
(the second fixit only becomes available after running docc again)The implementation is structured as follows:
PathHierarchy/Error/replacements(for:)
PathHierarchyBasedLinkResolver/resolve(_:in:fromSymbolLink:context:)
callsPathHierarchy/Error/replacements(for:)
and attaches them ascandidates
to theUnresolvedTopicReference
returned in theTopicReferenceResolutionResult/failure(_:errorMessage:)
(Adding the
candidates
field toUnresolvedTopicReference
avoids a breaking change inTopicReferenceResolutionResult/failure(_:errorMessage:)
).MarkupReferenceResolver
andReferenceResolver
pass thecandidates
tounresolvedReferenceProblem(reference:source:range:severity:uncuratedArticleMatch:underlyingErrorMessage:candidates:)
, which then generatesSolution
s if there arecandidates
available and attaches them to theProblem
Beyond that I had to make minor adaptions to make sure range offsets are applied not only to a
Problem
'sdiagnostic
, but also topossibleSolutions
.Dependencies
None.
Testing
Updated
SymbolTests/testUnresolvedReferenceWarningsInDocumentationExtension()
to also test fixits and added a couple of test-cases relevant for fixits.Make sure to set
DOCC_USE_HIERARCHY_BASED_LINK_RESOLVER=YES
when running the tests.For manual testing also make sure to enable fixits with
--emit-fixits
, e.g.:DOCC_USE_HIERARCHY_BASED_LINK_RESOLVER=YES .build/debug/docc convert Sources/SwiftDocC/SwiftDocC.docc --emit-fixits --additional-symbol-graph-dir .build/arm64-apple-macosx/symbolgraph
Checklist
Make sure you check off the following items. If they cannot be completed, provide a reason.
./bin/test
script and it succeeded