diff --git a/Sources/SWBCore/CapturedBuildInfo.swift b/Sources/SWBCore/CapturedBuildInfo.swift index cdeebdbf..f35e5d09 100644 --- a/Sources/SWBCore/CapturedBuildInfo.swift +++ b/Sources/SWBCore/CapturedBuildInfo.swift @@ -97,16 +97,16 @@ public struct CapturedBuildInfo: PropertyListItemConvertible, Sendable { let settings = settingsPerTarget[configuredTarget] let targetConstructionComponents = settings?.constructionComponents let projectXcconfigSettings: CapturedBuildSettingsTableInfo = { - let settings = targetConstructionComponents?.projectXcconfigSettings?.capturedRepresentation ?? [:] - return CapturedBuildSettingsTableInfo(name: CapturedBuildSettingsTableInfo.Name.projectXcconfig, path: targetConstructionComponents?.projectXcconfigPath, settings: settings) + let settings = targetConstructionComponents?.projectXcconfig?.settings.capturedRepresentation ?? [:] + return CapturedBuildSettingsTableInfo(name: CapturedBuildSettingsTableInfo.Name.projectXcconfig, path: targetConstructionComponents?.projectXcconfig?.path, settings: settings) }() let projectSettings: CapturedBuildSettingsTableInfo = { let settings = targetConstructionComponents?.projectSettings?.capturedRepresentation ?? [:] return CapturedBuildSettingsTableInfo(name: CapturedBuildSettingsTableInfo.Name.project, path: nil, settings: settings) }() let targetXcconfigSettings: CapturedBuildSettingsTableInfo = { - let settings = targetConstructionComponents?.targetXcconfigSettings?.capturedRepresentation ?? [:] - return CapturedBuildSettingsTableInfo(name: CapturedBuildSettingsTableInfo.Name.targetXcconfig, path: targetConstructionComponents?.targetXcconfigPath, settings: settings) + let settings = targetConstructionComponents?.targetXcconfig?.settings.capturedRepresentation ?? [:] + return CapturedBuildSettingsTableInfo(name: CapturedBuildSettingsTableInfo.Name.targetXcconfig, path: targetConstructionComponents?.targetXcconfig?.path, settings: settings) }() let targetSettings: CapturedBuildSettingsTableInfo = { let settings = targetConstructionComponents?.targetSettings?.capturedRepresentation ?? [:] diff --git a/Sources/SWBCore/Dependencies.swift b/Sources/SWBCore/Dependencies.swift index 92c30391..94455993 100644 --- a/Sources/SWBCore/Dependencies.swift +++ b/Sources/SWBCore/Dependencies.swift @@ -196,11 +196,11 @@ public struct ModuleDependenciesContext: Sendable, SerializableCodable { { self.init(sourceRange: .init(path: location.path, startLine: location.endLine, startColumn: location.endColumn, endLine: location.endLine, endColumn: location.endColumn), modificationStyle: .appendToExistingAssignment) } - else if let path = settings.constructionComponents.targetXcconfigPath { - self.init(sourceRange: .init(path: path, startLine: .max, startColumn: .max, endLine: .max, endColumn: .max), modificationStyle: .insertNewAssignment(targetNameCondition: nil)) + else if let xcconfig = settings.constructionComponents.targetXcconfig { + self.init(sourceRange: .init(path: xcconfig.path, startLine: xcconfig.finalLineNumber, startColumn: xcconfig.finalColumnNumber, endLine: xcconfig.finalLineNumber, endColumn: xcconfig.finalColumnNumber), modificationStyle: .insertNewAssignment(targetNameCondition: nil)) } - else if let path = settings.constructionComponents.projectXcconfigPath { - self.init(sourceRange: .init(path: path, startLine: .max, startColumn: .max, endLine: .max, endColumn: .max), modificationStyle: .insertNewAssignment(targetNameCondition: target.name)) + else if let xcconfig = settings.constructionComponents.projectXcconfig { + self.init(sourceRange: .init(path: xcconfig.path, startLine: xcconfig.finalLineNumber, startColumn: xcconfig.finalColumnNumber, endLine: xcconfig.finalLineNumber, endColumn: xcconfig.finalColumnNumber), modificationStyle: .insertNewAssignment(targetNameCondition: target.name)) } else { return nil @@ -357,11 +357,11 @@ public struct HeaderDependenciesContext: Sendable, SerializableCodable { { self.init(sourceRange: .init(path: location.path, startLine: location.endLine, startColumn: location.endColumn, endLine: location.endLine, endColumn: location.endColumn), modificationStyle: .appendToExistingAssignment) } - else if let path = settings.constructionComponents.targetXcconfigPath { - self.init(sourceRange: .init(path: path, startLine: .max, startColumn: .max, endLine: .max, endColumn: .max), modificationStyle: .insertNewAssignment(targetNameCondition: nil)) + else if let xcconfig = settings.constructionComponents.targetXcconfig { + self.init(sourceRange: .init(path: xcconfig.path, startLine: xcconfig.finalLineNumber, startColumn: xcconfig.finalColumnNumber, endLine: xcconfig.finalLineNumber, endColumn: xcconfig.finalColumnNumber), modificationStyle: .insertNewAssignment(targetNameCondition: nil)) } - else if let path = settings.constructionComponents.projectXcconfigPath { - self.init(sourceRange: .init(path: path, startLine: .max, startColumn: .max, endLine: .max, endColumn: .max), modificationStyle: .insertNewAssignment(targetNameCondition: target.name)) + else if let xcconfig = settings.constructionComponents.projectXcconfig { + self.init(sourceRange: .init(path: xcconfig.path, startLine: xcconfig.finalLineNumber, startColumn: xcconfig.finalColumnNumber, endLine: xcconfig.finalLineNumber, endColumn: xcconfig.finalColumnNumber), modificationStyle: .insertNewAssignment(targetNameCondition: target.name)) } else { return nil diff --git a/Sources/SWBCore/MacroConfigFileLoader.swift b/Sources/SWBCore/MacroConfigFileLoader.swift index cbb184b4..d9153351 100644 --- a/Sources/SWBCore/MacroConfigFileLoader.swift +++ b/Sources/SWBCore/MacroConfigFileLoader.swift @@ -30,6 +30,12 @@ import Foundation /// Whether we failed to read the file at all. If this is `true`, the macro table is guaranteed to be empty. @_spi(Testing) public let isFileReadFailure: Bool + + /// The final line number after parsing completes. + let finalLineNumber: Int + + /// The final column number after parsing completes. + let finalColumnNumber: Int } @_spi(Testing) public enum MacroConfigLoadContext: Sendable { @@ -54,7 +60,7 @@ final class MacroConfigFileLoader: Sendable { guard let data = try? fs.read(path) else { let table = MacroValueAssignmentTable(namespace: namespace) let dependencyPaths = [path] - return MacroConfigInfo(table: table, diagnostics: [], dependencyPaths: dependencyPaths, signature: filesSignature(dependencyPaths), isFileReadFailure: true) + return MacroConfigInfo(table: table, diagnostics: [], dependencyPaths: dependencyPaths, signature: filesSignature(dependencyPaths), isFileReadFailure: true, finalLineNumber: 1, finalColumnNumber: 1) } return loadSettingsFromConfig(data: data, path: path, namespace: namespace, searchPaths: searchPaths, filesSignature: filesSignature) @@ -287,7 +293,7 @@ final class MacroConfigFileLoader: Sendable { let delegate = SettingsConfigFileParserDelegate(fs: fs, basePath: path?.dirname, searchPaths: searchPaths, table: tableRef, diagnostics: diagnostics, nestedConfigurations: nestedConfigs, ancestorIncludes: ancestorIncludes, developerPath: core.developerPath) let parser = MacroConfigFileParser(byteString: data, path: path ?? Path("no path to xcconfig file provided"), delegate: delegate) parser.parse() - return MacroConfigInfo(table: tableRef.table, diagnostics: diagnostics.diagnostics, dependencyPaths: nestedConfigs.paths, signature: filesSignature(nestedConfigs.paths), isFileReadFailure: false) + return MacroConfigInfo(table: tableRef.table, diagnostics: diagnostics.diagnostics, dependencyPaths: nestedConfigs.paths, signature: filesSignature(nestedConfigs.paths), isFileReadFailure: false, finalLineNumber: parser.finalLineNumber, finalColumnNumber: parser.finalColumnNumber) } } diff --git a/Sources/SWBCore/Settings/Settings.swift b/Sources/SWBCore/Settings/Settings.swift index a1ccf9de..1d23935f 100644 --- a/Sources/SWBCore/Settings/Settings.swift +++ b/Sources/SWBCore/Settings/Settings.swift @@ -786,19 +786,16 @@ public final class Settings: PlatformBuildContext, Sendable { /// /// - remark: The overhead of this object should be very small, because the majority of the actual data are the linked lists of macro definitions, which are shared with the main table in the `Settings` object. public struct ConstructionComponents: Sendable { - // These properties are the individual tables (and info about them) of specific levels which contributed to the Settings. + struct XcconfigInfo: Sendable { + var path: Path + var settings: MacroValueAssignmentTable + var finalLineNumber: Int + var finalColumnNumber: Int + } - /// The path to the project-level xcconfig file. - let projectXcconfigPath: Path? - /// The project-level xcconfig settings table. - let projectXcconfigSettings: MacroValueAssignmentTable? - /// The project-level settings table. + let projectXcconfig: XcconfigInfo? let projectSettings: MacroValueAssignmentTable? - /// The path to the target-level xcconfig file. - let targetXcconfigPath: Path? - /// The target-level xcconfig settings table. - let targetXcconfigSettings: MacroValueAssignmentTable? - /// The target-level settings table. + let targetXcconfig: XcconfigInfo? let targetSettings: MacroValueAssignmentTable? // These properties are the actual tables of settings up to a certain point, which are used to compute the resolved values of settings at that level in the build settings editor (e.g., in the Levels view). @@ -966,9 +963,9 @@ public final class Settings: PlatformBuildContext, Sendable { return BuildSettingsEditorInfoPayload( // Assigned values targetSettingAssignments: assignedValues(for: constructionComponents.targetSettings), - targetXcconfigSettingAssignments: assignedValues(for: constructionComponents.targetXcconfigSettings), + targetXcconfigSettingAssignments: assignedValues(for: constructionComponents.targetXcconfig?.settings), projectSettingAssignments: assignedValues(for: constructionComponents.projectSettings), - projectXcconfigSettingAssignments: assignedValues(for: constructionComponents.projectXcconfigSettings), + projectXcconfigSettingAssignments: assignedValues(for: constructionComponents.projectXcconfig?.settings), // Resolved values targetResolvedSettingsValues: resolvedValues(for: constructionComponents.upToTargetSettings), @@ -1322,18 +1319,16 @@ private class SettingsBuilder { return core.coreSettings } - private var projectXcconfigPath: Path? = nil - private var projectXcconfigSettings: MacroValueAssignmentTable? = nil + private var projectXcconfig: Settings.ConstructionComponents.XcconfigInfo? = nil private var projectSettings: MacroValueAssignmentTable? = nil - private var targetXcconfigPath: Path? = nil - private var targetXcconfigSettings: MacroValueAssignmentTable? = nil + private var targetXcconfig: Settings.ConstructionComponents.XcconfigInfo? = nil private var targetSettings: MacroValueAssignmentTable? = nil /// Convenient array for iterating over all defined settings tables in the project for this target, from lowest to highest. private var allProjectSettingsLevels: [(table: MacroValueAssignmentTable?, path: Path?, level: String)] { return [ - (projectXcconfigSettings, projectXcconfigPath, "project-xcconfig"), + (projectXcconfig?.settings, projectXcconfig?.path, "project-xcconfig"), (projectSettings, nil, "project"), - (targetXcconfigSettings, targetXcconfigPath, "target-xcconfig"), + (targetXcconfig?.settings, targetXcconfig?.path, "target-xcconfig"), (targetSettings, nil, "target"), ] } @@ -1347,11 +1342,9 @@ private class SettingsBuilder { /// The project model components which were used to construct the settings made by this builder. var constructionComponents: Settings.ConstructionComponents { return Settings.ConstructionComponents( - projectXcconfigPath: self.projectXcconfigPath, - projectXcconfigSettings: self.projectXcconfigSettings, + projectXcconfig: self.projectXcconfig, projectSettings: self.projectSettings, - targetXcconfigPath: self.targetXcconfigPath, - targetXcconfigSettings: self.targetXcconfigSettings, + targetXcconfig: self.targetXcconfig, targetSettings: self.targetSettings, upToDefaultsSettings: self.upToDefaultsSettings, upToProjectXcconfigSettings: upToProjectXcconfigSettings, @@ -2807,8 +2800,7 @@ private class SettingsBuilder { } // Save the settings table as part of the construction components. - self.projectXcconfigPath = path - self.projectXcconfigSettings = info.table + self.projectXcconfig = .init(path: path, settings: info.table, finalLineNumber: info.finalLineNumber, finalColumnNumber: info.finalColumnNumber) // Also save the table we've constructed so far. self.upToProjectXcconfigSettings = MacroValueAssignmentTable(copying: _table) @@ -3015,8 +3007,7 @@ private class SettingsBuilder { } // Save the settings table as part of the construction components. - self.targetXcconfigPath = path - self.targetXcconfigSettings = info.table + self.targetXcconfig = .init(path: path, settings: info.table, finalLineNumber: info.finalLineNumber, finalColumnNumber: info.finalColumnNumber) // Save the table we've constructed so far. self.upToTargetXcconfigSettings = MacroValueAssignmentTable(copying: _table) diff --git a/Sources/SWBMacro/MacroConfigFileParser.swift b/Sources/SWBMacro/MacroConfigFileParser.swift index 7497f867..4e5ddc76 100644 --- a/Sources/SWBMacro/MacroConfigFileParser.swift +++ b/Sources/SWBMacro/MacroConfigFileParser.swift @@ -42,6 +42,15 @@ public final class MacroConfigFileParser { /// Current line number. Starts at one. var currLine: Int + /// The final line number after parsing completes. + public private(set) var finalLineNumber: Int = 1 + + /// The final column number after parsing completes. + public private(set) var finalColumnNumber: Int = 1 + + /// Index of the start of the current line in the byte array. + private var currentLineStartIdx: Int = 0 + /// Initializes the macro expression parser with the given string and delegate. How the string is parsed depends on the particular parse method that’s invoked, such as `parseAsString()` or `parseAsStringList()`, and not on the configuration of the parser. public init(byteString: ByteString, path: Path, delegate: (any MacroConfigFileParserDelegate)?) { @@ -55,6 +64,9 @@ public final class MacroConfigFileParser { /// Returns the current line number of the parser. This is commonly used from the custom implementations of the parser delegate function callbacks. Line numbers are one-based, and refer only to the source text of the parser itself (not taking into account any source text included using #include directives). public var lineNumber: Int { return currLine } + /// Returns the current column number of the parser. Column numbers are one-based. + public var columnNumber: Int { return currIdx - currentLineStartIdx + 1 } + /* Grammar: @@ -131,6 +143,7 @@ public final class MacroConfigFileParser { } advance(advancement) currLine += 1 + currentLineStartIdx = currIdx } @@ -461,6 +474,10 @@ public final class MacroConfigFileParser { // At this point, we expect to have seen all of the input string. assert(isAtEndOfStream) + + // Set the final line and column numbers + finalLineNumber = currLine + finalColumnNumber = columnNumber } diff --git a/Tests/SWBBuildSystemTests/DependencyValidationTests.swift b/Tests/SWBBuildSystemTests/DependencyValidationTests.swift index 7b57cd75..6dcea66c 100644 --- a/Tests/SWBBuildSystemTests/DependencyValidationTests.swift +++ b/Tests/SWBBuildSystemTests/DependencyValidationTests.swift @@ -10,6 +10,7 @@ // //===----------------------------------------------------------------------===// +import Foundation import SWBCore import SWBTestSupport import SWBUtil @@ -412,6 +413,11 @@ fileprivate struct DependencyValidationTests: CoreBasedTests { """ } + let projectXCConfigContents = try #require(tester.fs.read(projectXCConfigPath).stringValue) + let projectXCConfigLines = projectXCConfigContents.components(separatedBy: .newlines) + let projectXCConfigFinalLineNumber = projectXCConfigLines.count + let projectXCConfigFinalColumnNumber = (projectXCConfigLines.last?.count ?? 0) + 1 + let expectedDiagsByTarget: [String: [Diagnostic]] = [ "TargetA": [ Diagnostic( @@ -437,11 +443,11 @@ fileprivate struct DependencyValidationTests: CoreBasedTests { "TargetB": [ Diagnostic( behavior: .error, - location: Diagnostic.Location.path(projectXCConfigPath, line: .max, column: .max), + location: Diagnostic.Location.path(projectXCConfigPath, line: projectXCConfigFinalLineNumber, column: projectXCConfigFinalColumnNumber), data: DiagnosticData("Missing entries in MODULE_DEPENDENCIES: Foundation"), fixIts: [ Diagnostic.FixIt( - sourceRange: Diagnostic.SourceRange(path: projectXCConfigPath, startLine: .max, startColumn: .max, endLine: .max, endColumn: .max), + sourceRange: Diagnostic.SourceRange(path: projectXCConfigPath, startLine: projectXCConfigFinalLineNumber, startColumn: projectXCConfigFinalColumnNumber, endLine: projectXCConfigFinalLineNumber, endColumn: projectXCConfigFinalColumnNumber), newText: "\nMODULE_DEPENDENCIES[target=TargetB] = $(inherited) \\\n Foundation\n"), ], childDiagnostics: [ @@ -450,7 +456,7 @@ fileprivate struct DependencyValidationTests: CoreBasedTests { location: Diagnostic.Location.path(swiftSourcePath, line: 1, column: 8), data: DiagnosticData("Missing entry in MODULE_DEPENDENCIES: Foundation"), fixIts: [Diagnostic.FixIt( - sourceRange: Diagnostic.SourceRange(path: projectXCConfigPath, startLine: .max, startColumn: .max, endLine: .max, endColumn: .max), + sourceRange: Diagnostic.SourceRange(path: projectXCConfigPath, startLine: projectXCConfigFinalLineNumber, startColumn: projectXCConfigFinalColumnNumber, endLine: projectXCConfigFinalLineNumber, endColumn: projectXCConfigFinalColumnNumber), newText: "\nMODULE_DEPENDENCIES[target=TargetB] = $(inherited) \\\n Foundation\n")], ), ]), diff --git a/Tests/SWBMacroTests/MacroParsingTests.swift b/Tests/SWBMacroTests/MacroParsingTests.swift index 4c4c71cf..a5144803 100644 --- a/Tests/SWBMacroTests/MacroParsingTests.swift +++ b/Tests/SWBMacroTests/MacroParsingTests.swift @@ -824,6 +824,45 @@ fileprivate let testFileData = [ expectedIncludeDirectivesCount: 1 ) } + + @Test + func finalLineAndColumnTracking() { + // Test empty string + TestMacroConfigFileParser("", + expectedAssignments: [], + expectedDiagnostics: [], + expectedIncludeDirectivesCount: 0, + expectedEndLine: 1, + expectedEndColumn: 1 + ) + + // Test single line assignment + TestMacroConfigFileParser("A = B", + expectedAssignments: [(macro: "A", conditions: [], value: "B")], + expectedDiagnostics: [], + expectedIncludeDirectivesCount: 0, + expectedEndLine: 1, + expectedEndColumn: 6 + ) + + // Test multiple lines with empty line at end + TestMacroConfigFileParser("A = B\n\n", + expectedAssignments: [(macro: "A", conditions: [], value: "B")], + expectedDiagnostics: [], + expectedIncludeDirectivesCount: 0, + expectedEndLine: 3, + expectedEndColumn: 1 + ) + + // Test with comment-only line at end + TestMacroConfigFileParser("A = B\n// This is a comment", + expectedAssignments: [(macro: "A", conditions: [], value: "B")], + expectedDiagnostics: [], + expectedIncludeDirectivesCount: 0, + expectedEndLine: 2, + expectedEndColumn: 21 + ) + } } // We used typealiased tuples for simplicity and readability. @@ -832,7 +871,7 @@ typealias AssignmentInfo = (macro: String, conditions: [ConditionInfo], value: S typealias DiagnosticInfo = (level: MacroConfigFileDiagnostic.Level, kind: MacroConfigFileDiagnostic.Kind, line: Int) typealias LocationInfo = (macro: String, path: Path, startLine: Int, endLine: Int, startColumn: Int, endColumn: Int) -private func TestMacroConfigFileParser(_ string: String, expectedAssignments: [AssignmentInfo], expectedDiagnostics: [DiagnosticInfo], expectedLocations: [LocationInfo]? = nil, expectedIncludeDirectivesCount: Int, sourceLocation: SourceLocation = #_sourceLocation) { +private func TestMacroConfigFileParser(_ string: String, expectedAssignments: [AssignmentInfo], expectedDiagnostics: [DiagnosticInfo], expectedLocations: [LocationInfo]? = nil, expectedIncludeDirectivesCount: Int, expectedEndLine: Int? = nil, expectedEndColumn: Int? = nil, sourceLocation: SourceLocation = #_sourceLocation) { /// We use a custom delegate to test that we’re getting the expected results, which for the sake of convenience are just kept in (name, conds:[(cond-param, cond-value)], value) tuples, i.e. conditions is an array of two-element tuples. class ConfigFileParserTestDelegate : MacroConfigFileParserDelegate { @@ -873,6 +912,14 @@ private func TestMacroConfigFileParser(_ string: String, expectedAssignments: [A // Create a parser, and do the parse. let parser = MacroConfigFileParser(byteString: ByteString(encodingAsUTF8: string), path: Path("TestMacroConfigFileParser().xcconfig"), delegate: delegate) parser.parse() + + // Check the final line and column numbers if expected values are provided. + if let expectedEndLine { + #expect(parser.finalLineNumber == expectedEndLine, "expected final line number \(expectedEndLine), but instead got \(parser.finalLineNumber)", sourceLocation: sourceLocation) + } + if let expectedEndColumn { + #expect(parser.finalColumnNumber == expectedEndColumn, "expected final column number \(expectedEndColumn), but instead got \(parser.finalColumnNumber)", sourceLocation: sourceLocation) + } // Check the assignments that the delegate saw against the expected ones. #expect(delegate.assignments == expectedAssignments, "expected assignments \(expectedAssignments), but instead got \(delegate.assignments)", sourceLocation: sourceLocation)