mirror of
https://github.com/github/codeql.git
synced 2026-07-07 04:25:30 +02:00
Compare commits
1 Commits
idrissrio/
...
tausbn/swi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
676c552fb0 |
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -2850,6 +2850,10 @@ version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "swift-syntax-rs"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.106"
|
||||
|
||||
@@ -9,6 +9,7 @@ members = [
|
||||
"ruby/extractor",
|
||||
"unified/extractor",
|
||||
"unified/extractor/tree-sitter-swift",
|
||||
"unified/swift-syntax-rs",
|
||||
"rust/extractor",
|
||||
"rust/extractor/macros",
|
||||
"rust/ast-generator",
|
||||
|
||||
35
MODULE.bazel
35
MODULE.bazel
@@ -24,13 +24,15 @@ bazel_dep(name = "rules_python", version = "1.9.0")
|
||||
bazel_dep(name = "rules_shell", version = "0.7.1")
|
||||
bazel_dep(name = "bazel_skylib", version = "1.9.0")
|
||||
bazel_dep(name = "abseil-cpp", version = "20260107.1", repo_name = "absl")
|
||||
bazel_dep(name = "nlohmann_json", version = "3.11.3", repo_name = "json")
|
||||
bazel_dep(name = "nlohmann_json", version = "3.12.0.bcr.1", repo_name = "json")
|
||||
bazel_dep(name = "fmt", version = "12.1.0-codeql.1")
|
||||
bazel_dep(name = "rules_kotlin", version = "2.2.2-codeql.1")
|
||||
bazel_dep(name = "gazelle", version = "0.50.0")
|
||||
bazel_dep(name = "rules_dotnet", version = "0.21.5-codeql.1")
|
||||
bazel_dep(name = "googletest", version = "1.17.0.bcr.2")
|
||||
bazel_dep(name = "rules_rust", version = "0.69.0")
|
||||
bazel_dep(name = "rules_swift", version = "4.0.0-rc4")
|
||||
bazel_dep(name = "swift-syntax", version = "602.0.0.bcr.2")
|
||||
bazel_dep(name = "zstd", version = "1.5.7.bcr.1")
|
||||
|
||||
bazel_dep(name = "buildifier_prebuilt", version = "6.4.0", dev_dependency = True)
|
||||
@@ -219,6 +221,37 @@ use_repo(
|
||||
"swift-resource-dir-macos",
|
||||
)
|
||||
|
||||
# Hermetic Swift toolchain (from swift.org) for building the `swift-syntax`
|
||||
# based Rust wrapper in `unified/swift-syntax-rs`. This is the official
|
||||
# `rules_swift` standalone-toolchain extension and is independent of the
|
||||
# patched prebuilt toolchain wired up via `swift_deps` above.
|
||||
#
|
||||
# Bazel cannot auto-select between Linux distributions, so the toolchain is
|
||||
# registered explicitly for the distribution our CI runs on (ubuntu24.04,
|
||||
# x86_64). Add further platforms here if CI grows to cover them.
|
||||
#
|
||||
# We register the `exec` toolchain (normal host compilation, targeting
|
||||
# linux/x86_64) rather than the `embedded` one, which targets `os:none`
|
||||
# (Embedded Swift) and would not match a normal host build.
|
||||
#
|
||||
# The Swift version is read from `unified/swift-syntax-rs/.swift-version`, which
|
||||
# is the single source of truth shared with the local `cargo` build (via
|
||||
# swiftly / any Swift install) and `swift/Package.swift`.
|
||||
swift = use_extension("@rules_swift//swift:extensions.bzl", "swift")
|
||||
swift.toolchain(
|
||||
name = "swift_toolchain",
|
||||
swift_version_file = "//unified/swift-syntax-rs:.swift-version",
|
||||
)
|
||||
use_repo(
|
||||
swift,
|
||||
"swift_toolchain",
|
||||
"swift_toolchain_ubuntu24.04",
|
||||
)
|
||||
|
||||
register_toolchains(
|
||||
"@swift_toolchain//:swift_toolchain_exec_ubuntu24.04",
|
||||
)
|
||||
|
||||
node = use_extension("@rules_nodejs//nodejs:extensions.bzl", "node")
|
||||
node.toolchain(
|
||||
name = "nodejs",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
.NET 5, .NET 6, .NET 7, .NET 8, .NET 9, .NET 10","``.sln``, ``.slnx``, ``.csproj``, ``.cs``, ``.cshtml``, ``.xaml``"
|
||||
GitHub Actions,"Not applicable",Not applicable,"``.github/workflows/*.yml``, ``.github/workflows/*.yaml``, ``**/action.yml``, ``**/action.yaml``"
|
||||
Go (aka Golang), "Go up to 1.26", "Go 1.11 or more recent", ``.go``
|
||||
Java,"Java 7 to 27 [6]_","javac (OpenJDK and Oracle JDK),
|
||||
Java,"Java 7 to 26 [6]_","javac (OpenJDK and Oracle JDK),
|
||||
|
||||
Eclipse compiler for Java (ECJ) [7]_",``.java``
|
||||
Kotlin,"Kotlin 1.8.0 to 2.4.0\ *x*","kotlinc",``.kt``
|
||||
@@ -36,7 +36,7 @@
|
||||
.. [3] Objective-C, Objective-C++, C++/CLI, and C++/CX are not supported.
|
||||
.. [4] Support for the clang-cl compiler is preliminary.
|
||||
.. [5] Support for the Arm Compiler (armcc) is preliminary.
|
||||
.. [6] Builds that execute on Java 7 to 27 can be analyzed. The analysis understands standard language features in Java 8 to 27; "preview" and "incubator" features are not supported. Source code using Java language versions older than Java 8 are analyzed as Java 8 code.
|
||||
.. [6] Builds that execute on Java 7 to 26 can be analyzed. The analysis understands standard language features in Java 8 to 26; "preview" and "incubator" features are not supported. Source code using Java language versions older than Java 8 are analyzed as Java 8 code.
|
||||
.. [7] ECJ is supported when the build invokes it via the Maven Compiler plugin or the Takari Lifecycle plugin.
|
||||
.. [8] JSX and Flow code, YAML, JSON, HTML, and XML files may also be analyzed with JavaScript files.
|
||||
.. [9] The extractor requires Python 3 to run. To analyze Python 2.7 you should install both versions of Python.
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
category: minorAnalysis
|
||||
---
|
||||
* The Java extractor and QL libraries now support Java 27.
|
||||
2
unified/swift-syntax-rs/.gitignore
vendored
Normal file
2
unified/swift-syntax-rs/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
/swift/.build
|
||||
1
unified/swift-syntax-rs/.swift-version
Normal file
1
unified/swift-syntax-rs/.swift-version
Normal file
@@ -0,0 +1 @@
|
||||
6.2.4
|
||||
55
unified/swift-syntax-rs/BUILD.bazel
Normal file
55
unified/swift-syntax-rs/BUILD.bazel
Normal file
@@ -0,0 +1,55 @@
|
||||
load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test")
|
||||
load("@rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
# The pinned Swift version, shared with the local `cargo` build and
|
||||
# `swift/Package.swift`. Referenced by the `swift.toolchain` extension in
|
||||
# //:MODULE.bazel via `swift_version_file`.
|
||||
exports_files([".swift-version"])
|
||||
|
||||
# The Swift FFI shim: wraps swift-syntax and exposes a small C ABI
|
||||
# (`ssr_parse_json` / `ssr_string_free`). Built with the hermetic Swift
|
||||
# toolchain registered in //:MODULE.bazel; provides a `CcInfo` that the Rust
|
||||
# targets below link against.
|
||||
swift_library(
|
||||
name = "swift_syntax_ffi",
|
||||
srcs = ["swift/Sources/SwiftSyntaxFFI/SwiftSyntaxFFI.swift"],
|
||||
module_name = "SwiftSyntaxFFI",
|
||||
deps = [
|
||||
"@swift-syntax//:SwiftParser",
|
||||
"@swift-syntax//:SwiftSyntax",
|
||||
],
|
||||
)
|
||||
|
||||
# Safe Rust bindings on top of the C ABI. `build.rs` is intentionally *not*
|
||||
# wired up here (no `cargo_build_script`): under Bazel the Swift side is
|
||||
# provided by `:swift_syntax_ffi`, whereas `build.rs` only exists to build the
|
||||
# Swift shim in the local `cargo` workflow.
|
||||
rust_library(
|
||||
name = "swift_syntax_rs",
|
||||
srcs = ["src/lib.rs"],
|
||||
edition = "2024",
|
||||
deps = [":swift_syntax_ffi"],
|
||||
)
|
||||
|
||||
rust_binary(
|
||||
name = "swift-syntax-parse",
|
||||
srcs = ["src/main.rs"],
|
||||
# The Swift toolchain propagates a runfiles-relative RPATH to its runtime
|
||||
# `.so`s via `swift_syntax_ffi`'s `CcInfo`, but (unlike `swift_binary`)
|
||||
# `rust_binary` does not copy those libraries into runfiles. Add the
|
||||
# toolchain files as `data` so the runtime is found at execution time.
|
||||
# (rules_swift does not yet support statically linking the runtime.)
|
||||
data = ["@swift_toolchain_ubuntu24.04//:files"],
|
||||
edition = "2024",
|
||||
deps = [":swift_syntax_rs"],
|
||||
)
|
||||
|
||||
rust_test(
|
||||
name = "swift_syntax_rs_test",
|
||||
size = "small",
|
||||
crate = ":swift_syntax_rs",
|
||||
data = ["@swift_toolchain_ubuntu24.04//:files"],
|
||||
edition = "2024",
|
||||
)
|
||||
14
unified/swift-syntax-rs/Cargo.toml
Normal file
14
unified/swift-syntax-rs/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "swift-syntax-rs"
|
||||
description = "Rust wrapper around the swift-syntax package for parsing Swift source"
|
||||
version = "0.1.0"
|
||||
authors = ["GitHub"]
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
name = "swift_syntax_rs"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "swift-syntax-parse"
|
||||
path = "src/main.rs"
|
||||
180
unified/swift-syntax-rs/README.md
Normal file
180
unified/swift-syntax-rs/README.md
Normal file
@@ -0,0 +1,180 @@
|
||||
# swift-syntax-rs
|
||||
|
||||
A Rust wrapper around the [swift-syntax](https://github.com/swiftlang/swift-syntax)
|
||||
package, allowing Swift source code to be parsed from Rust.
|
||||
|
||||
Parsing is delegated to a small Swift shim (in [`swift/`](swift/)) that links
|
||||
against `SwiftSyntax`/`SwiftParser` and exposes a tiny C ABI. The Rust crate
|
||||
builds that shim (via `build.rs`) and provides safe bindings on top of it.
|
||||
|
||||
## Output format
|
||||
|
||||
The emitted JSON tree preserves the AST's named structure. Every node has a
|
||||
`kind` and a `range` with `start`/`end` positions (UTF-8 `offset` plus 1-based
|
||||
`line`/`column`). Beyond that:
|
||||
|
||||
- **Tokens** carry `text`, `tokenKind`, and `leadingTrivia`/`trailingTrivia`
|
||||
arrays of `{ kind, text }` pieces.
|
||||
- **Layout nodes** (e.g. `functionDecl`) embed their children directly as
|
||||
members keyed by the child's name in the parent (`name`, `signature`,
|
||||
`body`, …), alongside `kind`/`range`. Absent optional children are omitted.
|
||||
- **Collection nodes** (e.g. `codeBlockItemList`) are elided: a list-valued
|
||||
field is simply a JSON array of its elements (e.g. `parameters`, `statements`).
|
||||
This drops the collection node's own `kind`/`range`.
|
||||
|
||||
Only meaningful trivia is kept — the four comment kinds (`lineComment`,
|
||||
`blockComment`, `docLineComment`, `docBlockComment`) and `unexpectedText`
|
||||
(source the parser skipped). Whitespace trivia is dropped, since node ranges
|
||||
already encode positions.
|
||||
|
||||
### Example
|
||||
|
||||
Parsing `let x = 1 // c` produces the following (each `range` object is
|
||||
abbreviated here as `…`):
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"kind": "sourceFile",
|
||||
"range": …,
|
||||
"statements": [ // collection node elided to an array
|
||||
{
|
||||
"kind": "codeBlockItem",
|
||||
"range": …,
|
||||
"item": {
|
||||
"kind": "variableDecl",
|
||||
"range": …,
|
||||
"attributes": [], // empty collection → empty array
|
||||
"modifiers": [],
|
||||
"bindingSpecifier": { // a token
|
||||
"kind": "token",
|
||||
"text": "let",
|
||||
"tokenKind": "keyword(SwiftSyntax.Keyword.let)",
|
||||
"range": …,
|
||||
"leadingTrivia": [],
|
||||
"trailingTrivia": []
|
||||
},
|
||||
"bindings": [
|
||||
{
|
||||
"kind": "patternBinding",
|
||||
"range": …,
|
||||
"pattern": {
|
||||
"kind": "identifierPattern",
|
||||
"range": …,
|
||||
"identifier": { "kind": "token", "text": "x", "tokenKind": "identifier(\"x\")", "range": …, "leadingTrivia": [], "trailingTrivia": [] }
|
||||
},
|
||||
"initializer": {
|
||||
"kind": "initializerClause",
|
||||
"range": …,
|
||||
"equal": { "kind": "token", "text": "=", "tokenKind": "equal", "range": …, "leadingTrivia": [], "trailingTrivia": [] },
|
||||
"value": {
|
||||
"kind": "integerLiteralExpr",
|
||||
"range": …,
|
||||
"literal": {
|
||||
"kind": "token",
|
||||
"text": "1",
|
||||
"tokenKind": "integerLiteral(\"1\")",
|
||||
"range": …,
|
||||
"leadingTrivia": [],
|
||||
"trailingTrivia": [ { "kind": "lineComment", "text": "// c" } ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"endOfFileToken": { "kind": "token", "text": "", "tokenKind": "endOfFile", "range": …, "leadingTrivia": [], "trailingTrivia": [] }
|
||||
}
|
||||
```
|
||||
|
||||
Note how `statements`, `bindings`, `attributes`, and `modifiers` are plain
|
||||
arrays (their collection nodes are elided), layout children such as
|
||||
`bindingSpecifier` and `initializer` are embedded by name, and the `// c`
|
||||
comment rides along as `trailingTrivia` on the token it follows.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The build does not depend on any particular version manager. You need:
|
||||
|
||||
- **Rust** — pinned to `1.88` by the repo-root [`rust-toolchain.toml`](../../rust-toolchain.toml),
|
||||
which `rustup` picks up automatically.
|
||||
- **Swift** — pinned to the version in [`.swift-version`](.swift-version)
|
||||
(currently `6.2.4`), used to build `swift-syntax` `602.0.0`. Install it any way
|
||||
you like — [swift.org](https://www.swift.org/install/) or
|
||||
[swiftly](https://www.swift.org/swiftly/) (which reads `.swift-version`), or a
|
||||
system package. Just make sure `swift` is on your `PATH` (or point `build.rs`
|
||||
at it with the `SWIFT` environment variable).
|
||||
|
||||
On Debian/Ubuntu the Swift runtime also needs `libncurses6` (and related libs)
|
||||
available on the system.
|
||||
|
||||
## Building & testing
|
||||
|
||||
With `cargo` and `swift` on `PATH`:
|
||||
|
||||
```sh
|
||||
cargo build
|
||||
cargo test
|
||||
```
|
||||
|
||||
If your `swift`/`swiftc` are not on `PATH`, point the build at them explicitly:
|
||||
|
||||
```sh
|
||||
SWIFT=/path/to/swift SWIFTC=/path/to/swiftc cargo build
|
||||
```
|
||||
|
||||
The first build compiles `swift-syntax` and can take several minutes.
|
||||
|
||||
## Building with Bazel (CI)
|
||||
|
||||
CI builds this crate hermetically with Bazel. A Swift toolchain is downloaded
|
||||
from swift.org by the official `rules_swift` standalone toolchain extension
|
||||
(wired up in the repo-root `MODULE.bazel`), `swift-syntax` is pulled from the
|
||||
Bazel Central Registry, and the FFI shim is compiled as a `swift_library` that
|
||||
the Rust targets link against. `build.rs` is not used under Bazel; it only
|
||||
builds the Swift shim for the local `cargo` workflow.
|
||||
|
||||
```sh
|
||||
bazel build //unified/swift-syntax-rs:swift-syntax-parse
|
||||
bazel test //unified/swift-syntax-rs:swift_syntax_rs_test
|
||||
bazel run //unified/swift-syntax-rs:swift-syntax-parse < some.swift
|
||||
```
|
||||
|
||||
Requirements:
|
||||
|
||||
- **`clang`** must be installed on the runner. `rules_swift` requires the Bazel
|
||||
CC toolchain to use clang; the repo's `.bazelrc` already sets
|
||||
`--repo_env=CC=clang`, so no extra flags are needed.
|
||||
- The registered Swift toolchain currently targets **ubuntu24.04 / x86_64**
|
||||
only (Bazel cannot auto-select between Linux distributions). Add more
|
||||
platforms in `MODULE.bazel` (`swift.toolchain` + `register_toolchains`) if CI
|
||||
grows to cover them.
|
||||
|
||||
The Swift compiler version is read from [`.swift-version`](.swift-version) by
|
||||
both the Bazel toolchain (`swift.toolchain(swift_version_file = …)`) and the
|
||||
local build, and is kept in sync with the `swift-syntax` release pinned in
|
||||
`swift/Package.swift`, so local and CI builds behave identically.
|
||||
|
||||
## Usage
|
||||
|
||||
Library:
|
||||
|
||||
```rust
|
||||
let json = swift_syntax_rs::parse_to_json("let x = 1")?;
|
||||
println!("{json}");
|
||||
```
|
||||
|
||||
CLI (reads a file argument or stdin, prints the syntax tree as JSON):
|
||||
|
||||
```sh
|
||||
echo 'let x = 1' | cargo run --bin swift-syntax-parse
|
||||
```
|
||||
|
||||
## Layout
|
||||
|
||||
- `swift/` — Swift package exposing the `ssr_parse_json` / `ssr_string_free` C ABI.
|
||||
- `build.rs` — builds the Swift package and emits link/rpath flags (local `cargo` only).
|
||||
- `BUILD.bazel` — Bazel targets for the hermetic CI build (swift_library + rust targets).
|
||||
- `src/lib.rs` — safe Rust bindings (`parse_to_json`).
|
||||
- `src/main.rs` — demo CLI.
|
||||
102
unified/swift-syntax-rs/build.rs
Normal file
102
unified/swift-syntax-rs/build.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
let swift_dir = manifest_dir.join("swift");
|
||||
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
swift_dir.join("Package.swift").display()
|
||||
);
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
swift_dir
|
||||
.join("Sources/SwiftSyntaxFFI/SwiftSyntaxFFI.swift")
|
||||
.display()
|
||||
);
|
||||
println!("cargo:rerun-if-env-changed=SWIFT");
|
||||
println!("cargo:rerun-if-env-changed=SWIFTC");
|
||||
|
||||
// Build the Swift FFI package as a release dynamic library.
|
||||
let mut command = Command::new(swift_bin());
|
||||
command
|
||||
.args(["build", "-c", "release"])
|
||||
.current_dir(&swift_dir);
|
||||
apply_bare_repository_workaround(&mut command);
|
||||
let status = command.status().unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"failed to run `{swift} build`: {e}\n\
|
||||
Install a Swift toolchain (see https://www.swift.org/install/, e.g. via \
|
||||
swiftly) and ensure `swift` is on PATH, or set the `SWIFT` environment \
|
||||
variable to the `swift` executable. The pinned version is in `.swift-version`.",
|
||||
swift = swift_bin(),
|
||||
)
|
||||
});
|
||||
assert!(status.success(), "`swift build` failed");
|
||||
|
||||
// Link against the freshly built dynamic library.
|
||||
let build_dir = swift_dir.join(".build/release");
|
||||
println!("cargo:rustc-link-search=native={}", build_dir.display());
|
||||
println!("cargo:rustc-link-lib=dylib=SwiftSyntaxFFI");
|
||||
println!("cargo:rustc-link-arg=-Wl,-rpath,{}", build_dir.display());
|
||||
|
||||
// The executable also needs to find the Swift runtime libraries at run time.
|
||||
if let Some(runtime) = swift_runtime_dir() {
|
||||
println!("cargo:rustc-link-search=native={}", runtime.display());
|
||||
println!("cargo:rustc-link-arg=-Wl,-rpath,{}", runtime.display());
|
||||
}
|
||||
}
|
||||
|
||||
/// Query the active Swift toolchain for the directory containing its runtime
|
||||
/// shared libraries (e.g. `libswiftCore.so`).
|
||||
fn swift_runtime_dir() -> Option<PathBuf> {
|
||||
let output = Command::new(swiftc_bin())
|
||||
.arg("-print-target-info")
|
||||
.output()
|
||||
.ok()?;
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
let info = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
// Extract the value of `"runtimeResourcePath": "..."` without pulling in a
|
||||
// JSON dependency.
|
||||
let key = "\"runtimeResourcePath\"";
|
||||
let start = info.find(key)?;
|
||||
let rest = &info[start + key.len()..];
|
||||
let colon = rest.find(':')?;
|
||||
let after = &rest[colon + 1..];
|
||||
let open = after.find('"')?;
|
||||
let value_start = &after[open + 1..];
|
||||
let close = value_start.find('"')?;
|
||||
let resource_path = &value_start[..close];
|
||||
|
||||
Some(PathBuf::from(resource_path).join("linux"))
|
||||
}
|
||||
|
||||
/// The `swift` driver to invoke: `$SWIFT` if set, otherwise `swift` from `PATH`.
|
||||
/// This keeps the build tool-agnostic — any Swift install works; no particular
|
||||
/// version manager is required.
|
||||
fn swift_bin() -> String {
|
||||
env::var("SWIFT").unwrap_or_else(|_| "swift".to_string())
|
||||
}
|
||||
|
||||
/// The `swiftc` compiler to invoke: `$SWIFTC` if set, otherwise `swiftc` from
|
||||
/// `PATH`.
|
||||
fn swiftc_bin() -> String {
|
||||
env::var("SWIFTC").unwrap_or_else(|_| "swiftc".to_string())
|
||||
}
|
||||
|
||||
/// Some environments (notably GitHub Codespaces) inject
|
||||
/// `GIT_CONFIG_KEY_0=safe.bareRepository` / `GIT_CONFIG_VALUE_0=explicit`, which
|
||||
/// breaks the cached bare git repositories `swift build` uses. When exactly that
|
||||
/// key is present, relax it to `all` for the `swift build` subprocess only
|
||||
/// (rather than unconditionally, which could clobber an unrelated
|
||||
/// `GIT_CONFIG_VALUE_0`).
|
||||
fn apply_bare_repository_workaround(command: &mut Command) {
|
||||
if env::var("GIT_CONFIG_KEY_0").as_deref() == Ok("safe.bareRepository") {
|
||||
command.env("GIT_CONFIG_VALUE_0", "all");
|
||||
}
|
||||
}
|
||||
125
unified/swift-syntax-rs/src/lib.rs
Normal file
125
unified/swift-syntax-rs/src/lib.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
//! Rust wrapper around the [swift-syntax](https://github.com/swiftlang/swift-syntax)
|
||||
//! package, allowing Swift source code to be parsed from Rust.
|
||||
//!
|
||||
//! The heavy lifting is done by a small Swift shim (see `swift/`) that links
|
||||
//! against `SwiftSyntax`/`SwiftParser` and exposes a tiny C ABI. This module
|
||||
//! provides safe Rust bindings on top of that ABI.
|
||||
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
// C ABI exported by the `SwiftSyntaxFFI` dynamic library.
|
||||
unsafe extern "C" {
|
||||
/// Parse a NUL-terminated Swift source string, returning a heap-allocated
|
||||
/// NUL-terminated JSON string (or null on failure). The caller owns the
|
||||
/// returned pointer and must release it with `ssr_string_free`.
|
||||
fn ssr_parse_json(source: *const c_char) -> *mut c_char;
|
||||
|
||||
/// Free a string previously returned by `ssr_parse_json`.
|
||||
fn ssr_string_free(ptr: *mut c_char);
|
||||
}
|
||||
|
||||
/// Errors that can occur while parsing Swift source.
|
||||
#[derive(Debug)]
|
||||
pub enum ParseError {
|
||||
/// The provided source contained an interior NUL byte.
|
||||
NulByte,
|
||||
/// The Swift side failed to produce a result.
|
||||
SwiftFailure,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ParseError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ParseError::NulByte => write!(f, "source contained an interior NUL byte"),
|
||||
ParseError::SwiftFailure => write!(f, "swift-syntax failed to parse the source"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ParseError {}
|
||||
|
||||
/// Parse the given Swift `source` and return a JSON representation of its
|
||||
/// syntax tree.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// let json = swift_syntax_rs::parse_to_json("let x = 1").unwrap();
|
||||
/// println!("{json}");
|
||||
/// ```
|
||||
pub fn parse_to_json(source: &str) -> Result<String, ParseError> {
|
||||
let c_source = CString::new(source).map_err(|_| ParseError::NulByte)?;
|
||||
|
||||
// SAFETY: `c_source` is a valid NUL-terminated string for the duration of
|
||||
// the call. The returned pointer, if non-null, is owned by us and freed via
|
||||
// `ssr_string_free` before returning.
|
||||
unsafe {
|
||||
let ptr = ssr_parse_json(c_source.as_ptr());
|
||||
if ptr.is_null() {
|
||||
return Err(ParseError::SwiftFailure);
|
||||
}
|
||||
let json = CStr::from_ptr(ptr).to_string_lossy().into_owned();
|
||||
ssr_string_free(ptr);
|
||||
Ok(json)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_simple_declaration() {
|
||||
let json = parse_to_json("let x = 1").expect("parsing should succeed");
|
||||
assert!(
|
||||
json.contains("\"kind\":\"sourceFile\""),
|
||||
"unexpected tree: {json}"
|
||||
);
|
||||
assert!(json.contains("\"text\":\"x\""), "unexpected tree: {json}");
|
||||
// Source ranges are emitted for every node.
|
||||
assert!(json.contains("\"range\""), "missing ranges: {json}");
|
||||
assert!(
|
||||
json.contains("\"line\"") && json.contains("\"column\"") && json.contains("\"offset\""),
|
||||
"missing location fields: {json}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_named_structure() {
|
||||
let json = parse_to_json("let x = 1").expect("parsing should succeed");
|
||||
// Layout children are embedded directly under their field name.
|
||||
assert!(
|
||||
json.contains("\"initializer\""),
|
||||
"missing initializer field: {json}"
|
||||
);
|
||||
// Collection-valued fields are elided to plain JSON arrays.
|
||||
assert!(
|
||||
json.contains("\"statements\":["),
|
||||
"statements should be an array: {json}"
|
||||
);
|
||||
assert!(
|
||||
json.contains("\"bindings\":["),
|
||||
"bindings should be an array: {json}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_empty_source() {
|
||||
let json = parse_to_json("").expect("parsing empty source should succeed");
|
||||
assert!(
|
||||
json.contains("\"kind\":\"sourceFile\""),
|
||||
"unexpected tree: {json}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn captures_trivia() {
|
||||
// A leading comment is kept as trivia on the token it precedes.
|
||||
let json = parse_to_json("// hi\nlet x = 1").expect("parsing should succeed");
|
||||
assert!(json.contains("\"leadingTrivia\""), "missing trivia: {json}");
|
||||
assert!(
|
||||
json.contains("lineComment"),
|
||||
"comment trivia not captured: {json}"
|
||||
);
|
||||
}
|
||||
}
|
||||
42
unified/swift-syntax-rs/src/main.rs
Normal file
42
unified/swift-syntax-rs/src/main.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
//! Small demo CLI: parse Swift source and print the syntax tree as JSON.
|
||||
//!
|
||||
//! Usage:
|
||||
//! swift-syntax-parse [FILE]
|
||||
//!
|
||||
//! Reads from FILE if given, otherwise from standard input.
|
||||
|
||||
use std::io::Read;
|
||||
use std::process::ExitCode;
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let source = match read_source() {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
eprintln!("error reading source: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
match swift_syntax_rs::parse_to_json(&source) {
|
||||
Ok(json) => {
|
||||
println!("{json}");
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("error: {e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_source() -> std::io::Result<String> {
|
||||
let mut args = std::env::args().skip(1);
|
||||
match args.next() {
|
||||
Some(path) => std::fs::read_to_string(path),
|
||||
None => {
|
||||
let mut buf = String::new();
|
||||
std::io::stdin().read_to_string(&mut buf)?;
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
15
unified/swift-syntax-rs/swift/Package.resolved
Normal file
15
unified/swift-syntax-rs/swift/Package.resolved
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"originHash" : "ad84ad340ee01aa6e38b877dd29e38fa9cb40603dfdefa4e6a69e27f2db208d1",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "swift-syntax",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/swiftlang/swift-syntax.git",
|
||||
"state" : {
|
||||
"revision" : "4799286537280063c85a32f09884cfbca301b1a1",
|
||||
"version" : "602.0.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
}
|
||||
30
unified/swift-syntax-rs/swift/Package.swift
Normal file
30
unified/swift-syntax-rs/swift/Package.swift
Normal file
@@ -0,0 +1,30 @@
|
||||
// swift-tools-version:6.0
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "SwiftSyntaxFFI",
|
||||
products: [
|
||||
// Dynamic library so the produced .so records its dependency on the
|
||||
// Swift runtime libraries, keeping the Rust link step simple.
|
||||
.library(
|
||||
name: "SwiftSyntaxFFI",
|
||||
type: .dynamic,
|
||||
targets: ["SwiftSyntaxFFI"]
|
||||
)
|
||||
],
|
||||
dependencies: [
|
||||
.package(
|
||||
url: "https://github.com/swiftlang/swift-syntax.git",
|
||||
exact: "602.0.0"
|
||||
)
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "SwiftSyntaxFFI",
|
||||
dependencies: [
|
||||
.product(name: "SwiftSyntax", package: "swift-syntax"),
|
||||
.product(name: "SwiftParser", package: "swift-syntax"),
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,145 @@
|
||||
import Foundation
|
||||
import SwiftParser
|
||||
|
||||
// `@_spi(RawSyntax)` exposes the `childName(_:)` helper that maps a child's
|
||||
// key path to its field name in the parent layout; used to emit named fields.
|
||||
@_spi(RawSyntax) import SwiftSyntax
|
||||
|
||||
#if canImport(Glibc)
|
||||
import Glibc
|
||||
#elseif canImport(Darwin)
|
||||
import Darwin
|
||||
#endif
|
||||
|
||||
/// Convert an absolute position into an `{ offset, line, column }` dictionary.
|
||||
///
|
||||
/// `offset` is a UTF-8 byte offset; `line`/`column` are 1-based.
|
||||
private func location(
|
||||
_ position: AbsolutePosition,
|
||||
_ converter: SourceLocationConverter
|
||||
) -> [String: Any] {
|
||||
let loc = converter.location(for: position)
|
||||
return [
|
||||
"offset": position.utf8Offset,
|
||||
"line": loc.line,
|
||||
"column": loc.column,
|
||||
]
|
||||
}
|
||||
|
||||
/// Trivia kinds worth preserving in the serialized tree. Comments carry
|
||||
/// developer intent (including doc comments), and `unexpectedText` flags source
|
||||
/// the parser had to skip. Whitespace and multi-line-string escape markers are
|
||||
/// dropped: node ranges already encode positions, so they would only bloat the
|
||||
/// output.
|
||||
private let keptTriviaKinds: Set<String> = [
|
||||
"lineComment",
|
||||
"blockComment",
|
||||
"docLineComment",
|
||||
"docBlockComment",
|
||||
"unexpectedText",
|
||||
]
|
||||
|
||||
/// Serialize a trivia collection into an array of `{ kind, text }` pieces,
|
||||
/// keeping only the kinds in `keptTriviaKinds`.
|
||||
private func serializeTrivia(_ trivia: Trivia) -> [Any] {
|
||||
trivia.pieces.compactMap { piece -> [String: Any]? in
|
||||
// The label of an enum case mirror is the case name (e.g. "spaces",
|
||||
// "lineComment"), which gives us a stable kind without an exhaustive
|
||||
// switch over every `TriviaPiece` case.
|
||||
let kind = Mirror(reflecting: piece).children.first?.label ?? "\(piece)"
|
||||
guard keptTriviaKinds.contains(kind) else { return nil }
|
||||
return [
|
||||
"kind": kind,
|
||||
"text": Trivia(pieces: [piece]).description,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively convert a SwiftSyntax node into a JSON-serializable value.
|
||||
///
|
||||
/// * Tokens and layout nodes (e.g. `functionDecl`) become objects carrying
|
||||
/// `kind` and source `range`. Layout nodes additionally embed their children
|
||||
/// directly as members keyed by the child's name in the parent (e.g. `name`,
|
||||
/// `signature`, `body`); absent optional children are omitted. Field names
|
||||
/// never collide with `kind`/`range`.
|
||||
/// * Collection nodes (e.g. `codeBlockItemList`) are *elided*: they become a
|
||||
/// plain array of their serialized elements, taking the place of the
|
||||
/// collection node itself. A list-valued layout field (e.g. `parameters`) is
|
||||
/// therefore simply a JSON array. This drops the collection node's own
|
||||
/// `kind`/`range`, which are unnamed and largely recoverable from the
|
||||
/// elements.
|
||||
private func serialize(
|
||||
_ node: Syntax,
|
||||
_ converter: SourceLocationConverter
|
||||
) -> Any {
|
||||
if node.kind.isSyntaxCollection {
|
||||
return node.children(viewMode: .sourceAccurate).map {
|
||||
serialize($0, converter)
|
||||
}
|
||||
}
|
||||
|
||||
// Source range covering the node's content, excluding surrounding trivia.
|
||||
let range: [String: Any] = [
|
||||
"start": location(node.positionAfterSkippingLeadingTrivia, converter),
|
||||
"end": location(node.endPositionBeforeTrailingTrivia, converter),
|
||||
]
|
||||
|
||||
if let token = node.as(TokenSyntax.self) {
|
||||
return [
|
||||
"kind": "token",
|
||||
"tokenKind": "\(token.tokenKind)",
|
||||
"text": token.text,
|
||||
"range": range,
|
||||
"leadingTrivia": serializeTrivia(token.leadingTrivia),
|
||||
"trailingTrivia": serializeTrivia(token.trailingTrivia),
|
||||
]
|
||||
}
|
||||
|
||||
var result: [String: Any] = [
|
||||
"kind": "\(node.kind)",
|
||||
"range": range,
|
||||
]
|
||||
var unnamed = 0
|
||||
for child in node.children(viewMode: .sourceAccurate) {
|
||||
// Layout children are named; embed each under its field name in the
|
||||
// parent (the same mechanism SwiftSyntax uses for its debug dump). A
|
||||
// child that is a collection serializes to an array (see above).
|
||||
if let keyPath = child.keyPathInParent, let name = childName(keyPath) {
|
||||
result[name] = serialize(child, converter)
|
||||
} else {
|
||||
// Defensive fallback for any unnamed layout child.
|
||||
result["child\(unnamed)"] = serialize(child, converter)
|
||||
unnamed += 1
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/// Parse the given NUL-terminated Swift source string and return a
|
||||
/// heap-allocated, NUL-terminated JSON representation of the syntax tree.
|
||||
///
|
||||
/// The returned pointer is owned by the caller and MUST be released with
|
||||
/// `ssr_string_free`. Returns `nil` on failure.
|
||||
@_cdecl("ssr_parse_json")
|
||||
public func ssr_parse_json(_ source: UnsafePointer<CChar>?) -> UnsafeMutablePointer<CChar>? {
|
||||
guard let source = source else { return nil }
|
||||
let code = String(cString: source)
|
||||
let tree = Parser.parse(source: code)
|
||||
let converter = SourceLocationConverter(fileName: "<input>", tree: tree)
|
||||
let json = serialize(Syntax(tree), converter)
|
||||
|
||||
guard
|
||||
let data = try? JSONSerialization.data(
|
||||
withJSONObject: json, options: [.sortedKeys]),
|
||||
let string = String(data: data, encoding: .utf8)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
return strdup(string)
|
||||
}
|
||||
|
||||
/// Free a string previously returned by this library.
|
||||
@_cdecl("ssr_string_free")
|
||||
public func ssr_string_free(_ ptr: UnsafeMutablePointer<CChar>?) {
|
||||
free(ptr)
|
||||
}
|
||||
Reference in New Issue
Block a user