Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ permissions:

env:
CARGO_TERM_COLOR: always
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
check:
name: Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo check
Expand All @@ -26,7 +27,7 @@ jobs:
name: Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
Expand All @@ -36,7 +37,7 @@ jobs:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
Expand All @@ -50,7 +51,7 @@ jobs:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo test
Expand All @@ -59,7 +60,7 @@ jobs:
name: Coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- uses: taiki-e/install-action@cargo-llvm-cov
Expand Down
13 changes: 7 additions & 6 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,31 @@ permissions:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
docker:
name: Build and Push
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4

- name: Log in to GHCR
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
Expand All @@ -45,7 +46,7 @@ jobs:
type=raw,value=latest,enable=${{ !contains(github.ref, '-') }}

- name: Build and push
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
push: true
Expand Down
11 changes: 6 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ permissions:

env:
CARGO_TERM_COLOR: always
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
build:
Expand All @@ -36,7 +37,7 @@ jobs:
artifact: rubyfast.exe

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
Expand Down Expand Up @@ -75,7 +76,7 @@ jobs:
Compress-Archive -Path ${{ matrix.artifact }} -DestinationPath ../../../rubyfast-${{ matrix.target }}.zip

- name: Upload artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: rubyfast-${{ matrix.target }}
path: rubyfast-${{ matrix.target }}.*
Expand All @@ -85,10 +86,10 @@ jobs:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- name: Download artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
path: artifacts
merge-multiple: true
Expand All @@ -109,7 +110,7 @@ jobs:
needs: release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- run: cargo publish
env:
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

93 changes: 93 additions & 0 deletions src/analyzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,97 @@ mod tests {
assert_eq!(byte_offset_to_line(&positions, 6), 2);
assert_eq!(byte_offset_to_line(&positions, 12), 3);
}

#[test]
fn analyze_nonexistent_file_returns_error() {
let config = crate::config::Config::default();
let result = super::analyze_file(std::path::Path::new("/nonexistent.rb"), &config);
assert!(result.is_err());
}

#[test]
fn analyze_file_with_parse_errors_no_ast_returns_error() {
let dir = tempfile::TempDir::new().unwrap();
// This produces a fatal parse error with no recoverable AST
let file = dir.path().join("fatal.rb");
std::fs::write(&file, "\x00\x01\x02").unwrap();
let config = crate::config::Config::default();
let result = super::analyze_file(&file, &config);
// May be Ok with empty offenses or Err depending on parser behavior
// Either way it should not panic
let _ = result;
}

#[test]
fn analyze_file_with_recovered_ast_returns_empty() {
let dir = tempfile::TempDir::new().unwrap();
let file = dir.path().join("recovered.rb");
std::fs::write(&file, "def foo; end; def def; end").unwrap();
let config = crate::config::Config::default();
let result = super::analyze_file(&file, &config);
match result {
Ok(analysis) => assert!(analysis.offenses.is_empty()),
Err(_) => {} // Also acceptable — fatal parse error
}
}

#[test]
fn analyze_empty_file_returns_empty() {
let dir = tempfile::TempDir::new().unwrap();
let file = dir.path().join("empty.rb");
std::fs::write(&file, "").unwrap();
let config = crate::config::Config::default();
let result = super::analyze_file(&file, &config).unwrap();
assert!(result.offenses.is_empty());
}

#[test]
fn analyze_file_with_config_disabling_rule() {
let dir = tempfile::TempDir::new().unwrap();
let file = dir.path().join("test.rb");
std::fs::write(&file, "for x in [1]; end").unwrap();
let config =
crate::config::Config::parse_yaml("speedups:\n for_loop_vs_each: false\n").unwrap();
let result = super::analyze_file(&file, &config).unwrap();
assert!(result.offenses.is_empty());
}

#[test]
fn analyze_file_with_inline_disable() {
let dir = tempfile::TempDir::new().unwrap();
let file = dir.path().join("test.rb");
std::fs::write(
&file,
"for x in [1]; end # rubyfast:disable for_loop_vs_each\n",
)
.unwrap();
let config = crate::config::Config::default();
let result = super::analyze_file(&file, &config).unwrap();
assert!(result.offenses.is_empty());
}

#[test]
fn walk_node_block_with_non_send_call() {
// A numblock (numbered params) has a different call structure
let dir = tempfile::TempDir::new().unwrap();
let file = dir.path().join("test.rb");
std::fs::write(&file, "arr.map { |x| x.to_s }").unwrap();
let config = crate::config::Config::default();
let result = super::analyze_file(&file, &config).unwrap();
// Should find block_vs_symbol_to_proc
assert!(!result.offenses.is_empty());
}

#[test]
fn walk_node_nested_for_inside_method() {
let dir = tempfile::TempDir::new().unwrap();
let file = dir.path().join("test.rb");
std::fs::write(&file, "def foo\n for x in [1,2]; puts x; end\nend\n").unwrap();
let config = crate::config::Config::default();
let result = super::analyze_file(&file, &config).unwrap();
assert!(result
.offenses
.iter()
.any(|o| o.kind == crate::offense::OffenseKind::ForLoopVsEach));
}
}
Loading
Loading