Skip to content
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

FixItApplier crash when generating diagnostics for whole tree but applying them to a subtree #2749

Open
MahdiBM opened this issue Jul 23, 2024 · 11 comments
Labels
bug Something isn't working

Comments

@MahdiBM
Copy link
Contributor

MahdiBM commented Jul 23, 2024

Description

The crash:
Screenshot 2024-07-24 at 1 09 33 AM

Steps to Reproduce

Note that @Enumerator() does not modify the code at all in this case.
I'm just sharing the exact code otherwise I think this should be reproducible not that hard.
Late night here, if I were to wait till tomorrow I'm pretty sure I would have ended up not reporting this bug, let me know if reproducing this doesn't end up being easy.

The exact code:

func testAppliesFixIts() {
    let unterminatedString = """
    let unterminated = ℹ️"This is unterminated1️⃣
    """
    assertMacroExpansion(
        #"""
        @Enumerator("""
        \#(unterminatedString)
        """)
        enum TestEnum {
            case a(val1: String, Int)
            case b
            case testCase(testValue: String)
        }
        """#,
        expandedSource: #"""
        enum TestEnum {
            case a(val1: String, Int)
            case b
            case testCase(testValue: String)

            let unterminated = "This is unterminated"
        }
        """#,
        macros: EnumeratorMacroEntryPoint.macros
    )
}

How FixIts are applied in the pipeline of the macro:

var statement = statement
var diagnostics = ParseDiagnosticsGenerator.diagnostics(for: statement)

/// Returns if anything changed at all.
func tryApplyFixIts() -> Bool {
    guard diagnostics.contains(where: { !$0.fixIts.isEmpty }) else {
        return false
    }
    let fixedStatement = FixItApplier.applyFixes(
        from: diagnostics,
        filterByMessages: nil,
        to: statement
    )
    var parser = Parser(fixedStatement)
    let newStatement = CodeBlockItemSyntax.parse(from: &parser)
    guard statement != newStatement else {
        return false
    }
    let placeholderDetector = PlaceholderDetector()
    placeholderDetector.walk(newStatement)
    /// One of the FixIts added a placeholder, so the fixes are unacceptable
    /// Known behavior which is fine for now: even if one FixIt is
    /// misbehaving, still none of the FixIts will be applied.
    if placeholderDetector.containedPlaceholder {
        return false
    } else {
        statement = newStatement
        return true
    }
}

final class PlaceholderDetector: SyntaxVisitor {
    var containedPlaceholder = false

    override init(viewMode: SyntaxTreeViewMode = .sourceAccurate) {
        super.init(viewMode: viewMode)
    }
    
    override func visit(_ node: TokenSyntax) -> SyntaxVisitorContinueKind {
        if node.isEditorPlaceholder {
            self.containedPlaceholder = true
            return .skipChildren
        }
        return .visitChildren
    }
}

Edit: Added PlaceholderDetector for the sake of being complete, although it doesn't matter.

@MahdiBM MahdiBM added the bug Something isn't working label Jul 23, 2024
@ahoppen
Copy link
Member

ahoppen commented Jul 23, 2024

Synced to Apple’s issue tracker as rdar://132355564

@ahoppen
Copy link
Member

ahoppen commented Jul 23, 2024

If you could share an implementation of the Enumerator macro that reproduce the issue, that would greatly simplify the issue’s reproduction.

@MahdiBM
Copy link
Contributor Author

MahdiBM commented Jul 23, 2024

The current main branch with the test that I mentioned in the issue should work out to reproduce the issue.

I'll try to take another look at this in the next few days if you're not in a rush to get this fixed, though.

@MahdiBM
Copy link
Contributor Author

MahdiBM commented Jul 23, 2024

This repository btw 😅

https://github.com/MahdiBM/enumerator-macro

@ahoppen
Copy link
Member

ahoppen commented Jul 24, 2024

The issue is that ParseDiagnosticsGenerator.diagnostics(for:) generates diagnostics with edits that are relative to the entire source file (instead of the statement’s start) and FixItApplier will apply these edits based on offsets to the statement only, which leads to incompatible string offsets.

There are multiple ways to fix this at its root cause:

  • We need to define what it even means to get diagnostics for a subtree
  • FixItApplier should maybe verify that the Fix-Its actually apply to the current tree and do edit adjustments as necessary.

I’d like to keep the issue open for that but you should be able to fix your crash by detaching the statement before generating diagnostics

ParseDiagnosticsGenerator.diagnostics(for: statement.detached)

Also unrelated, I saw that you’re checking compiler(>=6.0) to check if FixItApplier is available. But that doesn’t have anything to do with the compiler that you’re using but with the version of swift-syntax that you are depending on. The correct check would be #if canImport(SwiftSyntax600). You should be able to also update your package manifest to allow both swift-syntax 510 and 600, independent of the compiler being used. See https://swiftpackageindex.com/swiftlang/swift-syntax/main/documentation/swiftsyntax/macro-versioning for more information.

@ahoppen ahoppen changed the title Crash when trying to apply FixIts FixItApplier crash when generating diagnostics for whole tree but applying them to a subtree Jul 24, 2024
@MahdiBM
Copy link
Contributor Author

MahdiBM commented Jul 24, 2024

canImport(SwiftSyntax600) doesn't seem to work because with "510.0.0" ..< "610.0.0" in Package.swift, even Swift 6 beta would pull 510 which i assume is because 600.0.0 is still in beta and SwiftPM is ignoring the beta versions in "510.0.0" ..< "610.0.0".

One reason that I'm to passing all diagnostics to FixItApplier is because i assumed it'd take care of this exact situation. Otherwise passing the diagnostics once by one would have been better since i could slowly but surely confirm that what FixItApplier does, isn't messing up the generated code (e.g. can't contain placeholders).

I think this will be a nice feature, generally. Even the current Xcode behavior is that when you apply one FixIt which moves lines around, the second possible FixIt won't notice that lines have moved and it'll modify the wrong lines (if you don't build again to get fresh diagnostics).
I can see it'll be a bit more complicated to get it working for this Xcode situation, but it should be possible.
I'm not sure if Xcode is already using these, i'd assume not considering FixItApplier looks new to SwiftSyntax 6.

@MahdiBM
Copy link
Contributor Author

MahdiBM commented Jul 24, 2024

Soooo.... canImport(SwiftSyntax600) looks like a wrong check in the end?! or something is going unexpectedly wrong here. Because it's not even checking the SwiftSyntax version apparently.

I'm also aware of existence of canImport(SwiftSyntax, _version: 600.0.0) but that also
doesn't work out and results in warnings that the declaration can't find the version of SwiftSyntax and is ignoring the condition.

Screenshot 2024-07-24 at 7 31 06 PM

@ahoppen
Copy link
Member

ahoppen commented Jul 24, 2024

With the "510.0.0" ..< "610.0.0" check, you aren’t opting in to prereleases. You should be doing using "510.0.0"..<"601.0.0-latest" if you also want to be able to resolve to swift-syntax 600 prereleases.

And the reason that canImport(SwiftSyntax600) was not true was probably because SwiftPM did not check out the swift-syntax 600 prerelease.

@MahdiBM
Copy link
Contributor Author

MahdiBM commented Jul 24, 2024

the canImport was actually true. Even though SwiftPM pulled 510, not 6xx.

@ahoppen
Copy link
Member

ahoppen commented Jul 24, 2024

Oh, that was probably because of a stale build directory then that still contained the module for SwiftSyntax600. Cleaning should make it evaluate to false again.

@MahdiBM
Copy link
Contributor Author

MahdiBM commented Jul 28, 2024

Tested it now, and you're right, just an inconsistency.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants