From e15bfaa4cc92cba562f1feb91ce3a7ec614451b2 Mon Sep 17 00:00:00 2001 From: Jakub Florek <63607600+MAJKFL@users.noreply.github.com> Date: Mon, 15 Jul 2024 16:41:43 +0200 Subject: [PATCH 01/27] Format. --- .../IntroducingToParentScopeSyntax.swift | 21 +++++++ Sources/SwiftLexicalLookup/LookupName.swift | 4 -- .../ScopeImplementations.swift | 62 +++++++++++++++++-- Sources/SwiftLexicalLookup/ScopeSyntax.swift | 6 +- .../NameLookupTests.swift | 17 +++-- 5 files changed, 97 insertions(+), 13 deletions(-) create mode 100644 Sources/SwiftLexicalLookup/IntroducingToParentScopeSyntax.swift diff --git a/Sources/SwiftLexicalLookup/IntroducingToParentScopeSyntax.swift b/Sources/SwiftLexicalLookup/IntroducingToParentScopeSyntax.swift new file mode 100644 index 00000000000..966e42bd6b3 --- /dev/null +++ b/Sources/SwiftLexicalLookup/IntroducingToParentScopeSyntax.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +@_spi(Experimental) public protocol IntroducingToParentScopeSyntax: ScopeSyntax { + func introducedToParent( + for name: String?, + at syntax: SyntaxProtocol, + with configDict: LookupConfigDictionary + ) -> [LookupResult] +} diff --git a/Sources/SwiftLexicalLookup/LookupName.swift b/Sources/SwiftLexicalLookup/LookupName.swift index 9d7459a4402..edaafa936a4 100644 --- a/Sources/SwiftLexicalLookup/LookupName.swift +++ b/Sources/SwiftLexicalLookup/LookupName.swift @@ -91,10 +91,6 @@ import SwiftSyntax functionCallExpr.arguments.flatMap { argument in getNames(from: argument.expression, accessibleAfter: accessibleAfter) } - case .guardStmt(let guardStmt): - guardStmt.conditions.flatMap { cond in - getNames(from: cond.condition, accessibleAfter: cond.endPosition) - } default: if let namedDecl = Syntax(syntax).asProtocol(SyntaxProtocol.self) as? NamedDeclSyntax { handle(namedDecl: namedDecl, accessibleAfter: accessibleAfter) diff --git a/Sources/SwiftLexicalLookup/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/ScopeImplementations.swift index 6eb2e0032d7..0dd27600fe8 100644 --- a/Sources/SwiftLexicalLookup/ScopeImplementations.swift +++ b/Sources/SwiftLexicalLookup/ScopeImplementations.swift @@ -67,10 +67,11 @@ extension SyntaxProtocol { let names = introducedNames(using: nameIntroductionStrategy) .filter { introducedName in - introducedName.isAccessible(at: syntax) && (name == nil || introducedName.refersTo(name!)) + does(name: name, referTo: introducedName, at: syntax) } - return [.fromFileScope(self, withNames: names, nameIntroductionStrategy: nameIntroductionStrategy)] + return names.isEmpty + ? [] : [.fromFileScope(self, withNames: names, nameIntroductionStrategy: nameIntroductionStrategy)] } } @@ -80,6 +81,45 @@ extension SyntaxProtocol { LookupName.getNames(from: codeBlockItem.item, accessibleAfter: codeBlockItem.endPosition) } } + + public func lookup( + for name: String?, + at syntax: SyntaxProtocol, + with configDict: LookupConfigDictionary + ) -> [LookupResult] { + var result = [LookupResult]() + var currentChunk = [LookupName]() + + for codeBlockItem in statements { + if let introducingToParentScope = Syntax(codeBlockItem.item).asProtocol(SyntaxProtocol.self) + as? IntroducingToParentScopeSyntax + { + if !currentChunk.isEmpty { + result.append(.fromScope(self, withNames: currentChunk)) + currentChunk = [] + } + + result.append(contentsOf: introducingToParentScope.introducedToParent(for: name, at: syntax, with: configDict)) + } else { + currentChunk.append( + contentsOf: + LookupName.getNames( + from: codeBlockItem.item, + accessibleAfter: codeBlockItem.endPosition + ).filter { introducedName in + does(name: name, referTo: introducedName, at: syntax) + } + ) + } + } + + if !currentChunk.isEmpty { + result.append(.fromScope(self, withNames: currentChunk)) + currentChunk = [] + } + + return result.reversed() + lookupInParent(for: name, at: syntax, with: configDict) + } } @_spi(Experimental) extension ForStmtSyntax: ScopeSyntax { @@ -171,7 +211,21 @@ extension SyntaxProtocol { } } -@_spi(Experimental) extension GuardStmtSyntax: ScopeSyntax { +@_spi(Experimental) extension GuardStmtSyntax: IntroducingToParentScopeSyntax { + public func introducedToParent( + for name: String?, + at syntax: SwiftSyntax.SyntaxProtocol, + with configDict: LookupConfigDictionary + ) -> [LookupResult] { + let names = conditions.flatMap { element in + LookupName.getNames(from: element.condition, accessibleAfter: element.endPosition) + }.filter { introducedName in + does(name: name, referTo: introducedName, at: syntax) + } + + return names.isEmpty ? [] : [.fromScope(self, withNames: names)] + } + public var introducedNames: [LookupName] { [] } @@ -182,7 +236,7 @@ extension SyntaxProtocol { with configDict: LookupConfigDictionary ) -> [LookupResult] { if body.position <= syntax.position && body.endPosition >= syntax.position { - lookupInParent(for: name, at: self, with: configDict) + lookupInParent(for: name, at: self, with: configDict) // Should we add a new config that will skip certain scopes in lookup? Could be more consistent. } else { defaultLookupImplementation(for: name, at: syntax, with: configDict) } diff --git a/Sources/SwiftLexicalLookup/ScopeSyntax.swift b/Sources/SwiftLexicalLookup/ScopeSyntax.swift index ffa505e0511..649045a6b35 100644 --- a/Sources/SwiftLexicalLookup/ScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/ScopeSyntax.swift @@ -85,7 +85,7 @@ extension SyntaxProtocol { let filteredNames = introducedNames .filter { introducedName in - introducedName.isAccessible(at: syntax) && (name == nil || introducedName.refersTo(name!)) + does(name: name, referTo: introducedName, at: syntax) } if filteredNames.isEmpty { @@ -103,4 +103,8 @@ extension SyntaxProtocol { ) -> [LookupResult] { parentScope?.lookup(for: name, at: syntax, with: configDict) ?? [] } + + func does(name: String?, referTo introducedName: LookupName, at syntax: SyntaxProtocol) -> Bool { + introducedName.isAccessible(at: syntax) && (name == nil || introducedName.refersTo(name!)) + } } diff --git a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift index 366ca895224..1babde6244f 100644 --- a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift +++ b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift @@ -440,8 +440,11 @@ final class testNameLookup: XCTestCase { references: [ "4️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"])], "5️⃣": [], - "6️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣", "2️⃣"])], - "7️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["3️⃣"])], + "6️⃣": [ + .fromScope(GuardStmtSyntax.self, expectedNames: ["2️⃣"]), + .fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"]), + ], + "7️⃣": [.fromScope(GuardStmtSyntax.self, expectedNames: ["3️⃣"])], ], expectedResultTypes: .all( IdentifierPatternSyntax.self @@ -459,8 +462,14 @@ final class testNameLookup: XCTestCase { """, references: [ "3️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"])], - "5️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣", "2️⃣"])], - "6️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣", "2️⃣", "4️⃣"])], + "5️⃣": [ + .fromScope(GuardStmtSyntax.self, expectedNames: ["2️⃣"]), + .fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"]), + ], + "6️⃣": [ + .fromScope(GuardStmtSyntax.self, expectedNames: ["2️⃣", "4️⃣"]), + .fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"]), + ], ], expectedResultTypes: .all( IdentifierPatternSyntax.self From 99b5294a1aa7884ff9d403552d09683f3e11fee7 Mon Sep 17 00:00:00 2001 From: Jakub Florek <63607600+MAJKFL@users.noreply.github.com> Date: Wed, 17 Jul 2024 15:28:11 +0200 Subject: [PATCH 02/27] Add guard support to file scope. --- .../Configurations/LookupConfig.swift | 12 +- .../IntroducingToParentScopeSyntax.swift | 2 +- .../ScopeImplementations.swift | 107 ++++++++++-------- .../SequentialScopeSyntax.swift | 71 ++++++++++++ Tests/SwiftLexicalLookupTest/Assertions.swift | 24 +++- .../NameLookupTests.swift | 47 +++++++- 6 files changed, 212 insertions(+), 51 deletions(-) create mode 100644 Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift diff --git a/Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift b/Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift index d96093037ca..ca6899603bc 100644 --- a/Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift +++ b/Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift @@ -15,9 +15,17 @@ import Foundation @_spi(Experimental) public struct LookupConfig { /// Specifies behaviour of file scope. /// `memberBlockUpToLastDecl` by default. - public var fileScopeHandling: FileScopeHandlingConfig = .memberBlockUpToLastDecl + public var fileScopeHandling: FileScopeHandlingConfig - public init(fileScopeHandling: FileScopeHandlingConfig = .memberBlockUpToLastDecl) { + /// Specifies scopes that introduce names to their parent and + /// should be skipped during lookup in sequential scopes. + public var ignoreChildrenToParentIntroductionsFrom: [IntroducingToParentScopeSyntax] + + public init( + fileScopeHandling: FileScopeHandlingConfig = .memberBlockUpToLastDecl, + ignoreChildrenToParentIntroductionsFrom: [IntroducingToParentScopeSyntax] = [] + ) { self.fileScopeHandling = fileScopeHandling + self.ignoreChildrenToParentIntroductionsFrom = ignoreChildrenToParentIntroductionsFrom } } diff --git a/Sources/SwiftLexicalLookup/IntroducingToParentScopeSyntax.swift b/Sources/SwiftLexicalLookup/IntroducingToParentScopeSyntax.swift index 966e42bd6b3..85df458d497 100644 --- a/Sources/SwiftLexicalLookup/IntroducingToParentScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/IntroducingToParentScopeSyntax.swift @@ -16,6 +16,6 @@ import SwiftSyntax func introducedToParent( for name: String?, at syntax: SyntaxProtocol, - with configDict: LookupConfigDictionary + with config: LookupConfig ) -> [LookupResult] } diff --git a/Sources/SwiftLexicalLookup/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/ScopeImplementations.swift index 122ddb9f94b..7fdc57dd162 100644 --- a/Sources/SwiftLexicalLookup/ScopeImplementations.swift +++ b/Sources/SwiftLexicalLookup/ScopeImplementations.swift @@ -23,7 +23,7 @@ extension SyntaxProtocol { } } -@_spi(Experimental) extension SourceFileSyntax: ScopeSyntax { +@_spi(Experimental) extension SourceFileSyntax: SequentialScopeSyntax { /// All names introduced in the file scope /// according to the default strategy: `memberBlockUpToLastDecl`. public var introducedNames: [LookupName] { @@ -105,16 +105,58 @@ extension SyntaxProtocol { at syntax: SyntaxProtocol, with config: LookupConfig ) -> [LookupResult] { - let names = introducedNames(using: config.fileScopeHandling) - .filter { introducedName in - does(name: name, referTo: introducedName, at: syntax) + switch config.fileScopeHandling { + case .codeBlock: + return sequentialLookup( + in: statements, + for: name, + at: syntax, + with: config, + createResultsForThisScopeWith: { .fromFileScope(self, withNames: $0) } + ) + case .memberBlock: + let names = introducedNames(using: .memberBlock) + .filter { lookupName in + does(name: name, referTo: lookupName, at: syntax) + } + + return names.isEmpty ? [] : [.fromFileScope(self, withNames: names)] + case .memberBlockUpToLastDecl: + var members = [LookupName]() + var sequentialItems = [CodeBlockItemSyntax]() + var encounteredNonDeclaration = false + + for codeBlockItem in statements { + let item = codeBlockItem.item + + if encounteredNonDeclaration { + sequentialItems.append(codeBlockItem) + } else { + if item.is(DeclSyntax.self) || item.is(VariableDeclSyntax.self) { + let foundNames = LookupName.getNames(from: item) + + members.append(contentsOf: foundNames.filter { does(name: name, referTo: $0, at: syntax) }) + } else { + encounteredNonDeclaration = true + sequentialItems.append(codeBlockItem) + } + } } - return names.isEmpty ? [] : [.fromFileScope(self, withNames: names)] + let sequentialNames = sequentialLookup( + in: sequentialItems, + for: name, + at: syntax, + with: config, + createResultsForThisScopeWith: { .fromFileScope(self, withNames: $0) } + ) + + return (members.isEmpty ? [] : [.fromFileScope(self, withNames: members)]) + sequentialNames + } } } -@_spi(Experimental) extension CodeBlockSyntax: ScopeSyntax { +@_spi(Experimental) extension CodeBlockSyntax: SequentialScopeSyntax { /// Names introduced in the code block scope /// accessible after their declaration. public var introducedNames: [LookupName] { @@ -123,43 +165,14 @@ extension SyntaxProtocol { } } - public func lookup( - for name: String?, - at syntax: SyntaxProtocol, - with configDict: LookupConfigDictionary - ) -> [LookupResult] { - var result = [LookupResult]() - var currentChunk = [LookupName]() - - for codeBlockItem in statements { - if let introducingToParentScope = Syntax(codeBlockItem.item).asProtocol(SyntaxProtocol.self) - as? IntroducingToParentScopeSyntax - { - if !currentChunk.isEmpty { - result.append(.fromScope(self, withNames: currentChunk)) - currentChunk = [] - } - - result.append(contentsOf: introducingToParentScope.introducedToParent(for: name, at: syntax, with: configDict)) - } else { - currentChunk.append( - contentsOf: - LookupName.getNames( - from: codeBlockItem.item, - accessibleAfter: codeBlockItem.endPosition - ).filter { introducedName in - does(name: name, referTo: introducedName, at: syntax) - } - ) - } - } - - if !currentChunk.isEmpty { - result.append(.fromScope(self, withNames: currentChunk)) - currentChunk = [] - } - - return result.reversed() + lookupInParent(for: name, at: syntax, with: configDict) + public func lookup(for name: String?, at syntax: SyntaxProtocol, with config: LookupConfig) -> [LookupResult] { + sequentialLookup( + in: statements, + for: name, + at: syntax, + with: config, + createResultsForThisScopeWith: { .fromScope(self, withNames: $0) } + ) } } @@ -300,7 +313,7 @@ extension SyntaxProtocol { public func introducedToParent( for name: String?, at syntax: SwiftSyntax.SyntaxProtocol, - with configDict: LookupConfigDictionary + with config: LookupConfig ) -> [LookupResult] { let names = conditions.flatMap { element in LookupName.getNames(from: element.condition, accessibleAfter: element.endPosition) @@ -332,9 +345,11 @@ extension SyntaxProtocol { with config: LookupConfig ) -> [LookupResult] { if body.position <= syntax.position && body.endPosition >= syntax.position { - lookupInParent(for: name, at: self, with: config) + var newConfig = config + newConfig.ignoreChildrenToParentIntroductionsFrom.append(self) + return lookupInParent(for: name, at: syntax, with: newConfig) } else { - defaultLookupImplementation(for: name, at: syntax, with: config) + return defaultLookupImplementation(for: name, at: syntax, with: config) } } } diff --git a/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift new file mode 100644 index 00000000000..1a74f9efd16 --- /dev/null +++ b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift @@ -0,0 +1,71 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +@_spi(Experimental) public protocol SequentialScopeSyntax: ScopeSyntax { + func sequentialLookup( + in codeBlockItems: any Collection, + for name: String?, + at syntax: SyntaxProtocol, + with config: LookupConfig, + createResultsForThisScopeWith getResults: ([LookupName]) -> (LookupResult) + ) -> [LookupResult] +} + +@_spi(Experimental) extension SequentialScopeSyntax { + public func sequentialLookup( + in codeBlockItems: any Collection, + for name: String?, + at syntax: SyntaxProtocol, + with config: LookupConfig, + createResultsForThisScopeWith getResults: ([LookupName]) -> (LookupResult) + ) -> [LookupResult] { + var result = [LookupResult]() + var currentChunk = [LookupName]() + + for codeBlockItem in codeBlockItems { + if let introducingToParentScope = Syntax(codeBlockItem.item).asProtocol(SyntaxProtocol.self) + as? IntroducingToParentScopeSyntax + { + guard !config.ignoreChildrenToParentIntroductionsFrom.contains(where: { $0.id == introducingToParentScope.id }) + else { + continue + } + + if !currentChunk.isEmpty { + result.append(getResults(currentChunk)) + currentChunk = [] + } + + result.append(contentsOf: introducingToParentScope.introducedToParent(for: name, at: syntax, with: config)) + } else { + currentChunk.append( + contentsOf: + LookupName.getNames( + from: codeBlockItem.item, + accessibleAfter: codeBlockItem.endPosition + ).filter { introducedName in + does(name: name, referTo: introducedName, at: syntax) + } + ) + } + } + + if !currentChunk.isEmpty { + result.append(getResults(currentChunk)) + currentChunk = [] + } + + return (result.isEmpty ? [] : result.reversed()) + lookupInParent(for: name, at: syntax, with: config) + } +} diff --git a/Tests/SwiftLexicalLookupTest/Assertions.swift b/Tests/SwiftLexicalLookupTest/Assertions.swift index 76106fb1453..ee545d04500 100644 --- a/Tests/SwiftLexicalLookupTest/Assertions.swift +++ b/Tests/SwiftLexicalLookupTest/Assertions.swift @@ -66,6 +66,26 @@ enum ResultExpectation { expectedNames } } + + var debugDescription: String { + switch self { + case .fromScope: + "fromScope" + case .fromFileScope: + "fromFileScope" + } + } +} + +extension LookupResult { + var debugDescription: String { + switch self { + case .fromScope: + "fromScope" + case .fromFileScope: + "fromFileScope" + } + } } /// `methodUnderTest` is called with the token at every position marker in the keys of `expected`. @@ -175,7 +195,9 @@ func assertLexicalNameLookup( case (.fromFileScope, .fromFileScope): break default: - XCTFail("For marker \(marker), result actual result kind \(actual) doesn't match expected \(expected)") + XCTFail( + "For marker \(marker), actual result kind \(actual.debugDescription) doesn't match expected \(expected.debugDescription)" + ) } } diff --git a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift index 138d41b2595..46012a62c2c 100644 --- a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift +++ b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift @@ -150,7 +150,7 @@ final class testNameLookup: XCTestCase { "6️⃣": [ .fromScope(ClosureExprSyntax.self, expectedNames: ["3️⃣"]), .fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"]), - ], + ] ], expectedResultTypes: .all( ClosureCaptureSyntax.self, @@ -567,4 +567,49 @@ final class testNameLookup: XCTestCase { config: LookupConfig(fileScopeHandling: .codeBlock) ) } + + func testGuardOnFileScope() { + assertLexicalNameLookup( + source: """ + let 1️⃣a = 0 + + class c {} + + guard let 2️⃣a else { fatalError() } + + 3️⃣class a {} + + let x = 4️⃣a + """, + references: [ + "4️⃣": [.fromFileScope(expectedNames: ["1️⃣"]), + .fromFileScope(expectedNames: ["3️⃣"]), + .fromScope(GuardStmtSyntax.self, expectedNames: ["2️⃣"])], + ], + expectedResultTypes: .all(IdentifierPatternSyntax.self, except: ["3️⃣": ClassDeclSyntax.self]) + ) + } + + func testGuardOnFileScopeCodeBlock() { + assertLexicalNameLookup( + source: """ + let 1️⃣a = 0 + + class c {} + + guard let 2️⃣a else { fatalError() } + + 3️⃣class a {} + + let x = 4️⃣a + """, + references: [ + "4️⃣": [.fromFileScope(expectedNames: ["3️⃣"]), + .fromScope(GuardStmtSyntax.self, expectedNames: ["2️⃣"]), + .fromFileScope(expectedNames: ["1️⃣"]),], + ], + expectedResultTypes: .all(IdentifierPatternSyntax.self, except: ["3️⃣": ClassDeclSyntax.self]), + config: LookupConfig(fileScopeHandling: .codeBlock) + ) + } } From c31046a36362f63cbe1afbe34201e859fe8c3035 Mon Sep 17 00:00:00 2001 From: Jakub Florek <63607600+MAJKFL@users.noreply.github.com> Date: Thu, 18 Jul 2024 20:34:00 +0200 Subject: [PATCH 03/27] Add implicit names and proper guard handling. --- .../Configurations/LookupConfig.swift | 6 +- .../IdentifiableSyntax.swift | 6 + ...ducingToSequentialParentScopeSyntax.swift} | 6 +- Sources/SwiftLexicalLookup/LookupName.swift | 54 +++++++- .../ScopeImplementations.swift | 29 ++++- .../SequentialScopeSyntax.swift | 17 ++- .../SwiftLexicalLookup/TypeScopeSyntax.swift | 27 ++++ Tests/SwiftLexicalLookupTest/Assertions.swift | 92 +------------- .../SwiftLexicalLookupTest/ExpectedName.swift | 76 ++++++++++++ .../MarkerExpectation.swift | 51 ++++++++ .../NameLookupTests.swift | 116 +++++++++++++++--- .../ResultExpectation.swift | 73 +++++++++++ 12 files changed, 437 insertions(+), 116 deletions(-) rename Sources/SwiftLexicalLookup/{IntroducingToParentScopeSyntax.swift => IntroducingToSequentialParentScopeSyntax.swift} (74%) create mode 100644 Sources/SwiftLexicalLookup/TypeScopeSyntax.swift create mode 100644 Tests/SwiftLexicalLookupTest/ExpectedName.swift create mode 100644 Tests/SwiftLexicalLookupTest/MarkerExpectation.swift create mode 100644 Tests/SwiftLexicalLookupTest/ResultExpectation.swift diff --git a/Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift b/Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift index ca6899603bc..c99e8bc796f 100644 --- a/Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift +++ b/Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift @@ -19,13 +19,13 @@ import Foundation /// Specifies scopes that introduce names to their parent and /// should be skipped during lookup in sequential scopes. - public var ignoreChildrenToParentIntroductionsFrom: [IntroducingToParentScopeSyntax] + public var ignoreChildrenToSequentialParentIntroductionsFrom: [IntroducingToSequentialParentScopeSyntax] public init( fileScopeHandling: FileScopeHandlingConfig = .memberBlockUpToLastDecl, - ignoreChildrenToParentIntroductionsFrom: [IntroducingToParentScopeSyntax] = [] + ignoreChildrenToSequentialParentIntroductionsFrom: [IntroducingToSequentialParentScopeSyntax] = [] ) { self.fileScopeHandling = fileScopeHandling - self.ignoreChildrenToParentIntroductionsFrom = ignoreChildrenToParentIntroductionsFrom + self.ignoreChildrenToSequentialParentIntroductionsFrom = ignoreChildrenToSequentialParentIntroductionsFrom } } diff --git a/Sources/SwiftLexicalLookup/IdentifiableSyntax.swift b/Sources/SwiftLexicalLookup/IdentifiableSyntax.swift index 67e6de6b4e9..d7f47ab98d6 100644 --- a/Sources/SwiftLexicalLookup/IdentifiableSyntax.swift +++ b/Sources/SwiftLexicalLookup/IdentifiableSyntax.swift @@ -41,3 +41,9 @@ extension ClosureCaptureSyntax: IdentifiableSyntax { expression.as(DeclReferenceExprSyntax.self)!.baseName } } + +extension AccessorParametersSyntax: IdentifiableSyntax { + @_spi(Experimental) public var identifier: TokenSyntax { + name + } +} diff --git a/Sources/SwiftLexicalLookup/IntroducingToParentScopeSyntax.swift b/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift similarity index 74% rename from Sources/SwiftLexicalLookup/IntroducingToParentScopeSyntax.swift rename to Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift index 85df458d497..35c70792087 100644 --- a/Sources/SwiftLexicalLookup/IntroducingToParentScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift @@ -12,8 +12,10 @@ import SwiftSyntax -@_spi(Experimental) public protocol IntroducingToParentScopeSyntax: ScopeSyntax { - func introducedToParent( +@_spi(Experimental) public protocol IntroducingToSequentialParentScopeSyntax: ScopeSyntax { + /// Returns names matching lookup that should be + /// handled by it's parent sequential scope. + func introducesToSequentialParent( for name: String?, at syntax: SyntaxProtocol, with config: LookupConfig diff --git a/Sources/SwiftLexicalLookup/LookupName.swift b/Sources/SwiftLexicalLookup/LookupName.swift index 8ef32ca8584..0767ad716a7 100644 --- a/Sources/SwiftLexicalLookup/LookupName.swift +++ b/Sources/SwiftLexicalLookup/LookupName.swift @@ -19,6 +19,18 @@ import SwiftSyntax /// Declaration associated with the name. /// Could be class, struct, actor, protocol, function and more case declaration(NamedDeclSyntax, accessibleAfter: AbsolutePosition?) + /// `self` keyword representing object instance. + case `self`(DeclSyntaxProtocol) + /// `Self` keyword representing object type. + case `Self`(DeclSyntaxProtocol) + /// `self` captured by a closure. + case selfCaptured(ClosureCaptureSyntax) + /// `error` available inside `catch` clause. + case error(CatchClauseSyntax) + /// `newValue` available by default inside `set` and `willSet`. + case newValue(AccessorDeclSyntax) + /// `oldValue` available by default inside `didSet`. + case oldValue(AccessorDeclSyntax) /// Syntax associated with this name. @_spi(Experimental) public var syntax: SyntaxProtocol { @@ -27,6 +39,18 @@ import SwiftSyntax syntax case .declaration(let syntax, _): syntax + case .self(let syntax): + syntax + case .Self(let syntax): + syntax + case .selfCaptured(let syntax): + syntax + case .error(let syntax): + syntax + case .newValue(let syntax): + syntax + case .oldValue(let syntax): + syntax } } @@ -37,6 +61,8 @@ import SwiftSyntax Identifier(syntax.identifier) case .declaration(let syntax, _): Identifier(syntax.name) + default: + nil } } @@ -46,6 +72,26 @@ import SwiftSyntax switch self { case .identifier(_, let absolutePosition), .declaration(_, let absolutePosition): absolutePosition + default: + nil + } + } + + /// Used for name comparison. + var name: String? { + switch self { + case .identifier, .declaration: + identifier?.name + case .self, .selfCaptured: + "self" + case .Self: + "Self" + case .error: + "error" + case .newValue: + "newValue" + case .oldValue: + "oldValue" } } @@ -57,7 +103,7 @@ import SwiftSyntax /// Checks if this name refers to the looked up phrase. func refersTo(_ lookedUpName: String) -> Bool { - guard let name = identifier?.name else { return false } + guard let name else { return false } return name == lookedUpName } @@ -104,7 +150,11 @@ import SwiftSyntax /// Extracts name introduced by `IdentifiableSyntax` node. private static func handle(identifiable: IdentifiableSyntax, accessibleAfter: AbsolutePosition? = nil) -> [LookupName] { - if identifiable.identifier.text != "_" { + if let closureCapture = identifiable as? ClosureCaptureSyntax, + closureCapture.identifier.tokenKind == .keyword(.self) + { + return [.selfCaptured(closureCapture)] // Handle `self` closure capture. + } else if identifiable.identifier.text != "_" { return [.identifier(identifiable, accessibleAfter: accessibleAfter)] } else { return [] diff --git a/Sources/SwiftLexicalLookup/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/ScopeImplementations.swift index 7fdc57dd162..6aad863395c 100644 --- a/Sources/SwiftLexicalLookup/ScopeImplementations.swift +++ b/Sources/SwiftLexicalLookup/ScopeImplementations.swift @@ -309,8 +309,8 @@ extension SyntaxProtocol { } } -@_spi(Experimental) extension GuardStmtSyntax: IntroducingToParentScopeSyntax { - public func introducedToParent( +@_spi(Experimental) extension GuardStmtSyntax: IntroducingToSequentialParentScopeSyntax { + public func introducesToSequentialParent( for name: String?, at syntax: SwiftSyntax.SyntaxProtocol, with config: LookupConfig @@ -346,10 +346,33 @@ extension SyntaxProtocol { ) -> [LookupResult] { if body.position <= syntax.position && body.endPosition >= syntax.position { var newConfig = config - newConfig.ignoreChildrenToParentIntroductionsFrom.append(self) + newConfig.ignoreChildrenToSequentialParentIntroductionsFrom.append(self) return lookupInParent(for: name, at: syntax, with: newConfig) } else { return defaultLookupImplementation(for: name, at: syntax, with: config) } } } + +@_spi(Experimental) extension ActorDeclSyntax: TypeScopeSyntax {} +@_spi(Experimental) extension ClassDeclSyntax: TypeScopeSyntax {} +@_spi(Experimental) extension StructDeclSyntax: TypeScopeSyntax {} +@_spi(Experimental) extension EnumDeclSyntax: TypeScopeSyntax {} +@_spi(Experimental) extension ExtensionDeclSyntax: TypeScopeSyntax {} + +@_spi(Experimental) extension AccessorDeclSyntax: ScopeSyntax { + public var introducedNames: [LookupName] { + if let parameters { + LookupName.getNames(from: parameters) + } else { + switch accessorSpecifier.tokenKind { + case .keyword(.set), .keyword(.willSet): + [.newValue(self)] + case .keyword(.didSet): + [.oldValue(self)] + default: + [] + } + } + } +} diff --git a/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift index 1a74f9efd16..e5658ffecc2 100644 --- a/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift @@ -12,7 +12,13 @@ import SwiftSyntax +/// Scope that, in addition to names introduced by itself, +/// also handles names introduced by +/// `IntroducingToSequentialParentScopeSyntax` children scopes. @_spi(Experimental) public protocol SequentialScopeSyntax: ScopeSyntax { + /// Returns names introduced by `codeBlockItems` + /// and included `IntroducingToSequentialParentScopeSyntax` children + /// scopes that match the lookup. func sequentialLookup( in codeBlockItems: any Collection, for name: String?, @@ -35,9 +41,12 @@ import SwiftSyntax for codeBlockItem in codeBlockItems { if let introducingToParentScope = Syntax(codeBlockItem.item).asProtocol(SyntaxProtocol.self) - as? IntroducingToParentScopeSyntax + as? IntroducingToSequentialParentScopeSyntax { - guard !config.ignoreChildrenToParentIntroductionsFrom.contains(where: { $0.id == introducingToParentScope.id }) + guard + !config.ignoreChildrenToSequentialParentIntroductionsFrom.contains(where: { + $0.id == introducingToParentScope.id + }) else { continue } @@ -47,7 +56,9 @@ import SwiftSyntax currentChunk = [] } - result.append(contentsOf: introducingToParentScope.introducedToParent(for: name, at: syntax, with: config)) + result.append( + contentsOf: introducingToParentScope.introducesToSequentialParent(for: name, at: syntax, with: config) + ) } else { currentChunk.append( contentsOf: diff --git a/Sources/SwiftLexicalLookup/TypeScopeSyntax.swift b/Sources/SwiftLexicalLookup/TypeScopeSyntax.swift new file mode 100644 index 00000000000..617410a1446 --- /dev/null +++ b/Sources/SwiftLexicalLookup/TypeScopeSyntax.swift @@ -0,0 +1,27 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +@_spi(Experimental) public protocol TypeScopeSyntax: ScopeSyntax, DeclSyntaxProtocol { + var implicitInstanceAndTypeNames: [LookupName] { get } +} + +@_spi(Experimental) extension TypeScopeSyntax { + public var implicitInstanceAndTypeNames: [LookupName] { + [.self(self), .Self(self)] + } + + public var introducedNames: [LookupName] { + implicitInstanceAndTypeNames + } +} diff --git a/Tests/SwiftLexicalLookupTest/Assertions.swift b/Tests/SwiftLexicalLookupTest/Assertions.swift index ee545d04500..18ca4a69a4f 100644 --- a/Tests/SwiftLexicalLookupTest/Assertions.swift +++ b/Tests/SwiftLexicalLookupTest/Assertions.swift @@ -10,84 +10,12 @@ // //===----------------------------------------------------------------------===// -import Foundation @_spi(Experimental) import SwiftLexicalLookup import SwiftParser import SwiftSyntax import XCTest import _SwiftSyntaxTestSupport -/// Used to define result type expectectations for given markers. -enum MarkerExpectation { - /// Specifies a separate type for each result marker. - case distinct([String: SyntaxProtocol.Type]) - /// Specifies a common type for all results - /// apart from the ones defined explicitly in `except`. - case all(SyntaxProtocol.Type, except: [String: SyntaxProtocol.Type] = [:]) - /// Does not assert result types. - case none - - /// Assert `actual` result labeled with `marker` - /// according to the rules represented by this expectation. - fileprivate func assertMarkerType(marker: String, actual: SyntaxProtocol) { - switch self { - case .all(let expectedType, except: let dictionary): - assertMarkerType(marker: marker, actual: actual, expectedType: dictionary[marker] ?? expectedType) - case .distinct(let dictionary): - if let expectedType = dictionary[marker] { - assertMarkerType(marker: marker, actual: actual, expectedType: expectedType) - } else { - XCTFail("For result \(marker), could not find type expectation") - } - case .none: - break - } - } - - /// Assert whether `actual` type matches `expectedType`. - private func assertMarkerType(marker: String, actual: SyntaxProtocol, expectedType: SyntaxProtocol.Type) { - XCTAssert( - actual.is(expectedType), - "For result \(marker), expected type \(expectedType) doesn't match the actual type \(actual.syntaxNodeType)" - ) - } -} - -/// Used to define -enum ResultExpectation { - case fromScope(ScopeSyntax.Type, expectedNames: [String]) - case fromFileScope(expectedNames: [String]) - - var expectedNames: [String] { - switch self { - case .fromScope(_, let expectedNames): - expectedNames - case .fromFileScope(expectedNames: let expectedNames): - expectedNames - } - } - - var debugDescription: String { - switch self { - case .fromScope: - "fromScope" - case .fromFileScope: - "fromFileScope" - } - } -} - -extension LookupResult { - var debugDescription: String { - switch self { - case .fromScope: - "fromScope" - case .fromFileScope: - "fromFileScope" - } - } -} - /// `methodUnderTest` is called with the token at every position marker in the keys of `expected`. /// It then asserts that the positions of the syntax nodes returned by `methodUnderTest` are the values in `expected`. /// It also checks whether result types match rules specified in `expectedResultTypes`. @@ -185,21 +113,7 @@ func assertLexicalNameLookup( return [] } - for (actual, expected) in zip(result, expectedValues) { - switch (actual, expected) { - case (.fromScope(let scope, withNames: _), .fromScope(let expectedType, expectedNames: _)): - XCTAssert( - scope.syntaxNodeType == expectedType, - "For marker \(marker), scope result type of \(scope.syntaxNodeType) doesn't match expected \(expectedType)" - ) - case (.fromFileScope, .fromFileScope): - break - default: - XCTFail( - "For marker \(marker), actual result kind \(actual.debugDescription) doesn't match expected \(expected.debugDescription)" - ) - } - } + ResultExpectation.assertResult(marker: marker, result: result, expectedValues: expectedValues) return result.flatMap { lookUpResult in lookUpResult.names.map { lookupName in @@ -209,7 +123,9 @@ func assertLexicalNameLookup( }, expected: references.mapValues { expectations in expectations.flatMap { expectation in - expectation.expectedNames + expectation.expectedNames.map { expectedName in + expectedName.marker + } } }, expectedResultTypes: expectedResultTypes diff --git a/Tests/SwiftLexicalLookupTest/ExpectedName.swift b/Tests/SwiftLexicalLookupTest/ExpectedName.swift new file mode 100644 index 00000000000..ce5e8a0db57 --- /dev/null +++ b/Tests/SwiftLexicalLookupTest/ExpectedName.swift @@ -0,0 +1,76 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +@_spi(Experimental) import SwiftLexicalLookup +import SwiftSyntax +import XCTest + +/// Used to define lookup name assertion. +protocol ExpectedName { + var marker: String { get } +} + +extension String: ExpectedName { + var marker: String { + self + } +} + +/// Can be used to optionally assert +/// exact lookup name kind. +enum NameExpectation: ExpectedName { + case identifier(String) + case declaration(String) + case selfInstance(String) + case selfType(String) + case selfCaptured(String) + case error(String) + case newValue(String) + case oldValue(String) + + var marker: String { + switch self { + case .identifier(let marker), + .declaration(let marker), + .selfInstance(let marker), + .selfType(let marker), + .selfCaptured(let marker), + .error(let marker), + .newValue(let marker), + .oldValue(let marker): + marker + } + } + + private func assertExpectation(marker: String, for name: LookupName) { + switch (name, self) { + case (.identifier, .identifier): break + case (.declaration, .declaration): break + case (.self, .selfInstance): break + case (.Self, .selfType): break + case (.selfCaptured, .selfCaptured): break + case (.error, .error): break + case (.newValue, .newValue): break + case (.oldValue, .oldValue): break + default: + XCTFail("For marker \(marker), actual name kind \(name) doesn't match expected \(self)") + } + } + + static func assertNames(marker: String, acutalNames: [LookupName], expectedNames: [ExpectedName]) { + for (actualName, expectedName) in zip(acutalNames, expectedNames) { + guard let nameExpectation = expectedName as? NameExpectation else { continue } + + nameExpectation.assertExpectation(marker: marker, for: actualName) + } + } +} diff --git a/Tests/SwiftLexicalLookupTest/MarkerExpectation.swift b/Tests/SwiftLexicalLookupTest/MarkerExpectation.swift new file mode 100644 index 00000000000..a39e981a543 --- /dev/null +++ b/Tests/SwiftLexicalLookupTest/MarkerExpectation.swift @@ -0,0 +1,51 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +@_spi(Experimental) import SwiftLexicalLookup +import SwiftSyntax +import XCTest + +/// Used to define result type expectectations for given markers. +enum MarkerExpectation { + /// Specifies a separate type for each result marker. + case distinct([String: SyntaxProtocol.Type]) + /// Specifies a common type for all results + /// apart from the ones defined explicitly in `except`. + case all(SyntaxProtocol.Type, except: [String: SyntaxProtocol.Type] = [:]) + /// Does not assert result types. + case none + + /// Assert `actual` result labeled with `marker` + /// according to the rules represented by this expectation. + func assertMarkerType(marker: String, actual: SyntaxProtocol) { + switch self { + case .all(let expectedType, except: let dictionary): + assertMarkerType(marker: marker, actual: actual, expectedType: dictionary[marker] ?? expectedType) + case .distinct(let dictionary): + if let expectedType = dictionary[marker] { + assertMarkerType(marker: marker, actual: actual, expectedType: expectedType) + } else { + XCTFail("For result \(marker), could not find type expectation") + } + case .none: + break + } + } + + /// Assert whether `actual` type matches `expectedType`. + private func assertMarkerType(marker: String, actual: SyntaxProtocol, expectedType: SyntaxProtocol.Type) { + XCTAssert( + actual.is(expectedType), + "For result \(marker), expected type \(expectedType) doesn't match the actual type \(actual.syntaxNodeType)" + ) + } +} diff --git a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift index 46012a62c2c..2b4d47b830b 100644 --- a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift +++ b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift @@ -138,24 +138,33 @@ final class testNameLookup: XCTestCase { func testClosureCaptureLookup() { assertLexicalNameLookup( source: """ - func foo() { - let 1️⃣a = 1 - let x = { [3️⃣a, 4️⃣unowned b] in - print(6️⃣a) + 7️⃣class a { + func foo() { + let 1️⃣a = 1 + let x = { [2️⃣weak self, 3️⃣a, 4️⃣unowned b] in + print(5️⃣self, 6️⃣a, 8️⃣b) + } + let b = 0 } - let b = 0 } """, references: [ + "5️⃣": [ + .fromScope(ClosureExprSyntax.self, expectedNames: [NameExpectation.selfCaptured("2️⃣")]), + .fromScope(ClassDeclSyntax.self, expectedNames: [NameExpectation.selfInstance("7️⃣")]), + ], "6️⃣": [ .fromScope(ClosureExprSyntax.self, expectedNames: ["3️⃣"]), .fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"]), - ] + .fromFileScope(expectedNames: ["7️⃣"]), + ], + "8️⃣": [.fromScope(ClosureExprSyntax.self, expectedNames: ["4️⃣"])], ], expectedResultTypes: .all( ClosureCaptureSyntax.self, except: [ - "1️⃣": IdentifierPatternSyntax.self + "1️⃣": IdentifierPatternSyntax.self, + "7️⃣": ClassDeclSyntax.self, ] ) ) @@ -401,6 +410,10 @@ final class testNameLookup: XCTestCase { "7️⃣": [ .fromScope(CodeBlockSyntax.self, expectedNames: ["4️⃣", "5️⃣"]), .fromScope(MemberBlockSyntax.self, expectedNames: ["1️⃣", "2️⃣", "3️⃣"]), + .fromScope( + ClassDeclSyntax.self, + expectedNames: [NameExpectation.selfInstance("🔟"), NameExpectation.selfType("🔟")] + ), .fromFileScope(expectedNames: ["🔟"]), ], "0️⃣": [ @@ -408,6 +421,10 @@ final class testNameLookup: XCTestCase { .fromScope(IfExprSyntax.self, expectedNames: ["6️⃣"]), .fromScope(CodeBlockSyntax.self, expectedNames: ["4️⃣", "5️⃣"]), .fromScope(MemberBlockSyntax.self, expectedNames: ["1️⃣", "2️⃣", "3️⃣"]), + .fromScope( + ClassDeclSyntax.self, + expectedNames: [NameExpectation.selfInstance("🔟"), NameExpectation.selfType("🔟")] + ), .fromFileScope(expectedNames: ["🔟"]), ], ], @@ -567,7 +584,7 @@ final class testNameLookup: XCTestCase { config: LookupConfig(fileScopeHandling: .codeBlock) ) } - + func testGuardOnFileScope() { assertLexicalNameLookup( source: """ @@ -582,14 +599,16 @@ final class testNameLookup: XCTestCase { let x = 4️⃣a """, references: [ - "4️⃣": [.fromFileScope(expectedNames: ["1️⃣"]), - .fromFileScope(expectedNames: ["3️⃣"]), - .fromScope(GuardStmtSyntax.self, expectedNames: ["2️⃣"])], + "4️⃣": [ + .fromFileScope(expectedNames: ["1️⃣"]), + .fromFileScope(expectedNames: ["3️⃣"]), + .fromScope(GuardStmtSyntax.self, expectedNames: ["2️⃣"]), + ] ], expectedResultTypes: .all(IdentifierPatternSyntax.self, except: ["3️⃣": ClassDeclSyntax.self]) ) } - + func testGuardOnFileScopeCodeBlock() { assertLexicalNameLookup( source: """ @@ -604,12 +623,79 @@ final class testNameLookup: XCTestCase { let x = 4️⃣a """, references: [ - "4️⃣": [.fromFileScope(expectedNames: ["3️⃣"]), - .fromScope(GuardStmtSyntax.self, expectedNames: ["2️⃣"]), - .fromFileScope(expectedNames: ["1️⃣"]),], + "4️⃣": [ + .fromFileScope(expectedNames: ["3️⃣"]), + .fromScope(GuardStmtSyntax.self, expectedNames: ["2️⃣"]), + .fromFileScope(expectedNames: ["1️⃣"]), + ] ], expectedResultTypes: .all(IdentifierPatternSyntax.self, except: ["3️⃣": ClassDeclSyntax.self]), config: LookupConfig(fileScopeHandling: .codeBlock) ) } + + func testImplicitSelf() { + assertLexicalNameLookup( + source: """ + 1️⃣extension a { + 2️⃣struct b { + func foo() { + let x: 3️⃣Self = 4️⃣self + } + } + + func bar() { + let x: 5️⃣Self = 6️⃣self + } + } + """, + references: [ + "3️⃣": [ + .fromScope(StructDeclSyntax.self, expectedNames: [NameExpectation.selfType("2️⃣")]), + .fromScope(ExtensionDeclSyntax.self, expectedNames: [NameExpectation.selfType("1️⃣")]), + ], + "4️⃣": [ + .fromScope(StructDeclSyntax.self, expectedNames: [NameExpectation.selfInstance("2️⃣")]), + .fromScope(ExtensionDeclSyntax.self, expectedNames: [NameExpectation.selfInstance("1️⃣")]), + ], + "5️⃣": [.fromScope(ExtensionDeclSyntax.self, expectedNames: [NameExpectation.selfType("1️⃣")])], + "6️⃣": [.fromScope(ExtensionDeclSyntax.self, expectedNames: [NameExpectation.selfInstance("1️⃣")])], + ] + ) + } + + func testAccessorImplicitNames() { + assertLexicalNameLookup( + source: """ + var a: Int { + get { y } + 1️⃣set { + y = 2️⃣newValue + } + } + + var b: Int { + get { y } + set3️⃣(newValue) { + y = 4️⃣newValue + } + } + + var c = 0 { + 5️⃣willSet { + 6️⃣newValue + } + 7️⃣didSet { + 8️⃣oldValue + } + } + """, + references: [ + "2️⃣": [.fromScope(AccessorDeclSyntax.self, expectedNames: [NameExpectation.newValue("1️⃣")])], + "4️⃣": [.fromScope(AccessorDeclSyntax.self, expectedNames: [NameExpectation.identifier("3️⃣")])], + "6️⃣": [.fromScope(AccessorDeclSyntax.self, expectedNames: [NameExpectation.newValue("5️⃣")])], + "8️⃣": [.fromScope(AccessorDeclSyntax.self, expectedNames: [NameExpectation.oldValue("7️⃣")])], + ] + ) + } } diff --git a/Tests/SwiftLexicalLookupTest/ResultExpectation.swift b/Tests/SwiftLexicalLookupTest/ResultExpectation.swift new file mode 100644 index 00000000000..5dc00d568f8 --- /dev/null +++ b/Tests/SwiftLexicalLookupTest/ResultExpectation.swift @@ -0,0 +1,73 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +@_spi(Experimental) import SwiftLexicalLookup +import SwiftSyntax +import XCTest + +/// Used to define lookup result assertion. +enum ResultExpectation { + case fromScope(ScopeSyntax.Type, expectedNames: [ExpectedName]) + case fromFileScope(expectedNames: [ExpectedName]) + + var expectedNames: [ExpectedName] { + switch self { + case .fromScope(_, let expectedNames): + expectedNames + case .fromFileScope(expectedNames: let expectedNames): + expectedNames + } + } + + var debugDescription: String { + switch self { + case .fromScope: + "fromScope" + case .fromFileScope: + "fromFileScope" + } + } + + static func assertResult(marker: String, result: [LookupResult], expectedValues: [ResultExpectation]) { + for (actual, expected) in zip(result, expectedValues) { + switch (actual, expected) { + case ( + .fromScope(let scope, withNames: let actualNames), + .fromScope(let expectedType, expectedNames: let expectedNames) + ): + XCTAssert( + scope.syntaxNodeType == expectedType, + "For marker \(marker), scope result type of \(scope.syntaxNodeType) doesn't match expected \(expectedType)" + ) + + NameExpectation.assertNames(marker: marker, acutalNames: actualNames, expectedNames: expectedNames) + case (.fromFileScope(_, let actualNames), .fromFileScope(let expectedNames)): + NameExpectation.assertNames(marker: marker, acutalNames: actualNames, expectedNames: expectedNames) + default: + XCTFail( + "For marker \(marker), actual result kind \(actual.debugDescription) doesn't match expected \(expected.debugDescription)" + ) + } + } + } +} + +extension LookupResult { + var debugDescription: String { + switch self { + case .fromScope: + "fromScope" + case .fromFileScope: + "fromFileScope" + } + } +} From 8b5fbd29fa491c9517e38e80d4edbf37ba355d15 Mon Sep 17 00:00:00 2001 From: Jakub Florek <63607600+MAJKFL@users.noreply.github.com> Date: Tue, 23 Jul 2024 09:57:45 +0200 Subject: [PATCH 04/27] Separate lookup state from config. --- .../Configurations/LookupConfig.swift | 8 +---- ...oducingToSequentialParentScopeSyntax.swift | 3 +- Sources/SwiftLexicalLookup/LookupState.swift | 21 +++++++++++ .../ScopeImplementations.swift | 36 ++++++++++++------- Sources/SwiftLexicalLookup/ScopeSyntax.swift | 24 ++++++++----- .../SequentialScopeSyntax.swift | 18 ++++++---- 6 files changed, 74 insertions(+), 36 deletions(-) create mode 100644 Sources/SwiftLexicalLookup/LookupState.swift diff --git a/Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift b/Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift index c99e8bc796f..c748bf0bdd7 100644 --- a/Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift +++ b/Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift @@ -17,15 +17,9 @@ import Foundation /// `memberBlockUpToLastDecl` by default. public var fileScopeHandling: FileScopeHandlingConfig - /// Specifies scopes that introduce names to their parent and - /// should be skipped during lookup in sequential scopes. - public var ignoreChildrenToSequentialParentIntroductionsFrom: [IntroducingToSequentialParentScopeSyntax] - public init( - fileScopeHandling: FileScopeHandlingConfig = .memberBlockUpToLastDecl, - ignoreChildrenToSequentialParentIntroductionsFrom: [IntroducingToSequentialParentScopeSyntax] = [] + fileScopeHandling: FileScopeHandlingConfig = .memberBlockUpToLastDecl ) { self.fileScopeHandling = fileScopeHandling - self.ignoreChildrenToSequentialParentIntroductionsFrom = ignoreChildrenToSequentialParentIntroductionsFrom } } diff --git a/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift b/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift index 35c70792087..185906c5d35 100644 --- a/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift @@ -18,6 +18,7 @@ import SwiftSyntax func introducesToSequentialParent( for name: String?, at syntax: SyntaxProtocol, - with config: LookupConfig + with config: LookupConfig, + state: LookupState ) -> [LookupResult] } diff --git a/Sources/SwiftLexicalLookup/LookupState.swift b/Sources/SwiftLexicalLookup/LookupState.swift new file mode 100644 index 00000000000..7bf27595dc5 --- /dev/null +++ b/Sources/SwiftLexicalLookup/LookupState.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Foundation + +@_spi(Experimental) public struct LookupState { + /// Specifies scopes that introduce names to their parent and + /// should be skipped during lookup in sequential scopes. + var skipSequentialIntroductionFrom: IntroducingToSequentialParentScopeSyntax? + + @_spi(Experimental) public init() {} +} diff --git a/Sources/SwiftLexicalLookup/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/ScopeImplementations.swift index 6aad863395c..169755e1850 100644 --- a/Sources/SwiftLexicalLookup/ScopeImplementations.swift +++ b/Sources/SwiftLexicalLookup/ScopeImplementations.swift @@ -103,7 +103,8 @@ extension SyntaxProtocol { public func lookup( for name: String?, at syntax: SyntaxProtocol, - with config: LookupConfig + with config: LookupConfig, + state: LookupState ) -> [LookupResult] { switch config.fileScopeHandling { case .codeBlock: @@ -112,6 +113,7 @@ extension SyntaxProtocol { for: name, at: syntax, with: config, + state: state, createResultsForThisScopeWith: { .fromFileScope(self, withNames: $0) } ) case .memberBlock: @@ -148,6 +150,7 @@ extension SyntaxProtocol { for: name, at: syntax, with: config, + state: state, createResultsForThisScopeWith: { .fromFileScope(self, withNames: $0) } ) @@ -165,18 +168,24 @@ extension SyntaxProtocol { } } - public func lookup(for name: String?, at syntax: SyntaxProtocol, with config: LookupConfig) -> [LookupResult] { + public func lookup( + for name: String?, + at syntax: SyntaxProtocol, + with config: LookupConfig, + state: LookupState + ) -> [LookupResult] { sequentialLookup( in: statements, for: name, at: syntax, with: config, + state: state, createResultsForThisScopeWith: { .fromScope(self, withNames: $0) } ) } } -@_spi(Experimental) extension ForStmtSyntax: ScopeSyntax { +@_spi(Experimental) extension ForStmtSyntax: ScopeSyntax { /// Names introduced in the `for` body. public var introducedNames: [LookupName] { LookupName.getNames(from: pattern) @@ -290,12 +299,13 @@ extension SyntaxProtocol { public func lookup( for name: String?, at syntax: SyntaxProtocol, - with config: LookupConfig + with config: LookupConfig, + state: LookupState ) -> [LookupResult] { if let elseBody, elseBody.position <= syntax.position, elseBody.endPosition >= syntax.position { - lookupInParent(for: name, at: syntax, with: config) + lookupInParent(for: name, at: syntax, with: config, state: state) } else { - defaultLookupImplementation(for: name, at: syntax, with: config) + defaultLookupImplementation(for: name, at: syntax, with: config, state: state) } } } @@ -313,7 +323,8 @@ extension SyntaxProtocol { public func introducesToSequentialParent( for name: String?, at syntax: SwiftSyntax.SyntaxProtocol, - with config: LookupConfig + with config: LookupConfig, + state: LookupState ) -> [LookupResult] { let names = conditions.flatMap { element in LookupName.getNames(from: element.condition, accessibleAfter: element.endPosition) @@ -342,14 +353,15 @@ extension SyntaxProtocol { public func lookup( for name: String?, at syntax: SyntaxProtocol, - with config: LookupConfig + with config: LookupConfig, + state: LookupState ) -> [LookupResult] { if body.position <= syntax.position && body.endPosition >= syntax.position { - var newConfig = config - newConfig.ignoreChildrenToSequentialParentIntroductionsFrom.append(self) - return lookupInParent(for: name, at: syntax, with: newConfig) + var newState = state + newState.skipSequentialIntroductionFrom = self + return lookupInParent(for: name, at: syntax, with: config, state: newState) } else { - return defaultLookupImplementation(for: name, at: syntax, with: config) + return defaultLookupImplementation(for: name, at: syntax, with: config, state: state) } } } diff --git a/Sources/SwiftLexicalLookup/ScopeSyntax.swift b/Sources/SwiftLexicalLookup/ScopeSyntax.swift index 9063c90aa86..0ca69795cc2 100644 --- a/Sources/SwiftLexicalLookup/ScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/ScopeSyntax.swift @@ -47,7 +47,7 @@ extension SyntaxProtocol { for name: String?, with config: LookupConfig = LookupConfig() ) -> [LookupResult] { - scope?.lookup(for: name, at: self, with: config) ?? [] + scope?.lookup(for: name, at: self, with: config, state: LookupState()) ?? [] } } @@ -58,7 +58,10 @@ extension SyntaxProtocol { var introducedNames: [LookupName] { get } /// Finds all declarations `name` refers to. `at` specifies the node lookup was triggered with. /// If `name` set to `nil`, returns all available names at the given node. - func lookup(for name: String?, at syntax: SyntaxProtocol, with config: LookupConfig) -> [LookupResult] + func lookup(for name: String?, + at syntax: SyntaxProtocol, + with config: LookupConfig, + state: LookupState) -> [LookupResult] } @_spi(Experimental) extension ScopeSyntax { @@ -72,9 +75,10 @@ extension SyntaxProtocol { public func lookup( for name: String?, at syntax: SyntaxProtocol, - with config: LookupConfig + with config: LookupConfig, + state: LookupState ) -> [LookupResult] { - defaultLookupImplementation(for: name, at: syntax, with: config) + defaultLookupImplementation(for: name, at: syntax, with: config, state: state) } /// Returns `LookupResult` of all names introduced in this scope that `name` @@ -83,7 +87,8 @@ extension SyntaxProtocol { func defaultLookupImplementation( for name: String?, at syntax: SyntaxProtocol, - with config: LookupConfig + with config: LookupConfig, + state: LookupState ) -> [LookupResult] { let filteredNames = introducedNames @@ -92,9 +97,9 @@ extension SyntaxProtocol { } if filteredNames.isEmpty { - return lookupInParent(for: name, at: syntax, with: config) + return lookupInParent(for: name, at: syntax, with: config, state: state) } else { - return [.fromScope(self, withNames: filteredNames)] + lookupInParent(for: name, at: syntax, with: config) + return [.fromScope(self, withNames: filteredNames)] + lookupInParent(for: name, at: syntax, with: config, state: state) } } @@ -102,9 +107,10 @@ extension SyntaxProtocol { func lookupInParent( for name: String?, at syntax: SyntaxProtocol, - with config: LookupConfig + with config: LookupConfig, + state: LookupState ) -> [LookupResult] { - parentScope?.lookup(for: name, at: syntax, with: config) ?? [] + parentScope?.lookup(for: name, at: syntax, with: config, state: state) ?? [] } func does(name: String?, referTo introducedName: LookupName, at syntax: SyntaxProtocol) -> Bool { diff --git a/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift index e5658ffecc2..3c1e3e300dc 100644 --- a/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift @@ -24,6 +24,7 @@ import SwiftSyntax for name: String?, at syntax: SyntaxProtocol, with config: LookupConfig, + state: LookupState, createResultsForThisScopeWith getResults: ([LookupName]) -> (LookupResult) ) -> [LookupResult] } @@ -34,6 +35,7 @@ import SwiftSyntax for name: String?, at syntax: SyntaxProtocol, with config: LookupConfig, + state: LookupState, createResultsForThisScopeWith getResults: ([LookupName]) -> (LookupResult) ) -> [LookupResult] { var result = [LookupResult]() @@ -43,23 +45,24 @@ import SwiftSyntax if let introducingToParentScope = Syntax(codeBlockItem.item).asProtocol(SyntaxProtocol.self) as? IntroducingToSequentialParentScopeSyntax { - guard - !config.ignoreChildrenToSequentialParentIntroductionsFrom.contains(where: { - $0.id == introducingToParentScope.id - }) - else { + // Check if the enocountered scope should be ignored. + if let scopeToSkip = state.skipSequentialIntroductionFrom, + scopeToSkip.id == introducingToParentScope.id { continue } + // If there are some names collected, create a new result for this scope. if !currentChunk.isEmpty { result.append(getResults(currentChunk)) currentChunk = [] } + // Add names introduced by the encountered scope. result.append( - contentsOf: introducingToParentScope.introducesToSequentialParent(for: name, at: syntax, with: config) + contentsOf: introducingToParentScope.introducesToSequentialParent(for: name, at: syntax, with: config, state: state) ) } else { + // Extract new names from encountered node. currentChunk.append( contentsOf: LookupName.getNames( @@ -72,11 +75,12 @@ import SwiftSyntax } } + // If there are some names collected, create a new result for this scope. if !currentChunk.isEmpty { result.append(getResults(currentChunk)) currentChunk = [] } - return (result.isEmpty ? [] : result.reversed()) + lookupInParent(for: name, at: syntax, with: config) + return (result.isEmpty ? [] : result.reversed()) + lookupInParent(for: name, at: syntax, with: config, state: state) } } From 93cb3ea1fbf9a0a8496ff8c32156a72a66232e23 Mon Sep 17 00:00:00 2001 From: Jakub Florek <63607600+MAJKFL@users.noreply.github.com> Date: Tue, 23 Jul 2024 10:15:39 +0200 Subject: [PATCH 05/27] Create a new enum representing all possible implicit name cases. Simplify LookupName. --- Sources/SwiftLexicalLookup/LookupName.swift | 73 ++++++++++++------- .../ScopeImplementations.swift | 4 +- .../SwiftLexicalLookup/TypeScopeSyntax.swift | 2 +- .../SwiftLexicalLookupTest/ExpectedName.swift | 56 +++++++++----- .../NameLookupTests.swift | 26 +++---- 5 files changed, 98 insertions(+), 63 deletions(-) diff --git a/Sources/SwiftLexicalLookup/LookupName.swift b/Sources/SwiftLexicalLookup/LookupName.swift index 0767ad716a7..18d58e83218 100644 --- a/Sources/SwiftLexicalLookup/LookupName.swift +++ b/Sources/SwiftLexicalLookup/LookupName.swift @@ -12,39 +12,25 @@ import SwiftSyntax -@_spi(Experimental) public enum LookupName { - /// Identifier associated with the name. - /// Could be an identifier of a variable, function or closure parameter and more - case identifier(IdentifiableSyntax, accessibleAfter: AbsolutePosition?) - /// Declaration associated with the name. - /// Could be class, struct, actor, protocol, function and more - case declaration(NamedDeclSyntax, accessibleAfter: AbsolutePosition?) +@_spi(Experimental) public enum LookupImplicitNameKind { /// `self` keyword representing object instance. - case `self`(DeclSyntaxProtocol) + case `self`(SyntaxProtocol) /// `Self` keyword representing object type. case `Self`(DeclSyntaxProtocol) /// `self` captured by a closure. - case selfCaptured(ClosureCaptureSyntax) - /// `error` available inside `catch` clause. case error(CatchClauseSyntax) /// `newValue` available by default inside `set` and `willSet`. case newValue(AccessorDeclSyntax) /// `oldValue` available by default inside `didSet`. case oldValue(AccessorDeclSyntax) - + /// Syntax associated with this name. @_spi(Experimental) public var syntax: SyntaxProtocol { switch self { - case .identifier(let syntax, _): - syntax - case .declaration(let syntax, _): - syntax case .self(let syntax): syntax case .Self(let syntax): syntax - case .selfCaptured(let syntax): - syntax case .error(let syntax): syntax case .newValue(let syntax): @@ -53,6 +39,45 @@ import SwiftSyntax syntax } } + + /// Used for name comparison. + var name: String { + switch self { + case .self: + "self" + case .Self: + "Self" + case .error: + "error" + case .newValue: + "newValue" + case .oldValue: + "oldValue" + } + } +} + +@_spi(Experimental) public enum LookupName { + /// Identifier associated with the name. + /// Could be an identifier of a variable, function or closure parameter and more. + case identifier(IdentifiableSyntax, accessibleAfter: AbsolutePosition?) + /// Declaration associated with the name. + /// Could be class, struct, actor, protocol, function and more. + case declaration(NamedDeclSyntax, accessibleAfter: AbsolutePosition?) + /// Name introduced implicitly certain syntax nodes. + case implicit(LookupImplicitNameKind) + + /// Syntax associated with this name. + @_spi(Experimental) public var syntax: SyntaxProtocol { + switch self { + case .identifier(let syntax, _): + syntax + case .declaration(let syntax, _): + syntax + case .implicit(let implicitName): + implicitName.syntax + } + } /// Introduced name. @_spi(Experimental) public var identifier: Identifier? { @@ -82,16 +107,8 @@ import SwiftSyntax switch self { case .identifier, .declaration: identifier?.name - case .self, .selfCaptured: - "self" - case .Self: - "Self" - case .error: - "error" - case .newValue: - "newValue" - case .oldValue: - "oldValue" + case .implicit(let implicitName): + implicitName.name } } @@ -153,7 +170,7 @@ import SwiftSyntax if let closureCapture = identifiable as? ClosureCaptureSyntax, closureCapture.identifier.tokenKind == .keyword(.self) { - return [.selfCaptured(closureCapture)] // Handle `self` closure capture. + return [.implicit(.self(closureCapture))] // Handle `self` closure capture. } else if identifiable.identifier.text != "_" { return [.identifier(identifiable, accessibleAfter: accessibleAfter)] } else { diff --git a/Sources/SwiftLexicalLookup/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/ScopeImplementations.swift index 169755e1850..899ab494ec9 100644 --- a/Sources/SwiftLexicalLookup/ScopeImplementations.swift +++ b/Sources/SwiftLexicalLookup/ScopeImplementations.swift @@ -379,9 +379,9 @@ extension SyntaxProtocol { } else { switch accessorSpecifier.tokenKind { case .keyword(.set), .keyword(.willSet): - [.newValue(self)] + [.implicit(.newValue(self))] case .keyword(.didSet): - [.oldValue(self)] + [.implicit(.oldValue(self))] default: [] } diff --git a/Sources/SwiftLexicalLookup/TypeScopeSyntax.swift b/Sources/SwiftLexicalLookup/TypeScopeSyntax.swift index 617410a1446..e81408bdf54 100644 --- a/Sources/SwiftLexicalLookup/TypeScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/TypeScopeSyntax.swift @@ -18,7 +18,7 @@ import SwiftSyntax @_spi(Experimental) extension TypeScopeSyntax { public var implicitInstanceAndTypeNames: [LookupName] { - [.self(self), .Self(self)] + [.implicit(.self(self)), .implicit(.Self(self))] } public var introducedNames: [LookupName] { diff --git a/Tests/SwiftLexicalLookupTest/ExpectedName.swift b/Tests/SwiftLexicalLookupTest/ExpectedName.swift index ce5e8a0db57..a169486e8d0 100644 --- a/Tests/SwiftLexicalLookupTest/ExpectedName.swift +++ b/Tests/SwiftLexicalLookupTest/ExpectedName.swift @@ -25,29 +25,51 @@ extension String: ExpectedName { } } +enum ImplicitNameExpectation { + case `self`(String) + case `Self`(String) + case error(String) + case newValue(String) + case oldValue(String) + + func assertExpectation(marker: String, for name: LookupImplicitNameKind) { + switch (name, self) { + case (.self, .self): break + case (.Self, .Self): break + case (.error, .error): break + case (.newValue, .newValue): break + case (.oldValue, .oldValue): break + default: + XCTFail("For marker \(marker), actual name kind \(name) doesn't match expected \(self)") + } + } + + var marker: String { + switch self { + case .self(let marker), + .Self(let marker), + .error(let marker), + .newValue(let marker), + .oldValue(let marker): + marker + } + } +} + /// Can be used to optionally assert /// exact lookup name kind. enum NameExpectation: ExpectedName { case identifier(String) case declaration(String) - case selfInstance(String) - case selfType(String) - case selfCaptured(String) - case error(String) - case newValue(String) - case oldValue(String) + case implicit(ImplicitNameExpectation) var marker: String { switch self { case .identifier(let marker), - .declaration(let marker), - .selfInstance(let marker), - .selfType(let marker), - .selfCaptured(let marker), - .error(let marker), - .newValue(let marker), - .oldValue(let marker): + .declaration(let marker): marker + case .implicit(let implicitName): + implicitName.marker } } @@ -55,12 +77,8 @@ enum NameExpectation: ExpectedName { switch (name, self) { case (.identifier, .identifier): break case (.declaration, .declaration): break - case (.self, .selfInstance): break - case (.Self, .selfType): break - case (.selfCaptured, .selfCaptured): break - case (.error, .error): break - case (.newValue, .newValue): break - case (.oldValue, .oldValue): break + case (.implicit(let implicitName), .implicit(let implicitNameExpectation)): + implicitNameExpectation.assertExpectation(marker: marker, for: implicitName) default: XCTFail("For marker \(marker), actual name kind \(name) doesn't match expected \(self)") } diff --git a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift index 2b4d47b830b..22e762c3f22 100644 --- a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift +++ b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift @@ -150,8 +150,8 @@ final class testNameLookup: XCTestCase { """, references: [ "5️⃣": [ - .fromScope(ClosureExprSyntax.self, expectedNames: [NameExpectation.selfCaptured("2️⃣")]), - .fromScope(ClassDeclSyntax.self, expectedNames: [NameExpectation.selfInstance("7️⃣")]), + .fromScope(ClosureExprSyntax.self, expectedNames: [NameExpectation.implicit(.self("2️⃣"))]), + .fromScope(ClassDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("7️⃣"))]), ], "6️⃣": [ .fromScope(ClosureExprSyntax.self, expectedNames: ["3️⃣"]), @@ -412,7 +412,7 @@ final class testNameLookup: XCTestCase { .fromScope(MemberBlockSyntax.self, expectedNames: ["1️⃣", "2️⃣", "3️⃣"]), .fromScope( ClassDeclSyntax.self, - expectedNames: [NameExpectation.selfInstance("🔟"), NameExpectation.selfType("🔟")] + expectedNames: [NameExpectation.implicit(.self("🔟")), NameExpectation.implicit(.Self("🔟"))] ), .fromFileScope(expectedNames: ["🔟"]), ], @@ -423,7 +423,7 @@ final class testNameLookup: XCTestCase { .fromScope(MemberBlockSyntax.self, expectedNames: ["1️⃣", "2️⃣", "3️⃣"]), .fromScope( ClassDeclSyntax.self, - expectedNames: [NameExpectation.selfInstance("🔟"), NameExpectation.selfType("🔟")] + expectedNames: [NameExpectation.implicit(.self("🔟")), NameExpectation.implicit(.Self("🔟"))] ), .fromFileScope(expectedNames: ["🔟"]), ], @@ -651,15 +651,15 @@ final class testNameLookup: XCTestCase { """, references: [ "3️⃣": [ - .fromScope(StructDeclSyntax.self, expectedNames: [NameExpectation.selfType("2️⃣")]), - .fromScope(ExtensionDeclSyntax.self, expectedNames: [NameExpectation.selfType("1️⃣")]), + .fromScope(StructDeclSyntax.self, expectedNames: [NameExpectation.implicit(.Self("2️⃣"))]), + .fromScope(ExtensionDeclSyntax.self, expectedNames: [NameExpectation.implicit(.Self("1️⃣"))]), ], "4️⃣": [ - .fromScope(StructDeclSyntax.self, expectedNames: [NameExpectation.selfInstance("2️⃣")]), - .fromScope(ExtensionDeclSyntax.self, expectedNames: [NameExpectation.selfInstance("1️⃣")]), + .fromScope(StructDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("2️⃣"))]), + .fromScope(ExtensionDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("1️⃣"))]), ], - "5️⃣": [.fromScope(ExtensionDeclSyntax.self, expectedNames: [NameExpectation.selfType("1️⃣")])], - "6️⃣": [.fromScope(ExtensionDeclSyntax.self, expectedNames: [NameExpectation.selfInstance("1️⃣")])], + "5️⃣": [.fromScope(ExtensionDeclSyntax.self, expectedNames: [NameExpectation.implicit(.Self("1️⃣"))])], + "6️⃣": [.fromScope(ExtensionDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("1️⃣"))])], ] ) } @@ -691,10 +691,10 @@ final class testNameLookup: XCTestCase { } """, references: [ - "2️⃣": [.fromScope(AccessorDeclSyntax.self, expectedNames: [NameExpectation.newValue("1️⃣")])], + "2️⃣": [.fromScope(AccessorDeclSyntax.self, expectedNames: [NameExpectation.implicit(.newValue("1️⃣"))])], "4️⃣": [.fromScope(AccessorDeclSyntax.self, expectedNames: [NameExpectation.identifier("3️⃣")])], - "6️⃣": [.fromScope(AccessorDeclSyntax.self, expectedNames: [NameExpectation.newValue("5️⃣")])], - "8️⃣": [.fromScope(AccessorDeclSyntax.self, expectedNames: [NameExpectation.oldValue("7️⃣")])], + "6️⃣": [.fromScope(AccessorDeclSyntax.self, expectedNames: [NameExpectation.implicit(.newValue("5️⃣"))])], + "8️⃣": [.fromScope(AccessorDeclSyntax.self, expectedNames: [NameExpectation.implicit(.oldValue("7️⃣"))])], ] ) } From 9945bf0e411db70da5e64e3fe61615edd2a42c76 Mon Sep 17 00:00:00 2001 From: Jakub Florek <63607600+MAJKFL@users.noreply.github.com> Date: Tue, 23 Jul 2024 11:46:55 +0200 Subject: [PATCH 06/27] Add suggested changes from the previous PR. --- .../Configurations/LookupConfig.swift | 4 +- .../IdentifiableSyntax.swift | 12 ++-- Sources/SwiftLexicalLookup/LookupName.swift | 20 +++++-- Sources/SwiftLexicalLookup/LookupResult.swift | 2 +- Sources/SwiftLexicalLookup/LookupState.swift | 2 +- .../ScopeImplementations.swift | 56 +++++++++---------- Sources/SwiftLexicalLookup/ScopeSyntax.swift | 12 ++-- .../SequentialScopeSyntax.swift | 2 +- .../SwiftLexicalLookup/TypeScopeSyntax.swift | 4 +- Tests/SwiftLexicalLookupTest/Assertions.swift | 18 +++--- .../NameLookupTests.swift | 5 +- 11 files changed, 70 insertions(+), 67 deletions(-) diff --git a/Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift b/Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift index c748bf0bdd7..a6442bb0e18 100644 --- a/Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift +++ b/Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift @@ -15,9 +15,9 @@ import Foundation @_spi(Experimental) public struct LookupConfig { /// Specifies behaviour of file scope. /// `memberBlockUpToLastDecl` by default. - public var fileScopeHandling: FileScopeHandlingConfig + @_spi(Experimental) public var fileScopeHandling: FileScopeHandlingConfig - public init( + @_spi(Experimental) public init( fileScopeHandling: FileScopeHandlingConfig = .memberBlockUpToLastDecl ) { self.fileScopeHandling = fileScopeHandling diff --git a/Sources/SwiftLexicalLookup/IdentifiableSyntax.swift b/Sources/SwiftLexicalLookup/IdentifiableSyntax.swift index d7f47ab98d6..033dad3244b 100644 --- a/Sources/SwiftLexicalLookup/IdentifiableSyntax.swift +++ b/Sources/SwiftLexicalLookup/IdentifiableSyntax.swift @@ -13,25 +13,25 @@ import SwiftSyntax /// Syntax node that can be refered to with an identifier. -public protocol IdentifiableSyntax: SyntaxProtocol { +@_spi(Experimental) public protocol IdentifiableSyntax: SyntaxProtocol { var identifier: TokenSyntax { get } } -extension IdentifierPatternSyntax: IdentifiableSyntax {} +@_spi(Experimental) extension IdentifierPatternSyntax: IdentifiableSyntax {} -extension ClosureParameterSyntax: IdentifiableSyntax { +@_spi(Experimental) extension ClosureParameterSyntax: IdentifiableSyntax { @_spi(Experimental) public var identifier: TokenSyntax { secondName ?? firstName } } -extension ClosureShorthandParameterSyntax: IdentifiableSyntax { +@_spi(Experimental) extension ClosureShorthandParameterSyntax: IdentifiableSyntax { @_spi(Experimental) public var identifier: TokenSyntax { name } } -extension ClosureCaptureSyntax: IdentifiableSyntax { +@_spi(Experimental) extension ClosureCaptureSyntax: IdentifiableSyntax { @_spi(Experimental) public var identifier: TokenSyntax { /* Doesn't work with closures like: _ = { [y=1+2] in @@ -42,7 +42,7 @@ extension ClosureCaptureSyntax: IdentifiableSyntax { } } -extension AccessorParametersSyntax: IdentifiableSyntax { +@_spi(Experimental) extension AccessorParametersSyntax: IdentifiableSyntax { @_spi(Experimental) public var identifier: TokenSyntax { name } diff --git a/Sources/SwiftLexicalLookup/LookupName.swift b/Sources/SwiftLexicalLookup/LookupName.swift index 18d58e83218..381e3b31e31 100644 --- a/Sources/SwiftLexicalLookup/LookupName.swift +++ b/Sources/SwiftLexicalLookup/LookupName.swift @@ -124,8 +124,11 @@ import SwiftSyntax return name == lookedUpName } - /// Extracts names introduced by the given `from` structure. - static func getNames(from syntax: SyntaxProtocol, accessibleAfter: AbsolutePosition? = nil) -> [LookupName] { + /// Extracts names introduced by the given `syntax` structure. + static func getNames( + from syntax: SyntaxProtocol, + accessibleAfter: AbsolutePosition? = nil + ) -> [LookupName] { switch Syntax(syntax).as(SyntaxEnum.self) { case .variableDecl(let variableDecl): variableDecl.bindings.flatMap { binding in @@ -165,13 +168,15 @@ import SwiftSyntax } /// Extracts name introduced by `IdentifiableSyntax` node. - private static func handle(identifiable: IdentifiableSyntax, accessibleAfter: AbsolutePosition? = nil) -> [LookupName] - { + private static func handle( + identifiable: IdentifiableSyntax, + accessibleAfter: AbsolutePosition? = nil + ) -> [LookupName] { if let closureCapture = identifiable as? ClosureCaptureSyntax, closureCapture.identifier.tokenKind == .keyword(.self) { return [.implicit(.self(closureCapture))] // Handle `self` closure capture. - } else if identifiable.identifier.text != "_" { + } else if identifiable.identifier.tokenKind != .wildcard { return [.identifier(identifiable, accessibleAfter: accessibleAfter)] } else { return [] @@ -179,7 +184,10 @@ import SwiftSyntax } /// Extracts name introduced by `NamedDeclSyntax` node. - private static func handle(namedDecl: NamedDeclSyntax, accessibleAfter: AbsolutePosition? = nil) -> [LookupName] { + private static func handle( + namedDecl: NamedDeclSyntax, + accessibleAfter: AbsolutePosition? = nil + ) -> [LookupName] { [.declaration(namedDecl, accessibleAfter: accessibleAfter)] } } diff --git a/Sources/SwiftLexicalLookup/LookupResult.swift b/Sources/SwiftLexicalLookup/LookupResult.swift index 4e5cc04f97b..6fabf2060dc 100644 --- a/Sources/SwiftLexicalLookup/LookupResult.swift +++ b/Sources/SwiftLexicalLookup/LookupResult.swift @@ -12,7 +12,7 @@ import SwiftSyntax -/// Represents resul +/// Represents result from a specific scope. @_spi(Experimental) public enum LookupResult { /// Scope and the names that matched lookup. case fromScope(ScopeSyntax, withNames: [LookupName]) diff --git a/Sources/SwiftLexicalLookup/LookupState.swift b/Sources/SwiftLexicalLookup/LookupState.swift index 7bf27595dc5..2874a2f0a02 100644 --- a/Sources/SwiftLexicalLookup/LookupState.swift +++ b/Sources/SwiftLexicalLookup/LookupState.swift @@ -15,7 +15,7 @@ import Foundation @_spi(Experimental) public struct LookupState { /// Specifies scopes that introduce names to their parent and /// should be skipped during lookup in sequential scopes. - var skipSequentialIntroductionFrom: IntroducingToSequentialParentScopeSyntax? + @_spi(Experimental) public var skipSequentialIntroductionFrom: IntroducingToSequentialParentScopeSyntax? @_spi(Experimental) public init() {} } diff --git a/Sources/SwiftLexicalLookup/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/ScopeImplementations.swift index 899ab494ec9..be94dede1a7 100644 --- a/Sources/SwiftLexicalLookup/ScopeImplementations.swift +++ b/Sources/SwiftLexicalLookup/ScopeImplementations.swift @@ -12,9 +12,9 @@ import SwiftSyntax -extension SyntaxProtocol { +@_spi(Experimental) extension SyntaxProtocol { /// Parent scope of this syntax node, or scope introduced by this syntax node. - var scope: ScopeSyntax? { + @_spi(Experimental) public var scope: ScopeSyntax? { if let scopeSyntax = Syntax(self).asProtocol(SyntaxProtocol.self) as? ScopeSyntax { scopeSyntax } else { @@ -26,7 +26,7 @@ extension SyntaxProtocol { @_spi(Experimental) extension SourceFileSyntax: SequentialScopeSyntax { /// All names introduced in the file scope /// according to the default strategy: `memberBlockUpToLastDecl`. - public var introducedNames: [LookupName] { + @_spi(Experimental) public var introducedNames: [LookupName] { introducedNames(using: .memberBlockUpToLastDecl) } @@ -47,10 +47,10 @@ extension SyntaxProtocol { /// ``` /// During lookup, according to different configurations, /// names available at the marked place are: - /// - for `fileScopeNameIntroductionStrategy` - a, b, c, d + /// - for `memberBlockUpToLastDecl` - a, b, c, d /// - for `memberBlock` - a, b, c, d, e, f /// - for `codeBlock` - a - public func introducedNames(using fileScopeHandling: FileScopeHandlingConfig) -> [LookupName] { + @_spi(Experimental) public func introducedNames(using fileScopeHandling: FileScopeHandlingConfig) -> [LookupName] { switch fileScopeHandling { case .memberBlockUpToLastDecl: var encounteredNonDeclaration = false @@ -61,7 +61,7 @@ extension SyntaxProtocol { if encounteredNonDeclaration { return LookupName.getNames(from: item, accessibleAfter: codeBlockItem.endPosition) } else { - if item.is(DeclSyntax.self) || item.is(VariableDeclSyntax.self) { + if item.is(DeclSyntax.self) { return LookupName.getNames(from: item) } else { encounteredNonDeclaration = true @@ -97,10 +97,10 @@ extension SyntaxProtocol { /// ``` /// According to different configurations, /// names available at the marked place are: - /// - for `fileScopeNameIntroductionStrategy` - a, b, c, d + /// - for `memberBlockUpToLastDecl` - a, b, c, d /// - for `memberBlock` - a, b, c, d, e, f /// - for `codeBlock` - a - public func lookup( + @_spi(Experimental) public func lookup( for name: String?, at syntax: SyntaxProtocol, with config: LookupConfig, @@ -162,13 +162,13 @@ extension SyntaxProtocol { @_spi(Experimental) extension CodeBlockSyntax: SequentialScopeSyntax { /// Names introduced in the code block scope /// accessible after their declaration. - public var introducedNames: [LookupName] { + @_spi(Experimental) public var introducedNames: [LookupName] { statements.flatMap { codeBlockItem in LookupName.getNames(from: codeBlockItem.item, accessibleAfter: codeBlockItem.endPosition) } } - public func lookup( + @_spi(Experimental) public func lookup( for name: String?, at syntax: SyntaxProtocol, with config: LookupConfig, @@ -187,7 +187,7 @@ extension SyntaxProtocol { @_spi(Experimental) extension ForStmtSyntax: ScopeSyntax { /// Names introduced in the `for` body. - public var introducedNames: [LookupName] { + @_spi(Experimental) public var introducedNames: [LookupName] { LookupName.getNames(from: pattern) } } @@ -204,26 +204,20 @@ extension SyntaxProtocol { /// ``` /// During lookup, names available at the marked place are: /// `self`, a, b. - public var introducedNames: [LookupName] { + @_spi(Experimental) public var introducedNames: [LookupName] { let captureNames = - signature?.capture?.children(viewMode: .sourceAccurate).flatMap { child in - if let captureList = child.as(ClosureCaptureListSyntax.self) { - captureList.children(viewMode: .sourceAccurate).flatMap { capture in - LookupName.getNames(from: capture) - } - } else { - LookupName.getNames(from: child) - } + signature?.capture?.items.flatMap { element in + LookupName.getNames(from: element) } ?? [] let parameterNames = signature?.parameterClause?.children(viewMode: .sourceAccurate).flatMap { parameter in if let parameterList = parameter.as(ClosureParameterListSyntax.self) { - parameterList.children(viewMode: .sourceAccurate).flatMap { parameter in + return parameterList.children(viewMode: .sourceAccurate).flatMap { parameter in LookupName.getNames(from: parameter) } } else { - LookupName.getNames(from: parameter) + return LookupName.getNames(from: parameter) } } ?? [] @@ -233,7 +227,7 @@ extension SyntaxProtocol { @_spi(Experimental) extension WhileStmtSyntax: ScopeSyntax { /// Names introduced by the `while` loop by its conditions. - public var introducedNames: [LookupName] { + @_spi(Experimental) public var introducedNames: [LookupName] { conditions.flatMap { element in LookupName.getNames(from: element.condition) } @@ -242,7 +236,7 @@ extension SyntaxProtocol { @_spi(Experimental) extension IfExprSyntax: ScopeSyntax { /// Parent scope, omitting ancestor `if` statements if part of their `else if` clause. - public var parentScope: ScopeSyntax? { + @_spi(Experimental) public var parentScope: ScopeSyntax? { getParent(for: self.parent, previousIfElse: self.elseKeyword == nil) } @@ -278,7 +272,7 @@ extension SyntaxProtocol { } /// Names introduced by the `if` optional binding conditions. - public var introducedNames: [LookupName] { + @_spi(Experimental) public var introducedNames: [LookupName] { conditions.flatMap { element in LookupName.getNames(from: element.condition, accessibleAfter: element.endPosition) } @@ -296,7 +290,7 @@ extension SyntaxProtocol { /// // <-- a is not visible here /// } /// ``` - public func lookup( + @_spi(Experimental) public func lookup( for name: String?, at syntax: SyntaxProtocol, with config: LookupConfig, @@ -312,7 +306,7 @@ extension SyntaxProtocol { @_spi(Experimental) extension MemberBlockSyntax: ScopeSyntax { /// All names introduced by members of this member scope. - public var introducedNames: [LookupName] { + @_spi(Experimental) public var introducedNames: [LookupName] { members.flatMap { member in LookupName.getNames(from: member.decl) } @@ -320,7 +314,7 @@ extension SyntaxProtocol { } @_spi(Experimental) extension GuardStmtSyntax: IntroducingToSequentialParentScopeSyntax { - public func introducesToSequentialParent( + @_spi(Experimental) public func introducesToSequentialParent( for name: String?, at syntax: SwiftSyntax.SyntaxProtocol, with config: LookupConfig, @@ -335,7 +329,7 @@ extension SyntaxProtocol { return names.isEmpty ? [] : [.fromScope(self, withNames: names)] } - public var introducedNames: [LookupName] { + @_spi(Experimental) public var introducedNames: [LookupName] { [] } @@ -350,7 +344,7 @@ extension SyntaxProtocol { /// } /// // a is visible here /// ``` - public func lookup( + @_spi(Experimental) public func lookup( for name: String?, at syntax: SyntaxProtocol, with config: LookupConfig, @@ -373,7 +367,7 @@ extension SyntaxProtocol { @_spi(Experimental) extension ExtensionDeclSyntax: TypeScopeSyntax {} @_spi(Experimental) extension AccessorDeclSyntax: ScopeSyntax { - public var introducedNames: [LookupName] { + @_spi(Experimental) public var introducedNames: [LookupName] { if let parameters { LookupName.getNames(from: parameters) } else { diff --git a/Sources/SwiftLexicalLookup/ScopeSyntax.swift b/Sources/SwiftLexicalLookup/ScopeSyntax.swift index 0ca69795cc2..2f8f8cdc2ef 100644 --- a/Sources/SwiftLexicalLookup/ScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/ScopeSyntax.swift @@ -14,10 +14,10 @@ import SwiftSyntax extension SyntaxProtocol { /// Returns all names that `for` refers to at this syntax node. - /// Optional configuration can be passed as `with` to customize the lookup behavior. + /// Optional configuration can be passed as `config` to customize the lookup behavior. /// - /// - Returns: An array of `LookupResult` for name `for` at this syntax node, - /// ordered by visibility. If set to `nil`, returns all available names ordered by visibility. + /// - Returns: An array of `LookupResult` for `name` at this syntax node, + /// ordered by visibility. If `name` is set to `nil`, returns all available names ordered by visibility. /// The order is from the innermost to the outermost scope, /// and within each result, names are ordered by their introduction /// in the source code. @@ -56,7 +56,7 @@ extension SyntaxProtocol { var parentScope: ScopeSyntax? { get } /// Names found in this scope. Ordered from first to last introduced. var introducedNames: [LookupName] { get } - /// Finds all declarations `name` refers to. `at` specifies the node lookup was triggered with. + /// Finds all declarations `name` refers to. `syntax` specifies the node lookup was triggered with. /// If `name` set to `nil`, returns all available names at the given node. func lookup(for name: String?, at syntax: SyntaxProtocol, @@ -65,14 +65,14 @@ extension SyntaxProtocol { } @_spi(Experimental) extension ScopeSyntax { - public var parentScope: ScopeSyntax? { + @_spi(Experimental) public var parentScope: ScopeSyntax? { self.parent?.scope } /// Returns `LookupResult` of all names introduced in this scope that `name` /// refers to and is accessible at given syntax node then passes lookup to the parent. /// If `name` set to `nil`, returns all available names at the given node. - public func lookup( + @_spi(Experimental) public func lookup( for name: String?, at syntax: SyntaxProtocol, with config: LookupConfig, diff --git a/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift index 3c1e3e300dc..ed646cf1c48 100644 --- a/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift @@ -30,7 +30,7 @@ import SwiftSyntax } @_spi(Experimental) extension SequentialScopeSyntax { - public func sequentialLookup( + @_spi(Experimental) public func sequentialLookup( in codeBlockItems: any Collection, for name: String?, at syntax: SyntaxProtocol, diff --git a/Sources/SwiftLexicalLookup/TypeScopeSyntax.swift b/Sources/SwiftLexicalLookup/TypeScopeSyntax.swift index e81408bdf54..4bcbe894a1a 100644 --- a/Sources/SwiftLexicalLookup/TypeScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/TypeScopeSyntax.swift @@ -17,11 +17,11 @@ import SwiftSyntax } @_spi(Experimental) extension TypeScopeSyntax { - public var implicitInstanceAndTypeNames: [LookupName] { + @_spi(Experimental) public var implicitInstanceAndTypeNames: [LookupName] { [.implicit(.self(self)), .implicit(.Self(self))] } - public var introducedNames: [LookupName] { + @_spi(Experimental) public var introducedNames: [LookupName] { implicitInstanceAndTypeNames } } diff --git a/Tests/SwiftLexicalLookupTest/Assertions.swift b/Tests/SwiftLexicalLookupTest/Assertions.swift index 18ca4a69a4f..5562512cc70 100644 --- a/Tests/SwiftLexicalLookupTest/Assertions.swift +++ b/Tests/SwiftLexicalLookupTest/Assertions.swift @@ -21,7 +21,7 @@ import _SwiftSyntaxTestSupport /// It also checks whether result types match rules specified in `expectedResultTypes`. func assertLexicalScopeQuery( source: String, - methodUnderTest: (String, TokenSyntax) -> ([SyntaxProtocol?]), + methodUnderTest: (_ marker: String, _ tokenAtMarker: TokenSyntax) -> ([SyntaxProtocol?]), expected: [String: [String?]], expectedResultTypes: MarkerExpectation = .none ) { @@ -66,27 +66,27 @@ func assertLexicalScopeQuery( } // Assert validity of the output - for (actual, expected) in zip(result, zip(expectedMarkers, expectedPositions)) { - if actual == nil && expected.1 == nil { continue } + for (actual, (expectedMarker, expectedPosition)) in zip(result, zip(expectedMarkers, expectedPositions)) { + if actual == nil && expectedPosition == nil { continue } guard let actual else { XCTFail( - "For marker \(marker), actual is nil while expected is \(sourceFileSyntax.token(at: expected.1!)?.description ?? "nil")" + "For marker \(marker), actual is nil while expected is \(sourceFileSyntax.token(at: expectedPosition!)?.description ?? "nil")" ) continue } - guard let expectedPosition = expected.1 else { + guard let expectedPosition else { XCTFail("For marker \(marker), actual is \(actual) while expected position is nil") continue } XCTAssert( actual.positionAfterSkippingLeadingTrivia == expectedPosition, - "For marker \(marker), actual result: \(actual) doesn't match expected value: \(sourceFileSyntax.token(at: expected.1!)?.description ?? "nil")" + "For marker \(marker), actual result: \(actual) doesn't match expected value: \(sourceFileSyntax.token(at: expectedPosition)?.description ?? "nil")" ) - if let expectedMarker = expected.0 { + if let expectedMarker { expectedResultTypes.assertMarkerType(marker: expectedMarker, actual: actual) } } @@ -105,8 +105,8 @@ func assertLexicalNameLookup( ) { assertLexicalScopeQuery( source: source, - methodUnderTest: { marker, argument in - let result = argument.lookup(for: useNilAsTheParameter ? nil : argument.text, with: config) + methodUnderTest: { marker, tokenAtMarker in + let result = tokenAtMarker.lookup(for: useNilAsTheParameter ? nil : tokenAtMarker.text, with: config) guard let expectedValues = references[marker] else { XCTFail("For marker \(marker), couldn't find result expectation") diff --git a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift index 22e762c3f22..4eaee393bee 100644 --- a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift +++ b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift @@ -73,8 +73,8 @@ final class testNameLookup: XCTestCase { source: """ for 1️⃣i in 1..<4 { let (a, b) = (2️⃣i, 3️⃣j) - for (4️⃣i, 5️⃣j) in foo { - let (c, d) = (6️⃣i, 7️⃣j) + for (4️⃣i, (5️⃣j, 8️⃣k)) in foo { + let (c, d, e) = (6️⃣i, 7️⃣j, 9️⃣k) } } """, @@ -86,6 +86,7 @@ final class testNameLookup: XCTestCase { .fromScope(ForStmtSyntax.self, expectedNames: ["1️⃣"]), ], "7️⃣": [.fromScope(ForStmtSyntax.self, expectedNames: ["5️⃣"])], + "9️⃣": [.fromScope(ForStmtSyntax.self, expectedNames: ["8️⃣"])], ], expectedResultTypes: .all(IdentifierPatternSyntax.self) ) From 6759b05a3d98e2b8ce23427efa5df738f81a2989 Mon Sep 17 00:00:00 2001 From: Jakub Florek <63607600+MAJKFL@users.noreply.github.com> Date: Tue, 23 Jul 2024 12:12:09 +0200 Subject: [PATCH 07/27] Make `IntroducingToSequentialParentScopeSyntax` and `SequentialScope` internal. Format. --- ...ntroducingToSequentialParentScopeSyntax.swift | 2 +- Sources/SwiftLexicalLookup/LookupName.swift | 4 ++-- Sources/SwiftLexicalLookup/LookupState.swift | 4 ++-- .../ScopeImplementations.swift | 4 +++- Sources/SwiftLexicalLookup/ScopeSyntax.swift | 13 ++++++++----- .../SequentialScopeSyntax.swift | 16 +++++++++++----- Sources/SwiftLexicalLookup/TypeScopeSyntax.swift | 2 ++ Tests/SwiftLexicalLookupTest/ExpectedName.swift | 6 +++--- 8 files changed, 32 insertions(+), 19 deletions(-) diff --git a/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift b/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift index 185906c5d35..9be86f972ba 100644 --- a/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift @@ -12,7 +12,7 @@ import SwiftSyntax -@_spi(Experimental) public protocol IntroducingToSequentialParentScopeSyntax: ScopeSyntax { +protocol IntroducingToSequentialParentScopeSyntax: ScopeSyntax { /// Returns names matching lookup that should be /// handled by it's parent sequential scope. func introducesToSequentialParent( diff --git a/Sources/SwiftLexicalLookup/LookupName.swift b/Sources/SwiftLexicalLookup/LookupName.swift index 381e3b31e31..4e0e6d457a5 100644 --- a/Sources/SwiftLexicalLookup/LookupName.swift +++ b/Sources/SwiftLexicalLookup/LookupName.swift @@ -23,7 +23,7 @@ import SwiftSyntax case newValue(AccessorDeclSyntax) /// `oldValue` available by default inside `didSet`. case oldValue(AccessorDeclSyntax) - + /// Syntax associated with this name. @_spi(Experimental) public var syntax: SyntaxProtocol { switch self { @@ -39,7 +39,7 @@ import SwiftSyntax syntax } } - + /// Used for name comparison. var name: String { switch self { diff --git a/Sources/SwiftLexicalLookup/LookupState.swift b/Sources/SwiftLexicalLookup/LookupState.swift index 2874a2f0a02..b94b7862656 100644 --- a/Sources/SwiftLexicalLookup/LookupState.swift +++ b/Sources/SwiftLexicalLookup/LookupState.swift @@ -15,7 +15,7 @@ import Foundation @_spi(Experimental) public struct LookupState { /// Specifies scopes that introduce names to their parent and /// should be skipped during lookup in sequential scopes. - @_spi(Experimental) public var skipSequentialIntroductionFrom: IntroducingToSequentialParentScopeSyntax? - + var skipSequentialIntroductionFrom: IntroducingToSequentialParentScopeSyntax? + @_spi(Experimental) public init() {} } diff --git a/Sources/SwiftLexicalLookup/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/ScopeImplementations.swift index be94dede1a7..1b203272366 100644 --- a/Sources/SwiftLexicalLookup/ScopeImplementations.swift +++ b/Sources/SwiftLexicalLookup/ScopeImplementations.swift @@ -185,7 +185,7 @@ import SwiftSyntax } } -@_spi(Experimental) extension ForStmtSyntax: ScopeSyntax { +@_spi(Experimental) extension ForStmtSyntax: ScopeSyntax { /// Names introduced in the `for` body. @_spi(Experimental) public var introducedNames: [LookupName] { LookupName.getNames(from: pattern) @@ -367,6 +367,8 @@ import SwiftSyntax @_spi(Experimental) extension ExtensionDeclSyntax: TypeScopeSyntax {} @_spi(Experimental) extension AccessorDeclSyntax: ScopeSyntax { + /// Implicit and/or explicit names introduced + /// withing the accessor.. @_spi(Experimental) public var introducedNames: [LookupName] { if let parameters { LookupName.getNames(from: parameters) diff --git a/Sources/SwiftLexicalLookup/ScopeSyntax.swift b/Sources/SwiftLexicalLookup/ScopeSyntax.swift index 2f8f8cdc2ef..8358378a446 100644 --- a/Sources/SwiftLexicalLookup/ScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/ScopeSyntax.swift @@ -58,10 +58,12 @@ extension SyntaxProtocol { var introducedNames: [LookupName] { get } /// Finds all declarations `name` refers to. `syntax` specifies the node lookup was triggered with. /// If `name` set to `nil`, returns all available names at the given node. - func lookup(for name: String?, - at syntax: SyntaxProtocol, - with config: LookupConfig, - state: LookupState) -> [LookupResult] + func lookup( + for name: String?, + at syntax: SyntaxProtocol, + with config: LookupConfig, + state: LookupState + ) -> [LookupResult] } @_spi(Experimental) extension ScopeSyntax { @@ -99,7 +101,8 @@ extension SyntaxProtocol { if filteredNames.isEmpty { return lookupInParent(for: name, at: syntax, with: config, state: state) } else { - return [.fromScope(self, withNames: filteredNames)] + lookupInParent(for: name, at: syntax, with: config, state: state) + return [.fromScope(self, withNames: filteredNames)] + + lookupInParent(for: name, at: syntax, with: config, state: state) } } diff --git a/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift index ed646cf1c48..16b7702fb2c 100644 --- a/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift @@ -15,7 +15,7 @@ import SwiftSyntax /// Scope that, in addition to names introduced by itself, /// also handles names introduced by /// `IntroducingToSequentialParentScopeSyntax` children scopes. -@_spi(Experimental) public protocol SequentialScopeSyntax: ScopeSyntax { +protocol SequentialScopeSyntax: ScopeSyntax { /// Returns names introduced by `codeBlockItems` /// and included `IntroducingToSequentialParentScopeSyntax` children /// scopes that match the lookup. @@ -29,8 +29,8 @@ import SwiftSyntax ) -> [LookupResult] } -@_spi(Experimental) extension SequentialScopeSyntax { - @_spi(Experimental) public func sequentialLookup( +extension SequentialScopeSyntax { + func sequentialLookup( in codeBlockItems: any Collection, for name: String?, at syntax: SyntaxProtocol, @@ -47,7 +47,8 @@ import SwiftSyntax { // Check if the enocountered scope should be ignored. if let scopeToSkip = state.skipSequentialIntroductionFrom, - scopeToSkip.id == introducingToParentScope.id { + scopeToSkip.id == introducingToParentScope.id + { continue } @@ -59,7 +60,12 @@ import SwiftSyntax // Add names introduced by the encountered scope. result.append( - contentsOf: introducingToParentScope.introducesToSequentialParent(for: name, at: syntax, with: config, state: state) + contentsOf: introducingToParentScope.introducesToSequentialParent( + for: name, + at: syntax, + with: config, + state: state + ) ) } else { // Extract new names from encountered node. diff --git a/Sources/SwiftLexicalLookup/TypeScopeSyntax.swift b/Sources/SwiftLexicalLookup/TypeScopeSyntax.swift index 4bcbe894a1a..1f852ad9bc9 100644 --- a/Sources/SwiftLexicalLookup/TypeScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/TypeScopeSyntax.swift @@ -13,6 +13,8 @@ import SwiftSyntax @_spi(Experimental) public protocol TypeScopeSyntax: ScopeSyntax, DeclSyntaxProtocol { + /// `self` and `Self` names referring to + /// this scope. var implicitInstanceAndTypeNames: [LookupName] { get } } diff --git a/Tests/SwiftLexicalLookupTest/ExpectedName.swift b/Tests/SwiftLexicalLookupTest/ExpectedName.swift index a169486e8d0..03ca70b83a9 100644 --- a/Tests/SwiftLexicalLookupTest/ExpectedName.swift +++ b/Tests/SwiftLexicalLookupTest/ExpectedName.swift @@ -31,7 +31,7 @@ enum ImplicitNameExpectation { case error(String) case newValue(String) case oldValue(String) - + func assertExpectation(marker: String, for name: LookupImplicitNameKind) { switch (name, self) { case (.self, .self): break @@ -43,7 +43,7 @@ enum ImplicitNameExpectation { XCTFail("For marker \(marker), actual name kind \(name) doesn't match expected \(self)") } } - + var marker: String { switch self { case .self(let marker), @@ -66,7 +66,7 @@ enum NameExpectation: ExpectedName { var marker: String { switch self { case .identifier(let marker), - .declaration(let marker): + .declaration(let marker): marker case .implicit(let implicitName): implicitName.marker From 940b06e4d8006286de1f0ba89d52b27e1368dad7 Mon Sep 17 00:00:00 2001 From: Jakub Florek <63607600+MAJKFL@users.noreply.github.com> Date: Tue, 23 Jul 2024 12:23:59 +0200 Subject: [PATCH 08/27] Add proper guard scope and implicit name lookup. --- .../Configurations/LookupConfig.swift | 6 +- .../IdentifiableSyntax.swift | 16 +- ...oducingToSequentialParentScopeSyntax.swift | 24 +++ Sources/SwiftLexicalLookup/LookupName.swift | 97 +++++++-- Sources/SwiftLexicalLookup/LookupResult.swift | 2 +- Sources/SwiftLexicalLookup/LookupState.swift | 21 ++ .../ScopeImplementations.swift | 185 +++++++++++++----- Sources/SwiftLexicalLookup/ScopeSyntax.swift | 45 +++-- .../SequentialScopeSyntax.swift | 92 +++++++++ .../SwiftLexicalLookup/TypeScopeSyntax.swift | 29 +++ Tests/SwiftLexicalLookupTest/Assertions.swift | 88 ++------- .../SwiftLexicalLookupTest/ExpectedName.swift | 94 +++++++++ .../MarkerExpectation.swift | 51 +++++ .../NameLookupTests.swift | 167 ++++++++++++++-- .../ResultExpectation.swift | 73 +++++++ 15 files changed, 821 insertions(+), 169 deletions(-) create mode 100644 Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift create mode 100644 Sources/SwiftLexicalLookup/LookupState.swift create mode 100644 Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift create mode 100644 Sources/SwiftLexicalLookup/TypeScopeSyntax.swift create mode 100644 Tests/SwiftLexicalLookupTest/ExpectedName.swift create mode 100644 Tests/SwiftLexicalLookupTest/MarkerExpectation.swift create mode 100644 Tests/SwiftLexicalLookupTest/ResultExpectation.swift diff --git a/Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift b/Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift index d96093037ca..a6442bb0e18 100644 --- a/Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift +++ b/Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift @@ -15,9 +15,11 @@ import Foundation @_spi(Experimental) public struct LookupConfig { /// Specifies behaviour of file scope. /// `memberBlockUpToLastDecl` by default. - public var fileScopeHandling: FileScopeHandlingConfig = .memberBlockUpToLastDecl + @_spi(Experimental) public var fileScopeHandling: FileScopeHandlingConfig - public init(fileScopeHandling: FileScopeHandlingConfig = .memberBlockUpToLastDecl) { + @_spi(Experimental) public init( + fileScopeHandling: FileScopeHandlingConfig = .memberBlockUpToLastDecl + ) { self.fileScopeHandling = fileScopeHandling } } diff --git a/Sources/SwiftLexicalLookup/IdentifiableSyntax.swift b/Sources/SwiftLexicalLookup/IdentifiableSyntax.swift index 67e6de6b4e9..033dad3244b 100644 --- a/Sources/SwiftLexicalLookup/IdentifiableSyntax.swift +++ b/Sources/SwiftLexicalLookup/IdentifiableSyntax.swift @@ -13,25 +13,25 @@ import SwiftSyntax /// Syntax node that can be refered to with an identifier. -public protocol IdentifiableSyntax: SyntaxProtocol { +@_spi(Experimental) public protocol IdentifiableSyntax: SyntaxProtocol { var identifier: TokenSyntax { get } } -extension IdentifierPatternSyntax: IdentifiableSyntax {} +@_spi(Experimental) extension IdentifierPatternSyntax: IdentifiableSyntax {} -extension ClosureParameterSyntax: IdentifiableSyntax { +@_spi(Experimental) extension ClosureParameterSyntax: IdentifiableSyntax { @_spi(Experimental) public var identifier: TokenSyntax { secondName ?? firstName } } -extension ClosureShorthandParameterSyntax: IdentifiableSyntax { +@_spi(Experimental) extension ClosureShorthandParameterSyntax: IdentifiableSyntax { @_spi(Experimental) public var identifier: TokenSyntax { name } } -extension ClosureCaptureSyntax: IdentifiableSyntax { +@_spi(Experimental) extension ClosureCaptureSyntax: IdentifiableSyntax { @_spi(Experimental) public var identifier: TokenSyntax { /* Doesn't work with closures like: _ = { [y=1+2] in @@ -41,3 +41,9 @@ extension ClosureCaptureSyntax: IdentifiableSyntax { expression.as(DeclReferenceExprSyntax.self)!.baseName } } + +@_spi(Experimental) extension AccessorParametersSyntax: IdentifiableSyntax { + @_spi(Experimental) public var identifier: TokenSyntax { + name + } +} diff --git a/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift b/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift new file mode 100644 index 00000000000..9be86f972ba --- /dev/null +++ b/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift @@ -0,0 +1,24 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +protocol IntroducingToSequentialParentScopeSyntax: ScopeSyntax { + /// Returns names matching lookup that should be + /// handled by it's parent sequential scope. + func introducesToSequentialParent( + for name: String?, + at syntax: SyntaxProtocol, + with config: LookupConfig, + state: LookupState + ) -> [LookupResult] +} diff --git a/Sources/SwiftLexicalLookup/LookupName.swift b/Sources/SwiftLexicalLookup/LookupName.swift index da7338a46f5..4e0e6d457a5 100644 --- a/Sources/SwiftLexicalLookup/LookupName.swift +++ b/Sources/SwiftLexicalLookup/LookupName.swift @@ -12,13 +12,60 @@ import SwiftSyntax +@_spi(Experimental) public enum LookupImplicitNameKind { + /// `self` keyword representing object instance. + case `self`(SyntaxProtocol) + /// `Self` keyword representing object type. + case `Self`(DeclSyntaxProtocol) + /// `self` captured by a closure. + case error(CatchClauseSyntax) + /// `newValue` available by default inside `set` and `willSet`. + case newValue(AccessorDeclSyntax) + /// `oldValue` available by default inside `didSet`. + case oldValue(AccessorDeclSyntax) + + /// Syntax associated with this name. + @_spi(Experimental) public var syntax: SyntaxProtocol { + switch self { + case .self(let syntax): + syntax + case .Self(let syntax): + syntax + case .error(let syntax): + syntax + case .newValue(let syntax): + syntax + case .oldValue(let syntax): + syntax + } + } + + /// Used for name comparison. + var name: String { + switch self { + case .self: + "self" + case .Self: + "Self" + case .error: + "error" + case .newValue: + "newValue" + case .oldValue: + "oldValue" + } + } +} + @_spi(Experimental) public enum LookupName { /// Identifier associated with the name. - /// Could be an identifier of a variable, function or closure parameter and more + /// Could be an identifier of a variable, function or closure parameter and more. case identifier(IdentifiableSyntax, accessibleAfter: AbsolutePosition?) /// Declaration associated with the name. - /// Could be class, struct, actor, protocol, function and more + /// Could be class, struct, actor, protocol, function and more. case declaration(NamedDeclSyntax, accessibleAfter: AbsolutePosition?) + /// Name introduced implicitly certain syntax nodes. + case implicit(LookupImplicitNameKind) /// Syntax associated with this name. @_spi(Experimental) public var syntax: SyntaxProtocol { @@ -27,6 +74,8 @@ import SwiftSyntax syntax case .declaration(let syntax, _): syntax + case .implicit(let implicitName): + implicitName.syntax } } @@ -37,6 +86,8 @@ import SwiftSyntax Identifier(syntax.identifier) case .declaration(let syntax, _): Identifier(syntax.name) + default: + nil } } @@ -46,6 +97,18 @@ import SwiftSyntax switch self { case .identifier(_, let absolutePosition), .declaration(_, let absolutePosition): absolutePosition + default: + nil + } + } + + /// Used for name comparison. + var name: String? { + switch self { + case .identifier, .declaration: + identifier?.name + case .implicit(let implicitName): + implicitName.name } } @@ -57,12 +120,15 @@ import SwiftSyntax /// Checks if this name refers to the looked up phrase. func refersTo(_ lookedUpName: String) -> Bool { - guard let name = identifier?.name else { return false } + guard let name else { return false } return name == lookedUpName } - /// Extracts names introduced by the given `from` structure. - static func getNames(from syntax: SyntaxProtocol, accessibleAfter: AbsolutePosition? = nil) -> [LookupName] { + /// Extracts names introduced by the given `syntax` structure. + static func getNames( + from syntax: SyntaxProtocol, + accessibleAfter: AbsolutePosition? = nil + ) -> [LookupName] { switch Syntax(syntax).as(SyntaxEnum.self) { case .variableDecl(let variableDecl): variableDecl.bindings.flatMap { binding in @@ -90,10 +156,6 @@ import SwiftSyntax functionCallExpr.arguments.flatMap { argument in getNames(from: argument.expression, accessibleAfter: accessibleAfter) } - case .guardStmt(let guardStmt): - guardStmt.conditions.flatMap { cond in - getNames(from: cond.condition, accessibleAfter: cond.endPosition) - } default: if let namedDecl = Syntax(syntax).asProtocol(SyntaxProtocol.self) as? NamedDeclSyntax { handle(namedDecl: namedDecl, accessibleAfter: accessibleAfter) @@ -106,9 +168,15 @@ import SwiftSyntax } /// Extracts name introduced by `IdentifiableSyntax` node. - private static func handle(identifiable: IdentifiableSyntax, accessibleAfter: AbsolutePosition? = nil) -> [LookupName] - { - if identifiable.identifier.text != "_" { + private static func handle( + identifiable: IdentifiableSyntax, + accessibleAfter: AbsolutePosition? = nil + ) -> [LookupName] { + if let closureCapture = identifiable as? ClosureCaptureSyntax, + closureCapture.identifier.tokenKind == .keyword(.self) + { + return [.implicit(.self(closureCapture))] // Handle `self` closure capture. + } else if identifiable.identifier.tokenKind != .wildcard { return [.identifier(identifiable, accessibleAfter: accessibleAfter)] } else { return [] @@ -116,7 +184,10 @@ import SwiftSyntax } /// Extracts name introduced by `NamedDeclSyntax` node. - private static func handle(namedDecl: NamedDeclSyntax, accessibleAfter: AbsolutePosition? = nil) -> [LookupName] { + private static func handle( + namedDecl: NamedDeclSyntax, + accessibleAfter: AbsolutePosition? = nil + ) -> [LookupName] { [.declaration(namedDecl, accessibleAfter: accessibleAfter)] } } diff --git a/Sources/SwiftLexicalLookup/LookupResult.swift b/Sources/SwiftLexicalLookup/LookupResult.swift index 4e5cc04f97b..6fabf2060dc 100644 --- a/Sources/SwiftLexicalLookup/LookupResult.swift +++ b/Sources/SwiftLexicalLookup/LookupResult.swift @@ -12,7 +12,7 @@ import SwiftSyntax -/// Represents resul +/// Represents result from a specific scope. @_spi(Experimental) public enum LookupResult { /// Scope and the names that matched lookup. case fromScope(ScopeSyntax, withNames: [LookupName]) diff --git a/Sources/SwiftLexicalLookup/LookupState.swift b/Sources/SwiftLexicalLookup/LookupState.swift new file mode 100644 index 00000000000..b94b7862656 --- /dev/null +++ b/Sources/SwiftLexicalLookup/LookupState.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Foundation + +@_spi(Experimental) public struct LookupState { + /// Specifies scopes that introduce names to their parent and + /// should be skipped during lookup in sequential scopes. + var skipSequentialIntroductionFrom: IntroducingToSequentialParentScopeSyntax? + + @_spi(Experimental) public init() {} +} diff --git a/Sources/SwiftLexicalLookup/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/ScopeImplementations.swift index 06eb4da0a0e..1b203272366 100644 --- a/Sources/SwiftLexicalLookup/ScopeImplementations.swift +++ b/Sources/SwiftLexicalLookup/ScopeImplementations.swift @@ -12,9 +12,9 @@ import SwiftSyntax -extension SyntaxProtocol { +@_spi(Experimental) extension SyntaxProtocol { /// Parent scope of this syntax node, or scope introduced by this syntax node. - var scope: ScopeSyntax? { + @_spi(Experimental) public var scope: ScopeSyntax? { if let scopeSyntax = Syntax(self).asProtocol(SyntaxProtocol.self) as? ScopeSyntax { scopeSyntax } else { @@ -23,10 +23,10 @@ extension SyntaxProtocol { } } -@_spi(Experimental) extension SourceFileSyntax: ScopeSyntax { +@_spi(Experimental) extension SourceFileSyntax: SequentialScopeSyntax { /// All names introduced in the file scope /// according to the default strategy: `memberBlockUpToLastDecl`. - public var introducedNames: [LookupName] { + @_spi(Experimental) public var introducedNames: [LookupName] { introducedNames(using: .memberBlockUpToLastDecl) } @@ -47,10 +47,10 @@ extension SyntaxProtocol { /// ``` /// During lookup, according to different configurations, /// names available at the marked place are: - /// - for `fileScopeNameIntroductionStrategy` - a, b, c, d + /// - for `memberBlockUpToLastDecl` - a, b, c, d /// - for `memberBlock` - a, b, c, d, e, f /// - for `codeBlock` - a - public func introducedNames(using fileScopeHandling: FileScopeHandlingConfig) -> [LookupName] { + @_spi(Experimental) public func introducedNames(using fileScopeHandling: FileScopeHandlingConfig) -> [LookupName] { switch fileScopeHandling { case .memberBlockUpToLastDecl: var encounteredNonDeclaration = false @@ -61,7 +61,7 @@ extension SyntaxProtocol { if encounteredNonDeclaration { return LookupName.getNames(from: item, accessibleAfter: codeBlockItem.endPosition) } else { - if item.is(DeclSyntax.self) || item.is(VariableDeclSyntax.self) { + if item.is(DeclSyntax.self) { return LookupName.getNames(from: item) } else { encounteredNonDeclaration = true @@ -97,36 +97,97 @@ extension SyntaxProtocol { /// ``` /// According to different configurations, /// names available at the marked place are: - /// - for `fileScopeNameIntroductionStrategy` - a, b, c, d + /// - for `memberBlockUpToLastDecl` - a, b, c, d /// - for `memberBlock` - a, b, c, d, e, f /// - for `codeBlock` - a - public func lookup( + @_spi(Experimental) public func lookup( for name: String?, at syntax: SyntaxProtocol, - with config: LookupConfig + with config: LookupConfig, + state: LookupState ) -> [LookupResult] { - let names = introducedNames(using: config.fileScopeHandling) - .filter { introducedName in - introducedName.isAccessible(at: syntax) && (name == nil || introducedName.refersTo(name!)) + switch config.fileScopeHandling { + case .codeBlock: + return sequentialLookup( + in: statements, + for: name, + at: syntax, + with: config, + state: state, + createResultsForThisScopeWith: { .fromFileScope(self, withNames: $0) } + ) + case .memberBlock: + let names = introducedNames(using: .memberBlock) + .filter { lookupName in + does(name: name, referTo: lookupName, at: syntax) + } + + return names.isEmpty ? [] : [.fromFileScope(self, withNames: names)] + case .memberBlockUpToLastDecl: + var members = [LookupName]() + var sequentialItems = [CodeBlockItemSyntax]() + var encounteredNonDeclaration = false + + for codeBlockItem in statements { + let item = codeBlockItem.item + + if encounteredNonDeclaration { + sequentialItems.append(codeBlockItem) + } else { + if item.is(DeclSyntax.self) || item.is(VariableDeclSyntax.self) { + let foundNames = LookupName.getNames(from: item) + + members.append(contentsOf: foundNames.filter { does(name: name, referTo: $0, at: syntax) }) + } else { + encounteredNonDeclaration = true + sequentialItems.append(codeBlockItem) + } + } } - return [.fromFileScope(self, withNames: names)] + let sequentialNames = sequentialLookup( + in: sequentialItems, + for: name, + at: syntax, + with: config, + state: state, + createResultsForThisScopeWith: { .fromFileScope(self, withNames: $0) } + ) + + return (members.isEmpty ? [] : [.fromFileScope(self, withNames: members)]) + sequentialNames + } } } -@_spi(Experimental) extension CodeBlockSyntax: ScopeSyntax { +@_spi(Experimental) extension CodeBlockSyntax: SequentialScopeSyntax { /// Names introduced in the code block scope /// accessible after their declaration. - public var introducedNames: [LookupName] { + @_spi(Experimental) public var introducedNames: [LookupName] { statements.flatMap { codeBlockItem in LookupName.getNames(from: codeBlockItem.item, accessibleAfter: codeBlockItem.endPosition) } } + + @_spi(Experimental) public func lookup( + for name: String?, + at syntax: SyntaxProtocol, + with config: LookupConfig, + state: LookupState + ) -> [LookupResult] { + sequentialLookup( + in: statements, + for: name, + at: syntax, + with: config, + state: state, + createResultsForThisScopeWith: { .fromScope(self, withNames: $0) } + ) + } } @_spi(Experimental) extension ForStmtSyntax: ScopeSyntax { /// Names introduced in the `for` body. - public var introducedNames: [LookupName] { + @_spi(Experimental) public var introducedNames: [LookupName] { LookupName.getNames(from: pattern) } } @@ -143,26 +204,20 @@ extension SyntaxProtocol { /// ``` /// During lookup, names available at the marked place are: /// `self`, a, b. - public var introducedNames: [LookupName] { + @_spi(Experimental) public var introducedNames: [LookupName] { let captureNames = - signature?.capture?.children(viewMode: .sourceAccurate).flatMap { child in - if let captureList = child.as(ClosureCaptureListSyntax.self) { - captureList.children(viewMode: .sourceAccurate).flatMap { capture in - LookupName.getNames(from: capture) - } - } else { - LookupName.getNames(from: child) - } + signature?.capture?.items.flatMap { element in + LookupName.getNames(from: element) } ?? [] let parameterNames = signature?.parameterClause?.children(viewMode: .sourceAccurate).flatMap { parameter in if let parameterList = parameter.as(ClosureParameterListSyntax.self) { - parameterList.children(viewMode: .sourceAccurate).flatMap { parameter in + return parameterList.children(viewMode: .sourceAccurate).flatMap { parameter in LookupName.getNames(from: parameter) } } else { - LookupName.getNames(from: parameter) + return LookupName.getNames(from: parameter) } } ?? [] @@ -172,7 +227,7 @@ extension SyntaxProtocol { @_spi(Experimental) extension WhileStmtSyntax: ScopeSyntax { /// Names introduced by the `while` loop by its conditions. - public var introducedNames: [LookupName] { + @_spi(Experimental) public var introducedNames: [LookupName] { conditions.flatMap { element in LookupName.getNames(from: element.condition) } @@ -181,7 +236,7 @@ extension SyntaxProtocol { @_spi(Experimental) extension IfExprSyntax: ScopeSyntax { /// Parent scope, omitting ancestor `if` statements if part of their `else if` clause. - public var parentScope: ScopeSyntax? { + @_spi(Experimental) public var parentScope: ScopeSyntax? { getParent(for: self.parent, previousIfElse: self.elseKeyword == nil) } @@ -217,7 +272,7 @@ extension SyntaxProtocol { } /// Names introduced by the `if` optional binding conditions. - public var introducedNames: [LookupName] { + @_spi(Experimental) public var introducedNames: [LookupName] { conditions.flatMap { element in LookupName.getNames(from: element.condition, accessibleAfter: element.endPosition) } @@ -235,32 +290,46 @@ extension SyntaxProtocol { /// // <-- a is not visible here /// } /// ``` - public func lookup( + @_spi(Experimental) public func lookup( for name: String?, at syntax: SyntaxProtocol, - with config: LookupConfig + with config: LookupConfig, + state: LookupState ) -> [LookupResult] { if let elseBody, elseBody.position <= syntax.position, elseBody.endPosition >= syntax.position { - lookupInParent(for: name, at: syntax, with: config) + lookupInParent(for: name, at: syntax, with: config, state: state) } else { - defaultLookupImplementation(for: name, at: syntax, with: config) + defaultLookupImplementation(for: name, at: syntax, with: config, state: state) } } } @_spi(Experimental) extension MemberBlockSyntax: ScopeSyntax { /// All names introduced by members of this member scope. - public var introducedNames: [LookupName] { + @_spi(Experimental) public var introducedNames: [LookupName] { members.flatMap { member in LookupName.getNames(from: member.decl) } } } -@_spi(Experimental) extension GuardStmtSyntax: ScopeSyntax { - /// Guard doesn't introduce any names to its children. - /// It's always empty. - public var introducedNames: [LookupName] { +@_spi(Experimental) extension GuardStmtSyntax: IntroducingToSequentialParentScopeSyntax { + @_spi(Experimental) public func introducesToSequentialParent( + for name: String?, + at syntax: SwiftSyntax.SyntaxProtocol, + with config: LookupConfig, + state: LookupState + ) -> [LookupResult] { + let names = conditions.flatMap { element in + LookupName.getNames(from: element.condition, accessibleAfter: element.endPosition) + }.filter { introducedName in + does(name: name, referTo: introducedName, at: syntax) + } + + return names.isEmpty ? [] : [.fromScope(self, withNames: names)] + } + + @_spi(Experimental) public var introducedNames: [LookupName] { [] } @@ -275,15 +344,43 @@ extension SyntaxProtocol { /// } /// // a is visible here /// ``` - public func lookup( + @_spi(Experimental) public func lookup( for name: String?, at syntax: SyntaxProtocol, - with config: LookupConfig + with config: LookupConfig, + state: LookupState ) -> [LookupResult] { if body.position <= syntax.position && body.endPosition >= syntax.position { - lookupInParent(for: name, at: self, with: config) + var newState = state + newState.skipSequentialIntroductionFrom = self + return lookupInParent(for: name, at: syntax, with: config, state: newState) + } else { + return defaultLookupImplementation(for: name, at: syntax, with: config, state: state) + } + } +} + +@_spi(Experimental) extension ActorDeclSyntax: TypeScopeSyntax {} +@_spi(Experimental) extension ClassDeclSyntax: TypeScopeSyntax {} +@_spi(Experimental) extension StructDeclSyntax: TypeScopeSyntax {} +@_spi(Experimental) extension EnumDeclSyntax: TypeScopeSyntax {} +@_spi(Experimental) extension ExtensionDeclSyntax: TypeScopeSyntax {} + +@_spi(Experimental) extension AccessorDeclSyntax: ScopeSyntax { + /// Implicit and/or explicit names introduced + /// withing the accessor.. + @_spi(Experimental) public var introducedNames: [LookupName] { + if let parameters { + LookupName.getNames(from: parameters) } else { - defaultLookupImplementation(for: name, at: syntax, with: config) + switch accessorSpecifier.tokenKind { + case .keyword(.set), .keyword(.willSet): + [.implicit(.newValue(self))] + case .keyword(.didSet): + [.implicit(.oldValue(self))] + default: + [] + } } } } diff --git a/Sources/SwiftLexicalLookup/ScopeSyntax.swift b/Sources/SwiftLexicalLookup/ScopeSyntax.swift index 55bebd9ef78..8358378a446 100644 --- a/Sources/SwiftLexicalLookup/ScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/ScopeSyntax.swift @@ -14,10 +14,10 @@ import SwiftSyntax extension SyntaxProtocol { /// Returns all names that `for` refers to at this syntax node. - /// Optional configuration can be passed as `with` to customize the lookup behavior. + /// Optional configuration can be passed as `config` to customize the lookup behavior. /// - /// - Returns: An array of `LookupResult` for name `for` at this syntax node, - /// ordered by visibility. If set to `nil`, returns all available names ordered by visibility. + /// - Returns: An array of `LookupResult` for `name` at this syntax node, + /// ordered by visibility. If `name` is set to `nil`, returns all available names ordered by visibility. /// The order is from the innermost to the outermost scope, /// and within each result, names are ordered by their introduction /// in the source code. @@ -47,7 +47,7 @@ extension SyntaxProtocol { for name: String?, with config: LookupConfig = LookupConfig() ) -> [LookupResult] { - scope?.lookup(for: name, at: self, with: config) ?? [] + scope?.lookup(for: name, at: self, with: config, state: LookupState()) ?? [] } } @@ -56,25 +56,31 @@ extension SyntaxProtocol { var parentScope: ScopeSyntax? { get } /// Names found in this scope. Ordered from first to last introduced. var introducedNames: [LookupName] { get } - /// Finds all declarations `name` refers to. `at` specifies the node lookup was triggered with. + /// Finds all declarations `name` refers to. `syntax` specifies the node lookup was triggered with. /// If `name` set to `nil`, returns all available names at the given node. - func lookup(for name: String?, at syntax: SyntaxProtocol, with config: LookupConfig) -> [LookupResult] + func lookup( + for name: String?, + at syntax: SyntaxProtocol, + with config: LookupConfig, + state: LookupState + ) -> [LookupResult] } @_spi(Experimental) extension ScopeSyntax { - public var parentScope: ScopeSyntax? { + @_spi(Experimental) public var parentScope: ScopeSyntax? { self.parent?.scope } /// Returns `LookupResult` of all names introduced in this scope that `name` /// refers to and is accessible at given syntax node then passes lookup to the parent. /// If `name` set to `nil`, returns all available names at the given node. - public func lookup( + @_spi(Experimental) public func lookup( for name: String?, at syntax: SyntaxProtocol, - with config: LookupConfig + with config: LookupConfig, + state: LookupState ) -> [LookupResult] { - defaultLookupImplementation(for: name, at: syntax, with: config) + defaultLookupImplementation(for: name, at: syntax, with: config, state: state) } /// Returns `LookupResult` of all names introduced in this scope that `name` @@ -83,18 +89,20 @@ extension SyntaxProtocol { func defaultLookupImplementation( for name: String?, at syntax: SyntaxProtocol, - with config: LookupConfig + with config: LookupConfig, + state: LookupState ) -> [LookupResult] { let filteredNames = introducedNames .filter { introducedName in - introducedName.isAccessible(at: syntax) && (name == nil || introducedName.refersTo(name!)) + does(name: name, referTo: introducedName, at: syntax) } if filteredNames.isEmpty { - return lookupInParent(for: name, at: syntax, with: config) + return lookupInParent(for: name, at: syntax, with: config, state: state) } else { - return [.fromScope(self, withNames: filteredNames)] + lookupInParent(for: name, at: syntax, with: config) + return [.fromScope(self, withNames: filteredNames)] + + lookupInParent(for: name, at: syntax, with: config, state: state) } } @@ -102,8 +110,13 @@ extension SyntaxProtocol { func lookupInParent( for name: String?, at syntax: SyntaxProtocol, - with config: LookupConfig + with config: LookupConfig, + state: LookupState ) -> [LookupResult] { - parentScope?.lookup(for: name, at: syntax, with: config) ?? [] + parentScope?.lookup(for: name, at: syntax, with: config, state: state) ?? [] + } + + func does(name: String?, referTo introducedName: LookupName, at syntax: SyntaxProtocol) -> Bool { + introducedName.isAccessible(at: syntax) && (name == nil || introducedName.refersTo(name!)) } } diff --git a/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift new file mode 100644 index 00000000000..16b7702fb2c --- /dev/null +++ b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift @@ -0,0 +1,92 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +/// Scope that, in addition to names introduced by itself, +/// also handles names introduced by +/// `IntroducingToSequentialParentScopeSyntax` children scopes. +protocol SequentialScopeSyntax: ScopeSyntax { + /// Returns names introduced by `codeBlockItems` + /// and included `IntroducingToSequentialParentScopeSyntax` children + /// scopes that match the lookup. + func sequentialLookup( + in codeBlockItems: any Collection, + for name: String?, + at syntax: SyntaxProtocol, + with config: LookupConfig, + state: LookupState, + createResultsForThisScopeWith getResults: ([LookupName]) -> (LookupResult) + ) -> [LookupResult] +} + +extension SequentialScopeSyntax { + func sequentialLookup( + in codeBlockItems: any Collection, + for name: String?, + at syntax: SyntaxProtocol, + with config: LookupConfig, + state: LookupState, + createResultsForThisScopeWith getResults: ([LookupName]) -> (LookupResult) + ) -> [LookupResult] { + var result = [LookupResult]() + var currentChunk = [LookupName]() + + for codeBlockItem in codeBlockItems { + if let introducingToParentScope = Syntax(codeBlockItem.item).asProtocol(SyntaxProtocol.self) + as? IntroducingToSequentialParentScopeSyntax + { + // Check if the enocountered scope should be ignored. + if let scopeToSkip = state.skipSequentialIntroductionFrom, + scopeToSkip.id == introducingToParentScope.id + { + continue + } + + // If there are some names collected, create a new result for this scope. + if !currentChunk.isEmpty { + result.append(getResults(currentChunk)) + currentChunk = [] + } + + // Add names introduced by the encountered scope. + result.append( + contentsOf: introducingToParentScope.introducesToSequentialParent( + for: name, + at: syntax, + with: config, + state: state + ) + ) + } else { + // Extract new names from encountered node. + currentChunk.append( + contentsOf: + LookupName.getNames( + from: codeBlockItem.item, + accessibleAfter: codeBlockItem.endPosition + ).filter { introducedName in + does(name: name, referTo: introducedName, at: syntax) + } + ) + } + } + + // If there are some names collected, create a new result for this scope. + if !currentChunk.isEmpty { + result.append(getResults(currentChunk)) + currentChunk = [] + } + + return (result.isEmpty ? [] : result.reversed()) + lookupInParent(for: name, at: syntax, with: config, state: state) + } +} diff --git a/Sources/SwiftLexicalLookup/TypeScopeSyntax.swift b/Sources/SwiftLexicalLookup/TypeScopeSyntax.swift new file mode 100644 index 00000000000..1f852ad9bc9 --- /dev/null +++ b/Sources/SwiftLexicalLookup/TypeScopeSyntax.swift @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +@_spi(Experimental) public protocol TypeScopeSyntax: ScopeSyntax, DeclSyntaxProtocol { + /// `self` and `Self` names referring to + /// this scope. + var implicitInstanceAndTypeNames: [LookupName] { get } +} + +@_spi(Experimental) extension TypeScopeSyntax { + @_spi(Experimental) public var implicitInstanceAndTypeNames: [LookupName] { + [.implicit(.self(self)), .implicit(.Self(self))] + } + + @_spi(Experimental) public var introducedNames: [LookupName] { + implicitInstanceAndTypeNames + } +} diff --git a/Tests/SwiftLexicalLookupTest/Assertions.swift b/Tests/SwiftLexicalLookupTest/Assertions.swift index 76106fb1453..5562512cc70 100644 --- a/Tests/SwiftLexicalLookupTest/Assertions.swift +++ b/Tests/SwiftLexicalLookupTest/Assertions.swift @@ -10,70 +10,18 @@ // //===----------------------------------------------------------------------===// -import Foundation @_spi(Experimental) import SwiftLexicalLookup import SwiftParser import SwiftSyntax import XCTest import _SwiftSyntaxTestSupport -/// Used to define result type expectectations for given markers. -enum MarkerExpectation { - /// Specifies a separate type for each result marker. - case distinct([String: SyntaxProtocol.Type]) - /// Specifies a common type for all results - /// apart from the ones defined explicitly in `except`. - case all(SyntaxProtocol.Type, except: [String: SyntaxProtocol.Type] = [:]) - /// Does not assert result types. - case none - - /// Assert `actual` result labeled with `marker` - /// according to the rules represented by this expectation. - fileprivate func assertMarkerType(marker: String, actual: SyntaxProtocol) { - switch self { - case .all(let expectedType, except: let dictionary): - assertMarkerType(marker: marker, actual: actual, expectedType: dictionary[marker] ?? expectedType) - case .distinct(let dictionary): - if let expectedType = dictionary[marker] { - assertMarkerType(marker: marker, actual: actual, expectedType: expectedType) - } else { - XCTFail("For result \(marker), could not find type expectation") - } - case .none: - break - } - } - - /// Assert whether `actual` type matches `expectedType`. - private func assertMarkerType(marker: String, actual: SyntaxProtocol, expectedType: SyntaxProtocol.Type) { - XCTAssert( - actual.is(expectedType), - "For result \(marker), expected type \(expectedType) doesn't match the actual type \(actual.syntaxNodeType)" - ) - } -} - -/// Used to define -enum ResultExpectation { - case fromScope(ScopeSyntax.Type, expectedNames: [String]) - case fromFileScope(expectedNames: [String]) - - var expectedNames: [String] { - switch self { - case .fromScope(_, let expectedNames): - expectedNames - case .fromFileScope(expectedNames: let expectedNames): - expectedNames - } - } -} - /// `methodUnderTest` is called with the token at every position marker in the keys of `expected`. /// It then asserts that the positions of the syntax nodes returned by `methodUnderTest` are the values in `expected`. /// It also checks whether result types match rules specified in `expectedResultTypes`. func assertLexicalScopeQuery( source: String, - methodUnderTest: (String, TokenSyntax) -> ([SyntaxProtocol?]), + methodUnderTest: (_ marker: String, _ tokenAtMarker: TokenSyntax) -> ([SyntaxProtocol?]), expected: [String: [String?]], expectedResultTypes: MarkerExpectation = .none ) { @@ -118,27 +66,27 @@ func assertLexicalScopeQuery( } // Assert validity of the output - for (actual, expected) in zip(result, zip(expectedMarkers, expectedPositions)) { - if actual == nil && expected.1 == nil { continue } + for (actual, (expectedMarker, expectedPosition)) in zip(result, zip(expectedMarkers, expectedPositions)) { + if actual == nil && expectedPosition == nil { continue } guard let actual else { XCTFail( - "For marker \(marker), actual is nil while expected is \(sourceFileSyntax.token(at: expected.1!)?.description ?? "nil")" + "For marker \(marker), actual is nil while expected is \(sourceFileSyntax.token(at: expectedPosition!)?.description ?? "nil")" ) continue } - guard let expectedPosition = expected.1 else { + guard let expectedPosition else { XCTFail("For marker \(marker), actual is \(actual) while expected position is nil") continue } XCTAssert( actual.positionAfterSkippingLeadingTrivia == expectedPosition, - "For marker \(marker), actual result: \(actual) doesn't match expected value: \(sourceFileSyntax.token(at: expected.1!)?.description ?? "nil")" + "For marker \(marker), actual result: \(actual) doesn't match expected value: \(sourceFileSyntax.token(at: expectedPosition)?.description ?? "nil")" ) - if let expectedMarker = expected.0 { + if let expectedMarker { expectedResultTypes.assertMarkerType(marker: expectedMarker, actual: actual) } } @@ -157,27 +105,15 @@ func assertLexicalNameLookup( ) { assertLexicalScopeQuery( source: source, - methodUnderTest: { marker, argument in - let result = argument.lookup(for: useNilAsTheParameter ? nil : argument.text, with: config) + methodUnderTest: { marker, tokenAtMarker in + let result = tokenAtMarker.lookup(for: useNilAsTheParameter ? nil : tokenAtMarker.text, with: config) guard let expectedValues = references[marker] else { XCTFail("For marker \(marker), couldn't find result expectation") return [] } - for (actual, expected) in zip(result, expectedValues) { - switch (actual, expected) { - case (.fromScope(let scope, withNames: _), .fromScope(let expectedType, expectedNames: _)): - XCTAssert( - scope.syntaxNodeType == expectedType, - "For marker \(marker), scope result type of \(scope.syntaxNodeType) doesn't match expected \(expectedType)" - ) - case (.fromFileScope, .fromFileScope): - break - default: - XCTFail("For marker \(marker), result actual result kind \(actual) doesn't match expected \(expected)") - } - } + ResultExpectation.assertResult(marker: marker, result: result, expectedValues: expectedValues) return result.flatMap { lookUpResult in lookUpResult.names.map { lookupName in @@ -187,7 +123,9 @@ func assertLexicalNameLookup( }, expected: references.mapValues { expectations in expectations.flatMap { expectation in - expectation.expectedNames + expectation.expectedNames.map { expectedName in + expectedName.marker + } } }, expectedResultTypes: expectedResultTypes diff --git a/Tests/SwiftLexicalLookupTest/ExpectedName.swift b/Tests/SwiftLexicalLookupTest/ExpectedName.swift new file mode 100644 index 00000000000..03ca70b83a9 --- /dev/null +++ b/Tests/SwiftLexicalLookupTest/ExpectedName.swift @@ -0,0 +1,94 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +@_spi(Experimental) import SwiftLexicalLookup +import SwiftSyntax +import XCTest + +/// Used to define lookup name assertion. +protocol ExpectedName { + var marker: String { get } +} + +extension String: ExpectedName { + var marker: String { + self + } +} + +enum ImplicitNameExpectation { + case `self`(String) + case `Self`(String) + case error(String) + case newValue(String) + case oldValue(String) + + func assertExpectation(marker: String, for name: LookupImplicitNameKind) { + switch (name, self) { + case (.self, .self): break + case (.Self, .Self): break + case (.error, .error): break + case (.newValue, .newValue): break + case (.oldValue, .oldValue): break + default: + XCTFail("For marker \(marker), actual name kind \(name) doesn't match expected \(self)") + } + } + + var marker: String { + switch self { + case .self(let marker), + .Self(let marker), + .error(let marker), + .newValue(let marker), + .oldValue(let marker): + marker + } + } +} + +/// Can be used to optionally assert +/// exact lookup name kind. +enum NameExpectation: ExpectedName { + case identifier(String) + case declaration(String) + case implicit(ImplicitNameExpectation) + + var marker: String { + switch self { + case .identifier(let marker), + .declaration(let marker): + marker + case .implicit(let implicitName): + implicitName.marker + } + } + + private func assertExpectation(marker: String, for name: LookupName) { + switch (name, self) { + case (.identifier, .identifier): break + case (.declaration, .declaration): break + case (.implicit(let implicitName), .implicit(let implicitNameExpectation)): + implicitNameExpectation.assertExpectation(marker: marker, for: implicitName) + default: + XCTFail("For marker \(marker), actual name kind \(name) doesn't match expected \(self)") + } + } + + static func assertNames(marker: String, acutalNames: [LookupName], expectedNames: [ExpectedName]) { + for (actualName, expectedName) in zip(acutalNames, expectedNames) { + guard let nameExpectation = expectedName as? NameExpectation else { continue } + + nameExpectation.assertExpectation(marker: marker, for: actualName) + } + } +} diff --git a/Tests/SwiftLexicalLookupTest/MarkerExpectation.swift b/Tests/SwiftLexicalLookupTest/MarkerExpectation.swift new file mode 100644 index 00000000000..a39e981a543 --- /dev/null +++ b/Tests/SwiftLexicalLookupTest/MarkerExpectation.swift @@ -0,0 +1,51 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +@_spi(Experimental) import SwiftLexicalLookup +import SwiftSyntax +import XCTest + +/// Used to define result type expectectations for given markers. +enum MarkerExpectation { + /// Specifies a separate type for each result marker. + case distinct([String: SyntaxProtocol.Type]) + /// Specifies a common type for all results + /// apart from the ones defined explicitly in `except`. + case all(SyntaxProtocol.Type, except: [String: SyntaxProtocol.Type] = [:]) + /// Does not assert result types. + case none + + /// Assert `actual` result labeled with `marker` + /// according to the rules represented by this expectation. + func assertMarkerType(marker: String, actual: SyntaxProtocol) { + switch self { + case .all(let expectedType, except: let dictionary): + assertMarkerType(marker: marker, actual: actual, expectedType: dictionary[marker] ?? expectedType) + case .distinct(let dictionary): + if let expectedType = dictionary[marker] { + assertMarkerType(marker: marker, actual: actual, expectedType: expectedType) + } else { + XCTFail("For result \(marker), could not find type expectation") + } + case .none: + break + } + } + + /// Assert whether `actual` type matches `expectedType`. + private func assertMarkerType(marker: String, actual: SyntaxProtocol, expectedType: SyntaxProtocol.Type) { + XCTAssert( + actual.is(expectedType), + "For result \(marker), expected type \(expectedType) doesn't match the actual type \(actual.syntaxNodeType)" + ) + } +} diff --git a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift index a04c35382e7..4eaee393bee 100644 --- a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift +++ b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift @@ -73,8 +73,8 @@ final class testNameLookup: XCTestCase { source: """ for 1️⃣i in 1..<4 { let (a, b) = (2️⃣i, 3️⃣j) - for (4️⃣i, 5️⃣j) in foo { - let (c, d) = (6️⃣i, 7️⃣j) + for (4️⃣i, (5️⃣j, 8️⃣k)) in foo { + let (c, d, e) = (6️⃣i, 7️⃣j, 9️⃣k) } } """, @@ -86,6 +86,7 @@ final class testNameLookup: XCTestCase { .fromScope(ForStmtSyntax.self, expectedNames: ["1️⃣"]), ], "7️⃣": [.fromScope(ForStmtSyntax.self, expectedNames: ["5️⃣"])], + "9️⃣": [.fromScope(ForStmtSyntax.self, expectedNames: ["8️⃣"])], ], expectedResultTypes: .all(IdentifierPatternSyntax.self) ) @@ -138,24 +139,33 @@ final class testNameLookup: XCTestCase { func testClosureCaptureLookup() { assertLexicalNameLookup( source: """ - func foo() { - let 1️⃣a = 1 - let x = { [3️⃣a, 4️⃣unowned b] in - print(6️⃣a) + 7️⃣class a { + func foo() { + let 1️⃣a = 1 + let x = { [2️⃣weak self, 3️⃣a, 4️⃣unowned b] in + print(5️⃣self, 6️⃣a, 8️⃣b) + } + let b = 0 } - let b = 0 } """, references: [ + "5️⃣": [ + .fromScope(ClosureExprSyntax.self, expectedNames: [NameExpectation.implicit(.self("2️⃣"))]), + .fromScope(ClassDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("7️⃣"))]), + ], "6️⃣": [ .fromScope(ClosureExprSyntax.self, expectedNames: ["3️⃣"]), .fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"]), - ] + .fromFileScope(expectedNames: ["7️⃣"]), + ], + "8️⃣": [.fromScope(ClosureExprSyntax.self, expectedNames: ["4️⃣"])], ], expectedResultTypes: .all( ClosureCaptureSyntax.self, except: [ - "1️⃣": IdentifierPatternSyntax.self + "1️⃣": IdentifierPatternSyntax.self, + "7️⃣": ClassDeclSyntax.self, ] ) ) @@ -401,6 +411,10 @@ final class testNameLookup: XCTestCase { "7️⃣": [ .fromScope(CodeBlockSyntax.self, expectedNames: ["4️⃣", "5️⃣"]), .fromScope(MemberBlockSyntax.self, expectedNames: ["1️⃣", "2️⃣", "3️⃣"]), + .fromScope( + ClassDeclSyntax.self, + expectedNames: [NameExpectation.implicit(.self("🔟")), NameExpectation.implicit(.Self("🔟"))] + ), .fromFileScope(expectedNames: ["🔟"]), ], "0️⃣": [ @@ -408,6 +422,10 @@ final class testNameLookup: XCTestCase { .fromScope(IfExprSyntax.self, expectedNames: ["6️⃣"]), .fromScope(CodeBlockSyntax.self, expectedNames: ["4️⃣", "5️⃣"]), .fromScope(MemberBlockSyntax.self, expectedNames: ["1️⃣", "2️⃣", "3️⃣"]), + .fromScope( + ClassDeclSyntax.self, + expectedNames: [NameExpectation.implicit(.self("🔟")), NameExpectation.implicit(.Self("🔟"))] + ), .fromFileScope(expectedNames: ["🔟"]), ], ], @@ -439,8 +457,11 @@ final class testNameLookup: XCTestCase { references: [ "4️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"])], "5️⃣": [], - "6️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣", "2️⃣"])], - "7️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["3️⃣"])], + "6️⃣": [ + .fromScope(GuardStmtSyntax.self, expectedNames: ["2️⃣"]), + .fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"]), + ], + "7️⃣": [.fromScope(GuardStmtSyntax.self, expectedNames: ["3️⃣"])], ], expectedResultTypes: .all( IdentifierPatternSyntax.self @@ -458,8 +479,14 @@ final class testNameLookup: XCTestCase { """, references: [ "3️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"])], - "5️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣", "2️⃣"])], - "6️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣", "2️⃣", "4️⃣"])], + "5️⃣": [ + .fromScope(GuardStmtSyntax.self, expectedNames: ["2️⃣"]), + .fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"]), + ], + "6️⃣": [ + .fromScope(GuardStmtSyntax.self, expectedNames: ["2️⃣", "4️⃣"]), + .fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"]), + ], ], expectedResultTypes: .all( IdentifierPatternSyntax.self @@ -558,4 +585,118 @@ final class testNameLookup: XCTestCase { config: LookupConfig(fileScopeHandling: .codeBlock) ) } + + func testGuardOnFileScope() { + assertLexicalNameLookup( + source: """ + let 1️⃣a = 0 + + class c {} + + guard let 2️⃣a else { fatalError() } + + 3️⃣class a {} + + let x = 4️⃣a + """, + references: [ + "4️⃣": [ + .fromFileScope(expectedNames: ["1️⃣"]), + .fromFileScope(expectedNames: ["3️⃣"]), + .fromScope(GuardStmtSyntax.self, expectedNames: ["2️⃣"]), + ] + ], + expectedResultTypes: .all(IdentifierPatternSyntax.self, except: ["3️⃣": ClassDeclSyntax.self]) + ) + } + + func testGuardOnFileScopeCodeBlock() { + assertLexicalNameLookup( + source: """ + let 1️⃣a = 0 + + class c {} + + guard let 2️⃣a else { fatalError() } + + 3️⃣class a {} + + let x = 4️⃣a + """, + references: [ + "4️⃣": [ + .fromFileScope(expectedNames: ["3️⃣"]), + .fromScope(GuardStmtSyntax.self, expectedNames: ["2️⃣"]), + .fromFileScope(expectedNames: ["1️⃣"]), + ] + ], + expectedResultTypes: .all(IdentifierPatternSyntax.self, except: ["3️⃣": ClassDeclSyntax.self]), + config: LookupConfig(fileScopeHandling: .codeBlock) + ) + } + + func testImplicitSelf() { + assertLexicalNameLookup( + source: """ + 1️⃣extension a { + 2️⃣struct b { + func foo() { + let x: 3️⃣Self = 4️⃣self + } + } + + func bar() { + let x: 5️⃣Self = 6️⃣self + } + } + """, + references: [ + "3️⃣": [ + .fromScope(StructDeclSyntax.self, expectedNames: [NameExpectation.implicit(.Self("2️⃣"))]), + .fromScope(ExtensionDeclSyntax.self, expectedNames: [NameExpectation.implicit(.Self("1️⃣"))]), + ], + "4️⃣": [ + .fromScope(StructDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("2️⃣"))]), + .fromScope(ExtensionDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("1️⃣"))]), + ], + "5️⃣": [.fromScope(ExtensionDeclSyntax.self, expectedNames: [NameExpectation.implicit(.Self("1️⃣"))])], + "6️⃣": [.fromScope(ExtensionDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("1️⃣"))])], + ] + ) + } + + func testAccessorImplicitNames() { + assertLexicalNameLookup( + source: """ + var a: Int { + get { y } + 1️⃣set { + y = 2️⃣newValue + } + } + + var b: Int { + get { y } + set3️⃣(newValue) { + y = 4️⃣newValue + } + } + + var c = 0 { + 5️⃣willSet { + 6️⃣newValue + } + 7️⃣didSet { + 8️⃣oldValue + } + } + """, + references: [ + "2️⃣": [.fromScope(AccessorDeclSyntax.self, expectedNames: [NameExpectation.implicit(.newValue("1️⃣"))])], + "4️⃣": [.fromScope(AccessorDeclSyntax.self, expectedNames: [NameExpectation.identifier("3️⃣")])], + "6️⃣": [.fromScope(AccessorDeclSyntax.self, expectedNames: [NameExpectation.implicit(.newValue("5️⃣"))])], + "8️⃣": [.fromScope(AccessorDeclSyntax.self, expectedNames: [NameExpectation.implicit(.oldValue("7️⃣"))])], + ] + ) + } } diff --git a/Tests/SwiftLexicalLookupTest/ResultExpectation.swift b/Tests/SwiftLexicalLookupTest/ResultExpectation.swift new file mode 100644 index 00000000000..5dc00d568f8 --- /dev/null +++ b/Tests/SwiftLexicalLookupTest/ResultExpectation.swift @@ -0,0 +1,73 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +@_spi(Experimental) import SwiftLexicalLookup +import SwiftSyntax +import XCTest + +/// Used to define lookup result assertion. +enum ResultExpectation { + case fromScope(ScopeSyntax.Type, expectedNames: [ExpectedName]) + case fromFileScope(expectedNames: [ExpectedName]) + + var expectedNames: [ExpectedName] { + switch self { + case .fromScope(_, let expectedNames): + expectedNames + case .fromFileScope(expectedNames: let expectedNames): + expectedNames + } + } + + var debugDescription: String { + switch self { + case .fromScope: + "fromScope" + case .fromFileScope: + "fromFileScope" + } + } + + static func assertResult(marker: String, result: [LookupResult], expectedValues: [ResultExpectation]) { + for (actual, expected) in zip(result, expectedValues) { + switch (actual, expected) { + case ( + .fromScope(let scope, withNames: let actualNames), + .fromScope(let expectedType, expectedNames: let expectedNames) + ): + XCTAssert( + scope.syntaxNodeType == expectedType, + "For marker \(marker), scope result type of \(scope.syntaxNodeType) doesn't match expected \(expectedType)" + ) + + NameExpectation.assertNames(marker: marker, acutalNames: actualNames, expectedNames: expectedNames) + case (.fromFileScope(_, let actualNames), .fromFileScope(let expectedNames)): + NameExpectation.assertNames(marker: marker, acutalNames: actualNames, expectedNames: expectedNames) + default: + XCTFail( + "For marker \(marker), actual result kind \(actual.debugDescription) doesn't match expected \(expected.debugDescription)" + ) + } + } + } +} + +extension LookupResult { + var debugDescription: String { + switch self { + case .fromScope: + "fromScope" + case .fromFileScope: + "fromFileScope" + } + } +} From c5988eda573bb85f11de39b829706fec07d06064 Mon Sep 17 00:00:00 2001 From: Jakub Florek <63607600+MAJKFL@users.noreply.github.com> Date: Thu, 25 Jul 2024 09:29:48 +0200 Subject: [PATCH 09/27] Improve documentation. --- .../FileScopeNameIntroductionStrategy.swift | 10 +++++----- .../Configurations/LookupConfig.swift | 7 +++++-- Sources/SwiftLexicalLookup/LookupName.swift | 7 +++++++ Sources/SwiftLexicalLookup/ScopeImplementations.swift | 2 +- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/Sources/SwiftLexicalLookup/Configurations/FileScopeNameIntroductionStrategy.swift b/Sources/SwiftLexicalLookup/Configurations/FileScopeNameIntroductionStrategy.swift index ca0c6642d8d..743f997218c 100644 --- a/Sources/SwiftLexicalLookup/Configurations/FileScopeNameIntroductionStrategy.swift +++ b/Sources/SwiftLexicalLookup/Configurations/FileScopeNameIntroductionStrategy.swift @@ -14,12 +14,12 @@ import SwiftSyntax /// Specifies how names should be introduced at the file scope. @_spi(Experimental) public enum FileScopeHandlingConfig { - /// Default behavior. Names introduced sequentially like in member block - /// scope up to the first non-declaration after and including which, - /// the declarations are treated like in code block scope. + /// This is the behavior that is being used + /// for Swift files with top-level code. case memberBlockUpToLastDecl - /// File scope behaves like member block scope. + /// This is the behavior that is being used + /// for Swift files that don’t allow top-level code. case memberBlock - /// File scope behaves like code block scope. + /// This is the behavior that is being used for e.g. function bodies. case codeBlock } diff --git a/Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift b/Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift index a6442bb0e18..973c9ea940d 100644 --- a/Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift +++ b/Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift @@ -13,10 +13,13 @@ import Foundation @_spi(Experimental) public struct LookupConfig { - /// Specifies behaviour of file scope. - /// `memberBlockUpToLastDecl` by default. + /// Specifies behavior of file scope. @_spi(Experimental) public var fileScopeHandling: FileScopeHandlingConfig + /// Creates a new lookup configuration. + /// + /// - `fileScopeHandling` - specifies behavior of file scope. + /// `memberBlockUpToLastDecl` by default. @_spi(Experimental) public init( fileScopeHandling: FileScopeHandlingConfig = .memberBlockUpToLastDecl ) { diff --git a/Sources/SwiftLexicalLookup/LookupName.swift b/Sources/SwiftLexicalLookup/LookupName.swift index 4e0e6d457a5..46c28ab9285 100644 --- a/Sources/SwiftLexicalLookup/LookupName.swift +++ b/Sources/SwiftLexicalLookup/LookupName.swift @@ -125,6 +125,13 @@ import SwiftSyntax } /// Extracts names introduced by the given `syntax` structure. + /// + /// When e.g. looking up a variable declaration like `let a = a`, + /// we expect `a` to be visible after the whole declaration. + /// That's why we can't just use `syntax.endPosition` for the `a` identifier pattern, + /// as the name would already be visible at the `a` reference withing the declaration. + /// That’s why code block and file scopes have to set + /// `accessibleAfter` to be the end position of the entire declaration syntax node. static func getNames( from syntax: SyntaxProtocol, accessibleAfter: AbsolutePosition? = nil diff --git a/Sources/SwiftLexicalLookup/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/ScopeImplementations.swift index 1b203272366..85838bc0abe 100644 --- a/Sources/SwiftLexicalLookup/ScopeImplementations.swift +++ b/Sources/SwiftLexicalLookup/ScopeImplementations.swift @@ -81,7 +81,7 @@ import SwiftSyntax } /// Returns names matching lookup using provided file - /// scope handling configuration (by default: `memberBlockUpToLastDecl`). + /// scope handling configuration. /// /// Example usage: /// ```swift From 0ef6a21e325e7cd29b5f1d53be4953e4544c449c Mon Sep 17 00:00:00 2001 From: Jakub Florek <63607600+MAJKFL@users.noreply.github.com> Date: Thu, 25 Jul 2024 11:13:14 +0200 Subject: [PATCH 10/27] Add identifier based name lookup. --- ...oducingToSequentialParentScopeSyntax.swift | 2 +- Sources/SwiftLexicalLookup/LookupName.swift | 31 ++++++-------- .../ScopeImplementations.swift | 30 +++++++------- Sources/SwiftLexicalLookup/ScopeSyntax.swift | 30 +++++++------- .../SequentialScopeSyntax.swift | 10 ++--- Sources/SwiftSyntax/Identifier.swift | 40 +++++++++++++++++-- Tests/SwiftLexicalLookupTest/Assertions.swift | 4 +- .../NameLookupTests.swift | 39 ++++++++++++++++++ Tests/SwiftSyntaxTest/IdentifierTests.swift | 2 +- 9 files changed, 129 insertions(+), 59 deletions(-) diff --git a/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift b/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift index 9be86f972ba..9a23170a74d 100644 --- a/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift @@ -16,7 +16,7 @@ protocol IntroducingToSequentialParentScopeSyntax: ScopeSyntax { /// Returns names matching lookup that should be /// handled by it's parent sequential scope. func introducesToSequentialParent( - for name: String?, + for identifier: Identifier?, at syntax: SyntaxProtocol, with config: LookupConfig, state: LookupState diff --git a/Sources/SwiftLexicalLookup/LookupName.swift b/Sources/SwiftLexicalLookup/LookupName.swift index 46c28ab9285..8e71bd70ab4 100644 --- a/Sources/SwiftLexicalLookup/LookupName.swift +++ b/Sources/SwiftLexicalLookup/LookupName.swift @@ -40,8 +40,8 @@ import SwiftSyntax } } - /// Used for name comparison. - var name: String { + /// Name associated with implicit name kind. + private var name: String { switch self { case .self: "self" @@ -55,6 +55,11 @@ import SwiftSyntax "oldValue" } } + + /// Identifier used for name comparison. + var identifier: Identifier { + Identifier(name) + } } @_spi(Experimental) public enum LookupName { @@ -79,15 +84,15 @@ import SwiftSyntax } } - /// Introduced name. + /// Identifier used for name comparison. @_spi(Experimental) public var identifier: Identifier? { switch self { case .identifier(let syntax, _): Identifier(syntax.identifier) case .declaration(let syntax, _): Identifier(syntax.name) - default: - nil + case .implicit(let kind): + kind.identifier } } @@ -102,16 +107,6 @@ import SwiftSyntax } } - /// Used for name comparison. - var name: String? { - switch self { - case .identifier, .declaration: - identifier?.name - case .implicit(let implicitName): - implicitName.name - } - } - /// Checks if this name was introduced before the syntax used for lookup. func isAccessible(at lookedUpSyntax: SyntaxProtocol) -> Bool { guard let accessibleAfter else { return true } @@ -119,9 +114,9 @@ import SwiftSyntax } /// Checks if this name refers to the looked up phrase. - func refersTo(_ lookedUpName: String) -> Bool { - guard let name else { return false } - return name == lookedUpName + func refersTo(_ lookedUpIdentifier: Identifier) -> Bool { + guard let identifier else { return false } + return identifier == lookedUpIdentifier } /// Extracts names introduced by the given `syntax` structure. diff --git a/Sources/SwiftLexicalLookup/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/ScopeImplementations.swift index 85838bc0abe..b359543b425 100644 --- a/Sources/SwiftLexicalLookup/ScopeImplementations.swift +++ b/Sources/SwiftLexicalLookup/ScopeImplementations.swift @@ -101,7 +101,7 @@ import SwiftSyntax /// - for `memberBlock` - a, b, c, d, e, f /// - for `codeBlock` - a @_spi(Experimental) public func lookup( - for name: String?, + for identifier: Identifier?, at syntax: SyntaxProtocol, with config: LookupConfig, state: LookupState @@ -110,7 +110,7 @@ import SwiftSyntax case .codeBlock: return sequentialLookup( in: statements, - for: name, + for: identifier, at: syntax, with: config, state: state, @@ -119,7 +119,7 @@ import SwiftSyntax case .memberBlock: let names = introducedNames(using: .memberBlock) .filter { lookupName in - does(name: name, referTo: lookupName, at: syntax) + does(identifier: identifier, referTo: lookupName, at: syntax) } return names.isEmpty ? [] : [.fromFileScope(self, withNames: names)] @@ -137,7 +137,7 @@ import SwiftSyntax if item.is(DeclSyntax.self) || item.is(VariableDeclSyntax.self) { let foundNames = LookupName.getNames(from: item) - members.append(contentsOf: foundNames.filter { does(name: name, referTo: $0, at: syntax) }) + members.append(contentsOf: foundNames.filter { does(identifier: identifier, referTo: $0, at: syntax) }) } else { encounteredNonDeclaration = true sequentialItems.append(codeBlockItem) @@ -147,7 +147,7 @@ import SwiftSyntax let sequentialNames = sequentialLookup( in: sequentialItems, - for: name, + for: identifier, at: syntax, with: config, state: state, @@ -169,14 +169,14 @@ import SwiftSyntax } @_spi(Experimental) public func lookup( - for name: String?, + for identifier: Identifier?, at syntax: SyntaxProtocol, with config: LookupConfig, state: LookupState ) -> [LookupResult] { sequentialLookup( in: statements, - for: name, + for: identifier, at: syntax, with: config, state: state, @@ -291,15 +291,15 @@ import SwiftSyntax /// } /// ``` @_spi(Experimental) public func lookup( - for name: String?, + for identifier: Identifier?, at syntax: SyntaxProtocol, with config: LookupConfig, state: LookupState ) -> [LookupResult] { if let elseBody, elseBody.position <= syntax.position, elseBody.endPosition >= syntax.position { - lookupInParent(for: name, at: syntax, with: config, state: state) + lookupInParent(for: identifier, at: syntax, with: config, state: state) } else { - defaultLookupImplementation(for: name, at: syntax, with: config, state: state) + defaultLookupImplementation(for: identifier, at: syntax, with: config, state: state) } } } @@ -315,7 +315,7 @@ import SwiftSyntax @_spi(Experimental) extension GuardStmtSyntax: IntroducingToSequentialParentScopeSyntax { @_spi(Experimental) public func introducesToSequentialParent( - for name: String?, + for identifier: Identifier?, at syntax: SwiftSyntax.SyntaxProtocol, with config: LookupConfig, state: LookupState @@ -323,7 +323,7 @@ import SwiftSyntax let names = conditions.flatMap { element in LookupName.getNames(from: element.condition, accessibleAfter: element.endPosition) }.filter { introducedName in - does(name: name, referTo: introducedName, at: syntax) + does(identifier: identifier, referTo: introducedName, at: syntax) } return names.isEmpty ? [] : [.fromScope(self, withNames: names)] @@ -345,7 +345,7 @@ import SwiftSyntax /// // a is visible here /// ``` @_spi(Experimental) public func lookup( - for name: String?, + for identifier: Identifier?, at syntax: SyntaxProtocol, with config: LookupConfig, state: LookupState @@ -353,9 +353,9 @@ import SwiftSyntax if body.position <= syntax.position && body.endPosition >= syntax.position { var newState = state newState.skipSequentialIntroductionFrom = self - return lookupInParent(for: name, at: syntax, with: config, state: newState) + return lookupInParent(for: identifier, at: syntax, with: config, state: newState) } else { - return defaultLookupImplementation(for: name, at: syntax, with: config, state: state) + return defaultLookupImplementation(for: identifier, at: syntax, with: config, state: state) } } } diff --git a/Sources/SwiftLexicalLookup/ScopeSyntax.swift b/Sources/SwiftLexicalLookup/ScopeSyntax.swift index 8358378a446..2952a1dd6cb 100644 --- a/Sources/SwiftLexicalLookup/ScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/ScopeSyntax.swift @@ -16,8 +16,8 @@ extension SyntaxProtocol { /// Returns all names that `for` refers to at this syntax node. /// Optional configuration can be passed as `config` to customize the lookup behavior. /// - /// - Returns: An array of `LookupResult` for `name` at this syntax node, - /// ordered by visibility. If `name` is set to `nil`, returns all available names ordered by visibility. + /// - Returns: An array of `LookupResult` for `identifier` at this syntax node, + /// ordered by visibility. If `identifier` is set to `nil`, returns all available names ordered by visibility. /// The order is from the innermost to the outermost scope, /// and within each result, names are ordered by their introduction /// in the source code. @@ -44,10 +44,10 @@ extension SyntaxProtocol { /// in this exact order. The constant declaration within the function body is omitted /// due to the ordering rules that prioritize visibility within the function body. @_spi(Experimental) public func lookup( - for name: String?, + for identifier: Identifier?, with config: LookupConfig = LookupConfig() ) -> [LookupResult] { - scope?.lookup(for: name, at: self, with: config, state: LookupState()) ?? [] + scope?.lookup(for: identifier, at: self, with: config, state: LookupState()) ?? [] } } @@ -59,7 +59,7 @@ extension SyntaxProtocol { /// Finds all declarations `name` refers to. `syntax` specifies the node lookup was triggered with. /// If `name` set to `nil`, returns all available names at the given node. func lookup( - for name: String?, + for identifier: Identifier?, at syntax: SyntaxProtocol, with config: LookupConfig, state: LookupState @@ -75,19 +75,19 @@ extension SyntaxProtocol { /// refers to and is accessible at given syntax node then passes lookup to the parent. /// If `name` set to `nil`, returns all available names at the given node. @_spi(Experimental) public func lookup( - for name: String?, + for identifier: Identifier?, at syntax: SyntaxProtocol, with config: LookupConfig, state: LookupState ) -> [LookupResult] { - defaultLookupImplementation(for: name, at: syntax, with: config, state: state) + defaultLookupImplementation(for: identifier, at: syntax, with: config, state: state) } /// Returns `LookupResult` of all names introduced in this scope that `name` /// refers to and is accessible at given syntax node then passes lookup to the parent. /// If `name` set to `nil`, returns all available names at the given node. func defaultLookupImplementation( - for name: String?, + for identifier: Identifier?, at syntax: SyntaxProtocol, with config: LookupConfig, state: LookupState @@ -95,28 +95,28 @@ extension SyntaxProtocol { let filteredNames = introducedNames .filter { introducedName in - does(name: name, referTo: introducedName, at: syntax) + does(identifier: identifier, referTo: introducedName, at: syntax) } if filteredNames.isEmpty { - return lookupInParent(for: name, at: syntax, with: config, state: state) + return lookupInParent(for: identifier, at: syntax, with: config, state: state) } else { return [.fromScope(self, withNames: filteredNames)] - + lookupInParent(for: name, at: syntax, with: config, state: state) + + lookupInParent(for: identifier, at: syntax, with: config, state: state) } } /// Looks up in parent scope. func lookupInParent( - for name: String?, + for identifier: Identifier?, at syntax: SyntaxProtocol, with config: LookupConfig, state: LookupState ) -> [LookupResult] { - parentScope?.lookup(for: name, at: syntax, with: config, state: state) ?? [] + parentScope?.lookup(for: identifier, at: syntax, with: config, state: state) ?? [] } - func does(name: String?, referTo introducedName: LookupName, at syntax: SyntaxProtocol) -> Bool { - introducedName.isAccessible(at: syntax) && (name == nil || introducedName.refersTo(name!)) + func does(identifier: Identifier?, referTo introducedName: LookupName, at syntax: SyntaxProtocol) -> Bool { + introducedName.isAccessible(at: syntax) && (identifier == nil || introducedName.refersTo(identifier!)) } } diff --git a/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift index 16b7702fb2c..20666cd5938 100644 --- a/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift @@ -21,7 +21,7 @@ protocol SequentialScopeSyntax: ScopeSyntax { /// scopes that match the lookup. func sequentialLookup( in codeBlockItems: any Collection, - for name: String?, + for identifier: Identifier?, at syntax: SyntaxProtocol, with config: LookupConfig, state: LookupState, @@ -32,7 +32,7 @@ protocol SequentialScopeSyntax: ScopeSyntax { extension SequentialScopeSyntax { func sequentialLookup( in codeBlockItems: any Collection, - for name: String?, + for identifier: Identifier?, at syntax: SyntaxProtocol, with config: LookupConfig, state: LookupState, @@ -61,7 +61,7 @@ extension SequentialScopeSyntax { // Add names introduced by the encountered scope. result.append( contentsOf: introducingToParentScope.introducesToSequentialParent( - for: name, + for: identifier, at: syntax, with: config, state: state @@ -75,7 +75,7 @@ extension SequentialScopeSyntax { from: codeBlockItem.item, accessibleAfter: codeBlockItem.endPosition ).filter { introducedName in - does(name: name, referTo: introducedName, at: syntax) + does(identifier: identifier, referTo: introducedName, at: syntax) } ) } @@ -87,6 +87,6 @@ extension SequentialScopeSyntax { currentChunk = [] } - return (result.isEmpty ? [] : result.reversed()) + lookupInParent(for: name, at: syntax, with: config, state: state) + return (result.isEmpty ? [] : result.reversed()) + lookupInParent(for: identifier, at: syntax, with: config, state: state) } } diff --git a/Sources/SwiftSyntax/Identifier.swift b/Sources/SwiftSyntax/Identifier.swift index 0864e754744..a8e2edc2d84 100644 --- a/Sources/SwiftSyntax/Identifier.swift +++ b/Sources/SwiftSyntax/Identifier.swift @@ -14,13 +14,14 @@ public struct Identifier: Equatable, Hashable, Sendable { /// The sanitized `text` of a token. public var name: String { - String(syntaxText: raw.name) + string?.name ?? String(syntaxText: raw!.name) } @_spi(RawSyntax) - public let raw: RawIdentifier + public let raw: RawIdentifier? + public let string: StringIdentifier? - private let arena: SyntaxArenaRef + private let arena: SyntaxArenaRef? public init?(_ token: TokenSyntax) { guard case .identifier = token.tokenKind else { @@ -29,6 +30,24 @@ public struct Identifier: Equatable, Hashable, Sendable { self.raw = RawIdentifier(token.tokenView) self.arena = token.tokenView.raw.arenaReference + self.string = nil + } + + public init(_ string: String) { + self.string = StringIdentifier(string) + self.raw = nil + self.arena = nil + } + + public static func == (lhs: Self, rhs: Self) -> Bool { + if let lhsRaw = lhs.raw, + let rhsRaw = rhs.raw, + let lhsArena = lhs.arena, + let rhsArena = rhs.arena { + return lhsRaw == rhsRaw && lhsArena == rhsArena + } else { + return lhs.name == rhs.name + } } } @@ -48,3 +67,18 @@ public struct RawIdentifier: Equatable, Hashable, Sendable { } } } + +public struct StringIdentifier: Equatable, Hashable, Sendable { + public let name: String + + fileprivate init(_ string: String) { + let backtick = "`" + if string.count > 2 && string.hasPrefix(backtick) && string.hasSuffix(backtick) { + let startIndex = string.index(after: string.startIndex) + let endIndex = string.index(before: string.endIndex) + self.name = String(string[startIndex.. Date: Thu, 25 Jul 2024 12:27:12 +0200 Subject: [PATCH 11/27] Fix handling inline variable declaration lists. --- Sources/SwiftLexicalLookup/LookupName.swift | 2 +- Tests/SwiftLexicalLookupTest/NameLookupTests.swift | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftLexicalLookup/LookupName.swift b/Sources/SwiftLexicalLookup/LookupName.swift index 4e0e6d457a5..4e51610f3d5 100644 --- a/Sources/SwiftLexicalLookup/LookupName.swift +++ b/Sources/SwiftLexicalLookup/LookupName.swift @@ -132,7 +132,7 @@ import SwiftSyntax switch Syntax(syntax).as(SyntaxEnum.self) { case .variableDecl(let variableDecl): variableDecl.bindings.flatMap { binding in - getNames(from: binding.pattern, accessibleAfter: accessibleAfter) + getNames(from: binding.pattern, accessibleAfter: accessibleAfter != nil ? binding.endPositionBeforeTrailingTrivia : nil) } case .tuplePattern(let tuplePattern): tuplePattern.elements.flatMap { tupleElement in diff --git a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift index 4eaee393bee..8389d1b6227 100644 --- a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift +++ b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift @@ -50,7 +50,7 @@ final class testNameLookup: XCTestCase { source: """ for i in 1..<4 { let (1️⃣a, 2️⃣b) = (1, 2) - let 3️⃣c = 3, 4️⃣d = 4 + let 3️⃣c = 3, 4️⃣d = 9️⃣c 5️⃣a 6️⃣b @@ -63,6 +63,7 @@ final class testNameLookup: XCTestCase { "6️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["2️⃣"])], "7️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["3️⃣"])], "8️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["4️⃣"])], + "9️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["3️⃣"])], ], expectedResultTypes: .all(IdentifierPatternSyntax.self) ) From 32fec701d07b1407cfe2b10d41105258195b170e Mon Sep 17 00:00:00 2001 From: Jakub Florek Date: Mon, 29 Jul 2024 18:33:25 +0200 Subject: [PATCH 12/27] Use IdentifierKind enum to represend different identifiers internally. --- Sources/SwiftSyntax/Identifier.swift | 73 ++++++++++++++-------------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/Sources/SwiftSyntax/Identifier.swift b/Sources/SwiftSyntax/Identifier.swift index a8e2edc2d84..a38d5b66beb 100644 --- a/Sources/SwiftSyntax/Identifier.swift +++ b/Sources/SwiftSyntax/Identifier.swift @@ -12,42 +12,58 @@ /// A canonicalized representation of an identifier that strips away backticks. public struct Identifier: Equatable, Hashable, Sendable { - /// The sanitized `text` of a token. + enum IdentifierKind: Hashable { + case token(raw: RawIdentifier, arena: SyntaxArenaRef) + case string(String) + + static func sanitize(string: String) -> IdentifierKind { + let backtick = "`" + if string.count > 2 && string.hasPrefix(backtick) && string.hasSuffix(backtick) { + let startIndex = string.index(after: string.startIndex) + let endIndex = string.index(before: string.endIndex) + return .string(String(string[startIndex.. Bool { - if let lhsRaw = lhs.raw, - let rhsRaw = rhs.raw, - let lhsArena = lhs.arena, - let rhsArena = rhs.arena { - return lhsRaw == rhsRaw && lhsArena == rhsArena - } else { - return lhs.name == rhs.name - } + lhs.name == rhs.name } } @@ -67,18 +83,3 @@ public struct RawIdentifier: Equatable, Hashable, Sendable { } } } - -public struct StringIdentifier: Equatable, Hashable, Sendable { - public let name: String - - fileprivate init(_ string: String) { - let backtick = "`" - if string.count > 2 && string.hasPrefix(backtick) && string.hasSuffix(backtick) { - let startIndex = string.index(after: string.startIndex) - let endIndex = string.index(before: string.endIndex) - self.name = String(string[startIndex.. Date: Tue, 30 Jul 2024 11:22:28 +0200 Subject: [PATCH 13/27] Fix merge artifacts. --- .../ScopeImplementations.swift | 15 +++-------- .../SequentialScopeSyntax.swift | 2 +- .../NameLookupTests.swift | 25 ------------------- 3 files changed, 4 insertions(+), 38 deletions(-) diff --git a/Sources/SwiftLexicalLookup/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/ScopeImplementations.swift index 5202ebd7bf1..565d1998fac 100644 --- a/Sources/SwiftLexicalLookup/ScopeImplementations.swift +++ b/Sources/SwiftLexicalLookup/ScopeImplementations.swift @@ -103,19 +103,10 @@ import SwiftSyntax state: LookupState ) -> [LookupResult] { switch config.fileScopeHandling { - case .codeBlock: - return sequentialLookup( - in: statements, - for: name, - at: syntax, - with: config, - state: state, - createResultsForThisScopeWith: { .fromFileScope(self, withNames: $0) } - ) case .memberBlock: let names = introducedNames(using: .memberBlock) .filter { lookupName in - does(name: name, referTo: lookupName, at: syntax) + checkName(name, refersTo: lookupName, at: syntax) } return names.isEmpty ? [] : [.fromFileScope(self, withNames: names)] @@ -133,7 +124,7 @@ import SwiftSyntax if item.is(DeclSyntax.self) || item.is(VariableDeclSyntax.self) { let foundNames = LookupName.getNames(from: item) - members.append(contentsOf: foundNames.filter { does(name: name, referTo: $0, at: syntax) }) + members.append(contentsOf: foundNames.filter { checkName(name, refersTo: $0, at: syntax) }) } else { encounteredNonDeclaration = true sequentialItems.append(codeBlockItem) @@ -319,7 +310,7 @@ import SwiftSyntax let names = conditions.flatMap { element in LookupName.getNames(from: element.condition, accessibleAfter: element.endPosition) }.filter { introducedName in - does(name: name, referTo: introducedName, at: syntax) + checkName(name, refersTo: introducedName, at: syntax) } return names.isEmpty ? [] : [.fromScope(self, withNames: names)] diff --git a/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift index 16b7702fb2c..acb7d94b8df 100644 --- a/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift @@ -75,7 +75,7 @@ extension SequentialScopeSyntax { from: codeBlockItem.item, accessibleAfter: codeBlockItem.endPosition ).filter { introducedName in - does(name: name, referTo: introducedName, at: syntax) + checkName(name, refersTo: introducedName, at: syntax) } ) } diff --git a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift index 0a86e292597..ab7d71ed00c 100644 --- a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift +++ b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift @@ -604,31 +604,6 @@ final class testNameLookup: XCTestCase { ) } - func testGuardOnFileScopeCodeBlock() { - assertLexicalNameLookup( - source: """ - let 1️⃣a = 0 - - class c {} - - guard let 2️⃣a else { fatalError() } - - 3️⃣class a {} - - let x = 4️⃣a - """, - references: [ - "4️⃣": [ - .fromFileScope(expectedNames: ["3️⃣"]), - .fromScope(GuardStmtSyntax.self, expectedNames: ["2️⃣"]), - .fromFileScope(expectedNames: ["1️⃣"]), - ] - ], - expectedResultTypes: .all(IdentifierPatternSyntax.self, except: ["3️⃣": ClassDeclSyntax.self]), - config: LookupConfig(fileScopeHandling: .codeBlock) - ) - } - func testImplicitSelf() { assertLexicalNameLookup( source: """ From a255fc94a6b48c4512f7e54d4a544f8e0a04ab12 Mon Sep 17 00:00:00 2001 From: Jakub Florek Date: Tue, 30 Jul 2024 11:35:18 +0200 Subject: [PATCH 14/27] Fix merge artifacts. --- Sources/SwiftLexicalLookup/LookupName.swift | 2 +- .../SwiftLexicalLookup/ScopeImplementations.swift | 10 +++++----- Sources/SwiftLexicalLookup/ScopeSyntax.swift | 4 ++-- .../SwiftLexicalLookup/SequentialScopeSyntax.swift | 5 +++-- Sources/SwiftSyntax/Identifier.swift | 12 ++++++------ Tests/SwiftLexicalLookupTest/Assertions.swift | 3 ++- Tests/SwiftLexicalLookupTest/NameLookupTests.swift | 10 +++++----- 7 files changed, 24 insertions(+), 22 deletions(-) diff --git a/Sources/SwiftLexicalLookup/LookupName.swift b/Sources/SwiftLexicalLookup/LookupName.swift index de068cf959e..6be060fe68a 100644 --- a/Sources/SwiftLexicalLookup/LookupName.swift +++ b/Sources/SwiftLexicalLookup/LookupName.swift @@ -55,7 +55,7 @@ import SwiftSyntax "oldValue" } } - + /// Identifier used for name comparison. var identifier: Identifier { Identifier(name) diff --git a/Sources/SwiftLexicalLookup/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/ScopeImplementations.swift index 14245760732..89af1bff769 100644 --- a/Sources/SwiftLexicalLookup/ScopeImplementations.swift +++ b/Sources/SwiftLexicalLookup/ScopeImplementations.swift @@ -106,7 +106,7 @@ import SwiftSyntax case .memberBlock: let names = introducedNames(using: .memberBlock) .filter { lookupName in - checkName(name, refersTo: lookupName, at: syntax) + checkName(identifier, refersTo: lookupName, at: syntax) } return names.isEmpty ? [] : [.fromFileScope(self, withNames: names)] @@ -124,7 +124,7 @@ import SwiftSyntax if item.is(DeclSyntax.self) || item.is(VariableDeclSyntax.self) { let foundNames = LookupName.getNames(from: item) - members.append(contentsOf: foundNames.filter { checkName(name, refersTo: $0, at: syntax) }) + members.append(contentsOf: foundNames.filter { checkName(identifier, refersTo: $0, at: syntax) }) } else { encounteredNonDeclaration = true sequentialItems.append(codeBlockItem) @@ -284,9 +284,9 @@ import SwiftSyntax state: LookupState ) -> [LookupResult] { if let elseBody, elseBody.position <= syntax.position, elseBody.endPosition >= syntax.position { - return lookupInParent(for: name, at: syntax, with: config, state: state) + return lookupInParent(for: identifier, at: syntax, with: config, state: state) } else { - return defaultLookupImplementation(for: name, at: syntax, with: config, state: state) + return defaultLookupImplementation(for: identifier, at: syntax, with: config, state: state) } } } @@ -310,7 +310,7 @@ import SwiftSyntax let names = conditions.flatMap { element in LookupName.getNames(from: element.condition, accessibleAfter: element.endPosition) }.filter { introducedName in - checkName(name, refersTo: introducedName, at: syntax) + checkName(identifier, refersTo: introducedName, at: syntax) } return names.isEmpty ? [] : [.fromScope(self, withNames: names)] diff --git a/Sources/SwiftLexicalLookup/ScopeSyntax.swift b/Sources/SwiftLexicalLookup/ScopeSyntax.swift index 609f82ae2ef..ba02c85eb5e 100644 --- a/Sources/SwiftLexicalLookup/ScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/ScopeSyntax.swift @@ -95,7 +95,7 @@ extension SyntaxProtocol { let filteredNames = introducedNames .filter { introducedName in - checkName(name, refersTo: introducedName, at: syntax) + checkName(identifier, refersTo: introducedName, at: syntax) } if filteredNames.isEmpty { @@ -116,7 +116,7 @@ extension SyntaxProtocol { parentScope?.lookup(for: identifier, at: syntax, with: config, state: state) ?? [] } - func checkName(_ name: String?, refersTo introducedName: LookupName, at syntax: SyntaxProtocol) -> Bool { + func checkName(_ name: Identifier?, refersTo introducedName: LookupName, at syntax: SyntaxProtocol) -> Bool { introducedName.isAccessible(at: syntax) && (name == nil || introducedName.refersTo(name!)) } } diff --git a/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift index 78eec2303d9..5300a9af311 100644 --- a/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift @@ -75,7 +75,7 @@ extension SequentialScopeSyntax { from: codeBlockItem.item, accessibleAfter: codeBlockItem.endPosition ).filter { introducedName in - checkName(name, refersTo: introducedName, at: syntax) + checkName(identifier, refersTo: introducedName, at: syntax) } ) } @@ -87,6 +87,7 @@ extension SequentialScopeSyntax { currentChunk = [] } - return (result.isEmpty ? [] : result.reversed()) + lookupInParent(for: identifier, at: syntax, with: config, state: state) + return (result.isEmpty ? [] : result.reversed()) + + lookupInParent(for: identifier, at: syntax, with: config, state: state) } } diff --git a/Sources/SwiftSyntax/Identifier.swift b/Sources/SwiftSyntax/Identifier.swift index a38d5b66beb..ce78479bafd 100644 --- a/Sources/SwiftSyntax/Identifier.swift +++ b/Sources/SwiftSyntax/Identifier.swift @@ -15,7 +15,7 @@ public struct Identifier: Equatable, Hashable, Sendable { enum IdentifierKind: Hashable { case token(raw: RawIdentifier, arena: SyntaxArenaRef) case string(String) - + static func sanitize(string: String) -> IdentifierKind { let backtick = "`" if string.count > 2 && string.hasPrefix(backtick) && string.hasSuffix(backtick) { @@ -27,7 +27,7 @@ public struct Identifier: Equatable, Hashable, Sendable { } } } - + /// The sanitized name of the identifier. public var name: String { switch identifier { @@ -37,7 +37,7 @@ public struct Identifier: Equatable, Hashable, Sendable { string } } - + @_spi(RawSyntax) public var raw: RawIdentifier? { switch identifier { @@ -54,14 +54,14 @@ public struct Identifier: Equatable, Hashable, Sendable { guard case .identifier = token.tokenKind else { return nil } - + self.identifier = .token(raw: RawIdentifier(token.tokenView), arena: token.tokenView.raw.arenaReference) } - + public init(_ string: String) { self.identifier = IdentifierKind.sanitize(string: string) } - + public static func == (lhs: Self, rhs: Self) -> Bool { lhs.name == rhs.name } diff --git a/Tests/SwiftLexicalLookupTest/Assertions.swift b/Tests/SwiftLexicalLookupTest/Assertions.swift index bb7b3ae3242..ccfeaf61788 100644 --- a/Tests/SwiftLexicalLookupTest/Assertions.swift +++ b/Tests/SwiftLexicalLookupTest/Assertions.swift @@ -15,6 +15,7 @@ import SwiftParser import SwiftSyntax import XCTest import _SwiftSyntaxTestSupport + /// `methodUnderTest` is called with the token at every position marker in the keys of `expected`. /// It then asserts that the positions of the syntax nodes returned by `methodUnderTest` are the values in `expected`. /// It also checks whether result types match rules specified in `expectedResultTypes`. @@ -93,7 +94,7 @@ func assertLexicalNameLookup( source: source, methodUnderTest: { marker, tokenAtMarker in let lookupIdentifier = Identifier(tokenAtMarker) ?? Identifier(tokenAtMarker.text) - + let result = tokenAtMarker.lookup(for: useNilAsTheParameter ? nil : lookupIdentifier, with: config) guard let expectedValues = references[marker] else { diff --git a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift index 08c2fa1503b..6627ef9951a 100644 --- a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift +++ b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift @@ -668,7 +668,7 @@ final class testNameLookup: XCTestCase { ] ) } - + func testBacktickCompatibility() { assertLexicalNameLookup( source: """ @@ -679,7 +679,7 @@ final class testNameLookup: XCTestCase { print(4️⃣`self`) } } - + 5️⃣struct Bar { func test() { print(6️⃣self) @@ -691,18 +691,18 @@ final class testNameLookup: XCTestCase { references: [ "3️⃣": [ .fromScope(CodeBlockSyntax.self, expectedNames: [NameExpectation.identifier("2️⃣")]), - .fromScope(StructDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("1️⃣"))]) + .fromScope(StructDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("1️⃣"))]), ], "4️⃣": [ .fromScope(CodeBlockSyntax.self, expectedNames: [NameExpectation.identifier("2️⃣")]), - .fromScope(StructDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("1️⃣"))]) + .fromScope(StructDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("1️⃣"))]), ], "6️⃣": [ .fromScope(StructDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("5️⃣"))]) ], "8️⃣": [ .fromScope(CodeBlockSyntax.self, expectedNames: [NameExpectation.identifier("7️⃣")]), - .fromScope(StructDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("5️⃣"))]) + .fromScope(StructDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("5️⃣"))]), ], ] ) From fbf48bbc2842a82edab34f88df4faf3c8ef96005 Mon Sep 17 00:00:00 2001 From: Jakub Florek Date: Tue, 30 Jul 2024 11:48:19 +0200 Subject: [PATCH 15/27] Fix merge artifacts --- .../SwiftLexicalLookup/ScopeImplementations.swift | 10 +++++----- Sources/SwiftSyntax/Identifier.swift | 8 ++++---- Tests/SwiftLexicalLookupTest/ExpectedName.swift | 6 +++--- Tests/SwiftLexicalLookupTest/ResultExpectation.swift | 12 ++++++------ 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Sources/SwiftLexicalLookup/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/ScopeImplementations.swift index 89af1bff769..2764e057904 100644 --- a/Sources/SwiftLexicalLookup/ScopeImplementations.swift +++ b/Sources/SwiftLexicalLookup/ScopeImplementations.swift @@ -14,7 +14,7 @@ import SwiftSyntax @_spi(Experimental) extension SyntaxProtocol { /// Parent scope of this syntax node, or scope introduced by this syntax node. - @_spi(Experimental) public var scope: ScopeSyntax? { + var scope: ScopeSyntax? { if let scopeSyntax = Syntax(self).asProtocol(SyntaxProtocol.self) as? ScopeSyntax { return scopeSyntax } else { @@ -358,15 +358,15 @@ import SwiftSyntax /// withing the accessor.. @_spi(Experimental) public var introducedNames: [LookupName] { if let parameters { - LookupName.getNames(from: parameters) + return LookupName.getNames(from: parameters) } else { switch accessorSpecifier.tokenKind { case .keyword(.set), .keyword(.willSet): - [.implicit(.newValue(self))] + return [.implicit(.newValue(self))] case .keyword(.didSet): - [.implicit(.oldValue(self))] + return [.implicit(.oldValue(self))] default: - [] + return [] } } } diff --git a/Sources/SwiftSyntax/Identifier.swift b/Sources/SwiftSyntax/Identifier.swift index ce78479bafd..ed0b511f0e1 100644 --- a/Sources/SwiftSyntax/Identifier.swift +++ b/Sources/SwiftSyntax/Identifier.swift @@ -32,9 +32,9 @@ public struct Identifier: Equatable, Hashable, Sendable { public var name: String { switch identifier { case .token(let raw, _): - String(syntaxText: raw.name) + return String(syntaxText: raw.name) case .string(let string): - string + return string } } @@ -42,9 +42,9 @@ public struct Identifier: Equatable, Hashable, Sendable { public var raw: RawIdentifier? { switch identifier { case .token(let raw, _): - raw + return raw default: - nil + return nil } } diff --git a/Tests/SwiftLexicalLookupTest/ExpectedName.swift b/Tests/SwiftLexicalLookupTest/ExpectedName.swift index 03ca70b83a9..867173d96eb 100644 --- a/Tests/SwiftLexicalLookupTest/ExpectedName.swift +++ b/Tests/SwiftLexicalLookupTest/ExpectedName.swift @@ -51,7 +51,7 @@ enum ImplicitNameExpectation { .error(let marker), .newValue(let marker), .oldValue(let marker): - marker + return marker } } } @@ -67,9 +67,9 @@ enum NameExpectation: ExpectedName { switch self { case .identifier(let marker), .declaration(let marker): - marker + return marker case .implicit(let implicitName): - implicitName.marker + return implicitName.marker } } diff --git a/Tests/SwiftLexicalLookupTest/ResultExpectation.swift b/Tests/SwiftLexicalLookupTest/ResultExpectation.swift index 5dc00d568f8..27a9d5b58a8 100644 --- a/Tests/SwiftLexicalLookupTest/ResultExpectation.swift +++ b/Tests/SwiftLexicalLookupTest/ResultExpectation.swift @@ -22,18 +22,18 @@ enum ResultExpectation { var expectedNames: [ExpectedName] { switch self { case .fromScope(_, let expectedNames): - expectedNames + return expectedNames case .fromFileScope(expectedNames: let expectedNames): - expectedNames + return expectedNames } } var debugDescription: String { switch self { case .fromScope: - "fromScope" + return "fromScope" case .fromFileScope: - "fromFileScope" + return "fromFileScope" } } @@ -65,9 +65,9 @@ extension LookupResult { var debugDescription: String { switch self { case .fromScope: - "fromScope" + return "fromScope" case .fromFileScope: - "fromFileScope" + return "fromFileScope" } } } From dda31f93e5b446021924377d67fa1cdee04f462d Mon Sep 17 00:00:00 2001 From: Jakub Florek Date: Tue, 30 Jul 2024 11:57:51 +0200 Subject: [PATCH 16/27] Merge changes from #2755 and add Identifier based lookup. --- Package.swift | 2 +- Release Notes/601.md | 4 + .../SwiftIfConfig/ActiveSyntaxRewriter.swift | 31 +-- .../SwiftIfConfig/ActiveSyntaxVisitor.swift | 49 +++-- .../SwiftIfConfig/BuildConfiguration.swift | 2 +- Sources/SwiftIfConfig/CMakeLists.txt | 5 +- Sources/SwiftIfConfig/ConfiguredRegions.swift | 21 +- .../SwiftIfConfig/IfConfigDecl+IfConfig.swift | 29 +-- Sources/SwiftIfConfig/IfConfigError.swift | 1 + .../SwiftIfConfig/IfConfigEvaluation.swift | 203 ++++++++++-------- ...nState.swift => IfConfigRegionState.swift} | 35 +-- .../SwiftIfConfig.docc/SwiftIfConfig.md | 4 +- .../SwiftIfConfig/SyntaxLiteralUtils.swift | 1 + .../SyntaxProtocol+IfConfig.swift | 42 ++-- .../SwiftIfConfig/VersionTuple+Parsing.swift | 1 + ...gy.swift => FileScopeHandlingConfig.swift} | 6 +- ...oducingToSequentialParentScopeSyntax.swift | 2 +- Sources/SwiftLexicalLookup/LookupName.swift | 76 ++++--- Sources/SwiftLexicalLookup/LookupResult.swift | 6 +- .../ScopeImplementations.swift | 55 ++--- Sources/SwiftLexicalLookup/ScopeSyntax.swift | 28 +-- .../SequentialScopeSyntax.swift | 11 +- Sources/SwiftRefactor/SyntaxUtils.swift | 2 +- Sources/SwiftSyntax/Identifier.swift | 47 +++- .../SyntaxNodeWithBody.swift | 31 ++- .../MacroProtocols/MemberMacro.swift | 5 + .../Assertions.swift | 1 + .../_SwiftSyntaxCShims/include/AtomicBool.h | 2 +- .../ParsingPerformanceTests.swift | 2 +- .../SyntaxClassifierPerformanceTests.swift | 2 +- .../VisitorPerformanceTests.swift | 6 +- .../SwiftIfConfigTest/ActiveRegionTests.swift | 89 ++++++-- Tests/SwiftIfConfigTest/Assertions.swift | 153 ------------- Tests/SwiftIfConfigTest/EvaluateTests.swift | 59 ++++- .../TestingBuildConfiguration.swift | 1 + Tests/SwiftIfConfigTest/VisitorTests.swift | 56 ++++- Tests/SwiftLexicalLookupTest/Assertions.swift | 19 +- .../SwiftLexicalLookupTest/ExpectedName.swift | 6 +- .../NameLookupTests.swift | 109 +++++----- .../ResultExpectation.swift | 12 +- Tests/SwiftParserTest/ParserTests.swift | 8 +- .../AccessorDeclTests.swift | 24 +++ Tests/SwiftSyntaxTest/IdentifierTests.swift | 2 +- cmake/modules/SwiftCompilerCapability.cmake | 22 +- 44 files changed, 686 insertions(+), 586 deletions(-) rename Sources/SwiftIfConfig/{ConfiguredRegionState.swift => IfConfigRegionState.swift} (58%) rename Sources/SwiftLexicalLookup/Configurations/{FileScopeNameIntroductionStrategy.swift => FileScopeHandlingConfig.swift} (81%) delete mode 100644 Tests/SwiftIfConfigTest/Assertions.swift create mode 100644 Tests/SwiftSyntaxBuilderTest/AccessorDeclTests.swift diff --git a/Package.swift b/Package.swift index 4d4a4a96a14..2d626ac9ab5 100644 --- a/Package.swift +++ b/Package.swift @@ -143,7 +143,7 @@ let package = Package( .target( name: "SwiftIfConfig", - dependencies: ["SwiftSyntax", "SwiftOperators"], + dependencies: ["SwiftSyntax", "SwiftDiagnostics", "SwiftOperators"], exclude: ["CMakeLists.txt"] ), diff --git a/Release Notes/601.md b/Release Notes/601.md index 7036e503b33..cb704e26814 100644 --- a/Release Notes/601.md +++ b/Release Notes/601.md @@ -15,6 +15,10 @@ - Description: This method translates an error into one or more diagnostics, recognizing `DiagnosticsError` and `DiagnosticMessage` instances or providing its own `Diagnostic` as needed. - Pull Request: https://github.com/swiftlang/swift-syntax/pull/1816 +- Added a new library `SwiftIfConfig`. + - Description: This new library provides facilities for evaluating `#if` conditions and determining which regions of a syntax tree are active according to a given build configuration. + - Pull Request: https://github.com/swiftlang/swift-syntax/pull/1816 + ## API Behavior Changes - `SyntaxProtocol.trimmed` detaches the node diff --git a/Sources/SwiftIfConfig/ActiveSyntaxRewriter.swift b/Sources/SwiftIfConfig/ActiveSyntaxRewriter.swift index a863c6776b2..0cdeb97e4e7 100644 --- a/Sources/SwiftIfConfig/ActiveSyntaxRewriter.swift +++ b/Sources/SwiftIfConfig/ActiveSyntaxRewriter.swift @@ -10,15 +10,6 @@ // //===----------------------------------------------------------------------===// -// -// This file defines the SyntaxRewriter, a class that performs a standard walk -// and tree-rebuilding pattern. -// -// Subclassers of this class can override the walking behavior for any syntax -// node and transform nodes however they like. -// -//===----------------------------------------------------------------------===// - import SwiftDiagnostics import SwiftSyntax @@ -34,7 +25,9 @@ extension SyntaxProtocol { /// clauses, e.g., `#if FOO > 10`, then the condition will be /// considered to have failed and the clauses's elements will be /// removed. - public func removingInactive(in configuration: some BuildConfiguration) -> (Syntax, [Diagnostic]) { + public func removingInactive( + in configuration: some BuildConfiguration + ) -> (result: Syntax, diagnostics: [Diagnostic]) { // First pass: Find all of the active clauses for the #ifs we need to // visit, along with any diagnostics produced along the way. This process // does not change the tree in any way. @@ -42,7 +35,7 @@ extension SyntaxProtocol { visitor.walk(self) // If there were no active clauses to visit, we're done! - if visitor.numIfClausesVisited == 0 { + if !visitor.visitedAnyIfClauses { return (Syntax(self), visitor.diagnostics) } @@ -88,12 +81,13 @@ extension SyntaxProtocol { /// than trivia). class ActiveSyntaxRewriter: SyntaxRewriter { let configuration: Configuration + var diagnostics: [Diagnostic] = [] init(configuration: Configuration) { self.configuration = configuration } - private func dropInactive( + private func dropInactive( _ node: List, elementAsIfConfig: (List.Element) -> IfConfigDeclSyntax? ) -> List { @@ -105,7 +99,10 @@ class ActiveSyntaxRewriter: SyntaxRewriter { // Find #ifs within the list. if let ifConfigDecl = elementAsIfConfig(element) { // Retrieve the active `#if` clause - let activeClause = ifConfigDecl.activeClause(in: configuration) + let (activeClause, localDiagnostics) = ifConfigDecl.activeClause(in: configuration) + + // Add these diagnostics. + diagnostics.append(contentsOf: localDiagnostics) // If this is the first element that changed, note that we have // changes and add all prior elements to the list of new elements. @@ -255,7 +252,8 @@ class ActiveSyntaxRewriter: SyntaxRewriter { return dropInactive(outerBase: base, postfixIfConfig: postfixIfConfig) } - preconditionFailure("Unhandled postfix expression in #if elimination") + assertionFailure("Unhandled postfix expression in #if elimination") + return postfix } /// Drop inactive regions from a postfix `#if` configuration, applying the @@ -265,7 +263,10 @@ class ActiveSyntaxRewriter: SyntaxRewriter { postfixIfConfig: PostfixIfConfigExprSyntax ) -> ExprSyntax { // Retrieve the active `if` clause. - let activeClause = postfixIfConfig.config.activeClause(in: configuration) + let (activeClause, localDiagnostics) = postfixIfConfig.config.activeClause(in: configuration) + + // Record these diagnostics. + diagnostics.append(contentsOf: localDiagnostics) guard case .postfixExpression(let postfixExpr) = activeClause?.elements else { diff --git a/Sources/SwiftIfConfig/ActiveSyntaxVisitor.swift b/Sources/SwiftIfConfig/ActiveSyntaxVisitor.swift index 4caba69c733..e202f11809f 100644 --- a/Sources/SwiftIfConfig/ActiveSyntaxVisitor.swift +++ b/Sources/SwiftIfConfig/ActiveSyntaxVisitor.swift @@ -9,6 +9,7 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// + import SwiftDiagnostics import SwiftSyntax @@ -36,18 +37,17 @@ import SwiftSyntax /// it would not visit either `f` or `g`. /// /// All notes visited by this visitor will have the "active" state, i.e., -/// `node.isActive(in: configuration)` will evaluate to `.active` or will -/// throw. When errors occur, they will be recorded in the set of -/// diagnostics. +/// `node.isActive(in: configuration)` will have evaluated to `.active`. +/// When errors occur, they will be recorded in the array of diagnostics. open class ActiveSyntaxVisitor: SyntaxVisitor { /// The build configuration, which will be queried for each relevant `#if`. public let configuration: Configuration - /// The set of diagnostics accumulated during this walk of active syntax. - public var diagnostics: [Diagnostic] = [] + /// The diagnostics accumulated during this walk of active syntax. + public private(set) var diagnostics: [Diagnostic] = [] - /// The number of "#if" clauses that were visited. - var numIfClausesVisited: Int = 0 + /// Whether we visited any "#if" clauses. + var visitedAnyIfClauses: Bool = false public init(viewMode: SyntaxTreeViewMode, configuration: Configuration) { self.configuration = configuration @@ -55,15 +55,16 @@ open class ActiveSyntaxVisitor: SyntaxVisitor } open override func visit(_ node: IfConfigDeclSyntax) -> SyntaxVisitorContinueKind { - let activeClause = node.activeClause(in: configuration) { diag in - self.diagnostics.append(diag) - } + // Note: there is a clone of this code in ActiveSyntaxAnyVisitor. If you + // change one, please also change the other. + let (activeClause, localDiagnostics) = node.activeClause(in: configuration) + diagnostics.append(contentsOf: localDiagnostics) - numIfClausesVisited += 1 + visitedAnyIfClauses = true // If there is an active clause, visit it's children. if let activeClause, let elements = activeClause.elements { - walk(Syntax(elements)) + walk(elements) } // Skip everything else in the #if. @@ -95,19 +96,14 @@ open class ActiveSyntaxVisitor: SyntaxVisitor /// it would not visit either `f` or `g`. /// /// All notes visited by this visitor will have the "active" state, i.e., -/// `node.isActive(in: configuration)` will evaluate to `.active` or will -/// throw. -/// -/// All notes visited by this visitor will have the "active" state, i.e., -/// `node.isActive(in: configuration)` will evaluate to `.active` or will -/// throw. When errors occur, they will be recorded in the set of -/// diagnostivs. +/// `node.isActive(in: configuration)` will have evaluated to `.active`. +/// When errors occur, they will be recorded in the array of diagnostics. open class ActiveSyntaxAnyVisitor: SyntaxAnyVisitor { /// The build configuration, which will be queried for each relevant `#if`. public let configuration: Configuration - /// The set of diagnostics accumulated during this walk of active syntax. - public var diagnostics: [Diagnostic] = [] + /// The diagnostics accumulated during this walk of active syntax. + public private(set) var diagnostics: [Diagnostic] = [] public init(viewMode: SyntaxTreeViewMode, configuration: Configuration) { self.configuration = configuration @@ -115,12 +111,15 @@ open class ActiveSyntaxAnyVisitor: SyntaxAnyV } open override func visit(_ node: IfConfigDeclSyntax) -> SyntaxVisitorContinueKind { + // Note: there is a clone of this code in ActiveSyntaxVisitor. If you + // change one, please also change the other. + // If there is an active clause, visit it's children. - let activeClause = node.activeClause(in: configuration) { diag in - self.diagnostics.append(diag) - } + let (activeClause, localDiagnostics) = node.activeClause(in: configuration) + diagnostics.append(contentsOf: localDiagnostics) + if let activeClause, let elements = activeClause.elements { - walk(Syntax(elements)) + walk(elements) } // Skip everything else in the #if. diff --git a/Sources/SwiftIfConfig/BuildConfiguration.swift b/Sources/SwiftIfConfig/BuildConfiguration.swift index 30cdac9c63d..a3a62a95da8 100644 --- a/Sources/SwiftIfConfig/BuildConfiguration.swift +++ b/Sources/SwiftIfConfig/BuildConfiguration.swift @@ -253,7 +253,7 @@ public protocol BuildConfiguration { /// /// The language version can be queried with the `swift` directive that checks /// how the supported language version compares, as described by - /// [SE-0212](https://github.com/apple/swift-evolution/blob/main/proposals/0212-compiler-version-directive.md). For example: + /// [SE-0212](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0212-compiler-version-directive.md). For example: /// /// ```swift /// #if swift(>=5.5) diff --git a/Sources/SwiftIfConfig/CMakeLists.txt b/Sources/SwiftIfConfig/CMakeLists.txt index 3a5d6ece0c1..350c134b9f9 100644 --- a/Sources/SwiftIfConfig/CMakeLists.txt +++ b/Sources/SwiftIfConfig/CMakeLists.txt @@ -11,7 +11,7 @@ add_swift_syntax_library(SwiftIfConfig ActiveSyntaxRewriter.swift BuildConfiguration.swift ConfiguredRegions.swift - ConfiguredRegionState.swift + IfConfigRegionState.swift IfConfigDecl+IfConfig.swift IfConfigError.swift IfConfigEvaluation.swift @@ -25,5 +25,4 @@ add_swift_syntax_library(SwiftIfConfig target_link_swift_syntax_libraries(SwiftIfConfig PUBLIC SwiftSyntax SwiftDiagnostics - SwiftOperators - SwiftParser) + SwiftOperators) diff --git a/Sources/SwiftIfConfig/ConfiguredRegions.swift b/Sources/SwiftIfConfig/ConfiguredRegions.swift index f47bb5a33c7..0706a7ba372 100644 --- a/Sources/SwiftIfConfig/ConfiguredRegions.swift +++ b/Sources/SwiftIfConfig/ConfiguredRegions.swift @@ -9,15 +9,14 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// + import SwiftDiagnostics import SwiftSyntax extension SyntaxProtocol { /// Find all of the #if/#elseif/#else clauses within the given syntax node, - /// indicating their active state. This operation will recurse into active - /// clauses to represent the flattened nested structure, while nonactive - /// clauses need no recursion (because there is no relevant structure in - /// them). + /// indicating their active state. This operation will recurse into all + /// clauses to indicate regions of active / inactive / unparsed code. /// /// For example, given code like the following: /// #if DEBUG @@ -37,7 +36,7 @@ extension SyntaxProtocol { /// - Inactive region for the final `#else`. public func configuredRegions( in configuration: some BuildConfiguration - ) -> [(IfConfigClauseSyntax, ConfiguredRegionState)] { + ) -> [(IfConfigClauseSyntax, IfConfigRegionState)] { let visitor = ConfiguredRegionVisitor(configuration: configuration) visitor.walk(self) return visitor.regions @@ -49,7 +48,7 @@ fileprivate class ConfiguredRegionVisitor: Sy let configuration: Configuration /// The regions we've found so far. - var regions: [(IfConfigClauseSyntax, ConfiguredRegionState)] = [] + var regions: [(IfConfigClauseSyntax, IfConfigRegionState)] = [] /// Whether we are currently within an active region. var inActiveRegion = true @@ -62,7 +61,7 @@ fileprivate class ConfiguredRegionVisitor: Sy override func visit(_ node: IfConfigDeclSyntax) -> SyntaxVisitorContinueKind { // If we're in an active region, find the active clause. Otherwise, // there isn't one. - let activeClause = inActiveRegion ? node.activeClause(in: configuration) : nil + let activeClause = inActiveRegion ? node.activeClause(in: configuration).clause : nil for clause in node.clauses { // If this is the active clause, record it and then recurse into the // elements. @@ -79,11 +78,9 @@ fileprivate class ConfiguredRegionVisitor: Sy } // For inactive clauses, distinguish between inactive and unparsed. - let isVersioned = - (try? clause.isVersioned( - configuration: configuration, - diagnosticHandler: nil - )) ?? true + let isVersioned = clause.isVersioned( + configuration: configuration + ).versioned // If this is within an active region, or this is an unparsed region, // record it. diff --git a/Sources/SwiftIfConfig/IfConfigDecl+IfConfig.swift b/Sources/SwiftIfConfig/IfConfigDecl+IfConfig.swift index 823724ee965..68e9e6b7b48 100644 --- a/Sources/SwiftIfConfig/IfConfigDecl+IfConfig.swift +++ b/Sources/SwiftIfConfig/IfConfigDecl+IfConfig.swift @@ -9,6 +9,7 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// + import SwiftDiagnostics import SwiftSyntax @@ -25,35 +26,35 @@ extension IfConfigDeclSyntax { /// ``` /// /// If the `A` configuration option was passed on the command line (e.g. via `-DA`), the first clause - /// (containing `func f()`) would be returned. If not, and if the `B`configuration was passed on the + /// (containing `func f()`) would be returned. If not, and if the `B` configuration was passed on the /// command line, the second clause (containing `func g()`) would be returned. If neither was /// passed, this function will return `nil` to indicate that none of the regions are active. /// - /// If an error occurrs while processing any of the `#if` clauses, + /// If an error occurs while processing any of the `#if` clauses, /// that clause will be considered inactive and this operation will /// continue to evaluate later clauses. public func activeClause( - in configuration: some BuildConfiguration, - diagnosticHandler: ((Diagnostic) -> Void)? = nil - ) -> IfConfigClauseSyntax? { + in configuration: some BuildConfiguration + ) -> (clause: IfConfigClauseSyntax?, diagnostics: [Diagnostic]) { + var diagnostics: [Diagnostic] = [] for clause in clauses { // If there is no condition, we have reached an unconditional clause. Return it. guard let condition = clause.condition else { - return clause + return (clause, diagnostics: diagnostics) } // If this condition evaluates true, return this clause. - let isActive = - (try? evaluateIfConfig( - condition: condition, - configuration: configuration, - diagnosticHandler: diagnosticHandler - ))?.active ?? false + let (isActive, _, localDiagnostics) = evaluateIfConfig( + condition: condition, + configuration: configuration + ) + diagnostics.append(contentsOf: localDiagnostics) + if isActive { - return clause + return (clause, diagnostics: diagnostics) } } - return nil + return (nil, diagnostics: diagnostics) } } diff --git a/Sources/SwiftIfConfig/IfConfigError.swift b/Sources/SwiftIfConfig/IfConfigError.swift index 47256debcbb..a0e33506c68 100644 --- a/Sources/SwiftIfConfig/IfConfigError.swift +++ b/Sources/SwiftIfConfig/IfConfigError.swift @@ -9,6 +9,7 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// + import SwiftDiagnostics import SwiftSyntax diff --git a/Sources/SwiftIfConfig/IfConfigEvaluation.swift b/Sources/SwiftIfConfig/IfConfigEvaluation.swift index bc9bb17b9ae..8faec1a7c31 100644 --- a/Sources/SwiftIfConfig/IfConfigEvaluation.swift +++ b/Sources/SwiftIfConfig/IfConfigEvaluation.swift @@ -9,6 +9,7 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// + import SwiftDiagnostics import SwiftSyntax @@ -18,35 +19,38 @@ import SwiftSyntax /// folded according to the logical operators table. /// - configuration: The configuration against which the condition will be /// evaluated. -/// - diagnosticHandler: Receives any diagnostics that are produced by the -/// evaluation, whether from errors in the source code or produced by the -/// build configuration itself. /// - Throws: Throws if an error occurs occur during evaluation that prevents /// this function from forming a valid result. The error will /// also be provided to the diagnostic handler before doing so. -/// - Returns: A pair of Boolean values. The first describes whether the -/// condition holds with the given build configuration. The second whether +/// - Returns: A pair of Boolean values and any diagnostics produced during the +/// evaluation. The first Boolean describes whether the condition holds with +/// the given build configuration. The second Boolean described whether /// the build condition is a "versioned" check that implies that we shouldn't /// diagnose syntax errors in blocks where the check fails. func evaluateIfConfig( condition: ExprSyntax, - configuration: some BuildConfiguration, - diagnosticHandler: ((Diagnostic) -> Void)? -) throws -> (active: Bool, versioned: Bool) { - /// Record the error before returning it. Use this for every 'throw' site - /// in this evaluation. - func recordedError(_ error: any Error, at node: some SyntaxProtocol) -> any Error { - if let diagnosticHandler { - error.asDiagnostics(at: node).forEach { diagnosticHandler($0) } - } - - return error + configuration: some BuildConfiguration +) -> (active: Bool, versioned: Bool, diagnostics: [Diagnostic]) { + var extraDiagnostics: [Diagnostic] = [] + + /// Record the error before returning the given value. + func recordError( + _ error: any Error, + at node: some SyntaxProtocol + ) -> (active: Bool, versioned: Bool, diagnostics: [Diagnostic]) { + return ( + active: false, + versioned: true, + diagnostics: extraDiagnostics + error.asDiagnostics(at: node) + ) } /// Record an if-config evaluation error before returning it. Use this for /// every 'throw' site in this evaluation. - func recordedError(_ error: IfConfigError) -> any Error { - return recordedError(error, at: error.syntax) + func recordError( + _ error: IfConfigError + ) -> (active: Bool, versioned: Bool, diagnostics: [Diagnostic]) { + return recordError(error, at: error.syntax) } /// Check a configuration condition, translating any thrown error into an @@ -54,17 +58,22 @@ func evaluateIfConfig( func checkConfiguration( at node: some SyntaxProtocol, body: () throws -> (Bool, Bool) - ) throws -> (active: Bool, versioned: Bool) { + ) -> (active: Bool, versioned: Bool, diagnostics: [Diagnostic]) { do { - return try body() + let (active, versioned) = try body() + return (active, versioned, extraDiagnostics) } catch let error { - throw recordedError(error, at: node) + return recordError(error, at: node) } } // Boolean literals evaluate as-is if let boolLiteral = condition.as(BooleanLiteralExprSyntax.self) { - return (active: boolLiteral.literalValue, versioned: false) + return ( + active: boolLiteral.literalValue, + versioned: false, + diagnostics: extraDiagnostics + ) } // Integer literals aren't allowed, but we recognize them. @@ -73,14 +82,16 @@ func evaluateIfConfig( { let result = intLiteral.literal.text == "1" - diagnosticHandler?( - IfConfigError.integerLiteralCondition( - syntax: condition, - replacement: result - ).asDiagnostic + return ( + active: result, + versioned: false, + diagnostics: [ + IfConfigError.integerLiteralCondition( + syntax: condition, + replacement: result + ).asDiagnostic + ] ) - - return (active: result, versioned: false) } // Declaration references are for custom compilation flags. @@ -89,7 +100,7 @@ func evaluateIfConfig( let ident = identExpr.baseName.text // Evaluate the custom condition. If the build configuration cannot answer this query, fail. - return try checkConfiguration(at: identExpr) { + return checkConfiguration(at: identExpr) { (active: try configuration.isCustomConditionSet(name: ident), versioned: false) } } @@ -98,13 +109,12 @@ func evaluateIfConfig( if let prefixOp = condition.as(PrefixOperatorExprSyntax.self), prefixOp.operator.text == "!" { - let (innerActive, innerVersioned) = try evaluateIfConfig( + let (innerActive, innerVersioned, innerDiagnostics) = evaluateIfConfig( condition: prefixOp.expression, - configuration: configuration, - diagnosticHandler: diagnosticHandler + configuration: configuration ) - return (active: !innerActive, versioned: innerVersioned) + return (active: !innerActive, versioned: innerVersioned, diagnostics: innerDiagnostics) } // Logical '&&' and '||'. @@ -113,40 +123,43 @@ func evaluateIfConfig( (op.operator.text == "&&" || op.operator.text == "||") { // Evaluate the left-hand side. - let (lhsActive, lhsVersioned) = try evaluateIfConfig( + let (lhsActive, lhsVersioned, lhsDiagnostics) = evaluateIfConfig( condition: binOp.leftOperand, - configuration: configuration, - diagnosticHandler: diagnosticHandler + configuration: configuration ) // Short-circuit evaluation if we know the answer and the left-hand side // was versioned. if lhsVersioned { switch (lhsActive, op.operator.text) { - case (true, "||"): return (active: true, versioned: lhsVersioned) - case (false, "&&"): return (active: false, versioned: lhsVersioned) - default: break + case (true, "||"): + return (active: true, versioned: lhsVersioned, diagnostics: lhsDiagnostics) + case (false, "&&"): + return (active: false, versioned: lhsVersioned, diagnostics: lhsDiagnostics) + default: + break } } // Evaluate the right-hand side. - let (rhsActive, rhsVersioned) = try evaluateIfConfig( + let (rhsActive, rhsVersioned, rhsDiagnostics) = evaluateIfConfig( condition: binOp.rightOperand, - configuration: configuration, - diagnosticHandler: diagnosticHandler + configuration: configuration ) switch op.operator.text { case "||": return ( active: lhsActive || rhsActive, - versioned: lhsVersioned && rhsVersioned + versioned: lhsVersioned && rhsVersioned, + diagnostics: lhsDiagnostics + rhsDiagnostics ) case "&&": return ( active: lhsActive && rhsActive, - versioned: lhsVersioned || rhsVersioned + versioned: lhsVersioned || rhsVersioned, + diagnostics: lhsDiagnostics + rhsDiagnostics ) default: @@ -158,10 +171,9 @@ func evaluateIfConfig( if let tuple = condition.as(TupleExprSyntax.self), tuple.isParentheses, let element = tuple.elements.first { - return try evaluateIfConfig( + return evaluateIfConfig( condition: element.expression, - configuration: configuration, - diagnosticHandler: diagnosticHandler + configuration: configuration ) } @@ -174,17 +186,17 @@ func evaluateIfConfig( func doSingleIdentifierArgumentCheck( _ body: (String) throws -> Bool, role: String - ) throws -> (active: Bool, versioned: Bool) { + ) -> (active: Bool, versioned: Bool, diagnostics: [Diagnostic]) { // Ensure that we have a single argument that is a simple identifier. guard let argExpr = call.arguments.singleUnlabeledExpression, let arg = argExpr.simpleIdentifierExpr else { - throw recordedError( + return recordError( .requiresUnlabeledArgument(name: fnName, role: role, syntax: ExprSyntax(call)) ) } - return try checkConfiguration(at: argExpr) { + return checkConfiguration(at: argExpr) { (active: try body(arg), versioned: fn.isVersioned) } } @@ -192,13 +204,13 @@ func evaluateIfConfig( /// Perform a check for a version constraint as used in the "swift" or "compiler" version checks. func doVersionComparisonCheck( _ actualVersion: VersionTuple - ) throws -> (active: Bool, versioned: Bool) { + ) -> (active: Bool, versioned: Bool, diagnostics: [Diagnostic]) { // Ensure that we have a single unlabeled argument that is either >= or < as a prefix // operator applied to a version. guard let argExpr = call.arguments.singleUnlabeledExpression, let unaryArg = argExpr.as(PrefixOperatorExprSyntax.self) else { - throw recordedError( + return recordError( .requiresUnlabeledArgument( name: fnName, role: "version comparison (>= or <= a version)", @@ -210,40 +222,48 @@ func evaluateIfConfig( // Parse the version. let opToken = unaryArg.operator guard let version = VersionTuple(parsing: unaryArg.expression.trimmedDescription) else { - throw recordedError(.invalidVersionOperand(name: fnName, syntax: unaryArg.expression)) + return recordError(.invalidVersionOperand(name: fnName, syntax: unaryArg.expression)) } switch opToken.text { case ">=": - return (active: actualVersion >= version, versioned: fn.isVersioned) + return ( + active: actualVersion >= version, + versioned: fn.isVersioned, + diagnostics: extraDiagnostics + ) case "<": - return (active: actualVersion < version, versioned: fn.isVersioned) + return ( + active: actualVersion < version, + versioned: fn.isVersioned, + diagnostics: extraDiagnostics + ) default: - throw recordedError(.unsupportedVersionOperator(name: fnName, operator: opToken)) + return recordError(.unsupportedVersionOperator(name: fnName, operator: opToken)) } } switch fn { case .hasAttribute: - return try doSingleIdentifierArgumentCheck(configuration.hasAttribute, role: "attribute") + return doSingleIdentifierArgumentCheck(configuration.hasAttribute, role: "attribute") case .hasFeature: - return try doSingleIdentifierArgumentCheck(configuration.hasFeature, role: "feature") + return doSingleIdentifierArgumentCheck(configuration.hasFeature, role: "feature") case .os: - return try doSingleIdentifierArgumentCheck(configuration.isActiveTargetOS, role: "operating system") + return doSingleIdentifierArgumentCheck(configuration.isActiveTargetOS, role: "operating system") case .arch: - return try doSingleIdentifierArgumentCheck(configuration.isActiveTargetArchitecture, role: "architecture") + return doSingleIdentifierArgumentCheck(configuration.isActiveTargetArchitecture, role: "architecture") case .targetEnvironment: - return try doSingleIdentifierArgumentCheck(configuration.isActiveTargetEnvironment, role: "environment") + return doSingleIdentifierArgumentCheck(configuration.isActiveTargetEnvironment, role: "environment") case ._runtime: - return try doSingleIdentifierArgumentCheck(configuration.isActiveTargetRuntime, role: "runtime") + return doSingleIdentifierArgumentCheck(configuration.isActiveTargetRuntime, role: "runtime") case ._ptrauth: - return try doSingleIdentifierArgumentCheck( + return doSingleIdentifierArgumentCheck( configuration.isActiveTargetPointerAuthentication, role: "pointer authentication scheme" ) @@ -255,7 +275,7 @@ func evaluateIfConfig( let arg = argExpr.simpleIdentifierExpr, let expectedEndianness = Endianness(rawValue: arg) else { - throw recordedError( + return recordError( .requiresUnlabeledArgument( name: fnName, role: "endiannes ('big' or 'little')", @@ -266,7 +286,8 @@ func evaluateIfConfig( return ( active: configuration.endianness == expectedEndianness, - versioned: fn.isVersioned + versioned: fn.isVersioned, + diagnostics: extraDiagnostics ) case ._pointerBitWidth, ._hasAtomicBitWidth: @@ -278,7 +299,7 @@ func evaluateIfConfig( argFirst == "_", let expectedBitWidth = Int(arg.dropFirst()) else { - throw recordedError( + return recordError( .requiresUnlabeledArgument( name: fnName, role: "bit width ('_' followed by an integer)", @@ -296,13 +317,13 @@ func evaluateIfConfig( fatalError("extraneous case above not handled") } - return (active: active, versioned: fn.isVersioned) + return (active: active, versioned: fn.isVersioned, diagnostics: extraDiagnostics) case .swift: - return try doVersionComparisonCheck(configuration.languageVersion) + return doVersionComparisonCheck(configuration.languageVersion) case .compiler: - return try doVersionComparisonCheck(configuration.compilerVersion) + return doVersionComparisonCheck(configuration.compilerVersion) case ._compiler_version: // Argument is a single unlabeled argument containing a string @@ -313,7 +334,7 @@ func evaluateIfConfig( let segment = stringLiteral.segments.first, case .stringSegment(let stringSegment) = segment else { - throw recordedError( + return recordError( .requiresUnlabeledArgument( name: "_compiler_version", role: "version", @@ -323,11 +344,17 @@ func evaluateIfConfig( } let versionString = stringSegment.content.text - let expectedVersion = try VersionTuple(parsingCompilerBuildVersion: versionString, argExpr) + let expectedVersion: VersionTuple + do { + expectedVersion = try VersionTuple(parsingCompilerBuildVersion: versionString, argExpr) + } catch { + return recordError(error, at: stringSegment.content) + } return ( active: configuration.compilerVersion >= expectedVersion, - versioned: fn.isVersioned + versioned: fn.isVersioned, + diagnostics: extraDiagnostics ) case .canImport: @@ -336,7 +363,7 @@ func evaluateIfConfig( guard let firstArg = call.arguments.first, firstArg.label == nil else { - throw recordedError(.canImportMissingModule(syntax: ExprSyntax(call))) + return recordError(.canImportMissingModule(syntax: ExprSyntax(call))) } // FIXME: This is a gross hack. Actually look at the sequence of @@ -348,7 +375,7 @@ func evaluateIfConfig( let version: CanImportVersion if let secondArg = call.arguments.dropFirst().first { if secondArg.label?.text != "_version" && secondArg.label?.text != "_underlyingVersion" { - throw recordedError(.canImportLabel(syntax: secondArg.expression)) + return recordError(.canImportLabel(syntax: secondArg.expression)) } let versionText: String @@ -363,7 +390,7 @@ func evaluateIfConfig( } guard var versionTuple = VersionTuple(parsing: versionText) else { - throw recordedError( + return recordError( .invalidVersionOperand(name: "canImport", syntax: secondArg.expression) ) } @@ -374,7 +401,7 @@ func evaluateIfConfig( versionTuple.components.removeSubrange(4...) // Warn that we did this. - diagnosticHandler?( + extraDiagnostics.append( IfConfigError.ignoredTrailingComponents( version: versionTuple, syntax: secondArg.expression @@ -390,13 +417,13 @@ func evaluateIfConfig( } if call.arguments.count > 2 { - throw recordedError(.canImportTwoParameters(syntax: ExprSyntax(call))) + return recordError(.canImportTwoParameters(syntax: ExprSyntax(call))) } } else { version = .unversioned } - return try checkConfiguration(at: call) { + return checkConfiguration(at: call) { ( active: try configuration.canImport( importPath: importPath.map { String($0) }, @@ -408,24 +435,24 @@ func evaluateIfConfig( } } - throw recordedError(.unknownExpression(condition)) + return recordError(.unknownExpression(condition)) } extension IfConfigClauseSyntax { /// Determine whether this condition is "versioned". func isVersioned( - configuration: some BuildConfiguration, - diagnosticHandler: ((Diagnostic) -> Void)? - ) throws -> Bool { - guard let condition else { return false } + configuration: some BuildConfiguration + ) -> (versioned: Bool, diagnostics: [Diagnostic]) { + guard let condition else { + return (versioned: false, diagnostics: []) + } // Evaluate this condition against the build configuration. - let (_, versioned) = try evaluateIfConfig( + let (_, versioned, diagnostics) = evaluateIfConfig( condition: condition, - configuration: configuration, - diagnosticHandler: diagnosticHandler + configuration: configuration ) - return versioned + return (versioned, diagnostics) } } diff --git a/Sources/SwiftIfConfig/ConfiguredRegionState.swift b/Sources/SwiftIfConfig/IfConfigRegionState.swift similarity index 58% rename from Sources/SwiftIfConfig/ConfiguredRegionState.swift rename to Sources/SwiftIfConfig/IfConfigRegionState.swift index 5596ea6e1d0..af6965de5ed 100644 --- a/Sources/SwiftIfConfig/ConfiguredRegionState.swift +++ b/Sources/SwiftIfConfig/IfConfigRegionState.swift @@ -9,12 +9,13 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// + import SwiftDiagnostics import SwiftOperators import SwiftSyntax /// Describes the state of a particular region guarded by `#if` or similar. -public enum ConfiguredRegionState { +public enum IfConfigRegionState { /// The region is not part of the compiled program and is not even parsed, /// and therefore many contain syntax that is invalid. case unparsed @@ -23,29 +24,29 @@ public enum ConfiguredRegionState { /// The region is active and is part of the compiled program. case active - /// Evaluate the given `#if` condition using the given build configuration, throwing an error if there is - /// insufficient information to make a determination. - public init( - condition: some ExprSyntaxProtocol, - configuration: some BuildConfiguration, - diagnosticHandler: ((Diagnostic) -> Void)? = nil - ) throws { + /// Evaluate the given `#if` condition using the given build configuration + /// to determine its state and identify any problems encountered along the + /// way. + public static func evaluating( + _ condition: some ExprSyntaxProtocol, + in configuration: some BuildConfiguration + ) -> (state: IfConfigRegionState, diagnostics: [Diagnostic]) { // Apply operator folding for !/&&/||. - let foldedCondition = try OperatorTable.logicalOperators.foldAll(condition) { error in - diagnosticHandler?(error.asDiagnostic) - throw error + var foldingDiagnostics: [Diagnostic] = [] + let foldedCondition = OperatorTable.logicalOperators.foldAll(condition) { error in + foldingDiagnostics.append(contentsOf: error.asDiagnostics(at: condition)) }.cast(ExprSyntax.self) - let (active, versioned) = try evaluateIfConfig( + let (active, versioned, evalDiagnostics) = evaluateIfConfig( condition: foldedCondition, - configuration: configuration, - diagnosticHandler: diagnosticHandler + configuration: configuration ) + let diagnostics = foldingDiagnostics + evalDiagnostics switch (active, versioned) { - case (true, _): self = .active - case (false, false): self = .inactive - case (false, true): self = .unparsed + case (true, _): return (.active, diagnostics) + case (false, false): return (.inactive, diagnostics) + case (false, true): return (.unparsed, diagnostics) } } } diff --git a/Sources/SwiftIfConfig/SwiftIfConfig.docc/SwiftIfConfig.md b/Sources/SwiftIfConfig/SwiftIfConfig.docc/SwiftIfConfig.md index 15e3922b0cf..cc1d40a57c4 100644 --- a/Sources/SwiftIfConfig/SwiftIfConfig.docc/SwiftIfConfig.md +++ b/Sources/SwiftIfConfig/SwiftIfConfig.docc/SwiftIfConfig.md @@ -1,4 +1,4 @@ -# `SwiftIfConfig` +# SwiftIfConfig A library to evaluate `#if` conditionals within a Swift syntax tree. @@ -27,7 +27,7 @@ The syntax tree and its parser do not reason about the build configuration. Rath The `SwiftIfConfig` library provides utilities to determine which syntax nodes are part of a particular build configuration. Each utility requires that one provide a specific build configuration (i.e., an instance of a type that conforms to the protocol), and provides a different view on essentially the same information: * and are visitor types that only visit the syntax nodes that are included ("active") for a given build configuration, implicitly skipping any nodes within inactive `#if` clauses. -* `SyntaxProtocol/removingInactive(in:)` produces a syntax node that removes all inactive regions (and their corresponding `IfConfigDeclSyntax` nodes) from the given syntax tree, returning a new tree that is free of `#if` conditions. +* `SyntaxProtocol.removingInactive(in:)` produces a syntax node that removes all inactive regions (and their corresponding `IfConfigDeclSyntax` nodes) from the given syntax tree, returning a new tree that is free of `#if` conditions. * `IfConfigDeclSyntax.activeClause(in:)` determines which of the clauses of an `#if` is active for the given build configuration, returning the active clause. * `SyntaxProtocol.isActive(in:)` determines whether the given syntax node is active for the given build configuration. The result is one of "active" (the node is included in the program), "inactive" (the node is not included diff --git a/Sources/SwiftIfConfig/SyntaxLiteralUtils.swift b/Sources/SwiftIfConfig/SyntaxLiteralUtils.swift index 85b946066e7..ebe6ec9af69 100644 --- a/Sources/SwiftIfConfig/SyntaxLiteralUtils.swift +++ b/Sources/SwiftIfConfig/SyntaxLiteralUtils.swift @@ -9,6 +9,7 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// + import SwiftSyntax extension BooleanLiteralExprSyntax { diff --git a/Sources/SwiftIfConfig/SyntaxProtocol+IfConfig.swift b/Sources/SwiftIfConfig/SyntaxProtocol+IfConfig.swift index e2b012dfd68..12c6dc2c5d2 100644 --- a/Sources/SwiftIfConfig/SyntaxProtocol+IfConfig.swift +++ b/Sources/SwiftIfConfig/SyntaxProtocol+IfConfig.swift @@ -9,6 +9,7 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// + import SwiftDiagnostics import SwiftSyntax @@ -30,35 +31,33 @@ extension SyntaxProtocol { /// a call to `isActive` on the syntax node for the function `g` would return `active` when the /// configuration options `DEBUG` and `B` are provided, but `A` is not. public func isActive( - in configuration: some BuildConfiguration, - diagnosticHandler: ((Diagnostic) -> Void)? = nil - ) throws -> ConfiguredRegionState { + in configuration: some BuildConfiguration + ) -> (state: IfConfigRegionState, diagnostics: [Diagnostic]) { var currentNode: Syntax = Syntax(self) - var currentState: ConfiguredRegionState = .active + var currentState: IfConfigRegionState = .active + var diagnostics: [Diagnostic] = [] while let parent = currentNode.parent { // If the parent is an `#if` configuration, check whether our current // clause is active. If not, we're in an inactive region. We also - // need to determine whether + // need to determine whether an inactive region should be parsed or not. if let ifConfigClause = currentNode.as(IfConfigClauseSyntax.self), let ifConfigDecl = ifConfigClause.parent?.parent?.as(IfConfigDeclSyntax.self) { - let activeClause = ifConfigDecl.activeClause( - in: configuration, - diagnosticHandler: diagnosticHandler - ) + let (activeClause, localDiagnostics) = ifConfigDecl.activeClause(in: configuration) + diagnostics.append(contentsOf: localDiagnostics) if activeClause != ifConfigClause { // This was not the active clause, so we know that we're in an // inactive block. However, if the condition is versioned, this is an // unparsed region. - let isVersioned = - (try? ifConfigClause.isVersioned( - configuration: configuration, - diagnosticHandler: diagnosticHandler - )) ?? true + let (isVersioned, localDiagnostics) = ifConfigClause.isVersioned( + configuration: configuration + ) + diagnostics.append(contentsOf: localDiagnostics) + if isVersioned { - return .unparsed + return (.unparsed, diagnostics) } currentState = .inactive @@ -68,18 +67,19 @@ extension SyntaxProtocol { currentNode = parent } - return currentState + return (currentState, diagnostics) } /// Determine whether the given syntax node is active given a set of /// configured regions as produced by `configuredRegions(in:)`. /// - /// This is - /// an approximation + /// If you are querying whether many syntax nodes in a particular file are + /// active, consider calling `configuredRegions(in:)` once and using + /// this function. For occasional queries, use `isActive(in:)`. public func isActive( - inConfiguredRegions regions: [(IfConfigClauseSyntax, ConfiguredRegionState)] - ) -> ConfiguredRegionState { - var currentState: ConfiguredRegionState = .active + inConfiguredRegions regions: [(IfConfigClauseSyntax, IfConfigRegionState)] + ) -> IfConfigRegionState { + var currentState: IfConfigRegionState = .active for (ifClause, state) in regions { if self.position < ifClause.position { return currentState diff --git a/Sources/SwiftIfConfig/VersionTuple+Parsing.swift b/Sources/SwiftIfConfig/VersionTuple+Parsing.swift index 03cad60653b..c8dc16eb8ec 100644 --- a/Sources/SwiftIfConfig/VersionTuple+Parsing.swift +++ b/Sources/SwiftIfConfig/VersionTuple+Parsing.swift @@ -9,6 +9,7 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// + import SwiftSyntax extension VersionTuple { diff --git a/Sources/SwiftLexicalLookup/Configurations/FileScopeNameIntroductionStrategy.swift b/Sources/SwiftLexicalLookup/Configurations/FileScopeHandlingConfig.swift similarity index 81% rename from Sources/SwiftLexicalLookup/Configurations/FileScopeNameIntroductionStrategy.swift rename to Sources/SwiftLexicalLookup/Configurations/FileScopeHandlingConfig.swift index 743f997218c..34c0861a91e 100644 --- a/Sources/SwiftLexicalLookup/Configurations/FileScopeNameIntroductionStrategy.swift +++ b/Sources/SwiftLexicalLookup/Configurations/FileScopeHandlingConfig.swift @@ -14,12 +14,10 @@ import SwiftSyntax /// Specifies how names should be introduced at the file scope. @_spi(Experimental) public enum FileScopeHandlingConfig { - /// This is the behavior that is being used + /// This is the behavior that is being used /// for Swift files with top-level code. case memberBlockUpToLastDecl - /// This is the behavior that is being used + /// This is the behavior that is being used /// for Swift files that don’t allow top-level code. case memberBlock - /// This is the behavior that is being used for e.g. function bodies. - case codeBlock } diff --git a/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift b/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift index 9be86f972ba..9a23170a74d 100644 --- a/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift @@ -16,7 +16,7 @@ protocol IntroducingToSequentialParentScopeSyntax: ScopeSyntax { /// Returns names matching lookup that should be /// handled by it's parent sequential scope. func introducesToSequentialParent( - for name: String?, + for identifier: Identifier?, at syntax: SyntaxProtocol, with config: LookupConfig, state: LookupState diff --git a/Sources/SwiftLexicalLookup/LookupName.swift b/Sources/SwiftLexicalLookup/LookupName.swift index b2caf3e40c6..6be060fe68a 100644 --- a/Sources/SwiftLexicalLookup/LookupName.swift +++ b/Sources/SwiftLexicalLookup/LookupName.swift @@ -40,8 +40,8 @@ import SwiftSyntax } } - /// Used for name comparison. - var name: String { + /// Name associated with implicit name kind. + private var name: String { switch self { case .self: "self" @@ -55,6 +55,11 @@ import SwiftSyntax "oldValue" } } + + /// Identifier used for name comparison. + var identifier: Identifier { + Identifier(name) + } } @_spi(Experimental) public enum LookupName { @@ -63,8 +68,8 @@ import SwiftSyntax case identifier(IdentifiableSyntax, accessibleAfter: AbsolutePosition?) /// Declaration associated with the name. /// Could be class, struct, actor, protocol, function and more. - case declaration(NamedDeclSyntax, accessibleAfter: AbsolutePosition?) - /// Name introduced implicitly certain syntax nodes. + case declaration(NamedDeclSyntax) + /// Name introduced implicitly by certain syntax nodes. case implicit(LookupImplicitNameKind) /// Syntax associated with this name. @@ -72,22 +77,22 @@ import SwiftSyntax switch self { case .identifier(let syntax, _): syntax - case .declaration(let syntax, _): + case .declaration(let syntax): syntax case .implicit(let implicitName): implicitName.syntax } } - /// Introduced name. + /// Identifier used for name comparison. @_spi(Experimental) public var identifier: Identifier? { switch self { case .identifier(let syntax, _): Identifier(syntax.identifier) - case .declaration(let syntax, _): + case .declaration(let syntax): Identifier(syntax.name) - default: - nil + case .implicit(let kind): + kind.identifier } } @@ -95,20 +100,10 @@ import SwiftSyntax /// If set to `nil`, the name is available at any point in scope. var accessibleAfter: AbsolutePosition? { switch self { - case .identifier(_, let absolutePosition), .declaration(_, let absolutePosition): - absolutePosition + case .identifier(_, let absolutePosition): + return absolutePosition default: - nil - } - } - - /// Used for name comparison. - var name: String? { - switch self { - case .identifier, .declaration: - identifier?.name - case .implicit(let implicitName): - implicitName.name + return nil } } @@ -119,9 +114,9 @@ import SwiftSyntax } /// Checks if this name refers to the looked up phrase. - func refersTo(_ lookedUpName: String) -> Bool { - guard let name else { return false } - return name == lookedUpName + func refersTo(_ lookedUpIdentifier: Identifier) -> Bool { + guard let identifier else { return false } + return identifier == lookedUpIdentifier } /// Extracts names introduced by the given `syntax` structure. @@ -138,38 +133,41 @@ import SwiftSyntax ) -> [LookupName] { switch Syntax(syntax).as(SyntaxEnum.self) { case .variableDecl(let variableDecl): - variableDecl.bindings.flatMap { binding in - getNames(from: binding.pattern, accessibleAfter: accessibleAfter != nil ? binding.endPositionBeforeTrailingTrivia : nil) + return variableDecl.bindings.flatMap { binding in + getNames( + from: binding.pattern, + accessibleAfter: accessibleAfter != nil ? binding.endPositionBeforeTrailingTrivia : nil + ) } case .tuplePattern(let tuplePattern): - tuplePattern.elements.flatMap { tupleElement in + return tuplePattern.elements.flatMap { tupleElement in getNames(from: tupleElement.pattern, accessibleAfter: accessibleAfter) } case .valueBindingPattern(let valueBindingPattern): - getNames(from: valueBindingPattern.pattern, accessibleAfter: accessibleAfter) + return getNames(from: valueBindingPattern.pattern, accessibleAfter: accessibleAfter) case .expressionPattern(let expressionPattern): - getNames(from: expressionPattern.expression, accessibleAfter: accessibleAfter) + return getNames(from: expressionPattern.expression, accessibleAfter: accessibleAfter) case .sequenceExpr(let sequenceExpr): - sequenceExpr.elements.flatMap { expression in + return sequenceExpr.elements.flatMap { expression in getNames(from: expression, accessibleAfter: accessibleAfter) } case .patternExpr(let patternExpr): - getNames(from: patternExpr.pattern, accessibleAfter: accessibleAfter) + return getNames(from: patternExpr.pattern, accessibleAfter: accessibleAfter) case .optionalBindingCondition(let optionalBinding): - getNames(from: optionalBinding.pattern, accessibleAfter: accessibleAfter) + return getNames(from: optionalBinding.pattern, accessibleAfter: accessibleAfter) case .matchingPatternCondition(let matchingPatternCondition): - getNames(from: matchingPatternCondition.pattern, accessibleAfter: accessibleAfter) + return getNames(from: matchingPatternCondition.pattern, accessibleAfter: accessibleAfter) case .functionCallExpr(let functionCallExpr): - functionCallExpr.arguments.flatMap { argument in + return functionCallExpr.arguments.flatMap { argument in getNames(from: argument.expression, accessibleAfter: accessibleAfter) } default: if let namedDecl = Syntax(syntax).asProtocol(SyntaxProtocol.self) as? NamedDeclSyntax { - handle(namedDecl: namedDecl, accessibleAfter: accessibleAfter) + return handle(namedDecl: namedDecl, accessibleAfter: accessibleAfter) } else if let identifiable = Syntax(syntax).asProtocol(SyntaxProtocol.self) as? IdentifiableSyntax { - handle(identifiable: identifiable, accessibleAfter: accessibleAfter) + return handle(identifiable: identifiable, accessibleAfter: accessibleAfter) } else { - [] + return [] } } } @@ -195,6 +193,6 @@ import SwiftSyntax namedDecl: NamedDeclSyntax, accessibleAfter: AbsolutePosition? = nil ) -> [LookupName] { - [.declaration(namedDecl, accessibleAfter: accessibleAfter)] + [.declaration(namedDecl)] } } diff --git a/Sources/SwiftLexicalLookup/LookupResult.swift b/Sources/SwiftLexicalLookup/LookupResult.swift index 6fabf2060dc..3fb4c30b046 100644 --- a/Sources/SwiftLexicalLookup/LookupResult.swift +++ b/Sources/SwiftLexicalLookup/LookupResult.swift @@ -23,9 +23,9 @@ import SwiftSyntax @_spi(Experimental) public var scope: ScopeSyntax? { switch self { case .fromScope(let scopeSyntax, _): - scopeSyntax + return scopeSyntax case .fromFileScope(let fileScopeSyntax, _): - fileScopeSyntax + return fileScopeSyntax } } @@ -33,7 +33,7 @@ import SwiftSyntax @_spi(Experimental) public var names: [LookupName] { switch self { case .fromScope(_, let names), .fromFileScope(_, let names): - names + return names } } } diff --git a/Sources/SwiftLexicalLookup/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/ScopeImplementations.swift index 85838bc0abe..2764e057904 100644 --- a/Sources/SwiftLexicalLookup/ScopeImplementations.swift +++ b/Sources/SwiftLexicalLookup/ScopeImplementations.swift @@ -14,11 +14,11 @@ import SwiftSyntax @_spi(Experimental) extension SyntaxProtocol { /// Parent scope of this syntax node, or scope introduced by this syntax node. - @_spi(Experimental) public var scope: ScopeSyntax? { + var scope: ScopeSyntax? { if let scopeSyntax = Syntax(self).asProtocol(SyntaxProtocol.self) as? ScopeSyntax { - scopeSyntax + return scopeSyntax } else { - self.parent?.scope + return self.parent?.scope } } } @@ -69,10 +69,6 @@ import SwiftSyntax } } } - case .codeBlock: - return statements.flatMap { codeBlockItem in - LookupName.getNames(from: codeBlockItem.item, accessibleAfter: codeBlockItem.endPosition) - } case .memberBlock: return statements.flatMap { codeBlockItem in LookupName.getNames(from: codeBlockItem.item) @@ -101,25 +97,16 @@ import SwiftSyntax /// - for `memberBlock` - a, b, c, d, e, f /// - for `codeBlock` - a @_spi(Experimental) public func lookup( - for name: String?, + for identifier: Identifier?, at syntax: SyntaxProtocol, with config: LookupConfig, state: LookupState ) -> [LookupResult] { switch config.fileScopeHandling { - case .codeBlock: - return sequentialLookup( - in: statements, - for: name, - at: syntax, - with: config, - state: state, - createResultsForThisScopeWith: { .fromFileScope(self, withNames: $0) } - ) case .memberBlock: let names = introducedNames(using: .memberBlock) .filter { lookupName in - does(name: name, referTo: lookupName, at: syntax) + checkName(identifier, refersTo: lookupName, at: syntax) } return names.isEmpty ? [] : [.fromFileScope(self, withNames: names)] @@ -137,7 +124,7 @@ import SwiftSyntax if item.is(DeclSyntax.self) || item.is(VariableDeclSyntax.self) { let foundNames = LookupName.getNames(from: item) - members.append(contentsOf: foundNames.filter { does(name: name, referTo: $0, at: syntax) }) + members.append(contentsOf: foundNames.filter { checkName(identifier, refersTo: $0, at: syntax) }) } else { encounteredNonDeclaration = true sequentialItems.append(codeBlockItem) @@ -147,7 +134,7 @@ import SwiftSyntax let sequentialNames = sequentialLookup( in: sequentialItems, - for: name, + for: identifier, at: syntax, with: config, state: state, @@ -169,14 +156,14 @@ import SwiftSyntax } @_spi(Experimental) public func lookup( - for name: String?, + for identifier: Identifier?, at syntax: SyntaxProtocol, with config: LookupConfig, state: LookupState ) -> [LookupResult] { sequentialLookup( in: statements, - for: name, + for: identifier, at: syntax, with: config, state: state, @@ -291,15 +278,15 @@ import SwiftSyntax /// } /// ``` @_spi(Experimental) public func lookup( - for name: String?, + for identifier: Identifier?, at syntax: SyntaxProtocol, with config: LookupConfig, state: LookupState ) -> [LookupResult] { if let elseBody, elseBody.position <= syntax.position, elseBody.endPosition >= syntax.position { - lookupInParent(for: name, at: syntax, with: config, state: state) + return lookupInParent(for: identifier, at: syntax, with: config, state: state) } else { - defaultLookupImplementation(for: name, at: syntax, with: config, state: state) + return defaultLookupImplementation(for: identifier, at: syntax, with: config, state: state) } } } @@ -315,7 +302,7 @@ import SwiftSyntax @_spi(Experimental) extension GuardStmtSyntax: IntroducingToSequentialParentScopeSyntax { @_spi(Experimental) public func introducesToSequentialParent( - for name: String?, + for identifier: Identifier?, at syntax: SwiftSyntax.SyntaxProtocol, with config: LookupConfig, state: LookupState @@ -323,7 +310,7 @@ import SwiftSyntax let names = conditions.flatMap { element in LookupName.getNames(from: element.condition, accessibleAfter: element.endPosition) }.filter { introducedName in - does(name: name, referTo: introducedName, at: syntax) + checkName(identifier, refersTo: introducedName, at: syntax) } return names.isEmpty ? [] : [.fromScope(self, withNames: names)] @@ -345,7 +332,7 @@ import SwiftSyntax /// // a is visible here /// ``` @_spi(Experimental) public func lookup( - for name: String?, + for identifier: Identifier?, at syntax: SyntaxProtocol, with config: LookupConfig, state: LookupState @@ -353,9 +340,9 @@ import SwiftSyntax if body.position <= syntax.position && body.endPosition >= syntax.position { var newState = state newState.skipSequentialIntroductionFrom = self - return lookupInParent(for: name, at: syntax, with: config, state: newState) + return lookupInParent(for: identifier, at: syntax, with: config, state: newState) } else { - return defaultLookupImplementation(for: name, at: syntax, with: config, state: state) + return defaultLookupImplementation(for: identifier, at: syntax, with: config, state: state) } } } @@ -371,15 +358,15 @@ import SwiftSyntax /// withing the accessor.. @_spi(Experimental) public var introducedNames: [LookupName] { if let parameters { - LookupName.getNames(from: parameters) + return LookupName.getNames(from: parameters) } else { switch accessorSpecifier.tokenKind { case .keyword(.set), .keyword(.willSet): - [.implicit(.newValue(self))] + return [.implicit(.newValue(self))] case .keyword(.didSet): - [.implicit(.oldValue(self))] + return [.implicit(.oldValue(self))] default: - [] + return [] } } } diff --git a/Sources/SwiftLexicalLookup/ScopeSyntax.swift b/Sources/SwiftLexicalLookup/ScopeSyntax.swift index 8358378a446..ba02c85eb5e 100644 --- a/Sources/SwiftLexicalLookup/ScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/ScopeSyntax.swift @@ -16,8 +16,8 @@ extension SyntaxProtocol { /// Returns all names that `for` refers to at this syntax node. /// Optional configuration can be passed as `config` to customize the lookup behavior. /// - /// - Returns: An array of `LookupResult` for `name` at this syntax node, - /// ordered by visibility. If `name` is set to `nil`, returns all available names ordered by visibility. + /// - Returns: An array of `LookupResult` for `identifier` at this syntax node, + /// ordered by visibility. If `identifier` is set to `nil`, returns all available names ordered by visibility. /// The order is from the innermost to the outermost scope, /// and within each result, names are ordered by their introduction /// in the source code. @@ -44,10 +44,10 @@ extension SyntaxProtocol { /// in this exact order. The constant declaration within the function body is omitted /// due to the ordering rules that prioritize visibility within the function body. @_spi(Experimental) public func lookup( - for name: String?, + for identifier: Identifier?, with config: LookupConfig = LookupConfig() ) -> [LookupResult] { - scope?.lookup(for: name, at: self, with: config, state: LookupState()) ?? [] + scope?.lookup(for: identifier, at: self, with: config, state: LookupState()) ?? [] } } @@ -59,7 +59,7 @@ extension SyntaxProtocol { /// Finds all declarations `name` refers to. `syntax` specifies the node lookup was triggered with. /// If `name` set to `nil`, returns all available names at the given node. func lookup( - for name: String?, + for identifier: Identifier?, at syntax: SyntaxProtocol, with config: LookupConfig, state: LookupState @@ -75,19 +75,19 @@ extension SyntaxProtocol { /// refers to and is accessible at given syntax node then passes lookup to the parent. /// If `name` set to `nil`, returns all available names at the given node. @_spi(Experimental) public func lookup( - for name: String?, + for identifier: Identifier?, at syntax: SyntaxProtocol, with config: LookupConfig, state: LookupState ) -> [LookupResult] { - defaultLookupImplementation(for: name, at: syntax, with: config, state: state) + defaultLookupImplementation(for: identifier, at: syntax, with: config, state: state) } /// Returns `LookupResult` of all names introduced in this scope that `name` /// refers to and is accessible at given syntax node then passes lookup to the parent. /// If `name` set to `nil`, returns all available names at the given node. func defaultLookupImplementation( - for name: String?, + for identifier: Identifier?, at syntax: SyntaxProtocol, with config: LookupConfig, state: LookupState @@ -95,28 +95,28 @@ extension SyntaxProtocol { let filteredNames = introducedNames .filter { introducedName in - does(name: name, referTo: introducedName, at: syntax) + checkName(identifier, refersTo: introducedName, at: syntax) } if filteredNames.isEmpty { - return lookupInParent(for: name, at: syntax, with: config, state: state) + return lookupInParent(for: identifier, at: syntax, with: config, state: state) } else { return [.fromScope(self, withNames: filteredNames)] - + lookupInParent(for: name, at: syntax, with: config, state: state) + + lookupInParent(for: identifier, at: syntax, with: config, state: state) } } /// Looks up in parent scope. func lookupInParent( - for name: String?, + for identifier: Identifier?, at syntax: SyntaxProtocol, with config: LookupConfig, state: LookupState ) -> [LookupResult] { - parentScope?.lookup(for: name, at: syntax, with: config, state: state) ?? [] + parentScope?.lookup(for: identifier, at: syntax, with: config, state: state) ?? [] } - func does(name: String?, referTo introducedName: LookupName, at syntax: SyntaxProtocol) -> Bool { + func checkName(_ name: Identifier?, refersTo introducedName: LookupName, at syntax: SyntaxProtocol) -> Bool { introducedName.isAccessible(at: syntax) && (name == nil || introducedName.refersTo(name!)) } } diff --git a/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift index 16b7702fb2c..5300a9af311 100644 --- a/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift @@ -21,7 +21,7 @@ protocol SequentialScopeSyntax: ScopeSyntax { /// scopes that match the lookup. func sequentialLookup( in codeBlockItems: any Collection, - for name: String?, + for identifier: Identifier?, at syntax: SyntaxProtocol, with config: LookupConfig, state: LookupState, @@ -32,7 +32,7 @@ protocol SequentialScopeSyntax: ScopeSyntax { extension SequentialScopeSyntax { func sequentialLookup( in codeBlockItems: any Collection, - for name: String?, + for identifier: Identifier?, at syntax: SyntaxProtocol, with config: LookupConfig, state: LookupState, @@ -61,7 +61,7 @@ extension SequentialScopeSyntax { // Add names introduced by the encountered scope. result.append( contentsOf: introducingToParentScope.introducesToSequentialParent( - for: name, + for: identifier, at: syntax, with: config, state: state @@ -75,7 +75,7 @@ extension SequentialScopeSyntax { from: codeBlockItem.item, accessibleAfter: codeBlockItem.endPosition ).filter { introducedName in - does(name: name, referTo: introducedName, at: syntax) + checkName(identifier, refersTo: introducedName, at: syntax) } ) } @@ -87,6 +87,7 @@ extension SequentialScopeSyntax { currentChunk = [] } - return (result.isEmpty ? [] : result.reversed()) + lookupInParent(for: name, at: syntax, with: config, state: state) + return (result.isEmpty ? [] : result.reversed()) + + lookupInParent(for: identifier, at: syntax, with: config, state: state) } } diff --git a/Sources/SwiftRefactor/SyntaxUtils.swift b/Sources/SwiftRefactor/SyntaxUtils.swift index 4949a5bbff6..62a108c325c 100644 --- a/Sources/SwiftRefactor/SyntaxUtils.swift +++ b/Sources/SwiftRefactor/SyntaxUtils.swift @@ -11,7 +11,7 @@ //===----------------------------------------------------------------------===// #if swift(>=6) -public import SwiftSyntax +internal import SwiftSyntax #else import SwiftSyntax #endif diff --git a/Sources/SwiftSyntax/Identifier.swift b/Sources/SwiftSyntax/Identifier.swift index 0864e754744..ed0b511f0e1 100644 --- a/Sources/SwiftSyntax/Identifier.swift +++ b/Sources/SwiftSyntax/Identifier.swift @@ -12,23 +12,58 @@ /// A canonicalized representation of an identifier that strips away backticks. public struct Identifier: Equatable, Hashable, Sendable { - /// The sanitized `text` of a token. + enum IdentifierKind: Hashable { + case token(raw: RawIdentifier, arena: SyntaxArenaRef) + case string(String) + + static func sanitize(string: String) -> IdentifierKind { + let backtick = "`" + if string.count > 2 && string.hasPrefix(backtick) && string.hasSuffix(backtick) { + let startIndex = string.index(after: string.startIndex) + let endIndex = string.index(before: string.endIndex) + return .string(String(string[startIndex.. Bool { + lhs.name == rhs.name } } diff --git a/Sources/SwiftSyntaxBuilder/SyntaxNodeWithBody.swift b/Sources/SwiftSyntaxBuilder/SyntaxNodeWithBody.swift index 2a00a4f2dc3..6d0f2dae993 100644 --- a/Sources/SwiftSyntaxBuilder/SyntaxNodeWithBody.swift +++ b/Sources/SwiftSyntaxBuilder/SyntaxNodeWithBody.swift @@ -12,8 +12,10 @@ #if swift(>=6) public import SwiftSyntax +internal import SwiftParser #else import SwiftSyntax +import SwiftParser #endif // MARK: - PartialSyntaxNode @@ -124,7 +126,22 @@ extension WithOptionalCodeBlockSyntax where Self: DeclSyntaxProtocol { _ header: SyntaxNodeString, @CodeBlockItemListBuilder bodyBuilder: () throws -> CodeBlockItemListSyntax ) throws { - let decl = DeclSyntax("\(header) {}") + // If the type provides a custom `SyntaxParseable` implementation, use that. Otherwise construct it as a + // `DeclSyntax`. + // We cannot use normal string interpolation here because the conformance to `ExpressibleByStringInterpolation` is + // not implied by `SyntaxParsable` but generated for each type by + // `SyntaxExpressibleByStringInterpolationConformances.swift`. And we can’t use that protocol in the `as?` check + // because then the compiler complains that `parsableType` is not instantiable. So, manually do the same work that + // a string literal with interpolation segments would do. + let decl: DeclSyntax + var stringInterpolation = SyntaxStringInterpolation(literalCapacity: 1, interpolationCount: 1) + stringInterpolation.appendInterpolation(header) + stringInterpolation.appendLiteral(" {}") + if let parsableType = Self.self as? SyntaxParseable.Type { + decl = parsableType.init(stringInterpolation: stringInterpolation).cast(DeclSyntax.self) + } else { + decl = DeclSyntax(stringInterpolation: stringInterpolation) + } guard let castedDecl = decl.as(Self.self) else { throw SyntaxStringInterpolationInvalidNodeTypeError(expectedType: Self.self, actualNode: decl) } @@ -170,7 +187,17 @@ extension HasTrailingMemberDeclBlock where Self: DeclSyntaxProtocol { _ header: SyntaxNodeString, @MemberBlockItemListBuilder membersBuilder: () throws -> MemberBlockItemListSyntax ) throws { - let decl = DeclSyntax("\(header) {}") + // If the type provides a custom `SyntaxParseable` implementation, use that. Otherwise construct it as a + // `DeclSyntax`. + let decl: DeclSyntax + var stringInterpolation = SyntaxStringInterpolation(literalCapacity: 1, interpolationCount: 1) + stringInterpolation.appendInterpolation(header) + stringInterpolation.appendLiteral(" {}") + if let parsableType = Self.self as? SyntaxParseable.Type { + decl = parsableType.init(stringInterpolation: stringInterpolation).cast(DeclSyntax.self) + } else { + decl = DeclSyntax(stringInterpolation: stringInterpolation) + } guard let castedDecl = decl.as(Self.self) else { throw SyntaxStringInterpolationInvalidNodeTypeError(expectedType: Self.self, actualNode: decl) } diff --git a/Sources/SwiftSyntaxMacros/MacroProtocols/MemberMacro.swift b/Sources/SwiftSyntaxMacros/MacroProtocols/MemberMacro.swift index 4e021220b76..d79205049a3 100644 --- a/Sources/SwiftSyntaxMacros/MacroProtocols/MemberMacro.swift +++ b/Sources/SwiftSyntaxMacros/MacroProtocols/MemberMacro.swift @@ -81,6 +81,11 @@ extension MemberMacro { } /// Default implementation that ignores the unhandled conformances. + @available( + *, + deprecated, + message: "`MemberMacro` conformance should implement the `expansion` function that takes a `conformingTo` parameter" + ) public static func expansion( of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, diff --git a/Sources/SwiftSyntaxMacrosGenericTestSupport/Assertions.swift b/Sources/SwiftSyntaxMacrosGenericTestSupport/Assertions.swift index 2023ed6d55f..0ab97de0785 100644 --- a/Sources/SwiftSyntaxMacrosGenericTestSupport/Assertions.swift +++ b/Sources/SwiftSyntaxMacrosGenericTestSupport/Assertions.swift @@ -365,6 +365,7 @@ public enum DiagnosticAssertionContext { } } +@_spi(Testing) public func assertDiagnostic( _ diag: Diagnostic, in expansionContext: DiagnosticAssertionContext, diff --git a/Sources/_SwiftSyntaxCShims/include/AtomicBool.h b/Sources/_SwiftSyntaxCShims/include/AtomicBool.h index 1d456ad4943..afa3caf4db9 100644 --- a/Sources/_SwiftSyntaxCShims/include/AtomicBool.h +++ b/Sources/_SwiftSyntaxCShims/include/AtomicBool.h @@ -21,7 +21,7 @@ typedef struct { } AtomicBool; static inline AtomicBool *_Nonnull swiftsyntax_atomic_bool_create(bool initialValue) { - AtomicBool *atomic = malloc(sizeof(AtomicBool)); + AtomicBool *atomic = (AtomicBool *)malloc(sizeof(AtomicBool)); atomic->value = initialValue; return atomic; } diff --git a/Tests/PerformanceTest/ParsingPerformanceTests.swift b/Tests/PerformanceTest/ParsingPerformanceTests.swift index 3eff949de25..714ad0cfb6d 100644 --- a/Tests/PerformanceTest/ParsingPerformanceTests.swift +++ b/Tests/PerformanceTest/ParsingPerformanceTests.swift @@ -27,7 +27,7 @@ class ParsingPerformanceTests: XCTestCase { func testNativeParsingPerformance() throws { try XCTSkipIf(longTestsDisabled) - let source = try String(contentsOf: inputFile) + let source = try String(contentsOf: inputFile, encoding: .utf8) try measureInstructions { _ = Parser.parse(source: source) diff --git a/Tests/PerformanceTest/SyntaxClassifierPerformanceTests.swift b/Tests/PerformanceTest/SyntaxClassifierPerformanceTests.swift index daa474ff25f..6be46deef1c 100644 --- a/Tests/PerformanceTest/SyntaxClassifierPerformanceTests.swift +++ b/Tests/PerformanceTest/SyntaxClassifierPerformanceTests.swift @@ -28,7 +28,7 @@ class SyntaxClassifierPerformanceTests: XCTestCase { func testClassifierPerformance() throws { try XCTSkipIf(longTestsDisabled) - let source = try String(contentsOf: inputFile) + let source = try String(contentsOf: inputFile, encoding: .utf8) let parsed = Parser.parse(source: source) try measureInstructions { diff --git a/Tests/PerformanceTest/VisitorPerformanceTests.swift b/Tests/PerformanceTest/VisitorPerformanceTests.swift index 71adefa173f..659c27dad55 100644 --- a/Tests/PerformanceTest/VisitorPerformanceTests.swift +++ b/Tests/PerformanceTest/VisitorPerformanceTests.swift @@ -28,7 +28,7 @@ class VisitorPerformanceTests: XCTestCase { try XCTSkipIf(longTestsDisabled) class EmptyVisitor: SyntaxVisitor {} - let source = try String(contentsOf: inputFile) + let source = try String(contentsOf: inputFile, encoding: .utf8) let parsed = Parser.parse(source: source) let emptyVisitor = EmptyVisitor(viewMode: .sourceAccurate) @@ -41,7 +41,7 @@ class VisitorPerformanceTests: XCTestCase { try XCTSkipIf(longTestsDisabled) class EmptyRewriter: SyntaxRewriter {} - let source = try String(contentsOf: inputFile) + let source = try String(contentsOf: inputFile, encoding: .utf8) let parsed = Parser.parse(source: source) let emptyRewriter = EmptyRewriter(viewMode: .sourceAccurate) @@ -54,7 +54,7 @@ class VisitorPerformanceTests: XCTestCase { try XCTSkipIf(longTestsDisabled) class EmptyAnyVisitor: SyntaxAnyVisitor {} - let source = try String(contentsOf: inputFile) + let source = try String(contentsOf: inputFile, encoding: .utf8) let parsed = Parser.parse(source: source) let emptyVisitor = EmptyAnyVisitor(viewMode: .sourceAccurate) diff --git a/Tests/SwiftIfConfigTest/ActiveRegionTests.swift b/Tests/SwiftIfConfigTest/ActiveRegionTests.swift index 8e553a8f032..33fd7c1b7f8 100644 --- a/Tests/SwiftIfConfigTest/ActiveRegionTests.swift +++ b/Tests/SwiftIfConfigTest/ActiveRegionTests.swift @@ -9,12 +9,15 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// + import SwiftDiagnostics import SwiftIfConfig import SwiftParser import SwiftSyntax import SwiftSyntaxMacrosGenericTestSupport import XCTest +import _SwiftSyntaxGenericTestSupport +import _SwiftSyntaxTestSupport public class ActiveRegionTests: XCTestCase { let linuxBuildConfig = TestingBuildConfiguration( @@ -26,28 +29,28 @@ public class ActiveRegionTests: XCTestCase { func testActiveRegions() throws { try assertActiveCode( """ - 4️⃣ + 1️⃣ #if DEBUG - 0️⃣func f() + 2️⃣func f() #elseif ASSERTS - 1️⃣func g() + 3️⃣func g() #if compiler(>=8.0) - 2️⃣func h() + 4️⃣func h() #else - 3️⃣var i + 5️⃣var i #endif #endif - 5️⃣token + 6️⃣token """, configuration: linuxBuildConfig, states: [ - "0️⃣": .active, - "1️⃣": .inactive, - "2️⃣": .unparsed, + "1️⃣": .active, + "2️⃣": .active, "3️⃣": .inactive, - "4️⃣": .active, - "5️⃣": .active, + "4️⃣": .unparsed, + "5️⃣": .inactive, + "6️⃣": .active, ] ) } @@ -55,27 +58,27 @@ public class ActiveRegionTests: XCTestCase { func testActiveRegionsInPostfix() throws { try assertActiveCode( """ - 4️⃣a.b() + 1️⃣a.b() #if DEBUG - 0️⃣.c() + 2️⃣.c() #elseif ASSERTS - 1️⃣.d() + 3️⃣.d() #if compiler(>=8.0) - 2️⃣.e() + 4️⃣.e() #else - 3️⃣.f() + 5️⃣.f() #endif #endif - 5️⃣.g() + 6️⃣.g() """, configuration: linuxBuildConfig, states: [ - "0️⃣": .active, - "1️⃣": .inactive, - "2️⃣": .unparsed, + "1️⃣": .active, + "2️⃣": .active, "3️⃣": .inactive, - "4️⃣": .active, - "5️⃣": .active, + "4️⃣": .unparsed, + "5️⃣": .inactive, + "6️⃣": .active, ] ) } @@ -98,3 +101,45 @@ public class ActiveRegionTests: XCTestCase { ) } } + +/// Assert that the various marked positions in the source code have the +/// expected active states. +fileprivate func assertActiveCode( + _ markedSource: String, + configuration: some BuildConfiguration = TestingBuildConfiguration(), + states: [String: IfConfigRegionState], + file: StaticString = #filePath, + line: UInt = #line +) throws { + // Pull out the markers that we'll use to dig out nodes to query. + let (markerLocations, source) = extractMarkers(markedSource) + + var parser = Parser(source) + let tree = SourceFileSyntax.parse(from: &parser) + + let configuredRegions = tree.configuredRegions(in: configuration) + + for (marker, location) in markerLocations { + guard let expectedState = states[marker] else { + XCTFail("Missing marker \(marker) in expected states", file: file, line: line) + continue + } + + guard let token = tree.token(at: AbsolutePosition(utf8Offset: location)) else { + XCTFail("Unable to find token at location \(location)", file: file, line: line) + continue + } + + let (actualState, _) = token.isActive(in: configuration) + XCTAssertEqual(actualState, expectedState, "isActive(in:) at marker \(marker)", file: file, line: line) + + let actualViaRegions = token.isActive(inConfiguredRegions: configuredRegions) + XCTAssertEqual( + actualViaRegions, + expectedState, + "isActive(inConfiguredRegions:) at marker \(marker)", + file: file, + line: line + ) + } +} diff --git a/Tests/SwiftIfConfigTest/Assertions.swift b/Tests/SwiftIfConfigTest/Assertions.swift deleted file mode 100644 index 00c3a442848..00000000000 --- a/Tests/SwiftIfConfigTest/Assertions.swift +++ /dev/null @@ -1,153 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// -import SwiftDiagnostics -import SwiftIfConfig -import SwiftParser -import SwiftSyntax -@_spi(XCTestFailureLocation) import SwiftSyntaxMacrosGenericTestSupport -import XCTest -import _SwiftSyntaxGenericTestSupport -import _SwiftSyntaxTestSupport - -/// Assert the results of evaluating the condition within an `#if` against the -/// given build configuration. -func assertIfConfig( - _ condition: ExprSyntax, - _ expectedState: ConfiguredRegionState?, - configuration: some BuildConfiguration = TestingBuildConfiguration(), - diagnostics expectedDiagnostics: [DiagnosticSpec] = [], - file: StaticString = #filePath, - line: UInt = #line -) { - // Evaluate the condition to check the state. - var actualDiagnostics: [Diagnostic] = [] - do { - let actualState = try ConfiguredRegionState(condition: condition, configuration: configuration) { diag in - actualDiagnostics.append(diag) - } - XCTAssertEqual(actualState, expectedState, file: file, line: line) - } catch { - XCTAssertNil(expectedState, file: file, line: line) - } - - // Check the diagnostics. - if actualDiagnostics.count != expectedDiagnostics.count { - XCTFail( - """ - Expected \(expectedDiagnostics.count) diagnostics, but got \(actualDiagnostics.count): - \(actualDiagnostics.map(\.debugDescription).joined(separator: "\n")) - """, - file: file, - line: line - ) - } else { - for (actualDiag, expectedDiag) in zip(actualDiagnostics, expectedDiagnostics) { - assertDiagnostic( - actualDiag, - in: .tree(condition), - expected: expectedDiag, - failureHandler: { - XCTFail($0.message, file: $0.location.staticFilePath, line: $0.location.unsignedLine) - } - ) - } - } -} - -/// Assert that the various marked positions in the source code have the -/// expected active states. -func assertActiveCode( - _ markedSource: String, - configuration: some BuildConfiguration = TestingBuildConfiguration(), - states: [String: ConfiguredRegionState], - file: StaticString = #filePath, - line: UInt = #line -) throws { - // Pull out the markers that we'll use to dig out nodes to query. - let (markerLocations, source) = extractMarkers(markedSource) - - var parser = Parser(source) - let tree = SourceFileSyntax.parse(from: &parser) - - let configuredRegions = tree.configuredRegions(in: configuration) - - for (marker, location) in markerLocations { - guard let expectedState = states[marker] else { - XCTFail("Missing marker \(marker) in expected states", file: file, line: line) - continue - } - - guard let token = tree.token(at: AbsolutePosition(utf8Offset: location)) else { - XCTFail("Unable to find token at location \(location)", file: file, line: line) - continue - } - - let actualState = try token.isActive(in: configuration) - XCTAssertEqual(actualState, expectedState, "isActive(in:) at marker \(marker)", file: file, line: line) - - let actualViaRegions = token.isActive(inConfiguredRegions: configuredRegions) - XCTAssertEqual( - actualViaRegions, - expectedState, - "isActive(inConfiguredRegions:) at marker \(marker)", - file: file, - line: line - ) - } -} - -/// Assert that applying the given build configuration to the source code -/// returns the expected source and diagnostics. -func assertRemoveInactive( - _ source: String, - configuration: some BuildConfiguration, - diagnostics expectedDiagnostics: [DiagnosticSpec] = [], - expectedSource: String, - file: StaticString = #filePath, - line: UInt = #line -) { - var parser = Parser(source) - let tree = SourceFileSyntax.parse(from: &parser) - - let (treeWithoutInactive, actualDiagnostics) = tree.removingInactive(in: configuration) - - // Check the resulting tree. - assertStringsEqualWithDiff( - treeWithoutInactive.description, - expectedSource, - file: file, - line: line - ) - - // Check the diagnostics. - if actualDiagnostics.count != expectedDiagnostics.count { - XCTFail( - """ - Expected \(expectedDiagnostics.count) diagnostics, but got \(actualDiagnostics.count): - \(actualDiagnostics.map(\.debugDescription).joined(separator: "\n")) - """, - file: file, - line: line - ) - } else { - for (actualDiag, expectedDiag) in zip(actualDiagnostics, expectedDiagnostics) { - assertDiagnostic( - actualDiag, - in: .tree(tree), - expected: expectedDiag, - failureHandler: { - XCTFail($0.message, file: $0.location.staticFilePath, line: $0.location.unsignedLine) - } - ) - } - } -} diff --git a/Tests/SwiftIfConfigTest/EvaluateTests.swift b/Tests/SwiftIfConfigTest/EvaluateTests.swift index 3fe5c9c1c6c..6f7bdd064a1 100644 --- a/Tests/SwiftIfConfigTest/EvaluateTests.swift +++ b/Tests/SwiftIfConfigTest/EvaluateTests.swift @@ -9,11 +9,14 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// + +import SwiftDiagnostics import SwiftIfConfig import SwiftParser import SwiftSyntax -import SwiftSyntaxMacrosGenericTestSupport +@_spi(XCTestFailureLocation) @_spi(Testing) import SwiftSyntaxMacrosGenericTestSupport import XCTest +import _SwiftSyntaxGenericTestSupport import _SwiftSyntaxTestSupport public class EvaluateTests: XCTestCase { @@ -55,7 +58,7 @@ public class EvaluateTests: XCTestCase { ) assertIfConfig( "2", - nil, + .unparsed, configuration: buildConfig, diagnostics: [ DiagnosticSpec( @@ -79,7 +82,7 @@ public class EvaluateTests: XCTestCase { assertIfConfig("nope && DEBUG", .inactive, configuration: buildConfig) assertIfConfig( "nope && 3.14159", - nil, + .unparsed, configuration: buildConfig, diagnostics: [ DiagnosticSpec( @@ -95,7 +98,7 @@ public class EvaluateTests: XCTestCase { assertIfConfig("nope || !DEBUG", .inactive, configuration: buildConfig) assertIfConfig( "DEBUG || 3.14159", - nil, + .active, configuration: buildConfig, diagnostics: [ DiagnosticSpec( @@ -107,7 +110,7 @@ public class EvaluateTests: XCTestCase { ) assertIfConfig( "(DEBUG) || 3.14159", - nil, + .active, configuration: buildConfig, diagnostics: [ DiagnosticSpec( @@ -124,7 +127,7 @@ public class EvaluateTests: XCTestCase { assertIfConfig( "3.14159", - nil, + .unparsed, configuration: buildConfig, diagnostics: [ DiagnosticSpec( @@ -182,7 +185,7 @@ public class EvaluateTests: XCTestCase { assertIfConfig("compiler(>=5.10) && 3.14159", .unparsed) assertIfConfig( "compiler(>=5.10) || 3.14159", - nil, + .unparsed, diagnostics: [ DiagnosticSpec( message: "invalid conditional compilation expression", @@ -194,7 +197,7 @@ public class EvaluateTests: XCTestCase { assertIfConfig("compiler(>=5.9) || 3.14159", .active) assertIfConfig( "compiler(>=5.9) && 3.14159", - nil, + .unparsed, diagnostics: [ DiagnosticSpec( message: "invalid conditional compilation expression", @@ -228,3 +231,43 @@ public class EvaluateTests: XCTestCase { ) } } + +/// Assert the results of evaluating the condition within an `#if` against the +/// given build configuration. +fileprivate func assertIfConfig( + _ condition: ExprSyntax, + _ expectedState: IfConfigRegionState, + configuration: some BuildConfiguration = TestingBuildConfiguration(), + diagnostics expectedDiagnostics: [DiagnosticSpec] = [], + file: StaticString = #filePath, + line: UInt = #line +) { + // Evaluate the condition to check the state. + let actualDiagnostics: [Diagnostic] + let actualState: IfConfigRegionState + (actualState, actualDiagnostics) = IfConfigRegionState.evaluating(condition, in: configuration) + XCTAssertEqual(actualState, expectedState, file: file, line: line) + + // Check the diagnostics. + if actualDiagnostics.count != expectedDiagnostics.count { + XCTFail( + """ + Expected \(expectedDiagnostics.count) diagnostics, but got \(actualDiagnostics.count): + \(actualDiagnostics.map(\.debugDescription).joined(separator: "\n")) + """, + file: file, + line: line + ) + } else { + for (actualDiag, expectedDiag) in zip(actualDiagnostics, expectedDiagnostics) { + assertDiagnostic( + actualDiag, + in: .tree(condition), + expected: expectedDiag, + failureHandler: { + XCTFail($0.message, file: $0.location.staticFilePath, line: $0.location.unsignedLine) + } + ) + } + } +} diff --git a/Tests/SwiftIfConfigTest/TestingBuildConfiguration.swift b/Tests/SwiftIfConfigTest/TestingBuildConfiguration.swift index 39a5702f71d..5f807fe0b1e 100644 --- a/Tests/SwiftIfConfigTest/TestingBuildConfiguration.swift +++ b/Tests/SwiftIfConfigTest/TestingBuildConfiguration.swift @@ -9,6 +9,7 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// + import SwiftIfConfig import SwiftSyntax diff --git a/Tests/SwiftIfConfigTest/VisitorTests.swift b/Tests/SwiftIfConfigTest/VisitorTests.swift index eddb8c1d3d7..4e669e2b33a 100644 --- a/Tests/SwiftIfConfigTest/VisitorTests.swift +++ b/Tests/SwiftIfConfigTest/VisitorTests.swift @@ -9,12 +9,15 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// + import SwiftDiagnostics import SwiftIfConfig import SwiftParser import SwiftSyntax -import SwiftSyntaxMacrosGenericTestSupport +@_spi(XCTestFailureLocation) @_spi(Testing) import SwiftSyntaxMacrosGenericTestSupport import XCTest +import _SwiftSyntaxGenericTestSupport +import _SwiftSyntaxTestSupport /// Visitor that ensures that all of the nodes we visit are active. /// @@ -25,9 +28,7 @@ class AllActiveVisitor: ActiveSyntaxAnyVisitor { super.init(viewMode: .sourceAccurate, configuration: configuration) } open override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind { - var active: ConfiguredRegionState = .inactive - XCTAssertNoThrow(try active = node.isActive(in: configuration)) - XCTAssertEqual(active, .active) + XCTAssertEqual(node.isActive(in: configuration).state, .active) return .visitChildren } } @@ -253,3 +254,50 @@ public class VisitorTests: XCTestCase { ) } } + +/// Assert that applying the given build configuration to the source code +/// returns the expected source and diagnostics. +fileprivate func assertRemoveInactive( + _ source: String, + configuration: some BuildConfiguration, + diagnostics expectedDiagnostics: [DiagnosticSpec] = [], + expectedSource: String, + file: StaticString = #filePath, + line: UInt = #line +) { + var parser = Parser(source) + let tree = SourceFileSyntax.parse(from: &parser) + + let (treeWithoutInactive, actualDiagnostics) = tree.removingInactive(in: configuration) + + // Check the resulting tree. + assertStringsEqualWithDiff( + treeWithoutInactive.description, + expectedSource, + file: file, + line: line + ) + + // Check the diagnostics. + if actualDiagnostics.count != expectedDiagnostics.count { + XCTFail( + """ + Expected \(expectedDiagnostics.count) diagnostics, but got \(actualDiagnostics.count): + \(actualDiagnostics.map(\.debugDescription).joined(separator: "\n")) + """, + file: file, + line: line + ) + } else { + for (actualDiag, expectedDiag) in zip(actualDiagnostics, expectedDiagnostics) { + assertDiagnostic( + actualDiag, + in: .tree(tree), + expected: expectedDiag, + failureHandler: { + XCTFail($0.message, file: $0.location.staticFilePath, line: $0.location.unsignedLine) + } + ) + } + } +} diff --git a/Tests/SwiftLexicalLookupTest/Assertions.swift b/Tests/SwiftLexicalLookupTest/Assertions.swift index 5562512cc70..ccfeaf61788 100644 --- a/Tests/SwiftLexicalLookupTest/Assertions.swift +++ b/Tests/SwiftLexicalLookupTest/Assertions.swift @@ -67,20 +67,7 @@ func assertLexicalScopeQuery( // Assert validity of the output for (actual, (expectedMarker, expectedPosition)) in zip(result, zip(expectedMarkers, expectedPositions)) { - if actual == nil && expectedPosition == nil { continue } - - guard let actual else { - XCTFail( - "For marker \(marker), actual is nil while expected is \(sourceFileSyntax.token(at: expectedPosition!)?.description ?? "nil")" - ) - continue - } - - guard let expectedPosition else { - XCTFail("For marker \(marker), actual is \(actual) while expected position is nil") - continue - } - + guard let actual, let expectedPosition else { continue } XCTAssert( actual.positionAfterSkippingLeadingTrivia == expectedPosition, "For marker \(marker), actual result: \(actual) doesn't match expected value: \(sourceFileSyntax.token(at: expectedPosition)?.description ?? "nil")" @@ -106,7 +93,9 @@ func assertLexicalNameLookup( assertLexicalScopeQuery( source: source, methodUnderTest: { marker, tokenAtMarker in - let result = tokenAtMarker.lookup(for: useNilAsTheParameter ? nil : tokenAtMarker.text, with: config) + let lookupIdentifier = Identifier(tokenAtMarker) ?? Identifier(tokenAtMarker.text) + + let result = tokenAtMarker.lookup(for: useNilAsTheParameter ? nil : lookupIdentifier, with: config) guard let expectedValues = references[marker] else { XCTFail("For marker \(marker), couldn't find result expectation") diff --git a/Tests/SwiftLexicalLookupTest/ExpectedName.swift b/Tests/SwiftLexicalLookupTest/ExpectedName.swift index 03ca70b83a9..867173d96eb 100644 --- a/Tests/SwiftLexicalLookupTest/ExpectedName.swift +++ b/Tests/SwiftLexicalLookupTest/ExpectedName.swift @@ -51,7 +51,7 @@ enum ImplicitNameExpectation { .error(let marker), .newValue(let marker), .oldValue(let marker): - marker + return marker } } } @@ -67,9 +67,9 @@ enum NameExpectation: ExpectedName { switch self { case .identifier(let marker), .declaration(let marker): - marker + return marker case .implicit(let implicitName): - implicitName.marker + return implicitName.marker } } diff --git a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift index 8389d1b6227..6627ef9951a 100644 --- a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift +++ b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift @@ -512,13 +512,13 @@ final class testNameLookup: XCTestCase { 9️⃣class d {} - let x = 0️⃣d + let 🔟a = 0️⃣d """, references: [ "3️⃣": [.fromFileScope(expectedNames: ["1️⃣", "8️⃣"])], "4️⃣": [.fromFileScope(expectedNames: ["2️⃣"])], "5️⃣": [.fromFileScope(expectedNames: ["7️⃣"])], - "6️⃣": [], + "6️⃣": [.fromFileScope(expectedNames: ["9️⃣"])], "0️⃣": [.fromFileScope(expectedNames: ["9️⃣"])], ], expectedResultTypes: .all(ClassDeclSyntax.self, except: ["8️⃣": IdentifierPatternSyntax.self]) @@ -542,48 +542,41 @@ final class testNameLookup: XCTestCase { 9️⃣class d {} - let x = 0️⃣d + let 🔟a = 0️⃣d """, references: [ - "3️⃣": [.fromFileScope(expectedNames: ["1️⃣", "8️⃣"])], + "3️⃣": [.fromFileScope(expectedNames: ["1️⃣", "8️⃣", "🔟"])], "4️⃣": [.fromFileScope(expectedNames: ["2️⃣"])], "5️⃣": [.fromFileScope(expectedNames: ["7️⃣"])], "6️⃣": [.fromFileScope(expectedNames: ["9️⃣"])], "0️⃣": [.fromFileScope(expectedNames: ["9️⃣"])], ], - expectedResultTypes: .all(ClassDeclSyntax.self, except: ["8️⃣": IdentifierPatternSyntax.self]), + expectedResultTypes: .all( + ClassDeclSyntax.self, + except: [ + "8️⃣": IdentifierPatternSyntax.self, + "🔟": IdentifierPatternSyntax.self, + ] + ), config: LookupConfig(fileScopeHandling: .memberBlock) ) } - func testFileScopeAsCodeBlock() { + func testDeclarationAvailabilityInCodeBlock() { assertLexicalNameLookup( source: """ - 1️⃣class a {} - - 2️⃣class b { - let x = 3️⃣a + 4️⃣b + 5️⃣c + 6️⃣d - } - - let 8️⃣a = 0 - - 7️⃣class c {} - - if a == 0 {} + func x { + 1️⃣class A {} - 9️⃣class d {} + let a = 2️⃣A() - let x = 0️⃣d + 3️⃣class A {} + } """, references: [ - "3️⃣": [.fromFileScope(expectedNames: ["1️⃣"])], - "4️⃣": [], - "5️⃣": [], - "6️⃣": [], - "0️⃣": [.fromFileScope(expectedNames: ["9️⃣"])], + "2️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣", "3️⃣"])] ], - expectedResultTypes: .all(ClassDeclSyntax.self, except: ["8️⃣": IdentifierPatternSyntax.self]), - config: LookupConfig(fileScopeHandling: .codeBlock) + expectedResultTypes: .all(ClassDeclSyntax.self) ) } @@ -611,31 +604,6 @@ final class testNameLookup: XCTestCase { ) } - func testGuardOnFileScopeCodeBlock() { - assertLexicalNameLookup( - source: """ - let 1️⃣a = 0 - - class c {} - - guard let 2️⃣a else { fatalError() } - - 3️⃣class a {} - - let x = 4️⃣a - """, - references: [ - "4️⃣": [ - .fromFileScope(expectedNames: ["3️⃣"]), - .fromScope(GuardStmtSyntax.self, expectedNames: ["2️⃣"]), - .fromFileScope(expectedNames: ["1️⃣"]), - ] - ], - expectedResultTypes: .all(IdentifierPatternSyntax.self, except: ["3️⃣": ClassDeclSyntax.self]), - config: LookupConfig(fileScopeHandling: .codeBlock) - ) - } - func testImplicitSelf() { assertLexicalNameLookup( source: """ @@ -700,4 +668,43 @@ final class testNameLookup: XCTestCase { ] ) } + + func testBacktickCompatibility() { + assertLexicalNameLookup( + source: """ + 1️⃣struct Foo { + func test() { + let 2️⃣`self` = 1 + print(3️⃣self) + print(4️⃣`self`) + } + } + + 5️⃣struct Bar { + func test() { + print(6️⃣self) + let 7️⃣`self` = 1 + print(8️⃣`self`) + } + } + """, + references: [ + "3️⃣": [ + .fromScope(CodeBlockSyntax.self, expectedNames: [NameExpectation.identifier("2️⃣")]), + .fromScope(StructDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("1️⃣"))]), + ], + "4️⃣": [ + .fromScope(CodeBlockSyntax.self, expectedNames: [NameExpectation.identifier("2️⃣")]), + .fromScope(StructDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("1️⃣"))]), + ], + "6️⃣": [ + .fromScope(StructDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("5️⃣"))]) + ], + "8️⃣": [ + .fromScope(CodeBlockSyntax.self, expectedNames: [NameExpectation.identifier("7️⃣")]), + .fromScope(StructDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("5️⃣"))]), + ], + ] + ) + } } diff --git a/Tests/SwiftLexicalLookupTest/ResultExpectation.swift b/Tests/SwiftLexicalLookupTest/ResultExpectation.swift index 5dc00d568f8..27a9d5b58a8 100644 --- a/Tests/SwiftLexicalLookupTest/ResultExpectation.swift +++ b/Tests/SwiftLexicalLookupTest/ResultExpectation.swift @@ -22,18 +22,18 @@ enum ResultExpectation { var expectedNames: [ExpectedName] { switch self { case .fromScope(_, let expectedNames): - expectedNames + return expectedNames case .fromFileScope(expectedNames: let expectedNames): - expectedNames + return expectedNames } } var debugDescription: String { switch self { case .fromScope: - "fromScope" + return "fromScope" case .fromFileScope: - "fromFileScope" + return "fromFileScope" } } @@ -65,9 +65,9 @@ extension LookupResult { var debugDescription: String { switch self { case .fromScope: - "fromScope" + return "fromScope" case .fromFileScope: - "fromFileScope" + return "fromFileScope" } } } diff --git a/Tests/SwiftParserTest/ParserTests.swift b/Tests/SwiftParserTest/ParserTests.swift index f451890d3db..de26d78ba20 100644 --- a/Tests/SwiftParserTest/ParserTests.swift +++ b/Tests/SwiftParserTest/ParserTests.swift @@ -61,8 +61,7 @@ class ParserTests: ParserTestCase { checkDiagnostics: Bool, shouldExclude: @Sendable (URL) -> Bool = { _ in false } ) { - // nonisolated(unsafe) because [URL] is not marked Sendable on Linux. - let _fileURLs = FileManager.default + let fileURLs = FileManager.default .enumerator(at: path, includingPropertiesForKeys: nil)! .compactMap({ $0 as? URL }) .filter { @@ -70,11 +69,6 @@ class ParserTests: ParserTestCase { || $0.pathExtension == "sil" || $0.pathExtension == "swiftinterface" } - #if swift(>=6.0) - nonisolated(unsafe) let fileURLs = _fileURLs - #else - let fileURLs = _fileURLs - #endif print("\(name) - processing \(fileURLs.count) source files") DispatchQueue.concurrentPerform(iterations: fileURLs.count) { fileURLIndex in diff --git a/Tests/SwiftSyntaxBuilderTest/AccessorDeclTests.swift b/Tests/SwiftSyntaxBuilderTest/AccessorDeclTests.swift new file mode 100644 index 00000000000..a89cde0c37d --- /dev/null +++ b/Tests/SwiftSyntaxBuilderTest/AccessorDeclTests.swift @@ -0,0 +1,24 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax +import SwiftSyntaxBuilder +import XCTest + +final class AccessorDeclTests: XCTestCase { + func testCreateAccessorWithHeaderAndBody() throws { + let accessor = try AccessorDeclSyntax("get") { + ExprSyntax("1") + } + XCTAssertEqual(accessor.body?.statements.count, 1) + } +} diff --git a/Tests/SwiftSyntaxTest/IdentifierTests.swift b/Tests/SwiftSyntaxTest/IdentifierTests.swift index e0083a0a5c4..8e110807df8 100644 --- a/Tests/SwiftSyntaxTest/IdentifierTests.swift +++ b/Tests/SwiftSyntaxTest/IdentifierTests.swift @@ -39,7 +39,7 @@ class IdentifierTests: XCTestCase { public func testIdentifier() { let token = TokenSyntax(stringLiteral: "sometoken") withExtendedLifetime(token) { token in - XCTAssertEqual(token.identifier?.raw.name, SyntaxText("sometoken")) + XCTAssertEqual(token.identifier?.raw?.name, SyntaxText("sometoken")) } } diff --git a/cmake/modules/SwiftCompilerCapability.cmake b/cmake/modules/SwiftCompilerCapability.cmake index 951e563a49c..d62e387184f 100644 --- a/cmake/modules/SwiftCompilerCapability.cmake +++ b/cmake/modules/SwiftCompilerCapability.cmake @@ -22,6 +22,19 @@ macro(swift_supports_implicit_module module_name out_var) ) endmacro() +function(swift_get_swiftlang_version out_var) + execute_process( + COMMAND "${CMAKE_Swift_COMPILER}" -version + OUTPUT_VARIABLE output ERROR_VARIABLE output + RESULT_VARIABLE result + TIMEOUT 10 + ) + + if(output MATCHES [[swiftlang-([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)]]) + set("${out_var}" "${CMAKE_MATCH_1}" PARENT_SCOPE) + endif() +endfunction() + # Get "package cross-module-optimization" compiler arguments suitable for the compiler. function(swift_get_package_cmo_support out_var) # > 6.0 : Fixed feature. @@ -43,8 +56,13 @@ function(swift_get_package_cmo_support out_var) -Xfrontend -experimental-package-bypass-resilience ) if(result) - set(${out_var} EXPERIMENTAL PARENT_SCOPE) - return() + # Package CMO is implmented in Xcode 16 Beta 4 (swiftlang-6.0.0.6.8) or later. + # Consider it's not supported in non Xcode toolchain with "-experimental" options. + swift_get_swiftlang_version(swiftlang_version) + if(swiftlang_version AND swiftlang_version VERSION_GREATER_EQUAL 6.0.0.6) + set(${out_var} EXPERIMENTAL PARENT_SCOPE) + return() + endif() endif() # < 6.0 : Not supported. From 12a5380bb14e99a6b440b9167b7fff3c5bd715e8 Mon Sep 17 00:00:00 2001 From: Jakub Florek Date: Tue, 30 Jul 2024 12:04:14 +0200 Subject: [PATCH 17/27] Revert "Merge changes from #2755 and add Identifier based lookup." This reverts commit dda31f93e5b446021924377d67fa1cdee04f462d. --- Package.swift | 2 +- Release Notes/601.md | 4 - .../SwiftIfConfig/ActiveSyntaxRewriter.swift | 31 ++- .../SwiftIfConfig/ActiveSyntaxVisitor.swift | 49 ++--- .../SwiftIfConfig/BuildConfiguration.swift | 2 +- Sources/SwiftIfConfig/CMakeLists.txt | 5 +- ...tate.swift => ConfiguredRegionState.swift} | 35 ++- Sources/SwiftIfConfig/ConfiguredRegions.swift | 21 +- .../SwiftIfConfig/IfConfigDecl+IfConfig.swift | 29 ++- Sources/SwiftIfConfig/IfConfigError.swift | 1 - .../SwiftIfConfig/IfConfigEvaluation.swift | 203 ++++++++---------- .../SwiftIfConfig.docc/SwiftIfConfig.md | 4 +- .../SwiftIfConfig/SyntaxLiteralUtils.swift | 1 - .../SyntaxProtocol+IfConfig.swift | 42 ++-- .../SwiftIfConfig/VersionTuple+Parsing.swift | 1 - ...> FileScopeNameIntroductionStrategy.swift} | 6 +- ...oducingToSequentialParentScopeSyntax.swift | 2 +- Sources/SwiftLexicalLookup/LookupName.swift | 76 +++---- Sources/SwiftLexicalLookup/LookupResult.swift | 6 +- .../ScopeImplementations.swift | 55 +++-- Sources/SwiftLexicalLookup/ScopeSyntax.swift | 28 +-- .../SequentialScopeSyntax.swift | 11 +- Sources/SwiftRefactor/SyntaxUtils.swift | 2 +- Sources/SwiftSyntax/Identifier.swift | 47 +--- .../SyntaxNodeWithBody.swift | 31 +-- .../MacroProtocols/MemberMacro.swift | 5 - .../Assertions.swift | 1 - .../_SwiftSyntaxCShims/include/AtomicBool.h | 2 +- .../ParsingPerformanceTests.swift | 2 +- .../SyntaxClassifierPerformanceTests.swift | 2 +- .../VisitorPerformanceTests.swift | 6 +- .../SwiftIfConfigTest/ActiveRegionTests.swift | 89 ++------ Tests/SwiftIfConfigTest/Assertions.swift | 153 +++++++++++++ Tests/SwiftIfConfigTest/EvaluateTests.swift | 59 +---- .../TestingBuildConfiguration.swift | 1 - Tests/SwiftIfConfigTest/VisitorTests.swift | 56 +---- Tests/SwiftLexicalLookupTest/Assertions.swift | 19 +- .../SwiftLexicalLookupTest/ExpectedName.swift | 6 +- .../NameLookupTests.swift | 109 +++++----- .../ResultExpectation.swift | 12 +- Tests/SwiftParserTest/ParserTests.swift | 8 +- .../AccessorDeclTests.swift | 24 --- Tests/SwiftSyntaxTest/IdentifierTests.swift | 2 +- cmake/modules/SwiftCompilerCapability.cmake | 22 +- 44 files changed, 586 insertions(+), 686 deletions(-) rename Sources/SwiftIfConfig/{IfConfigRegionState.swift => ConfiguredRegionState.swift} (58%) rename Sources/SwiftLexicalLookup/Configurations/{FileScopeHandlingConfig.swift => FileScopeNameIntroductionStrategy.swift} (81%) create mode 100644 Tests/SwiftIfConfigTest/Assertions.swift delete mode 100644 Tests/SwiftSyntaxBuilderTest/AccessorDeclTests.swift diff --git a/Package.swift b/Package.swift index 2d626ac9ab5..4d4a4a96a14 100644 --- a/Package.swift +++ b/Package.swift @@ -143,7 +143,7 @@ let package = Package( .target( name: "SwiftIfConfig", - dependencies: ["SwiftSyntax", "SwiftDiagnostics", "SwiftOperators"], + dependencies: ["SwiftSyntax", "SwiftOperators"], exclude: ["CMakeLists.txt"] ), diff --git a/Release Notes/601.md b/Release Notes/601.md index cb704e26814..7036e503b33 100644 --- a/Release Notes/601.md +++ b/Release Notes/601.md @@ -15,10 +15,6 @@ - Description: This method translates an error into one or more diagnostics, recognizing `DiagnosticsError` and `DiagnosticMessage` instances or providing its own `Diagnostic` as needed. - Pull Request: https://github.com/swiftlang/swift-syntax/pull/1816 -- Added a new library `SwiftIfConfig`. - - Description: This new library provides facilities for evaluating `#if` conditions and determining which regions of a syntax tree are active according to a given build configuration. - - Pull Request: https://github.com/swiftlang/swift-syntax/pull/1816 - ## API Behavior Changes - `SyntaxProtocol.trimmed` detaches the node diff --git a/Sources/SwiftIfConfig/ActiveSyntaxRewriter.swift b/Sources/SwiftIfConfig/ActiveSyntaxRewriter.swift index 0cdeb97e4e7..a863c6776b2 100644 --- a/Sources/SwiftIfConfig/ActiveSyntaxRewriter.swift +++ b/Sources/SwiftIfConfig/ActiveSyntaxRewriter.swift @@ -10,6 +10,15 @@ // //===----------------------------------------------------------------------===// +// +// This file defines the SyntaxRewriter, a class that performs a standard walk +// and tree-rebuilding pattern. +// +// Subclassers of this class can override the walking behavior for any syntax +// node and transform nodes however they like. +// +//===----------------------------------------------------------------------===// + import SwiftDiagnostics import SwiftSyntax @@ -25,9 +34,7 @@ extension SyntaxProtocol { /// clauses, e.g., `#if FOO > 10`, then the condition will be /// considered to have failed and the clauses's elements will be /// removed. - public func removingInactive( - in configuration: some BuildConfiguration - ) -> (result: Syntax, diagnostics: [Diagnostic]) { + public func removingInactive(in configuration: some BuildConfiguration) -> (Syntax, [Diagnostic]) { // First pass: Find all of the active clauses for the #ifs we need to // visit, along with any diagnostics produced along the way. This process // does not change the tree in any way. @@ -35,7 +42,7 @@ extension SyntaxProtocol { visitor.walk(self) // If there were no active clauses to visit, we're done! - if !visitor.visitedAnyIfClauses { + if visitor.numIfClausesVisited == 0 { return (Syntax(self), visitor.diagnostics) } @@ -81,13 +88,12 @@ extension SyntaxProtocol { /// than trivia). class ActiveSyntaxRewriter: SyntaxRewriter { let configuration: Configuration - var diagnostics: [Diagnostic] = [] init(configuration: Configuration) { self.configuration = configuration } - private func dropInactive( + private func dropInactive( _ node: List, elementAsIfConfig: (List.Element) -> IfConfigDeclSyntax? ) -> List { @@ -99,10 +105,7 @@ class ActiveSyntaxRewriter: SyntaxRewriter { // Find #ifs within the list. if let ifConfigDecl = elementAsIfConfig(element) { // Retrieve the active `#if` clause - let (activeClause, localDiagnostics) = ifConfigDecl.activeClause(in: configuration) - - // Add these diagnostics. - diagnostics.append(contentsOf: localDiagnostics) + let activeClause = ifConfigDecl.activeClause(in: configuration) // If this is the first element that changed, note that we have // changes and add all prior elements to the list of new elements. @@ -252,8 +255,7 @@ class ActiveSyntaxRewriter: SyntaxRewriter { return dropInactive(outerBase: base, postfixIfConfig: postfixIfConfig) } - assertionFailure("Unhandled postfix expression in #if elimination") - return postfix + preconditionFailure("Unhandled postfix expression in #if elimination") } /// Drop inactive regions from a postfix `#if` configuration, applying the @@ -263,10 +265,7 @@ class ActiveSyntaxRewriter: SyntaxRewriter { postfixIfConfig: PostfixIfConfigExprSyntax ) -> ExprSyntax { // Retrieve the active `if` clause. - let (activeClause, localDiagnostics) = postfixIfConfig.config.activeClause(in: configuration) - - // Record these diagnostics. - diagnostics.append(contentsOf: localDiagnostics) + let activeClause = postfixIfConfig.config.activeClause(in: configuration) guard case .postfixExpression(let postfixExpr) = activeClause?.elements else { diff --git a/Sources/SwiftIfConfig/ActiveSyntaxVisitor.swift b/Sources/SwiftIfConfig/ActiveSyntaxVisitor.swift index e202f11809f..4caba69c733 100644 --- a/Sources/SwiftIfConfig/ActiveSyntaxVisitor.swift +++ b/Sources/SwiftIfConfig/ActiveSyntaxVisitor.swift @@ -9,7 +9,6 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// - import SwiftDiagnostics import SwiftSyntax @@ -37,17 +36,18 @@ import SwiftSyntax /// it would not visit either `f` or `g`. /// /// All notes visited by this visitor will have the "active" state, i.e., -/// `node.isActive(in: configuration)` will have evaluated to `.active`. -/// When errors occur, they will be recorded in the array of diagnostics. +/// `node.isActive(in: configuration)` will evaluate to `.active` or will +/// throw. When errors occur, they will be recorded in the set of +/// diagnostics. open class ActiveSyntaxVisitor: SyntaxVisitor { /// The build configuration, which will be queried for each relevant `#if`. public let configuration: Configuration - /// The diagnostics accumulated during this walk of active syntax. - public private(set) var diagnostics: [Diagnostic] = [] + /// The set of diagnostics accumulated during this walk of active syntax. + public var diagnostics: [Diagnostic] = [] - /// Whether we visited any "#if" clauses. - var visitedAnyIfClauses: Bool = false + /// The number of "#if" clauses that were visited. + var numIfClausesVisited: Int = 0 public init(viewMode: SyntaxTreeViewMode, configuration: Configuration) { self.configuration = configuration @@ -55,16 +55,15 @@ open class ActiveSyntaxVisitor: SyntaxVisitor } open override func visit(_ node: IfConfigDeclSyntax) -> SyntaxVisitorContinueKind { - // Note: there is a clone of this code in ActiveSyntaxAnyVisitor. If you - // change one, please also change the other. - let (activeClause, localDiagnostics) = node.activeClause(in: configuration) - diagnostics.append(contentsOf: localDiagnostics) + let activeClause = node.activeClause(in: configuration) { diag in + self.diagnostics.append(diag) + } - visitedAnyIfClauses = true + numIfClausesVisited += 1 // If there is an active clause, visit it's children. if let activeClause, let elements = activeClause.elements { - walk(elements) + walk(Syntax(elements)) } // Skip everything else in the #if. @@ -96,14 +95,19 @@ open class ActiveSyntaxVisitor: SyntaxVisitor /// it would not visit either `f` or `g`. /// /// All notes visited by this visitor will have the "active" state, i.e., -/// `node.isActive(in: configuration)` will have evaluated to `.active`. -/// When errors occur, they will be recorded in the array of diagnostics. +/// `node.isActive(in: configuration)` will evaluate to `.active` or will +/// throw. +/// +/// All notes visited by this visitor will have the "active" state, i.e., +/// `node.isActive(in: configuration)` will evaluate to `.active` or will +/// throw. When errors occur, they will be recorded in the set of +/// diagnostivs. open class ActiveSyntaxAnyVisitor: SyntaxAnyVisitor { /// The build configuration, which will be queried for each relevant `#if`. public let configuration: Configuration - /// The diagnostics accumulated during this walk of active syntax. - public private(set) var diagnostics: [Diagnostic] = [] + /// The set of diagnostics accumulated during this walk of active syntax. + public var diagnostics: [Diagnostic] = [] public init(viewMode: SyntaxTreeViewMode, configuration: Configuration) { self.configuration = configuration @@ -111,15 +115,12 @@ open class ActiveSyntaxAnyVisitor: SyntaxAnyV } open override func visit(_ node: IfConfigDeclSyntax) -> SyntaxVisitorContinueKind { - // Note: there is a clone of this code in ActiveSyntaxVisitor. If you - // change one, please also change the other. - // If there is an active clause, visit it's children. - let (activeClause, localDiagnostics) = node.activeClause(in: configuration) - diagnostics.append(contentsOf: localDiagnostics) - + let activeClause = node.activeClause(in: configuration) { diag in + self.diagnostics.append(diag) + } if let activeClause, let elements = activeClause.elements { - walk(elements) + walk(Syntax(elements)) } // Skip everything else in the #if. diff --git a/Sources/SwiftIfConfig/BuildConfiguration.swift b/Sources/SwiftIfConfig/BuildConfiguration.swift index a3a62a95da8..30cdac9c63d 100644 --- a/Sources/SwiftIfConfig/BuildConfiguration.swift +++ b/Sources/SwiftIfConfig/BuildConfiguration.swift @@ -253,7 +253,7 @@ public protocol BuildConfiguration { /// /// The language version can be queried with the `swift` directive that checks /// how the supported language version compares, as described by - /// [SE-0212](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0212-compiler-version-directive.md). For example: + /// [SE-0212](https://github.com/apple/swift-evolution/blob/main/proposals/0212-compiler-version-directive.md). For example: /// /// ```swift /// #if swift(>=5.5) diff --git a/Sources/SwiftIfConfig/CMakeLists.txt b/Sources/SwiftIfConfig/CMakeLists.txt index 350c134b9f9..3a5d6ece0c1 100644 --- a/Sources/SwiftIfConfig/CMakeLists.txt +++ b/Sources/SwiftIfConfig/CMakeLists.txt @@ -11,7 +11,7 @@ add_swift_syntax_library(SwiftIfConfig ActiveSyntaxRewriter.swift BuildConfiguration.swift ConfiguredRegions.swift - IfConfigRegionState.swift + ConfiguredRegionState.swift IfConfigDecl+IfConfig.swift IfConfigError.swift IfConfigEvaluation.swift @@ -25,4 +25,5 @@ add_swift_syntax_library(SwiftIfConfig target_link_swift_syntax_libraries(SwiftIfConfig PUBLIC SwiftSyntax SwiftDiagnostics - SwiftOperators) + SwiftOperators + SwiftParser) diff --git a/Sources/SwiftIfConfig/IfConfigRegionState.swift b/Sources/SwiftIfConfig/ConfiguredRegionState.swift similarity index 58% rename from Sources/SwiftIfConfig/IfConfigRegionState.swift rename to Sources/SwiftIfConfig/ConfiguredRegionState.swift index af6965de5ed..5596ea6e1d0 100644 --- a/Sources/SwiftIfConfig/IfConfigRegionState.swift +++ b/Sources/SwiftIfConfig/ConfiguredRegionState.swift @@ -9,13 +9,12 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// - import SwiftDiagnostics import SwiftOperators import SwiftSyntax /// Describes the state of a particular region guarded by `#if` or similar. -public enum IfConfigRegionState { +public enum ConfiguredRegionState { /// The region is not part of the compiled program and is not even parsed, /// and therefore many contain syntax that is invalid. case unparsed @@ -24,29 +23,29 @@ public enum IfConfigRegionState { /// The region is active and is part of the compiled program. case active - /// Evaluate the given `#if` condition using the given build configuration - /// to determine its state and identify any problems encountered along the - /// way. - public static func evaluating( - _ condition: some ExprSyntaxProtocol, - in configuration: some BuildConfiguration - ) -> (state: IfConfigRegionState, diagnostics: [Diagnostic]) { + /// Evaluate the given `#if` condition using the given build configuration, throwing an error if there is + /// insufficient information to make a determination. + public init( + condition: some ExprSyntaxProtocol, + configuration: some BuildConfiguration, + diagnosticHandler: ((Diagnostic) -> Void)? = nil + ) throws { // Apply operator folding for !/&&/||. - var foldingDiagnostics: [Diagnostic] = [] - let foldedCondition = OperatorTable.logicalOperators.foldAll(condition) { error in - foldingDiagnostics.append(contentsOf: error.asDiagnostics(at: condition)) + let foldedCondition = try OperatorTable.logicalOperators.foldAll(condition) { error in + diagnosticHandler?(error.asDiagnostic) + throw error }.cast(ExprSyntax.self) - let (active, versioned, evalDiagnostics) = evaluateIfConfig( + let (active, versioned) = try evaluateIfConfig( condition: foldedCondition, - configuration: configuration + configuration: configuration, + diagnosticHandler: diagnosticHandler ) - let diagnostics = foldingDiagnostics + evalDiagnostics switch (active, versioned) { - case (true, _): return (.active, diagnostics) - case (false, false): return (.inactive, diagnostics) - case (false, true): return (.unparsed, diagnostics) + case (true, _): self = .active + case (false, false): self = .inactive + case (false, true): self = .unparsed } } } diff --git a/Sources/SwiftIfConfig/ConfiguredRegions.swift b/Sources/SwiftIfConfig/ConfiguredRegions.swift index 0706a7ba372..f47bb5a33c7 100644 --- a/Sources/SwiftIfConfig/ConfiguredRegions.swift +++ b/Sources/SwiftIfConfig/ConfiguredRegions.swift @@ -9,14 +9,15 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// - import SwiftDiagnostics import SwiftSyntax extension SyntaxProtocol { /// Find all of the #if/#elseif/#else clauses within the given syntax node, - /// indicating their active state. This operation will recurse into all - /// clauses to indicate regions of active / inactive / unparsed code. + /// indicating their active state. This operation will recurse into active + /// clauses to represent the flattened nested structure, while nonactive + /// clauses need no recursion (because there is no relevant structure in + /// them). /// /// For example, given code like the following: /// #if DEBUG @@ -36,7 +37,7 @@ extension SyntaxProtocol { /// - Inactive region for the final `#else`. public func configuredRegions( in configuration: some BuildConfiguration - ) -> [(IfConfigClauseSyntax, IfConfigRegionState)] { + ) -> [(IfConfigClauseSyntax, ConfiguredRegionState)] { let visitor = ConfiguredRegionVisitor(configuration: configuration) visitor.walk(self) return visitor.regions @@ -48,7 +49,7 @@ fileprivate class ConfiguredRegionVisitor: Sy let configuration: Configuration /// The regions we've found so far. - var regions: [(IfConfigClauseSyntax, IfConfigRegionState)] = [] + var regions: [(IfConfigClauseSyntax, ConfiguredRegionState)] = [] /// Whether we are currently within an active region. var inActiveRegion = true @@ -61,7 +62,7 @@ fileprivate class ConfiguredRegionVisitor: Sy override func visit(_ node: IfConfigDeclSyntax) -> SyntaxVisitorContinueKind { // If we're in an active region, find the active clause. Otherwise, // there isn't one. - let activeClause = inActiveRegion ? node.activeClause(in: configuration).clause : nil + let activeClause = inActiveRegion ? node.activeClause(in: configuration) : nil for clause in node.clauses { // If this is the active clause, record it and then recurse into the // elements. @@ -78,9 +79,11 @@ fileprivate class ConfiguredRegionVisitor: Sy } // For inactive clauses, distinguish between inactive and unparsed. - let isVersioned = clause.isVersioned( - configuration: configuration - ).versioned + let isVersioned = + (try? clause.isVersioned( + configuration: configuration, + diagnosticHandler: nil + )) ?? true // If this is within an active region, or this is an unparsed region, // record it. diff --git a/Sources/SwiftIfConfig/IfConfigDecl+IfConfig.swift b/Sources/SwiftIfConfig/IfConfigDecl+IfConfig.swift index 68e9e6b7b48..823724ee965 100644 --- a/Sources/SwiftIfConfig/IfConfigDecl+IfConfig.swift +++ b/Sources/SwiftIfConfig/IfConfigDecl+IfConfig.swift @@ -9,7 +9,6 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// - import SwiftDiagnostics import SwiftSyntax @@ -26,35 +25,35 @@ extension IfConfigDeclSyntax { /// ``` /// /// If the `A` configuration option was passed on the command line (e.g. via `-DA`), the first clause - /// (containing `func f()`) would be returned. If not, and if the `B` configuration was passed on the + /// (containing `func f()`) would be returned. If not, and if the `B`configuration was passed on the /// command line, the second clause (containing `func g()`) would be returned. If neither was /// passed, this function will return `nil` to indicate that none of the regions are active. /// - /// If an error occurs while processing any of the `#if` clauses, + /// If an error occurrs while processing any of the `#if` clauses, /// that clause will be considered inactive and this operation will /// continue to evaluate later clauses. public func activeClause( - in configuration: some BuildConfiguration - ) -> (clause: IfConfigClauseSyntax?, diagnostics: [Diagnostic]) { - var diagnostics: [Diagnostic] = [] + in configuration: some BuildConfiguration, + diagnosticHandler: ((Diagnostic) -> Void)? = nil + ) -> IfConfigClauseSyntax? { for clause in clauses { // If there is no condition, we have reached an unconditional clause. Return it. guard let condition = clause.condition else { - return (clause, diagnostics: diagnostics) + return clause } // If this condition evaluates true, return this clause. - let (isActive, _, localDiagnostics) = evaluateIfConfig( - condition: condition, - configuration: configuration - ) - diagnostics.append(contentsOf: localDiagnostics) - + let isActive = + (try? evaluateIfConfig( + condition: condition, + configuration: configuration, + diagnosticHandler: diagnosticHandler + ))?.active ?? false if isActive { - return (clause, diagnostics: diagnostics) + return clause } } - return (nil, diagnostics: diagnostics) + return nil } } diff --git a/Sources/SwiftIfConfig/IfConfigError.swift b/Sources/SwiftIfConfig/IfConfigError.swift index a0e33506c68..47256debcbb 100644 --- a/Sources/SwiftIfConfig/IfConfigError.swift +++ b/Sources/SwiftIfConfig/IfConfigError.swift @@ -9,7 +9,6 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// - import SwiftDiagnostics import SwiftSyntax diff --git a/Sources/SwiftIfConfig/IfConfigEvaluation.swift b/Sources/SwiftIfConfig/IfConfigEvaluation.swift index 8faec1a7c31..bc9bb17b9ae 100644 --- a/Sources/SwiftIfConfig/IfConfigEvaluation.swift +++ b/Sources/SwiftIfConfig/IfConfigEvaluation.swift @@ -9,7 +9,6 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// - import SwiftDiagnostics import SwiftSyntax @@ -19,38 +18,35 @@ import SwiftSyntax /// folded according to the logical operators table. /// - configuration: The configuration against which the condition will be /// evaluated. +/// - diagnosticHandler: Receives any diagnostics that are produced by the +/// evaluation, whether from errors in the source code or produced by the +/// build configuration itself. /// - Throws: Throws if an error occurs occur during evaluation that prevents /// this function from forming a valid result. The error will /// also be provided to the diagnostic handler before doing so. -/// - Returns: A pair of Boolean values and any diagnostics produced during the -/// evaluation. The first Boolean describes whether the condition holds with -/// the given build configuration. The second Boolean described whether +/// - Returns: A pair of Boolean values. The first describes whether the +/// condition holds with the given build configuration. The second whether /// the build condition is a "versioned" check that implies that we shouldn't /// diagnose syntax errors in blocks where the check fails. func evaluateIfConfig( condition: ExprSyntax, - configuration: some BuildConfiguration -) -> (active: Bool, versioned: Bool, diagnostics: [Diagnostic]) { - var extraDiagnostics: [Diagnostic] = [] - - /// Record the error before returning the given value. - func recordError( - _ error: any Error, - at node: some SyntaxProtocol - ) -> (active: Bool, versioned: Bool, diagnostics: [Diagnostic]) { - return ( - active: false, - versioned: true, - diagnostics: extraDiagnostics + error.asDiagnostics(at: node) - ) + configuration: some BuildConfiguration, + diagnosticHandler: ((Diagnostic) -> Void)? +) throws -> (active: Bool, versioned: Bool) { + /// Record the error before returning it. Use this for every 'throw' site + /// in this evaluation. + func recordedError(_ error: any Error, at node: some SyntaxProtocol) -> any Error { + if let diagnosticHandler { + error.asDiagnostics(at: node).forEach { diagnosticHandler($0) } + } + + return error } /// Record an if-config evaluation error before returning it. Use this for /// every 'throw' site in this evaluation. - func recordError( - _ error: IfConfigError - ) -> (active: Bool, versioned: Bool, diagnostics: [Diagnostic]) { - return recordError(error, at: error.syntax) + func recordedError(_ error: IfConfigError) -> any Error { + return recordedError(error, at: error.syntax) } /// Check a configuration condition, translating any thrown error into an @@ -58,22 +54,17 @@ func evaluateIfConfig( func checkConfiguration( at node: some SyntaxProtocol, body: () throws -> (Bool, Bool) - ) -> (active: Bool, versioned: Bool, diagnostics: [Diagnostic]) { + ) throws -> (active: Bool, versioned: Bool) { do { - let (active, versioned) = try body() - return (active, versioned, extraDiagnostics) + return try body() } catch let error { - return recordError(error, at: node) + throw recordedError(error, at: node) } } // Boolean literals evaluate as-is if let boolLiteral = condition.as(BooleanLiteralExprSyntax.self) { - return ( - active: boolLiteral.literalValue, - versioned: false, - diagnostics: extraDiagnostics - ) + return (active: boolLiteral.literalValue, versioned: false) } // Integer literals aren't allowed, but we recognize them. @@ -82,16 +73,14 @@ func evaluateIfConfig( { let result = intLiteral.literal.text == "1" - return ( - active: result, - versioned: false, - diagnostics: [ - IfConfigError.integerLiteralCondition( - syntax: condition, - replacement: result - ).asDiagnostic - ] + diagnosticHandler?( + IfConfigError.integerLiteralCondition( + syntax: condition, + replacement: result + ).asDiagnostic ) + + return (active: result, versioned: false) } // Declaration references are for custom compilation flags. @@ -100,7 +89,7 @@ func evaluateIfConfig( let ident = identExpr.baseName.text // Evaluate the custom condition. If the build configuration cannot answer this query, fail. - return checkConfiguration(at: identExpr) { + return try checkConfiguration(at: identExpr) { (active: try configuration.isCustomConditionSet(name: ident), versioned: false) } } @@ -109,12 +98,13 @@ func evaluateIfConfig( if let prefixOp = condition.as(PrefixOperatorExprSyntax.self), prefixOp.operator.text == "!" { - let (innerActive, innerVersioned, innerDiagnostics) = evaluateIfConfig( + let (innerActive, innerVersioned) = try evaluateIfConfig( condition: prefixOp.expression, - configuration: configuration + configuration: configuration, + diagnosticHandler: diagnosticHandler ) - return (active: !innerActive, versioned: innerVersioned, diagnostics: innerDiagnostics) + return (active: !innerActive, versioned: innerVersioned) } // Logical '&&' and '||'. @@ -123,43 +113,40 @@ func evaluateIfConfig( (op.operator.text == "&&" || op.operator.text == "||") { // Evaluate the left-hand side. - let (lhsActive, lhsVersioned, lhsDiagnostics) = evaluateIfConfig( + let (lhsActive, lhsVersioned) = try evaluateIfConfig( condition: binOp.leftOperand, - configuration: configuration + configuration: configuration, + diagnosticHandler: diagnosticHandler ) // Short-circuit evaluation if we know the answer and the left-hand side // was versioned. if lhsVersioned { switch (lhsActive, op.operator.text) { - case (true, "||"): - return (active: true, versioned: lhsVersioned, diagnostics: lhsDiagnostics) - case (false, "&&"): - return (active: false, versioned: lhsVersioned, diagnostics: lhsDiagnostics) - default: - break + case (true, "||"): return (active: true, versioned: lhsVersioned) + case (false, "&&"): return (active: false, versioned: lhsVersioned) + default: break } } // Evaluate the right-hand side. - let (rhsActive, rhsVersioned, rhsDiagnostics) = evaluateIfConfig( + let (rhsActive, rhsVersioned) = try evaluateIfConfig( condition: binOp.rightOperand, - configuration: configuration + configuration: configuration, + diagnosticHandler: diagnosticHandler ) switch op.operator.text { case "||": return ( active: lhsActive || rhsActive, - versioned: lhsVersioned && rhsVersioned, - diagnostics: lhsDiagnostics + rhsDiagnostics + versioned: lhsVersioned && rhsVersioned ) case "&&": return ( active: lhsActive && rhsActive, - versioned: lhsVersioned || rhsVersioned, - diagnostics: lhsDiagnostics + rhsDiagnostics + versioned: lhsVersioned || rhsVersioned ) default: @@ -171,9 +158,10 @@ func evaluateIfConfig( if let tuple = condition.as(TupleExprSyntax.self), tuple.isParentheses, let element = tuple.elements.first { - return evaluateIfConfig( + return try evaluateIfConfig( condition: element.expression, - configuration: configuration + configuration: configuration, + diagnosticHandler: diagnosticHandler ) } @@ -186,17 +174,17 @@ func evaluateIfConfig( func doSingleIdentifierArgumentCheck( _ body: (String) throws -> Bool, role: String - ) -> (active: Bool, versioned: Bool, diagnostics: [Diagnostic]) { + ) throws -> (active: Bool, versioned: Bool) { // Ensure that we have a single argument that is a simple identifier. guard let argExpr = call.arguments.singleUnlabeledExpression, let arg = argExpr.simpleIdentifierExpr else { - return recordError( + throw recordedError( .requiresUnlabeledArgument(name: fnName, role: role, syntax: ExprSyntax(call)) ) } - return checkConfiguration(at: argExpr) { + return try checkConfiguration(at: argExpr) { (active: try body(arg), versioned: fn.isVersioned) } } @@ -204,13 +192,13 @@ func evaluateIfConfig( /// Perform a check for a version constraint as used in the "swift" or "compiler" version checks. func doVersionComparisonCheck( _ actualVersion: VersionTuple - ) -> (active: Bool, versioned: Bool, diagnostics: [Diagnostic]) { + ) throws -> (active: Bool, versioned: Bool) { // Ensure that we have a single unlabeled argument that is either >= or < as a prefix // operator applied to a version. guard let argExpr = call.arguments.singleUnlabeledExpression, let unaryArg = argExpr.as(PrefixOperatorExprSyntax.self) else { - return recordError( + throw recordedError( .requiresUnlabeledArgument( name: fnName, role: "version comparison (>= or <= a version)", @@ -222,48 +210,40 @@ func evaluateIfConfig( // Parse the version. let opToken = unaryArg.operator guard let version = VersionTuple(parsing: unaryArg.expression.trimmedDescription) else { - return recordError(.invalidVersionOperand(name: fnName, syntax: unaryArg.expression)) + throw recordedError(.invalidVersionOperand(name: fnName, syntax: unaryArg.expression)) } switch opToken.text { case ">=": - return ( - active: actualVersion >= version, - versioned: fn.isVersioned, - diagnostics: extraDiagnostics - ) + return (active: actualVersion >= version, versioned: fn.isVersioned) case "<": - return ( - active: actualVersion < version, - versioned: fn.isVersioned, - diagnostics: extraDiagnostics - ) + return (active: actualVersion < version, versioned: fn.isVersioned) default: - return recordError(.unsupportedVersionOperator(name: fnName, operator: opToken)) + throw recordedError(.unsupportedVersionOperator(name: fnName, operator: opToken)) } } switch fn { case .hasAttribute: - return doSingleIdentifierArgumentCheck(configuration.hasAttribute, role: "attribute") + return try doSingleIdentifierArgumentCheck(configuration.hasAttribute, role: "attribute") case .hasFeature: - return doSingleIdentifierArgumentCheck(configuration.hasFeature, role: "feature") + return try doSingleIdentifierArgumentCheck(configuration.hasFeature, role: "feature") case .os: - return doSingleIdentifierArgumentCheck(configuration.isActiveTargetOS, role: "operating system") + return try doSingleIdentifierArgumentCheck(configuration.isActiveTargetOS, role: "operating system") case .arch: - return doSingleIdentifierArgumentCheck(configuration.isActiveTargetArchitecture, role: "architecture") + return try doSingleIdentifierArgumentCheck(configuration.isActiveTargetArchitecture, role: "architecture") case .targetEnvironment: - return doSingleIdentifierArgumentCheck(configuration.isActiveTargetEnvironment, role: "environment") + return try doSingleIdentifierArgumentCheck(configuration.isActiveTargetEnvironment, role: "environment") case ._runtime: - return doSingleIdentifierArgumentCheck(configuration.isActiveTargetRuntime, role: "runtime") + return try doSingleIdentifierArgumentCheck(configuration.isActiveTargetRuntime, role: "runtime") case ._ptrauth: - return doSingleIdentifierArgumentCheck( + return try doSingleIdentifierArgumentCheck( configuration.isActiveTargetPointerAuthentication, role: "pointer authentication scheme" ) @@ -275,7 +255,7 @@ func evaluateIfConfig( let arg = argExpr.simpleIdentifierExpr, let expectedEndianness = Endianness(rawValue: arg) else { - return recordError( + throw recordedError( .requiresUnlabeledArgument( name: fnName, role: "endiannes ('big' or 'little')", @@ -286,8 +266,7 @@ func evaluateIfConfig( return ( active: configuration.endianness == expectedEndianness, - versioned: fn.isVersioned, - diagnostics: extraDiagnostics + versioned: fn.isVersioned ) case ._pointerBitWidth, ._hasAtomicBitWidth: @@ -299,7 +278,7 @@ func evaluateIfConfig( argFirst == "_", let expectedBitWidth = Int(arg.dropFirst()) else { - return recordError( + throw recordedError( .requiresUnlabeledArgument( name: fnName, role: "bit width ('_' followed by an integer)", @@ -317,13 +296,13 @@ func evaluateIfConfig( fatalError("extraneous case above not handled") } - return (active: active, versioned: fn.isVersioned, diagnostics: extraDiagnostics) + return (active: active, versioned: fn.isVersioned) case .swift: - return doVersionComparisonCheck(configuration.languageVersion) + return try doVersionComparisonCheck(configuration.languageVersion) case .compiler: - return doVersionComparisonCheck(configuration.compilerVersion) + return try doVersionComparisonCheck(configuration.compilerVersion) case ._compiler_version: // Argument is a single unlabeled argument containing a string @@ -334,7 +313,7 @@ func evaluateIfConfig( let segment = stringLiteral.segments.first, case .stringSegment(let stringSegment) = segment else { - return recordError( + throw recordedError( .requiresUnlabeledArgument( name: "_compiler_version", role: "version", @@ -344,17 +323,11 @@ func evaluateIfConfig( } let versionString = stringSegment.content.text - let expectedVersion: VersionTuple - do { - expectedVersion = try VersionTuple(parsingCompilerBuildVersion: versionString, argExpr) - } catch { - return recordError(error, at: stringSegment.content) - } + let expectedVersion = try VersionTuple(parsingCompilerBuildVersion: versionString, argExpr) return ( active: configuration.compilerVersion >= expectedVersion, - versioned: fn.isVersioned, - diagnostics: extraDiagnostics + versioned: fn.isVersioned ) case .canImport: @@ -363,7 +336,7 @@ func evaluateIfConfig( guard let firstArg = call.arguments.first, firstArg.label == nil else { - return recordError(.canImportMissingModule(syntax: ExprSyntax(call))) + throw recordedError(.canImportMissingModule(syntax: ExprSyntax(call))) } // FIXME: This is a gross hack. Actually look at the sequence of @@ -375,7 +348,7 @@ func evaluateIfConfig( let version: CanImportVersion if let secondArg = call.arguments.dropFirst().first { if secondArg.label?.text != "_version" && secondArg.label?.text != "_underlyingVersion" { - return recordError(.canImportLabel(syntax: secondArg.expression)) + throw recordedError(.canImportLabel(syntax: secondArg.expression)) } let versionText: String @@ -390,7 +363,7 @@ func evaluateIfConfig( } guard var versionTuple = VersionTuple(parsing: versionText) else { - return recordError( + throw recordedError( .invalidVersionOperand(name: "canImport", syntax: secondArg.expression) ) } @@ -401,7 +374,7 @@ func evaluateIfConfig( versionTuple.components.removeSubrange(4...) // Warn that we did this. - extraDiagnostics.append( + diagnosticHandler?( IfConfigError.ignoredTrailingComponents( version: versionTuple, syntax: secondArg.expression @@ -417,13 +390,13 @@ func evaluateIfConfig( } if call.arguments.count > 2 { - return recordError(.canImportTwoParameters(syntax: ExprSyntax(call))) + throw recordedError(.canImportTwoParameters(syntax: ExprSyntax(call))) } } else { version = .unversioned } - return checkConfiguration(at: call) { + return try checkConfiguration(at: call) { ( active: try configuration.canImport( importPath: importPath.map { String($0) }, @@ -435,24 +408,24 @@ func evaluateIfConfig( } } - return recordError(.unknownExpression(condition)) + throw recordedError(.unknownExpression(condition)) } extension IfConfigClauseSyntax { /// Determine whether this condition is "versioned". func isVersioned( - configuration: some BuildConfiguration - ) -> (versioned: Bool, diagnostics: [Diagnostic]) { - guard let condition else { - return (versioned: false, diagnostics: []) - } + configuration: some BuildConfiguration, + diagnosticHandler: ((Diagnostic) -> Void)? + ) throws -> Bool { + guard let condition else { return false } // Evaluate this condition against the build configuration. - let (_, versioned, diagnostics) = evaluateIfConfig( + let (_, versioned) = try evaluateIfConfig( condition: condition, - configuration: configuration + configuration: configuration, + diagnosticHandler: diagnosticHandler ) - return (versioned, diagnostics) + return versioned } } diff --git a/Sources/SwiftIfConfig/SwiftIfConfig.docc/SwiftIfConfig.md b/Sources/SwiftIfConfig/SwiftIfConfig.docc/SwiftIfConfig.md index cc1d40a57c4..15e3922b0cf 100644 --- a/Sources/SwiftIfConfig/SwiftIfConfig.docc/SwiftIfConfig.md +++ b/Sources/SwiftIfConfig/SwiftIfConfig.docc/SwiftIfConfig.md @@ -1,4 +1,4 @@ -# SwiftIfConfig +# `SwiftIfConfig` A library to evaluate `#if` conditionals within a Swift syntax tree. @@ -27,7 +27,7 @@ The syntax tree and its parser do not reason about the build configuration. Rath The `SwiftIfConfig` library provides utilities to determine which syntax nodes are part of a particular build configuration. Each utility requires that one provide a specific build configuration (i.e., an instance of a type that conforms to the protocol), and provides a different view on essentially the same information: * and are visitor types that only visit the syntax nodes that are included ("active") for a given build configuration, implicitly skipping any nodes within inactive `#if` clauses. -* `SyntaxProtocol.removingInactive(in:)` produces a syntax node that removes all inactive regions (and their corresponding `IfConfigDeclSyntax` nodes) from the given syntax tree, returning a new tree that is free of `#if` conditions. +* `SyntaxProtocol/removingInactive(in:)` produces a syntax node that removes all inactive regions (and their corresponding `IfConfigDeclSyntax` nodes) from the given syntax tree, returning a new tree that is free of `#if` conditions. * `IfConfigDeclSyntax.activeClause(in:)` determines which of the clauses of an `#if` is active for the given build configuration, returning the active clause. * `SyntaxProtocol.isActive(in:)` determines whether the given syntax node is active for the given build configuration. The result is one of "active" (the node is included in the program), "inactive" (the node is not included diff --git a/Sources/SwiftIfConfig/SyntaxLiteralUtils.swift b/Sources/SwiftIfConfig/SyntaxLiteralUtils.swift index ebe6ec9af69..85b946066e7 100644 --- a/Sources/SwiftIfConfig/SyntaxLiteralUtils.swift +++ b/Sources/SwiftIfConfig/SyntaxLiteralUtils.swift @@ -9,7 +9,6 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// - import SwiftSyntax extension BooleanLiteralExprSyntax { diff --git a/Sources/SwiftIfConfig/SyntaxProtocol+IfConfig.swift b/Sources/SwiftIfConfig/SyntaxProtocol+IfConfig.swift index 12c6dc2c5d2..e2b012dfd68 100644 --- a/Sources/SwiftIfConfig/SyntaxProtocol+IfConfig.swift +++ b/Sources/SwiftIfConfig/SyntaxProtocol+IfConfig.swift @@ -9,7 +9,6 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// - import SwiftDiagnostics import SwiftSyntax @@ -31,33 +30,35 @@ extension SyntaxProtocol { /// a call to `isActive` on the syntax node for the function `g` would return `active` when the /// configuration options `DEBUG` and `B` are provided, but `A` is not. public func isActive( - in configuration: some BuildConfiguration - ) -> (state: IfConfigRegionState, diagnostics: [Diagnostic]) { + in configuration: some BuildConfiguration, + diagnosticHandler: ((Diagnostic) -> Void)? = nil + ) throws -> ConfiguredRegionState { var currentNode: Syntax = Syntax(self) - var currentState: IfConfigRegionState = .active - var diagnostics: [Diagnostic] = [] + var currentState: ConfiguredRegionState = .active while let parent = currentNode.parent { // If the parent is an `#if` configuration, check whether our current // clause is active. If not, we're in an inactive region. We also - // need to determine whether an inactive region should be parsed or not. + // need to determine whether if let ifConfigClause = currentNode.as(IfConfigClauseSyntax.self), let ifConfigDecl = ifConfigClause.parent?.parent?.as(IfConfigDeclSyntax.self) { - let (activeClause, localDiagnostics) = ifConfigDecl.activeClause(in: configuration) - diagnostics.append(contentsOf: localDiagnostics) + let activeClause = ifConfigDecl.activeClause( + in: configuration, + diagnosticHandler: diagnosticHandler + ) if activeClause != ifConfigClause { // This was not the active clause, so we know that we're in an // inactive block. However, if the condition is versioned, this is an // unparsed region. - let (isVersioned, localDiagnostics) = ifConfigClause.isVersioned( - configuration: configuration - ) - diagnostics.append(contentsOf: localDiagnostics) - + let isVersioned = + (try? ifConfigClause.isVersioned( + configuration: configuration, + diagnosticHandler: diagnosticHandler + )) ?? true if isVersioned { - return (.unparsed, diagnostics) + return .unparsed } currentState = .inactive @@ -67,19 +68,18 @@ extension SyntaxProtocol { currentNode = parent } - return (currentState, diagnostics) + return currentState } /// Determine whether the given syntax node is active given a set of /// configured regions as produced by `configuredRegions(in:)`. /// - /// If you are querying whether many syntax nodes in a particular file are - /// active, consider calling `configuredRegions(in:)` once and using - /// this function. For occasional queries, use `isActive(in:)`. + /// This is + /// an approximation public func isActive( - inConfiguredRegions regions: [(IfConfigClauseSyntax, IfConfigRegionState)] - ) -> IfConfigRegionState { - var currentState: IfConfigRegionState = .active + inConfiguredRegions regions: [(IfConfigClauseSyntax, ConfiguredRegionState)] + ) -> ConfiguredRegionState { + var currentState: ConfiguredRegionState = .active for (ifClause, state) in regions { if self.position < ifClause.position { return currentState diff --git a/Sources/SwiftIfConfig/VersionTuple+Parsing.swift b/Sources/SwiftIfConfig/VersionTuple+Parsing.swift index c8dc16eb8ec..03cad60653b 100644 --- a/Sources/SwiftIfConfig/VersionTuple+Parsing.swift +++ b/Sources/SwiftIfConfig/VersionTuple+Parsing.swift @@ -9,7 +9,6 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// - import SwiftSyntax extension VersionTuple { diff --git a/Sources/SwiftLexicalLookup/Configurations/FileScopeHandlingConfig.swift b/Sources/SwiftLexicalLookup/Configurations/FileScopeNameIntroductionStrategy.swift similarity index 81% rename from Sources/SwiftLexicalLookup/Configurations/FileScopeHandlingConfig.swift rename to Sources/SwiftLexicalLookup/Configurations/FileScopeNameIntroductionStrategy.swift index 34c0861a91e..743f997218c 100644 --- a/Sources/SwiftLexicalLookup/Configurations/FileScopeHandlingConfig.swift +++ b/Sources/SwiftLexicalLookup/Configurations/FileScopeNameIntroductionStrategy.swift @@ -14,10 +14,12 @@ import SwiftSyntax /// Specifies how names should be introduced at the file scope. @_spi(Experimental) public enum FileScopeHandlingConfig { - /// This is the behavior that is being used + /// This is the behavior that is being used /// for Swift files with top-level code. case memberBlockUpToLastDecl - /// This is the behavior that is being used + /// This is the behavior that is being used /// for Swift files that don’t allow top-level code. case memberBlock + /// This is the behavior that is being used for e.g. function bodies. + case codeBlock } diff --git a/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift b/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift index 9a23170a74d..9be86f972ba 100644 --- a/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift @@ -16,7 +16,7 @@ protocol IntroducingToSequentialParentScopeSyntax: ScopeSyntax { /// Returns names matching lookup that should be /// handled by it's parent sequential scope. func introducesToSequentialParent( - for identifier: Identifier?, + for name: String?, at syntax: SyntaxProtocol, with config: LookupConfig, state: LookupState diff --git a/Sources/SwiftLexicalLookup/LookupName.swift b/Sources/SwiftLexicalLookup/LookupName.swift index 6be060fe68a..b2caf3e40c6 100644 --- a/Sources/SwiftLexicalLookup/LookupName.swift +++ b/Sources/SwiftLexicalLookup/LookupName.swift @@ -40,8 +40,8 @@ import SwiftSyntax } } - /// Name associated with implicit name kind. - private var name: String { + /// Used for name comparison. + var name: String { switch self { case .self: "self" @@ -55,11 +55,6 @@ import SwiftSyntax "oldValue" } } - - /// Identifier used for name comparison. - var identifier: Identifier { - Identifier(name) - } } @_spi(Experimental) public enum LookupName { @@ -68,8 +63,8 @@ import SwiftSyntax case identifier(IdentifiableSyntax, accessibleAfter: AbsolutePosition?) /// Declaration associated with the name. /// Could be class, struct, actor, protocol, function and more. - case declaration(NamedDeclSyntax) - /// Name introduced implicitly by certain syntax nodes. + case declaration(NamedDeclSyntax, accessibleAfter: AbsolutePosition?) + /// Name introduced implicitly certain syntax nodes. case implicit(LookupImplicitNameKind) /// Syntax associated with this name. @@ -77,22 +72,22 @@ import SwiftSyntax switch self { case .identifier(let syntax, _): syntax - case .declaration(let syntax): + case .declaration(let syntax, _): syntax case .implicit(let implicitName): implicitName.syntax } } - /// Identifier used for name comparison. + /// Introduced name. @_spi(Experimental) public var identifier: Identifier? { switch self { case .identifier(let syntax, _): Identifier(syntax.identifier) - case .declaration(let syntax): + case .declaration(let syntax, _): Identifier(syntax.name) - case .implicit(let kind): - kind.identifier + default: + nil } } @@ -100,10 +95,20 @@ import SwiftSyntax /// If set to `nil`, the name is available at any point in scope. var accessibleAfter: AbsolutePosition? { switch self { - case .identifier(_, let absolutePosition): - return absolutePosition + case .identifier(_, let absolutePosition), .declaration(_, let absolutePosition): + absolutePosition default: - return nil + nil + } + } + + /// Used for name comparison. + var name: String? { + switch self { + case .identifier, .declaration: + identifier?.name + case .implicit(let implicitName): + implicitName.name } } @@ -114,9 +119,9 @@ import SwiftSyntax } /// Checks if this name refers to the looked up phrase. - func refersTo(_ lookedUpIdentifier: Identifier) -> Bool { - guard let identifier else { return false } - return identifier == lookedUpIdentifier + func refersTo(_ lookedUpName: String) -> Bool { + guard let name else { return false } + return name == lookedUpName } /// Extracts names introduced by the given `syntax` structure. @@ -133,41 +138,38 @@ import SwiftSyntax ) -> [LookupName] { switch Syntax(syntax).as(SyntaxEnum.self) { case .variableDecl(let variableDecl): - return variableDecl.bindings.flatMap { binding in - getNames( - from: binding.pattern, - accessibleAfter: accessibleAfter != nil ? binding.endPositionBeforeTrailingTrivia : nil - ) + variableDecl.bindings.flatMap { binding in + getNames(from: binding.pattern, accessibleAfter: accessibleAfter != nil ? binding.endPositionBeforeTrailingTrivia : nil) } case .tuplePattern(let tuplePattern): - return tuplePattern.elements.flatMap { tupleElement in + tuplePattern.elements.flatMap { tupleElement in getNames(from: tupleElement.pattern, accessibleAfter: accessibleAfter) } case .valueBindingPattern(let valueBindingPattern): - return getNames(from: valueBindingPattern.pattern, accessibleAfter: accessibleAfter) + getNames(from: valueBindingPattern.pattern, accessibleAfter: accessibleAfter) case .expressionPattern(let expressionPattern): - return getNames(from: expressionPattern.expression, accessibleAfter: accessibleAfter) + getNames(from: expressionPattern.expression, accessibleAfter: accessibleAfter) case .sequenceExpr(let sequenceExpr): - return sequenceExpr.elements.flatMap { expression in + sequenceExpr.elements.flatMap { expression in getNames(from: expression, accessibleAfter: accessibleAfter) } case .patternExpr(let patternExpr): - return getNames(from: patternExpr.pattern, accessibleAfter: accessibleAfter) + getNames(from: patternExpr.pattern, accessibleAfter: accessibleAfter) case .optionalBindingCondition(let optionalBinding): - return getNames(from: optionalBinding.pattern, accessibleAfter: accessibleAfter) + getNames(from: optionalBinding.pattern, accessibleAfter: accessibleAfter) case .matchingPatternCondition(let matchingPatternCondition): - return getNames(from: matchingPatternCondition.pattern, accessibleAfter: accessibleAfter) + getNames(from: matchingPatternCondition.pattern, accessibleAfter: accessibleAfter) case .functionCallExpr(let functionCallExpr): - return functionCallExpr.arguments.flatMap { argument in + functionCallExpr.arguments.flatMap { argument in getNames(from: argument.expression, accessibleAfter: accessibleAfter) } default: if let namedDecl = Syntax(syntax).asProtocol(SyntaxProtocol.self) as? NamedDeclSyntax { - return handle(namedDecl: namedDecl, accessibleAfter: accessibleAfter) + handle(namedDecl: namedDecl, accessibleAfter: accessibleAfter) } else if let identifiable = Syntax(syntax).asProtocol(SyntaxProtocol.self) as? IdentifiableSyntax { - return handle(identifiable: identifiable, accessibleAfter: accessibleAfter) + handle(identifiable: identifiable, accessibleAfter: accessibleAfter) } else { - return [] + [] } } } @@ -193,6 +195,6 @@ import SwiftSyntax namedDecl: NamedDeclSyntax, accessibleAfter: AbsolutePosition? = nil ) -> [LookupName] { - [.declaration(namedDecl)] + [.declaration(namedDecl, accessibleAfter: accessibleAfter)] } } diff --git a/Sources/SwiftLexicalLookup/LookupResult.swift b/Sources/SwiftLexicalLookup/LookupResult.swift index 3fb4c30b046..6fabf2060dc 100644 --- a/Sources/SwiftLexicalLookup/LookupResult.swift +++ b/Sources/SwiftLexicalLookup/LookupResult.swift @@ -23,9 +23,9 @@ import SwiftSyntax @_spi(Experimental) public var scope: ScopeSyntax? { switch self { case .fromScope(let scopeSyntax, _): - return scopeSyntax + scopeSyntax case .fromFileScope(let fileScopeSyntax, _): - return fileScopeSyntax + fileScopeSyntax } } @@ -33,7 +33,7 @@ import SwiftSyntax @_spi(Experimental) public var names: [LookupName] { switch self { case .fromScope(_, let names), .fromFileScope(_, let names): - return names + names } } } diff --git a/Sources/SwiftLexicalLookup/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/ScopeImplementations.swift index 2764e057904..85838bc0abe 100644 --- a/Sources/SwiftLexicalLookup/ScopeImplementations.swift +++ b/Sources/SwiftLexicalLookup/ScopeImplementations.swift @@ -14,11 +14,11 @@ import SwiftSyntax @_spi(Experimental) extension SyntaxProtocol { /// Parent scope of this syntax node, or scope introduced by this syntax node. - var scope: ScopeSyntax? { + @_spi(Experimental) public var scope: ScopeSyntax? { if let scopeSyntax = Syntax(self).asProtocol(SyntaxProtocol.self) as? ScopeSyntax { - return scopeSyntax + scopeSyntax } else { - return self.parent?.scope + self.parent?.scope } } } @@ -69,6 +69,10 @@ import SwiftSyntax } } } + case .codeBlock: + return statements.flatMap { codeBlockItem in + LookupName.getNames(from: codeBlockItem.item, accessibleAfter: codeBlockItem.endPosition) + } case .memberBlock: return statements.flatMap { codeBlockItem in LookupName.getNames(from: codeBlockItem.item) @@ -97,16 +101,25 @@ import SwiftSyntax /// - for `memberBlock` - a, b, c, d, e, f /// - for `codeBlock` - a @_spi(Experimental) public func lookup( - for identifier: Identifier?, + for name: String?, at syntax: SyntaxProtocol, with config: LookupConfig, state: LookupState ) -> [LookupResult] { switch config.fileScopeHandling { + case .codeBlock: + return sequentialLookup( + in: statements, + for: name, + at: syntax, + with: config, + state: state, + createResultsForThisScopeWith: { .fromFileScope(self, withNames: $0) } + ) case .memberBlock: let names = introducedNames(using: .memberBlock) .filter { lookupName in - checkName(identifier, refersTo: lookupName, at: syntax) + does(name: name, referTo: lookupName, at: syntax) } return names.isEmpty ? [] : [.fromFileScope(self, withNames: names)] @@ -124,7 +137,7 @@ import SwiftSyntax if item.is(DeclSyntax.self) || item.is(VariableDeclSyntax.self) { let foundNames = LookupName.getNames(from: item) - members.append(contentsOf: foundNames.filter { checkName(identifier, refersTo: $0, at: syntax) }) + members.append(contentsOf: foundNames.filter { does(name: name, referTo: $0, at: syntax) }) } else { encounteredNonDeclaration = true sequentialItems.append(codeBlockItem) @@ -134,7 +147,7 @@ import SwiftSyntax let sequentialNames = sequentialLookup( in: sequentialItems, - for: identifier, + for: name, at: syntax, with: config, state: state, @@ -156,14 +169,14 @@ import SwiftSyntax } @_spi(Experimental) public func lookup( - for identifier: Identifier?, + for name: String?, at syntax: SyntaxProtocol, with config: LookupConfig, state: LookupState ) -> [LookupResult] { sequentialLookup( in: statements, - for: identifier, + for: name, at: syntax, with: config, state: state, @@ -278,15 +291,15 @@ import SwiftSyntax /// } /// ``` @_spi(Experimental) public func lookup( - for identifier: Identifier?, + for name: String?, at syntax: SyntaxProtocol, with config: LookupConfig, state: LookupState ) -> [LookupResult] { if let elseBody, elseBody.position <= syntax.position, elseBody.endPosition >= syntax.position { - return lookupInParent(for: identifier, at: syntax, with: config, state: state) + lookupInParent(for: name, at: syntax, with: config, state: state) } else { - return defaultLookupImplementation(for: identifier, at: syntax, with: config, state: state) + defaultLookupImplementation(for: name, at: syntax, with: config, state: state) } } } @@ -302,7 +315,7 @@ import SwiftSyntax @_spi(Experimental) extension GuardStmtSyntax: IntroducingToSequentialParentScopeSyntax { @_spi(Experimental) public func introducesToSequentialParent( - for identifier: Identifier?, + for name: String?, at syntax: SwiftSyntax.SyntaxProtocol, with config: LookupConfig, state: LookupState @@ -310,7 +323,7 @@ import SwiftSyntax let names = conditions.flatMap { element in LookupName.getNames(from: element.condition, accessibleAfter: element.endPosition) }.filter { introducedName in - checkName(identifier, refersTo: introducedName, at: syntax) + does(name: name, referTo: introducedName, at: syntax) } return names.isEmpty ? [] : [.fromScope(self, withNames: names)] @@ -332,7 +345,7 @@ import SwiftSyntax /// // a is visible here /// ``` @_spi(Experimental) public func lookup( - for identifier: Identifier?, + for name: String?, at syntax: SyntaxProtocol, with config: LookupConfig, state: LookupState @@ -340,9 +353,9 @@ import SwiftSyntax if body.position <= syntax.position && body.endPosition >= syntax.position { var newState = state newState.skipSequentialIntroductionFrom = self - return lookupInParent(for: identifier, at: syntax, with: config, state: newState) + return lookupInParent(for: name, at: syntax, with: config, state: newState) } else { - return defaultLookupImplementation(for: identifier, at: syntax, with: config, state: state) + return defaultLookupImplementation(for: name, at: syntax, with: config, state: state) } } } @@ -358,15 +371,15 @@ import SwiftSyntax /// withing the accessor.. @_spi(Experimental) public var introducedNames: [LookupName] { if let parameters { - return LookupName.getNames(from: parameters) + LookupName.getNames(from: parameters) } else { switch accessorSpecifier.tokenKind { case .keyword(.set), .keyword(.willSet): - return [.implicit(.newValue(self))] + [.implicit(.newValue(self))] case .keyword(.didSet): - return [.implicit(.oldValue(self))] + [.implicit(.oldValue(self))] default: - return [] + [] } } } diff --git a/Sources/SwiftLexicalLookup/ScopeSyntax.swift b/Sources/SwiftLexicalLookup/ScopeSyntax.swift index ba02c85eb5e..8358378a446 100644 --- a/Sources/SwiftLexicalLookup/ScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/ScopeSyntax.swift @@ -16,8 +16,8 @@ extension SyntaxProtocol { /// Returns all names that `for` refers to at this syntax node. /// Optional configuration can be passed as `config` to customize the lookup behavior. /// - /// - Returns: An array of `LookupResult` for `identifier` at this syntax node, - /// ordered by visibility. If `identifier` is set to `nil`, returns all available names ordered by visibility. + /// - Returns: An array of `LookupResult` for `name` at this syntax node, + /// ordered by visibility. If `name` is set to `nil`, returns all available names ordered by visibility. /// The order is from the innermost to the outermost scope, /// and within each result, names are ordered by their introduction /// in the source code. @@ -44,10 +44,10 @@ extension SyntaxProtocol { /// in this exact order. The constant declaration within the function body is omitted /// due to the ordering rules that prioritize visibility within the function body. @_spi(Experimental) public func lookup( - for identifier: Identifier?, + for name: String?, with config: LookupConfig = LookupConfig() ) -> [LookupResult] { - scope?.lookup(for: identifier, at: self, with: config, state: LookupState()) ?? [] + scope?.lookup(for: name, at: self, with: config, state: LookupState()) ?? [] } } @@ -59,7 +59,7 @@ extension SyntaxProtocol { /// Finds all declarations `name` refers to. `syntax` specifies the node lookup was triggered with. /// If `name` set to `nil`, returns all available names at the given node. func lookup( - for identifier: Identifier?, + for name: String?, at syntax: SyntaxProtocol, with config: LookupConfig, state: LookupState @@ -75,19 +75,19 @@ extension SyntaxProtocol { /// refers to and is accessible at given syntax node then passes lookup to the parent. /// If `name` set to `nil`, returns all available names at the given node. @_spi(Experimental) public func lookup( - for identifier: Identifier?, + for name: String?, at syntax: SyntaxProtocol, with config: LookupConfig, state: LookupState ) -> [LookupResult] { - defaultLookupImplementation(for: identifier, at: syntax, with: config, state: state) + defaultLookupImplementation(for: name, at: syntax, with: config, state: state) } /// Returns `LookupResult` of all names introduced in this scope that `name` /// refers to and is accessible at given syntax node then passes lookup to the parent. /// If `name` set to `nil`, returns all available names at the given node. func defaultLookupImplementation( - for identifier: Identifier?, + for name: String?, at syntax: SyntaxProtocol, with config: LookupConfig, state: LookupState @@ -95,28 +95,28 @@ extension SyntaxProtocol { let filteredNames = introducedNames .filter { introducedName in - checkName(identifier, refersTo: introducedName, at: syntax) + does(name: name, referTo: introducedName, at: syntax) } if filteredNames.isEmpty { - return lookupInParent(for: identifier, at: syntax, with: config, state: state) + return lookupInParent(for: name, at: syntax, with: config, state: state) } else { return [.fromScope(self, withNames: filteredNames)] - + lookupInParent(for: identifier, at: syntax, with: config, state: state) + + lookupInParent(for: name, at: syntax, with: config, state: state) } } /// Looks up in parent scope. func lookupInParent( - for identifier: Identifier?, + for name: String?, at syntax: SyntaxProtocol, with config: LookupConfig, state: LookupState ) -> [LookupResult] { - parentScope?.lookup(for: identifier, at: syntax, with: config, state: state) ?? [] + parentScope?.lookup(for: name, at: syntax, with: config, state: state) ?? [] } - func checkName(_ name: Identifier?, refersTo introducedName: LookupName, at syntax: SyntaxProtocol) -> Bool { + func does(name: String?, referTo introducedName: LookupName, at syntax: SyntaxProtocol) -> Bool { introducedName.isAccessible(at: syntax) && (name == nil || introducedName.refersTo(name!)) } } diff --git a/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift index 5300a9af311..16b7702fb2c 100644 --- a/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift @@ -21,7 +21,7 @@ protocol SequentialScopeSyntax: ScopeSyntax { /// scopes that match the lookup. func sequentialLookup( in codeBlockItems: any Collection, - for identifier: Identifier?, + for name: String?, at syntax: SyntaxProtocol, with config: LookupConfig, state: LookupState, @@ -32,7 +32,7 @@ protocol SequentialScopeSyntax: ScopeSyntax { extension SequentialScopeSyntax { func sequentialLookup( in codeBlockItems: any Collection, - for identifier: Identifier?, + for name: String?, at syntax: SyntaxProtocol, with config: LookupConfig, state: LookupState, @@ -61,7 +61,7 @@ extension SequentialScopeSyntax { // Add names introduced by the encountered scope. result.append( contentsOf: introducingToParentScope.introducesToSequentialParent( - for: identifier, + for: name, at: syntax, with: config, state: state @@ -75,7 +75,7 @@ extension SequentialScopeSyntax { from: codeBlockItem.item, accessibleAfter: codeBlockItem.endPosition ).filter { introducedName in - checkName(identifier, refersTo: introducedName, at: syntax) + does(name: name, referTo: introducedName, at: syntax) } ) } @@ -87,7 +87,6 @@ extension SequentialScopeSyntax { currentChunk = [] } - return (result.isEmpty ? [] : result.reversed()) - + lookupInParent(for: identifier, at: syntax, with: config, state: state) + return (result.isEmpty ? [] : result.reversed()) + lookupInParent(for: name, at: syntax, with: config, state: state) } } diff --git a/Sources/SwiftRefactor/SyntaxUtils.swift b/Sources/SwiftRefactor/SyntaxUtils.swift index 62a108c325c..4949a5bbff6 100644 --- a/Sources/SwiftRefactor/SyntaxUtils.swift +++ b/Sources/SwiftRefactor/SyntaxUtils.swift @@ -11,7 +11,7 @@ //===----------------------------------------------------------------------===// #if swift(>=6) -internal import SwiftSyntax +public import SwiftSyntax #else import SwiftSyntax #endif diff --git a/Sources/SwiftSyntax/Identifier.swift b/Sources/SwiftSyntax/Identifier.swift index ed0b511f0e1..0864e754744 100644 --- a/Sources/SwiftSyntax/Identifier.swift +++ b/Sources/SwiftSyntax/Identifier.swift @@ -12,58 +12,23 @@ /// A canonicalized representation of an identifier that strips away backticks. public struct Identifier: Equatable, Hashable, Sendable { - enum IdentifierKind: Hashable { - case token(raw: RawIdentifier, arena: SyntaxArenaRef) - case string(String) - - static func sanitize(string: String) -> IdentifierKind { - let backtick = "`" - if string.count > 2 && string.hasPrefix(backtick) && string.hasSuffix(backtick) { - let startIndex = string.index(after: string.startIndex) - let endIndex = string.index(before: string.endIndex) - return .string(String(string[startIndex.. Bool { - lhs.name == rhs.name + self.raw = RawIdentifier(token.tokenView) + self.arena = token.tokenView.raw.arenaReference } } diff --git a/Sources/SwiftSyntaxBuilder/SyntaxNodeWithBody.swift b/Sources/SwiftSyntaxBuilder/SyntaxNodeWithBody.swift index 6d0f2dae993..2a00a4f2dc3 100644 --- a/Sources/SwiftSyntaxBuilder/SyntaxNodeWithBody.swift +++ b/Sources/SwiftSyntaxBuilder/SyntaxNodeWithBody.swift @@ -12,10 +12,8 @@ #if swift(>=6) public import SwiftSyntax -internal import SwiftParser #else import SwiftSyntax -import SwiftParser #endif // MARK: - PartialSyntaxNode @@ -126,22 +124,7 @@ extension WithOptionalCodeBlockSyntax where Self: DeclSyntaxProtocol { _ header: SyntaxNodeString, @CodeBlockItemListBuilder bodyBuilder: () throws -> CodeBlockItemListSyntax ) throws { - // If the type provides a custom `SyntaxParseable` implementation, use that. Otherwise construct it as a - // `DeclSyntax`. - // We cannot use normal string interpolation here because the conformance to `ExpressibleByStringInterpolation` is - // not implied by `SyntaxParsable` but generated for each type by - // `SyntaxExpressibleByStringInterpolationConformances.swift`. And we can’t use that protocol in the `as?` check - // because then the compiler complains that `parsableType` is not instantiable. So, manually do the same work that - // a string literal with interpolation segments would do. - let decl: DeclSyntax - var stringInterpolation = SyntaxStringInterpolation(literalCapacity: 1, interpolationCount: 1) - stringInterpolation.appendInterpolation(header) - stringInterpolation.appendLiteral(" {}") - if let parsableType = Self.self as? SyntaxParseable.Type { - decl = parsableType.init(stringInterpolation: stringInterpolation).cast(DeclSyntax.self) - } else { - decl = DeclSyntax(stringInterpolation: stringInterpolation) - } + let decl = DeclSyntax("\(header) {}") guard let castedDecl = decl.as(Self.self) else { throw SyntaxStringInterpolationInvalidNodeTypeError(expectedType: Self.self, actualNode: decl) } @@ -187,17 +170,7 @@ extension HasTrailingMemberDeclBlock where Self: DeclSyntaxProtocol { _ header: SyntaxNodeString, @MemberBlockItemListBuilder membersBuilder: () throws -> MemberBlockItemListSyntax ) throws { - // If the type provides a custom `SyntaxParseable` implementation, use that. Otherwise construct it as a - // `DeclSyntax`. - let decl: DeclSyntax - var stringInterpolation = SyntaxStringInterpolation(literalCapacity: 1, interpolationCount: 1) - stringInterpolation.appendInterpolation(header) - stringInterpolation.appendLiteral(" {}") - if let parsableType = Self.self as? SyntaxParseable.Type { - decl = parsableType.init(stringInterpolation: stringInterpolation).cast(DeclSyntax.self) - } else { - decl = DeclSyntax(stringInterpolation: stringInterpolation) - } + let decl = DeclSyntax("\(header) {}") guard let castedDecl = decl.as(Self.self) else { throw SyntaxStringInterpolationInvalidNodeTypeError(expectedType: Self.self, actualNode: decl) } diff --git a/Sources/SwiftSyntaxMacros/MacroProtocols/MemberMacro.swift b/Sources/SwiftSyntaxMacros/MacroProtocols/MemberMacro.swift index d79205049a3..4e021220b76 100644 --- a/Sources/SwiftSyntaxMacros/MacroProtocols/MemberMacro.swift +++ b/Sources/SwiftSyntaxMacros/MacroProtocols/MemberMacro.swift @@ -81,11 +81,6 @@ extension MemberMacro { } /// Default implementation that ignores the unhandled conformances. - @available( - *, - deprecated, - message: "`MemberMacro` conformance should implement the `expansion` function that takes a `conformingTo` parameter" - ) public static func expansion( of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, diff --git a/Sources/SwiftSyntaxMacrosGenericTestSupport/Assertions.swift b/Sources/SwiftSyntaxMacrosGenericTestSupport/Assertions.swift index 0ab97de0785..2023ed6d55f 100644 --- a/Sources/SwiftSyntaxMacrosGenericTestSupport/Assertions.swift +++ b/Sources/SwiftSyntaxMacrosGenericTestSupport/Assertions.swift @@ -365,7 +365,6 @@ public enum DiagnosticAssertionContext { } } -@_spi(Testing) public func assertDiagnostic( _ diag: Diagnostic, in expansionContext: DiagnosticAssertionContext, diff --git a/Sources/_SwiftSyntaxCShims/include/AtomicBool.h b/Sources/_SwiftSyntaxCShims/include/AtomicBool.h index afa3caf4db9..1d456ad4943 100644 --- a/Sources/_SwiftSyntaxCShims/include/AtomicBool.h +++ b/Sources/_SwiftSyntaxCShims/include/AtomicBool.h @@ -21,7 +21,7 @@ typedef struct { } AtomicBool; static inline AtomicBool *_Nonnull swiftsyntax_atomic_bool_create(bool initialValue) { - AtomicBool *atomic = (AtomicBool *)malloc(sizeof(AtomicBool)); + AtomicBool *atomic = malloc(sizeof(AtomicBool)); atomic->value = initialValue; return atomic; } diff --git a/Tests/PerformanceTest/ParsingPerformanceTests.swift b/Tests/PerformanceTest/ParsingPerformanceTests.swift index 714ad0cfb6d..3eff949de25 100644 --- a/Tests/PerformanceTest/ParsingPerformanceTests.swift +++ b/Tests/PerformanceTest/ParsingPerformanceTests.swift @@ -27,7 +27,7 @@ class ParsingPerformanceTests: XCTestCase { func testNativeParsingPerformance() throws { try XCTSkipIf(longTestsDisabled) - let source = try String(contentsOf: inputFile, encoding: .utf8) + let source = try String(contentsOf: inputFile) try measureInstructions { _ = Parser.parse(source: source) diff --git a/Tests/PerformanceTest/SyntaxClassifierPerformanceTests.swift b/Tests/PerformanceTest/SyntaxClassifierPerformanceTests.swift index 6be46deef1c..daa474ff25f 100644 --- a/Tests/PerformanceTest/SyntaxClassifierPerformanceTests.swift +++ b/Tests/PerformanceTest/SyntaxClassifierPerformanceTests.swift @@ -28,7 +28,7 @@ class SyntaxClassifierPerformanceTests: XCTestCase { func testClassifierPerformance() throws { try XCTSkipIf(longTestsDisabled) - let source = try String(contentsOf: inputFile, encoding: .utf8) + let source = try String(contentsOf: inputFile) let parsed = Parser.parse(source: source) try measureInstructions { diff --git a/Tests/PerformanceTest/VisitorPerformanceTests.swift b/Tests/PerformanceTest/VisitorPerformanceTests.swift index 659c27dad55..71adefa173f 100644 --- a/Tests/PerformanceTest/VisitorPerformanceTests.swift +++ b/Tests/PerformanceTest/VisitorPerformanceTests.swift @@ -28,7 +28,7 @@ class VisitorPerformanceTests: XCTestCase { try XCTSkipIf(longTestsDisabled) class EmptyVisitor: SyntaxVisitor {} - let source = try String(contentsOf: inputFile, encoding: .utf8) + let source = try String(contentsOf: inputFile) let parsed = Parser.parse(source: source) let emptyVisitor = EmptyVisitor(viewMode: .sourceAccurate) @@ -41,7 +41,7 @@ class VisitorPerformanceTests: XCTestCase { try XCTSkipIf(longTestsDisabled) class EmptyRewriter: SyntaxRewriter {} - let source = try String(contentsOf: inputFile, encoding: .utf8) + let source = try String(contentsOf: inputFile) let parsed = Parser.parse(source: source) let emptyRewriter = EmptyRewriter(viewMode: .sourceAccurate) @@ -54,7 +54,7 @@ class VisitorPerformanceTests: XCTestCase { try XCTSkipIf(longTestsDisabled) class EmptyAnyVisitor: SyntaxAnyVisitor {} - let source = try String(contentsOf: inputFile, encoding: .utf8) + let source = try String(contentsOf: inputFile) let parsed = Parser.parse(source: source) let emptyVisitor = EmptyAnyVisitor(viewMode: .sourceAccurate) diff --git a/Tests/SwiftIfConfigTest/ActiveRegionTests.swift b/Tests/SwiftIfConfigTest/ActiveRegionTests.swift index 33fd7c1b7f8..8e553a8f032 100644 --- a/Tests/SwiftIfConfigTest/ActiveRegionTests.swift +++ b/Tests/SwiftIfConfigTest/ActiveRegionTests.swift @@ -9,15 +9,12 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// - import SwiftDiagnostics import SwiftIfConfig import SwiftParser import SwiftSyntax import SwiftSyntaxMacrosGenericTestSupport import XCTest -import _SwiftSyntaxGenericTestSupport -import _SwiftSyntaxTestSupport public class ActiveRegionTests: XCTestCase { let linuxBuildConfig = TestingBuildConfiguration( @@ -29,28 +26,28 @@ public class ActiveRegionTests: XCTestCase { func testActiveRegions() throws { try assertActiveCode( """ - 1️⃣ + 4️⃣ #if DEBUG - 2️⃣func f() + 0️⃣func f() #elseif ASSERTS - 3️⃣func g() + 1️⃣func g() #if compiler(>=8.0) - 4️⃣func h() + 2️⃣func h() #else - 5️⃣var i + 3️⃣var i #endif #endif - 6️⃣token + 5️⃣token """, configuration: linuxBuildConfig, states: [ - "1️⃣": .active, - "2️⃣": .active, + "0️⃣": .active, + "1️⃣": .inactive, + "2️⃣": .unparsed, "3️⃣": .inactive, - "4️⃣": .unparsed, - "5️⃣": .inactive, - "6️⃣": .active, + "4️⃣": .active, + "5️⃣": .active, ] ) } @@ -58,27 +55,27 @@ public class ActiveRegionTests: XCTestCase { func testActiveRegionsInPostfix() throws { try assertActiveCode( """ - 1️⃣a.b() + 4️⃣a.b() #if DEBUG - 2️⃣.c() + 0️⃣.c() #elseif ASSERTS - 3️⃣.d() + 1️⃣.d() #if compiler(>=8.0) - 4️⃣.e() + 2️⃣.e() #else - 5️⃣.f() + 3️⃣.f() #endif #endif - 6️⃣.g() + 5️⃣.g() """, configuration: linuxBuildConfig, states: [ - "1️⃣": .active, - "2️⃣": .active, + "0️⃣": .active, + "1️⃣": .inactive, + "2️⃣": .unparsed, "3️⃣": .inactive, - "4️⃣": .unparsed, - "5️⃣": .inactive, - "6️⃣": .active, + "4️⃣": .active, + "5️⃣": .active, ] ) } @@ -101,45 +98,3 @@ public class ActiveRegionTests: XCTestCase { ) } } - -/// Assert that the various marked positions in the source code have the -/// expected active states. -fileprivate func assertActiveCode( - _ markedSource: String, - configuration: some BuildConfiguration = TestingBuildConfiguration(), - states: [String: IfConfigRegionState], - file: StaticString = #filePath, - line: UInt = #line -) throws { - // Pull out the markers that we'll use to dig out nodes to query. - let (markerLocations, source) = extractMarkers(markedSource) - - var parser = Parser(source) - let tree = SourceFileSyntax.parse(from: &parser) - - let configuredRegions = tree.configuredRegions(in: configuration) - - for (marker, location) in markerLocations { - guard let expectedState = states[marker] else { - XCTFail("Missing marker \(marker) in expected states", file: file, line: line) - continue - } - - guard let token = tree.token(at: AbsolutePosition(utf8Offset: location)) else { - XCTFail("Unable to find token at location \(location)", file: file, line: line) - continue - } - - let (actualState, _) = token.isActive(in: configuration) - XCTAssertEqual(actualState, expectedState, "isActive(in:) at marker \(marker)", file: file, line: line) - - let actualViaRegions = token.isActive(inConfiguredRegions: configuredRegions) - XCTAssertEqual( - actualViaRegions, - expectedState, - "isActive(inConfiguredRegions:) at marker \(marker)", - file: file, - line: line - ) - } -} diff --git a/Tests/SwiftIfConfigTest/Assertions.swift b/Tests/SwiftIfConfigTest/Assertions.swift new file mode 100644 index 00000000000..00c3a442848 --- /dev/null +++ b/Tests/SwiftIfConfigTest/Assertions.swift @@ -0,0 +1,153 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// +import SwiftDiagnostics +import SwiftIfConfig +import SwiftParser +import SwiftSyntax +@_spi(XCTestFailureLocation) import SwiftSyntaxMacrosGenericTestSupport +import XCTest +import _SwiftSyntaxGenericTestSupport +import _SwiftSyntaxTestSupport + +/// Assert the results of evaluating the condition within an `#if` against the +/// given build configuration. +func assertIfConfig( + _ condition: ExprSyntax, + _ expectedState: ConfiguredRegionState?, + configuration: some BuildConfiguration = TestingBuildConfiguration(), + diagnostics expectedDiagnostics: [DiagnosticSpec] = [], + file: StaticString = #filePath, + line: UInt = #line +) { + // Evaluate the condition to check the state. + var actualDiagnostics: [Diagnostic] = [] + do { + let actualState = try ConfiguredRegionState(condition: condition, configuration: configuration) { diag in + actualDiagnostics.append(diag) + } + XCTAssertEqual(actualState, expectedState, file: file, line: line) + } catch { + XCTAssertNil(expectedState, file: file, line: line) + } + + // Check the diagnostics. + if actualDiagnostics.count != expectedDiagnostics.count { + XCTFail( + """ + Expected \(expectedDiagnostics.count) diagnostics, but got \(actualDiagnostics.count): + \(actualDiagnostics.map(\.debugDescription).joined(separator: "\n")) + """, + file: file, + line: line + ) + } else { + for (actualDiag, expectedDiag) in zip(actualDiagnostics, expectedDiagnostics) { + assertDiagnostic( + actualDiag, + in: .tree(condition), + expected: expectedDiag, + failureHandler: { + XCTFail($0.message, file: $0.location.staticFilePath, line: $0.location.unsignedLine) + } + ) + } + } +} + +/// Assert that the various marked positions in the source code have the +/// expected active states. +func assertActiveCode( + _ markedSource: String, + configuration: some BuildConfiguration = TestingBuildConfiguration(), + states: [String: ConfiguredRegionState], + file: StaticString = #filePath, + line: UInt = #line +) throws { + // Pull out the markers that we'll use to dig out nodes to query. + let (markerLocations, source) = extractMarkers(markedSource) + + var parser = Parser(source) + let tree = SourceFileSyntax.parse(from: &parser) + + let configuredRegions = tree.configuredRegions(in: configuration) + + for (marker, location) in markerLocations { + guard let expectedState = states[marker] else { + XCTFail("Missing marker \(marker) in expected states", file: file, line: line) + continue + } + + guard let token = tree.token(at: AbsolutePosition(utf8Offset: location)) else { + XCTFail("Unable to find token at location \(location)", file: file, line: line) + continue + } + + let actualState = try token.isActive(in: configuration) + XCTAssertEqual(actualState, expectedState, "isActive(in:) at marker \(marker)", file: file, line: line) + + let actualViaRegions = token.isActive(inConfiguredRegions: configuredRegions) + XCTAssertEqual( + actualViaRegions, + expectedState, + "isActive(inConfiguredRegions:) at marker \(marker)", + file: file, + line: line + ) + } +} + +/// Assert that applying the given build configuration to the source code +/// returns the expected source and diagnostics. +func assertRemoveInactive( + _ source: String, + configuration: some BuildConfiguration, + diagnostics expectedDiagnostics: [DiagnosticSpec] = [], + expectedSource: String, + file: StaticString = #filePath, + line: UInt = #line +) { + var parser = Parser(source) + let tree = SourceFileSyntax.parse(from: &parser) + + let (treeWithoutInactive, actualDiagnostics) = tree.removingInactive(in: configuration) + + // Check the resulting tree. + assertStringsEqualWithDiff( + treeWithoutInactive.description, + expectedSource, + file: file, + line: line + ) + + // Check the diagnostics. + if actualDiagnostics.count != expectedDiagnostics.count { + XCTFail( + """ + Expected \(expectedDiagnostics.count) diagnostics, but got \(actualDiagnostics.count): + \(actualDiagnostics.map(\.debugDescription).joined(separator: "\n")) + """, + file: file, + line: line + ) + } else { + for (actualDiag, expectedDiag) in zip(actualDiagnostics, expectedDiagnostics) { + assertDiagnostic( + actualDiag, + in: .tree(tree), + expected: expectedDiag, + failureHandler: { + XCTFail($0.message, file: $0.location.staticFilePath, line: $0.location.unsignedLine) + } + ) + } + } +} diff --git a/Tests/SwiftIfConfigTest/EvaluateTests.swift b/Tests/SwiftIfConfigTest/EvaluateTests.swift index 6f7bdd064a1..3fe5c9c1c6c 100644 --- a/Tests/SwiftIfConfigTest/EvaluateTests.swift +++ b/Tests/SwiftIfConfigTest/EvaluateTests.swift @@ -9,14 +9,11 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// - -import SwiftDiagnostics import SwiftIfConfig import SwiftParser import SwiftSyntax -@_spi(XCTestFailureLocation) @_spi(Testing) import SwiftSyntaxMacrosGenericTestSupport +import SwiftSyntaxMacrosGenericTestSupport import XCTest -import _SwiftSyntaxGenericTestSupport import _SwiftSyntaxTestSupport public class EvaluateTests: XCTestCase { @@ -58,7 +55,7 @@ public class EvaluateTests: XCTestCase { ) assertIfConfig( "2", - .unparsed, + nil, configuration: buildConfig, diagnostics: [ DiagnosticSpec( @@ -82,7 +79,7 @@ public class EvaluateTests: XCTestCase { assertIfConfig("nope && DEBUG", .inactive, configuration: buildConfig) assertIfConfig( "nope && 3.14159", - .unparsed, + nil, configuration: buildConfig, diagnostics: [ DiagnosticSpec( @@ -98,7 +95,7 @@ public class EvaluateTests: XCTestCase { assertIfConfig("nope || !DEBUG", .inactive, configuration: buildConfig) assertIfConfig( "DEBUG || 3.14159", - .active, + nil, configuration: buildConfig, diagnostics: [ DiagnosticSpec( @@ -110,7 +107,7 @@ public class EvaluateTests: XCTestCase { ) assertIfConfig( "(DEBUG) || 3.14159", - .active, + nil, configuration: buildConfig, diagnostics: [ DiagnosticSpec( @@ -127,7 +124,7 @@ public class EvaluateTests: XCTestCase { assertIfConfig( "3.14159", - .unparsed, + nil, configuration: buildConfig, diagnostics: [ DiagnosticSpec( @@ -185,7 +182,7 @@ public class EvaluateTests: XCTestCase { assertIfConfig("compiler(>=5.10) && 3.14159", .unparsed) assertIfConfig( "compiler(>=5.10) || 3.14159", - .unparsed, + nil, diagnostics: [ DiagnosticSpec( message: "invalid conditional compilation expression", @@ -197,7 +194,7 @@ public class EvaluateTests: XCTestCase { assertIfConfig("compiler(>=5.9) || 3.14159", .active) assertIfConfig( "compiler(>=5.9) && 3.14159", - .unparsed, + nil, diagnostics: [ DiagnosticSpec( message: "invalid conditional compilation expression", @@ -231,43 +228,3 @@ public class EvaluateTests: XCTestCase { ) } } - -/// Assert the results of evaluating the condition within an `#if` against the -/// given build configuration. -fileprivate func assertIfConfig( - _ condition: ExprSyntax, - _ expectedState: IfConfigRegionState, - configuration: some BuildConfiguration = TestingBuildConfiguration(), - diagnostics expectedDiagnostics: [DiagnosticSpec] = [], - file: StaticString = #filePath, - line: UInt = #line -) { - // Evaluate the condition to check the state. - let actualDiagnostics: [Diagnostic] - let actualState: IfConfigRegionState - (actualState, actualDiagnostics) = IfConfigRegionState.evaluating(condition, in: configuration) - XCTAssertEqual(actualState, expectedState, file: file, line: line) - - // Check the diagnostics. - if actualDiagnostics.count != expectedDiagnostics.count { - XCTFail( - """ - Expected \(expectedDiagnostics.count) diagnostics, but got \(actualDiagnostics.count): - \(actualDiagnostics.map(\.debugDescription).joined(separator: "\n")) - """, - file: file, - line: line - ) - } else { - for (actualDiag, expectedDiag) in zip(actualDiagnostics, expectedDiagnostics) { - assertDiagnostic( - actualDiag, - in: .tree(condition), - expected: expectedDiag, - failureHandler: { - XCTFail($0.message, file: $0.location.staticFilePath, line: $0.location.unsignedLine) - } - ) - } - } -} diff --git a/Tests/SwiftIfConfigTest/TestingBuildConfiguration.swift b/Tests/SwiftIfConfigTest/TestingBuildConfiguration.swift index 5f807fe0b1e..39a5702f71d 100644 --- a/Tests/SwiftIfConfigTest/TestingBuildConfiguration.swift +++ b/Tests/SwiftIfConfigTest/TestingBuildConfiguration.swift @@ -9,7 +9,6 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// - import SwiftIfConfig import SwiftSyntax diff --git a/Tests/SwiftIfConfigTest/VisitorTests.swift b/Tests/SwiftIfConfigTest/VisitorTests.swift index 4e669e2b33a..eddb8c1d3d7 100644 --- a/Tests/SwiftIfConfigTest/VisitorTests.swift +++ b/Tests/SwiftIfConfigTest/VisitorTests.swift @@ -9,15 +9,12 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// - import SwiftDiagnostics import SwiftIfConfig import SwiftParser import SwiftSyntax -@_spi(XCTestFailureLocation) @_spi(Testing) import SwiftSyntaxMacrosGenericTestSupport +import SwiftSyntaxMacrosGenericTestSupport import XCTest -import _SwiftSyntaxGenericTestSupport -import _SwiftSyntaxTestSupport /// Visitor that ensures that all of the nodes we visit are active. /// @@ -28,7 +25,9 @@ class AllActiveVisitor: ActiveSyntaxAnyVisitor { super.init(viewMode: .sourceAccurate, configuration: configuration) } open override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind { - XCTAssertEqual(node.isActive(in: configuration).state, .active) + var active: ConfiguredRegionState = .inactive + XCTAssertNoThrow(try active = node.isActive(in: configuration)) + XCTAssertEqual(active, .active) return .visitChildren } } @@ -254,50 +253,3 @@ public class VisitorTests: XCTestCase { ) } } - -/// Assert that applying the given build configuration to the source code -/// returns the expected source and diagnostics. -fileprivate func assertRemoveInactive( - _ source: String, - configuration: some BuildConfiguration, - diagnostics expectedDiagnostics: [DiagnosticSpec] = [], - expectedSource: String, - file: StaticString = #filePath, - line: UInt = #line -) { - var parser = Parser(source) - let tree = SourceFileSyntax.parse(from: &parser) - - let (treeWithoutInactive, actualDiagnostics) = tree.removingInactive(in: configuration) - - // Check the resulting tree. - assertStringsEqualWithDiff( - treeWithoutInactive.description, - expectedSource, - file: file, - line: line - ) - - // Check the diagnostics. - if actualDiagnostics.count != expectedDiagnostics.count { - XCTFail( - """ - Expected \(expectedDiagnostics.count) diagnostics, but got \(actualDiagnostics.count): - \(actualDiagnostics.map(\.debugDescription).joined(separator: "\n")) - """, - file: file, - line: line - ) - } else { - for (actualDiag, expectedDiag) in zip(actualDiagnostics, expectedDiagnostics) { - assertDiagnostic( - actualDiag, - in: .tree(tree), - expected: expectedDiag, - failureHandler: { - XCTFail($0.message, file: $0.location.staticFilePath, line: $0.location.unsignedLine) - } - ) - } - } -} diff --git a/Tests/SwiftLexicalLookupTest/Assertions.swift b/Tests/SwiftLexicalLookupTest/Assertions.swift index ccfeaf61788..5562512cc70 100644 --- a/Tests/SwiftLexicalLookupTest/Assertions.swift +++ b/Tests/SwiftLexicalLookupTest/Assertions.swift @@ -67,7 +67,20 @@ func assertLexicalScopeQuery( // Assert validity of the output for (actual, (expectedMarker, expectedPosition)) in zip(result, zip(expectedMarkers, expectedPositions)) { - guard let actual, let expectedPosition else { continue } + if actual == nil && expectedPosition == nil { continue } + + guard let actual else { + XCTFail( + "For marker \(marker), actual is nil while expected is \(sourceFileSyntax.token(at: expectedPosition!)?.description ?? "nil")" + ) + continue + } + + guard let expectedPosition else { + XCTFail("For marker \(marker), actual is \(actual) while expected position is nil") + continue + } + XCTAssert( actual.positionAfterSkippingLeadingTrivia == expectedPosition, "For marker \(marker), actual result: \(actual) doesn't match expected value: \(sourceFileSyntax.token(at: expectedPosition)?.description ?? "nil")" @@ -93,9 +106,7 @@ func assertLexicalNameLookup( assertLexicalScopeQuery( source: source, methodUnderTest: { marker, tokenAtMarker in - let lookupIdentifier = Identifier(tokenAtMarker) ?? Identifier(tokenAtMarker.text) - - let result = tokenAtMarker.lookup(for: useNilAsTheParameter ? nil : lookupIdentifier, with: config) + let result = tokenAtMarker.lookup(for: useNilAsTheParameter ? nil : tokenAtMarker.text, with: config) guard let expectedValues = references[marker] else { XCTFail("For marker \(marker), couldn't find result expectation") diff --git a/Tests/SwiftLexicalLookupTest/ExpectedName.swift b/Tests/SwiftLexicalLookupTest/ExpectedName.swift index 867173d96eb..03ca70b83a9 100644 --- a/Tests/SwiftLexicalLookupTest/ExpectedName.swift +++ b/Tests/SwiftLexicalLookupTest/ExpectedName.swift @@ -51,7 +51,7 @@ enum ImplicitNameExpectation { .error(let marker), .newValue(let marker), .oldValue(let marker): - return marker + marker } } } @@ -67,9 +67,9 @@ enum NameExpectation: ExpectedName { switch self { case .identifier(let marker), .declaration(let marker): - return marker + marker case .implicit(let implicitName): - return implicitName.marker + implicitName.marker } } diff --git a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift index 6627ef9951a..8389d1b6227 100644 --- a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift +++ b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift @@ -512,13 +512,13 @@ final class testNameLookup: XCTestCase { 9️⃣class d {} - let 🔟a = 0️⃣d + let x = 0️⃣d """, references: [ "3️⃣": [.fromFileScope(expectedNames: ["1️⃣", "8️⃣"])], "4️⃣": [.fromFileScope(expectedNames: ["2️⃣"])], "5️⃣": [.fromFileScope(expectedNames: ["7️⃣"])], - "6️⃣": [.fromFileScope(expectedNames: ["9️⃣"])], + "6️⃣": [], "0️⃣": [.fromFileScope(expectedNames: ["9️⃣"])], ], expectedResultTypes: .all(ClassDeclSyntax.self, except: ["8️⃣": IdentifierPatternSyntax.self]) @@ -542,41 +542,48 @@ final class testNameLookup: XCTestCase { 9️⃣class d {} - let 🔟a = 0️⃣d + let x = 0️⃣d """, references: [ - "3️⃣": [.fromFileScope(expectedNames: ["1️⃣", "8️⃣", "🔟"])], + "3️⃣": [.fromFileScope(expectedNames: ["1️⃣", "8️⃣"])], "4️⃣": [.fromFileScope(expectedNames: ["2️⃣"])], "5️⃣": [.fromFileScope(expectedNames: ["7️⃣"])], "6️⃣": [.fromFileScope(expectedNames: ["9️⃣"])], "0️⃣": [.fromFileScope(expectedNames: ["9️⃣"])], ], - expectedResultTypes: .all( - ClassDeclSyntax.self, - except: [ - "8️⃣": IdentifierPatternSyntax.self, - "🔟": IdentifierPatternSyntax.self, - ] - ), + expectedResultTypes: .all(ClassDeclSyntax.self, except: ["8️⃣": IdentifierPatternSyntax.self]), config: LookupConfig(fileScopeHandling: .memberBlock) ) } - func testDeclarationAvailabilityInCodeBlock() { + func testFileScopeAsCodeBlock() { assertLexicalNameLookup( source: """ - func x { - 1️⃣class A {} - - let a = 2️⃣A() + 1️⃣class a {} - 3️⃣class A {} + 2️⃣class b { + let x = 3️⃣a + 4️⃣b + 5️⃣c + 6️⃣d } + + let 8️⃣a = 0 + + 7️⃣class c {} + + if a == 0 {} + + 9️⃣class d {} + + let x = 0️⃣d """, references: [ - "2️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣", "3️⃣"])] + "3️⃣": [.fromFileScope(expectedNames: ["1️⃣"])], + "4️⃣": [], + "5️⃣": [], + "6️⃣": [], + "0️⃣": [.fromFileScope(expectedNames: ["9️⃣"])], ], - expectedResultTypes: .all(ClassDeclSyntax.self) + expectedResultTypes: .all(ClassDeclSyntax.self, except: ["8️⃣": IdentifierPatternSyntax.self]), + config: LookupConfig(fileScopeHandling: .codeBlock) ) } @@ -604,6 +611,31 @@ final class testNameLookup: XCTestCase { ) } + func testGuardOnFileScopeCodeBlock() { + assertLexicalNameLookup( + source: """ + let 1️⃣a = 0 + + class c {} + + guard let 2️⃣a else { fatalError() } + + 3️⃣class a {} + + let x = 4️⃣a + """, + references: [ + "4️⃣": [ + .fromFileScope(expectedNames: ["3️⃣"]), + .fromScope(GuardStmtSyntax.self, expectedNames: ["2️⃣"]), + .fromFileScope(expectedNames: ["1️⃣"]), + ] + ], + expectedResultTypes: .all(IdentifierPatternSyntax.self, except: ["3️⃣": ClassDeclSyntax.self]), + config: LookupConfig(fileScopeHandling: .codeBlock) + ) + } + func testImplicitSelf() { assertLexicalNameLookup( source: """ @@ -668,43 +700,4 @@ final class testNameLookup: XCTestCase { ] ) } - - func testBacktickCompatibility() { - assertLexicalNameLookup( - source: """ - 1️⃣struct Foo { - func test() { - let 2️⃣`self` = 1 - print(3️⃣self) - print(4️⃣`self`) - } - } - - 5️⃣struct Bar { - func test() { - print(6️⃣self) - let 7️⃣`self` = 1 - print(8️⃣`self`) - } - } - """, - references: [ - "3️⃣": [ - .fromScope(CodeBlockSyntax.self, expectedNames: [NameExpectation.identifier("2️⃣")]), - .fromScope(StructDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("1️⃣"))]), - ], - "4️⃣": [ - .fromScope(CodeBlockSyntax.self, expectedNames: [NameExpectation.identifier("2️⃣")]), - .fromScope(StructDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("1️⃣"))]), - ], - "6️⃣": [ - .fromScope(StructDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("5️⃣"))]) - ], - "8️⃣": [ - .fromScope(CodeBlockSyntax.self, expectedNames: [NameExpectation.identifier("7️⃣")]), - .fromScope(StructDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("5️⃣"))]), - ], - ] - ) - } } diff --git a/Tests/SwiftLexicalLookupTest/ResultExpectation.swift b/Tests/SwiftLexicalLookupTest/ResultExpectation.swift index 27a9d5b58a8..5dc00d568f8 100644 --- a/Tests/SwiftLexicalLookupTest/ResultExpectation.swift +++ b/Tests/SwiftLexicalLookupTest/ResultExpectation.swift @@ -22,18 +22,18 @@ enum ResultExpectation { var expectedNames: [ExpectedName] { switch self { case .fromScope(_, let expectedNames): - return expectedNames + expectedNames case .fromFileScope(expectedNames: let expectedNames): - return expectedNames + expectedNames } } var debugDescription: String { switch self { case .fromScope: - return "fromScope" + "fromScope" case .fromFileScope: - return "fromFileScope" + "fromFileScope" } } @@ -65,9 +65,9 @@ extension LookupResult { var debugDescription: String { switch self { case .fromScope: - return "fromScope" + "fromScope" case .fromFileScope: - return "fromFileScope" + "fromFileScope" } } } diff --git a/Tests/SwiftParserTest/ParserTests.swift b/Tests/SwiftParserTest/ParserTests.swift index de26d78ba20..f451890d3db 100644 --- a/Tests/SwiftParserTest/ParserTests.swift +++ b/Tests/SwiftParserTest/ParserTests.swift @@ -61,7 +61,8 @@ class ParserTests: ParserTestCase { checkDiagnostics: Bool, shouldExclude: @Sendable (URL) -> Bool = { _ in false } ) { - let fileURLs = FileManager.default + // nonisolated(unsafe) because [URL] is not marked Sendable on Linux. + let _fileURLs = FileManager.default .enumerator(at: path, includingPropertiesForKeys: nil)! .compactMap({ $0 as? URL }) .filter { @@ -69,6 +70,11 @@ class ParserTests: ParserTestCase { || $0.pathExtension == "sil" || $0.pathExtension == "swiftinterface" } + #if swift(>=6.0) + nonisolated(unsafe) let fileURLs = _fileURLs + #else + let fileURLs = _fileURLs + #endif print("\(name) - processing \(fileURLs.count) source files") DispatchQueue.concurrentPerform(iterations: fileURLs.count) { fileURLIndex in diff --git a/Tests/SwiftSyntaxBuilderTest/AccessorDeclTests.swift b/Tests/SwiftSyntaxBuilderTest/AccessorDeclTests.swift deleted file mode 100644 index a89cde0c37d..00000000000 --- a/Tests/SwiftSyntaxBuilderTest/AccessorDeclTests.swift +++ /dev/null @@ -1,24 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import SwiftSyntax -import SwiftSyntaxBuilder -import XCTest - -final class AccessorDeclTests: XCTestCase { - func testCreateAccessorWithHeaderAndBody() throws { - let accessor = try AccessorDeclSyntax("get") { - ExprSyntax("1") - } - XCTAssertEqual(accessor.body?.statements.count, 1) - } -} diff --git a/Tests/SwiftSyntaxTest/IdentifierTests.swift b/Tests/SwiftSyntaxTest/IdentifierTests.swift index 8e110807df8..e0083a0a5c4 100644 --- a/Tests/SwiftSyntaxTest/IdentifierTests.swift +++ b/Tests/SwiftSyntaxTest/IdentifierTests.swift @@ -39,7 +39,7 @@ class IdentifierTests: XCTestCase { public func testIdentifier() { let token = TokenSyntax(stringLiteral: "sometoken") withExtendedLifetime(token) { token in - XCTAssertEqual(token.identifier?.raw?.name, SyntaxText("sometoken")) + XCTAssertEqual(token.identifier?.raw.name, SyntaxText("sometoken")) } } diff --git a/cmake/modules/SwiftCompilerCapability.cmake b/cmake/modules/SwiftCompilerCapability.cmake index d62e387184f..951e563a49c 100644 --- a/cmake/modules/SwiftCompilerCapability.cmake +++ b/cmake/modules/SwiftCompilerCapability.cmake @@ -22,19 +22,6 @@ macro(swift_supports_implicit_module module_name out_var) ) endmacro() -function(swift_get_swiftlang_version out_var) - execute_process( - COMMAND "${CMAKE_Swift_COMPILER}" -version - OUTPUT_VARIABLE output ERROR_VARIABLE output - RESULT_VARIABLE result - TIMEOUT 10 - ) - - if(output MATCHES [[swiftlang-([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)]]) - set("${out_var}" "${CMAKE_MATCH_1}" PARENT_SCOPE) - endif() -endfunction() - # Get "package cross-module-optimization" compiler arguments suitable for the compiler. function(swift_get_package_cmo_support out_var) # > 6.0 : Fixed feature. @@ -56,13 +43,8 @@ function(swift_get_package_cmo_support out_var) -Xfrontend -experimental-package-bypass-resilience ) if(result) - # Package CMO is implmented in Xcode 16 Beta 4 (swiftlang-6.0.0.6.8) or later. - # Consider it's not supported in non Xcode toolchain with "-experimental" options. - swift_get_swiftlang_version(swiftlang_version) - if(swiftlang_version AND swiftlang_version VERSION_GREATER_EQUAL 6.0.0.6) - set(${out_var} EXPERIMENTAL PARENT_SCOPE) - return() - endif() + set(${out_var} EXPERIMENTAL PARENT_SCOPE) + return() endif() # < 6.0 : Not supported. From 5705b96a447a559d2c66b01222d9f7a69b1208c8 Mon Sep 17 00:00:00 2001 From: Jakub Florek Date: Tue, 30 Jul 2024 13:24:56 +0200 Subject: [PATCH 18/27] Add internal `_lookup` function that passes state. --- Sources/SwiftLexicalLookup/LookupState.swift | 2 +- .../ScopeImplementations.swift | 8 ++--- Sources/SwiftLexicalLookup/ScopeSyntax.swift | 30 +++++++++++++++++-- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/Sources/SwiftLexicalLookup/LookupState.swift b/Sources/SwiftLexicalLookup/LookupState.swift index b94b7862656..9bb469b2aa2 100644 --- a/Sources/SwiftLexicalLookup/LookupState.swift +++ b/Sources/SwiftLexicalLookup/LookupState.swift @@ -17,5 +17,5 @@ import Foundation /// should be skipped during lookup in sequential scopes. var skipSequentialIntroductionFrom: IntroducingToSequentialParentScopeSyntax? - @_spi(Experimental) public init() {} + init() {} } diff --git a/Sources/SwiftLexicalLookup/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/ScopeImplementations.swift index 2764e057904..42afcd62599 100644 --- a/Sources/SwiftLexicalLookup/ScopeImplementations.swift +++ b/Sources/SwiftLexicalLookup/ScopeImplementations.swift @@ -96,7 +96,7 @@ import SwiftSyntax /// - for `memberBlockUpToLastDecl` - a, b, c, d /// - for `memberBlock` - a, b, c, d, e, f /// - for `codeBlock` - a - @_spi(Experimental) public func lookup( + @_spi(Experimental) public func _lookup( for identifier: Identifier?, at syntax: SyntaxProtocol, with config: LookupConfig, @@ -155,7 +155,7 @@ import SwiftSyntax } } - @_spi(Experimental) public func lookup( + @_spi(Experimental) public func _lookup( for identifier: Identifier?, at syntax: SyntaxProtocol, with config: LookupConfig, @@ -277,7 +277,7 @@ import SwiftSyntax /// // <-- a is not visible here /// } /// ``` - @_spi(Experimental) public func lookup( + @_spi(Experimental) public func _lookup( for identifier: Identifier?, at syntax: SyntaxProtocol, with config: LookupConfig, @@ -331,7 +331,7 @@ import SwiftSyntax /// } /// // a is visible here /// ``` - @_spi(Experimental) public func lookup( + @_spi(Experimental) public func _lookup( for identifier: Identifier?, at syntax: SyntaxProtocol, with config: LookupConfig, diff --git a/Sources/SwiftLexicalLookup/ScopeSyntax.swift b/Sources/SwiftLexicalLookup/ScopeSyntax.swift index ba02c85eb5e..f04ff41a771 100644 --- a/Sources/SwiftLexicalLookup/ScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/ScopeSyntax.swift @@ -47,7 +47,7 @@ extension SyntaxProtocol { for identifier: Identifier?, with config: LookupConfig = LookupConfig() ) -> [LookupResult] { - scope?.lookup(for: identifier, at: self, with: config, state: LookupState()) ?? [] + scope?.lookup(for: identifier, at: self, with: config) ?? [] } } @@ -59,6 +59,16 @@ extension SyntaxProtocol { /// Finds all declarations `name` refers to. `syntax` specifies the node lookup was triggered with. /// If `name` set to `nil`, returns all available names at the given node. func lookup( + for identifier: Identifier?, + at syntax: SyntaxProtocol, + with config: LookupConfig + ) -> [LookupResult] + /// Finds all declarations `name` refers to. `syntax` specifies the node lookup was triggered with. + /// If `name` set to `nil`, returns all available names at the given node. + /// `state` represents lookup state passed between lookup methods. + /// + /// - Note: This method is intended for internal use only. For public usage, use ``ScopeSyntax/lookup(for:at:with:)`` instead. + func _lookup( for identifier: Identifier?, at syntax: SyntaxProtocol, with config: LookupConfig, @@ -74,7 +84,10 @@ extension SyntaxProtocol { /// Returns `LookupResult` of all names introduced in this scope that `name` /// refers to and is accessible at given syntax node then passes lookup to the parent. /// If `name` set to `nil`, returns all available names at the given node. - @_spi(Experimental) public func lookup( + /// `state` represents lookup state passed between lookup methods. + /// + /// - Note: This method is intended for internal use only. For public usage, use ``ScopeSyntax/lookup(for:at:with:)`` instead. + @_spi(Experimental) public func _lookup( for identifier: Identifier?, at syntax: SyntaxProtocol, with config: LookupConfig, @@ -82,6 +95,17 @@ extension SyntaxProtocol { ) -> [LookupResult] { defaultLookupImplementation(for: identifier, at: syntax, with: config, state: state) } + + /// Returns `LookupResult` of all names introduced in this scope that `name` + /// refers to and is accessible at given syntax node then passes lookup to the parent. + /// If `name` set to `nil`, returns all available names at the given node. + @_spi(Experimental) public func lookup( + for identifier: Identifier?, + at syntax: SyntaxProtocol, + with config: LookupConfig + ) -> [LookupResult] { + _lookup(for: identifier, at: syntax, with: config, state: LookupState()) + } /// Returns `LookupResult` of all names introduced in this scope that `name` /// refers to and is accessible at given syntax node then passes lookup to the parent. @@ -113,7 +137,7 @@ extension SyntaxProtocol { with config: LookupConfig, state: LookupState ) -> [LookupResult] { - parentScope?.lookup(for: identifier, at: syntax, with: config, state: state) ?? [] + parentScope?._lookup(for: identifier, at: syntax, with: config, state: state) ?? [] } func checkName(_ name: Identifier?, refersTo introducedName: LookupName, at syntax: SyntaxProtocol) -> Bool { From 539b1d7369efa4917a7478741fba16ec3c058fcb Mon Sep 17 00:00:00 2001 From: Jakub Florek Date: Thu, 1 Aug 2024 10:56:51 +0200 Subject: [PATCH 19/27] Add suggested changes. --- ...oducingToSequentialParentScopeSyntax.swift | 2 +- Sources/SwiftLexicalLookup/LookupName.swift | 76 ++++++++++++------- Sources/SwiftLexicalLookup/LookupState.swift | 2 + .../ScopeImplementations.swift | 36 ++++----- Sources/SwiftLexicalLookup/ScopeSyntax.swift | 51 ++++++------- .../SequentialScopeSyntax.swift | 71 ++++++++--------- .../SwiftLexicalLookup/TypeScopeSyntax.swift | 8 +- .../SwiftLexicalLookupTest/ExpectedName.swift | 2 +- .../NameLookupTests.swift | 28 ++++++- 9 files changed, 159 insertions(+), 117 deletions(-) diff --git a/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift b/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift index 9a23170a74d..ce7906406e8 100644 --- a/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift @@ -17,7 +17,7 @@ protocol IntroducingToSequentialParentScopeSyntax: ScopeSyntax { /// handled by it's parent sequential scope. func introducesToSequentialParent( for identifier: Identifier?, - at syntax: SyntaxProtocol, + at origin: SyntaxProtocol, with config: LookupConfig, state: LookupState ) -> [LookupResult] diff --git a/Sources/SwiftLexicalLookup/LookupName.swift b/Sources/SwiftLexicalLookup/LookupName.swift index 6be060fe68a..94c27f4fd37 100644 --- a/Sources/SwiftLexicalLookup/LookupName.swift +++ b/Sources/SwiftLexicalLookup/LookupName.swift @@ -12,12 +12,17 @@ import SwiftSyntax -@_spi(Experimental) public enum LookupImplicitNameKind { +/// An entity that is implicitly declared based on the syntactic structure of the program. +@_spi(Experimental) public enum ImplicitDecl { /// `self` keyword representing object instance. - case `self`(SyntaxProtocol) + /// Could be associated with type declaration, extension, + /// or closure captures. + case `self`(DeclSyntaxProtocol) /// `Self` keyword representing object type. + /// Could be associated with type declaration or extension. case `Self`(DeclSyntaxProtocol) - /// `self` captured by a closure. + /// `error` value caught by a `catch` + /// block that does not specify a catch pattern. case error(CatchClauseSyntax) /// `newValue` available by default inside `set` and `willSet`. case newValue(AccessorDeclSyntax) @@ -28,35 +33,58 @@ import SwiftSyntax @_spi(Experimental) public var syntax: SyntaxProtocol { switch self { case .self(let syntax): - syntax + return syntax case .Self(let syntax): - syntax + return syntax case .error(let syntax): - syntax + return syntax case .newValue(let syntax): - syntax + return syntax case .oldValue(let syntax): - syntax + return syntax } } - /// Name associated with implicit name kind. + /// The name of the implicit declaration. private var name: String { switch self { case .self: - "self" + return "self" case .Self: - "Self" + return "Self" case .error: - "error" + return "error" case .newValue: - "newValue" + return "newValue" case .oldValue: - "oldValue" + return "oldValue" } } /// Identifier used for name comparison. + /// + /// + /// ```swift + /// class Foo { + /// func test() { + /// let `Self` = "abc" + /// print(Self.self) + /// + /// let `self` = "def" + /// print(self) + /// } + /// } + /// + /// Foo().test() + /// ``` + /// prints: + /// ``` + /// abc + /// def + /// ``` + /// `self` and `Self` identifers override + /// implicit `self` and `Self` introduced by + /// the `Foo` class declaration. var identifier: Identifier { Identifier(name) } @@ -70,17 +98,17 @@ import SwiftSyntax /// Could be class, struct, actor, protocol, function and more. case declaration(NamedDeclSyntax) /// Name introduced implicitly by certain syntax nodes. - case implicit(LookupImplicitNameKind) + case implicit(ImplicitDecl) /// Syntax associated with this name. @_spi(Experimental) public var syntax: SyntaxProtocol { switch self { case .identifier(let syntax, _): - syntax + return syntax case .declaration(let syntax): - syntax + return syntax case .implicit(let implicitName): - implicitName.syntax + return implicitName.syntax } } @@ -88,11 +116,11 @@ import SwiftSyntax @_spi(Experimental) public var identifier: Identifier? { switch self { case .identifier(let syntax, _): - Identifier(syntax.identifier) + return Identifier(syntax.identifier) ?? Identifier(syntax.identifier.text) case .declaration(let syntax): - Identifier(syntax.name) + return Identifier(syntax.name) case .implicit(let kind): - kind.identifier + return kind.identifier } } @@ -177,11 +205,7 @@ import SwiftSyntax identifiable: IdentifiableSyntax, accessibleAfter: AbsolutePosition? = nil ) -> [LookupName] { - if let closureCapture = identifiable as? ClosureCaptureSyntax, - closureCapture.identifier.tokenKind == .keyword(.self) - { - return [.implicit(.self(closureCapture))] // Handle `self` closure capture. - } else if identifiable.identifier.tokenKind != .wildcard { + if identifiable.identifier.tokenKind != .wildcard { return [.identifier(identifiable, accessibleAfter: accessibleAfter)] } else { return [] diff --git a/Sources/SwiftLexicalLookup/LookupState.swift b/Sources/SwiftLexicalLookup/LookupState.swift index 9bb469b2aa2..a56d9aad436 100644 --- a/Sources/SwiftLexicalLookup/LookupState.swift +++ b/Sources/SwiftLexicalLookup/LookupState.swift @@ -12,6 +12,8 @@ import Foundation +/// Represents internal state for lookup. +/// It shouldn't be used by clients. @_spi(Experimental) public struct LookupState { /// Specifies scopes that introduce names to their parent and /// should be skipped during lookup in sequential scopes. diff --git a/Sources/SwiftLexicalLookup/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/ScopeImplementations.swift index 42afcd62599..9c507a4dcb0 100644 --- a/Sources/SwiftLexicalLookup/ScopeImplementations.swift +++ b/Sources/SwiftLexicalLookup/ScopeImplementations.swift @@ -98,7 +98,7 @@ import SwiftSyntax /// - for `codeBlock` - a @_spi(Experimental) public func _lookup( for identifier: Identifier?, - at syntax: SyntaxProtocol, + at origin: SyntaxProtocol, with config: LookupConfig, state: LookupState ) -> [LookupResult] { @@ -106,13 +106,13 @@ import SwiftSyntax case .memberBlock: let names = introducedNames(using: .memberBlock) .filter { lookupName in - checkName(identifier, refersTo: lookupName, at: syntax) + checkName(identifier, refersTo: lookupName, at: origin) } return names.isEmpty ? [] : [.fromFileScope(self, withNames: names)] case .memberBlockUpToLastDecl: - var members = [LookupName]() - var sequentialItems = [CodeBlockItemSyntax]() + var members: [LookupName] = [] + var sequentialItems: [CodeBlockItemSyntax] = [] var encounteredNonDeclaration = false for codeBlockItem in statements { @@ -124,7 +124,7 @@ import SwiftSyntax if item.is(DeclSyntax.self) || item.is(VariableDeclSyntax.self) { let foundNames = LookupName.getNames(from: item) - members.append(contentsOf: foundNames.filter { checkName(identifier, refersTo: $0, at: syntax) }) + members.append(contentsOf: foundNames.filter { checkName(identifier, refersTo: $0, at: origin) }) } else { encounteredNonDeclaration = true sequentialItems.append(codeBlockItem) @@ -135,7 +135,7 @@ import SwiftSyntax let sequentialNames = sequentialLookup( in: sequentialItems, for: identifier, - at: syntax, + at: origin, with: config, state: state, createResultsForThisScopeWith: { .fromFileScope(self, withNames: $0) } @@ -157,14 +157,14 @@ import SwiftSyntax @_spi(Experimental) public func _lookup( for identifier: Identifier?, - at syntax: SyntaxProtocol, + at origin: SyntaxProtocol, with config: LookupConfig, state: LookupState ) -> [LookupResult] { sequentialLookup( in: statements, for: identifier, - at: syntax, + at: origin, with: config, state: state, createResultsForThisScopeWith: { .fromScope(self, withNames: $0) } @@ -279,14 +279,14 @@ import SwiftSyntax /// ``` @_spi(Experimental) public func _lookup( for identifier: Identifier?, - at syntax: SyntaxProtocol, + at origin: SyntaxProtocol, with config: LookupConfig, state: LookupState ) -> [LookupResult] { - if let elseBody, elseBody.position <= syntax.position, elseBody.endPosition >= syntax.position { - return lookupInParent(for: identifier, at: syntax, with: config, state: state) + if let elseBody, elseBody.position <= origin.position, elseBody.endPosition >= origin.position { + return lookupInParent(for: identifier, at: origin, with: config, state: state) } else { - return defaultLookupImplementation(for: identifier, at: syntax, with: config, state: state) + return defaultLookupImplementation(for: identifier, at: origin, with: config, state: state) } } } @@ -303,14 +303,14 @@ import SwiftSyntax @_spi(Experimental) extension GuardStmtSyntax: IntroducingToSequentialParentScopeSyntax { @_spi(Experimental) public func introducesToSequentialParent( for identifier: Identifier?, - at syntax: SwiftSyntax.SyntaxProtocol, + at origin: SyntaxProtocol, with config: LookupConfig, state: LookupState ) -> [LookupResult] { let names = conditions.flatMap { element in LookupName.getNames(from: element.condition, accessibleAfter: element.endPosition) }.filter { introducedName in - checkName(identifier, refersTo: introducedName, at: syntax) + checkName(identifier, refersTo: introducedName, at: origin) } return names.isEmpty ? [] : [.fromScope(self, withNames: names)] @@ -333,16 +333,16 @@ import SwiftSyntax /// ``` @_spi(Experimental) public func _lookup( for identifier: Identifier?, - at syntax: SyntaxProtocol, + at origin: SyntaxProtocol, with config: LookupConfig, state: LookupState ) -> [LookupResult] { - if body.position <= syntax.position && body.endPosition >= syntax.position { + if body.position <= origin.position && body.endPosition >= origin.position { var newState = state newState.skipSequentialIntroductionFrom = self - return lookupInParent(for: identifier, at: syntax, with: config, state: newState) + return lookupInParent(for: identifier, at: origin, with: config, state: newState) } else { - return defaultLookupImplementation(for: identifier, at: syntax, with: config, state: state) + return defaultLookupImplementation(for: identifier, at: origin, with: config, state: state) } } } diff --git a/Sources/SwiftLexicalLookup/ScopeSyntax.swift b/Sources/SwiftLexicalLookup/ScopeSyntax.swift index f04ff41a771..2217bf79d68 100644 --- a/Sources/SwiftLexicalLookup/ScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/ScopeSyntax.swift @@ -56,21 +56,14 @@ extension SyntaxProtocol { var parentScope: ScopeSyntax? { get } /// Names found in this scope. Ordered from first to last introduced. var introducedNames: [LookupName] { get } - /// Finds all declarations `name` refers to. `syntax` specifies the node lookup was triggered with. - /// If `name` set to `nil`, returns all available names at the given node. - func lookup( - for identifier: Identifier?, - at syntax: SyntaxProtocol, - with config: LookupConfig - ) -> [LookupResult] - /// Finds all declarations `name` refers to. `syntax` specifies the node lookup was triggered with. - /// If `name` set to `nil`, returns all available names at the given node. + /// Finds all declarations `identifier` refers to. `syntax` specifies the node lookup was triggered with. + /// If `identifier` set to `nil`, returns all available names at the given node. /// `state` represents lookup state passed between lookup methods. /// /// - Note: This method is intended for internal use only. For public usage, use ``ScopeSyntax/lookup(for:at:with:)`` instead. func _lookup( for identifier: Identifier?, - at syntax: SyntaxProtocol, + at origin: SyntaxProtocol, with config: LookupConfig, state: LookupState ) -> [LookupResult] @@ -81,66 +74,66 @@ extension SyntaxProtocol { self.parent?.scope } - /// Returns `LookupResult` of all names introduced in this scope that `name` + /// Returns `LookupResult` of all names introduced in this scope that `identifier` /// refers to and is accessible at given syntax node then passes lookup to the parent. - /// If `name` set to `nil`, returns all available names at the given node. + /// If `identifier` set to `nil`, returns all available names at the given node. /// `state` represents lookup state passed between lookup methods. /// /// - Note: This method is intended for internal use only. For public usage, use ``ScopeSyntax/lookup(for:at:with:)`` instead. @_spi(Experimental) public func _lookup( for identifier: Identifier?, - at syntax: SyntaxProtocol, + at origin: SyntaxProtocol, with config: LookupConfig, state: LookupState ) -> [LookupResult] { - defaultLookupImplementation(for: identifier, at: syntax, with: config, state: state) + defaultLookupImplementation(for: identifier, at: origin, with: config, state: state) } - - /// Returns `LookupResult` of all names introduced in this scope that `name` + + /// Returns `LookupResult` of all names introduced in this scope that `identifier` /// refers to and is accessible at given syntax node then passes lookup to the parent. - /// If `name` set to `nil`, returns all available names at the given node. + /// If `identifier` set to `nil`, returns all available names at the given node. @_spi(Experimental) public func lookup( for identifier: Identifier?, - at syntax: SyntaxProtocol, + at origin: SyntaxProtocol, with config: LookupConfig ) -> [LookupResult] { - _lookup(for: identifier, at: syntax, with: config, state: LookupState()) + _lookup(for: identifier, at: origin, with: config, state: LookupState()) } - /// Returns `LookupResult` of all names introduced in this scope that `name` + /// Returns `LookupResult` of all names introduced in this scope that `identifier` /// refers to and is accessible at given syntax node then passes lookup to the parent. - /// If `name` set to `nil`, returns all available names at the given node. + /// If `identifier` set to `nil`, returns all available names at the given node. func defaultLookupImplementation( for identifier: Identifier?, - at syntax: SyntaxProtocol, + at origin: SyntaxProtocol, with config: LookupConfig, state: LookupState ) -> [LookupResult] { let filteredNames = introducedNames .filter { introducedName in - checkName(identifier, refersTo: introducedName, at: syntax) + checkName(identifier, refersTo: introducedName, at: origin) } if filteredNames.isEmpty { - return lookupInParent(for: identifier, at: syntax, with: config, state: state) + return lookupInParent(for: identifier, at: origin, with: config, state: state) } else { return [.fromScope(self, withNames: filteredNames)] - + lookupInParent(for: identifier, at: syntax, with: config, state: state) + + lookupInParent(for: identifier, at: origin, with: config, state: state) } } /// Looks up in parent scope. func lookupInParent( for identifier: Identifier?, - at syntax: SyntaxProtocol, + at origin: SyntaxProtocol, with config: LookupConfig, state: LookupState ) -> [LookupResult] { - parentScope?._lookup(for: identifier, at: syntax, with: config, state: state) ?? [] + parentScope?._lookup(for: identifier, at: origin, with: config, state: state) ?? [] } - func checkName(_ name: Identifier?, refersTo introducedName: LookupName, at syntax: SyntaxProtocol) -> Bool { - introducedName.isAccessible(at: syntax) && (name == nil || introducedName.refersTo(name!)) + func checkName(_ name: Identifier?, refersTo introducedName: LookupName, at origin: SyntaxProtocol) -> Bool { + introducedName.isAccessible(at: origin) && (name == nil || introducedName.refersTo(name!)) } } diff --git a/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift index 5300a9af311..af8c4d9e119 100644 --- a/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift @@ -15,31 +15,39 @@ import SwiftSyntax /// Scope that, in addition to names introduced by itself, /// also handles names introduced by /// `IntroducingToSequentialParentScopeSyntax` children scopes. -protocol SequentialScopeSyntax: ScopeSyntax { +protocol SequentialScopeSyntax: ScopeSyntax {} + +extension SequentialScopeSyntax { /// Returns names introduced by `codeBlockItems` /// and included `IntroducingToSequentialParentScopeSyntax` children /// scopes that match the lookup. + /// + /// Example: + /// ```swift + /// func foo() { + /// let a = 1 + /// guard let a = x else { return } + /// let a = a // <-- 1 + /// guard let a = y else { return } + /// a // <-- 2 + /// } + /// ``` + /// For the first `a` reference, sequential lookup returns + /// two results: from `guard` scope and from code block scope + /// in this exact order. For the second `a` reference, + /// sequential lookup yields results from four scopes starting + /// from the bottom: `guard`, code block, `guard` and + /// code block scope in this exact order. func sequentialLookup( - in codeBlockItems: any Collection, - for identifier: Identifier?, - at syntax: SyntaxProtocol, - with config: LookupConfig, - state: LookupState, - createResultsForThisScopeWith getResults: ([LookupName]) -> (LookupResult) - ) -> [LookupResult] -} - -extension SequentialScopeSyntax { - func sequentialLookup( - in codeBlockItems: any Collection, + in codeBlockItems: some Collection, for identifier: Identifier?, - at syntax: SyntaxProtocol, + at origin: SyntaxProtocol, with config: LookupConfig, state: LookupState, createResultsForThisScopeWith getResults: ([LookupName]) -> (LookupResult) ) -> [LookupResult] { - var result = [LookupResult]() - var currentChunk = [LookupName]() + var result: [LookupResult] = [] + var currentChunk: [LookupName] = [] for codeBlockItem in codeBlockItems { if let introducingToParentScope = Syntax(codeBlockItem.item).asProtocol(SyntaxProtocol.self) @@ -59,35 +67,28 @@ extension SequentialScopeSyntax { } // Add names introduced by the encountered scope. - result.append( - contentsOf: introducingToParentScope.introducesToSequentialParent( - for: identifier, - at: syntax, - with: config, - state: state - ) + result += introducingToParentScope.introducesToSequentialParent( + for: identifier, + at: origin, + with: config, + state: state ) } else { // Extract new names from encountered node. - currentChunk.append( - contentsOf: - LookupName.getNames( - from: codeBlockItem.item, - accessibleAfter: codeBlockItem.endPosition - ).filter { introducedName in - checkName(identifier, refersTo: introducedName, at: syntax) - } - ) + currentChunk += LookupName.getNames( + from: codeBlockItem.item, + accessibleAfter: codeBlockItem.endPosition + ).filter { introducedName in + checkName(identifier, refersTo: introducedName, at: origin) + } } } // If there are some names collected, create a new result for this scope. if !currentChunk.isEmpty { result.append(getResults(currentChunk)) - currentChunk = [] } - return (result.isEmpty ? [] : result.reversed()) - + lookupInParent(for: identifier, at: syntax, with: config, state: state) + return result.reversed() + lookupInParent(for: identifier, at: origin, with: config, state: state) } } diff --git a/Sources/SwiftLexicalLookup/TypeScopeSyntax.swift b/Sources/SwiftLexicalLookup/TypeScopeSyntax.swift index 1f852ad9bc9..5f344405792 100644 --- a/Sources/SwiftLexicalLookup/TypeScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/TypeScopeSyntax.swift @@ -12,13 +12,9 @@ import SwiftSyntax -@_spi(Experimental) public protocol TypeScopeSyntax: ScopeSyntax, DeclSyntaxProtocol { - /// `self` and `Self` names referring to - /// this scope. - var implicitInstanceAndTypeNames: [LookupName] { get } -} +@_spi(Experimental) public protocol TypeScopeSyntax: ScopeSyntax, DeclSyntaxProtocol {} -@_spi(Experimental) extension TypeScopeSyntax { +extension TypeScopeSyntax { @_spi(Experimental) public var implicitInstanceAndTypeNames: [LookupName] { [.implicit(.self(self)), .implicit(.Self(self))] } diff --git a/Tests/SwiftLexicalLookupTest/ExpectedName.swift b/Tests/SwiftLexicalLookupTest/ExpectedName.swift index 867173d96eb..4047ef90f3b 100644 --- a/Tests/SwiftLexicalLookupTest/ExpectedName.swift +++ b/Tests/SwiftLexicalLookupTest/ExpectedName.swift @@ -32,7 +32,7 @@ enum ImplicitNameExpectation { case newValue(String) case oldValue(String) - func assertExpectation(marker: String, for name: LookupImplicitNameKind) { + func assertExpectation(marker: String, for name: ImplicitDecl) { switch (name, self) { case (.self, .self): break case (.Self, .Self): break diff --git a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift index 6627ef9951a..8cbc4dc26d0 100644 --- a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift +++ b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift @@ -152,7 +152,7 @@ final class testNameLookup: XCTestCase { """, references: [ "5️⃣": [ - .fromScope(ClosureExprSyntax.self, expectedNames: [NameExpectation.implicit(.self("2️⃣"))]), + .fromScope(ClosureExprSyntax.self, expectedNames: [NameExpectation.identifier("2️⃣")]), .fromScope(ClassDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("7️⃣"))]), ], "6️⃣": [ @@ -707,4 +707,30 @@ final class testNameLookup: XCTestCase { ] ) } + + func testImplicitSelfOverride() { + assertLexicalNameLookup( + source: """ + 1️⃣class Foo { + func test() { + let 2️⃣`Self` = "abc" + print(3️⃣Self.self) + + let 4️⃣`self` = "def" + print(5️⃣self) + } + } + """, + references: [ + "3️⃣": [ + .fromScope(CodeBlockSyntax.self, expectedNames: [NameExpectation.identifier("2️⃣")]), + .fromScope(ClassDeclSyntax.self, expectedNames: [NameExpectation.implicit(.Self("1️⃣"))]), + ], + "5️⃣": [ + .fromScope(CodeBlockSyntax.self, expectedNames: [NameExpectation.identifier("4️⃣")]), + .fromScope(ClassDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("1️⃣"))]), + ], + ] + ) + } } From 5461b0720806820fea6f10371184cbe4e59080af Mon Sep 17 00:00:00 2001 From: Jakub Florek Date: Thu, 1 Aug 2024 12:14:34 +0200 Subject: [PATCH 20/27] Add catch clause implicit `error`. Fix declaration name hoisting in sequential scopes. Fix partitioning with non matching results from `guard` scopes. --- .../ScopeImplementations.swift | 9 +- .../SequentialScopeSyntax.swift | 33 +++++-- .../NameLookupTests.swift | 94 ++++++++++++++++++- .../ResultExpectation.swift | 5 + 4 files changed, 132 insertions(+), 9 deletions(-) diff --git a/Sources/SwiftLexicalLookup/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/ScopeImplementations.swift index 9c507a4dcb0..40971b5c5f3 100644 --- a/Sources/SwiftLexicalLookup/ScopeImplementations.swift +++ b/Sources/SwiftLexicalLookup/ScopeImplementations.swift @@ -355,7 +355,7 @@ import SwiftSyntax @_spi(Experimental) extension AccessorDeclSyntax: ScopeSyntax { /// Implicit and/or explicit names introduced - /// withing the accessor.. + /// within the accessor. @_spi(Experimental) public var introducedNames: [LookupName] { if let parameters { return LookupName.getNames(from: parameters) @@ -371,3 +371,10 @@ import SwiftSyntax } } } + +@_spi(Experimental) extension CatchClauseSyntax: ScopeSyntax { + /// Implicit `error` when there are no catch items. + public var introducedNames: [LookupName] { + return catchItems.isEmpty ? [.implicit(.error(self))] : [] + } +} diff --git a/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift index af8c4d9e119..986657914ee 100644 --- a/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift @@ -48,8 +48,22 @@ extension SequentialScopeSyntax { ) -> [LookupResult] { var result: [LookupResult] = [] var currentChunk: [LookupName] = [] + var itemsWithoutNamedDecl: [CodeBlockItemSyntax] = [] for codeBlockItem in codeBlockItems { + if Syntax(codeBlockItem.item).isProtocol(NamedDeclSyntax.self) { + currentChunk += LookupName.getNames( + from: codeBlockItem.item, + accessibleAfter: codeBlockItem.endPosition + ).filter { introducedName in + checkName(identifier, refersTo: introducedName, at: origin) + } + } else { + itemsWithoutNamedDecl.append(codeBlockItem) + } + } + + for codeBlockItem in itemsWithoutNamedDecl { if let introducingToParentScope = Syntax(codeBlockItem.item).asProtocol(SyntaxProtocol.self) as? IntroducingToSequentialParentScopeSyntax { @@ -60,19 +74,24 @@ extension SequentialScopeSyntax { continue } + // Get results from encountered scope. + let introducedResults = introducingToParentScope.introducesToSequentialParent( + for: identifier, + at: origin, + with: config, + state: state + ) + + // Skip, if no results were found. + guard !introducedResults.isEmpty else { continue } + // If there are some names collected, create a new result for this scope. if !currentChunk.isEmpty { result.append(getResults(currentChunk)) currentChunk = [] } - // Add names introduced by the encountered scope. - result += introducingToParentScope.introducesToSequentialParent( - for: identifier, - at: origin, - with: config, - state: state - ) + result += introducedResults } else { // Extract new names from encountered node. currentChunk += LookupName.getNames( diff --git a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift index 8cbc4dc26d0..a5e66e8d547 100644 --- a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift +++ b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift @@ -596,8 +596,8 @@ final class testNameLookup: XCTestCase { references: [ "4️⃣": [ .fromFileScope(expectedNames: ["1️⃣"]), - .fromFileScope(expectedNames: ["3️⃣"]), .fromScope(GuardStmtSyntax.self, expectedNames: ["2️⃣"]), + .fromFileScope(expectedNames: ["3️⃣"]), ] ], expectedResultTypes: .all(IdentifierPatternSyntax.self, except: ["3️⃣": ClassDeclSyntax.self]) @@ -733,4 +733,96 @@ final class testNameLookup: XCTestCase { ] ) } + + func testImplicitErrorInCatchClause() { + assertLexicalNameLookup( + source: """ + func foo() { + let 1️⃣error = 0 + + do { + try x.bar() + 2️⃣error + } catch SomeError { + 3️⃣error + } 4️⃣catch { + 5️⃣error + } + } + """, + references: [ + "2️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: [NameExpectation.identifier("1️⃣")])], + "3️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: [NameExpectation.identifier("1️⃣")])], + "5️⃣": [ + .fromScope(CatchClauseSyntax.self, expectedNames: [NameExpectation.implicit(.error("4️⃣"))]), + .fromScope(CodeBlockSyntax.self, expectedNames: [NameExpectation.identifier("1️⃣")]), + ], + ] + ) + } + + func testTypeDeclAvaialabilityInSequentialScope() { + let declExpectation: [ResultExpectation] = [ + .fromScope( + CodeBlockSyntax.self, + expectedNames: [ + NameExpectation.declaration("2️⃣"), + NameExpectation.declaration("5️⃣"), + NameExpectation.declaration("8️⃣"), + ] + ) + ] + + assertLexicalNameLookup( + source: """ + func foo() { + 1️⃣a + 2️⃣class a {} + 3️⃣a + guard let x else { return } + 4️⃣a + 5️⃣actor a {} + 6️⃣a + guard let x else { return } + 7️⃣a + 8️⃣struct a {} + 9️⃣a + } + """, + references: [ + "1️⃣": declExpectation, + "3️⃣": declExpectation, + "4️⃣": declExpectation, + "6️⃣": declExpectation, + "7️⃣": declExpectation, + "9️⃣": declExpectation, + ] + ) + } + + func testNonMatchingGuardScopeDoesntPartitionResult() { + assertLexicalNameLookup( + source: """ + func foo() { + let 1️⃣a = 1 + let 2️⃣b = 2 + + guard let 3️⃣b = a else { return } + + let 4️⃣a = 3 + let 5️⃣b = 4 + + print(6️⃣a, 7️⃣b) + } + """, + references: [ + "6️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣", "4️⃣"])], + "7️⃣": [ + .fromScope(CodeBlockSyntax.self, expectedNames: ["5️⃣"]), + .fromScope(GuardStmtSyntax.self, expectedNames: ["3️⃣"]), + .fromScope(CodeBlockSyntax.self, expectedNames: ["2️⃣"]), + ], + ] + ) + } } diff --git a/Tests/SwiftLexicalLookupTest/ResultExpectation.swift b/Tests/SwiftLexicalLookupTest/ResultExpectation.swift index 27a9d5b58a8..b4a8c62f6db 100644 --- a/Tests/SwiftLexicalLookupTest/ResultExpectation.swift +++ b/Tests/SwiftLexicalLookupTest/ResultExpectation.swift @@ -38,6 +38,11 @@ enum ResultExpectation { } static func assertResult(marker: String, result: [LookupResult], expectedValues: [ResultExpectation]) { + XCTAssert( + result.count == expectedValues.count, + "For marker \(marker), actual result count \(result.count) doesn't match expected \(expectedValues.count)" + ) + for (actual, expected) in zip(result, expectedValues) { switch (actual, expected) { case ( From 059860a66135ee92954ab42c34d82a7100a88151 Mon Sep 17 00:00:00 2001 From: Jakub Florek Date: Thu, 1 Aug 2024 12:16:52 +0200 Subject: [PATCH 21/27] Break early from sequential for loop when over lookup origin position. --- Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift index 986657914ee..fd000f35bb7 100644 --- a/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift @@ -64,6 +64,8 @@ extension SequentialScopeSyntax { } for codeBlockItem in itemsWithoutNamedDecl { + guard codeBlockItem.position < origin.position else { break } + if let introducingToParentScope = Syntax(codeBlockItem.item).asProtocol(SyntaxProtocol.self) as? IntroducingToSequentialParentScopeSyntax { From 252d3be80c010add2bac9f9712ac32d9892ea574 Mon Sep 17 00:00:00 2001 From: Jakub Florek Date: Thu, 1 Aug 2024 12:50:18 +0200 Subject: [PATCH 22/27] Simplify lookup from `guard ... else` handling. --- Sources/SwiftLexicalLookup/LookupState.swift | 4 -- .../ScopeImplementations.swift | 40 +++++++------------ .../SequentialScopeSyntax.swift | 7 ---- 3 files changed, 14 insertions(+), 37 deletions(-) diff --git a/Sources/SwiftLexicalLookup/LookupState.swift b/Sources/SwiftLexicalLookup/LookupState.swift index a56d9aad436..22995129f40 100644 --- a/Sources/SwiftLexicalLookup/LookupState.swift +++ b/Sources/SwiftLexicalLookup/LookupState.swift @@ -15,9 +15,5 @@ import Foundation /// Represents internal state for lookup. /// It shouldn't be used by clients. @_spi(Experimental) public struct LookupState { - /// Specifies scopes that introduce names to their parent and - /// should be skipped during lookup in sequential scopes. - var skipSequentialIntroductionFrom: IntroducingToSequentialParentScopeSyntax? - init() {} } diff --git a/Sources/SwiftLexicalLookup/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/ScopeImplementations.swift index 40971b5c5f3..f78ac627bc4 100644 --- a/Sources/SwiftLexicalLookup/ScopeImplementations.swift +++ b/Sources/SwiftLexicalLookup/ScopeImplementations.swift @@ -301,12 +301,26 @@ import SwiftSyntax } @_spi(Experimental) extension GuardStmtSyntax: IntroducingToSequentialParentScopeSyntax { + /// Returns names matching lookup. + /// Lookup triggered from inside of `else` + /// returns no names. + /// + /// Example: + /// ```swift + /// guard let a = x else { + /// return // a is not visible here + /// } + /// // a is visible here + /// ``` @_spi(Experimental) public func introducesToSequentialParent( for identifier: Identifier?, at origin: SyntaxProtocol, with config: LookupConfig, state: LookupState ) -> [LookupResult] { + guard body.position > origin.position || body.endPosition < origin.position + else { return [] } + let names = conditions.flatMap { element in LookupName.getNames(from: element.condition, accessibleAfter: element.endPosition) }.filter { introducedName in @@ -319,32 +333,6 @@ import SwiftSyntax @_spi(Experimental) public var introducedNames: [LookupName] { [] } - - /// Returns names matching lookup. - /// Lookup triggered from inside of `else` - /// clause is immediately forwarded to parent scope. - /// - /// Example: - /// ```swift - /// guard let a = x else { - /// return // a is not visible here - /// } - /// // a is visible here - /// ``` - @_spi(Experimental) public func _lookup( - for identifier: Identifier?, - at origin: SyntaxProtocol, - with config: LookupConfig, - state: LookupState - ) -> [LookupResult] { - if body.position <= origin.position && body.endPosition >= origin.position { - var newState = state - newState.skipSequentialIntroductionFrom = self - return lookupInParent(for: identifier, at: origin, with: config, state: newState) - } else { - return defaultLookupImplementation(for: identifier, at: origin, with: config, state: state) - } - } } @_spi(Experimental) extension ActorDeclSyntax: TypeScopeSyntax {} diff --git a/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift index fd000f35bb7..1ecad435c69 100644 --- a/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift @@ -69,13 +69,6 @@ extension SequentialScopeSyntax { if let introducingToParentScope = Syntax(codeBlockItem.item).asProtocol(SyntaxProtocol.self) as? IntroducingToSequentialParentScopeSyntax { - // Check if the enocountered scope should be ignored. - if let scopeToSkip = state.skipSequentialIntroductionFrom, - scopeToSkip.id == introducingToParentScope.id - { - continue - } - // Get results from encountered scope. let introducedResults = introducingToParentScope.introducesToSequentialParent( for: identifier, From be2e58859e019d87473372e00335452a4b7fe57a Mon Sep 17 00:00:00 2001 From: Jakub Florek Date: Thu, 1 Aug 2024 12:51:26 +0200 Subject: [PATCH 23/27] Format. --- Sources/SwiftLexicalLookup/ScopeImplementations.swift | 2 +- Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftLexicalLookup/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/ScopeImplementations.swift index f78ac627bc4..2ec6a23efc2 100644 --- a/Sources/SwiftLexicalLookup/ScopeImplementations.swift +++ b/Sources/SwiftLexicalLookup/ScopeImplementations.swift @@ -320,7 +320,7 @@ import SwiftSyntax ) -> [LookupResult] { guard body.position > origin.position || body.endPosition < origin.position else { return [] } - + let names = conditions.flatMap { element in LookupName.getNames(from: element.condition, accessibleAfter: element.endPosition) }.filter { introducedName in diff --git a/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift index 1ecad435c69..3f2e0058778 100644 --- a/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift @@ -65,7 +65,7 @@ extension SequentialScopeSyntax { for codeBlockItem in itemsWithoutNamedDecl { guard codeBlockItem.position < origin.position else { break } - + if let introducingToParentScope = Syntax(codeBlockItem.item).asProtocol(SyntaxProtocol.self) as? IntroducingToSequentialParentScopeSyntax { From 01f2887cb59f45a65c347b71ff3fd978977cb1b6 Mon Sep 17 00:00:00 2001 From: Jakub Florek Date: Fri, 2 Aug 2024 11:21:24 +0200 Subject: [PATCH 24/27] Add suggestions --- ...oducingToSequentialParentScopeSyntax.swift | 14 ++-- Sources/SwiftLexicalLookup/LookupName.swift | 54 +++++++++++---- Sources/SwiftLexicalLookup/LookupState.swift | 19 ------ .../ScopeImplementations.swift | 66 +++++++++---------- Sources/SwiftLexicalLookup/ScopeSyntax.swift | 47 ++++--------- .../SequentialScopeSyntax.swift | 20 +++--- Sources/SwiftSyntax/Identifier.swift | 58 +++++++--------- Tests/SwiftLexicalLookupTest/Assertions.swift | 4 +- .../SwiftLexicalLookupTest/ExpectedName.swift | 8 ++- .../NameLookupTests.swift | 2 +- 10 files changed, 138 insertions(+), 154 deletions(-) delete mode 100644 Sources/SwiftLexicalLookup/LookupState.swift diff --git a/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift b/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift index ce7906406e8..528f470ff23 100644 --- a/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift @@ -13,12 +13,14 @@ import SwiftSyntax protocol IntroducingToSequentialParentScopeSyntax: ScopeSyntax { - /// Returns names matching lookup that should be - /// handled by it's parent sequential scope. - func introducesToSequentialParent( + /// Returns all names introduced to parent. + var namesIntroducedToSequentialParent: [LookupName] { get } + + /// Returns results matching lookup that should be + /// interleaved with results of the sequential parent. + func lookupFromSequentialParent( for identifier: Identifier?, - at origin: SyntaxProtocol, - with config: LookupConfig, - state: LookupState + at origin: AbsolutePosition, + with config: LookupConfig ) -> [LookupResult] } diff --git a/Sources/SwiftLexicalLookup/LookupName.swift b/Sources/SwiftLexicalLookup/LookupName.swift index 94c27f4fd37..0355ef3ca61 100644 --- a/Sources/SwiftLexicalLookup/LookupName.swift +++ b/Sources/SwiftLexicalLookup/LookupName.swift @@ -61,7 +61,8 @@ import SwiftSyntax } } - /// Identifier used for name comparison. + /// Note that `self` and `Self` are treated as identifiers for name lookup purposes + /// and that a variable named `self` can shadow the `self` keyword. For example. /// /// /// ```swift @@ -82,11 +83,21 @@ import SwiftSyntax /// abc /// def /// ``` - /// `self` and `Self` identifers override - /// implicit `self` and `Self` introduced by + /// `self` and `Self` identifers override implicit `self` and `Self` introduced by /// the `Foo` class declaration. var identifier: Identifier { - Identifier(name) + switch self { + case .self: + return Identifier("self") + case .Self: + return Identifier("Self") + case .error: + return Identifier("error") + case .newValue: + return Identifier("newValue") + case .oldValue: + return Identifier("oldValue") + } } } @@ -99,6 +110,10 @@ import SwiftSyntax case declaration(NamedDeclSyntax) /// Name introduced implicitly by certain syntax nodes. case implicit(ImplicitDecl) + /// Explicit `self` keyword. + case `self`(IdentifiableSyntax, accessibleAfter: AbsolutePosition?) + /// Explicit `Self` keyword. + case `Self`(IdentifiableSyntax, accessibleAfter: AbsolutePosition?) /// Syntax associated with this name. @_spi(Experimental) public var syntax: SyntaxProtocol { @@ -109,6 +124,8 @@ import SwiftSyntax return syntax case .implicit(let implicitName): return implicitName.syntax + case .self(let syntax, _), .Self(let syntax, _): + return syntax } } @@ -116,11 +133,15 @@ import SwiftSyntax @_spi(Experimental) public var identifier: Identifier? { switch self { case .identifier(let syntax, _): - return Identifier(syntax.identifier) ?? Identifier(syntax.identifier.text) + return Identifier(syntax.identifier) case .declaration(let syntax): return Identifier(syntax.name) case .implicit(let kind): return kind.identifier + case .self: + return Identifier("self") + case .Self: + return Identifier("Self") } } @@ -128,7 +149,9 @@ import SwiftSyntax /// If set to `nil`, the name is available at any point in scope. var accessibleAfter: AbsolutePosition? { switch self { - case .identifier(_, let absolutePosition): + case .identifier(_, let absolutePosition), + .self(_, let absolutePosition), + .Self(_, let absolutePosition): return absolutePosition default: return nil @@ -136,9 +159,9 @@ import SwiftSyntax } /// Checks if this name was introduced before the syntax used for lookup. - func isAccessible(at lookedUpSyntax: SyntaxProtocol) -> Bool { + func isAccessible(at origin: AbsolutePosition) -> Bool { guard let accessibleAfter else { return true } - return accessibleAfter <= lookedUpSyntax.position + return accessibleAfter <= origin } /// Checks if this name refers to the looked up phrase. @@ -205,10 +228,17 @@ import SwiftSyntax identifiable: IdentifiableSyntax, accessibleAfter: AbsolutePosition? = nil ) -> [LookupName] { - if identifiable.identifier.tokenKind != .wildcard { - return [.identifier(identifiable, accessibleAfter: accessibleAfter)] - } else { - return [] + switch identifiable.identifier.tokenKind { + case .keyword(.self): + return [.self(identifiable, accessibleAfter: accessibleAfter)] + case .keyword(.Self): + return [.Self(identifiable, accessibleAfter: accessibleAfter)] + default: + if identifiable.identifier.tokenKind != .wildcard { + return [.identifier(identifiable, accessibleAfter: accessibleAfter)] + } else { + return [] + } } } diff --git a/Sources/SwiftLexicalLookup/LookupState.swift b/Sources/SwiftLexicalLookup/LookupState.swift deleted file mode 100644 index 22995129f40..00000000000 --- a/Sources/SwiftLexicalLookup/LookupState.swift +++ /dev/null @@ -1,19 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -/// Represents internal state for lookup. -/// It shouldn't be used by clients. -@_spi(Experimental) public struct LookupState { - init() {} -} diff --git a/Sources/SwiftLexicalLookup/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/ScopeImplementations.swift index 2ec6a23efc2..701a51ff886 100644 --- a/Sources/SwiftLexicalLookup/ScopeImplementations.swift +++ b/Sources/SwiftLexicalLookup/ScopeImplementations.swift @@ -96,11 +96,10 @@ import SwiftSyntax /// - for `memberBlockUpToLastDecl` - a, b, c, d /// - for `memberBlock` - a, b, c, d, e, f /// - for `codeBlock` - a - @_spi(Experimental) public func _lookup( + @_spi(Experimental) public func lookup( for identifier: Identifier?, - at origin: SyntaxProtocol, - with config: LookupConfig, - state: LookupState + at origin: AbsolutePosition, + with config: LookupConfig ) -> [LookupResult] { switch config.fileScopeHandling { case .memberBlock: @@ -121,7 +120,7 @@ import SwiftSyntax if encounteredNonDeclaration { sequentialItems.append(codeBlockItem) } else { - if item.is(DeclSyntax.self) || item.is(VariableDeclSyntax.self) { + if item.is(DeclSyntax.self) { let foundNames = LookupName.getNames(from: item) members.append(contentsOf: foundNames.filter { checkName(identifier, refersTo: $0, at: origin) }) @@ -137,7 +136,6 @@ import SwiftSyntax for: identifier, at: origin, with: config, - state: state, createResultsForThisScopeWith: { .fromFileScope(self, withNames: $0) } ) @@ -155,18 +153,16 @@ import SwiftSyntax } } - @_spi(Experimental) public func _lookup( + @_spi(Experimental) public func lookup( for identifier: Identifier?, - at origin: SyntaxProtocol, - with config: LookupConfig, - state: LookupState + at origin: AbsolutePosition, + with config: LookupConfig ) -> [LookupResult] { sequentialLookup( in: statements, for: identifier, at: origin, with: config, - state: state, createResultsForThisScopeWith: { .fromScope(self, withNames: $0) } ) } @@ -277,16 +273,15 @@ import SwiftSyntax /// // <-- a is not visible here /// } /// ``` - @_spi(Experimental) public func _lookup( + @_spi(Experimental) public func lookup( for identifier: Identifier?, - at origin: SyntaxProtocol, - with config: LookupConfig, - state: LookupState + at origin: AbsolutePosition, + with config: LookupConfig ) -> [LookupResult] { - if let elseBody, elseBody.position <= origin.position, elseBody.endPosition >= origin.position { - return lookupInParent(for: identifier, at: origin, with: config, state: state) + if let elseBody, elseBody.position <= origin, elseBody.endPosition >= origin { + return lookupInParent(for: identifier, at: origin, with: config) } else { - return defaultLookupImplementation(for: identifier, at: origin, with: config, state: state) + return defaultLookupImplementation(for: identifier, at: origin, with: config) } } } @@ -301,8 +296,19 @@ import SwiftSyntax } @_spi(Experimental) extension GuardStmtSyntax: IntroducingToSequentialParentScopeSyntax { - /// Returns names matching lookup. - /// Lookup triggered from inside of `else` + var namesIntroducedToSequentialParent: [LookupName] { + conditions.flatMap { element in + LookupName.getNames(from: element.condition, accessibleAfter: element.endPosition) + } + } + + @_spi(Experimental) public var introducedNames: [LookupName] { + [] + } + + /// Returns results matching lookup that should be + /// interleaved with sequential parent's results. + /// Lookup triggered from within of the `else` body /// returns no names. /// /// Example: @@ -312,27 +318,20 @@ import SwiftSyntax /// } /// // a is visible here /// ``` - @_spi(Experimental) public func introducesToSequentialParent( + func lookupFromSequentialParent( for identifier: Identifier?, - at origin: SyntaxProtocol, - with config: LookupConfig, - state: LookupState + at origin: AbsolutePosition, + with config: LookupConfig ) -> [LookupResult] { - guard body.position > origin.position || body.endPosition < origin.position + guard body.position > origin || body.endPosition < origin else { return [] } - let names = conditions.flatMap { element in - LookupName.getNames(from: element.condition, accessibleAfter: element.endPosition) - }.filter { introducedName in + let names = namesIntroducedToSequentialParent.filter { introducedName in checkName(identifier, refersTo: introducedName, at: origin) } return names.isEmpty ? [] : [.fromScope(self, withNames: names)] } - - @_spi(Experimental) public var introducedNames: [LookupName] { - [] - } } @_spi(Experimental) extension ActorDeclSyntax: TypeScopeSyntax {} @@ -342,8 +341,7 @@ import SwiftSyntax @_spi(Experimental) extension ExtensionDeclSyntax: TypeScopeSyntax {} @_spi(Experimental) extension AccessorDeclSyntax: ScopeSyntax { - /// Implicit and/or explicit names introduced - /// within the accessor. + /// Implicit and/or explicit names introduced within the accessor. @_spi(Experimental) public var introducedNames: [LookupName] { if let parameters { return LookupName.getNames(from: parameters) diff --git a/Sources/SwiftLexicalLookup/ScopeSyntax.swift b/Sources/SwiftLexicalLookup/ScopeSyntax.swift index 2217bf79d68..af9a247a40c 100644 --- a/Sources/SwiftLexicalLookup/ScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/ScopeSyntax.swift @@ -47,7 +47,7 @@ extension SyntaxProtocol { for identifier: Identifier?, with config: LookupConfig = LookupConfig() ) -> [LookupResult] { - scope?.lookup(for: identifier, at: self, with: config) ?? [] + scope?.lookup(for: identifier, at: self.position, with: config) ?? [] } } @@ -59,13 +59,10 @@ extension SyntaxProtocol { /// Finds all declarations `identifier` refers to. `syntax` specifies the node lookup was triggered with. /// If `identifier` set to `nil`, returns all available names at the given node. /// `state` represents lookup state passed between lookup methods. - /// - /// - Note: This method is intended for internal use only. For public usage, use ``ScopeSyntax/lookup(for:at:with:)`` instead. - func _lookup( + func lookup( for identifier: Identifier?, - at origin: SyntaxProtocol, - with config: LookupConfig, - state: LookupState + at origin: AbsolutePosition, + with config: LookupConfig ) -> [LookupResult] } @@ -78,26 +75,12 @@ extension SyntaxProtocol { /// refers to and is accessible at given syntax node then passes lookup to the parent. /// If `identifier` set to `nil`, returns all available names at the given node. /// `state` represents lookup state passed between lookup methods. - /// - /// - Note: This method is intended for internal use only. For public usage, use ``ScopeSyntax/lookup(for:at:with:)`` instead. - @_spi(Experimental) public func _lookup( - for identifier: Identifier?, - at origin: SyntaxProtocol, - with config: LookupConfig, - state: LookupState - ) -> [LookupResult] { - defaultLookupImplementation(for: identifier, at: origin, with: config, state: state) - } - - /// Returns `LookupResult` of all names introduced in this scope that `identifier` - /// refers to and is accessible at given syntax node then passes lookup to the parent. - /// If `identifier` set to `nil`, returns all available names at the given node. @_spi(Experimental) public func lookup( for identifier: Identifier?, - at origin: SyntaxProtocol, + at origin: AbsolutePosition, with config: LookupConfig ) -> [LookupResult] { - _lookup(for: identifier, at: origin, with: config, state: LookupState()) + defaultLookupImplementation(for: identifier, at: origin, with: config) } /// Returns `LookupResult` of all names introduced in this scope that `identifier` @@ -105,9 +88,8 @@ extension SyntaxProtocol { /// If `identifier` set to `nil`, returns all available names at the given node. func defaultLookupImplementation( for identifier: Identifier?, - at origin: SyntaxProtocol, - with config: LookupConfig, - state: LookupState + at origin: AbsolutePosition, + with config: LookupConfig ) -> [LookupResult] { let filteredNames = introducedNames @@ -116,24 +98,23 @@ extension SyntaxProtocol { } if filteredNames.isEmpty { - return lookupInParent(for: identifier, at: origin, with: config, state: state) + return lookupInParent(for: identifier, at: origin, with: config) } else { return [.fromScope(self, withNames: filteredNames)] - + lookupInParent(for: identifier, at: origin, with: config, state: state) + + lookupInParent(for: identifier, at: origin, with: config) } } /// Looks up in parent scope. func lookupInParent( for identifier: Identifier?, - at origin: SyntaxProtocol, - with config: LookupConfig, - state: LookupState + at origin: AbsolutePosition, + with config: LookupConfig ) -> [LookupResult] { - parentScope?._lookup(for: identifier, at: origin, with: config, state: state) ?? [] + parentScope?.lookup(for: identifier, at: origin, with: config) ?? [] } - func checkName(_ name: Identifier?, refersTo introducedName: LookupName, at origin: SyntaxProtocol) -> Bool { + func checkName(_ name: Identifier?, refersTo introducedName: LookupName, at origin: AbsolutePosition) -> Bool { introducedName.isAccessible(at: origin) && (name == nil || introducedName.refersTo(name!)) } } diff --git a/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift index 3f2e0058778..21f6de167cd 100644 --- a/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift @@ -41,12 +41,11 @@ extension SequentialScopeSyntax { func sequentialLookup( in codeBlockItems: some Collection, for identifier: Identifier?, - at origin: SyntaxProtocol, + at origin: AbsolutePosition, with config: LookupConfig, - state: LookupState, createResultsForThisScopeWith getResults: ([LookupName]) -> (LookupResult) ) -> [LookupResult] { - var result: [LookupResult] = [] + var results: [LookupResult] = [] var currentChunk: [LookupName] = [] var itemsWithoutNamedDecl: [CodeBlockItemSyntax] = [] @@ -64,17 +63,16 @@ extension SequentialScopeSyntax { } for codeBlockItem in itemsWithoutNamedDecl { - guard codeBlockItem.position < origin.position else { break } + guard codeBlockItem.position < origin else { break } if let introducingToParentScope = Syntax(codeBlockItem.item).asProtocol(SyntaxProtocol.self) as? IntroducingToSequentialParentScopeSyntax { // Get results from encountered scope. - let introducedResults = introducingToParentScope.introducesToSequentialParent( + let introducedResults = introducingToParentScope.lookupFromSequentialParent( for: identifier, at: origin, - with: config, - state: state + with: config ) // Skip, if no results were found. @@ -82,11 +80,11 @@ extension SequentialScopeSyntax { // If there are some names collected, create a new result for this scope. if !currentChunk.isEmpty { - result.append(getResults(currentChunk)) + results.append(getResults(currentChunk)) currentChunk = [] } - result += introducedResults + results += introducedResults } else { // Extract new names from encountered node. currentChunk += LookupName.getNames( @@ -100,9 +98,9 @@ extension SequentialScopeSyntax { // If there are some names collected, create a new result for this scope. if !currentChunk.isEmpty { - result.append(getResults(currentChunk)) + results.append(getResults(currentChunk)) } - return result.reversed() + lookupInParent(for: identifier, at: origin, with: config, state: state) + return results.reversed() + lookupInParent(for: identifier, at: origin, with: config) } } diff --git a/Sources/SwiftSyntax/Identifier.swift b/Sources/SwiftSyntax/Identifier.swift index ed0b511f0e1..97e9c7a027f 100644 --- a/Sources/SwiftSyntax/Identifier.swift +++ b/Sources/SwiftSyntax/Identifier.swift @@ -12,54 +12,27 @@ /// A canonicalized representation of an identifier that strips away backticks. public struct Identifier: Equatable, Hashable, Sendable { - enum IdentifierKind: Hashable { - case token(raw: RawIdentifier, arena: SyntaxArenaRef) - case string(String) - - static func sanitize(string: String) -> IdentifierKind { - let backtick = "`" - if string.count > 2 && string.hasPrefix(backtick) && string.hasSuffix(backtick) { - let startIndex = string.index(after: string.startIndex) - let endIndex = string.index(before: string.endIndex) - return .string(String(string[startIndex.. Bool { @@ -67,6 +40,13 @@ public struct Identifier: Equatable, Hashable, Sendable { } } +extension Identifier { + @_spi(Testing) public init(anyToken token: TokenSyntax) { + self.raw = RawIdentifier(token.tokenView.rawText) + self.arena = token.raw.arenaReference + } +} + @_spi(RawSyntax) public struct RawIdentifier: Equatable, Hashable, Sendable { public let name: SyntaxText @@ -82,4 +62,12 @@ public struct RawIdentifier: Equatable, Hashable, Sendable { self.name = raw.rawText } } + + fileprivate init(_ staticString: StaticString) { + self.init(SyntaxText(staticString)) + } + + fileprivate init(_ syntaxText: SyntaxText) { + name = syntaxText + } } diff --git a/Tests/SwiftLexicalLookupTest/Assertions.swift b/Tests/SwiftLexicalLookupTest/Assertions.swift index ccfeaf61788..eb671c5ccce 100644 --- a/Tests/SwiftLexicalLookupTest/Assertions.swift +++ b/Tests/SwiftLexicalLookupTest/Assertions.swift @@ -12,7 +12,7 @@ @_spi(Experimental) import SwiftLexicalLookup import SwiftParser -import SwiftSyntax +@_spi(Testing) import SwiftSyntax import XCTest import _SwiftSyntaxTestSupport @@ -93,7 +93,7 @@ func assertLexicalNameLookup( assertLexicalScopeQuery( source: source, methodUnderTest: { marker, tokenAtMarker in - let lookupIdentifier = Identifier(tokenAtMarker) ?? Identifier(tokenAtMarker.text) + let lookupIdentifier = Identifier(tokenAtMarker) ?? Identifier(anyToken: tokenAtMarker) let result = tokenAtMarker.lookup(for: useNilAsTheParameter ? nil : lookupIdentifier, with: config) diff --git a/Tests/SwiftLexicalLookupTest/ExpectedName.swift b/Tests/SwiftLexicalLookupTest/ExpectedName.swift index 4047ef90f3b..8db81175767 100644 --- a/Tests/SwiftLexicalLookupTest/ExpectedName.swift +++ b/Tests/SwiftLexicalLookupTest/ExpectedName.swift @@ -62,11 +62,15 @@ enum NameExpectation: ExpectedName { case identifier(String) case declaration(String) case implicit(ImplicitNameExpectation) + case `self`(String) + case `Self`(String) var marker: String { switch self { case .identifier(let marker), - .declaration(let marker): + .declaration(let marker), + .self(let marker), + .Self(let marker): return marker case .implicit(let implicitName): return implicitName.marker @@ -77,6 +81,8 @@ enum NameExpectation: ExpectedName { switch (name, self) { case (.identifier, .identifier): break case (.declaration, .declaration): break + case (.self, .self): break + case (.Self, .Self): break case (.implicit(let implicitName), .implicit(let implicitNameExpectation)): implicitNameExpectation.assertExpectation(marker: marker, for: implicitName) default: diff --git a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift index a5e66e8d547..128d19145cd 100644 --- a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift +++ b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift @@ -152,7 +152,7 @@ final class testNameLookup: XCTestCase { """, references: [ "5️⃣": [ - .fromScope(ClosureExprSyntax.self, expectedNames: [NameExpectation.identifier("2️⃣")]), + .fromScope(ClosureExprSyntax.self, expectedNames: [NameExpectation.`self`("2️⃣")]), .fromScope(ClassDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("7️⃣"))]), ], "6️⃣": [ From cf273f8943c52a8df8c7348c81a5120b16c577c7 Mon Sep 17 00:00:00 2001 From: Jakub Florek Date: Fri, 2 Aug 2024 11:21:40 +0200 Subject: [PATCH 25/27] Format. --- .../IntroducingToSequentialParentScopeSyntax.swift | 2 +- Sources/SwiftLexicalLookup/LookupName.swift | 6 +++--- Sources/SwiftLexicalLookup/ScopeImplementations.swift | 4 ++-- Sources/SwiftSyntax/Identifier.swift | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift b/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift index 528f470ff23..5d58aa7a87b 100644 --- a/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift @@ -15,7 +15,7 @@ import SwiftSyntax protocol IntroducingToSequentialParentScopeSyntax: ScopeSyntax { /// Returns all names introduced to parent. var namesIntroducedToSequentialParent: [LookupName] { get } - + /// Returns results matching lookup that should be /// interleaved with results of the sequential parent. func lookupFromSequentialParent( diff --git a/Sources/SwiftLexicalLookup/LookupName.swift b/Sources/SwiftLexicalLookup/LookupName.swift index 0355ef3ca61..95cb87bea43 100644 --- a/Sources/SwiftLexicalLookup/LookupName.swift +++ b/Sources/SwiftLexicalLookup/LookupName.swift @@ -61,7 +61,7 @@ import SwiftSyntax } } - /// Note that `self` and `Self` are treated as identifiers for name lookup purposes + /// Note that `self` and `Self` are treated as identifiers for name lookup purposes /// and that a variable named `self` can shadow the `self` keyword. For example. /// /// @@ -150,8 +150,8 @@ import SwiftSyntax var accessibleAfter: AbsolutePosition? { switch self { case .identifier(_, let absolutePosition), - .self(_, let absolutePosition), - .Self(_, let absolutePosition): + .self(_, let absolutePosition), + .Self(_, let absolutePosition): return absolutePosition default: return nil diff --git a/Sources/SwiftLexicalLookup/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/ScopeImplementations.swift index 701a51ff886..432f99b900f 100644 --- a/Sources/SwiftLexicalLookup/ScopeImplementations.swift +++ b/Sources/SwiftLexicalLookup/ScopeImplementations.swift @@ -301,11 +301,11 @@ import SwiftSyntax LookupName.getNames(from: element.condition, accessibleAfter: element.endPosition) } } - + @_spi(Experimental) public var introducedNames: [LookupName] { [] } - + /// Returns results matching lookup that should be /// interleaved with sequential parent's results. /// Lookup triggered from within of the `else` body diff --git a/Sources/SwiftSyntax/Identifier.swift b/Sources/SwiftSyntax/Identifier.swift index 97e9c7a027f..97a588395e3 100644 --- a/Sources/SwiftSyntax/Identifier.swift +++ b/Sources/SwiftSyntax/Identifier.swift @@ -62,11 +62,11 @@ public struct RawIdentifier: Equatable, Hashable, Sendable { self.name = raw.rawText } } - + fileprivate init(_ staticString: StaticString) { self.init(SyntaxText(staticString)) } - + fileprivate init(_ syntaxText: SyntaxText) { name = syntaxText } From c83b3c6f3454a05b0a22ce26f12de594de19d68b Mon Sep 17 00:00:00 2001 From: Jakub Florek Date: Fri, 2 Aug 2024 11:55:05 +0200 Subject: [PATCH 26/27] Improve documentation. Improve sequential scope test case. --- Sources/SwiftLexicalLookup/LookupName.swift | 4 ++-- Tests/SwiftLexicalLookupTest/NameLookupTests.swift | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Sources/SwiftLexicalLookup/LookupName.swift b/Sources/SwiftLexicalLookup/LookupName.swift index 95cb87bea43..aac72f6c24f 100644 --- a/Sources/SwiftLexicalLookup/LookupName.swift +++ b/Sources/SwiftLexicalLookup/LookupName.swift @@ -61,10 +61,10 @@ import SwiftSyntax } } + /// Identifier used for name comparison. + /// /// Note that `self` and `Self` are treated as identifiers for name lookup purposes /// and that a variable named `self` can shadow the `self` keyword. For example. - /// - /// /// ```swift /// class Foo { /// func test() { diff --git a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift index 128d19145cd..fa9df364c22 100644 --- a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift +++ b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift @@ -811,13 +811,18 @@ final class testNameLookup: XCTestCase { let 4️⃣a = 3 let 5️⃣b = 4 + + guard let 6️⃣a = b else { return } - print(6️⃣a, 7️⃣b) + print(7️⃣a, 8️⃣b) } """, references: [ - "6️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣", "4️⃣"])], "7️⃣": [ + .fromScope(GuardStmtSyntax.self, expectedNames: ["6️⃣"]), + .fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣", "4️⃣"]), + ], + "8️⃣": [ .fromScope(CodeBlockSyntax.self, expectedNames: ["5️⃣"]), .fromScope(GuardStmtSyntax.self, expectedNames: ["3️⃣"]), .fromScope(CodeBlockSyntax.self, expectedNames: ["2️⃣"]), From fcaecc71c34160d5e6f271ed0df4f49a55a2fab7 Mon Sep 17 00:00:00 2001 From: Jakub Florek Date: Fri, 2 Aug 2024 22:56:14 +0200 Subject: [PATCH 27/27] Remove optional chaining from identifier test. --- Tests/SwiftLexicalLookupTest/NameLookupTests.swift | 2 +- Tests/SwiftSyntaxTest/IdentifierTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift index fa9df364c22..121610a1ed9 100644 --- a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift +++ b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift @@ -811,7 +811,7 @@ final class testNameLookup: XCTestCase { let 4️⃣a = 3 let 5️⃣b = 4 - + guard let 6️⃣a = b else { return } print(7️⃣a, 8️⃣b) diff --git a/Tests/SwiftSyntaxTest/IdentifierTests.swift b/Tests/SwiftSyntaxTest/IdentifierTests.swift index 8e110807df8..e0083a0a5c4 100644 --- a/Tests/SwiftSyntaxTest/IdentifierTests.swift +++ b/Tests/SwiftSyntaxTest/IdentifierTests.swift @@ -39,7 +39,7 @@ class IdentifierTests: XCTestCase { public func testIdentifier() { let token = TokenSyntax(stringLiteral: "sometoken") withExtendedLifetime(token) { token in - XCTAssertEqual(token.identifier?.raw?.name, SyntaxText("sometoken")) + XCTAssertEqual(token.identifier?.raw.name, SyntaxText("sometoken")) } }