diff --git a/.cargo/config.toml b/.cargo/config.toml index eb89fa1e55..7e4e7a0f90 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,8 +1,8 @@ [alias] xtask = "run --package xtask --" -# @fb-only: [build] -# @fb-only: target-dir = "../../../buck-out/elp" +# @fb-only +# @fb-only [profile.release] codegen-units = 1 diff --git a/.github/workflows/build-website.yml b/.github/workflows/build-website.yml index 54d9750971..62072bac4f 100644 --- a/.github/workflows/build-website.yml +++ b/.github/workflows/build-website.yml @@ -24,4 +24,4 @@ jobs: - name: Install dependencies run: yarn install --frozen-lockfile - name: Build website - run: yarn build-oss + run: yarn build diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9980283f2..8d9d952c4f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: strategy: fail-fast: false matrix: - platform-arch: [ubuntu-22.04-x64, ubuntu-22.04-arm, macos-15-x64, macos-latest-arm, windows-2022-x64] + platform-arch: [ubuntu-22.04-x64, ubuntu-22.04-arm, macos-13-x64, macos-latest-arm, windows-latest-x64] otp-version: [26.2, 27.3, 28.0] include: - otp-version: 26.2 @@ -55,8 +55,8 @@ jobs: os: linux target: aarch64-unknown-linux-gnu vscode-target: linux-arm64 - - platform-arch: macos-15-x64 - platform: macos-15-intel + - platform-arch: macos-13-x64 + platform: macos-13 os: macos target: x86_64-apple-darwin vscode-target: darwin-x64 @@ -65,8 +65,8 @@ jobs: os: macos target: aarch64-apple-darwin vscode-target: darwin-arm64 - - platform-arch: windows-2022-x64 - platform: windows-2022 + - platform-arch: windows-latest-x64 + platform: windows-latest os: windows target: x86_64-pc-windows-msvc vscode-target: win32-x64 @@ -75,8 +75,12 @@ jobs: steps: - name: Checkout erlang-language-platform uses: "actions/checkout@v3" + - name: Checkout eqwalizer + uses: "actions/checkout@v3" with: - submodules: true + repository: WhatsApp/eqwalizer + path: eqwalizer + ref: main - name: Set up GraalVM uses: graalvm/setup-graalvm@v1 with: @@ -91,14 +95,11 @@ jobs: uses: dtolnay/rust-toolchain@stable with: target: ${{ matrix.target }} - components: rustfmt - name: Set up cross-compiler if: matrix.platform-arch == 'ubuntu-22.04-arm' run: | sudo apt-get update sudo apt-get install -y crossbuild-essential-arm64 - - name: Install Buck2 - uses: dtolnay/install-buck2@latest - id: setup-erlang uses: ./.github/actions/setup-erlang with: @@ -137,7 +138,7 @@ jobs: - name: Test elp # Do not run the tests in case of cross-compilation or on Windows if: matrix.platform-arch != 'macos-latest-arm' && matrix.os != 'windows' - run: 'cargo test --workspace --target ${{ matrix.target }}' + run: 'cargo test --no-default-features --workspace --target ${{ matrix.target }}' - name: Build elp (No Windows) if: matrix.os != 'windows' run: 'cargo build --release --target ${{ matrix.target }} --config target.aarch64-unknown-linux-gnu.linker=\"aarch64-linux-gnu-gcc\"' @@ -202,8 +203,6 @@ jobs: node-version: 20 - name: Install VSCE run: npm install -g vsce - - name: Install OVSX - run: npm install -g ovsx - name: Prepare VS Code Extension to host binaries (No Windows) if: matrix.os != 'windows' run: mkdir -p editors/code/bin @@ -289,7 +288,3 @@ jobs: working-directory: editors/code if: ${{ github.event_name == 'release' && matrix.vscode-publish && matrix.os != 'windows' }} run: vsce publish -p ${{ secrets.VSCE_PAT }} --packagePath erlang-language-platform.vsix - - name: Publish extension to OpenVSX marketplace - working-directory: editors/code - if: ${{ github.event_name == 'release' && matrix.vscode-publish && matrix.os != 'windows' }} - run: ovsx publish -p ${{ secrets.OVSX_PAT }} --packagePath erlang-language-platform.vsix diff --git a/.github/workflows/deploy-website.yml b/.github/workflows/deploy-website.yml index 97d9454702..679cd80c89 100644 --- a/.github/workflows/deploy-website.yml +++ b/.github/workflows/deploy-website.yml @@ -24,7 +24,7 @@ jobs: - name: Install dependencies run: yarn install --frozen-lockfile - name: Build website - run: yarn build-oss + run: yarn build # Popular action to deploy to GitHub Pages: # Docs: https://github.com/peaceiris/actions-gh-pages#%EF%B8%8F-docusaurus diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 1445b3a80b..0000000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "eqwalizer"] - path = eqwalizer - url = https://github.com/WhatsApp/eqwalizer diff --git a/.llms/rules/elp_development.md b/.llms/rules/elp_development.md index efd132ac4b..0f7e215021 100644 --- a/.llms/rules/elp_development.md +++ b/.llms/rules/elp_development.md @@ -1,36 +1,13 @@ --- llms-gk: 'devmate_elp_development_md' apply_to_regex: '^(.*\.rs|.*\.md)$' -oncalls: ['vscode_erlang'] --- -# ELP Development Rules for LLMs (OSS) + +# ELP Development Rules for LLMs ## Project Overview -ELP (Erlang Language Platform) is a language server and development tools suite -for Erlang, built in Rust. This project provides IDE features, diagnostics, and -code analysis for Erlang codebases. - -## Build System - -Use standard Cargo commands: - -```bash -# Build -cargo build --release - -# Run tests -cargo test --workspace - -# Run clippy -cargo clippy --tests - -# Format code -cargo fmt - -# Code generation -cargo xtask codegen -``` +ELP (Erlang Language Platform) is a language server and development tools suite for Erlang, built in Rust. This project provides IDE features, diagnostics, and code analysis for Erlang codebases. ## Diagnostic Code Management @@ -38,13 +15,13 @@ cargo xtask codegen When adding new diagnostic codes to `DiagnosticCode` enum: -1. **Naming Convention**: Use descriptive PascalCase names that clearly indicate - the issue +1. **Naming Convention**: Use descriptive PascalCase names that clearly indicate the issue - Good: `UnusedFunctionArg`, `MissingCompileWarnMissingSpec` - Bad: `Error1`, `BadCode` 2. **Code Assignment**: Follow the established numbering scheme - `W0000-W9999`: Native ELP diagnostics, visible in the OSS version + - `WA000-WA999`: WhatsApp-specific warnings, only visible in Meta builds - Use the next available number in the appropriate range - Never change the number of an existing diagnostic code - Never change the label of an existing diagnostic code @@ -58,8 +35,7 @@ When adding new diagnostic codes to `DiagnosticCode` enum: 4. **Documentation**: Add comments explaining complex diagnostic codes -5. **Documentation File**: Create a corresponding documentation file in the - website +5. **Documentation File**: Create a corresponding documentation file in the website - Location: `website/docs/erlang-error-index/{namespace}/{code}.md` - Example: `W0051` → `website/docs/erlang-error-index/w/W0051.md` - Include frontmatter with `sidebar_position` matching the code number @@ -73,19 +49,16 @@ When adding new diagnostic codes to `DiagnosticCode` enum: ### Creating DiagnosticDescriptor -Every diagnostic must have a corresponding `DiagnosticDescriptor` that defines -when and how the diagnostic runs: +Every diagnostic must have a corresponding `DiagnosticDescriptor` that defines when and how the diagnostic runs: -1. **Static Descriptor Declaration**: Create a public static descriptor in your - diagnostic module +1. **Static Descriptor Declaration**: Create a public static descriptor in your diagnostic module - Use `pub(crate) static DESCRIPTOR: DiagnosticDescriptor` pattern - Define `DiagnosticConditions` with appropriate flags - Provide a checker function that implements the diagnostic logic 2. **Diagnostic Conditions**: Configure when the diagnostic should run - `experimental`: Mark as true for experimental/unstable diagnostics - - `include_generated`: Set to false if diagnostic shouldn't run on generated - code + - `include_generated`: Set to false if diagnostic shouldn't run on generated code - `include_tests`: Set to false if diagnostic shouldn't run on test files - `default_disabled`: Set to true if diagnostic requires explicit enabling @@ -94,8 +67,7 @@ when and how the diagnostic runs: - Push diagnostics to the `diags` vector using `Diagnostic::new()` - Use helper functions to keep the checker clean and focused -4. **Registration**: Add the descriptor to `diagnostics_descriptors()` function - in `diagnostics.rs` +4. **Registration**: Add the descriptor to `diagnostics_descriptors()` function in `diagnostics.rs` - Include your module's `DESCRIPTOR` in the returned vector 5. **Module Structure**: Follow the established pattern @@ -104,6 +76,12 @@ when and how the diagnostic runs: - Include comprehensive tests with `#[cfg(test)]` - Use SSR patterns when appropriate for complex matching +### Meta-Only vs OSS Code + +- Use `@fb-only` and `@oss-only` comments to mark platform-specific code +- Meta-only diagnostics should use `MetaOnlyDiagnosticCode` wrapper +- Ensure OSS builds work by providing fallbacks for Meta-only features + ## Rust Code Style ### Error Handling @@ -138,96 +116,12 @@ when and how the diagnostic runs: - Group related tests in the same module - Use descriptive test names that explain the scenario -### Declarative Test Fixtures - -ELP uses a declarative test fixture system that allows you to write tests with -inline annotations and markers directly in test strings. This system is defined -in `crates/project_model/src/test_fixture.rs`. - -#### Key Features - -1. **File Organization**: Use `//- /path/to/file.erl` to define multiple files - in a single test -2. **Metadata Markers**: Specify app names, include paths, OTP apps, etc. using - metadata after the path -3. **Annotations**: Mark expected diagnostics or ranges using `%% ^^^` syntax -4. **Cursors and Ranges**: Use `~` markers to indicate positions or ranges in - test code - -#### Annotation Syntax - -Annotations allow you to mark expected diagnostics, types, or other information -directly in test code: - -- **Basic annotation**: `%% ^^^ some text` - Points to the range above matching - the caret length -- **Top-of-file marker**: `%% <<< text` (at file start) - Creates annotation at - position 0..0 -- **File-wide annotation**: `%% ^^^file text` - Annotation spans the entire file - contents -- **Left-margin annotation**: `%%<^^^ text` - Annotation starts at `%%` position - instead of first `^` -- **Multiline annotations**: Use continuation lines with `%% | next line` - - Continuation lines are particularly useful for diagnostics with related information: - ```erlang - foo() -> syntax error oops. - %% ^^^^^ error: P1711: syntax error before: error - %% | Related info: 0:45-50 function foo/0 undefined - ``` - -#### Example Test Fixture - -```rust -let fixture = r#" -//- /src/main.erl --module(main). - -foo( -> ok. %% -%% ^ error: W0004: Missing ')'~ -"#; -``` - ### Test Data - Create minimal test cases that focus on specific functionality - Use realistic Erlang code examples in tests - Test both positive and negative cases -### Running Tests for Specific Crates - -When running tests for a specific crate, you need to specify the crate name, not -the directory name. The mapping is: - -| Crate Name | Directory Name | -| -------------------- | ----------------------- | -| `elp` | `crates/elp` | -| `elp_base_db` | `crates/base_db` | -| `elp_eqwalizer` | `crates/eqwalizer` | -| `elp_erlang_service` | `crates/erlang_service` | -| `elp_ide` | `crates/ide` | -| `elp_ide_assists` | `crates/ide_assists` | -| `elp_ide_completion` | `crates/ide_completion` | -| `elp_ide_db` | `crates/ide_db` | -| `elp_ide_ssr` | `crates/ide_ssr` | -| `elp_log` | `crates/elp_log` | -| `elp_project_model` | `crates/project_model` | -| `elp_syntax` | `crates/syntax` | -| `elp_text_edit` | `crates/text_edit` | -| `elp_types_db` | `crates/types_db` | -| `hir` | `crates/hir` | - -Example: To run tests for the `elp_ide` crate: - -```bash -cargo test -p elp_ide -``` - -Or to run tests in a specific directory: - -```bash -cargo test --manifest-path crates/ide/Cargo.toml -``` - ### Existing tests - Do not change existing tests without asking @@ -315,8 +209,14 @@ cargo test --manifest-path crates/ide/Cargo.toml - Collect multiple errors rather than failing on the first one - Provide partial results when full analysis isn't possible +### Tools + +- ELP uses a cargo workspace. +- Inside Meta, use `./meta/cargo.sh` instead of `cargo` +- Inside Meta, use `./meta/clippy.sh` to run clippy +- Use `arc lint --apply-patches` for formatting. + ### Process -- Always run tests before finishing -- Always run `cargo clippy --tests` before submitting PRs -- Use `cargo fmt` for code formatting +- Always run tests before finishing. +- Always run `./meta/cargo.sh clippy --tests` before submitting a diff diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index 7572a84e98..0000000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "ELP: build (debug)", - "type": "shell", - // @fb-only: "command": "./meta/cargo.sh build", - "command": "cargo build", // @oss-only - "group": { - "kind": "build", - "is_default": true, - - }, - "presentation": { - "reveal": "always", - "panel": "new" - } - }, - { - "label": "ELP: build (release)", - "type": "shell", - // @fb-only: "command": "./meta/cargo.sh build --release", - "command": "cargo build --release", // @oss-only - "group": { - "kind": "build", - "is_default": true, - - }, - "presentation": { - "reveal": "always", - "panel": "new" - } - }, - { - "label": "ELP: build (release-thin)", - "type": "shell", - // @fb-only: "command": "./meta/cargo.sh build --profile release-thin --bins", - "command": "cargo build --profile release-thin --bins", // @oss-only - "group": { - "kind": "build", - "is_default": true, - - }, - "presentation": { - "reveal": "always", - "panel": "new" - } - }, - { - "label": "ELP: run clippy on workspace", - "type": "shell", - // @fb-only: "command": "./meta/clippy.sh --workspace --tests", - "command": "cargo clippy --workspace --tests", // @oss-only - "group": { - "kind": "build", - "is_default": true, - - }, - "presentation": { - "reveal": "always", - "panel": "new" - } - }, - { - "label": "ELP: run clippy on workspace, apply fixes", - "type": "shell", - // @fb-only: "command": "./meta/clippy.sh --workspace --tests --fix", - "command": "cargo clippy --workspace --tests --fix", // @oss-only - "group": { - "kind": "build", - "is_default": true, - - }, - "presentation": { - "reveal": "always", - "panel": "new" - } - }, - { - "label": "ELP: run tests on workspace", - "type": "shell", - // @fb-only: "command": "./meta/cargo.sh test --workspace", - "command": "cargo test --workspace", // @oss-only - "group": { - "kind": "build", - "is_default": true, - - }, - "presentation": { - "reveal": "always", - "panel": "new" - } - }, - ] -} diff --git a/Cargo.lock b/Cargo.lock index 2da9907eca..a03c78f3cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -446,10 +446,10 @@ dependencies = [ "crossbeam-channel", "elp_eqwalizer", "elp_ide", - "elp_ide_db", "elp_log", "elp_project_model", "elp_syntax", + "elp_text_edit", "env_logger", "expect-test", "fs_extra", @@ -572,6 +572,7 @@ dependencies = [ "elp_ide_ssr", "elp_project_model", "elp_syntax", + "elp_text_edit", "elp_types_db", "env_logger", "expect-test", @@ -603,6 +604,7 @@ dependencies = [ "cov-mark", "elp_ide_db", "elp_syntax", + "elp_text_edit", "expect-test", "fxhash", "hir", @@ -635,7 +637,6 @@ name = "elp_ide_db" version = "1.1.0" dependencies = [ "anyhow", - "cov-mark", "eetf", "either", "elp_base_db", @@ -643,12 +644,12 @@ dependencies = [ "elp_erlang_service", "elp_project_model", "elp_syntax", + "elp_text_edit", "elp_types_db", "expect-test", "fxhash", "hir", "indexmap 2.9.0", - "itertools 0.10.5", "lazy_static", "log", "memchr", @@ -663,7 +664,6 @@ dependencies = [ "strum", "strum_macros", "tempfile", - "text-size", "toml", "tracing", ] @@ -734,8 +734,10 @@ dependencies = [ name = "elp_syntax" version = "1.1.0" dependencies = [ + "cov-mark", "eetf", "elp_ide_db", + "elp_text_edit", "expect-test", "fxhash", "indexmap 2.9.0", @@ -755,6 +757,14 @@ dependencies = [ "tree-sitter-erlang", ] +[[package]] +name = "elp_text_edit" +version = "1.1.0" +dependencies = [ + "itertools 0.10.5", + "text-size", +] + [[package]] name = "elp_types_db" version = "1.1.0" @@ -2524,11 +2534,10 @@ dependencies = [ [[package]] name = "tree-sitter-erlang" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2091cce4eda19c03d77928c608ac6617445a6a25691dde1e93ac0102467a6be" +version = "0.14.0" dependencies = [ "cc", + "tree-sitter", "tree-sitter-language", ] diff --git a/Cargo.toml b/Cargo.toml index 825530fe4c..7ec49a5f83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,9 +30,13 @@ elp_ide_ssr = { path = "./crates/ide_ssr" } elp_log = { path = "./crates/elp_log" } elp_project_model = { path = "./crates/project_model" } elp_syntax = { path = "./crates/syntax" } +elp_text_edit = { path = "./crates/text_edit" } elp_types_db = { path = "./crates/types_db" } hir = { path = "./crates/hir" } +# Forks +erl_ast = { path = "./crates/erl_ast" } + # External crates trie-rs = "0.4.2" always-assert = "0.1.3" @@ -108,9 +112,8 @@ threadpool = "1.8.1" timeout-readwrite = "0.3.3" toml = "0.5" tree-sitter = "0.23.2" -# When developing the grammar, you may want to point to a local version -# tree-sitter-erlang = { path = "./tree-sitter-erlang" } -tree-sitter-erlang = "0.15.0" +# @fb-only +tree-sitter-erlang = "0.14.0" # @oss-only url = "2.5.4" ustr = { version = "1.1.0", features = ["serde"] } vfs = { git = "https://github.com/rust-lang/rust-analyzer", rev = "2025-03-04" } diff --git a/bench_runner/example_bench/benches/main.rs b/bench_runner/example_bench/benches/main.rs new file mode 100644 index 0000000000..6b2733b5b9 --- /dev/null +++ b/bench_runner/example_bench/benches/main.rs @@ -0,0 +1,60 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is dual-licensed under either the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree or the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. You may select, at your option, one of the + * above-listed licenses. + */ + +use std::thread; +use std::time; + +use criterion::BenchmarkId; +use criterion::Criterion; +use criterion::criterion_group; +use criterion::criterion_main; + +fn fibonacci_slow(n: u64) -> u64 { + match n { + 0 => 1, + 1 => 1, + n => fibonacci_slow(n - 1) + fibonacci_slow(n - 2), + } +} + +fn fibonacci_fast(n: u64) -> u64 { + let mut a = 0; + let mut b = 1; + let millis = time::Duration::from_millis(12); + thread::sleep(millis); + + match n { + 0 => b, + _ => { + for _ in 0..n { + let c = a + b; + a = b; + b = c; + } + b + } + } +} + +fn bench_fibs(c: &mut Criterion) { + let mut group = c.benchmark_group("Fibonacci"); + for i in [20u64, 21u64].iter() { + group.bench_with_input(BenchmarkId::new("Recursive", i), i, |b, i| { + b.iter(|| fibonacci_slow(*i)) + }); + group.bench_with_input(BenchmarkId::new("Iterative", i), i, |b, i| { + b.iter(|| fibonacci_fast(*i)) + }); + } + group.finish(); +} + +criterion_group!(benches, bench_fibs); +criterion_main!(benches); diff --git a/bench_runner/runner/main.rs b/bench_runner/runner/main.rs new file mode 100644 index 0000000000..f895c08c52 --- /dev/null +++ b/bench_runner/runner/main.rs @@ -0,0 +1,16 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is dual-licensed under either the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree or the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. You may select, at your option, one of the + * above-listed licenses. + */ + +use std::env; + +fn main() { + let args: Vec = env::args().collect(); + println!("ARGS: {:?}", args); +} diff --git a/crates/base_db/src/fixture.rs b/crates/base_db/src/fixture.rs index cd1328d14d..574e56e036 100644 --- a/crates/base_db/src/fixture.rs +++ b/crates/base_db/src/fixture.rs @@ -87,7 +87,6 @@ pub trait WithFixture: Default + SourceDatabaseExt + 'static { let (fixture, change) = ChangeFixture::parse(fixture_str); let mut db = Self::default(); change.apply(&mut db, &|path| fixture.resolve_file_id(path)); - fixture.validate(&db); (db, fixture) } } @@ -102,7 +101,6 @@ pub struct ChangeFixture { pub diagnostics_enabled: DiagnosticsEnabled, pub tags: FxHashMap)>>, pub annotations: FxHashMap>, - pub expect_parse_errors: bool, } struct Builder { @@ -174,7 +172,6 @@ impl ChangeFixture { let FixtureWithProjectMeta { fixture, mut diagnostics_enabled, - expect_parse_errors, } = fixture_with_meta.clone(); let builder = Builder::new(diagnostics_enabled.clone()); @@ -298,7 +295,7 @@ impl ChangeFixture { ProjectManifest::discover(&AbsPathBuf::assert(json_config_file.into())).unwrap(); let loaded_project = Project::load( &manifest, - &elp_config, + elp_config.eqwalizer, &BuckQueryConfig::BuildGeneratedCode, &|_| {}, ) @@ -347,7 +344,6 @@ impl ChangeFixture { diagnostics_enabled, tags, annotations, - expect_parse_errors, }, change, project, @@ -409,64 +405,6 @@ impl ChangeFixture { .get(&VfsPath::from(path.clone())) .cloned() } - - /// Validate all files in the fixture for syntax errors. - /// Panics with context if any syntax errors are found. - /// Skips validation if `expect_parse_errors` is set to true. - #[track_caller] - pub fn validate(&self, db: &DB) { - if self.expect_parse_errors { - return; - } - - let mut errors_found = Vec::new(); - - for file_id in &self.files { - let parse = db.parse(*file_id); - let errors = parse.errors(); - - if !errors.is_empty() { - let path = self - .files_by_path - .iter() - .find_map(|(vfs_path, id)| { - if id == file_id { - Some( - vfs_path - .as_path() - .map(|p| p.to_string()) - .unwrap_or_else(|| format!("{:?}", vfs_path)), - ) - } else { - None - } - }) - .unwrap_or_else(|| format!("FileId({:?})", file_id)); - - let file_text = SourceDatabaseExt::file_text(db, *file_id); - let tree = parse.tree(); - errors_found.push((path, file_text.to_string(), errors.to_vec(), tree)); - } - } - - if !errors_found.is_empty() { - let mut message = - String::from("Fixture validation failed: syntax errors found in test fixture\n\n"); - - for (path, text, errors, tree) in errors_found { - message.push_str(&format!("File: {}\n", path)); - message.push_str(&format!("Errors: {:?}\n", errors)); - message.push_str(&format!("Content:\n{}\n", text)); - message.push_str(&format!("Parse Tree:\n{:#?}\n", tree)); - message.push_str("---\n"); - } - message.push_str( - "If this is expected, add `//- expect_parse_errors` to the start of the fixture\n", - ); - - panic!("{}", message); - } - } } fn inc_file_id(file_id: &mut FileId) { @@ -546,8 +484,8 @@ bar() -> ?FOO. app_map: { SourceRootId( 0, - ): AppMapData { - app_data: Some( + ): ( + Some( AppData { project_id: ProjectId( 0, @@ -581,13 +519,12 @@ bar() -> ?FOO. is_test_target: None, }, ), - applicable_files: None, - gen_src_files: None, - }, + None, + ), SourceRootId( 2, - ): AppMapData { - app_data: Some( + ): ( + Some( AppData { project_id: ProjectId( 1, @@ -632,13 +569,12 @@ bar() -> ?FOO. is_test_target: None, }, ), - applicable_files: None, - gen_src_files: None, - }, + None, + ), SourceRootId( 1, - ): AppMapData { - app_data: Some( + ): ( + Some( AppData { project_id: ProjectId( 0, @@ -672,16 +608,14 @@ bar() -> ?FOO. is_test_target: None, }, ), - applicable_files: None, - gen_src_files: None, - }, + None, + ), SourceRootId( 3, - ): AppMapData { - app_data: None, - applicable_files: None, - gen_src_files: None, - }, + ): ( + None, + None, + ), }, project_map: { ProjectId( @@ -807,8 +741,8 @@ foo() -> ?BAR. app_map: { SourceRootId( 0, - ): AppMapData { - app_data: Some( + ): ( + Some( AppData { project_id: ProjectId( 0, @@ -858,16 +792,14 @@ foo() -> ?BAR. is_test_target: None, }, ), - applicable_files: None, - gen_src_files: None, - }, + None, + ), SourceRootId( 1, - ): AppMapData { - app_data: None, - applicable_files: None, - gen_src_files: None, - }, + ): ( + None, + None, + ), }, project_map: { ProjectId( diff --git a/crates/base_db/src/input.rs b/crates/base_db/src/input.rs index eb914e2a7d..29da6ed0c5 100644 --- a/crates/base_db/src/input.rs +++ b/crates/base_db/src/input.rs @@ -15,13 +15,13 @@ use std::sync::Arc; use elp_project_model::AppName; use elp_project_model::AppType; +use elp_project_model::ApplicableFiles; use elp_project_model::EqwalizerConfig; use elp_project_model::Project; use elp_project_model::ProjectAppData; use elp_project_model::buck::IncludeMapping; use elp_project_model::buck::TargetFullName; use fxhash::FxHashMap; -use fxhash::FxHashSet; use paths::RelPath; use paths::Utf8Path; use vfs::AbsPathBuf; @@ -179,23 +179,11 @@ impl AppData { /// Note that `AppStructure` is build-system agnostic #[derive(Debug, Clone, Default /* Serialize, Deserialize */)] pub struct AppStructure { - pub(crate) app_map: FxHashMap, + pub(crate) app_map: FxHashMap, Option)>, pub(crate) project_map: FxHashMap, pub(crate) catch_all_source_root: SourceRootId, } -#[derive(Debug, Clone, Default)] -pub struct AppMapData { - app_data: Option, // TODO: should this be Arc? - applicable_files: Option>, - gen_src_files: Option>, -} - -pub struct ApplyOutput { - pub unresolved_app_id_paths: FxHashMap, - pub gen_src_inputs: FxHashMap, -} - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)] pub struct AppDataId(pub u32); @@ -204,20 +192,13 @@ impl AppStructure { &mut self, source_root_id: SourceRootId, app_data: Option, - applicable_files: Option>, - gen_src_files: Option>, + applicable_files: Option, ) { - let prev = self.app_map.insert( - source_root_id, - AppMapData { - app_data, - applicable_files, - gen_src_files, - }, - ); + let prev = self + .app_map + .insert(source_root_id, (app_data, applicable_files)); assert!(prev.is_none()); } - pub fn add_project_data(&mut self, project_id: ProjectId, project_data: ProjectData) { let prev = self.project_map.insert(project_id, project_data); assert!(prev.is_none()); @@ -228,16 +209,15 @@ impl AppStructure { self, db: &mut dyn SourceDatabaseExt, resolve_file_id: &impl Fn(&AbsPathBuf) -> Option, - ) -> ApplyOutput { + ) -> FxHashMap { let mut app_index = AppDataIndex::default(); let mut app_data_id = AppDataId(0); let mut unresolved_paths = FxHashMap::default(); - let mut gen_src_inputs = FxHashMap::default(); - for (source_root_id, app_map_data) in self.app_map { - let arc_data = app_map_data.app_data.map(Arc::new); + for (source_root_id, (data, applicable_files)) in self.app_map { + let arc_data = data.map(Arc::new); db.set_app_data_by_id(app_data_id, arc_data); db.set_app_data_id(source_root_id, app_data_id); - if let Some(files) = app_map_data.applicable_files { + if let Some(files) = applicable_files { files.iter().for_each(|path| { if let Some(file_id) = resolve_file_id(path) { app_index.map.insert(file_id, app_data_id); @@ -246,11 +226,6 @@ impl AppStructure { } }) } - if let Some(files) = app_map_data.gen_src_files { - for file in files { - gen_src_inputs.insert(file.clone(), app_data_id); - } - } app_data_id = AppDataId(app_data_id.0 + 1); } for (project_id, project_data) in self.project_map { @@ -259,10 +234,7 @@ impl AppStructure { db.set_app_index(Arc::new(app_index)); db.set_catch_all_source_root(self.catch_all_source_root); - ApplyOutput { - unresolved_app_id_paths: unresolved_paths, - gen_src_inputs, - } + unresolved_paths } } @@ -408,12 +380,7 @@ impl<'a> ProjectApps<'a> { ebin_path: app.ebin.clone(), is_test_target: app.is_test_target, }; - app_structure.add_app_data( - root_id, - Some(input_data), - app.applicable_files.clone(), - app.gen_src_files.clone(), - ); + app_structure.add_app_data(root_id, Some(input_data), app.applicable_files.clone()); } let mut app_roots = project_root_map.remove(&project_id).unwrap_or_default(); @@ -436,7 +403,7 @@ impl<'a> ProjectApps<'a> { // Final SourceRoot for out-of-project files log::info!("Final source root: {:?}", SourceRootId(app_idx)); - app_structure.add_app_data(SourceRootId(app_idx), None, None, None); + app_structure.add_app_data(SourceRootId(app_idx), None, None); app_structure.catch_all_source_root = SourceRootId(app_idx); app_structure } diff --git a/crates/base_db/src/lib.rs b/crates/base_db/src/lib.rs index 0cd8df74c9..6b3757ff43 100644 --- a/crates/base_db/src/lib.rs +++ b/crates/base_db/src/lib.rs @@ -32,7 +32,7 @@ mod module_index; // Public API pub mod fixture; -// @fb-only: mod meta_only; +// @fb-only pub mod test_utils; pub use change::Change; pub use elp_project_model::AppType; @@ -476,7 +476,7 @@ static ref IGNORED_SOURCES: Vec = { let regexes: Vec> = vec![ vec![Regex::new(r"^.*_SUITE_data/.+$").unwrap()], //ignore sources goes here - // @fb-only: meta_only::ignored_sources_regexes() + // @fb-only ]; regexes.into_iter().flatten().collect::>() }; diff --git a/crates/elp/Cargo.toml b/crates/elp/Cargo.toml index 2a1b6e8b65..66552ada1d 100644 --- a/crates/elp/Cargo.toml +++ b/crates/elp/Cargo.toml @@ -18,10 +18,10 @@ workspace = true [dependencies] elp_eqwalizer.workspace = true elp_ide.workspace = true -elp_ide_db.workspace = true elp_log.workspace = true elp_project_model.workspace = true elp_syntax.workspace = true +elp_text_edit.workspace = true hir.workspace = true always-assert.workspace = true diff --git a/crates/elp/src/arc_types.rs b/crates/elp/src/arc_types.rs index 374dcdba2f..a1b40a5b25 100644 --- a/crates/elp/src/arc_types.rs +++ b/crates/elp/src/arc_types.rs @@ -8,14 +8,13 @@ * above-listed licenses. */ -// @fb-only: /// Types as defined in https://www.internalfb.com/intern/wiki/Linting/adding-linters/#flow-type -// @fb-only: /// and https://www.internalfb.com/code/fbsource/[1238f73dac0efd4009443fee6a345a680dc9401b]/whatsapp/server/erl/tools/lint/arcanist.py?lines=17 +/// Types as defined in https://www.internalfb.com/intern/wiki/Linting/adding-linters/#flow-type +/// and https://www.internalfb.com/code/fbsource/[1238f73dac0efd4009443fee6a345a680dc9401b]/whatsapp/server/erl/tools/lint/arcanist.py?lines=17 / use std::path::Path; use serde::Serialize; #[derive(Debug, Serialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] pub struct Diagnostic { // Filepath path: String, @@ -30,7 +29,6 @@ pub struct Diagnostic { original: Option, replacement: Option, description: Option, - doc_path: Option, } #[derive(Debug, Serialize, PartialEq, Eq)] @@ -44,7 +42,6 @@ pub enum Severity { } impl Diagnostic { - #[allow(clippy::too_many_arguments)] pub fn new( path: &Path, line: u32, @@ -53,7 +50,6 @@ impl Diagnostic { name: String, description: String, original: Option, - doc_path: Option, ) -> Self { Diagnostic { path: path.display().to_string(), // lossy on Windows for unicode paths @@ -65,7 +61,6 @@ impl Diagnostic { original, replacement: None, description: Some(description), - doc_path, } } } diff --git a/crates/elp/src/bin/args.rs b/crates/elp/src/bin/args.rs index c9790e9314..ecb0e2504f 100644 --- a/crates/elp/src/bin/args.rs +++ b/crates/elp/src/bin/args.rs @@ -11,20 +11,14 @@ use std::cmp::Ordering; use std::env; use std::fs; -use std::io::IsTerminal; use std::path::PathBuf; -use anyhow::Result; -use anyhow::bail; use bpaf::Bpaf; use bpaf::Parser; use bpaf::construct; use bpaf::long; use elp_ide::elp_ide_db::DiagnosticCode; use elp_project_model::buck::BuckQueryConfig; -use hir::fold::MacroStrategy; -use hir::fold::ParenStrategy; -use hir::fold::Strategy; use itertools::Itertools; use serde::Deserialize; @@ -69,20 +63,6 @@ pub struct ParseAllElp { guard(format_guard, "Please use json") )] pub format: Option, - /// Report system memory usage and other statistics - #[bpaf(long("report-system-stats"))] - pub report_system_stats: bool, - /// Minimum severity level to report. Valid values: error, warning, weak_warning, information - #[bpaf( - argument("SEVERITY"), - complete(severity_completer), - fallback(None), - guard( - severity_guard, - "Please use error, warning, weak_warning, or information" - ) - )] - pub severity: Option, } #[derive(Clone, Debug, Bpaf)] @@ -155,6 +135,8 @@ pub struct EqwalizeAll { /// Also eqwalize opted-in generated modules from project (deprecated) #[bpaf(hide)] pub include_generated: bool, + /// Also eqwalize test modules from project + pub include_tests: bool, /// Exit with a non-zero status code if any errors are found pub bail_on_error: bool, /// Print statistics when done @@ -171,6 +153,8 @@ pub struct EqwalizeTarget { /// Also eqwalize opted-in generated modules from application (deprecated) #[bpaf(hide)] pub include_generated: bool, + /// Also eqwalize test modules from project + pub include_tests: bool, /// Exit with a non-zero status code if any errors are found pub bail_on_error: bool, /// target, like //erl/chatd/... @@ -189,6 +173,8 @@ pub struct EqwalizeApp { /// Also eqwalize opted-in generated modules from project (deprecated) #[bpaf(hide)] pub include_generated: bool, + /// Also eqwalize test modules from project + pub include_tests: bool, /// Run with rebar pub rebar: bool, /// Exit with a non-zero status code if any errors are found @@ -211,6 +197,8 @@ pub struct EqwalizeStats { /// Also eqwalize opted-in generated modules from project (deprecated) #[bpaf(hide)] pub include_generated: bool, + /// Also eqwalize test modules from project + pub include_tests: bool, /// If specified, use the provided CLI severity mapping instead of the default one pub use_cli_severity: bool, } @@ -278,6 +266,8 @@ pub struct Lint { guard(format_guard, "Please use json") )] pub format: Option, + /// Optional prefix to prepend to each diagnostic file path. Only used when --format=json is set + pub prefix: Option, /// Include diagnostics produced by erlc pub include_erlc_diagnostics: bool, @@ -330,106 +320,11 @@ pub struct Lint { /// than one at a time. pub one_shot: bool, - /// Report system memory usage and other statistics - #[bpaf(long("report-system-stats"))] - pub report_system_stats: bool, - - /// Disable streaming of diagnostics when applying fixes (collect all before printing) - pub no_stream: bool, - /// Rest of args are space separated list of apps to ignore #[bpaf(positional("IGNORED_APPS"))] pub ignore_apps: Vec, } -#[derive(Clone, Debug, Bpaf)] -pub struct Ssr { - /// Path to directory with project, or to a JSON file (defaults to `.`) - #[bpaf(argument("PROJECT"), fallback(PathBuf::from(".")))] - pub project: PathBuf, - /// Parse a single module from the project, not the entire project. - #[bpaf(argument("MODULE"))] - pub module: Option, - /// Parse a single application from the project, not the entire project. - #[bpaf(long("app"), long("application"), argument("APP"))] - pub app: Option, - /// Parse a single file from the project, not the entire project. This can be an include file or escript, etc. - #[bpaf(argument("FILE"))] - pub file: Option, - - /// Run with rebar - pub rebar: bool, - /// Rebar3 profile to pickup (default is test) - #[bpaf(long("as"), argument("PROFILE"), fallback("test".to_string()))] - pub profile: String, - - /// Also generate diagnostics for generated files - pub include_generated: bool, - /// Also generate diagnostics for test files - pub include_tests: bool, - - /// Show diagnostics in JSON format - #[bpaf( - argument("FORMAT"), - complete(format_completer), - fallback(None), - guard(format_guard, "Please use json") - )] - pub format: Option, - - /// Macro expansion strategy: expand | no-expand | visible-expand (default expand) - #[bpaf( - long("macros"), - argument("STRATEGY"), - complete(macros_completer), - fallback(None), - guard(macros_guard, "Please supply a valid macro expansion value") - )] - pub macro_strategy: Option, - - /// Explicitly match parentheses. If omitted, they are ignored. - #[bpaf(long("parens"))] - pub paren_strategy: bool, - - /// Dump a configuration snippet that can be put in .elp_lint.toml to match the given SSR patterns - pub dump_config: bool, - - /// Show source code context for matches - #[bpaf(long("show-source"))] - pub show_source: bool, - - /// Print NUM lines of leading context, enables --show-source - #[bpaf(short('B'), long("before-context"), argument("NUM"))] - pub before_context: Option, - - /// Print NUM lines of trailing context, enables --show-source - #[bpaf(short('A'), long("after-context"), argument("NUM"))] - pub after_context: Option, - - /// Print NUM lines of output context, enables --show-source - #[bpaf(short('C'), long("context"), argument("NUM"))] - pub context: Option, - - /// Print SEP on line between matches with context, enables --show-source - #[bpaf(long("group-separator"), argument("SEP"))] - pub group_separator: Option, - - /// Do not print separator for matches with context, enables --show-source - #[bpaf(long("no-group-separator"))] - pub no_group_separator: bool, - - /// Report system memory usage and other statistics - #[bpaf(long("report-system-stats"))] - pub report_system_stats: bool, - - /// SSR specs to use - #[bpaf( - positional("SSR_SPECS"), - guard(at_least_1, "there should be at least one spec") - )] - pub ssr_specs: Vec, -} - #[derive(Clone, Debug, Bpaf)] pub struct Explain { /// Error code to explain @@ -454,8 +349,6 @@ pub struct ProjectInfo { pub to: Option, /// Include the buck uquery results in the output pub buck_query: bool, - /// Dump a list of targets and their types - pub target_types: bool, } #[derive(Clone, Debug, Bpaf)] @@ -474,6 +367,8 @@ pub struct Glean { pub pretty: bool, /// Output each fact separately pub multi: bool, + /// Optional prefix to prepend to each fact + pub prefix: Option, } #[derive(Clone, Debug, Bpaf)] @@ -493,7 +388,6 @@ pub enum Command { GenerateCompletions(GenerateCompletions), RunServer(RunServer), Lint(Lint), - Ssr(Ssr), Version(Version), Shell(Shell), Explain(Explain), @@ -517,47 +411,18 @@ pub struct Args { /// When using buck, do not invoke a build step for generated files. pub no_buck_generated: bool, - /// Use buck2 targets for first stage project loading - pub buck_quick_start: bool, - - /// Use color in output; WHEN is 'always', 'never', or 'auto' - #[bpaf( - long("color"), - long("colour"), - argument("WHEN"), - fallback(Some("always".to_string())), - guard(color_guard, "Please use always, never, or auto") - )] - pub color: Option, - #[bpaf(external(command))] pub command: Command, } impl Args { pub fn query_config(&self) -> BuckQueryConfig { - if self.buck_quick_start { - BuckQueryConfig::BuckTargetsOnly - } else if self.no_buck_generated { + if self.no_buck_generated { BuckQueryConfig::NoBuildGeneratedCode } else { BuckQueryConfig::BuildGeneratedCode } } - - /// Determine if color should be used based on the --color argument - pub fn should_use_color(&self) -> bool { - match self.color.as_deref() { - Some("always") => true, - Some("never") => false, - Some("auto") | None => { - // Check NO_COLOR environment variable - if set (regardless of value), disable color - // Also check if stdout is connected to a TTY - env::var("NO_COLOR").is_err() && std::io::stdout().is_terminal() - } - _ => false, // Should be caught by the guard, but handle anyway - } - } } pub fn command() -> impl Parser { @@ -601,8 +466,7 @@ pub fn command() -> impl Parser { .map(Command::EqwalizeStats) .to_options() .command("eqwalize-stats") - .help("Return statistics about code quality for eqWAlizer") - .hide(); + .help("Return statistics about code quality for eqWAlizer"); let dialyze_all = dialyze_all() .map(Command::DialyzeAll) @@ -629,18 +493,6 @@ pub fn command() -> impl Parser { .command("lint") .help("Parse files in project and emit diagnostics, optionally apply fixes."); - let search = ssr() - .map(Command::Ssr) - .to_options() - .command("search") - .help("Alias for 'ssr': Run SSR (Structural Search and Replace) pattern matching on project files."); - - let ssr = ssr() - .map(Command::Ssr) - .to_options() - .command("ssr") - .help("Run SSR (Structural Search and Replace) pattern matching on project files."); - let run_server = run_server() .map(Command::RunServer) .to_options() @@ -684,26 +536,23 @@ pub fn command() -> impl Parser { .help("Dump a JSON config stanza suitable for use in VS Code project.json"); construct!([ - // Note: The order here is what is used for `elp --help` output - version, - run_server, - shell, eqwalize, eqwalize_all, eqwalize_app, eqwalize_target, - eqwalize_stats, dialyze_all, lint, - ssr, - search, + run_server, + generate_completions, parse_all, parse_elp, - explain, build_info, + version, + shell, + eqwalize_stats, + explain, project_info, glean, - generate_completions, config_stanza, ]) .fallback(Help()) @@ -786,48 +635,6 @@ fn format_guard(format: &Option) -> bool { } } -fn severity_completer(_: &Option) -> Vec<(String, Option)> { - vec![ - ("error".to_string(), None), - ("warning".to_string(), None), - ("weak_warning".to_string(), None), - ("information".to_string(), None), - ] -} - -fn severity_guard(severity: &Option) -> bool { - match severity { - None => true, - Some(s) if s == "error" || s == "warning" || s == "weak_warning" || s == "information" => { - true - } - _ => false, - } -} - -fn macros_completer(_: &Option) -> Vec<(String, Option)> { - vec![ - ("expand".to_string(), None), - ("no-expand".to_string(), None), - ("visible-expand".to_string(), None), - ] -} - -fn macros_guard(format: &Option) -> bool { - match format { - None => true, - Some(_) => parse_macro_strategy(format).is_ok(), - } -} - -fn color_guard(color: &Option) -> bool { - match color { - None => true, - Some(c) if c == "always" || c == "never" || c == "auto" => true, - _ => false, - } -} - #[allow(clippy::ptr_arg)] // This is needed in the BPAF macros fn at_least_1(data: &Vec) -> bool { !data.is_empty() @@ -908,44 +715,6 @@ impl Lint { pub fn is_format_json(&self) -> bool { self.format == Some("json".to_string()) } - - /// To prevent flaky test results we allow disabling streaming when applying fixes - pub fn skip_stream_print(&self) -> bool { - self.apply_fix || self.no_stream - } -} - -fn parse_macro_strategy(macro_strategy: &Option) -> Result { - match macro_strategy.as_deref() { - Some("no-expand") => Ok(MacroStrategy::DoNotExpand), - Some("expand") => Ok(MacroStrategy::Expand), - Some("visible-expand") => Ok(MacroStrategy::ExpandButIncludeMacroCall), - None => Ok(MacroStrategy::Expand), - Some(s) => bail!( - "Invalid macro strategy '{}'. Valid options are: expand, no-expand, visible-expand", - s - ), - } -} - -impl Ssr { - pub fn is_format_normal(&self) -> bool { - self.format.is_none() - } - - pub fn is_format_json(&self) -> bool { - self.format == Some("json".to_string()) - } - - pub fn parse_strategy(&self) -> Result { - let macros = parse_macro_strategy(&self.macro_strategy)?; - let parens = if self.paren_strategy { - ParenStrategy::VisibleParens - } else { - ParenStrategy::InvisibleParens - }; - Ok(Strategy { macros, parens }) - } } impl ParseAllElp { diff --git a/crates/elp/src/bin/build_info_cli.rs b/crates/elp/src/bin/build_info_cli.rs index 2308acb2a7..d091fc8e90 100644 --- a/crates/elp/src/bin/build_info_cli.rs +++ b/crates/elp/src/bin/build_info_cli.rs @@ -15,18 +15,15 @@ use std::io::Write; use anyhow::Result; use elp_ide::elp_ide_db::elp_base_db::AbsPath; use elp_ide::elp_ide_db::elp_base_db::AbsPathBuf; -use elp_project_model::AppType; use elp_project_model::ElpConfig; +use elp_project_model::EqwalizerConfig; use elp_project_model::IncludeParentDirs; use elp_project_model::Project; -use elp_project_model::ProjectAppData; use elp_project_model::ProjectBuildData; use elp_project_model::ProjectManifest; use elp_project_model::buck::BuckQueryConfig; -use elp_project_model::buck::BuckTarget; -use elp_project_model::buck::query_buck_targets; +use elp_project_model::buck::query_buck_targets_bxl; use elp_project_model::json::JsonConfig; -use fxhash::FxHashMap; use crate::args::BuildInfo; use crate::args::ProjectInfo; @@ -34,8 +31,8 @@ use crate::args::ProjectInfo; pub(crate) fn save_build_info(args: BuildInfo, query_config: &BuckQueryConfig) -> Result<()> { let root = fs::canonicalize(&args.project)?; let root = AbsPathBuf::assert_utf8(root); - let (elp_config, manifest) = ProjectManifest::discover(&root)?; - let project = Project::load(&manifest, &elp_config, query_config, &|_| {})?; + let (_elp_config, manifest) = ProjectManifest::discover(&root)?; + let project = Project::load(&manifest, EqwalizerConfig::default(), query_config, &|_| {})?; let mut writer = File::create(&args.to)?; let json_str = serde_json::to_string_pretty::(&project.as_json(root))?; writer.write_all(json_str.as_bytes())?; @@ -69,60 +66,17 @@ pub(crate) fn save_project_info(args: ProjectInfo, query_config: &BuckQueryConfi if args.buck_query && let ProjectBuildData::Buck(buck) = &project.project_build_data { - let buck_targets_query = query_buck_targets(&buck.buck_conf, query_config); - if let Ok(targets) = &buck_targets_query { - writer.write_all(format!("{:#?}\n", sort_buck_targets(targets)).as_bytes())?; - } else { - writer.write_all(format!("{:#?}\n", &buck_targets_query).as_bytes())?; - } - } else if args.target_types { - writer.write_all(b"================target types================\n")?; - for line in buck_targets_and_types(&project.project_apps) { - writer.write_all(format!("{}\n", line).as_bytes())?; - } - } else { - writer.write_all(b"================manifest================\n")?; - writer.write_all(format!("{:#?}\n", &manifest).as_bytes())?; - writer.write_all(b"================project_build_data================\n")?; - writer.write_all(format!("{:#?}\n", &project.project_build_data).as_bytes())?; - writer.write_all(b"================project_app_data================\n")?; - writer.write_all(format!("{:#?}\n", &project.project_apps).as_bytes())?; - } - Ok(()) -} - -fn sort_buck_targets(hash_map: &FxHashMap) -> Vec<(String, &BuckTarget)> { - let mut vec = hash_map - .iter() - .map(|(n, t)| (format!("target_name:{}", n), t)) - .collect::>(); - vec.sort_by(|a, b| a.0.cmp(&b.0)); - vec -} - -fn buck_targets_and_types(apps: &[ProjectAppData]) -> Vec { - let tn = |tn| -> String { - if let Some(tn) = tn { - tn - } else { - "".to_string() - } + let buck_targets_query = query_buck_targets_bxl(&buck.buck_conf, query_config); + writer.write_all(b"================buck targets query raw================\n")?; + writer.write_all(format!("{:#?}\n", &buck_targets_query).as_bytes())?; }; - let mut vec = apps - .iter() - .filter(|app| app.app_type != AppType::Otp) - .filter(|app| app.is_buck_generated != Some(true)) - .map(|app| { - format!( - "{:?} {:<30} {}", - app.app_type, - app.name, - tn(app.buck_target_name.clone()) - ) - }) - .collect::>(); - vec.sort(); - vec + writer.write_all(b"================manifest================\n")?; + writer.write_all(format!("{:#?}\n", &manifest).as_bytes())?; + writer.write_all(b"================project_build_data================\n")?; + writer.write_all(format!("{:#?}\n", &project.project_build_data).as_bytes())?; + writer.write_all(b"================project_app_data================\n")?; + writer.write_all(format!("{:#?}\n", &project.project_apps).as_bytes())?; + Ok(()) } fn load_project( @@ -130,7 +84,7 @@ fn load_project( query_config: &BuckQueryConfig, ) -> Result<(ProjectManifest, Project)> { let (elp_config, manifest) = ProjectManifest::discover(root)?; - let project = Project::load(&manifest, &elp_config, query_config, &|_| {})?; + let project = Project::load(&manifest, elp_config.eqwalizer, query_config, &|_| {})?; Ok((manifest, project)) } @@ -140,6 +94,6 @@ fn load_fallback( ) -> Result<(ProjectManifest, Project)> { let manifest = ProjectManifest::discover_no_manifest(root, IncludeParentDirs::Yes); let elp_config = ElpConfig::default(); - let project = Project::load(&manifest, &elp_config, query_config, &|_| {})?; + let project = Project::load(&manifest, elp_config.eqwalizer, query_config, &|_| {})?; Ok((manifest, project)) } diff --git a/crates/elp/src/bin/elp_parse_cli.rs b/crates/elp/src/bin/elp_parse_cli.rs index 770ab60456..6d29ba38e5 100644 --- a/crates/elp/src/bin/elp_parse_cli.rs +++ b/crates/elp/src/bin/elp_parse_cli.rs @@ -14,7 +14,6 @@ use std::io::Write; use std::path::Path; use std::path::PathBuf; use std::str; -use std::time::SystemTime; use anyhow::Result; use anyhow::bail; @@ -22,8 +21,8 @@ use elp::build::load; use elp::build::types::LoadResult; use elp::cli::Cli; use elp::convert; -use elp::memory_usage::MemoryUsage; use elp::otp_file_to_ignore; +use elp::server::file_id_to_url; use elp_eqwalizer::Mode; use elp_ide::Analysis; use elp_ide::diagnostics; @@ -40,7 +39,6 @@ use elp_ide::elp_ide_db::elp_base_db::IncludeOtp; use elp_ide::elp_ide_db::elp_base_db::ModuleName; use elp_ide::elp_ide_db::elp_base_db::Vfs; use elp_ide::elp_ide_db::elp_base_db::VfsPath; -use elp_log::telemetry; use elp_project_model::AppType; use elp_project_model::DiscoverConfig; use elp_project_model::buck::BuckQueryConfig; @@ -55,36 +53,6 @@ use vfs::AbsPath; use crate::args::ParseAllElp; use crate::reporting; -use crate::reporting::print_memory_usage; - -fn parse_severity(severity: &str) -> Option { - match severity { - "error" => Some(diagnostics::Severity::Error), - "warning" => Some(diagnostics::Severity::Warning), - "weak_warning" => Some(diagnostics::Severity::WeakWarning), - "information" => Some(diagnostics::Severity::Information), - _ => None, - } -} - -fn severity_rank(severity: diagnostics::Severity) -> u8 { - match severity { - diagnostics::Severity::Error => 1, - diagnostics::Severity::Warning => 2, - diagnostics::Severity::WeakWarning => 3, - diagnostics::Severity::Information => 4, - } -} - -fn meets_severity_threshold( - diag_severity: diagnostics::Severity, - min_severity: Option, -) -> bool { - match min_severity { - None => true, - Some(min) => severity_rank(diag_severity) <= severity_rank(min), - } -} #[derive(Debug)] struct ParseResult { @@ -100,10 +68,6 @@ pub fn parse_all( ) -> Result<()> { log::info!("Loading project at: {:?}", args.project); - let start_time = SystemTime::now(); - // Track memory usage at the start - let memory_start = MemoryUsage::now(); - let config = DiscoverConfig::new(args.rebar, &args.profile); let loaded = load::load_project_at( cli, @@ -160,7 +124,8 @@ pub fn parse_all( (None, _, true) => do_parse_all_seq(cli, &loaded, &cfg, &args.to)?, (None, _, false) => do_parse_all_par(cli, &loaded, &cfg, &args.to)?, (Some(file_id), Some(name), _) => { - do_parse_one(&analysis, &cfg, &args.to, file_id, &name)?.map_or(vec![], |x| vec![x]) + do_parse_one(&analysis, &loaded.vfs, &cfg, &args.to, file_id, &name)? + .map_or(vec![], |x| vec![x]) } (Some(file_id), _, _) => panic!("Could not get name from file_id for {file_id:?}"), }; @@ -171,32 +136,14 @@ pub fn parse_all( let db = loaded.analysis_host.raw_database(); - telemetry::report_elapsed_time("parse-elp operational", start_time); - - let memory_end = MemoryUsage::now(); - let memory_used = memory_end - memory_start; - - let min_severity = args - .severity - .as_ref() - .and_then(|s| parse_severity(s.as_str())); - - res.retain(|parse_result| { - parse_result - .diagnostics - .diagnostics_for(parse_result.file_id) - .iter() - .any(|diag| meets_severity_threshold(diag.severity, min_severity)) - }); + // We need a `Url` for converting to the lsp_types::Diagnostic for + // printing, but do not print it out. So just create a dummy value + let url = lsp_types::Url::parse("file:///unused_url").ok().unwrap(); if res.is_empty() { if args.is_format_normal() { writeln!(cli, "No errors reported")?; } - if args.is_format_normal() && args.report_system_stats { - print_memory_usage(loaded.analysis_host, loaded.vfs, cli)?; - writeln!(cli, "{}", memory_used)?; - } Ok(()) } else { if args.is_format_normal() { @@ -207,7 +154,6 @@ pub fn parse_all( for diags in res { let mut combined: Vec = diags.diagnostics.diagnostics_for(diags.file_id); - combined.retain(|diag| meets_severity_threshold(diag.severity, min_severity)); if args.is_format_normal() { writeln!(cli, " {}: {}", diags.name, combined.len())?; } @@ -234,19 +180,11 @@ pub fn parse_all( cli, )?; } else { - print_diagnostic(&diag, &line_index, &mut err_in_diag, cli)?; + print_diagnostic(&diag, &line_index, &url, &mut err_in_diag, cli)?; } } } } - - telemetry::report_elapsed_time("parse-elp done", start_time); - - if args.is_format_normal() && args.report_system_stats { - print_memory_usage(loaded.analysis_host, loaded.vfs, cli)?; - writeln!(cli, "{}", memory_used)?; - } - if err_in_diag { bail!("Parse failures found") } else { @@ -279,10 +217,11 @@ fn print_diagnostic_json( fn print_diagnostic( diag: &diagnostics::Diagnostic, line_index: &LineIndex, + url: &lsp_types::Url, err_in_diag: &mut bool, cli: &mut dyn Cli, ) -> Result<(), anyhow::Error> { - let diag = convert::ide_to_lsp_diagnostic(line_index, diag, |_file_id| None); + let diag = convert::ide_to_lsp_diagnostic(line_index, url, diag); let severity = match diag.severity { None => DiagnosticSeverity::ERROR, Some(sev) => { @@ -325,6 +264,7 @@ fn do_parse_all_par( let pb = cli.progress(module_iter.len() as u64, "Parsing modules"); + let vfs = &loaded.vfs; Ok(module_iter .par_bridge() .progress_with(pb) @@ -335,7 +275,7 @@ fn do_parse_all_par( && file_source == FileSource::Src && db.file_app_type(file_id).ok() != Some(Some(AppType::Dep)) { - do_parse_one(db, config, to, file_id, module_name.as_str()).unwrap() + do_parse_one(db, vfs, config, to, file_id, module_name.as_str()).unwrap() } else { None } @@ -356,6 +296,7 @@ fn do_parse_all_seq( let pb = cli.progress(module_iter.len() as u64, "Parsing modules (sequential)"); + let vfs = &loaded.vfs; let db = loaded.analysis(); Ok(module_iter .progress_with(pb) @@ -364,7 +305,7 @@ fn do_parse_all_seq( && file_source == FileSource::Src && db.file_app_type(file_id).ok() != Some(Some(AppType::Dep)) { - do_parse_one(&db, config, to, file_id, module_name.as_str()).unwrap() + do_parse_one(&db, vfs, config, to, file_id, module_name.as_str()).unwrap() } else { None } @@ -374,11 +315,13 @@ fn do_parse_all_seq( fn do_parse_one( db: &Analysis, + vfs: &Vfs, config: &DiagnosticsConfig, to: &Option, file_id: FileId, name: &str, ) -> Result> { + let url = file_id_to_url(vfs, file_id); let native = db.native_diagnostics(config, &vec![], file_id)?; let erlang_service_diagnostics = db.erlang_service_diagnostics(file_id, config, RemoveElpReported::Yes)?; @@ -396,13 +339,11 @@ fn do_parse_one( let mut output = File::create(to_path)?; for diagnostic in native.iter() { - let diagnostic = - convert::ide_to_lsp_diagnostic(&line_index, diagnostic, |_file_id| None); + let diagnostic = convert::ide_to_lsp_diagnostic(&line_index, &url, diagnostic); writeln!(output, "{diagnostic:?}")?; } for diagnostic in erlang_service.iter() { - let diagnostic = - convert::ide_to_lsp_diagnostic(&line_index, diagnostic, |_file_id| None); + let diagnostic = convert::ide_to_lsp_diagnostic(&line_index, &url, diagnostic); writeln!(output, "{diagnostic:?}")?; } } diff --git a/crates/elp/src/bin/eqwalizer_cli.rs b/crates/elp/src/bin/eqwalizer_cli.rs index 141b2157d0..e9159372be 100644 --- a/crates/elp/src/bin/eqwalizer_cli.rs +++ b/crates/elp/src/bin/eqwalizer_cli.rs @@ -10,7 +10,6 @@ use std::path::Path; use std::sync::Arc; -use std::time::SystemTime; use anyhow::Context; use anyhow::Result; @@ -39,7 +38,6 @@ use elp_ide::elp_ide_db::elp_base_db::FileId; use elp_ide::elp_ide_db::elp_base_db::IncludeOtp; use elp_ide::elp_ide_db::elp_base_db::ModuleName; use elp_ide::elp_ide_db::elp_base_db::VfsPath; -use elp_log::telemetry; use elp_project_model::AppName; use elp_project_model::DiscoverConfig; use elp_project_model::ProjectBuildData; @@ -78,7 +76,6 @@ pub fn eqwalize_module( cli: &mut dyn Cli, query_config: &BuckQueryConfig, ) -> Result<()> { - let start_time = SystemTime::now(); let config = DiscoverConfig::new(args.rebar, &args.profile); let mut loaded = load::load_project_at( cli, @@ -89,10 +86,7 @@ pub fn eqwalize_module( query_config, )?; build::compile_deps(&loaded, cli)?; - telemetry::report_elapsed_time("eqwalize operational", start_time); - let r = do_eqwalize_module(args, &mut loaded, cli); - telemetry::report_elapsed_time("eqwalize done", start_time); - r + do_eqwalize_module(args, &mut loaded, cli) } pub fn do_eqwalize_module( @@ -149,7 +143,6 @@ pub fn eqwalize_all( cli: &mut dyn Cli, query_config: &BuckQueryConfig, ) -> Result<()> { - let start_time = SystemTime::now(); // Hack to avoid hint appearing in tests cli.spinner(SHELL_HINT).finish(); let config = DiscoverConfig::new(args.rebar, &args.profile); @@ -162,10 +155,7 @@ pub fn eqwalize_all( query_config, )?; build::compile_deps(&loaded, cli)?; - telemetry::report_elapsed_time("eqwalize-all operational", start_time); - let r = do_eqwalize_all(args, &mut loaded, cli); - telemetry::report_elapsed_time("eqwalize-all done", start_time); - r + do_eqwalize_all(args, &mut loaded, cli) } pub fn do_eqwalize_all( @@ -186,7 +176,10 @@ pub fn do_eqwalize_all( .par_bridge() .progress_with(pb.clone()) .map_with(analysis.clone(), |analysis, (name, _source, file_id)| { - if analysis.should_eqwalize(file_id).unwrap() && !otp_file_to_ignore(analysis, file_id) + if analysis + .should_eqwalize(file_id, args.include_tests) + .unwrap() + && !otp_file_to_ignore(analysis, file_id) { if args.stats { add_stat(name.to_string()); @@ -233,7 +226,6 @@ pub fn eqwalize_app( cli: &mut dyn Cli, query_config: &BuckQueryConfig, ) -> Result<()> { - let start_time = SystemTime::now(); let config = DiscoverConfig::new(args.rebar, &args.profile); let mut loaded = load::load_project_at( cli, @@ -244,10 +236,7 @@ pub fn eqwalize_app( query_config, )?; build::compile_deps(&loaded, cli)?; - telemetry::report_elapsed_time("eqwalize-app operational", start_time); - let r = do_eqwalize_app(args, &mut loaded, cli); - telemetry::report_elapsed_time("eqwalize-app done", start_time); - r + do_eqwalize_app(args, &mut loaded, cli) } pub fn do_eqwalize_app( @@ -266,7 +255,9 @@ pub fn do_eqwalize_app( .iter_own() .filter_map(|(_name, _source, file_id)| { if analysis.file_app_name(file_id).ok()? == Some(AppName(args.app.clone())) - && analysis.should_eqwalize(file_id).unwrap() + && analysis + .should_eqwalize(file_id, args.include_tests) + .unwrap() && !otp_file_to_ignore(analysis, file_id) { Some(file_id) @@ -292,7 +283,6 @@ pub fn eqwalize_target( cli: &mut dyn Cli, query_config: &BuckQueryConfig, ) -> Result<()> { - let start_time = SystemTime::now(); let config = DiscoverConfig::buck(); let mut loaded = load::load_project_at( cli, @@ -304,7 +294,6 @@ pub fn eqwalize_target( )?; set_eqwalizer_config(&mut loaded); - telemetry::report_elapsed_time("eqwalize-target operational", start_time); let buck = match &loaded.project.project_build_data { ProjectBuildData::Buck(buck) => buck, @@ -334,7 +323,9 @@ pub fn eqwalize_target( let vfs_path = VfsPath::from(src.clone()); if let Some((file_id, _)) = loaded.vfs.file_id(&vfs_path) { at_least_one_found = true; - if analysis.should_eqwalize(file_id).unwrap() + if analysis + .should_eqwalize(file_id, args.include_tests) + .unwrap() && !otp_file_to_ignore(analysis, file_id) { file_ids.push(file_id); @@ -362,15 +353,13 @@ elp eqwalize-target erl/chatd #same as //erl/chatd/... but enables shell complet let mut reporter = reporting::PrettyReporter::new(analysis, &loaded, cli); let bail_on_error = args.bail_on_error; - let r = eqwalize(EqwalizerInternalArgs { + eqwalize(EqwalizerInternalArgs { analysis, loaded: &loaded, file_ids, reporter: &mut reporter, bail_on_error, - }); - telemetry::report_elapsed_time("eqwalize-target done", start_time); - r + }) } pub fn eqwalize_stats( @@ -401,7 +390,9 @@ pub fn eqwalize_stats( .par_bridge() .progress_with(pb.clone()) .map_with(analysis.clone(), |analysis, (name, _source, file_id)| { - if analysis.should_eqwalize(file_id).expect("cancelled") + if analysis + .should_eqwalize(file_id, args.include_tests) + .expect("cancelled") && !otp_file_to_ignore(analysis, file_id) { analysis @@ -473,6 +464,8 @@ fn eqwalize( bail!("No files to eqWAlize detected") } + pre_parse_for_speed(reporter, analysis.clone(), &file_ids); + let files_count = file_ids.len(); let pb = reporter.progress(files_count as u64, "EqWAlizing"); let output = loaded.with_eqwalizer_progress_bar(pb.clone(), move |analysis| { @@ -591,6 +584,17 @@ fn eqwalize( } } +fn pre_parse_for_speed(reporter: &dyn Reporter, analysis: Analysis, file_ids: &[FileId]) { + let pb = reporter.progress(file_ids.len() as u64, "Parsing modules"); + file_ids + .par_iter() + .progress_with(pb.clone()) + .for_each_with(analysis, |analysis, &file_id| { + let _ = analysis.module_ast(file_id); + }); + pb.finish(); +} + fn set_eqwalizer_config(loaded: &mut LoadResult) { let config = EqwalizerConfig::default(); let db = loaded.analysis_host.raw_database_mut(); diff --git a/crates/elp/src/bin/erlang_service_cli.rs b/crates/elp/src/bin/erlang_service_cli.rs index 5b39a4aa06..0c1946b329 100644 --- a/crates/elp/src/bin/erlang_service_cli.rs +++ b/crates/elp/src/bin/erlang_service_cli.rs @@ -11,7 +11,6 @@ use std::fs; use std::path::Path; use std::str; -use std::time::SystemTime; use anyhow::Context; use anyhow::Error; @@ -27,7 +26,6 @@ use elp_ide::Analysis; use elp_ide::elp_ide_db::elp_base_db::FileId; use elp_ide::elp_ide_db::elp_base_db::IncludeOtp; use elp_ide::erlang_service::DiagnosticLocation; -use elp_log::telemetry; use elp_log::timeit; use elp_project_model::AppType; use elp_project_model::DiscoverConfig; @@ -42,7 +40,6 @@ use crate::reporting::add_stat; use crate::reporting::dump_stats; pub fn parse_all(args: &ParseAll, cli: &mut dyn Cli, query_config: &BuckQueryConfig) -> Result<()> { - let start_time = SystemTime::now(); let config = DiscoverConfig::new(!args.buck, &args.profile); let loaded = load::load_project_at( cli, @@ -55,15 +52,10 @@ pub fn parse_all(args: &ParseAll, cli: &mut dyn Cli, query_config: &BuckQueryCon build::compile_deps(&loaded, cli)?; fs::create_dir_all(&args.to)?; - telemetry::report_elapsed_time("parse-all operational", start_time); - let parse_diagnostics = do_parse_all(cli, &loaded, &args.to, &args.module, args.buck)?; if args.stats { dump_stats(cli, args.list_modules); } - - telemetry::report_elapsed_time("parse-all done", start_time); - if !parse_diagnostics.is_empty() { writeln!( cli, @@ -150,15 +142,14 @@ pub fn do_parse_one( .chain(result.warnings.iter()) .map(|err| { let relative_path: &Path = err.path.strip_prefix(root_dir).unwrap_or(&err.path); - let (range, line_num) = match &err.location { + let (range, line_num) = match err.location { None => (None, convert::position(&line_index, 0.into()).line + 1), Some(DiagnosticLocation::Normal(range)) => ( Some(range), convert::position(&line_index, range.start()).line + 1, ), Some(DiagnosticLocation::Included { - file_attribute_location: directive_location, - error_path: _, + directive_location, error_location: _, }) => ( Some(directive_location), @@ -170,7 +161,7 @@ pub fn do_parse_one( relative_path: relative_path.to_owned(), line_num, msg: err.msg.to_owned(), - range: range.copied(), + range, } }) .collect(); diff --git a/crates/elp/src/bin/glean.rs b/crates/elp/src/bin/glean.rs index cb420261d2..f9566cb870 100644 --- a/crates/elp/src/bin/glean.rs +++ b/crates/elp/src/bin/glean.rs @@ -8,9 +8,9 @@ * above-listed licenses. */ -use core::option::Option::None; use std::io::Write; use std::mem; +use std::path::Path; use anyhow::Result; use elp::build::load; @@ -24,7 +24,6 @@ use elp_ide::elp_ide_db::EqwalizerDatabase; use elp_ide::elp_ide_db::LineIndexDatabase; use elp_ide::elp_ide_db::RootDatabase; use elp_ide::elp_ide_db::docs::DocDatabase; -use elp_ide::elp_ide_db::docs::Documentation; use elp_ide::elp_ide_db::elp_base_db::FileId; use elp_ide::elp_ide_db::elp_base_db::IncludeOtp; use elp_ide::elp_ide_db::elp_base_db::ModuleName; @@ -52,11 +51,9 @@ use hir::DefineId; use hir::Expr; use hir::ExprId; use hir::ExprSource; -use hir::File; use hir::InFile; use hir::Literal; use hir::MacroName; -use hir::Module; use hir::Name; use hir::NameArity; use hir::PPDirective; @@ -84,7 +81,7 @@ const REC_ARITY: u32 = 99; const HEADER_ARITY: u32 = 100; const FACTS_FILE: &str = "facts.json"; -// @fb-only: mod meta_only; +// @fb-only #[derive(Serialize, Debug, Eq, Hash, PartialEq, Clone)] struct GleanFileId(u32); @@ -92,6 +89,7 @@ struct GleanFileId(u32); #[derive(Clone, Debug, Default)] struct IndexConfig { pub multi: bool, + pub prefix: Option, } impl From for FileId { @@ -145,45 +143,6 @@ impl FileLinesFact { } } -#[derive(Serialize, Debug)] -pub(crate) struct ModuleFact { - #[serde(rename = "file")] - file_id: GleanFileId, - name: String, - #[serde(skip_serializing_if = "Option::is_none")] - oncall: Option, - #[serde(skip_serializing_if = "Option::is_none")] - exports: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - behaviours: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - module_doc: Option, - #[serde(skip_serializing_if = "Option::is_none")] - exdoc_link: Option, -} - -impl ModuleFact { - fn new( - file_id: FileId, - name: String, - oncall: Option, - exports: Option>, - behaviours: Option>, - module_doc: Option, - exdoc_link: Option, - ) -> Self { - Self { - file_id: file_id.into(), - name, - oncall, - exports, - behaviours, - module_doc, - exdoc_link, - } - } -} - #[derive(Serialize, Debug)] pub(crate) struct FunctionDeclarationFact { #[serde(rename = "file")] @@ -278,8 +237,6 @@ pub(crate) enum Fact { XRef { facts: Vec> }, #[serde(rename = "erlang.DeclarationComment")] DeclarationComment { facts: Vec> }, - #[serde(rename = "erlang.Module")] - Module { facts: Vec> }, } #[derive(Serialize, Debug)] @@ -343,10 +300,6 @@ pub(crate) struct MacroTarget { expansion: Option, #[serde(skip_serializing_if = "Option::is_none")] ods_url: Option, - #[serde(skip_serializing_if = "Option::is_none")] - logview_url: Option, - #[serde(skip_serializing_if = "Option::is_none")] - scuba_urls: Option>, } #[derive(Serialize, Debug)] @@ -361,8 +314,6 @@ pub(crate) struct RecordTarget { #[serde(rename = "file")] file_id: GleanFileId, name: String, - #[serde(skip_serializing_if = "Option::is_none")] - wam_url: Option, } #[derive(Serialize, Debug)] @@ -483,7 +434,6 @@ struct IndexedFacts { file_line_facts: Vec, declaration_facts: Vec, xref_facts: Vec, - module_facts: Vec, //v2 facts file_declarations: Vec, xref_v2: Vec, @@ -496,7 +446,6 @@ impl IndexedFacts { decl: FileDeclaration, xref: XRefFile, facts_v1: Option<(Vec, XRefFact)>, - module_fact: Option, ) -> Self { let mut facts = Self::default(); facts.file_facts.push(file_fact); @@ -507,9 +456,6 @@ impl IndexedFacts { facts.declaration_facts.extend(decl); facts.xref_facts.push(xref); } - if let Some(module_fact) = module_fact { - facts.module_facts.push(module_fact); - } facts } @@ -520,7 +466,6 @@ impl IndexedFacts { decl: FileDeclaration, xref: XRefFile, facts: Option<(Vec, XRefFact)>, - module_fact: Option, ) { self.file_facts.push(file_fact); self.file_line_facts.push(line_fact); @@ -530,9 +475,6 @@ impl IndexedFacts { self.declaration_facts.extend(decl); self.xref_facts.push(xref); } - if let Some(module_fact) = module_fact { - self.module_facts.push(module_fact); - } } fn into_v1_facts(mut self) -> Vec { @@ -738,8 +680,6 @@ impl IndexedFacts { }); } let xref_fact = xrefs.into_iter().map_into().collect(); - let module_facts = mem::take(&mut self.module_facts); - let module_facts = module_facts.into_iter().map_into().collect(); vec![ Fact::File { facts: mem::take(&mut self.file_facts), @@ -752,9 +692,6 @@ impl IndexedFacts { }, Fact::XRef { facts: xref_fact }, Fact::DeclarationComment { facts: comments }, - Fact::Module { - facts: module_facts, - }, ] } } @@ -767,7 +704,10 @@ pub struct GleanIndexer { pub fn index(args: &Glean, cli: &mut dyn Cli, query_config: &BuckQueryConfig) -> Result<()> { let (indexer, _loaded) = GleanIndexer::new(args, cli, query_config)?; - let config = IndexConfig { multi: args.multi }; + let config = IndexConfig { + multi: args.multi, + prefix: args.prefix.clone(), + }; let (facts, module_index) = indexer.index(config)?; write_results(facts, module_index, cli, args) } @@ -856,12 +796,19 @@ impl GleanIndexer { let source_root_id = db.file_source_root(file_id); let source_root = db.source_root(source_root_id); let path = source_root.path_for_file(&file_id).unwrap(); - match Self::index_file(db, file_id, path, project_id, &module_index) { - Some((file, line, decl, xref, facts, module_fact)) => { + match Self::index_file( + db, + file_id, + path, + project_id, + &module_index, + config.prefix.as_ref(), + ) { + Some((file, line, decl, xref, facts)) => { let mut result = FxHashMap::default(); result.insert( FACTS_FILE.to_string(), - IndexedFacts::new(file, line, decl, xref, facts, module_fact), + IndexedFacts::new(file, line, decl, xref, facts), ); result } @@ -872,14 +819,21 @@ impl GleanIndexer { .into_par_iter() .map_with(self.analysis.clone(), |analysis, (file_id, path)| { analysis.with_db(|db| { - Self::index_file(db, file_id, &path, project_id, &module_index) + Self::index_file( + db, + file_id, + &path, + project_id, + &module_index, + config.prefix.as_ref(), + ) }) }) .flatten() .flatten(); if config.multi { - iter.map(|(file, line, decl, xref, facts, module_fact)| { - IndexedFacts::new(file, line, decl, xref, facts, module_fact) + iter.map(|(file, line, decl, xref, facts)| { + IndexedFacts::new(file, line, decl, xref, facts) }) .collect::>() .into_iter() @@ -890,8 +844,8 @@ impl GleanIndexer { let mut result = FxHashMap::default(); let facts = iter.collect::>().into_iter().fold( IndexedFacts::default(), - |mut acc, (file_fact, line_fact, decl, xref, facts, module_fact)| { - acc.add(file_fact, line_fact, decl, xref, facts, module_fact); + |mut acc, (file_fact, line_fact, decl, xref, facts)| { + acc.add(file_fact, line_fact, decl, xref, facts); acc }, ); @@ -929,15 +883,15 @@ impl GleanIndexer { path: &VfsPath, project_id: ProjectId, module_index: &FxHashMap, + prefix: Option<&String>, ) -> Option<( FileFact, FileLinesFact, FileDeclaration, XRefFile, Option<(Vec, XRefFact)>, - Option, )> { - let file_fact = Self::file_fact(db, file_id, path, project_id)?; + let file_fact = Self::file_fact(db, file_id, path, project_id, prefix)?; let line_fact = Self::line_fact(db, file_id); let mut xref_v2 = Self::xrefs_v2(db, file_id, module_index); let mut file_decl = Self::declarations_v2(db, file_id, path)?; @@ -947,65 +901,9 @@ impl GleanIndexer { if let Some(module) = elp_module_index.module_for_file(file_id) { let decl = Self::declarations_v1(db, file_id, module); let xref = Self::xrefs(db, file_id, module_index); - let module_fact = Self::module_fact(db, file_id, module); - return Some(( - file_fact, - line_fact, - file_decl, - xref_v2, - Some((decl, xref)), - Some(module_fact), - )); + return Some((file_fact, line_fact, file_decl, xref_v2, Some((decl, xref)))); } - Some((file_fact, line_fact, file_decl, xref_v2, None, None)) - } - - fn module_fact(db: &RootDatabase, file_id: FileId, module_name: &ModuleName) -> ModuleFact { - let module = Module { - file: File { file_id }, - }; - let name = module_name.to_string(); - - // Extract exported functions from def_map - let def_map = db.def_map_local(file_id); - - let mut exports = vec![]; - for (fun, def) in def_map.get_functions() { - if def.exported { - exports.push(format!("{}/{}", fun.name(), fun.arity())); - } - } - - // Extract oncall, behaviour, and moduledoc using form_list API - let sema = Semantic::new(db); - let form_list = sema.form_list(file_id); - - let oncall = sema.attribute_value_as_string(file_id, hir::known::oncall); - - let behaviours: Vec = form_list - .behaviour_attributes() - .map(|(_, behaviour)| behaviour.name.to_string()) - .collect(); - - let module_doc = sema.module_attribute(file_id).and_then(|module_attribute| { - let docs = Documentation::new(db, &sema); - docs.to_doc(InFile::new(file_id, &module_attribute)) - .map(|doc| doc.markdown_text().to_string()) - .filter(|text| !text.is_empty()) - }); - - // @fb-only: let exdoc_link = elp_ide::meta_only::exdoc_links::module_exdoc_link(&module, &sema); - let exdoc_link: Option = None; // @oss-only - - ModuleFact::new( - file_id, - name, - oncall, - (!exports.is_empty()).then_some(exports), - (!behaviours.is_empty()).then_some(behaviours), - module_doc, - exdoc_link, - ) + Some((file_fact, line_fact, file_decl, xref_v2, None)) } fn add_xref_based_declarations( @@ -1030,43 +928,12 @@ impl GleanIndexer { let range = def.source(db).syntax().text_range(); let text = &db.file_text(id)[range]; let text = format!("```erlang\n{text}\n```"); - let scuba_links = - x.key - .scuba_urls - .as_ref() - .map_or(String::new(), |scuba_urls| { - scuba_urls - .iter() - .map(|(display_name, url)| { - format!("[{}]({})", display_name, url) - }) - .collect::>() - .join(" | ") - }); - - let scuba_section = if !scuba_links.is_empty() { - format!("Scuba: {}\n", scuba_links) - } else { - String::new() + let doc = match (&x.key.expansion, &x.key.ods_url) { + (None, None) => text, + (None, Some(o)) => format!("[ODS]({o})\n{text}"), + (Some(e), None) => format!("{text}\n---\n\n{e}"), + (Some(e), Some(o)) => format!("[ODS]({o})\n{text}\n---\n\n{e}"), }; - - let doc = format!( - "{}{}{}{}{}", - x.key - .ods_url - .as_ref() - .map_or(String::new(), |o| format!("[ODS]({})\n", o)), - x.key - .logview_url - .as_ref() - .map_or(String::new(), |l| format!("[LogView]({})\n", l)), - scuba_section, - text, - x.key - .expansion - .as_ref() - .map_or(String::new(), |e| format!("\n---\n\n{}", e)) - ); let decl = Declaration::MacroDeclaration( MacroDecl { name: x.key.name.clone(), @@ -1089,38 +956,6 @@ impl GleanIndexer { x.key.arity = Some(xref.source.start); } } - XRefTarget::Record(x) => { - // Add WAM documentation for records that have wam_url - if let Some(wam_url) = &x.key.wam_url { - let id: FileId = x.key.file_id.clone().into(); - let def_map = db.def_map(id); - let record_name = Name::from_erlang_service(&x.key.name); - if let Some(def) = def_map.get_record(&record_name) { - let range = def.source(db).syntax().text_range(); - let text = &db.file_text(id)[range]; - let text = format!("```erlang\n{text}\n```"); - let doc = format!("[WAM]({})\n{}", wam_url, text); - let decl = Declaration::RecordDeclaration( - RecordDecl { - name: x.key.name.clone(), - span: xref.source.clone(), - } - .into(), - ); - let doc_decl = Declaration::DocDeclaration( - DocDecl { - target: Box::new(decl.clone()), - span: xref.source.clone(), - text: doc, - } - .into(), - ); - file_decl.declarations.push(decl); - file_decl.declarations.push(doc_decl); - x.key.file_id = file_id.into(); - } - } - } _ => (), } } @@ -1153,12 +988,16 @@ impl GleanIndexer { file_id: FileId, path: &VfsPath, project_id: ProjectId, + prefix: Option<&String>, ) -> Option { let project_data = db.project_data(project_id); let root = project_data.root_dir.as_path(); let file_path = path.as_path()?; let file_path = file_path.strip_prefix(root)?; - let file_path = file_path.as_str().to_string(); + let file_path = match prefix { + Some(prefix) => Path::new(&prefix).join(file_path).to_str()?.into(), + None => file_path.as_str().to_string(), + }; Some(FileFact::new(file_id, file_path)) } @@ -1532,7 +1371,7 @@ impl GleanIndexer { }) => { let def = macro_def.as_ref()?; let mut resolved = Self::resolve_macro_v2(sema, def, source_file, ctx)?; - // @fb-only: meta_only::resolve_macro_expansion(sema, *expansion, ctx, &mut resolved); + // @fb-only Some(resolved) } hir::AnyExpr::Pat(Pat::MacroCall { macro_def, .. }) @@ -1560,7 +1399,7 @@ impl GleanIndexer { vars: FxHashMap<&Location, &String>, ) -> Vec { let mut result = vec![]; - if !db.is_eqwalizer_enabled(file_id) { + if !db.is_eqwalizer_enabled(file_id, false) { return result; } let module_diagnostics = db.eqwalizer_diagnostics_by_project(project_id, vec![file_id]); @@ -1780,8 +1619,6 @@ impl GleanIndexer { arity: define.name.arity(), expansion, ods_url: None, - logview_url: None, - scuba_urls: None, }; Some(XRef { source: range.into(), @@ -1874,19 +1711,12 @@ impl GleanIndexer { let (_, _, expr_source) = ctx.body_with_expr_source(sema)?; let source_file = sema.parse(file_id); let range = Self::find_range(sema, ctx, &source_file, &expr_source)?; - - // @fb-only: use elp_ide::meta_only::wam_links; - // @fb-only: let wam_ctx = wam_links::WamEventCtx::new(sema.db.upcast()); - // @fb-only: let wam_url = wam_ctx.build_wam_link(name).map(|link| link.url()); - let wam_url = None; // @oss-only - Some(XRef { source: range.into(), target: XRefTarget::Record( RecordTarget { file_id: def.file.file_id.into(), name: def.record.name.to_string(), - wam_url, } .into(), ), @@ -1971,22 +1801,22 @@ mod tests { start: 0, length: 10, }; - let module_name = "test_module"; - let func_name = "test_function"; + let module = "smax_product_catalog"; + let name = "product_visibility_update_request_iq"; let arity = 0; - let file_facts = vec![FileFact::new( - file_id, - "/test/app/src/test_module.erl".into(), - )]; - + let file_facts = vec![ + FileFact::new( + file_id, + "/local/whatsapp/server/erl/groupd_service/test/p13n/grpd_p13n_new_create_group_SUITE.erl".into(), + ) + ]; let file_line_facts = vec![FileLinesFact::new(file_id, vec![71, 42], true)]; - let decl = FileDeclaration { file_id: file_id.into(), declarations: vec![Declaration::FunctionDeclaration( FuncDecl { - name: func_name.to_string(), + name: name.to_string(), arity, span: location.clone(), exported: false, @@ -1995,7 +1825,6 @@ mod tests { .into(), )], }; - let xref = XRefFile { file_id: file_id.into(), xrefs: vec![XRef { @@ -2003,7 +1832,7 @@ mod tests { target: XRefTarget::Function( FunctionTarget { file_id: file_id.into(), - name: func_name.to_string(), + name: name.to_string(), arity, } .into(), @@ -2011,22 +1840,11 @@ mod tests { }], }; - let module = ModuleFact { - file_id: file_id.into(), - name: module_name.to_string(), - oncall: Some("test_team".to_string()), - exports: Some(vec![format!("{func_name}/{arity}")]), - behaviours: Some(vec!["test_behaviour".to_string()]), - module_doc: Some("Test module documentation".to_string()), - exdoc_link: Some("https://example.com/docs/test_module.html".to_string()), - }; - let facts = IndexedFacts { file_facts, file_line_facts, declaration_facts: vec![], xref_facts: vec![], - module_facts: vec![module], file_declarations: vec![decl], xref_v2: vec![xref], }; @@ -2039,14 +1857,16 @@ mod tests { v2: true, pretty: false, multi: false, + prefix: None, }; let mut module_index = FxHashMap::default(); - module_index.insert(file_id.into(), module_name.to_string()); + module_index.insert(file_id.into(), module.to_string()); write_results(map, module_index, &mut cli, &args).expect("success"); + let (out, err) = cli.to_strings(); let expected = expect_file!["../resources/test/glean/serialization_test.out"]; - assert_eq!(expected.data().trim(), &out); + expected.assert_eq(&out); assert_eq!(err, "") } @@ -2065,6 +1885,25 @@ mod tests { ); } + #[test] + fn file_fact_prefix_test() { + let spec = r#" + //- /glean/app_glean/src/glean_module2.erl + -module(glean_module2). + "#; + let config = IndexConfig { + multi: false, + prefix: Some("my/prefix".to_string()), + }; + let result = facts_with_annotations_with_config(spec, config).0; + assert_eq!(result.file_facts.len(), 1); + let file_fact = &result.file_facts[0]; + assert_eq!( + file_fact.file_path.as_str(), + "my/prefix/glean/app_glean/src/glean_module2.erl" + ); + } + #[test] fn line_fact_with_new_line_test() { let spec = r#" @@ -2335,10 +2174,10 @@ mod tests { fn xref_types_test() { let spec = r#" //- /glean/app_glean/src/glean_module81.erl - -type small() :: {non_neg_integer() | infinity}. + -type small() :: #{non_neg_integer() | infinity}. //- /glean/app_glean/src/glean_module8.erl - -type huuuge() :: {non_neg_integer() | infinity}. + -type huuuge() :: #{non_neg_integer() | infinity}. -spec baz( A :: huuuge(), %% ^^^^^^ glean_module8/huuuge/0 @@ -2393,10 +2232,10 @@ mod tests { fn xref_types_v2_test() { let spec = r#" //- /glean/app_glean/src/glean_module81.erl - -type small() :: {non_neg_integer() | infinity}. + -type small() :: #{non_neg_integer() | infinity}. //- /glean/app_glean/src/glean_module8.erl - -type huuuge() :: {non_neg_integer() | infinity}. + -type huuuge() :: #{non_neg_integer() | infinity}. -spec baz( A :: huuuge(), %% ^^^^^^ glean_module8.erl/type/huuuge/0 @@ -2435,7 +2274,7 @@ mod tests { }). baz(A) -> #query{ size = A }. - %% ^^^^^^ glean_module9.erl/rec/query/no_wam + %% ^^^^^^ glean_module9.erl/rec/query "#; xref_v2_check(spec); @@ -2465,9 +2304,9 @@ mod tests { -record(stats, {count, time}). baz(Time) -> [{#stats.count, 1}, - %% ^^^^^^ glean_module10.erl/rec/stats/no_wam + %% ^^^^^^ glean_module10.erl/rec/stats {#stats.time, Time}]. - %% ^^^^^^ glean_module10.erl/rec/stats/no_wam + %% ^^^^^^ glean_module10.erl/rec/stats "#; @@ -2496,7 +2335,7 @@ mod tests { -record(stats, {count, time}). baz(Stats) -> Stats#stats.count. - %% ^^^^^^ glean_module11.erl/rec/stats/no_wam + %% ^^^^^^ glean_module11.erl/rec/stats "#; xref_v2_check(spec); @@ -2524,7 +2363,7 @@ mod tests { -record(stats, {count, time}). baz(Stats, NewCnt) -> Stats#stats{count = NewCnt}. - %% ^^^^^^ glean_module12.erl/rec/stats/no_wam + %% ^^^^^^ glean_module12.erl/rec/stats "#; xref_v2_check(spec); @@ -2554,7 +2393,7 @@ mod tests { -record(stats, {count, time}). baz(Stats) -> #stats{count = Count, time = Time} = Stats. - %% ^^^^^^ glean_module13.erl/rec/stats/no_wam + %% ^^^^^^ glean_module13.erl/rec/stats "#; xref_v2_check(spec); @@ -2578,7 +2417,7 @@ mod tests { //- /glean/app_glean/src/glean_module14.erl -record(rec, {field}). foo(#rec.field) -> ok. - %% ^^^^ glean_module14.erl/rec/rec/no_wam + %% ^^^^ glean_module14.erl/rec/rec "#; xref_v2_check(spec); @@ -2604,10 +2443,10 @@ mod tests { //- /glean/app_glean/src/glean_module15.erl -record(stats, {count, time}). -spec baz() -> #stats{}. - %% ^^^^^^ glean_module15.erl/rec/stats/no_wam + %% ^^^^^^ glean_module15.erl/rec/stats baz() -> #stats{count = 1, time = 2}. - %% ^^^^^^ glean_module15.erl/rec/stats/no_wam + %% ^^^^^^ glean_module15.erl/rec/stats "#; xref_v2_check(spec); @@ -2628,12 +2467,25 @@ mod tests { baz(1) -> ?TAU; %% ^^^ macro.erl/macro/TAU/117/no_ods/6.28 baz(N) -> ?MAX(N, 200). - %% ^^^ macro.erl/macro/MAX/137/no_ods/if (N > 200) -> N; true -> 200 end + %% ^^^ macro.erl/macro/MAX/137/no_ods/if (N > 200) -> N; 'true' -> 200 end "#; xref_v2_check(spec); } + #[test] + fn xref_macro_ods_v2_test() { + let spec = r#" + //- /src/macro.erl + -module(macro). + -define(COUNT_INFRA(X), wa_stats_counter:count(X)). + baz(atom) -> ?COUNT_INFRA(atom), + %% ^^^^^^^^^^^ macro.erl/macro/COUNT_INFRA/94/has_ods/'wa_stats_counter':'count'( 'atom' ) + + "#; + // @fb-only + } + #[test] fn xref_macro_in_pat_v2_test() { let spec = r#" @@ -2656,7 +2508,7 @@ mod tests { -define(TYPE, integer()). -spec baz(ok) -> ?TYPE. - %% ^^^^ macro.erl/macro/TYPE/73/no_ods/erlang:integer() + %% ^^^^ macro.erl/macro/TYPE/73/no_ods/'erlang':'integer'() baz(ok) -> 1. "#; @@ -2670,114 +2522,14 @@ mod tests { -module(macro). -define(FOO(X), X). -wild(?FOO(atom)). - %% ^^^ macro.erl/macro/FOO/53/no_ods/atom + %% ^^^ macro.erl/macro/FOO/53/no_ods/'atom' "#; xref_v2_check(spec); } - #[test] - fn module_fact_test() { - let spec = r#" - //- /src/sample_worker.erl - %%% This is a module documentation - %%% It explains what this module does - -module(sample_worker). - -oncall("platform_team"). - -behaviour(test_behaviour). - -export([init/1, handle_task/2]). - - init(Args) -> {ok, Args}. - handle_task(Task, State) -> {reply, ok, State}. - internal_helper(X) -> X + 1. - "#; - let (facts, _, _, _, _) = facts_with_annotations(spec); - assert_eq!(facts.module_facts.len(), 1); - let module_fact = &facts.module_facts[0]; - assert_eq!(module_fact.name, "sample_worker"); - assert_eq!(module_fact.oncall, Some("platform_team".to_string())); - assert_eq!( - module_fact.behaviours, - Some(vec!["test_behaviour".to_string()]) - ); - assert_eq!(module_fact.exports.as_ref().map(|v| v.len()), Some(2)); - for expected in ["handle_task/2", "init/1"] { - assert!( - module_fact - .exports - .as_ref() - .unwrap() - .contains(&expected.to_string()) - ); - } - assert!(module_fact.module_doc.is_none()); - } - - #[test] - fn module_fact_multiple_behaviours_test() { - let spec = r#" - //- /src/factory_service.erl - -module(factory_service). - -oncall("manufacturing_team"). - -behaviour(supervisor). - -behaviour(test_behaviour1). - -behaviour(test_behaviour2). - - start_supervision() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). - create_product(Type, Config) -> test_behaviour1:produce(Type, Config). - manage_assembly(Parts) -> test_behaviour2:assemble(Parts). - "#; - let (facts, _, _, _, _) = facts_with_annotations(spec); - assert_eq!(facts.module_facts.len(), 1); - let module_fact = &facts.module_facts[0]; - assert_eq!(module_fact.name, "factory_service"); - assert_eq!(module_fact.oncall, Some("manufacturing_team".to_string())); - assert_eq!(module_fact.behaviours.as_ref().map(|v| v.len()), Some(3)); - for expected in ["test_behaviour1", "test_behaviour2", "supervisor"] { - assert!( - module_fact - .behaviours - .as_ref() - .unwrap() - .contains(&expected.to_string()) - ); - } - assert!(module_fact.exports.is_none()); - assert!(module_fact.module_doc.is_none()); - } - - #[test] - fn module_fact_no_oncall_test() { - let spec = r#" - //- /src/utility_helper.erl - -module(utility_helper). - -behaviour(supervisor). - -export([start_link/0, add_child/2]). - - start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). - add_child(ChildSpec, Opts) -> supervisor:start_child(?MODULE, {ChildSpec, Opts}). - "#; - let (facts, _, _, _, _) = facts_with_annotations(spec); - assert_eq!(facts.module_facts.len(), 1); - let module_fact = &facts.module_facts[0]; - assert_eq!(module_fact.name, "utility_helper"); - assert_eq!(module_fact.oncall, None); - assert_eq!(module_fact.behaviours, Some(vec!["supervisor".to_string()])); - assert_eq!(module_fact.exports.as_ref().map(|v| v.len()), Some(2)); - for expected in ["add_child/2", "start_link/0"] { - assert!( - module_fact - .exports - .as_ref() - .unwrap() - .contains(&expected.to_string()) - ); - } - assert!(module_fact.module_doc.is_none()); - } - #[allow(clippy::type_complexity)] - pub(crate) fn facts_with_annotations( + fn facts_with_annotations( spec: &str, ) -> ( IndexedFacts, @@ -2871,7 +2623,7 @@ mod tests { } #[track_caller] - pub(crate) fn xref_v2_check(spec: &str) { + fn xref_v2_check(spec: &str) { let (facts, mut expected_by_file, file_names, _d, _) = facts_with_annotations(spec); for xref_fact in facts.xref_v2 { let file_id = xref_fact.file_id; @@ -3066,15 +2818,9 @@ mod tests { Some(arity) => arity.to_string(), None => "no_arity".to_string(), }; - let ods_link = match ( - &xref.key.ods_url, - &xref.key.logview_url, - &xref.key.scuba_urls, - ) { - (Some(_), _, _) => "has_ods", - (None, Some(_), _) => "has_logview", - (None, None, Some(_)) => "has_scuba", - (None, None, None) => "no_ods", + let ods_link = match &xref.key.ods_url { + Some(_) => "has_ods", + None => "no_ods", }; let exp = match &xref.key.expansion { Some(exp) => exp @@ -3092,13 +2838,7 @@ mod tests { ) } XRefTarget::Header(_) => f.write_str("header"), - XRefTarget::Record(xref) => { - let wam_link = match &xref.key.wam_url { - Some(_) => "has_wam", - None => "no_wam", - }; - f.write_str(format!("rec/{}/{}", xref.key.name, wam_link).as_str()) - } + XRefTarget::Record(xref) => f.write_str(format!("rec/{}", xref.key.name).as_str()), XRefTarget::Type(xref) => { f.write_str(format!("type/{}/{}", xref.key.name, xref.key.arity).as_str()) } diff --git a/crates/elp/src/bin/lint_cli.rs b/crates/elp/src/bin/lint_cli.rs index 241a3d4481..3b81a8e119 100644 --- a/crates/elp/src/bin/lint_cli.rs +++ b/crates/elp/src/bin/lint_cli.rs @@ -13,19 +13,16 @@ use std::fs; use std::fs::File; use std::io::Write; use std::path::Path; +use std::path::PathBuf; use std::str; use std::sync::Arc; -use std::thread; -use std::time::SystemTime; use anyhow::Result; use anyhow::bail; -use crossbeam_channel::unbounded; use elp::build::load; use elp::build::types::LoadResult; use elp::cli::Cli; use elp::convert; -use elp::memory_usage::MemoryUsage; use elp::otp_file_to_ignore; use elp::read_lint_config_file; use elp_eqwalizer::Mode; @@ -53,16 +50,16 @@ use elp_ide::elp_ide_db::elp_base_db::ProjectId; use elp_ide::elp_ide_db::elp_base_db::Vfs; use elp_ide::elp_ide_db::elp_base_db::VfsPath; use elp_ide::elp_ide_db::source_change::SourceChange; -use elp_ide_db::text_edit::TextSize; -use elp_log::telemetry; use elp_project_model::AppName; use elp_project_model::AppType; use elp_project_model::DiscoverConfig; use elp_project_model::buck::BuckQueryConfig; +use elp_text_edit::TextSize; use fxhash::FxHashMap; use fxhash::FxHashSet; use hir::FormIdx; use hir::InFile; +use indicatif::ParallelProgressIterator; use itertools::Itertools; use paths::Utf8PathBuf; use rayon::prelude::ParallelBridge; @@ -70,16 +67,12 @@ use rayon::prelude::ParallelIterator; use crate::args::Lint; use crate::reporting; -use crate::reporting::print_memory_usage; pub fn run_lint_command( args: &Lint, cli: &mut dyn Cli, query_config: &BuckQueryConfig, ) -> Result<()> { - let start_time = SystemTime::now(); - let memory_start = MemoryUsage::now(); - if let Some(to) = &args.to { fs::create_dir_all(to)? }; @@ -89,22 +82,8 @@ pub fn run_lint_command( // We load the project after loading config, in case it bails with // errors. No point wasting time if the config is wrong. let mut loaded = load_project(args, cli, query_config)?; - telemetry::report_elapsed_time("lint operational", start_time); - let result = do_codemod(cli, &mut loaded, &diagnostics_config, args); - - telemetry::report_elapsed_time("lint done", start_time); - - let memory_end = MemoryUsage::now(); - let memory_used = memory_end - memory_start; - - // Print memory usage at the end if requested and format is normal - if args.is_format_normal() && args.report_system_stats { - print_memory_usage(loaded.analysis_host, loaded.vfs, cli)?; - writeln!(cli, "{}", memory_used)?; - } - - result + do_codemod(cli, &mut loaded, &diagnostics_config, args) } fn get_and_report_diagnostics_config(args: &Lint, cli: &mut dyn Cli) -> Result { @@ -115,7 +94,7 @@ fn get_and_report_diagnostics_config(args: &Lint, cli: &mut dyn Cli) -> Result, -) -> Result<(Vec<(String, FileId, DiagnosticCollection)>, bool, bool)> { +) -> Result> { let module_index = analysis.module_index(*project_id).unwrap(); + let module_iter = module_index.iter_own(); let ignored_apps: FxHashSet>> = args .ignore_apps .iter() .map(|name| Some(Some(AppName(name.to_string())))) .collect(); + let pb = cli.progress(module_iter.len() as u64, "Parsing modules"); let app_name = args.app.as_ref().map(|name| AppName(name.to_string())); - // Create a channel for streaming results - let (tx, rx) = unbounded(); - - // Collect modules into an owned vector - let modules: Vec<_> = module_index - .iter_own() - .map(|(name, source, file_id)| (name.as_str().to_string(), source, file_id)) - .collect(); - - let analysis_clone = analysis.clone(); - let config_clone = config.clone(); - let args_clone = args.clone(); - - let join_handle = thread::spawn(move || { - modules - .into_iter() - .par_bridge() - .map_with( - (analysis_clone, tx), - |(db, tx), (module_name, _file_source, file_id)| { - if !otp_file_to_ignore(db, file_id) - && db.file_app_type(file_id).ok() != Some(Some(AppType::Dep)) - && !ignored_apps.contains(&db.file_app_name(file_id).ok()) - && (app_name.is_none() - || db.file_app_name(file_id).ok().as_ref() == Some(&app_name)) - && let Ok(Some(result)) = do_diagnostics_one( - db, - &config_clone, - file_id, - &module_name, - &args_clone, - ) - { - // Send result through channel - let _ = tx.send(result); - } - }, - ) - .for_each(|_| {}); // Consume the iterator - }); - - // Collect results as they arrive from the channel - let mut results = Vec::new(); - let mut err_in_diag = false; - let mut module_count = 0; - let mut any_diagnostics_printed = false; - - for result in rx { - let printed = if args.skip_stream_print() { - false - } else { - print_diagnostic_result( - cli, - analysis, - config, - args, - loaded, - module, - &mut err_in_diag, - &mut module_count, - &result, - )? - }; - any_diagnostics_printed = any_diagnostics_printed || printed; - results.push(result); - } - - // Wait for the thread to complete before returning - // This ensures that analysis_clone is dropped and its read lock is released - join_handle - .join() - .expect("Failed to join diagnostics thread"); - - Ok((results, err_in_diag, any_diagnostics_printed)) + Ok(module_iter + .par_bridge() + .progress_with(pb) + .map_with( + analysis.clone(), + |db, (module_name, _file_source, file_id)| { + if !otp_file_to_ignore(db, file_id) + && db.file_app_type(file_id).ok() != Some(Some(AppType::Dep)) + && !ignored_apps.contains(&db.file_app_name(file_id).ok()) + && (app_name.is_none() + || db.file_app_name(file_id).ok().as_ref() == Some(&app_name)) + { + do_parse_one(db, config, file_id, module_name.as_str(), args).unwrap() + } else { + None + } + }, + ) + .flatten() + .collect()) } -fn do_diagnostics_one( +fn do_parse_one( db: &Analysis, config: &DiagnosticsConfig, file_id: FileId, @@ -293,8 +218,6 @@ pub fn do_codemod( ) -> Result<()> { // Declare outside the block so it has the right lifetime for filter_diagnostics let res; - let streamed_err_in_diag; - let mut any_diagnostics_printed = false; let mut initial_diags = { // We put this in its own block so that analysis is // freed before we apply lints. To apply lints @@ -335,18 +258,7 @@ pub fn do_codemod( res = match (file_id, name) { (None, _) => { - let (results, err_in_diag, any_printed) = do_diagnostics_all( - cli, - &analysis, - &loaded.project_id, - diagnostics_config, - args, - loaded, - &args.module, - )?; - streamed_err_in_diag = err_in_diag; - any_diagnostics_printed = any_printed; - results + do_parse_all(cli, &analysis, &loaded.project_id, diagnostics_config, args)? } (Some(file_id), Some(name)) => { if let Some(app) = &args.app @@ -355,124 +267,87 @@ pub fn do_codemod( { panic!("Module {} does not belong to app {}", name.as_str(), app) } - let result = - do_diagnostics_one(&analysis, diagnostics_config, file_id, &name, args)? - .map_or(vec![], |x| vec![x]); - - // Print diagnostics for the single file - let mut err_in_diag = false; - let mut module_count = 0; - if args.skip_stream_print() { - any_diagnostics_printed = false; - } else { - for r in &result { - let printed = print_diagnostic_result( - cli, - &analysis, - diagnostics_config, - args, - loaded, - &args.module, - &mut err_in_diag, - &mut module_count, - r, - )?; - any_diagnostics_printed = any_diagnostics_printed || printed; - } - } - - streamed_err_in_diag = err_in_diag; - result + do_parse_one(&analysis, diagnostics_config, file_id, &name, args)? + .map_or(vec![], |x| vec![x]) } (Some(file_id), _) => { panic!("Could not get name from file_id for {file_id:?}") } }; - res + filter_diagnostics( + &analysis, + &args.module, + Some(&diagnostics_config.enabled), + &res, + &FxHashSet::default(), + )? }; - let mut err_in_diag = streamed_err_in_diag; - // At this point, the analysis variable from above is dropped - - // When streaming is disabled (--no-stream) and we're not applying fixes, - // we need to print diagnostics now since they weren't printed during streaming - if args.no_stream && !args.apply_fix && !initial_diags.is_empty() { - let analysis = loaded.analysis(); - let mut module_count = 0; - initial_diags.sort_by(|(a, _, _), (b, _, _)| a.cmp(b)); - for result in &initial_diags { - let printed = print_diagnostic_result( - cli, - &analysis, - diagnostics_config, - args, - loaded, - &args.module, - &mut err_in_diag, - &mut module_count, - result, - )?; - any_diagnostics_printed = any_diagnostics_printed || printed; + if initial_diags.is_empty() { + if args.is_format_normal() { + writeln!(cli, "No diagnostics reported")?; } - } + } else { + initial_diags.sort_by(|(a, _, _), (b, _, _)| a.cmp(b)); + let mut err_in_diag = false; + if args.is_format_json() { + for (_name, file_id, diags) in &initial_diags { + if args.print_diags { + for diag in diags { + // We use JSON output for CI, and want to see warnings too. + // So do not filter on errors only + err_in_diag = true; + let vfs_path = loaded.vfs.file_path(*file_id); + let analysis = loaded.analysis(); + let root_path = &analysis + .project_data(*file_id) + .unwrap_or_else(|_err| panic!("could not find project data")) + .unwrap_or_else(|| panic!("could not find project data")) + .root_dir; + let relative_path = reporting::get_relative_path(root_path, vfs_path); + let prefix = args.prefix.as_ref(); + print_diagnostic_json( + diag, + &analysis, + *file_id, + with_prefix(relative_path, prefix).as_path(), + args.use_cli_severity, + cli, + )?; + } + } + } + } else { + writeln!( + cli, + "Diagnostics reported in {} modules:", + initial_diags.len() + )?; - // Handle apply_fix case separately since it needs to filter diagnostics anyway - if args.apply_fix { - if diagnostics_config.enabled.all_enabled() { + for (name, file_id, diags) in &initial_diags { + writeln!(cli, " {}: {}", name, diags.len())?; + if args.print_diags { + for diag in diags { + if let diagnostics::Severity::Error = diag.severity { + err_in_diag = true; + }; + print_diagnostic( + diag, + &loaded.analysis(), + *file_id, + args.use_cli_severity, + cli, + )?; + } + } + } + } + if args.apply_fix && diagnostics_config.enabled.all_enabled() { bail!( "We cannot apply fixes if all diagnostics enabled. Perhaps provide --diagnostic-filter" ); } - - let mut filtered_diags = { - let analysis = loaded.analysis(); - filter_diagnostics( - &analysis, - &args.module, - Some(&diagnostics_config.enabled), - &initial_diags, - &FxHashSet::default(), - )? - }; - - if filtered_diags.is_empty() { - if args.is_format_normal() { - writeln!(cli, "No diagnostics reported")?; - } - } else { - if args.skip_stream_print() { - filtered_diags.sort_by(|(a, _, _), (b, _, _)| a.cmp(b)); - let module_count: &mut i32 = &mut 0; - let has_diagnostics: &mut bool = &mut false; - if args.is_format_json() { - do_print_diagnostics_json_filtered( - cli, - args, - loaded, - &mut err_in_diag, - module_count, - has_diagnostics, - &filtered_diags, - )?; - } else { - { - // Scope the analysis instance to ensure it's dropped before creating Lints - let analysis = loaded.analysis(); - do_print_diagnostics_filtered( - cli, - &analysis, - args, - loaded, - &mut err_in_diag, - module_count, - has_diagnostics, - &filtered_diags, - )?; - // Analysis is dropped here - } - } - } - + if args.apply_fix && !diagnostics_config.enabled.all_enabled() { let mut changed_files = FxHashSet::default(); let mut lints = Lints::new( &mut loaded.analysis_host, @@ -480,7 +355,7 @@ pub fn do_codemod( &mut loaded.vfs, args, &mut changed_files, - filtered_diags, + initial_diags, ); // We handle the fix application result here, so // the overall status of whether error-severity @@ -492,232 +367,20 @@ pub fn do_codemod( writeln!(cli, "Apply fix failed: {err:#}").ok(); } }; - - if err_in_diag { - bail!("Errors found") - } } - } else { - // Non-apply-fix case: rely on any_diagnostics_printed which is set - // correctly based on filtered diagnostics during streaming/batch printing - if !any_diagnostics_printed { - if args.is_format_normal() { - writeln!(cli, "No diagnostics reported")?; - } - } else if err_in_diag { + if err_in_diag { bail!("Errors found") } } Ok(()) } -#[allow(clippy::too_many_arguments)] -fn print_diagnostic_result( - cli: &mut dyn Cli, - analysis: &Analysis, - config: &DiagnosticsConfig, - args: &Lint, - loaded: &LoadResult, - module: &Option, - err_in_diag: &mut bool, - module_count: &mut i32, - result: &(String, FileId, DiagnosticCollection), -) -> Result { - if args.is_format_json() { - do_print_diagnostic_collection_json( - cli, - analysis, - config, - args, - loaded, - module, - err_in_diag, - module_count, - result, - ) - } else { - do_print_diagnostic_collection( - cli, - analysis, - config, - args, - loaded, - module, - err_in_diag, - module_count, - result, - ) - } -} - -#[allow(clippy::too_many_arguments)] -fn do_print_diagnostic_collection( - cli: &mut dyn Cli, - analysis: &Analysis, - config: &DiagnosticsConfig, - args: &Lint, - loaded: &LoadResult, - module: &Option, - err_in_diag: &mut bool, - module_count: &mut i32, - result: &(String, FileId, DiagnosticCollection), -) -> Result { - let single_result = vec![result.clone()]; - let mut has_diagnostics = false; - if let Ok(filtered) = filter_diagnostics( - analysis, - module, - Some(&config.enabled), - &single_result, - &FxHashSet::default(), - ) { - do_print_diagnostics_filtered( - cli, - analysis, - args, - loaded, - err_in_diag, - module_count, - &mut has_diagnostics, - &filtered, - )?; - } - Ok(has_diagnostics) -} - -#[allow(clippy::too_many_arguments)] -fn do_print_diagnostics_filtered( - cli: &mut dyn Cli, - analysis: &Analysis, - args: &Lint, - loaded: &LoadResult, - err_in_diag: &mut bool, - module_count: &mut i32, - has_diagnostics: &mut bool, - filtered: &[(String, FileId, Vec)], -) -> Result<(), anyhow::Error> { - let _: () = for (name, file_id, diags) in filtered { - if !diags.is_empty() { - *has_diagnostics = true; - if *module_count == 0 { - writeln!(cli, "Diagnostics reported:")?; - } - *module_count += 1; - if !args.print_diags { - writeln!(cli, " {}: {}", name, diags.len())?; - } else { - for diag in diags { - if let diagnostics::Severity::Error = diag.severity { - *err_in_diag = true; - }; - // Get relative path for diagnostic output - let vfs_path = loaded.vfs.file_path(*file_id); - let root_path = &analysis - .project_data(*file_id) - .unwrap_or_else(|_err| panic!("could not find project data")) - .unwrap_or_else(|| panic!("could not find project data")) - .root_dir; - let relative_path = reporting::get_relative_path(root_path, vfs_path); - print_diagnostic( - diag, - analysis, - &loaded.vfs, - *file_id, - Some(relative_path), - args.use_cli_severity, - cli, - )?; - } - } - } - }; - Ok(()) -} - -#[allow(clippy::too_many_arguments)] -fn do_print_diagnostic_collection_json( - cli: &mut dyn Cli, - analysis: &Analysis, - config: &DiagnosticsConfig, - args: &Lint, - loaded: &LoadResult, - module: &Option, - err_in_diag: &mut bool, - module_count: &mut i32, - result: &(String, FileId, DiagnosticCollection), -) -> Result { - let single_result = vec![result.clone()]; - let mut has_diagnostics = false; - if let Ok(filtered) = filter_diagnostics( - analysis, - module, - Some(&config.enabled), - &single_result, - &FxHashSet::default(), - ) { - do_print_diagnostics_json_filtered( - cli, - args, - loaded, - err_in_diag, - module_count, - &mut has_diagnostics, - &filtered, - )?; - } - Ok(has_diagnostics) -} - -fn do_print_diagnostics_json_filtered( - cli: &mut dyn Cli, - args: &Lint, - loaded: &LoadResult, - err_in_diag: &mut bool, - module_count: &mut i32, - has_diagnostics: &mut bool, - filtered: &[(String, FileId, Vec)], -) -> Result<(), anyhow::Error> { - let _: () = for (name, file_id, diags) in filtered { - if !diags.is_empty() { - *has_diagnostics = true; - *module_count += 1; - if !args.print_diags { - writeln!(cli, " {}: {}", name, diags.len())?; - } else { - for diag in diags { - *err_in_diag = true; - - // Get relative path for diagnostic output - let vfs_path = loaded.vfs.file_path(*file_id); - let analysis = loaded.analysis(); - let root_path = &analysis - .project_data(*file_id) - .unwrap_or_else(|_err| panic!("could not find project data")) - .unwrap_or_else(|| panic!("could not find project data")) - .root_dir; - let relative_path = reporting::get_relative_path(root_path, vfs_path); - print_diagnostic_json( - diag, - &analysis, - *file_id, - relative_path, - args.use_cli_severity, - cli, - )?; - } - } - } - }; - Ok(()) -} - fn get_diagnostics_config(args: &Lint) -> Result { let cfg_from_file = if args.read_config || args.config_file.is_some() { read_lint_config_file(&args.project, &args.config_file)? } else { LintConfig::default() }; - let cfg = DiagnosticsConfig::default() .configure_diagnostics( &cfg_from_file, @@ -736,82 +399,12 @@ fn get_diagnostics_config(args: &Lint) -> Result { fn print_diagnostic( diag: &diagnostics::Diagnostic, analysis: &Analysis, - vfs: &Vfs, file_id: FileId, - path: Option<&Path>, use_cli_severity: bool, cli: &mut dyn Cli, ) -> Result<(), anyhow::Error> { let line_index = analysis.line_index(file_id)?; - let diag_str = diag.print(&line_index, use_cli_severity); - if let Some(path) = path { - writeln!(cli, "{}:{}", path.display(), diag_str)?; - } else { - writeln!(cli, " {}", diag_str)?; - } - - // Print any related information, indented - if let Some(related_info) = &diag.related_info { - for info in related_info { - let info_line_index = analysis.line_index(info.file_id)?; - let start = info_line_index.line_col(info.range.start()); - let end = info_line_index.line_col(info.range.end()); - - // Include file identifier if related info is from a different file - if info.file_id != file_id { - let file_identifier = - if let Ok(Some(module_name)) = analysis.module_name(info.file_id) { - // It's a module (.erl file), use module name - format!("[{}]", module_name.as_str()) - } else { - // Not a module (e.g., include file), use relative path - let vfs_path = vfs.file_path(info.file_id); - if let Ok(Some(project_data)) = analysis.project_data(info.file_id) { - let relative_path = - reporting::get_relative_path(&project_data.root_dir, vfs_path); - format!("[{}]", relative_path.display()) - } else { - // Fallback: just show location without file identifier - String::new() - } - }; - - if file_identifier.is_empty() { - writeln!( - cli, - " {}:{}-{}:{}: {}", - start.line + 1, - start.col_utf16 + 1, - end.line + 1, - end.col_utf16 + 1, - info.message - )?; - } else { - writeln!( - cli, - " {} {}:{}-{}:{}: {}", - file_identifier, - start.line + 1, - start.col_utf16 + 1, - end.line + 1, - end.col_utf16 + 1, - info.message - )?; - } - } else { - writeln!( - cli, - " {}:{}-{}:{}: {}", - start.line + 1, - start.col_utf16 + 1, - end.line + 1, - end.col_utf16 + 1, - info.message - )?; - } - } - } - + writeln!(cli, " {}", diag.print(&line_index, use_cli_severity))?; Ok(()) } @@ -996,10 +589,13 @@ impl<'a> Lints<'a> { if self.args.check_eqwalize_all { writeln!(cli, "Running eqwalize-all to check for knock-on problems.")?; } - let diags = { - let analysis = self.analysis_host.analysis(); - do_diagnostics_one(&analysis, self.cfg, file_id, &name, self.args)? - }; + let diags = do_parse_one( + &self.analysis_host.analysis(), + self.cfg, + file_id, + &name, + self.args, + )?; let err_in_diags = diags.iter().any(|(_, file_id, diags)| { let diags = diags.diagnostics_for(*file_id); diags @@ -1010,15 +606,14 @@ impl<'a> Lints<'a> { bail!("Applying change introduces an error diagnostic"); } else { self.changed_files.insert((file_id, name.clone())); - let changed_forms = { - let analysis = self.analysis_host.analysis(); - changes - .iter() - .filter_map(|d| form_from_diff(&analysis, file_id, d)) - .collect::>() - }; + let changes = changes + .iter() + .filter_map(|d| { + form_from_diff(&self.analysis_host.analysis(), file_id, d) + }) + .collect::>(); - for form_id in &changed_forms { + for form_id in &changes { self.changed_forms.insert(InFile::new(file_id, *form_id)); } @@ -1031,24 +626,24 @@ impl<'a> Lints<'a> { .flatten() .collect::>(); - let new_diagnostics = { - let analysis = self.analysis_host.analysis(); - filter_diagnostics(&analysis, &None, None, &new_diags, &self.changed_forms)? - }; + let new_diagnostics = filter_diagnostics( + &self.analysis_host.analysis(), + &None, + None, + &new_diags, + &self.changed_forms, + )?; self.diags = diagnostics_by_file_id(&new_diagnostics); if !self.diags.is_empty() { writeln!(cli, "---------------------------------------------\n")?; writeln!(cli, "New filtered diagnostics")?; - let analysis = self.analysis_host.analysis(); for (file_id, (name, diags)) in &self.diags { writeln!(cli, " {}: {}", name, diags.len())?; for diag in diags.iter() { print_diagnostic( diag, - &analysis, - self.vfs, + &self.analysis_host.analysis(), *file_id, - None, self.args.use_cli_severity, cli, )?; @@ -1120,13 +715,10 @@ impl<'a> Lints<'a> { if format_normal { writeln!(cli, "---------------------------------------------\n")?; writeln!(cli, "Applying fix in module '{name}' for")?; - let analysis = self.analysis_host.analysis(); print_diagnostic( diagnostic, - &analysis, - self.vfs, + &self.analysis_host.analysis(), file_id, - None, self.args.use_cli_severity, cli, )?; @@ -1193,9 +785,7 @@ impl<'a> Lints<'a> { print_diagnostic( &diagnostic, &self.analysis_host.analysis(), - self.vfs, file_id, - None, self.args.use_cli_severity, cli, )?; @@ -1330,6 +920,13 @@ fn get_form_id_at_offset( Some(form_id) } +fn with_prefix(path: &Path, prefix: Option<&String>) -> PathBuf { + match prefix { + Some(prefix) => Path::new(prefix).join(path), + None => path.into(), + } +} + #[cfg(test)] mod tests { use std::ffi::OsString; @@ -1338,7 +935,6 @@ mod tests { use elp::cli::Fake; use elp_ide::FunctionMatch; use elp_ide::diagnostics::DiagnosticCode; - use elp_ide::diagnostics::ErlangServiceConfig; use elp_ide::diagnostics::Lint; use elp_ide::diagnostics::LintsFromConfig; use elp_ide::diagnostics::ReplaceCall; @@ -1346,7 +942,6 @@ mod tests { use elp_ide::diagnostics::Replacement; use expect_test::Expect; use expect_test::expect; - use fxhash::FxHashMap; use super::LintConfig; use super::do_codemod; @@ -1371,19 +966,12 @@ mod tests { }, enabled_lints: vec![DiagnosticCode::HeadMismatch], disabled_lints: vec![], - linters: FxHashMap::default(), - erlang_service: ErlangServiceConfig { - warnings_as_errors: true, - }, }) .unwrap(); expect![[r#" enabled_lints = ["P1700"] disabled_lints = [] - - [erlang_service] - warnings_as_errors = true [[ad_hoc_lints.lints]] type = "ReplaceCall" @@ -1395,8 +983,6 @@ mod tests { [ad_hoc_lints.lints.action] action = "Replace" type = "UseOk" - - [linters] "#]] .assert_eq(&result); } @@ -1417,13 +1003,9 @@ mod tests { TrivialMatch, ], disabled_lints: [], - erlang_service: ErlangServiceConfig { - warnings_as_errors: false, - }, ad_hoc_lints: LintsFromConfig { lints: [], }, - linters: {}, } "#]] .assert_debug_eq(&lint_config); @@ -1469,11 +1051,11 @@ mod tests { head_mismatcX(0) -> 0. "#, expect![[r#" - module specified: lints - Diagnostics reported: - app_a/src/lints.erl:5:3-5:16::[Error] [P1700] head mismatch 'head_mismatcX' vs 'head_mismatch' - 4:3-4:16: Mismatched clause name - "#]], + module specified: lints + Diagnostics reported in 1 modules: + lints: 1 + 4:2-4:15::[Error] [P1700] head mismatch 'head_mismatcX' vs 'head_mismatch' + "#]], expect![""], ); } @@ -1490,8 +1072,9 @@ mod tests { "#, expect![[r#" module specified: lints - Diagnostics reported: - app_a/src/lints.erl:3:3-3:6::[Warning] [L1230] function foo/0 is unused + Diagnostics reported in 1 modules: + lints: 1 + 2:2-2:5::[Warning] [L1230] function foo/0 is unused "#]], expect![""], ); diff --git a/crates/elp/src/bin/main.rs b/crates/elp/src/bin/main.rs index 56535e4492..142b4f952e 100644 --- a/crates/elp/src/bin/main.rs +++ b/crates/elp/src/bin/main.rs @@ -40,10 +40,8 @@ mod erlang_service_cli; mod explain_cli; mod glean; mod lint_cli; -// @fb-only: mod meta_only; mod reporting; mod shell; -mod ssr_cli; // Use jemalloc as the global allocator #[cfg(not(any(target_env = "msvc", target_os = "openbsd")))] @@ -65,14 +63,9 @@ const THREAD_STACK_SIZE: usize = 10_000_000; fn main() { let _timer = timeit!("main"); + let mut cli = cli::Real::default(); let args = args::args().run(); - let use_color = args.should_use_color(); - let mut cli: Box = if use_color { - Box::new(cli::Real::default()) - } else { - Box::new(cli::NoColor::default()) - }; - let res = try_main(&mut *cli, args); + let res = try_main(&mut cli, args); let code = handle_res(res, cli.err()); process::exit(code); } @@ -103,28 +96,14 @@ fn setup_static(args: &Args) { } } -fn setup_cli_telemetry(args: &Args) { - match &args.command { - args::Command::RunServer(_) => { - // Do nothing, we have server telemetry - } - _ => { - // Initialize CLI telemetry, if used - // @fb-only: meta_only::initialize_telemetry(); - } - } -} - fn try_main(cli: &mut dyn Cli, args: Args) -> Result<()> { let logger = setup_logging(&args.log_file, args.no_log_buffering)?; - setup_cli_telemetry(&args); INIT.call_once(|| { setup_static(&args); setup_thread_pool(); }); let query_config = args.query_config(); - let use_color = args.should_use_color(); match args.command { args::Command::RunServer(_) => run_server(logger)?, args::Command::ParseAll(args) => erlang_service_cli::parse_all(&args, cli, &query_config)?, @@ -142,9 +121,6 @@ fn try_main(cli: &mut dyn Cli, args: Args) -> Result<()> { args::Command::BuildInfo(args) => build_info_cli::save_build_info(args, &query_config)?, args::Command::ProjectInfo(args) => build_info_cli::save_project_info(args, &query_config)?, args::Command::Lint(args) => lint_cli::run_lint_command(&args, cli, &query_config)?, - args::Command::Ssr(ssr_args) => { - ssr_cli::run_ssr_command(&ssr_args, cli, &query_config, use_color)? - } args::Command::GenerateCompletions(args) => { let instructions = args::gen_completions(&args.shell); writeln!(cli, "#Please run this:\n{instructions}")? @@ -288,7 +264,7 @@ mod tests { let (_stdout, stderr, code) = elp(args_vec![ "parse-all", "--project", - "../../test/test_projects/standard", + "../../test_projects/standard", "--to", tmp.path(), ]); @@ -306,7 +282,7 @@ mod tests { fn parse_all_complete(project: &str) -> Result { // Just check the command returns. - let project_path = format!("../../test/test_projects/{project}"); + let project_path = format!("../../test_projects/{project}"); let tmp = Builder::new().prefix("elp_parse_all_").tempdir().unwrap(); let (_stdout, _stderr, code) = elp(args_vec![ "parse-all", @@ -443,34 +419,16 @@ mod tests { }) .unwrap(); + let otp_version = OTP_VERSION.as_ref().expect("MISSING OTP VERSION"); let exp_path = expect_file!(format!( - "../resources/test/{}/{}/{}.pretty", + "../resources/test/{}/{}/{}-OTP-{}.pretty", project, app, module.as_str(), + otp_version, )); let (stdout, _) = cli.to_strings(); - - let otp_version = OTP_VERSION.as_ref().expect("MISSING OTP VERSION"); - let otp_version_regex = - regex::bytes::Regex::new(&format!("{}OTP([0-9]+)Only", "@")).unwrap(); - let contents = analysis.file_text(file_id).unwrap(); - let otp_version_capture = otp_version_regex - .captures(&contents.as_bytes()[0..(2001.min(contents.len()))]); - if let Some((_, [otp_version_only])) = - otp_version_capture.map(|cap| cap.extract()) - { - if otp_version_only == otp_version.as_bytes() { - assert_normalised_file( - exp_path, - &stdout, - project_path.into(), - false, - ); - } - } else { - assert_normalised_file(exp_path, &stdout, project_path.into(), false); - } + assert_normalised_file(exp_path, &stdout, project_path.into(), false); } } EqwalizerDiagnostics::NoAst { module } => { @@ -605,7 +563,10 @@ mod tests { fn eqwalize_target_diagnostics_match_snapshot_pretty() { if cfg!(feature = "buck") { simple_snapshot( - args_vec!["eqwalize-target", "//standard:app_a",], + args_vec![ + "eqwalize-target", + "//whatsapp/elp/test_projects/standard:app_a", + ], "standard", expect_file!("../resources/test/standard/eqwalize_target_diagnostics.pretty"), true, @@ -669,24 +630,6 @@ mod tests { ); } - #[test_case(false ; "rebar")] - #[test_case(true ; "buck")] - fn parse_all_diagnostics_severity(buck: bool) { - simple_snapshot_expect_error( - args_vec![ - "parse-elp", - "--module", - "diagnostics", - "--severity", - "error" - ], - "diagnostics", - expect_file!("../resources/test/diagnostics/parse_all_diagnostics_error.stdout"), - buck, - None, - ); - } - #[test_case(false ; "rebar")] #[test_case(true ; "buck")] fn parse_elp_file_attribute(buck: bool) { @@ -872,25 +815,6 @@ mod tests { ); } - #[test_case(false ; "rebar")] - #[test_case(true ; "buck")] - fn parse_elp_report_system_stats(buck: bool) { - simple_snapshot_output_contains( - args_vec!["parse-elp", "--report-system-stats", "--module", "app_a"], - "standard", - &[ - "Memory usage:", - "allocated:", - "active:", - "resident:", - "FileTextQuery", - ], - buck, - None, - 0, - ); - } - #[test] fn build_info_json_not_buck() { let tmp_dir = make_tmp_dir(); @@ -970,9 +894,7 @@ mod tests { assert!(tmp_file.clone().exists()); let content = fs::read_to_string(tmp_file).unwrap(); let mut buck_config = BuckConfig::default(); - buck_config.buck_root = Some(AbsPathBuf::assert_utf8( - current_dir().unwrap().join(path_str.clone()), - )); + buck_config.buck_root = Some(AbsPathBuf::assert_utf8(current_dir().unwrap())); let prelude_cell = get_prelude_cell(&buck_config).expect("could not get prelude"); let prelude_cell = prelude_cell.strip_prefix("/").unwrap(); let content = content.replace(prelude_cell, "/[prelude]/"); @@ -984,13 +906,38 @@ mod tests { Some(AbsPathBuf::assert(Utf8PathBuf::from_path_buf(abs).unwrap())); let content = normalise_prelude_path(content, buck_config); - let content = sort_json(&content); - expect![[r#" { "apps": [ { + "name": "test_exec", + "dir": "/[prelude]//erlang/common_test/test_exec/src", + "src_dirs": [ + "" + ], + "extra_src_dirs": [], + "include_dirs": [], + "macros": {} + }, + { + "name": "diagnostics_app_a", + "dir": "app_a", + "src_dirs": [ + "src" + ], + "extra_src_dirs": [], + "include_dirs": [ + "include" + ], + "macros": { + "COMMON_TEST": "true", + "TEST": "true" + } + }, + { + "name": "app_a_SUITE", "dir": "app_a/test", + "src_dirs": [], "extra_src_dirs": [ "" ], @@ -998,88 +945,61 @@ mod tests { "macros": { "COMMON_TEST": "true", "TEST": "true" - }, - "name": "app_a_SUITE", - "src_dirs": [] + } }, { - "dir": "/[prelude]//erlang/common_test/test_exec/src", - "extra_src_dirs": [], - "include_dirs": [], - "macros": {}, - "name": "test_exec", - "src_dirs": [ - "" - ] - }, - { - "dir": "/[prelude]//erlang/common_test/common", - "extra_src_dirs": [], - "include_dirs": [ - "include" - ], - "macros": {}, "name": "common", + "dir": "/[prelude]//erlang/common_test/common", "src_dirs": [ "src" - ] - }, - { - "dir": "/[prelude]//erlang/common_test/cth_hooks/src", - "extra_src_dirs": [], - "include_dirs": [ - "" ], - "macros": {}, - "name": "cth_hooks", - "src_dirs": [ - "" - ] - }, - { - "dir": "/[prelude]//erlang/shell/src", - "extra_src_dirs": [], - "include_dirs": [], - "macros": {}, - "name": "buck2_shell_utils", - "src_dirs": [ - "" - ] - }, - { - "dir": "app_a", "extra_src_dirs": [], "include_dirs": [ "include" ], - "macros": { - "COMMON_TEST": "true", - "TEST": "true" - }, - "name": "diagnostics_app_a", - "src_dirs": [ - "src" - ] + "macros": {} }, { - "dir": "/[prelude]//erlang/common_test/test_binary/src", + "name": "cth_hooks", + "dir": "/[prelude]//erlang/common_test/cth_hooks/src", + "src_dirs": [ + "" + ], + "extra_src_dirs": [], + "include_dirs": [ + "" + ], + "macros": {} + }, + { + "name": "buck2_shell_utils", + "dir": "/[prelude]//erlang/shell/src", + "src_dirs": [ + "" + ], "extra_src_dirs": [], "include_dirs": [], - "macros": {}, + "macros": {} + }, + { "name": "test_binary", + "dir": "/[prelude]//erlang/common_test/test_binary/src", "src_dirs": [ "" - ] - }, - { - "dir": "/[prelude]//erlang/common_test/test_cli_lib/src", + ], "extra_src_dirs": [], "include_dirs": [], - "macros": {}, + "macros": {} + }, + { "name": "test_cli_lib", + "dir": "/[prelude]//erlang/common_test/test_cli_lib/src", "src_dirs": [ "" - ] + ], + "extra_src_dirs": [], + "include_dirs": [], + "macros": {} } ], "deps": [] @@ -1094,12 +1014,6 @@ mod tests { content.replace(prelude_cell, "/[prelude]/") } - fn sort_json(content: &str) -> String { - let mut json: serde_json::Value = serde_json::from_str(content).unwrap(); - json.sort_all_objects(); - serde_json::to_string_pretty(&json).unwrap() - } - #[test] #[ignore] fn build_info_json_buck_bxl_generated() { @@ -1113,7 +1027,7 @@ mod tests { "--to", tmp_file.clone(), "--project", - path_str.clone() + path_str ]; let (stdout, stderr, code) = elp(args); assert_eq!( @@ -1128,9 +1042,7 @@ mod tests { assert!(tmp_file.clone().exists()); let content = fs::read_to_string(tmp_file).unwrap(); let mut buck_config = BuckConfig::default(); - buck_config.buck_root = Some(AbsPathBuf::assert_utf8( - current_dir().unwrap().join(path_str.clone()), - )); + buck_config.buck_root = Some(AbsPathBuf::assert_utf8(current_dir().unwrap())); let prelude_cell = get_prelude_cell(&buck_config).expect("could not get prelude"); let prelude_cell = prelude_cell.strip_prefix("/").unwrap(); let content = content.replace(prelude_cell, "/[prelude]/"); @@ -1366,7 +1278,6 @@ mod tests { check_lint_fix( args_vec![ "lint", - "--no-stream", "--diagnostic-filter", "W0010", "--experimental", @@ -1394,7 +1305,6 @@ mod tests { check_lint_fix( args_vec![ "lint", - "--no-stream", "--diagnostic-filter", "W0010", "--experimental", @@ -1420,7 +1330,7 @@ mod tests { fn lint_config_file_used(buck: bool) { let tmp_dir = make_tmp_dir(); let tmp_path = tmp_dir.path(); - check_lint_fix_stderr_sorted( + check_lint_fix( args_vec![ "lint", "--diagnostic-filter", @@ -1437,9 +1347,6 @@ mod tests { Path::new("../resources/test/lint/lint_recursive"), &[], false, - Some(expect![[r#" - Errors found - "#]]), ) .expect("bad test"); } @@ -1454,7 +1361,7 @@ mod tests { "lint", "--experimental", "--config-file", - "../../test/test_projects/linter/does_not_exist.toml" + "../../test_projects/linter/does_not_exist.toml" ], "linter", expect_file!("../resources/test/linter/parse_elp_lint_custom_config_invalid_output.stdout"), @@ -1466,7 +1373,7 @@ mod tests { &[], false, Some(expect![[r#" - unable to read "../../test/test_projects/linter/does_not_exist.toml": No such file or directory (os error 2) + unable to read "../../test_projects/linter/does_not_exist.toml": No such file or directory (os error 2) "#]]), ) .expect("bad test"); @@ -1477,12 +1384,12 @@ mod tests { fn lint_custom_config_file_used(buck: bool) { let tmp_dir = make_tmp_dir(); let tmp_path = tmp_dir.path(); - check_lint_fix_stderr_sorted( + check_lint_fix( args_vec![ "lint", "--experimental", "--config-file", - "../../test/test_projects/linter/elp_lint_test1.toml" + "../../test_projects/linter/elp_lint_test1.toml" ], "linter", expect_file!("../resources/test/linter/parse_elp_lint_custom_config_output.stdout"), @@ -1493,7 +1400,6 @@ mod tests { Path::new("../resources/test/lint/lint_recursive"), &[], false, - None, ) .expect("bad test"); } @@ -1508,7 +1414,7 @@ mod tests { "lint", "--experimental", "--config-file", - "../../test/test_projects/linter/elp_lint_adhoc.toml", + "../../test_projects/linter/elp_lint_adhoc.toml", "--module", "app_b", "--apply-fix", @@ -1532,14 +1438,14 @@ mod tests { #[test_case(false ; "rebar")] #[test_case(true ; "buck")] fn lint_diagnostic_ignore(buck: bool) { - simple_snapshot_sorted( + simple_snapshot( args_vec![ "lint", "--experimental", "--diagnostic-ignore", "W0011", "--config-file", - "../../test/test_projects/linter/elp_lint_test_ignore.toml" + "../../test_projects/linter/elp_lint_test_ignore.toml" ], "linter", expect_file!("../resources/test/linter/parse_elp_lint_ignore.stdout"), @@ -1583,7 +1489,7 @@ mod tests { &[], false, Some(expect![[r#" - failed to read "../../test/test_projects/linter_bad_config/.elp_lint.toml":expected a right bracket, found an identifier at line 6 column 4 + failed to read "../../test_projects/linter_bad_config/.elp_lint.toml":expected a right bracket, found an identifier at line 6 column 4 "#]]), ) .expect("bad test"); @@ -1592,8 +1498,8 @@ mod tests { #[test_case(false ; "rebar")] #[test_case(true ; "buck")] fn lint_no_diagnostics_filter_all_enabled(buck: bool) { - simple_snapshot_expect_error_sorted( - args_vec!["lint"], + simple_snapshot_expect_error( + args_vec!["lint",], "linter", expect_file!("../resources/test/linter/parse_elp_no_lint_specified_output.stdout"), buck, @@ -1601,24 +1507,10 @@ mod tests { ); } - #[test_case(false ; "rebar")] - #[test_case(true ; "buck")] - fn lint_no_stream_produces_output(buck: bool) { - if otp::supports_eep66_sigils() { - simple_snapshot_expect_error( - args_vec!["lint", "--no-stream"], - "diagnostics", - expect_file!("../resources/test/diagnostics/lint_no_stream.stdout"), - buck, - None, - ); - } - } - #[test_case(false ; "rebar")] #[test_case(true ; "buck")] fn lint_no_diagnostics_filter_all_enabled_json(buck: bool) { - simple_snapshot_expect_error_sorted( + simple_snapshot_expect_error( args_vec!["lint", "--format", "json"], "linter", expect_file!("../resources/test/linter/parse_elp_no_lint_specified_json_output.stdout"), @@ -1645,11 +1537,11 @@ mod tests { fn lint_explicit_enable_diagnostic(buck: bool) { let tmp_dir = make_tmp_dir(); let tmp_path = tmp_dir.path(); - check_lint_fix_stderr_sorted( + check_lint_fix( args_vec![ "lint", "--config-file", - "../../test/test_projects/linter/elp_lint_test2.toml" + "../../test_projects/linter/elp_lint_test2.toml" ], "linter", expect_file!("../resources/test/linter/parse_elp_lint_explicit_enable_output.stdout"), @@ -1660,9 +1552,6 @@ mod tests { Path::new("../resources/test/lint/lint_recursive"), &[], false, - Some(expect![[r#" - Errors found - "#]]), ) .expect("bad test"); } @@ -1672,7 +1561,7 @@ mod tests { fn lint_json_output(buck: bool) { let tmp_dir = make_tmp_dir(); let tmp_path = tmp_dir.path(); - check_lint_fix_stderr_sorted( + check_lint_fix( args_vec![ "lint", "--diagnostic-filter", @@ -1690,9 +1579,35 @@ mod tests { Path::new("../resources/test/lint/lint_recursive"), &[], false, - Some(expect![[r#" - Errors found - "#]]), + ) + .expect("bad test"); + } + + #[test_case(false ; "rebar")] + #[test_case(true ; "buck")] + fn lint_json_output_prefix(buck: bool) { + let tmp_dir = make_tmp_dir(); + let tmp_path = tmp_dir.path(); + check_lint_fix( + args_vec![ + "lint", + "--diagnostic-filter", + "W0010", + "--experimental", + "--format", + "json", + "--prefix", + "my/prefix" + ], + "linter", + expect_file!("../resources/test/linter/parse_elp_lint_json_output_prefix.stdout"), + 101, + buck, + None, + tmp_path, + Path::new("../resources/test/lint/lint_recursive"), + &[], + false, ) .expect("bad test"); } @@ -1702,7 +1617,7 @@ mod tests { fn lint_applies_fix_using_to_dir(buck: bool) { let tmp_dir = make_tmp_dir(); let tmp_path = tmp_dir.path(); - check_lint_fix_stderr( + check_lint_fix( args_vec![ "lint", "--module", @@ -1711,7 +1626,7 @@ mod tests { "P1700", "--to", tmp_path, - "--apply-fix", + "--apply-fix" ], "diagnostics", expect_file!("../resources/test/diagnostics/parse_elp_lint_fix.stdout"), @@ -1722,9 +1637,6 @@ mod tests { Path::new("../resources/test/lint/head_mismatch"), &[("app_a/src/lints.erl", "lints.erl")], false, - Some(expect![[r#" - Errors found - "#]]), ) .expect("Bad test"); } @@ -1734,7 +1646,7 @@ mod tests { fn lint_applies_fix_using_to_dir_json_output(buck: bool) { let tmp_dir = make_tmp_dir(); let tmp_path = tmp_dir.path(); - check_lint_fix_stderr( + check_lint_fix( args_vec![ "lint", "--module", @@ -1756,9 +1668,6 @@ mod tests { Path::new("../resources/test/lint/head_mismatch"), &[("app_a/src/lints.erl", "lints.erl")], false, - Some(expect![[r#" - Errors found - "#]]), ) .expect("Bad test"); } @@ -1779,7 +1688,7 @@ mod tests { fn do_lint_applies_fix_in_place(buck: bool) { let project = "in_place_tests"; - check_lint_fix_stderr( + check_lint_fix( args_vec![ "lint", "--module", @@ -1798,9 +1707,6 @@ mod tests { Path::new("../resources/test/lint/head_mismatch"), &[("app_a/src/lints.erl", "app_a/src/lints.erl")], true, - Some(expect![[r#" - Errors found - "#]]), ) .expect("Bad test"); } @@ -1840,7 +1746,7 @@ mod tests { fn lint_applies_code_action_fixme_if_requested(buck: bool) { let tmp_dir = make_tmp_dir(); let tmp_path = tmp_dir.path(); - check_lint_fix_stderr( + check_lint_fix( args_vec![ "lint", "--module", @@ -1861,9 +1767,6 @@ mod tests { Path::new("../resources/test/lint/ignore_app_env"), &[("app_a/src/spelling.erl", "spelling.erl")], false, - Some(expect![[r#" - Errors found - "#]]), ) .expect("Bad test"); } @@ -1884,7 +1787,7 @@ mod tests { #[test_case(false ; "rebar")] #[test_case(true ; "buck")] fn lint_edoc(buck: bool) { - simple_snapshot_sorted( + simple_snapshot( args_vec![ "lint", "--include-edoc-diagnostics", @@ -1918,7 +1821,7 @@ mod tests { #[test_case(false ; "rebar")] #[test_case(true ; "buck")] fn lint_ct_include_tests(buck: bool) { - simple_snapshot_expect_error_sorted( + simple_snapshot_expect_error( args_vec![ "lint", "--include-ct-diagnostics", @@ -1936,8 +1839,8 @@ mod tests { #[test] fn lint_resolves_generated_includes() { if cfg!(feature = "buck") { - simple_snapshot_expect_error_sorted( - args_vec!["lint", "--module", "top_includer",], + simple_snapshot_expect_error( + args_vec!["lint"], "buck_tests_2", expect_file!("../resources/test/buck_tests_2/resolves_generated_includes.stdout"), true, @@ -1952,8 +1855,7 @@ mod tests { simple_snapshot_expect_stderror( args_vec!["lint",], "buck_bad_config", - // @fb-only: expect_file!("../resources/test/buck_bad_config/bxl_error_message.stdout"), - expect_file!("../resources/test/buck_bad_config/bxl_error_message_oss.stdout"), // @oss-only + expect_file!("../resources/test/buck_bad_config/bxl_error_message.stdout"), true, None, true, @@ -1961,320 +1863,6 @@ mod tests { } } - #[test] - fn lint_warnings_as_errors() { - simple_snapshot_expect_error_sorted( - args_vec![ - "lint", - "--no-stream" - "--config-file", - "../../test/test_projects/linter/elp_lint_warnings_as_errors.toml" - ], - "linter", - expect_file!("../resources/test/linter/warnings_as_errors.stdout"), - true, - None, - ) - } - - #[test] - fn lint_custom_function_matches() { - simple_snapshot( - args_vec![ - "lint", - "--config-file", - "../../test/test_projects/linter/elp_lint_custom_function_matches.toml", - "--module", - "custom_function_matches" - ], - "linter", - expect_file!("../resources/test/linter/custom_function_matches.stdout"), - true, - None, - ) - } - - #[test] - fn lint_unavailable_type() { - simple_snapshot( - args_vec![ - "lint", - "--config-file", - "../../test/test_projects/xref/elp_lint_unavailable_type.toml", - "--module", - "unavailable_type" - ], - "xref", - expect_file!("../resources/test/xref/unavailable_type.stdout"), - true, - None, - ) - } - - #[test] - fn lint_ssr_from_config() { - simple_snapshot_sorted( - args_vec![ - "lint", - "--config-file", - "../../test/test_projects/linter/elp_lint_ssr_adhoc.toml", - ], - "linter", - expect_file!("../resources/test/linter/ssr_ad_hoc.stdout"), - true, - None, - ) - } - - #[test] - fn lint_ssr_from_bad_config() { - simple_snapshot_expect_stderror( - args_vec![ - "lint", - "--config-file", - "../../test/test_projects/linter/elp_lint_ssr_adhoc_parse_fail.toml", - ], - "linter", - expect_file!("../resources/test/linter/ssr_ad_hoc_parse_fail.stdout"), - true, - None, - false, - ) - } - - #[test] - fn lint_ssr_as_cli_arg() { - simple_snapshot( - args_vec!["ssr", "ssr: {_@A, _@B}.",], - "linter", - expect_file!("../resources/test/linter/ssr_ad_hoc_cli.stdout"), - true, - None, - ) - } - - #[test] - fn lint_ssr_as_cli_arg_without_prefix() { - simple_snapshot( - args_vec!["ssr", "{_@A, _@B}",], - "linter", - expect_file!("../resources/test/linter/ssr_ad_hoc_cli.stdout"), - true, - None, - ) - } - - #[test] - fn lint_ssr_with_context_and_separator() { - simple_snapshot( - args_vec![ - "--colour", - "never", - "ssr", - "--context", - "2", - "--group-separator", - "====", - "{_@A, _@B}", - ], - "linter", - expect_file!("../resources/test/linter/ssr_context_separator.stdout"), - true, - None, - ) - } - - #[test] - fn lint_ssr_with_context_and_separator_color() { - simple_snapshot( - args_vec![ - "--colour", - "always", - "ssr", - "--context", - "2", - "--group-separator", - "====", - "{_@A, _@B}", - ], - "linter", - expect_file!("../resources/test/linter/ssr_context_separator_color.stdout"), - true, - None, - ) - } - - #[test] - fn lint_ssr_as_cli_arg_multiple_patterns() { - simple_snapshot( - args_vec!["ssr", "3" "{4}",], - "linter", - expect_file!("../resources/test/linter/ssr_ad_hoc_cli_multiple.stdout"), - true, - None, - ) - } - - #[test] - fn lint_ssr_as_cli_arg_malformed() { - simple_snapshot_expect_stderror( - args_vec!["ssr", "ssr: {_@A, = _@B}.",], - "linter", - expect_file!("../resources/test/linter/ssr_ad_hoc_cli_parse_error.stdout"), - true, - None, - false, - ) - } - - #[test] - fn lint_ssr_as_cli_parens_visible() { - simple_snapshot( - args_vec!["ssr", "--parens", "(_@A)",], - "linter", - expect_file!("../resources/test/linter/ssr_ad_hoc_cli_parens_visible.stdout"), - true, - None, - ) - } - - #[test] - fn lint_ssr_as_cli_parens_invisible() { - // Invisible parens are the default - simple_snapshot( - args_vec!["ssr", "(((3)))",], - "linter", - expect_file!("../resources/test/linter/ssr_ad_hoc_cli_parens_invisible.stdout"), - true, - None, - ) - } - - #[test] - fn lint_ssr_as_cli_macros_expand() { - simple_snapshot( - args_vec!["ssr", "--macros", "expand", "?BAR(_@AA)", "{4}"], - "linter", - expect_file!("../resources/test/linter/ssr_ad_hoc_cli_macros_expand.stdout"), - true, - None, - ) - } - - #[test] - fn lint_ssr_as_cli_macros_expand_is_default() { - simple_snapshot( - args_vec!["ssr", "?BAR(_@AA)", "{4}"], - "linter", - expect_file!("../resources/test/linter/ssr_ad_hoc_cli_macros_expand.stdout"), - true, - None, - ) - } - - #[test] - fn lint_ssr_as_cli_macros_visible_expand() { - simple_snapshot( - args_vec!["ssr", "--macros", "visible-expand", "?BAR(_@AA)", "{4}"], - "linter", - expect_file!("../resources/test/linter/ssr_ad_hoc_cli_macros_visible_expand.stdout"), - true, - None, - ) - } - - #[test] - fn lint_ssr_as_cli_macros_no_expand() { - simple_snapshot( - args_vec!["ssr", "--macros", "no-expand", "?BAR(_@AA)", "{4}"], - "linter", - expect_file!("../resources/test/linter/ssr_ad_hoc_cli_macros_no_expand.stdout"), - true, - None, - ) - } - - #[test] - fn lint_ssr_as_cli_dump_config() { - simple_snapshot( - args_vec!["ssr", "--dump-config", "?BAR(_@AA)", "{4}"], - "linter", - expect_file!("../resources/test/linter/ssr_ad_hoc_cli_dump_config.stdout"), - true, - None, - ) - } - - #[test] - fn lint_ssr_as_cli_dump_config_without_info() { - simple_snapshot( - args_vec!["ssr", "--dump-config", "?BAR(_@AA)", "{4}"], - "linter", - expect_file!("../resources/test/linter/ssr_ad_hoc_cli_dump_config.stdout"), - true, - None, - ) - } - - #[test_case(false ; "rebar")] - #[test_case(true ; "buck")] - fn ssr_exclude_generated_by_default(buck: bool) { - simple_snapshot( - args_vec!["ssr", "--module", "erlang_diagnostics_errors_gen", "ok"], - "diagnostics", - expect_file!("../resources/test/diagnostics/ssr_exclude_generated.stdout"), - buck, - None, - ); - } - - #[test_case(false ; "rebar")] - #[test_case(true ; "buck")] - fn ssr_include_generated_when_requested(buck: bool) { - simple_snapshot( - args_vec![ - "ssr", - "--module", - "erlang_diagnostics_errors_gen", - "--include-generated", - "ok" - ], - "diagnostics", - expect_file!("../resources/test/diagnostics/ssr_include_generated.stdout"), - buck, - None, - ); - } - - #[test_case(false ; "rebar")] - #[test_case(true ; "buck")] - // We cannot use `should_panic` for this test, since the OSS CI runs with the `buck` feature disabled. - // When this happens the test is translated into a no-op, which does not panic. - // TODO(T248259687): Switch to should_panic once Buck2 is available on GitHub. - // Or remove the ignore once hierarchical support is implemented. - #[ignore] // Support for hierarchical config is not implemented yet - fn lint_hierarchical_config_basic(buck: bool) { - simple_snapshot_sorted( - args_vec!["lint", "--read-config"], - "hierarchical_config", - expect_file!("../resources/test/hierarchical_config/basic.stdout"), - buck, - None, - ); - } - - #[test_case(false ; "rebar")] - #[test_case(true ; "buck")] - fn lint_linter_config_basic(buck: bool) { - simple_snapshot_sorted( - args_vec!["lint", "--read-config", "--no-stream"], - "linter_config", - expect_file!("../resources/test/linter_config/basic.stdout"), - buck, - None, - ); - } - #[test_case(false ; "rebar")] #[test_case(true ; "buck")] fn eqwalizer_tests_check(buck: bool) { @@ -2406,117 +1994,6 @@ mod tests { } } - #[test_case(false ; "rebar")] - #[test_case(true ; "buck")] - fn eqwalize_with_color_vs_no_color(buck: bool) { - if otp_supported_by_eqwalizer() { - // Test with color (default) - let (mut args_color, _path) = - add_project(args_vec!["eqwalize", "app_a"], "standard", None, None); - if !buck { - args_color.push("--rebar".into()); - } - - // Test without color - let (mut args_no_color, _) = add_project( - args_vec!["--color", "never", "eqwalize", "app_a"], - "standard", - None, - None, - ); - if !buck { - args_no_color.push("--rebar".into()); - } - - let (stdout_color, stderr_color, code_color) = elp(args_color); - let (stdout_no_color, stderr_no_color, code_no_color) = elp(args_no_color); - - // Both should have same exit code - assert_eq!(code_color, code_no_color); - - // Both should have same stderr behavior - if code_color == 0 { - assert!(stderr_color.is_empty()); - assert!(stderr_no_color.is_empty()); - } - - // The content should be similar but no-color version should not contain ANSI escape codes - // ANSI color codes typically start with \x1b[ or \u{1b}[ - let _has_ansi_color = stdout_color.contains('\x1b'); - let has_ansi_no_color = stdout_no_color.contains('\x1b'); - - // With --color never, there should be no ANSI escape sequences - assert!( - !has_ansi_no_color, - "Output with --color never should not contain ANSI escape codes" - ); - - // The outputs should be functionally equivalent when ANSI codes are stripped - let stripped_color = strip_ansi_codes(&stdout_color); - assert_eq!( - stripped_color, stdout_no_color, - "Content should be identical after stripping ANSI codes" - ); - } - } - - #[test_case(false ; "rebar")] - #[test_case(true ; "buck")] - fn eqwalize_with_no_color_env_var(buck: bool) { - if otp_supported_by_eqwalizer() { - // Test with NO_COLOR environment variable set - unsafe { - env::set_var("NO_COLOR", "1"); - } - - let (mut args_no_color_env, _) = - add_project(args_vec!["eqwalize", "app_a"], "standard", None, None); - if !buck { - args_no_color_env.push("--rebar".into()); - } - - let (stdout_no_color_env, stderr_no_color_env, code_no_color_env) = - elp(args_no_color_env); - - // Clean up environment variable - unsafe { - env::remove_var("NO_COLOR"); - } - - // Test with normal color (for comparison) - let (mut args_color, _) = - add_project(args_vec!["eqwalize", "app_a"], "standard", None, None); - if !buck { - args_color.push("--rebar".into()); - } - - let (stdout_color, stderr_color, code_color) = elp(args_color); - - // Both should have same exit code - assert_eq!(code_color, code_no_color_env); - - // Both should have same stderr behavior - if code_color == 0 { - assert!(stderr_color.is_empty()); - assert!(stderr_no_color_env.is_empty()); - } - - // The NO_COLOR env var version should not contain ANSI escape codes - let has_ansi_no_color_env = stdout_no_color_env.contains('\x1b'); - assert!( - !has_ansi_no_color_env, - "Output with NO_COLOR env var should not contain ANSI escape codes" - ); - - // The outputs should be functionally equivalent when ANSI codes are stripped - let stripped_color = strip_ansi_codes(&stdout_color); - assert_eq!( - stripped_color, stdout_no_color_env, - "Content should be identical after stripping ANSI codes" - ); - } - } - // ----------------------------------------------------------------- #[test] @@ -2607,26 +2084,6 @@ mod tests { expected.assert_eq(&stdout); } - #[test] - fn ssr_help() { - let args = args::args() - .run_inner(Args::from(&["ssr", "--help"])) - .unwrap_err(); - let expected = expect_file!["../resources/test/ssr_help.stdout"]; - let stdout = args.unwrap_stdout(); - expected.assert_eq(&stdout); - } - - #[test] - fn search_help() { - let args = args::args() - .run_inner(Args::from(&["search", "--help"])) - .unwrap_err(); - let expected = expect_file!["../resources/test/ssr_help.stdout"]; - let stdout = args.unwrap_stdout(); - expected.assert_eq(&stdout); - } - #[test] fn build_info_help() { let args = args::args() @@ -2845,61 +2302,6 @@ mod tests { } } - fn simple_snapshot_expect_error_sorted( - args: Vec, - project: &str, - expected: ExpectFile, - buck: bool, - file: Option<&str>, - ) { - if !buck || cfg!(feature = "buck") { - let (mut args, path) = add_project(args, project, file, None); - if !buck { - args.push("--rebar".into()); - } - let (stdout, stderr, code) = elp(args); - assert_eq!( - code, 101, - "Expected exit code 101, got: {code}\nstdout:\n{stdout}\nstderr:\n{stderr}" - ); - let sorted_stdout = sort_lines(&stdout); - assert_normalised_file(expected, &sorted_stdout, path, false); - } - } - - fn sort_lines(s: &str) -> String { - let mut lines: Vec<&str> = s.lines().collect(); - lines.sort(); - lines.join("\n") - } - - #[track_caller] - fn simple_snapshot_sorted( - args: Vec, - project: &str, - expected: ExpectFile, - buck: bool, - file: Option<&str>, - ) { - if !buck || cfg!(feature = "buck") { - let (mut args, path) = add_project(args, project, file, None); - if !buck { - args.push("--rebar".into()); - } - let (stdout, stderr, code) = elp(args); - assert_eq!( - code, 0, - "failed with unexpected exit code: got {code} not 0\nstdout:\n{stdout}\nstderr:\n{stderr}" - ); - let sorted_stdout = sort_lines(&stdout); - assert_normalised_file(expected, &sorted_stdout, path, false); - assert!( - stderr.is_empty(), - "expected stderr to be empty, got:\n{stderr}" - ) - } - } - fn simple_snapshot_expect_stderror( args: Vec, project: &str, @@ -2922,41 +2324,6 @@ mod tests { } } - #[track_caller] - fn simple_snapshot_output_contains( - args: Vec, - project: &str, - expected_patterns: &[&str], - buck: bool, - file: Option<&str>, - expected_code: i32, - ) { - if !buck || cfg!(feature = "buck") { - let (mut args, _path) = add_project(args, project, file, None); - if !buck { - args.push("--rebar".into()); - } - let (stdout, stderr, code) = elp(args); - assert_eq!( - code, expected_code, - "Expected exit code {expected_code}, got: {code}\nstdout:\n{stdout}\nstderr:\n{stderr}" - ); - - if expected_code == 0 { - assert!(stderr.is_empty(), "Expected empty stderr, got:\n{stderr}"); - } - - for pattern in expected_patterns { - assert!( - stdout.contains(pattern), - "Expected stdout to contain '{}', but got:\n{}", - pattern, - stdout - ); - } - } - } - #[allow(clippy::too_many_arguments)] fn check_lint_fix( args: Vec, @@ -3018,8 +2385,6 @@ mod tests { ); if let Some(expected_stderr) = expected_stderr { expected_stderr.assert_eq(&stderr); - } else { - expect![[""]].assert_eq(&stderr); } assert_normalised_file(expected, &stdout, path, false); for (expected_file, file) in files { @@ -3033,55 +2398,6 @@ mod tests { Ok(()) } - #[allow(clippy::too_many_arguments)] - fn check_lint_fix_stderr_sorted( - args: Vec, - project: &str, - expected: ExpectFile, - expected_code: i32, - buck: bool, - file: Option<&str>, - actual_dir: &Path, - expected_dir: &Path, - files: &[(&str, &str)], - backup_files: bool, - expected_stderr: Option, - ) -> Result<()> { - if !buck || cfg!(feature = "buck") { - let (mut args, path) = add_project(args, project, file, None); - if !buck { - args.push("--rebar".into()); - } - let orig_files = files.iter().map(|x| x.0).collect::>(); - // Take a backup. The Drop instance will restore at the end - let _backup = if backup_files { - BackupFiles::save_files(project, &orig_files) - } else { - BackupFiles::save_files(project, &[]) - }; - let (stdout, stderr, code) = elp(args); - assert_eq!( - code, expected_code, - "Expected exit code {expected_code}, got: {code}\nstdout:\n{stdout}\nstderr:\n{stderr}" - ); - if let Some(expected_stderr) = expected_stderr { - expected_stderr.assert_eq(&stderr); - } else { - expect![[""]].assert_eq(&stderr); - } - let sorted_stdout = sort_lines(&stdout); - assert_normalised_file(expected, &sorted_stdout, path, false); - for (expected_file, file) in files { - let expected = expect_file!(expected_dir.join(expected_file)); - let actual = actual_dir.join(file); - assert!(actual.exists()); - let content = fs::read_to_string(actual).unwrap(); - expected.assert_eq(content.as_str()); - } - } - Ok(()) - } - fn assert_normalised_file( expected: ExpectFile, actual: &str, @@ -3136,14 +2452,7 @@ mod tests { } fn project_path(project: &str) -> String { - format!("../../test/test_projects/{project}") - } - - fn strip_ansi_codes(s: &str) -> String { - lazy_static! { - static ref ANSI_RE: Regex = Regex::new(r"\x1b\[[0-9;]*m").unwrap(); - } - ANSI_RE.replace_all(s, "").to_string() + format!("../../test_projects/{project}") } struct BackupFiles { diff --git a/crates/elp/src/bin/reporting.rs b/crates/elp/src/bin/reporting.rs index 7ab10459fe..64fe8168c2 100644 --- a/crates/elp/src/bin/reporting.rs +++ b/crates/elp/src/bin/reporting.rs @@ -29,7 +29,6 @@ use elp::cli::Cli; use elp::convert; use elp::memory_usage::MemoryUsage; use elp_ide::Analysis; -use elp_ide::AnalysisHost; use elp_ide::TextRange; use elp_ide::elp_ide_db::EqwalizerDiagnostic; use elp_ide::elp_ide_db::elp_base_db::AbsPath; @@ -39,7 +38,6 @@ use indicatif::ProgressBar; use itertools::Itertools; use lazy_static::lazy_static; use parking_lot::Mutex; -use vfs::Vfs; pub trait Reporter { fn write_eqwalizer_diagnostics( @@ -227,6 +225,8 @@ impl Reporter for JsonReporter<'_> { diagnostics: &[EqwalizerDiagnostic], ) -> Result<()> { let line_index = self.analysis.line_index(file_id)?; + // Pass include_Tests = false so that errors for tests files that are not opted-in are tagged as arc_types::Severity::Disabled + let eqwalizer_enabled = self.analysis.is_eqwalizer_enabled(file_id, false).unwrap(); let file_path = &self.loaded.vfs.file_path(file_id); let root_path = &self .analysis @@ -235,8 +235,12 @@ impl Reporter for JsonReporter<'_> { .root_dir; let relative_path = get_relative_path(root_path, file_path); for diagnostic in diagnostics { - let diagnostic = - convert::eqwalizer_to_arc_diagnostic(diagnostic, &line_index, relative_path); + let diagnostic = convert::eqwalizer_to_arc_diagnostic( + diagnostic, + &line_index, + relative_path, + eqwalizer_enabled, + ); let diagnostic = serde_json::to_string(&diagnostic)?; writeln!(self.cli, "{diagnostic}")?; } @@ -254,7 +258,6 @@ impl Reporter for JsonReporter<'_> { "ELP".to_string(), diagnostic.msg.clone(), None, - None, ); let diagnostic = serde_json::to_string(&diagnostic)?; writeln!(self.cli, "{diagnostic}")?; @@ -278,7 +281,6 @@ impl Reporter for JsonReporter<'_> { "ELP".to_string(), description, None, - None, ); let diagnostic = serde_json::to_string(&diagnostic)?; writeln!(self.cli, "{diagnostic}")?; @@ -374,28 +376,3 @@ pub(crate) fn add_stat(stat: String) { let mut stats = STATS.lock(); stats.push(stat); } - -pub(crate) fn print_memory_usage( - mut host: AnalysisHost, - vfs: Vfs, - cli: &mut dyn Cli, -) -> Result<()> { - let mem = host.per_query_memory_usage(); - - let before = profile::memory_usage(); - drop(vfs); - let vfs = before.allocated - profile::memory_usage().allocated; - - let before = profile::memory_usage(); - drop(host); - let unaccounted = before.allocated - profile::memory_usage().allocated; - let remaining = profile::memory_usage().allocated; - - for (name, bytes, entries) in mem { - writeln!(cli, "{bytes:>8} {entries:>6} {name}")?; - } - writeln!(cli, "{vfs:>8} VFS")?; - writeln!(cli, "{unaccounted:>8} Unaccounted")?; - writeln!(cli, "{remaining:>8} Remaining")?; - Ok(()) -} diff --git a/crates/elp/src/bin/shell.rs b/crates/elp/src/bin/shell.rs index 13ff79ed36..490c852893 100644 --- a/crates/elp/src/bin/shell.rs +++ b/crates/elp/src/bin/shell.rs @@ -15,7 +15,6 @@ use std::path::Path; use std::path::PathBuf; use std::process::Command; use std::sync::Arc; -use std::time::SystemTime; use anyhow::Result; use elp::build::load; @@ -30,7 +29,6 @@ use elp_ide::elp_ide_db::elp_base_db::SourceDatabaseExt; use elp_ide::elp_ide_db::elp_base_db::SourceRoot; use elp_ide::elp_ide_db::elp_base_db::SourceRootId; use elp_ide::elp_ide_db::elp_base_db::VfsPath; -use elp_log::telemetry; use elp_project_model::DiscoverConfig; use elp_project_model::buck::BuckQueryConfig; use paths::Utf8PathBuf; @@ -157,9 +155,10 @@ impl ShellCommand { } "eqwalize-app" => { let include_generated = options.contains(&"--include-generated"); + let include_tests = options.contains(&"--include-tests"); if let Some(other) = options .into_iter() - .find(|&opt| opt != "--include-generated") + .find(|&opt| opt != "--include-generated" && opt != "--include-tests") { return Err(ShellError::UnexpectedOption( "eqwalize-app".into(), @@ -176,6 +175,7 @@ impl ShellCommand { rebar, app: app.into(), include_generated, + include_tests, bail_on_error: false, }))); } @@ -183,9 +183,10 @@ impl ShellCommand { } "eqwalize-all" => { let include_generated = options.contains(&"--include-generated"); + let include_tests = options.contains(&"--include-tests"); if let Some(other) = options .into_iter() - .find(|&opt| opt != "--include-generated") + .find(|&opt| opt != "--include-generated" && opt != "--include-tests") { return Err(ShellError::UnexpectedOption( "eqwalize-all".into(), @@ -201,6 +202,7 @@ impl ShellCommand { rebar, format: None, include_generated, + include_tests, bail_on_error: false, stats: false, list_modules: false, @@ -222,8 +224,10 @@ COMMANDS: eqwalize Eqwalize specified modules --clause-coverage Use experimental clause coverage checker eqwalize-all Eqwalize all modules in the current project + --include-tests Also eqwalize test modules from project --clause-coverage Use experimental clause coverage checker eqwalize-app Eqwalize all modules in specified application + --include-tests Also eqwalize test modules from project --clause-coverage Use experimental clause coverage checker "; @@ -327,7 +331,6 @@ fn update_changes( } pub fn run_shell(shell: &Shell, cli: &mut dyn Cli, query_config: &BuckQueryConfig) -> Result<()> { - let start_time = SystemTime::now(); let mut cmd = Command::new("watchman"); let _ = cmd.arg("--version").output().map_err(|_| { anyhow::Error::msg("`watchman` command not found. install it from https://facebook.github.io/watchman/ to use `elp shell`.") @@ -346,7 +349,6 @@ pub fn run_shell(shell: &Shell, cli: &mut dyn Cli, query_config: &BuckQueryConfi Mode::Shell, query_config, )?; - telemetry::report_elapsed_time("shell operational", start_time); let mut rl = rustyline::DefaultEditor::new()?; let mut last_read = watchman.get_clock()?; write!(cli, "{WELCOME}")?; @@ -401,6 +403,5 @@ pub fn run_shell(shell: &Shell, cli: &mut dyn Cli, query_config: &BuckQueryConfi } } } - telemetry::report_elapsed_time("shell done", start_time); Ok(()) } diff --git a/crates/elp/src/bin/ssr_cli.rs b/crates/elp/src/bin/ssr_cli.rs deleted file mode 100644 index 36f26d4554..0000000000 --- a/crates/elp/src/bin/ssr_cli.rs +++ /dev/null @@ -1,717 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is dual-licensed under either the MIT license found in the - * LICENSE-MIT file in the root directory of this source tree or the Apache - * License, Version 2.0 found in the LICENSE-APACHE file in the root directory - * of this source tree. You may select, at your option, one of the - * above-listed licenses. - */ - -use std::fs; -use std::path::Path; -use std::str; -use std::thread; -use std::time::SystemTime; - -use anyhow::Result; -use anyhow::bail; -use crossbeam_channel::unbounded; -use elp::build::load; -use elp::build::types::LoadResult; -use elp::cli::Cli; -use elp::convert; -use elp::memory_usage::MemoryUsage; -use elp::otp_file_to_ignore; -use elp_eqwalizer::Mode; -use elp_ide::Analysis; -use elp_ide::AnalysisHost; -use elp_ide::diagnostics; -use elp_ide::diagnostics::DiagnosticsConfig; -use elp_ide::diagnostics::FallBackToAll; -use elp_ide::diagnostics::LintConfig; -use elp_ide::diagnostics::LintsFromConfig; -use elp_ide::diagnostics::MatchSsr; -use elp_ide::elp_ide_db::LineCol; -use elp_ide::elp_ide_db::elp_base_db::AbsPath; -use elp_ide::elp_ide_db::elp_base_db::FileId; -use elp_ide::elp_ide_db::elp_base_db::IncludeOtp; -use elp_ide::elp_ide_db::elp_base_db::ModuleName; -use elp_ide::elp_ide_db::elp_base_db::ProjectId; -use elp_ide::elp_ide_db::elp_base_db::VfsPath; -use elp_log::telemetry; -use elp_project_model::AppName; -use elp_project_model::AppType; -use elp_project_model::DiscoverConfig; -use elp_project_model::buck::BuckQueryConfig; -use hir::Semantic; -use paths::Utf8PathBuf; -use rayon::prelude::ParallelBridge; -use rayon::prelude::ParallelIterator; - -use crate::args::Ssr; -use crate::reporting; -use crate::reporting::print_memory_usage; - -fn normalize_ssr_pattern(pattern: &str) -> String { - if pattern.starts_with("ssr:") { - pattern.to_string() - } else { - format!("ssr: {}.", pattern) - } -} - -pub fn run_ssr_command( - args: &Ssr, - cli: &mut dyn Cli, - query_config: &BuckQueryConfig, - use_color: bool, -) -> Result<()> { - let start_time = SystemTime::now(); - let memory_start = MemoryUsage::now(); - - // Validate all SSR patterns early - let analysis_host = AnalysisHost::default(); - let analysis = analysis_host.analysis(); - for pattern in &args.ssr_specs { - let normalized_pattern = normalize_ssr_pattern(pattern); - match analysis.validate_ssr_pattern(&normalized_pattern) { - Ok(Ok(())) => {} - Ok(Err(e)) => bail!("invalid SSR pattern '{}': {}", pattern, e), - Err(_cancelled) => bail!("SSR pattern validation was cancelled"), - } - } - - // Parse the strategy from CLI arguments - let strategy = args.parse_strategy()?; - - // Create the lint config with all SSR patterns - let mut lint_config = LintConfig::default(); - for pattern in &args.ssr_specs { - let normalized_pattern = normalize_ssr_pattern(pattern); - let severity = if args.dump_config { - // Set the severity so that squiggles are shown in the VS Code UI - Some(diagnostics::Severity::Information) - } else { - None - }; - let ssr_lint = diagnostics::Lint::LintMatchSsr(MatchSsr { - ssr_pattern: normalized_pattern, - message: None, - strategy: Some(strategy), - severity, - }); - lint_config.ad_hoc_lints.lints.push(ssr_lint); - } - - // Build the diagnostics config - let diagnostics_config = DiagnosticsConfig::default() - .configure_diagnostics( - &lint_config, - &Some("ad-hoc: ssr-match".to_string()), - &None, - FallBackToAll::Yes, - )? - .set_include_generated(args.include_generated) - .set_experimental(false) - .set_use_cli_severity(false); - - if args.dump_config { - let result = toml::to_string::(&diagnostics_config.lints_from_config)?; - // This is a subsection of .elp_lint.toml, add subsection prefix - let result = result.replace("[[lints]]", "[[ad_hoc_lints.lints]]"); - writeln!(cli, "\n# Add this to your .elp_lint.toml")?; - writeln!(cli, "{}", result)?; - return Ok(()); - } - - // Load the project - let mut loaded = load_project(args, cli, query_config)?; - telemetry::report_elapsed_time("ssr operational", start_time); - - let r = run_ssr(cli, &mut loaded, &diagnostics_config, args, use_color); - - telemetry::report_elapsed_time("ssr done", start_time); - - let memory_end = MemoryUsage::now(); - let memory_used = memory_end - memory_start; - - // Print memory usage at the end if requested and format is normal - if args.is_format_normal() && args.report_system_stats { - print_memory_usage(loaded.analysis_host, loaded.vfs, cli)?; - writeln!(cli, "{}", memory_used)?; - } - r -} - -pub fn run_ssr( - cli: &mut dyn Cli, - loaded: &mut LoadResult, - diagnostics_config: &DiagnosticsConfig, - args: &Ssr, - use_color: bool, -) -> Result<()> { - let analysis = loaded.analysis(); - let (file_id, name) = match &args.module { - Some(module) => match analysis.module_file_id(loaded.project_id, module)? { - Some(file_id) => { - if args.is_format_normal() { - writeln!(cli, "module specified: {module}")?; - } - (Some(file_id), analysis.module_name(file_id)?) - } - None => panic!("Module not found: {module}"), - }, - None => match &args.file { - Some(file_name) => { - if args.is_format_normal() { - writeln!(cli, "file specified: {file_name}")?; - } - let path_buf = Utf8PathBuf::from_path_buf(fs::canonicalize(file_name).unwrap()) - .expect("UTF8 conversion failed"); - let path = AbsPath::assert(&path_buf); - let path = path.as_os_str().to_str().unwrap(); - ( - loaded - .vfs - .file_id(&VfsPath::new_real_path(path.to_string())) - .map(|(id, _)| id), - path_buf.as_path().file_name().map(ModuleName::new), - ) - } - None => (None, None), - }, - }; - - let mut match_count = 0; - - match (file_id, name) { - (None, _) => { - // Streaming case: process all modules - let project_id = loaded.project_id; - do_parse_all_streaming( - cli, - &analysis, - &project_id, - diagnostics_config, - args, - use_color, - loaded, - &mut match_count, - )?; - } - (Some(file_id), Some(name)) => { - if let Some(app) = &args.app - && let Ok(Some(file_app)) = analysis.file_app_name(file_id) - && file_app != AppName(app.to_string()) - { - panic!("Module {} does not belong to app {}", name.as_str(), app) - } - if let Some(diag) = do_parse_one(&analysis, diagnostics_config, file_id, &name, args)? { - match_count = 1; - print_single_result(cli, loaded, &diag, args, use_color)?; - } - } - (Some(file_id), _) => { - panic!("Could not get name from file_id for {file_id:?}") - } - }; - - if match_count == 0 { - if args.is_format_normal() { - writeln!(cli, "No matches found")?; - } - } else if args.is_format_normal() { - writeln!(cli, "\nMatches found in {} modules", match_count)?; - } - - Ok(()) -} - -#[allow(clippy::too_many_arguments)] -fn do_parse_all_streaming( - cli: &mut dyn Cli, - analysis: &Analysis, - project_id: &ProjectId, - config: &DiagnosticsConfig, - args: &Ssr, - use_color: bool, - loaded: &mut LoadResult, - match_count: &mut usize, -) -> Result<()> { - let module_index = analysis.module_index(*project_id).unwrap(); - let app_name = args.app.as_ref().map(|name| AppName(name.to_string())); - - // Create a channel for streaming results - let (tx, rx) = unbounded(); - - // Spawn a thread to process modules in parallel and send results - let analysis_clone = analysis.clone(); - let config_clone = config.clone(); - let args_clone = args.clone(); - - // Collect modules into an owned vector - let modules: Vec<_> = module_index - .iter_own() - .map(|(name, source, file_id)| (name.as_str().to_string(), source, file_id)) - .collect(); - - thread::spawn(move || { - modules - .into_iter() - .par_bridge() - .map_with( - (analysis_clone, tx), - |(db, tx), (module_name, _file_source, file_id)| { - if !otp_file_to_ignore(db, file_id) - && db.file_app_type(file_id).ok() != Some(Some(AppType::Dep)) - && (app_name.is_none() - || db.file_app_name(file_id).ok().as_ref() == Some(&app_name)) - && let Ok(Some(result)) = - do_parse_one(db, &config_clone, file_id, &module_name, &args_clone) - { - // Send result through channel - let _ = tx.send(result); - } - }, - ) - .for_each(|_| {}); // Consume the iterator - // Channel is dropped here, signaling end of results - }); - - // Process and print results as they arrive from the channel - for result in rx { - *match_count += 1; - print_single_result(cli, loaded, &result, args, use_color)?; - } - - Ok(()) -} - -fn print_single_result( - cli: &mut dyn Cli, - loaded: &mut LoadResult, - result: &(String, FileId, Vec), - args: &Ssr, - use_color: bool, -) -> Result<()> { - let (name, file_id, diags) = result; - - if args.is_format_json() { - for diag in diags { - let vfs_path = loaded.vfs.file_path(*file_id); - let analysis = loaded.analysis(); - let root_path = &analysis - .project_data(*file_id) - .unwrap_or_else(|_err| panic!("could not find project data")) - .unwrap_or_else(|| panic!("could not find project data")) - .root_dir; - let relative_path = reporting::get_relative_path(root_path, vfs_path); - print_diagnostic_json(diag, &analysis, *file_id, relative_path, false, cli)?; - } - } else { - writeln!(cli, " {}: {}", name, diags.len())?; - - // Determine if we should show source context - let show_source = args.show_source - || args.before_context.is_some() - || args.after_context.is_some() - || args.context.is_some() - || args.group_separator.is_some() - || args.no_group_separator; - let (before_lines, after_lines) = calculate_context_lines(args); - let has_context = before_lines > 0 || after_lines > 0; - let group_separator = should_show_group_separator(args, has_context && show_source); - - for (idx, diag) in diags.iter().enumerate() { - // Print group separator before each match (except the first) if showing source with context - if show_source - && idx > 0 - && let Some(ref sep) = group_separator - { - writeln!(cli, "{}", sep)?; - } - // Get relative path for diagnostic output - let vfs_path = loaded.vfs.file_path(*file_id); - let analysis = loaded.analysis(); - let root_path = &analysis - .project_data(*file_id) - .unwrap_or_else(|_err| panic!("could not find project data")) - .unwrap_or_else(|| panic!("could not find project data")) - .root_dir; - let relative_path = reporting::get_relative_path(root_path, vfs_path); - - // Only show path when showing source context - let path_to_show = if show_source { - Some(relative_path) - } else { - None - }; - print_diagnostic(diag, &loaded.analysis(), *file_id, path_to_show, false, cli)?; - - // Only show source context if --show-source or --show-source-markers is set - if show_source { - if use_color { - print_source_with_context( - diag, - &loaded.analysis(), - *file_id, - before_lines, - after_lines, - true, - cli, - )?; - } else { - print_source_with_context_markers( - diag, - &loaded.analysis(), - *file_id, - before_lines, - after_lines, - cli, - )?; - } - writeln!(cli)?; - } - } - } - Ok(()) -} - -fn load_project( - args: &Ssr, - cli: &mut dyn Cli, - query_config: &BuckQueryConfig, -) -> Result { - log::info!("Loading project at: {:?}", args.project); - let config = DiscoverConfig::new(args.rebar, &args.profile); - load::load_project_at( - cli, - &args.project, - config, - IncludeOtp::Yes, - Mode::Server, - query_config, - ) -} -fn do_parse_one( - db: &Analysis, - config: &DiagnosticsConfig, - file_id: FileId, - name: &str, - args: &Ssr, -) -> Result)>> { - if !args.include_generated && db.is_generated(file_id)? { - return Ok(None); - } - if !args.include_tests && db.is_test_suite_or_test_helper(file_id)?.unwrap_or(false) { - return Ok(None); - } - - // Run only the SSR lint configured in lints_from_config - let diagnostics = db.with_db(|database| { - let sema = Semantic::new(database); - let mut diags = Vec::new(); - config - .lints_from_config - .get_diagnostics(&mut diags, &sema, file_id); - diags - })?; - - if !diagnostics.is_empty() { - let res = (name.to_string(), file_id, diagnostics); - Ok(Some(res)) - } else { - Ok(None) - } -} - -fn print_diagnostic( - diag: &diagnostics::Diagnostic, - analysis: &Analysis, - file_id: FileId, - path: Option<&Path>, - use_cli_severity: bool, - cli: &mut dyn Cli, -) -> Result<(), anyhow::Error> { - let line_index = analysis.line_index(file_id)?; - let diag_str = diag.print(&line_index, use_cli_severity); - if let Some(path) = path { - writeln!(cli, "{}:{}", path.display(), diag_str)?; - } else { - writeln!(cli, " {}", diag_str)?; - } - Ok(()) -} - -fn print_diagnostic_json( - diagnostic: &diagnostics::Diagnostic, - analysis: &Analysis, - file_id: FileId, - path: &Path, - use_cli_severity: bool, - cli: &mut dyn Cli, -) -> Result<(), anyhow::Error> { - let line_index = analysis.line_index(file_id)?; - let converted_diagnostic = - convert::ide_to_arc_diagnostic(&line_index, path, diagnostic, use_cli_severity); - writeln!( - cli, - "{}", - serde_json::to_string(&converted_diagnostic).unwrap_or_else(|err| panic!( - "print_diagnostics_json failed for '{converted_diagnostic:?}': {err}" - )) - )?; - Ok(()) -} - -/// Print a line with color highlighting -fn print_line_with_color( - line_num: usize, - line_content: &str, - is_match_line: bool, - start: &LineCol, - end: &LineCol, - current_line: u32, - cli: &mut dyn Cli, -) -> Result<(), anyhow::Error> { - // Line number in gray - write!(cli, "\x1b[90m{:4} |\x1b[0m ", line_num)?; - - if !is_match_line { - // Non-match line: print normally - writeln!(cli, "{}", line_content)?; - } else { - // Match line: highlight the matched portion - if current_line == start.line && current_line == end.line { - // Single-line match - let start_col = start.col_utf16 as usize; - let end_col = end.col_utf16 as usize; - - let before = &line_content[..start_col.min(line_content.len())]; - let matched = - &line_content[start_col.min(line_content.len())..end_col.min(line_content.len())]; - let after = &line_content[end_col.min(line_content.len())..]; - - write!(cli, "{}", before)?; - write!(cli, "\x1b[91;1m{}\x1b[0m", matched)?; // Red bold - writeln!(cli, "{}", after)?; - } else if current_line == start.line { - // First line of multi-line match - let start_col = start.col_utf16 as usize; - let before = &line_content[..start_col.min(line_content.len())]; - let matched = &line_content[start_col.min(line_content.len())..]; - - write!(cli, "{}", before)?; - writeln!(cli, "\x1b[91;1m{}\x1b[0m", matched)?; // Red bold - } else if current_line == end.line { - // Last line of multi-line match - let end_col = end.col_utf16 as usize; - let matched = &line_content[..end_col.min(line_content.len())]; - let after = &line_content[end_col.min(line_content.len())..]; - - write!(cli, "\x1b[91;1m{}\x1b[0m", matched)?; // Red bold - writeln!(cli, "{}", after)?; - } else { - // Middle line of multi-line match - writeln!(cli, "\x1b[91;1m{}\x1b[0m", line_content)?; // Red bold - } - } - - Ok(()) -} - -/// Calculate context lines from the new grep-style arguments -fn calculate_context_lines(args: &Ssr) -> (usize, usize) { - // -C/--context takes precedence and sets both before and after - if let Some(context) = args.context { - return (context, context); - } - - // Otherwise use individual before/after values, defaulting to 0 - let before = args.before_context.unwrap_or(0); - let after = args.after_context.unwrap_or(0); - (before, after) -} - -/// Determine if a group separator should be shown -fn should_show_group_separator(args: &Ssr, has_context: bool) -> Option { - // If --no-group-separator is set, don't show separator - if args.no_group_separator { - return None; - } - - // Only show separators if there's context to separate - if !has_context { - return None; - } - - // Use custom separator if provided, otherwise default to "--" - Some( - args.group_separator - .clone() - .unwrap_or_else(|| "--".to_string()), - ) -} - -/// Print source code context with the specified before/after context lines -fn print_source_with_context( - diag: &diagnostics::Diagnostic, - analysis: &Analysis, - file_id: FileId, - before_lines: usize, - after_lines: usize, - use_color: bool, - cli: &mut dyn Cli, -) -> Result<(), anyhow::Error> { - let line_index = analysis.line_index(file_id)?; - let source = &analysis.file_text(file_id)?; - - let range = diag.range; - let start = line_index.line_col(range.start()); - let end = line_index.line_col(range.end()); - - let lines: Vec<&str> = source.lines().collect(); - let total_lines = lines.len(); - - // Calculate the range of lines to display - let first_line = start.line.saturating_sub(before_lines as u32) as usize; - let last_line = ((end.line + after_lines as u32 + 1) as usize).min(total_lines); - - // Display the source context - for line_idx in first_line..last_line { - let line_num = line_idx + 1; - let line_content = lines.get(line_idx).unwrap_or(&""); - - // Check if this line contains part of the match - let is_match_line = line_idx >= start.line as usize && line_idx <= end.line as usize; - - if use_color { - print_line_with_color( - line_num, - line_content, - is_match_line, - &start, - &end, - line_idx as u32, - cli, - )?; - } else { - // Just print the line without any highlighting - write!(cli, "{:4} | ", line_num)?; - writeln!(cli, "{}", line_content)?; - } - } - - Ok(()) -} - -/// Print source code context with text markers -fn print_source_with_context_markers( - diag: &diagnostics::Diagnostic, - analysis: &Analysis, - file_id: FileId, - before_lines: usize, - after_lines: usize, - cli: &mut dyn Cli, -) -> Result<(), anyhow::Error> { - let line_index = analysis.line_index(file_id)?; - let source = &analysis.file_text(file_id)?; - - let range = diag.range; - let start = line_index.line_col(range.start()); - let end = line_index.line_col(range.end()); - - let lines: Vec<&str> = source.lines().collect(); - let total_lines = lines.len(); - - // Calculate the range of lines to display - let first_line = start.line.saturating_sub(before_lines as u32) as usize; - let last_line = ((end.line + after_lines as u32 + 1) as usize).min(total_lines); - - // Display the source context - for line_idx in first_line..last_line { - let line_num = line_idx + 1; - let line_content = lines.get(line_idx).unwrap_or(&""); - - // Check if this line contains part of the match - let is_match_line = line_idx >= start.line as usize && line_idx <= end.line as usize; - - print_line_with_markers( - line_num, - line_content, - is_match_line, - &start, - &end, - line_idx as u32, - cli, - )?; - } - - Ok(()) -} - -/// Print a line with text markers (like diagnostic carets) -fn print_line_with_markers( - line_num: usize, - line_content: &str, - is_match_line: bool, - start: &LineCol, - end: &LineCol, - current_line: u32, - cli: &mut dyn Cli, -) -> Result<(), anyhow::Error> { - // Line number - write!(cli, "{:4} | ", line_num)?; - writeln!(cli, "{}", line_content)?; - - if is_match_line { - // Print marker line with ^^^ under the match - write!(cli, " | ")?; // Indent to match line content - - if current_line == start.line && current_line == end.line { - // Single-line match - let start_col = start.col_utf16 as usize; - let end_col = end.col_utf16 as usize; - let marker_len = (end_col - start_col).max(1); - - // Spaces before the marker - for _ in 0..start_col { - write!(cli, " ")?; - } - // Marker carets - for _ in 0..marker_len { - write!(cli, "^")?; - } - writeln!(cli)?; - } else if current_line == start.line { - // First line of multi-line match - let start_col = start.col_utf16 as usize; - let marker_len = line_content.len().saturating_sub(start_col).max(1); - - for _ in 0..start_col { - write!(cli, " ")?; - } - for _ in 0..marker_len { - write!(cli, "^")?; - } - writeln!(cli)?; - } else if current_line == end.line { - // Last line of multi-line match - let end_col = end.col_utf16 as usize; - - for _ in 0..end_col { - write!(cli, "^")?; - } - writeln!(cli)?; - } else { - // Middle line of multi-line match - for _ in 0..line_content.len() { - write!(cli, "^")?; - } - writeln!(cli)?; - } - } - - Ok(()) -} diff --git a/crates/elp/src/build/load.rs b/crates/elp/src/build/load.rs index 67457e049f..cb9cb6570f 100644 --- a/crates/elp/src/build/load.rs +++ b/crates/elp/src/build/load.rs @@ -80,7 +80,12 @@ pub fn load_project_at( log::info!("Discovered project: {manifest:?}"); let pb = cli.spinner("Loading build info"); - let project = Project::load(&manifest, &elp_config, query_config, &|_progress| {})?; + let project = Project::load( + &manifest, + elp_config.eqwalizer.clone(), + query_config, + &|_progress| {}, + )?; pb.finish(); load_project(cli, project, include_otp, eqwalizer_mode) diff --git a/crates/elp/src/cli.rs b/crates/elp/src/cli.rs index 0678aaebc1..b3070768d6 100644 --- a/crates/elp/src/cli.rs +++ b/crates/elp/src/cli.rs @@ -30,13 +30,18 @@ pub trait Cli: Write + WriteColor { fn err(&mut self) -> &mut dyn Write; } -pub struct StandardCli(StandardStream, Stderr); +pub struct Real(StandardStream, Stderr); -impl StandardCli { - fn new(color_choice: ColorChoice) -> Self { - Self(StandardStream::stdout(color_choice), std::io::stderr()) +impl Default for Real { + fn default() -> Self { + Self( + StandardStream::stdout(ColorChoice::Always), + std::io::stderr(), + ) } +} +impl Real { fn progress_with_style( &self, len: u64, @@ -54,7 +59,7 @@ impl StandardCli { } } -impl Cli for StandardCli { +impl Cli for Real { fn progress(&self, len: u64, prefix: &'static str) -> ProgressBar { self.progress_with_style(len, prefix, " {prefix:25!} {bar} {pos}/{len} {wide_msg}") } @@ -79,63 +84,6 @@ impl Cli for StandardCli { } } -impl Write for StandardCli { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - self.0.write(buf) - } - - fn flush(&mut self) -> std::io::Result<()> { - self.0.flush() - } -} - -impl WriteColor for StandardCli { - fn supports_color(&self) -> bool { - self.0.supports_color() - } - - fn set_color(&mut self, spec: &ColorSpec) -> std::io::Result<()> { - self.0.set_color(spec) - } - - fn reset(&mut self) -> std::io::Result<()> { - self.0.reset() - } -} - -pub struct Real(StandardCli); -pub struct NoColor(StandardCli); - -impl Default for Real { - fn default() -> Self { - Real(StandardCli::new(ColorChoice::Always)) - } -} - -impl Default for NoColor { - fn default() -> Self { - NoColor(StandardCli::new(ColorChoice::Never)) - } -} - -impl Cli for Real { - fn progress(&self, len: u64, prefix: &'static str) -> ProgressBar { - self.0.progress(len, prefix) - } - - fn simple_progress(&self, len: u64, prefix: &'static str) -> ProgressBar { - self.0.simple_progress(len, prefix) - } - - fn spinner(&self, prefix: &'static str) -> ProgressBar { - self.0.spinner(prefix) - } - - fn err(&mut self) -> &mut dyn Write { - self.0.err() - } -} - impl Write for Real { fn write(&mut self, buf: &[u8]) -> std::io::Result { self.0.write(buf) @@ -160,48 +108,6 @@ impl WriteColor for Real { } } -impl Cli for NoColor { - fn progress(&self, len: u64, prefix: &'static str) -> ProgressBar { - self.0.progress(len, prefix) - } - - fn simple_progress(&self, len: u64, prefix: &'static str) -> ProgressBar { - self.0.simple_progress(len, prefix) - } - - fn spinner(&self, prefix: &'static str) -> ProgressBar { - self.0.spinner(prefix) - } - - fn err(&mut self) -> &mut dyn Write { - self.0.err() - } -} - -impl Write for NoColor { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - self.0.write(buf) - } - - fn flush(&mut self) -> std::io::Result<()> { - self.0.flush() - } -} - -impl WriteColor for NoColor { - fn supports_color(&self) -> bool { - self.0.supports_color() - } - - fn set_color(&mut self, spec: &ColorSpec) -> std::io::Result<()> { - self.0.set_color(spec) - } - - fn reset(&mut self) -> std::io::Result<()> { - self.0.reset() - } -} - pub struct Fake(Buffer, Vec); impl Default for Fake { diff --git a/crates/elp/src/config.rs b/crates/elp/src/config.rs index 681a022261..fab48abbe1 100644 --- a/crates/elp/src/config.rs +++ b/crates/elp/src/config.rs @@ -30,7 +30,7 @@ use serde::de::DeserializeOwned; use serde_json::json; use crate::from_json; -// @fb-only: use crate::meta_only; +// @fb-only // Defines the server-side configuration of ELP. We generate *parts* // of VS Code's `package.json` config from this. @@ -42,8 +42,6 @@ use crate::from_json; // `new_name | `old_name` so that we keep parsing the old name. config_data! { struct ConfigData { - /// Whether to use the expermintal `buck2 targets` quick start process. - buck_quickStart: bool = json! { false }, /// Whether to show experimental ELP diagnostics that might /// have more false positives than usual. diagnostics_enableExperimental: bool = json! { false }, @@ -91,12 +89,6 @@ config_data! { /// Whether to show the `Link` lenses. Only applies when /// `#elp.lens.enable#` is set. lens_links_enable: bool = json! { false }, - /// Whether to enable LogView lens links. - lens_logview_links: bool = json! { false }, - /// Whether to enable Scuba lens links. - lens_scuba_links: bool = json! { false }, - /// Whether to enable WAM lens links. - lens_wam_links: bool = json! { false }, /// Configure LSP-based logging using env_logger syntax. log: String = json! { "error" }, /// Whether to show Signature Help. @@ -136,9 +128,6 @@ pub struct LensConfig { pub buck2_mode: Option, pub debug: bool, pub links: bool, - pub logview_links: bool, - pub scuba_links: bool, - pub wam_links: bool, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -180,7 +169,7 @@ impl Config { return; } self.data = ConfigData::from_json(json); - // @fb-only: meta_only::harmonise_gks(self); + // @fb-only } pub fn update_gks(&mut self, json: serde_json::Value) { @@ -334,9 +323,6 @@ impl Config { && self.data.lens_run_coverage_enable, debug: self.data.lens_enable && self.data.lens_debug_enable, links: self.data.lens_enable && self.data.lens_links_enable, - logview_links: self.data.lens_enable && self.data.lens_logview_links, - scuba_links: self.data.lens_enable && self.data.lens_scuba_links, - wam_links: self.data.lens_enable && self.data.lens_wam_links, } } @@ -381,38 +367,14 @@ impl Config { try_or!(self.caps.window.as_ref()?.work_done_progress?, false) } - pub fn set_buck_quick_start(&mut self, value: bool) { - self.data.buck_quickStart = value; - } - - pub fn buck_quick_start(&self) -> bool { - self.data.buck_quickStart - } - pub fn buck_query(&self) -> BuckQueryConfig { - if self.buck_quick_start() { - BuckQueryConfig::BuckTargetsOnly - } else { - BuckQueryConfig::BuildGeneratedCode - } + BuckQueryConfig::BuildGeneratedCode } pub fn set_eqwalizer_all(&mut self, value: bool) { self.data.eqwalizer_all = value; } - pub fn set_lens_logview_links(&mut self, value: bool) { - self.data.lens_logview_links = value; - } - - pub fn set_lens_scuba_links(&mut self, value: bool) { - self.data.lens_scuba_links = value; - } - - pub fn set_lens_wam_links(&mut self, value: bool) { - self.data.lens_wam_links = value; - } - pub fn inlay_hints(&self) -> InlayHintsConfig { InlayHintsConfig { parameter_hints: self.data.inlayHints_parameterHints_enable, @@ -618,15 +580,10 @@ mod tests { let s = remove_ws(&schema); - expect![[r#""elp.buck.quickStart":{"default":false,"markdownDescription":"Whethertousetheexpermintal`buck2targets`quickstartprocess.","type":"boolean"},"elp.diagnostics.disabled":{"default":[],"items":{"type":"string"},"markdownDescription":"ListofELPdiagnosticstodisable.","type":"array","uniqueItems":true},"elp.diagnostics.enableExperimental":{"default":false,"markdownDescription":"WhethertoshowexperimentalELPdiagnosticsthatmight\nhavemorefalsepositivesthanusual.","type":"boolean"},"elp.diagnostics.enableOtp":{"default":false,"markdownDescription":"WhethertoreportdiagnosticsforOTPfiles.","type":"boolean"},"elp.diagnostics.onSave.enable":{"default":false,"markdownDescription":"Updatenativediagnosticsonlywhenthefileissaved.","type":"boolean"},"elp.edoc.enable":{"default":false,"markdownDescription":"WhethertoreportEDocdiagnostics.","type":"boolean"},"elp.eqwalizer.all":{"default":false,"markdownDescription":"WhethertoreportEqwalizerdiagnosticsforthewholeprojectandnotonlyforopenedfiles.","type":"boolean"},"elp.eqwalizer.chunkSize":{"default":100,"markdownDescription":"Chunksizetouseforproject-wideeqwalization.","minimum":0,"type":"integer"},"elp.eqwalizer.maxTasks":{"default":32,"markdownDescription":"Maximumnumberoftaskstoruninparallelforproject-wideeqwalization.","minimum":0,"type":"integer"},"elp.highlightDynamic.enable":{"default":false,"markdownDescription":"Ifenabled,highlightvariableswithtype`dynamic()`whenEqwalizerresultsareavailable.","type":"boolean"},"elp.hoverActions.docLinks.enable":{"default":false,"markdownDescription":"WhethertoshowHoverActionsoftype`docs`.Onlyapplieswhen\n`#elp.hoverActions.enable#`isset.","type":"boolean"},"elp.hoverActions.enable":{"default":false,"markdownDescription":"WhethertoshowHoverActions.","type":"boolean"},"elp.inlayHints.parameterHints.enable":{"default":true,"markdownDescription":"Whethertoshowfunctionparameternameinlayhintsatthecall\nsite.","type":"boolean"},"elp.lens.buck2.mode":{"default":null,"markdownDescription":"Thebuck2modetouseforrunningtestsviathecodelenses.","type":["null","string"]},"elp.lens.debug.enable":{"default":false,"markdownDescription":"Whethertoshowthe`Debug`lenses.Onlyapplieswhen\n`#elp.lens.enable#`isset.","type":"boolean"},"elp.lens.enable":{"default":false,"markdownDescription":"WhethertoshowCodeLensesinErlangfiles.","type":"boolean"},"elp.lens.links.enable":{"default":false,"markdownDescription":"Whethertoshowthe`Link`lenses.Onlyapplieswhen\n`#elp.lens.enable#`isset.","type":"boolean"},"elp.lens.logview.links":{"default":false,"markdownDescription":"WhethertoenableLogViewlenslinks.","type":"boolean"},"elp.lens.run.coverage.enable":{"default":true,"markdownDescription":"Displaycodecoverageinformationwhenrunningtestsviathe\nCodeLenses.Onlyapplieswhen`#elp.lens.enabled`and\n`#elp.lens.run.enable#`areset.","type":"boolean"},"elp.lens.run.enable":{"default":false,"markdownDescription":"Whethertoshowthe`Run`lenses.Onlyapplieswhen\n`#elp.lens.enable#`isset.","type":"boolean"},"elp.lens.run.interactive.enable":{"default":false,"markdownDescription":"Whethertoshowthe`RunInteractive`lenses.Onlyapplieswhen\n`#elp.lens.enable#`isset.","type":"boolean"},"elp.lens.scuba.links":{"default":false,"markdownDescription":"WhethertoenableScubalenslinks.","type":"boolean"},"elp.lens.wam.links":{"default":false,"markdownDescription":"WhethertoenableWAMlenslinks.","type":"boolean"},"elp.log":{"default":"error","markdownDescription":"ConfigureLSP-basedloggingusingenv_loggersyntax.","type":"string"},"elp.signatureHelp.enable":{"default":true,"markdownDescription":"WhethertoshowSignatureHelp.","type":"boolean"},"elp.typesOnHover.enable":{"default":false,"markdownDescription":"Displaytypeswhenhoveringoverexpressions.","type":"boolean"},"#]] + expect![[r#""elp.diagnostics.disabled":{"default":[],"items":{"type":"string"},"markdownDescription":"ListofELPdiagnosticstodisable.","type":"array","uniqueItems":true},"elp.diagnostics.enableExperimental":{"default":false,"markdownDescription":"WhethertoshowexperimentalELPdiagnosticsthatmight\nhavemorefalsepositivesthanusual.","type":"boolean"},"elp.diagnostics.enableOtp":{"default":false,"markdownDescription":"WhethertoreportdiagnosticsforOTPfiles.","type":"boolean"},"elp.diagnostics.onSave.enable":{"default":false,"markdownDescription":"Updatenativediagnosticsonlywhenthefileissaved.","type":"boolean"},"elp.edoc.enable":{"default":false,"markdownDescription":"WhethertoreportEDocdiagnostics.","type":"boolean"},"elp.eqwalizer.all":{"default":false,"markdownDescription":"WhethertoreportEqwalizerdiagnosticsforthewholeprojectandnotonlyforopenedfiles.","type":"boolean"},"elp.eqwalizer.chunkSize":{"default":100,"markdownDescription":"Chunksizetouseforproject-wideeqwalization.","minimum":0,"type":"integer"},"elp.eqwalizer.maxTasks":{"default":32,"markdownDescription":"Maximumnumberoftaskstoruninparallelforproject-wideeqwalization.","minimum":0,"type":"integer"},"elp.highlightDynamic.enable":{"default":false,"markdownDescription":"Ifenabled,highlightvariableswithtype`dynamic()`whenEqwalizerresultsareavailable.","type":"boolean"},"elp.hoverActions.docLinks.enable":{"default":false,"markdownDescription":"WhethertoshowHoverActionsoftype`docs`.Onlyapplieswhen\n`#elp.hoverActions.enable#`isset.","type":"boolean"},"elp.hoverActions.enable":{"default":false,"markdownDescription":"WhethertoshowHoverActions.","type":"boolean"},"elp.inlayHints.parameterHints.enable":{"default":true,"markdownDescription":"Whethertoshowfunctionparameternameinlayhintsatthecall\nsite.","type":"boolean"},"elp.lens.buck2.mode":{"default":null,"markdownDescription":"Thebuck2modetouseforrunningtestsviathecodelenses.","type":["null","string"]},"elp.lens.debug.enable":{"default":false,"markdownDescription":"Whethertoshowthe`Debug`lenses.Onlyapplieswhen\n`#elp.lens.enable#`isset.","type":"boolean"},"elp.lens.enable":{"default":false,"markdownDescription":"WhethertoshowCodeLensesinErlangfiles.","type":"boolean"},"elp.lens.links.enable":{"default":false,"markdownDescription":"Whethertoshowthe`Link`lenses.Onlyapplieswhen\n`#elp.lens.enable#`isset.","type":"boolean"},"elp.lens.run.coverage.enable":{"default":true,"markdownDescription":"Displaycodecoverageinformationwhenrunningtestsviathe\nCodeLenses.Onlyapplieswhen`#elp.lens.enabled`and\n`#elp.lens.run.enable#`areset.","type":"boolean"},"elp.lens.run.enable":{"default":false,"markdownDescription":"Whethertoshowthe`Run`lenses.Onlyapplieswhen\n`#elp.lens.enable#`isset.","type":"boolean"},"elp.lens.run.interactive.enable":{"default":false,"markdownDescription":"Whethertoshowthe`RunInteractive`lenses.Onlyapplieswhen\n`#elp.lens.enable#`isset.","type":"boolean"},"elp.log":{"default":"error","markdownDescription":"ConfigureLSP-basedloggingusingenv_loggersyntax.","type":"string"},"elp.signatureHelp.enable":{"default":true,"markdownDescription":"WhethertoshowSignatureHelp.","type":"boolean"},"elp.typesOnHover.enable":{"default":false,"markdownDescription":"Displaytypeswhenhoveringoverexpressions.","type":"boolean"},"#]] .assert_eq(s.as_str()); expect![[r#" - "elp.buck.quickStart": { - "default": false, - "markdownDescription": "Whether to use the expermintal `buck2 targets` quick start process.", - "type": "boolean" - }, "elp.diagnostics.disabled": { "default": [], "items": { @@ -716,11 +673,6 @@ mod tests { "markdownDescription": "Whether to show the `Link` lenses. Only applies when\n`#elp.lens.enable#` is set.", "type": "boolean" }, - "elp.lens.logview.links": { - "default": false, - "markdownDescription": "Whether to enable LogView lens links.", - "type": "boolean" - }, "elp.lens.run.coverage.enable": { "default": true, "markdownDescription": "Display code coverage information when running tests via the\nCode Lenses. Only applies when `#elp.lens.enabled` and\n`#elp.lens.run.enable#` are set.", @@ -736,16 +688,6 @@ mod tests { "markdownDescription": "Whether to show the `Run Interactive` lenses. Only applies when\n`#elp.lens.enable#` is set.", "type": "boolean" }, - "elp.lens.scuba.links": { - "default": false, - "markdownDescription": "Whether to enable Scuba lens links.", - "type": "boolean" - }, - "elp.lens.wam.links": { - "default": false, - "markdownDescription": "Whether to enable WAM lens links.", - "type": "boolean" - }, "elp.log": { "default": "error", "markdownDescription": "Configure LSP-based logging using env_logger syntax.", diff --git a/crates/elp/src/convert.rs b/crates/elp/src/convert.rs index c8db07a5d0..1f42e6f454 100644 --- a/crates/elp/src/convert.rs +++ b/crates/elp/src/convert.rs @@ -26,7 +26,6 @@ use elp_ide::elp_ide_db::assists::AssistContextDiagnostic; use elp_ide::elp_ide_db::assists::AssistContextDiagnosticCode; use elp_ide::elp_ide_db::elp_base_db::AbsPath; use elp_ide::elp_ide_db::elp_base_db::AbsPathBuf; -use elp_ide::elp_ide_db::elp_base_db::FileId; use elp_ide::elp_ide_db::elp_base_db::VfsPath; use lsp_types::DiagnosticRelatedInformation; use lsp_types::Location; @@ -68,14 +67,11 @@ pub fn diagnostic_severity(severity: Severity) -> lsp_types::DiagnosticSeverity } } -pub fn ide_to_lsp_diagnostic( +pub fn ide_to_lsp_diagnostic( line_index: &LineIndex, + url: &Url, d: &Diagnostic, - get_file_info: F, -) -> lsp_types::Diagnostic -where - F: Fn(FileId) -> Option<(LineIndex, Url)>, -{ +) -> lsp_types::Diagnostic { let code_description = match &d.code_doc_uri { Some(uri) => match lsp_types::Url::parse(uri) { Ok(href) => Some(lsp_types::CodeDescription { href }), @@ -94,16 +90,17 @@ where code_description, source, message: d.message.clone(), - related_information: from_related(get_file_info, &d.related_info), - tags: d.tag.as_ref().map(lsp_diagnostic_tags), + related_information: from_related(line_index, url, &d.related_info), + tags: lsp_diagnostic_tags(&d.tag), data: None, } } -fn lsp_diagnostic_tags(d: &DiagnosticTag) -> Vec { +fn lsp_diagnostic_tags(d: &DiagnosticTag) -> Option> { match d { - DiagnosticTag::Unused => vec![lsp_types::DiagnosticTag::UNNECESSARY], - DiagnosticTag::Deprecated => vec![lsp_types::DiagnosticTag::DEPRECATED], + DiagnosticTag::None => None, + DiagnosticTag::Unused => Some(vec![lsp_types::DiagnosticTag::UNNECESSARY]), + DiagnosticTag::Deprecated => Some(vec![lsp_types::DiagnosticTag::DEPRECATED]), } } @@ -126,11 +123,19 @@ pub fn eqwalizer_to_arc_diagnostic( d: &EqwalizerDiagnostic, line_index: &LineIndex, relative_path: &Path, + eqwalizer_enabled: bool, ) -> arc_types::Diagnostic { let pos = position(line_index, d.range.start()); let line_num = pos.line + 1; let character = Some(pos.character + 1); - let severity = arc_types::Severity::Error; + let severity = if eqwalizer_enabled { + arc_types::Severity::Error + } else { + // We use Severity::Disabled so that we have the ability in our arc linter to choose + // to display lints for *new* files with errors that are not opted in (T118466310). + // See comment at the top of eqwalizer_cli.rs for more information. + arc_types::Severity::Disabled + }; // formatting: https://fburl.com/max_wiki_link_to_phabricator_rich_text let explanation = match &d.explanation { Some(s) => format!("```\n{s}\n```"), @@ -158,30 +163,25 @@ pub fn eqwalizer_to_arc_diagnostic( name, message, d.expression.clone(), - None, ) } -fn from_related( - get_file_info: F, +fn from_related( + line_index: &LineIndex, + url: &Url, r: &Option>, -) -> Option> -where - F: Fn(elp_ide::elp_ide_db::elp_base_db::FileId) -> Option<(LineIndex, Url)>, -{ +) -> Option> { r.as_ref().map(|ri| { ri.iter() - .filter_map(|i| { - // Get the line index and URL for the file that contains the related information - let (line_index, uri) = get_file_info(i.file_id)?; + .map(|i| { let location = Location { - range: range(&line_index, i.range), - uri, + range: range(line_index, i.range), + uri: url.clone(), }; - Some(DiagnosticRelatedInformation { + DiagnosticRelatedInformation { location, message: i.message.clone(), - }) + } }) .collect() }) @@ -251,7 +251,6 @@ pub fn ide_to_arc_diagnostic( None => message, }; let severity = diagnostic.severity(use_cli_severity); - let doc_path = diagnostic.code.as_doc_path(); arc_types::Diagnostic::new( path, line_num, @@ -260,6 +259,5 @@ pub fn ide_to_arc_diagnostic( diagnostic.code.as_labeled_code(), description, None, - doc_path, ) } diff --git a/crates/elp/src/handlers.rs b/crates/elp/src/handlers.rs index d4f4cb4e96..8fa9ef7665 100644 --- a/crates/elp/src/handlers.rs +++ b/crates/elp/src/handlers.rs @@ -17,7 +17,6 @@ use std::time::SystemTime; use anyhow::Result; use anyhow::bail; use elp_ide::Cancellable; -use elp_ide::DocResult; use elp_ide::HighlightedRange; use elp_ide::NavigationTarget; use elp_ide::RangeInfo; @@ -33,8 +32,6 @@ use elp_ide::elp_ide_db::elp_base_db::FilePosition; use elp_ide::elp_ide_db::elp_base_db::FileRange; use elp_ide::elp_ide_db::elp_base_db::ProjectId; use elp_log::telemetry; -use elp_log::timeit_with_telemetry; -use elp_syntax::SmolStr; use itertools::Itertools; use lsp_server::ErrorCode; use lsp_types::CallHierarchyIncomingCall; @@ -67,7 +64,6 @@ use crate::convert::lsp_to_assist_context_diagnostic; use crate::from_proto; use crate::lsp_ext; use crate::snapshot::Snapshot; -use crate::snapshot::TelemetryData; use crate::to_proto; pub(crate) fn handle_code_action( @@ -343,22 +339,16 @@ fn goto_definition_telemetry(snap: &Snapshot, targets: &[NavigationTarget], star .iter() .map(|tgt| snap.file_id_to_url(tgt.file_id)) .collect(); - let target_names: Vec<_> = targets.iter().map(|tgt| tgt.name.clone()).collect(); - let target_kinds: Vec<_> = targets.iter().map(|tgt| tgt.kind).collect(); #[derive(serde::Serialize)] struct Data { targets_include_generated: bool, target_urls: Vec, - target_names: Vec, - target_kinds: Vec, } let detail = Data { targets_include_generated, target_urls, - target_names, - target_kinds, }; let duration = start.elapsed().map(|e| e.as_millis()).unwrap_or(0) as u32; let data = serde_json::to_value(detail).unwrap_or_else(|err| { @@ -367,24 +357,6 @@ fn goto_definition_telemetry(snap: &Snapshot, targets: &[NavigationTarget], star telemetry::send_with_duration("goto_definition".to_string(), data, duration, start); } -fn send_hover_telemetry(doc_result: &DocResult) { - #[derive(serde::Serialize)] - struct Data { - docs_found: bool, - text: String, - kind: String, - } - let detail = Data { - docs_found: doc_result.doc.is_some(), - text: doc_result.token_text.clone(), - kind: format!("{:?}", doc_result.token_kind), - }; - let data = serde_json::to_value(detail).unwrap_or_else(|err| { - serde_json::Value::String(format!("JSON serialization failed: {err}")) - }); - telemetry::send("hover".to_string(), data); -} - pub(crate) fn handle_goto_type_definition( snap: Snapshot, params: lsp_types::GotoDefinitionParams, @@ -414,16 +386,10 @@ pub(crate) fn handle_references( params: lsp_types::ReferenceParams, ) -> Result>> { let _p = tracing::info_span!("handle_references").entered(); - let _timer = timeit_with_telemetry!(TelemetryData::References { - file_url: params.text_document_position.text_document.uri.clone(), - position: params.text_document_position.position - }); - let mut position = from_proto::file_position(&snap, params.text_document_position)?; position.offset = snap .analysis .clamp_offset(position.file_id, position.offset)?; - let refs = match snap.analysis.find_all_refs(position)? { None => return Ok(None), Some(it) => it, @@ -447,7 +413,6 @@ pub(crate) fn handle_references( .chain(decl) }) .collect(); - Ok(Some(locations)) } @@ -484,10 +449,8 @@ pub(crate) fn handle_completion_resolve( position.offset = snap .analysis .clamp_offset(position.file_id, position.offset)?; - if let Ok(Some(doc_result)) = snap.analysis.get_docs_at_position(position) - && let Some(doc) = doc_result.doc - { - let docs = doc.markdown_text().to_string(); + if let Ok(Some(res)) = snap.analysis.get_docs_at_position(position) { + let docs = res.0.markdown_text().to_string(); let documentation = lsp_types::Documentation::MarkupContent(lsp_types::MarkupContent { kind: lsp_types::MarkupKind::Markdown, @@ -612,13 +575,8 @@ pub(crate) fn handle_hover(snap: Snapshot, params: HoverParams) -> Result