Integrate import parser with import analysis

Changed files
+134 -19
Sources
PterodactylBuild
PterodactylSyntax
+1
Package.swift
···
name: "PterodactylBuildTests",
dependencies: [
"PterodactylBuild",
+
"PterodactylSyntax",
.product(name: "llbuild2fx", package: "swift-llbuild2")
]
),
+42 -14
Sources/PterodactylBuild/Keys/AnalyseImports.swift
···
import Foundation
import TSCBasic
import llbuild2fx
+
import PterodactylSyntax
+
+
private struct ImportParser {
+
private var lexer: PterodactylSyntax.Lexer
+
public private(set) var imports: [String] = []
+
+
public init(input: String) {
+
self.lexer = PterodactylSyntax.Lexer(input: input)
+
}
+
+
public mutating func parseHeader() {
+
while true {
+
let token = nextSignificantToken()
+
switch token.kind {
+
case .keyword(.import): parseImportStatement()
+
default: return
+
}
+
}
+
}
+
+
/// Returns the next non-whitespace token.
+
private mutating func nextSignificantToken() -> Token {
+
var token = lexer.nextToken()
+
while case .whitespace = token.kind {
+
token = lexer.nextToken()
+
}
+
return token
+
}
+
+
/// Parses a single `import xyz` line.
+
private mutating func parseImportStatement() {
+
let next = nextSignificantToken()
+
guard next.kind == .identifier else { return }
+
imports.append(next.text)
+
}
+
}
+
extension Keys {
struct AnalyseImports: BuildKey {
···
func computeValue(_ ctx: BuildContext<Self>) async throws -> [UnitName] {
let contents = try await ctx.load(blobId)
let code = try await String(decoding: Data(ctx.read(blob: contents.blob!)), as: UTF8.self)
-
-
var results: [UnitName] = []
-
let lines = code.split(separator: "\n", omittingEmptySubsequences: false)
-
-
for line in lines {
-
let trimmed = line.trimmingCharacters(in: .whitespaces)
-
guard trimmed.hasPrefix("import ") else { continue }
-
let parts = trimmed.split(separator: " ", maxSplits: 1, omittingEmptySubsequences: true)
-
if parts.count == 2 {
-
let name = parts[1].trimmingCharacters(in: .whitespaces)
-
results.append(UnitName(name: name))
-
}
+
var importParser = ImportParser(input: code)
+
importParser.parseHeader()
+
+
return importParser.imports.map { name in
+
UnitName(basename: name)
}
-
-
return results
}
}
}
Sources/PterodactylBuild/Keys/Keys.swift Sources/PterodactylBuild/Keys.swift
+5 -5
Sources/PterodactylBuild/Types/UnitName.swift
···
// SPDX-License-Identifier: MPL-2.0
import Foundation
-
import llbuild2fx
import TSCBasic
+
import llbuild2fx
struct UnitName: Codable, Equatable, Hashable {
-
var name: String
+
var basename: String
}
extension UnitName: FXValue {}
extension UnitName {
-
static func fromPath(_ path: AbsolutePath) -> Self {
-
Self(name: path.basenameWithoutExt)
-
}
+
static func fromPath(_ path: AbsolutePath) -> Self {
+
Self(basename: path.basenameWithoutExt)
+
}
}
+73
Sources/PterodactylSyntax/Lexer.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
import Foundation
+
+
public struct Lexer {
+
private let input: String
+
private var index: String.Index
+
private var tokens: [PterodactylSyntax.Token]
+
+
public init(input: String) {
+
self.input = input
+
self.index = input.startIndex
+
self.tokens = []
+
}
+
+
static let keywords: [String: Keyword] = [
+
"import": .import
+
]
+
+
public mutating func nextToken() -> Token {
+
guard !isAtEnd else {
+
return Token(kind: .eof, text: "")
+
}
+
+
let char = currentChar
+
+
if char.isNewline {
+
advance()
+
return Token(kind: .whitespace(.newline), text: String(char))
+
}
+
+
if char.isWhitespace {
+
let text = readWhile { $0.isWhitespace && !$0.isNewline }
+
return Token(kind: .whitespace(.other), text: text)
+
}
+
+
if char.isLetter {
+
let word = readWhile { $0.isLetter || $0.isNumber || $0 == "_" }
+
let kind: TokenKind =
+
if let keyword = Self.keywords[word] {
+
.keyword(keyword)
+
} else {
+
.identifier
+
}
+
return Token(kind: kind, text: word)
+
}
+
+
advance()
+
return Token(kind: .error, text: String(char))
+
}
+
+
private mutating func readWhile(_ condition: (Character) -> Bool) -> String {
+
var result = ""
+
while !isAtEnd && condition(currentChar) {
+
result.append(currentChar)
+
advance()
+
}
+
return result
+
}
+
+
private var currentChar: Character { input[index] }
+
+
private var isAtEnd: Bool {
+
index == input.endIndex
+
}
+
+
private mutating func advance() {
+
guard index < input.endIndex else { return }
+
index = input.index(after: index)
+
}
+
}
+13
Sources/PterodactylSyntax/Types.swift
···
import Foundation
+
public enum Keyword: Codable, Equatable, Sendable {
+
case `import`
+
}
+
+
public enum Whitespace: Codable, Equatable, Sendable {
+
case newline
+
case other
+
}
+
public enum TokenKind: Codable, Equatable, Sendable {
case eof
+
case keyword(Keyword)
+
case error
+
case identifier
+
case whitespace(Whitespace)
}
public enum SyntaxTreeKind: Codable, Equatable, Sendable {