Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d108d0f
Create `ImportContent` which can recursively collect imports.
fummicc1 Nov 30, 2025
29813a1
Change `ImportMap` type to make it more structured.
fummicc1 Nov 30, 2025
f438d08
Add Clause type to IfMacroModel.
fummicc1 Nov 30, 2025
8f625f0
Parse IfConfigDeclSyntax recursively and extract imports inside them …
fummicc1 Nov 30, 2025
5bd510a
Render all entities of IfMacroModel.
fummicc1 Nov 30, 2025
49b62cb
Update import rendering logic to consider both top-level imports and …
fummicc1 Nov 30, 2025
c473455
Update tests around IfMacro
fummicc1 Nov 30, 2025
4d0a100
Add public access modifier to fix build error.
fummicc1 Nov 30, 2025
b5e8319
Add testcases for IfMacro.
fummicc1 Nov 30, 2025
d9bc429
Add testcases for nested macro and duplicatedImports in macro.
fummicc1 Nov 30, 2025
976c215
Merge branch 'master' into feature/if-else-directive
fummicc1 Dec 1, 2025
46a71d6
Delete `ParsedImports` type, and use `[ImportContent]`
fummicc1 Dec 5, 2025
4835eef
Remove public acl from SourceParser and related types.
fummicc1 Dec 18, 2025
d2b8d09
Remove unnecessary changes
sidepelican Dec 26, 2025
8a084a4
Change from function-focused to structure-focused modeling
sidepelican Dec 26, 2025
37babbc
Combine duplicate processes
sidepelican Dec 26, 2025
7e6ca03
Fix redundant logic
sidepelican Dec 26, 2025
edec992
The `prefix` `suffix` became unnecessary with the introduction of Con…
sidepelican Dec 26, 2025
f040ca5
remove unused propertry
sidepelican Dec 26, 2025
0f99d8c
Integrate separated logic between topLevel and nested
sidepelican Dec 26, 2025
8076b21
Add test and handle @testable in nested import
sidepelican Dec 26, 2025
cbdf6a8
Remove unused property
sidepelican Dec 26, 2025
668097c
stop using legacy style coding
sidepelican Dec 26, 2025
74a2589
Fix waning
sidepelican Dec 26, 2025
bd332f8
Remove unnecessary changes
sidepelican Dec 26, 2025
ee70651
Merge branch 'feature/if-else-directive' into fix328
fummicc1 Jan 29, 2026
d849dcb
Merge pull request #3 from sidepelican/fix328
fummicc1 Jan 29, 2026
87d1fef
Rename ambiguous name
fummicc1 Jan 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions Sources/MockoloFramework/Models/ConditionalImportBlock.swift
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[self review]

Except for Import, all nodes within IfDeclConfigSyntax are stored in IfMacroModel's entities.
However, import statement is not represented as one of them, rather it is represented as ImportContent.

Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// Copyright (c) 2018. Uber Technologies
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

/// Represents import content: either a simple import statement or a nested conditional block
public indirect enum ImportContent {
case simple(Import)
case conditional(ConditionalImportBlock)
}

/// Represents a conditional import block (#if/#elseif/#else/#endif)
public struct ConditionalImportBlock {
/// Represents a single clause in a conditional import block
public struct Clause {
public let type: ClauseType
public let condition: String? // nil for #else
public var contents: [ImportContent]

public init(type: ClauseType, condition: String?, contents: [ImportContent]) {
self.type = type
self.condition = condition
self.contents = contents
}
}

public let clauses: [Clause]
public let offset: Int64

public init(clauses: [Clause], offset: Int64) {
self.clauses = clauses
self.offset = offset
}
}
63 changes: 48 additions & 15 deletions Sources/MockoloFramework/Models/IfMacroModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,36 +14,69 @@
// limitations under the License.
//

/// Represents the type of a clause in an #if/#elseif/#else block
public enum ClauseType: Comparable {
case `if`
case elseif(order: Int)
case `else`

// Comparable implementation: if < elseif(0) < elseif(1) < ... < else
public static func < (lhs: ClauseType, rhs: ClauseType) -> Bool {
switch (lhs, rhs) {
case (.if, .elseif), (.if, .else), (.elseif, .else):
true
case (.elseif(let l), .elseif(let r)):
l < r
default:
false
}
}
}

