Skip to content

Commit

Permalink
Trailing Annotation Parsing (#1296)
Browse files Browse the repository at this point in the history
* Refactoring

* Squashed commit of the following:

commit 864505a
Author: Cal Stephens <[email protected]>
Date:   Thu Mar 7 23:10:56 2024 -0800

    Update SwiftSyntax dependency to 510.0.0 (#1294)

    * Update to SwiftSyntax for Swift 5.10

    * Patch build

* Fixed build error

* Removed TODO

* Adding new annotation parsing method

* Squashed commit of the following:

commit 5e6df69
Author: Ruslan Alikhamov <[email protected]>
Date:   Sun Mar 10 11:53:37 2024 +0400

    Resolved all SwiftSyntax Warnings (#1295)

    * Resolved all swiftSyntax warnings

    * Set concrete Ubuntu version to fix GH action

    * Switch to another GH action

commit 864505a
Author: Cal Stephens <[email protected]>
Date:   Thu Mar 7 23:10:56 2024 -0800

    Update SwiftSyntax dependency to 510.0.0 (#1294)

    * Update to SwiftSyntax for Swift 5.10

    * Patch build

* Fixed incorrect parsing of annotations in trailing trivia

* Removed redundant file

* Removed redundant method

* Reverted redundant changes
  • Loading branch information
art-divin authored Mar 10, 2024
1 parent 5e6df69 commit e5447a6
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 31 deletions.
94 changes: 64 additions & 30 deletions SourceryFramework/Sources/Parsing/Utils/AnnotationsParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@ public struct AnnotationsParser {
let blockAnnotations: Annotations
}

private let lines: [Line]
private let lines: [AnnotationsParser.Line]
private let contents: String
private var parseDocumentation: Bool
internal var sourceLocationConverter: SourceLocationConverter?
internal var sourceLocationConverter: SourceLocationConverter

/// Initializes parser
///
/// - Parameter contents: Contents to parse
init(contents: String, parseDocumentation: Bool = false, sourceLocationConverter: SourceLocationConverter? = nil) {
init(contents: String, parseDocumentation: Bool = false, sourceLocationConverter: SourceLocationConverter) {
self.parseDocumentation = parseDocumentation
self.lines = AnnotationsParser.parse(contents: contents)
self.sourceLocationConverter = sourceLocationConverter
Expand All @@ -65,13 +65,15 @@ public struct AnnotationsParser {

func annotations(from node: IdentifierSyntax) -> Annotations {
from(
location: findLocation(syntax: node.identifier)
positionAfterLeadingTrivia: findLocationAfterLeadingTrivia(syntax: node.identifier),
positionBeforeTrailingTrivia: findLocationBeforeTrailingTrivia(syntax: node.identifier)
)
}

func annotations(fromToken token: SyntaxProtocol) -> Annotations {
from(
location: findLocation(syntax: token)
positionAfterLeadingTrivia: findLocationAfterLeadingTrivia(syntax: token),
positionBeforeTrailingTrivia: findLocationBeforeTrailingTrivia(syntax: token)
)
}

Expand All @@ -80,36 +82,47 @@ public struct AnnotationsParser {
return []
}
return documentationFrom(
location: findLocation(syntax: node.identifier)
location: findLocationAfterLeadingTrivia(syntax: node.identifier)
)
}

func documentation(fromToken token: SyntaxProtocol) -> Documentation {
guard parseDocumentation else {
return []
return []
}
return documentationFrom(
location: findLocation(syntax: token)
location: findLocationAfterLeadingTrivia(syntax: token)
)
}

// TODO: once removing SourceKitten just kill this optionality
private func findLocation(syntax: SyntaxProtocol) -> SwiftSyntax.SourceLocation {
return sourceLocationConverter!.location(for: syntax.positionAfterSkippingLeadingTrivia)
private func findLocationAfterLeadingTrivia(syntax: SyntaxProtocol) -> SwiftSyntax.SourceLocation {
sourceLocationConverter.location(for: syntax.positionAfterSkippingLeadingTrivia)
}

private func findLocationBeforeTrailingTrivia(syntax: SyntaxProtocol) -> SwiftSyntax.SourceLocation {
sourceLocationConverter.location(for: syntax.endPositionBeforeTrailingTrivia)
}

private func from(location: SwiftSyntax.SourceLocation) -> Annotations {
private func from(positionAfterLeadingTrivia: SwiftSyntax.SourceLocation, positionBeforeTrailingTrivia: SwiftSyntax.SourceLocation) -> Annotations {
var stop = false
var annotations = inlineFrom(line: (location.line, location.column), stop: &stop)
var position = positionAfterLeadingTrivia
var (annotations, shouldUsePositionBeforeTrailing) = inlineFrom(
positionAfterLeadingTrivia: (positionAfterLeadingTrivia.line, positionAfterLeadingTrivia.column),
positionBeforeTrailingTrivia: (positionBeforeTrailingTrivia.line, positionBeforeTrailingTrivia.column),
stop: &stop
)
if shouldUsePositionBeforeTrailing {
position = positionBeforeTrailingTrivia
}
guard !stop else { return annotations }

let reversedArray = lines[0..<location.line-1].reversed()
let reversedArray = lines[0..<position.line-1].reversed()
for line in reversedArray {
line.annotations.forEach { annotation in
AnnotationsParser.append(key: annotation.key, value: annotation.value, to: &annotations)
}

if line.type != .comment
if line.type != .comment
&& line.type != .documentationComment
&& line.type != .macros
&& line.type != .propertyWrapper
Expand All @@ -118,7 +131,7 @@ public struct AnnotationsParser {
}
}

lines[location.line-1].annotations.forEach { annotation in
lines[position.line-1].annotations.forEach { annotation in
AnnotationsParser.append(key: annotation.key, value: annotation.value, to: &annotations)
}

Expand Down Expand Up @@ -157,16 +170,37 @@ public struct AnnotationsParser {
return documentation.reversed()
}

func inlineFrom(line lineInfo: (line: Int, character: Int), stop: inout Bool) -> Annotations {
let sourceLine = lines[lineInfo.line - 1]
let utf8View = sourceLine.content.utf8
let startIndex = utf8View.startIndex
let endIndex = sourceLine.content.utf8.index(startIndex, offsetBy: (lineInfo.character - 1))
let utf8Slice = utf8View[startIndex ..< endIndex]
let relevantContent = String(decoding: utf8Slice, as: UTF8.self)
var prefix = relevantContent.trimmingCharacters(in: .whitespaces)
func inlineFrom(positionAfterLeadingTrivia: (line: Int, character: Int), positionBeforeTrailingTrivia: (line: Int, character: Int), stop: inout Bool) -> (Annotations, Bool) {
var shouldUsePositionBeforeTrailing = false
var position: (line: Int, character: Int) = positionAfterLeadingTrivia
// first try checking for annotations in the beginning of the line (i.e. `positionAfterLeadingTrivia`)
// next, try checking for annotations in the end of the line (i.e. `positionBeforeTrailingTrivia`)
let findPrefix: (((line: Int, character: Int), Bool) -> (String, Line)) = { position, shouldStart in
let sourceLine = lines[position.line - 1]
let utf8View = sourceLine.content.utf8
var startIndex: String.UTF8View.Index
var endIndex: String.UTF8View.Index
if shouldUsePositionBeforeTrailing {
startIndex = utf8View.index(utf8View.startIndex, offsetBy: (position.character - 1))
endIndex = utf8View.endIndex
} else {
startIndex = utf8View.startIndex
endIndex = utf8View.index(startIndex, offsetBy: (position.character - 1))
}
let utf8Slice = utf8View[startIndex ..< endIndex]
let relevantContent = String(decoding: utf8Slice, as: UTF8.self)
return (relevantContent.trimmingCharacters(in: .whitespaces), sourceLine)
}

guard !prefix.isEmpty else { return [:] }
var (prefix, sourceLine) = findPrefix(positionAfterLeadingTrivia, shouldUsePositionBeforeTrailing)
if prefix.isEmpty {
shouldUsePositionBeforeTrailing = true
(prefix, sourceLine) = findPrefix(positionBeforeTrailingTrivia, shouldUsePositionBeforeTrailing)
if shouldUsePositionBeforeTrailing {
position = positionBeforeTrailingTrivia
}
}
guard !prefix.isEmpty else { return ([:], shouldUsePositionBeforeTrailing) }
var annotations = sourceLine.blockAnnotations // get block annotations for this line
sourceLine.annotations.forEach { annotation in // TODO: verify
AnnotationsParser.append(key: annotation.key, value: annotation.value, to: &annotations)
Expand All @@ -193,23 +227,23 @@ public struct AnnotationsParser {
if (inlineCommentFound || isInsideCaseDefinition) && !prefix.isEmpty {
stop = true
return annotations
return (annotations, shouldUsePositionBeforeTrailing)
}
// if previous line is not comment or has some trailing non-comment blocks
// we return currently aggregated annotations
// as annotations on previous line belong to previous declaration
if lineInfo.line - 2 > 0 {
let previousLine = lines[lineInfo.line - 2]
if position.line - 2 > 0 {
let previousLine = lines[position.line - 2]
let content = previousLine.content.trimmingCharacters(in: .whitespaces)
guard previousLine.type == .comment || previousLine.type == .documentationComment || previousLine.type == .propertyWrapper || previousLine.type == .macros, content.hasPrefix("//") || content.hasSuffix("*/") || content.hasPrefix("@") || content.hasPrefix("#") else {
stop = true
return annotations
return (annotations, shouldUsePositionBeforeTrailing)
}
}

return annotations
return (annotations, shouldUsePositionBeforeTrailing)
}

private static func parse(contents: String) -> [Line] {
Expand Down
20 changes: 20 additions & 0 deletions SourceryTests/Parsing/FileParser_MethodsSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,26 @@ class FileParserMethodsSpec: QuickSpec {
]))
}

it("extracts parameter inline prefix and suffix annotations") {
let parsed = parse("""
class Foo {
func foo(paramA: String, // sourcery: anAnnotation = "PARAM A AND METHOD ONLY"
/* sourcery: testAnnotation="PARAM B ONLY"*/ paramB: String,
paramC: String, // sourcery: anotherAnnotation = "PARAM C ONLY"
paramD: String
) {}
}
""")
expect(parsed).to(equal([
Class(name: "Foo", methods: [
Method(name: "foo(paramA: String, paramB: String, paramC: String, paramD: String)", selectorName: "foo(paramA:paramB:paramC:paramD:)", parameters: [
MethodParameter(name: "paramA", typeName: TypeName(name: "String"), annotations: ["anAnnotation": "PARAM A AND METHOD ONLY" as NSString]),
MethodParameter(name: "paramB", typeName: TypeName(name: "String"), annotations: ["testAnnotation": "PARAM B ONLY" as NSString]),
MethodParameter(name: "paramC", typeName: TypeName(name: "String"), annotations: ["anotherAnnotation": "PARAM C ONLY" as NSString]),
MethodParameter(name: "paramD", typeName: TypeName(name: "String"), annotations: [:]),
], annotations: ["anAnnotation": "PARAM A AND METHOD ONLY" as NSString], definedInTypeName: TypeName(name: "Foo"))
])]))
}
}
}
}
Expand Down
24 changes: 23 additions & 1 deletion SourceryTests/Parsing/Helpers/AnnotationsParserSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import Foundation
#endif
@testable import SourceryFramework
@testable import SourceryRuntime
import SwiftParser
import SwiftSyntax

class AnnotationsParserSpec: QuickSpec {
override func spec() {
Expand Down Expand Up @@ -43,7 +45,10 @@ class AnnotationsParserSpec: QuickSpec {
}
describe("parse(content:)") {
func parse(_ content: String) -> Annotations {
return AnnotationsParser(contents: content).all
let tree = Parser.parse(source: content)
let fileName = "in-memory"
let sourceLocationConverter = SourceLocationConverter(fileName: fileName, tree: tree)
return AnnotationsParser(contents: content, sourceLocationConverter: sourceLocationConverter).all
}

it("extracts inline annotations") {
Expand Down Expand Up @@ -76,6 +81,23 @@ class AnnotationsParserSpec: QuickSpec {
expect(result).to(equal(annotations))
}

it("extracts suffix annotations, both block and inline") {
let annotations = ["anAnnotation": "PARAM A ONLY" as NSString,
"testAnnotation": "PARAM B ONLY" as NSString,
"anotherAnnotation": "PARAM C ONLY" as NSString]

let result = parse("""
class Foo {
func bar(paramA: String, // sourcery: anAnnotation = "PARAM A ONLY"
/* sourcery: testAnnotation="PARAM B ONLY"*/ paramB: String,
paramC: String, // sourcery: anotherAnnotation = "PARAM C ONLY"
paramD: String
) {}
}
""")
expect(result).to(equal(annotations))
}

it("extracts repeated annotations into array") {
let parsedAnnotations = parse("// sourcery: implements = \"Service1\"\n// sourcery: implements = \"Service2\"")
expect(parsedAnnotations["implements"] as? [String]).to(equal(["Service1", "Service2"]))
Expand Down

0 comments on commit e5447a6

Please sign in to comment.