Skip to content
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

[Draft] Add buildFilesAreOrderedRule #4

Merged
merged 2 commits into from
Nov 1, 2023

Conversation

aidaan
Copy link
Contributor

@aidaan aidaan commented Oct 24, 2023

This adds the buildFilesAreOrderedRule and some simple tests to validate it.

The buildFilesAreOrderedRule verifies that the items in the PBXBuildFile and PBXFileReference sections of the project file are ordered by their identifiers. In normal use, Xcode tolerates items that are out of order, but writes them out in order. This can create churn on a large project files with several contributors. And this frequently occurs on bad merges or rebases, where the author is not careful to maintain the order when resolving conflicts.

buildFilesAreOrderedRule follows the existing pattern of being a pure function, but this ends up being a bit cumbersome. I added several utility functions to make this code easier to understand. And while these are private, it feels odd having them at the global level. I tried making them nested functions of buildFilesAreOrderedRule(), but that was even harder to read. If these methods were members of specific type, then there would be no risk of collisions with other functions elsewhere in the library. This rule also uses an NSRegularExpression to parse lines in the project file, and this also has to be a global value.

This rule does not operate on the parsed XcodeProj model, instead it operates on the raw text of the project file. Luckily, Environment maintains the URL of the xcodeproj bundle on disk, so it's easy to read this data in. But if there are other rules in the future that also need to operate on the text, then it may be wasteful to read it in multiple times.

I copied the tests in EmbeddedBuildSettingsRuleTests and updated them for this new rule, using a test project file I added. This project includes out of order entries in both the PBXBuildFile and PBXFileReference sections.

You should feel empowered to make any and all edits you deem appropriate, or even reject this PR outright. I'm excited to contribute to this project, but I acknowledge not much is setup yet and you may have a vision for where you want it to go. Thank you!

@mattmassicotte
Copy link
Owner

First, I absolutely love this rule! It's excellent.

I had a feeling the way I did that embeddedBuildSettingsRule was going to be confusing. I was just experimenting, since that particular thing didn't need any state at all. However! Just because XCLinter has a collection of rule-functions does not mean they all have to be that way. What do you think of this?

extension XCLinter {
    public static let defaultRules: [Rule] = [
        embeddedBuildSettingsRule,
        { MyNewRule().doStuff(with: $0) }
    ]
}

final class MyNewRule {
    // state! things! whatever!
    private var value: Int

    init() {
        self.value = 5
    }

    func doStuff(with env: XCLinter.Environment) -> [Violation] {
        return []
    }
}

I was worried that removing the text would be an issue, but I figured since the URL is available, if a Rule actually needed it, it could just grab it. Yeah, it's true this could potentially be a little wasteful. Let's burn that bridge when we come to it!

I'm also kinda surprised that the contents of the XcodeProj structure cannot be used to determine file ordering. It must be encoded in there somewhere, no? Is it that XcodeProject discards some information as part of its parsing process?

@mattmassicotte
Copy link
Owner

Another point you bring up is the mapping of rule identifier to actual code that puts the corresponding Rule function (or whatever) into the list of things XCLinter runs. I believe fundamentally this is always going to require some kind of giant switch statement-y thing. It's also probably possible to save typing by using some NSClassFromString stuff. But I usually find that fragile, and I don't think the typing will be too painful overall. What do you think?

@mattmassicotte
Copy link
Owner

I just began poking around a little, because I was curious. I'm now more confident (but not 100%) that the information you need is available via the XcodeProj structure.

Here's some relevant stuff:

		C965BD2A2AE6E5D700E5836A /* StockMacOSApp */ = {
			isa = PBXGroup;
			children = (
				C965BD2B2AE6E5D700E5836A /* StockMacOSAppApp.swift */,
				C965BD2D2AE6E5D700E5836A /* ContentView.swift */,
				C965BD2F2AE6E5D800E5836A /* Assets.xcassets */,
				C965BD342AE6E5D800E5836A /* StockMacOSApp.entitlements */,
				C965BD312AE6E5D800E5836A /* Preview Content */,
			);
			path = StockMacOSApp;
			sourceTree = "<group>";
		};
(lldb) po project.pbxproj.groups[1].children.map { $0.uuid }
▿ 5 elements
  - 0 : "C965BD2B2AE6E5D700E5836A"
  - 1 : "C965BD2D2AE6E5D700E5836A"
  - 2 : "C965BD2F2AE6E5D800E5836A"
  - 3 : "C965BD342AE6E5D800E5836A"
  - 4 : "C965BD312AE6E5D800E5836A"
(lldb) po project.pbxproj.fileReferences.first(where: { $0.uuid == "C965BD2B2AE6E5D700E5836A" })?.path
▿ Optional<String>
  - some : "StockMacOSAppApp.swift"

I think this proves that, at least in this specific case, the order and name information is available. Not in the most convenient form, mind you, but at least it looks like you may not need to mess around with the text anymore!

@aidaan
Copy link
Contributor Author

aidaan commented Oct 28, 2023

