Skip to content

Commit eca9ed5

Browse files
authored
Initial support for tracking locations for assigned values in XCConfigs (#513)
This can be used to emit fix-its for XCConfigs files during the build process.
1 parent 7e6e3bd commit eca9ed5

File tree

5 files changed

+154
-16
lines changed

5 files changed

+154
-16
lines changed

Sources/SWBCore/MacroConfigFileLoader.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ final class MacroConfigFileLoader: Sendable {
242242
return MacroConfigFileParser(byteString: data, path: path, delegate: delegate)
243243
}
244244

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

@@ -253,7 +253,8 @@ final class MacroConfigFileLoader: Sendable {
253253
}
254254

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

259260
func handleDiagnostic(_ diagnostic: MacroConfigFileDiagnostic, parser: MacroConfigFileParser) {
@@ -301,8 +302,8 @@ fileprivate final class MacroValueAssignmentTableRef {
301302
table.namespace
302303
}
303304

304-
func push(_ macro: MacroDeclaration, _ value: MacroExpression, conditions: MacroConditionSet? = nil) {
305-
table.push(macro, value, conditions: conditions)
305+
func push(_ macro: MacroDeclaration, _ value: MacroExpression, conditions: MacroConditionSet? = nil, location: MacroValueAssignmentLocation? = nil) {
306+
table.push(macro, value, conditions: conditions, location: location)
306307
}
307308
}
308309

Sources/SWBMacro/MacroConfigFileParser.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ public final class MacroConfigFileParser {
276276
// MARK: Parsing of value assignment starts here.
277277
/// Parses a macro value assignment line of the form MACRONAME [ optional conditions ] ... = VALUE ';'?
278278
private func parseMacroValueAssignment() {
279+
let startOfLine = currIdx - 1
279280
// First skip over any whitespace and comments.
280281
skipWhitespaceAndComments()
281282

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

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

389391
public func parseNonListAssignmentRHS() -> String? {
@@ -518,7 +520,7 @@ public final class MacroConfigFileParser {
518520
}
519521
func endPreprocessorInclusion() {
520522
}
521-
func foundMacroValueAssignment(_ macroName: String, conditions: [(param: String, pattern: String)], value: String, parser: MacroConfigFileParser) {
523+
func foundMacroValueAssignment(_ macroName: String, conditions: [(param: String, pattern: String)], value: String, path: Path, line: Int, startColumn: Int, endColumn: Int, parser: MacroConfigFileParser) {
522524
self.macroName = macroName
523525
self.conditions = conditions.isEmpty ? nil : conditions
524526
}
@@ -565,7 +567,7 @@ public protocol MacroConfigFileParserDelegate {
565567
func endPreprocessorInclusion()
566568

567569
/// 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.
568-
mutating func foundMacroValueAssignment(_ macroName: String, conditions: [(param: String, pattern: String)], value: String, parser: MacroConfigFileParser)
570+
mutating func foundMacroValueAssignment(_ macroName: String, conditions: [(param: String, pattern: String)], value: String, path: Path, line: Int, startColumn: Int, endColumn: Int, parser: MacroConfigFileParser)
569571

570572
/// Invoked if an error, warning, or other diagnostic is detected.
571573
func handleDiagnostic(_ diagnostic: MacroConfigFileDiagnostic, parser: MacroConfigFileParser)

Sources/SWBMacro/MacroValueAssignmentTable.swift

Lines changed: 105 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,23 @@ public struct MacroValueAssignmentTable: Serializable, Sendable {
2020
/// Maps macro declarations to corresponding linked lists of assignments.
2121
public var valueAssignments: [MacroDeclaration: MacroValueAssignment]
2222

23-
private init(namespace: MacroNamespace, valueAssignments: [MacroDeclaration: MacroValueAssignment]) {
23+
private var valueLocations: [String: InternedMacroValueAssignmentLocation]
24+
private var macroConfigPaths: OrderedSet<Path>
25+
26+
private init(namespace: MacroNamespace, valueAssignments: [MacroDeclaration: MacroValueAssignment], valueLocations: [String: InternedMacroValueAssignmentLocation], macroConfigPaths: OrderedSet<Path>) {
2427
self.namespace = namespace
2528
self.valueAssignments = valueAssignments
29+
self.valueLocations = valueLocations
30+
self.macroConfigPaths = macroConfigPaths
2631
}
2732

2833
public init(namespace: MacroNamespace) {
29-
self.init(namespace: namespace, valueAssignments: [:])
34+
self.init(namespace: namespace, valueAssignments: [:], valueLocations: [:], macroConfigPaths: OrderedSet())
3035
}
3136

3237
/// Convenience initializer to create a `MacroValueAssignmentTable` from another instance (i.e., to create a copy).
3338
public init(copying table: MacroValueAssignmentTable) {
34-
self.init(namespace: table.namespace, valueAssignments: table.valueAssignments)
39+
self.init(namespace: table.namespace, valueAssignments: table.valueAssignments, valueLocations: table.valueLocations, macroConfigPaths: table.macroConfigPaths)
3540
}
3641

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

7883

7984
/// 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.
80-
public mutating func push(_ macro: MacroDeclaration, _ value: MacroExpression, conditions: MacroConditionSet? = nil) {
85+
public mutating func push(_ macro: MacroDeclaration, _ value: MacroExpression, conditions: MacroConditionSet? = nil, location: MacroValueAssignmentLocation? = nil) {
8186
assert(namespace.lookupMacroDeclaration(macro.name) === macro)
8287
// Validate the type.
8388
assert(macro.type.matchesExpressionType(value))
8489
valueAssignments[macro] = MacroValueAssignment(expression: value, conditions: conditions, next: valueAssignments[macro])
90+
91+
if let location {
92+
let index = macroConfigPaths.append(location.path).index
93+
valueLocations[macro.name] = InternedMacroValueAssignmentLocation(pathRef: index, line: location.line, startColumn: location.startColumn, endColumn: location.endColumn)
94+
}
95+
}
96+
97+
private mutating func mergeLocations(from otherTable: MacroValueAssignmentTable) {
98+
otherTable.valueLocations.forEach {
99+
let path = otherTable.macroConfigPaths[$0.value.pathRef]
100+
let index = macroConfigPaths.append(path).index
101+
valueLocations[$0.key] = .init(pathRef: index, line: $0.value.line, startColumn: $0.value.startColumn, endColumn: $0.value.endColumn)
102+
}
85103
}
86104

87105
/// 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).
88106
public mutating func pushContentsOf(_ otherTable: MacroValueAssignmentTable) {
89107
for (macro, firstAssignment) in otherTable.valueAssignments {
90108
valueAssignments[macro] = insertCopiesOfMacroValueAssignmentNodes(firstAssignment, inFrontOf: valueAssignments[macro])
91109
}
110+
mergeLocations(from: otherTable)
92111
}
93112

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

128+
public func location(of macro: MacroDeclaration) -> MacroValueAssignmentLocation? {
129+
guard let location = valueLocations[macro.name] else {
130+
return nil
131+
}
132+
return MacroValueAssignmentLocation(
133+
path: macroConfigPaths[location.pathRef],
134+
line: location.line,
135+
startColumn: location.startColumn,
136+
endColumn: location.endColumn
137+
)
138+
}
139+
109140
public func bindConditionParameter(_ parameter: MacroConditionParameter, _ conditionValues: [String]) -> MacroValueAssignmentTable {
110141
return bindConditionParameter(parameter, conditionValues.map { .string($0) })
111142
}
@@ -192,6 +223,7 @@ public struct MacroValueAssignmentTable: Serializable, Sendable {
192223
bindAndPushAssignment(firstAssignment)
193224

194225
}
226+
table.mergeLocations(from: self)
195227
return table
196228
}
197229

@@ -219,7 +251,7 @@ public struct MacroValueAssignmentTable: Serializable, Sendable {
219251
// MARK: Serialization
220252

221253
public func serialize<T: Serializer>(to serializer: T) {
222-
serializer.beginAggregate(1)
254+
serializer.beginAggregate(3)
223255

224256
// 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.
225257
serializer.beginAggregate(valueAssignments.count)
@@ -247,6 +279,17 @@ public struct MacroValueAssignmentTable: Serializable, Sendable {
247279
}
248280
serializer.endAggregate() // valueAssignments
249281

282+
serializer.beginAggregate(valueLocations.count)
283+
for (decl, loc) in valueLocations.sorted(by: { $0.0 < $1.0 }) {
284+
serializer.beginAggregate(2)
285+
serializer.serialize(decl)
286+
serializer.serialize(loc)
287+
serializer.endAggregate()
288+
}
289+
serializer.endAggregate()
290+
291+
serializer.serialize(macroConfigPaths)
292+
250293
serializer.endAggregate() // the whole table
251294
}
252295

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

259303
// Deserialize the table.
260-
try deserializer.beginAggregate(1)
304+
try deserializer.beginAggregate(3)
261305

262306
// Iterate over all the key-value pairs.
263307
let count: Int = try deserializer.beginAggregate()
@@ -304,6 +348,16 @@ public struct MacroValueAssignmentTable: Serializable, Sendable {
304348
// Add it to the dictionary.
305349
self.valueAssignments[decl] = asgn
306350
}
351+
352+
let count2 = try deserializer.beginAggregate()
353+
for _ in 0..<count2 {
354+
try deserializer.beginAggregate(2)
355+
let name: String = try deserializer.deserialize()
356+
let location: InternedMacroValueAssignmentLocation = try deserializer.deserialize()
357+
self.valueLocations[name] = location
358+
}
359+
360+
self.macroConfigPaths = try deserializer.deserialize()
307361
}
308362
}
309363

@@ -396,6 +450,51 @@ public final class MacroValueAssignment: Serializable, CustomStringConvertible,
396450
}
397451
}
398452

453+
public struct MacroValueAssignmentLocation: Sendable, Equatable {
454+
public let path: Path
455+
public let line: Int
456+
public let startColumn: Int
457+
public let endColumn: Int
458+
459+
public init(path: Path, line: Int, startColumn: Int, endColumn: Int) {
460+
self.path = path
461+
self.line = line
462+
self.startColumn = startColumn
463+
self.endColumn = endColumn
464+
}
465+
}
466+
467+
private struct InternedMacroValueAssignmentLocation: Serializable, Sendable {
468+
let pathRef: OrderedSet<Path>.Index
469+
let line: Int
470+
let startColumn: Int
471+
let endColumn: Int
472+
473+
init(pathRef: OrderedSet<Path>.Index, line: Int, startColumn: Int, endColumn: Int) {
474+
self.pathRef = pathRef
475+
self.line = line
476+
self.startColumn = startColumn
477+
self.endColumn = endColumn
478+
}
479+
480+
public func serialize<T>(to serializer: T) where T : SWBUtil.Serializer {
481+
serializer.beginAggregate(4)
482+
serializer.serialize(pathRef)
483+
serializer.serialize(line)
484+
serializer.serialize(startColumn)
485+
serializer.serialize(endColumn)
486+
serializer.endAggregate()
487+
}
488+
489+
public init(from deserializer: any SWBUtil.Deserializer) throws {
490+
try deserializer.beginAggregate(4)
491+
self.pathRef = try deserializer.deserialize()
492+
self.line = try deserializer.deserialize()
493+
self.startColumn = try deserializer.deserialize()
494+
self.endColumn = try deserializer.deserialize()
495+
}
496+
}
497+
399498
/// 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.
400499
private func insertCopiesOfMacroValueAssignmentNodes(_ srcAsgn: MacroValueAssignment, inFrontOf dstAsgn: MacroValueAssignment?) -> MacroValueAssignment {
401500
// If we aren't inserting in front of anything, we can preserve the input as is.

Tests/SWBCoreTests/SettingsTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ import SWBMacro
134134
// Verify that the settings from the xcconfig were added.
135135
let XCCONFIG_USER_SETTING = try #require(settings.userNamespace.lookupMacroDeclaration("XCCONFIG_USER_SETTING"))
136136
#expect(settings.tableForTesting.lookupMacro(XCCONFIG_USER_SETTING)?.expression.stringRep == "from-xcconfig")
137+
#expect(settings.tableForTesting.location(of: XCCONFIG_USER_SETTING) == MacroValueAssignmentLocation(path: .init("/tmp/xcconfigs/Base0.xcconfig"), line: 1, startColumn: 24, endColumn: 38))
137138

138139
// Verify the user project settings.
139140
let USER_PROJECT_SETTING = try #require(settings.userNamespace.lookupMacroDeclaration("USER_PROJECT_SETTING"))

0 commit comments

Comments
 (0)