final class IfMacroModel: Model {
let name: String
/// Represents a single clause in a conditional compilation block
struct Clause {
let type: ClauseType
let condition: String? // nil for #else
let entities: [(String, Model)]
}
Comment on lines 34 to 38
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[self review]

Now entities property is moved from IfMacroModel to IfMacroModel.Clause so that we can know which compiler directive stores them.

Because IfMacroModel self is also Model, this recursively stores.


let clauses: [Clause]
let offset: Int64
let entities: [(String, Model)]

var modelType: ModelType {
return .macro
.macro
}

var name: String {
clauses.first?.condition ?? ""
}

var fullName: String {
return entities.map {$0.0}.joined(separator: "_")
clauses.flatMap(\.entities).map { $0.0 }.joined(separator: "_")
}

init(name: String,
offset: Int64,
entities: [(String, Model)]) {
self.name = name
self.entities = entities

/// Creates an IfMacroModel with multiple clauses
init(clauses: [Clause], offset: Int64) {
self.clauses = clauses
self.offset = offset
}


/// Initializer for simple #if blocks
convenience init(name: String,
offset: Int64,
entities: [(String, Model)]) {
let clause = Clause(type: .if, condition: name, entities: entities)
self.init(clauses: [clause], offset: offset)
}

func render(
context: RenderContext,
arguments: GenerationArguments
) -> String? {
return applyMacroTemplate(
name: name,
applyMacroTemplate(
context: context,
arguments: arguments,
entities: entities
arguments: arguments
)
}
}
22 changes: 11 additions & 11 deletions Sources/MockoloFramework/Models/Import.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
//

/// A structure defining an "import" statement parsed by `Generator`, including various modifiers.
struct Import: CustomStringConvertible {
public struct Import: CustomStringConvertible {

/// The access level of the import
enum ACL: String {
public enum ACL: String {
case `private`
case `fileprivate`
case `internal`
Expand All @@ -37,18 +37,18 @@ struct Import: CustomStringConvertible {
}

/// A modifier that precedes the "import" keyword. ACL and "@testable" are mutually exclusive.
enum Modifier: RawRepresentable {
public enum Modifier: RawRepresentable {
case acl(ACL)
case testable

var rawValue: String {
public var rawValue: String {
switch self {
case .acl(let acl): acl.rawValue
case .testable: "@testable"
}
}

init?(rawValue: String) {
public init?(rawValue: String) {
if rawValue == "@testable" {
self = .testable
} else if let acl = ACL(rawValue: rawValue) {
Expand All @@ -60,18 +60,18 @@ struct Import: CustomStringConvertible {
}

/// Name of the module
let moduleName: String
public let moduleName: String

/// A modifier preceding the "import" keyword (e.g. public, internal, @testable)
var modifier: Modifier?
public var modifier: Modifier?

/// An opaque string preceding the entire import statement (typically `#if FOO\n` for nested macro support)
let prefix: String?
public let prefix: String?

/// An opaque string following the entire import statement (typically `\n#endif` for nested macro support)
let suffix: String?
public let suffix: String?

var description: String {
public var description: String {
let line: String
if let modifier {
line = "\(modifier.rawValue) import \(moduleName)"
Expand All @@ -81,7 +81,7 @@ struct Import: CustomStringConvertible {
return [prefix, line, suffix].compactMap { $0 }.joined()
}

init(
public init(
moduleName: String,
modifier: Modifier? = nil,
prefix: String? = nil,
Expand Down
15 changes: 14 additions & 1 deletion Sources/MockoloFramework/Models/ParsedEntity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,20 @@ struct GenerationArguments {
)
}

public typealias ImportMap = [String: [String: [String]]]
/// Structured import data parsed from a source file
public struct ParsedImports {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ParsedImports and ConditionalImportBlock appear to be representing the same thing.
Wouldn't it make sense to combine them if ClauseType includes a .topLevel ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ParsedImports can be removed, but I don't think adding topLevel case to ClauseType is better bacause topLevel is not a part of ClauseType.
I deleted ParsedImports in this commit 46a71d6.

Could you check if it gets better?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleImports has become more complex, but structurally it's improved—so the issue might be in handleImports. please give me some time to understand this.

/// Top-level imports without conditional compilation
public var topLevel: [Import]
/// Conditional import blocks (#if/#elseif/#else/#endif)
public var conditional: [ConditionalImportBlock]

public init(topLevel: [Import] = [], conditional: [ConditionalImportBlock] = []) {
self.topLevel = topLevel
self.conditional = conditional
}
}

public typealias ImportMap = [String: ParsedImports]

/// Metadata for a type being mocked
public final class Entity {
Expand Down
138 changes: 94 additions & 44 deletions Sources/MockoloFramework/Operations/ImportsHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,63 +22,113 @@ func handleImports(pathToImportsMap: ImportMap,
testableImports: [String]?,
relevantPaths: [String]) -> String {

var imports = [String: [Import]]()
let defaultKey = ""
if imports[defaultKey] == nil {
imports[defaultKey] = []
}
var topLevelImports: [Import] = []
var conditionalBlocks: [ConditionalImportBlock] = []

for (path, importMap) in pathToImportsMap {
// 1. Collect imports from all relevant files
for (path, parsedImports) in pathToImportsMap {
guard relevantPaths.contains(path) else { continue }
for (k, v) in importMap {
if imports[k] == nil {
imports[k] = []
}

if let ex = excludeImports {
let filtered = v.filter{ !ex.contains($0.moduleNameInImport) }
imports[k]?.append(contentsOf: filtered.compactMap { Import(line: $0) })
} else {
imports[k]?.append(contentsOf: v.compactMap { Import(line: $0) })
}
}
// Collect top-level imports
topLevelImports.append(contentsOf: parsedImports.topLevel)

// Collect conditional blocks
conditionalBlocks.append(contentsOf: parsedImports.conditional)
}

// 2. Apply excludeImports filter to top-level imports
if let excludes = excludeImports {
topLevelImports = topLevelImports.filter { !excludes.contains($0.moduleName) }
}

// 3. Add custom imports
if let customImports = customImports {
imports[defaultKey]?.append(contentsOf: customImports.compactMap { Import(moduleName: $0) })
topLevelImports.append(contentsOf: customImports.map { Import(moduleName: $0) })
}

// 4. Resolve duplicates in top-level imports
var resolvedTopLevel = topLevelImports.resolved()

// 5. Apply testableImports modifier
if let testableImportNames = testableImports, !testableImportNames.isEmpty {
let (passthroughImports, candidateImports) = resolvedTopLevel.partitioned(by: { testableImportNames.contains($0.moduleName) })
let mappedImports = candidateImports.map(\.asTestable)
let newImports: [Import] = testableImportNames.compactMap { name in
guard !mappedImports.contains(where: { $0.moduleName == name }) else { return nil }
return Import(moduleName: name, modifier: .testable)
}
resolvedTopLevel = (passthroughImports + mappedImports + newImports).resolved()
}

// 6. Sort conditional blocks by offset (file appearance order)
let sortedBlocks = conditionalBlocks.sorted(by: { $0.offset < $1.offset })

// 7. Generate output
var lines: [String] = []

if !resolvedTopLevel.isEmpty {
lines.append(resolvedTopLevel.lines())
}

var sortedImports = [String: [Import]]()
for (k, v) in imports {
sortedImports[k] = v.resolved()
for block in sortedBlocks {
lines.append(renderConditionalBlock(block, excludeImports: excludeImports))
}

if let existingSet = sortedImports[defaultKey] {
if let testableImportNames = testableImports, !testableImportNames.isEmpty {
let (passthroughImports, candidateImports) = existingSet.partitioned(by: { testableImportNames.contains($0.moduleName) })
let mappedImports = candidateImports.map(\.asTestable)
let newImports: [Import] = testableImportNames.compactMap { name in
guard !mappedImports.contains(where: { $0.moduleName == name }) else { return nil }
return Import(moduleName: name, modifier: .testable)
return lines.joined(separator: "\n")
}

/// Recursively renders a ConditionalImportBlock
private func renderConditionalBlock(_ block: ConditionalImportBlock, excludeImports: [String]?) -> String {
var result = ""

for (index, clause) in block.clauses.enumerated() {
// Render directive line
switch clause.type {
case .if:
result += "#if \(clause.condition ?? "")\n"
case .elseif:
result += "#elseif \(clause.condition ?? "")\n"
case .else:
result += "#else\n"
}

// Render contents of this clause
var clauseLines: [String] = []
var simpleImports: [Import] = []

for content in clause.contents {
switch content {
case .simple(let imp):
// Filter excluded imports
if let excludes = excludeImports, excludes.contains(imp.moduleName) {
continue
}
simpleImports.append(imp)
case .conditional(let nestedBlock):
// First output accumulated simple imports
if !simpleImports.isEmpty {
clauseLines.append(simpleImports.resolved().lines())
simpleImports = []
}
// Recursively render nested block
clauseLines.append(renderConditionalBlock(nestedBlock, excludeImports: excludeImports))
}
sortedImports[defaultKey] = (passthroughImports + mappedImports + newImports).resolved()
}
}

let sortedKeys = sortedImports.keys.sorted()
let importsStr = sortedKeys.map { k in
let v = sortedImports[k]
let lines = v?.lines() ?? ""
if k.isEmpty {
return lines
} else {
return """
#if \(k)
\(lines)
#endif
"""
// Output remaining simple imports
if !simpleImports.isEmpty {
clauseLines.append(simpleImports.resolved().lines())
}

let clauseContent = clauseLines.joined(separator: "\n")
if !clauseContent.isEmpty {
result += clauseContent
if index < block.clauses.count - 1 {
result += "\n"
}
}
}.joined(separator: "\n")
}

return importsStr
result += "\n#endif"
return result
}
Loading