Skip to content
Merged
62 changes: 62 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,65 @@ jobs:

- name: Run cargo clippy
run: cargo clippy --all-targets --all-features
build:
# dist builds releases on these runners, the ones its plan picks for the
# targets in dist-workspace.toml, but only when releasing. Prism is C, so
# each needs a C compiler and the libclang that bindgen uses.
name: Build (${{ matrix.target }})
strategy:
fail-fast: false
matrix:
include:
- target: aarch64-apple-darwin
runner: macos-26
- target: aarch64-unknown-linux-gnu
runner: ubuntu-22.04-arm
- target: x86_64-unknown-linux-gnu
runner: ubuntu-22.04
- target: x86_64-unknown-linux-musl
runner: ubuntu-22.04
- target: x86_64-pc-windows-msvc
runner: windows-2022
# dist links the C runtime statically, so cc builds Prism with /MT
rustflags: -Ctarget-feature=+crt-static
runs-on: ${{ matrix.runner }}
env:
TARGET: ${{ matrix.target }}
RUSTFLAGS: ${{ matrix.rustflags }}
steps:
- name: Checkout sources
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

# As dist does for this target
- name: Install musl-tools
if: matrix.target == 'x86_64-unknown-linux-musl'
run: sudo apt-get update && sudo apt-get install -y musl-tools

- name: Run cargo build
shell: bash
run: |
rustup toolchain install
rustup target add "$TARGET"
cargo build --profile dist --target "$TARGET"

