Files
gp4/Sources/Common/SourceCode.swift
T
2026-05-11 07:37:23 -04:00

314 lines
9.1 KiB
Swift

// p4rse, Copyright 2026, Will Hawkins
//
// This file is part of p4rse.
//
// This file is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import Foundation
import SystemPackage
/// Represent a location in a post-preprocessed piece of P4 source code.
public struct SourceLocation: Equatable, CustomStringConvertible {
public let range: Range<Int>
public init(_ start: Int, _ extent: Int) {
self.range = start..<(start + extent)
}
public init(_ range: Range<Int>) {
self.range = range
}
public func contains(_ other: SourceLocation) -> Bool {
return self.range.contains(other.range)
}
public var description: String {
return "{\(self.range.lowerBound), \(self.self.range.upperBound - self.range.lowerBound)}"
}
}
/// Represent search paths for P4 code that can be accessed with relative paths.
public struct SourceManager {
let paths: [FilePath]
/// Create a `SourceManager`
///
/// Any relative `FilePath`s in `paths` will be absolutized
/// if a `FileManager` is given.
///
/// parameters:
/// - paths: The include paths searched for files with relative paths.
/// - fm: An optional instance of a `FileManager` that will be used to
/// convert relative paths in `paths` to absolute paths.
public init(_ paths: [FilePath], _ fm: FileManager? = .none) {
// If the user gives a file manager, we will convert relative paths
// to absolute paths. Otherwise, we do not.
guard let fm else {
self.paths = paths
return
}
// There is a file manager, so we should try to absolutize any
// relative paths
self.paths = paths.map {
if !$0.isAbsolute {
return FilePath(fm.currentDirectoryPath + "/" + $0.string).lexicallyNormalized()
}
return $0
}
}
/// Return `FilePath` of `file` in search paths.
///
/// Only if `file` is relative will the search paths be searched.
///
/// parameters:
/// - file: A file to look for in the search paths.
public func firstExisting(_ file: FilePath) -> FilePath? {
if file.isAbsolute {
return file
}
let fm = FileManager()
for path in self.paths {
let combined = path.pushing(file)
if fm.fileExists(atPath: combined.string) {
return combined
}
}
return .none
}
}
/// Represent preprocessed P4 code
///
/// The preprocessed code has metadata to recover the paths of any
/// code generated by a preprocessor directive.
public struct FileSourceLocation: Equatable, CustomStringConvertible {
let location: SourceLocation
let path: FilePath
let nested: [FileSourceLocation]
public init(
_ location: SourceLocation, _ path: FilePath, _ nested: [FileSourceLocation] = Array()
) {
self.location = location
self.path = path
self.nested = nested
}
public func getLocation() -> SourceLocation {
return self.location
}
public func getPath() -> FilePath {
return self.path
}
public func getNestedLocations() -> [FileSourceLocation] {
return self.nested
}
public var description: String {
return "\(self.path): \(self.location) (Nested: "
+ self.nested.map({ location in
return "\(location)"
}).joined(separator: ",") + ")"
}
public func pathForLocation(_ location: Int) -> FilePath? {
let queried_location = SourceLocation(location, 1)
if !self.location.contains(queried_location) {
return .none
}
for nested in self.nested {
if nested.location.contains(queried_location) {
return nested.pathForLocation(location)
}
}
return self.getPath()
}
}
/// Represent preprocessed P4 code.
public struct SourceCode {
let code: String
let manager: SourceManager
let locations: FileSourceLocation
public init(_ contents: String, _ manager: SourceManager, _ locations: FileSourceLocation) {
self.code = contents
self.manager = manager
self.locations = locations
}
public func getManager() -> SourceManager {
return self.manager
}
static func do_annotate(
_ contents: String, _ manager: SourceManager, _ locations: FileSourceLocation,
_ formatter: Formattable, _ style: Style
) -> String {
var result = ""
// Keep track of the start of any gap between nested locations.
var gap_start = contents.startIndex
// contents are devoid of any preceding source code, but the locations do not know that. So,
// when we use a range from locations we must adjust appropriately.
let offset = locations.location.range.lowerBound
for nested in locations.getNestedLocations() {
let nested_start = contents.index(
contents.startIndex, offsetBy: nested.location.range.lowerBound - offset)
let nested_end = contents.index(
contents.startIndex, offsetBy: nested.location.range.upperBound - offset)
// Add in any gap.
result += contents[gap_start..<nested_start]
// Handle this range (recursively)
result += do_annotate(
"\(contents[nested_start ..< nested_end])", manager, nested, formatter, style)
// Adjust where the next gap will start.
gap_start = nested_end
}
// Is there anything left?
let remainder = contents[gap_start...]
return formatter.formatWithStyle(result + remainder, style)
}
public func getSource(
annotated: Bool = false, formatter: Formattable = FormatterDelimited("<", ">"),
initialStyle: Style = Style(StyleColor.Red)
) -> String {
if annotated {
return SourceCode.do_annotate(
self.code, self.manager, self.locations, formatter, initialStyle)
}
return self.code
}
public func getLocations() -> FileSourceLocation {
return self.locations
}
public func pathForLocation(_ location: Int) -> FilePath? {
return self.locations.pathForLocation(location)
}
}
func do_preprocess(
_ file: URL, _ manager: SourceManager, _ starting: Int = 0
) -> Result<(FileSourceLocation, String)> {
// First (1) match group has the name of the include file.
let re = /\#include[\s]*<([^\s>]*)>/
do {
var locations: [FileSourceLocation] = Array()
var contents = try String.init(contentsOf: file, encoding: String.defaultCStringEncoding)
var changed = true
while changed {
changed = false
for match in contents.matches(of: re) {
let before = contents[..<match.range.lowerBound]
let after = contents[match.range.upperBound...]
// Determine whether there is a file with that name in the include path.
guard let included_path = manager.firstExisting(FilePath.init("\(match.1)")) else {
return .Error(
Error(
withMessage:
"Could not open \(match.1) for inclusion"))
}
// Try to make a url from the configured file.
let included_url = URL.init(filePath: included_path.string)
// By calling ourselves recursively, the include being processed will
// be _completely_ expanded (including any nested includes).
switch do_preprocess(included_url, manager, starting + before.count) {
case .Ok((let location, let expanded)):
// Recombine what was before and after the include being processed
// with the expanded text.
contents = before + expanded + after
// Remember the location (and those it has nested) that were found in
// the expanded text.
locations.append(location)
case .Error(let e):
return .Error(e)
}
// Only process one at a time.
changed = true
break
}
}
return .Ok(
(
FileSourceLocation(
SourceLocation(starting..<(starting + contents.count)), FilePath(file.path()), locations),
contents
))
} catch (let e) {
return .Error(Error(withMessage: "\(e)"))
}
}
/// Preprocess P4 code.
public struct SourceCodePreprocessor {
let manager: SourceManager
public init(_ manager: SourceManager) {
self.manager = manager
}
public func preprocess(_ file: FilePath) -> Result<SourceCode> {
// First, decide whether the given file exists at the path the user gave.
let fm = FileManager()
let file_to_open =
if !fm.fileExists(atPath: file.string) {
self.manager.firstExisting(file)
} else {
file
}
guard let file_to_open else {
return .Error(Error(withMessage: "Could not open \(file) for preprocessing"))
}
let url = URL.init(filePath: file_to_open.string)
switch do_preprocess(url, self.manager) {
case .Ok((let location, let contents)):
return .Ok(SourceCode(contents, self.manager, location))
case .Error(let e): return .Error(e)
}
}
}