Skip to content

Initial support for tracking locations for assigned values in XCConfigs #513

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 5 additions & 4 deletions Sources/SWBCore/MacroConfigFileLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ final class MacroConfigFileLoader: Sendable {
return MacroConfigFileParser(byteString: data, path: path, delegate: delegate)
}

mutating func foundMacroValueAssignment(_ macroName: String, conditions: [(param: String, pattern: String)], value: String, parser: MacroConfigFileParser) {
mutating func foundMacroValueAssignment(_ macroName: String, conditions: [(param: String, pattern: String)], value: String, path: Path, line: Int, startColumn: Int, endColumn: Int, parser: MacroConfigFileParser) {
// Look up the macro name, creating it as a user-defined macro if it isn’t already known.
let macro = table.namespace.lookupOrDeclareMacro(UserDefinedMacroDeclaration.self, macroName)

Expand All @@ -253,7 +253,8 @@ final class MacroConfigFileLoader: Sendable {
}

// Parse the value in a manner consistent with the macro definition.
table.push(macro, table.namespace.parseForMacro(macro, value: value), conditions: conditionSet)
let location = MacroValueAssignmentLocation(path: path, line: line, startColumn: startColumn, endColumn: endColumn)
table.push(macro, table.namespace.parseForMacro(macro, value: value), conditions: conditionSet, location: location)
}

func handleDiagnostic(_ diagnostic: MacroConfigFileDiagnostic, parser: MacroConfigFileParser) {
Expand Down Expand Up @@ -301,8 +302,8 @@ fileprivate final class MacroValueAssignmentTableRef {
table.namespace
}

func push(_ macro: MacroDeclaration, _ value: MacroExpression, conditions: MacroConditionSet? = nil) {
table.push(macro, value, conditions: conditions)
func push(_ macro: MacroDeclaration, _ value: MacroExpression, conditions: MacroConditionSet? = nil, location: MacroValueAssignmentLocation? = nil) {
table.push(macro, value, conditions: conditions, location: location)
}
}

Expand Down
8 changes: 5 additions & 3 deletions Sources/SWBMacro/MacroConfigFileParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ public final class MacroConfigFileParser {
// MARK: Parsing of value assignment starts here.
/// Parses a macro value assignment line of the form MACRONAME [ optional conditions ] ... = VALUE ';'?
private func parseMacroValueAssignment() {
let startOfLine = currIdx - 1
// First skip over any whitespace and comments.
skipWhitespaceAndComments()

Expand Down Expand Up @@ -361,6 +362,7 @@ public final class MacroConfigFileParser {
// Skip over the equals sign.
assert(currChar == /* '=' */ 61)
advance()
let startColumn = currIdx - startOfLine

var chunks : [String] = []
while let chunk = parseNonListAssignmentRHS() {
Expand All @@ -383,7 +385,7 @@ public final class MacroConfigFileParser {
}
// Finally, now that we have the name, conditions, and value, we tell the delegate about it.
let value = chunks.joined(separator: " ")
delegate?.foundMacroValueAssignment(name, conditions: conditions, value: value, parser: self)
delegate?.foundMacroValueAssignment(name, conditions: conditions, value: value, path: path, line: currLine, startColumn: startColumn, endColumn: currIdx - startOfLine, parser: self)
}

public func parseNonListAssignmentRHS() -> String? {
Expand Down Expand Up @@ -518,7 +520,7 @@ public final class MacroConfigFileParser {
}
func endPreprocessorInclusion() {
}
func foundMacroValueAssignment(_ macroName: String, conditions: [(param: String, pattern: String)], value: String, parser: MacroConfigFileParser) {
func foundMacroValueAssignment(_ macroName: String, conditions: [(param: String, pattern: String)], value: String, path: Path, line: Int, startColumn: Int, endColumn: Int, parser: MacroConfigFileParser) {
self.macroName = macroName
self.conditions = conditions.isEmpty ? nil : conditions
}
Expand Down Expand Up @@ -565,7 +567,7 @@ public protocol MacroConfigFileParserDelegate {
func endPreprocessorInclusion()

/// Invoked once for each macro value assignment. The `macroName` is guaranteed to be non-empty, but `value` may be empty. Any macro conditions are passed as tuples in the `conditions`; parameters are guaranteed to be non-empty strings, but patterns may be empty.
mutating func foundMacroValueAssignment(_ macroName: String, conditions: [(param: String, pattern: String)], value: String, parser: MacroConfigFileParser)
mutating func foundMacroValueAssignment(_ macroName: String, conditions: [(param: String, pattern: String)], value: String, path: Path, line: Int, startColumn: Int, endColumn: Int, parser: MacroConfigFileParser)

/// Invoked if an error, warning, or other diagnostic is detected.
func handleDiagnostic(_ diagnostic: MacroConfigFileDiagnostic, parser: MacroConfigFileParser)
Expand Down
111 changes: 105 additions & 6 deletions Sources/SWBMacro/MacroValueAssignmentTable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,23 @@ public struct MacroValueAssignmentTable: Serializable, Sendable {
/// Maps macro declarations to corresponding linked lists of assignments.
public var valueAssignments: [MacroDeclaration: MacroValueAssignment]

private init(namespace: MacroNamespace, valueAssignments: [MacroDeclaration: MacroValueAssignment]) {
private var valueLocations: [String: InternedMacroValueAssignmentLocation]
private var macroConfigPaths: OrderedSet<Path>

private init(namespace: MacroNamespace, valueAssignments: [MacroDeclaration: MacroValueAssignment], valueLocations: [String: InternedMacroValueAssignmentLocation], macroConfigPaths: OrderedSet<Path>) {
self.namespace = namespace
self.valueAssignments = valueAssignments
self.valueLocations = valueLocations
self.macroConfigPaths = macroConfigPaths
}

public init(namespace: MacroNamespace) {
self.init(namespace: namespace, valueAssignments: [:])
self.init(namespace: namespace, valueAssignments: [:], valueLocations: [:], macroConfigPaths: OrderedSet())
}

/// Convenience initializer to create a `MacroValueAssignmentTable` from another instance (i.e., to create a copy).
public init(copying table: MacroValueAssignmentTable) {
self.init(namespace: table.namespace, valueAssignments: table.valueAssignments)
self.init(namespace: table.namespace, valueAssignments: table.valueAssignments, valueLocations: table.valueLocations, macroConfigPaths: table.macroConfigPaths)
}

/// Remove all assignments for the given macro.
Expand Down Expand Up @@ -77,18 +82,32 @@ public struct MacroValueAssignmentTable: Serializable, Sendable {


/// Adds a mapping from `macro` to `value`, inserting it ahead of any already existing assignment for the same macro. Unless the value refers to the lower-precedence expression (using `$(inherited)` notation), any existing assignments are shadowed but not removed.
public mutating func push(_ macro: MacroDeclaration, _ value: MacroExpression, conditions: MacroConditionSet? = nil) {
public mutating func push(_ macro: MacroDeclaration, _ value: MacroExpression, conditions: MacroConditionSet? = nil, location: MacroValueAssignmentLocation? = nil) {
assert(namespace.lookupMacroDeclaration(macro.name) === macro)
// Validate the type.
assert(macro.type.matchesExpressionType(value))
valueAssignments[macro] = MacroValueAssignment(expression: value, conditions: conditions, next: valueAssignments[macro])

if let location {
let index = macroConfigPaths.append(location.path).index
valueLocations[macro.name] = InternedMacroValueAssignmentLocation(pathRef: index, line: location.line, startColumn: location.startColumn, endColumn: location.endColumn)
}
}

private mutating func mergeLocations(from otherTable: MacroValueAssignmentTable) {
otherTable.valueLocations.forEach {
let path = otherTable.macroConfigPaths[$0.value.pathRef]
let index = macroConfigPaths.append(path).index
valueLocations[$0.key] = .init(pathRef: index, line: $0.value.line, startColumn: $0.value.startColumn, endColumn: $0.value.endColumn)
}
}

/// Adds a mapping from each of the macro-to-value mappings in `otherTable`, inserting them ahead of any already existing assignments in the receiving table. The other table isn’t affected in any way (in particular, no reference is kept from the receiver to the other table).
public mutating func pushContentsOf(_ otherTable: MacroValueAssignmentTable) {
for (macro, firstAssignment) in otherTable.valueAssignments {
valueAssignments[macro] = insertCopiesOfMacroValueAssignmentNodes(firstAssignment, inFrontOf: valueAssignments[macro])
}
mergeLocations(from: otherTable)
}

/// Looks up and returns the first (highest-precedence) macro value assignment for `macro`, if there is one.
Expand All @@ -106,6 +125,18 @@ public struct MacroValueAssignmentTable: Serializable, Sendable {
return valueAssignments.isEmpty
}

public func location(of macro: MacroDeclaration) -> MacroValueAssignmentLocation? {
guard let location = valueLocations[macro.name] else {
return nil
}
return MacroValueAssignmentLocation(
path: macroConfigPaths[location.pathRef],
line: location.line,
startColumn: location.startColumn,
endColumn: location.endColumn
)
}

public func bindConditionParameter(_ parameter: MacroConditionParameter, _ conditionValues: [String]) -> MacroValueAssignmentTable {
return bindConditionParameter(parameter, conditionValues.map { .string($0) })
}
Expand Down Expand Up @@ -192,6 +223,7 @@ public struct MacroValueAssignmentTable: Serializable, Sendable {
bindAndPushAssignment(firstAssignment)

}
table.mergeLocations(from: self)
return table
}

Expand Down Expand Up @@ -219,7 +251,7 @@ public struct MacroValueAssignmentTable: Serializable, Sendable {
// MARK: Serialization

public func serialize<T: Serializer>(to serializer: T) {
serializer.beginAggregate(1)
serializer.beginAggregate(3)

// We don't directly serialize MacroDeclarations, but rather serialize their contents "by hand" so when we deserialize we can re-use existing declarations in our namespace.
serializer.beginAggregate(valueAssignments.count)
Expand Down Expand Up @@ -247,6 +279,17 @@ public struct MacroValueAssignmentTable: Serializable, Sendable {
}
serializer.endAggregate() // valueAssignments

serializer.beginAggregate(valueLocations.count)
for (decl, loc) in valueLocations.sorted(by: { $0.0 < $1.0 }) {
serializer.beginAggregate(2)
serializer.serialize(decl)
serializer.serialize(loc)
serializer.endAggregate()
}
serializer.endAggregate()

serializer.serialize(macroConfigPaths)

serializer.endAggregate() // the whole table
}

Expand All @@ -255,9 +298,10 @@ public struct MacroValueAssignmentTable: Serializable, Sendable {
guard let delegate = deserializer.delegate as? (any MacroValueAssignmentTableDeserializerDelegate) else { throw DeserializerError.invalidDelegate("delegate must be a MacroValueAssignmentTableDeserializerDelegate") }
self.namespace = delegate.namespace
self.valueAssignments = [:]
self.valueLocations = [:]

// Deserialize the table.
try deserializer.beginAggregate(1)
try deserializer.beginAggregate(3)

// Iterate over all the key-value pairs.
let count: Int = try deserializer.beginAggregate()
Expand Down Expand Up @@ -304,6 +348,16 @@ public struct MacroValueAssignmentTable: Serializable, Sendable {
// Add it to the dictionary.
self.valueAssignments[decl] = asgn
}

let count2 = try deserializer.beginAggregate()
for _ in 0..<count2 {
try deserializer.beginAggregate(2)
let name: String = try deserializer.deserialize()
let location: InternedMacroValueAssignmentLocation = try deserializer.deserialize()
self.valueLocations[name] = location
}

self.macroConfigPaths = try deserializer.deserialize()
}
}

Expand Down Expand Up @@ -396,6 +450,51 @@ public final class MacroValueAssignment: Serializable, CustomStringConvertible,
}
}

public struct MacroValueAssignmentLocation: Sendable, Equatable {
public let path: Path
public let line: Int
public let startColumn: Int
public let endColumn: Int

public init(path: Path, line: Int, startColumn: Int, endColumn: Int) {
self.path = path
self.line = line
self.startColumn = startColumn
self.endColumn = endColumn
}
}

private struct InternedMacroValueAssignmentLocation: Serializable, Sendable {
let pathRef: OrderedSet<Path>.Index
let line: Int
let startColumn: Int
let endColumn: Int

init(pathRef: OrderedSet<Path>.Index, line: Int, startColumn: Int, endColumn: Int) {
self.pathRef = pathRef
self.line = line
self.startColumn = startColumn
self.endColumn = endColumn
}

public func serialize<T>(to serializer: T) where T : SWBUtil.Serializer {
serializer.beginAggregate(4)
serializer.serialize(pathRef)
serializer.serialize(line)
serializer.serialize(startColumn)
serializer.serialize(endColumn)
serializer.endAggregate()
}

public init(from deserializer: any SWBUtil.Deserializer) throws {
try deserializer.beginAggregate(4)
self.pathRef = try deserializer.deserialize()
self.line = try deserializer.deserialize()
self.startColumn = try deserializer.deserialize()
self.endColumn = try deserializer.deserialize()
}
}

/// Private function that inserts a copy of the given linked list of MacroValueAssignments (starting at `srcAsgn`) in front of `dstAsgn` (which is optional). The order of the copies is the same as the order of the originals, and the last one will have `dstAsgn` as its `next` property. This function returns the copy that corresponds to `srcAsgn` so the client can add a reference to it wherever it sees fit.
private func insertCopiesOfMacroValueAssignmentNodes(_ srcAsgn: MacroValueAssignment, inFrontOf dstAsgn: MacroValueAssignment?) -> MacroValueAssignment {
// If we aren't inserting in front of anything, we can preserve the input as is.
Expand Down
1 change: 1 addition & 0 deletions Tests/SWBCoreTests/SettingsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ import SWBMacro
// Verify that the settings from the xcconfig were added.
let XCCONFIG_USER_SETTING = try #require(settings.userNamespace.lookupMacroDeclaration("XCCONFIG_USER_SETTING"))
#expect(settings.tableForTesting.lookupMacro(XCCONFIG_USER_SETTING)?.expression.stringRep == "from-xcconfig")
#expect(settings.tableForTesting.location(of: XCCONFIG_USER_SETTING) == MacroValueAssignmentLocation(path: .init("/tmp/xcconfigs/Base0.xcconfig"), line: 1, startColumn: 24, endColumn: 38))

// Verify the user project settings.
let USER_PROJECT_SETTING = try #require(settings.userNamespace.lookupMacroDeclaration("USER_PROJECT_SETTING"))
Expand Down
Loading
Loading