# Only Test Suite runs Prism otherwise, and only on x86_64 Linux
- name: Run pks on a fixture
shell: bash
run: |
"target/$TARGET/dist/pks" --project-root tests/fixtures/simple_app \
--no-cache list-references | grep -F '"constant_name":"::Bar"'
build-all:
# The one check for the ruleset to require, since the matrix's names change
# with its targets. A skipped required check counts as passing, so this runs
# even when a build fails, and fails then.
name: Build
needs: build
if: always()
runs-on: ubuntu-latest
steps:
- name: Check that every target built
env:
RESULT: ${{ needs.build.result }}
run: test "$RESULT" = success
5 changes: 0 additions & 5 deletions .github/workflows/dotslash-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,6 @@
"path": "pks-aarch64-apple-darwin/pks",
"format": "tar.xz"
},
"macos-x86_64": {
"name": "pks-x86_64-apple-darwin.tar.xz",
"path": "pks-x86_64-apple-darwin/pks",
"format": "tar.xz"
},
"linux-aarch64": {
"name": "pks-aarch64-unknown-linux-gnu.tar.xz",
"path": "pks-aarch64-unknown-linux-gnu/pks",
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ Rustfmt is configured with `max_width=80` (`.rustfmt.toml`). CI runs clippy with
- `src/packs/configuration.rs` — `Configuration` struct (the central data object passed everywhere). Built from `packwerk.yml` via `raw_configuration.rs` + `walk_directory.rs`.
- `src/packs/checker.rs` — Core violation-checking engine. Defines `CheckerInterface` and `ValidatorInterface` traits, `Violation`/`ViolationIdentifier` types, and `check_all()`/`update()`/`validate_all()`.
- `src/packs/checker/` — Individual checkers: `dependency`, `privacy`, `visibility`, `layer`, `folder_privacy`. Each implements `CheckerInterface`.
- `src/packs/parsing/` — File parsing. Two modes:
- `src/packs/parsing/` — File parsing, with [Prism](https://github.com/ruby/prism) through the `ruby-prism` crate. Two modes, which resolve constants differently:
- **Packwerk mode** (default): Zeitwerk-based constant resolution from file paths (`parsing/ruby/zeitwerk/`)
- **Experimental mode** (`--experimental-parser`): AST-based using `lib-ruby-parser` (`parsing/ruby/experimental/`)
- **Experimental mode** (`--experimental-parser`): resolution from the definitions parsed out of every file (`parsing/ruby/experimental/`)
- Both Ruby (`.rb`, `.rake`, `Gemfile`, `.gemspec`) and ERB (`.erb`) are supported.
- `src/packs/pack.rs` / `pack_set.rs` — `Pack` struct (represents one `package.yml`) and `PackSet` (the collection).
- `src/packs/package_todo.rs` — Read/write `package_todo.yml` files that record allowed violations.
Expand Down
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
# Changelog

## Unreleased

### Changes

#### pks parses Ruby with Prism

pks now parses Ruby with [Prism](https://github.com/ruby/prism), the parser
Ruby itself has used since 3.4, in place of lib-ruby-parser, which supports
Ruby syntax only up to 3.1 and is no longer maintained. Files that use newer
syntax, such as Ruby 3.2's anonymous argument forwarding (`def foo(*) =
bar(*)`), no longer fail to parse, so pks finds their references and no longer
warns about them.

On files that lib-ruby-parser could parse, the references and definitions pks
finds are the same, except where newer Rubies read code differently, since
Prism reads Ruby as its newest version does. In a block that names no
parameters, `it` is the block's parameter, not a method call, so with
`--experimental-parser` it no longer makes a class count as defined. pks also
warns about syntax that newer Rubies reject, such as `a[k: 1] = v`.

The syntax errors it warns about read as Prism words them, and a reference in
a file with an error can be found where it wasn't before, since Prism recovers
more of the file.

Building pks from source now needs a C compiler and libclang.

#### No Intel macOS binary

Releases no longer include a binary for Intel Macs (`x86_64-apple-darwin`),
and the Apple Silicon binary is built on macOS 26.

## 0.7.0

### Features
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ tracing = "0.1.37" # logging
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } # logging
glob = "0.3.1" # globbing
globset = "0.4" # globbing
lib-ruby-parser = "4.0.6" # ruby parser
ruby-prism = "1.9.0" # ruby parser
md5 = "0.7.0" # md5 hashing to take and compare md5 digests of file contents to ensure cache validity
line-col = "0.2.1" # for creating source maps of violations
ruby_inflector = '0.0.8' # for inflecting strings, e.g. turning `has_many :companies` into `Company`
Expand Down
5 changes: 3 additions & 2 deletions INSTALLATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
- Download the `pks` DotSlash file from a release, for example https://github.com/rubyatscale/pks/releases/latest/download/pks, and save it to your Ruby project's `bin/` directory.
- Run `bin/pks`.

The file pins that release's binaries by hash, so everyone working on the project runs the same version. It covers macOS, Linux (x86_64 and aarch64), and Windows (x86_64).
The file pins that release's binaries by hash, so everyone working on the project runs the same version. It covers macOS (Apple Silicon), Linux (x86_64 and aarch64), and Windows (x86_64).

## Option 2: Prebuilt binary

From v0.5.0, every release publishes prebuilt binaries for macOS, Linux, and Windows, along with installer scripts that put `pks` in `$CARGO_HOME/bin` (usually `~/.cargo/bin`).
From v0.5.0, every release publishes prebuilt binaries for macOS, Linux, and Windows, along with installer scripts that put `pks` in `$CARGO_HOME/bin` (usually `~/.cargo/bin`). Releases after 0.7.0 build macOS binaries for Apple Silicon only.

macOS and Linux:

Expand All @@ -29,6 +29,7 @@ To pin a version, replace `latest/download` with `download/v0.5.0` (or whichever
## Option 3: Build from source

- Install Rust: https://www.rust-lang.org/tools/install
- Have a C compiler and libclang. pks's Ruby parser, [Prism](https://github.com/ruby/prism), is written in C, and its Rust bindings are generated when it builds. Xcode's command line tools provide both on macOS. On Debian and Ubuntu, `build-essential` provides the C compiler, which Rust needs there too, and `libclang-dev` provides libclang. On Windows, install LLVM (`winget install LLVM.LLVM`), and set `LIBCLANG_PATH` to its `bin` directory if it isn't in `C:\Program Files\LLVM`. For other systems, see bindgen's [requirements](https://rust-lang.github.io/rust-bindgen/requirements.html).
- `cargo install --git https://github.com/rubyatscale/pks`

Note that `cargo install pks` installs the crates.io `pks` crate, which is the original [alexevanczuk/packs](https://github.com/alexevanczuk/packs) at 0.2.40, not this repository.
6 changes: 5 additions & 1 deletion dist-workspace.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ installers = ["shell", "powershell"]
targets = [
"aarch64-apple-darwin",
"aarch64-unknown-linux-gnu",
"x86_64-apple-darwin",
"x86_64-unknown-linux-gnu",
"x86_64-unknown-linux-musl",
"x86_64-pc-windows-msvc",
Expand All @@ -31,6 +30,11 @@ post-announce-jobs = ["./publish-dotslash"]
# dispatches one when Cargo.toml's version changes on main.
dispatch-releases = true

# dist's default for this target is macos-14, which GitHub has deprecated.
# ci.yml builds on the same runner.
[dist.github-custom-runners]
aarch64-apple-darwin = "macos-26"

[dist.github-custom-job-permissions]
"publish-dotslash" = { contents = "write" }

Expand Down
2 changes: 1 addition & 1 deletion rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[toolchain]
channel = "1.97.1"
components = ["clippy", "rustfmt"]
targets = ["x86_64-apple-darwin", "aarch64-apple-darwin", "x86_64-unknown-linux-gnu"]
targets = ["aarch64-apple-darwin", "x86_64-unknown-linux-gnu"]
4 changes: 2 additions & 2 deletions src/packs/parsing/erb/experimental/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::packs::{
};
use std::path::Path;

use crate::packs::parsing::ruby::experimental::parser::process_from_contents as process_from_ruby_contents;
use crate::packs::parsing::ruby::experimental::parser::process_from_template_contents;

pub(crate) fn process_from_path(
path: &Path,
Expand All @@ -23,7 +23,7 @@ pub(crate) fn process_from_contents(
) -> ProcessedFile {
let ruby_contents = convert_erb_to_ruby_without_sourcemaps(contents);
let processed_file =
process_from_ruby_contents(ruby_contents, path, configuration);
process_from_template_contents(ruby_contents, path, configuration);
let references = processed_file.unresolved_references;
// let references_without_range = references
let references_without_range = references
Expand Down
4 changes: 2 additions & 2 deletions src/packs/parsing/erb/packwerk/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::packs::{
};
use std::path::Path;

use crate::packs::parsing::ruby::packwerk::parser::process_from_contents as process_from_ruby_contents;
use crate::packs::parsing::ruby::packwerk::parser::process_from_template_contents;

pub(crate) fn process_from_path(
path: &Path,
Expand All @@ -23,7 +23,7 @@ pub(crate) fn process_from_contents(
) -> ProcessedFile {
let ruby_contents = convert_erb_to_ruby_without_sourcemaps(contents);
let processed_file =
process_from_ruby_contents(ruby_contents, path, configuration);
process_from_template_contents(ruby_contents, path, configuration);
let references = processed_file.unresolved_references;
// let references_without_range = references
let references_without_range = references
Expand Down
2 changes: 1 addition & 1 deletion src/packs/parsing/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ fn warn_about_syntax_error(
);
}

/// The first error lib-ruby-parser reports in a file. What it can't parse, pks
/// The first error Prism reports in a file. What it can't parse, pks
/// can't find references in.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct SyntaxError {
Expand Down
67 changes: 67 additions & 0 deletions src/packs/parsing/ruby/experimental.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,73 @@ end
assert_eq!(expected, actual);
}

fn definition_names(contents: &str) -> Vec<String> {
process_from_contents(
contents.to_owned(),
&PathBuf::from("path/to/file.rb"),
&Configuration::default(),
)
.definitions
.into_iter()
.map(|definition| definition.fully_qualified_name)
.collect()
}

#[test]
fn class_definition_with_calls_that_count() {
// Each changes the class's behavior
for body in [
"@a.b",
"BAR.[](1)",
"self.a ||= 1",
"self.a &&= 1",
"self.a += 1",
"self.a, self.b = 1, 2",
"/(?<a>.)/ =~ BAR",
] {
let contents = format!("class Foo\n {}\nend\n", body);
assert_eq!(definition_names(&contents), vec!["::Foo"], "{}", body);
}
}

#[test]
fn class_definition_with_calls_that_dont_count() {
for body in ["BAR[1]", "BAR[1] = 2", "@a&.b", "@a&.b ||= 1"] {
let contents = format!("class Foo\n {}\nend\n", body);
assert!(definition_names(&contents).is_empty(), "{}", body);
}
}

#[test]
fn constant_assignments() {
// Each defines its targets, and only the value assigned is a reference,
// not a target or its scope
for (contents, definitions) in [
("A = Bar", &["::A"][..]),
("A::B = Bar", &["::A::B"]),
("A ||= Bar", &["::A"]),
("A &&= Bar", &["::A"]),
("A += Bar", &["::A"]),
("A::B ||= Bar", &["::A::B"]),
("A::B &&= Bar", &["::A::B"]),
("A::B += Bar", &["::A::B"]),
("A, B::C = Bar", &["::A", "::B::C"]),
("begin\nrescue Bar => A\nend", &["::A"]),
] {
let references: Vec<String> = process_from_contents(
contents.to_owned(),
&PathBuf::from("path/to/file.rb"),
&Configuration::default(),
)
.unresolved_references
.into_iter()
.map(|reference| reference.name)
.collect();
assert_eq!(references, vec!["Bar"], "{}", contents);
assert_eq!(definition_names(contents), definitions, "{}", contents);
}
}

#[test]
fn class_definition_with_private_constant() {
let contents: String = String::from(
Expand Down
Loading
Loading