I just began poking around a little, because I was curious. I'm now more confident (but not 100%) that the information you need is available via the XcodeProj structure.

Oh interesting. I remember looking in the past and finding that the items were unordered. But I might be thinking of a different API.

The snippets above look like the items in a PBXGroup, which is ordered (since they are in an array). The order of items in a group is preserved by Xcode, and can be manually changed by the user. The issue I'm trying to address is the items in the PBXBuildFile and PBXFileReference sections of the main object collection. This is technically an unordered collection. The identifier is the key and the object is the value. But, Xcode writes these objects out to disk in order. So at the top of the project file There's this:

// !$*UTF8*$!
{
	archiveVersion = 1;
	classes = {
	};
	objectVersion = 56;
	objects = {

/* Begin PBXBuildFile section */
		C965BD2C2AE6E5D700E5836A /* StockMacOSAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C965BD2B2AE6E5D700E5836A /* StockMacOSAppApp.swift */; };
		C965BD2E2AE6E5D700E5836A /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C965BD2D2AE6E5D700E5836A /* ContentView.swift */; };
		C965BD332AE6E5D800E5836A /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C965BD322AE6E5D800E5836A /* Preview Assets.xcassets */; };
		C965BD302AE6E5D800E5836A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C965BD2F2AE6E5D800E5836A /* Assets.xcassets */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
		C965BD282AE6E5D700E5836A /* StockMacOSApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StockMacOSApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
		C965BD2B2AE6E5D700E5836A /* StockMacOSAppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StockMacOSAppApp.swift; sourceTree = "<group>"; };
		C965BD2D2AE6E5D700E5836A /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
		C965BD322AE6E5D800E5836A /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
		C965BD2F2AE6E5D800E5836A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
		C965BD342AE6E5D800E5836A /* StockMacOSApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = StockMacOSApp.entitlements; sourceTree = "<group>"; };
/* End PBXFileReference section */

The goal of this rule is to avoid churn in these sections when bad merges result in items being out of order. Xcode will tolerate these items being out of order, since it's technically an unordered collection. But my enforcing that items must always be ordered, you can avoid this churn.

I'll take another look at XcodeProj to see if it does provide these object in the order they were specified in the file. If it does, then yes this would definitely be cleaner going through that API.

@aidaan
Copy link
Contributor Author

aidaan commented Oct 28, 2023

Ok yeah, it looks like the PBXObjects type in XcodeProj stores all the objects in unordered collections when it reads them in: https://github.com/tuist/XcodeProj/blob/main/Sources/XcodeProj/Objects/Project/PBXObjects.swift#L193

And it looks like PBXProj itself is constructed using a PropertyListDecoder, so the ordering on disk would be lost:
https://github.com/tuist/XcodeProj/blob/main/Sources/XcodeProj/Objects/Project/PBXProj.swift#L171

@aidaan
Copy link
Contributor Author

aidaan commented Oct 28, 2023

Another point you bring up is the mapping of rule identifier to actual code that puts the corresponding Rule function (or whatever) into the list of things XCLinter runs. I believe fundamentally this is always going to require some kind of giant switch statement-y thing. It's also probably possible to save typing by using some NSClassFromString stuff. But I usually find that fragile, and I don't think the typing will be too painful overall. What do you think?

A big ol' switch is totally fine. In my own project I started that way. I just found it convenient to have the name of the rule defined with the logic. But even in this approach I had a big array of all the rule types. So I'm not sure if there's any real "magic" approach that would be worth it. Certainly nothing using NSClassFromString, I think that only works NSObject subclasses, yeah? And even String(describing: type(of: foo)) isn't much better. I think a big switch would be way more readable and maintainable.

But there's another point to consider maybe in favor of defining types for rules. As rules get more complex, some of them may allow for customization. Imagine a rule that enforces items in groups being in alphabetic order. Such a rule might have a parameter to specifies how subgroups are handled. Either ordered with the files, ordered separately at the top of the group, or at the bottom. Would it be easier to customize such a rule (with data from a config file) if it had a well defined type?

@aidaan
Copy link
Contributor Author

aidaan commented Oct 28, 2023

Updated with a commit to construct a type to contain the logic, as you suggested. And it's pretty nice!

@mattmassicotte
Copy link
Owner

Ah I totally didn't understand what this rule did, but I think I do now. Sorry about the wild goose chase!

Would it be easier to customize such a rule (with data from a config file) if it had a well defined type?

I don't know! I've typically found this kind of general-purpose parameters tough to handle with Codable. But, I think it's doable. I'm absolutely not opposed to a type. I was just playing around. Either way, I think what you have here is really useful. And I also really appreciate you putting this time in. Do you think there's more you want to do? It feels pretty ready to me.

@aidaan aidaan marked this pull request as ready for review November 1, 2023 00:23
@aidaan
Copy link
Contributor Author

aidaan commented Nov 1, 2023

@mattmassicotte Feels ready to merge to me! Go for it!

@mattmassicotte mattmassicotte merged commit a623fcd into mattmassicotte:main Nov 1, 2023
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants