041009a22e
When the user gives relative path names for p4 files, report those in error messages (etc.). The SourceManager can/does resolve those to absolute path names. Signed-off-by: Will Hawkins <hawkinsw@obs.cr>
325 lines
9.7 KiB
Swift
325 lines
9.7 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).lexicallyNormalized()
|
|
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 getSourceSnippet(
|
|
location: SourceLocation, context: Int = 0
|
|
) -> (FilePath, String, String, String)? {
|
|
guard let path = self.pathForLocation(location.range.lowerBound) else {
|
|
return .none
|
|
}
|
|
let lower = String.UTF8View.Index(utf16Offset: location.range.lowerBound, in: self.code)
|
|
let upper = String.UTF8View.Index(utf16Offset: location.range.upperBound, in: self.code)
|
|
let prior_start =
|
|
if location.range.lowerBound - context >= 0 {
|
|
String.UTF8View.Index(utf16Offset: location.range.lowerBound - context, in: self.code)
|
|
} else {
|
|
String.UTF8View.Index(utf16Offset: location.range.lowerBound, in: self.code)
|
|
}
|
|
let after_end =
|
|
if location.range.upperBound + context < self.code.count {
|
|
String.UTF8View.Index(utf16Offset: location.range.upperBound + context, in: self.code)
|
|
} else {
|
|
String.UTF8View.Index(utf16Offset: location.range.upperBound, in: self.code)
|
|
}
|
|
|
|
let result = String(self.code.utf16[lower..<upper])!
|
|
let prior = String(self.code.utf16[prior_start..<lower])!
|
|
let after = String(self.code.utf16[upper...after_end])!
|
|
return (path, result, prior, after)
|
|
}
|
|
|
|
public func getLocations() -> FileSourceLocation {
|
|
return self.locations
|
|
}
|
|
|
|
public func pathForLocation(_ location: Int) -> FilePath? {
|
|
return self.locations.pathForLocation(location)
|
|
}
|
|
}
|
|
|
|
func do_preprocess(
|
|
_ file: FilePath, _ 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()
|
|
|
|
// First, try to find the file.
|
|
guard let included_path = manager.firstExisting(file) else {
|
|
return .Error(
|
|
Error(
|
|
withMessage:
|
|
"Could not open \(file) for preprocessing"))
|
|
}
|
|
|
|
var contents = try String.init(
|
|
contentsOf: URL(filePath: included_path.string), 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...]
|
|
|
|
// By calling ourselves recursively, the include being processed will
|
|
// be _completely_ expanded (including any nested includes).
|
|
switch do_preprocess(FilePath("\(match.1)"), 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)), file, 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> {
|
|
switch do_preprocess(file, self.manager) {
|
|
case .Ok((let location, let contents)):
|
|
return .Ok(SourceCode(contents, self.manager, location))
|
|
case .Error(let e): return .Error(e)
|
|
}
|
|
}
|
|
}
|