diff --git a/Package.swift b/Package.swift index 79081a0a625..7789a535c0e 100644 --- a/Package.swift +++ b/Package.swift @@ -184,7 +184,7 @@ let package = Package( .target( name: "SwiftLexicalLookup", - dependencies: ["SwiftSyntax"] + dependencies: ["SwiftSyntax", "SwiftIfConfig"] ), .testTarget( diff --git a/Sources/SwiftLexicalLookup/CMakeLists.txt b/Sources/SwiftLexicalLookup/CMakeLists.txt index 5c136ce22d0..3a73e794869 100644 --- a/Sources/SwiftLexicalLookup/CMakeLists.txt +++ b/Sources/SwiftLexicalLookup/CMakeLists.txt @@ -26,5 +26,6 @@ add_swift_syntax_library(SwiftLexicalLookup ) target_link_swift_syntax_libraries(SwiftLexicalLookup PUBLIC - SwiftSyntax) + SwiftSyntax + SwiftIfConfig) diff --git a/Sources/SwiftLexicalLookup/LookupConfig.swift b/Sources/SwiftLexicalLookup/LookupConfig.swift index 627d8edf2ea..7b9ebaa376d 100644 --- a/Sources/SwiftLexicalLookup/LookupConfig.swift +++ b/Sources/SwiftLexicalLookup/LookupConfig.swift @@ -10,6 +10,8 @@ // //===----------------------------------------------------------------------===// +import SwiftIfConfig + @_spi(Experimental) public struct LookupConfig { /// Specifies whether lookup should finish in the closest sequential scope. /// @@ -31,14 +33,17 @@ /// If `finishInSequentialScope` would be set to `false`, the only name /// returned by lookup would be the `a` declaration from inside function body. @_spi(Experimental) public var finishInSequentialScope: Bool + @_spi(Experimental) public var configuredRegions: ConfiguredRegions? /// Creates a new lookup configuration. /// /// - `finishInSequentialScope` - specifies whether lookup should finish /// in the closest sequential scope. `false` by default. @_spi(Experimental) public init( - finishInSequentialScope: Bool = false + finishInSequentialScope: Bool = false, + configuredRegions: ConfiguredRegions? = nil ) { self.finishInSequentialScope = finishInSequentialScope + self.configuredRegions = configuredRegions } } diff --git a/Sources/SwiftLexicalLookup/LookupName.swift b/Sources/SwiftLexicalLookup/LookupName.swift index 4530473d3bb..28cd320f00e 100644 --- a/Sources/SwiftLexicalLookup/LookupName.swift +++ b/Sources/SwiftLexicalLookup/LookupName.swift @@ -104,6 +104,10 @@ import SwiftSyntax case .variableDecl(let variableDecl): return variableDecl.bindings.first?.accessorBlock?.positionAfterSkippingLeadingTrivia ?? variableDecl.endPosition + case .accessorDecl(let accessorDecl): + return accessorDecl.accessorSpecifier.positionAfterSkippingLeadingTrivia + case .deinitializerDecl(let deinitializerDecl): + return deinitializerDecl.deinitKeyword.positionAfterSkippingLeadingTrivia default: return declSyntax.positionAfterSkippingLeadingTrivia } @@ -111,13 +115,15 @@ import SwiftSyntax switch Syntax(declSyntax).as(SyntaxEnum.self) { case .protocolDecl(let protocolDecl): return protocolDecl.name.positionAfterSkippingLeadingTrivia + case .extensionDecl(let extensionDecl): + return extensionDecl.extensionKeyword.positionAfterSkippingLeadingTrivia default: return declSyntax.positionAfterSkippingLeadingTrivia } + case .newValue(let accessorDecl), .oldValue(let accessorDecl): + return accessorDecl.accessorSpecifier.positionAfterSkippingLeadingTrivia case .error(let catchClause): return catchClause.catchItems.positionAfterSkippingLeadingTrivia - default: - return syntax.positionAfterSkippingLeadingTrivia } } } @@ -276,7 +282,11 @@ import SwiftSyntax identifiable: IdentifiableSyntax, accessibleAfter: AbsolutePosition? = nil ) -> [LookupName] { - [.identifier(identifiable, accessibleAfter: accessibleAfter)] + if case .wildcard = identifiable.identifier.tokenKind { + return [] + } + + return [.identifier(identifiable, accessibleAfter: accessibleAfter)] } /// Extracts name introduced by `NamedDeclSyntax` node. diff --git a/Sources/SwiftLexicalLookup/Scopes/FunctionScopeSyntax.swift b/Sources/SwiftLexicalLookup/Scopes/FunctionScopeSyntax.swift index 246a8df9cec..b7453f0f851 100644 --- a/Sources/SwiftLexicalLookup/Scopes/FunctionScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/Scopes/FunctionScopeSyntax.swift @@ -22,7 +22,7 @@ extension FunctionScopeSyntax { @_spi(Experimental) public var defaultIntroducedNames: [LookupName] { signature.parameterClause.parameters.flatMap { parameter in LookupName.getNames(from: parameter) - } + (parentScope?.is(MemberBlockSyntax.self) ?? false ? [.implicit(.self(self))] : []) + } + (isMember ? [.implicit(.self(self))] : []) } /// Lookup results from this function scope. diff --git a/Sources/SwiftLexicalLookup/Scopes/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/Scopes/ScopeImplementations.swift index 81e8e649929..882c6c69f9e 100644 --- a/Sources/SwiftLexicalLookup/Scopes/ScopeImplementations.swift +++ b/Sources/SwiftLexicalLookup/Scopes/ScopeImplementations.swift @@ -10,6 +10,7 @@ // //===----------------------------------------------------------------------===// +import SwiftIfConfig import SwiftSyntax @_spi(Experimental) extension SyntaxProtocol { @@ -391,7 +392,11 @@ import SwiftSyntax } @_spi(Experimental) extension ExtensionDeclSyntax: LookInMembersScopeSyntax { @_spi(Experimental) public var lookupMembersPosition: AbsolutePosition { - extendedType.position + if let memberType = extendedType.as(MemberTypeSyntax.self) { + return memberType.name.positionAfterSkippingLeadingTrivia + } + + return extendedType.positionAfterSkippingLeadingTrivia } @_spi(Experimental) public var defaultIntroducedNames: [LookupName] { @@ -420,8 +425,8 @@ import SwiftSyntax + defaultLookupImplementation(identifier, at: lookUpPosition, with: config, propagateToParent: false) + [.lookInMembers(self)] + lookupInParent(identifier, at: lookUpPosition, with: config) - } else if !extendedType.range.contains(lookUpPosition) && genericWhereClause != nil { - if inRightTypeOrSameTypeRequirement(lookUpPosition) { + } else if !extendedType.range.contains(lookUpPosition), let genericWhereClause { + if genericWhereClause.range.contains(lookUpPosition) { return [.lookInGenericParametersOfExtendedType(self)] + [.lookInMembers(self)] + defaultLookupImplementation(identifier, at: lookUpPosition, with: config) } @@ -433,23 +438,6 @@ import SwiftSyntax return [.lookInGenericParametersOfExtendedType(self)] + lookupInParent(identifier, at: lookUpPosition, with: config) } - - /// Returns `true` if `checkedPosition` is a right type of a - /// conformance requirement or inside a same type requirement. - private func inRightTypeOrSameTypeRequirement( - _ checkedPosition: AbsolutePosition - ) -> Bool { - genericWhereClause?.requirements.contains { elem in - switch Syntax(elem.requirement).as(SyntaxEnum.self) { - case .conformanceRequirement(let conformanceRequirement): - return conformanceRequirement.rightType.range.contains(checkedPosition) - case .sameTypeRequirement(let sameTypeRequirement): - return sameTypeRequirement.range.contains(checkedPosition) - default: - return false - } - } ?? false - } } @_spi(Experimental) extension AccessorDeclSyntax: ScopeSyntax { @@ -491,7 +479,7 @@ import SwiftSyntax let implicitSelf: [LookupName] = [.implicit(.self(self))] .filter { name in - checkIdentifier(identifier, refersTo: name, at: lookUpPosition) + checkIdentifier(identifier, refersTo: name, at: lookUpPosition) && !attributes.range.contains(lookUpPosition) } return defaultLookupImplementation( @@ -510,15 +498,40 @@ import SwiftSyntax } @_spi(Experimental) extension CatchClauseSyntax: ScopeSyntax { - /// Implicit `error` when there are no catch items. + /// Name introduced by the catch clause. + /// + /// `defaultIntroducedNames` contains implicit `error` name if + /// no names are declared in catch items and they don't contain any expression patterns. + /// Otherwise, `defaultIntroducedNames` contains names introduced by the clause. + /// + /// ### Example + /// ```swift + /// do { + /// // ... + /// } catch SomeError, .x(let a) { + /// // <-- lookup here, result: [a] + /// } catch .x(let a) { + /// // <-- lookup here, result: [a] + /// } catch SomeError { + /// // <-- lookup here, result: [empty] + /// } catch { + /// // <-- lookup here, result: implicit(error) + /// } + /// ``` @_spi(Experimental) public var defaultIntroducedNames: [LookupName] { + var containsExpressionSyntax = false + let extractedNames = catchItems.flatMap { item in guard let pattern = item.pattern else { return [LookupName]() } + if !containsExpressionSyntax && pattern.is(ExpressionPatternSyntax.self) { + containsExpressionSyntax = true + } + return LookupName.getNames(from: pattern) } - return extractedNames.isEmpty ? [.implicit(.error(self))] : extractedNames + return extractedNames.isEmpty && !containsExpressionSyntax ? [.implicit(.error(self))] : extractedNames } @_spi(Experimental) public var scopeDebugName: String { @@ -594,14 +607,27 @@ import SwiftSyntax checkIdentifier(identifier, refersTo: name, at: lookUpPosition) } - return sequentialLookup( - in: statements, - identifier, - at: lookUpPosition, - with: config, - propagateToParent: false - ) + LookupResult.getResultArray(for: self, withNames: filteredNamesFromLabel) - + (config.finishInSequentialScope ? [] : lookupInParent(identifier, at: lookUpPosition, with: config)) + if label.range.contains(lookUpPosition) { + return config.finishInSequentialScope ? [] : lookupInParent(identifier, at: lookUpPosition, with: config) + } else if config.finishInSequentialScope { + return sequentialLookup( + in: statements, + identifier, + at: lookUpPosition, + with: config, + propagateToParent: false + ) + } else { + return sequentialLookup( + in: statements, + identifier, + at: lookUpPosition, + with: config, + propagateToParent: false + ) + + LookupResult.getResultArray(for: self, withNames: filteredNamesFromLabel) + + lookupInParent(identifier, at: lookUpPosition, with: config) + } } } @@ -697,6 +723,18 @@ import SwiftSyntax } } +@_spi(Experimental) extension MacroDeclSyntax: WithGenericParametersScopeSyntax { + public var defaultIntroducedNames: [LookupName] { + signature.parameterClause.parameters.flatMap { parameter in + LookupName.getNames(from: parameter) + } + } + + @_spi(Experimental) public var scopeDebugName: String { + "MacroDeclScope" + } +} + @_spi(Experimental) extension SubscriptDeclSyntax: WithGenericParametersScopeSyntax, CanInterleaveResultsLaterScopeSyntax { /// Parameters introduced by this subscript and possibly `self` keyword. @@ -758,7 +796,7 @@ extension SubscriptDeclSyntax: WithGenericParametersScopeSyntax, CanInterleaveRe ) -> [LookupResult] { var thisScopeResults: [LookupResult] = [] - if !parameterClause.range.contains(lookUpPosition) && !returnClause.range.contains(lookUpPosition) { + if accessorBlock?.range.contains(lookUpPosition) ?? false { thisScopeResults = defaultLookupImplementation( identifier, at: position, @@ -866,8 +904,6 @@ extension SubscriptDeclSyntax: WithGenericParametersScopeSyntax, CanInterleaveRe with config: LookupConfig ) -> [LookupResult] { if bindings.first?.accessorBlock?.range.contains(lookUpPosition) ?? false { - let isMember = parentScope?.is(MemberBlockSyntax.self) ?? false - return defaultLookupImplementation( in: (isMember ? [.implicit(.self(self))] : LookupName.getNames(from: self)), identifier, @@ -887,10 +923,99 @@ extension SubscriptDeclSyntax: WithGenericParametersScopeSyntax, CanInterleaveRe with config: LookupConfig, resultsToInterleave: [LookupResult] ) -> [LookupResult] { - guard parentScope?.is(MemberBlockSyntax.self) ?? false else { + guard isMember else { return lookup(identifier, at: lookUpPosition, with: config) } return resultsToInterleave + lookupInParent(identifier, at: lookUpPosition, with: config) } } + +@_spi(Experimental) extension DeinitializerDeclSyntax: ScopeSyntax { + @_spi(Experimental) public var defaultIntroducedNames: [LookupName] { + [.implicit(.self(self))] + } + + @_spi(Experimental) public var scopeDebugName: String { + "DeinitializerScope" + } +} + +@_spi(Experimental) extension IfConfigDeclSyntax: IntroducingToSequentialParentScopeSyntax, SequentialScopeSyntax { + /// Names from all clauses. + var namesIntroducedToSequentialParent: [LookupName] { + clauses.flatMap { clause in + clause.elements.flatMap { element in + LookupName.getNames(from: element, accessibleAfter: element.endPosition) + } ?? [] + } + } + + /// Performs sequential lookup in the active clause. + /// Active clause is determined by the `BuildConfiguration` + /// inside `config`. If not specified, defaults to the `#else` clause. + func lookupFromSequentialParent( + _ identifier: Identifier?, + at lookUpPosition: AbsolutePosition, + with config: LookupConfig + ) -> [LookupResult] { + let clause: IfConfigClauseSyntax? + + if let configuredRegions = config.configuredRegions { + clause = configuredRegions.activeClause(for: self) + } else { + clause = + clauses + .first { clause in + clause.poundKeyword.tokenKind == .poundElse + } + } + + return sequentialLookup( + in: clause?.elements?.as(CodeBlockItemListSyntax.self) ?? [], + identifier, + at: lookUpPosition, + with: config, + ignoreNamedDecl: true, + propagateToParent: false + ) + } + + /// Returns all `NamedDeclSyntax` nodes in the active clause specified + /// by `BuildConfiguration` in `config` from bottom-most to top-most. + func getNamedDecls(for config: LookupConfig) -> [NamedDeclSyntax] { + let clause: IfConfigClauseSyntax? + + if let configuredRegions = config.configuredRegions { + clause = configuredRegions.activeClause(for: self) + } else { + clause = + clauses + .first { clause in + clause.poundKeyword.tokenKind == .poundElse + } + } + + guard let clauseElements = clause?.elements?.as(CodeBlockItemListSyntax.self) else { return [] } + + var result: [NamedDeclSyntax] = [] + + for elem in clauseElements.reversed() { + if let namedDecl = elem.item.asProtocol(NamedDeclSyntax.self) { + result.append(namedDecl) + } else if let ifConfigDecl = elem.item.as(IfConfigDeclSyntax.self) { + result += ifConfigDecl.getNamedDecls(for: config) + } + } + + return result + } + + @_spi(Experimental) public var defaultIntroducedNames: [LookupName] { + [] + } + + @_spi(Experimental) public var scopeDebugName: String { + "IfConfigScope" + } +} diff --git a/Sources/SwiftLexicalLookup/Scopes/ScopeSyntax.swift b/Sources/SwiftLexicalLookup/Scopes/ScopeSyntax.swift index 64e2bc50d65..ab67dd07f15 100644 --- a/Sources/SwiftLexicalLookup/Scopes/ScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/Scopes/ScopeSyntax.swift @@ -122,8 +122,27 @@ extension SyntaxProtocol { introducedName.isAccessible(at: lookUpPosition) && introducedName.refersTo(identifier) } + var isMember: Bool { + if parentScope?.is(MemberBlockSyntax.self) ?? false { + return true + } else if let parentIfConfig = parentScope?.as(IfConfigDeclSyntax.self) { + return parentIfConfig.isMember + } else { + return false + } + } + /// Debug description of this scope. @_spi(Experimental) public var scopeDebugDescription: String { scopeDebugName + " " + debugLineWithColumnDescription } + + /// Hierarchy of scopes starting from this scope. + @_spi(Experimental) public var scopeDebugHierarchyDescription: String { + if let parentScope = parentScope { + return parentScope.scopeDebugHierarchyDescription + " <-- " + scopeDebugDescription + } else { + return scopeDebugDescription + } + } } diff --git a/Sources/SwiftLexicalLookup/Scopes/SequentialScopeSyntax.swift b/Sources/SwiftLexicalLookup/Scopes/SequentialScopeSyntax.swift index a4fd0b0850e..cca7f8e5e95 100644 --- a/Sources/SwiftLexicalLookup/Scopes/SequentialScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/Scopes/SequentialScopeSyntax.swift @@ -43,6 +43,7 @@ extension SequentialScopeSyntax { _ identifier: Identifier?, at lookUpPosition: AbsolutePosition, with config: LookupConfig, + ignoreNamedDecl: Bool = false, propagateToParent: Bool = true ) -> [LookupResult] { // Sequential scope needs to ensure all type declarations are @@ -55,27 +56,24 @@ extension SequentialScopeSyntax { // as we need to partition them based on results // obtained from IntroducingToSequentialParentScopeSyntax var currentChunk: [LookupName] = [] - var itemsWithoutNamedDecl: [CodeBlockItemSyntax] = [] + // During first iteration, the algorithm collects all named + // decls from the code block and (nested) active clauses + // of if config declarations. After the first pass, performs + // name matching on these and appends as a separate result to the results. + var collectedNamedDecls: [NamedDeclSyntax] = [] - for codeBlockItem in codeBlockItems { - if Syntax(codeBlockItem.item).isProtocol(NamedDeclSyntax.self) { - currentChunk += LookupName.getNames( - from: codeBlockItem.item, - accessibleAfter: codeBlockItem.endPosition - ).filter { introducedName in - checkIdentifier(identifier, refersTo: introducedName, at: lookUpPosition) - } - } else { - itemsWithoutNamedDecl.append(codeBlockItem) - } - } + for codeBlockItem in codeBlockItems.reversed() { + if let namedDecl = codeBlockItem.item.asProtocol(NamedDeclSyntax.self) { + guard !ignoreNamedDecl else { continue } - if !currentChunk.isEmpty { - results.append(LookupResult.getResult(for: self, withNames: currentChunk)) - currentChunk = [] - } + collectedNamedDecls.append(namedDecl) + continue + } else if let ifConfigDecl = codeBlockItem.item.as(IfConfigDeclSyntax.self), + !ignoreNamedDecl + { + collectedNamedDecls += ifConfigDecl.getNamedDecls(for: config) + } - for codeBlockItem in itemsWithoutNamedDecl { if let introducingToParentScope = Syntax(codeBlockItem.item).asProtocol(SyntaxProtocol.self) as? IntroducingToSequentialParentScopeSyntax { @@ -91,7 +89,7 @@ extension SequentialScopeSyntax { // If there are some names collected, create a new result for this scope. if !currentChunk.isEmpty { - results.append(LookupResult.getResult(for: self, withNames: currentChunk.reversed())) + results.append(LookupResult.getResult(for: self, withNames: currentChunk)) currentChunk = [] } @@ -100,8 +98,8 @@ extension SequentialScopeSyntax { // Extract new names from encountered node. currentChunk += LookupName.getNames( from: codeBlockItem.item, - accessibleAfter: codeBlockItem.endPosition - ).reversed().filter { introducedName in + accessibleAfter: codeBlockItem.item.endPosition + ).filter { introducedName in checkIdentifier(identifier, refersTo: introducedName, at: lookUpPosition) } } @@ -109,10 +107,23 @@ extension SequentialScopeSyntax { // If there are some names collected, create a new result for this scope. if !currentChunk.isEmpty { - results.append(LookupResult.getResult(for: self, withNames: currentChunk.reversed())) + results.append(LookupResult.getResult(for: self, withNames: currentChunk)) + currentChunk = [] } - return results.reversed() + // Filter named decls to be appended to the results. + for namedDecl in collectedNamedDecls.reversed() { + currentChunk += LookupName.getNames( + from: namedDecl, + accessibleAfter: namedDecl.endPosition + ).filter { introducedName in + checkIdentifier(identifier, refersTo: introducedName, at: lookUpPosition) + } + } + + results += LookupResult.getResultArray(for: self, withNames: currentChunk) + + return results + (config.finishInSequentialScope || !propagateToParent ? [] : lookupInParent(identifier, at: lookUpPosition, with: config)) } diff --git a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift index 61e30abf590..6c4f937c75e 100644 --- a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift +++ b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift @@ -705,10 +705,7 @@ final class testNameLookup: XCTestCase { """, references: [ "2️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: [NameExpectation.identifier("1️⃣")])], - "3️⃣": [ - .fromScope(CatchClauseSyntax.self, expectedNames: [NameExpectation.implicit(.error("6️⃣"))]), - .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️⃣")]), @@ -1012,4 +1009,82 @@ final class testNameLookup: XCTestCase { ) ) } + + func testDefaultIfConfigBehavior() { + assertLexicalNameLookup( + source: """ + func foo() { + let 0️⃣a = 1️⃣x + + #if DEBUG + + let b = 2️⃣x + class A {} + + #else + + let 3️⃣c = 4️⃣x + 5️⃣class B {} + + #if DEBUG + + let d = 6️⃣x + class C {} + + #else + + let 7️⃣e = 8️⃣x + 9️⃣class D {} + + #endif + + #endif + + 🔟class E {} + } + """, + references: [ + "1️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["5️⃣", "9️⃣", "🔟"])], + "2️⃣": [ + .fromScope(CodeBlockSyntax.self, expectedNames: ["0️⃣"]), + .fromScope(CodeBlockSyntax.self, expectedNames: ["5️⃣", "9️⃣", "🔟"]), + ], + "4️⃣": [ + .fromScope(CodeBlockSyntax.self, expectedNames: ["0️⃣"]), + .fromScope(CodeBlockSyntax.self, expectedNames: ["5️⃣", "9️⃣", "🔟"]), + ], + "6️⃣": [ + .fromScope(IfConfigDeclSyntax.self, expectedNames: ["3️⃣"]), + .fromScope(CodeBlockSyntax.self, expectedNames: ["0️⃣"]), + .fromScope(CodeBlockSyntax.self, expectedNames: ["5️⃣", "9️⃣", "🔟"]), + ], + "8️⃣": [ + .fromScope(IfConfigDeclSyntax.self, expectedNames: ["3️⃣"]), + .fromScope(CodeBlockSyntax.self, expectedNames: ["0️⃣"]), + .fromScope(CodeBlockSyntax.self, expectedNames: ["5️⃣", "9️⃣", "🔟"]), + ], + ], + useNilAsTheParameter: true + ) + } + + func testMacroDeclaration() { + let sameResult: [ResultExpectation] = [ + .fromScope(MacroDeclSyntax.self, expectedNames: ["1️⃣", "3️⃣"]), + .fromScope(GenericParameterClauseSyntax.self, expectedNames: ["0️⃣"]), + ] + + assertLexicalNameLookup( + source: """ + public macro externalMacro<0️⃣T>(1️⃣module: 2️⃣String, 3️⃣type: 4️⃣String) -> 5️⃣T = 6️⃣X + """, + references: [ + "2️⃣": sameResult, + "4️⃣": sameResult, + "5️⃣": sameResult, + "6️⃣": sameResult, + ], + useNilAsTheParameter: true + ) + } }