Skip to content

Commit a619ee1

Browse files
committed
Initial support for tracking locations for assigned values in XCConfigs
This can be used to emit fix-its for XCConfigs files during the build process.
1 parent 1d66c4f commit a619ee1

File tree

4 files changed

+112
-16
lines changed

4 files changed

+112
-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: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,21 @@ 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+
public var valueLocations: [String: MacroValueAssignmentLocation]
24+
25+
private init(namespace: MacroNamespace, valueAssignments: [MacroDeclaration: MacroValueAssignment], valueLocations: [String: MacroValueAssignmentLocation]) {
2426
self.namespace = namespace
2527
self.valueAssignments = valueAssignments
28+
self.valueLocations = valueLocations
2629
}
2730

2831
public init(namespace: MacroNamespace) {
29-
self.init(namespace: namespace, valueAssignments: [:])
32+
self.init(namespace: namespace, valueAssignments: [:], valueLocations: [:])
3033
}
3134

3235
/// Convenience initializer to create a `MacroValueAssignmentTable` from another instance (i.e., to create a copy).
3336
public init(copying table: MacroValueAssignmentTable) {
34-
self.init(namespace: table.namespace, valueAssignments: table.valueAssignments)
37+
self.init(namespace: table.namespace, valueAssignments: table.valueAssignments, valueLocations: table.valueLocations)
3538
}
3639

3740
/// Remove all assignments for the given macro.
@@ -77,18 +80,23 @@ public struct MacroValueAssignmentTable: Serializable, Sendable {
7780

7881

7982
/// 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) {
83+
public mutating func push(_ macro: MacroDeclaration, _ value: MacroExpression, conditions: MacroConditionSet? = nil, location: MacroValueAssignmentLocation? = nil) {
8184
assert(namespace.lookupMacroDeclaration(macro.name) === macro)
8285
// Validate the type.
8386
assert(macro.type.matchesExpressionType(value))
8487
valueAssignments[macro] = MacroValueAssignment(expression: value, conditions: conditions, next: valueAssignments[macro])
88+
89+
if let location {
90+
valueLocations[macro.name] = location
91+
}
8592
}
8693

8794
/// 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).
8895
public mutating func pushContentsOf(_ otherTable: MacroValueAssignmentTable) {
8996
for (macro, firstAssignment) in otherTable.valueAssignments {
9097
valueAssignments[macro] = insertCopiesOfMacroValueAssignmentNodes(firstAssignment, inFrontOf: valueAssignments[macro])
9198
}
99+
valueLocations.merge(otherTable.valueLocations, uniquingKeysWith: { a, b in a })
92100
}
93101

94102
/// Looks up and returns the first (highest-precedence) macro value assignment for `macro`, if there is one.
@@ -192,6 +200,7 @@ public struct MacroValueAssignmentTable: Serializable, Sendable {
192200
bindAndPushAssignment(firstAssignment)
193201

194202
}
203+
table.valueLocations.merge(self.valueLocations) { a, b in a }
195204
return table
196205
}
197206

@@ -219,7 +228,7 @@ public struct MacroValueAssignmentTable: Serializable, Sendable {
219228
// MARK: Serialization
220229

221230
public func serialize<T: Serializer>(to serializer: T) {
222-
serializer.beginAggregate(1)
231+
serializer.beginAggregate(2)
223232

224233
// 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.
225234
serializer.beginAggregate(valueAssignments.count)
@@ -247,6 +256,15 @@ public struct MacroValueAssignmentTable: Serializable, Sendable {
247256
}
248257
serializer.endAggregate() // valueAssignments
249258

259+
serializer.beginAggregate(valueLocations.count)
260+
for (decl, loc) in valueLocations.sorted(by: { $0.0 < $1.0 }) {
261+
serializer.beginAggregate(2)
262+
serializer.serialize(decl)
263+
serializer.serialize(loc)
264+
serializer.endAggregate()
265+
}
266+
serializer.endAggregate()
267+
250268
serializer.endAggregate() // the whole table
251269
}
252270

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

259278
// Deserialize the table.
260-
try deserializer.beginAggregate(1)
279+
try deserializer.beginAggregate(2)
261280

262281
// Iterate over all the key-value pairs.
263282
let count: Int = try deserializer.beginAggregate()
@@ -304,6 +323,14 @@ public struct MacroValueAssignmentTable: Serializable, Sendable {
304323
// Add it to the dictionary.
305324
self.valueAssignments[decl] = asgn
306325
}
326+
327+
let count2 = try deserializer.beginAggregate()
328+
for _ in 0..<count2 {
329+
try deserializer.beginAggregate(2)
330+
let name: String = try deserializer.deserialize()
331+
let location: MacroValueAssignmentLocation = try deserializer.deserialize()
332+
self.valueLocations[name] = location
333+
}
307334
}
308335
}
309336

@@ -396,6 +423,37 @@ public final class MacroValueAssignment: Serializable, CustomStringConvertible,
396423
}
397424
}
398425

426+
public struct MacroValueAssignmentLocation: Serializable, Sendable {
427+
public let path: Path
428+
public let line: Int
429+
public let startColumn: Int
430+
public let endColumn: Int
431+
432+
public init(path: Path, line: Int, startColumn: Int, endColumn: Int) {
433+
self.path = path
434+
self.line = line
435+
self.startColumn = startColumn
436+
self.endColumn = endColumn
437+
}
438+
439+
public func serialize<T>(to serializer: T) where T : SWBUtil.Serializer {
440+
serializer.beginAggregate(4)
441+
serializer.serialize(path)
442+
serializer.serialize(line)
443+
serializer.serialize(startColumn)
444+
serializer.serialize(endColumn)
445+
serializer.endAggregate()
446+
}
447+
448+
public init(from deserializer: any SWBUtil.Deserializer) throws {
449+
try deserializer.beginAggregate(4)
450+
self.path = try deserializer.deserialize()
451+
self.line = try deserializer.deserialize()
452+
self.startColumn = try deserializer.deserialize()
453+
self.endColumn = try deserializer.deserialize()
454+
}
455+
}
456+
399457
/// 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.
400458
private func insertCopiesOfMacroValueAssignmentNodes(_ srcAsgn: MacroValueAssignment, inFrontOf dstAsgn: MacroValueAssignment?) -> MacroValueAssignment {
401459
// If we aren't inserting in front of anything, we can preserve the input as is.

Tests/SWBMacroTests/MacroParsingTests.swift

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -790,7 +790,7 @@ fileprivate let testFileData = [
790790
}
791791
func endPreprocessorInclusion() {
792792
}
793-
func foundMacroValueAssignment(_ macroName: String, conditions: [(param: String, pattern: String)], value: String, parser: MacroConfigFileParser) {
793+
func foundMacroValueAssignment(_ macroName: String, conditions: [(param: String, pattern: String)], value: String, path: Path, line: Int, startColumn: Int, endColumn: Int, parser: MacroConfigFileParser) {
794794
}
795795

796796
func handleDiagnostic(_ diagnostic: MacroConfigFileDiagnostic, parser: MacroConfigFileParser) {
@@ -804,19 +804,41 @@ fileprivate let testFileData = [
804804
MacroConfigFileParser(byteString: "// [-Wnullability-completeness-on-arrays] \t\t\t(on) Warns about missing nullability annotations on array parameters.", path: Path(""), delegate: delegate).parse()
805805
#expect(delegate.diagnosticMessages == [String]())
806806
}
807+
808+
@Test
809+
func parserProvidesLocationInformation() throws {
810+
TestMacroConfigFileParser("#include \"Multiline.xcconfig\"",
811+
expectedAssignments: [
812+
(macro: "FEATURE_DEFINES_A", conditions: [], value: "$(A) $(B) $(C)"),
813+
(macro: "FEATURE_DEFINES_B", conditions: [], value: "$(D) $(E) $(F)"),
814+
(macro: "FEATURE_DEFINES_C", conditions: [], value: "$(G) $(H)"),
815+
(macro: "FEATURE_DEFINES_D", conditions: [], value: "$(I)")
816+
],
817+
expectedDiagnostics: [],
818+
expectedLocations: [
819+
(macro: "FEATURE_DEFINES_A", path: .init("Multiline.xcconfig"), line: 2, startColumn: 20, endColumn: 37),
820+
(macro: "FEATURE_DEFINES_B", path: .init("Multiline.xcconfig"), line: 5, startColumn: 20, endColumn: 87),
821+
(macro: "FEATURE_DEFINES_C", path: .init("Multiline.xcconfig"), line: 9, startColumn: 20, endColumn: 61),
822+
(macro: "FEATURE_DEFINES_D", path: .init("Multiline.xcconfig"), line: 11, startColumn: 20, endColumn: 45),
823+
],
824+
expectedIncludeDirectivesCount: 1
825+
)
826+
}
807827
}
808828

809829
// We used typealiased tuples for simplicity and readability.
810830
typealias ConditionInfo = (param: String, pattern: String)
811831
typealias AssignmentInfo = (macro: String, conditions: [ConditionInfo], value: String)
812832
typealias DiagnosticInfo = (level: MacroConfigFileDiagnostic.Level, kind: MacroConfigFileDiagnostic.Kind, line: Int)
833+
typealias LocationInfo = (macro: String, path: Path, line: Int, startColumn: Int, endColumn: Int)
813834

814-
private func TestMacroConfigFileParser(_ string: String, expectedAssignments: [AssignmentInfo], expectedDiagnostics: [DiagnosticInfo], expectedIncludeDirectivesCount: Int, sourceLocation: SourceLocation = #_sourceLocation) {
835+
private func TestMacroConfigFileParser(_ string: String, expectedAssignments: [AssignmentInfo], expectedDiagnostics: [DiagnosticInfo], expectedLocations: [LocationInfo]? = nil, expectedIncludeDirectivesCount: Int, sourceLocation: SourceLocation = #_sourceLocation) {
815836

816837
/// 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.
817838
class ConfigFileParserTestDelegate : MacroConfigFileParserDelegate {
818839
var assignments = Array<AssignmentInfo>()
819840
var diagnostics = Array<DiagnosticInfo>()
841+
var locations = Array<LocationInfo>()
820842

821843
var includeDirectivesCount = 0
822844

@@ -834,9 +856,10 @@ private func TestMacroConfigFileParser(_ string: String, expectedAssignments: [A
834856
func endPreprocessorInclusion() {
835857
self.includeDirectivesCount += 1
836858
}
837-
func foundMacroValueAssignment(_ macroName: String, conditions: [(param: String, pattern: String)], value: String, parser: MacroConfigFileParser) {
859+
func foundMacroValueAssignment(_ macroName: String, conditions: [(param: String, pattern: String)], value: String, path: Path, line: Int, startColumn: Int, endColumn: Int, parser: MacroConfigFileParser) {
838860
// print("\(parser.lineNumber): \(macroName)\(conditions.map({ "[\($0.param)=\($0.pattern)]" }).joinWithSeparator(""))=\(value)")
839861
assignments.append((macro: macroName, conditions: conditions, value: value))
862+
locations.append((macro: macroName, path: path, line: line, startColumn: startColumn, endColumn: endColumn))
840863
}
841864
func handleDiagnostic(_ diagnostic: MacroConfigFileDiagnostic, parser: MacroConfigFileParser) {
842865
// print("\(parser.lineNumber): \(diagnostic)")
@@ -857,6 +880,10 @@ private func TestMacroConfigFileParser(_ string: String, expectedAssignments: [A
857880
// Check the diagnostics that the delegate saw against the expected ones.
858881
#expect(delegate.diagnostics == expectedDiagnostics, "expected parse diagnostics \(expectedDiagnostics), but instead got \(delegate.diagnostics)", sourceLocation: sourceLocation)
859882

883+
if let expectedLocations {
884+
#expect(delegate.locations == expectedLocations, "expected parse locations \(expectedLocations), but instead ogt \(delegate.locations)", sourceLocation: sourceLocation)
885+
}
886+
860887
#expect(delegate.includeDirectivesCount == expectedIncludeDirectivesCount, "expected number of configs parsed to be \(expectedIncludeDirectivesCount), but instead got \(delegate.includeDirectivesCount)", sourceLocation: sourceLocation)
861888
}
862889

@@ -885,6 +912,14 @@ func ==(lhs: [DiagnosticInfo], rhs: [DiagnosticInfo]) -> Bool {
885912
return lhs.count == rhs.count && zip(lhs, rhs).filter({ return !($0.0 == $0.1) }).isEmpty
886913
}
887914

915+
func ==(lhs: LocationInfo, rhs: LocationInfo) -> Bool {
916+
return (lhs.macro == rhs.macro) && (lhs.path == rhs.path) && (lhs.line == rhs.line) && (lhs.startColumn == rhs.startColumn) && (lhs.endColumn == rhs.endColumn)
917+
}
918+
919+
func ==(lhs: [LocationInfo], rhs: [LocationInfo]) -> Bool {
920+
return lhs.count == rhs.count && zip(lhs, rhs).filter({ return !($0.0 == $0.1) }).isEmpty
921+
}
922+
888923

889924
/// Private helper function that parses a string representation as either a string or a string list (depending on the parameter), and checks the resulting parser delegate method call sequence and diagnostics (if applicable) against what’s expected. This is a private function that’s called by the two internal test functions TestMacroStringParsing() and TestMacroStringListParsing(). The original file name and line number are passed in so that Xcode diagnostics will refer to the call site. Each diagnostic is provided by the unit test as a tuple containing the level, kind, and associated range (expressed as start and end “distances”, in the manner of Int.Distance, into the original string).
890925
private func TestMacroParsing(_ string: String, asList: Bool, expectedCallLogEntries: [ParseDelegateCallLogEntry], expectedDiagnosticInfos: [(level: MacroExpressionDiagnostic.Level, kind: MacroExpressionDiagnostic.Kind, start: Int, end: Int)], sourceLocation: SourceLocation = #_sourceLocation) {

0 commit comments

Comments
 (0)