From 68677d4d15da4b7f6cf48ab3b8edc375ae90d000 Mon Sep 17 00:00:00 2001 From: facebook-github-bot Date: Wed, 2 Aug 2023 07:54:59 -0700 Subject: [PATCH] Initial commit fbshipit-source-id: c86303b25936d586b185a0b4f4e6dc8d307cf4aa --- .cargo/config.toml | 14 + .github/ISSUE_TEMPLATE/bug_report.md | 29 + .../pull_request_template.md | 5 + .github/workflows/build-website.yml | 27 + .github/workflows/ci.yml | 100 + .github/workflows/deploy-website.yml | 36 + .github/workflows/release.yml | 249 + .gitignore | 1 + .vscode/launch.json | 14 + CODE_OF_CONDUCT.md | 80 + CONTRIBUTING.md | 73 + Cargo.lock | 2380 ++++ Cargo.toml | 97 + LICENSE-APACHE | 201 + LICENSE-MIT | 21 + README.md | 92 + bench_runner/example_bench/benches/main.rs | 59 + bench_runner/runner/main.rs | 15 + crates/ai/Cargo.toml | 17 + crates/ai/src/lib.rs | 182 + crates/base_db/Cargo.toml | 27 + crates/base_db/src/change.rs | 89 + crates/base_db/src/fixture.rs | 1233 ++ crates/base_db/src/input.rs | 365 + crates/base_db/src/lib.rs | 226 + crates/base_db/src/module_index.rs | 197 + crates/base_db/src/test_fixture.rs | 345 + crates/base_db/src/test_utils.rs | 56 + crates/elp/Cargo.toml | 62 + crates/elp/build.rs | 43 + crates/elp/src/arc_types.rs | 65 + crates/elp/src/bin/args.rs | 517 + crates/elp/src/bin/build_info_cli.rs | 40 + crates/elp/src/bin/elp_parse_cli.rs | 351 + crates/elp/src/bin/eqwalizer_cli.rs | 457 + crates/elp/src/bin/erlang_service_cli.rs | 164 + crates/elp/src/bin/lint_cli.rs | 667 + crates/elp/src/bin/main.rs | 872 ++ crates/elp/src/bin/reporting.rs | 359 + crates/elp/src/bin/shell.rs | 329 + crates/elp/src/build/load.rs | 167 + crates/elp/src/build/mod.rs | 30 + crates/elp/src/build/types.rs | 103 + crates/elp/src/cli.rs | 148 + crates/elp/src/config.rs | 482 + crates/elp/src/convert.rs | 273 + crates/elp/src/diagnostics.rs | 165 + crates/elp/src/document.rs | 82 + crates/elp/src/from_proto.rs | 119 + crates/elp/src/handlers.rs | 801 ++ crates/elp/src/lib.rs | 111 + crates/elp/src/line_endings.rs | 36 + crates/elp/src/lsp_ext.rs | 128 + crates/elp/src/op_queue.rs | 58 + crates/elp/src/project_loader.rs | 89 + crates/elp/src/reload.rs | 88 + .../diagnostics/parse_all_diagnostics1.stdout | 8 + ...arse_all_diagnostics_errors_escript.stdout | 4 + .../parse_all_diagnostics_escript.stdout | 2 + .../parse_all_diagnostics_hrl.stdout | 6 + ...se_all_diagnostics_warnings_escript.stdout | 4 + .../test/diagnostics/parse_elp_lint1.stdout | 4 + .../diagnostics/parse_elp_lint_fix.stdout | 17 + .../parse_elp_lint_fix_json.stdout | 1 + .../parse_elp_lint_recursive.stdout | 45 + .../resources/test/eqwalize_all_help.stdout | 9 + crates/elp/src/resources/test/help.stdout | 22 + .../lint/head_mismatch/app_a/src/lints.erl | 6 + .../app_a/src/lint_recursive.erl | 15 + .../elp/src/resources/test/lint_help.stdout | 25 + .../test/linter/parse_elp_lint2.stdout | 4 + .../linter/parse_elp_lint_ignore_apps.stdout | 3 + .../parse_elp_lint_ignore_apps_b.stdout | 5 + .../linter/parse_elp_lint_json_output.stdout | 3 + .../src/resources/test/parse_all_help.stdout | 9 + .../src/resources/test/parse_elp_help.stdout | 15 + .../eqwalize_all_diagnostics.jsonl | 3 + ...qwalize_elpt2_reference_nonexistent.pretty | 3 + .../parse_error/eqwalize_parse_error_a.pretty | 11 + .../eqwalize_parse_error_a_bad.pretty | 6 + ...qwalize_parse_error_a_reference_bad.pretty | 6 + .../standard/eqwalize_all_diagnostics.jsonl | 19 + .../standard/eqwalize_all_diagnostics.pretty | 232 + .../eqwalize_all_diagnostics_gen.jsonl | 20 + .../standard/eqwalize_all_parse_error.jsonl | 1 + .../test/standard/eqwalize_app_a.pretty | 110 + .../test/standard/eqwalize_app_a_fast.pretty | 3 + .../standard/eqwalize_app_a_lists_fast.pretty | 79 + .../test/standard/eqwalize_app_a_mod2.pretty | 3 + .../standard/eqwalize_app_a_mod2_fast.pretty | 19 + .../test/standard/eqwalize_app_b.pretty | 0 .../test/standard/eqwalize_app_b_fast.pretty | 0 .../standard/eqwalize_app_diagnostics.pretty | 226 + .../eqwalize_app_diagnostics_gen.pretty | 236 + .../test/standard/eqwalize_meinong.pretty | 1 + .../eqwalize_target_diagnostics.pretty | 206 + crates/elp/src/semantic_tokens.rs | 345 + crates/elp/src/server.rs | 1165 ++ crates/elp/src/server/capabilities.rs | 144 + crates/elp/src/server/dispatch.rs | 254 + crates/elp/src/server/logger.rs | 116 + crates/elp/src/server/progress.rs | 165 + crates/elp/src/server/setup.rs | 180 + crates/elp/src/snapshot.rs | 274 + crates/elp/src/task_pool.rs | 77 + crates/elp/src/to_proto.rs | 837 ++ crates/elp/tests/slow-tests/buck_tests.rs | 303 + crates/elp/tests/slow-tests/main.rs | 378 + crates/elp/tests/slow-tests/support.rs | 414 + crates/elp_log/Cargo.toml | 18 + crates/elp_log/src/file.rs | 90 + crates/elp_log/src/lib.rs | 236 + crates/elp_log/src/telemetry.rs | 98 + crates/eqwalizer/Cargo.toml | 22 + crates/eqwalizer/build.rs | 94 + crates/eqwalizer/src/ast/auto_import.rs | 710 + crates/eqwalizer/src/ast/binary_specifier.rs | 24 + crates/eqwalizer/src/ast/compiler_macro.rs | 31 + crates/eqwalizer/src/ast/contractivity.rs | 381 + crates/eqwalizer/src/ast/convert.rs | 2215 +++ crates/eqwalizer/src/ast/convert_types.rs | 459 + crates/eqwalizer/src/ast/db.rs | 275 + crates/eqwalizer/src/ast/expand.rs | 793 + crates/eqwalizer/src/ast/expr.rs | 378 + crates/eqwalizer/src/ast/ext_types.rs | 320 + crates/eqwalizer/src/ast/form.rs | 278 + crates/eqwalizer/src/ast/guard.rs | 158 + .../eqwalizer/src/ast/invalid_diagnostics.rs | 90 + crates/eqwalizer/src/ast/mod.rs | 340 + crates/eqwalizer/src/ast/pat.rs | 149 + crates/eqwalizer/src/ast/stub.rs | 45 + crates/eqwalizer/src/ast/subst.rs | 112 + crates/eqwalizer/src/ast/trans_valid.rs | 415 + crates/eqwalizer/src/ast/types.rs | 381 + crates/eqwalizer/src/ast/variance_check.rs | 417 + crates/eqwalizer/src/ipc.rs | 156 + crates/eqwalizer/src/lib.rs | 603 + crates/erlang_service/Cargo.toml | 22 + crates/erlang_service/build.rs | 53 + .../erlang_service/fixtures/edoc_errors.erl | 11 + .../fixtures/edoc_errors.expected | 16 + .../erlang_service/fixtures/edoc_warnings.erl | 10 + .../fixtures/edoc_warnings.expected | 26 + crates/erlang_service/fixtures/error.erl | 26 + crates/erlang_service/fixtures/error.expected | 153 + crates/erlang_service/fixtures/error_attr.erl | 7 + .../fixtures/error_attr.expected | 56 + .../fixtures/misplaced_comment_error.erl | 16 + .../fixtures/misplaced_comment_error.expected | 81 + crates/erlang_service/fixtures/regular.erl | 1 + .../erlang_service/fixtures/regular.expected | 16 + .../fixtures/structured_comment.erl | 9 + .../fixtures/structured_comment.expected | 47 + .../erlang_service/fixtures/unused_record.erl | 3 + .../fixtures/unused_record.expected | 35 + .../fixtures/unused_record_in_header.expected | 20 + .../fixtures/unused_record_in_header.hrl | 1 + crates/erlang_service/src/lib.rs | 815 ++ crates/hir/Cargo.toml | 21 + crates/hir/src/body.rs | 576 + crates/hir/src/body/lower.rs | 2390 ++++ crates/hir/src/body/pretty.rs | 841 ++ crates/hir/src/body/scope.rs | 801 ++ crates/hir/src/body/tests.rs | 2052 +++ crates/hir/src/body/tree_print.rs | 2634 ++++ crates/hir/src/db.rs | 180 + crates/hir/src/def_map.rs | 760 + crates/hir/src/diagnostics.rs | 36 + crates/hir/src/edoc.rs | 417 + crates/hir/src/expr.rs | 553 + crates/hir/src/fold.rs | 939 ++ crates/hir/src/form_list.rs | 716 + crates/hir/src/form_list/form_id.rs | 113 + crates/hir/src/form_list/lower.rs | 669 + crates/hir/src/form_list/pretty.rs | 434 + crates/hir/src/form_list/tests.rs | 476 + crates/hir/src/include.rs | 185 + crates/hir/src/intern.rs | 53 + crates/hir/src/lib.rs | 188 + crates/hir/src/macro_exp.rs | 485 + crates/hir/src/module_data.rs | 350 + crates/hir/src/name.rs | 236 + crates/hir/src/resolver.rs | 56 + crates/hir/src/sema.rs | 1222 ++ crates/hir/src/sema/find.rs | 308 + crates/hir/src/sema/to_def.rs | 750 + crates/hir/src/test_db.rs | 62 + crates/ide/Cargo.toml | 32 + crates/ide/src/annotations.rs | 101 + crates/ide/src/call_hierarchy.rs | 395 + crates/ide/src/codemod_helpers.rs | 532 + crates/ide/src/common_test.rs | 656 + crates/ide/src/diagnostics.rs | 1349 ++ crates/ide/src/diagnostics/application_env.rs | 263 + .../src/diagnostics/effect_free_statement.rs | 491 + crates/ide/src/diagnostics/head_mismatch.rs | 404 + .../missing_compile_warn_missing_spec.rs | 386 + .../src/diagnostics/misspelled_attribute.rs | 225 + crates/ide/src/diagnostics/module_mismatch.rs | 101 + .../ide/src/diagnostics/mutable_variable.rs | 123 + .../src/diagnostics/redundant_assignment.rs | 195 + crates/ide/src/diagnostics/replace_call.rs | 598 + crates/ide/src/diagnostics/trivial_match.rs | 471 + .../src/diagnostics/unused_function_args.rs | 289 + crates/ide/src/diagnostics/unused_include.rs | 427 + crates/ide/src/diagnostics/unused_macro.rs | 193 + .../src/diagnostics/unused_record_field.rs | 137 + crates/ide/src/diff.rs | 237 + crates/ide/src/doc_links.rs | 151 + crates/ide/src/document_symbols.rs | 362 + crates/ide/src/expand_macro.rs | 586 + crates/ide/src/extend_selection.rs | 373 + crates/ide/src/fixture.rs | 67 + crates/ide/src/folding_ranges.rs | 129 + crates/ide/src/handlers/get_docs.rs | 31 + crates/ide/src/handlers/goto_definition.rs | 3458 +++++ crates/ide/src/handlers/mod.rs | 12 + crates/ide/src/handlers/references.rs | 644 + crates/ide/src/highlight_related.rs | 1040 ++ crates/ide/src/inlay_hints.rs | 250 + crates/ide/src/inlay_hints/param_name.rs | 233 + crates/ide/src/lib.rs | 585 + crates/ide/src/navigation_target.rs | 248 + crates/ide/src/rename.rs | 1025 ++ crates/ide/src/runnables.rs | 418 + crates/ide/src/signature_help.rs | 646 + crates/ide/src/syntax_highlighting.rs | 365 + .../ide/src/syntax_highlighting/highlights.rs | 115 + crates/ide/src/syntax_highlighting/tags.rs | 176 + crates/ide/src/tests.rs | 227 + crates/ide_assists/Cargo.toml | 21 + crates/ide_assists/src/assist_config.rs | 24 + crates/ide_assists/src/assist_context.rs | 347 + crates/ide_assists/src/handlers/add_edoc.rs | 213 + crates/ide_assists/src/handlers/add_format.rs | 135 + crates/ide_assists/src/handlers/add_impl.rs | 202 + crates/ide_assists/src/handlers/add_spec.rs | 191 + .../src/handlers/bump_variables.rs | 286 + .../src/handlers/create_function.rs | 191 + .../src/handlers/delete_function.rs | 279 + .../src/handlers/export_function.rs | 230 + .../src/handlers/extract_function.rs | 1305 ++ .../src/handlers/extract_variable.rs | 341 + crates/ide_assists/src/handlers/flip_sep.rs | 492 + .../src/handlers/ignore_variable.rs | 122 + .../src/handlers/implement_behaviour.rs | 559 + .../src/handlers/inline_function.rs | 2039 +++ .../src/handlers/inline_local_variable.rs | 612 + crates/ide_assists/src/helpers.rs | 547 + crates/ide_assists/src/lib.rs | 113 + crates/ide_assists/src/tests.rs | 701 + crates/ide_completion/Cargo.toml | 20 + crates/ide_completion/src/attributes.rs | 288 + crates/ide_completion/src/ctx.rs | 629 + crates/ide_completion/src/export_functions.rs | 97 + crates/ide_completion/src/export_types.rs | 97 + crates/ide_completion/src/functions.rs | 692 + crates/ide_completion/src/helpers.rs | 83 + crates/ide_completion/src/keywords.rs | 252 + crates/ide_completion/src/lib.rs | 206 + crates/ide_completion/src/macros.rs | 219 + crates/ide_completion/src/modules.rs | 165 + crates/ide_completion/src/records.rs | 403 + crates/ide_completion/src/tests.rs | 26 + crates/ide_completion/src/types.rs | 266 + crates/ide_completion/src/vars.rs | 171 + crates/ide_db/Cargo.toml | 30 + crates/ide_db/src/apply_change.rs | 23 + crates/ide_db/src/assists.rs | 213 + crates/ide_db/src/defs.rs | 423 + crates/ide_db/src/docs.rs | 434 + crates/ide_db/src/eqwalizer.rs | 368 + crates/ide_db/src/erl_ast.rs | 127 + crates/ide_db/src/fixmes.rs | 89 + crates/ide_db/src/helpers.rs | 37 + crates/ide_db/src/label.rs | 58 + crates/ide_db/src/lib.rs | 350 + crates/ide_db/src/line_index.rs | 203 + crates/ide_db/src/rename.rs | 381 + crates/ide_db/src/search.rs | 333 + crates/ide_db/src/source_change.rs | 221 + crates/project_model/Cargo.toml | 20 + .../fbsource/deeply/nested/rebar.config | 3 + .../fixtures/missing_build_info/rebar.config | 0 .../fixtures/rebar.config.script | 3 + crates/project_model/src/buck.rs | 860 ++ crates/project_model/src/lib.rs | 587 + crates/project_model/src/otp.rs | 81 + crates/project_model/src/rebar.rs | 259 + crates/syntax/Cargo.toml | 28 + crates/syntax/src/algo.rs | 754 + crates/syntax/src/ast.rs | 106 + crates/syntax/src/ast/edit.rs | 219 + crates/syntax/src/ast/erlang.rs | 250 + crates/syntax/src/ast/generated.rs | 12 + crates/syntax/src/ast/generated/nodes.rs | 6081 ++++++++ crates/syntax/src/ast/node_ext.rs | 807 ++ crates/syntax/src/ast/operators.rs | 191 + crates/syntax/src/ast/traits.rs | 48 + crates/syntax/src/lib.rs | 854 ++ crates/syntax/src/ptr.rs | 167 + crates/syntax/src/syntax_error.rs | 33 + crates/syntax/src/syntax_kind.rs | 22 + crates/syntax/src/syntax_kind/generated.rs | 328 + crates/syntax/src/syntax_node.rs | 36 + crates/syntax/src/ted.rs | 213 + crates/syntax/src/token_text.rs | 106 + crates/syntax/src/tree_sitter_elp.rs | 27 + crates/syntax/src/unescape.rs | 251 + docs/ARCHITECTURE.md | 1181 ++ docs/CODE_ACTIONS.md | 295 + docs/ELP-parser-dataflow.excalidraw | 1219 ++ docs/ELP-parser-dataflow.png | Bin 0 -> 263768 bytes docs/ELP-parser-dataflow.svg | 16 + docs/PROJECT_LOADING.md | 137 + docs/README.md | 20 + docs/crate_graph.dot | 63 + docs/crate_graph.png | Bin 0 -> 237633 bytes docs/databases.dot | 80 + docs/databases.png | Bin 0 -> 107227 bytes docs/images/code-action-add-edoc.png | Bin 0 -> 174990 bytes docs/images/code-action-remove-function.png | Bin 0 -> 110129 bytes docs/input_data_graph.dot | 20 + docs/input_data_graph.png | Bin 0 -> 55472 bytes editors/code/.eslintignore | 3 + editors/code/.eslintrc.js | 29 + editors/code/.gitignore | 4 + editors/code/.vscodeignore | 15 + editors/code/README.md | 37 + editors/code/client/package-lock.json | 945 ++ editors/code/client/package.json | 22 + editors/code/client/src/extension.ts | 60 + .../code/client/src/test/completion.test.ts | 49 + .../code/client/src/test/diagnostics.test.ts | 47 + editors/code/client/src/test/helper.ts | 53 + editors/code/client/src/test/index.ts | 49 + editors/code/client/src/test/runTest.ts | 33 + .../code/client/testFixture/completion.txt | 0 .../code/client/testFixture/diagnostics.txt | 1 + editors/code/client/tsconfig.json | 12 + editors/code/language-configuration.json | 65 + editors/code/package-lock.json | 3714 +++++ editors/code/package.json | 103 + editors/code/scripts/e2e.sh | 15 + editors/code/server/src/server.ts | 236 + editors/code/server/tsconfig.json | 14 + editors/code/third-party/README.md | 6 + editors/code/third-party/grammar/Erlang.plist | 2754 ++++ editors/code/third-party/grammar/LICENSE | 201 + editors/code/third-party/grammar/README.md | 70 + editors/code/tsconfig.json | 20 + elisp/dotemacs.el | 137 + erlang_service/.gitignore | 20 + erlang_service/README.md | 15 + erlang_service/erlang_ls.config | 0 erlang_service/rebar.config | 14 + erlang_service/rebar.lock | 1 + erlang_service/src/edoc_report.erl | 125 + erlang_service/src/elp_epp.erl | 2105 +++ erlang_service/src/elp_escript.erl | 271 + erlang_service/src/elp_lint.erl | 4404 ++++++ erlang_service/src/elp_metadata.erl | 19 + erlang_service/src/elp_parse.yrl | 1905 +++ erlang_service/src/elp_scan.erl | 897 ++ erlang_service/src/erlang_service.app.src | 14 + erlang_service/src/erlang_service.erl | 1027 ++ erlang_service/src/render_eep48_docs.erl | 858 ++ .../vendored/docsh_0_7_2/docsh_docs_v1.erl | 155 + .../vendored/docsh_0_7_2/docsh_edoc_xmerl.erl | 289 + .../src/vendored/docsh_0_7_2/docsh_format.erl | 62 + .../vendored/docsh_0_7_2/docsh_internal.erl | 77 + .../src/vendored/docsh_0_7_2/docsh_writer.erl | 3 + rustfmt.toml | 3 + test_projects/.gitignore | 5 + test_projects/README.md | 15 + test_projects/buck_tests/.elp.toml | 9 + test_projects/buck_tests/TARGETS.v2_ | 41 + test_projects/buck_tests/test_elp/TARGETS.v2_ | 77 + .../buck_tests/test_elp/include/test_elp.hrl | 1 + .../buck_tests/test_elp/src/test_elp.app.src | 9 + .../buck_tests/test_elp/src/test_elp.erl | 33 + .../test_elp/test/test_elp_SUITE.erl | 30 + .../handle_update_test1.json | 1 + .../handle_update_test2.json | 1 + .../test_elp_SUITE_data/untracked_header.hrl | 1 + .../test_elp_SUITE_data/untracked_module.erl | 1 + .../test_elp/test/test_elp_test_utils.erl | 12 + .../test_elp/test/test_elp_test_utils.hrl | 1 + .../test_elp_direct_dep/TARGETS.v2_ | 16 + .../include/test_elp_direct_dep.hrl | 2 + .../src/test_elp_direct_dep.erl | 23 + .../src/test_elp_direct_dep_private.hrl | 1 + .../test_elp_flat_inside_target/TARGETS.v2_ | 11 + .../test_elp_flat_inside_target.erl | 1 + .../test_elp_flat_inside_target.hrl | 1 + .../test_elp_flat_outside_target.erl | 1 + .../test_elp_flat_outside_target.hrl | 1 + .../test_elp_ignored/test_elp_ignored.erl | 1 + .../include/test_elp_no_private_headers.hrl | 1 + .../src/test_elp_no_private_headers.erl | 1 + .../src/test_elp_no_headers.erl | 1 + .../test_elp_transitive_dep/TARGETS.v2_ | 11 + .../include/test_elp_transitive_dep.hrl | 2 + .../src/test_elp_transitive_dep.erl | 22 + .../src/test_elp_transitive_dep_private.hrl | 1 + test_projects/diagnostics/.elp.toml | 8 + test_projects/diagnostics/README.md | 2 + .../diagnostics/app_a/extra/app_a.erl | 5 + .../diagnostics/app_a/include/app_a.hrl | 1 + .../app_a/include/broken_diagnostics.hrl | 4 + .../diagnostics/app_a/include/diagnostics.hrl | 2 + .../diagnostics/app_a/src/app_a.app.src | 3 + test_projects/diagnostics/app_a/src/app_a.erl | 82 + .../diagnostics/app_a/src/diagnostics.erl | 12 + .../diagnostics/app_a/src/diagnostics.escript | 23 + .../app_a/src/diagnostics_errors.escript | 26 + .../app_a/src/diagnostics_warnings.escript | 25 + .../diagnostics/app_a/src/lint_recursive.erl | 16 + test_projects/diagnostics/app_a/src/lints.erl | 6 + .../diagnostics/app_a/test/app_a_SUITE.erl | 19 + test_projects/diagnostics/erlang_ls.config | 25 + test_projects/diagnostics/rebar.config | 8 + .../wa_utils/src/wa_build_info_prv.erl | 384 + .../diagnostics/wa_utils/src/wa_utils.app.src | 3 + .../diagnostics/wa_utils/src/wa_utils.erl | 15 + test_projects/end_to_end/.elp.toml | 7 + .../src/assist_examples.app.src | 3 + .../assist_examples/src/code_completion.erl | 10 + .../assist_examples/src/head_mismatch.erl | 6 + .../end_to_end/definitions/README.md | 8 + .../definitions/src/definitions.app.src | 3 + .../end_to_end/definitions/src/local_def.erl | 7 + test_projects/end_to_end/erlang_ls.config | 26 + test_projects/end_to_end/hover/README.md | 8 + .../end_to_end/hover/src/doc_examples.erl | 23 + .../end_to_end/hover/src/hover.app.src | 3 + test_projects/end_to_end/rebar.config | 10 + .../end_to_end/single_errors/README.md | 2 + .../single_errors/src/single_errors.app.src | 3 + .../single_errors/src/spec_mismatch.erl | 8 + .../single_errors/src/spec_mismatch.erl.2 | 16 + .../wa_utils/src/wa_build_info_prv.erl | 384 + .../end_to_end/wa_utils/src/wa_utils.app.src | 3 + .../end_to_end/wa_utils/src/wa_utils.erl | 15 + test_projects/eqwalizer/src/eqwalizer.app.src | 3 + .../eqwalizer/src/eqwalizer_specs.erl | 288 + test_projects/in_place_tests/.elp.toml | 5 + test_projects/in_place_tests/README.md | 2 + .../in_place_tests/app_a/extra/app_a.erl | 5 + .../in_place_tests/app_a/include/app_a.hrl | 1 + .../app_a/include/broken_diagnostics.hrl | 4 + .../app_a/include/diagnostics.hrl | 2 + .../in_place_tests/app_a/src/app_a.app.src | 3 + .../in_place_tests/app_a/src/app_a.erl | 7 + .../in_place_tests/app_a/src/lints.erl | 6 + .../in_place_tests/app_a/test/app_a_SUITE.erl | 19 + test_projects/in_place_tests/erlang_ls.config | 25 + test_projects/in_place_tests/rebar.config | 8 + .../wa_utils/src/wa_build_info_prv.erl | 384 + .../wa_utils/src/wa_utils.app.src | 3 + .../in_place_tests/wa_utils/src/wa_utils.erl | 15 + test_projects/linter/.elp.toml | 5 + test_projects/linter/.gitignore | 4 + test_projects/linter/app_a/include/app_a.hrl | 1 + test_projects/linter/app_a/src/app_a.app.src | 3 + test_projects/linter/app_a/src/app_a.erl | 10 + .../linter/app_a/src/app_a_unused_param.erl | 6 + .../linter/app_a/test/app_a_SUITE.erl | 19 + .../linter/app_a/test/app_a_test_helpers.erl | 10 + .../test/app_a_test_helpers_not_opted_in.erl | 9 + .../app_a/test/app_test_helpers_no_errors.erl | 6 + test_projects/linter/app_b/src/app_b.app.src | 3 + test_projects/linter/app_b/src/app_b.erl | 5 + .../linter/app_b/src/app_b_unused_param.erl | 6 + test_projects/linter/rebar.config | 9 + .../linter/wa_utils/src/wa_build_info_prv.erl | 384 + .../linter/wa_utils/src/wa_utils.app.src | 3 + .../linter/wa_utils/src/wa_utils.erl | 15 + test_projects/parse_error/.elp.toml | 8 + test_projects/parse_error/.gitignore | 4 + test_projects/parse_error/.rebar.root | 1 + .../eqwalizer/src/eqwalizer.app.src | 3 + .../parse_error/eqwalizer/src/eqwalizer.erl | 30 + .../eqwalizer/src/eqwalizer_specs.erl | 264 + .../parse_error_a/src/parse_error_a.app.src | 3 + .../parse_error_a/src/parse_error_a.erl | 15 + .../parse_error_a/src/parse_error_a_bad.erl | 6 + .../src/parse_error_a_reference_bad.erl | 6 + .../src/parse_error_a_syntax_error.erl | 3 + .../parse_error_a/src/parse_error_a_worst.erl | 7 + test_projects/parse_error/rebar.config | 9 + .../wa_utils/src/wa_build_info_prv.erl | 384 + .../parse_error/wa_utils/src/wa_utils.app.src | 3 + .../parse_error/wa_utils/src/wa_utils.erl | 15 + test_projects/standard/.elp.toml | 5 + test_projects/standard/.gitignore | 4 + test_projects/standard/.rebar.root | 1 + test_projects/standard/app_a/.eqwalizer | 0 test_projects/standard/app_a/extra/app_a.erl | 5 + .../standard/app_a/include/app_a.hrl | 1 + .../standard/app_a/src/app_a.app.src | 3 + test_projects/standard/app_a/src/app_a.erl | 124 + .../app_a/src/app_a_errors_generated.erl | 8 + .../standard/app_a/src/app_a_fixme.erl | 6 + .../standard/app_a/src/app_a_ignored.erl | 6 + .../standard/app_a/src/app_a_lists.erl | 2829 ++++ .../standard/app_a/src/app_a_mod2.erl | 32 + .../standard/app_a/src/app_a_no_errors.erl | 6 + .../app_a/src/app_a_no_errors_generated.erl | 8 + .../app_a/src/app_a_no_errors_opted_in.erl | 6 + .../standard/app_a/test/app_a_SUITE.erl | 19 + .../app_a/test/app_a_test_helpers.erl | 10 + .../test/app_a_test_helpers_not_opted_in.erl | 9 + .../app_a/test/app_test_helpers_no_errors.erl | 6 + .../standard/app_b/src/app_b.app.src | 3 + test_projects/standard/app_b/src/app_b.erl | 36 + .../standard/eqwalizer/src/eqwalizer.app.src | 3 + .../standard/eqwalizer/src/eqwalizer.erl | 30 + .../eqwalizer/src/eqwalizer_specs.erl | 264 + test_projects/standard/erlang_ls.config | 28 + test_projects/standard/rebar.config | 10 + .../wa_utils/src/wa_build_info_prv.erl | 384 + .../standard/wa_utils/src/wa_utils.app.src | 3 + .../standard/wa_utils/src/wa_utils.erl | 15 + website/.gitignore | 20 + website/.npmrc | 2 + website/README.md | 25 + website/babel.config.js | 13 + website/docs/architecture.md | 5 + website/docs/erlang-error-index.md | 5 + website/docs/feature-gallery.md | 5 + website/docs/get-started/_category_.json | 5 + website/docs/get-started/emacs.md | 5 + website/docs/get-started/get-started.md | 5 + website/docs/get-started/vscode.md | 5 + website/docusaurus.config.js | 142 + website/package.json | 48 + website/sidebars.js | 36 + website/src/components/HomepageFeatures.js | 78 + .../components/HomepageFeatures.module.css | 21 + website/src/css/custom.css | 42 + website/src/pages/index.js | 57 + website/src/pages/index.module.css | 33 + website/static/.nojekyll | 0 website/static/img/elp_icon_color.svg | 1 + website/static/img/elp_logo_black_white.svg | 1 + website/static/img/elp_logo_color.svg | 1 + website/static/img/elp_logo_white_text.svg | 1 + website/yarn.lock | 11900 ++++++++++++++++ xtask/Cargo.toml | 15 + xtask/src/codegen.rs | 892 ++ xtask/src/main.rs | 233 + 552 files changed, 140276 insertions(+) create mode 100644 .cargo/config.toml create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/PULL_REQUEST_TEMPLATE/pull_request_template.md create mode 100644 .github/workflows/build-website.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/deploy-website.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT create mode 100644 README.md create mode 100644 bench_runner/example_bench/benches/main.rs create mode 100644 bench_runner/runner/main.rs create mode 100644 crates/ai/Cargo.toml create mode 100644 crates/ai/src/lib.rs create mode 100644 crates/base_db/Cargo.toml create mode 100644 crates/base_db/src/change.rs create mode 100644 crates/base_db/src/fixture.rs create mode 100644 crates/base_db/src/input.rs create mode 100644 crates/base_db/src/lib.rs create mode 100644 crates/base_db/src/module_index.rs create mode 100644 crates/base_db/src/test_fixture.rs create mode 100644 crates/base_db/src/test_utils.rs create mode 100644 crates/elp/Cargo.toml create mode 100644 crates/elp/build.rs create mode 100644 crates/elp/src/arc_types.rs create mode 100644 crates/elp/src/bin/args.rs create mode 100644 crates/elp/src/bin/build_info_cli.rs create mode 100644 crates/elp/src/bin/elp_parse_cli.rs create mode 100644 crates/elp/src/bin/eqwalizer_cli.rs create mode 100644 crates/elp/src/bin/erlang_service_cli.rs create mode 100644 crates/elp/src/bin/lint_cli.rs create mode 100644 crates/elp/src/bin/main.rs create mode 100644 crates/elp/src/bin/reporting.rs create mode 100644 crates/elp/src/bin/shell.rs create mode 100644 crates/elp/src/build/load.rs create mode 100644 crates/elp/src/build/mod.rs create mode 100644 crates/elp/src/build/types.rs create mode 100644 crates/elp/src/cli.rs create mode 100644 crates/elp/src/config.rs create mode 100644 crates/elp/src/convert.rs create mode 100644 crates/elp/src/diagnostics.rs create mode 100644 crates/elp/src/document.rs create mode 100644 crates/elp/src/from_proto.rs create mode 100644 crates/elp/src/handlers.rs create mode 100644 crates/elp/src/lib.rs create mode 100644 crates/elp/src/line_endings.rs create mode 100644 crates/elp/src/lsp_ext.rs create mode 100644 crates/elp/src/op_queue.rs create mode 100644 crates/elp/src/project_loader.rs create mode 100644 crates/elp/src/reload.rs create mode 100644 crates/elp/src/resources/test/diagnostics/parse_all_diagnostics1.stdout create mode 100644 crates/elp/src/resources/test/diagnostics/parse_all_diagnostics_errors_escript.stdout create mode 100644 crates/elp/src/resources/test/diagnostics/parse_all_diagnostics_escript.stdout create mode 100644 crates/elp/src/resources/test/diagnostics/parse_all_diagnostics_hrl.stdout create mode 100644 crates/elp/src/resources/test/diagnostics/parse_all_diagnostics_warnings_escript.stdout create mode 100644 crates/elp/src/resources/test/diagnostics/parse_elp_lint1.stdout create mode 100644 crates/elp/src/resources/test/diagnostics/parse_elp_lint_fix.stdout create mode 100644 crates/elp/src/resources/test/diagnostics/parse_elp_lint_fix_json.stdout create mode 100644 crates/elp/src/resources/test/diagnostics/parse_elp_lint_recursive.stdout create mode 100644 crates/elp/src/resources/test/eqwalize_all_help.stdout create mode 100644 crates/elp/src/resources/test/help.stdout create mode 100644 crates/elp/src/resources/test/lint/head_mismatch/app_a/src/lints.erl create mode 100644 crates/elp/src/resources/test/lint/lint_recursive/app_a/src/lint_recursive.erl create mode 100644 crates/elp/src/resources/test/lint_help.stdout create mode 100644 crates/elp/src/resources/test/linter/parse_elp_lint2.stdout create mode 100644 crates/elp/src/resources/test/linter/parse_elp_lint_ignore_apps.stdout create mode 100644 crates/elp/src/resources/test/linter/parse_elp_lint_ignore_apps_b.stdout create mode 100644 crates/elp/src/resources/test/linter/parse_elp_lint_json_output.stdout create mode 100644 crates/elp/src/resources/test/parse_all_help.stdout create mode 100644 crates/elp/src/resources/test/parse_elp_help.stdout create mode 100644 crates/elp/src/resources/test/parse_error/eqwalize_all_diagnostics.jsonl create mode 100644 crates/elp/src/resources/test/parse_error/eqwalize_elpt2_reference_nonexistent.pretty create mode 100644 crates/elp/src/resources/test/parse_error/eqwalize_parse_error_a.pretty create mode 100644 crates/elp/src/resources/test/parse_error/eqwalize_parse_error_a_bad.pretty create mode 100644 crates/elp/src/resources/test/parse_error/eqwalize_parse_error_a_reference_bad.pretty create mode 100644 crates/elp/src/resources/test/standard/eqwalize_all_diagnostics.jsonl create mode 100644 crates/elp/src/resources/test/standard/eqwalize_all_diagnostics.pretty create mode 100644 crates/elp/src/resources/test/standard/eqwalize_all_diagnostics_gen.jsonl create mode 100644 crates/elp/src/resources/test/standard/eqwalize_all_parse_error.jsonl create mode 100644 crates/elp/src/resources/test/standard/eqwalize_app_a.pretty create mode 100644 crates/elp/src/resources/test/standard/eqwalize_app_a_fast.pretty create mode 100644 crates/elp/src/resources/test/standard/eqwalize_app_a_lists_fast.pretty create mode 100644 crates/elp/src/resources/test/standard/eqwalize_app_a_mod2.pretty create mode 100644 crates/elp/src/resources/test/standard/eqwalize_app_a_mod2_fast.pretty create mode 100644 crates/elp/src/resources/test/standard/eqwalize_app_b.pretty create mode 100644 crates/elp/src/resources/test/standard/eqwalize_app_b_fast.pretty create mode 100644 crates/elp/src/resources/test/standard/eqwalize_app_diagnostics.pretty create mode 100644 crates/elp/src/resources/test/standard/eqwalize_app_diagnostics_gen.pretty create mode 100644 crates/elp/src/resources/test/standard/eqwalize_meinong.pretty create mode 100644 crates/elp/src/resources/test/standard/eqwalize_target_diagnostics.pretty create mode 100644 crates/elp/src/semantic_tokens.rs create mode 100644 crates/elp/src/server.rs create mode 100644 crates/elp/src/server/capabilities.rs create mode 100644 crates/elp/src/server/dispatch.rs create mode 100644 crates/elp/src/server/logger.rs create mode 100644 crates/elp/src/server/progress.rs create mode 100644 crates/elp/src/server/setup.rs create mode 100644 crates/elp/src/snapshot.rs create mode 100644 crates/elp/src/task_pool.rs create mode 100644 crates/elp/src/to_proto.rs create mode 100644 crates/elp/tests/slow-tests/buck_tests.rs create mode 100644 crates/elp/tests/slow-tests/main.rs create mode 100644 crates/elp/tests/slow-tests/support.rs create mode 100644 crates/elp_log/Cargo.toml create mode 100644 crates/elp_log/src/file.rs create mode 100644 crates/elp_log/src/lib.rs create mode 100644 crates/elp_log/src/telemetry.rs create mode 100644 crates/eqwalizer/Cargo.toml create mode 100644 crates/eqwalizer/build.rs create mode 100644 crates/eqwalizer/src/ast/auto_import.rs create mode 100644 crates/eqwalizer/src/ast/binary_specifier.rs create mode 100644 crates/eqwalizer/src/ast/compiler_macro.rs create mode 100644 crates/eqwalizer/src/ast/contractivity.rs create mode 100644 crates/eqwalizer/src/ast/convert.rs create mode 100644 crates/eqwalizer/src/ast/convert_types.rs create mode 100644 crates/eqwalizer/src/ast/db.rs create mode 100644 crates/eqwalizer/src/ast/expand.rs create mode 100644 crates/eqwalizer/src/ast/expr.rs create mode 100644 crates/eqwalizer/src/ast/ext_types.rs create mode 100644 crates/eqwalizer/src/ast/form.rs create mode 100644 crates/eqwalizer/src/ast/guard.rs create mode 100644 crates/eqwalizer/src/ast/invalid_diagnostics.rs create mode 100644 crates/eqwalizer/src/ast/mod.rs create mode 100644 crates/eqwalizer/src/ast/pat.rs create mode 100644 crates/eqwalizer/src/ast/stub.rs create mode 100644 crates/eqwalizer/src/ast/subst.rs create mode 100644 crates/eqwalizer/src/ast/trans_valid.rs create mode 100644 crates/eqwalizer/src/ast/types.rs create mode 100644 crates/eqwalizer/src/ast/variance_check.rs create mode 100644 crates/eqwalizer/src/ipc.rs create mode 100644 crates/eqwalizer/src/lib.rs create mode 100644 crates/erlang_service/Cargo.toml create mode 100644 crates/erlang_service/build.rs create mode 100644 crates/erlang_service/fixtures/edoc_errors.erl create mode 100644 crates/erlang_service/fixtures/edoc_errors.expected create mode 100644 crates/erlang_service/fixtures/edoc_warnings.erl create mode 100644 crates/erlang_service/fixtures/edoc_warnings.expected create mode 100644 crates/erlang_service/fixtures/error.erl create mode 100644 crates/erlang_service/fixtures/error.expected create mode 100644 crates/erlang_service/fixtures/error_attr.erl create mode 100644 crates/erlang_service/fixtures/error_attr.expected create mode 100644 crates/erlang_service/fixtures/misplaced_comment_error.erl create mode 100644 crates/erlang_service/fixtures/misplaced_comment_error.expected create mode 100644 crates/erlang_service/fixtures/regular.erl create mode 100644 crates/erlang_service/fixtures/regular.expected create mode 100644 crates/erlang_service/fixtures/structured_comment.erl create mode 100644 crates/erlang_service/fixtures/structured_comment.expected create mode 100644 crates/erlang_service/fixtures/unused_record.erl create mode 100644 crates/erlang_service/fixtures/unused_record.expected create mode 100644 crates/erlang_service/fixtures/unused_record_in_header.expected create mode 100644 crates/erlang_service/fixtures/unused_record_in_header.hrl create mode 100644 crates/erlang_service/src/lib.rs create mode 100644 crates/hir/Cargo.toml create mode 100644 crates/hir/src/body.rs create mode 100644 crates/hir/src/body/lower.rs create mode 100644 crates/hir/src/body/pretty.rs create mode 100644 crates/hir/src/body/scope.rs create mode 100644 crates/hir/src/body/tests.rs create mode 100644 crates/hir/src/body/tree_print.rs create mode 100644 crates/hir/src/db.rs create mode 100644 crates/hir/src/def_map.rs create mode 100644 crates/hir/src/diagnostics.rs create mode 100644 crates/hir/src/edoc.rs create mode 100644 crates/hir/src/expr.rs create mode 100644 crates/hir/src/fold.rs create mode 100644 crates/hir/src/form_list.rs create mode 100644 crates/hir/src/form_list/form_id.rs create mode 100644 crates/hir/src/form_list/lower.rs create mode 100644 crates/hir/src/form_list/pretty.rs create mode 100644 crates/hir/src/form_list/tests.rs create mode 100644 crates/hir/src/include.rs create mode 100644 crates/hir/src/intern.rs create mode 100644 crates/hir/src/lib.rs create mode 100644 crates/hir/src/macro_exp.rs create mode 100644 crates/hir/src/module_data.rs create mode 100644 crates/hir/src/name.rs create mode 100644 crates/hir/src/resolver.rs create mode 100644 crates/hir/src/sema.rs create mode 100644 crates/hir/src/sema/find.rs create mode 100644 crates/hir/src/sema/to_def.rs create mode 100644 crates/hir/src/test_db.rs create mode 100644 crates/ide/Cargo.toml create mode 100644 crates/ide/src/annotations.rs create mode 100644 crates/ide/src/call_hierarchy.rs create mode 100644 crates/ide/src/codemod_helpers.rs create mode 100644 crates/ide/src/common_test.rs create mode 100644 crates/ide/src/diagnostics.rs create mode 100644 crates/ide/src/diagnostics/application_env.rs create mode 100644 crates/ide/src/diagnostics/effect_free_statement.rs create mode 100644 crates/ide/src/diagnostics/head_mismatch.rs create mode 100644 crates/ide/src/diagnostics/missing_compile_warn_missing_spec.rs create mode 100644 crates/ide/src/diagnostics/misspelled_attribute.rs create mode 100644 crates/ide/src/diagnostics/module_mismatch.rs create mode 100644 crates/ide/src/diagnostics/mutable_variable.rs create mode 100644 crates/ide/src/diagnostics/redundant_assignment.rs create mode 100644 crates/ide/src/diagnostics/replace_call.rs create mode 100644 crates/ide/src/diagnostics/trivial_match.rs create mode 100644 crates/ide/src/diagnostics/unused_function_args.rs create mode 100644 crates/ide/src/diagnostics/unused_include.rs create mode 100644 crates/ide/src/diagnostics/unused_macro.rs create mode 100644 crates/ide/src/diagnostics/unused_record_field.rs create mode 100644 crates/ide/src/diff.rs create mode 100644 crates/ide/src/doc_links.rs create mode 100644 crates/ide/src/document_symbols.rs create mode 100644 crates/ide/src/expand_macro.rs create mode 100644 crates/ide/src/extend_selection.rs create mode 100644 crates/ide/src/fixture.rs create mode 100644 crates/ide/src/folding_ranges.rs create mode 100644 crates/ide/src/handlers/get_docs.rs create mode 100644 crates/ide/src/handlers/goto_definition.rs create mode 100644 crates/ide/src/handlers/mod.rs create mode 100644 crates/ide/src/handlers/references.rs create mode 100644 crates/ide/src/highlight_related.rs create mode 100644 crates/ide/src/inlay_hints.rs create mode 100644 crates/ide/src/inlay_hints/param_name.rs create mode 100644 crates/ide/src/lib.rs create mode 100644 crates/ide/src/navigation_target.rs create mode 100644 crates/ide/src/rename.rs create mode 100644 crates/ide/src/runnables.rs create mode 100644 crates/ide/src/signature_help.rs create mode 100644 crates/ide/src/syntax_highlighting.rs create mode 100644 crates/ide/src/syntax_highlighting/highlights.rs create mode 100644 crates/ide/src/syntax_highlighting/tags.rs create mode 100644 crates/ide/src/tests.rs create mode 100644 crates/ide_assists/Cargo.toml create mode 100644 crates/ide_assists/src/assist_config.rs create mode 100644 crates/ide_assists/src/assist_context.rs create mode 100644 crates/ide_assists/src/handlers/add_edoc.rs create mode 100644 crates/ide_assists/src/handlers/add_format.rs create mode 100644 crates/ide_assists/src/handlers/add_impl.rs create mode 100644 crates/ide_assists/src/handlers/add_spec.rs create mode 100644 crates/ide_assists/src/handlers/bump_variables.rs create mode 100644 crates/ide_assists/src/handlers/create_function.rs create mode 100644 crates/ide_assists/src/handlers/delete_function.rs create mode 100644 crates/ide_assists/src/handlers/export_function.rs create mode 100644 crates/ide_assists/src/handlers/extract_function.rs create mode 100644 crates/ide_assists/src/handlers/extract_variable.rs create mode 100644 crates/ide_assists/src/handlers/flip_sep.rs create mode 100644 crates/ide_assists/src/handlers/ignore_variable.rs create mode 100644 crates/ide_assists/src/handlers/implement_behaviour.rs create mode 100644 crates/ide_assists/src/handlers/inline_function.rs create mode 100644 crates/ide_assists/src/handlers/inline_local_variable.rs create mode 100644 crates/ide_assists/src/helpers.rs create mode 100644 crates/ide_assists/src/lib.rs create mode 100644 crates/ide_assists/src/tests.rs create mode 100644 crates/ide_completion/Cargo.toml create mode 100644 crates/ide_completion/src/attributes.rs create mode 100644 crates/ide_completion/src/ctx.rs create mode 100644 crates/ide_completion/src/export_functions.rs create mode 100644 crates/ide_completion/src/export_types.rs create mode 100644 crates/ide_completion/src/functions.rs create mode 100644 crates/ide_completion/src/helpers.rs create mode 100644 crates/ide_completion/src/keywords.rs create mode 100644 crates/ide_completion/src/lib.rs create mode 100644 crates/ide_completion/src/macros.rs create mode 100644 crates/ide_completion/src/modules.rs create mode 100644 crates/ide_completion/src/records.rs create mode 100644 crates/ide_completion/src/tests.rs create mode 100644 crates/ide_completion/src/types.rs create mode 100644 crates/ide_completion/src/vars.rs create mode 100644 crates/ide_db/Cargo.toml create mode 100644 crates/ide_db/src/apply_change.rs create mode 100644 crates/ide_db/src/assists.rs create mode 100644 crates/ide_db/src/defs.rs create mode 100644 crates/ide_db/src/docs.rs create mode 100644 crates/ide_db/src/eqwalizer.rs create mode 100644 crates/ide_db/src/erl_ast.rs create mode 100644 crates/ide_db/src/fixmes.rs create mode 100644 crates/ide_db/src/helpers.rs create mode 100644 crates/ide_db/src/label.rs create mode 100644 crates/ide_db/src/lib.rs create mode 100644 crates/ide_db/src/line_index.rs create mode 100644 crates/ide_db/src/rename.rs create mode 100644 crates/ide_db/src/search.rs create mode 100644 crates/ide_db/src/source_change.rs create mode 100644 crates/project_model/Cargo.toml create mode 100644 crates/project_model/fixtures/fbsource/deeply/nested/rebar.config create mode 100644 crates/project_model/fixtures/missing_build_info/rebar.config create mode 100644 crates/project_model/fixtures/rebar.config.script create mode 100644 crates/project_model/src/buck.rs create mode 100644 crates/project_model/src/lib.rs create mode 100644 crates/project_model/src/otp.rs create mode 100644 crates/project_model/src/rebar.rs create mode 100644 crates/syntax/Cargo.toml create mode 100644 crates/syntax/src/algo.rs create mode 100644 crates/syntax/src/ast.rs create mode 100644 crates/syntax/src/ast/edit.rs create mode 100644 crates/syntax/src/ast/erlang.rs create mode 100644 crates/syntax/src/ast/generated.rs create mode 100644 crates/syntax/src/ast/generated/nodes.rs create mode 100644 crates/syntax/src/ast/node_ext.rs create mode 100644 crates/syntax/src/ast/operators.rs create mode 100644 crates/syntax/src/ast/traits.rs create mode 100644 crates/syntax/src/lib.rs create mode 100644 crates/syntax/src/ptr.rs create mode 100644 crates/syntax/src/syntax_error.rs create mode 100644 crates/syntax/src/syntax_kind.rs create mode 100644 crates/syntax/src/syntax_kind/generated.rs create mode 100644 crates/syntax/src/syntax_node.rs create mode 100644 crates/syntax/src/ted.rs create mode 100644 crates/syntax/src/token_text.rs create mode 100644 crates/syntax/src/tree_sitter_elp.rs create mode 100644 crates/syntax/src/unescape.rs create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/CODE_ACTIONS.md create mode 100644 docs/ELP-parser-dataflow.excalidraw create mode 100644 docs/ELP-parser-dataflow.png create mode 100644 docs/ELP-parser-dataflow.svg create mode 100644 docs/PROJECT_LOADING.md create mode 100644 docs/README.md create mode 100644 docs/crate_graph.dot create mode 100644 docs/crate_graph.png create mode 100644 docs/databases.dot create mode 100644 docs/databases.png create mode 100644 docs/images/code-action-add-edoc.png create mode 100644 docs/images/code-action-remove-function.png create mode 100644 docs/input_data_graph.dot create mode 100644 docs/input_data_graph.png create mode 100644 editors/code/.eslintignore create mode 100644 editors/code/.eslintrc.js create mode 100644 editors/code/.gitignore create mode 100644 editors/code/.vscodeignore create mode 100644 editors/code/README.md create mode 100644 editors/code/client/package-lock.json create mode 100644 editors/code/client/package.json create mode 100644 editors/code/client/src/extension.ts create mode 100644 editors/code/client/src/test/completion.test.ts create mode 100644 editors/code/client/src/test/diagnostics.test.ts create mode 100644 editors/code/client/src/test/helper.ts create mode 100644 editors/code/client/src/test/index.ts create mode 100644 editors/code/client/src/test/runTest.ts create mode 100644 editors/code/client/testFixture/completion.txt create mode 100644 editors/code/client/testFixture/diagnostics.txt create mode 100644 editors/code/client/tsconfig.json create mode 100644 editors/code/language-configuration.json create mode 100644 editors/code/package-lock.json create mode 100644 editors/code/package.json create mode 100755 editors/code/scripts/e2e.sh create mode 100644 editors/code/server/src/server.ts create mode 100644 editors/code/server/tsconfig.json create mode 100644 editors/code/third-party/README.md create mode 100644 editors/code/third-party/grammar/Erlang.plist create mode 100644 editors/code/third-party/grammar/LICENSE create mode 100644 editors/code/third-party/grammar/README.md create mode 100644 editors/code/tsconfig.json create mode 100644 elisp/dotemacs.el create mode 100644 erlang_service/.gitignore create mode 100644 erlang_service/README.md create mode 100644 erlang_service/erlang_ls.config create mode 100644 erlang_service/rebar.config create mode 100644 erlang_service/rebar.lock create mode 100644 erlang_service/src/edoc_report.erl create mode 100644 erlang_service/src/elp_epp.erl create mode 100644 erlang_service/src/elp_escript.erl create mode 100644 erlang_service/src/elp_lint.erl create mode 100644 erlang_service/src/elp_metadata.erl create mode 100644 erlang_service/src/elp_parse.yrl create mode 100644 erlang_service/src/elp_scan.erl create mode 100644 erlang_service/src/erlang_service.app.src create mode 100644 erlang_service/src/erlang_service.erl create mode 100644 erlang_service/src/render_eep48_docs.erl create mode 100644 erlang_service/src/vendored/docsh_0_7_2/docsh_docs_v1.erl create mode 100644 erlang_service/src/vendored/docsh_0_7_2/docsh_edoc_xmerl.erl create mode 100644 erlang_service/src/vendored/docsh_0_7_2/docsh_format.erl create mode 100644 erlang_service/src/vendored/docsh_0_7_2/docsh_internal.erl create mode 100644 erlang_service/src/vendored/docsh_0_7_2/docsh_writer.erl create mode 100644 rustfmt.toml create mode 100644 test_projects/.gitignore create mode 100644 test_projects/README.md create mode 100644 test_projects/buck_tests/.elp.toml create mode 100644 test_projects/buck_tests/TARGETS.v2_ create mode 100644 test_projects/buck_tests/test_elp/TARGETS.v2_ create mode 100644 test_projects/buck_tests/test_elp/include/test_elp.hrl create mode 100644 test_projects/buck_tests/test_elp/src/test_elp.app.src create mode 100644 test_projects/buck_tests/test_elp/src/test_elp.erl create mode 100644 test_projects/buck_tests/test_elp/test/test_elp_SUITE.erl create mode 100644 test_projects/buck_tests/test_elp/test/test_elp_SUITE_data/handle_update_test1.json create mode 100644 test_projects/buck_tests/test_elp/test/test_elp_SUITE_data/handle_update_test2.json create mode 100644 test_projects/buck_tests/test_elp/test/test_elp_SUITE_data/untracked_header.hrl create mode 100644 test_projects/buck_tests/test_elp/test/test_elp_SUITE_data/untracked_module.erl create mode 100644 test_projects/buck_tests/test_elp/test/test_elp_test_utils.erl create mode 100644 test_projects/buck_tests/test_elp/test/test_elp_test_utils.hrl create mode 100644 test_projects/buck_tests/test_elp_direct_dep/TARGETS.v2_ create mode 100644 test_projects/buck_tests/test_elp_direct_dep/include/test_elp_direct_dep.hrl create mode 100644 test_projects/buck_tests/test_elp_direct_dep/src/test_elp_direct_dep.erl create mode 100644 test_projects/buck_tests/test_elp_direct_dep/src/test_elp_direct_dep_private.hrl create mode 100644 test_projects/buck_tests/test_elp_flat_inside_target/TARGETS.v2_ create mode 100644 test_projects/buck_tests/test_elp_flat_inside_target/test_elp_flat_inside_target.erl create mode 100644 test_projects/buck_tests/test_elp_flat_inside_target/test_elp_flat_inside_target.hrl create mode 100644 test_projects/buck_tests/test_elp_flat_outside_target/test_elp_flat_outside_target.erl create mode 100644 test_projects/buck_tests/test_elp_flat_outside_target/test_elp_flat_outside_target.hrl create mode 100644 test_projects/buck_tests/test_elp_ignored/test_elp_ignored.erl create mode 100644 test_projects/buck_tests/test_elp_no_private_headers/include/test_elp_no_private_headers.hrl create mode 100644 test_projects/buck_tests/test_elp_no_private_headers/src/test_elp_no_private_headers.erl create mode 100644 test_projects/buck_tests/test_elp_no_public_headers/src/test_elp_no_headers.erl create mode 100644 test_projects/buck_tests/test_elp_transitive_dep/TARGETS.v2_ create mode 100644 test_projects/buck_tests/test_elp_transitive_dep/include/test_elp_transitive_dep.hrl create mode 100644 test_projects/buck_tests/test_elp_transitive_dep/src/test_elp_transitive_dep.erl create mode 100644 test_projects/buck_tests/test_elp_transitive_dep/src/test_elp_transitive_dep_private.hrl create mode 100644 test_projects/diagnostics/.elp.toml create mode 100644 test_projects/diagnostics/README.md create mode 100644 test_projects/diagnostics/app_a/extra/app_a.erl create mode 100644 test_projects/diagnostics/app_a/include/app_a.hrl create mode 100644 test_projects/diagnostics/app_a/include/broken_diagnostics.hrl create mode 100644 test_projects/diagnostics/app_a/include/diagnostics.hrl create mode 100644 test_projects/diagnostics/app_a/src/app_a.app.src create mode 100644 test_projects/diagnostics/app_a/src/app_a.erl create mode 100644 test_projects/diagnostics/app_a/src/diagnostics.erl create mode 100644 test_projects/diagnostics/app_a/src/diagnostics.escript create mode 100644 test_projects/diagnostics/app_a/src/diagnostics_errors.escript create mode 100644 test_projects/diagnostics/app_a/src/diagnostics_warnings.escript create mode 100644 test_projects/diagnostics/app_a/src/lint_recursive.erl create mode 100644 test_projects/diagnostics/app_a/src/lints.erl create mode 100644 test_projects/diagnostics/app_a/test/app_a_SUITE.erl create mode 100644 test_projects/diagnostics/erlang_ls.config create mode 100644 test_projects/diagnostics/rebar.config create mode 100644 test_projects/diagnostics/wa_utils/src/wa_build_info_prv.erl create mode 100644 test_projects/diagnostics/wa_utils/src/wa_utils.app.src create mode 100644 test_projects/diagnostics/wa_utils/src/wa_utils.erl create mode 100644 test_projects/end_to_end/.elp.toml create mode 100644 test_projects/end_to_end/assist_examples/src/assist_examples.app.src create mode 100644 test_projects/end_to_end/assist_examples/src/code_completion.erl create mode 100644 test_projects/end_to_end/assist_examples/src/head_mismatch.erl create mode 100644 test_projects/end_to_end/definitions/README.md create mode 100644 test_projects/end_to_end/definitions/src/definitions.app.src create mode 100644 test_projects/end_to_end/definitions/src/local_def.erl create mode 100644 test_projects/end_to_end/erlang_ls.config create mode 100644 test_projects/end_to_end/hover/README.md create mode 100644 test_projects/end_to_end/hover/src/doc_examples.erl create mode 100644 test_projects/end_to_end/hover/src/hover.app.src create mode 100644 test_projects/end_to_end/rebar.config create mode 100644 test_projects/end_to_end/single_errors/README.md create mode 100644 test_projects/end_to_end/single_errors/src/single_errors.app.src create mode 100644 test_projects/end_to_end/single_errors/src/spec_mismatch.erl create mode 100644 test_projects/end_to_end/single_errors/src/spec_mismatch.erl.2 create mode 100644 test_projects/end_to_end/wa_utils/src/wa_build_info_prv.erl create mode 100644 test_projects/end_to_end/wa_utils/src/wa_utils.app.src create mode 100644 test_projects/end_to_end/wa_utils/src/wa_utils.erl create mode 100644 test_projects/eqwalizer/src/eqwalizer.app.src create mode 100644 test_projects/eqwalizer/src/eqwalizer_specs.erl create mode 100644 test_projects/in_place_tests/.elp.toml create mode 100644 test_projects/in_place_tests/README.md create mode 100644 test_projects/in_place_tests/app_a/extra/app_a.erl create mode 100644 test_projects/in_place_tests/app_a/include/app_a.hrl create mode 100644 test_projects/in_place_tests/app_a/include/broken_diagnostics.hrl create mode 100644 test_projects/in_place_tests/app_a/include/diagnostics.hrl create mode 100644 test_projects/in_place_tests/app_a/src/app_a.app.src create mode 100644 test_projects/in_place_tests/app_a/src/app_a.erl create mode 100644 test_projects/in_place_tests/app_a/src/lints.erl create mode 100644 test_projects/in_place_tests/app_a/test/app_a_SUITE.erl create mode 100644 test_projects/in_place_tests/erlang_ls.config create mode 100644 test_projects/in_place_tests/rebar.config create mode 100644 test_projects/in_place_tests/wa_utils/src/wa_build_info_prv.erl create mode 100644 test_projects/in_place_tests/wa_utils/src/wa_utils.app.src create mode 100644 test_projects/in_place_tests/wa_utils/src/wa_utils.erl create mode 100644 test_projects/linter/.elp.toml create mode 100644 test_projects/linter/.gitignore create mode 100644 test_projects/linter/app_a/include/app_a.hrl create mode 100644 test_projects/linter/app_a/src/app_a.app.src create mode 100644 test_projects/linter/app_a/src/app_a.erl create mode 100644 test_projects/linter/app_a/src/app_a_unused_param.erl create mode 100644 test_projects/linter/app_a/test/app_a_SUITE.erl create mode 100644 test_projects/linter/app_a/test/app_a_test_helpers.erl create mode 100644 test_projects/linter/app_a/test/app_a_test_helpers_not_opted_in.erl create mode 100644 test_projects/linter/app_a/test/app_test_helpers_no_errors.erl create mode 100644 test_projects/linter/app_b/src/app_b.app.src create mode 100644 test_projects/linter/app_b/src/app_b.erl create mode 100644 test_projects/linter/app_b/src/app_b_unused_param.erl create mode 100644 test_projects/linter/rebar.config create mode 100644 test_projects/linter/wa_utils/src/wa_build_info_prv.erl create mode 100644 test_projects/linter/wa_utils/src/wa_utils.app.src create mode 100644 test_projects/linter/wa_utils/src/wa_utils.erl create mode 100644 test_projects/parse_error/.elp.toml create mode 100644 test_projects/parse_error/.gitignore create mode 100644 test_projects/parse_error/.rebar.root create mode 100644 test_projects/parse_error/eqwalizer/src/eqwalizer.app.src create mode 100644 test_projects/parse_error/eqwalizer/src/eqwalizer.erl create mode 100644 test_projects/parse_error/eqwalizer/src/eqwalizer_specs.erl create mode 100644 test_projects/parse_error/parse_error_a/src/parse_error_a.app.src create mode 100644 test_projects/parse_error/parse_error_a/src/parse_error_a.erl create mode 100644 test_projects/parse_error/parse_error_a/src/parse_error_a_bad.erl create mode 100644 test_projects/parse_error/parse_error_a/src/parse_error_a_reference_bad.erl create mode 100644 test_projects/parse_error/parse_error_a/src/parse_error_a_syntax_error.erl create mode 100644 test_projects/parse_error/parse_error_a/src/parse_error_a_worst.erl create mode 100644 test_projects/parse_error/rebar.config create mode 100644 test_projects/parse_error/wa_utils/src/wa_build_info_prv.erl create mode 100644 test_projects/parse_error/wa_utils/src/wa_utils.app.src create mode 100644 test_projects/parse_error/wa_utils/src/wa_utils.erl create mode 100644 test_projects/standard/.elp.toml create mode 100644 test_projects/standard/.gitignore create mode 100644 test_projects/standard/.rebar.root create mode 100644 test_projects/standard/app_a/.eqwalizer create mode 100644 test_projects/standard/app_a/extra/app_a.erl create mode 100644 test_projects/standard/app_a/include/app_a.hrl create mode 100644 test_projects/standard/app_a/src/app_a.app.src create mode 100644 test_projects/standard/app_a/src/app_a.erl create mode 100644 test_projects/standard/app_a/src/app_a_errors_generated.erl create mode 100644 test_projects/standard/app_a/src/app_a_fixme.erl create mode 100644 test_projects/standard/app_a/src/app_a_ignored.erl create mode 100644 test_projects/standard/app_a/src/app_a_lists.erl create mode 100644 test_projects/standard/app_a/src/app_a_mod2.erl create mode 100644 test_projects/standard/app_a/src/app_a_no_errors.erl create mode 100644 test_projects/standard/app_a/src/app_a_no_errors_generated.erl create mode 100644 test_projects/standard/app_a/src/app_a_no_errors_opted_in.erl create mode 100644 test_projects/standard/app_a/test/app_a_SUITE.erl create mode 100644 test_projects/standard/app_a/test/app_a_test_helpers.erl create mode 100644 test_projects/standard/app_a/test/app_a_test_helpers_not_opted_in.erl create mode 100644 test_projects/standard/app_a/test/app_test_helpers_no_errors.erl create mode 100644 test_projects/standard/app_b/src/app_b.app.src create mode 100644 test_projects/standard/app_b/src/app_b.erl create mode 100644 test_projects/standard/eqwalizer/src/eqwalizer.app.src create mode 100644 test_projects/standard/eqwalizer/src/eqwalizer.erl create mode 100644 test_projects/standard/eqwalizer/src/eqwalizer_specs.erl create mode 100644 test_projects/standard/erlang_ls.config create mode 100644 test_projects/standard/rebar.config create mode 100644 test_projects/standard/wa_utils/src/wa_build_info_prv.erl create mode 100644 test_projects/standard/wa_utils/src/wa_utils.app.src create mode 100644 test_projects/standard/wa_utils/src/wa_utils.erl create mode 100644 website/.gitignore create mode 100644 website/.npmrc create mode 100644 website/README.md create mode 100644 website/babel.config.js create mode 100644 website/docs/architecture.md create mode 100644 website/docs/erlang-error-index.md create mode 100644 website/docs/feature-gallery.md create mode 100644 website/docs/get-started/_category_.json create mode 100644 website/docs/get-started/emacs.md create mode 100644 website/docs/get-started/get-started.md create mode 100644 website/docs/get-started/vscode.md create mode 100644 website/docusaurus.config.js create mode 100644 website/package.json create mode 100644 website/sidebars.js create mode 100644 website/src/components/HomepageFeatures.js create mode 100644 website/src/components/HomepageFeatures.module.css create mode 100644 website/src/css/custom.css create mode 100644 website/src/pages/index.js create mode 100644 website/src/pages/index.module.css create mode 100644 website/static/.nojekyll create mode 100644 website/static/img/elp_icon_color.svg create mode 100644 website/static/img/elp_logo_black_white.svg create mode 100644 website/static/img/elp_logo_color.svg create mode 100644 website/static/img/elp_logo_white_text.svg create mode 100644 website/yarn.lock create mode 100644 xtask/Cargo.toml create mode 100644 xtask/src/codegen.rs create mode 100644 xtask/src/main.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000000..1cbab12e1a --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,14 @@ +[alias] +xtask = "run --package xtask --" + +# @fb-only: [build] +# @fb-only: target-dir = "../../../buck-out/elp" + +[profile.release] +lto = "thin" +strip = true + +# Workaround to easily locate workspace root +# See https://github.com/rust-lang/cargo/issues/3946 +[env] +CARGO_WORKSPACE_DIR = {value = "", relative = true} diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..7da3da8c9d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Create a Bug Report +title: '' +labels: bug +assignees: '' + +--- + +### Describe the bug + +A clear and concise description of what the bug is. + +### To Reproduce** + +Steps to reproduce the behavior. + +### Expected behavior + +A clear and concise description of what you expected to happen. + +### Actual behavior + +A clear and concise description of what happens instead. + +### Context + + - ELP Version (output of `elp version`): + - Editor used: diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md new file mode 100644 index 0000000000..458b431cd6 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -0,0 +1,5 @@ +### Describe your changes + + diff --git a/.github/workflows/build-website.yml b/.github/workflows/build-website.yml new file mode 100644 index 0000000000..5142181169 --- /dev/null +++ b/.github/workflows/build-website.yml @@ -0,0 +1,27 @@ +name: Build Website + +defaults: + run: + working-directory: website + +on: + pull_request: + branches: + - main + +jobs: + build-website: + name: Build Website + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 18 + cache: yarn + cache-dependency-path: website/yarn.lock + + - name: Install dependencies + run: yarn install --frozen-lockfile + - name: Build website + run: yarn build diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..8c47362785 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,100 @@ +jobs: + linux-ci: + runs-on: ubuntu-20.04 + name: Linux CI (OTP ${{matrix.otp}}) + strategy: + matrix: + otp: ['26.0', '25.3'] + steps: + - name: Checkout erlang-language-platform + uses: "actions/checkout@v3" + - name: Checkout eqwalizer + uses: "actions/checkout@v3" + with: + repository: WhatsApp/eqwalizer + path: eqwalizer + - name: Set up GraalVM + uses: graalvm/setup-graalvm@v1 + with: + java-version: '17' + distribution: 'graalvm' + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Install musl-tools for rust toolchain + uses: awalsh128/cache-apt-pkgs-action@latest + with: + packages: musl-tools + version: 1.0 + - name: Set up rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + target: x86_64-unknown-linux-musl + - name: Install OTP + uses: erlef/setup-beam@v1 + with: + otp-version: ${{matrix.otp}} + install-rebar: false + install-hex: false + - name: Install rebar3 + run: "curl https://s3.amazonaws.com/rebar3/rebar3 -o rebar3 && chmod +x rebar3" + - name: "add rebar3 to path" + run: 'echo "$GITHUB_WORKSPACE/rebar3" >> $GITHUB_PATH' + - name: Assemble eqwalizer.jar + run: "cd eqwalizer/eqwalizer; sbt assembly" + - name: Assemble eqwalizer binary + run: "cd eqwalizer/eqwalizer && native-image -H:IncludeResources=application.conf --no-server --no-fallback -jar target/scala-2.13/eqwalizer.jar eqwalizer" + - name: Test elp + run: "export ELP_EQWALIZER_PATH=$GITHUB_WORKSPACE/eqwalizer/eqwalizer/eqwalizer && cargo test --no-default-features --workspace --target x86_64-unknown-linux-musl" + - name: Assemble elp + run: "export ELP_EQWALIZER_PATH=$GITHUB_WORKSPACE/eqwalizer/eqwalizer/eqwalizer && cargo build --release --target x86_64-unknown-linux-musl" + - name: Add elp to path + run: 'echo "$GITHUB_WORKSPACE/target/x86_64-unknown-linux-musl/release" >> $GITHUB_PATH' + - name: Test eqwalizer + run: 'cd eqwalizer/eqwalizer && sbt test' + - name: Upload eqwalizer.jar + if: matrix.otp == '25.3' + uses: "actions/upload-artifact@v3" + with: + name: eqwalizer.jar + path: eqwalizer/eqwalizer/target/scala-2.13/eqwalizer.jar + macos-ci: + needs: + - linux-ci + runs-on: macos-latest + name: MacOS CI (${{matrix.brew_erlang}}) + strategy: + matrix: + brew_erlang: ['erlang@25'] + steps: + - name: Checkout erlang-language-platform + uses: "actions/checkout@v3" + - name: Checkout eqwalizer + uses: "actions/checkout@v3" + with: + repository: WhatsApp/eqwalizer + path: eqwalizer + - name: Set up GraalVM + uses: graalvm/setup-graalvm@v1 + with: + java-version: '17' + distribution: 'graalvm' + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Install Erlang + run: brew install ${{matrix.brew_erlang}} + - name: Install rebar3 + run: "mkdir rebar3 && curl https://s3.amazonaws.com/rebar3/rebar3 -o rebar3/rebar3 && chmod +x rebar3/rebar3" + - name: Set up rust toolchain + uses: dtolnay/rust-toolchain@stable + - name: Download eqwalizer.jar + uses: "actions/download-artifact@v2" + with: + name: eqwalizer.jar + path: eqwalizer/eqwalizer/target/scala-2.13 + - name: Assemble eqwalizer binary + run: "cd eqwalizer/eqwalizer && native-image -H:IncludeResources=application.conf --no-server --no-fallback -jar target/scala-2.13/eqwalizer.jar eqwalizer" + - name: Test elp + run: "export PATH=$GITHUB_WORKSPACE/rebar3:/usr/local/opt/${{matrix.brew_erlang}}/bin:$PATH ELP_EQWALIZER_PATH=$GITHUB_WORKSPACE/eqwalizer/eqwalizer/eqwalizer && cargo test --no-default-features --workspace" + - name: Assemble elp + run: "export PATH=$GITHUB_WORKSPACE/rebar3:/usr/local/opt/${{matrix.brew_erlang}}/bin:$PATH ELP_EQWALIZER_PATH=$GITHUB_WORKSPACE/eqwalizer/eqwalizer/eqwalizer && cargo build --release" +name: erlang-language-platform CI +on: + push: {} diff --git a/.github/workflows/deploy-website.yml b/.github/workflows/deploy-website.yml new file mode 100644 index 0000000000..de7c9232e0 --- /dev/null +++ b/.github/workflows/deploy-website.yml @@ -0,0 +1,36 @@ +name: Deploy Website to GitHub Pages + +defaults: + run: + working-directory: website + +on: + push: + branches: + - main + +jobs: + deploy-website: + name: Deploy Website to GitHub Pages + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 18 + cache: yarn + cache-dependency-path: website/yarn.lock + + - name: Install dependencies + run: yarn install --frozen-lockfile + - name: Build website + run: yarn build + + # Popular action to deploy to GitHub Pages: + # Docs: https://github.com/peaceiris/actions-gh-pages#%EF%B8%8F-docusaurus + - name: Deploy Website to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + # Build output to publish to the `gh-pages` branch: + publish_dir: ./website/build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..ecc890bc10 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,249 @@ +jobs: + linux-release-otp-25: + runs-on: ubuntu-20.04 + steps: + - name: Checkout erlang-language-platform + uses: "actions/checkout@v3" + - name: Checkout eqwalizer + uses: "actions/checkout@v3" + with: + repository: WhatsApp/eqwalizer + path: eqwalizer + - name: Set up GraalVM + uses: "DeLaGuardo/setup-graalvm@5.0" + with: + graalvm: '22.1.0' + java: 'java11' + - name: Install Native Image Plugin + run: gu install native-image + - name: Install musl-tools for rust toolchain + uses: awalsh128/cache-apt-pkgs-action@latest + with: + packages: musl-tools + version: 1.0 + - name: Set up rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + target: x86_64-unknown-linux-musl + - name: Install OTP + uses: erlef/setup-beam@v1 + with: + otp-version: '25.2' + install-rebar: false + install-hex: false + - name: Install rebar3 + run: "curl https://s3.amazonaws.com/rebar3/rebar3 -o rebar3 && chmod +x rebar3" + - name: "add rebar3 to path" + run: 'echo "$GITHUB_WORKSPACE/rebar3" >> $GITHUB_PATH' + - name: Assemble eqwalizer.jar + run: "cd eqwalizer/eqwalizer; sbt assembly" + - name: Assemble eqwalizer binary + run: "cd eqwalizer/eqwalizer && native-image -H:IncludeResources=application.conf --no-server --no-fallback -jar target/scala-2.13/eqwalizer.jar eqwalizer" + - name: Test elp + run: "export ELP_EQWALIZER_PATH=$GITHUB_WORKSPACE/eqwalizer/eqwalizer/eqwalizer && cargo test --no-default-features --workspace --target x86_64-unknown-linux-musl" + - name: Assemble elp + run: "export ELP_EQWALIZER_PATH=$GITHUB_WORKSPACE/eqwalizer/eqwalizer/eqwalizer && cargo build --release --target x86_64-unknown-linux-musl" + - name: Add elp to path + run: 'echo "$GITHUB_WORKSPACE/target/x86_64-unknown-linux-musl/release" >> $GITHUB_PATH' + - name: Test eqwalizer + run: 'cd eqwalizer && sbt test' + - name: Upload eqwalizer.jar + uses: "actions/upload-artifact@v2" + with: + name: eqwalizer.jar + path: eqwalizer/eqwalizer/target/scala-2.13/eqwalizer.jar + - name: Upload eqwalizer native binary + uses: "actions/upload-artifact@v2" + with: + name: eqwalizer + path: ./eqwalizer/eqwalizer/eqwalizer + - name: Make elp-linux.tar.gz + run: 'tar -zcvf elp-linux.tar.gz -C target/x86_64-unknown-linux-musl/release/ elp' + - env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + id: get_release_url + name: Get release url + uses: "bruceadams/get-release@v1.3.2" + - env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + name: Upload release elp-linux.tar.gz + uses: "actions/upload-release-asset@v1.0.2" + with: + asset_content_type: application/octet-stream + asset_name: elp-linux.tar.gz + asset_path: elp-linux.tar.gz + upload_url: "${{ steps.get_release_url.outputs.upload_url }}" + linux-release-otp-23: + needs: + - linux-release-otp-25 + runs-on: ubuntu-20.04 + steps: + - name: Checkout erlang-language-platform + uses: "actions/checkout@v3" + - name: Checkout eqwalizer + uses: "actions/checkout@v3" + with: + repository: WhatsApp/eqwalizer + path: eqwalizer + - name: Set up GraalVM + uses: "DeLaGuardo/setup-graalvm@5.0" + with: + graalvm: '22.1.0' + java: 'java11' + - name: Install Native Image Plugin + run: gu install native-image + - name: Install musl-tools for rust toolchain + uses: awalsh128/cache-apt-pkgs-action@latest + with: + packages: musl-tools + version: 1.0 + - name: Set up rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + target: x86_64-unknown-linux-musl + - name: Install OTP + uses: erlef/setup-beam@v1 + with: + otp-version: '23.3' + install-rebar: false + install-hex: false + - name: Install rebar3 + run: "curl https://s3.amazonaws.com/rebar3/rebar3 -o rebar3 && chmod +x rebar3" + - name: "add rebar3 to path" + run: 'echo "$GITHUB_WORKSPACE/rebar3" >> $GITHUB_PATH' + - name: Download eqwalizer.jar + uses: "actions/download-artifact@v2" + with: + name: eqwalizer.jar + path: eqwalizer/eqwalizer/target/scala-2.13 + - name: Assemble eqwalizer binary + run: "cd eqwalizer/eqwalizer && native-image -H:IncludeResources=application.conf --no-server --no-fallback -jar target/scala-2.13/eqwalizer.jar eqwalizer" + - name: Test elp + run: "export ELP_EQWALIZER_PATH=$GITHUB_WORKSPACE/eqwalizer/eqwalizer/eqwalizer && cargo test --no-default-features --workspace --target x86_64-unknown-linux-musl" + - name: Assemble elp + run: "export ELP_EQWALIZER_PATH=$GITHUB_WORKSPACE/eqwalizer/eqwalizer/eqwalizer && cargo build --release --target x86_64-unknown-linux-musl" + - name: Make elp-linux-otp-23.tar.gz + run: 'tar -zcvf elp-linux-otp-23.tar.gz -C target/x86_64-unknown-linux-musl/release/ elp' + - env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + id: get_release_url + name: Get release url + uses: "bruceadams/get-release@v1.3.2" + - env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + name: Upload release elp-linux-otp-23.tar.gz + uses: "actions/upload-release-asset@v1.0.2" + with: + asset_content_type: application/octet-stream + asset_name: elp-linux-otp-23.tar.gz + asset_path: elp-linux-otp-23.tar.gz + upload_url: "${{ steps.get_release_url.outputs.upload_url }}" + macos-release-otp-25: + needs: + - linux-release-otp-25 + runs-on: macos-latest + steps: + - name: Checkout erlang-language-platform + uses: "actions/checkout@v3" + - name: Checkout eqwalizer + uses: "actions/checkout@v3" + with: + repository: WhatsApp/eqwalizer + path: eqwalizer + - name: Set up GraalVM + uses: "DeLaGuardo/setup-graalvm@5.0" + with: + graalvm: '22.1.0' + java: 'java11' + - name: Install Native Image Plugin + run: gu install native-image + - name: Install erlang + run: brew install erlang@25 + - name: Install rebar3 + run: "mkdir rebar3 && curl https://s3.amazonaws.com/rebar3/rebar3 -o rebar3/rebar3 && chmod +x rebar3/rebar3" + - name: Set up rust toolchain + uses: dtolnay/rust-toolchain@stable + - name: Download eqwalizer.jar + uses: "actions/download-artifact@v2" + with: + name: eqwalizer.jar + path: eqwalizer/eqwalizer/target/scala-2.13 + - name: Assemble eqwalizer binary + run: "cd eqwalizer/eqwalizer && native-image -H:IncludeResources=application.conf --no-server --no-fallback -jar target/scala-2.13/eqwalizer.jar eqwalizer" + - name: Test elp + run: "export PATH=$GITHUB_WORKSPACE/rebar3:/usr/local/opt/erlang@25/bin:$PATH ELP_EQWALIZER_PATH=$GITHUB_WORKSPACE/eqwalizer/eqwalizer/eqwalizer && cargo test --no-default-features --workspace" + - name: Assemble elp + run: "export PATH=$GITHUB_WORKSPACE/rebar3:/usr/local/opt/erlang@25/bin:$PATH ELP_EQWALIZER_PATH=$GITHUB_WORKSPACE/eqwalizer/eqwalizer/eqwalizer && cargo build --release" + - name: Make elp-macos.tar.gz + run: 'tar -zcvf elp-macos.tar.gz -C target/release/ elp' + - env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + id: get_release_url + name: Get release url + uses: "bruceadams/get-release@v1.3.2" + - env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + name: Upload release elp-macos.tar.gz + uses: "actions/upload-release-asset@v1.0.2" + with: + asset_content_type: application/octet-stream + asset_name: elp-macos.tar.gz + asset_path: elp-macos.tar.gz + upload_url: "${{ steps.get_release_url.outputs.upload_url }}" + macos-release-otp-23: + needs: + - linux-release-otp-25 + runs-on: macos-latest + steps: + - name: Checkout erlang-language-platform + uses: "actions/checkout@v3" + - name: Checkout eqwalizer + uses: "actions/checkout@v3" + with: + repository: WhatsApp/eqwalizer + path: eqwalizer + - name: Set up GraalVM + uses: "DeLaGuardo/setup-graalvm@5.0" + with: + graalvm: '22.1.0' + java: 'java11' + - name: Install Native Image Plugin + run: gu install native-image + - name: Install erlang + run: brew install erlang@23 + - name: Install rebar3 + run: "mkdir rebar3 && curl https://s3.amazonaws.com/rebar3/rebar3 -o rebar3/rebar3 && chmod +x rebar3/rebar3" + - name: Set up rust toolchain + uses: dtolnay/rust-toolchain@stable + - name: Download eqwalizer.jar + uses: "actions/download-artifact@v2" + with: + name: eqwalizer.jar + path: eqwalizer/eqwalizer/target/scala-2.13 + - name: Assemble eqwalizer binary + run: "cd eqwalizer/eqwalizer && native-image -H:IncludeResources=application.conf --no-server --no-fallback -jar target/scala-2.13/eqwalizer.jar eqwalizer" + - name: Test elp + run: "export PATH=$GITHUB_WORKSPACE/rebar3:/usr/local/opt/erlang@23/bin:$PATH ELP_EQWALIZER_PATH=$GITHUB_WORKSPACE/eqwalizer/eqwalizer/eqwalizer && cargo test --no-default-features --workspace" + - name: Assemble elp + run: "export PATH=$GITHUB_WORKSPACE/rebar3:/usr/local/opt/erlang@23/bin:$PATH ELP_EQWALIZER_PATH=$GITHUB_WORKSPACE/eqwalizer/eqwalizer/eqwalizer && cargo build --release" + - name: Make elp-macos-otp-23.tar.gz + run: 'tar -zcvf elp-macos-otp-23.tar.gz -C target/release/ elp' + - env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + id: get_release_url + name: Get release url + uses: "bruceadams/get-release@v1.3.2" + - env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + name: Upload release elp-macos-otp-23.tar.gz + uses: "actions/upload-release-asset@v1.0.2" + with: + asset_content_type: application/octet-stream + asset_name: elp-macos-otp-23.tar.gz + asset_path: elp-macos-otp-23.tar.gz + upload_url: "${{ steps.get_release_url.outputs.upload_url }}" +name: erlang-language-platform release +on: + release: + types: + - created diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..9da4a887b3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +!Cargo.lock diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000..9487d72038 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "attach", + "name": "Attach to a running ELP instance", + "pid": "${command:pickMyProcess}", + "sourceLanguages": [ + "rust" + ] + } + ] +} \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..3232ed6655 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,80 @@ +# Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to make participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic +address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a +professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies within all project spaces, and it also applies when +an individual is representing the project or its community in public spaces. +Examples of representing a project or community include using an official +project e-mail address, posting via an official social media account, or acting +as an appointed representative at an online or offline event. Representation of +a project may be further defined and clarified by project maintainers. + +This Code of Conduct also applies outside the project spaces when there is a +reasonable belief that an individual's behavior may have a negative impact on +the project or its community. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at . All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..f50b454603 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,73 @@ +# Contributing to Erlang Language Platform + +We want to make contributing to this project as easy and transparent as possible. + +## Our Development Process + +ELP is currently developed in Meta's internal repositories and then exported +out to GitHub by a Meta team member; however, we invite you to submit pull +requests as described below. + +## Developing Locally + +The gold standard is the current config in [ci.yml](.github/workflows/ci.yml). + +A summary of these is + +Check out [eqwalizer](https://github.com/WhatsApp/eqwalizer/) next to erlang-language-platform (this repo). + +Install [sbt](https://www.scala-sbt.org/), and java >= 11 + +```sh +cd $PATH_TO_REPO/eqwalizer/eqwalizer +sbt assembly +``` + +It gives a path to the file + +```sh +export ELP_EQWALIZER_PATH= +``` + +e.g. on a particular machine this would be + +```sh +export ELP_EQWALIZER_PATH=/home/alanz/mysrc/github/WhatsApp/eqwalizer/eqwalizer/target/scala-2.13/eqwalizer.jar +``` + +Then use `cargo build` as usual from this repository. + +Note: if you set `ELP_EQWALIZER_PATH` in your shell profile, it will be used by rust_analyzer in your IDE too. + +An alternative way to build is + +```sh +ELP_EQWALIZER_PATH=../../../eqwalizer/eqwalizer/target/scala-2.13/eqwalizer.jar cargo build +``` + +## Pull Requests + +We actively welcome your pull requests. + +1. Fork the repo and create your branch from `main`. +2. If you've added code that should be tested, add tests. +3. Ensure the test suite passes. +4. If you haven't already, complete the Contributor License Agreement ("CLA"). + +## Contributor License Agreement ("CLA") + +In order to accept your pull request, we need you to submit a CLA. You only need +to do this once to work on any of Facebook's open source projects. + +Complete your CLA here: + +## Issues + +We use GitHub issues to track public bugs. Please ensure your description is +clear and has sufficient instructions to be able to reproduce the issue. + +## License + +By contributing to Erlang Language Platform, you agree that your contributions will be +licensed under the [APACHE2](LICENSE-APACHE) and [MIT](LICENSE-MIT) licenses in the root +directory of this source tree. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000000..6cc8e88be6 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2380 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +dependencies = [ + "memchr", +] + +[[package]] +name = "always-assert" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4436e0292ab1bb631b42973c61205e704475fe8126af845c8d923c0996328127" +dependencies = [ + "log", +] + +[[package]] +name = "anyhow" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bpaf" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f4c9de9c67618395106c81fb9461290a8910af29aa0188daec29001a1181ae" +dependencies = [ + "bpaf_derive", +] + +[[package]] +name = "bpaf_derive" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "223f3c9e7034f98c9f315d9945fcc22831b3f03d9f4c42c96a7ab6abd209a195" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "camino" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c530edf18f37068ac2d977409ed5cd50d53d73bc653c7647b48eb78976ac9ae2" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cfg-expr" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bbc13bf6290a6b202cc3efb36f7ec2b739a80634215630c8053a313edf6abef" +dependencies = [ + "smallvec", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clipboard-win" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" +dependencies = [ + "error-code", + "str-buf", + "winapi", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "console" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d79fbe8970a77e3e34151cc13d3b3e248aa0faaecb9f6091fa07ebefe5ad60" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.42.0", +] + +[[package]] +name = "countme" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" +dependencies = [ + "dashmap", + "once_cell", + "rustc-hash", +] + +[[package]] +name = "cov-mark" +version = "2.0.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d48d8f76bd9331f19fe2aaf3821a9f9fb32c3963e1e3d6ce82a8c09cef7444a" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "dashmap" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" +dependencies = [ + "cfg-if", + "hashbrown", + "lock_api", + "once_cell", + "parking_lot_core 0.9.7", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dissimilar" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "210ec60ae7d710bed8683e333e9d2855a8a56a3e9892b38bad3bb0d4d29b0d5e" + +[[package]] +name = "eetf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7421e7747faa71f9b7e0bff19653c9e1dd5aa9b840de664b9510250b38fd4887" +dependencies = [ + "byteorder", + "libflate", + "num", + "ordered-float", + "thiserror", +] + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + +[[package]] +name = "elp" +version = "1.1.0" +dependencies = [ + "always-assert", + "anyhow", + "bpaf", + "codespan-reporting", + "crossbeam-channel", + "elp_ai", + "elp_ide", + "elp_log", + "elp_project_model", + "elp_syntax", + "env_logger", + "expect-test", + "fs_extra", + "fxhash", + "indicatif", + "itertools", + "jod-thread", + "lazy_static", + "log", + "lsp-server", + "lsp-types", + "parking_lot 0.12.1", + "pico-args", + "profile", + "rayon", + "regex", + "rustyline", + "serde", + "serde_json", + "serde_path_to_error", + "stdx", + "strsim", + "tempfile", + "test-case", + "text-edit", + "threadpool", + "tikv-jemallocator", + "time", + "toml", + "vfs-notify", +] + +[[package]] +name = "elp_ai" +version = "1.1.0" +dependencies = [ + "anyhow", + "crossbeam-channel", + "jod-thread", + "log", + "stdx", + "tempfile", +] + +[[package]] +name = "elp_base_db" +version = "1.1.0" +dependencies = [ + "dissimilar", + "eetf", + "either", + "elp_project_model", + "elp_syntax", + "expect-test", + "fxhash", + "lazy_static", + "log", + "paths", + "profile", + "regex", + "salsa", + "stdx", + "vfs", +] + +[[package]] +name = "elp_eqwalizer" +version = "1.1.0" +dependencies = [ + "anyhow", + "eetf", + "elp_base_db", + "elp_syntax", + "fxhash", + "lazy_static", + "log", + "parking_lot 0.12.1", + "salsa", + "serde", + "serde_json", + "serde_with", + "stdx", + "tempfile", + "timeout-readwrite", +] + +[[package]] +name = "elp_erlang_service" +version = "1.1.0" +dependencies = [ + "anyhow", + "crossbeam-channel", + "eetf", + "env_logger", + "expect-test", + "fxhash", + "jod-thread", + "lazy_static", + "log", + "parking_lot 0.12.1", + "regex", + "stdx", + "tempfile", + "text-size", +] + +[[package]] +name = "elp_ide" +version = "1.1.0" +dependencies = [ + "anyhow", + "elp_ide_assists", + "elp_ide_completion", + "elp_ide_db", + "elp_project_model", + "elp_syntax", + "env_logger", + "expect-test", + "fxhash", + "hir", + "imara-diff", + "itertools", + "lazy_static", + "log", + "profile", + "regex", + "smallvec", + "stdx", + "strsim", + "strum", + "strum_macros", + "text-edit", + "triple_accel", +] + +[[package]] +name = "elp_ide_assists" +version = "1.1.0" +dependencies = [ + "cov-mark", + "elp_ide_db", + "elp_syntax", + "expect-test", + "fxhash", + "hir", + "itertools", + "lazy_static", + "log", + "regex", + "stdx", + "text-edit", +] + +[[package]] +name = "elp_ide_completion" +version = "1.1.0" +dependencies = [ + "elp_base_db", + "elp_ide_db", + "elp_syntax", + "expect-test", + "fxhash", + "hir", + "lazy_static", + "log", + "lsp-types", + "serde_json", + "stdx", +] + +[[package]] +name = "elp_ide_db" +version = "1.1.0" +dependencies = [ + "anyhow", + "eetf", + "either", + "elp_base_db", + "elp_eqwalizer", + "elp_erlang_service", + "elp_project_model", + "elp_syntax", + "expect-test", + "fxhash", + "hir", + "indexmap", + "log", + "memchr", + "once_cell", + "parking_lot 0.12.1", + "profile", + "rustc-hash", + "serde", + "stdx", + "text-edit", +] + +[[package]] +name = "elp_log" +version = "1.1.0" +dependencies = [ + "crossbeam-channel", + "env_logger", + "expect-test", + "fxhash", + "lazy_static", + "log", + "parking_lot 0.12.1", + "serde", + "serde_json", + "tempfile", +] + +[[package]] +name = "elp_project_model" +version = "1.1.0" +dependencies = [ + "anyhow", + "eetf", + "elp_log", + "fxhash", + "itertools", + "lazy_static", + "log", + "parking_lot 0.12.1", + "paths", + "serde", + "serde_json", + "tempfile", + "toml", +] + +[[package]] +name = "elp_syntax" +version = "1.1.0" +dependencies = [ + "cov-mark", + "eetf", + "elp_ide_db", + "expect-test", + "fxhash", + "indexmap", + "itertools", + "log", + "num-derive", + "num-traits", + "once_cell", + "profile", + "rowan", + "smol_str", + "stdx", + "text-edit", + "tree-sitter", + "tree-sitter-erlang", +] + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "error-code" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" +dependencies = [ + "libc", + "str-buf", +] + +[[package]] +name = "expect-test" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d9eafeadd538e68fb28016364c9732d78e420b9ff8853fa5e4058861e9f8d3" +dependencies = [ + "dissimilar", + "once_cell", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fd-lock" +version = "3.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ae6b3d9530211fb3b12a95374b8b0823be812f53d09e18c5675c0146b09642" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "filetime" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.2.16", + "windows-sys 0.48.0", +] + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "fst" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab85b9b05e3978cc9a9cf8fea7f01b494e1a09ed3037e16ba39edc7a29eb61a" + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "getrandom" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + +[[package]] +name = "hir" +version = "1.1.0" +dependencies = [ + "either", + "elp_base_db", + "elp_syntax", + "expect-test", + "fxhash", + "itertools", + "la-arena", + "lazy_static", + "log", + "profile", + "regex", + "stdx", + "triple_accel", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "imara-diff" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e98c1d0ad70fc91b8b9654b1f33db55e59579d3b3de2bffdced0fdb810570cb8" +dependencies = [ + "ahash", + "hashbrown", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "indicatif" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cef509aa9bc73864d6756f0d34d35504af3cf0844373afe9b8669a5b8005a729" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "rayon", + "unicode-width", +] + +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +dependencies = [ + "hermit-abi 0.3.1", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "is-terminal" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +dependencies = [ + "hermit-abi 0.3.1", + "io-lifetimes", + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "jod-thread" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b23360e99b8717f20aaa4598f5a6541efbe30630039fbc7706cf954a87947ae" + +[[package]] +name = "kqueue" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8fc60ba15bf51257aa9807a48a61013db043fcf3a78cb0d916e8e396dcad98" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "krates" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "942c43a6cba1c201dfe81a943c89fa5c9140b34993e0c027f542c80b92e319a7" +dependencies = [ + "cargo_metadata", + "cfg-expr", + "petgraph", + "semver", +] + +[[package]] +name = "la-arena" +version = "0.3.0" +source = "git+https://github.com/rust-lang/rust-analyzer?rev=2022-09-05#67920f797511c360b25dab4d30730be304848f32" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.142" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" + +[[package]] +name = "libflate" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97822bf791bd4d5b403713886a5fbe8bf49520fe78e323b0dc480ca1a03e50b0" +dependencies = [ + "adler32", + "crc32fast", + "libflate_lz77", +] + +[[package]] +name = "libflate_lz77" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a52d3a8bfc85f250440e4424db7d857e241a3aebbbe301f3eb606ab15c39acbf" +dependencies = [ + "rle-decode-fast", +] + +[[package]] +name = "linux-raw-sys" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eb31c1778188ae1e64398743890d0877fef36d11521ac60406b42016e8c2cf" + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "lsp-server" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a9b4c78d1c3f35c5864c90e9633377b5f374a4a4983ac64c30b8ae898f9305" +dependencies = [ + "crossbeam-channel", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "lsp-types" +version = "0.93.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9be6e9c7e2d18f651974370d7aff703f9513e0df6e464fd795660edc77e6ca51" +dependencies = [ + "bitflags", + "serde", + "serde_json", + "serde_repr", + "url", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memoffset" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mio" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.45.0", +] + +[[package]] +name = "miow" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7377f7792b3afb6a3cba68daa54ca23c032137010460d667fda53a8d66be00e" +dependencies = [ + "windows-sys 0.28.0", +] + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nix" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +dependencies = [ + "bitflags", + "cfg-if", + "libc", + "static_assertions", +] + +[[package]] +name = "notify" +version = "5.0.0-pre.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "530f6314d6904508082f4ea424a0275cf62d341e118b313663f266429cb19693" +dependencies = [ + "bitflags", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "mio", + "walkdir", + "winapi", +] + +[[package]] +name = "num" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e0d21255c828d6f128a1e41534206671e8c3ea0c62f32291e808dc82cff17d" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi 0.2.6", + "libc", +] + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + +[[package]] +name = "oorandom" +version = "11.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" + +[[package]] +name = "ordered-float" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7940cf2ca942593318d07fcf2596cdca60a85c9e7fab408a5e21a4f9dcd40d87" +dependencies = [ + "num-traits", +] + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.7", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "windows-sys 0.45.0", +] + +[[package]] +name = "paste" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" + +[[package]] +name = "paths" +version = "0.0.0" +source = "git+https://github.com/rust-lang/rust-analyzer?rev=2022-09-05#67920f797511c360b25dab4d30730be304848f32" + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "perf-event" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4d6393d9238342159080d79b78cb59c67399a8e7ecfa5d410bd614169e4e823" +dependencies = [ + "libc", + "perf-event-open-sys", +] + +[[package]] +name = "perf-event-open-sys" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c44fb1c7651a45a3652c4afc6e754e40b3d6e6556f1487e2b230bfc4f33c2a8" +dependencies = [ + "libc", +] + +[[package]] +name = "petgraph" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "portable-atomic" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26f6a7b87c2e435a3241addceeeff740ff8b7e76b74c13bf9acb17fa454ea00b" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profile" +version = "0.0.0" +source = "git+https://github.com/rust-lang/rust-analyzer?rev=2022-09-05#67920f797511c360b25dab4d30730be304848f32" +dependencies = [ + "cfg-if", + "countme", + "la-arena", + "libc", + "once_cell", + "perf-event", + "tikv-jemalloc-ctl", + "winapi", +] + +[[package]] +name = "quote" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[package]] +name = "rayon" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall 0.2.16", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" + +[[package]] +name = "rle-decode-fast" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422" + +[[package]] +name = "rowan" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64449cfef9483a475ed56ae30e2da5ee96448789fb2aa240a04beb6a055078bf" +dependencies = [ + "countme", + "hashbrown", + "memoffset", + "rustc-hash", + "text-size", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.37.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0661814f891c57c930a610266415528da53c4933e6dea5fb350cbfe048a9ece" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustversion" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" + +[[package]] +name = "rustyline" +version = "11.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfc8644681285d1fb67a467fb3021bfea306b99b4146b166a1fe3ada965eece" +dependencies = [ + "bitflags", + "cfg-if", + "clipboard-win", + "dirs-next", + "fd-lock", + "libc", + "log", + "memchr", + "nix", + "radix_trie", + "scopeguard", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "winapi", +] + +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + +[[package]] +name = "salsa" +version = "0.17.0-pre.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b223dccb46c32753144d0b51290da7230bb4aedcd8379d6b4c9a474c18bf17a" +dependencies = [ + "crossbeam-utils", + "indexmap", + "lock_api", + "log", + "oorandom", + "parking_lot 0.11.2", + "rustc-hash", + "salsa-macros", + "smallvec", +] + +[[package]] +name = "salsa-macros" +version = "0.17.0-pre.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac6c2e352df550bf019da7b16164ed2f7fa107c39653d1311d1bba42d1582ff7" +dependencies = [ + "heck 0.3.3", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "semver" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" +dependencies = [ + "serde", +] + +[[package]] +name = "serde" +version = "1.0.160" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.160" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] + +[[package]] +name = "serde_json" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7f05c1d5476066defcdfacce1f52fc3cae3af1d3089727100c02ae92e5abbe0" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] + +[[package]] +name = "serde_with" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" +dependencies = [ + "serde", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "smol_str" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad6c857cbab2627dcf01ec85a623ca4e7dcb5691cbaa3d7fb7653671f0d09c9" +dependencies = [ + "serde", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stdx" +version = "0.0.0" +source = "git+https://github.com/rust-lang/rust-analyzer?rev=2022-09-05#67920f797511c360b25dab4d30730be304848f32" +dependencies = [ + "always-assert", + "libc", + "miow", + "winapi", +] + +[[package]] +name = "str-buf" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" + +[[package]] +name = "strum_macros" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9f3bd7d2e45dcc5e265fbb88d6513e4747d8ef9444cf01a533119bce28a157" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.15", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall 0.3.5", + "rustix", + "windows-sys 0.45.0", +] + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "test-case" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21d6cf5a7dffb3f9dceec8e6b8ca528d9bd71d36c9f074defb548ce161f598c0" +dependencies = [ + "test-case-macros", +] + +[[package]] +name = "test-case-macros" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e45b7bf6e19353ddd832745c8fcf77a17a93171df7151187f26623f2b75b5b26" +dependencies = [ + "cfg-if", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "text-edit" +version = "0.0.0" +source = "git+https://github.com/rust-lang/rust-analyzer?rev=2022-09-05#67920f797511c360b25dab4d30730be304848f32" +dependencies = [ + "itertools", + "text-size", +] + +[[package]] +name = "text-size" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "288cb548dbe72b652243ea797201f3d481a0609a967980fcc5b2315ea811560a" + +[[package]] +name = "thiserror" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "tikv-jemalloc-ctl" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e37706572f4b151dff7a0146e040804e9c26fe3a3118591112f05cf12a4216c1" +dependencies = [ + "libc", + "paste", + "tikv-jemalloc-sys", +] + +[[package]] +name = "tikv-jemalloc-sys" +version = "0.5.3+5.3.0-patched" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a678df20055b43e57ef8cddde41cdfda9a3c1a060b67f4c5836dfb1d78543ba8" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "tikv-jemallocator" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20612db8a13a6c06d57ec83953694185a367e16945f66565e8028d2c0bd76979" +dependencies = [ + "libc", + "tikv-jemalloc-sys", +] + +[[package]] +name = "time" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" +dependencies = [ + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + +[[package]] +name = "time-macros" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +dependencies = [ + "time-core", +] + +[[package]] +name = "timeout-readwrite" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37312ddc0adbd0f112618a4250ac586448151ff6d69241ff061b29b883349f3e" +dependencies = [ + "nix", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "tracing" +version = "0.1.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9cf6a813d3f40c88b0b6b6f29a5c95c6cdbf97c1f9cc53fb820200f5ad814d" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] + +[[package]] +name = "tracing-core" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tree-sitter" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e747b1f9b7b931ed39a548c1fae149101497de3c1fc8d9e18c62c1a66c683d3d" +dependencies = [ + "cc", + "regex", +] + +[[package]] +name = "tree-sitter-erlang" +version = "0.1.0" +dependencies = [ + "cc", + "tree-sitter", +] + +[[package]] +name = "triple_accel" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22048bc95dfb2ffd05b1ff9a756290a009224b60b2f0e7525faeee7603851e63" + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "vfs" +version = "0.0.0" +source = "git+https://github.com/rust-lang/rust-analyzer?rev=2022-09-05#67920f797511c360b25dab4d30730be304848f32" +dependencies = [ + "fst", + "indexmap", + "paths", + "rustc-hash", + "stdx", +] + +[[package]] +name = "vfs-notify" +version = "0.0.0" +source = "git+https://github.com/rust-lang/rust-analyzer?rev=2022-09-05#67920f797511c360b25dab4d30730be304848f32" +dependencies = [ + "crossbeam-channel", + "jod-thread", + "notify", + "paths", + "tracing", + "vfs", + "walkdir", +] + +[[package]] +name = "walkdir" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82ca39602d5cbfa692c4b67e3bcbb2751477355141c1ed434c94da4186836ff6" +dependencies = [ + "windows_aarch64_msvc 0.28.0", + "windows_i686_gnu 0.28.0", + "windows_i686_msvc 0.28.0", + "windows_x86_64_gnu 0.28.0", + "windows_x86_64_msvc 0.28.0", +] + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52695a41e536859d5308cc613b4a022261a274390b25bd29dfff4bf08505f3c2" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f54725ac23affef038fecb177de6c9bf065787c2f432f79e3c373da92f3e1d8a" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d5158a43cc43623c0729d1ad6647e62fa384a3d135fd15108d37c683461f64" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc31f409f565611535130cfe7ee8e6655d3fa99c1c61013981e491921b5ce954" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f2b8c7cbd3bfdddd9ab98769f9746a7fad1bca236554cd032b78d768bc0e89f" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "xshell" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "962c039b3a7b16cf4e9a4248397c6585c07547412e7d6a6e035389a802dcfe90" +dependencies = [ + "xshell-macros", +] + +[[package]] +name = "xshell-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dbabb1cbd15a1d6d12d9ed6b35cc6777d4af87ab3ba155ea37215f20beab80c" + +[[package]] +name = "xtask" +version = "1.1.0" +dependencies = [ + "anyhow", + "krates", + "pico-args", + "proc-macro2", + "quote", + "serde", + "serde_json", + "tree-sitter-erlang", + "xshell", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000000..0ab02811cc --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,97 @@ +[workspace] +members = ["crates/*", "xtask"] +default-members = ["crates/*", "xtask"] + +[workspace.package] +edition = "2021" +version = "1.1.0" + +[workspace.dependencies] +# own local crates +elp_ai = { path = "./crates/ai" } +elp_base_db = { path = "./crates/base_db" } +elp_eqwalizer = { path = "./crates/eqwalizer" } +elp_erlang_service = { path = "./crates/erlang_service" } +elp_ide = { path = "./crates/ide" } +elp_ide_assists = { path = "./crates/ide_assists" } +elp_ide_completion = { path = "./crates/ide_completion" } +elp_ide_db = { path = "./crates/ide_db" } +elp_log = { path = "./crates/elp_log" } +elp_project_model = { path = "./crates/project_model" } +elp_syntax = { path = "./crates/syntax" } +hir = { path = "./crates/hir" } + +# Forks +erl_ast = { path = "./crates/erl_ast" } + +# External crates +always-assert = "0.1.3" +anyhow = "1.0.70" +bpaf = { version = "=0.7.9", features = ["derive", "autocomplete", "batteries"] } +codespan-reporting = "0.11.1" +cov-mark = "2.0.0-pre.1" +criterion = "0.3.6" +crossbeam-channel = "0.5.8" +dissimilar = "1.0.6" +triple_accel = "0.4.0" +eetf = "0.8.0" +either = "1.8.1" +env_logger = "0.10.0" +expect-test = "1.4.1" +fs_extra = "1.3.0" +fxhash = "0.2.1" +imara-diff = "0.1.5" +indexmap = "1.9.3" +indicatif = { version = "0.17.3", features = ["rayon"] } +itertools = "0.10.5" +jemallocator = { version = "0.5.0", package = "tikv-jemallocator" } +jod-thread = "0.1.2" +krates = "0.12.6" +la-arena = { git = "https://github.com/rust-lang/rust-analyzer", rev = "2022-09-05" } +lazy_static = "1.4.0" +log = "0.4.17" +lsp-server = "0.7.0" +lsp-types = { version = "0.93.2", features = ["proposed"] } +memchr = "2.5.0" +num-derive = "0.3.3" +num-traits = "0.2.15" +once_cell = "1.17.1" +parking_lot = "0.12.1" +paths = { git = "https://github.com/rust-lang/rust-analyzer", rev = "2022-09-05" } +pico-args = "0.5.0" +proc-macro2 = "1.0.56" +profile = { features = [ + "jemalloc", +], git = "https://github.com/rust-lang/rust-analyzer", rev = "2022-09-05" } +quote = "1.0.26" +rayon = "1.7.0" +regex = "1.7.3" +rowan = "0.15.11" +rust-ini = "0.18" +rustc-hash = "1.1.0" +rustyline = "11.0.0" +salsa = "0.17.0-pre.2" +serde = { version = "1.0.160", features = ["derive"] } +serde_json = "1.0.96" +serde_path_to_error = "0.1.11" +serde_with = "1.6.0" +smallvec = { version = "1.10.0", features = ["const_new", "union", "const_generics"] } +smol_str = "0.1.24" +stdx = { git = "https://github.com/rust-lang/rust-analyzer", rev = "2022-09-05" } +strsim = { version = "0.10.0" } +strum = "0.25.0" +strum_macros = "0.25.0" +tempfile = "3.5.0" +test-case = "2.2.2" +text-edit = { git = "https://github.com/rust-lang/rust-analyzer", rev = "2022-09-05" } +text-size = "1.1.0" +threadpool = "1.8.1" +timeout-readwrite = "0.3.3" +toml = "0.5" +tree-sitter = "0.20.10" +# @fb-only: tree-sitter-erlang = { path = "./tree-sitter-erlang" } +tree-sitter-erlang = "0.1.0" # @oss-only +vfs = { git = "https://github.com/rust-lang/rust-analyzer", rev = "2022-09-05" } +vfs-notify = { git = "https://github.com/rust-lang/rust-analyzer", rev = "2022-09-05" } +walkdir = "2.3.3" +xshell = "0.2.3" diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000000..b93be90515 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Meta Platforms, Inc. and affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000000..1ba8166baf --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +# Erlang Language Platform (ELP) + +## Description + +ELP integrates Erlang into modern IDEs via the language server protocol. + +ELP was inspired by [rust-analyzer](https://github.com/rust-lang/rust-analyzer). + +## Terms of Use + +You are free to copy, modify, and distribute ELP with attribution under the +terms of the Apache-2.0 and MIT licences. See [LICENCE-APACHE](./LICENCE-APACHE) and +[LICENCE-MIT](./LICENSE-MIT) for details. + +## Prerequisites + +### VS Code +1. [Install VS Code](https://www.internalfb.com/intern/wiki/Vscode/Getting_Started/First_Run_Launch_Login_VS_Code/#installation) +1. Install [Erlang LS @ Meta] (not yet published on app store) + +### Emacs +1. Download the appropriate elp executable from https://github.com/WhatsApp/erlang-language-platform/releases, and make sure it is on your `$PATH`. +1. Add the following to your emacs init file + +```elisp +(use-package lsp-mode + :custom + (lsp-semantic-tokens-enable t) + + :config + ;; Enable LSP automatically for Erlang files + (add-hook 'erlang-mode-hook #'lsp) + + ;; ELP, added as priority 0 (> -1) so takes priority over the built-in one + (lsp-register-client + (make-lsp-client :new-connection (lsp-stdio-connection '("elp" "server")) + :major-modes '(erlang-mode) + :priority 0 + :server-id 'erlang-language-platform)) + ) +``` + +## How to use ELP + +### Using it with rebar3 projects + +1. Use OTP 25 +2. Download the `elp` binary for your system from https://github.com/WhatsApp/erlang-language-platform/releases + + > On Mac you will probably get the following message when trying to run the executable the first time: "elp cannot be opened because the developer cannot be verified.". + To solve this, go to Preferences > Security and Privacy and add an exception. Alternatively, you can build elp from source. + +3. Add `eqwalizer_support` dependency and `eqwalizer_rebar3` plugin + to your rebar3 project definition (see below) +4. From the project directory run: + - `elp eqwalize ` to type-check a single module + - `elp eqwalize-all` to type-check all `src` modules in the project + + +Adding `eqwalizer_support` and `eqwalizer_rebar3`: + +``` +{deps, [ + {eqwalizer_support, + {git_subdir, + "https://github.com/whatsapp/eqwalizer.git", + {branch, "main"}, + "eqwalizer_support"}} +]}. + +{project_plugins, [ + {eqwalizer_rebar3, + {git_subdir, + "https://github.com/whatsapp/eqwalizer.git", + {branch, "main"}, + "eqwalizer_rebar3"}} +]}. +``` + +## References + +* [rust-analyzer](https://github.com/rust-lang/rust-analyzer) + +## Contributing + +* [CONTRIBUTING.md]: Provides an overview of how to contribute changes to ELP (e.g., diffs, testing, etc) + +## License + +erlang-language-platform is dual-licensed +* [Apache](./LICENSE-APACHE). +* [MIT](./LICENSE-MIT). diff --git a/bench_runner/example_bench/benches/main.rs b/bench_runner/example_bench/benches/main.rs new file mode 100644 index 0000000000..61f60c447c --- /dev/null +++ b/bench_runner/example_bench/benches/main.rs @@ -0,0 +1,59 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::thread; +use std::time; + +use criterion::criterion_group; +use criterion::criterion_main; +use criterion::BenchmarkId; +use criterion::Criterion; + +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..2e2f81829e --- /dev/null +++ b/bench_runner/runner/main.rs @@ -0,0 +1,15 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::env; + +fn main() { + let args: Vec = env::args().collect(); + println!("ARGS: {:?}", args); +} diff --git a/crates/ai/Cargo.toml b/crates/ai/Cargo.toml new file mode 100644 index 0000000000..bef4832283 --- /dev/null +++ b/crates/ai/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "elp_ai" +edition.workspace = true +version.workspace = true + +[lib] +doctest = false + +[dependencies] +anyhow.workspace = true +crossbeam-channel.workspace = true +log.workspace = true +stdx.workspace = true +jod-thread.workspace = true +tempfile.workspace = true + +[dev-dependencies] diff --git a/crates/ai/src/lib.rs b/crates/ai/src/lib.rs new file mode 100644 index 0000000000..01d563d33b --- /dev/null +++ b/crates/ai/src/lib.rs @@ -0,0 +1,182 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::io::BufRead; +use std::io::BufReader; +use std::io::BufWriter; +use std::io::Read; +use std::io::Write; +use std::path::Path; +use std::process::Child; +use std::process::ChildStdin; +use std::process::ChildStdout; +use std::process::Command; +use std::process::Stdio; +use std::sync::Arc; + +use anyhow::Result; +use crossbeam_channel::bounded; +use crossbeam_channel::unbounded; +use crossbeam_channel::Receiver; +use crossbeam_channel::Sender; +use jod_thread::JoinHandle; +use stdx::JodChild; +use tempfile::TempPath; + +// @fb-only: mod meta_only; + +#[derive(Debug)] +struct SharedState { + _thread_for_drop: JoinHandle, + _child_for_drop: JodChild, + pub(crate) _file_for_drop: Option, +} + +#[derive(Clone, Debug)] +pub struct Connection { + sender: Sender, + pub(crate) _for_drop: Arc, +} + +#[derive(Debug, Clone)] +enum Request { + Complete(String, Sender>), +} + +pub struct AiCompletion { + connection: Option, +} + +pub type CompletionReceiver = Receiver>; + +impl AiCompletion { + pub fn disabled() -> AiCompletion { + AiCompletion { connection: None } + } + + pub fn complete(&mut self, code: String) -> CompletionReceiver { + if let Some(connection) = &self.connection { + let (sender, receiver) = bounded(0); + let msg = Request::Complete(code, sender); + match connection.sender.try_send(msg) { + Ok(()) => { + return receiver; + } + Err(_err) => { + // failed to send - connection was dropped, disable ai completions + log::warn!("disabling ai completions"); + self.connection = None + } + } + } + + always(None) + } +} + +pub fn always(value: T) -> Receiver { + let (sender, receiver) = unbounded(); + sender.send(value).unwrap(); + receiver +} + +impl Connection { + pub fn spawn(path: &Path) -> Result { + let mut cmd = Command::new(path); + cmd.stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()); + + let mut proc = cmd.spawn()?; + + let (sender, thread) = stdio_transport(&mut proc); + + Ok(Connection { + sender, + _for_drop: Arc::new(SharedState { + _file_for_drop: None, + _child_for_drop: JodChild(proc), + _thread_for_drop: thread, + }), + }) + } +} + +fn stdio_transport(proc: &mut Child) -> (Sender, JoinHandle) { + let instream = BufWriter::new(proc.stdin.take().unwrap()); + let mut outstream = BufReader::new(proc.stdout.take().unwrap()); + + let (sender, receiver) = bounded::(0); + let handle = jod_thread::spawn( + move || match thread_run(receiver, instream, &mut outstream) { + Result::Ok(()) => {} + Err(err) => { + let mut buf = vec![0; 512]; + let _ = outstream.read(&mut buf); + let remaining = String::from_utf8_lossy(&buf); + log::error!( + "thread failed with {}\nremaining data:\n\n{}", + err, + remaining + ); + } + }, + ); + + (sender, handle) +} + +fn thread_run( + receiver: Receiver, + mut instream: BufWriter, + outstream: &mut BufReader, +) -> Result<()> { + let mut line_buf = String::new(); + + while let Ok(request) = receiver.recv() { + match request { + Request::Complete(string, sender) => { + writeln!(instream, "COMPLETE {}", string.len())?; + instream.write_all(string.as_bytes())?; + instream.flush()?; + + line_buf.clear(); + outstream.read_line(&mut line_buf)?; + let parts = line_buf.split_ascii_whitespace().collect::>(); + if parts.is_empty() { + break; + } + + match parts[0] { + "OK" => { + let value = parts.get(1).map(ToString::to_string).unwrap_or_default(); + log::debug!("received result {}", value); + let _ = sender.send(Some(value)); + } + "NO_RESULT" => { + log::debug!("received no result"); + let _ = sender.send(None); + } + "ERROR" => { + log::error!("AI server error: {}", line_buf); + let _ = sender.send(None); + break; + } + _ => { + log::error!("Unrecognised message: {}", line_buf); + let _ = sender.send(None); + break; + } + } + } + } + } + + Ok(()) +} diff --git a/crates/base_db/Cargo.toml b/crates/base_db/Cargo.toml new file mode 100644 index 0000000000..aa88b18611 --- /dev/null +++ b/crates/base_db/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "elp_base_db" +edition.workspace = true +version.workspace = true + +[lib] +doctest = false + +[dependencies] +elp_project_model.workspace = true +elp_syntax.workspace = true + +dissimilar.workspace = true +eetf.workspace = true +either.workspace = true +fxhash.workspace = true +lazy_static.workspace = true +log.workspace = true +paths.workspace = true +profile.workspace = true +regex.workspace = true +salsa.workspace = true +stdx.workspace = true +vfs.workspace = true + +[dev-dependencies] +expect-test.workspace = true diff --git a/crates/base_db/src/change.rs b/crates/base_db/src/change.rs new file mode 100644 index 0000000000..110a3f7c4b --- /dev/null +++ b/crates/base_db/src/change.rs @@ -0,0 +1,89 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! Defines a unit of change that can applied to the database to get the next +//! state. Changes are transactional. + +use std::fmt; +use std::sync::Arc; + +use vfs::FileId; + +use crate::input::AppStructure; +use crate::SourceDatabaseExt; +use crate::SourceRoot; +use crate::SourceRootId; + +/// Encapsulate a bunch of raw `.set` calls on the database. +#[derive(Clone, Default)] +pub struct Change { + pub roots: Option>, + pub files_changed: Vec<(FileId, Option>)>, + pub app_structure: Option, +} + +impl fmt::Debug for Change { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + let mut d = fmt.debug_struct("Change"); + if let Some(roots) = &self.roots { + d.field("roots", roots); + } + if !self.files_changed.is_empty() { + d.field("files_changed", &self.files_changed.len()); + } + if self.app_structure.is_some() { + d.field("app_structure", &self.app_structure); + } + d.finish() + } +} + +impl Change { + pub fn new() -> Change { + Change::default() + } + + pub fn set_roots(&mut self, roots: Vec) { + self.roots = Some(roots); + } + + pub fn change_file(&mut self, file_id: FileId, new_text: Option>) { + self.files_changed.push((file_id, new_text)) + } + + pub fn set_app_structure(&mut self, a: AppStructure) { + self.app_structure = Some(a); + } + + pub fn apply(self, db: &mut dyn SourceDatabaseExt) -> Vec { + let _p = profile::span("RootDatabase::apply_change"); + if let Some(roots) = self.roots { + for (idx, root) in roots.into_iter().enumerate() { + let root_id = SourceRootId(idx as u32); + for file_id in root.iter() { + db.set_file_source_root(file_id, root_id); + } + db.set_source_root(root_id, Arc::new(root)); + } + } + + if let Some(set_app_structure) = self.app_structure { + set_app_structure.apply(db); + } + + let mut res = vec![]; + for (file_id, text) in self.files_changed { + // XXX: can't actually remove the file, just reset the text + let text = text.unwrap_or_default(); + db.set_file_text(file_id, text); + res.push(file_id); + } + res + } +} diff --git a/crates/base_db/src/fixture.rs b/crates/base_db/src/fixture.rs new file mode 100644 index 0000000000..d31d7c552a --- /dev/null +++ b/crates/base_db/src/fixture.rs @@ -0,0 +1,1233 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! A set of high-level utility fixture methods to use in tests. + +// Based on rust-analyzer base_db::fixture + +use std::collections::hash_map::Entry; +use std::collections::BTreeMap; +use std::convert::TryFrom; +use std::convert::TryInto; +use std::mem; +use std::sync::Arc; + +use elp_project_model::otp::Otp; +use elp_project_model::rebar::RebarProject; +use elp_project_model::AppName; +use elp_project_model::Project; +use elp_project_model::ProjectAppData; +use elp_project_model::ProjectBuildData; +use elp_syntax::TextRange; +use elp_syntax::TextSize; +use fxhash::FxHashMap; +use lazy_static::lazy_static; +use paths::AbsPathBuf; +use regex::Regex; +use vfs::file_set::FileSet; +use vfs::FileId; +use vfs::VfsPath; + +use crate::change::Change; +use crate::input::IncludeOtp; +use crate::test_fixture::Fixture; +use crate::FilePosition; +use crate::FileRange; +use crate::ProjectApps; +use crate::SourceDatabaseExt; +use crate::SourceRoot; + +pub trait WithFixture: Default + SourceDatabaseExt + 'static { + fn with_single_file(fixture: &str) -> (Self, FileId) { + let (db, fixture) = Self::with_fixture(fixture); + assert_eq!(fixture.files.len(), 1); + (db, fixture.files[0]) + } + + fn with_many_files(fixture: &str) -> (Self, Vec) { + let (db, fixture) = Self::with_fixture(fixture); + assert!(fixture.file_position.is_none()); + (db, fixture.files) + } + + fn with_position(fixture: &str) -> (Self, FilePosition) { + let (db, fixture) = Self::with_fixture(fixture); + (db, fixture.position()) + } + + fn with_range(fixture: &str) -> (Self, FileRange) { + let (db, fixture) = Self::with_fixture(fixture); + (db, fixture.range()) + } + + fn with_range_or_offset(fixture: &str) -> (Self, FileId, RangeOrOffset) { + let (db, fixture) = Self::with_fixture(fixture); + let (file_id, range_or_offset) = fixture + .file_position + .expect("Could not find file position in fixture. Did you forget to add an `~`?"); + (db, file_id, range_or_offset) + } + + fn with_fixture(fixture: &str) -> (Self, ChangeFixture) { + let (fixture, change) = ChangeFixture::parse(fixture); + let mut db = Self::default(); + change.apply(&mut db); + (db, fixture) + } +} + +impl WithFixture for DB {} + +#[derive(Clone, Debug)] +pub struct ChangeFixture { + pub file_position: Option<(FileId, RangeOrOffset)>, + pub files: Vec, +} + +impl ChangeFixture { + fn parse(text_fixture: &str) -> (ChangeFixture, Change) { + let fixture = Fixture::parse(text_fixture); + let mut change = Change::new(); + + let mut files = Vec::new(); + let source_root_prefix = "/".to_string(); + let mut file_id = FileId(0); + + let mut file_position = None; + let mut app_map = AppMap::default(); + let mut otp: Option = None; + let mut app_files = SourceRootMap::default(); + + for entry in fixture { + let (text, file_pos) = Self::get_text_and_pos(&entry.text, file_id); + if file_pos.is_some() { + assert!(file_position.is_none()); + file_position = file_pos; + } + + assert!(entry.path.starts_with(&source_root_prefix)); + + let app_name = if let Some(otp) = &entry.otp { + otp.apps[0].name.clone() + } else { + entry.app_data.as_ref().unwrap().name.clone() + }; + + if let Some(app_data) = entry.app_data { + app_map.combine(app_data); + } + + if let Some(otp_extra) = entry.otp { + if let Some(otp) = &mut otp { + otp.combine(otp_extra); + } else { + otp = Some(otp_extra) + } + }; + + change.change_file(file_id, Some(Arc::new(text))); + + let path = VfsPath::new_real_path(entry.path); + app_files.insert(app_name, file_id, path); + files.push(file_id); + + file_id.0 += 1; + } + + let otp = otp.unwrap_or_else(|| Otp { + // We only care about the otp lib_dir for the tests + lib_dir: AbsPathBuf::assert("/".into()), + apps: Default::default(), + }); + let root = AbsPathBuf::assert("/".into()); + let apps = app_map.app_map.values().cloned().collect(); + let rebar_project = RebarProject::new(apps, vec![], root, Default::default(), &otp.lib_dir); + let mut project = Project::empty(otp); + project.project_build_data = ProjectBuildData::Rebar(rebar_project); + let projects = [project]; + + let project_apps = ProjectApps::new(&projects, IncludeOtp::Yes); + change.set_app_structure(project_apps.app_structure()); + + let mut roots = Vec::new(); + for (_app, file_set) in app_files.app_map.iter_mut() { + let root = SourceRoot::new(mem::take(file_set)); + roots.push(root); + } + change.set_roots(roots); + + ( + ChangeFixture { + file_position, + files, + }, + change, + ) + } + + pub fn annotations(&self, db: &dyn SourceDatabaseExt) -> Vec<(FileRange, String)> { + self.files + .iter() + .flat_map(|&file_id| { + let text = SourceDatabaseExt::file_text(db, file_id); + extract_annotations(&text) + .into_iter() + .map(move |(range, data)| (FileRange { file_id, range }, data)) + }) + .collect() + } + + pub fn position(&self) -> FilePosition { + let (file_id, range_or_offset) = self + .file_position + .expect("Could not find file position in fixture. Did you forget to add a `~`?"); + FilePosition { + file_id, + offset: range_or_offset.expect_offset(), + } + } + + pub fn range(&self) -> FileRange { + let (file_id, range_or_offset) = self + .file_position + .expect("Could not find file position in fixture. Did you forget to add a `~`?"); + FileRange { + file_id, + range: range_or_offset.into(), + } + } + + fn get_text_and_pos( + entry_text: &str, + file_id: FileId, + ) -> (String, Option<(FileId, RangeOrOffset)>) { + if entry_text.contains(CURSOR_MARKER) { + if entry_text.contains(ESCAPED_CURSOR_MARKER) { + ( + entry_text.replace(ESCAPED_CURSOR_MARKER, CURSOR_MARKER), + None, + ) + } else { + let (range_or_offset, text) = extract_range_or_offset(entry_text); + let file_position = Some((file_id, range_or_offset)); + (text, file_position) + } + } else { + (entry_text.to_string(), None) + } + } +} + +#[derive(Debug, Clone, Default)] +pub struct AppMap { + app_map: FxHashMap, +} + +impl AppMap { + fn combine(&mut self, other: ProjectAppData) { + match self.app_map.entry(other.name.clone()) { + Entry::Occupied(mut occupied) => { + occupied.get_mut().combine(other); + } + Entry::Vacant(vacant) => { + vacant.insert(other); + } + } + } +} + +#[derive(Debug, Clone, Default)] +pub struct SourceRootMap { + app_map: FxHashMap, +} + +impl SourceRootMap { + fn insert(&mut self, app_name: AppName, file_id: FileId, path: VfsPath) { + self.app_map + .entry(app_name) + .or_default() + .insert(file_id, path); + } +} + +// --------------------------------------------------------------------- + +/// Infallible version of `try_extract_offset()`. +pub fn extract_offset(text: &str) -> (TextSize, String) { + match try_extract_marker(CURSOR_MARKER, text) { + None => panic!("text should contain cursor marker"), + Some(result) => result, + } +} + +/// Infallible version of `try_extract_offset()`. +pub fn extract_marker_offset(marker: &str, text: &str) -> (TextSize, String) { + match try_extract_marker(marker, text) { + None => panic!("text should contain marker '{}'", marker), + Some(result) => result, + } +} + +pub const CURSOR_MARKER: &str = "~"; +pub const ESCAPED_CURSOR_MARKER: &str = "\\~"; + +/// Returns the offset of the first occurrence of `^` marker and the copy of `text` +/// without the marker. +fn try_extract_offset(text: &str) -> Option<(TextSize, String)> { + try_extract_marker(CURSOR_MARKER, text) +} + +fn try_extract_marker(marker: &str, text: &str) -> Option<(TextSize, String)> { + let cursor_pos = text.find(marker)?; + let mut new_text = String::with_capacity(text.len() - marker.len()); + new_text.push_str(&text[..cursor_pos]); + new_text.push_str(&text[cursor_pos + marker.len()..]); + let cursor_pos = TextSize::from(cursor_pos as u32); + Some((cursor_pos, new_text)) +} + +/// Infallible version of `try_extract_range()`. +pub fn extract_range(text: &str) -> (TextRange, String) { + match try_extract_range(text) { + None => panic!("text should contain cursor marker"), + Some(result) => result, + } +} + +/// Returns `TextRange` between the first two markers `^...^` and the copy +/// of `text` without both of these markers. +fn try_extract_range(text: &str) -> Option<(TextRange, String)> { + let (start, text) = try_extract_offset(text)?; + let (end, text) = try_extract_offset(&text)?; + Some((TextRange::new(start, end), text)) +} + +/// Extracts ranges, marked with ` ` pairs from the `text` +pub fn extract_tags(mut text: &str, tag: &str) -> (Vec<(TextRange, Option)>, String) { + let open = format!("<{tag}"); + let close = format!(""); + let mut ranges = Vec::new(); + let mut res = String::new(); + let mut stack = Vec::new(); + loop { + match text.find('<') { + None => { + res.push_str(text); + break; + } + Some(i) => { + res.push_str(&text[..i]); + text = &text[i..]; + if text.starts_with(&open) { + let close_open = text.find('>').unwrap(); + let attr = text[open.len()..close_open].trim(); + let attr = if attr.is_empty() { + None + } else { + Some(attr.to_string()) + }; + text = &text[close_open + '>'.len_utf8()..]; + let from = TextSize::of(&res); + stack.push((from, attr)); + } else if text.starts_with(&close) { + text = &text[close.len()..]; + let (from, attr) = stack.pop().unwrap_or_else(|| panic!("unmatched ")); + let to = TextSize::of(&res); + ranges.push((TextRange::new(from, to), attr)); + } else { + res.push('<'); + text = &text['<'.len_utf8()..]; + } + } + } + } + assert!(stack.is_empty(), "unmatched <{}>", tag); + ranges.sort_by_key(|r| (r.0.start(), r.0.end())); + (ranges, res) +} + +#[test] +fn test_extract_tags_1() { + let (tags, text) = extract_tags(r#"foo() -> ok."#, "tag"); + let actual = tags + .into_iter() + .map(|(range, attr)| (&text[range], attr)) + .collect::>(); + assert_eq!(actual, vec![("foo() -> ok.", Some("region".into()))]); +} + +#[test] +fn test_extract_tags_2() { + let (tags, text) = extract_tags( + r#"bar() -> ok.\nfoo() -> ok.\nbaz() -> ok."#, + "tag", + ); + let actual = tags + .into_iter() + .map(|(range, attr)| (&text[range], attr)) + .collect::>(); + assert_eq!(actual, vec![("foo() -> ok.", Some("region".into()))]); +} + +#[derive(Clone, Copy, Debug)] +pub enum RangeOrOffset { + Range(TextRange), + Offset(TextSize), +} + +impl RangeOrOffset { + pub fn expect_offset(self) -> TextSize { + match self { + RangeOrOffset::Offset(it) => it, + RangeOrOffset::Range(_) => panic!("expected an offset but got a range instead"), + } + } + pub fn expect_range(self) -> TextRange { + match self { + RangeOrOffset::Range(it) => it, + RangeOrOffset::Offset(_) => panic!("expected a range but got an offset"), + } + } +} + +impl From for TextRange { + fn from(selection: RangeOrOffset) -> Self { + match selection { + RangeOrOffset::Range(it) => it, + RangeOrOffset::Offset(it) => TextRange::empty(it), + } + } +} + +/// Extracts `TextRange` or `TextSize` depending on the amount of `^` markers +/// found in `text`. +/// +/// # Panics +/// Panics if no `^` marker is present in the `text`. +pub fn extract_range_or_offset(text: &str) -> (RangeOrOffset, String) { + if let Some((range, text)) = try_extract_range(text) { + return (RangeOrOffset::Range(range), text); + } + let (offset, text) = extract_offset(text); + (RangeOrOffset::Offset(offset), text) +} + +// --------------------------------------------------------------------- + +/// Extracts `%%^^^ some text` annotations. +/// +/// A run of `^^^` can be arbitrary long and points to the corresponding range +/// in the line above. +/// +/// The `%% ^file text` syntax can be used to attach `text` to the entirety of +/// the file. +/// +/// Multiline string values are supported: +/// +/// %% ^^^ first line +/// %% | second line +/// +/// Annotations point to the last line that actually was long enough for the +/// range, not counting annotations themselves. So overlapping annotations are +/// possible: +/// ```no_run +/// %% stuff other stuff +/// %% ^^ 'st' +/// %% ^^^^^ 'stuff' +/// %% ^^^^^^^^^^^ 'other stuff' +/// ``` +pub fn extract_annotations(text: &str) -> Vec<(TextRange, String)> { + let mut res = Vec::new(); + // map from line length to beginning of last line that had that length + let mut line_start_map = BTreeMap::new(); + let mut line_start: TextSize = 0.into(); + let mut prev_line_annotations: Vec<(TextSize, usize)> = Vec::new(); + for (idx, line) in text.split_inclusive('\n').enumerate() { + if idx == 0 && line.starts_with(TOP_OF_FILE_MARKER) { + // First line, look for header marker + if let Some(anno) = line.strip_prefix(TOP_OF_FILE_MARKER) { + res.push((*TOP_OF_FILE_RANGE, anno.trim_end().to_string())); + } + } else { + if line.contains(TOP_OF_FILE_MARKER) { + assert!( + false, + "Annotation line {} is invalid here. \ + The top of file marker '{}' can only appear first in the file on the left margin.\n\ + The offending line: {:?}", + idx, TOP_OF_FILE_MARKER, line + ); + } + } + let mut this_line_annotations = Vec::new(); + let line_length = if let Some((prefix, suffix)) = line.split_once("%%") { + let ss_len = TextSize::of("%%"); + let annotation_offset = TextSize::of(prefix) + ss_len; + for annotation in extract_line_annotations(suffix.trim_end_matches('\n')) { + match annotation { + LineAnnotation::Annotation { + mut range, + content, + file, + } => { + range += annotation_offset; + this_line_annotations.push((range.end(), res.len())); + let range = if file { + TextRange::up_to(TextSize::of(text)) + } else { + let zero: TextSize = 0.into(); + let line_start = line_start_map + .range(range.end()..) + .next() + .unwrap_or((&zero, &zero)); + + range + line_start.1 + }; + res.push((range, content)) + } + LineAnnotation::Continuation { + mut offset, + content, + } => { + offset += annotation_offset; + this_line_annotations.push((offset, res.len() - 1)); + let &(_, idx) = prev_line_annotations + .iter() + .find(|&&(off, _idx)| off == offset) + .unwrap(); + res[idx].1.push('\n'); + res[idx].1.push_str(&content); + // res[idx].1.push('\n'); + } + } + } + annotation_offset + } else { + TextSize::of(line) + }; + + line_start_map = line_start_map.split_off(&line_length); + line_start_map.insert(line_length, line_start); + + line_start += TextSize::of(line); + + prev_line_annotations = this_line_annotations; + } + + res +} + +lazy_static! { + static ref TOP_OF_FILE_RANGE: TextRange = TextRange::new(0.into(), 0.into()); +} + +const TOP_OF_FILE_MARKER: &str = "%% <<< "; + +/// Return a copy of the input text, with all `%% ^^^ 💡 some text` annotations removed +pub fn remove_annotations(marker: Option<&str>, text: &str) -> String { + let mut lines = Vec::new(); + for line in text.split('\n') { + if !contains_annotation(line) { + if let Some(marker) = marker { + if let Some((_pos, clean_line)) = try_extract_marker(marker, line) { + lines.push(clean_line) + } else { + lines.push(line.to_string()) + } + } else { + lines.push(line.to_string()) + } + } + } + lines.join("\n") +} + +/// Check if the given line contains a `%% ^^^ 💡 some text` annotation +pub fn contains_annotation(line: &str) -> bool { + lazy_static! { + static ref RE: Regex = Regex::new(r"^\s*%%\s+(\^)* 💡.*$").unwrap(); + } + RE.is_match(line) +} + +#[derive(Debug)] +enum LineAnnotation { + Annotation { + range: TextRange, + content: String, + file: bool, + }, + Continuation { + offset: TextSize, + content: String, + }, +} + +fn extract_line_annotations(mut line: &str) -> Vec { + let mut res = Vec::new(); + let mut offset: TextSize = 0.into(); + let marker: fn(char) -> bool = if line.contains('^') { + |c| c == '^' + } else { + |c| c == '|' + }; + while let Some(idx) = line.find(marker) { + offset += TextSize::try_from(idx).unwrap(); + line = &line[idx..]; + + let mut len = line.chars().take_while(|&it| it == '^').count(); + let mut continuation = false; + if len == 0 { + assert!(line.starts_with('|')); + continuation = true; + len = 1; + } + let range = TextRange::at(offset, len.try_into().unwrap()); + let next = line[len..].find(marker).map_or(line.len(), |it| it + len); + let mut content = &line[len..][..next - len]; + + let mut file = false; + if !continuation && content.starts_with("file") { + file = true; + content = &content["file".len()..] + } + + let content = content.trim().to_string(); + + let annotation = if continuation { + LineAnnotation::Continuation { + offset: range.end(), + content, + } + } else { + LineAnnotation::Annotation { + range, + content, + file, + } + }; + res.push(annotation); + + line = &line[next..]; + offset += TextSize::try_from(next).unwrap(); + } + + res +} + +#[cfg(test)] +mod tests { + use expect_test::expect; + + use super::ChangeFixture; + use crate::fixture::extract_annotations; + use crate::fixture::remove_annotations; + + #[test] + fn test_extract_annotations_1() { + let text = stdx::trim_indent( + r#" +fn main() { + let (x, y) = (9, 2); + %%^ def ^ def + zoo + 1 +} %%^^^ type: + %% | i32 + +%% ^file + "#, + ); + let res = extract_annotations(&text) + .into_iter() + .map(|(range, ann)| (&text[range], ann)) + .collect::>(); + + assert_eq!( + res[..3], + [ + ("x", "def".into()), + ("y", "def".into()), + ("zoo", "type:\ni32".into()) + ] + ); + assert_eq!(res[3].0.len(), 115); + } + + #[test] + fn test_extract_annotations_2() { + let text = stdx::trim_indent( + r#" +fn main() { + (x, y); + %%^ a + %% ^ b + %%^^^^^^^^ c +}"#, + ); + let res = extract_annotations(&text) + .into_iter() + .map(|(range, ann)| (&text[range], ann)) + .collect::>(); + + assert_eq!( + res, + [ + ("x", "a".into()), + ("y", "b".into()), + ("(x, y)", "c".into()) + ] + ); + } + + #[test] + fn test_extract_annotations_3() { + let text = stdx::trim_indent( + r#" +-module(foo). +bar() -> ?FOO. + %% ^^^ error: unresolved macro `FOO` + +"#, + ); + let res = extract_annotations(&text) + .into_iter() + .map(|(range, ann)| (format!("{:?}", range), &text[range], ann)) + .collect::>(); + + assert_eq!( + res, + [ + ( + "24..27".into(), + "FOO", + "error: unresolved macro `FOO`".into() + ), + // TODO: something weird here, this range does not tie in + // to what the diagnostic reports. But it shows up correcly in VsCode. + // No time to look more deeply now. + // ("25..28".into(), "FOO", "error: unresolved macro `FOO`".into()), + ] + ); + } + + #[test] + fn extract_annotation_top_of_file_no_location() { + let text = stdx::trim_indent( + r#" + %% <<< top of file, location zero as it is not associated with anything particular + -module(main). + main() -> ok."#, + ); + let res = extract_annotations(&text); + + expect![[r#" + [ + ( + 0..0, + "top of file, location zero as it is not associated with anything particular", + ), + ] + "#]] + .assert_debug_eq(&res); + } + + #[test] + #[should_panic] + fn extract_annotations_top_of_file_syntax_only_if_at_top_of_file() { + let text = stdx::trim_indent( + r#" + -module(main). + %% <<< NOT top of file, no annotation + main() -> ok."#, + ); + let res = extract_annotations(&text); + + expect![[r#" + [] + "#]] + .assert_debug_eq(&res); + } + + #[test] + fn unresolved_macro_diag_include_dir() { + // Test the scenario where the include file is referenced + // relative to a project include directory + let (_fixture, change) = ChangeFixture::parse( + r#" +//- /opt/lib/comp-1.3/include/comp.hrl otp_app:/opt/lib/comp-1.3 +-define(COMP,3). +//- /include/foo.hrl include_path:/include app:foo-app +-define(FOO,3). +//- /src/foo.erl +-module(foo). +-include("foo.hrl"). +bar() -> ?FOO. +"#, + ); + + expect![[r#" + Some( + AppStructure { + app_map: { + SourceRootId( + 0, + ): Some( + AppData { + project_id: ProjectId( + 0, + ), + name: AppName( + "test-fixture", + ), + dir: AbsPathBuf( + "/", + ), + include_path: [ + AbsPathBuf( + "/src", + ), + AbsPathBuf( + "/opt/lib", + ), + ], + src_path: [ + AbsPathBuf( + "/src", + ), + ], + extra_src_dirs: [], + macros: [], + parse_transforms: [], + app_type: App, + ebin_path: Some( + AbsPathBuf( + "/ebin", + ), + ), + }, + ), + SourceRootId( + 2, + ): Some( + AppData { + project_id: ProjectId( + 1, + ), + name: AppName( + "comp", + ), + dir: AbsPathBuf( + "/opt/lib/comp-1.3", + ), + include_path: [ + AbsPathBuf( + "/opt/lib/comp-1.3/include", + ), + AbsPathBuf( + "/opt/lib/comp-1.3/src", + ), + AbsPathBuf( + "/opt/lib", + ), + ], + src_path: [ + AbsPathBuf( + "/opt/lib/comp-1.3/src", + ), + ], + extra_src_dirs: [], + macros: [], + parse_transforms: [], + app_type: Otp, + ebin_path: Some( + AbsPathBuf( + "/opt/lib/comp-1.3/ebin", + ), + ), + }, + ), + SourceRootId( + 1, + ): Some( + AppData { + project_id: ProjectId( + 0, + ), + name: AppName( + "foo-app", + ), + dir: AbsPathBuf( + "/", + ), + include_path: [ + AbsPathBuf( + "/include", + ), + AbsPathBuf( + "/opt/lib", + ), + ], + src_path: [], + extra_src_dirs: [], + macros: [], + parse_transforms: [], + app_type: App, + ebin_path: Some( + AbsPathBuf( + "/ebin", + ), + ), + }, + ), + SourceRootId( + 3, + ): None, + }, + project_map: { + ProjectId( + 0, + ): ProjectData { + source_roots: [ + SourceRootId( + 0, + ), + SourceRootId( + 1, + ), + ], + root_dir: AbsPathBuf( + "/", + ), + deps_ebins: [], + build_info_path: None, + otp_project_id: Some( + ProjectId( + 1, + ), + ), + app_roots: AppRoots { + otp: Some( + AppRoots { + otp: None, + app_map: { + AppName( + "comp", + ): SourceRootId( + 2, + ), + }, + }, + ), + app_map: { + AppName( + "test-fixture", + ): SourceRootId( + 0, + ), + AppName( + "foo-app", + ): SourceRootId( + 1, + ), + }, + }, + eqwalizer_config: EqwalizerConfig { + enable_all: false, + }, + }, + ProjectId( + 1, + ): ProjectData { + source_roots: [ + SourceRootId( + 2, + ), + ], + root_dir: AbsPathBuf( + "/opt/lib", + ), + deps_ebins: [], + build_info_path: None, + otp_project_id: Some( + ProjectId( + 1, + ), + ), + app_roots: AppRoots { + otp: None, + app_map: { + AppName( + "comp", + ): SourceRootId( + 2, + ), + }, + }, + eqwalizer_config: EqwalizerConfig { + enable_all: false, + }, + }, + }, + }, + )"#]] + .assert_eq(format!("{:#?}", change.app_structure).as_str()); + } + + #[test] + fn unresolved_macro_diag_include_dir2() { + // Test the scenario where the include file is referenced + // relative to a project include directory + let (_fixture, change) = ChangeFixture::parse( + r#" +//- /extra/include/bar.hrl include_path:/extra/include +-define(BAR,4). +//- /include/foo.hrl include_path:/include +-define(FOO,3). +//- /src/foo.erl +-module(foo). +-include("foo.hrl"). +-include("bar.hrl"). +bar() -> ?FOO. +foo() -> ?BAR. +"#, + ); + + expect![[r#" + Some( + AppStructure { + app_map: { + SourceRootId( + 0, + ): Some( + AppData { + project_id: ProjectId( + 0, + ), + name: AppName( + "test-fixture", + ), + dir: AbsPathBuf( + "/extra", + ), + include_path: [ + AbsPathBuf( + "/", + ), + AbsPathBuf( + "/extra/include", + ), + AbsPathBuf( + "/include", + ), + AbsPathBuf( + "/src", + ), + AbsPathBuf( + "/", + ), + ], + src_path: [ + AbsPathBuf( + "/src", + ), + ], + extra_src_dirs: [], + macros: [], + parse_transforms: [], + app_type: App, + ebin_path: Some( + AbsPathBuf( + "/extra/ebin", + ), + ), + }, + ), + SourceRootId( + 1, + ): None, + }, + project_map: { + ProjectId( + 0, + ): ProjectData { + source_roots: [ + SourceRootId( + 0, + ), + ], + root_dir: AbsPathBuf( + "/", + ), + deps_ebins: [], + build_info_path: None, + otp_project_id: Some( + ProjectId( + 1, + ), + ), + app_roots: AppRoots { + otp: None, + app_map: { + AppName( + "test-fixture", + ): SourceRootId( + 0, + ), + }, + }, + eqwalizer_config: EqwalizerConfig { + enable_all: false, + }, + }, + ProjectId( + 1, + ): ProjectData { + source_roots: [], + root_dir: AbsPathBuf( + "/", + ), + deps_ebins: [], + build_info_path: None, + otp_project_id: Some( + ProjectId( + 1, + ), + ), + app_roots: AppRoots { + otp: None, + app_map: {}, + }, + eqwalizer_config: EqwalizerConfig { + enable_all: false, + }, + }, + }, + }, + )"#]] + .assert_eq(format!("{:#?}", change.app_structure).as_str()); + } + + #[test] + fn test_remove_annotations() { + let text = stdx::trim_indent( + r#" +-module(my_module). +-export([meaning_of_life/0]). +meaning_of_life() -> + Thoughts = thinking(), + %% ^^^^^^^^ 💡 L1268: variable 'Thoughts' is unused + 42. +"#, + ); + expect![[r#" + -module(my_module). + -export([meaning_of_life/0]). + meaning_of_life() -> + Thoughts = thinking(), + 42. + "#]] + .assert_eq(remove_annotations(None, &text).as_str()); + } + + #[test] + fn extract_annotations_continuation_1() { + let text = stdx::trim_indent( + r#" + fn main() { + zoo + 1 + } %%^^^ type: + %% | i32 + "#, + ); + let res = extract_annotations(&text) + .into_iter() + .map(|(range, ann)| (&text[range], range, ann)) + .collect::>(); + + expect![[r#" + [ + ( + "zoo", + 16..19, + "type:\ni32", + ), + ] + "#]] + .assert_debug_eq(&res); + } + + #[test] + fn extract_annotations_continuation_2() { + let text = stdx::trim_indent( + r#" + -module(main). + + foo(Node) -> + erlang:spawn(Node, fun() -> ok end). + %% ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ warning: + %% | + %% | Production code blah + %% | more + "#, + ); + let res = extract_annotations(&text) + .into_iter() + .map(|(range, ann)| (&text[range], range, ann)) + .collect::>(); + expect![[r#" + [ + ( + "erlang:spawn(Node, fun() -> ok end)", + 33..68, + "warning:\n\nProduction code blah\nmore", + ), + ] + "#]] + .assert_debug_eq(&res); + } + + #[test] + fn extract_annotations_continuation_3() { + let text = stdx::trim_indent( + r#" + -module(main). + + main() -> + zoo + 1. + %%^^^ type: + %% | i32 + + foo(Node) -> + erlang:spawn(Node, fun() -> ok end). + %% ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ warning: + %% | Production code blah + %% | more + "#, + ); + let res = extract_annotations(&text) + .into_iter() + .map(|(range, ann)| (&text[range], range, ann)) + .collect::>(); + expect![[r#" + [ + ( + "zoo", + 31..34, + "type:\ni32", + ), + ( + "erlang:spawn(Node, fun() -> ok end)", + 86..121, + "warning:\nProduction code blah\nmore", + ), + ] + "#]] + .assert_debug_eq(&res); + } +} diff --git a/crates/base_db/src/input.rs b/crates/base_db/src/input.rs new file mode 100644 index 0000000000..d123e48ad2 --- /dev/null +++ b/crates/base_db/src/input.rs @@ -0,0 +1,365 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::borrow::Borrow; +use std::hash::Hash; +use std::path::Path; +use std::sync::Arc; + +use elp_project_model::buck::EqwalizerConfig; +use elp_project_model::AppName; +use elp_project_model::AppType; +use elp_project_model::Project; +use elp_project_model::ProjectAppData; +use fxhash::FxHashMap; +use paths::RelPath; +use vfs::file_set::FileSet; +use vfs::AbsPathBuf; +use vfs::FileId; +use vfs::VfsPath; + +use crate::SourceDatabaseExt; + +/// Files are grouped into source roots. A source root is a directory on the +/// file systems which is watched for changes. Typically it corresponds to an OTP +/// application. Source roots *might* be nested: in this case, a file belongs to +/// the nearest enclosing source root. Paths to files are always relative to a +/// source root, and ELP does not know the root path of the source root at +/// all. So, a file from one source root can't refer to a file in another source +/// root by path. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct SourceRootId(pub u32); + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SourceRoot { + file_set: FileSet, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum FileSource { + Src, + Extra, +} + +impl SourceRoot { + pub fn new(file_set: FileSet) -> SourceRoot { + SourceRoot { file_set } + } + + pub fn path_for_file(&self, file: &FileId) -> Option<&VfsPath> { + self.file_set.path_for_file(file) + } + + pub fn file_for_path(&self, path: &VfsPath) -> Option { + self.file_set.file_for_path(path).copied() + } + + pub fn relative_path(&self, file: FileId, segment: &str) -> Option { + let base_path = self.path_for_file(&file)?; + self.file_for_path(&base_path.parent()?.join(segment)?) + } + + pub fn iter(&self) -> impl Iterator + '_ { + self.file_set.iter() + } + + pub fn iter_app_files<'a>( + &'a self, + app_data: &'a AppData, + ) -> impl Iterator + 'a { + self.iter().flat_map(move |file_id| { + let path = self.path_for_file(&file_id)?; + if app_data.is_extra_src_file(path) { + Some((file_id, FileSource::Extra, path)) + } else if app_data.is_src_file(path) { + Some((file_id, FileSource::Src, path)) + } else { + None + } + }) + } + + pub fn has_eqwalizer_marker<'a>(&'a self, app_data: &'a AppData) -> bool { + self.iter().any(|file_id| { + self.path_for_file(&file_id) + .iter() + .any(|p| app_data.is_eqwalizer_marker(p)) + }) + } +} + +/// Source roots (apps) are grouped into projects that share some +/// of the configuration +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct ProjectId(pub u32); + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ProjectData { + pub source_roots: Vec, + pub root_dir: AbsPathBuf, + pub deps_ebins: Vec, + pub build_info_path: Option, + pub otp_project_id: Option, + pub app_roots: AppRoots, + pub eqwalizer_config: EqwalizerConfig, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct AppData { + pub project_id: ProjectId, + pub name: AppName, + pub dir: AbsPathBuf, + pub include_path: Vec, + pub src_path: Vec, + pub extra_src_dirs: Vec, + pub macros: Vec, + pub parse_transforms: Vec, + pub app_type: AppType, + pub ebin_path: Option, +} + +impl AppData { + fn is_src_file(&self, path: &VfsPath) -> bool { + if let Some(path) = path.as_path() { + // src_dirs are recursive, check path begins with one + return self + .src_path + .iter() + .any(|src_dir| path.as_ref().starts_with(src_dir)); + } + false + } + + pub(crate) fn is_extra_src_file(&self, path: &VfsPath) -> bool { + if let Some(path) = self.local_file_path(path) { + // extra_src_dirs are not recursive, check parent dir is one + if let Some(parent) = path.as_ref().parent() { + return self + .extra_src_dirs + .iter() + .any(|src_dir| parent == Path::new(src_dir)); + } + } + false + } + + fn is_eqwalizer_marker(&self, path: &VfsPath) -> bool { + if let Some(path) = self.local_file_path(path) { + return path.as_ref() == Path::new(".eqwalizer"); + } + false + } + + fn local_file_path<'a>(&self, path: &'a VfsPath) -> Option<&'a RelPath> { + path.as_path()?.strip_prefix(&self.dir) + } +} + +/// Note that `AppStructure` is build-system agnostic +#[derive(Debug, Clone, Default /* Serialize, Deserialize */)] +pub struct AppStructure { + pub(crate) app_map: FxHashMap>, + pub(crate) project_map: FxHashMap, +} + +impl AppStructure { + pub fn add_app_data(&mut self, source_root_id: SourceRootId, app_data: Option) { + let prev = self.app_map.insert(source_root_id, app_data); + 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()); + } + + /// Set the salsa inputs according to this AppStructure + pub fn apply(self, db: &mut dyn SourceDatabaseExt) { + for (source_root_id, data) in self.app_map { + let arc_data = data.map(Arc::new); + db.set_app_data(source_root_id, arc_data); + } + for (project_id, project_data) in self.project_map { + db.set_project_data(project_id, Arc::new(project_data)); + } + } +} + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct AppRoots { + otp: Option>, + app_map: FxHashMap, +} + +impl AppRoots { + pub fn insert(&mut self, app: AppName, source_root_id: SourceRootId) { + self.app_map.insert(app, source_root_id); + } + + pub fn set_otp(&mut self, otp: Option>) { + self.otp = otp; + } + + pub fn get(&self, app: &Q) -> Option + where + AppName: Borrow, + Q: Hash + Eq, + { + self.app_map + .get(app) + .cloned() + .or_else(|| self.otp.as_ref().and_then(|otp| otp.get(app))) + } +} + +// --------------------------------------------------------------------- +/// Currently we don't eqWAlize OTP +/// Historical reasons: +/// - We used to not support the full language. Now we may actually be able to eqWAlize OTP if we could get the project model +/// - OTP code is written very differently from WhatsApp code, so we didn't want to bias our trade-offs at the beginning. +/// It could change in future +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IncludeOtp { + Yes, + No, +} + +#[derive(Debug)] +pub struct ProjectApps<'a> { + /// All the applications in a set of projects. The order here + /// will correspond with the vfs sourceRootId's + pub all_apps: Vec<(ProjectId, &'a ProjectAppData)>, + /// Sometimes we don't have an OTP project because we are explicitly + /// opting out of using it, e.g. for eqWAlizer compatibility + pub otp_project_id: Option, + // We store the original projects to we can make the AppStructure later + projects: Vec, +} + +impl<'a> ProjectApps<'a> { + pub fn new(projects: &'a [Project], include_otp: IncludeOtp) -> ProjectApps<'a> { + let mut all_apps: Vec<(ProjectId, &ProjectAppData)> = projects + .iter() + .enumerate() + .flat_map(|(project_idx, project)| { + project + .all_apps() + .into_iter() + .map(move |p| (ProjectId(project_idx as u32), p)) + }) + .collect(); + + // We assume that all of the `Project`s use the same OTP. + // And so we treat the very first one as another + // `RebarProject`, but for OTP. + let otp = &projects[0].otp; + let mut projects: Vec<_> = projects.into(); + let otp_project_id = if include_otp == IncludeOtp::Yes { + let otp_project_id = ProjectId(projects.len() as u32); + let mut all_otp_apps: Vec<(ProjectId, &ProjectAppData)> = + otp.apps.iter().map(|app| (otp_project_id, app)).collect(); + all_apps.append(&mut all_otp_apps); + // The only part of this we (currently) use in + // ProjectRootMap::app_structure() is Project.otp + let otp_project = Project::otp(otp.clone()); + projects.push(otp_project); + Some(otp_project_id) + } else { + None + }; + + ProjectApps { + all_apps, + otp_project_id, + projects, + } + } + + pub fn app_structure(&self) -> AppStructure { + let mut app_structure = AppStructure::default(); + let mut app_idx = 0; + + // Reconstruct the per-project list + let mut apps_by_project: FxHashMap> = FxHashMap::default(); + + for (project_id, appdata) in self.all_apps.iter() { + apps_by_project + .entry(*project_id) + .or_default() + .push(appdata); + } + + let mut project_root_map = app_source_roots(&self.all_apps); + let otp_root = self + .otp_project_id + .and_then(|otp_project_id| project_root_map.get(&otp_project_id)) + .cloned() + .map(Arc::new); + + for (project_idx, project) in self.projects.iter().enumerate() { + let project_id = ProjectId(project_idx as u32); + let empty = vec![]; + let apps = apps_by_project.get(&project_id).unwrap_or(&empty); + + let mut project_source_roots = vec![]; + for app in apps { + let root_id = SourceRootId(app_idx); + app_idx += 1; + project_source_roots.push(root_id); + let input_data = AppData { + project_id, + name: app.name.clone(), + dir: app.dir.clone(), + include_path: app.include_path.clone(), + extra_src_dirs: app.extra_src_dirs.clone(), + macros: app.macros.clone(), + parse_transforms: app.parse_transforms.clone(), + app_type: app.app_type, + src_path: app.abs_src_dirs.clone(), + ebin_path: app.ebin.clone(), + }; + app_structure.add_app_data(root_id, Some(input_data)); + } + + let mut app_roots = project_root_map.remove(&project_id).unwrap_or_default(); + + if self.otp_project_id != Some(project_id) { + app_roots.set_otp(otp_root.clone()); + } + + let project_data = ProjectData { + source_roots: project_source_roots, + root_dir: project.root().into_owned(), + deps_ebins: project.deps_ebins(), + build_info_path: project.build_info_file(), + otp_project_id: self.otp_project_id, + app_roots, + eqwalizer_config: project.eqwalizer_config(), + }; + app_structure.add_project_data(project_id, project_data); + } + + // Final SourceRoot for out-of-project files + log::info!("Final source root: {:?}", SourceRootId(app_idx)); + app_structure.add_app_data(SourceRootId(app_idx), None); + app_structure + } +} + +fn app_source_roots(all_apps: &[(ProjectId, &ProjectAppData)]) -> FxHashMap { + let mut app_source_roots: FxHashMap = FxHashMap::default(); + + for (idx, (project_id, app)) in all_apps.iter().enumerate() { + let source_root_id = SourceRootId(idx as u32); + app_source_roots + .entry(*project_id) + .or_default() + .insert(app.name.clone(), source_root_id); + } + app_source_roots +} diff --git a/crates/base_db/src/lib.rs b/crates/base_db/src/lib.rs new file mode 100644 index 0000000000..c2d86d8867 --- /dev/null +++ b/crates/base_db/src/lib.rs @@ -0,0 +1,226 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::sync::Arc; + +use elp_project_model::AppName; +use elp_syntax::ast::SourceFile; +use elp_syntax::Parse; +use elp_syntax::TextRange; +use elp_syntax::TextSize; + +mod change; +mod input; +mod module_index; + +// --------------------------------------------------------------------- +// Public API + +pub mod fixture; +pub mod test_fixture; +pub mod test_utils; +pub use change::Change; +pub use elp_project_model::AppType; +pub use input::AppData; +pub use input::AppRoots; +pub use input::AppStructure; +pub use input::FileSource; +pub use input::IncludeOtp; +pub use input::ProjectApps; +pub use input::ProjectData; +pub use input::ProjectId; +pub use input::SourceRoot; +pub use input::SourceRootId; +pub use module_index::ModuleIndex; +pub use module_index::ModuleName; +pub use module_index::Modules; +pub use paths::AbsPath; +pub use paths::AbsPathBuf; +pub use paths::RelPath; +pub use paths::RelPathBuf; +pub use salsa; +pub use vfs::file_set::FileSet; +pub use vfs::file_set::FileSetConfig; +pub use vfs::file_set::FileSetConfigBuilder; +pub use vfs::loader; +pub use vfs::AnchoredPath; +pub use vfs::AnchoredPathBuf; +pub use vfs::ChangeKind; +pub use vfs::ChangedFile; +pub use vfs::FileId; +pub use vfs::Vfs; +pub use vfs::VfsPath; + +// --------------------------------------------------------------------- + +#[macro_export] +macro_rules! impl_intern_key { + ($name:ident) => { + impl $crate::salsa::InternKey for $name { + fn from_intern_id(v: $crate::salsa::InternId) -> Self { + $name(v) + } + fn as_intern_id(&self) -> $crate::salsa::InternId { + self.0 + } + } + }; +} + +pub trait Upcast { + fn upcast(&self) -> &T; +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +pub struct FilePosition { + pub file_id: FileId, + pub offset: TextSize, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +pub struct FileRange { + pub file_id: FileId, + pub range: TextRange, +} + +pub trait FileLoader { + /// Text of the file. + fn file_text(&self, file_id: FileId) -> Arc; +} + +/// Database which stores all significant input facts: source code and project +/// model. Everything else in ELP is derived from these queries. +#[salsa::query_group(SourceDatabaseStorage)] +pub trait SourceDatabase: FileLoader + salsa::Database { + /// Path to a file, relative to the root of its source root. + /// Source root of the file. + #[salsa::input] + fn file_source_root(&self, file_id: FileId) -> SourceRootId; + + /// Contents of the source root. + #[salsa::input] + fn source_root(&self, id: SourceRootId) -> Arc; + + #[salsa::input] + fn app_data(&self, id: SourceRootId) -> Option>; + + #[salsa::input] + fn project_data(&self, id: ProjectId) -> Arc; + + /// A global ID used to invalidate the database when making untracked changes. + #[salsa::input] + fn include_files_revision(&self) -> u64; + + /// Returns a map from module name to FileId of the containing file. + fn module_index(&self, project_id: ProjectId) -> Arc; + + /// Parse the file_id to AST + fn parse(&self, file_id: FileId) -> Parse; + + fn is_generated(&self, file_id: FileId) -> bool; + + fn is_test_suite_or_test_helper(&self, file_id: FileId) -> Option; + + fn file_app_type(&self, file_id: FileId) -> Option; + + fn file_app_name(&self, file_id: FileId) -> Option; +} + +fn module_index(db: &dyn SourceDatabase, project_id: ProjectId) -> Arc { + let mut builder = ModuleIndex::builder(); + + let project_data = db.project_data(project_id); + for &source_root_id in &project_data.source_roots { + if let Some(app_data) = db.app_data(source_root_id) { + let source_root = db.source_root(source_root_id); + for (file_id, file_source, path) in source_root.iter_app_files(&app_data) { + if let Some((name, Some("erl"))) = path.name_and_extension() { + builder.insert(file_id, file_source, ModuleName::new(name)); + } + } + } + } + + project_data + .otp_project_id + .iter() + .for_each(|otp_project_id| { + if *otp_project_id == project_id { + builder.is_otp() + } else { + builder.set_otp(db.module_index(*otp_project_id)) + } + }); + + builder.build() +} + +fn parse(db: &dyn SourceDatabase, file_id: FileId) -> Parse { + let text = db.file_text(file_id); + SourceFile::parse_text(&text) +} + +fn is_generated(db: &dyn SourceDatabase, file_id: FileId) -> bool { + let contents = db.file_text(file_id); + contents[0..(2001.min(contents.len()))].contains(&format!("{}generated", "@")) +} + +fn is_test_suite_or_test_helper(db: &dyn SourceDatabase, file_id: FileId) -> Option { + let root_id = db.file_source_root(file_id); + let root = db.source_root(root_id); + let app_data = db.app_data(root_id)?; + let path = root.path_for_file(&file_id)?; + if app_data.is_extra_src_file(path) { + Some(true) + } else { + Some(false) + } +} + +fn file_app_type(db: &dyn SourceDatabase, file_id: FileId) -> Option { + let app_data = db.app_data(db.file_source_root(file_id))?; + Some(app_data.app_type) +} + +fn file_app_name(db: &dyn SourceDatabase, file_id: FileId) -> Option { + let app_data = db.app_data(db.file_source_root(file_id))?; + Some(app_data.name.clone()) +} + +/// We don't want to give HIR knowledge of source roots, hence we extract these +/// methods into a separate DB. +#[salsa::query_group(SourceDatabaseExtStorage)] +pub trait SourceDatabaseExt: SourceDatabase { + #[salsa::input] + fn file_text(&self, file_id: FileId) -> Arc; +} + +/// Silly workaround for cyclic deps between the traits +pub struct FileLoaderDelegate(pub T); + +impl FileLoader for FileLoaderDelegate<&'_ T> { + fn file_text(&self, file_id: FileId) -> Arc { + SourceDatabaseExt::file_text(self.0, file_id) + } +} + +/// If the `input` string represents an atom, and needs quoting, quote +/// it. +pub fn to_quoted_string(input: &str) -> String { + fn is_valid_atom(input: &str) -> bool { + let mut chars = input.chars(); + chars.next().map_or(false, |c| c.is_lowercase()) + && chars.all(|c| char::is_alphanumeric(c) || c == '_' || c == '@') + } + if is_valid_atom(input) { + input.to_string() + } else { + format!("'{}'", &input) + } +} diff --git a/crates/base_db/src/module_index.rs b/crates/base_db/src/module_index.rs new file mode 100644 index 0000000000..5685b559c9 --- /dev/null +++ b/crates/base_db/src/module_index.rs @@ -0,0 +1,197 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::borrow::Borrow; +use std::fmt; +use std::hash::Hash; +use std::ops::Deref; +use std::sync::Arc; + +use elp_syntax::SmolStr; +use fxhash::FxHashMap; + +use crate::to_quoted_string; +use crate::FileId; +use crate::FileSource; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct ModuleName(SmolStr); + +impl ModuleName { + pub fn new(name: &str) -> Self { + ModuleName(SmolStr::new(name)) + } + + pub fn as_str(&self) -> &str { + self + } + + pub fn to_quoted_string(&self) -> String { + to_quoted_string(&self.as_str()) + } +} + +impl Deref for ModuleName { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Borrow for ModuleName { + fn borrow(&self) -> &str { + self.as_str() + } +} + +pub type Modules = Vec; + +#[derive(Clone, PartialEq, Eq)] +pub struct ModuleIndex { + /// - None: No OTP being tracked + /// - Some(There(_)): There's OTP's module index + /// - Some(Here): This index is itself OTP + otp: Option, + mod2file: FxHashMap, + file2mod: FxHashMap, +} + +impl fmt::Debug for ModuleIndex { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("ModuleIndex(")?; + let mut map = f.debug_map(); + for entry in &self.mod2file { + map.entry(&entry.0, &entry.1); + } + map.finish()?; + f.write_str(")") + } +} + +impl ModuleIndex { + pub fn builder() -> Builder { + Builder::default() + } + + pub fn file_for_module(&self, name: &Q) -> Option + where + ModuleName: Borrow, + Q: Hash + Eq, + { + self.mod2file + .get(name) + .map(|(_source, id)| *id) + .or_else(|| { + self.otp.as_ref().and_then(|otp| match otp { + OtpModuleIndex::There(index) => index.file_for_module(name), + OtpModuleIndex::Here => None, + }) + }) + } + + pub fn file_source_for_file(&self, file_id: FileId) -> Option { + self.file2mod + .get(&file_id) + .and_then(|name| self.mod2file.get(name).map(|(source, _id)| *source)) + .or_else(|| { + self.otp.as_ref().and_then(|otp| match otp { + OtpModuleIndex::There(index) => index.file_source_for_file(file_id), + OtpModuleIndex::Here => None, + }) + }) + } + + pub fn module_for_file(&self, file_id: FileId) -> Option<&ModuleName> { + self.file2mod.get(&file_id).map_or_else( + || { + self.otp.as_ref().and_then(|otp| match otp { + OtpModuleIndex::There(index) => index.module_for_file(file_id), + OtpModuleIndex::Here => None, + }) + }, + Some, + ) + } + + /// Iterate over project-owned modules, without OTP + pub fn iter_own( + &self, + ) -> impl Iterator + ExactSizeIterator + '_ { + self.mod2file + .iter() + .map(|(name, (source, id))| (name, *source, *id)) + } + + /// Number of project-owned modules, without OTP + pub fn len_own(&self) -> usize { + self.mod2file.len() + } + + /// All project-owned modules and OTP modules + pub fn all_modules(&self) -> Modules { + match &self.otp { + Some(OtpModuleIndex::There(otp)) => self + .mod2file + .iter() + .chain(otp.mod2file.iter()) + .map(|(m, _f)| m.clone()) + .collect::>(), + Some(_) | None => self + .mod2file + .iter() + .map(|(m, _f)| m.clone()) + .collect::>(), + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum OtpModuleIndex { + /// "Use this index as the OTP index" + There(Arc), + /// "We are currently indexing OTP, but it needs to refer to OTP's index - we need to tie the knot!" + Here, +} + +#[derive(Default)] +pub struct Builder( + FxHashMap, + Option, +); + +impl Builder { + pub fn insert(&mut self, file_id: FileId, source: FileSource, name: ModuleName) { + self.0.insert(name, (source, file_id)); + } + + /// Use a given, existing index as OTP + pub fn set_otp(&mut self, otp: Arc) { + self.1 = Some(OtpModuleIndex::There(otp)) + } + + /// You are OTP, so use yourself as your OTP index + pub fn is_otp(&mut self) { + self.1 = Some(OtpModuleIndex::Here) + } + + pub fn build(self) -> Arc { + let file2mod = self + .0 + .iter() + .map(|(name, (_source, file))| (*file, name.clone())) + .collect::>(); + + Arc::new(ModuleIndex { + otp: self.1, + mod2file: self.0, + file2mod, + }) + } +} diff --git a/crates/base_db/src/test_fixture.rs b/crates/base_db/src/test_fixture.rs new file mode 100644 index 0000000000..1e2c2011c7 --- /dev/null +++ b/crates/base_db/src/test_fixture.rs @@ -0,0 +1,345 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! Based on rust-analyzer test_utils::fixture + +//! Defines `Fixture` -- a convenient way to describe the initial state of +//! ELP database from a single string. +//! +//! Fixtures are strings containing Erlang source code with optional metadata. +//! A fixture without metadata is parsed into a single source file. +//! Use this to test functionality local to one file. +//! +//! Simple Example: +//! ``` +//! r#" +//! main() -> +//! ok. +//! "# +//! ``` +//! +//! Metadata can be added to a fixture after a `//-` comment. +//! The basic form is specifying filenames, +//! which is also how to define multiple files in a single test fixture +//! +//! Example using two files in the same crate: +//! ``` +//! " +//! //- /main.erl +//! -module(main). +//! main() -> +//! foo:bar(). +//! +//! //- /foo.erl +//! -module(foo). +//! bar() -> ok. +//! " +//! ``` +//! +//! Specify OTP, and an OTP app +//! ``` +//! " +//! //- /opt/lib/comp-1.3/include/comp.hrl otp_app:/opt/lib/comp-1.3 +//! -define(COMP,3). +//! " +//! ``` +//! +//! Example setting up multi-app project, and OTP +//! ``` +//! " +//! //- /opt/lib/comp-1.3/include/comp.hrl otp_app:/opt/lib/comp-1.3 +//! -define(COMP,3). +//! //- /extra/include/bar.hrl include_path:/extra/include app:app_a +//! -define(BAR,4). +//! //- /include/foo.hrl include_path:/include app:app_a +//! -define(FOO,3). +//! //- /src/foo.erl app:app_b +//! -module(foo). +//! -include("foo.hrl"). +//! -include("bar.hrl"). +//! bar() -> ?FOO. +//! foo() -> ?BAR. +//! " +//! ``` + +use std::path::Path; +use std::path::PathBuf; + +use elp_project_model::otp::Otp; +use elp_project_model::AppName; +use elp_project_model::ProjectAppData; +use paths::AbsPath; +use paths::AbsPathBuf; +pub use stdx::trim_indent; + +#[derive(Debug, Eq, PartialEq)] +pub struct Fixture { + pub path: String, + pub text: String, + pub app_data: Option, + pub otp: Option, +} + +impl Fixture { + /// Parses text which looks like this: + /// + /// ```not_rust + /// //- some meta + /// line 1 + /// line 2 + /// //- other meta + /// ``` + /// + pub fn parse(fixture: &str) -> Vec { + let fixture = trim_indent(fixture); + let mut res: Vec = Vec::new(); + + let default = if fixture.contains("//-") { + None + } else { + Some("//- /main.erl") + }; + + for (ix, line) in default + .into_iter() + .chain(fixture.split_inclusive('\n')) + .enumerate() + { + if line.contains("//-") { + assert!( + line.starts_with("//-"), + "Metadata line {} has invalid indentation. \ + All metadata lines need to have the same indentation.\n\ + The offending line: {:?}", + ix, + line + ); + } + + if line.starts_with("//-") { + let meta = Fixture::parse_meta_line(line); + res.push(meta) + } else { + if line.starts_with("// ") + && line.contains(':') + && !line.contains("::") + && line.chars().all(|it| !it.is_uppercase()) + { + panic!("looks like invalid metadata line: {:?}", line) + } + + if let Some(entry) = res.last_mut() { + entry.text.push_str(line); + } + } + } + + res + } + + //- /module.erl app:foo + //- /opt/lib/comp-1.3/include/comp.hrl otp_app:/opt/lib/comp-1.3 + //- /my_app/test/file_SUITE.erl extra:test + fn parse_meta_line(meta: &str) -> Fixture { + assert!(meta.starts_with("//-")); + let meta = meta["//-".len()..].trim(); + let components = meta.split_ascii_whitespace().collect::>(); + + let path = components[0].to_string(); + assert!( + path.starts_with('/'), + "fixture path does not start with `/`: {:?}", + path + ); + + let mut app_name = None; + let mut include_dirs = Vec::new(); + let mut extra_dirs = Vec::new(); + let mut otp = None; + + for component in components[1..].iter() { + let (key, value) = component + .split_once(':') + .unwrap_or_else(|| panic!("invalid meta line: {:?}", meta)); + match key { + "app" => app_name = Some(AppName(value.to_string())), + "include_path" => include_dirs + .push(AbsPath::assert(&PathBuf::from(value.to_string())).normalize()), + "otp_app" => { + // We have an app directory, the OTP lib dir is its parent + let path = AbsPathBuf::assert(PathBuf::from(value.to_string())); + let lib_dir = path.parent().unwrap().normalize(); + let versioned_name = path.file_name().unwrap().to_str().unwrap().to_string(); + let app = ProjectAppData::otp_app_data(&versioned_name, path); + + otp = Some(Otp { + lib_dir, + apps: vec![app], + }); + } + "extra" => { + // We have an extra directory, such as for a test suite + // It needs to be relative to the app dir. + let dir = value.to_string(); + extra_dirs.push(dir); + } + _ => panic!("bad component: {:?}", component), + } + } + + let app_data = if otp.is_some() { + None + } else { + // Try inferring dir - parent once to get to ./src, parent twice to get to app root + let dir = AbsPath::assert(Path::new(&path)).parent().unwrap(); + let dir = dir.parent().unwrap_or(dir).normalize(); + let app_name = app_name.unwrap_or(AppName("test-fixture".to_string())); + let abs_path = AbsPathBuf::assert(PathBuf::from(path.clone())); + let mut src_dirs = vec![]; + if let Some(ext) = abs_path.extension() { + if ext == "erl" { + if let Some(parent) = abs_path.parent() { + let path = parent.to_path_buf(); + src_dirs.push(path) + } + } + } + Some(ProjectAppData::fixture_app_data( + app_name, + dir, + include_dirs, + src_dirs, + extra_dirs, + )) + }; + + Fixture { + path, + text: String::new(), + app_data, + otp, + } + } +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use expect_test::expect; + use paths::AbsPath; + + use super::Fixture; + + #[test] + #[should_panic] + fn parse_fixture_checks_further_indented_metadata() { + Fixture::parse( + r" + //- /lib.rs + mod bar; + + fn foo() {} + //- /bar.rs + pub fn baz() {} + ", + ); + } + + #[test] + fn parse_fixture_multiple_files() { + let parsed = Fixture::parse( + r#" +//- /foo.erl +-module(foo). +foo() -> ok. +//- /bar.erl +-module(bar). +bar() -> ok. +"#, + ); + // assert_eq!(mini_core.unwrap().activated_flags, vec!["coerce_unsized".to_string()]); + assert_eq!(2, parsed.len()); + + let meta0 = &parsed[0]; + assert_eq!("-module(foo).\nfoo() -> ok.\n", meta0.text); + + let meta1 = &parsed[1]; + assert_eq!("-module(bar).\nbar() -> ok.\n", meta1.text); + + assert_eq!("/foo.erl", meta0.path); + + assert_eq!("/bar.erl", meta1.path); + } + + #[test] + fn parse_fixture_gets_app_data() { + let parsed = Fixture::parse( + r#" +//- /include/foo.hrl include_path:/include +-define(FOO,3). +//- /src/foo.erl +-module(foo). +foo() -> ok. +//- /src/bar.erl +-module(bar). +bar() -> ok. +"#, + ); + // assert_eq!(mini_core.unwrap().activated_flags, vec!["coerce_unsized".to_string()]); + assert_eq!(3, parsed.len()); + + let app_data = parsed[0].app_data.as_ref().unwrap(); + assert_eq!( + vec![AbsPath::assert(&PathBuf::from("/include")).normalize()], + app_data.include_dirs + ); + let meta0 = &parsed[0]; + assert_eq!("-define(FOO,3).\n", meta0.text); + + let meta1 = &parsed[1]; + assert_eq!("-module(foo).\nfoo() -> ok.\n", meta1.text); + + let meta2 = &parsed[2]; + assert_eq!("-module(bar).\nbar() -> ok.\n", meta2.text); + + assert_eq!("/include/foo.hrl", meta0.path); + + assert_eq!("/src/foo.erl", meta1.path); + + assert_eq!("/src/bar.erl", meta2.path); + + expect![[r#" + ProjectAppData { + name: AppName( + "test-fixture", + ), + dir: AbsPathBuf( + "/", + ), + ebin: Some( + AbsPathBuf( + "/ebin", + ), + ), + extra_src_dirs: [], + include_dirs: [ + AbsPathBuf( + "/include", + ), + ], + abs_src_dirs: [], + macros: [], + parse_transforms: [], + app_type: App, + include_path: [], + }"#]] + .assert_eq(format!("{:#?}", meta0.app_data.as_ref().unwrap()).as_str()); + } +} diff --git a/crates/base_db/src/test_utils.rs b/crates/base_db/src/test_utils.rs new file mode 100644 index 0000000000..24d5586ebe --- /dev/null +++ b/crates/base_db/src/test_utils.rs @@ -0,0 +1,56 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +pub use dissimilar::diff as __diff; + +// --------------------------------------------------------------------- + +// From rust-analyzer test_utils/src/lib.rs +// +/// Asserts that two strings are equal, otherwise displays a rich diff between them. +/// +/// The diff shows changes from the "original" left string to the "actual" right string. +/// +/// All arguments starting from and including the 3rd one are passed to +/// `eprintln!()` macro in case of text inequality. +#[macro_export] +macro_rules! assert_eq_text { + ($left:expr, $right:expr) => { + assert_eq_text!($left, $right,) + }; + ($left:expr, $right:expr, $($tt:tt)*) => {{ + let left = $left; + let right = $right; + if left != right { + if left.trim() == right.trim() { + std::eprintln!("Left:\n{:?}\n\nRight:\n{:?}\n\nWhitespace difference\n", left, right); + } else { + let diff = $crate::test_utils::__diff(left, right); + std::eprintln!("Left:\n{}\n\nRight:\n{}\n\nDiff:\n{}\n", left, right, $crate::test_utils::format_diff(diff)); + } + std::eprintln!($($tt)*); + panic!("text differs"); + } + }}; +} + +pub fn format_diff(chunks: Vec) -> String { + let mut buf = String::new(); + for chunk in chunks { + let formatted = match chunk { + dissimilar::Chunk::Equal(text) => text.into(), + dissimilar::Chunk::Delete(text) => format!("\x1b[41m{}\x1b[0m", text), + dissimilar::Chunk::Insert(text) => format!("\x1b[42m{}\x1b[0m", text), + }; + buf.push_str(&formatted); + } + buf +} + +// --------------------------------------------------------------------- diff --git a/crates/elp/Cargo.toml b/crates/elp/Cargo.toml new file mode 100644 index 0000000000..08d7c4891c --- /dev/null +++ b/crates/elp/Cargo.toml @@ -0,0 +1,62 @@ +[package] +autobins = false +name = "elp" +edition.workspace = true +version.workspace = true + +[features] +default = ["buck"] +buck = [] + +[[bin]] +name = "elp" +path = "src/bin/main.rs" + +[dependencies] +elp_ai.workspace = true +elp_ide.workspace = true +elp_log.workspace = true +elp_project_model.workspace = true +elp_syntax.workspace = true + +always-assert.workspace = true +anyhow.workspace = true +bpaf.workspace = true +codespan-reporting.workspace = true +crossbeam-channel.workspace = true +env_logger.workspace = true +fs_extra.workspace = true +fxhash.workspace = true +indicatif.workspace = true +itertools.workspace = true +jod-thread.workspace = true +lazy_static.workspace = true +log.workspace = true +lsp-server.workspace = true +lsp-types.workspace = true +parking_lot.workspace = true +pico-args.workspace = true +profile.workspace = true +rayon.workspace = true +regex.workspace = true +rustyline.workspace = true +serde_json.workspace = true +serde_path_to_error.workspace = true +serde.workspace = true +stdx.workspace = true +strsim.workspace = true +tempfile.workspace = true +text-edit.workspace = true +threadpool.workspace = true +toml.workspace = true +vfs-notify.workspace = true + +[target.'cfg(not(target_env = "msvc"))'.dependencies] +jemallocator.workspace = true + +[dev-dependencies] +expect-test.workspace = true +test-case.workspace = true + +[build-dependencies] +time = { version = "0.3.20", features = ["formatting"] } diff --git a/crates/elp/build.rs b/crates/elp/build.rs new file mode 100644 index 0000000000..b51b201e3a --- /dev/null +++ b/crates/elp/build.rs @@ -0,0 +1,43 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::env; +use std::str::FromStr; + +use time::format_description; +use time::OffsetDateTime; + +const CI: &str = "CI"; +const SOURCE_DATE_EPOCH: &str = "SOURCE_DATE_EPOCH"; + +fn main() { + let date_format = + format_description::parse("build-[year]-[month]-[day]").expect("wrong format"); + + let is_ci = env::var(CI).is_ok(); + let epoch = env::var(SOURCE_DATE_EPOCH); + let build_id = if is_ci || epoch.is_ok() { + let date = match epoch { + Ok(v) => { + let timestamp = i64::from_str(&v).expect("parsing SOURCE_DATE_EPOCH"); + OffsetDateTime::from_unix_timestamp(timestamp).expect("parsing SOURCE_DATE_EPOCH") + } + Err(std::env::VarError::NotPresent) => OffsetDateTime::now_utc(), + Err(e) => panic!("Error getting SOURCE_DATE_EPOCH: {}", e), + }; + date.format(&date_format).expect("formatting date") + } else { + "local".to_string() + }; + + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-env-changed={}", SOURCE_DATE_EPOCH); + println!("cargo:rerun-if-env-changed={}", CI); + println!("cargo:rustc-env=BUILD_ID={}", build_id); +} diff --git a/crates/elp/src/arc_types.rs b/crates/elp/src/arc_types.rs new file mode 100644 index 0000000000..7233c13229 --- /dev/null +++ b/crates/elp/src/arc_types.rs @@ -0,0 +1,65 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +/// Types as defined in https://www.internalfb.com/intern/wiki/Linting/adding-linters/#flow-type +/// and https://www.internalfb.com/code/whatsapp-server/[4dcee4c563dd9d160ad885069a816907216c9e40]/erl/tools/lint/arcanist.py?lines=17 / +use std::path::Path; + +use serde::Serialize; + +#[derive(Debug, Serialize, PartialEq, Eq)] +pub struct Diagnostic { + // Filepath + path: String, + line: Option, + char: Option, + // Linter name (normally this would need to match code in fbsource-lint-engine.toml) + code: String, + // Message severity + severity: Severity, + // Rule name + name: String, + original: Option, + replacement: Option, + description: Option, +} + +#[derive(Debug, Serialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum Severity { + Error, // May crash (eg. syntax errors); always shown; need confirmation + Warning, // Minor problems (eg. readability); shown on change; need confirmation + Autofix, // Warning that contains an automatic fix in description + Advice, // Improvements (eg. leftover comments); shown on change; no confrimation + Disabled, // Suppressed error message +} + +impl Diagnostic { + pub fn new( + path: &Path, + line: u32, + character: Option, + severity: Severity, + name: String, + description: String, + original: Option, + ) -> Self { + Diagnostic { + path: path.display().to_string(), // lossy on Windows for unicode paths + line: Some(line), + r#char: character, + code: "ELP".to_owned(), + severity, + name, + original, + replacement: None, + description: Some(description), + } + } +} diff --git a/crates/elp/src/bin/args.rs b/crates/elp/src/bin/args.rs new file mode 100644 index 0000000000..7891220e00 --- /dev/null +++ b/crates/elp/src/bin/args.rs @@ -0,0 +1,517 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::cmp::Ordering; +use std::env; +use std::fs; +use std::path::PathBuf; + +use bpaf::construct; +use bpaf::long; +use bpaf::Bpaf; +use bpaf::Parser; +use itertools::Itertools; +use serde::Deserialize; + +use crate::args::Command::Help; + +#[derive(Clone, Debug, Bpaf)] +pub struct ParseAllElp { + /// Path to directory with project (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"), complete(module_completer), optional)] + pub module: Option, + /// Parse a single file from the project, not the entire project. \nThis can be an include file or escript, etc. + pub file: Option, + /// Path to a directory where to dump result files + #[bpaf(argument("TO"))] + pub to: Option, + #[bpaf(external(parse_print_diags))] + pub print_diags: bool, + #[bpaf(external(parse_experimental_diags))] + pub experimental_diags: bool, + /// Rebar3 profile to pickup (default is test) + #[bpaf(long("as"), argument("PROFILE"), fallback("test".to_string()))] + pub profile: String, + /// Report the resolution of include directives for comparison with OTP ones + #[bpaf(long("dump-includes"))] + pub dump_include_resolutions: bool, + /// Run with rebar + pub rebar: bool, + /// Also eqwalize opted-in generated modules from application + pub include_generated: bool, + /// Parse the files serially, not in parallel + pub serial: bool, +} + +#[derive(Clone, Debug, Bpaf)] +pub struct ParseAll { + /// Path to directory with project (defaults to `.`) + #[bpaf(argument("PROJECT"), fallback(PathBuf::from(".")))] + pub project: PathBuf, + /// Path to a directory where to dump .etf files + pub to: PathBuf, + /// Rebar3 profile to pickup (default is test) + #[bpaf(long("as"), argument("PROFILE"), fallback("test".to_string()))] + pub profile: String, + /// Parse a single module from the project, not the entire project + #[bpaf(argument("MODULE"), complete(module_completer), optional)] + pub module: Option, + /// Run with buck + pub buck: bool, +} + +#[derive(Clone, Debug, Bpaf)] +pub struct Eqwalize { + /// Path to directory with project (defaults to `.`) + #[bpaf(argument("PROJECT"), fallback(PathBuf::from(".")))] + pub project: PathBuf, + /// Rebar3 profile to pickup (default is test) + #[bpaf(long("as"), argument("PROFILE"), fallback("test".to_string()))] + pub profile: String, + /// Run with rebar + pub rebar: bool, + /// Eqwalize specified module + #[bpaf(positional::< String > ("MODULE"), complete(module_completer))] + pub module: String, +} + +#[derive(Clone, Debug, Bpaf)] +pub struct EqwalizeAll { + /// Path to directory with project (defaults to `.`) + #[bpaf(argument("PROJECT"), fallback(PathBuf::from(".")))] + pub project: PathBuf, + /// Rebar3 profile to pickup (default is test) + #[bpaf(long("as"), argument("PROFILE"), fallback("test".to_string()))] + pub profile: String, + /// Show diagnostics in JSON format + #[bpaf( + argument("FORMAT"), + complete(format_completer), + fallback(None), + guard(format_guard, "Please use json") + )] + pub format: Option, + /// Run with rebar + pub rebar: bool, + /// Also eqwalize opted-in generated modules from project + pub include_generated: bool, +} + +#[derive(Clone, Debug, Bpaf)] +pub struct EqwalizePassthrough { + /// Path to directory with project (defaults to `.`) + #[bpaf(argument("PROJECT"), fallback(PathBuf::from(".")))] + pub project: PathBuf, + /// Rebar3 profile to pickup (default is test) + #[bpaf(long("as"), argument("PROFILE"), fallback("test".to_string()))] + pub profile: String, + /// Run with buck + pub buck: bool, + #[bpaf(positional::< String > ("ARGS"), many)] + pub args: Vec, +} + +#[derive(Clone, Debug, Bpaf)] +pub struct EqwalizeTarget { + /// Path to directory with project (defaults to `.`) + #[bpaf(argument("PROJECT"), fallback(PathBuf::from(".")))] + pub project: PathBuf, + /// Also eqwalize opted-in generated modules from application + pub include_generated: bool, + /// target, like //erl/chatd/... + #[bpaf(positional::< String > ("TARGET"))] + pub target: String, +} + +#[derive(Clone, Debug, Bpaf)] +pub struct EqwalizeApp { + /// Path to directory with project (defaults to `.`) + #[bpaf(argument("PROJECT"), fallback(PathBuf::from(".")))] + pub project: PathBuf, + /// Rebar3 profile to pickup (default is test) + #[bpaf(long("as"), argument("PROFILE"), fallback("test".to_string()))] + pub profile: String, + /// Also eqwalize opted-in generated modules from project + pub include_generated: bool, + /// Run with rebar + pub rebar: bool, + /// app name + #[bpaf(positional::< String > ("APP"))] + pub app: String, +} + +#[derive(Clone, Debug, Bpaf)] +pub struct EqwalizeStats { + /// Path to directory with project (defaults to `.`) + #[bpaf(argument("PROJECT"), fallback(PathBuf::from(".")))] + pub project: PathBuf, + /// Rebar3 profile to pickup (default is test) + #[bpaf(long("as"), argument("PROFILE"), fallback("test".to_string()))] + pub profile: String, + /// Run with rebar + pub rebar: bool, + /// Also eqwalize opted-in generated modules from project + pub include_generated: bool, +} + +#[derive(Clone, Debug, Bpaf)] +pub struct BuildInfo { + /// Path to directory with project (defaults to `.`) + #[bpaf(argument("PROJECT"), fallback(PathBuf::from(".")))] + pub project: PathBuf, + /// Path to a directory where to dump wa.build_info + #[bpaf(argument("TO"))] + pub to: PathBuf, +} + +#[derive(Clone, Debug, Bpaf)] +pub struct GenerateCompletions { + #[bpaf(positional::< String > ("shell"), complete(shell_completer), guard(shell_guard, "Please use bash|zsh|fish"))] + /// bash, zsh or fish + pub shell: String, +} + +#[derive(Clone, Debug, Bpaf)] +pub struct RunServer {} + +#[derive(Clone, Debug, Bpaf)] +pub struct Version {} + +#[derive(Debug, Clone, Bpaf)] +pub struct Lint { + /// Path to directory with project (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 file from the project, not the entire project. This can be an include file or escript, etc. + #[bpaf(argument("FILE"))] + pub file: Option, + /// Path to a directory where to dump result files + #[bpaf(argument("TO"))] + pub to: Option, + /// Do not print the full diagnostics for a file, just the count + #[bpaf(external(parse_print_diags))] + pub print_diags: bool, + #[bpaf(external(parse_experimental_diags))] + pub experimental_diags: bool, + /// Rebar3 profile to pickup (default is test) + #[bpaf(long("as"), argument("PROFILE"), fallback("test".to_string()))] + pub profile: String, + /// Show diagnostics in JSON format + #[bpaf( + argument("FORMAT"), + complete(format_completer), + fallback(None), + guard(format_guard, "Please use json") + )] + pub format: Option, + /// Run with rebar + pub rebar: bool, + pub include_generated: bool, + /// If the diagnostic has an associated fix, apply it. The modified file will be in the --to directory, or original file if --in-place is set. + pub apply_fix: bool, + /// If applying fixes, apply any new ones that arise from the + /// prior fixes recursively. Limited in scope to the clause of the + /// prior change. + pub recursive: bool, + /// When applying a fix, modify the original file. + pub in_place: bool, + /// Filter out all reported diagnostics except this one + #[bpaf(argument("FILTER"))] + pub diagnostic_filter: Option, + /// Filter out all reported diagnostics before this line. Valid only for single file + #[bpaf(argument("LINE_FROM"))] + pub line_from: Option, + /// Filter out all reported diagnostics after this line. Valid only for single file + #[bpaf(argument("LINE_TO"))] + pub line_to: Option, + /// 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 Shell { + /// Path to directory with project (defaults to `.`) + #[bpaf(argument("PROJECT"), fallback(PathBuf::from(".")))] + pub project: PathBuf, +} + +#[derive(Clone, Debug)] +pub enum Command { + ParseAllElp(ParseAllElp), + ParseAll(ParseAll), + Eqwalize(Eqwalize), + EqwalizeAll(EqwalizeAll), + EqwalizePassthrough(EqwalizePassthrough), + EqwalizeTarget(EqwalizeTarget), + EqwalizeApp(EqwalizeApp), + EqwalizeStats(EqwalizeStats), + BuildInfo(BuildInfo), + GenerateCompletions(GenerateCompletions), + RunServer(RunServer), + Lint(Lint), + Version(Version), + Shell(Shell), + Help(), +} + +#[derive(Debug, Clone, Bpaf)] +#[bpaf(options)] +pub struct Args { + #[bpaf(argument("LOG_FILE"))] + pub log_file: Option, + pub no_log_buffering: bool, + #[bpaf(external(command))] + pub command: Command, +} + +pub fn command() -> impl Parser { + let parse_elp = parse_all_elp() + .map(Command::ParseAllElp) + .to_options() + .command("parse-elp") + .help("Tree-sitter parse all files in a project for specified rebar.config file"); + + let parse_all = parse_all() + .map(Command::ParseAll) + .to_options() + .command("parse-all") + .help("Dump ast for all files in a project for specified rebar.config file"); + + let eqwalize = eqwalize() + .map(Command::Eqwalize) + .to_options() + .command("eqwalize") + .help("Eqwalize specified module"); + + let eqwalize_all = eqwalize_all() + .map(Command::EqwalizeAll) + .to_options() + .command("eqwalize-all") + .help("Eqwalize all opted-in modules in a project"); + + let eqwalize_passthrough = eqwalize_passthrough() + .map(Command::EqwalizePassthrough) + .to_options() + .command("eqwalize-passthrough") + .help("Pass args to eqwalizer"); + + let eqwalize_target = eqwalize_target() + .map(Command::EqwalizeTarget) + .to_options() + .command("eqwalize-target") + .help("Eqwalize all opted-in modules in specified buck target"); + + let eqwalize_app = eqwalize_app() + .map(Command::EqwalizeApp) + .to_options() + .command("eqwalize-app") + .help("Eqwalize all opted-in modules in specified application"); + + let eqwalize_stats = eqwalize_stats() + .map(Command::EqwalizeStats) + .to_options() + .command("eqwalize-stats") + .help("Return statistics about code quality for eqWAlizer"); + + let build_info = build_info() + .map(Command::BuildInfo) + .to_options() + .command("build-info") + .help("Generate build info file"); + let generate_completions = generate_completions() + .map(Command::GenerateCompletions) + .to_options() + .command("generate-completions") + .help("Generate shell completions"); + + let lint = lint() + .map(Command::Lint) + .to_options() + .command("lint") + .help("Parse files in project and emit diagnostics, optionally apply fixes."); + + let run_server = run_server() + .map(Command::RunServer) + .to_options() + .command("server") + .help("Run lsp server"); + + let version = version() + .map(Command::Version) + .to_options() + .command("version") + .help("Print version"); + + let shell = shell() + .map(Command::Shell) + .to_options() + .command("shell") + .help("Starts an interactive ELP shell"); + + construct!([ + eqwalize, + eqwalize_all, + eqwalize_app, + eqwalize_target, + lint, + run_server, + generate_completions, + parse_all, + parse_elp, + eqwalize_passthrough, + build_info, + version, + shell, + eqwalize_stats, + ]) + .fallback(Help()) +} + +fn parse_print_diags() -> impl Parser { + long("no-diags") + .help("Do not print the full diagnostics for a file, just the count") + .switch() + .map(|v| { + !v // negate + }) +} + +fn parse_experimental_diags() -> impl Parser { + long("experimental") + .help("Report experimental diagnostics too, if diagnostics are enabled") + .switch() + .map(|v| { + !v // negate + }) +} + +#[derive(Deserialize)] +struct ModuleConfig { + modules: Vec, +} + +const MODULES_TILE: &str = ".modules.toml"; + +fn module_completer(input: &String) -> Vec<(String, Option)> { + let mut modules = vec![]; + let curr = env::current_dir().unwrap(); + let mut potential_path = Some(curr.as_path()); + while let Some(path) = potential_path { + let file_path = path.join(MODULES_TILE); + + if !file_path.is_file() { + potential_path = path.parent(); + continue; + } else { + if let Ok(content) = fs::read_to_string(file_path) { + if let Ok(config) = toml::from_str::(&content) { + for module_name in config.modules.into_iter() { + modules.push(module_name) + } + } + } + break; + } + } + get_suggesions(input, modules) +} + +fn format_completer(_: &Option) -> Vec<(String, Option)> { + vec![("json".to_string(), None)] +} + +fn format_guard(format: &Option) -> bool { + match format { + None => true, + Some(f) if f == "json" => true, + _ => false, + } +} + +fn shell_completer(shell: &String) -> Vec<(String, Option)> { + let completions = match shell.to_lowercase().chars().next() { + Some('b') => vec!["bash"], + Some('f') => vec!["fish"], + Some('z') => vec!["zsh"], + _ => vec!["bash", "fish", "zsh"], + }; + completions + .into_iter() + .map(|sh| (sh.into(), None)) + .collect() +} + +fn shell_guard(shell: &String) -> bool { + match shell.as_ref() { + "bash" => true, + "zsh" => true, + "fish" => true, + _ => false, + } +} + +fn get_suggesions(input: &String, modules: Vec) -> Vec<(String, Option)> { + const MAX_RESULTS: usize = 10; + + modules + .into_iter() + .map(|key| (strsim::normalized_damerau_levenshtein(input, &key), key)) + .sorted_by(|a, b| PartialOrd::partial_cmp(&b.0, &a.0).unwrap_or(Ordering::Less)) + .take(MAX_RESULTS) + .map(|(_lev, v)| (v, None)) + .collect() +} + +#[cfg(target_os = "macos")] +pub fn gen_completions(shell: &str) -> &str { + match shell { + "bash" => "elp --bpaf-complete-style-bash", + "zsh" => { + "elp --bpaf-complete-style-zsh | sudo dd of=/usr/local/share/zsh/site-functions/_elp && echo 'autoload -U compinit; compinit ' >> ~/.zshrc && zsh" + } + "fish" => "elp --bpaf-complete-style-fish > ~/.config/fish/completions/elp.fish", + _ => unreachable!(), + } +} + +#[cfg(target_os = "linux")] +pub fn gen_completions(shell: &str) -> &str { + match shell { + "bash" => { + "elp --bpaf-complete-style-bash | sudo dd of=/usr/share/bash-completion/completions/elp && bash" + } + "zsh" => { + "elp --bpaf-complete-style-zsh | sudo dd of=/usr/share/zsh/site-functions/_elp && echo 'autoload -U compinit; compinit ' >> ~/.zshrc && zsh" + } + "fish" => "elp --bpaf-complete-style-fish > ~/.config/fish/completions/elp.fish", + _ => unreachable!(), + } +} + +#[cfg(not(any(target_os = "linux", target_os = "macos")))] +pub fn gen_completions(shell: &str) -> String { + format!("elp --bpaf-complete-style-{}", shell) +} + +impl Lint { + pub fn is_format_normal(&self) -> bool { + self.format.is_none() + } + + pub fn is_format_json(&self) -> bool { + self.format == Some("json".to_string()) + } +} diff --git a/crates/elp/src/bin/build_info_cli.rs b/crates/elp/src/bin/build_info_cli.rs new file mode 100644 index 0000000000..a5cab6c58e --- /dev/null +++ b/crates/elp/src/bin/build_info_cli.rs @@ -0,0 +1,40 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::fs; +use std::fs::File; + +use anyhow::bail; +use anyhow::Result; +use elp_ide::elp_ide_db::elp_base_db::AbsPathBuf; +use elp_project_model::buck; +use elp_project_model::otp::Otp; +use elp_project_model::DiscoverConfig; +use elp_project_model::ProjectManifest; + +use crate::args::BuildInfo; + +pub(crate) fn save_build_info(args: BuildInfo) -> Result<()> { + let root = fs::canonicalize(&args.project)?; + let root = AbsPathBuf::assert(root); + let manifest = ProjectManifest::discover_single(&root, &DiscoverConfig::buck()); + + let config = match manifest { + Ok(ProjectManifest::BuckConfig(buck)) => buck, + _ => bail!("Can't find buck root for {:?}", root), + }; + + let target_info = buck::load_buck_targets(&config.buck)?; + let project_app_data = buck::targets_to_project_data(&target_info.targets); + let otp_root = Otp::find_otp()?; + let build_info_term = buck::build_info(&config.buck, &project_app_data, &otp_root); + let writer = File::create(&args.to)?; + build_info_term.encode(writer)?; + Ok(()) +} diff --git a/crates/elp/src/bin/elp_parse_cli.rs b/crates/elp/src/bin/elp_parse_cli.rs new file mode 100644 index 0000000000..c0309b4a94 --- /dev/null +++ b/crates/elp/src/bin/elp_parse_cli.rs @@ -0,0 +1,351 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::fs; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; +use std::str; + +use anyhow::bail; +use anyhow::Result; +use elp::build::load; +use elp::build::types::LoadResult; +use elp::cli::Cli; +use elp::convert; +use elp::otp_file_to_ignore; +use elp::server::file_id_to_url; +use elp_ide::diagnostics::DiagnosticsConfig; +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::FileSource; +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_ide::elp_ide_db::Includes; +use elp_ide::Analysis; +use elp_project_model::AppType; +use elp_project_model::DiscoverConfig; +use indicatif::ParallelProgressIterator; +use indicatif::ProgressIterator; +use lsp_types::Diagnostic; +use lsp_types::DiagnosticSeverity; +use lsp_types::NumberOrString; +use rayon::iter::ParallelBridge; +use rayon::iter::ParallelIterator; + +use crate::args::ParseAllElp; + +pub fn parse_all(args: &ParseAllElp, cli: &mut dyn Cli) -> Result<()> { + log::info!("Loading project at: {:?}", args.project); + + let config = DiscoverConfig::new(args.rebar, &args.profile); + let loaded = load::load_project_at(cli, &args.project, config, IncludeOtp::Yes)?; + + if let Some(to) = &args.to { + fs::create_dir_all(to)? + }; + + let analysis = loaded.analysis(); + + let (file_id, name) = match &args.module { + Some(module) => { + writeln!(cli, "module specified: {}", module)?; + let file_id = analysis.module_file_id(loaded.project_id, module)?; + (file_id, analysis.module_name(file_id.unwrap())?) + } + + None => match &args.file { + Some(file_name) => { + writeln!(cli, "file specified: {}", file_name)?; + let path_buf = fs::canonicalize(file_name).unwrap(); + 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())), + path_buf + .as_path() + .file_name() + .map(|n| ModuleName::new(n.to_str().unwrap())), + ) + } + None => (None, None), + }, + }; + + let mut cfg = DiagnosticsConfig::default(); + cfg.disable_experimental = args.experimental_diags; + + let mut res = match (file_id, name, args.serial) { + (None, _, true) => do_parse_all_seq(cli, &loaded, &cfg, &args.to, args.include_generated)?, + (None, _, false) => do_parse_all_par(cli, &loaded, &cfg, &args.to, args.include_generated)?, + (Some(file_id), Some(name), _) => do_parse_one( + &analysis, + &loaded.vfs, + &cfg, + &args.to, + file_id, + &name, + args.include_generated, + )? + .map_or(vec![], |x| vec![x]), + (Some(file_id), _, _) => panic!("Could not get name from file_id for {:?}", file_id), + }; + + if args.dump_include_resolutions { + dump_includes_resolutions(cli, &loaded, &args.to)?; + } + + if res.is_empty() { + writeln!(cli, "No errors reported")?; + Ok(()) + } else { + writeln!(cli, "Diagnostics reported in {} modules:", res.len())?; + res.sort_by(|(a, _), (b, _)| a.cmp(b)); + let mut err_in_diag = false; + for (name, diags) in res { + writeln!(cli, " {}: {}", name, diags.len())?; + if args.print_diags { + for diag in diags { + let severity = match diag.severity { + None => DiagnosticSeverity::ERROR, + Some(sev) => { + err_in_diag |= sev == DiagnosticSeverity::ERROR; + sev + } + }; + writeln!( + cli, + " {}:{}-{}:{}::[{:?}] [{}] {}", + diag.range.start.line, + diag.range.start.character, + diag.range.end.line, + diag.range.end.character, + severity, + maybe_code_as_string(diag.code), + diag.message + )?; + } + } + } + if err_in_diag { + bail!("Parse failures found") + } else { + Ok(()) + } + } +} + +fn maybe_code_as_string(mc: Option) -> String { + match mc { + Some(ns) => match ns { + NumberOrString::Number(n) => format!("{}", n), + NumberOrString::String(s) => s, + }, + None => "".to_string(), + } +} + +fn do_parse_all_par( + cli: &dyn Cli, + loaded: &LoadResult, + config: &DiagnosticsConfig, + to: &Option, + include_generated: bool, +) -> Result)>> { + let module_index = loaded.analysis().module_index(loaded.project_id).unwrap(); + let module_iter = module_index.iter_own(); + + let pb = cli.progress(module_iter.len() as u64, "Parsing modules (parallel)"); + + let vfs = &loaded.vfs; + Ok(module_iter + .par_bridge() + .progress_with(pb) + .map_with( + loaded.analysis(), + |db, (module_name, file_source, file_id)| { + if !otp_file_to_ignore(db, file_id) + && file_source == FileSource::Src + && db.file_app_type(file_id).ok() != Some(Some(AppType::Dep)) + { + do_parse_one( + db, + vfs, + config, + to, + file_id, + module_name.as_str(), + include_generated, + ) + .unwrap() + } else { + None + } + }, + ) + .flatten() + .collect()) +} + +fn do_parse_all_seq( + cli: &dyn Cli, + loaded: &LoadResult, + config: &DiagnosticsConfig, + to: &Option, + include_generated: bool, +) -> Result)>> { + let module_index = loaded.analysis().module_index(loaded.project_id).unwrap(); + let module_iter = module_index.iter_own(); + + 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) + .flat_map(|(module_name, file_source, file_id)| { + if !otp_file_to_ignore(&db, file_id) + && file_source == FileSource::Src + && db.file_app_type(file_id).ok() != Some(Some(AppType::Dep)) + { + do_parse_one( + &db, + vfs, + config, + to, + file_id, + module_name.as_str(), + include_generated, + ) + .unwrap() + } else { + None + } + }) + .collect()) +} +fn do_parse_one( + db: &Analysis, + vfs: &Vfs, + config: &DiagnosticsConfig, + to: &Option, + file_id: FileId, + name: &str, + include_generated: bool, +) -> Result)>> { + let url = file_id_to_url(vfs, file_id); + let mut diagnostics = db.diagnostics(config, file_id, include_generated)?; + let erlang_service_diagnostics = db.erlang_service_diagnostics(file_id)?; + diagnostics.extend( + erlang_service_diagnostics + .into_iter() + // Should we return the included file diagnostics as well? Not doing so now. + .filter_map(|(f, diags)| if f == file_id { Some(diags) } else { None }) + .flatten(), + ); + let line_index = db.line_index(file_id)?; + + if let Some(to) = to { + let to_path = to.join(format!("{}.diag", name)); + let mut output = File::create(to_path)?; + + for diagnostic in diagnostics.iter() { + writeln!( + output, + "{:?}", + convert::ide_to_lsp_diagnostic(&line_index, &url, diagnostic) + )?; + } + } + let lsp_diagnostics = diagnostics + .iter() + .map(|diagnostic| convert::ide_to_lsp_diagnostic(&line_index, &url, diagnostic)) + .collect::>(); + if !diagnostics.is_empty() { + let res = (name.to_string(), lsp_diagnostics); + Ok(Some(res)) + } else { + Ok(None) + } +} + +// --------------------------------------------------------------------- + +fn dump_includes_resolutions( + cli: &dyn Cli, + loaded: &LoadResult, + to: &Option, +) -> Result<()> { + let module_index = loaded.analysis().module_index(loaded.project_id).unwrap(); + let module_iter = module_index.iter_own(); + + let pb = cli.progress(module_iter.len() as u64, "Analyzing include resolutions"); + + let vfs = &loaded.vfs; + let mut all_includes: Vec<_> = module_iter + .par_bridge() + .progress_with(pb) + .map_with( + loaded.analysis(), + |db, (_module_name, file_source, file_id)| { + if !otp_file_to_ignore(db, file_id) + && file_source == FileSource::Src + && db.file_app_type(file_id).ok() != Some(Some(AppType::Dep)) + { + if is_project_file(vfs, db, file_id) { + db.resolved_includes(file_id).ok() + } else { + None + } + } else { + None + } + }, + ) + .filter_map(|it| { + it.unwrap_or_default() + .map(|include| { + if !include.file.contains("SUITE") { + Some(include) + } else { + None + } + }) + .unwrap_or_default() + }) + .collect(); + + all_includes.sort_by(|a, b| a.file.cmp(&b.file)); + if let Some(to) = to { + let to_path = to.join("includes.json"); + let mut output = File::create(to_path)?; + dump_include_resolution(&all_includes, &mut output); + } else { + let mut output = std::io::stdout(); + dump_include_resolution(&all_includes, &mut output); + }; + Ok(()) +} + +fn is_project_file(vfs: &Vfs, db: &Analysis, id: FileId) -> bool { + let root_abs = &db.project_data(id).unwrap().unwrap().root_dir; + let path = vfs.file_path(id); + let path = path.as_path().unwrap(); + path.starts_with(root_abs) +} + +fn dump_include_resolution(includes: &[Includes], to: &mut impl std::io::Write) { + if let Ok(str) = serde_json::to_string(includes) { + _ = to.write(str.as_bytes()); + } +} diff --git a/crates/elp/src/bin/eqwalizer_cli.rs b/crates/elp/src/bin/eqwalizer_cli.rs new file mode 100644 index 0000000000..0ad53b2c1e --- /dev/null +++ b/crates/elp/src/bin/eqwalizer_cli.rs @@ -0,0 +1,457 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::fs; +use std::io; +use std::path::Path; + +use anyhow::anyhow; +use anyhow::bail; +use anyhow::Context; +use anyhow::Result; +use elp::build; +use elp::build::load; +use elp::build::types::LoadResult; +use elp::cli::Cli; +use elp_ide::elp_ide_db::elp_base_db::FileId; +use elp_ide::elp_ide_db::elp_base_db::FileSource; +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_ide::elp_ide_db::EqwalizerDiagnostics; +use elp_ide::elp_ide_db::EqwalizerStats; +use elp_ide::erlang_service; +use elp_ide::Analysis; +use elp_project_model::AppName; +use elp_project_model::AppType; +use elp_project_model::DiscoverConfig; +use elp_project_model::ProjectBuildData; +use fxhash::FxHashMap; +use indicatif::ParallelProgressIterator; +use itertools::Itertools; +use rayon::prelude::*; + +use crate::args::Eqwalize; +use crate::args::EqwalizeAll; +use crate::args::EqwalizeApp; +use crate::args::EqwalizePassthrough; +use crate::args::EqwalizeStats; +use crate::args::EqwalizeTarget; +use crate::erlang_service_cli; +use crate::reporting; +use crate::reporting::Reporter; + +/// Max parallel eqWAlizer tasks. +/// +/// Since eqWAlizer is frequently limited by memory, this can't be fully parallel +const MAX_EQWALIZER_TASKS: usize = 4; + +/// Thread stack size for eqWAlizer tasks, in bytes. +/// +/// Due to inefficient encoding of lists, the default stack size of 2MiB may not be +/// enough for some generated modules. +const THREAD_STACK_SIZE: usize = 10_000_000; + +struct EqwalizerInternalArgs<'a> { + analysis: &'a Analysis, + loaded: &'a LoadResult, + file_ids: Vec, + reporter: &'a mut dyn reporting::Reporter, +} + +pub fn eqwalize_module(args: &Eqwalize, cli: &mut dyn Cli) -> Result<()> { + let config = DiscoverConfig::new(args.rebar, &args.profile); + let loaded = load::load_project_at(cli, &args.project, config, IncludeOtp::Yes)?; + build::compile_deps(&loaded, cli)?; + do_eqwalize_module(args, &loaded, cli) +} + +pub fn do_eqwalize_module(args: &Eqwalize, loaded: &LoadResult, cli: &mut dyn Cli) -> Result<()> { + let analysis = &loaded.analysis(); + let file_id = analysis + .module_file_id(loaded.project_id, &args.module)? + .with_context(|| format!("Module {} not found", &args.module))?; + let reporter = &mut reporting::PrettyReporter::new(analysis, &loaded, cli); + eqwalize(EqwalizerInternalArgs { + analysis, + loaded: &loaded, + file_ids: vec![file_id], + reporter, + }) +} + +pub fn eqwalize_all(args: &EqwalizeAll, cli: &mut dyn Cli) -> Result<()> { + let config = DiscoverConfig::new(args.rebar, &args.profile); + let loaded = load::load_project_at(cli, &args.project, config, IncludeOtp::Yes)?; + build::compile_deps(&loaded, cli)?; + do_eqwalize_all(args, &loaded, cli) +} + +pub fn do_eqwalize_all(args: &EqwalizeAll, loaded: &LoadResult, cli: &mut dyn Cli) -> Result<()> { + let analysis = &loaded.analysis(); + let module_index = analysis.module_index(loaded.project_id)?; + let include_generated = args.include_generated; + let pb = cli.progress(module_index.len_own() as u64, "Gathering modules"); + let file_ids: Vec = module_index + .iter_own() + .par_bridge() + .progress_with(pb.clone()) + .map_with(analysis.clone(), |analysis, (_name, _source, file_id)| { + if should_eqwalize(analysis, file_id, include_generated) { + Some(file_id) + } else { + None + } + }) + .flatten() + .collect(); + pb.finish(); + + let mut json_reporter; + let mut pretty_reporter; + + let reporter: &mut dyn Reporter = match args.format { + None => { + pretty_reporter = reporting::PrettyReporter::new(analysis, &loaded, cli); + &mut pretty_reporter + } + Some(_) => { + json_reporter = reporting::JsonReporter::new(analysis, &loaded, cli); + &mut json_reporter + } + }; + + advise_on_suite_modules_that_should_not_be_opted_in(&loaded, analysis, reporter)?; + eqwalize(EqwalizerInternalArgs { + analysis, + loaded: &loaded, + file_ids, + reporter, + }) +} + +pub fn eqwalize_app(args: &EqwalizeApp, cli: &mut dyn Cli) -> Result<()> { + let config = DiscoverConfig::new(args.rebar, &args.profile); + let loaded = load::load_project_at(cli, &args.project, config, IncludeOtp::Yes)?; + build::compile_deps(&loaded, cli)?; + do_eqwalize_app(args, &loaded, cli) +} + +pub fn do_eqwalize_app(args: &EqwalizeApp, loaded: &LoadResult, cli: &mut dyn Cli) -> Result<()> { + let analysis = &loaded.analysis(); + let module_index = analysis.module_index(loaded.project_id)?; + let include_generated = args.include_generated; + let file_ids: Vec = module_index + .iter_own() + .filter_map(|(_name, _source, file_id)| { + if analysis.file_app_name(file_id).ok()? == Some(AppName(args.app.clone())) + && should_eqwalize(analysis, file_id, include_generated) + { + Some(file_id) + } else { + None + } + }) + .collect(); + let mut reporter = reporting::PrettyReporter::new(analysis, &loaded, cli); + eqwalize(EqwalizerInternalArgs { + analysis, + loaded: &loaded, + file_ids, + reporter: &mut reporter, + }) +} + +pub fn eqwalize_target(args: &EqwalizeTarget, cli: &mut dyn Cli) -> Result<()> { + let config = DiscoverConfig::buck(); + let loaded = load::load_project_at(cli, &args.project, config, IncludeOtp::Yes)?; + + let buck = match &loaded.project.project_build_data { + ProjectBuildData::Buck(buck) => buck, + _ => bail!("only buck project supported"), + }; + let (_, target) = args.target.split_once("//").unwrap_or(("", &args.target)); + let buck_target = target.strip_suffix("/...").unwrap_or(target); + let buck_target = buck_target.strip_suffix(":").unwrap_or(buck_target); + + let analysis = &loaded.analysis(); + let include_generated = args.include_generated; + let mut file_ids: Vec = Default::default(); + let mut at_least_one_found = false; + let exact_match = buck_target.contains(":"); + for (name, target) in &buck.target_info.targets { + let (_, name) = name.split_once("//").unwrap(); + let matches = if exact_match { + name == buck_target + } else { + name.starts_with(buck_target) + }; + if matches { + for src in &target.src_files { + let vfs_path = VfsPath::from(src.clone()); + if let Some(file_id) = loaded.vfs.file_id(&vfs_path) { + at_least_one_found = true; + if should_eqwalize(analysis, file_id, include_generated) { + file_ids.push(file_id); + } + } + } + } + } + match (file_ids.is_empty(), at_least_one_found) { + (true, true) => bail!("Eqwalizer is disabled for all source files in given target"), + (true, false) => bail!( + r###"Can't find any source files for given target. +You can verify your target with: buck2 targets {0} +Examples of valid targets: +elp eqwalize-target //erl/util/... #all targets listed in buck2 targets //erl/util/... +elp eqwalize-target waserver//erl/chatd #all targets listed in buck2 targets waserver//erl/chatd/... +elp eqwalize-target //erl/chatd:chatd #concrete target: buck2 targets waserver//erl/chatd:chatd +elp eqwalize-target erl/chatd #same as //erl/chatd/... but enables shell completion + "###, + args.target + ), + _ => (), + }; + + let mut reporter = reporting::PrettyReporter::new(analysis, &loaded, cli); + eqwalize(EqwalizerInternalArgs { + analysis, + loaded: &loaded, + file_ids, + reporter: &mut reporter, + }) +} + +pub fn eqwalize_passthrough(args: &EqwalizePassthrough, cli: &mut dyn Cli) -> Result<()> { + let config = DiscoverConfig::new(!args.buck, &args.profile); + let loaded = load::load_project_at(cli, &args.project, config, IncludeOtp::No)?; + build::compile_deps(&loaded, cli)?; + + let ast_dir = loaded.project.root().join("_build").join("elp").join("ast"); + + ensure_empty_directory_exists(&ast_dir)?; + let parse_diagnostics = erlang_service_cli::do_parse_all( + cli, + &loaded, + ast_dir.as_ref(), + erlang_service::Format::OffsetEtf, + &None, + args.buck, + )?; + if !parse_diagnostics.is_empty() { + writeln!( + cli, + "{}", + reporting::format_json_parse_error(&parse_diagnostics) + ) + .unwrap(); + bail!("Aborting because there was an error parsing"); + } + + let status = loaded.analysis().eqwalizer().passthrough( + args.args.as_ref(), + loaded.project.build_info_file().unwrap().as_ref(), + ast_dir.as_ref(), + )?; + + let code = status + .code() + .ok_or_else(|| anyhow!("No return code for {:?}", args.args))?; + std::process::exit(code); +} + +pub fn eqwalize_stats(args: &EqwalizeStats, cli: &mut dyn Cli) -> Result<()> { + let config = DiscoverConfig::new(args.rebar, &args.profile); + let loaded = load::load_project_at(cli, &args.project, config, IncludeOtp::Yes)?; + build::compile_deps(&loaded, cli)?; + let analysis = &loaded.analysis(); + let module_index = analysis.module_index(loaded.project_id)?; + let include_generated = args.include_generated; + let project_id = loaded.project_id; + let pb = cli.progress(module_index.len_own() as u64, "Computing stats"); + let stats: FxHashMap<&str, EqwalizerStats> = module_index + .iter_own() + .par_bridge() + .progress_with(pb.clone()) + .map_with(analysis.clone(), |analysis, (name, _source, file_id)| { + if should_eqwalize(analysis, file_id, include_generated) { + analysis + .eqwalizer_stats(project_id, file_id) + .expect("cancelled") + .map(|stats| (name.as_str(), (*stats).clone())) + } else { + None + } + }) + .flatten() + .collect(); + pb.finish(); + cli.write(serde_json::to_string(&stats)?.as_bytes())?; + Ok(()) +} + +fn eqwalize( + EqwalizerInternalArgs { + analysis, + loaded, + file_ids, + reporter, + }: EqwalizerInternalArgs, +) -> Result<()> { + if file_ids.is_empty() { + 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| { + let chunk_size = (files_count + MAX_EQWALIZER_TASKS - 1) / MAX_EQWALIZER_TASKS; + let pool = rayon::ThreadPoolBuilder::new() + .stack_size(THREAD_STACK_SIZE) + .build() + .unwrap(); + let project_id = loaded.project_id; + pool.install(|| { + file_ids + .chunks(chunk_size) + .par_bridge() + .map_with(analysis, move |analysis, file_ids| { + analysis + .eqwalizer_diagnostics(project_id, file_ids.to_vec()) + .expect("cancelled") + }) + .fold(EqwalizerDiagnostics::default, |acc, output| { + acc.combine(&*output) + }) + .reduce(EqwalizerDiagnostics::default, |acc, other| { + acc.combine(&other) + }) + }) + }); + let eqwalized = pb.position(); + pb.finish(); + match output { + EqwalizerDiagnostics::Diagnostics(diagnostics_by_module) => { + for (module, diagnostics) in diagnostics_by_module + .into_iter() + .sorted_by(|(name1, _), (name2, _)| Ord::cmp(name1, name2)) + { + let file_id = analysis + .module_index(loaded.project_id)? + .file_for_module(module.as_str()) + .with_context(|| format!("module {} not found", module))?; + reporter.write_eqwalizer_diagnostics(file_id, &diagnostics)?; + } + if analysis.eqwalizer().shell { + reporter.write_stats(eqwalized, files_count as u64)?; + } + reporter.write_error_count()?; + Ok(()) + } + EqwalizerDiagnostics::NoAst { module } => { + if let Some(file_id) = analysis.module_file_id(loaded.project_id, &module)? { + let parse_diagnostics = erlang_service_cli::do_parse_one( + analysis, + None, + file_id, + erlang_service::Format::OffsetEtf, + )?; + // The cached parse errors must be non-empty otherwise we wouldn't have `NoAst` + assert!(!parse_diagnostics.is_empty()); + reporter.write_parse_diagnostics(&parse_diagnostics)?; + Ok(()) + } else { + bail!( + "Could not type-check because module {} was not found", + module + ) + } + } + EqwalizerDiagnostics::Error(error) => { + bail!("Could not eqwalize: {}", error) + } + } +} + +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, erlang_service::Format::OffsetEtf); + }); + pb.finish(); +} + +fn should_eqwalize(analysis: &Analysis, file_id: FileId, include_generated: bool) -> bool { + let is_in_app = analysis.file_app_type(file_id).ok() == Some(Some(AppType::App)); + is_in_app + && analysis + .is_eqwalizer_enabled(file_id, include_generated) + .unwrap() +} + +/// should conform to contract at the top of this Rust file +fn advise_on_suite_modules_that_should_not_be_opted_in( + loaded: &LoadResult, + analysis: &Analysis, + reporter: &mut dyn reporting::Reporter, +) -> Result<()> { + let project_id = loaded.project_id; + let module_index = analysis.module_index(project_id).unwrap(); + let mut enabled_files: Vec<_> = module_index + .iter_own() + .par_bridge() + .map_with(analysis.clone(), |analysis, (name, source, file_id)| { + let enabled = analysis + .is_eqwalizer_enabled(file_id, false) + .unwrap_or(false); + (name, source, file_id, enabled) + }) + .filter_map(|(name, source, file_id, enabled)| { + if enabled { + Some((name, source, file_id)) + } else { + None + } + }) + .filter(|(name, source, _file_id)| is_test_suite(name, *source)) + .map(|(name, _source, file_id)| (name, file_id)) + .collect(); + + enabled_files.sort_by(|(name1, _), (name2, _)| name1.cmp(name2)); + + for (_name, file_id) in enabled_files.iter() { + let description = "Please remove `-typing([eqwalizer])`. SUITE modules are not checked when eqWAlizing a project."; + reporter.write_file_advice(*file_id, description.to_string())?; + } + Ok(()) +} + +/// Approximation: assumes that `tests + test helpers == extra` +fn is_test_suite_or_test_helper(source: FileSource) -> bool { + source == FileSource::Extra +} + +// Hacky, relies on that we use Common Test specifically, where test modules end in _SUITE. +// We can do something better when we switch to Buck instead of Rebar +fn is_test_suite(name: &ModuleName, source: FileSource) -> bool { + name.ends_with("_SUITE") && is_test_suite_or_test_helper(source) +} + +fn ensure_empty_directory_exists(path: impl AsRef) -> io::Result<()> { + let path = path.as_ref(); + fs::create_dir_all(path)?; + fs::remove_dir_all(path)?; + fs::create_dir(path) +} diff --git a/crates/elp/src/bin/erlang_service_cli.rs b/crates/elp/src/bin/erlang_service_cli.rs new file mode 100644 index 0000000000..5dc54e6d95 --- /dev/null +++ b/crates/elp/src/bin/erlang_service_cli.rs @@ -0,0 +1,164 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::fs; +use std::path::Path; +use std::str; + +use anyhow::Context; +use anyhow::Error; +use anyhow::Result; +use elp::build; +use elp::build::load; +use elp::build::types::LoadResult; +use elp::cli::Cli; +use elp::convert; +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::LineCol; +use elp_ide::erlang_service; +use elp_ide::erlang_service::DiagnosticLocation; +use elp_ide::erlang_service::Location; +use elp_ide::Analysis; +use elp_ide::TextRange; +use elp_log::timeit; +use elp_project_model::AppType; +use elp_project_model::DiscoverConfig; +use indicatif::ParallelProgressIterator; +use rayon::prelude::*; + +use crate::args::ParseAll; +use crate::reporting; +use crate::reporting::ParseDiagnostic; + +pub fn parse_all(args: &ParseAll, cli: &mut dyn Cli) -> Result<()> { + let config = DiscoverConfig::new(!args.buck, &args.profile); + let loaded = load::load_project_at(cli, &args.project, config, IncludeOtp::No)?; + build::compile_deps(&loaded, cli)?; + fs::create_dir_all(&args.to)?; + let format = erlang_service::Format::OffsetEtf; + + let parse_diagnostics = do_parse_all(cli, &loaded, &args.to, format, &args.module, args.buck)?; + if !parse_diagnostics.is_empty() { + writeln!( + cli, + "{}", + reporting::format_raw_parse_error(&parse_diagnostics) + ) + .unwrap(); + return Err(Error::msg("Parsing failed with diagnostics.")); + } + Ok(()) +} + +pub fn do_parse_all( + cli: &dyn Cli, + loaded: &LoadResult, + to: &Path, + format: erlang_service::Format, + module: &Option, + buck: bool, +) -> Result> { + let file_cnt = loaded.vfs.len(); + let _timer = timeit!("parse {} files", file_cnt); + + let pb = cli.progress(file_cnt as u64, "Parsing modules"); + let mut result = loaded + .analysis() + .module_index(loaded.project_id)? + .iter_own() + .par_bridge() + .progress_with(pb) + .map_with( + loaded.analysis(), + move |db, (name, _, file_id)| -> Result> { + let empty = Ok(vec![]); + match module { + Some(module) if module != name.as_str() => { + return empty; + } + _ => {} + } + if !buck && db.file_app_type(file_id).ok() == Some(Some(AppType::Dep)) { + return empty; + } + + do_parse_one(db, Some((name, to)), file_id, format) + .with_context(|| format!("Failed to parse module {}", name.as_str())) + }, + ) + .try_reduce(Vec::new, |mut acc, diagnostics| { + acc.extend(diagnostics); + Ok(acc) + })?; + result.sort_by(|f, l| f.relative_path.cmp(&l.relative_path)); + Ok(result) +} + +pub fn do_parse_one( + db: &Analysis, + to: Option<(&str, &Path)>, + file_id: FileId, + format: erlang_service::Format, +) -> Result> { + if format == erlang_service::Format::Text { + panic!("text format is for test purposes only!") + } + + let result = db.module_ast(file_id, format)?; + if result.is_ok() { + if let Some((name, to)) = to { + let to_path = to.join(format!("{}.etf", name)); + fs::write(to_path, &*result.ast)?; + } + Ok(vec![]) + } else { + let line_index = db.line_index(file_id)?; + let root_dir = &db.project_data(file_id)?.unwrap().root_dir; + let diagnostic = result + .errors + .iter() + .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 { + None => (None, convert::position(&line_index, 0.into()).line + 1), + Some(DiagnosticLocation::Normal(Location::TextRange(range))) => ( + Some(range), + convert::position(&line_index, range.start()).line + 1, + ), + Some(DiagnosticLocation::Normal(Location::StartLocation(start))) => { + let offset = line_index.offset(LineCol { + line: start.line - 1, + col_utf16: start.column - 1, + }); + (Some(TextRange::empty(offset)), start.line) + } + Some(DiagnosticLocation::Included { + directive_location, + error_location: _, + }) => ( + Some(directive_location), + convert::position(&line_index, directive_location.start()).line + 1, + ), + }; + ParseDiagnostic { + file_id, + relative_path: relative_path.to_owned(), + line_num, + msg: err.msg.to_owned(), + range, + } + }) + .collect(); + Ok(diagnostic) + } +} + +// --------------------------------------------------------------------- diff --git a/crates/elp/src/bin/lint_cli.rs b/crates/elp/src/bin/lint_cli.rs new file mode 100644 index 0000000000..5cf7698795 --- /dev/null +++ b/crates/elp/src/bin/lint_cli.rs @@ -0,0 +1,667 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +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 anyhow::bail; +use anyhow::Result; +use elp::build::load; +use elp::build::types::LoadResult; +use elp::cli::Cli; +use elp::convert; +use elp::document::Document; +use elp::otp_file_to_ignore; +use elp_ide::diagnostics; +use elp_ide::diagnostics::DiagnosticsConfig; +use elp_ide::diff::diff_from_textedit; +use elp_ide::diff::DiffRange; +use elp_ide::elp_ide_assists::Assist; +use elp_ide::elp_ide_db::elp_base_db::AbsPath; +use elp_ide::elp_ide_db::elp_base_db::Change; +use elp_ide::elp_ide_db::elp_base_db::FileId; +use elp_ide::elp_ide_db::elp_base_db::FilePosition; +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::Vfs; +use elp_ide::elp_ide_db::elp_base_db::VfsPath; +use elp_ide::elp_ide_db::LineCol; +use elp_ide::Analysis; +use elp_ide::AnalysisHost; +use elp_project_model::AppName; +use elp_project_model::AppType; +use elp_project_model::DiscoverConfig; +use fxhash::FxHashSet; +use indicatif::ParallelProgressIterator; +use rayon::prelude::ParallelBridge; +use rayon::prelude::ParallelIterator; + +use crate::args::Lint; +use crate::reporting; + +pub fn lint_all(args: &Lint, cli: &mut dyn Cli) -> Result<()> { + log::info!("Loading project at: {:?}", args.project); + let config = DiscoverConfig::new(args.rebar, &args.profile); + let mut loaded = load::load_project_at(cli, &args.project, config, IncludeOtp::Yes)?; + + if let Some(to) = &args.to { + fs::create_dir_all(to)? + }; + + do_codemod(cli, &mut loaded, args) +} + +/// Changed lines, from and to +type ChangeRange = (u32, u32); + +fn do_parse_all( + cli: &dyn Cli, + analysis: &Analysis, + project_id: &ProjectId, + config: &DiagnosticsConfig, + include_generated: bool, + ignore_apps: &[String], +) -> Result< + Vec<( + String, + FileId, + Vec, + Vec, + )>, +> { + let module_index = analysis.module_index(*project_id).unwrap(); + let module_iter = module_index.iter_own(); + + let ignored_apps: FxHashSet>> = ignore_apps + .iter() + .map(|name| Some(Some(AppName(name.to_string())))) + .collect(); + let pb = cli.progress(module_iter.len() as u64, "Parsing modules (parallel)"); + + 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()) + { + do_parse_one( + db, + config, + file_id, + module_name.as_str(), + include_generated, + Vec::default(), + ) + .unwrap() + } else { + None + } + }, + ) + .flatten() + .collect()) +} + +fn do_parse_one( + db: &Analysis, + config: &DiagnosticsConfig, + file_id: FileId, + name: &str, + include_generated: bool, + changes: Vec, +) -> Result< + Option<( + String, + FileId, + Vec, + Vec, + )>, +> { + let diagnostics = db.diagnostics(config, file_id, include_generated)?; + if !diagnostics.is_empty() { + let res = (name.to_string(), file_id, diagnostics, changes); + Ok(Some(res)) + } else { + Ok(None) + } +} + +// --------------------------------------------------------------------- + +pub fn do_codemod(cli: &mut dyn Cli, loaded: &mut LoadResult, args: &Lint) -> Result<()> { + // First check if we are doing a codemod. We need to have a whole + // bunch of args set + match args { + Lint { + project: _, + module: _, + file: _, + to: _, + print_diags: _, + experimental_diags: _, + profile: _, + rebar: _, + include_generated: _, + apply_fix: _, + recursive, + in_place, + diagnostic_filter: Some(diagnostic_filter), + line_from, + line_to, + ignore_apps, + format: _, + } => { + let mut cfg = DiagnosticsConfig::default(); + cfg.disable_experimental = args.experimental_diags; + // Declare outside the block so it has the right lifetime for filter_diagnostics + let res; + let mut diags = { + // We put this in its own block so they analysis is + // freed before we apply lints. To apply lints + // recursively, we need to update the underlying + // ananalysis_host, which will deadlock if there is + // still an active analysis(). + let analysis = loaded.analysis(); + + let (file_id, name) = match &args.module { + Some(module) => { + if args.is_format_normal() { + writeln!(cli, "module specified: {}", module)?; + } + let file_id = analysis.module_file_id(loaded.project_id, module)?; + (file_id, analysis.module_name(file_id.unwrap())?) + } + None => match &args.file { + Some(file_name) => { + if args.is_format_normal() { + writeln!(cli, "file specified: {}", file_name)?; + } + let path_buf = fs::canonicalize(file_name).unwrap(); + 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())), + path_buf + .as_path() + .file_name() + .map(|n| ModuleName::new(n.to_str().unwrap())), + ) + } + None => (None, None), + }, + }; + + res = match (file_id, name) { + (None, _) => do_parse_all( + cli, + &analysis, + &loaded.project_id, + &cfg, + args.include_generated, + ignore_apps, + )?, + (Some(file_id), Some(name)) => do_parse_one( + &analysis, + &cfg, + file_id, + &name, + args.include_generated, + vec![], + )? + .map_or(vec![], |x| vec![x]), + (Some(file_id), _) => { + panic!("Could not get name from file_id for {:?}", file_id) + } + }; + + filter_diagnostics( + &analysis, + &args.module, + Some(diagnostic_filter), + *line_from, + *line_to, + &res, + )? + }; + if diags.is_empty() { + if args.is_format_normal() { + writeln!(cli, "No diagnostics reported")?; + } + } else { + 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 &diags { + if args.print_diags { + for diag in diags { + match diag.severity { + diagnostics::Severity::Error => { + 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); + print_diagnostic_json( + diag, + &analysis, + *file_id, + &relative_path, + cli, + )?; + } + } + } + } else { + writeln!(cli, "Diagnostics reported in {} modules:", diags.len())?; + + for (name, file_id, diags) in &diags { + writeln!(cli, " {}: {}", name, diags.len())?; + if args.print_diags { + for diag in diags { + match diag.severity { + diagnostics::Severity::Error => { + err_in_diag = true; + } + _ => {} + }; + print_diagnostic(diag, &loaded.analysis(), *file_id, cli)?; + } + } + } + } + if args.apply_fix { + let mut changed_files = FxHashSet::default(); + let mut lints = Lints::new( + &mut loaded.analysis_host, + &cfg, + &mut loaded.vfs, + &args.to, + args.include_generated, + *in_place, + *recursive, + &mut changed_files, + diags, + ); + match lints.apply_relevant_fixes(args.is_format_normal(), cli) { + Ok(_) => {} + Err(err) => { + writeln!(cli, "Apply fix failed: {:?}", err).ok(); + } + }; + } + if err_in_diag { + bail!("Errors found") + } + } + Ok(()) + } + _ => bail!("Expecting --diagnostic-filter"), + } +} + +fn print_diagnostic( + diag: &diagnostics::Diagnostic, + analysis: &Analysis, + file_id: FileId, + cli: &mut dyn Cli, +) -> Result<(), anyhow::Error> { + let line_index = analysis.line_index(file_id)?; + writeln!(cli, " {}", diag.print(&line_index))?; + Ok(()) +} + +fn print_diagnostic_json( + diagnostic: &diagnostics::Diagnostic, + analysis: &Analysis, + file_id: FileId, + path: &Path, + 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); + writeln!( + cli, + "{}", + serde_json::to_string(&converted_diagnostic).unwrap_or_else(|err| panic!( + "print_diagnostics_json failed for '{:?}': {}", + converted_diagnostic, err + )) + )?; + Ok(()) +} + +fn filter_diagnostics<'a>( + db: &Analysis, + module: &'a Option, + diagnostic_code: Option<&'a String>, + line_from: Option, + line_to: Option, + diags: &'a Vec<( + String, + FileId, + Vec, + Vec, + )>, +) -> Result)>> { + Ok(diags + .clone() + .into_iter() + .filter_map(|(m, file_id, ds, changes)| { + let line_index = db.line_index(file_id).ok()?; + if module.is_none() || &Some(m.to_string()) == module { + let ds2 = ds + .into_iter() + .filter(|d| { + let range = convert::range(&line_index, d.range); + let line = range.start.line; + (diagnostic_code.is_none() || Some(&d.code.to_string()) == diagnostic_code) + && check(&line_from, |l| &line >= l) + && check(&line_to, |l| &line <= l) + && check_changes(&changes, line) + }) + .collect::>(); + if !ds2.is_empty() { + Some((m, file_id, ds2)) + } else { + None + } + } else { + None + } + }) + .collect::>()) +} + +// No changes mean no constraint, so the condition passes. If there +// are changes, the given line must be in at least one of the changed +// ranges. +fn check_changes(changes: &[ChangeRange], line: u32) -> bool { + changes.is_empty() + || changes + .iter() + .any(|(from, to)| line >= *from && line <= *to) +} + +fn check(maybe_constraint: &Option, f: impl FnOnce(&T) -> bool) -> bool { + if let Some(constraint) = maybe_constraint { + f(constraint) + } else { + true + } +} + +struct Lints<'a> { + analysis_host: &'a mut AnalysisHost, + cfg: &'a DiagnosticsConfig<'a>, + vfs: &'a mut Vfs, + to: &'a Option, + include_generated: bool, + in_place: bool, + recursive: bool, + changed_files: &'a mut FxHashSet<(FileId, String)>, + diags: Vec<(String, FileId, Vec)>, +} + +#[derive(Debug)] +struct FixResult { + file_id: FileId, + name: String, + source: String, + changes: Vec, + diff: Option, +} + +const LINT_APPLICATION_RECURSION_LIMIT: i32 = 10; + +impl<'a> Lints<'a> { + pub fn new( + analysis_host: &'a mut AnalysisHost, + cfg: &'a DiagnosticsConfig, + vfs: &'a mut Vfs, + to: &'a Option, + include_generated: bool, + in_place: bool, + recursive: bool, + changed_files: &'a mut FxHashSet<(FileId, String)>, + diags: Vec<(String, FileId, Vec)>, + ) -> Lints<'a> { + Lints { + analysis_host, + cfg, + vfs, + to, + include_generated, + in_place, + recursive, + changed_files, + diags, + } + } + + fn apply_relevant_fixes(&mut self, format_normal: bool, cli: &mut dyn Cli) -> Result<()> { + let mut recursion_limit = LINT_APPLICATION_RECURSION_LIMIT; + loop { + let changes = self.apply_diagnostics_fixes(format_normal, cli)?; + if recursion_limit <= 0 || *(&changes.is_empty()) { + if recursion_limit < 0 { + bail!( + "Hit recursion limit ({}) while applying fixes", + LINT_APPLICATION_RECURSION_LIMIT + ); + } + break; + } + recursion_limit -= 1; + let new_diags: Vec<_> = changes + .into_iter() + .map( + |FixResult { + file_id, + name, + source, + changes, + diff: _, + }| + -> Result< + Option<( + String, + FileId, + Vec, + Vec, + )>, + > { + self.changed_files.insert((file_id, name.clone())); + let path = self.vfs.file_path(file_id); + self.vfs + .set_file_contents(path, Some(source.clone().into_bytes())); + + self.analysis_host.apply_change(Change { + roots: None, + files_changed: vec![(file_id, Some(Arc::new(source)))], + app_structure: None, + }); + + do_parse_one( + &self.analysis_host.analysis(), + &self.cfg, + file_id, + &name, + self.include_generated, + changes, + ) + }, + ) + .collect::>>>()? + .into_iter() + .filter_map(|x| x) + .collect::>(); + self.diags = filter_diagnostics( + &self.analysis_host.analysis(), + &None, + None, // TODO: should we have a set of valid diagnostics codes? + None, // TODO: range + None, // TODO: range + &new_diags, + )?; + if !self.recursive { + break; + } + } + self.changed_files.iter().for_each(|(file_id, name)| { + let bytes = self.vfs.file_contents(*file_id); + let document = Document::from_bytes(bytes.to_vec()); + self.write_fix_result(*file_id, name, &document.content); + }); + Ok(()) + } + + fn apply_diagnostics_fixes( + &self, + format_normal: bool, + cli: &mut dyn Cli, + ) -> Result> { + // Only apply a single fix, then re-parse. This avoids potentially + // conflicting changes. + let changes = self + .diags + .iter() + .flat_map(|(m, file_id, ds)| { + ds.iter().next().map_or(Ok(vec![]), |d| { + self.apply_fixes(m, d, *file_id, format_normal, cli) + }) + }) + .flatten() + .collect::>(); + Ok(changes) + } + + /// Apply any assists included in the diagnostic + fn apply_fixes( + &self, + name: &String, + diagnostic: &diagnostics::Diagnostic, + file_id: FileId, + format_normal: bool, + cli: &mut dyn Cli, + ) -> Result> { + if let Some(fixes) = &diagnostic.fixes { + if format_normal { + writeln!(cli, "---------------------------------------------\n")?; + writeln!(cli, "Applying fix in module '{name}' for")?; + print_diagnostic(diagnostic, &self.analysis_host.analysis(), file_id, cli)?; + } + let changed = fixes + .iter() + .filter_map(|fix| self.apply_one_fix(fix, name)) + .collect::>(); + if format_normal { + changed.iter().for_each(|r| { + if let Some(unified) = &r.diff { + _ = writeln!(cli, "{unified}"); + } + }); + } + Ok(changed) + } else { + bail!("No fixes in {:?}", diagnostic); + } + } + + /// Apply a single assist + fn apply_one_fix(&self, fix: &Assist, name: &String) -> Option { + let source_change = fix.source_change.as_ref()?; + let file_id = *source_change.source_file_edits.keys().next().unwrap(); + let mut actual = self + .analysis_host + .analysis() + .file_text(file_id) + .ok()? + .to_string(); + let original = actual.clone(); + + for edit in source_change.source_file_edits.values() { + // The invariant for a `TextEdit` requires that they + // disjoint and sorted by `delete` + edit.apply(&mut actual); + } + let (diff, unified) = diff_from_textedit(&original, &actual); + let changes = diff + .iter() + .filter_map(|d| form_range_from_diff(&self.analysis_host.analysis(), file_id, d)) + .collect::>(); + + Some(FixResult { + file_id, + name: name.clone(), + source: actual, + changes, + diff: unified, + }) + } + + fn write_fix_result(&self, file_id: FileId, name: &String, actual: &String) -> Option<()> { + Some(if self.in_place { + let file_path = self.vfs.file_path(file_id); + let to_path = file_path.as_path()?; + let mut output = File::create(to_path).ok()?; + write!(output, "{actual}").ok()?; + } else { + if let Some(to) = self.to { + let to_path = to.join(format!("{}.erl", name)); + let mut output = File::create(to_path).ok()?; + write!(output, "{actual}").ok()?; + } else { + return None; + } + }) + } +} + +/// Take the diff location, and expand it to the start and end line of +/// its enclosing form. +fn form_range_from_diff( + analysis: &Analysis, + file_id: FileId, + diff: &DiffRange, +) -> Option { + let line_index = analysis.line_index(file_id).ok()?; + let pos = line_index.offset(LineCol { + line: diff.after_start, + col_utf16: 0, + }); + let range = analysis + .enclosing_text_range(FilePosition { + file_id, + offset: pos, + }) + .ok()??; + let start_line = line_index.line_col(range.start()).line; + let end_line = line_index.line_col(range.end()).line; + Some((start_line, end_line)) +} diff --git a/crates/elp/src/bin/main.rs b/crates/elp/src/bin/main.rs new file mode 100644 index 0000000000..9a878995d4 --- /dev/null +++ b/crates/elp/src/bin/main.rs @@ -0,0 +1,872 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::env; +use std::fs; +use std::io::Write; +use std::path::PathBuf; +use std::process; + +use anyhow::Result; +use bpaf::batteries; +use elp::cli; +use elp::cli::Cli; +use elp::ServerSetup; +use elp_log::timeit; +use elp_log::FileLogger; +use elp_log::Logger; +use lsp_server::Connection; + +mod args; +mod build_info_cli; +mod elp_parse_cli; +mod eqwalizer_cli; +mod erlang_service_cli; +mod lint_cli; +mod reporting; +mod shell; + +// Use jemalloc as the global allocator +#[cfg(not(target_env = "msvc"))] +use jemallocator::Jemalloc; + +use crate::args::Args; + +#[cfg(not(target_env = "msvc"))] +#[global_allocator] +static GLOBAL: Jemalloc = Jemalloc; + +fn main() { + let _timer = timeit!("main"); + let mut cli = cli::Real::default(); + let args = args::args().run(); + let res = try_main(&mut cli, args); + let code = handle_res(res, cli.err()); + process::exit(code); +} + +fn handle_res(result: Result<()>, stderr: &mut dyn Write) -> i32 { + if let Err(err) = result { + writeln!(stderr, "{:#}", err).unwrap(); + 101 + } else { + 0 + } +} + +fn try_main(cli: &mut dyn Cli, args: Args) -> Result<()> { + let logger = setup_logging(args.log_file, args.no_log_buffering)?; + match args.command { + args::Command::RunServer(_) => run_server(logger)?, + args::Command::ParseAll(args) => erlang_service_cli::parse_all(&args, cli)?, + args::Command::ParseAllElp(args) => elp_parse_cli::parse_all(&args, cli)?, + args::Command::Eqwalize(args) => eqwalizer_cli::eqwalize_module(&args, cli)?, + args::Command::EqwalizeAll(args) => eqwalizer_cli::eqwalize_all(&args, cli)?, + args::Command::EqwalizeApp(args) => eqwalizer_cli::eqwalize_app(&args, cli)?, + args::Command::EqwalizeStats(args) => eqwalizer_cli::eqwalize_stats(&args, cli)?, + args::Command::EqwalizeTarget(args) => eqwalizer_cli::eqwalize_target(&args, cli)?, + args::Command::EqwalizePassthrough(args) => { + eqwalizer_cli::eqwalize_passthrough(&args, cli)? + } + args::Command::BuildInfo(args) => build_info_cli::save_build_info(args)?, + args::Command::Lint(args) => lint_cli::lint_all(&args, cli)?, + args::Command::GenerateCompletions(args) => { + let instructions = args::gen_completions(&args.shell); + writeln!(cli, "#Please run this:\n{}", instructions)? + } + args::Command::Version(_) => writeln!(cli, "elp {}", elp::version())?, + args::Command::Shell(args) => shell::run_shell(&args, cli)?, + args::Command::Help() => { + let help = batteries::get_usage(args::args()); + writeln!(cli, "{}", help)? + } + } + + log::logger().flush(); + + Ok(()) +} + +fn setup_logging(log_file: Option, no_buffering: bool) -> Result { + env::set_var("RUST_BACKTRACE", "short"); + + let log_file = match log_file { + Some(path) => { + if let Some(parent) = path.parent() { + let _ = fs::create_dir_all(parent); + } + Some(fs::File::create(path)?) + } + None => None, + }; + let filter = env::var("ELP_LOG").ok(); + let file_logger = FileLogger::new(log_file, no_buffering, filter.as_deref()); + + let logger = Logger::default(); + logger.register_logger("default", Box::new(file_logger)); + logger.install(); + + Ok(logger) +} + +fn run_server(logger: Logger) -> Result<()> { + log::info!("server will start, pid: {}", process::id()); + let (connection, io_threads) = Connection::stdio(); + + ServerSetup::new(connection, logger) + .to_server()? + .main_loop()?; + + io_threads.join()?; + log::info!("server did shut down"); + + Ok(()) +} + +// To run the tests +// cargo test --package elp --bin elp + +#[cfg(test)] +mod tests { + use std::ffi::OsString; + use std::path::Path; + use std::str; + + use bpaf::Args; + use elp::cli::Fake; + use expect_test::expect_file; + use expect_test::ExpectFile; + use tempfile::Builder; + use tempfile::TempDir; + use test_case::test_case; + + use super::*; + + macro_rules! args_vec { + ($($e:expr$(,)?)+) => { + vec![$(OsString::from($e),)+] + } + } + + fn elp(args: Vec) -> (String, String, i32) { + let mut cli = Fake::default(); + let args = Args::from(args.as_slice()); + let args = args::args().run_inner(args).unwrap(); + let res = try_main(&mut cli, args); + let code = handle_res(res, cli.err()); + let (stdout, stderr) = cli.to_strings(); + (stdout, stderr, code) + } + + #[test] + fn etf_files_from_dummy_project_are_generated() { + // Create tmp dir for output, typically /tmp/elp_xxxxxx on unix. + let tmp = Builder::new().prefix("elp_").tempdir().unwrap(); + let outdir = PathBuf::from(tmp.path()); + let (_stdout, stderr, code) = elp(args_vec![ + "parse-all", + "--project", + "../../test_projects/standard", + "--to", + tmp.path(), + ]); + // Now check .etf files were generated. + let exists = |p| outdir.join(p).exists(); + assert!(exists("app_a.etf")); + assert!(exists("app_a_SUITE.etf")); + assert!(exists("app_a_mod2.etf")); + assert!(exists("app_b.etf")); + // The source file has a syntax error, so we don't create an etf + assert!(!exists("parse_error_a_example_bad.etf")); + assert_eq!(code, 0); + assert!(stderr.is_empty()); + } + + fn parse_all_complete(project: &str) -> Result { + // Just check the command returns. + 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", + "--project", + project_path, + "--to", + tmp.path(), + ]); + Ok(code) + } + + fn eqwalize_snapshot(project: &str, module: &str, fast: bool, buck: bool) { + if !buck || cfg!(feature = "buck") { + let mut args = args_vec!["eqwalize", module]; + if !buck { + args.push("--rebar".into()); + } + let (args, path) = add_project(args, project, None); + let fast_str = if fast { "_fast" } else { "" }; + let exp_path = expect_file!(format!( + "../resources/test/{}/eqwalize_{}{}.pretty", + project, module, fast_str + )); + + let (stdout, stderr, code) = elp(args); + match code { + 0 => { + assert_normalised_file(exp_path, &stdout, path); + assert!(stderr.is_empty()); + } + _ => { + assert_normalised_file(exp_path, &stderr, path); + assert!(stdout.is_empty()); + } + } + } + } + + #[test] + fn elp_parse_all_report_compile_error() { + // We just check the process doesn't hang. See T114609762. + let code = parse_all_complete("parse_error").unwrap(); + assert_eq!(code, 101); + } + + #[test_case(false ; "rebar")] + #[test_case(true ; "buck")] + fn eqwalize_diagnostics_match_snapshot_app_a(buck: bool) { + eqwalize_snapshot("standard", "app_a", false, buck); + } + + #[test_case(false ; "rebar")] + #[test_case(true ; "buck")] + fn eqwalize_diagnostics_match_snapshot_app_a_mod2(buck: bool) { + eqwalize_snapshot("standard", "app_a_mod2", true, buck); + } + + #[test_case(false ; "rebar")] + #[test_case(true ; "buck")] + fn eqwalize_diagnostics_match_snapshot_app_a_lists(buck: bool) { + eqwalize_snapshot("standard", "app_a_lists", true, buck); + } + + #[test_case(false ; "rebar")] + #[test_case(true ; "buck")] + fn eqwalize_fails_on_bad_module_name(buck: bool) { + eqwalize_snapshot("standard", "meinong", false, buck); + } + + #[test_case(false ; "rebar")] + #[test_case(true ; "buck")] + fn eqwalize_fails_on_bad_parse(buck: bool) { + eqwalize_snapshot("parse_error", "parse_error_a_bad", false, buck); + } + + #[test_case(false ; "rebar")] + #[test_case(true ; "buck")] + fn eqwalize_fails_on_bad_parse_of_related_module(buck: bool) { + eqwalize_snapshot("parse_error", "parse_error_a_reference_bad", false, buck); + } + + #[test_case(false ; "rebar")] + #[test_case(true ; "buck")] + fn eqwalize_succeeds_even_when_unrelated_module_has_parse_error(buck: bool) { + eqwalize_snapshot("parse_error", "parse_error_a", false, buck); + } + + #[test_case(false ; "rebar")] + #[test_case(true ; "buck")] + fn eqwalize_all_diagnostics_match_snapshot_jsonl(buck: bool) { + simple_snapshot( + args_vec!["eqwalize-all", "--format", "json"], + "standard", + expect_file!("../resources/test/standard/eqwalize_all_diagnostics.jsonl"), + buck, + None, + ); + } + + #[test_case(false ; "rebar")] + #[test_case(true ; "buck")] + fn eqwalize_all_diagnostics_match_snapshot_jsonl_gen(buck: bool) { + simple_snapshot( + args_vec!["eqwalize-all", "--format", "json", "--include-generated"], + "standard", + expect_file!("../resources/test/standard/eqwalize_all_diagnostics_gen.jsonl"), + buck, + None, + ); + } + + #[test_case(false ; "rebar")] + #[test_case(true ; "buck")] + fn eqwalize_all_diagnostics_match_snapshot_pretty(buck: bool) { + simple_snapshot( + args_vec!["eqwalize-all"], + "standard", + expect_file!("../resources/test/standard/eqwalize_all_diagnostics.pretty"), + buck, + None, + ); + } + + #[test_case(false ; "rebar")] + #[test_case(true ; "buck")] + fn eqwalize_app_diagnostics_match_snapshot_pretty(buck: bool) { + simple_snapshot( + args_vec!["eqwalize-app", "app_a",], + "standard", + expect_file!("../resources/test/standard/eqwalize_app_diagnostics.pretty"), + buck, + None, + ); + } + + #[test] + fn eqwalize_target_diagnostics_match_snapshot_pretty() { + if cfg!(feature = "buck") { + simple_snapshot( + args_vec![ + "eqwalize-target", + "//whatsapp/elp/test_projects/standard:app_a", + ], + "standard", + expect_file!("../resources/test/standard/eqwalize_target_diagnostics.pretty"), + true, + None, + ); + } + } + + #[test_case(false ; "rebar")] + #[test_case(true ; "buck")] + fn eqwalize_app_diagnostics_match_snapshot_pretty_gen(buck: bool) { + simple_snapshot( + args_vec!["eqwalize-app", "app_a", "--include-generated",], + "standard", + expect_file!("../resources/test/standard/eqwalize_app_diagnostics_gen.pretty"), + buck, + None, + ); + } + + #[test_case(false ; "rebar")] + #[test_case(true ; "buck")] + fn eqwalize_all_fails_on_bad_parse(buck: bool) { + simple_snapshot( + args_vec!["eqwalize-all", "--format", "json",], + "parse_error", + expect_file!("../resources/test/standard/eqwalize_all_parse_error.jsonl"), + buck, + None, + ); + } + + #[test_case(false ; "rebar")] + #[test_case(true ; "buck")] + fn parse_all_diagnostics1(buck: bool) { + simple_snapshot_expect_error( + args_vec!["parse-elp", "--module", "diagnostics",], + "diagnostics", + expect_file!("../resources/test/diagnostics/parse_all_diagnostics1.stdout"), + buck, + None, + ); + } + + #[test_case(false ; "rebar")] + #[test_case(true ; "buck")] + fn parse_all_diagnostics_hrl(buck: bool) { + simple_snapshot_expect_error( + args_vec!["parse-elp",], + "diagnostics", + expect_file!("../resources/test/diagnostics/parse_all_diagnostics_hrl.stdout"), + buck, + Some("app_a/include/broken_diagnostics.hrl"), + ); + } + + #[test_case(false ; "rebar")] + #[test_case(true ; "buck")] + fn parse_all_diagnostics_escript1(buck: bool) { + simple_snapshot( + args_vec!["parse-elp",], + "diagnostics", + expect_file!("../resources/test/diagnostics/parse_all_diagnostics_escript.stdout"), + buck, + Some("app_a/src/diagnostics.escript"), + ); + } + + #[test_case(false ; "rebar")] + #[test_case(true ; "buck")] + fn parse_all_diagnostics_escript2(buck: bool) { + simple_snapshot_expect_error( + args_vec!["parse-elp",], + "diagnostics", + expect_file!( + "../resources/test/diagnostics/parse_all_diagnostics_errors_escript.stdout" + ), + buck, + Some("app_a/src/diagnostics_errors.escript"), + ); + } + + #[test_case(false ; "rebar")] + #[test_case(true ; "buck")] + fn parse_all_diagnostics_escript3(buck: bool) { + simple_snapshot( + args_vec!["parse-elp",], + "diagnostics", + expect_file!( + "../resources/test/diagnostics/parse_all_diagnostics_warnings_escript.stdout" + ), + buck, + Some("app_a/src/diagnostics_warnings.escript"), + ); + } + + #[test_case(false ; "rebar")] + #[test_case(true ; "buck")] + fn lint_1(buck: bool) { + simple_snapshot_expect_error( + args_vec!["lint", "--module", "lints", "--diagnostic-filter", "P1700",], + "diagnostics", + expect_file!("../resources/test/diagnostics/parse_elp_lint1.stdout"), + buck, + None, + ); + } + + #[test_case(false ; "rebar")] + #[test_case(true ; "buck")] + fn lint_2(buck: bool) { + simple_snapshot_expect_error( + args_vec!["lint", "--module", "app_a", "--diagnostic-filter", "P1700",], + "linter", + expect_file!("../resources/test/linter/parse_elp_lint2.stdout"), + buck, + None, + ); + } + + #[test_case(false ; "rebar")] + #[test_case(true ; "buck")] + fn lint_recursive(buck: bool) { + let tmp_dir = TempDir::new().expect("Could not create temporary directory"); + let tmp_path = tmp_dir.path(); + fs::create_dir_all(tmp_path).expect("Could not create temporary directory path"); + check_lint_fix( + args_vec![ + "lint", + "--module", + "lint_recursive", + "--diagnostic-filter", + "W0007", + "--apply-fix", + "--recursive", + "--experimental", + "--to", + tmp_path, + ], + "diagnostics", + expect_file!("../resources/test/diagnostics/parse_elp_lint_recursive.stdout"), + 0, + buck, + None, + &tmp_path, + Path::new("../resources/test/lint/lint_recursive"), + &[("app_a/src/lint_recursive.erl", "lint_recursive.erl")], + false, + ) + .expect("bad test"); + } + + #[test_case(false ; "rebar")] + #[test_case(true ; "buck")] + fn lint_ignore_apps_a(buck: bool) { + let tmp_dir = TempDir::new().expect("Could not create temporary directory"); + let tmp_path = tmp_dir.path(); + fs::create_dir_all(tmp_path).expect("Could not create temporary directory path"); + check_lint_fix( + args_vec![ + "lint", + "--diagnostic-filter", + "W0010", + "--experimental", + // ignored apps + "app_a", + ], + "linter", + expect_file!("../resources/test/linter/parse_elp_lint_ignore_apps.stdout"), + 0, + buck, + None, + &tmp_path, + Path::new("../resources/test/lint/lint_recursive"), + &[], + false, + ) + .expect("bad test"); + } + + #[test_case(false ; "rebar")] + #[test_case(true ; "buck")] + fn lint_ignore_apps_b(buck: bool) { + let tmp_dir = TempDir::new().expect("Could not create temporary directory"); + let tmp_path = tmp_dir.path(); + fs::create_dir_all(tmp_path).expect("Could not create temporary directory path"); + check_lint_fix( + args_vec![ + "lint", + "--diagnostic-filter", + "W0010", + "--experimental", + // ignored apps + "app_b", + "app_c", + ], + "linter", + expect_file!("../resources/test/linter/parse_elp_lint_ignore_apps_b.stdout"), + 0, + buck, + None, + &tmp_path, + Path::new("../resources/test/lint/lint_recursive"), + &[], + false, + ) + .expect("bad test"); + } + + #[test_case(false ; "rebar")] + #[test_case(true ; "buck")] + fn lint_json_output(buck: bool) { + let tmp_dir = TempDir::new().expect("Could not create temporary directory"); + let tmp_path = tmp_dir.path(); + fs::create_dir_all(tmp_path).expect("Could not create temporary directory path"); + check_lint_fix( + args_vec![ + "lint", + "--diagnostic-filter", + "W0010", + "--experimental", + "--format", + "json", + ], + "linter", + expect_file!("../resources/test/linter/parse_elp_lint_json_output.stdout"), + 0, + buck, + None, + &tmp_path, + Path::new("../resources/test/lint/lint_recursive"), + &[], + false, + ) + .expect("bad test"); + } + + #[test_case(false ; "rebar")] + #[test_case(true ; "buck")] + fn lint_applies_fix_using_to_dir(buck: bool) { + let tmp_dir = TempDir::new().expect("Could not create temporary directory"); + let tmp_path = tmp_dir.path(); + fs::create_dir_all(tmp_path).expect("Could not create temporary directory path"); + check_lint_fix( + args_vec![ + "lint", + "--module", + "lints", + "--diagnostic-filter", + "P1700", + "--to", + tmp_path, + "--apply-fix" + ], + "diagnostics", + expect_file!("../resources/test/diagnostics/parse_elp_lint_fix.stdout"), + 101, + buck, + None, + &tmp_path, + Path::new("../resources/test/lint/head_mismatch"), + &[("app_a/src/lints.erl", "lints.erl")], + false, + ) + .expect("Bad test"); + } + + #[test_case(false ; "rebar")] + #[test_case(true ; "buck")] + fn lint_applies_fix_using_to_dir_json_output(buck: bool) { + let tmp_dir = TempDir::new().expect("Could not create temporary directory"); + let tmp_path = tmp_dir.path(); + fs::create_dir_all(tmp_path).expect("Could not create temporary directory path"); + check_lint_fix( + args_vec![ + "lint", + "--module", + "lints", + "--diagnostic-filter", + "P1700", + "--format", + "json", + "--to", + tmp_path, + "--apply-fix" + ], + "diagnostics", + expect_file!("../resources/test/diagnostics/parse_elp_lint_fix_json.stdout"), + 101, + buck, + None, + &tmp_path, + Path::new("../resources/test/lint/head_mismatch"), + &[("app_a/src/lints.erl", "lints.erl")], + false, + ) + .expect("Bad test"); + } + + #[test] + fn lint_applies_fix_in_place() { + // These tests make changes in the source tree. + + // We manually force them to run sequentially, and no other + // test should access the same test project, to prevent race + // conditions. + + do_lint_applies_fix_in_place(false); + if cfg!(feature = "buck") { + do_lint_applies_fix_in_place(true); + } + } + + fn do_lint_applies_fix_in_place(buck: bool) { + let project = "in_place_tests"; + check_lint_fix( + args_vec![ + "lint", + "--module", + "lints", + "--diagnostic-filter", + "P1700", + "--apply-fix", + "--in-place" + ], + project, + expect_file!("../resources/test/diagnostics/parse_elp_lint_fix.stdout"), + 101, + buck, + None, + Path::new(&project_path(project)), + Path::new("../resources/test/lint/head_mismatch"), + &[("app_a/src/lints.erl", "app_a/src/lints.erl")], + true, + ) + .expect("Bad test"); + } + + #[test] + fn help() { + let args = args::args().run_inner(Args::from(&["--help"])).unwrap_err(); + let expected = expect_file!["../resources/test/help.stdout"]; + let stdout = args.unwrap_stdout(); + expected.assert_eq(&stdout); + } + + #[test] + fn eqwalize_all_help() { + let args = args::args() + .run_inner(Args::from(&["eqwalize-all", "--help"])) + .unwrap_err(); + let expected = expect_file!["../resources/test/eqwalize_all_help.stdout"]; + let stdout = args.unwrap_stdout(); + expected.assert_eq(&stdout); + } + + #[test] + fn parse_all_help() { + let args = args::args() + .run_inner(Args::from(&["parse-all", "--help"])) + .unwrap_err(); + let expected = expect_file!["../resources/test/parse_all_help.stdout"]; + let stdout = args.unwrap_stdout(); + expected.assert_eq(&stdout); + } + + #[test] + fn parse_elp_help() { + let args = args::args() + .run_inner(Args::from(&["parse-elp", "--help"])) + .unwrap_err(); + let expected = expect_file!["../resources/test/parse_elp_help.stdout"]; + let stdout = args.unwrap_stdout(); + expected.assert_eq(&stdout); + } + + #[test] + fn lint_help() { + let args = args::args() + .run_inner(Args::from(&["lint", "--help"])) + .unwrap_err(); + let expected = expect_file!["../resources/test/lint_help.stdout"]; + let stdout = args.unwrap_stdout(); + expected.assert_eq(&stdout); + } + + fn simple_snapshot( + 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); + if !buck { + args.push("--rebar".into()); + } + let (stdout, stderr, code) = elp(args); + assert_eq!( + code, 0, + "failed with exit code: {}\nstdout:\n{}\nstderr:\n{}", + code, stdout, stderr + ); + assert_normalised_file(expected, &stdout, path); + assert_eq!( + stderr.is_empty(), + true, + "expected stderr to be empty, got:\n{}", + stderr + ) + } + } + + fn simple_snapshot_expect_error( + 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); + if !buck { + args.push("--rebar".into()); + } + let (stdout, stderr, code) = elp(args); + assert_eq!( + code, 101, + "Expected exit code 101, got: {}\nstdout:\n{}\nstderr:\n{}", + code, stdout, stderr + ); + assert_normalised_file(expected, &stdout, path); + } + } + + fn check_lint_fix( + 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, + ) -> Result<()> { + if !buck || cfg!(feature = "buck") { + let (mut args, path) = add_project(args, project, file); + if !buck { + args.push("--rebar".into()); + } + let orig_files = files.into_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: {}\nstdout:\n{}\nstderr:\n{}", + code, stdout, stderr + ); + assert_normalised_file(expected, &stdout, path); + 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, project_path: PathBuf) { + let project_path: &str = &project_path.to_string_lossy(); + let normalised = actual.replace(project_path, "{project_path}"); + expected.assert_eq(&normalised); + } + + fn add_project( + mut args: Vec, + project: &str, + file: Option<&str>, + ) -> (Vec, PathBuf) { + let path_str = project_path(project); + let project_path: PathBuf = path_str.clone().into(); + args.push("--project".into()); + args.push(path_str.into()); + if let Some(file) = file { + args.push("--file".into()); + let file_path = project_path.join(file).into(); + args.push(file_path); + } + (args, project_path) + } + + fn project_path(project: &str) -> String { + format!("../../test_projects/{}", project) + } + + struct BackupFiles { + // Restore the first Path to the second + restore: Vec<(PathBuf, PathBuf)>, + } + impl BackupFiles { + fn save_files(project: &str, files: &[&str]) -> Result { + let path_str = project_path(project); + let project_path: PathBuf = path_str.clone().into(); + let mut restore = Vec::default(); + files.iter().for_each(|file| { + let file_path = project_path.join(file); + let bak_file_path = file_path.with_extension("bak"); + assert!(file_path.clone().exists()); + assert!(!bak_file_path.clone().exists()); + fs::copy(file_path.clone(), bak_file_path.clone()).expect("Failed to copy file"); + restore.push((bak_file_path, file_path)); + }); + Ok(BackupFiles { restore }) + } + } + + impl Drop for BackupFiles { + fn drop(&mut self) { + self.restore.iter().for_each(|(from, to)| { + assert!(from.clone().exists()); + fs::copy(from, to).expect("Failed to restore file"); + fs::remove_file(from).expect("Failed to delete file"); + }); + } + } +} diff --git a/crates/elp/src/bin/reporting.rs b/crates/elp/src/bin/reporting.rs new file mode 100644 index 0000000000..77b2e5fc3f --- /dev/null +++ b/crates/elp/src/bin/reporting.rs @@ -0,0 +1,359 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::ops::Range; +use std::path::Path; +use std::path::PathBuf; +use std::str; +use std::time::Instant; + +use anyhow::Context; +use anyhow::Result; +use codespan_reporting::diagnostic::Diagnostic as ReportingDiagnostic; +use codespan_reporting::diagnostic::Label; +use codespan_reporting::files::SimpleFiles; +use codespan_reporting::term; +use codespan_reporting::term::termcolor::Color; +use codespan_reporting::term::termcolor::ColorSpec; +use elp::arc_types; +use elp::build::types::LoadResult; +use elp::cli::Cli; +use elp::convert; +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::VfsPath; +use elp_ide::elp_ide_db::EqwalizerDiagnostic; +use elp_ide::Analysis; +use elp_ide::TextRange; +use indicatif::ProgressBar; +use lazy_static::lazy_static; + +pub trait Reporter { + fn write_eqwalizer_diagnostics( + &mut self, + file_id: FileId, + diagnostics: &[EqwalizerDiagnostic], + ) -> Result<()>; + fn write_parse_diagnostics(&mut self, diagnostics: &[ParseDiagnostic]) -> Result<()>; + fn write_file_advice(&mut self, file_id: FileId, description: String) -> Result<()>; + fn write_error_count(&mut self) -> Result<()>; + fn write_stats(&mut self, count: u64, total: u64) -> Result<()>; + + fn progress(&self, len: u64, prefix: &'static str) -> ProgressBar; +} + +#[derive(Debug, Clone)] +pub struct ParseDiagnostic { + pub file_id: FileId, + pub relative_path: PathBuf, + pub line_num: u32, + pub msg: String, + pub range: Option, +} + +pub struct PrettyReporter<'a> { + analysis: &'a Analysis, + loaded: &'a LoadResult, + cli: &'a mut dyn Cli, + error_count: usize, + start: Instant, +} + +pub struct JsonReporter<'a> { + analysis: &'a Analysis, + loaded: &'a LoadResult, + cli: &'a mut dyn Cli, +} + +impl<'a> PrettyReporter<'a> { + pub fn new(analysis: &'a Analysis, loaded: &'a LoadResult, cli: &'a mut dyn Cli) -> Self { + Self { + analysis, + loaded, + cli, + error_count: 0, + start: Instant::now(), + } + } + + fn get_reporting_data(&self, file_id: FileId) -> Result<(SimpleFiles, usize)> { + let file_path = &self.loaded.vfs.file_path(file_id); + let root_path = &self + .analysis + .project_data(file_id)? + .with_context(|| "could not find project data")? + .root_dir; + let relative_path = get_relative_path(root_path, file_path); + let content = str::from_utf8(self.loaded.vfs.file_contents(file_id)).unwrap(); + let mut files: SimpleFiles = SimpleFiles::new(); + let id = files.add(relative_path.display().to_string(), content); + Ok((files, id)) + } +} + +impl<'a> Reporter for PrettyReporter<'a> { + fn write_eqwalizer_diagnostics( + &mut self, + file_id: FileId, + diagnostics: &[EqwalizerDiagnostic], + ) -> Result<()> { + let (reporting_files, reporting_id) = self.get_reporting_data(file_id)?; + for diagnostic in diagnostics { + let range: Range = + diagnostic.range.start().into()..diagnostic.range.end().into(); + let expr = match &diagnostic.expression { + Some(s) => format!("{}.\n", s), + None => "".to_string(), + }; + + let msg = format!("{}{}\n\nSee {}", expr, diagnostic.message, diagnostic.uri); + let msg_label = Label::primary(reporting_id, range.clone()).with_message(&msg); + let mut labels = vec![msg_label]; + if let Some(s) = &diagnostic.explanation { + let explanation_label = + Label::secondary(reporting_id, range).with_message(format!("\n\n{}", s)); + labels.push(explanation_label); + }; + let d: ReportingDiagnostic = ReportingDiagnostic::error() + .with_message(&diagnostic.code) + .with_labels(labels); + + term::emit(&mut self.cli, &REPORTING_CONFIG, &reporting_files, &d).unwrap(); + } + self.error_count += diagnostics.len(); + Ok(()) + } + + fn write_parse_diagnostics(&mut self, diagnostics: &[ParseDiagnostic]) -> Result<()> { + for diagnostic in diagnostics { + let range = diagnostic.range.unwrap_or_default(); + let range: Range = range.start().into()..range.end().into(); + let (reporting_files, reporting_id) = self.get_reporting_data(diagnostic.file_id)?; + let label = Label::primary(reporting_id, range).with_message(&diagnostic.msg); + let d: ReportingDiagnostic = ReportingDiagnostic::error() + .with_message("parse_error") + .with_labels(vec![label]); + term::emit(&mut self.cli, &REPORTING_CONFIG, &reporting_files, &d).unwrap(); + } + Ok(()) + } + + fn write_file_advice(&mut self, file_id: FileId, description: String) -> Result<()> { + let (reporting_files, reporting_id) = self.get_reporting_data(file_id)?; + let label = Label::primary(reporting_id, 1..2).with_message(&description); + let d: ReportingDiagnostic = ReportingDiagnostic::note() + .with_message("advice") + .with_labels(vec![label]); + term::emit(&mut self.cli, &REPORTING_CONFIG, &reporting_files, &d).unwrap(); + Ok(()) + } + + fn write_error_count(&mut self) -> Result<()> { + if self.error_count == 0 { + self.cli.set_color(&GREEN_COLOR_SPEC)?; + write!(self.cli, "NO ERRORS")?; + self.cli.reset()?; + writeln!(self.cli)?; + } else { + self.cli.set_color(&CYAN_COLOR_SPEC)?; + let noun = if self.error_count == 1 { + "ERROR" + } else { + "ERRORS" + }; + write!(self.cli, "{} {}", self.error_count, noun)?; + self.cli.reset()?; + writeln!(self.cli)?; + } + Ok(()) + } + + fn write_stats(&mut self, count: u64, total: u64) -> Result<()> { + let duration = self.start.elapsed().as_secs(); + self.cli.set_color(&YELLOW_COLOR_SPEC)?; + if count == total { + write!(self.cli, "eqWAlized {} module(s) in {}s", count, duration)?; + } else { + write!( + self.cli, + "eqWAlized {} module(s) ({} cached) in {}s", + count, + total - count, + duration + )?; + } + self.cli.reset()?; + writeln!(self.cli)?; + Ok(()) + } + + fn progress(&self, len: u64, prefix: &'static str) -> ProgressBar { + self.cli.progress(len, prefix) + } +} + +impl<'a> JsonReporter<'a> { + pub fn new(analysis: &'a Analysis, loaded: &'a LoadResult, cli: &'a mut dyn Cli) -> Self { + Self { + analysis, + loaded, + cli, + } + } +} + +impl<'a> Reporter for JsonReporter<'a> { + fn write_eqwalizer_diagnostics( + &mut self, + file_id: FileId, + diagnostics: &[EqwalizerDiagnostic], + ) -> Result<()> { + let line_index = self.analysis.line_index(file_id)?; + let eqwalizer_enabled = self.analysis.is_eqwalizer_enabled(file_id, true).unwrap(); + let file_path = &self.loaded.vfs.file_path(file_id); + let root_path = &self + .analysis + .project_data(file_id)? + .with_context(|| "could not find project data")? + .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, + eqwalizer_enabled, + ); + let diagnostic = serde_json::to_string(&diagnostic)?; + writeln!(self.cli, "{}", diagnostic)?; + } + Ok(()) + } + + fn write_parse_diagnostics(&mut self, diagnostics: &[ParseDiagnostic]) -> Result<()> { + for diagnostic in diagnostics { + let severity = arc_types::Severity::Error; + let diagnostic = arc_types::Diagnostic::new( + diagnostic.relative_path.as_path(), + diagnostic.line_num, + None, + severity, + "ELP".to_string(), + diagnostic.msg.clone(), + None, + ); + let diagnostic = serde_json::to_string(&diagnostic)?; + writeln!(self.cli, "{}", diagnostic)?; + } + Ok(()) + } + + fn write_file_advice(&mut self, file_id: FileId, description: String) -> Result<()> { + let file_path = &self.loaded.vfs.file_path(file_id); + let root_path = &self + .analysis + .project_data(file_id)? + .with_context(|| "could not find project data")? + .root_dir; + let relative_path = get_relative_path(root_path, file_path); + let diagnostic = arc_types::Diagnostic::new( + relative_path, + 1, + None, + arc_types::Severity::Advice, + "ELP".to_string(), + description, + None, + ); + let diagnostic = serde_json::to_string(&diagnostic)?; + writeln!(self.cli, "{}", diagnostic)?; + Ok(()) + } + + fn write_error_count(&mut self) -> Result<()> { + Ok(()) + } + + fn write_stats(&mut self, _count: u64, _total: u64) -> Result<()> { + Ok(()) + } + + fn progress(&self, len: u64, prefix: &'static str) -> ProgressBar { + self.cli.progress(len, prefix) + } +} + +pub fn format_raw_parse_error(errs: &[ParseDiagnostic]) -> String { + errs.iter() + .map(|err| { + format!( + "{}:{} {}", + err.relative_path.display(), + err.line_num, + err.msg, + ) + }) + .collect::>() + .join("\n") +} + +pub fn format_json_parse_error(errs: &[ParseDiagnostic]) -> String { + errs.iter() + .map(|err| { + let severity = arc_types::Severity::Error; + let diagnostic = arc_types::Diagnostic::new( + err.relative_path.as_path(), + err.line_num, + None, + severity, + "ELP".to_string(), + err.msg.clone(), + None, + ); + serde_json::to_string(&diagnostic).unwrap() + }) + .collect::>() + .join("\n") +} + +pub fn get_relative_path<'a>(root: &AbsPath, file: &'a VfsPath) -> &'a Path { + let file = file.as_path().unwrap(); + match file.strip_prefix(root) { + Some(relative) => relative.as_ref(), + None => file.as_ref(), + } +} + +lazy_static! { + static ref REPORTING_CONFIG: term::Config = { + let mut config = codespan_reporting::term::Config::default(); + config + .styles + .primary_label_error + .set_fg(Some(Color::Ansi256(9))); + config.styles.line_number.set_fg(Some(Color::Ansi256(33))); + config.styles.source_border.set_fg(Some(Color::Ansi256(33))); + config + }; + static ref GREEN_COLOR_SPEC: ColorSpec = { + let mut spec = ColorSpec::default(); + spec.set_fg(Some(Color::Green)); + spec + }; + static ref CYAN_COLOR_SPEC: ColorSpec = { + let mut spec = ColorSpec::default(); + spec.set_fg(Some(Color::Cyan)); + spec + }; + static ref YELLOW_COLOR_SPEC: ColorSpec = { + let mut spec = ColorSpec::default(); + spec.set_fg(Some(Color::Yellow)); + spec + }; +} diff --git a/crates/elp/src/bin/shell.rs b/crates/elp/src/bin/shell.rs new file mode 100644 index 0000000000..7ea299aa48 --- /dev/null +++ b/crates/elp/src/bin/shell.rs @@ -0,0 +1,329 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::fmt; +use std::fs; +use std::path::PathBuf; +use std::process::Command; +use std::sync::Arc; + +use anyhow::Result; +use elp::build::load; +use elp::build::types::LoadResult; +use elp::cli::Cli; +use elp::document::Document; +use elp_ide::elp_ide_db::elp_base_db::AbsPathBuf; +use elp_ide::elp_ide_db::elp_base_db::IncludeOtp; +use elp_ide::elp_ide_db::elp_base_db::SourceDatabase; +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_project_model::DiscoverConfig; +use rustyline::error::ReadlineError; +use serde::Deserialize; + +use crate::args::Eqwalize; +use crate::args::EqwalizeAll; +use crate::args::EqwalizeApp; +use crate::args::Shell; +use crate::eqwalizer_cli; + +#[derive(Debug, Clone, Deserialize)] +struct Watchman { + watch: PathBuf, +} + +#[derive(Debug, Clone, Deserialize)] +struct WatchmanClock { + clock: String, +} + +#[derive(Debug, Clone, Deserialize)] +struct WatchmanChanges { + files: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +struct WatchmanFile { + name: String, + exists: bool, +} + +impl Watchman { + fn cmd() -> Command { + Command::new("watchman") + } + + fn new(project: &PathBuf) -> Result { + let mut cmd = Self::cmd(); + cmd.arg("watch-project"); + cmd.arg(project.as_os_str()); + Ok(serde_json::from_slice(&cmd.output()?.stdout)?) + } + + fn get_clock(&self) -> Result { + let mut cmd = Self::cmd(); + cmd.arg("clock"); + cmd.arg(self.watch.as_os_str()); + Ok(serde_json::from_slice(&cmd.output()?.stdout)?) + } + + fn get_changes(&self, from: &WatchmanClock, patterns: Vec<&str>) -> Result { + let mut cmd = Command::new("watchman"); + cmd.arg("since"); + cmd.arg(self.watch.as_os_str()); + cmd.arg(&from.clock); + cmd.args(patterns); + Ok(serde_json::from_slice(&cmd.output()?.stdout)?) + } +} + +#[derive(Debug, Clone)] +enum ShellError { + UnexpectedCommand(String), + UnexpectedOption(String, String), + UnexpectedArg(String, String), + MissingArg(String), +} +impl fmt::Display for ShellError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ShellError::UnexpectedCommand(cmd) => write!(f, "Unexpected command {}", cmd), + ShellError::UnexpectedOption(cmd, arg) => { + write!(f, "Unexpected option {} for command {}", arg, cmd) + } + ShellError::UnexpectedArg(cmd, arg) => { + write!(f, "Unexpected arg {} for command {}", arg, cmd) + } + ShellError::MissingArg(cmd) => write!(f, "Missing arg for command {}", cmd), + } + } +} + +#[derive(Debug, Clone)] +enum ShellCommand { + ShellEqwalize(Eqwalize), + ShellEqwalizeAll(EqwalizeAll), + ShellEqwalizeApp(EqwalizeApp), + Help, + Quit, +} +impl ShellCommand { + fn parse(shell: &Shell, line: String) -> Result, ShellError> { + let project = shell.project.clone(); + let rebar = false; + let profile = "test".to_string(); + let tokens: Vec<&str> = line.split_ascii_whitespace().collect(); + if let [cmd, args @ ..] = &tokens[..] { + let (options, args): (Vec<&str>, Vec<_>) = + args.into_iter().partition(|&&arg| arg.starts_with("-")); + match *cmd { + "help" => return Ok(Some(ShellCommand::Help)), + "eqwalize" => { + if let [option, ..] = options[..] { + return Err(ShellError::UnexpectedOption( + "eqwalize".into(), + option.into(), + )); + } + if let [_, arg, ..] = args[..] { + return Err(ShellError::UnexpectedArg("eqwalize".into(), arg.into())); + } + if let [module] = args[..] { + return Ok(Some(ShellCommand::ShellEqwalize(Eqwalize { + project, + profile, + rebar, + module: module.into(), + }))); + } + return Err(ShellError::MissingArg("eqwalize".into())); + } + "eqwalize-app" => { + let include_generated = options.contains(&"--include-generated"); + if let Some(other) = options + .into_iter() + .find(|&opt| opt != "--include-generated") + { + return Err(ShellError::UnexpectedOption( + "eqwalize-app".into(), + other.into(), + )); + } + if let [_, arg, ..] = args[..] { + return Err(ShellError::UnexpectedArg("eqwalize-app".into(), arg.into())); + } + if let [app] = args[..] { + return Ok(Some(ShellCommand::ShellEqwalizeApp(EqwalizeApp { + project, + profile, + rebar, + app: app.into(), + include_generated, + }))); + } + return Err(ShellError::MissingArg("eqwalize-app".into())); + } + "eqwalize-all" => { + let include_generated = options.contains(&"--include-generated"); + if let Some(other) = options + .into_iter() + .find(|&opt| opt != "--include-generated") + { + return Err(ShellError::UnexpectedOption( + "eqwalize-all".into(), + other.into(), + )); + } + if let [arg, ..] = args[..] { + return Err(ShellError::UnexpectedArg("eqwalize-all".into(), arg.into())); + } + return Ok(Some(ShellCommand::ShellEqwalizeAll(EqwalizeAll { + project, + profile, + rebar, + format: None, + include_generated, + }))); + } + "exit" | "quit" => return Ok(Some(ShellCommand::Quit)), + s => return Err(ShellError::UnexpectedCommand(s.into())), + } + } + Ok(None) + } +} + +pub const HELP: &str = "\ +COMMANDS: + help Print this help + exit Exit the interactive session + quit Exit the interactive session + eqwalize Eqwalize specified module + eqwalize-all Eqwalize all modules in the current project + --include-generated Include generated modules + eqwalize-app Eqwalize all modules in specified application + --include-generated Include generated modules +"; + +// Adapted from elp::server +fn process_changes_to_vfs_store(loaded: &mut LoadResult) -> bool { + let changed_files = loaded.vfs.take_changes(); + + if changed_files.is_empty() { + return false; + } + + let raw_database = loaded.analysis_host.raw_database_mut(); + + for file in &changed_files { + let file_path = loaded.vfs.file_path(file.file_id); + if let Some((_, Some("hrl"))) = file_path.name_and_extension() { + raw_database.set_include_files_revision(raw_database.include_files_revision() + 1); + } + if file.exists() { + let bytes = loaded.vfs.file_contents(file.file_id).to_vec(); + let document = Document::from_bytes(bytes); + raw_database.set_file_text(file.file_id, Arc::new(document.content)); + } else { + raw_database.set_file_text(file.file_id, Default::default()); + }; + } + + if changed_files + .iter() + .any(|file| file.is_created_or_deleted()) + { + let sets = loaded.file_set_config.partition(&loaded.vfs); + for (idx, set) in sets.into_iter().enumerate() { + let root_id = SourceRootId(idx as u32); + for file_id in set.iter() { + raw_database.set_file_source_root(file_id, root_id); + } + let root = SourceRoot::new(set); + raw_database.set_source_root(root_id, Arc::new(root)); + } + } + + true +} + +fn update_changes( + loaded: &mut LoadResult, + watchman: &Watchman, + last_read: &WatchmanClock, +) -> Result { + let vfs = &mut loaded.vfs; + let time = watchman.get_clock()?; + let file_changes = watchman.get_changes(last_read, vec!["**/*.hrl", "**/*.erl"])?; + file_changes.files.into_iter().for_each(|file| { + let path = watchman.watch.join(file.name); + let vfs_path = VfsPath::from(AbsPathBuf::assert(path.clone())); + if !file.exists { + vfs.set_file_contents(vfs_path, None); + } else { + let contents = fs::read(&path).expect(&format!("Cannot read created file {:?}", path)); + vfs.set_file_contents(vfs_path, Some(contents)); + } + }); + process_changes_to_vfs_store(loaded); + Ok(time) +} + +pub fn run_shell(shell: &Shell, cli: &mut dyn Cli) -> Result<()> { + let watchman = Watchman::new(&shell.project) + .map_err(|_err| anyhow::Error::msg( + "Could not find project. Are you in an Erlang project directory, or is one specified using --project?" + ))?; + let config = DiscoverConfig::new(false, &"test".to_string()); + let mut loaded = load::load_project_at(cli, &shell.project, config, IncludeOtp::Yes)?; + loaded.analysis_host.raw_database_mut().in_shell(); + let mut rl = rustyline::DefaultEditor::new()?; + let mut last_read = watchman.get_clock()?; + loop { + let readline = rl.readline("> "); + match readline { + Ok(line) => { + rl.add_history_entry(line.as_str())?; + last_read = update_changes(&mut loaded, &watchman, &last_read)?; + match ShellCommand::parse(shell, line) { + Ok(None) => (), + Ok(Some(ShellCommand::Help)) => write!(cli, "{}", HELP)?, + Ok(Some(ShellCommand::Quit)) => break, + Ok(Some(ShellCommand::ShellEqwalize(eqwalize))) => { + eqwalizer_cli::do_eqwalize_module(&eqwalize, &loaded, cli) + .or_else(|e| writeln!(cli, "Error: {}", e))?; + } + Ok(Some(ShellCommand::ShellEqwalizeApp(eqwalize_app))) => { + eqwalizer_cli::do_eqwalize_app(&eqwalize_app, &loaded, cli) + .or_else(|e| writeln!(cli, "Error: {}", e))?; + } + Ok(Some(ShellCommand::ShellEqwalizeAll(eqwalize_all))) => { + eqwalizer_cli::do_eqwalize_all(&eqwalize_all, &loaded, cli) + .or_else(|e| writeln!(cli, "Error: {}", e))?; + } + Err(err) => write!(cli, "{}\n{}", err, HELP)?, + } + } + Err(ReadlineError::Interrupted) => { + writeln!(cli, "Interrupted")?; + break; + } + Err(ReadlineError::Eof) => { + break; + } + Err(err) => { + writeln!(cli, "Error: {:?}", err)?; + break; + } + } + } + return Ok(()); +} diff --git a/crates/elp/src/build/load.rs b/crates/elp/src/build/load.rs new file mode 100644 index 0000000000..9aa21b0e58 --- /dev/null +++ b/crates/elp/src/build/load.rs @@ -0,0 +1,167 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! Loads a rebar project into a static instance of ELP, +//! without support for incorporating changes +use std::fs; +use std::path::Path; +use std::sync::Arc; + +use anyhow::Result; +use crossbeam_channel::unbounded; +use crossbeam_channel::Receiver; +use elp_ide::elp_ide_db::elp_base_db::loader; +use elp_ide::elp_ide_db::elp_base_db::loader::Handle; +use elp_ide::elp_ide_db::elp_base_db::AbsPathBuf; +use elp_ide::elp_ide_db::elp_base_db::FileSetConfig; +use elp_ide::elp_ide_db::elp_base_db::IncludeOtp; +use elp_ide::elp_ide_db::elp_base_db::ProjectApps; +use elp_ide::elp_ide_db::elp_base_db::ProjectId; +use elp_ide::elp_ide_db::elp_base_db::SourceDatabase; +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::Vfs; +use elp_ide::AnalysisHost; +use elp_project_model::DiscoverConfig; +use elp_project_model::Project; +use elp_project_model::ProjectManifest; + +use crate::build::types::LoadResult; +use crate::cli::Cli; +use crate::reload::ProjectFolders; + +pub fn load_project_at( + cli: &dyn Cli, + root: &Path, + conf: DiscoverConfig, + include_otp: IncludeOtp, +) -> Result { + let root = fs::canonicalize(root)?; + let root = AbsPathBuf::assert(root); + let manifest = ProjectManifest::discover_single(&root, &conf)?; + + log::info!("Discovered project: {:?}", manifest); + let pb = cli.spinner("Loading build info"); + let project = Project::load(manifest)?; + pb.finish(); + + load_project(cli, project, include_otp) +} + +fn load_project(cli: &dyn Cli, project: Project, include_otp: IncludeOtp) -> Result { + let project_id = ProjectId(0); + let (sender, receiver) = unbounded(); + let mut vfs = Vfs::default(); + let mut loader = { + let loader = + vfs_notify::NotifyHandle::spawn(Box::new(move |msg| sender.send(msg).unwrap())); + Box::new(loader) + }; + + let projects = [project.clone()]; + let project_apps = ProjectApps::new(&projects, include_otp); + let folders = ProjectFolders::new(&project_apps); + + let vfs_loader_config = loader::Config { + load: folders.load, + watch: vec![], + version: 0, + }; + loader.set_config(vfs_loader_config); + + let analysis_host = load_database( + cli, + &project_apps, + &folders.file_set_config, + &mut vfs, + &receiver, + )?; + Ok(LoadResult::new( + analysis_host, + vfs, + project_id, + project, + folders.file_set_config, + )) +} + +fn load_database( + cli: &dyn Cli, + project_apps: &ProjectApps, + file_set_config: &FileSetConfig, + vfs: &mut Vfs, + receiver: &Receiver, +) -> Result { + let mut analysis_host = AnalysisHost::default(); + let db = analysis_host.raw_database_mut(); + + let pb = cli.progress(0, "Loading applications"); + + for task in receiver { + match task { + loader::Message::Progress { + n_done, n_total, .. + } => { + pb.set_length(n_total as u64); + pb.set_position(n_done as u64); + if n_done == n_total { + break; + } + } + loader::Message::Loaded { files } => { + for (path, contents) in files { + vfs.set_file_contents(path.into(), contents); + } + } + } + } + + pb.finish(); + + let pb = cli.spinner("Seeding database"); + + let sets = file_set_config.partition(vfs); + for (idx, set) in sets.into_iter().enumerate() { + let root_id = SourceRootId(idx as u32); + for file_id in set.iter() { + db.set_file_source_root(file_id, root_id); + } + let root = SourceRoot::new(set); + db.set_source_root(root_id, Arc::new(root)); + } + + project_apps.app_structure().apply(db); + + let project_id = ProjectId(0); + db.ensure_erlang_service(project_id)?; + let changes = vfs.take_changes(); + for file in changes { + if file.exists() { + let contents = vfs.file_contents(file.file_id).to_vec(); + match String::from_utf8(contents) { + Ok(text) => { + db.set_file_text(file.file_id, Arc::new(text)); + } + Err(err) => { + // Fall back to lossy latin1 loading of files. + // This should only affect files from yaws, and + // possibly OTP that are latin1 encoded. + let contents = err.into_bytes(); + let text = contents.into_iter().map(|byte| byte as char).collect(); + db.set_file_text(file.file_id, Arc::new(text)); + } + } + } + } + + pb.finish(); + + Ok(analysis_host) +} diff --git a/crates/elp/src/build/mod.rs b/crates/elp/src/build/mod.rs new file mode 100644 index 0000000000..6c76f9ae08 --- /dev/null +++ b/crates/elp/src/build/mod.rs @@ -0,0 +1,30 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +pub mod load; +pub mod types; + +use anyhow::Result; +use elp_project_model::ProjectBuildData::Rebar; + +use crate::build::types::LoadResult; +use crate::cli::Cli; + +pub fn compile_deps(loaded: &LoadResult, cli: &dyn Cli) -> Result<()> { + match loaded.project.project_build_data { + Rebar(_) => { + let pb = cli.spinner("Compiling dependencies"); + loaded.project.compile_deps()?; + loaded.update_erlang_service_paths(); + pb.finish(); + } + _ => (), + } + Ok(()) +} diff --git a/crates/elp/src/build/types.rs b/crates/elp/src/build/types.rs new file mode 100644 index 0000000000..e8087740b6 --- /dev/null +++ b/crates/elp/src/build/types.rs @@ -0,0 +1,103 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_ide::elp_ide_db::elp_base_db::FileSetConfig; +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::EqwalizerProgressReporter; +use elp_ide::Analysis; +use elp_ide::AnalysisHost; +use elp_project_model::Project; +use fxhash::FxHashSet; +use indicatif::ProgressBar; +use itertools::Itertools; + +pub const DEFAULT_BUCK_TARGET: &str = "//erl/..."; + +#[derive(Debug)] +pub struct LoadResult { + pub analysis_host: AnalysisHost, + pub vfs: Vfs, + pub project_id: ProjectId, + pub project: Project, + pub file_set_config: FileSetConfig, +} + +impl LoadResult { + pub fn new( + analysis_host: AnalysisHost, + vfs: Vfs, + project_id: ProjectId, + project: Project, + file_set_config: FileSetConfig, + ) -> Self { + LoadResult { + analysis_host, + vfs, + project_id, + project, + file_set_config, + } + } + + pub fn with_eqwalizer_progress_bar( + &self, + pb: ProgressBar, + f: impl FnOnce(Analysis) -> R, + ) -> R { + struct Reporter { + bar: ProgressBar, + current: FxHashSet, + } + + impl EqwalizerProgressReporter for Reporter { + fn start_module(&mut self, module: String) { + self.current.insert(module); + let current = self.current.iter().join(", "); + self.bar.set_message(current); + } + + fn done_module(&mut self, module: &str) { + self.current.remove(module); + self.bar.inc(1); + } + } + + impl Drop for Reporter { + fn drop(&mut self) { + self.bar.set_message("") + } + } + + self.analysis_host + .raw_database() + .set_eqwalizer_progress_reporter(Some(Box::new(Reporter { + bar: pb, + current: Default::default(), + }))); + + let r = f(self.analysis()); + + self.analysis_host + .raw_database() + .set_eqwalizer_progress_reporter(None); + + r + } + + pub fn analysis(&self) -> Analysis { + self.analysis_host.analysis() + } + + pub fn update_erlang_service_paths(&self) { + self.analysis_host + .raw_database() + .update_erlang_service_paths(); + } +} diff --git a/crates/elp/src/cli.rs b/crates/elp/src/cli.rs new file mode 100644 index 0000000000..218f14029f --- /dev/null +++ b/crates/elp/src/cli.rs @@ -0,0 +1,148 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::io::Stderr; +use std::io::Write; +use std::time::Duration; + +use codespan_reporting::term::termcolor::Buffer; +use codespan_reporting::term::termcolor::ColorChoice; +use codespan_reporting::term::termcolor::ColorSpec; +use codespan_reporting::term::termcolor::StandardStream; +use codespan_reporting::term::termcolor::WriteColor; +use indicatif::ProgressBar; +use indicatif::ProgressStyle; + +pub trait Cli: Write + WriteColor { + fn progress(&self, len: u64, prefix: &'static str) -> ProgressBar; + + fn spinner(&self, prefix: &'static str) -> ProgressBar; + + fn err(&mut self) -> &mut dyn Write; +} + +pub struct Real(StandardStream, Stderr); + +impl Default for Real { + fn default() -> Self { + Self( + StandardStream::stdout(ColorChoice::Always), + std::io::stderr(), + ) + } +} + +impl Cli for Real { + fn progress(&self, len: u64, prefix: &'static str) -> ProgressBar { + if len == 1 { + self.spinner(prefix) + } else { + let pb = ProgressBar::new(len); + pb.set_style( + ProgressStyle::with_template(" {prefix:25!} {bar} {pos}/{len} {wide_msg}") + .expect("BUG: invalid template"), + ); + pb.set_prefix(prefix); + pb + } + } + + fn spinner(&self, prefix: &'static str) -> ProgressBar { + let pb = ProgressBar::new_spinner(); + pb.set_style( + ProgressStyle::with_template("{spinner} {prefix} {wide_msg}") + .expect("BUG: invalid template"), + ); + pb.enable_steady_tick(Duration::from_millis(120)); + pb.set_prefix(prefix); + pb + } + + fn err(&mut self) -> &mut dyn Write { + &mut self.1 + } +} + +impl Write for Real { + 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 Real { + 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 { + fn default() -> Self { + Self(Buffer::no_color(), Vec::new()) + } +} + +impl Fake { + pub fn to_strings(self) -> (String, String) { + let stdout = String::from_utf8(self.0.into_inner()).unwrap(); + let stderr = String::from_utf8(self.1).unwrap(); + (stdout, stderr) + } +} + +impl Cli for Fake { + fn progress(&self, _len: u64, _prefix: &str) -> ProgressBar { + ProgressBar::hidden() + } + + fn spinner(&self, _prefix: &str) -> ProgressBar { + ProgressBar::hidden() + } + + fn err(&mut self) -> &mut dyn Write { + &mut self.1 + } +} + +impl Write for Fake { + 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 Fake { + 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() + } +} diff --git a/crates/elp/src/config.rs b/crates/elp/src/config.rs new file mode 100644 index 0000000000..0c943ec255 --- /dev/null +++ b/crates/elp/src/config.rs @@ -0,0 +1,482 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::iter; + +use elp_ide::diagnostics::DiagnosticCode; +use elp_ide::diagnostics::DiagnosticsConfig; +use elp_ide::elp_ide_assists::AssistConfig; +use elp_ide::elp_ide_db::elp_base_db::AbsPathBuf; +use elp_ide::elp_ide_db::helpers::SnippetCap; +use elp_ide::InlayHintsConfig; +use fxhash::FxHashSet; +use lsp_types::ClientCapabilities; +use serde::de::DeserializeOwned; +use serde_json::json; + +// Defines the server-side configuration of ELP. We generate *parts* +// of VS Code's `package.json` config from this. +// +// However, editor specific config, which the server doesn't know +// about, should be specified directly in `package.json`. +// +// To deprecate an option by replacing it with another name use +// `new_name | `old_name` so that we keep parsing the old name. +config_data! { + struct ConfigData { + /// Enable support for AI-based completions. + ai_enable: bool = json! { false }, + /// Whether to show experimental ELP diagnostics that might + /// have more false positives than usual. + diagnostics_enableExperimental: bool = json! { false }, + /// List of ELP diagnostics to disable. + diagnostics_disabled: FxHashSet = json! { [] }, + /// Whether to show function parameter name inlay hints at the call + /// site. + inlayHints_parameterHints_enable: bool = json! { false }, + /// Whether to show Code Lenses in Erlang files. + lens_enable: bool = json! { false }, + /// Whether to show the `Run` lenses. Only applies when + /// `#elp.lens.enable#` is set. + lens_run_enable: bool = json! { false }, + /// Whether to show the `Debug` lenses. Only applies when + /// `#elp.lens.enable#` is set. + lens_debug_enable: bool = json! { false }, + /// Configure LSP-based logging using env_logger syntax. + log: String = json! { "error" }, + /// Whether to show Signature Help. + signatureHelp_enable: bool = json! { false }, + } +} + +impl Default for ConfigData { + fn default() -> Self { + ConfigData::from_json(serde_json::Value::Null) + } +} + +#[derive(Clone, Debug)] +pub struct Config { + pub root_path: AbsPathBuf, + pub caps: ClientCapabilities, + data: ConfigData, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LensConfig { + pub run: bool, + pub debug: bool, +} + +macro_rules! try_ { + ($expr:expr) => { + || -> _ { Some($expr) }() + }; +} +macro_rules! try_or { + ($expr:expr, $or:expr) => { + try_!($expr).unwrap_or($or) + }; +} + +impl Config { + pub fn new(root_path: AbsPathBuf, caps: ClientCapabilities) -> Config { + Config { + root_path, + caps, + data: ConfigData::default(), + } + } + + pub fn update(&mut self, json: serde_json::Value) { + log::info!("updating config from JSON: {:#}", json); + if json.is_null() || json.as_object().map_or(false, |it| it.is_empty()) { + return; + } + self.data = ConfigData::from_json(json); + } + + pub fn did_save_text_document_dynamic_registration(&self) -> bool { + let caps = try_or!( + self.caps.text_document.as_ref()?.synchronization.clone()?, + Default::default() + ); + caps.did_save == Some(true) && caps.dynamic_registration == Some(true) + } + + pub fn did_change_configuration_dynamic_registration(&self) -> bool { + let caps = try_or!( + self.caps.workspace.as_ref()?.did_change_configuration?, + Default::default() + ); + caps.dynamic_registration == Some(true) + } + + pub fn code_action_literals(&self) -> bool { + try_!( + self.caps + .text_document + .as_ref()? + .code_action + .as_ref()? + .code_action_literal_support + .as_ref()? + ) + .is_some() + } + + pub fn code_action_resolve(&self) -> bool { + try_or!( + self.caps + .text_document + .as_ref()? + .code_action + .as_ref()? + .resolve_support + .as_ref()? + .properties + .as_slice(), + &[] + ) + .iter() + .any(|it| it == "edit") + } + + pub fn location_link(&self) -> bool { + try_or!( + self.caps.text_document.as_ref()?.definition?.link_support?, + false + ) + } + + pub fn hierarchical_symbols(&self) -> bool { + try_or!( + self.caps + .text_document + .as_ref()? + .document_symbol + .as_ref()? + .hierarchical_document_symbol_support?, + false + ) + } + + fn experimental(&self, index: &'static str) -> bool { + try_or!( + self.caps.experimental.as_ref()?.get(index)?.as_bool()?, + false + ) + } + + pub fn diagnostics(&self) -> DiagnosticsConfig { + // Look up disabled diagnostics using both label and code. + DiagnosticsConfig::new( + !self.data.diagnostics_enableExperimental, + self.data + .diagnostics_disabled + .iter() + .filter_map(DiagnosticCode::maybe_from_string) + .collect(), + vec![], + ) + } + + pub fn code_action_group(&self) -> bool { + self.experimental("codeActionGroup") + } + + pub fn server_status_notification(&self) -> bool { + // Under experimental umbrella. Rationale: + // - Only used for end-to-end tests for now. + // - Mimic rust-analyzer (at 2021-02-08 revision). + self.experimental("serverStatusNotification") + } + + pub fn lens(&self) -> LensConfig { + LensConfig { + run: self.data.lens_enable && self.data.lens_run_enable, + debug: self.data.lens_enable && self.data.lens_debug_enable, + } + } + + pub fn signature_help(&self) -> bool { + self.data.signatureHelp_enable + } + + pub fn assist(&self) -> AssistConfig { + AssistConfig { + snippet_cap: SnippetCap::new(self.experimental("snippetTextEdit")), + allowed: None, + } + } + + pub fn work_done_progress(&self) -> bool { + try_or!(self.caps.window.as_ref()?.work_done_progress?, false) + } + + pub fn ai_enabled(&self) -> bool { + self.data.ai_enable + } + + pub fn inlay_hints(&self) -> InlayHintsConfig { + InlayHintsConfig { + parameter_hints: self.data.inlayHints_parameterHints_enable, + } + } + + pub fn log_filter(&self) -> elp_log::Builder { + let mut builder = elp_log::Builder::new(); + builder.parse(&self.data.log); + builder + } + + // Used for setting up tests + pub fn ignore_diagnostic(&mut self, diagnostic: DiagnosticCode) { + self.data.diagnostics_disabled.insert(diagnostic.as_code()); + } + + pub fn json_schema() -> serde_json::Value { + ConfigData::json_schema() + } +} + +macro_rules! _config_data { + (struct $name:ident { + $( + $(#[doc=$doc:literal])* + $field:ident $(| $alias:ident)*: $ty:ty = $default:expr, + )* + }) => { + #[allow(non_snake_case)] + #[derive(Debug, Clone)] + struct $name { $($field: $ty,)* } + impl $name { + fn from_json(mut json: serde_json::Value) -> $name { + $name {$( + $field: get_field( + &mut json, + stringify!($field), + None$(.or(Some(stringify!($alias))))*, + $default, + ), + )*} + } + + fn json_schema() -> serde_json::Value { + schema(&[ + $({ + let field = stringify!($field); + let ty = stringify!($ty); + + (field, ty, &[$($doc),*], $default) + },)* + ]) + } + + } + }; +} +use _config_data as config_data; + +fn get_field( + json: &mut serde_json::Value, + field: &'static str, + alias: Option<&'static str>, + default: serde_json::Value, +) -> T { + let default = serde_json::from_value(default).unwrap(); + + // XXX: check alias first, to work-around the VS Code where it pre-fills the + // defaults instead of sending an empty object. + alias + .into_iter() + .chain(iter::once(field)) + .find_map(move |field| { + let mut pointer = field.replace('_', "/"); + pointer.insert(0, '/'); + json.pointer_mut(&pointer) + .and_then(|it| serde_json::from_value(it.take()).ok()) + }) + .unwrap_or(default) +} +fn schema( + fields: &[(&'static str, &'static str, &[&str], serde_json::Value)], +) -> serde_json::Value { + for ((f1, ..), (f2, ..)) in fields.iter().zip(&fields[1..]) { + fn key(f: &str) -> &str { + f.split_once('_').map_or(f, |x| x.0) + } + assert!(key(f1) <= key(f2), "wrong field order: {:?} {:?}", f1, f2); + } + + let map = fields + .iter() + .map(|(field, ty, doc, default)| { + let name = format!("elp.{}", field.replace('_', ".")); + let props = field_props(field, ty, doc, default); + (name, props) + }) + .collect::>(); + map.into() +} + +fn field_props( + field: &str, + ty: &str, + doc: &[&str], + default: &serde_json::Value, +) -> serde_json::Value { + let doc = doc_comment_to_string(doc); + let doc = doc.trim_end_matches('\n'); + assert!( + doc.ends_with('.') && doc.starts_with(char::is_uppercase), + "bad docs for {}: {:?}", + field, + doc + ); + + let mut map = serde_json::Map::default(); + macro_rules! set { + ($($key:literal: $value:tt),*$(,)?) => {{$( + map.insert($key.into(), serde_json::json!($value)); + )*}}; + } + set!("markdownDescription": doc); + set!("default": default); + + match ty { + "bool" => set!("type": "boolean"), + "String" => set!("type": "string"), + "Vec" => set! { + "type": "array", + "items": { "type": "string" }, + }, + "Vec" => set! { + "type": "array", + "items": { "type": "string" }, + }, + "FxHashSet" => set! { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + }, + "FxHashMap" => set! { + "type": "object", + }, + "Option" => set! { + "type": ["null", "integer"], + "minimum": 0, + }, + "Option" => set! { + "type": ["null", "string"], + }, + "Option" => set! { + "type": ["null", "string"], + }, + "Option" => set! { + "type": ["null", "boolean"], + }, + "Option>" => set! { + "type": ["null", "array"], + "items": { "type": "string" }, + }, + _ => panic!("{}: {}", ty, default), + } + + map.into() +} + +fn doc_comment_to_string(doc: &[&str]) -> String { + doc.iter() + .map(|it| it.strip_prefix(' ').unwrap_or(it)) + .map(|it| format!("{}\n", it)) + .collect() +} + +#[cfg(test)] +mod tests { + use expect_test::expect; + + use super::*; + + #[test] + fn generate_package_json_config() { + let s = Config::json_schema(); + let schema = format!("{:#}", s); + let mut schema = schema + .trim_start_matches('{') + .trim_end_matches('}') + .replace("\n ", "\n") + .trim_start_matches('\n') + .trim_end() + .to_string(); + schema.push_str(",\n"); + + let s = remove_ws(&schema); + + expect![[r#""elp.ai.enable":{"default":false,"markdownDescription":"EnablesupportforAI-basedcompletions.","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.inlayHints.parameterHints.enable":{"default":false,"markdownDescription":"Whethertoshowfunctionparameternameinlayhintsatthecall\nsite.","type":"boolean"},"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.run.enable":{"default":false,"markdownDescription":"Whethertoshowthe`Run`lenses.Onlyapplieswhen\n`#elp.lens.enable#`isset.","type":"boolean"},"elp.log":{"default":"error","markdownDescription":"ConfigureLSP-basedloggingusingenv_loggersyntax.","type":"string"},"elp.signatureHelp.enable":{"default":false,"markdownDescription":"WhethertoshowSignatureHelp.","type":"boolean"},"#]] + .assert_eq(s.as_str()); + + expect![[r#" + "elp.ai.enable": { + "default": false, + "markdownDescription": "Enable support for AI-based completions.", + "type": "boolean" + }, + "elp.diagnostics.disabled": { + "default": [], + "items": { + "type": "string" + }, + "markdownDescription": "List of ELP diagnostics to disable.", + "type": "array", + "uniqueItems": true + }, + "elp.diagnostics.enableExperimental": { + "default": false, + "markdownDescription": "Whether to show experimental ELP diagnostics that might\nhave more false positives than usual.", + "type": "boolean" + }, + "elp.inlayHints.parameterHints.enable": { + "default": false, + "markdownDescription": "Whether to show function parameter name inlay hints at the call\nsite.", + "type": "boolean" + }, + "elp.lens.debug.enable": { + "default": false, + "markdownDescription": "Whether to show the `Debug` lenses. Only applies when\n`#elp.lens.enable#` is set.", + "type": "boolean" + }, + "elp.lens.enable": { + "default": false, + "markdownDescription": "Whether to show Code Lenses in Erlang files.", + "type": "boolean" + }, + "elp.lens.run.enable": { + "default": false, + "markdownDescription": "Whether to show the `Run` lenses. Only applies when\n`#elp.lens.enable#` is set.", + "type": "boolean" + }, + "elp.log": { + "default": "error", + "markdownDescription": "Configure LSP-based logging using env_logger syntax.", + "type": "string" + }, + "elp.signatureHelp.enable": { + "default": false, + "markdownDescription": "Whether to show Signature Help.", + "type": "boolean" + }, + "#]].assert_eq(schema.as_str()); + } + + fn remove_ws(text: &str) -> String { + text.replace(char::is_whitespace, "") + } +} diff --git a/crates/elp/src/convert.rs b/crates/elp/src/convert.rs new file mode 100644 index 0000000000..04371bb83f --- /dev/null +++ b/crates/elp/src/convert.rs @@ -0,0 +1,273 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::path; +use std::path::Path; +use std::str::FromStr; + +use anyhow::anyhow; +use anyhow::Result; +use elp_ide::diagnostics::Diagnostic; +use elp_ide::diagnostics::RelatedInformation; +use elp_ide::diagnostics::Severity; +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::VfsPath; +use elp_ide::elp_ide_db::EqwalizerDiagnostic; +use elp_ide::elp_ide_db::LineIndex; +use elp_ide::TextRange; +use elp_ide::TextSize; +use lsp_types::DiagnosticRelatedInformation; +use lsp_types::Location; +use lsp_types::Url; + +use crate::arc_types; +use crate::from_proto; + +pub fn abs_path(url: &lsp_types::Url) -> Result { + let path = url + .to_file_path() + .map_err(|()| anyhow!("url '{}' is not a file", url))?; + Ok(AbsPathBuf::assert(path)) +} + +pub fn vfs_path(url: &lsp_types::Url) -> Result { + abs_path(url).map(VfsPath::from) +} + +pub fn range(line_index: &LineIndex, range: TextRange) -> lsp_types::Range { + let start = position(line_index, range.start()); + let end = position(line_index, range.end()); + lsp_types::Range::new(start, end) +} + +pub fn position(line_index: &LineIndex, offset: TextSize) -> lsp_types::Position { + let line_col = line_index.line_col(offset); + lsp_types::Position::new(line_col.line, line_col.col_utf16) +} + +pub fn diagnostic_severity(severity: Severity) -> lsp_types::DiagnosticSeverity { + match severity { + Severity::Error => lsp_types::DiagnosticSeverity::ERROR, + Severity::Warning => lsp_types::DiagnosticSeverity::WARNING, + Severity::WeakWarning => lsp_types::DiagnosticSeverity::HINT, + } +} + +pub fn ide_to_lsp_diagnostic( + line_index: &LineIndex, + url: &Url, + d: &Diagnostic, +) -> lsp_types::Diagnostic { + lsp_types::Diagnostic { + range: range(line_index, d.range), + severity: Some(diagnostic_severity(d.severity)), + code: Some(lsp_types::NumberOrString::String(d.code.to_string())), + code_description: None, + source: Some("elp".into()), + message: d.message.clone(), + related_information: from_related(line_index, url, &d.related_info), + tags: None, + data: None, + } +} + +pub fn lsp_to_assist_context_diagnostic( + line_index: &LineIndex, + d: lsp_types::Diagnostic, +) -> Option { + let range = from_proto::safe_text_range(line_index, d.range)?; + if let Some(lsp_types::NumberOrString::String(code)) = d.code { + match AssistContextDiagnosticCode::from_str(&code) { + Ok(code) => Some(AssistContextDiagnostic::new(code, d.message, range)), + Err(_) => None, + } + } else { + None + } +} + +pub fn eqwalizer_to_lsp_diagnostic( + d: &EqwalizerDiagnostic, + line_index: &LineIndex, + eqwalizer_enabled: bool, +) -> lsp_types::Diagnostic { + let range = range(line_index, d.range); + let severity = if eqwalizer_enabled { + lsp_types::DiagnosticSeverity::ERROR + } else { + lsp_types::DiagnosticSeverity::INFORMATION + }; + let explanation = match &d.explanation { + Some(s) => format!("\n\n{}", s), + None => "".to_string(), + }; + let message = format!( + "{}{}{}\n See {}", + expr_string(d), + d.message, + explanation, + d.uri + ); + lsp_types::Diagnostic { + range, + severity: Some(severity), + code: Some(lsp_types::NumberOrString::String("eqwalizer".to_string())), + code_description: None, + source: Some("elp".into()), + message, + related_information: None, + tags: None, + data: None, + } +} + +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 = 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{}\n```", s), + None => "".to_string(), + }; + let link = format!("> [docs on `{}`]({})", d.code, d.uri); + let message = format!( + "```lang=error,counterexample +{} +{} +``` +{} +{}", + expr_string(d), + d.message, + explanation, + link + ); + let name = format!("eqWAlizer: {}", d.code); + arc_types::Diagnostic::new( + relative_path, + line_num, + character, + severity, + name, + message, + d.expression.clone(), + ) +} + +fn expr_string(d: &EqwalizerDiagnostic) -> String { + match &d.expression { + Some(s) => format!("`{}`.\n", s), + None => "".to_string(), + } +} + +fn from_related( + line_index: &LineIndex, + url: &Url, + r: &Option>, +) -> Option> { + r.as_ref().map(|ri| { + ri.iter() + .map(|i| { + let location = Location { + range: range(line_index, i.range), + uri: url.clone(), + }; + DiagnosticRelatedInformation { + location, + message: i.message.clone(), + } + }) + .collect() + }) +} + +// Taken from rust-analyzer to_proto.rs + +/// Returns a `Url` object from a given path, will lowercase drive letters if present. +/// This will only happen when processing windows paths. +/// +/// When processing non-windows path, this is essentially the same as `Url::from_file_path`. +pub(crate) fn url_from_abs_path(path: &AbsPath) -> lsp_types::Url { + let url = lsp_types::Url::from_file_path(path).unwrap(); + match path.as_ref().components().next() { + Some(path::Component::Prefix(prefix)) + if matches!( + prefix.kind(), + path::Prefix::Disk(_) | path::Prefix::VerbatimDisk(_) + ) => + { + // Need to lowercase driver letter + } + _ => return url, + } + + let driver_letter_range = { + let mut segments = url.as_str().splitn(3, ':'); + let start = match segments.next() { + Some(scheme) => scheme.len() + ':'.len_utf8(), + None => return url, + }; + match segments.next() { + Some(drive_letter) => start..(start + drive_letter.len()), + None => return url, + } + }; + + // Note: lowercasing the `path` itself doesn't help, the `Url::parse` + // machinery *also* canonicalizes the drive letter. So, just massage the + // string in place. + let mut url: String = url.into(); + url[driver_letter_range].make_ascii_lowercase(); + lsp_types::Url::parse(&url).unwrap() +} + +fn ide_to_arc_severity(severity: Severity) -> arc_types::Severity { + match severity { + Severity::Error => arc_types::Severity::Error, + Severity::Warning => arc_types::Severity::Warning, + Severity::WeakWarning => arc_types::Severity::Advice, + } +} + +pub fn ide_to_arc_diagnostic( + line_index: &LineIndex, + path: &Path, + diagnostic: &Diagnostic, +) -> arc_types::Diagnostic { + let pos = position(line_index, diagnostic.range.start()); + let line_num = pos.line + 1; + let character = Some(pos.character + 1); + arc_types::Diagnostic::new( + path, + line_num, + character, + ide_to_arc_severity(diagnostic.severity), + diagnostic.code.as_label(), + diagnostic.message.clone(), + None, + ) +} diff --git a/crates/elp/src/diagnostics.rs b/crates/elp/src/diagnostics.rs new file mode 100644 index 0000000000..1a1be67949 --- /dev/null +++ b/crates/elp/src/diagnostics.rs @@ -0,0 +1,165 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +// From https://github.com/rust-lang/rust-analyzer/blob/cf44953210cbfe189043417690fabd0037a6e74e/crates/rust-analyzer/src/diagnostics.rs + +use std::mem; +use std::str::FromStr; + +use elp_ide::elp_ide_db::elp_base_db::FileId; +use fxhash::FxHashMap; +use fxhash::FxHashSet; +use lsp_types::Diagnostic; + +#[derive(Debug, Default, Clone)] +pub(crate) struct DiagnosticCollection { + pub(crate) native: FxHashMap>, + pub(crate) erlang_service: FxHashMap>, + pub(crate) eqwalizer: FxHashMap>, + pub(crate) edoc: FxHashMap>, + changes: FxHashSet, +} + +impl DiagnosticCollection { + pub fn set_native(&mut self, file_id: FileId, diagnostics: Vec) { + if !are_all_diagnostics_equal(&self.native, file_id, &diagnostics) { + set_diagnostics(&mut self.native, file_id, diagnostics); + self.changes.insert(file_id); + } + } + + pub fn set_eqwalizer(&mut self, file_id: FileId, diagnostics: Vec) { + if !are_all_diagnostics_equal(&self.eqwalizer, file_id, &diagnostics) { + set_diagnostics(&mut self.eqwalizer, file_id, diagnostics); + self.changes.insert(file_id); + } + } + + pub fn set_edoc(&mut self, file_id: FileId, diagnostics: Vec) { + if !are_all_diagnostics_equal(&self.edoc, file_id, &diagnostics) { + set_diagnostics(&mut self.edoc, file_id, diagnostics); + self.changes.insert(file_id); + } + } + + pub fn set_erlang_service(&mut self, file_id: FileId, diagnostics: Vec) { + if !are_all_diagnostics_equal(&self.erlang_service, file_id, &diagnostics) { + set_diagnostics(&mut self.erlang_service, file_id, diagnostics); + self.changes.insert(file_id); + } + } + + pub fn diagnostics_for(&self, file_id: FileId) -> impl Iterator { + let native = self.native.get(&file_id).into_iter().flatten(); + let erlang_service = self.erlang_service.get(&file_id).into_iter().flatten(); + let eqwalizer = self.eqwalizer.get(&file_id).into_iter().flatten(); + let edoc = self.edoc.get(&file_id).into_iter().flatten(); + native.chain(erlang_service).chain(eqwalizer).chain(edoc) + } + + pub fn take_changes(&mut self) -> Option> { + if self.changes.is_empty() { + return None; + } + Some(mem::take(&mut self.changes)) + } +} + +fn are_all_diagnostics_equal( + map: &FxHashMap>, + file_id: FileId, + new: &[Diagnostic], +) -> bool { + let existing = map.get(&file_id).map(Vec::as_slice).unwrap_or_default(); + + existing.len() == new.len() + && new + .iter() + .zip(existing) + .all(|(left, right)| are_diagnostics_equal(left, right)) +} + +fn are_diagnostics_equal(left: &Diagnostic, right: &Diagnostic) -> bool { + left.source == right.source + && left.severity == right.severity + && left.range == right.range + && left.message == right.message +} + +fn set_diagnostics( + map: &mut FxHashMap>, + file_id: FileId, + new: Vec, +) { + if new.is_empty() { + map.remove(&file_id); + } else { + map.insert(file_id, new); + } +} + +#[derive(Clone, Debug)] +pub enum DiagnosticSource { + ErlangLsCompiler, +} + +impl FromStr for DiagnosticSource { + type Err = String; + fn from_str(s: &str) -> Result { + match s { + "Compiler" => Ok(DiagnosticSource::ErlangLsCompiler), + unknown => Err(format!("Unknown DiagnosticSource: '{unknown}'")), + } + } +} + +// --------------------------------------------------------------------- +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn does_not_mark_change_from_empty_to_empty() { + let mut diagnostics = DiagnosticCollection::default(); + let file_id = FileId(0); + + diagnostics.set_eqwalizer(file_id, vec![]); + diagnostics.set_native(file_id, vec![]); + + assert_eq!(diagnostics.take_changes(), None); + assert_eq!(diagnostics.diagnostics_for(file_id).next(), None); + } + + #[test] + fn resets_diagnostics() { + let mut diagnostics = DiagnosticCollection::default(); + let file_id = FileId(0); + + let diagnostic = Diagnostic::default(); + + // Set some diagnostic initially + diagnostics.set_native(file_id, vec![diagnostic.clone()]); + + let changes = diagnostics.take_changes(); + let mut expected_changes = FxHashSet::default(); + expected_changes.insert(file_id); + assert_eq!(changes.as_ref(), Some(&expected_changes)); + + let stored = diagnostics.diagnostics_for(file_id).collect::>(); + assert_eq!(stored, vec![&diagnostic]); + + // Reset to empty + diagnostics.set_native(file_id, vec![]); + + let changes = diagnostics.take_changes(); + assert_eq!(changes.as_ref(), Some(&expected_changes)); + assert_eq!(diagnostics.diagnostics_for(file_id).next(), None); + } +} diff --git a/crates/elp/src/document.rs b/crates/elp/src/document.rs new file mode 100644 index 0000000000..ef4e3b2476 --- /dev/null +++ b/crates/elp/src/document.rs @@ -0,0 +1,82 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::ops::Range; + +use elp_ide::elp_ide_db::LineIndex; +use lsp_types::TextDocumentContentChangeEvent; + +use crate::from_proto::text_range; + +pub struct Document { + pub content: String, +} + +impl Document { + pub fn from_bytes(bytes: Vec) -> Document { + let content = match String::from_utf8(bytes) { + Ok(text) => text, + Err(err) => { + // Fall back to lossy latin1 loading of files. + // This should only affect files from yaws, and + // possibly OTP that are latin1 encoded. + let contents = err.into_bytes(); + contents.into_iter().map(|byte| byte as char).collect() + } + }; + Document { content } + } + + // From https://github.com/rust-lang/rust-analyzer/blob/607b9ea160149bacca41c0638f16d372c3b235cd/crates/rust-analyzer/src/lsp_utils.rs#L90 + pub fn apply_changes(&mut self, changes: Vec) { + let mut line_index = LineIndex::new(&self.content); + + // The changes we got must be applied sequentially, but can cross lines so we + // have to keep our line index updated. + // Some clients (e.g. Code) sort the ranges in reverse. As an optimization, we + // remember the last valid line in the index and only rebuild it if needed. + // The VFS will normalize the end of lines to `\n`. + enum IndexValid { + All, + UpToLineExclusive(u32), + } + + impl IndexValid { + fn covers(&self, line: u32) -> bool { + match *self { + IndexValid::UpToLineExclusive(to) => to > line, + _ => true, + } + } + } + + let mut index_valid = IndexValid::All; + for change in changes { + match change.range { + Some(range) => { + if !index_valid.covers(range.end.line) { + line_index = LineIndex::new(&self.content); + } + index_valid = IndexValid::UpToLineExclusive(range.start.line); + let range = text_range(&line_index, range); + self.content + .replace_range(Range::::from(range), &change.text); + } + None => { + self.content = change.text; + index_valid = IndexValid::UpToLineExclusive(0); + } + } + } + } + + pub fn into_bytes(self) -> Vec { + self.content.into_bytes() + } +} diff --git a/crates/elp/src/from_proto.rs b/crates/elp/src/from_proto.rs new file mode 100644 index 0000000000..e5029f1021 --- /dev/null +++ b/crates/elp/src/from_proto.rs @@ -0,0 +1,119 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! Conversion lsp_types types to ELP specific ones. + +use elp_ide::elp_ide_assists::AssistKind; +use elp_ide::elp_ide_db::elp_base_db::FileId; +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::LineCol; +use elp_ide::elp_ide_db::LineIndex; +use elp_ide::TextRange; +use elp_ide::TextSize; + +use crate::snapshot::Snapshot; +use crate::Result; + +pub(crate) fn offset(line_index: &LineIndex, position: lsp_types::Position) -> TextSize { + let line_col = LineCol { + line: position.line as u32, + col_utf16: position.character as u32, + }; + // Temporary for T147609435 + let _pctx = stdx::panic_context::enter(format!("\nfrom_proto::offset")); + line_index.offset(line_col) +} + +pub(crate) fn text_range(line_index: &LineIndex, range: lsp_types::Range) -> TextRange { + let start = offset(line_index, range.start); + let end = offset(line_index, range.end); + // Temporary for T148094436 + let _pctx = stdx::panic_context::enter(format!("\nfrom_proto::text_range")); + TextRange::new(start, end) +} + +pub(crate) fn safe_offset( + line_index: &LineIndex, + position: lsp_types::Position, +) -> Option { + let line_col = LineCol { + line: position.line as u32, + col_utf16: position.character as u32, + }; + line_index.safe_offset(line_col) +} + +/// If we receive an LSP Range from a possibly earlier version of the +/// file, it may not map into our current file, or may not have start < end +pub(crate) fn safe_text_range( + line_index: &LineIndex, + range: lsp_types::Range, +) -> Option { + // TODO: Remove the logging once we know that we have averted the problem (T147609435) + let start = if let Some(offset) = safe_offset(line_index, range.start) { + offset + } else { + log::warn!("from_proto::safe_text_range failed for {:?}", range.start); + return None; + }; + let end = if let Some(offset) = safe_offset(line_index, range.end) { + offset + } else { + log::warn!("from_proto::safe_text_range failed for {:?}", range.end); + return None; + }; + // Temporary for T148094436 + let _pctx = stdx::panic_context::enter(format!("\nfrom_proto::safe_text_range")); + if start <= end { + Some(TextRange::new(start, end)) + } else { + None + } +} + +pub(crate) fn file_id(snap: &Snapshot, url: &lsp_types::Url) -> Result { + snap.url_to_file_id(url) +} + +pub(crate) fn file_position( + snap: &Snapshot, + tdpp: lsp_types::TextDocumentPositionParams, +) -> Result { + let file_id = snap.url_to_file_id(&tdpp.text_document.uri)?; + let line_index = snap.analysis.line_index(file_id)?; + let offset = offset(&line_index, tdpp.position); + Ok(FilePosition { file_id, offset }) +} + +pub(crate) fn file_range( + snap: &Snapshot, + text_document_identifier: lsp_types::TextDocumentIdentifier, + range: lsp_types::Range, +) -> Result { + let file_id = snap.url_to_file_id(&text_document_identifier.uri)?; + let line_index = snap.analysis.line_index(file_id)?; + let range = + safe_text_range(&line_index, range).ok_or(anyhow::anyhow!("invalid range: {:?}", range))?; + Ok(FileRange { file_id, range }) +} + +pub(crate) fn assist_kind(kind: lsp_types::CodeActionKind) -> Option { + let assist_kind = match &kind { + k if k == &lsp_types::CodeActionKind::EMPTY => AssistKind::None, + k if k == &lsp_types::CodeActionKind::QUICKFIX => AssistKind::QuickFix, + k if k == &lsp_types::CodeActionKind::REFACTOR => AssistKind::Refactor, + k if k == &lsp_types::CodeActionKind::REFACTOR_EXTRACT => AssistKind::RefactorExtract, + k if k == &lsp_types::CodeActionKind::REFACTOR_INLINE => AssistKind::RefactorInline, + k if k == &lsp_types::CodeActionKind::REFACTOR_REWRITE => AssistKind::RefactorRewrite, + _ => return None, + }; + + Some(assist_kind) +} diff --git a/crates/elp/src/handlers.rs b/crates/elp/src/handlers.rs new file mode 100644 index 0000000000..830fa9d2fd --- /dev/null +++ b/crates/elp/src/handlers.rs @@ -0,0 +1,801 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! This module is responsible for implementing handlers for Language Server +//! Protocol. The majority of requests are fulfilled by calling into the +//! `ide` crate. + +use anyhow::bail; +use anyhow::Result; +use elp_ide::elp_ide_assists::AssistKind; +use elp_ide::elp_ide_assists::AssistResolveStrategy; +use elp_ide::elp_ide_assists::SingleResolve; +use elp_ide::elp_ide_completion::Completion; +use elp_ide::elp_ide_completion::Kind; +use elp_ide::elp_ide_db::assists::AssistContextDiagnostic; +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_ide::elp_ide_db::LineIndex; +use elp_ide::elp_ide_db::SymbolKind; +use elp_ide::Cancellable; +use elp_ide::HighlightedRange; +use elp_ide::RangeInfo; +use elp_ide::TextRange; +use itertools::Itertools; +use lsp_server::ErrorCode; +use lsp_types::CallHierarchyIncomingCall; +use lsp_types::CallHierarchyIncomingCallsParams; +use lsp_types::CallHierarchyItem; +use lsp_types::CallHierarchyOutgoingCall; +use lsp_types::CallHierarchyOutgoingCallsParams; +use lsp_types::CallHierarchyPrepareParams; +use lsp_types::CodeLens; +use lsp_types::CompletionItem; +use lsp_types::Diagnostic; +use lsp_types::DocumentSymbol; +use lsp_types::FoldingRange; +use lsp_types::FoldingRangeParams; +use lsp_types::Hover; +use lsp_types::HoverParams; +use lsp_types::RenameParams; +use lsp_types::SemanticTokensDeltaParams; +use lsp_types::SemanticTokensFullDeltaResult; +use lsp_types::SemanticTokensParams; +use lsp_types::SemanticTokensRangeParams; +use lsp_types::SemanticTokensRangeResult; +use lsp_types::SemanticTokensResult; +use lsp_types::SymbolInformation; +use lsp_types::TextDocumentIdentifier; +use lsp_types::Url; +use lsp_types::WorkspaceEdit; + +use crate::convert::lsp_to_assist_context_diagnostic; +use crate::from_proto; +use crate::lsp_ext; +use crate::snapshot::Snapshot; +use crate::to_proto; +use crate::LspError; + +pub(crate) fn handle_code_action( + snap: Snapshot, + params: lsp_types::CodeActionParams, +) -> Result>> { + let _p = profile::span("handle_code_action"); + + if !snap.config.code_action_literals() { + // We intentionally don't support command-based actions, as those either + // require either custom client-code or server-initiated edits. Server + // initiated edits break causality, so we avoid those. + return Ok(None); + } + + let frange = from_proto::file_range(&snap, params.text_document.clone(), params.range)?; + + let mut assists_config = snap.config.assist(); + assists_config.allowed = params + .context + .only + .clone() + .map(|it| it.into_iter().filter_map(from_proto::assist_kind).collect()); + + let mut res: Vec = Vec::new(); + + let code_action_resolve_cap = snap.config.code_action_resolve(); + let resolve = if code_action_resolve_cap { + AssistResolveStrategy::None + } else { + AssistResolveStrategy::All + }; + + let file_id = snap.url_to_file_id(¶ms.text_document.uri)?; + let line_index = snap.analysis.line_index(file_id)?; + let diagnostics = params.clone().context.diagnostics; + let assist_context_diagnostics = to_assist_context_diagnostics(&line_index, diagnostics); + let assists = snap.analysis.assists_with_fixes( + &assists_config, + &snap.config.diagnostics(), + resolve, + frange, + &assist_context_diagnostics, + None, + )?; + for (index, assist) in assists.into_iter().enumerate() { + let resolve_data = if code_action_resolve_cap { + Some((index, params.clone(), assist.user_input.clone())) + } else { + None + }; + let code_action = to_proto::code_action(&snap, assist, resolve_data)?; + res.push(code_action) + } + + Ok(Some(res)) +} + +pub(crate) fn handle_code_action_resolve( + snap: Snapshot, + mut code_action: lsp_types::CodeAction, +) -> Result { + let _p = profile::span("handle_code_action_resolve"); + let params_raw = match code_action.data.take() { + Some(it) => it, + None => bail!("can't resolve code action without data"), + }; + + let params: lsp_ext::CodeActionData = serde::Deserialize::deserialize(params_raw)?; + + let file_id = snap.url_to_file_id(¶ms.code_action_params.text_document.uri)?; + let line_index = snap.analysis.line_index(file_id)?; + // Temporary for T147609435 + let _pctx = stdx::panic_context::enter(format!("\nhandle_code_action_resolve")); + let range = from_proto::text_range(&line_index, params.code_action_params.range); + let frange = FileRange { file_id, range }; + + let mut assists_config = snap.config.assist(); + assists_config.allowed = params + .code_action_params + .context + .only + .map(|it| it.into_iter().filter_map(from_proto::assist_kind).collect()); + + let (assist_index, assist_resolve) = match parse_action_id(¶ms.id) { + Ok(parsed_data) => parsed_data, + Err(e) => { + return Err(LspError::new( + ErrorCode::InvalidParams as i32, + format!("Failed to parse action id string '{}': {}", params.id, e), + ) + .into()); + } + }; + + let expected_assist_id = assist_resolve.assist_id.clone(); + let expected_kind = assist_resolve.assist_kind; + + let diagnostics = params.code_action_params.context.diagnostics; + let assist_context_diagnostics = to_assist_context_diagnostics(&line_index, diagnostics); + let assists = snap.analysis.assists_with_fixes( + &assists_config, + &snap.config.diagnostics(), + AssistResolveStrategy::Single(assist_resolve), + frange, + &assist_context_diagnostics, + params.user_input, + )?; + + let assist = match assists.get(assist_index) { + Some(assist) => assist, + None => return Err(LspError::new( + ErrorCode::InvalidParams as i32, + format!( + "Failed to find the assist for index {} provided by the resolve request. Resolve request assist id: {}", + assist_index, params.id, + ), + ) + .into()) + }; + if assist.id.0 != expected_assist_id || assist.id.1 != expected_kind { + return Err(LspError::new( + ErrorCode::InvalidParams as i32, + format!( + "Mismatching assist at index {} for the resolve parameters given. Resolve request assist id: {}, actual id: {:?}.", + assist_index, params.id, assist.id + ), + ) + .into()); + } + match to_proto::code_action(&snap, assist.clone(), None)? { + lsp_types::CodeActionOrCommand::Command(_) => {} + lsp_types::CodeActionOrCommand::CodeAction(ca) => code_action.edit = ca.edit, + } + Ok(code_action) +} + +fn parse_action_id(action_id: &str) -> Result<(usize, SingleResolve), String> { + let id_parts = action_id.split(':').collect_vec(); + match id_parts.as_slice() { + &[assist_id_string, assist_kind_string, index_string] => { + let assist_kind: AssistKind = assist_kind_string.parse()?; + let index: usize = match index_string.parse() { + Ok(index) => index, + Err(e) => return Err(format!("Incorrect index string: {}", e)), + }; + Ok(( + index, + SingleResolve { + assist_id: assist_id_string.to_string(), + assist_kind, + }, + )) + } + _ => Err("Action id contains incorrect number of segments".to_string()), + } +} + +pub(crate) fn handle_expand_macro( + snap: Snapshot, + params: lsp_ext::ExpandMacroParams, +) -> Result> { + let _p = profile::span("handle_expand_macro"); + let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; + let line_index = snap.analysis.line_index(file_id)?; + let offset = from_proto::offset(&line_index, params.position); + + let res = snap + .analysis + .expand_macro(FilePosition { file_id, offset })?; + match res { + Some(it) => Ok(Some(lsp_ext::ExpandedMacro { + name: it.name, + expansion: it.expansion, + })), + None => Ok(Some(lsp_ext::ExpandedMacro { + name: "Expansion Failed".to_string(), + expansion: "".to_string(), + })), + } +} + +pub(crate) fn pong(_: Snapshot, _: Vec) -> Result { + Ok("pong".to_string()) +} + +pub(crate) fn handle_selection_range( + snap: Snapshot, + params: lsp_types::SelectionRangeParams, +) -> Result>> { + let _p = profile::span("handle_selection_range"); + let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; + let line_index = snap.analysis.line_index(file_id)?; + let res: Result> = params + .positions + .into_iter() + .map(|position| { + let offset = from_proto::offset(&line_index, position); + let mut ranges = Vec::new(); + { + let mut range = TextRange::new(offset, offset); + loop { + ranges.push(range); + let frange = FileRange { file_id, range }; + let next = snap.analysis.extend_selection(frange)?; + if next == range { + break; + } else { + range = next + } + } + } + let mut range = lsp_types::SelectionRange { + range: to_proto::range(&line_index, *ranges.last().unwrap()), + parent: None, + }; + for &r in ranges.iter().rev().skip(1) { + range = lsp_types::SelectionRange { + range: to_proto::range(&line_index, r), + parent: Some(Box::new(range)), + } + } + Ok(range) + }) + .collect(); + + Ok(Some(res?)) +} + +pub(crate) fn handle_goto_definition( + snap: Snapshot, + params: lsp_types::GotoDefinitionParams, +) -> Result> { + let _p = profile::span("handle_goto_definition"); + let position = from_proto::file_position(&snap, params.text_document_position_params)?; + let nav_info = match snap.analysis.goto_definition(position)? { + None => return Ok(None), + Some(it) => it, + }; + let src = FileRange { + file_id: position.file_id, + range: nav_info.range, + }; + let res = to_proto::goto_definition_response(&snap, Some(src), nav_info.info)?; + Ok(Some(res)) +} + +pub(crate) fn handle_references( + snap: Snapshot, + params: lsp_types::ReferenceParams, +) -> Result>> { + let _p = profile::span("handle_references"); + let position = from_proto::file_position(&snap, params.text_document_position)?; + let refs = match snap.analysis.find_all_refs(position)? { + None => return Ok(None), + Some(it) => it, + }; + let include_declaration = params.context.include_declaration; + let locations = refs + .into_iter() + .flat_map(|refs| { + let decl = if include_declaration { + to_proto::location_from_nav(&snap, refs.declaration).ok() + } else { + None + }; + refs.references + .into_iter() + .flat_map(|(file_id, refs)| { + refs.into_iter() + .map(move |range| FileRange { file_id, range }) + .flat_map(|range| to_proto::location(&snap, range).ok()) + }) + .chain(decl) + }) + .collect(); + Ok(Some(locations)) +} + +pub(crate) fn handle_completion( + snap: Snapshot, + params: lsp_types::CompletionParams, +) -> Result> { + let _p = profile::span("handle_completion"); + let position = from_proto::file_position(&snap, params.text_document_position)?; + let completion_trigger_character = params + .context + .and_then(|ctx| ctx.trigger_character) + .and_then(|s| s.chars().next()); + + let ai_receiver = + if completion_trigger_character.is_none() || completion_trigger_character != Some(':') { + elp_ai::always(None) + } else { + snap.ai_completion(position)? + }; + + let mut completions = snap + .analysis + .completions(position, completion_trigger_character)?; + + let ai_result = if let Ok(Some(ai_result)) = ai_receiver.recv() { + ai_result + } else { + return Ok(Some(to_proto::completion_response(snap, completions))); + }; + + if completions.is_empty() { + completions.push(Completion { + label: ai_result.clone(), + kind: Kind::AiAssist, + contents: elp_ide::elp_ide_completion::Contents::SameAsLabel, + position: None, + sort_text: Some("\0".to_string()), + deprecated: false, + }); + } else { + for c in completions.iter_mut() { + let split_char = '/'; + let parts: Vec<&str> = c.label.splitn(2, split_char).collect(); + let fname = parts[0]; + if fname == ai_result { + c.sort_text = Some("\0".to_string()); + c.kind = Kind::AiAssist; + } else if fname.starts_with(&ai_result) { + c.sort_text = Some("\x01".to_string()); + c.kind = Kind::AiAssist; + } + } + } + + Ok(Some(to_proto::completion_response(snap, completions))) +} + +pub(crate) fn handle_completion_resolve( + snap: Snapshot, + mut original_completion: CompletionItem, +) -> Result { + let _p = profile::span("handle_completion_resolve"); + + if let Some(data) = original_completion.clone().data { + let data: lsp_ext::CompletionData = serde_json::from_value(data)?; + if let Ok(position) = from_proto::file_position(&snap, data.position) { + 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, + value: docs, + }); + original_completion.documentation = Some(documentation) + } + } + } + Ok(original_completion) +} + +pub(crate) fn handle_document_symbol( + snap: Snapshot, + params: lsp_types::DocumentSymbolParams, +) -> Result> { + let _p = profile::span("handle_document_symbol"); + let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; + let line_index = snap.analysis.line_index(file_id)?; + + let res: Vec = snap + .analysis + .document_symbols(file_id)? + .iter() + .map(|symbol| to_proto::document_symbol(&line_index, symbol)) + .collect(); + Ok(Some(res.into())) +} + +pub(crate) fn handle_workspace_symbol( + snap: Snapshot, + params: lsp_types::WorkspaceSymbolParams, +) -> Result>> { + let _p = profile::span("handle_workspace_symbol"); + + let mut res = Vec::new(); + for (project_id, _project) in snap.projects.iter().enumerate() { + let project_id = ProjectId(project_id as u32); + for nav in snap.analysis.symbol_search(project_id, ¶ms.query)? { + #[allow(deprecated)] + let info = SymbolInformation { + name: nav.name.to_string(), + kind: to_proto::symbol_kind(nav.kind), + tags: None, + location: to_proto::location_from_nav(&snap, nav)?, + container_name: None, + deprecated: None, + }; + res.push(info); + } + } + res.sort_by(|a, b| a.name.cmp(&b.name)); + Ok(Some(res)) +} + +pub(crate) fn handle_rename(snap: Snapshot, params: RenameParams) -> Result> { + let _p = profile::span("handle_rename"); + let position = from_proto::file_position(&snap, params.text_document_position)?; + + let change = snap + .analysis + .rename(position, ¶ms.new_name)? + .map_err(to_proto::rename_error)?; + + let workspace_edit = to_proto::workspace_edit(&snap, change)?; + Ok(Some(workspace_edit)) +} + +fn to_assist_context_diagnostics( + line_index: &LineIndex, + diagnostics: Vec, +) -> Vec { + diagnostics + .into_iter() + .filter_map(|d| lsp_to_assist_context_diagnostic(line_index, d)) + .collect() +} + +pub(crate) fn handle_hover(snap: Snapshot, params: HoverParams) -> Result> { + let _p = profile::span("handle_hover"); + let position = from_proto::file_position(&snap, params.text_document_position_params)?; + + let docs = snap.analysis.get_docs_at_position(position)?; + + to_proto::hover_response(&snap, docs) +} + +pub(crate) fn handle_folding_range( + snap: Snapshot, + params: FoldingRangeParams, +) -> Result>> { + let _p = profile::span("handle_folding_range"); + let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; + let folds = snap.analysis.folding_ranges(file_id)?; + let line_index = snap.analysis.line_index(file_id)?; + let res = folds + .into_iter() + .map(|it| to_proto::folding_range(&line_index, it)) + .collect(); + Ok(Some(res)) +} + +pub(crate) fn handle_document_highlight( + snap: Snapshot, + params: lsp_types::DocumentHighlightParams, +) -> Result>> { + let _p = profile::span("handle_document_highlight"); + let position = from_proto::file_position(&snap, params.text_document_position_params)?; + + let line_index = snap.analysis.line_index(position.file_id)?; + + let refs = match snap.analysis.highlight_related(position)? { + None => return Ok(None), + Some(refs) => refs, + }; + let res = refs + .into_iter() + .map( + |HighlightedRange { range, category }| lsp_types::DocumentHighlight { + range: to_proto::range(&line_index, range), + kind: category.and_then(to_proto::document_highlight_kind), + }, + ) + .collect(); + Ok(Some(res)) +} + +pub(crate) fn handle_call_hierarchy_prepare( + snap: Snapshot, + params: CallHierarchyPrepareParams, +) -> Result>> { + let _p = profile::span("handle_call_hierarchy_prepare"); + let position = from_proto::file_position(&snap, params.text_document_position_params)?; + + let nav_info = match snap.analysis.call_hierarchy_prepare(position)? { + None => return Ok(None), + Some(it) => it, + }; + + let RangeInfo { + range: _, + info: navs, + } = nav_info; + let res = navs + .into_iter() + .filter(|it| it.kind == SymbolKind::Function) + .map(|it| to_proto::call_hierarchy_item(&snap, it)) + .collect::>>()?; + + Ok(Some(res)) +} + +pub(crate) fn handle_call_hierarchy_incoming( + snap: Snapshot, + params: CallHierarchyIncomingCallsParams, +) -> Result>> { + let _p = profile::span("handle_call_hierarchy_incoming"); + let item = params.item; + + let doc = TextDocumentIdentifier::new(item.uri); + let frange = from_proto::file_range(&snap, doc, item.selection_range)?; + let fpos = FilePosition { + file_id: frange.file_id, + offset: frange.range.start(), + }; + + let call_items = match snap.analysis.incoming_calls(fpos)? { + None => return Ok(None), + Some(it) => it, + }; + + let mut res = vec![]; + + for call_item in call_items.into_iter() { + let file_id = call_item.target.file_id; + let line_index = snap.analysis.line_index(file_id)?; + let item = to_proto::call_hierarchy_item(&snap, call_item.target)?; + res.push(CallHierarchyIncomingCall { + from: item, + from_ranges: call_item + .ranges + .into_iter() + .map(|it| to_proto::range(&line_index, it)) + .collect(), + }); + } + + Ok(Some(res)) +} + +pub(crate) fn handle_call_hierarchy_outgoing( + snap: Snapshot, + params: CallHierarchyOutgoingCallsParams, +) -> Result>> { + let _p = profile::span("handle_call_hierarchy_outgoing"); + let item = params.item; + + let doc = TextDocumentIdentifier::new(item.uri); + let frange = from_proto::file_range(&snap, doc, item.selection_range)?; + let fpos = FilePosition { + file_id: frange.file_id, + offset: frange.range.start(), + }; + + let call_items = match snap.analysis.outgoing_calls(fpos)? { + None => return Ok(None), + Some(it) => it, + }; + + let mut res = vec![]; + + for call_item in call_items.into_iter() { + let file_id = call_item.target.file_id; + let line_index = snap.analysis.line_index(file_id)?; + let item = to_proto::call_hierarchy_item(&snap, call_item.target)?; + res.push(CallHierarchyOutgoingCall { + to: item, + from_ranges: call_item + .ranges + .into_iter() + .map(|it| to_proto::range(&line_index, it)) + .collect(), + }); + } + + Ok(Some(res)) +} + +pub(crate) fn handle_signature_help( + snap: Snapshot, + params: lsp_types::SignatureHelpParams, +) -> Result> { + let _p = profile::span("handle_signature_help"); + + if !snap.config.signature_help() { + // early return before any db query! + return Ok(None); + } + + let position = from_proto::file_position(&snap, params.text_document_position_params)?; + let (help, active_parameter) = match snap.analysis.signature_help(position)? { + Some((h, Some(ap))) => (h, ap), + _ => return Ok(None), + }; + let res = to_proto::signature_help(help, active_parameter); + Ok(Some(res)) +} + +// --------------------------------------------------------------------- + +pub(crate) fn handle_semantic_tokens_full( + snap: Snapshot, + params: SemanticTokensParams, +) -> Result> { + let _p = profile::span("handle_semantic_tokens_full"); + + let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; + let text = snap.analysis.file_text(file_id)?; + let line_index = snap.analysis.line_index(file_id)?; + + let highlights = snap.analysis.highlight(file_id)?; + let semantic_tokens = to_proto::semantic_tokens(&text, &line_index, highlights); + + // Unconditionally cache the tokens + snap.semantic_tokens_cache + .lock() + .insert(params.text_document.uri, semantic_tokens.clone()); + + Ok(Some(semantic_tokens.into())) +} + +pub(crate) fn handle_semantic_tokens_full_delta( + snap: Snapshot, + params: SemanticTokensDeltaParams, +) -> Result> { + let _p = profile::span("handle_semantic_tokens_full_delta"); + + let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; + let text = snap.analysis.file_text(file_id)?; + let line_index = snap.analysis.line_index(file_id)?; + + let highlights = snap.analysis.highlight(file_id)?; + let semantic_tokens = to_proto::semantic_tokens(&text, &line_index, highlights); + + let mut cache = snap.semantic_tokens_cache.lock(); + let cached_tokens = cache.entry(params.text_document.uri).or_default(); + + if let Some(prev_id) = &cached_tokens.result_id { + if *prev_id == params.previous_result_id { + let delta = to_proto::semantic_token_delta(cached_tokens, &semantic_tokens); + *cached_tokens = semantic_tokens; + return Ok(Some(delta.into())); + } + } + + *cached_tokens = semantic_tokens.clone(); + + Ok(Some(semantic_tokens.into())) +} + +pub(crate) fn handle_semantic_tokens_range( + snap: Snapshot, + params: SemanticTokensRangeParams, +) -> Result> { + let _p = profile::span("handle_semantic_tokens_range"); + + let frange = from_proto::file_range(&snap, params.text_document, params.range)?; + let text = snap.analysis.file_text(frange.file_id)?; + let line_index = snap.analysis.line_index(frange.file_id)?; + + let highlights = snap.analysis.highlight_range(frange)?; + let semantic_tokens = to_proto::semantic_tokens(&text, &line_index, highlights); + Ok(Some(semantic_tokens.into())) +} + +pub(crate) fn handle_code_lens( + snap: Snapshot, + params: lsp_types::CodeLensParams, +) -> Result>> { + let _p = profile::span("handle_code_lens"); + + let mut res = Vec::new(); + let lens_config = snap.config.lens(); + if !lens_config.run { + // early return before any db query! + return Ok(Some(res)); + } + + let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; + + let annotations = snap.analysis.annotations(file_id)?; + let project_build_data = match snap.analysis.project_id(file_id) { + Ok(Some(project_id)) => snap + .get_project(project_id) + .map(|project| project.project_build_data), + _ => None, + }; + + for a in annotations { + to_proto::code_lens(&mut res, &snap, a, project_build_data.clone())?; + } + + Ok(Some(res)) +} + +pub(crate) fn handle_external_docs( + snap: Snapshot, + params: lsp_types::TextDocumentPositionParams, +) -> Result>> { + let _p = profile::span("handle_external_docs"); + + let position = from_proto::file_position(&snap, params)?; + + let docs = snap.analysis.external_docs(position)?; + Ok(docs.map(|links| { + links + .iter() + .filter_map(|link| Url::parse(&link).ok()) + .collect() + })) +} + +pub(crate) fn handle_inlay_hints( + snap: Snapshot, + params: lsp_types::InlayHintParams, +) -> Result>> { + let _p = profile::span("handle_inlay_hints"); + let document_uri = ¶ms.text_document.uri; + let FileRange { file_id, range } = from_proto::file_range( + &snap, + TextDocumentIdentifier::new(document_uri.to_owned()), + params.range, + )?; + let line_index = snap.analysis.line_index(file_id)?; + let inlay_hints_config = snap.config.inlay_hints(); + Ok(Some( + snap.analysis + .inlay_hints(&inlay_hints_config, file_id, Some(range))? + .into_iter() + .map(|it| to_proto::inlay_hint(&snap, &line_index, it)) + .collect::>>()?, + )) +} + +pub(crate) fn handle_inlay_hints_resolve( + _snap: Snapshot, + hint: lsp_types::InlayHint, +) -> Result { + let _p = profile::span("handle_inlay_hints_resolve"); + Ok(hint) +} + +// --------------------------------------------------------------------- diff --git a/crates/elp/src/lib.rs b/crates/elp/src/lib.rs new file mode 100644 index 0000000000..c3bf4d2bbc --- /dev/null +++ b/crates/elp/src/lib.rs @@ -0,0 +1,111 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::fmt; + +use anyhow::anyhow; +use anyhow::Result; +use elp_ide::elp_ide_db::elp_base_db::FileId; +use elp_ide::Analysis; +use elp_syntax::SmolStr; +use fxhash::FxHashSet; +use lazy_static::lazy_static; +use serde::de::DeserializeOwned; +pub use server::setup::ServerSetup; + +pub mod arc_types; +pub mod build; +pub mod cli; +pub mod config; +pub mod convert; +mod diagnostics; +pub mod document; +mod from_proto; +mod handlers; +mod line_endings; +pub mod lsp_ext; +mod op_queue; +mod project_loader; +pub mod reload; +mod semantic_tokens; +pub mod server; +mod snapshot; +mod task_pool; +mod to_proto; + +pub fn from_json(what: &'static str, json: serde_json::Value) -> Result { + let res = serde_path_to_error::deserialize(&json) + .map_err(|e| anyhow!("Failed to deserialize {}: {}; {}", what, e, json))?; + Ok(res) +} + +#[derive(Debug)] +struct LspError { + code: i32, + message: String, +} + +impl LspError { + fn new(code: i32, message: String) -> LspError { + LspError { code, message } + } +} + +impl fmt::Display for LspError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Language Server request failed with {}. ({})", + self.code, self.message + ) + } +} + +impl std::error::Error for LspError {} + +/// Gets ELP version. +/// +/// This uses the version as set in the Cargo.toml file +/// as well as either `+local` for locally-build versions +/// or `+build-YEAR-MONTH-DAY` for versions built on CI. +/// +/// To check for CI a `CI` env var is inspected. +/// It's possible to override the date with +/// [`SOURCE_DATE_EPOCH`](https://reproducible-builds.org/docs/source-date-epoch/). +/// See the `build.rs` file for more details. +pub fn version() -> String { + format!("{}+{}", env!("CARGO_PKG_VERSION"), env!("BUILD_ID")) +} + +/// Some modules use a macro such as `-define(CATCH, catch).`. +/// Our grammar cannot handle it at the moment, so we keep a list of +/// these modules to skip when doing elp parsing for CI. +pub fn otp_file_to_ignore(db: &Analysis, file_id: FileId) -> bool { + lazy_static! { + static ref SET: FxHashSet = + vec!["ttb", + // Not all files in the dependencies compile with ELP, + // also using unusual macros. Rather than skip + // checking deps, we list the known bad ones. + "jsone", "jsone_decode", "jsone_encode", + "piqirun_props", + "yaws_server", "yaws_appmod_dav", "yaws_runmod_lock", + "jsonrpc", + "redbug_dtop" + ] + .iter() + .map(SmolStr::new) + .collect(); + } + if let Some(module_name) = db.module_name(file_id).unwrap() { + SET.contains(module_name.as_str()) + } else { + false + } +} diff --git a/crates/elp/src/line_endings.rs b/crates/elp/src/line_endings.rs new file mode 100644 index 0000000000..2da1ac8d23 --- /dev/null +++ b/crates/elp/src/line_endings.rs @@ -0,0 +1,36 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! We maintain invariant that all internal strings use `\n` as line separator. +//! This module does line ending conversion and detection (so that we can +//! convert back to `\r\n` on the way out). + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum LineEndings { + Unix, + Dos, +} + +impl LineEndings { + /// Replaces `\r\n` with `\n` in `src`. + pub fn normalize(src: String) -> (String, Self) { + if !src.as_bytes().contains(&b'\r') { + (src, Self::Unix) + } else { + (src.replace("\r\n", "\n"), Self::Dos) + } + } + + pub fn revert(&self, src: String) -> String { + match self { + Self::Unix => src, + Self::Dos => src.replace('\n', "\r\n"), + } + } +} diff --git a/crates/elp/src/lsp_ext.rs b/crates/elp/src/lsp_ext.rs new file mode 100644 index 0000000000..53e9f62456 --- /dev/null +++ b/crates/elp/src/lsp_ext.rs @@ -0,0 +1,128 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! ELP extensions to the LSP. + +use std::path::PathBuf; + +use elp_ide::elp_ide_db::assists::AssistUserInput; +use lsp_types::notification::Notification; +use lsp_types::request::Request; +use lsp_types::Position; +use lsp_types::TextDocumentIdentifier; +use lsp_types::TextDocumentPositionParams; +use serde::Deserialize; +use serde::Serialize; + +/// Custom data we put into the generic code action 'data' field to +/// tie a code action back to its original context in ELP. +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodeActionData { + pub code_action_params: lsp_types::CodeActionParams, + pub id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub user_input: Option, +} + +/// Custom data we put into the generic completion item 'data' field to +/// use it in the respective 'resolve' step +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct CompletionData { + pub position: TextDocumentPositionParams, +} + +// --------------------------------------------------------------------- + +pub enum ExpandMacro {} + +impl Request for ExpandMacro { + type Params = ExpandMacroParams; + type Result = Option; + const METHOD: &'static str = "elp/expandMacro"; +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ExpandMacroParams { + pub text_document: TextDocumentIdentifier, + pub position: Position, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ExpandedMacro { + pub name: String, + pub expansion: String, +} + +// --------------------------------------------------------------------- +pub enum StatusNotification {} + +#[derive(Debug, Serialize, Deserialize)] +pub enum Status { + Loading, + Running, + ShuttingDown, + Invalid, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct StatusParams { + pub status: Status, +} + +impl Notification for StatusNotification { + type Params = StatusParams; + const METHOD: &'static str = "elp/status"; +} + +// --------------------------------------------------------------------- + +pub enum Ping {} +impl Request for Ping { + type Params = Vec; + type Result = String; + const METHOD: &'static str = "elp/ping"; +} + +// --------------------------------------------------------------------- + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Runnable { + pub label: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub location: Option, + pub kind: RunnableKind, + pub args: Buck2RunnableArgs, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "lowercase")] +pub enum RunnableKind { + Buck2, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Buck2RunnableArgs { + pub workspace_root: PathBuf, + pub command: String, + pub args: Vec, + pub target: String, + pub id: String, +} +pub enum ExternalDocs {} + +impl Request for ExternalDocs { + type Params = lsp_types::TextDocumentPositionParams; + type Result = Option>; + const METHOD: &'static str = "experimental/externalDocs"; +} diff --git a/crates/elp/src/op_queue.rs b/crates/elp/src/op_queue.rs new file mode 100644 index 0000000000..0d2ef5a21f --- /dev/null +++ b/crates/elp/src/op_queue.rs @@ -0,0 +1,58 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! Bookkeeping to make sure only one long-running operation is being executed +//! at a time. + +// From https://github.com/rust-lang/rust-analyzer/blob/1efd220f2f844596dd22bfd73a8a0c596354be38/crates/rust-analyzer/src/op_queue.rs + +pub struct OpQueue { + op_requested: Option, + op_in_progress: bool, + last_op_result: Output, +} + +impl Default for OpQueue { + fn default() -> Self { + Self { + op_requested: None, + op_in_progress: false, + last_op_result: Default::default(), + } + } +} + +#[allow(unused)] +impl OpQueue { + pub fn request_op(&mut self, data: Args) { + self.op_requested = Some(data); + } + pub fn should_start_op(&mut self) -> Option { + if self.op_in_progress { + return None; + } + self.op_in_progress = self.op_requested.is_some(); + self.op_requested.take() + } + pub fn op_completed(&mut self, result: Output) { + assert!(self.op_in_progress); + self.op_in_progress = false; + self.last_op_result = result; + } + + pub fn last_op_result(&self) -> &Output { + &self.last_op_result + } + pub fn op_in_progress(&self) -> bool { + self.op_in_progress + } + pub fn op_requested(&self) -> bool { + self.op_requested.is_some() + } +} diff --git a/crates/elp/src/project_loader.rs b/crates/elp/src/project_loader.rs new file mode 100644 index 0000000000..e7c13d677b --- /dev/null +++ b/crates/elp/src/project_loader.rs @@ -0,0 +1,89 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::time::Instant; + +use anyhow::bail; +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_log::telemetry; +use elp_project_model::otp::Otp; +use elp_project_model::DiscoverConfig; +use elp_project_model::ProjectManifest; +use fxhash::FxHashSet; + +pub struct ProjectLoader { + project_roots: FxHashSet, + start: Instant, + initialized: bool, +} + +impl ProjectLoader { + pub fn new() -> Self { + let mut project_roots = FxHashSet::default(); + let otp_root = Otp::find_otp().unwrap(); + let otp_root = AbsPathBuf::assert(otp_root); + project_roots.insert(otp_root); + let start = Instant::now(); + let initialized = false; + ProjectLoader { + project_roots, + start, + initialized, + } + } + + pub fn load_manifest_if_new(&mut self, path: &AbsPath) -> Result> { + let mut path_it = path; + while let Some(path) = path_it.parent() { + if self.project_roots.contains(path) { + return Ok(None); + } + path_it = path; + } + let conf = DiscoverConfig::buck(); + let manifest = match ProjectManifest::discover_single(&path, &conf) { + Ok(manifest) => Ok(manifest), + Err(buck_err) => ProjectManifest::discover_single(&path, &conf.to_rebar()) + .map_err(|rebar_err| (buck_err, rebar_err)), + }; + + match manifest { + Ok(manifest) => { + if let Some(root) = manifest.root().parent() { + log::info!("Opening new project with root {:?}", &root); + self.project_roots.insert(root.to_path_buf()); + } + Ok(Some(manifest)) + } + Err((buck_err, rebar_err)) => { + log::warn!( + "Couldn't open neither buck nor rebar project for path {:?}. buck err: {:?}, rebar err: {:?}", + path, + buck_err, + rebar_err + ); + bail!( + "Couldn't open neither buck nor rebar project for path {:?}", + path + ) + } + } + } + + pub fn load_completed(&mut self) { + if !self.initialized { + self.initialized = true; + let diff = self.start.elapsed().as_millis() as u32; + let data = serde_json::Value::String("project_loading_completed".to_string()); + telemetry::send_with_duration(module_path!().to_string(), data, diff); + } + } +} diff --git a/crates/elp/src/reload.rs b/crates/elp/src/reload.rs new file mode 100644 index 0000000000..3b583eced4 --- /dev/null +++ b/crates/elp/src/reload.rs @@ -0,0 +1,88 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::iter; + +use elp_ide::elp_ide_db::elp_base_db::loader; +use elp_ide::elp_ide_db::elp_base_db::AppType; +use elp_ide::elp_ide_db::elp_base_db::FileSetConfig; +use elp_ide::elp_ide_db::elp_base_db::ProjectApps; +use elp_ide::elp_ide_db::elp_base_db::VfsPath; + +#[derive(Debug)] +pub struct ProjectFolders { + pub load: Vec, + pub watch: Vec, + pub file_set_config: FileSetConfig, +} + +impl ProjectFolders { + pub fn new(project_apps: &ProjectApps) -> ProjectFolders { + let file_set_config = project_apps + .all_apps + .iter() + .fold( + FileSetConfig::builder(), + |mut builder, (_project_id, app)| { + let mut file_sets: Vec = app + .abs_src_dirs + .iter() + .map(|src| VfsPath::from(src.clone())) + .collect(); + let dir = VfsPath::from(app.dir.clone()); + file_sets.push(dir); + builder.add_file_set(file_sets); + builder + }, + ) + .build(); + + let load = project_apps + .all_apps + .iter() + .flat_map(|(_, app)| { + let dirs = loader::Directories { + extensions: vec!["erl".to_string(), "hrl".to_string(), "escript".to_string()], + include: app.all_source_dirs(), + exclude: vec![], + }; + let dir_entry = loader::Entry::Directories(dirs); + match app.app_type { + AppType::App => vec![ + dir_entry, + loader::Entry::Files(vec![app.dir.join(".eqwalizer")]), + ], + _ => vec![dir_entry], + } + }) + .collect(); + + let watch = project_apps + .all_apps + .iter() + .flat_map(|(project_id, app)| iter::repeat(project_id).zip(app.all_source_dirs())) + .filter_map(|(project_id, root)| { + if Some(*project_id) != project_apps.otp_project_id { + Some(lsp_types::FileSystemWatcher { + glob_pattern: format!("{}/**/*.{{e,h}}rl", root.display()), + kind: None, + }) + } else { + None + } + }) + .collect(); + + ProjectFolders { + load, + watch, + file_set_config, + } + } +} diff --git a/crates/elp/src/resources/test/diagnostics/parse_all_diagnostics1.stdout b/crates/elp/src/resources/test/diagnostics/parse_all_diagnostics1.stdout new file mode 100644 index 0000000000..c06d295c23 --- /dev/null +++ b/crates/elp/src/resources/test/diagnostics/parse_all_diagnostics1.stdout @@ -0,0 +1,8 @@ +module specified: diagnostics +Diagnostics reported in 1 modules: + diagnostics: 5 + 3:0-3:35::[Warning] [L1500] Unused file: broken_diagnostics.hrl + 3:0-3:0::[Error] [L0000] Issue in included file + 5:30-5:44::[Error] [L1295] type undefined_type() undefined + 6:0-6:4::[Warning] [L1230] function main/1 is unused + 9:0-9:3::[Warning] [L1230] function foo/0 is unused diff --git a/crates/elp/src/resources/test/diagnostics/parse_all_diagnostics_errors_escript.stdout b/crates/elp/src/resources/test/diagnostics/parse_all_diagnostics_errors_escript.stdout new file mode 100644 index 0000000000..c9a49bb482 --- /dev/null +++ b/crates/elp/src/resources/test/diagnostics/parse_all_diagnostics_errors_escript.stdout @@ -0,0 +1,4 @@ +file specified: {project_path}/app_a/src/diagnostics_errors.escript +Diagnostics reported in 1 modules: + diagnostics_errors.escript: 1 + 23:5-23:20::[Error] [P1711] syntax error before: tion_with_error diff --git a/crates/elp/src/resources/test/diagnostics/parse_all_diagnostics_escript.stdout b/crates/elp/src/resources/test/diagnostics/parse_all_diagnostics_escript.stdout new file mode 100644 index 0000000000..006718d31b --- /dev/null +++ b/crates/elp/src/resources/test/diagnostics/parse_all_diagnostics_escript.stdout @@ -0,0 +1,2 @@ +file specified: {project_path}/app_a/src/diagnostics.escript +No errors reported diff --git a/crates/elp/src/resources/test/diagnostics/parse_all_diagnostics_hrl.stdout b/crates/elp/src/resources/test/diagnostics/parse_all_diagnostics_hrl.stdout new file mode 100644 index 0000000000..5b4d9cd6a4 --- /dev/null +++ b/crates/elp/src/resources/test/diagnostics/parse_all_diagnostics_hrl.stdout @@ -0,0 +1,6 @@ +file specified: {project_path}/app_a/include/broken_diagnostics.hrl +Diagnostics reported in 1 modules: + broken_diagnostics.hrl: 3 + 0:1-0:6::[Error] [W0013] misspelled attribute, saw 'defin' but expected 'define' + 0:7-0:14::[Error] [P1702] bad attribute + 2:5-2:14::[Error] [P1702] bad attribute diff --git a/crates/elp/src/resources/test/diagnostics/parse_all_diagnostics_warnings_escript.stdout b/crates/elp/src/resources/test/diagnostics/parse_all_diagnostics_warnings_escript.stdout new file mode 100644 index 0000000000..9ac4638a97 --- /dev/null +++ b/crates/elp/src/resources/test/diagnostics/parse_all_diagnostics_warnings_escript.stdout @@ -0,0 +1,4 @@ +file specified: {project_path}/app_a/src/diagnostics_warnings.escript +Diagnostics reported in 1 modules: + diagnostics_warnings.escript: 1 + 23:0-23:6::[Warning] [L1230] function unused/0 is unused diff --git a/crates/elp/src/resources/test/diagnostics/parse_elp_lint1.stdout b/crates/elp/src/resources/test/diagnostics/parse_elp_lint1.stdout new file mode 100644 index 0000000000..128ee26cae --- /dev/null +++ b/crates/elp/src/resources/test/diagnostics/parse_elp_lint1.stdout @@ -0,0 +1,4 @@ +module specified: lints +Diagnostics reported in 1 modules: + lints: 1 + 4:0-4:13::[Error] [P1700] head mismatch 'head_mismatcX' vs 'head_mismatch' diff --git a/crates/elp/src/resources/test/diagnostics/parse_elp_lint_fix.stdout b/crates/elp/src/resources/test/diagnostics/parse_elp_lint_fix.stdout new file mode 100644 index 0000000000..b3a5f365b7 --- /dev/null +++ b/crates/elp/src/resources/test/diagnostics/parse_elp_lint_fix.stdout @@ -0,0 +1,17 @@ +module specified: lints +Diagnostics reported in 1 modules: + lints: 1 + 4:0-4:13::[Error] [P1700] head mismatch 'head_mismatcX' vs 'head_mismatch' +--------------------------------------------- + +Applying fix in module 'lints' for + 4:0-4:13::[Error] [P1700] head mismatch 'head_mismatcX' vs 'head_mismatch' +@@ -1,6 +1,6 @@ + -module(lints). + -export([head_mismatch/1]). + + head_mismatch(X) -> X; +-head_mismatcX(0) -> 0. ++head_mismatch(0) -> 0. + + diff --git a/crates/elp/src/resources/test/diagnostics/parse_elp_lint_fix_json.stdout b/crates/elp/src/resources/test/diagnostics/parse_elp_lint_fix_json.stdout new file mode 100644 index 0000000000..0da3869de5 --- /dev/null +++ b/crates/elp/src/resources/test/diagnostics/parse_elp_lint_fix_json.stdout @@ -0,0 +1 @@ +{"path":"app_a/src/lints.erl","line":5,"char":1,"code":"ELP","severity":"error","name":"head_mismatch","original":null,"replacement":null,"description":"head mismatch 'head_mismatcX' vs 'head_mismatch'"} diff --git a/crates/elp/src/resources/test/diagnostics/parse_elp_lint_recursive.stdout b/crates/elp/src/resources/test/diagnostics/parse_elp_lint_recursive.stdout new file mode 100644 index 0000000000..5a95580e82 --- /dev/null +++ b/crates/elp/src/resources/test/diagnostics/parse_elp_lint_recursive.stdout @@ -0,0 +1,45 @@ +module specified: lint_recursive +Diagnostics reported in 1 modules: + lint_recursive: 1 + 10:4-10:20::[Warning] [W0007] match is redundant +--------------------------------------------- + +Applying fix in module 'lint_recursive' for + 10:4-10:20::[Warning] [W0007] match is redundant +@@ -8,7 +8,7 @@ + + test_foo(Config) -> + do_something(), +- Config1 = Config, ++ Config, + clean_mocks(). + + clean_mocks() -> + +--------------------------------------------- + +Applying fix in module 'lint_recursive' for + 10:4-10:10::[Warning] [W0006] this statement has no effect +@@ -8,7 +8,6 @@ + + test_foo(Config) -> + do_something(), +- Config, + clean_mocks(). + + clean_mocks() -> + +--------------------------------------------- + +Applying fix in module 'lint_recursive' for + 8:9-8:15::[Warning] [W0010] this variable is unused +@@ -6,7 +6,7 @@ + ok, + ok. + +-test_foo(Config) -> ++test_foo(_Config) -> + do_something(), + clean_mocks(). + + diff --git a/crates/elp/src/resources/test/eqwalize_all_help.stdout b/crates/elp/src/resources/test/eqwalize_all_help.stdout new file mode 100644 index 0000000000..e3ebbda395 --- /dev/null +++ b/crates/elp/src/resources/test/eqwalize_all_help.stdout @@ -0,0 +1,9 @@ +Usage: [--project PROJECT] [--as PROFILE] [[--format FORMAT]] [--rebar] [--include-generated] + +Available options: + --project Path to directory with project (defaults to `.`) + --as Rebar3 profile to pickup (default is test) + --format Show diagnostics in JSON format + --rebar Run with rebar + --include-generated Also eqwalize opted-in generated modules from project + -h, --help Prints help information diff --git a/crates/elp/src/resources/test/help.stdout b/crates/elp/src/resources/test/help.stdout new file mode 100644 index 0000000000..a0f9907beb --- /dev/null +++ b/crates/elp/src/resources/test/help.stdout @@ -0,0 +1,22 @@ +Usage: [--log-file LOG_FILE] [--no-log-buffering] [COMMAND ...] + +Available options: + --log-file + --no-log-buffering + -h, --help Prints help information + +Available commands: + eqwalize Eqwalize specified module + eqwalize-all Eqwalize all opted-in modules in a project + eqwalize-app Eqwalize all opted-in modules in specified application + eqwalize-target Eqwalize all opted-in modules in specified buck target + lint Parse files in project and emit diagnostics, optionally apply fixes. + server Run lsp server + generate-completions Generate shell completions + parse-all Dump ast for all files in a project for specified rebar.config file + parse-elp Tree-sitter parse all files in a project for specified rebar.config file + eqwalize-passthrough Pass args to eqwalizer + build-info Generate build info file + version Print version + shell Starts an interactive ELP shell + eqwalize-stats Return statistics about code quality for eqWAlizer diff --git a/crates/elp/src/resources/test/lint/head_mismatch/app_a/src/lints.erl b/crates/elp/src/resources/test/lint/head_mismatch/app_a/src/lints.erl new file mode 100644 index 0000000000..1357b031de --- /dev/null +++ b/crates/elp/src/resources/test/lint/head_mismatch/app_a/src/lints.erl @@ -0,0 +1,6 @@ +-module(lints). +-export([head_mismatch/1]). + +head_mismatch(X) -> X; +head_mismatch(0) -> 0. + diff --git a/crates/elp/src/resources/test/lint/lint_recursive/app_a/src/lint_recursive.erl b/crates/elp/src/resources/test/lint/lint_recursive/app_a/src/lint_recursive.erl new file mode 100644 index 0000000000..a09ebcec66 --- /dev/null +++ b/crates/elp/src/resources/test/lint/lint_recursive/app_a/src/lint_recursive.erl @@ -0,0 +1,15 @@ +-module(lint_recursive). + +-export([test_foo/1]). + +do_something() -> + ok, + ok. + +test_foo(_Config) -> + do_something(), + clean_mocks(). + +clean_mocks() -> + redundant, + ok. diff --git a/crates/elp/src/resources/test/lint_help.stdout b/crates/elp/src/resources/test/lint_help.stdout new file mode 100644 index 0000000000..52ba518494 --- /dev/null +++ b/crates/elp/src/resources/test/lint_help.stdout @@ -0,0 +1,25 @@ +Usage: [--project PROJECT] [--module MODULE] [--file FILE] [--to TO] [--no-diags] [--experimental] [--as PROFILE] [[--format FORMAT]] [--rebar] [--include-generated] [--apply-fix] [--recursive] [--in-place] [--diagnostic-filter FILTER] [--line-from LINE_FROM] [--line-to LINE_TO] ... + +Available positional items: + Rest of args are space separated list of apps to ignore + +Available options: + --project Path to directory with project (defaults to `.`) + --module Parse a single module from the project, not the entire project. + --file Parse a single file from the project, not the entire project. This can be an include file or escript, etc. + --to Path to a directory where to dump result files + --no-diags Do not print the full diagnostics for a file, just the count + --experimental Report experimental diagnostics too, if diagnostics are enabled + --as Rebar3 profile to pickup (default is test) + --format Show diagnostics in JSON format + --rebar Run with rebar + --include-generated + --apply-fix If the diagnostic has an associated fix, apply it. The modified file will be in the --to directory, or original file if --in-place is set. + --recursive If applying fixes, apply any new ones that arise from the + prior fixes recursively. Limited in scope to the clause of the + prior change. + --in-place When applying a fix, modify the original file. + --diagnostic-filter Filter out all reported diagnostics except this one + --line-from Filter out all reported diagnostics before this line. Valid only for single file + --line-to Filter out all reported diagnostics after this line. Valid only for single file + -h, --help Prints help information diff --git a/crates/elp/src/resources/test/linter/parse_elp_lint2.stdout b/crates/elp/src/resources/test/linter/parse_elp_lint2.stdout new file mode 100644 index 0000000000..b0cc8a1337 --- /dev/null +++ b/crates/elp/src/resources/test/linter/parse_elp_lint2.stdout @@ -0,0 +1,4 @@ +module specified: app_a +Diagnostics reported in 1 modules: + app_a: 1 + 8:0-8:4::[Error] [P1700] head mismatch 'fooX' vs 'food' diff --git a/crates/elp/src/resources/test/linter/parse_elp_lint_ignore_apps.stdout b/crates/elp/src/resources/test/linter/parse_elp_lint_ignore_apps.stdout new file mode 100644 index 0000000000..e7a35d0262 --- /dev/null +++ b/crates/elp/src/resources/test/linter/parse_elp_lint_ignore_apps.stdout @@ -0,0 +1,3 @@ +Diagnostics reported in 1 modules: + app_b_unused_param: 1 + 4:4-4:5::[Warning] [W0010] this variable is unused diff --git a/crates/elp/src/resources/test/linter/parse_elp_lint_ignore_apps_b.stdout b/crates/elp/src/resources/test/linter/parse_elp_lint_ignore_apps_b.stdout new file mode 100644 index 0000000000..00d4e2355e --- /dev/null +++ b/crates/elp/src/resources/test/linter/parse_elp_lint_ignore_apps_b.stdout @@ -0,0 +1,5 @@ +Diagnostics reported in 2 modules: + app_a: 1 + 8:5-8:6::[Warning] [W0010] this variable is unused + app_a_unused_param: 1 + 4:4-4:5::[Warning] [W0010] this variable is unused diff --git a/crates/elp/src/resources/test/linter/parse_elp_lint_json_output.stdout b/crates/elp/src/resources/test/linter/parse_elp_lint_json_output.stdout new file mode 100644 index 0000000000..d0bc91a5d7 --- /dev/null +++ b/crates/elp/src/resources/test/linter/parse_elp_lint_json_output.stdout @@ -0,0 +1,3 @@ +{"path":"app_a/src/app_a.erl","line":9,"char":6,"code":"ELP","severity":"warning","name":"unused_function_arg","original":null,"replacement":null,"description":"this variable is unused"} +{"path":"app_a/src/app_a_unused_param.erl","line":5,"char":5,"code":"ELP","severity":"warning","name":"unused_function_arg","original":null,"replacement":null,"description":"this variable is unused"} +{"path":"app_b/src/app_b_unused_param.erl","line":5,"char":5,"code":"ELP","severity":"warning","name":"unused_function_arg","original":null,"replacement":null,"description":"this variable is unused"} diff --git a/crates/elp/src/resources/test/parse_all_help.stdout b/crates/elp/src/resources/test/parse_all_help.stdout new file mode 100644 index 0000000000..553a0d436e --- /dev/null +++ b/crates/elp/src/resources/test/parse_all_help.stdout @@ -0,0 +1,9 @@ +Usage: [--project PROJECT] --to ARG [--as PROFILE] [--module MODULE] [--buck] + +Available options: + --project Path to directory with project (defaults to `.`) + --to Path to a directory where to dump .etf files + --as Rebar3 profile to pickup (default is test) + --module Parse a single module from the project, not the entire project + --buck Run with buck + -h, --help Prints help information diff --git a/crates/elp/src/resources/test/parse_elp_help.stdout b/crates/elp/src/resources/test/parse_elp_help.stdout new file mode 100644 index 0000000000..08b82d6ae8 --- /dev/null +++ b/crates/elp/src/resources/test/parse_elp_help.stdout @@ -0,0 +1,15 @@ +Usage: [--project PROJECT] [--module MODULE] [--file ARG] [--to TO] [--no-diags] [--experimental] [--as PROFILE] [--dump-includes] [--rebar] [--include-generated] [--serial] + +Available options: + --project Path to directory with project (defaults to `.`) + --module Parse a single module from the project, not the entire project + --file Parse a single file from the project, not the entire project. \nThis can be an include file or escript, etc. + --to Path to a directory where to dump result files + --no-diags Do not print the full diagnostics for a file, just the count + --experimental Report experimental diagnostics too, if diagnostics are enabled + --as Rebar3 profile to pickup (default is test) + --dump-includes Report the resolution of include directives for comparison with OTP ones + --rebar Run with rebar + --include-generated Also eqwalize opted-in generated modules from application + --serial Parse the files serially, not in parallel + -h, --help Prints help information diff --git a/crates/elp/src/resources/test/parse_error/eqwalize_all_diagnostics.jsonl b/crates/elp/src/resources/test/parse_error/eqwalize_all_diagnostics.jsonl new file mode 100644 index 0000000000..e51056da6e --- /dev/null +++ b/crates/elp/src/resources/test/parse_error/eqwalize_all_diagnostics.jsonl @@ -0,0 +1,3 @@ +Failed to parse module parse_error_a_bad + +[cause truncated in tests to avoid absolute paths] diff --git a/crates/elp/src/resources/test/parse_error/eqwalize_elpt2_reference_nonexistent.pretty b/crates/elp/src/resources/test/parse_error/eqwalize_elpt2_reference_nonexistent.pretty new file mode 100644 index 0000000000..a9c9c02af0 --- /dev/null +++ b/crates/elp/src/resources/test/parse_error/eqwalize_elpt2_reference_nonexistent.pretty @@ -0,0 +1,3 @@ +Module elpt2_reference_nonexistent not found + +[cause truncated in tests to avoid absolute paths] \ No newline at end of file diff --git a/crates/elp/src/resources/test/parse_error/eqwalize_parse_error_a.pretty b/crates/elp/src/resources/test/parse_error/eqwalize_parse_error_a.pretty new file mode 100644 index 0000000000..34664e4151 --- /dev/null +++ b/crates/elp/src/resources/test/parse_error/eqwalize_parse_error_a.pretty @@ -0,0 +1,11 @@ +error: incompatible_types + ┌─ parse_error_a/src/parse_error_a.erl:15:5 + │ +15 │ X. + │ ^ X. +Expression has type: [number()] +Context expected type: atom() + +See https://fb.me/eqwalizer_errors#incompatible_types + +1 ERROR diff --git a/crates/elp/src/resources/test/parse_error/eqwalize_parse_error_a_bad.pretty b/crates/elp/src/resources/test/parse_error/eqwalize_parse_error_a_bad.pretty new file mode 100644 index 0000000000..e30f7e78e9 --- /dev/null +++ b/crates/elp/src/resources/test/parse_error/eqwalize_parse_error_a_bad.pretty @@ -0,0 +1,6 @@ +error: parse_error + ┌─ parse_error_a/src/parse_error_a_bad.erl:6:1 + │ +6 │ foon() -> ok. % head-mismatch + │ ^^^^^^^^^^^^ head mismatch + diff --git a/crates/elp/src/resources/test/parse_error/eqwalize_parse_error_a_reference_bad.pretty b/crates/elp/src/resources/test/parse_error/eqwalize_parse_error_a_reference_bad.pretty new file mode 100644 index 0000000000..e30f7e78e9 --- /dev/null +++ b/crates/elp/src/resources/test/parse_error/eqwalize_parse_error_a_reference_bad.pretty @@ -0,0 +1,6 @@ +error: parse_error + ┌─ parse_error_a/src/parse_error_a_bad.erl:6:1 + │ +6 │ foon() -> ok. % head-mismatch + │ ^^^^^^^^^^^^ head mismatch + diff --git a/crates/elp/src/resources/test/standard/eqwalize_all_diagnostics.jsonl b/crates/elp/src/resources/test/standard/eqwalize_all_diagnostics.jsonl new file mode 100644 index 0000000000..602b04ad81 --- /dev/null +++ b/crates/elp/src/resources/test/standard/eqwalize_all_diagnostics.jsonl @@ -0,0 +1,19 @@ +{"path":"app_a/test/app_a_SUITE.erl","line":1,"char":null,"code":"ELP","severity":"advice","name":"ELP","original":null,"replacement":null,"description":"Please remove `-typing([eqwalizer])`. SUITE modules are not checked when eqWAlizing a project."} +{"path":"app_a/src/app_a.erl","line":9,"char":5,"code":"ELP","severity":"error","name":"eqWAlizer: incompatible_types","original":"'error'","replacement":null,"description":"```lang=error,counterexample\n`'error'`.\n\nExpression has type: 'error'\nContext expected type: 'ok'\n```\n\n> [docs on `incompatible_types`](https://fb.me/eqwalizer_errors#incompatible_types)"} +{"path":"app_a/src/app_a.erl","line":13,"char":5,"code":"ELP","severity":"error","name":"eqWAlizer: incompatible_types","original":"'error'","replacement":null,"description":"```lang=error,counterexample\n`'error'`.\n\nExpression has type: 'error'\nContext expected type: 'ok'\n```\n\n> [docs on `incompatible_types`](https://fb.me/eqwalizer_errors#incompatible_types)"} +{"path":"app_a/src/app_a.erl","line":17,"char":13,"code":"ELP","severity":"error","name":"eqWAlizer: incompatible_types","original":"'an_atom'","replacement":null,"description":"```lang=error,counterexample\n`'an_atom'`.\n\nExpression has type: 'an_atom'\nContext expected type: number()\n```\n\n> [docs on `incompatible_types`](https://fb.me/eqwalizer_errors#incompatible_types)"} +{"path":"app_a/src/app_a.erl","line":55,"char":5,"code":"ELP","severity":"error","name":"eqWAlizer: redundant_fixme","original":null,"replacement":null,"description":"```lang=error,counterexample\n\nredundant fixme\n```\n\n> [docs on `redundant_fixme`](https://fb.me/eqwalizer_errors#redundant_fixme)"} +{"path":"app_a/src/app_a.erl","line":77,"char":5,"code":"ELP","severity":"error","name":"eqWAlizer: incompatible_types","original":"X","replacement":null,"description":"```lang=error,counterexample\n`X`.\n\nExpression has type: #S{k_extra => term(), k_ok => term(), k_req1 => term(), k_req2 => term(), k_wrong1 => pid(), k_wrong2 => pid()}\nContext expected type: #S{k_ok => term(), k_req1 := atom(), k_req2 := atom(), k_req3 := atom(), k_wrong1 => atom(), k_wrong2 => atom()}\n```\n```\nThese associations do not match:\n\n #S{\n+ k_extra => ...\n- k_req1 := ...\n+ k_req1 => ...\n- k_req2 := ...\n+ k_req2 => ...\n- k_req3 := ...\n ...\n }\n```\n> [docs on `incompatible_types`](https://fb.me/eqwalizer_errors#incompatible_types)"} +{"path":"app_a/src/app_a.erl","line":101,"char":5,"code":"ELP","severity":"error","name":"eqWAlizer: incompatible_types","original":"X","replacement":null,"description":"```lang=error,counterexample\n`X`.\n\nExpression has type: id(#S{a := 'va', b := #S{c := #S{d => atom()}}})\nContext expected type: #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}}\n```\n```\n id(#S{a := 'va', b := #S{c := #S{d => atom()}}}) is not compatible with #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}}\n because\n at shape key 'b':\n #S{a := 'va', b := #S{c := #S{d => atom()}}} is not compatible with #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}}\n because\n at shape key 'c':\n #S{c := #S{d => atom()}} is not compatible with #S{c := id(#S{d := atom(), e := atom()})}\n because\n #S{d => atom()} is not compatible with id(#S{d := atom(), e := atom()})\n```\n> [docs on `incompatible_types`](https://fb.me/eqwalizer_errors#incompatible_types)"} +{"path":"app_a/src/app_a.erl","line":124,"char":5,"code":"ELP","severity":"error","name":"eqWAlizer: incompatible_types","original":"X","replacement":null,"description":"```lang=error,counterexample\n`X`.\n\nExpression has type: id(#S{a := 'va', b := #S{c := #S{d := pid(), e := pid()}}})\nContext expected type: #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}}\n```\n```\n id(#S{a := 'va', b := #S{c := #S{d := pid(), e := pid()}}}) is not compatible with #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}}\n because\n at shape key 'b':\n #S{a := 'va', b := #S{c := #S{d := pid(), e := pid()}}} is not compatible with #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}}\n because\n at shape key 'c':\n #S{c := #S{d := pid(), e := pid()}} is not compatible with #S{c := id(#S{d := atom(), e := atom()})}\n because\n #S{d := pid(), e := pid()} is not compatible with id(#S{d := atom(), e := atom()})\n```\n> [docs on `incompatible_types`](https://fb.me/eqwalizer_errors#incompatible_types)"} +{"path":"app_a/test/app_a_SUITE.erl","line":18,"char":5,"code":"ELP","severity":"error","name":"eqWAlizer: incompatible_types","original":"app_a_test_helpers:fail()","replacement":null,"description":"```lang=error,counterexample\n`app_a_test_helpers:fail()`.\n\nExpression has type: 'error'\nContext expected type: 'ok'\n```\n\n> [docs on `incompatible_types`](https://fb.me/eqwalizer_errors#incompatible_types)"} +{"path":"app_a/src/app_a_lists.erl","line":576,"char":5,"code":"ELP","severity":"error","name":"eqWAlizer: incompatible_types","original":"flatmap(thing_to_list/1, List)","replacement":null,"description":"```lang=error,counterexample\n`flatmap(thing_to_list/1, List)`.\n\nExpression has type: [term()]\nContext expected type: string()\n```\n\n> [docs on `incompatible_types`](https://fb.me/eqwalizer_errors#incompatible_types)"} +{"path":"app_a/src/app_a_lists.erl","line":588,"char":29,"code":"ELP","severity":"error","name":"eqWAlizer: recursive_constraint","original":null,"replacement":null,"description":"```lang=error,counterexample\n\nRecursive constraint: DeepList\n```\n\n> [docs on `recursive_constraint`](https://fb.me/eqwalizer_errors#recursive_constraint)"} +{"path":"app_a/src/app_a_lists.erl","line":595,"char":29,"code":"ELP","severity":"error","name":"eqWAlizer: recursive_constraint","original":null,"replacement":null,"description":"```lang=error,counterexample\n\nRecursive constraint: DeepList\n```\n\n> [docs on `recursive_constraint`](https://fb.me/eqwalizer_errors#recursive_constraint)"} +{"path":"app_a/src/app_a_lists.erl","line":613,"char":29,"code":"ELP","severity":"error","name":"eqWAlizer: recursive_constraint","original":null,"replacement":null,"description":"```lang=error,counterexample\n\nRecursive constraint: DeepList\n```\n\n> [docs on `recursive_constraint`](https://fb.me/eqwalizer_errors#recursive_constraint)"} +{"path":"app_a/src/app_a_lists.erl","line":1114,"char":36,"code":"ELP","severity":"error","name":"eqWAlizer: incompatible_types","original":"H3","replacement":null,"description":"```lang=error,counterexample\n`H3`.\n\nExpression has type: term()\nContext expected type: [term()]\n```\n\n> [docs on `incompatible_types`](https://fb.me/eqwalizer_errors#incompatible_types)"} +{"path":"app_a/src/app_a_lists.erl","line":1305,"char":5,"code":"ELP","severity":"error","name":"eqWAlizer: incompatible_types","original":"filtermap(eqwalizer:dynamic_cast(F), L)","replacement":null,"description":"```lang=error,counterexample\n`filtermap(eqwalizer:dynamic_cast(F), L)`.\n\nExpression has type: [term()]\nContext expected type: [T | X]\n```\n```\n [term()] is not compatible with [T | X]\n because\n term() is not compatible with T | X\n```\n> [docs on `incompatible_types`](https://fb.me/eqwalizer_errors#incompatible_types)"} +{"path":"app_a/src/app_a_lists.erl","line":1305,"char":15,"code":"ELP","severity":"error","name":"eqWAlizer: incompatible_types","original":"F","replacement":null,"description":"```lang=error,counterexample\n`F`.\n\nExpression has type: fun((T) -> boolean() | {'true', X})\nContext expected type: fun((term()) -> boolean() | {'true', term()})\n```\n```\n fun((T) -> boolean() | {'true', X}) is not compatible with fun((term()) -> boolean() | {'true', term()})\n because\n term() is not compatible with T\n```\n> [docs on `incompatible_types`](https://fb.me/eqwalizer_errors#incompatible_types)"} +{"path":"app_a/src/app_a_mod2.erl","line":22,"char":1,"code":"ELP","severity":"error","name":"eqWAlizer: type_alias_is_non_productive","original":null,"replacement":null,"description":"```lang=error,counterexample\n\nrecursive type invalid/0 is not productive\n```\n\n> [docs on `type_alias_is_non_productive`](https://fb.me/eqwalizer_errors#type_alias_is_non_productive)"} +{"path":"app_a/src/app_a_mod2.erl","line":31,"char":9,"code":"ELP","severity":"error","name":"eqWAlizer: incompatible_types","original":"'an_atom'","replacement":null,"description":"```lang=error,counterexample\n`'an_atom'`.\n\nExpression has type: 'an_atom'\nContext expected type: number()\n```\n\n> [docs on `incompatible_types`](https://fb.me/eqwalizer_errors#incompatible_types)"} +{"path":"app_a/test/app_a_test_helpers.erl","line":6,"char":11,"code":"ELP","severity":"error","name":"eqWAlizer: incompatible_types","original":"'wrong_ret'","replacement":null,"description":"```lang=error,counterexample\n`'wrong_ret'`.\n\nExpression has type: 'wrong_ret'\nContext expected type: 'error'\n```\n\n> [docs on `incompatible_types`](https://fb.me/eqwalizer_errors#incompatible_types)"} diff --git a/crates/elp/src/resources/test/standard/eqwalize_all_diagnostics.pretty b/crates/elp/src/resources/test/standard/eqwalize_all_diagnostics.pretty new file mode 100644 index 0000000000..434120af63 --- /dev/null +++ b/crates/elp/src/resources/test/standard/eqwalize_all_diagnostics.pretty @@ -0,0 +1,232 @@ +note: advice + ┌─ app_a/test/app_a_SUITE.erl:1:2 + │ +1 │ -module(app_a_SUITE). + │ ^ Please remove `-typing([eqwalizer])`. SUITE modules are not checked when eqWAlizing a project. + +error: incompatible_types + ┌─ app_a/src/app_a.erl:9:5 + │ +9 │ ?OK. + │ ^^^ 'error'. +Expression has type: 'error' +Context expected type: 'ok' + +See https://fb.me/eqwalizer_errors#incompatible_types + +error: incompatible_types + ┌─ app_a/src/app_a.erl:13:5 + │ +13 │ error. + │ ^^^^^ 'error'. +Expression has type: 'error' +Context expected type: 'ok' + +See https://fb.me/eqwalizer_errors#incompatible_types + +error: incompatible_types + ┌─ app_a/src/app_a.erl:17:13 + │ +17 │ _ = 3 * an_atom, ok. + │ ^^^^^^^ 'an_atom'. +Expression has type: 'an_atom' +Context expected type: number() + +See https://fb.me/eqwalizer_errors#incompatible_types + +error: redundant_fixme + ┌─ app_a/src/app_a.erl:55:5 + │ +55 │ % eqwalizer:fixme redundant issue should be reported + │ ^^^^^^^^^^^^^^^^^ redundant fixme + +See https://fb.me/eqwalizer_errors#redundant_fixme + +error: incompatible_types + ┌─ app_a/src/app_a.erl:77:5 + │ +77 │ X. + │ ^ + │ │ + │ X. +Expression has type: #S{k_extra => term(), k_ok => term(), k_req1 => term(), k_req2 => term(), k_wrong1 => pid(), k_wrong2 => pid()} +Context expected type: #S{k_ok => term(), k_req1 := atom(), k_req2 := atom(), k_req3 := atom(), k_wrong1 => atom(), k_wrong2 => atom()} + +See https://fb.me/eqwalizer_errors#incompatible_types + │ + +These associations do not match: + + #S{ ++ k_extra => ... +- k_req1 := ... ++ k_req1 => ... +- k_req2 := ... ++ k_req2 => ... +- k_req3 := ... + ... + } + +error: incompatible_types + ┌─ app_a/src/app_a.erl:101:5 + │ +101 │ X. + │ ^ + │ │ + │ X. +Expression has type: id(#S{a := 'va', b := #S{c := #S{d => atom()}}}) +Context expected type: #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}} + +See https://fb.me/eqwalizer_errors#incompatible_types + │ + + id(#S{a := 'va', b := #S{c := #S{d => atom()}}}) is not compatible with #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}} + because + at shape key 'b': + #S{a := 'va', b := #S{c := #S{d => atom()}}} is not compatible with #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}} + because + at shape key 'c': + #S{c := #S{d => atom()}} is not compatible with #S{c := id(#S{d := atom(), e := atom()})} + because + #S{d => atom()} is not compatible with id(#S{d := atom(), e := atom()}) + +error: incompatible_types + ┌─ app_a/src/app_a.erl:124:5 + │ +124 │ X. + │ ^ + │ │ + │ X. +Expression has type: id(#S{a := 'va', b := #S{c := #S{d := pid(), e := pid()}}}) +Context expected type: #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}} + +See https://fb.me/eqwalizer_errors#incompatible_types + │ + + id(#S{a := 'va', b := #S{c := #S{d := pid(), e := pid()}}}) is not compatible with #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}} + because + at shape key 'b': + #S{a := 'va', b := #S{c := #S{d := pid(), e := pid()}}} is not compatible with #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}} + because + at shape key 'c': + #S{c := #S{d := pid(), e := pid()}} is not compatible with #S{c := id(#S{d := atom(), e := atom()})} + because + #S{d := pid(), e := pid()} is not compatible with id(#S{d := atom(), e := atom()}) + +error: incompatible_types + ┌─ app_a/test/app_a_SUITE.erl:18:5 + │ +18 │ app_a_test_helpers:fail(). + │ ^^^^^^^^^^^^^^^^^^^^^^^^^ app_a_test_helpers:fail(). +Expression has type: 'error' +Context expected type: 'ok' + +See https://fb.me/eqwalizer_errors#incompatible_types + +error: incompatible_types + ┌─ app_a/src/app_a_lists.erl:576:5 + │ +576 │ flatmap(fun thing_to_list/1, List). + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ flatmap(thing_to_list/1, List). +Expression has type: [term()] +Context expected type: string() + +See https://fb.me/eqwalizer_errors#incompatible_types + +error: recursive_constraint + ┌─ app_a/src/app_a_lists.erl:588:29 + │ +588 │ DeepList :: [term() | DeepList], + │ ^^^^^^^^ Recursive constraint: DeepList + +See https://fb.me/eqwalizer_errors#recursive_constraint + +error: recursive_constraint + ┌─ app_a/src/app_a_lists.erl:595:29 + │ +595 │ DeepList :: [term() | DeepList], + │ ^^^^^^^^ Recursive constraint: DeepList + +See https://fb.me/eqwalizer_errors#recursive_constraint + +error: recursive_constraint + ┌─ app_a/src/app_a_lists.erl:613:29 + │ +613 │ DeepList :: [term() | DeepList]. + │ ^^^^^^^^ Recursive constraint: DeepList + +See https://fb.me/eqwalizer_errors#recursive_constraint + +error: incompatible_types + ┌─ app_a/src/app_a_lists.erl:1114:36 + │ +1114 │ lists:reverse(umerge3_1(L1, [H2 | H3], T2, H2, [], T3, H3), []). + │ ^^^^^ H3. +Expression has type: term() +Context expected type: [term()] + +See https://fb.me/eqwalizer_errors#incompatible_types + +error: incompatible_types + ┌─ app_a/src/app_a_lists.erl:1305:5 + │ +1305 │ filtermap(F, L). + │ ^^^^^^^^^^^^^^^ + │ │ + │ filtermap(eqwalizer:dynamic_cast(F), L). +Expression has type: [term()] +Context expected type: [T | X] + +See https://fb.me/eqwalizer_errors#incompatible_types + │ + + [term()] is not compatible with [T | X] + because + term() is not compatible with T | X + +error: incompatible_types + ┌─ app_a/src/app_a_lists.erl:1305:15 + │ +1305 │ filtermap(F, L). + │ ^ + │ │ + │ F. +Expression has type: fun((T) -> boolean() | {'true', X}) +Context expected type: fun((term()) -> boolean() | {'true', term()}) + +See https://fb.me/eqwalizer_errors#incompatible_types + │ + + fun((T) -> boolean() | {'true', X}) is not compatible with fun((term()) -> boolean() | {'true', term()}) + because + term() is not compatible with T + +error: type_alias_is_non_productive + ┌─ app_a/src/app_a_mod2.erl:22:1 + │ +22 │ -type invalid() :: invalid(). + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^ recursive type invalid/0 is not productive + +See https://fb.me/eqwalizer_errors#type_alias_is_non_productive + +error: incompatible_types + ┌─ app_a/src/app_a_mod2.erl:31:9 + │ +31 │ 1 + an_atom, + │ ^^^^^^^ 'an_atom'. +Expression has type: 'an_atom' +Context expected type: number() + +See https://fb.me/eqwalizer_errors#incompatible_types + +error: incompatible_types + ┌─ app_a/test/app_a_test_helpers.erl:6:11 + │ +6 │ fail() -> wrong_ret. + │ ^^^^^^^^^ 'wrong_ret'. +Expression has type: 'wrong_ret' +Context expected type: 'error' + +See https://fb.me/eqwalizer_errors#incompatible_types + +18 ERRORS diff --git a/crates/elp/src/resources/test/standard/eqwalize_all_diagnostics_gen.jsonl b/crates/elp/src/resources/test/standard/eqwalize_all_diagnostics_gen.jsonl new file mode 100644 index 0000000000..663e7bbfdb --- /dev/null +++ b/crates/elp/src/resources/test/standard/eqwalize_all_diagnostics_gen.jsonl @@ -0,0 +1,20 @@ +{"path":"app_a/test/app_a_SUITE.erl","line":1,"char":null,"code":"ELP","severity":"advice","name":"ELP","original":null,"replacement":null,"description":"Please remove `-typing([eqwalizer])`. SUITE modules are not checked when eqWAlizing a project."} +{"path":"app_a/src/app_a.erl","line":9,"char":5,"code":"ELP","severity":"error","name":"eqWAlizer: incompatible_types","original":"'error'","replacement":null,"description":"```lang=error,counterexample\n`'error'`.\n\nExpression has type: 'error'\nContext expected type: 'ok'\n```\n\n> [docs on `incompatible_types`](https://fb.me/eqwalizer_errors#incompatible_types)"} +{"path":"app_a/src/app_a.erl","line":13,"char":5,"code":"ELP","severity":"error","name":"eqWAlizer: incompatible_types","original":"'error'","replacement":null,"description":"```lang=error,counterexample\n`'error'`.\n\nExpression has type: 'error'\nContext expected type: 'ok'\n```\n\n> [docs on `incompatible_types`](https://fb.me/eqwalizer_errors#incompatible_types)"} +{"path":"app_a/src/app_a.erl","line":17,"char":13,"code":"ELP","severity":"error","name":"eqWAlizer: incompatible_types","original":"'an_atom'","replacement":null,"description":"```lang=error,counterexample\n`'an_atom'`.\n\nExpression has type: 'an_atom'\nContext expected type: number()\n```\n\n> [docs on `incompatible_types`](https://fb.me/eqwalizer_errors#incompatible_types)"} +{"path":"app_a/src/app_a.erl","line":55,"char":5,"code":"ELP","severity":"error","name":"eqWAlizer: redundant_fixme","original":null,"replacement":null,"description":"```lang=error,counterexample\n\nredundant fixme\n```\n\n> [docs on `redundant_fixme`](https://fb.me/eqwalizer_errors#redundant_fixme)"} +{"path":"app_a/src/app_a.erl","line":77,"char":5,"code":"ELP","severity":"error","name":"eqWAlizer: incompatible_types","original":"X","replacement":null,"description":"```lang=error,counterexample\n`X`.\n\nExpression has type: #S{k_extra => term(), k_ok => term(), k_req1 => term(), k_req2 => term(), k_wrong1 => pid(), k_wrong2 => pid()}\nContext expected type: #S{k_ok => term(), k_req1 := atom(), k_req2 := atom(), k_req3 := atom(), k_wrong1 => atom(), k_wrong2 => atom()}\n```\n```\nThese associations do not match:\n\n #S{\n+ k_extra => ...\n- k_req1 := ...\n+ k_req1 => ...\n- k_req2 := ...\n+ k_req2 => ...\n- k_req3 := ...\n ...\n }\n```\n> [docs on `incompatible_types`](https://fb.me/eqwalizer_errors#incompatible_types)"} +{"path":"app_a/src/app_a.erl","line":101,"char":5,"code":"ELP","severity":"error","name":"eqWAlizer: incompatible_types","original":"X","replacement":null,"description":"```lang=error,counterexample\n`X`.\n\nExpression has type: id(#S{a := 'va', b := #S{c := #S{d => atom()}}})\nContext expected type: #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}}\n```\n```\n id(#S{a := 'va', b := #S{c := #S{d => atom()}}}) is not compatible with #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}}\n because\n at shape key 'b':\n #S{a := 'va', b := #S{c := #S{d => atom()}}} is not compatible with #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}}\n because\n at shape key 'c':\n #S{c := #S{d => atom()}} is not compatible with #S{c := id(#S{d := atom(), e := atom()})}\n because\n #S{d => atom()} is not compatible with id(#S{d := atom(), e := atom()})\n```\n> [docs on `incompatible_types`](https://fb.me/eqwalizer_errors#incompatible_types)"} +{"path":"app_a/src/app_a.erl","line":124,"char":5,"code":"ELP","severity":"error","name":"eqWAlizer: incompatible_types","original":"X","replacement":null,"description":"```lang=error,counterexample\n`X`.\n\nExpression has type: id(#S{a := 'va', b := #S{c := #S{d := pid(), e := pid()}}})\nContext expected type: #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}}\n```\n```\n id(#S{a := 'va', b := #S{c := #S{d := pid(), e := pid()}}}) is not compatible with #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}}\n because\n at shape key 'b':\n #S{a := 'va', b := #S{c := #S{d := pid(), e := pid()}}} is not compatible with #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}}\n because\n at shape key 'c':\n #S{c := #S{d := pid(), e := pid()}} is not compatible with #S{c := id(#S{d := atom(), e := atom()})}\n because\n #S{d := pid(), e := pid()} is not compatible with id(#S{d := atom(), e := atom()})\n```\n> [docs on `incompatible_types`](https://fb.me/eqwalizer_errors#incompatible_types)"} +{"path":"app_a/test/app_a_SUITE.erl","line":18,"char":5,"code":"ELP","severity":"error","name":"eqWAlizer: incompatible_types","original":"app_a_test_helpers:fail()","replacement":null,"description":"```lang=error,counterexample\n`app_a_test_helpers:fail()`.\n\nExpression has type: 'error'\nContext expected type: 'ok'\n```\n\n> [docs on `incompatible_types`](https://fb.me/eqwalizer_errors#incompatible_types)"} +{"path":"app_a/src/app_a_errors_generated.erl","line":8,"char":10,"code":"ELP","severity":"error","name":"eqWAlizer: incompatible_types","original":"'wrong_ret'","replacement":null,"description":"```lang=error,counterexample\n`'wrong_ret'`.\n\nExpression has type: 'wrong_ret'\nContext expected type: 'foo'\n```\n\n> [docs on `incompatible_types`](https://fb.me/eqwalizer_errors#incompatible_types)"} +{"path":"app_a/src/app_a_lists.erl","line":576,"char":5,"code":"ELP","severity":"error","name":"eqWAlizer: incompatible_types","original":"flatmap(thing_to_list/1, List)","replacement":null,"description":"```lang=error,counterexample\n`flatmap(thing_to_list/1, List)`.\n\nExpression has type: [term()]\nContext expected type: string()\n```\n\n> [docs on `incompatible_types`](https://fb.me/eqwalizer_errors#incompatible_types)"} +{"path":"app_a/src/app_a_lists.erl","line":588,"char":29,"code":"ELP","severity":"error","name":"eqWAlizer: recursive_constraint","original":null,"replacement":null,"description":"```lang=error,counterexample\n\nRecursive constraint: DeepList\n```\n\n> [docs on `recursive_constraint`](https://fb.me/eqwalizer_errors#recursive_constraint)"} +{"path":"app_a/src/app_a_lists.erl","line":595,"char":29,"code":"ELP","severity":"error","name":"eqWAlizer: recursive_constraint","original":null,"replacement":null,"description":"```lang=error,counterexample\n\nRecursive constraint: DeepList\n```\n\n> [docs on `recursive_constraint`](https://fb.me/eqwalizer_errors#recursive_constraint)"} +{"path":"app_a/src/app_a_lists.erl","line":613,"char":29,"code":"ELP","severity":"error","name":"eqWAlizer: recursive_constraint","original":null,"replacement":null,"description":"```lang=error,counterexample\n\nRecursive constraint: DeepList\n```\n\n> [docs on `recursive_constraint`](https://fb.me/eqwalizer_errors#recursive_constraint)"} +{"path":"app_a/src/app_a_lists.erl","line":1114,"char":36,"code":"ELP","severity":"error","name":"eqWAlizer: incompatible_types","original":"H3","replacement":null,"description":"```lang=error,counterexample\n`H3`.\n\nExpression has type: term()\nContext expected type: [term()]\n```\n\n> [docs on `incompatible_types`](https://fb.me/eqwalizer_errors#incompatible_types)"} +{"path":"app_a/src/app_a_lists.erl","line":1305,"char":5,"code":"ELP","severity":"error","name":"eqWAlizer: incompatible_types","original":"filtermap(eqwalizer:dynamic_cast(F), L)","replacement":null,"description":"```lang=error,counterexample\n`filtermap(eqwalizer:dynamic_cast(F), L)`.\n\nExpression has type: [term()]\nContext expected type: [T | X]\n```\n```\n [term()] is not compatible with [T | X]\n because\n term() is not compatible with T | X\n```\n> [docs on `incompatible_types`](https://fb.me/eqwalizer_errors#incompatible_types)"} +{"path":"app_a/src/app_a_lists.erl","line":1305,"char":15,"code":"ELP","severity":"error","name":"eqWAlizer: incompatible_types","original":"F","replacement":null,"description":"```lang=error,counterexample\n`F`.\n\nExpression has type: fun((T) -> boolean() | {'true', X})\nContext expected type: fun((term()) -> boolean() | {'true', term()})\n```\n```\n fun((T) -> boolean() | {'true', X}) is not compatible with fun((term()) -> boolean() | {'true', term()})\n because\n term() is not compatible with T\n```\n> [docs on `incompatible_types`](https://fb.me/eqwalizer_errors#incompatible_types)"} +{"path":"app_a/src/app_a_mod2.erl","line":22,"char":1,"code":"ELP","severity":"error","name":"eqWAlizer: type_alias_is_non_productive","original":null,"replacement":null,"description":"```lang=error,counterexample\n\nrecursive type invalid/0 is not productive\n```\n\n> [docs on `type_alias_is_non_productive`](https://fb.me/eqwalizer_errors#type_alias_is_non_productive)"} +{"path":"app_a/src/app_a_mod2.erl","line":31,"char":9,"code":"ELP","severity":"error","name":"eqWAlizer: incompatible_types","original":"'an_atom'","replacement":null,"description":"```lang=error,counterexample\n`'an_atom'`.\n\nExpression has type: 'an_atom'\nContext expected type: number()\n```\n\n> [docs on `incompatible_types`](https://fb.me/eqwalizer_errors#incompatible_types)"} +{"path":"app_a/test/app_a_test_helpers.erl","line":6,"char":11,"code":"ELP","severity":"error","name":"eqWAlizer: incompatible_types","original":"'wrong_ret'","replacement":null,"description":"```lang=error,counterexample\n`'wrong_ret'`.\n\nExpression has type: 'wrong_ret'\nContext expected type: 'error'\n```\n\n> [docs on `incompatible_types`](https://fb.me/eqwalizer_errors#incompatible_types)"} diff --git a/crates/elp/src/resources/test/standard/eqwalize_all_parse_error.jsonl b/crates/elp/src/resources/test/standard/eqwalize_all_parse_error.jsonl new file mode 100644 index 0000000000..9af368563e --- /dev/null +++ b/crates/elp/src/resources/test/standard/eqwalize_all_parse_error.jsonl @@ -0,0 +1 @@ +{"path":"parse_error_a/src/parse_error_a_bad.erl","line":6,"char":null,"code":"ELP","severity":"error","name":"ELP","original":null,"replacement":null,"description":"head mismatch"} diff --git a/crates/elp/src/resources/test/standard/eqwalize_app_a.pretty b/crates/elp/src/resources/test/standard/eqwalize_app_a.pretty new file mode 100644 index 0000000000..20de448e64 --- /dev/null +++ b/crates/elp/src/resources/test/standard/eqwalize_app_a.pretty @@ -0,0 +1,110 @@ +error: incompatible_types + ┌─ app_a/src/app_a.erl:9:5 + │ +9 │ ?OK. + │ ^^^ 'error'. +Expression has type: 'error' +Context expected type: 'ok' + +See https://fb.me/eqwalizer_errors#incompatible_types + +error: incompatible_types + ┌─ app_a/src/app_a.erl:13:5 + │ +13 │ error. + │ ^^^^^ 'error'. +Expression has type: 'error' +Context expected type: 'ok' + +See https://fb.me/eqwalizer_errors#incompatible_types + +error: incompatible_types + ┌─ app_a/src/app_a.erl:17:13 + │ +17 │ _ = 3 * an_atom, ok. + │ ^^^^^^^ 'an_atom'. +Expression has type: 'an_atom' +Context expected type: number() + +See https://fb.me/eqwalizer_errors#incompatible_types + +error: redundant_fixme + ┌─ app_a/src/app_a.erl:55:5 + │ +55 │ % eqwalizer:fixme redundant issue should be reported + │ ^^^^^^^^^^^^^^^^^ redundant fixme + +See https://fb.me/eqwalizer_errors#redundant_fixme + +error: incompatible_types + ┌─ app_a/src/app_a.erl:77:5 + │ +77 │ X. + │ ^ + │ │ + │ X. +Expression has type: #S{k_extra => term(), k_ok => term(), k_req1 => term(), k_req2 => term(), k_wrong1 => pid(), k_wrong2 => pid()} +Context expected type: #S{k_ok => term(), k_req1 := atom(), k_req2 := atom(), k_req3 := atom(), k_wrong1 => atom(), k_wrong2 => atom()} + +See https://fb.me/eqwalizer_errors#incompatible_types + │ + +These associations do not match: + + #S{ ++ k_extra => ... +- k_req1 := ... ++ k_req1 => ... +- k_req2 := ... ++ k_req2 => ... +- k_req3 := ... + ... + } + +error: incompatible_types + ┌─ app_a/src/app_a.erl:101:5 + │ +101 │ X. + │ ^ + │ │ + │ X. +Expression has type: id(#S{a := 'va', b := #S{c := #S{d => atom()}}}) +Context expected type: #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}} + +See https://fb.me/eqwalizer_errors#incompatible_types + │ + + id(#S{a := 'va', b := #S{c := #S{d => atom()}}}) is not compatible with #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}} + because + at shape key 'b': + #S{a := 'va', b := #S{c := #S{d => atom()}}} is not compatible with #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}} + because + at shape key 'c': + #S{c := #S{d => atom()}} is not compatible with #S{c := id(#S{d := atom(), e := atom()})} + because + #S{d => atom()} is not compatible with id(#S{d := atom(), e := atom()}) + +error: incompatible_types + ┌─ app_a/src/app_a.erl:124:5 + │ +124 │ X. + │ ^ + │ │ + │ X. +Expression has type: id(#S{a := 'va', b := #S{c := #S{d := pid(), e := pid()}}}) +Context expected type: #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}} + +See https://fb.me/eqwalizer_errors#incompatible_types + │ + + id(#S{a := 'va', b := #S{c := #S{d := pid(), e := pid()}}}) is not compatible with #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}} + because + at shape key 'b': + #S{a := 'va', b := #S{c := #S{d := pid(), e := pid()}}} is not compatible with #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}} + because + at shape key 'c': + #S{c := #S{d := pid(), e := pid()}} is not compatible with #S{c := id(#S{d := atom(), e := atom()})} + because + #S{d := pid(), e := pid()} is not compatible with id(#S{d := atom(), e := atom()}) + +7 ERRORS diff --git a/crates/elp/src/resources/test/standard/eqwalize_app_a_fast.pretty b/crates/elp/src/resources/test/standard/eqwalize_app_a_fast.pretty new file mode 100644 index 0000000000..9a19413a53 --- /dev/null +++ b/crates/elp/src/resources/test/standard/eqwalize_app_a_fast.pretty @@ -0,0 +1,3 @@ +Module elpt_example not found + +[cause truncated in tests to avoid absolute paths] \ No newline at end of file diff --git a/crates/elp/src/resources/test/standard/eqwalize_app_a_lists_fast.pretty b/crates/elp/src/resources/test/standard/eqwalize_app_a_lists_fast.pretty new file mode 100644 index 0000000000..a1aace2c8b --- /dev/null +++ b/crates/elp/src/resources/test/standard/eqwalize_app_a_lists_fast.pretty @@ -0,0 +1,79 @@ +error: incompatible_types + ┌─ app_a/src/app_a_lists.erl:576:5 + │ +576 │ flatmap(fun thing_to_list/1, List). + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ flatmap(thing_to_list/1, List). +Expression has type: [term()] +Context expected type: string() + +See https://fb.me/eqwalizer_errors#incompatible_types + +error: recursive_constraint + ┌─ app_a/src/app_a_lists.erl:588:29 + │ +588 │ DeepList :: [term() | DeepList], + │ ^^^^^^^^ Recursive constraint: DeepList + +See https://fb.me/eqwalizer_errors#recursive_constraint + +error: recursive_constraint + ┌─ app_a/src/app_a_lists.erl:595:29 + │ +595 │ DeepList :: [term() | DeepList], + │ ^^^^^^^^ Recursive constraint: DeepList + +See https://fb.me/eqwalizer_errors#recursive_constraint + +error: recursive_constraint + ┌─ app_a/src/app_a_lists.erl:613:29 + │ +613 │ DeepList :: [term() | DeepList]. + │ ^^^^^^^^ Recursive constraint: DeepList + +See https://fb.me/eqwalizer_errors#recursive_constraint + +error: incompatible_types + ┌─ app_a/src/app_a_lists.erl:1114:36 + │ +1114 │ lists:reverse(umerge3_1(L1, [H2 | H3], T2, H2, [], T3, H3), []). + │ ^^^^^ H3. +Expression has type: term() +Context expected type: [term()] + +See https://fb.me/eqwalizer_errors#incompatible_types + +error: incompatible_types + ┌─ app_a/src/app_a_lists.erl:1305:5 + │ +1305 │ filtermap(F, L). + │ ^^^^^^^^^^^^^^^ + │ │ + │ filtermap(eqwalizer:dynamic_cast(F), L). +Expression has type: [term()] +Context expected type: [T | X] + +See https://fb.me/eqwalizer_errors#incompatible_types + │ + + [term()] is not compatible with [T | X] + because + term() is not compatible with T | X + +error: incompatible_types + ┌─ app_a/src/app_a_lists.erl:1305:15 + │ +1305 │ filtermap(F, L). + │ ^ + │ │ + │ F. +Expression has type: fun((T) -> boolean() | {'true', X}) +Context expected type: fun((term()) -> boolean() | {'true', term()}) + +See https://fb.me/eqwalizer_errors#incompatible_types + │ + + fun((T) -> boolean() | {'true', X}) is not compatible with fun((term()) -> boolean() | {'true', term()}) + because + term() is not compatible with T + +7 ERRORS diff --git a/crates/elp/src/resources/test/standard/eqwalize_app_a_mod2.pretty b/crates/elp/src/resources/test/standard/eqwalize_app_a_mod2.pretty new file mode 100644 index 0000000000..9a19413a53 --- /dev/null +++ b/crates/elp/src/resources/test/standard/eqwalize_app_a_mod2.pretty @@ -0,0 +1,3 @@ +Module elpt_example not found + +[cause truncated in tests to avoid absolute paths] \ No newline at end of file diff --git a/crates/elp/src/resources/test/standard/eqwalize_app_a_mod2_fast.pretty b/crates/elp/src/resources/test/standard/eqwalize_app_a_mod2_fast.pretty new file mode 100644 index 0000000000..e636282d66 --- /dev/null +++ b/crates/elp/src/resources/test/standard/eqwalize_app_a_mod2_fast.pretty @@ -0,0 +1,19 @@ +error: type_alias_is_non_productive + ┌─ app_a/src/app_a_mod2.erl:22:1 + │ +22 │ -type invalid() :: invalid(). + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^ recursive type invalid/0 is not productive + +See https://fb.me/eqwalizer_errors#type_alias_is_non_productive + +error: incompatible_types + ┌─ app_a/src/app_a_mod2.erl:31:9 + │ +31 │ 1 + an_atom, + │ ^^^^^^^ 'an_atom'. +Expression has type: 'an_atom' +Context expected type: number() + +See https://fb.me/eqwalizer_errors#incompatible_types + +2 ERRORS diff --git a/crates/elp/src/resources/test/standard/eqwalize_app_b.pretty b/crates/elp/src/resources/test/standard/eqwalize_app_b.pretty new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/elp/src/resources/test/standard/eqwalize_app_b_fast.pretty b/crates/elp/src/resources/test/standard/eqwalize_app_b_fast.pretty new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/elp/src/resources/test/standard/eqwalize_app_diagnostics.pretty b/crates/elp/src/resources/test/standard/eqwalize_app_diagnostics.pretty new file mode 100644 index 0000000000..12af791432 --- /dev/null +++ b/crates/elp/src/resources/test/standard/eqwalize_app_diagnostics.pretty @@ -0,0 +1,226 @@ +error: incompatible_types + ┌─ app_a/src/app_a.erl:9:5 + │ +9 │ ?OK. + │ ^^^ 'error'. +Expression has type: 'error' +Context expected type: 'ok' + +See https://fb.me/eqwalizer_errors#incompatible_types + +error: incompatible_types + ┌─ app_a/src/app_a.erl:13:5 + │ +13 │ error. + │ ^^^^^ 'error'. +Expression has type: 'error' +Context expected type: 'ok' + +See https://fb.me/eqwalizer_errors#incompatible_types + +error: incompatible_types + ┌─ app_a/src/app_a.erl:17:13 + │ +17 │ _ = 3 * an_atom, ok. + │ ^^^^^^^ 'an_atom'. +Expression has type: 'an_atom' +Context expected type: number() + +See https://fb.me/eqwalizer_errors#incompatible_types + +error: redundant_fixme + ┌─ app_a/src/app_a.erl:55:5 + │ +55 │ % eqwalizer:fixme redundant issue should be reported + │ ^^^^^^^^^^^^^^^^^ redundant fixme + +See https://fb.me/eqwalizer_errors#redundant_fixme + +error: incompatible_types + ┌─ app_a/src/app_a.erl:77:5 + │ +77 │ X. + │ ^ + │ │ + │ X. +Expression has type: #S{k_extra => term(), k_ok => term(), k_req1 => term(), k_req2 => term(), k_wrong1 => pid(), k_wrong2 => pid()} +Context expected type: #S{k_ok => term(), k_req1 := atom(), k_req2 := atom(), k_req3 := atom(), k_wrong1 => atom(), k_wrong2 => atom()} + +See https://fb.me/eqwalizer_errors#incompatible_types + │ + +These associations do not match: + + #S{ ++ k_extra => ... +- k_req1 := ... ++ k_req1 => ... +- k_req2 := ... ++ k_req2 => ... +- k_req3 := ... + ... + } + +error: incompatible_types + ┌─ app_a/src/app_a.erl:101:5 + │ +101 │ X. + │ ^ + │ │ + │ X. +Expression has type: id(#S{a := 'va', b := #S{c := #S{d => atom()}}}) +Context expected type: #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}} + +See https://fb.me/eqwalizer_errors#incompatible_types + │ + + id(#S{a := 'va', b := #S{c := #S{d => atom()}}}) is not compatible with #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}} + because + at shape key 'b': + #S{a := 'va', b := #S{c := #S{d => atom()}}} is not compatible with #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}} + because + at shape key 'c': + #S{c := #S{d => atom()}} is not compatible with #S{c := id(#S{d := atom(), e := atom()})} + because + #S{d => atom()} is not compatible with id(#S{d := atom(), e := atom()}) + +error: incompatible_types + ┌─ app_a/src/app_a.erl:124:5 + │ +124 │ X. + │ ^ + │ │ + │ X. +Expression has type: id(#S{a := 'va', b := #S{c := #S{d := pid(), e := pid()}}}) +Context expected type: #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}} + +See https://fb.me/eqwalizer_errors#incompatible_types + │ + + id(#S{a := 'va', b := #S{c := #S{d := pid(), e := pid()}}}) is not compatible with #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}} + because + at shape key 'b': + #S{a := 'va', b := #S{c := #S{d := pid(), e := pid()}}} is not compatible with #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}} + because + at shape key 'c': + #S{c := #S{d := pid(), e := pid()}} is not compatible with #S{c := id(#S{d := atom(), e := atom()})} + because + #S{d := pid(), e := pid()} is not compatible with id(#S{d := atom(), e := atom()}) + +error: incompatible_types + ┌─ app_a/test/app_a_SUITE.erl:18:5 + │ +18 │ app_a_test_helpers:fail(). + │ ^^^^^^^^^^^^^^^^^^^^^^^^^ app_a_test_helpers:fail(). +Expression has type: 'error' +Context expected type: 'ok' + +See https://fb.me/eqwalizer_errors#incompatible_types + +error: incompatible_types + ┌─ app_a/src/app_a_lists.erl:576:5 + │ +576 │ flatmap(fun thing_to_list/1, List). + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ flatmap(thing_to_list/1, List). +Expression has type: [term()] +Context expected type: string() + +See https://fb.me/eqwalizer_errors#incompatible_types + +error: recursive_constraint + ┌─ app_a/src/app_a_lists.erl:588:29 + │ +588 │ DeepList :: [term() | DeepList], + │ ^^^^^^^^ Recursive constraint: DeepList + +See https://fb.me/eqwalizer_errors#recursive_constraint + +error: recursive_constraint + ┌─ app_a/src/app_a_lists.erl:595:29 + │ +595 │ DeepList :: [term() | DeepList], + │ ^^^^^^^^ Recursive constraint: DeepList + +See https://fb.me/eqwalizer_errors#recursive_constraint + +error: recursive_constraint + ┌─ app_a/src/app_a_lists.erl:613:29 + │ +613 │ DeepList :: [term() | DeepList]. + │ ^^^^^^^^ Recursive constraint: DeepList + +See https://fb.me/eqwalizer_errors#recursive_constraint + +error: incompatible_types + ┌─ app_a/src/app_a_lists.erl:1114:36 + │ +1114 │ lists:reverse(umerge3_1(L1, [H2 | H3], T2, H2, [], T3, H3), []). + │ ^^^^^ H3. +Expression has type: term() +Context expected type: [term()] + +See https://fb.me/eqwalizer_errors#incompatible_types + +error: incompatible_types + ┌─ app_a/src/app_a_lists.erl:1305:5 + │ +1305 │ filtermap(F, L). + │ ^^^^^^^^^^^^^^^ + │ │ + │ filtermap(eqwalizer:dynamic_cast(F), L). +Expression has type: [term()] +Context expected type: [T | X] + +See https://fb.me/eqwalizer_errors#incompatible_types + │ + + [term()] is not compatible with [T | X] + because + term() is not compatible with T | X + +error: incompatible_types + ┌─ app_a/src/app_a_lists.erl:1305:15 + │ +1305 │ filtermap(F, L). + │ ^ + │ │ + │ F. +Expression has type: fun((T) -> boolean() | {'true', X}) +Context expected type: fun((term()) -> boolean() | {'true', term()}) + +See https://fb.me/eqwalizer_errors#incompatible_types + │ + + fun((T) -> boolean() | {'true', X}) is not compatible with fun((term()) -> boolean() | {'true', term()}) + because + term() is not compatible with T + +error: type_alias_is_non_productive + ┌─ app_a/src/app_a_mod2.erl:22:1 + │ +22 │ -type invalid() :: invalid(). + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^ recursive type invalid/0 is not productive + +See https://fb.me/eqwalizer_errors#type_alias_is_non_productive + +error: incompatible_types + ┌─ app_a/src/app_a_mod2.erl:31:9 + │ +31 │ 1 + an_atom, + │ ^^^^^^^ 'an_atom'. +Expression has type: 'an_atom' +Context expected type: number() + +See https://fb.me/eqwalizer_errors#incompatible_types + +error: incompatible_types + ┌─ app_a/test/app_a_test_helpers.erl:6:11 + │ +6 │ fail() -> wrong_ret. + │ ^^^^^^^^^ 'wrong_ret'. +Expression has type: 'wrong_ret' +Context expected type: 'error' + +See https://fb.me/eqwalizer_errors#incompatible_types + +18 ERRORS diff --git a/crates/elp/src/resources/test/standard/eqwalize_app_diagnostics_gen.pretty b/crates/elp/src/resources/test/standard/eqwalize_app_diagnostics_gen.pretty new file mode 100644 index 0000000000..7957fd5765 --- /dev/null +++ b/crates/elp/src/resources/test/standard/eqwalize_app_diagnostics_gen.pretty @@ -0,0 +1,236 @@ +error: incompatible_types + ┌─ app_a/src/app_a.erl:9:5 + │ +9 │ ?OK. + │ ^^^ 'error'. +Expression has type: 'error' +Context expected type: 'ok' + +See https://fb.me/eqwalizer_errors#incompatible_types + +error: incompatible_types + ┌─ app_a/src/app_a.erl:13:5 + │ +13 │ error. + │ ^^^^^ 'error'. +Expression has type: 'error' +Context expected type: 'ok' + +See https://fb.me/eqwalizer_errors#incompatible_types + +error: incompatible_types + ┌─ app_a/src/app_a.erl:17:13 + │ +17 │ _ = 3 * an_atom, ok. + │ ^^^^^^^ 'an_atom'. +Expression has type: 'an_atom' +Context expected type: number() + +See https://fb.me/eqwalizer_errors#incompatible_types + +error: redundant_fixme + ┌─ app_a/src/app_a.erl:55:5 + │ +55 │ % eqwalizer:fixme redundant issue should be reported + │ ^^^^^^^^^^^^^^^^^ redundant fixme + +See https://fb.me/eqwalizer_errors#redundant_fixme + +error: incompatible_types + ┌─ app_a/src/app_a.erl:77:5 + │ +77 │ X. + │ ^ + │ │ + │ X. +Expression has type: #S{k_extra => term(), k_ok => term(), k_req1 => term(), k_req2 => term(), k_wrong1 => pid(), k_wrong2 => pid()} +Context expected type: #S{k_ok => term(), k_req1 := atom(), k_req2 := atom(), k_req3 := atom(), k_wrong1 => atom(), k_wrong2 => atom()} + +See https://fb.me/eqwalizer_errors#incompatible_types + │ + +These associations do not match: + + #S{ ++ k_extra => ... +- k_req1 := ... ++ k_req1 => ... +- k_req2 := ... ++ k_req2 => ... +- k_req3 := ... + ... + } + +error: incompatible_types + ┌─ app_a/src/app_a.erl:101:5 + │ +101 │ X. + │ ^ + │ │ + │ X. +Expression has type: id(#S{a := 'va', b := #S{c := #S{d => atom()}}}) +Context expected type: #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}} + +See https://fb.me/eqwalizer_errors#incompatible_types + │ + + id(#S{a := 'va', b := #S{c := #S{d => atom()}}}) is not compatible with #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}} + because + at shape key 'b': + #S{a := 'va', b := #S{c := #S{d => atom()}}} is not compatible with #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}} + because + at shape key 'c': + #S{c := #S{d => atom()}} is not compatible with #S{c := id(#S{d := atom(), e := atom()})} + because + #S{d => atom()} is not compatible with id(#S{d := atom(), e := atom()}) + +error: incompatible_types + ┌─ app_a/src/app_a.erl:124:5 + │ +124 │ X. + │ ^ + │ │ + │ X. +Expression has type: id(#S{a := 'va', b := #S{c := #S{d := pid(), e := pid()}}}) +Context expected type: #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}} + +See https://fb.me/eqwalizer_errors#incompatible_types + │ + + id(#S{a := 'va', b := #S{c := #S{d := pid(), e := pid()}}}) is not compatible with #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}} + because + at shape key 'b': + #S{a := 'va', b := #S{c := #S{d := pid(), e := pid()}}} is not compatible with #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}} + because + at shape key 'c': + #S{c := #S{d := pid(), e := pid()}} is not compatible with #S{c := id(#S{d := atom(), e := atom()})} + because + #S{d := pid(), e := pid()} is not compatible with id(#S{d := atom(), e := atom()}) + +error: incompatible_types + ┌─ app_a/test/app_a_SUITE.erl:18:5 + │ +18 │ app_a_test_helpers:fail(). + │ ^^^^^^^^^^^^^^^^^^^^^^^^^ app_a_test_helpers:fail(). +Expression has type: 'error' +Context expected type: 'ok' + +See https://fb.me/eqwalizer_errors#incompatible_types + +error: incompatible_types + ┌─ app_a/src/app_a_errors_generated.erl:8:10 + │ +8 │ foo() -> wrong_ret. + │ ^^^^^^^^^ 'wrong_ret'. +Expression has type: 'wrong_ret' +Context expected type: 'foo' + +See https://fb.me/eqwalizer_errors#incompatible_types + +error: incompatible_types + ┌─ app_a/src/app_a_lists.erl:576:5 + │ +576 │ flatmap(fun thing_to_list/1, List). + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ flatmap(thing_to_list/1, List). +Expression has type: [term()] +Context expected type: string() + +See https://fb.me/eqwalizer_errors#incompatible_types + +error: recursive_constraint + ┌─ app_a/src/app_a_lists.erl:588:29 + │ +588 │ DeepList :: [term() | DeepList], + │ ^^^^^^^^ Recursive constraint: DeepList + +See https://fb.me/eqwalizer_errors#recursive_constraint + +error: recursive_constraint + ┌─ app_a/src/app_a_lists.erl:595:29 + │ +595 │ DeepList :: [term() | DeepList], + │ ^^^^^^^^ Recursive constraint: DeepList + +See https://fb.me/eqwalizer_errors#recursive_constraint + +error: recursive_constraint + ┌─ app_a/src/app_a_lists.erl:613:29 + │ +613 │ DeepList :: [term() | DeepList]. + │ ^^^^^^^^ Recursive constraint: DeepList + +See https://fb.me/eqwalizer_errors#recursive_constraint + +error: incompatible_types + ┌─ app_a/src/app_a_lists.erl:1114:36 + │ +1114 │ lists:reverse(umerge3_1(L1, [H2 | H3], T2, H2, [], T3, H3), []). + │ ^^^^^ H3. +Expression has type: term() +Context expected type: [term()] + +See https://fb.me/eqwalizer_errors#incompatible_types + +error: incompatible_types + ┌─ app_a/src/app_a_lists.erl:1305:5 + │ +1305 │ filtermap(F, L). + │ ^^^^^^^^^^^^^^^ + │ │ + │ filtermap(eqwalizer:dynamic_cast(F), L). +Expression has type: [term()] +Context expected type: [T | X] + +See https://fb.me/eqwalizer_errors#incompatible_types + │ + + [term()] is not compatible with [T | X] + because + term() is not compatible with T | X + +error: incompatible_types + ┌─ app_a/src/app_a_lists.erl:1305:15 + │ +1305 │ filtermap(F, L). + │ ^ + │ │ + │ F. +Expression has type: fun((T) -> boolean() | {'true', X}) +Context expected type: fun((term()) -> boolean() | {'true', term()}) + +See https://fb.me/eqwalizer_errors#incompatible_types + │ + + fun((T) -> boolean() | {'true', X}) is not compatible with fun((term()) -> boolean() | {'true', term()}) + because + term() is not compatible with T + +error: type_alias_is_non_productive + ┌─ app_a/src/app_a_mod2.erl:22:1 + │ +22 │ -type invalid() :: invalid(). + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^ recursive type invalid/0 is not productive + +See https://fb.me/eqwalizer_errors#type_alias_is_non_productive + +error: incompatible_types + ┌─ app_a/src/app_a_mod2.erl:31:9 + │ +31 │ 1 + an_atom, + │ ^^^^^^^ 'an_atom'. +Expression has type: 'an_atom' +Context expected type: number() + +See https://fb.me/eqwalizer_errors#incompatible_types + +error: incompatible_types + ┌─ app_a/test/app_a_test_helpers.erl:6:11 + │ +6 │ fail() -> wrong_ret. + │ ^^^^^^^^^ 'wrong_ret'. +Expression has type: 'wrong_ret' +Context expected type: 'error' + +See https://fb.me/eqwalizer_errors#incompatible_types + +19 ERRORS diff --git a/crates/elp/src/resources/test/standard/eqwalize_meinong.pretty b/crates/elp/src/resources/test/standard/eqwalize_meinong.pretty new file mode 100644 index 0000000000..9ed5b146c2 --- /dev/null +++ b/crates/elp/src/resources/test/standard/eqwalize_meinong.pretty @@ -0,0 +1 @@ +Module meinong not found diff --git a/crates/elp/src/resources/test/standard/eqwalize_target_diagnostics.pretty b/crates/elp/src/resources/test/standard/eqwalize_target_diagnostics.pretty new file mode 100644 index 0000000000..95016fc2d9 --- /dev/null +++ b/crates/elp/src/resources/test/standard/eqwalize_target_diagnostics.pretty @@ -0,0 +1,206 @@ +error: incompatible_types + ┌─ app_a/src/app_a.erl:9:5 + │ +9 │ ?OK. + │ ^^^ 'error'. +Expression has type: 'error' +Context expected type: 'ok' + +See https://fb.me/eqwalizer_errors#incompatible_types + +error: incompatible_types + ┌─ app_a/src/app_a.erl:13:5 + │ +13 │ error. + │ ^^^^^ 'error'. +Expression has type: 'error' +Context expected type: 'ok' + +See https://fb.me/eqwalizer_errors#incompatible_types + +error: incompatible_types + ┌─ app_a/src/app_a.erl:17:13 + │ +17 │ _ = 3 * an_atom, ok. + │ ^^^^^^^ 'an_atom'. +Expression has type: 'an_atom' +Context expected type: number() + +See https://fb.me/eqwalizer_errors#incompatible_types + +error: redundant_fixme + ┌─ app_a/src/app_a.erl:55:5 + │ +55 │ % eqwalizer:fixme redundant issue should be reported + │ ^^^^^^^^^^^^^^^^^ redundant fixme + +See https://fb.me/eqwalizer_errors#redundant_fixme + +error: incompatible_types + ┌─ app_a/src/app_a.erl:77:5 + │ +77 │ X. + │ ^ + │ │ + │ X. +Expression has type: #S{k_extra => term(), k_ok => term(), k_req1 => term(), k_req2 => term(), k_wrong1 => pid(), k_wrong2 => pid()} +Context expected type: #S{k_ok => term(), k_req1 := atom(), k_req2 := atom(), k_req3 := atom(), k_wrong1 => atom(), k_wrong2 => atom()} + +See https://fb.me/eqwalizer_errors#incompatible_types + │ + +These associations do not match: + + #S{ ++ k_extra => ... +- k_req1 := ... ++ k_req1 => ... +- k_req2 := ... ++ k_req2 => ... +- k_req3 := ... + ... + } + +error: incompatible_types + ┌─ app_a/src/app_a.erl:101:5 + │ +101 │ X. + │ ^ + │ │ + │ X. +Expression has type: id(#S{a := 'va', b := #S{c := #S{d => atom()}}}) +Context expected type: #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}} + +See https://fb.me/eqwalizer_errors#incompatible_types + │ + + id(#S{a := 'va', b := #S{c := #S{d => atom()}}}) is not compatible with #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}} + because + at shape key 'b': + #S{a := 'va', b := #S{c := #S{d => atom()}}} is not compatible with #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}} + because + at shape key 'c': + #S{c := #S{d => atom()}} is not compatible with #S{c := id(#S{d := atom(), e := atom()})} + because + #S{d => atom()} is not compatible with id(#S{d := atom(), e := atom()}) + +error: incompatible_types + ┌─ app_a/src/app_a.erl:124:5 + │ +124 │ X. + │ ^ + │ │ + │ X. +Expression has type: id(#S{a := 'va', b := #S{c := #S{d := pid(), e := pid()}}}) +Context expected type: #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}} + +See https://fb.me/eqwalizer_errors#incompatible_types + │ + + id(#S{a := 'va', b := #S{c := #S{d := pid(), e := pid()}}}) is not compatible with #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}} + because + at shape key 'b': + #S{a := 'va', b := #S{c := #S{d := pid(), e := pid()}}} is not compatible with #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}} + because + at shape key 'c': + #S{c := #S{d := pid(), e := pid()}} is not compatible with #S{c := id(#S{d := atom(), e := atom()})} + because + #S{d := pid(), e := pid()} is not compatible with id(#S{d := atom(), e := atom()}) + +error: incompatible_types + ┌─ app_a/src/app_a_lists.erl:576:5 + │ +576 │ flatmap(fun thing_to_list/1, List). + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ flatmap(thing_to_list/1, List). +Expression has type: [term()] +Context expected type: string() + +See https://fb.me/eqwalizer_errors#incompatible_types + +error: recursive_constraint + ┌─ app_a/src/app_a_lists.erl:588:29 + │ +588 │ DeepList :: [term() | DeepList], + │ ^^^^^^^^ Recursive constraint: DeepList + +See https://fb.me/eqwalizer_errors#recursive_constraint + +error: recursive_constraint + ┌─ app_a/src/app_a_lists.erl:595:29 + │ +595 │ DeepList :: [term() | DeepList], + │ ^^^^^^^^ Recursive constraint: DeepList + +See https://fb.me/eqwalizer_errors#recursive_constraint + +error: recursive_constraint + ┌─ app_a/src/app_a_lists.erl:613:29 + │ +613 │ DeepList :: [term() | DeepList]. + │ ^^^^^^^^ Recursive constraint: DeepList + +See https://fb.me/eqwalizer_errors#recursive_constraint + +error: incompatible_types + ┌─ app_a/src/app_a_lists.erl:1114:36 + │ +1114 │ lists:reverse(umerge3_1(L1, [H2 | H3], T2, H2, [], T3, H3), []). + │ ^^^^^ H3. +Expression has type: term() +Context expected type: [term()] + +See https://fb.me/eqwalizer_errors#incompatible_types + +error: incompatible_types + ┌─ app_a/src/app_a_lists.erl:1305:5 + │ +1305 │ filtermap(F, L). + │ ^^^^^^^^^^^^^^^ + │ │ + │ filtermap(eqwalizer:dynamic_cast(F), L). +Expression has type: [term()] +Context expected type: [T | X] + +See https://fb.me/eqwalizer_errors#incompatible_types + │ + + [term()] is not compatible with [T | X] + because + term() is not compatible with T | X + +error: incompatible_types + ┌─ app_a/src/app_a_lists.erl:1305:15 + │ +1305 │ filtermap(F, L). + │ ^ + │ │ + │ F. +Expression has type: fun((T) -> boolean() | {'true', X}) +Context expected type: fun((term()) -> boolean() | {'true', term()}) + +See https://fb.me/eqwalizer_errors#incompatible_types + │ + + fun((T) -> boolean() | {'true', X}) is not compatible with fun((term()) -> boolean() | {'true', term()}) + because + term() is not compatible with T + +error: type_alias_is_non_productive + ┌─ app_a/src/app_a_mod2.erl:22:1 + │ +22 │ -type invalid() :: invalid(). + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^ recursive type invalid/0 is not productive + +See https://fb.me/eqwalizer_errors#type_alias_is_non_productive + +error: incompatible_types + ┌─ app_a/src/app_a_mod2.erl:31:9 + │ +31 │ 1 + an_atom, + │ ^^^^^^^ 'an_atom'. +Expression has type: 'an_atom' +Context expected type: number() + +See https://fb.me/eqwalizer_errors#incompatible_types + +16 ERRORS diff --git a/crates/elp/src/semantic_tokens.rs b/crates/elp/src/semantic_tokens.rs new file mode 100644 index 0000000000..40e863b815 --- /dev/null +++ b/crates/elp/src/semantic_tokens.rs @@ -0,0 +1,345 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! Semantic Tokens helpers + +use std::ops; + +use lsp_types::Range; +use lsp_types::SemanticToken; +use lsp_types::SemanticTokenModifier; +use lsp_types::SemanticTokenType; +use lsp_types::SemanticTokens; +use lsp_types::SemanticTokensEdit; + +macro_rules! define_semantic_token_types { + ( + standard { + $($standard:ident),*$(,)? + } + custom { + $(($custom:ident, $string:literal)),*$(,)? + } + + ) => { + $(pub(crate) const $standard: SemanticTokenType = SemanticTokenType::$standard;)* + $(pub(crate) const $custom: SemanticTokenType = SemanticTokenType::new($string);)* + + pub(crate) const SUPPORTED_TYPES: &[SemanticTokenType] = &[ + $(SemanticTokenType::$standard,)* + $($custom),* + ]; + }; +} + +define_semantic_token_types![ + standard { + // COMMENT, + // DECORATOR, + // ENUM_MEMBER, + // ENUM, + FUNCTION, + // INTERFACE, + // KEYWORD, + MACRO, + // METHOD, + NAMESPACE, + // NUMBER, + // OPERATOR, + // PARAMETER, + // PROPERTY, + STRING, + STRUCT, + TYPE_PARAMETER, + VARIABLE, + } + + custom { + (GENERIC, "generic"), + } +]; + +macro_rules! define_semantic_token_modifiers { + ( + standard { + $($standard:ident),*$(,)? + } + custom { + $(($custom:ident, $string:literal)),*$(,)? + } + + ) => { + + $(pub(crate) const $standard: SemanticTokenModifier = SemanticTokenModifier::$standard;)* + $(pub(crate) const $custom: SemanticTokenModifier = SemanticTokenModifier::new($string);)* + + pub(crate) const SUPPORTED_MODIFIERS: &[SemanticTokenModifier] = &[ + $(SemanticTokenModifier::$standard,)* + $($custom),* + ]; + }; +} + +define_semantic_token_modifiers![ + standard { + } + custom { + (BOUND, "bound"), + (EXPORTED_FUNCTION, "exported_function"), + (DEPRECATED_FUNCTION, "deprecated_function"), + } +]; + +#[derive(Default)] +pub(crate) struct ModifierSet(pub(crate) u32); + +impl ops::BitOrAssign for ModifierSet { + fn bitor_assign(&mut self, rhs: SemanticTokenModifier) { + let idx = SUPPORTED_MODIFIERS + .iter() + .position(|it| it == &rhs) + .unwrap(); + self.0 |= 1 << idx; + } +} + +/// Tokens are encoded relative to each other. +/// +/// This is a direct port of +pub(crate) struct SemanticTokensBuilder { + id: String, + prev_line: u32, + prev_char: u32, + data: Vec, +} + +impl SemanticTokensBuilder { + pub(crate) fn new(id: String) -> Self { + SemanticTokensBuilder { + id, + prev_line: 0, + prev_char: 0, + data: Default::default(), + } + } + + /// Push a new token onto the builder + pub(crate) fn push(&mut self, range: Range, token_index: u32, modifier_bitset: u32) { + let mut push_line = range.start.line; + let mut push_char = range.start.character; + + if !self.data.is_empty() { + push_line -= self.prev_line; + if push_line == 0 { + push_char -= self.prev_char; + } + } + + // A token cannot be multiline + let token_len = range.end.character - range.start.character; + + let token = SemanticToken { + delta_line: push_line, + delta_start: push_char, + length: token_len, + token_type: token_index, + token_modifiers_bitset: modifier_bitset, + }; + + self.data.push(token); + + self.prev_line = range.start.line; + self.prev_char = range.start.character; + } + + pub(crate) fn build(self) -> SemanticTokens { + SemanticTokens { + result_id: Some(self.id), + data: self.data, + } + } +} + +pub(crate) fn diff_tokens(old: &[SemanticToken], new: &[SemanticToken]) -> Vec { + let offset = new + .iter() + .zip(old.iter()) + .take_while(|&(n, p)| n == p) + .count(); + + let (_, old) = old.split_at(offset); + let (_, new) = new.split_at(offset); + + let offset_from_end = new + .iter() + .rev() + .zip(old.iter().rev()) + .take_while(|&(n, p)| n == p) + .count(); + + let (old, _) = old.split_at(old.len() - offset_from_end); + let (new, _) = new.split_at(new.len() - offset_from_end); + + if old.is_empty() && new.is_empty() { + vec![] + } else { + // The lsp data field is actually a byte-diff but we + // travel in tokens so `start` and `delete_count` are in multiples of the + // serialized size of `SemanticToken`. + vec![SemanticTokensEdit { + start: 5 * offset as u32, + delete_count: 5 * old.len() as u32, + data: Some(new.into()), + }] + } +} + +pub(crate) fn type_index(ty: SemanticTokenType) -> u32 { + SUPPORTED_TYPES.iter().position(|it| *it == ty).unwrap() as u32 +} + +#[cfg(test)] +mod tests { + use super::*; + + fn from(t: (u32, u32, u32, u32, u32)) -> SemanticToken { + SemanticToken { + delta_line: t.0, + delta_start: t.1, + length: t.2, + token_type: t.3, + token_modifiers_bitset: t.4, + } + } + + #[test] + fn test_diff_insert_at_end() { + let before = [from((1, 2, 3, 4, 5)), from((6, 7, 8, 9, 10))]; + let after = [ + from((1, 2, 3, 4, 5)), + from((6, 7, 8, 9, 10)), + from((11, 12, 13, 14, 15)), + ]; + + let edits = diff_tokens(&before, &after); + assert_eq!( + edits[0], + SemanticTokensEdit { + start: 10, + delete_count: 0, + data: Some(vec![from((11, 12, 13, 14, 15))]) + } + ); + } + + #[test] + fn test_diff_insert_at_beginning() { + let before = [from((1, 2, 3, 4, 5)), from((6, 7, 8, 9, 10))]; + let after = [ + from((11, 12, 13, 14, 15)), + from((1, 2, 3, 4, 5)), + from((6, 7, 8, 9, 10)), + ]; + + let edits = diff_tokens(&before, &after); + assert_eq!( + edits[0], + SemanticTokensEdit { + start: 0, + delete_count: 0, + data: Some(vec![from((11, 12, 13, 14, 15))]) + } + ); + } + + #[test] + fn test_diff_insert_in_middle() { + let before = [from((1, 2, 3, 4, 5)), from((6, 7, 8, 9, 10))]; + let after = [ + from((1, 2, 3, 4, 5)), + from((10, 20, 30, 40, 50)), + from((60, 70, 80, 90, 100)), + from((6, 7, 8, 9, 10)), + ]; + + let edits = diff_tokens(&before, &after); + assert_eq!( + edits[0], + SemanticTokensEdit { + start: 5, + delete_count: 0, + data: Some(vec![ + from((10, 20, 30, 40, 50)), + from((60, 70, 80, 90, 100)) + ]) + } + ); + } + + #[test] + fn test_diff_remove_from_end() { + let before = [ + from((1, 2, 3, 4, 5)), + from((6, 7, 8, 9, 10)), + from((11, 12, 13, 14, 15)), + ]; + let after = [from((1, 2, 3, 4, 5)), from((6, 7, 8, 9, 10))]; + + let edits = diff_tokens(&before, &after); + assert_eq!( + edits[0], + SemanticTokensEdit { + start: 10, + delete_count: 5, + data: Some(vec![]) + } + ); + } + + #[test] + fn test_diff_remove_from_beginning() { + let before = [ + from((11, 12, 13, 14, 15)), + from((1, 2, 3, 4, 5)), + from((6, 7, 8, 9, 10)), + ]; + let after = [from((1, 2, 3, 4, 5)), from((6, 7, 8, 9, 10))]; + + let edits = diff_tokens(&before, &after); + assert_eq!( + edits[0], + SemanticTokensEdit { + start: 0, + delete_count: 5, + data: Some(vec![]) + } + ); + } + + #[test] + fn test_diff_remove_from_middle() { + let before = [ + from((1, 2, 3, 4, 5)), + from((10, 20, 30, 40, 50)), + from((60, 70, 80, 90, 100)), + from((6, 7, 8, 9, 10)), + ]; + let after = [from((1, 2, 3, 4, 5)), from((6, 7, 8, 9, 10))]; + + let edits = diff_tokens(&before, &after); + assert_eq!( + edits[0], + SemanticTokensEdit { + start: 5, + delete_count: 10, + data: Some(vec![]) + } + ); + } +} diff --git a/crates/elp/src/server.rs b/crates/elp/src/server.rs new file mode 100644 index 0000000000..b6e97ad39c --- /dev/null +++ b/crates/elp/src/server.rs @@ -0,0 +1,1165 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::fmt; +use std::mem; +use std::sync::Arc; + +use always_assert::always; +use anyhow::bail; +use anyhow::Result; +use crossbeam_channel::select; +use crossbeam_channel::Receiver; +use dispatch::NotificationDispatcher; +use elp_ai::AiCompletion; +use elp_ide::elp_ide_db::elp_base_db::loader; +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::ChangeKind; +use elp_ide::elp_ide_db::elp_base_db::ChangedFile; +use elp_ide::elp_ide_db::elp_base_db::FileId; +use elp_ide::elp_ide_db::elp_base_db::FileSetConfig; +use elp_ide::elp_ide_db::elp_base_db::IncludeOtp; +use elp_ide::elp_ide_db::elp_base_db::ProjectApps; +use elp_ide::elp_ide_db::elp_base_db::ProjectId; +use elp_ide::elp_ide_db::elp_base_db::SourceDatabase; +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::Vfs; +use elp_ide::elp_ide_db::elp_base_db::VfsPath; +use elp_ide::AnalysisHost; +use elp_log::telemetry; +use elp_log::telemetry::TelemetryMessage; +use elp_log::timeit; +use elp_log::Logger; +use elp_log::TimeIt; +use elp_project_model::Project; +use lsp_server::Connection; +use lsp_server::ErrorCode; +use lsp_server::Notification; +use lsp_server::Request; +use lsp_server::RequestId; +use lsp_server::Response; +use lsp_types::notification; +use lsp_types::notification::Notification as _; +use lsp_types::request; +use lsp_types::request::Request as _; +use lsp_types::Diagnostic; +use lsp_types::Url; +use parking_lot::Mutex; +use parking_lot::RwLock; + +use self::dispatch::RequestDispatcher; +use self::progress::ProgressBar; +use self::progress::ProgressManager; +use self::progress::ProgressTask; +use self::progress::Spinner; +use crate::config::Config; +use crate::convert; +use crate::diagnostics::DiagnosticCollection; +use crate::document::Document; +use crate::handlers; +use crate::line_endings::LineEndings; +use crate::lsp_ext; +use crate::project_loader::ProjectLoader; +use crate::reload::ProjectFolders; +use crate::snapshot::SharedMap; +use crate::snapshot::Snapshot; +use crate::task_pool::TaskPool; + +mod capabilities; +mod dispatch; +mod logger; +mod progress; +pub mod setup; + +const LOGGER_NAME: &str = "lsp"; +const PARSE_SERVER_SUPPORTED_EXTENSIONS: &[&str] = &["erl", "hrl"]; +const EDOC_SUPPORTED_EXTENSIONS: &[&str] = &["erl"]; + +enum Event { + Lsp(lsp_server::Message), + Vfs(loader::Message), + Task(Task), + Telemetry(TelemetryMessage), +} + +#[derive(Debug)] +pub enum Task { + Response(lsp_server::Response), + FetchProject(Result), + NativeDiagnostics(Vec<(FileId, Vec)>), + EqwalizerDiagnostics(Spinner, Vec<(FileId, Vec)>), + EdocDiagnostics(Spinner, Vec<(FileId, Vec)>), + ParseServerDiagnostics(Vec<(FileId, Vec)>), + CompileDeps(Spinner), + Progress(ProgressTask), + ScheduleCache, + UpdateCache(Spinner, Vec), +} + +impl fmt::Debug for Event { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Event::Lsp(lsp_server::Message::Notification(notif)) + if notif.method == notification::DidOpenTextDocument::METHOD + || notif.method == notification::DidChangeTextDocument::METHOD + || notif.method == notification::LogMessage::METHOD => + { + f.debug_struct("Notification") + .field("method", ¬if.method) + .finish_non_exhaustive() + } + Event::Lsp(it) => fmt::Debug::fmt(it, f), + Event::Vfs(it) => fmt::Debug::fmt(it, f), + Event::Task(it) => fmt::Debug::fmt(it, f), + Event::Telemetry(it) => fmt::Debug::fmt(it, f), + } + } +} + +type ReqHandler = fn(&mut Server, Response) -> Result<()>; +type ReqQueue = lsp_server::ReqQueue<(String, TimeIt), ReqHandler>; + +#[derive(Debug)] +pub enum Status { + Initialising, + Loading(ProgressBar), + Running, + ShuttingDown, + Invalid, +} + +impl Status { + pub fn as_lsp_status(&self) -> lsp_ext::Status { + match self { + Status::Initialising => lsp_ext::Status::Loading, + Status::Loading(_) => lsp_ext::Status::Loading, + Status::Running => lsp_ext::Status::Running, + Status::ShuttingDown => lsp_ext::Status::ShuttingDown, + Status::Invalid => lsp_ext::Status::Invalid, + } + } +} + +impl PartialEq for Status { + fn eq(&self, other: &Self) -> bool { + mem::discriminant(self) == mem::discriminant(other) + } +} + +pub struct Handle { + pub(crate) handle: H, + pub(crate) receiver: C, +} + +pub type VfsHandle = Handle, Receiver>; +pub type TaskHandle = Handle, Receiver>; + +pub struct Server { + connection: Connection, + vfs_loader: VfsHandle, + task_pool: TaskHandle, + project_pool: TaskHandle, + cache_pool: TaskHandle, + diagnostics: DiagnosticCollection, + req_queue: ReqQueue, + progress: ProgressManager, + open_document_versions: SharedMap, + newly_opened_documents: Vec, + vfs: Arc>, + file_set_config: FileSetConfig, + line_ending_map: SharedMap, + config: Arc, + analysis_host: AnalysisHost, + status: Status, + projects: Arc>, + project_loader: Arc>, + eqwalizer_diagnostics_requested: bool, + edoc_diagnostics_requested: bool, + logger: Logger, + ai_completion: Arc>, + + // Progress reporting + vfs_config_version: u32, +} + +impl Server { + pub fn new( + connection: Connection, + vfs_loader: VfsHandle, + task_pool: TaskHandle, + project_pool: TaskHandle, + cache_pool: TaskHandle, + logger: Logger, + config: Config, + ai_completion: AiCompletion, + ) -> Server { + let mut this = Server { + connection, + progress: ProgressManager::default(), + vfs_loader, + task_pool, + project_pool, + cache_pool, + diagnostics: DiagnosticCollection::default(), + req_queue: ReqQueue::default(), + open_document_versions: SharedMap::default(), + newly_opened_documents: Vec::default(), + vfs: Arc::new(RwLock::new(Vfs::default())), + file_set_config: FileSetConfig::default(), + line_ending_map: SharedMap::default(), + config: Arc::new(config.clone()), + analysis_host: AnalysisHost::default(), + status: Status::Initialising, + projects: Arc::new(vec![]), + project_loader: Arc::new(Mutex::new(ProjectLoader::new())), + eqwalizer_diagnostics_requested: false, + edoc_diagnostics_requested: false, + logger, + ai_completion: Arc::new(Mutex::new(ai_completion)), + vfs_config_version: 0, + }; + + // Run config-based initialisation + this.update_configuration(config); + this + } + + pub fn snapshot(&self) -> Snapshot { + Snapshot::new( + Arc::clone(&self.config), + self.analysis_host.analysis(), + Arc::clone(&self.vfs), + Arc::clone(&self.open_document_versions), + Arc::clone(&self.line_ending_map), + Arc::clone(&self.projects), + Arc::clone(&self.ai_completion), + ) + } + + pub fn main_loop(mut self) -> Result<()> { + if self.config.did_save_text_document_dynamic_registration() { + let save_registration_options = lsp_types::TextDocumentSaveRegistrationOptions { + include_text: Some(false), + text_document_registration_options: lsp_types::TextDocumentRegistrationOptions { + document_selector: Some(vec![ + lsp_types::DocumentFilter { + language: None, + scheme: None, + pattern: Some("**/*.{e,h}rl".to_string()), + }, + lsp_types::DocumentFilter { + language: None, + scheme: None, + pattern: Some("**/rebar.{config,config.script,lock}".to_string()), + }, + ]), + }, + }; + + let registration = lsp_types::Registration { + id: notification::DidSaveTextDocument::METHOD.to_string(), + method: notification::DidSaveTextDocument::METHOD.to_string(), + register_options: Some(serde_json::to_value(save_registration_options).unwrap()), + }; + + self.send_request::( + lsp_types::RegistrationParams { + registrations: vec![registration], + }, + |_, _| Ok(()), + ) + } + + while let Some(event) = self.next_event() { + if let Event::Lsp(lsp_server::Message::Notification(notif)) = &event { + if notif.method == notification::Exit::METHOD { + return Ok(()); + } + } + self.handle_event(event)?; + } + + bail!("client exited without proper shutdown sequence"); + } + + fn next_event(&self) -> Option { + select! { + recv(self.connection.receiver) -> msg => { + msg.ok().map(Event::Lsp) + } + + recv(self.vfs_loader.receiver) -> msg => { + Some(Event::Vfs(msg.unwrap())) + } + + recv(self.task_pool.receiver) -> msg => { + Some(Event::Task(msg.unwrap())) + } + + recv(self.progress.receiver()) -> msg => { + Some(Event::Task(Task::Progress(msg.unwrap()))) + } + + recv(telemetry::receiver()) -> msg => { + Some(Event::Telemetry(msg.unwrap())) + } + + recv (self.project_pool.receiver) -> msg => { + Some(Event::Task(msg.unwrap())) + } + + recv (self.cache_pool.receiver) -> msg => { + Some(Event::Task(msg.unwrap())) + } + + } + } + + fn handle_event(&mut self, event: Event) -> Result<()> { + log::info!("handle_event {:?}", event); + + match event { + Event::Lsp(msg) => match msg { + lsp_server::Message::Request(req) => self.on_request(req)?, + lsp_server::Message::Notification(notif) => self.on_notification(notif)?, + lsp_server::Message::Response(resp) => self.complete_request(resp)?, + }, + Event::Vfs(mut msg) => loop { + match msg { + loader::Message::Loaded { files } => self.on_loader_loaded(files), + loader::Message::Progress { + n_total, + n_done, + config_version, + } => self.on_loader_progress(n_total, n_done, config_version), + } + + // Coalesce many VFS event into a single main loop turn + msg = match self.vfs_loader.receiver.try_recv() { + Ok(msg) => msg, + Err(_) => break, + } + }, + Event::Task(mut task) => loop { + match task { + Task::Response(response) => self.send_response(response), + Task::FetchProject(project) => self.fetch_project_completed(project)?, + Task::NativeDiagnostics(diags) => self.native_diagnostics_completed(diags), + Task::EqwalizerDiagnostics(spinner, diags) => { + spinner.end(); + self.eqwalizer_diagnostics_completed(diags) + } + Task::EdocDiagnostics(spinner, diags) => { + spinner.end(); + self.edoc_diagnostics_completed(diags) + } + Task::ParseServerDiagnostics(diags) => { + self.erlang_service_diagnostics_completed(diags) + } + Task::CompileDeps(spinner) => { + self.analysis_host + .raw_database() + .update_erlang_service_paths(); + spinner.end(); + self.eqwalizer_diagnostics_requested = true; + } + Task::Progress(progress) => self.report_progress(progress), + Task::UpdateCache(spinner, files) => self.update_cache(spinner, files), + Task::ScheduleCache => self.schedule_cache(), + } + + // Coalesce many tasks into a single main loop turn + task = match self.task_pool.receiver.try_recv() { + Ok(task) => task, + Err(_) => break, + } + }, + Event::Telemetry(message) => self.on_telemetry(message), + } + + if self.status == Status::ShuttingDown { + return Ok(()); + } + + let changed = self.process_changes_to_vfs_store(); + + if self.status == Status::Running { + if changed { + self.update_native_diagnostics(); + } + + if mem::take(&mut self.eqwalizer_diagnostics_requested) { + self.update_eqwalizer_diagnostics(); + self.update_erlang_service_diagnostics(); + } + + if mem::take(&mut self.edoc_diagnostics_requested) { + self.update_edoc_diagnostics(); + } + } + + if let Some(diagnostic_changes) = self.diagnostics.take_changes() { + log::info!("changed diagnostics: {:?}", diagnostic_changes); + + for file_id in diagnostic_changes { + let url = file_id_to_url(&self.vfs.read(), file_id); + let diagnostics = self.diagnostics.diagnostics_for(file_id).cloned().collect(); + let version = convert::vfs_path(&url) + .map(|path| self.open_document_versions.read().get(&path).cloned()) + .unwrap_or_default(); + + self.send_notification::( + lsp_types::PublishDiagnosticsParams { + uri: url, + diagnostics, + version, + }, + ); + } + } + + Ok(()) + } + + fn on_request(&mut self, req: Request) -> Result<()> { + let request_timer = timeit!("handle req {}#{}", req.method, req.id); + self.register_request(&req, request_timer); + + match self.status { + Status::Initialising | Status::Loading(_) + if req.method != request::Shutdown::METHOD && !req.method.starts_with("elp/") => + { + let id = req.id.clone(); + self.send_response(Response::new_err( + id, + ErrorCode::ContentModified as i32, + "elp is still loading".to_string(), + )); + return Ok(()); + } + Status::ShuttingDown => { + self.send_response(Response::new_err( + req.id.clone(), + ErrorCode::InvalidRequest as i32, + "shutdown already requested".to_string(), + )); + + return Ok(()); + } + _ => {} + } + + RequestDispatcher::new(self, req) + .on_sync::(|this, ()| { + this.transition(Status::ShuttingDown); + this.analysis_host.request_cancellation(); + Ok(()) + })? + .on::(handlers::handle_code_action) + .on::(handlers::handle_code_action_resolve) + .on::(handlers::handle_goto_definition) + .on::(handlers::handle_references) + .on::(handlers::handle_completion) + .on::(handlers::handle_completion_resolve) + .on::(handlers::handle_document_symbol) + .on::(handlers::handle_workspace_symbol) + .on::(handlers::handle_rename) + .on::(handlers::handle_hover) + .on::(handlers::handle_folding_range) + .on::(handlers::handle_document_highlight) + .on::(handlers::handle_call_hierarchy_prepare) + .on::( + handlers::handle_call_hierarchy_incoming, + ) + .on::( + handlers::handle_call_hierarchy_outgoing, + ) + .on::(handlers::handle_signature_help) + .on::(handlers::handle_selection_range) + .on::(handlers::handle_semantic_tokens_full) + .on::( + handlers::handle_semantic_tokens_full_delta, + ) + .on::(handlers::handle_semantic_tokens_range) + .on::(handlers::handle_code_lens) + .on::(handlers::handle_inlay_hints) + .on::(handlers::handle_inlay_hints_resolve) + .on::(handlers::handle_expand_macro) + .on::(handlers::pong) + .on::(handlers::handle_external_docs) + .finish(); + + Ok(()) + } + + fn on_notification(&mut self, notif: Notification) -> Result<()> { + NotificationDispatcher::new(self, notif) + .on::(|this, params| { + let id = parse_id(params.id); + this.cancel(id); + Ok(()) + })? + .on::(|this, params| { + this.eqwalizer_diagnostics_requested = true; + this.edoc_diagnostics_requested = true; + if let Ok(path) = convert::abs_path(¶ms.text_document.uri) { + this.fetch_projects_if_needed(&path); + let path = VfsPath::from(path); + if this + .open_document_versions + .write() + .insert(path.clone(), params.text_document.version) + .is_some() + { + log::error!("duplicate DidOpenTextDocument: {}", path); + } + + let mut vfs = this.vfs.write(); + vfs.set_file_contents( + path.clone(), + Some(params.text_document.text.into_bytes()), + ); + + // Until we bring over the full rust-analyzer + // style change processing, make a list of files + // that are freshly opened so diagnostics are + // generated for them, despite no changes being + // registered in vfs. + let file_id = vfs.file_id(&path).unwrap(); + this.newly_opened_documents.push(ChangedFile { + file_id, + change_kind: ChangeKind::Modify, + }); + } else { + log::error!( + "DidOpenTextDocument: could not get vfs path for {}", + params.text_document.uri + ); + } + + Ok(()) + })? + .on::(|this, params| { + if let Ok(path) = convert::vfs_path(¶ms.text_document.uri) { + match this.open_document_versions.write().get_mut(&path) { + Some(doc) => { + // The version passed in DidChangeTextDocument is the version after all edits are applied + // so we should apply it before the vfs is notified. + *doc = params.text_document.version; + } + None => { + log::error!("unexpected DidChangeTextDocument: {}", path); + return Ok(()); + } + }; + let mut vfs = this.vfs.write(); + let file_id = vfs.file_id(&path).unwrap(); + let mut document = Document::from_bytes(vfs.file_contents(file_id).to_vec()); + document.apply_changes(params.content_changes); + + vfs.set_file_contents(path, Some(document.into_bytes())); + } + Ok(()) + })? + .on::(|this, params| { + if let Ok(path) = convert::vfs_path(¶ms.text_document.uri) { + if this.open_document_versions.write().remove(&path).is_none() { + log::error!("unexpected DidCloseTextDocument: {}", path); + } + } + + // Clear the diagnostics for the previously known version of the file. + this.send_notification::( + lsp_types::PublishDiagnosticsParams { + uri: params.text_document.uri, + diagnostics: Vec::new(), + version: None, + }, + ); + + Ok(()) + })? + .on::(|this, params| { + if convert::vfs_path(¶ms.text_document.uri).is_ok() { + this.eqwalizer_diagnostics_requested = true; + this.edoc_diagnostics_requested = true; + } + Ok(()) + })? + .on::(|this, _params| { + // As stated in https://github.com/microsoft/language-server-protocol/issues/676, + // this notification's parameters should be ignored and the actual config queried separately. + this.refresh_config(); + + Ok(()) + })? + .on::(|_, _| { + // Nothing to do for now + Ok(()) + })? + .on::(|this, params| { + for change in params.changes { + if let Ok(path) = convert::abs_path(&change.uri) { + let opened = convert::vfs_path(&change.uri) + .map(|vfs_path| { + this.open_document_versions.read().contains_key(&vfs_path) + }) + .unwrap_or(false); + if !opened { + this.vfs_loader.handle.invalidate(path); + } + } + } + this.eqwalizer_diagnostics_requested = true; + this.edoc_diagnostics_requested = true; + Ok(()) + })? + .finish(); + + Ok(()) + } + + fn on_loader_progress(&mut self, n_total: usize, n_done: usize, config_version: u32) { + // report progress + always!(config_version <= self.vfs_config_version); + + if n_total == 0 { + self.transition(Status::Invalid); + } else if n_done == 0 { + let pb = self + .progress + .begin_bar("Applications loaded".into(), n_total); + self.transition(Status::Loading(pb)); + } else if n_done < n_total { + if let Status::Loading(pb) = &self.status { + pb.report(n_done, n_total); + } + } else { + assert_eq!(n_done, n_total); + self.transition(Status::Running); + self.schedule_compile_deps(); + self.schedule_cache(); + } + } + + fn on_loader_loaded(&mut self, files: Vec<(AbsPathBuf, Option>)>) { + let mut vfs = self.vfs.write(); + for (path, contents) in files { + let path = VfsPath::from(path); + if !self.open_document_versions.read().contains_key(&path) { + // This call will add the file to the changed_files, picked + // up in `process_changes`. + vfs.set_file_contents(path, contents); + } + } + } + + fn process_changes_to_vfs_store(&mut self) -> bool { + let changed_files = { + // Don't hold write lock, while modifying db - this can lead to deadlocks! + let mut vfs = self.vfs.write(); + let mut changed_files = vfs.take_changes(); + // Note: the append operations clears out self.newly_opened_documents too + changed_files.append(&mut self.newly_opened_documents); + changed_files + }; + + if changed_files.is_empty() { + return false; + } + + // The writes to salsa as these changes are applied below will + // trigger Cancellation any pending processing. This makes + // sure all calculations see a consistent view of the + // database. + + let vfs = self.vfs.read(); + let raw_database = self.analysis_host.raw_database_mut(); + + for file in &changed_files { + let file_path = vfs.file_path(file.file_id); + // Invalidate DB when making changes to header files + if let Some((_, Some("hrl"))) = file_path.name_and_extension() { + raw_database.set_include_files_revision(raw_database.include_files_revision() + 1); + } + if file.exists() { + let bytes = vfs.file_contents(file.file_id).to_vec(); + let document = Document::from_bytes(bytes); + let (text, line_ending) = LineEndings::normalize(document.content); + self.line_ending_map + .write() + .insert(file.file_id, line_ending); + raw_database.set_file_text(file.file_id, Arc::new(text)); + // causes us to remove stale squiggles from the UI + self.diagnostics.set_eqwalizer(file.file_id, vec![]); + } else { + // TODO (T105975906): Clean up stale .etf files + + // We can't actually delete things from salsa, just set it to empty + raw_database.set_file_text(file.file_id, Default::default()); + }; + } + + if changed_files + .iter() + .any(|file| file.is_created_or_deleted()) + { + let sets = self.file_set_config.partition(&vfs); + for (idx, set) in sets.into_iter().enumerate() { + let root_id = SourceRootId(idx as u32); + for file_id in set.iter() { + raw_database.set_file_source_root(file_id, root_id); + } + let root = SourceRoot::new(set); + raw_database.set_source_root(root_id, Arc::new(root)); + } + } + + true + } + + fn opened_documents(&self) -> Vec { + let vfs = self.vfs.read(); + self.open_document_versions + .read() + .keys() + .map(|path| vfs.file_id(path).unwrap()) + .collect() + } + + fn update_native_diagnostics(&mut self) { + let opened_documents = self.opened_documents(); + let snapshot = self.snapshot(); + + self.task_pool.handle.spawn(move || { + let diagnostics = opened_documents + .into_iter() + .filter_map(|file_id| Some((file_id, snapshot.native_diagnostics(file_id)?))) + .collect(); + + Task::NativeDiagnostics(diagnostics) + }); + } + + fn native_diagnostics_completed(&mut self, diags: Vec<(FileId, Vec)>) { + for (file_id, diagnostics) in diags { + self.diagnostics.set_native(file_id, diagnostics); + } + } + + fn update_eqwalizer_diagnostics(&mut self) { + if self.status != Status::Running { + return; + } + + log::info!("Recomputing EqWAlizer diagnostics"); + + let opened_documents = self.opened_documents(); + let snapshot = self.snapshot(); + + let spinner = self.progress.begin_spinner("EqWAlizing".to_string()); + + self.task_pool.handle.spawn(move || { + let diagnostics = opened_documents + .into_iter() + .filter_map(|file_id| Some((file_id, snapshot.eqwalizer_diagnostics(file_id)?))) + .collect(); + + Task::EqwalizerDiagnostics(spinner, diagnostics) + }); + } + + fn update_edoc_diagnostics(&mut self) { + if self.status != Status::Running { + return; + } + + log::info!("Recomputing EDoc diagnostics"); + + let opened_documents = self.opened_documents(); + let snapshot = self.snapshot(); + + let spinner = self.progress.begin_spinner("EDoc".to_string()); + + let supported_opened_documents: Vec = opened_documents + .into_iter() + .filter(|file_id| is_supported_by_edoc(&self.vfs.read(), *file_id)) + .collect(); + self.task_pool.handle.spawn(move || { + let diagnostics = supported_opened_documents + .into_iter() + .filter_map(|file_id| snapshot.edoc_diagnostics(file_id)) + .flatten() + .collect(); + + Task::EdocDiagnostics(spinner, diagnostics) + }); + } + + fn eqwalizer_diagnostics_completed(&mut self, diags: Vec<(FileId, Vec)>) { + for (file_id, diagnostics) in diags { + self.diagnostics.set_eqwalizer(file_id, diagnostics); + } + } + + fn edoc_diagnostics_completed(&mut self, diags: Vec<(FileId, Vec)>) { + for (file_id, diagnostics) in diags { + self.diagnostics.set_edoc(file_id, diagnostics); + } + } + + fn update_erlang_service_diagnostics(&mut self) { + if self.status != Status::Running { + return; + } + + log::info!("Recomputing Erlang Service diagnostics"); + + let opened_documents = self.opened_documents(); + let snapshot = self.snapshot(); + let supported_opened_documents: Vec = opened_documents + .into_iter() + .filter(|file_id| is_supported_by_parse_server(&self.vfs.read(), *file_id)) + .collect(); + self.task_pool.handle.spawn(move || { + let diagnostics = supported_opened_documents + .into_iter() + .filter_map(|file_id| snapshot.erlang_service_diagnostics(file_id)) + .flatten() + .collect(); + + Task::ParseServerDiagnostics(diagnostics) + }); + } + + fn erlang_service_diagnostics_completed(&mut self, diags: Vec<(FileId, Vec)>) { + for (file_id, diagnostics) in diags.clone() { + self.diagnostics.set_erlang_service(file_id, diagnostics); + } + } + + fn switch_workspaces(&mut self, project: Result) -> Result<()> { + log::info!("will switch workspaces"); + + let project = match project { + Ok(project) => project, + Err(err) if self.projects.len() > 0 => { + log::error!("ELP failed to switch workspaces: {:#}", err); + return Ok(()); + } + Err(err) => bail!("ELP failed to switch workspaces: {:#}", err), + }; + + let mut projects: Vec = self.projects.iter().cloned().collect(); + projects.push(project); + + let raw_db = self.analysis_host.raw_database_mut(); + raw_db.clear_erlang_services(); + + let project_apps = ProjectApps::new(&projects, IncludeOtp::Yes); + let folders = ProjectFolders::new(&project_apps); + project_apps.app_structure().apply(raw_db); + + for (project_id, _) in projects.iter().enumerate() { + let project_id = ProjectId(project_id as u32); + raw_db.ensure_erlang_service(project_id)?; + } + if let Some(otp_project_id) = project_apps.otp_project_id { + raw_db.ensure_erlang_service(otp_project_id)?; + } + + self.file_set_config = folders.file_set_config; + + let register_options = lsp_types::DidChangeWatchedFilesRegistrationOptions { + watchers: folders.watch, + }; + + let registrations = vec![lsp_types::Registration { + id: "workspace/didChangeWatchedFiles".to_string(), + method: notification::DidChangeWatchedFiles::METHOD.to_string(), + register_options: Some(serde_json::to_value(register_options).unwrap()), + }]; + + self.send_request::( + lsp_types::RegistrationParams { registrations }, + |_, _| Ok(()), + ); + + let vfs_loader_config = loader::Config { + load: folders.load, + watch: vec![], + version: 0, + }; + self.vfs_loader.handle.set_config(vfs_loader_config); + + self.projects = Arc::new(projects); + self.project_loader.lock().load_completed(); + Ok(()) + } + + pub fn refresh_config(&mut self) { + self.send_request::( + lsp_types::ConfigurationParams { + items: vec![lsp_types::ConfigurationItem { + scope_uri: None, + section: Some("elp".to_string()), + }], + }, + |this, resp| { + log::debug!("config update response: '{:?}", resp); + let lsp_server::Response { error, result, .. } = resp; + + match (error, result) { + (Some(err), _) => { + log::error!("failed to fetch the server settings: {:?}", err) + } + (None, Some(mut configs)) => { + if let Some(json) = configs.get_mut(0) { + // Note that json can be null according to the spec if the client can't + // provide a configuration. This is handled in Config::update below. + let mut config = Config::clone(&*this.config); + config.update(json.take()); + this.update_configuration(config); + } + } + (None, None) => { + log::error!("received empty server settings response from the client") + } + } + + Ok(()) + }, + ); + } + + fn update_configuration(&mut self, config: Config) { + let _p = profile::span("Server::update_configuration"); + let _old_config = mem::replace(&mut self.config, Arc::new(config)); + + self.logger + .reconfigure(LOGGER_NAME, self.config.log_filter()); + self.logger.reconfigure("default", self.config.log_filter()); + } + + fn transition(&mut self, status: Status) { + if self.status != status { + log::info!("transitioning from {:?} to {:?}", self.status, status); + self.status = status; + if self.config.server_status_notification() { + self.send_notification::(lsp_ext::StatusParams { + status: self.status.as_lsp_status(), + }); + } + } + } + + fn show_message(&mut self, typ: lsp_types::MessageType, message: String) { + self.send_notification::( + lsp_types::ShowMessageParams { typ, message }, + ) + } + + fn send_response(&mut self, response: Response) { + if let Some((method, request_timer)) = self.req_queue.incoming.complete(response.id.clone()) + { + log::debug!("response {}#{}: {:?}", method, response.id, response); + // logs time to complete request + drop(request_timer); + self.send(response.into()); + } + } + + fn cancel(&mut self, request_id: RequestId) { + if let Some(response) = self.req_queue.incoming.cancel(request_id) { + self.send(response.into()); + } + } + + fn send_request(&mut self, params: R::Params, handler: ReqHandler) { + let request = self + .req_queue + .outgoing + .register(R::METHOD.to_string(), params, handler); + self.send(request.into()); + } + + fn complete_request(&mut self, response: Response) -> Result<()> { + if let Some(handler) = self.req_queue.outgoing.complete(response.id.clone()) { + handler(self, response)?; + } + Ok(()) + } + + fn send_notification(&mut self, params: N::Params) { + let not = Notification::new(N::METHOD.to_string(), params); + self.send(not.into()); + } + + fn send(&self, message: lsp_server::Message) { + self.connection.sender.send(message).unwrap() + } + + fn register_request(&mut self, request: &Request, received_timer: TimeIt) { + self.req_queue + .incoming + .register(request.id.clone(), (request.method.clone(), received_timer)) + } + + fn fetch_projects_if_needed(&mut self, path: &AbsPath) { + let path = path.to_path_buf(); + let loader = self.project_loader.clone(); + self.project_pool.handle.spawn_with_sender({ + move |sender| { + let manifest = loader.lock().load_manifest_if_new(&path); + let project = match manifest { + Ok(Some(manifest)) => Project::load(manifest), + Err(err) => Err(err), + Ok(None) => return, + }; + + log::info!("did fetch project"); + log::debug!("fetched projects {:?}", project); + + sender.send(Task::FetchProject(project)).unwrap(); + } + }) + } + + fn fetch_project_completed(&mut self, project: Result) -> Result<()> { + if let Err(err) = self.switch_workspaces(project) { + self.show_message(lsp_types::MessageType::ERROR, err.to_string()) + } + Ok(()) + } + + fn schedule_compile_deps(&mut self) { + let snapshot = self.snapshot(); + + let spinner = self + .progress + .begin_spinner("ELP compiling dependencies for EqWAlizer".to_string()); + + self.task_pool.handle.spawn_with_sender(move |sender| { + snapshot.set_up_projects(); + + sender.send(Task::CompileDeps(spinner)).unwrap(); + }); + } + + fn schedule_cache(&mut self) { + let snapshot = self.snapshot(); + let spinner = self.progress.begin_spinner("Parsing codebase".to_string()); + + self.cache_pool.handle.spawn_with_sender(move |sender| { + let mut files = vec![]; + for (i, _) in snapshot.projects.iter().enumerate() { + let module_index = match snapshot.analysis.module_index(ProjectId(i as u32)) { + Ok(module_index) => module_index, + //rescheduling canceled + Err(_) => { + sender.send(Task::ScheduleCache).unwrap(); + return; + } + }; + + for (_, _, file_id) in module_index.iter_own() { + files.push(file_id); + } + } + sender.send(Task::UpdateCache(spinner, files)).unwrap(); + }); + } + + fn update_cache(&mut self, spinner: Spinner, mut files: Vec) { + if files.is_empty() { + spinner.end(); + return; + } + let snapshot = self.snapshot(); + self.cache_pool.handle.spawn_with_sender(move |sender| { + while !files.is_empty() { + let file_id = files.remove(files.len() - 1); + if let Err(_) = snapshot.analysis.def_map(file_id) { + //got canceled + files.push(file_id); + break; + } + } + if files.is_empty() { + spinner.end(); + } else { + sender.send(Task::UpdateCache(spinner, files)).unwrap(); + } + }); + } + + fn report_progress(&mut self, task: ProgressTask) { + let params = match task { + ProgressTask::BeginNotify(params) => { + self.send_request::( + lsp_types::WorkDoneProgressCreateParams { + token: params.token.clone(), + }, + |_, _| Ok(()), + ); + params + } + ProgressTask::Notify(params) => params, + }; + self.send_notification::(params); + } + + fn on_telemetry(&mut self, message: TelemetryMessage) { + match serde_json::to_value(message.clone()) { + Ok(params) => self.send_notification::(params), + Err(err) => log::warn!( + "Error serializing telemetry. message: {:?} error: {}", + message, + err + ), + } + } +} + +fn parse_id(id: lsp_types::NumberOrString) -> RequestId { + match id { + lsp_types::NumberOrString::Number(id) => id.into(), + lsp_types::NumberOrString::String(id) => id.into(), + } +} + +pub fn file_id_to_path(vfs: &Vfs, id: FileId) -> Result { + let url = file_id_to_url(vfs, id); + convert::abs_path(&url) +} + +pub fn file_id_to_url(vfs: &Vfs, id: FileId) -> Url { + let path = vfs.file_path(id); + let path = path.as_path().unwrap(); + convert::url_from_abs_path(path) +} + +pub fn is_supported_by_parse_server(vfs: &Vfs, id: FileId) -> bool { + let path = vfs.file_path(id); + match path.name_and_extension() { + Some((_name, Some(ext))) => PARSE_SERVER_SUPPORTED_EXTENSIONS.contains(&ext), + _ => false, + } +} + +pub fn is_supported_by_edoc(vfs: &Vfs, id: FileId) -> bool { + let path = vfs.file_path(id); + match path.name_and_extension() { + Some((_name, Some(ext))) => EDOC_SUPPORTED_EXTENSIONS.contains(&ext), + _ => false, + } +} diff --git a/crates/elp/src/server/capabilities.rs b/crates/elp/src/server/capabilities.rs new file mode 100644 index 0000000000..714c10741f --- /dev/null +++ b/crates/elp/src/server/capabilities.rs @@ -0,0 +1,144 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use lsp_types::CallHierarchyServerCapability; +use lsp_types::ClientCapabilities; +use lsp_types::CodeActionKind; +use lsp_types::CodeActionOptions; +use lsp_types::CodeActionProviderCapability; +use lsp_types::CodeLensOptions; +use lsp_types::CompletionOptions; +use lsp_types::FoldingRangeProviderCapability; +use lsp_types::HoverProviderCapability; +use lsp_types::InlayHintOptions; +use lsp_types::InlayHintServerCapabilities; +use lsp_types::OneOf; +use lsp_types::RenameOptions; +use lsp_types::SaveOptions; +use lsp_types::SelectionRangeProviderCapability; +use lsp_types::SemanticTokensFullOptions; +use lsp_types::SemanticTokensLegend; +use lsp_types::SemanticTokensOptions; +use lsp_types::ServerCapabilities; +use lsp_types::SignatureHelpOptions; +use lsp_types::TextDocumentSyncCapability; +use lsp_types::TextDocumentSyncKind; +use lsp_types::TextDocumentSyncOptions; +use lsp_types::WorkDoneProgressOptions; + +use crate::semantic_tokens; + +pub fn compute(client: &ClientCapabilities) -> ServerCapabilities { + ServerCapabilities { + position_encoding: None, + selection_range_provider: Some(SelectionRangeProviderCapability::Simple(true)), + text_document_sync: Some(TextDocumentSyncCapability::Options( + TextDocumentSyncOptions { + open_close: Some(true), + change: Some(TextDocumentSyncKind::INCREMENTAL), + will_save: None, + will_save_wait_until: None, + save: Some(SaveOptions::default().into()), + }, + )), + hover_provider: Some(HoverProviderCapability::Simple(true)), + completion_provider: Some(CompletionOptions { + resolve_provider: Some(true), + trigger_characters: Some( + [":", "#", "?", ".", "-", "\\"] + .iter() + .map(|s| s.to_string()) + .collect(), + ), + ..Default::default() + }), + signature_help_provider: Some(SignatureHelpOptions { + trigger_characters: Some(vec!["(".to_string(), ",".to_string()]), + retrigger_characters: None, + work_done_progress_options: WorkDoneProgressOptions { + work_done_progress: None, + }, + }), + definition_provider: Some(OneOf::Left(true)), + type_definition_provider: None, + implementation_provider: None, + references_provider: Some(OneOf::Left(true)), + document_highlight_provider: Some(OneOf::Left(true)), + document_symbol_provider: Some(OneOf::Left(true)), + workspace_symbol_provider: Some(OneOf::Left(true)), + code_action_provider: Some(code_action_capabilities(client)), + // TODO: This will be put behind a GK before shipping + code_lens_provider: Some(CodeLensOptions { + resolve_provider: Some(false), + }), + document_formatting_provider: None, + document_range_formatting_provider: None, + document_on_type_formatting_provider: None, + rename_provider: Some(OneOf::Right(RenameOptions { + prepare_provider: Some(false), + work_done_progress_options: WorkDoneProgressOptions { + work_done_progress: None, + }, + })), + document_link_provider: None, + color_provider: None, + folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)), + declaration_provider: None, + execute_command_provider: None, + workspace: None, + call_hierarchy_provider: Some(CallHierarchyServerCapability::Simple(true)), + semantic_tokens_provider: Some( + SemanticTokensOptions { + legend: SemanticTokensLegend { + token_types: semantic_tokens::SUPPORTED_TYPES.to_vec(), + token_modifiers: semantic_tokens::SUPPORTED_MODIFIERS.to_vec(), + }, + + full: Some(SemanticTokensFullOptions::Delta { delta: Some(true) }), + range: Some(true), + work_done_progress_options: Default::default(), + } + .into(), + ), + moniker_provider: None, + inlay_hint_provider: Some(OneOf::Right(InlayHintServerCapabilities::Options( + InlayHintOptions { + work_done_progress_options: Default::default(), + resolve_provider: Some(true), + }, + ))), + linked_editing_range_provider: None, + experimental: None, + } +} + +fn code_action_capabilities(client_caps: &ClientCapabilities) -> CodeActionProviderCapability { + client_caps + .text_document + .as_ref() + .and_then(|it| it.code_action.as_ref()) + .and_then(|it| it.code_action_literal_support.as_ref()) + .map_or(CodeActionProviderCapability::Simple(true), |_| { + CodeActionProviderCapability::Options(CodeActionOptions { + // Advertise support for all built-in CodeActionKinds. + // Ideally we would base this off of the client capabilities + // but the client is supposed to fall back gracefully for unknown values. + code_action_kinds: Some(vec![ + CodeActionKind::EMPTY, + CodeActionKind::QUICKFIX, + CodeActionKind::REFACTOR, + CodeActionKind::REFACTOR_EXTRACT, + CodeActionKind::REFACTOR_INLINE, + CodeActionKind::REFACTOR_REWRITE, + ]), + resolve_provider: Some(true), + work_done_progress_options: Default::default(), + }) + }) +} diff --git a/crates/elp/src/server/dispatch.rs b/crates/elp/src/server/dispatch.rs new file mode 100644 index 0000000000..58db04a8eb --- /dev/null +++ b/crates/elp/src/server/dispatch.rs @@ -0,0 +1,254 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::fmt; +use std::panic; + +use anyhow::bail; +use anyhow::Result; +use crossbeam_channel::Sender; +use elp_ide::is_cancelled; +use lsp_server::ExtractError; +use lsp_server::RequestId; +use lsp_server::Response; +use serde::de::DeserializeOwned; +use serde::Serialize; + +use super::Server; +use super::Snapshot; +use crate::server::Task; +use crate::LspError; + +#[must_use] +pub(crate) struct RequestDispatcher<'a> { + pub(crate) req: Option, + pub(crate) server: &'a mut Server, +} + +impl<'a> RequestDispatcher<'a> { + pub fn new(server: &'a mut Server, req: lsp_server::Request) -> Self { + RequestDispatcher { + req: Some(req), + server, + } + } + + /// Dispatches the request onto the current thread + pub(crate) fn on_sync( + mut self, + f: fn(&mut Server, R::Params) -> Result, + ) -> Result + where + R: lsp_types::request::Request + 'static, + R::Params: DeserializeOwned + panic::UnwindSafe + fmt::Debug + 'static, + R::Result: Serialize + 'static, + { + let (id, params) = match self.parse::() { + Some(it) => it, + None => return Ok(self), + }; + + let world = panic::AssertUnwindSafe(&mut *self.server); + + let response = panic::catch_unwind(move || { + // force closure to capture the entire world, not just &mut Server + // and dropping the AssertUnwindSafe information + // (since we only use world.0 and closures in Rust 2021 capture only used fields) + let world = world; + let _pctx = + stdx::panic_context::enter(format!("\nrequest: {} {:#?}", R::METHOD, params)); + let result = f(world.0, params); + result_to_response::(id, result) + }) + .map_err(|_err| anyhow::Error::msg(format!("sync task {:?} panicked", R::METHOD)))?; + self.server.send_response(response); + Ok(self) + } + + /// Dispatches the request onto thread pool + pub(crate) fn on(mut self, f: fn(Snapshot, R::Params) -> Result) -> Self + where + R: lsp_types::request::Request + 'static, + R::Params: DeserializeOwned + Send + fmt::Debug + 'static, + R::Result: Serialize + 'static, + { + let (id, params) = match self.parse::() { + Some(it) => it, + None => return self, + }; + + self.server.task_pool.handle.spawn_with_sender({ + let world = self.server.snapshot(); + + move |sender| { + let _pctx = + stdx::panic_context::enter(format!("\nrequest: {} {:#?}", R::METHOD, params)); + let error_bomb = ErrorBomb::new(sender.clone(), id.clone()); + let result = f(world, params); + error_bomb.defuse(); + sender + .send(Task::Response(result_to_response::(id, result))) + .unwrap(); + } + }); + + self + } + + pub(crate) fn finish(mut self) { + if let Some(req) = self.req.take() { + // The request has not been processed by any of the dispatch handlers + let id = req.id.clone(); + self.server.send_response(Response::new_err( + id, + lsp_server::ErrorCode::MethodNotFound as i32, + "unknown request".to_string(), + )); + } + } + + fn parse(&mut self) -> Option<(lsp_server::RequestId, R::Params)> + where + R: lsp_types::request::Request + 'static, + R::Params: DeserializeOwned + 'static, + { + let req = match &self.req { + Some(req) if req.method == R::METHOD => self.req.take().unwrap(), + _ => return None, + }; + + let res = crate::from_json(R::METHOD, req.params); + match res { + Ok(params) => Some((req.id, params)), + Err(err) => { + let response = lsp_server::Response::new_err( + req.id, + lsp_server::ErrorCode::InvalidParams as i32, + err.to_string(), + ); + self.server.send_response(response); + None + } + } + } +} + +// --------------------------------------------------------------------- + +fn result_to_response( + id: lsp_server::RequestId, + result: Result, +) -> lsp_server::Response +where + R: lsp_types::request::Request + 'static, + R::Params: DeserializeOwned + 'static, + R::Result: Serialize + 'static, +{ + match result { + Ok(resp) => lsp_server::Response::new_ok(id, &resp), + Err(e) => match e.downcast::() { + Ok(lsp_error) => lsp_server::Response::new_err(id, lsp_error.code, lsp_error.message), + Err(e) => { + if is_cancelled(&*e) { + lsp_server::Response::new_err( + id, + lsp_server::ErrorCode::ContentModified as i32, + "content modified".to_string(), + ) + } else { + lsp_server::Response::new_err( + id, + lsp_server::ErrorCode::InternalError as i32, + e.to_string(), + ) + } + } + }, + } +} + +struct ErrorBomb { + sender: Sender, + request_id: Option, +} + +impl ErrorBomb { + fn new(sender: Sender, id: RequestId) -> Self { + Self { + sender, + request_id: Some(id), + } + } + + fn defuse(mut self) { + self.request_id = None + } +} + +impl Drop for ErrorBomb { + fn drop(&mut self) { + if let Some(id) = self.request_id.take() { + self.sender + .send(Task::Response(lsp_server::Response::new_err( + id, + lsp_server::ErrorCode::InternalError as i32, + "internal error".to_string(), + ))) + .unwrap(); + } + } +} + +#[must_use] +pub struct NotificationDispatcher<'a, S, R> { + notif: Option, + server: &'a mut S, + result: Option, +} + +impl<'a, S, R> NotificationDispatcher<'a, S, R> { + pub fn new(server: &'a mut S, notif: lsp_server::Notification) -> Self { + NotificationDispatcher { + notif: Some(notif), + server, + result: None, + } + } + + pub fn on(mut self, f: fn(&mut S, N::Params) -> Result) -> Result + where + N: lsp_types::notification::Notification + 'static, + N::Params: DeserializeOwned + Send + 'static, + { + let notif = match self.notif.take() { + Some(notif) => notif, + None => return Ok(self), + }; + + let params = match notif.extract(N::METHOD) { + Ok(params) => params, + Err(ExtractError::JsonError { method, error }) => { + bail!("Invalid request\nMethod: {method}\n error: {error}"); + } + Err(ExtractError::MethodMismatch(notif)) => { + self.notif = Some(notif); + return Ok(self); + } + }; + + self.result = Some(f(self.server, params)?); + Ok(self) + } + + pub fn finish(self) { + if let Some(notif) = self.notif { + log::error!("unhandled notification: {:?}", notif); + } + } +} diff --git a/crates/elp/src/server/logger.rs b/crates/elp/src/server/logger.rs new file mode 100644 index 0000000000..39e2e10682 --- /dev/null +++ b/crates/elp/src/server/logger.rs @@ -0,0 +1,116 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! Simple logger that logs using lsp logs using `env_logger` filter syntax + +use crossbeam_channel::Sender; +use elp_log::Builder; +use elp_log::Filter; +use elp_log::ReconfigureLog; +use lsp_server::Message; +use lsp_types::notification::LogMessage; +use lsp_types::notification::Notification; +use lsp_types::LogMessageParams; +use lsp_types::MessageType; + +pub struct LspLogger { + filter: Filter, + sender: Sender, +} + +impl LspLogger { + pub fn new(sender: Sender, filter: Option<&str>) -> Self { + let filter = { + let mut builder = Builder::new(); + if let Some(filter) = filter { + builder.parse(filter); + } else { + builder.filter_level(log::LevelFilter::Error); + } + filter_server_logs(&mut builder); + builder.build() + }; + + Self { sender, filter } + } +} + +impl ReconfigureLog for LspLogger { + fn filter(&self) -> &Filter { + &self.filter + } + + fn reconfigure(&mut self, mut filter: Builder) { + filter_server_logs(&mut filter); + self.filter = filter.build(); + } + + fn write(&self, record: &log::Record) { + let message = format!("[{}] {}", record.target(), record.args()); + + let typ = match record.level() { + log::Level::Error => MessageType::ERROR, + log::Level::Warn => MessageType::WARNING, + log::Level::Info => MessageType::INFO, + log::Level::Debug => MessageType::LOG, + log::Level::Trace => MessageType::LOG, + }; + + let params = LogMessageParams { typ, message }; + let not = lsp_server::Notification::new(LogMessage::METHOD.to_string(), params); + self.sender.send(not.into()).unwrap(); + } + + fn flush(&self) {} +} + +fn filter_server_logs(builder: &mut Builder) { + // Disable logs from `lsp_server` crate - since we're logging using code + // from that crate, this can lead to cyclic dependencies and ultimately + // locking up the server in an infinite loop + builder.filter_module("lsp_server", log::LevelFilter::Off); +} + +#[cfg(test)] +mod tests { + use elp_log::Logger; + use expect_test::expect; + + use super::*; + + #[test] + fn it_works() { + let (sender, receiver) = crossbeam_channel::unbounded(); + + let lsp_logger = LspLogger::new(sender, None); + + let logger = Logger::default(); + logger.register_logger("test", Box::new(lsp_logger)); + logger.install(); + + log::error!("An error occured!"); + log::trace!("This won't be logged"); + + let msg = receiver.try_recv().unwrap(); + expect![[r#" + Notification( + Notification { + method: "window/logMessage", + params: Object { + "message": String("[elp::server::logger::tests] An error occured!"), + "type": Number(1), + }, + }, + ) + "#]] + .assert_debug_eq(&msg); + + assert!(receiver.try_recv().is_err()); + } +} diff --git a/crates/elp/src/server/progress.rs b/crates/elp/src/server/progress.rs new file mode 100644 index 0000000000..83f9d577c2 --- /dev/null +++ b/crates/elp/src/server/progress.rs @@ -0,0 +1,165 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use crossbeam_channel::Receiver; +use crossbeam_channel::Sender; +use lsp_types::NumberOrString; +use lsp_types::ProgressParams; +use lsp_types::ProgressParamsValue; +use lsp_types::WorkDoneProgress; +use lsp_types::WorkDoneProgressBegin; +use lsp_types::WorkDoneProgressEnd; +use lsp_types::WorkDoneProgressReport; + +#[derive(Debug)] +pub enum ProgressTask { + BeginNotify(ProgressParams), + Notify(ProgressParams), +} + +// Follow structs don't derive Clone on purpose - this would violate the invariants +// and result in duplicate messages + +#[derive(Debug)] +pub struct ProgressManager { + counter: usize, + sender: Sender, + receiver: Receiver, +} + +impl Default for ProgressManager { + fn default() -> Self { + let (sender, receiver) = crossbeam_channel::unbounded(); + ProgressManager { + counter: 0, + sender, + receiver, + } + } +} + +impl ProgressManager { + pub fn receiver(&self) -> &Receiver { + &self.receiver + } + + pub fn begin_spinner(&mut self, title: String) -> Spinner { + Spinner::begin(self.sender.clone(), self.next_token(), title) + } + + pub fn begin_bar(&mut self, title: String, total: usize) -> ProgressBar { + ProgressBar::begin(self.sender.clone(), self.next_token(), title, total) + } + + fn next_token(&mut self) -> NumberOrString { + let token = NumberOrString::String(format!("ELP/{}", self.counter)); + self.counter += 1; + token + } +} + +#[derive(Debug)] +#[must_use] +pub struct Spinner { + token: NumberOrString, + sender: Sender, +} + +impl Spinner { + fn begin(sender: Sender, token: NumberOrString, title: String) -> Self { + let msg = WorkDoneProgressBegin { + title, + cancellable: None, + message: None, + percentage: None, + }; + send_begin(&sender, token.clone(), msg); + Self { token, sender } + } + + pub fn end(self) { + // let Drop do the job + } +} + +impl Drop for Spinner { + fn drop(&mut self) { + send_progress( + &self.sender, + self.token.clone(), + WorkDoneProgress::End(WorkDoneProgressEnd { message: None }), + ) + } +} + +#[derive(Debug)] +#[must_use] +pub struct ProgressBar { + token: NumberOrString, + sender: Sender, +} + +impl ProgressBar { + fn begin( + sender: Sender, + token: NumberOrString, + title: String, + total: usize, + ) -> Self { + let msg = WorkDoneProgressBegin { + title, + cancellable: None, + message: Some(format!("0/{}", total)), + percentage: Some(0), + }; + send_begin(&sender, token.clone(), msg); + Self { token, sender } + } + + pub fn report(&self, done: usize, total: usize) { + let message = format!("{}/{}", done, total); + let percent = done as f64 / total.max(1) as f64; + let msg = WorkDoneProgress::Report(WorkDoneProgressReport { + cancellable: None, + message: Some(message), + percentage: Some((percent * 100.0) as u32), + }); + send_progress(&self.sender, self.token.clone(), msg); + } + + pub fn end(self) { + // let Drop do the job + } +} + +impl Drop for ProgressBar { + fn drop(&mut self) { + send_progress( + &self.sender, + self.token.clone(), + WorkDoneProgress::End(WorkDoneProgressEnd { message: None }), + ) + } +} + +fn send_begin(sender: &Sender, token: NumberOrString, msg: WorkDoneProgressBegin) { + let params = ProgressParams { + token, + value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin(msg)), + }; + sender.send(ProgressTask::BeginNotify(params)).unwrap(); +} + +fn send_progress(sender: &Sender, token: NumberOrString, msg: WorkDoneProgress) { + let params = ProgressParams { + token, + value: ProgressParamsValue::WorkDone(msg), + }; + sender.send(ProgressTask::Notify(params)).unwrap(); +} diff --git a/crates/elp/src/server/setup.rs b/crates/elp/src/server/setup.rs new file mode 100644 index 0000000000..3c418de92c --- /dev/null +++ b/crates/elp/src/server/setup.rs @@ -0,0 +1,180 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::convert::TryFrom; +use std::env; + +use anyhow::Context; +use anyhow::Result; +use elp_ai::AiCompletion; +use elp_ide::elp_ide_db::elp_base_db::loader; +use elp_ide::elp_ide_db::elp_base_db::AbsPathBuf; +use elp_log::timeit_with_telemetry; +use elp_log::Logger; +use lsp_server::Connection; +use lsp_server::Notification; +use lsp_types::notification::Notification as _; +use lsp_types::InitializeParams; +use lsp_types::InitializeResult; +use lsp_types::ServerInfo; +use threadpool::ThreadPool; + +use super::logger::LspLogger; +use crate::config::Config; +use crate::from_json; +use crate::server::capabilities; +use crate::server::Handle; +use crate::server::Server; +use crate::server::TaskHandle; +use crate::server::VfsHandle; +use crate::server::LOGGER_NAME; +use crate::snapshot::TelemetryData; +use crate::task_pool::TaskPool; + +pub struct ServerSetup { + connection: Connection, + logger: Logger, +} + +impl ServerSetup { + pub fn new(connection: Connection, logger: Logger) -> ServerSetup { + ServerSetup { connection, logger } + } + + pub fn to_server(self) -> Result { + let config = self.initialize()?; + self.set_up_logger(); + setup_server(config, self.connection, self.logger) + } + + fn initialize(&self) -> Result { + let _timer = timeit_with_telemetry!(TelemetryData::Initialize); + let (id, params) = self.connection.initialize_start()?; + let params = from_json::("InitializeParams", params)?; + + let server_capabilities = capabilities::compute(¶ms.capabilities); + + let server_info = ServerInfo { + name: "elp".to_string(), + version: Some(crate::version()), + }; + + let result = InitializeResult { + capabilities: server_capabilities, + server_info: Some(server_info), + offset_encoding: None, + }; + + self.connection + .initialize_finish(id, serde_json::to_value(result.clone()).unwrap()) + .with_context(|| format!("during initialization finish: {:?}", result))?; + + let message = format!("ELP version: {}", crate::version()); + let show_message_params = lsp_types::ShowMessageParams { + typ: lsp_types::MessageType::INFO, + message, + }; + let notif = Notification::new( + lsp_types::notification::ShowMessage::METHOD.to_string(), + show_message_params, + ); + _ = self.connection.sender.send(notif.into()); + + // At this point the Client is able to start sending us normal + // operational requests. + + if let Some(client_info) = ¶ms.client_info { + let client_version = client_info.version.as_deref().unwrap_or(""); + log::info!("Client '{}' {}", client_info.name, client_version); + } + + let root_path = root_path(¶ms)?; + // Note: the LSP spec says initialization_options can be + // anything. If they match config, that is because we + // choose this to be so in the client. + let mut config = Config::new(root_path, params.capabilities); + if let Some(options) = params.initialization_options { + config.update(options); + } + + Ok(config) + } + + fn set_up_logger(&self) { + let sender = self.connection.sender.clone(); + let logger = LspLogger::new(sender, None); + self.logger.register_logger(LOGGER_NAME, Box::new(logger)); + } +} + +pub fn setup_server(config: Config, connection: Connection, logger: Logger) -> Result { + let vfs_loader = set_up_vfs_loader(); + let task_pool = set_up_task_pool(); + let cache_pool = set_up_single_thread_pool(); + let project_pool = set_up_single_thread_pool(); + let ai_completion = set_up_ai_completion(&config); + + log::debug!("initial state: {:#?}", config); + + Ok(Server::new( + connection, + vfs_loader, + task_pool, + project_pool, + cache_pool, + logger, + config, + ai_completion, + )) +} + +fn set_up_vfs_loader() -> VfsHandle { + let (sender, receiver) = crossbeam_channel::unbounded(); + let handle: vfs_notify::NotifyHandle = + loader::Handle::spawn(Box::new(move |msg| sender.send(msg).unwrap())); + let handle = Box::new(handle) as Box; + Handle { handle, receiver } +} + +fn set_up_task_pool() -> TaskHandle { + let (sender, receiver) = crossbeam_channel::unbounded(); + let handle = TaskPool::new(sender); + Handle { handle, receiver } +} + +fn set_up_single_thread_pool() -> TaskHandle { + let (sender, receiver) = crossbeam_channel::unbounded(); + let pool = ThreadPool::new(1); + let handle = TaskPool::new_with_pool(sender, pool); + Handle { handle, receiver } +} + +fn set_up_ai_completion(config: &Config) -> AiCompletion { + if config.ai_enabled() { + AiCompletion::disabled() // @oss-only + // @fb-only: AiCompletion::startup() + } else { + AiCompletion::disabled() + } +} + +fn root_path(params: &InitializeParams) -> Result { + match params + .root_uri + .as_ref() + .and_then(|uri| uri.to_file_path().ok()) + .and_then(|path| AbsPathBuf::try_from(path).ok()) + { + Some(path) => Ok(path), + None => { + let cwd = env::current_dir()?; + Ok(AbsPathBuf::assert(cwd)) + } + } +} diff --git a/crates/elp/src/snapshot.rs b/crates/elp/src/snapshot.rs new file mode 100644 index 0000000000..23c5895e13 --- /dev/null +++ b/crates/elp/src/snapshot.rs @@ -0,0 +1,274 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::fmt; +use std::sync::Arc; + +use anyhow::Context; +use anyhow::Result; +use elp_ai::AiCompletion; +use elp_ai::CompletionReceiver; +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::FilePosition; +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::EqwalizerDiagnostics; +use elp_ide::Analysis; +use elp_log::timeit_with_telemetry; +use elp_project_model::Project; +use fxhash::FxHashMap; +use itertools::Itertools; +use lsp_types::Diagnostic; +use lsp_types::SemanticTokens; +use lsp_types::Url; +use parking_lot::Mutex; +use parking_lot::RwLock; +use serde::Deserialize; +use serde::Serialize; + +use crate::config::Config; +use crate::convert; +use crate::line_endings::LineEndings; +use crate::server::file_id_to_path; +use crate::server::file_id_to_url; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum TelemetryData { + NativeDiagnostics { file_url: Url }, + EqwalizerDiagnostics { file_url: Url }, + ParseServerDiagnostics { file_url: Url }, + EdocDiagnostics { file_url: Url }, + Initialize, +} + +impl fmt::Display for TelemetryData { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + TelemetryData::NativeDiagnostics { file_url } => { + write!(f, "Native Diagnostics file_url: {}", file_url) + } + TelemetryData::EqwalizerDiagnostics { file_url } => { + write!(f, "Eqwalizer Diagnostics file_url: {}", file_url) + } + TelemetryData::ParseServerDiagnostics { file_url } => { + write!(f, "Parse Server Diagnostics file_url: {}", file_url) + } + TelemetryData::EdocDiagnostics { file_url } => { + write!(f, "EDoc Diagnostics file_url: {}", file_url) + } + TelemetryData::Initialize => { + write!(f, "Initialize") + } + } + } +} + +pub type SharedMap = Arc>>; + +/// An immutable snapshot of the world's state at a point in time. +pub struct Snapshot { + pub(crate) config: Arc, + // Note: Analysis is a salsa::Snapshot. According to the docs, + // any attempt to `set` an input will block. + pub(crate) analysis: Analysis, + pub(crate) semantic_tokens_cache: Arc>>, + vfs: Arc>, + open_document_versions: SharedMap, + line_ending_map: SharedMap, + pub(crate) projects: Arc>, + ai_completion: Arc>, +} + +impl Snapshot { + pub fn new( + config: Arc, + analysis: Analysis, + vfs: Arc>, + open_document_versions: Arc>>, + line_ending_map: Arc>>, + projects: Arc>, + ai_completion: Arc>, + ) -> Self { + Snapshot { + config, + analysis, + semantic_tokens_cache: Arc::new(Default::default()), + vfs, + open_document_versions, + line_ending_map, + projects, + ai_completion, + } + } + + pub(crate) fn url_to_file_id(&self, url: &Url) -> Result { + let path = convert::vfs_path(url)?; + let vfs = self.vfs.read(); + let res = vfs + .file_id(&path) + .context(format!("file not found: {}", path))?; + Ok(res) + } + + pub(crate) fn file_id_to_path(&self, id: FileId) -> Option { + file_id_to_path(&self.vfs.read(), id).ok() + } + + pub(crate) fn file_id_to_url(&self, id: FileId) -> Url { + file_id_to_url(&self.vfs.read(), id) + } + + pub(crate) fn url_file_version(&self, url: &Url) -> Option { + let path = convert::vfs_path(url).ok()?; + Some(*self.open_document_versions.read().get(&path)?) + } + + pub(crate) fn line_endings(&self, id: FileId) -> LineEndings { + self.line_ending_map.read()[&id] + } + + pub(crate) fn ai_completion(&self, position: FilePosition) -> Result { + let mut ai_completion = self.ai_completion.lock(); + let code = self.analysis.file_text(position.file_id)?; + let offset = u32::from(position.offset) as usize; + let prefix = &code[..offset]; + Ok(ai_completion.complete(prefix.to_string())) + } + + pub fn native_diagnostics(&self, file_id: FileId) -> Option> { + let file_url = self.file_id_to_url(file_id); + let _timer = timeit_with_telemetry!(TelemetryData::NativeDiagnostics { file_url }); + + let line_index = self.analysis.line_index(file_id).ok()?; + let url = file_id_to_url(&self.vfs.read(), file_id); + + Some( + self.analysis + .diagnostics(&self.config.diagnostics(), file_id, false) + .ok()? + .into_iter() + .map(|d| convert::ide_to_lsp_diagnostic(&line_index, &url, &d)) + .collect(), + ) + } + + pub fn eqwalizer_diagnostics(&self, file_id: FileId) -> Option> { + let file_url = self.file_id_to_url(file_id); + let _timer = timeit_with_telemetry!(TelemetryData::EqwalizerDiagnostics { file_url }); + + // Check, if the file is actually a module + let _ = self.analysis.module_name(file_id).ok()??; + + let project_id = self.analysis.project_id(file_id).ok()??; + + let eqwalizer_enabled = self.analysis.is_eqwalizer_enabled(file_id, false).ok()?; + if !eqwalizer_enabled { + return Some(vec![]); + } + + let line_index = self.analysis.line_index(file_id).ok()?; + + let diags = self + .analysis + .eqwalizer_diagnostics(project_id, vec![file_id]) + .ok()?; + match &*diags { + EqwalizerDiagnostics::Diagnostics(diags) => Some( + diags + .iter() + .flat_map(|(_, diags)| { + diags.iter().map(|d| { + convert::eqwalizer_to_lsp_diagnostic(d, &line_index, eqwalizer_enabled) + }) + }) + .collect(), + ), + EqwalizerDiagnostics::NoAst { .. } => Some(vec![]), + EqwalizerDiagnostics::Error(err) => { + log::error!("EqWAlizer failed for {:?}: {}", file_id, err); + return Some(vec![]); + } + } + } + + pub fn edoc_diagnostics(&self, file_id: FileId) -> Option)>> { + let file_url = self.file_id_to_url(file_id); + let _timer = timeit_with_telemetry!(TelemetryData::EdocDiagnostics { file_url }); + let url = file_id_to_url(&self.vfs.read(), file_id); + let line_index = self.analysis.line_index(file_id).ok()?; + + let diags = &*self.analysis.edoc_diagnostics(file_id).ok()?; + + Some( + diags + .into_iter() + .map(|(file_id, ds)| { + ( + *file_id, + ds.iter() + .map(|d| convert::ide_to_lsp_diagnostic(&line_index, &url, d)) + .collect(), + ) + }) + .collect(), + ) + } + + pub fn erlang_service_diagnostics( + &self, + file_id: FileId, + ) -> Option)>> { + let file_url = self.file_id_to_url(file_id); + let _timer = timeit_with_telemetry!(TelemetryData::ParseServerDiagnostics { file_url }); + let url = file_id_to_url(&self.vfs.read(), file_id); + let line_index = self.analysis.line_index(file_id).ok()?; + + let diags = &*self.analysis.erlang_service_diagnostics(file_id).ok()?; + + Some( + diags + .into_iter() + .map(|(file_id, ds)| { + ( + *file_id, + ds.iter() + .map(|d| convert::ide_to_lsp_diagnostic(&line_index, &url, d)) + .collect(), + ) + }) + .collect(), + ) + } + + pub fn get_project(&self, project_id: ProjectId) -> Option { + self.projects + .iter() + .enumerate() + .find_or_first(|(id, _project)| ProjectId(*id as u32) == project_id) + .map(|(_id, project)| project.clone()) + } + + pub fn set_up_projects(&self) { + for project in self.projects.as_ref() { + if let Err(err) = set_up_project(project) { + log::error!( + "Failed to set up project {} for parsing: {}", + project.name(), + err + ); + }; + } + } +} + +fn set_up_project(project: &Project) -> Result<()> { + project.compile_deps() +} diff --git a/crates/elp/src/task_pool.rs b/crates/elp/src/task_pool.rs new file mode 100644 index 0000000000..1ceda98e88 --- /dev/null +++ b/crates/elp/src/task_pool.rs @@ -0,0 +1,77 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! A thin wrapper around `ThreadPool` to make sure that we join all things +//! properly. + +// From https://github.com/rust-lang/rust-analyzer/blob/7435b9e98c9280043605748c11a1f450669e04d6/crates/rust-analyzer/src/thread_pool.rs +use crossbeam_channel::Sender; + +/// Thread stack size for server tasks, in bytes. +/// +/// Mostly needed for eqWAlizer AST conversion. +/// Due to inefficient encoding of lists, the default stack size of 2MiB may +/// not be enough for some generated modules. +const THREAD_STACK_SIZE: usize = 10_000_000; + +pub struct TaskPool { + sender: Sender, + inner: threadpool::ThreadPool, +} + +#[allow(unused)] +impl TaskPool { + pub fn new(sender: Sender) -> TaskPool { + TaskPool { + sender, + inner: threadpool::Builder::new() + .thread_stack_size(THREAD_STACK_SIZE) + .build(), + } + } + + pub fn new_with_pool(sender: Sender, pool: threadpool::ThreadPool) -> TaskPool { + TaskPool { + sender, + inner: pool, + } + } + + pub fn spawn(&mut self, task: F) + where + F: FnOnce() -> T + Send + 'static, + T: Send + 'static, + { + self.inner.execute({ + let sender = self.sender.clone(); + move || sender.send(task()).unwrap() + }) + } + + pub fn spawn_with_sender(&mut self, task: F) + where + F: FnOnce(Sender) + Send + 'static, + T: Send + 'static, + { + self.inner.execute({ + let sender = self.sender.clone(); + move || task(sender) + }) + } + + pub fn len(&self) -> usize { + self.inner.queued_count() + } +} + +impl Drop for TaskPool { + fn drop(&mut self) { + self.inner.join() + } +} diff --git a/crates/elp/src/to_proto.rs b/crates/elp/src/to_proto.rs new file mode 100644 index 0000000000..0364dfadce --- /dev/null +++ b/crates/elp/src/to_proto.rs @@ -0,0 +1,837 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! Conversion of rust-analyzer specific types to lsp_types equivalents. + +use std::sync::atomic::AtomicU32; +use std::sync::atomic::Ordering; + +use elp_ide::elp_ide_assists::Assist; +use elp_ide::elp_ide_assists::AssistKind; +use elp_ide::elp_ide_completion::Completion; +use elp_ide::elp_ide_completion::Contents; +use elp_ide::elp_ide_completion::Kind; +use elp_ide::elp_ide_db::assists::AssistUserInput; +use elp_ide::elp_ide_db::docs::Doc; +use elp_ide::elp_ide_db::elp_base_db::FileId; +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::rename::RenameError; +use elp_ide::elp_ide_db::source_change::SourceChange; +use elp_ide::elp_ide_db::LineIndex; +use elp_ide::elp_ide_db::ReferenceCategory; +use elp_ide::elp_ide_db::SymbolKind; +use elp_ide::AnnotationKind; +use elp_ide::Cancellable; +use elp_ide::Fold; +use elp_ide::FoldKind; +use elp_ide::Highlight; +use elp_ide::HlMod; +use elp_ide::HlRange; +use elp_ide::HlTag; +use elp_ide::InlayHintLabel; +use elp_ide::InlayHintLabelPart; +use elp_ide::InlayKind; +use elp_ide::NavigationTarget; +use elp_ide::Runnable; +use elp_ide::SignatureHelp; +use elp_ide::TextRange; +use elp_ide::TextSize; +use elp_project_model::ProjectBuildData; +use lsp_types::CompletionItemTag; +use lsp_types::Hover; +use lsp_types::HoverContents; +use lsp_types::MarkupContent; +use lsp_types::MarkupKind; +use text_edit::Indel; +use text_edit::TextEdit; + +use crate::line_endings::LineEndings; +use crate::lsp_ext; +use crate::lsp_ext::CompletionData; +use crate::semantic_tokens; +use crate::snapshot::Snapshot; +use crate::LspError; +use crate::Result; + +pub(crate) fn position(line_index: &LineIndex, offset: TextSize) -> lsp_types::Position { + let line_col = line_index.line_col(offset); + lsp_types::Position::new(line_col.line, line_col.col_utf16) +} + +pub(crate) fn range(line_index: &LineIndex, range: TextRange) -> lsp_types::Range { + let start = position(line_index, range.start()); + let end = position(line_index, range.end()); + lsp_types::Range::new(start, end) +} + +pub(crate) fn symbol_kind(symbol_kind: SymbolKind) -> lsp_types::SymbolKind { + match symbol_kind { + SymbolKind::Function => lsp_types::SymbolKind::FUNCTION, + SymbolKind::Record => lsp_types::SymbolKind::STRUCT, + SymbolKind::Type => lsp_types::SymbolKind::TYPE_PARAMETER, + SymbolKind::Define => lsp_types::SymbolKind::CONSTANT, + SymbolKind::File => lsp_types::SymbolKind::FILE, + SymbolKind::Module => lsp_types::SymbolKind::MODULE, + SymbolKind::RecordField => lsp_types::SymbolKind::STRUCT, + SymbolKind::Variable => lsp_types::SymbolKind::VARIABLE, + SymbolKind::Callback => lsp_types::SymbolKind::FUNCTION, + } +} + +pub(crate) fn text_edit( + line_index: &LineIndex, + line_endings: LineEndings, + indel: Indel, +) -> lsp_types::TextEdit { + let range = range(line_index, indel.delete); + let new_text = line_endings.revert(indel.insert); + lsp_types::TextEdit { range, new_text } +} + +pub(crate) fn url(snap: &Snapshot, file_id: FileId) -> lsp_types::Url { + snap.file_id_to_url(file_id) +} + +pub(crate) fn optional_versioned_text_document_identifier( + snap: &Snapshot, + file_id: FileId, +) -> lsp_types::OptionalVersionedTextDocumentIdentifier { + let url = url(snap, file_id); + let version = snap.url_file_version(&url); + lsp_types::OptionalVersionedTextDocumentIdentifier { uri: url, version } +} + +pub(crate) fn text_document_edit( + snap: &Snapshot, + file_id: FileId, + edit: TextEdit, +) -> Result { + let text_document = optional_versioned_text_document_identifier(snap, file_id); + let line_index = snap.analysis.line_index(file_id)?; + let line_endings = snap.line_endings(file_id); + let edits: Vec> = edit + .into_iter() + .map(|it| lsp_types::OneOf::Left(text_edit(&line_index, line_endings, it))) + .collect(); + + // if snap.analysis.is_library_file(file_id)? && snap.config.change_annotation_support() { + // for edit in &mut edits { + // edit.annotation_id = Some(outside_workspace_annotation_id()) + // } + // } + Ok(lsp_types::TextDocumentEdit { + text_document, + edits, + }) +} + +pub(crate) fn workspace_edit( + snap: &Snapshot, + source_change: SourceChange, +) -> Result { + let mut edits: Vec<_> = vec![]; + for (file_id, edit) in source_change.source_file_edits { + // let edit = snippet_text_document_edit(snap, source_change.is_snippet, file_id, edit)?; + let edit = text_document_edit(snap, file_id, edit)?; + edits.push(lsp_types::TextDocumentEdit { + text_document: edit.text_document, + edits: edit.edits.into_iter().map(From::from).collect(), + }); + } + let document_changes = lsp_types::DocumentChanges::Edits(edits); + let workspace_edit = lsp_types::WorkspaceEdit { + changes: None, + document_changes: Some(document_changes), + change_annotations: None, + }; + Ok(workspace_edit) +} + +pub(crate) fn code_action_kind(kind: AssistKind) -> lsp_types::CodeActionKind { + match kind { + AssistKind::None | AssistKind::Generate => lsp_types::CodeActionKind::EMPTY, + AssistKind::QuickFix => lsp_types::CodeActionKind::QUICKFIX, + AssistKind::Refactor => lsp_types::CodeActionKind::REFACTOR, + AssistKind::RefactorExtract => lsp_types::CodeActionKind::REFACTOR_EXTRACT, + AssistKind::RefactorInline => lsp_types::CodeActionKind::REFACTOR_INLINE, + AssistKind::RefactorRewrite => lsp_types::CodeActionKind::REFACTOR_REWRITE, + } +} + +pub(crate) fn code_action( + snap: &Snapshot, + assist: Assist, + resolve_data: Option<(usize, lsp_types::CodeActionParams, Option)>, +) -> Result { + let mut res = lsp_types::CodeAction { + title: assist.label.to_string(), + // group: assist + // .group + // .filter(|_| snap.config.code_action_group()) + // .map(|gr| gr.0), + kind: Some(code_action_kind(assist.id.1)), + edit: None, + is_preferred: None, + data: None, + diagnostics: None, + command: None, + disabled: None, + }; + match (assist.source_change, resolve_data) { + (Some(it), _) => res.edit = Some(workspace_edit(snap, it)?), + (None, Some((index, code_action_params, user_input))) => { + let data = lsp_ext::CodeActionData { + id: format!("{}:{}:{}", assist.id.0, assist.id.1.name(), index), + code_action_params, + user_input, + }; + res.data = Some(serde_json::value::to_value(data)?); + } + (None, None) => { + stdx::never!("assist should always be resolved if client can't do lazy resolving") + } + }; + Ok(lsp_types::CodeActionOrCommand::CodeAction(res)) +} + +pub(crate) fn location(snap: &Snapshot, file_range: FileRange) -> Cancellable { + let url = url(snap, file_range.file_id); + let line_index = snap.analysis.line_index(file_range.file_id)?; + let range = range(&line_index, file_range.range); + let loc = lsp_types::Location::new(url, range); + Ok(loc) +} + +/// Prefer using `location_link`, if the client has the cap. +pub(crate) fn location_from_nav( + snap: &Snapshot, + nav: NavigationTarget, +) -> Cancellable { + location(snap, nav.file_range()) +} + +pub(crate) fn location_link( + snap: &Snapshot, + src: Option, + target: NavigationTarget, +) -> Result { + let origin_selection_range = match src { + Some(src) => { + let line_index = snap.analysis.line_index(src.file_id)?; + let range = range(&line_index, src.range); + Some(range) + } + None => None, + }; + let (target_uri, target_range, target_selection_range) = location_info(snap, target)?; + let res = lsp_types::LocationLink { + origin_selection_range, + target_uri, + target_range, + target_selection_range, + }; + Ok(res) +} + +fn location_info( + snap: &Snapshot, + target: NavigationTarget, +) -> Result<(lsp_types::Url, lsp_types::Range, lsp_types::Range)> { + let line_index = snap.analysis.line_index(target.file_id)?; + + let target_uri = url(snap, target.file_id); + let target_range = range(&line_index, target.full_range); + let target_selection_range = target + .focus_range + .map(|it| range(&line_index, it)) + .unwrap_or(target_range); + Ok((target_uri, target_range, target_selection_range)) +} + +pub(crate) fn goto_definition_response( + snap: &Snapshot, + src: Option, + targets: Vec, +) -> Result { + if snap.config.location_link() { + let links = targets + .into_iter() + .map(|nav| location_link(snap, src, nav)) + .collect::>>()?; + Ok(links.into()) + } else { + let locations = targets + .into_iter() + .map(|nav| location_from_nav(snap, nav)) + .collect::>>()?; + Ok(locations.into()) + } +} + +pub(crate) fn hover_response( + snap: &Snapshot, + maybe_doc: Option<(Doc, FileRange)>, +) -> Result> { + let (markup, id_range) = match maybe_doc { + Some((doc, src_range)) => (doc.markdown_text().to_string(), Some(src_range)), + None => return Result::Ok(None), + }; + let markup_kind = MarkupKind::Markdown; + let hover_contents = HoverContents::Markup(MarkupContent { + kind: markup_kind, + value: markup, + }); + let hover_selection_range = match id_range { + Some(fr) => { + let line_index = snap.analysis.line_index(fr.file_id)?; + Some(range(&line_index, fr.range)) + } + None => None, + }; + Result::Ok(Some(Hover { + contents: hover_contents, + range: hover_selection_range, + })) +} + +pub(crate) fn rename_error(err: RenameError) -> crate::LspError { + // This is wrong, but we don't have a better alternative I suppose? + // https://github.com/microsoft/language-server-protocol/issues/1341 + + // Update when // https://github.com/rust-lang/rust-analyzer/pull/13280 + // lands and a new crate is published. T132682932 + invalid_params_error(err.to_string()) +} + +pub(crate) fn invalid_params_error(message: String) -> LspError { + LspError { + code: lsp_server::ErrorCode::InvalidParams as i32, + message, + } +} + +pub fn completion_response( + snap: Snapshot, + completions: Vec, +) -> lsp_types::CompletionResponse { + let items = completions + .into_iter() + .map(|it| completion_item(&snap, it)) + .collect(); + lsp_types::CompletionResponse::Array(items) +} + +fn completion_item(snap: &Snapshot, c: Completion) -> lsp_types::CompletionItem { + use lsp_types::CompletionItemKind as K; + use Kind::*; + + // Trigger Signature Help after completion for functions + let command = if c.kind == Function { + Some(command::trigger_parameter_hints()) + } else { + None + }; + let mut tags = Vec::new(); + if c.deprecated { + tags.push(CompletionItemTag::DEPRECATED); + }; + lsp_types::CompletionItem { + label: c.label, + kind: Some(match c.kind { + Attribute => K::KEYWORD, + Behavior => K::INTERFACE, + Function => K::FUNCTION, + Keyword => K::KEYWORD, + Macro => K::CONSTANT, + Module => K::MODULE, + Operator => K::OPERATOR, + RecordField => K::FIELD, + Record => K::STRUCT, + Type => K::INTERFACE, + Variable => K::VARIABLE, + AiAssist => K::EVENT, + }), + detail: None, + documentation: None, + deprecated: Some(c.deprecated), + preselect: None, + insert_text_format: match c.contents { + Contents::SameAsLabel | Contents::String(_) => { + Some(lsp_types::InsertTextFormat::PLAIN_TEXT) + } + Contents::Snippet(_) => Some(lsp_types::InsertTextFormat::SNIPPET), + }, + insert_text_mode: None, + text_edit: None, + additional_text_edits: None, + commit_characters: None, + data: match completion_item_data(snap, c.position) { + Some(data) => match serde_json::value::to_value(data) { + Ok(data) => Some(data), + Err(_) => None, + }, + None => None, + }, + sort_text: c.sort_text, + filter_text: None, + insert_text: match c.contents { + Contents::Snippet(snippet) => Some(snippet), + Contents::String(string) => Some(string), + Contents::SameAsLabel => None, + }, + command, + tags: if tags.len() > 0 { Some(tags) } else { None }, + label_details: None, + } +} + +fn completion_item_data(snap: &Snapshot, pos: Option) -> Option { + let file_id = pos?.file_id; + if let Ok(line_index) = snap.analysis.line_index(file_id) { + let uri = url(snap, file_id); + let text_document = lsp_types::TextDocumentIdentifier { uri }; + let pos = position(&line_index, pos?.offset); + let doc_pos = lsp_types::TextDocumentPositionParams::new(text_document, pos); + Some(lsp_ext::CompletionData { position: doc_pos }) + } else { + None + } +} + +pub(crate) fn folding_range(line_index: &LineIndex, fold: Fold) -> lsp_types::FoldingRange { + let kind = match fold.kind { + FoldKind::Function | FoldKind::Record => Some(lsp_types::FoldingRangeKind::Region), + }; + + let range = range(line_index, fold.range); + + lsp_types::FoldingRange { + start_line: range.start.line, + start_character: Some(range.start.character), + end_line: range.end.line, + end_character: Some(range.end.character), + kind, + } +} + +// --------------------------------------------------------------------- + +pub(crate) fn call_hierarchy_item( + snap: &Snapshot, + target: NavigationTarget, +) -> Result { + let name = target.name.to_string(); + let kind = lsp_types::SymbolKind::FUNCTION; + let (uri, range, selection_range) = location_info(snap, target)?; + Ok(lsp_types::CallHierarchyItem { + name, + kind, + tags: None, + detail: None, + uri, + range, + selection_range, + data: None, + }) +} + +pub(crate) fn signature_help( + calls_info: Vec, + active_parameter: usize, +) -> lsp_types::SignatureHelp { + let mut signatures = Vec::new(); + for call_info in calls_info { + signatures.push(signature_information(call_info)); + } + let active_signature = signatures + .iter() + .take_while(|sig| match &sig.parameters { + Some(parameters) => parameters.len() <= active_parameter, + None => false, + }) + .count(); + lsp_types::SignatureHelp { + signatures, + active_signature: Some(active_signature as u32), + active_parameter: None, + } +} + +pub(crate) fn signature_information(call_info: SignatureHelp) -> lsp_types::SignatureInformation { + let label = call_info.signature.clone(); + let parameters = call_info + .parameter_labels() + .map(|label| lsp_types::ParameterInformation { + label: lsp_types::ParameterLabel::Simple(label.to_string()), + documentation: match call_info.parameters_doc.get(label) { + Some(doc) => Some(lsp_types::Documentation::MarkupContent( + lsp_types::MarkupContent { + kind: lsp_types::MarkupKind::Markdown, + value: format!("`{}`: {}", label, doc.clone()), + }, + )), + None => None, + }, + }) + .collect::>(); + + let documentation = call_info.function_doc.map(|doc| { + lsp_types::Documentation::MarkupContent(lsp_types::MarkupContent { + kind: lsp_types::MarkupKind::Markdown, + value: doc, + }) + }); + + let active_parameter = call_info.active_parameter.map(|it| it as u32); + + lsp_types::SignatureInformation { + label, + documentation, + parameters: Some(parameters), + active_parameter, + } +} + +// --------------------------------------------------------------------- + +static TOKEN_RESULT_COUNTER: AtomicU32 = AtomicU32::new(1); + +pub(crate) fn semantic_tokens( + text: &str, + line_index: &LineIndex, + highlights: Vec, +) -> lsp_types::SemanticTokens { + let id = TOKEN_RESULT_COUNTER + .fetch_add(1, Ordering::SeqCst) + .to_string(); + let mut builder = semantic_tokens::SemanticTokensBuilder::new(id); + + for highlight_range in highlights { + if highlight_range.highlight.is_empty() { + continue; + } + + let (ty, mods) = semantic_token_type_and_modifiers(highlight_range.highlight); + let token_index = semantic_tokens::type_index(ty); + let modifier_bitset = mods.0; + + for mut text_range in line_index.lines(highlight_range.range) { + if text[text_range].ends_with('\n') { + // Temporary for T148094436 + let _pctx = stdx::panic_context::enter(format!("\nto_proto::semantic_tokens")); + text_range = + TextRange::new(text_range.start(), text_range.end() - TextSize::of('\n')); + } + let range = range(line_index, text_range); + builder.push(range, token_index, modifier_bitset); + } + } + + builder.build() +} + +pub(crate) fn semantic_token_delta( + previous: &lsp_types::SemanticTokens, + current: &lsp_types::SemanticTokens, +) -> lsp_types::SemanticTokensDelta { + let result_id = current.result_id.clone(); + let edits = semantic_tokens::diff_tokens(&previous.data, ¤t.data); + lsp_types::SemanticTokensDelta { result_id, edits } +} + +fn semantic_token_type_and_modifiers( + highlight: Highlight, +) -> (lsp_types::SemanticTokenType, semantic_tokens::ModifierSet) { + let mut mods = semantic_tokens::ModifierSet::default(); + let type_ = match highlight.tag { + HlTag::Symbol(symbol) => match symbol { + SymbolKind::File => semantic_tokens::STRING, + SymbolKind::Module => semantic_tokens::NAMESPACE, + SymbolKind::Function => semantic_tokens::FUNCTION, + SymbolKind::Record => semantic_tokens::STRUCT, + SymbolKind::RecordField => semantic_tokens::STRUCT, + SymbolKind::Type => semantic_tokens::TYPE_PARAMETER, + SymbolKind::Define => semantic_tokens::MACRO, + SymbolKind::Variable => semantic_tokens::VARIABLE, + SymbolKind::Callback => semantic_tokens::FUNCTION, + }, + HlTag::None => semantic_tokens::GENERIC, + }; + + for modifier in highlight.mods.iter() { + let modifier = match modifier { + HlMod::Bound => semantic_tokens::BOUND, + HlMod::ExportedFunction => semantic_tokens::EXPORTED_FUNCTION, + HlMod::DeprecatedFunction => semantic_tokens::DEPRECATED_FUNCTION, + }; + mods |= modifier; + } + + (type_, mods) +} + +pub(crate) fn document_highlight_kind( + category: ReferenceCategory, +) -> Option { + match category { + ReferenceCategory::Read => Some(lsp_types::DocumentHighlightKind::READ), + ReferenceCategory::Write => Some(lsp_types::DocumentHighlightKind::WRITE), + } +} + +pub(crate) fn runnable( + snap: &Snapshot, + runnable: Runnable, + project_build_data: Option, +) -> Result { + let file_id = runnable.nav.file_id.clone(); + let file_path = snap.file_id_to_path(file_id); + match project_build_data { + Some(elp_project_model::ProjectBuildData::Buck(buck_project)) => match file_path { + None => Err("Could not extract file path".into()), + Some(file_path) => match buck_project + .target_info + .path_to_target_name + .get(&file_path) + .cloned() + { + Some(target) => { + let project_data = snap.analysis.project_data(file_id); + let workspace_root = match project_data { + Ok(Some(data)) => data.root_dir.clone(), + _ => snap.config.root_path.clone(), + }; + + let location = location_link(snap, None, runnable.clone().nav).ok(); + Ok(lsp_ext::Runnable { + label: "Buck2".to_string(), + location, + kind: lsp_ext::RunnableKind::Buck2, + args: lsp_ext::Buck2RunnableArgs { + workspace_root: workspace_root.into(), + command: "test".to_string(), + args: runnable.buck2_args(target.clone()), + target: target.to_string(), + id: runnable.id(), + }, + }) + } + None => Err("Could not find test target for file".into()), + }, + }, + _ => Err("Only Buck2 Projects Supported".into()), + } +} + +pub(crate) fn code_lens( + acc: &mut Vec, + snap: &Snapshot, + annotation: elp_ide::Annotation, + project_build_data: Option, +) -> Result<()> { + match annotation.kind { + AnnotationKind::Runnable(run) => { + let line_index = snap.analysis.line_index(run.nav.file_id)?; + let annotation_range = range(&line_index, annotation.range); + let run_title = &run.run_title(); + let debug_title = &run.debug_title(); + match runnable(snap, run, project_build_data) { + Ok(r) => { + let lens_config = snap.config.lens(); + if lens_config.run { + let run_command = command::run_single(&r, &run_title); + acc.push(lsp_types::CodeLens { + range: annotation_range, + command: Some(run_command), + data: None, + }); + } + if lens_config.debug { + let debug_command = command::debug_single(&r, &debug_title); + acc.push(lsp_types::CodeLens { + range: annotation_range, + command: Some(debug_command), + data: None, + }) + } + } + Err(e) => { + log::warn!("Error while extracting runnables {e}"); + () + } + }; + } + } + Ok(()) +} + +pub(crate) mod command { + use serde_json::to_value; + + use crate::lsp_ext; + + pub(crate) fn run_single(runnable: &lsp_ext::Runnable, title: &str) -> lsp_types::Command { + lsp_types::Command { + title: title.to_string(), + command: "elp.runSingle".into(), + arguments: Some(vec![to_value(runnable).unwrap()]), + } + } + + pub(crate) fn debug_single(runnable: &lsp_ext::Runnable, title: &str) -> lsp_types::Command { + lsp_types::Command { + title: title.to_string(), + command: "elp.debugSingle".into(), + arguments: Some(vec![to_value(runnable).unwrap()]), + } + } + + pub(crate) fn trigger_parameter_hints() -> lsp_types::Command { + lsp_types::Command { + title: "triggerParameterHints".into(), + command: "editor.action.triggerParameterHints".into(), + arguments: None, + } + } +} + +pub(crate) fn inlay_hint( + snap: &Snapshot, + line_index: &LineIndex, + mut inlay_hint: elp_ide::InlayHint, +) -> Cancellable { + match inlay_hint.kind { + InlayKind::Parameter => inlay_hint.label.append_str(":"), + } + + let (label, tooltip) = inlay_hint_label(snap, inlay_hint.label)?; + + Ok(lsp_types::InlayHint { + position: match inlay_hint.kind { + // before annotated thing + InlayKind::Parameter => position(line_index, inlay_hint.range.start()), + // after annotated thing + // _ => position(line_index, inlay_hint.range.end()), + }, + padding_left: Some(match inlay_hint.kind { + InlayKind::Parameter => false, + }), + padding_right: Some(match inlay_hint.kind { + InlayKind::Parameter => true, + }), + kind: match inlay_hint.kind { + InlayKind::Parameter => Some(lsp_types::InlayHintKind::PARAMETER), + }, + text_edits: None, + data: None, + tooltip, + label, + }) +} + +fn inlay_hint_label( + snap: &Snapshot, + mut label: InlayHintLabel, +) -> Cancellable<( + lsp_types::InlayHintLabel, + Option, +)> { + let res = match &*label.parts { + [ + InlayHintLabelPart { + linked_location: None, + .. + }, + ] => { + let InlayHintLabelPart { text, tooltip, .. } = label.parts.pop().unwrap(); + ( + lsp_types::InlayHintLabel::String(text), + match tooltip { + Some(elp_ide::InlayTooltip::String(s)) => { + Some(lsp_types::InlayHintTooltip::String(s)) + } + Some(elp_ide::InlayTooltip::Markdown(s)) => Some( + lsp_types::InlayHintTooltip::MarkupContent(lsp_types::MarkupContent { + kind: lsp_types::MarkupKind::Markdown, + value: s, + }), + ), + None => None, + }, + ) + } + _ => { + let parts = label + .parts + .into_iter() + .map(|part| { + part.linked_location + .map(|range| location(snap, range)) + .transpose() + .map(|location| lsp_types::InlayHintLabelPart { + value: part.text, + tooltip: match part.tooltip { + Some(elp_ide::InlayTooltip::String(s)) => { + Some(lsp_types::InlayHintLabelPartTooltip::String(s)) + } + Some(elp_ide::InlayTooltip::Markdown(s)) => { + Some(lsp_types::InlayHintLabelPartTooltip::MarkupContent( + lsp_types::MarkupContent { + kind: lsp_types::MarkupKind::Markdown, + value: s, + }, + )) + } + None => None, + }, + location, + command: None, + }) + }) + .collect::>()?; + (lsp_types::InlayHintLabel::LabelParts(parts), None) + } + }; + Ok(res) +} + +#[allow(deprecated)] +pub(crate) fn document_symbol( + line_index: &LineIndex, + symbol: &elp_ide::DocumentSymbol, +) -> lsp_types::DocumentSymbol { + let mut tags = Vec::new(); + if symbol.deprecated { + tags.push(lsp_types::SymbolTag::DEPRECATED) + }; + let selection_range = range(line_index, symbol.selection_range); + let range = range(line_index, symbol.range); + let children = match &symbol.children { + None => None, + Some(children) => Some( + children + .into_iter() + .map(|c| document_symbol(line_index, c)) + .collect(), + ), + }; + lsp_types::DocumentSymbol { + name: symbol.name.clone(), + detail: symbol.detail.clone(), + kind: symbol_kind(symbol.kind), + tags: Some(tags), + deprecated: Some(false), + range, + selection_range, + children, + } +} + +// --------------------------------------------------------------------- diff --git a/crates/elp/tests/slow-tests/buck_tests.rs b/crates/elp/tests/slow-tests/buck_tests.rs new file mode 100644 index 0000000000..fe1836f5bb --- /dev/null +++ b/crates/elp/tests/slow-tests/buck_tests.rs @@ -0,0 +1,303 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +#[cfg(test)] +mod tests { + use std::fs; + use std::path::PathBuf; + + use elp::build::load; + use elp::cli::Fake; + 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::IncludeOtp; + use elp_ide::erlang_service::Format; + use elp_project_model::AppType; + use elp_project_model::DiscoverConfig; + use elp_project_model::Project; + use elp_project_model::ProjectAppData; + use elp_project_model::ProjectBuildData; + use elp_project_model::ProjectManifest; + use itertools::Itertools; + + #[test] + #[ignore] + fn test_success_case() { + let path_str = "../../test_projects/buck_tests"; + let path: PathBuf = path_str.clone().into(); + let cli = Fake::default(); + + let conf = DiscoverConfig::buck(); + let result = load::load_project_at(&cli, &path, conf, IncludeOtp::No).expect(&format!( + "Can't load project from path {}", + &path.to_string_lossy() + )); + let analisys = &result.analysis(); + let modules = vec![ + ("test_elp", true), + ("test_elp_SUITE", false), + ("test_elp_direct_dep", true), + ("test_elp_transitive_dep", true), + ]; + let project_id = result.project_id; + for (module, eqwalizer_enabled) in modules { + let file_id = analisys.module_file_id(project_id, module).unwrap(); + let file_id = file_id.expect(&format!("Can't find file id for {module}")); + analisys + .file_text(file_id) + .expect(&format!("No file text for {module}")); + let prj_id = analisys.project_id(file_id).unwrap(); + let prj_id = prj_id.expect(&format!("Can't find project id for {module}")); + assert_eq!(prj_id, project_id); + let ast = analisys.module_ast(file_id, Format::OffsetEtf).unwrap(); + assert_eq!(ast.errors, vec![]); + let eq_enabled = analisys + .is_eqwalizer_enabled(file_id, false) + .expect(&format!( + "Failed to check if eqwalizer enabled for {module}" + )); + assert_eq!(eq_enabled, eqwalizer_enabled); + let project_data = analisys.project_data(file_id).unwrap(); + project_data.expect(&format!("Can't find project data for {module}")); + } + } + + #[test] + #[ignore] + fn test_load_buck_targets() { + let path_str = "../../test_projects/buck_tests"; + let path: PathBuf = path_str.clone().into(); + + let buck_config = ProjectManifest::discover_single( + AbsPathBuf::assert(path).as_path(), + &DiscoverConfig::buck(), + ) + .unwrap(); + + let project = Project::load(buck_config).unwrap(); + + let project = match project.project_build_data { + ProjectBuildData::Buck(project) => project, + _ => panic!("not reachable"), + }; + + let project_data: Vec = project + .project_app_data + .into_iter() + .filter(|app| app.app_type == AppType::App) + .filter(|app| { + !app.dir + .as_os_str() + .to_string_lossy() + .contains("third-party") + }) + .sorted_by_key(|data| data.name.0.clone()) + .collect(); + + let test_data: Vec = get_test_data() + .into_iter() + .sorted_by_key(|data| data.name.clone()) + .collect(); + + //this assert checks absence of test_elp_suite_data app + assert_eq!(&project_data.len(), &test_data.len()); + + for (proj, test) in project_data.iter().zip(test_data.iter()) { + assert_project_app_data(proj, test); + } + } + + //this data represents ~/local/whatsapp/server/tools/elp/_build/buck_tests. Structure: + //following 4 targets has buck inside + //test_elp: [BUCK, src, test[tests, test_elp_SUITE_data], include] + // has direct dep to [test_elp_direct_dep, test_elp_no_private_headers, test_elp_no_public_headers, + // test_elp_flat_outside_target, test_elp_flat_inside_target] and transitive to test_elp_transitive_dep + //test_elp_direct_dep: [BUCK, src, test, include] + // depends on test_elp_transitive_dep and on test_elp making cycle + //test_elp_transitive_dep: [BUCK, src, test, include] + //test_elp_flat_inside_target: [BUCK, *.erl, *.hrl] + // this app has flat structure (without src, include) and BUCK file inside app root + //BUCK + // following 3 apps has target definition + //test_elp_no_private_headers: [src, include] + // app without hrl file in src dir + //test_elp_no_public_headers: [src] + // app without include dir + //test_elp_flat_outside_target: [*.erl, *.hrl] + // flat structure app with + fn get_test_data() -> Vec> { + let test_elp = ProjectAppTestData { + name: "test_elp", + dir: "server/tools/elp/_build/buck_tests/test_elp", + abs_src_dirs: vec!["server/tools/elp/_build/buck_tests/test_elp/src"], + extra_src_dirs: vec!["test"], + include_dirs: vec!["server/tools/elp/_build/buck_tests/test_elp/include"], + include_path: vec![ + //self include + "server/tools/elp/_build/buck_tests/test_elp/include", + //root dir of test_elp + "server/tools/elp/_build/buck_tests", + ], + include_path_must_be_absent: vec![ + "server/tools/elp/_build/buck_tests/test_elp_no_private_headers/src", + "server/tools/elp/_build/buck_tests/test_elp_no_public_headers/include", + "server/tools/elp/_build/buck_tests/test_elp_no_public_headers/src", + ], + }; + let test_elp_direct_dep = ProjectAppTestData { + name: "test_elp_direct_dep", + dir: "server/tools/elp/_build/buck_tests/test_elp_direct_dep", + abs_src_dirs: vec!["server/tools/elp/_build/buck_tests/test_elp_direct_dep/src"], + extra_src_dirs: vec![], + include_dirs: vec!["server/tools/elp/_build/buck_tests/test_elp_direct_dep/include"], + include_path: vec![ + //self deps + "server/tools/elp/_build/buck_tests/test_elp_direct_dep/src", + "server/tools/elp/_build/buck_tests/test_elp_direct_dep/include", + //root dir + "server/tools/elp/_build/buck_tests", + ], + include_path_must_be_absent: vec![ + //src should be empty because we don't have tests in this module + "server/tools/elp/_build/buck_tests/test_elp_transitive_dep/src", + ], + }; + let test_elp_transitive_dep = ProjectAppTestData { + name: "test_elp_transitive_dep", + dir: "server/tools/elp/_build/buck_tests/test_elp_transitive_dep", + abs_src_dirs: vec!["server/tools/elp/_build/buck_tests/test_elp_transitive_dep/src"], + extra_src_dirs: vec![], + include_dirs: vec![ + "server/tools/elp/_build/buck_tests/test_elp_transitive_dep/include", + ], + include_path: vec![ + //self + "server/tools/elp/_build/buck_tests/test_elp_transitive_dep/src", + "server/tools/elp/_build/buck_tests/test_elp_transitive_dep/include", + //root + "server/tools/elp/_build/buck_tests", + ], + include_path_must_be_absent: vec![], + }; + let test_elp_no_public_headers = ProjectAppTestData { + name: "test_elp_no_public_headers", + dir: "server/tools/elp/_build/buck_tests/test_elp_no_public_headers", + abs_src_dirs: vec!["server/tools/elp/_build/buck_tests/test_elp_no_public_headers/src"], + extra_src_dirs: vec![], + include_dirs: vec![], + include_path: vec!["server/tools/elp/_build/buck_tests"], + include_path_must_be_absent: vec![ + "server/tools/elp/_build/buck_tests/test_elp_no_public_headers/include", + ], + }; + let test_elp_flat_inside_target = ProjectAppTestData { + name: "test_elp_flat_inside_target", + dir: "server/tools/elp/_build/buck_tests/test_elp_flat_inside_target", + abs_src_dirs: vec![""], + extra_src_dirs: vec![], + include_dirs: vec!["server/tools/elp/_build/buck_tests/test_elp_flat_inside_target"], + include_path: vec![ + //self + "server/tools/elp/_build/buck_tests/test_elp_flat_inside_target", + //root + "server/tools/elp/_build/buck_tests", + ], + include_path_must_be_absent: vec![], + }; + let test_elp_flat_outside_target = ProjectAppTestData { + name: "test_elp_flat_outside_target", + dir: "server/tools/elp/_build/buck_tests/test_elp_flat_outside_target", + abs_src_dirs: vec![""], + extra_src_dirs: vec![], + include_dirs: vec!["server/tools/elp/_build/buck_tests/test_elp_flat_outside_target"], + include_path: vec![ + //self + "server/tools/elp/_build/buck_tests/test_elp_flat_outside_target", + //root + "server/tools/elp/_build/buck_tests", + ], + include_path_must_be_absent: vec![], + }; + let test_elp_no_private_headers = ProjectAppTestData { + name: "test_elp_no_private_headers", + dir: "server/tools/elp/_build/buck_tests/test_elp_no_private_headers", + abs_src_dirs: vec![ + "server/tools/elp/_build/buck_tests/test_elp_no_private_headers/src", + ], + extra_src_dirs: vec![], + include_dirs: vec![ + "server/tools/elp/_build/buck_tests/test_elp_no_private_headers/include", + ], + include_path: vec![ + //self + "server/tools/elp/_build/buck_tests/test_elp_no_private_headers/include", + //root + "server/tools/elp/_build/buck_tests", + ], + include_path_must_be_absent: vec![], + }; + vec![ + test_elp, + test_elp_direct_dep, + test_elp_transitive_dep, + test_elp_no_public_headers, + test_elp_flat_inside_target, + test_elp_flat_outside_target, + test_elp_no_private_headers, + ] + } + + fn assert_project_app_data(app: &ProjectAppData, test: &ProjectAppTestData) { + assert_eq!(&app.name.0, test.name); + assert!( + ends_with(&app.dir, test.dir), + "dir: {:?}, expected: {:?}", + &app.dir, + &test.dir + ); + for src in &test.abs_src_dirs { + assert!(app.abs_src_dirs.iter().any(|path| ends_with(&path, src))); + } + assert_eq!(app.extra_src_dirs, test.extra_src_dirs); + for include in &test.include_dirs { + assert!(app.include_dirs.iter().any(|path| ends_with(path, include))); + } + for include_path in &test.include_path { + assert!( + app.include_path + .iter() + .any(|path| ends_with(path, include_path)) + ); + } + + for include_path in &test.include_path_must_be_absent { + assert!( + !app.include_path + .iter() + .any(|path| ends_with(path, include_path)) + ); + } + } + + fn ends_with(path: &AbsPath, s: &str) -> bool { + let path = fs::canonicalize(path).unwrap(); + let path = path.as_os_str().to_string_lossy(); + path.ends_with(s) + } + + struct ProjectAppTestData<'a> { + name: &'a str, + dir: &'a str, + abs_src_dirs: Vec<&'a str>, + extra_src_dirs: Vec<&'a str>, + include_dirs: Vec<&'a str>, + include_path: Vec<&'a str>, + include_path_must_be_absent: Vec<&'a str>, + } +} diff --git a/crates/elp/tests/slow-tests/main.rs b/crates/elp/tests/slow-tests/main.rs new file mode 100644 index 0000000000..63eef2afec --- /dev/null +++ b/crates/elp/tests/slow-tests/main.rs @@ -0,0 +1,378 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! The most high-level integrated tests for elp. +//! Based on rust-analyzer at 2021-02-08 revision. +//! +//! This tests run a full LSP event loop, spawn cargo and process stdlib from +//! sysroot. For this reason, the tests here are very slow, and should be +//! avoided unless absolutely necessary. +//! +//! In particular, it's fine *not* to test that client & server agree on +//! specific JSON shapes here -- there's little value in such tests, as we can't +//! be sure without a real client anyway. + +#[cfg(test)] +mod buck_tests; +mod support; + +use std::path::Path; + +use elp_ide::elp_ide_db::elp_base_db::AbsPathBuf; +use expect_test::expect; +use lsp_types::Position; +use lsp_types::Range; + +use crate::support::code_action_project; +use crate::support::diagnostic_project; + +const PROFILE: &str = ""; + +#[test] +fn test_run_mock_lsp() { + let workspace_root = + AbsPathBuf::assert(Path::new(env!("CARGO_WORKSPACE_DIR")).join("test_projects/end_to_end")); + + // Sanity check + assert!(std::fs::metadata(&workspace_root).is_ok()); + + // This is an end to end test, mocking the a client, + // requesting all quick fixes available ("assist"). + code_action_project( + &workspace_root, + r#"assist_examples/src/head_mismatch.erl"#, + Range::new(Position::new(3, 0), Position::new(5, 10)), + expect![[r#" + [ + { + "edit": { + "documentChanges": [ + { + "edits": [ + { + "newText": "bar", + "range": { + "end": { + "character": 0, + "line": 5 + }, + "start": { + "character": 0, + "line": 5 + } + } + }, + { + "newText": "", + "range": { + "end": { + "character": 3, + "line": 5 + }, + "start": { + "character": 0, + "line": 5 + } + } + } + ], + "textDocument": { + "uri": "file:///[..]/test_projects/end_to_end/assist_examples/src/head_mismatch.erl", + "version": 0 + } + } + ] + }, + "kind": "quickfix", + "title": "Fix head mismatch" + }, + { + "edit": { + "documentChanges": [ + { + "edits": [ + { + "newText": "%% @doc {@link https://www.erlang.org/doc/apps/edoc/chapter.html EDoc Manual}\n%% @param Arg1 Argument description\n%% @returns Return description\n", + "range": { + "end": { + "character": 0, + "line": 3 + }, + "start": { + "character": 0, + "line": 3 + } + } + } + ], + "textDocument": { + "uri": "file:///[..]/test_projects/end_to_end/assist_examples/src/head_mismatch.erl", + "version": 0 + } + } + ] + }, + "kind": "", + "title": "Add edoc comment" + }, + { + "edit": { + "documentChanges": [ + { + "edits": [ + { + "newText": "-spec bar(type1()) -> return_type().\n", + "range": { + "end": { + "character": 0, + "line": 3 + }, + "start": { + "character": 0, + "line": 3 + } + } + } + ], + "textDocument": { + "uri": "file:///[..]/test_projects/end_to_end/assist_examples/src/head_mismatch.erl", + "version": 0 + } + } + ] + }, + "kind": "", + "title": "Add spec stub" + }, + { + "edit": { + "documentChanges": [ + { + "edits": [ + { + "newText": "fun_name()", + "range": { + "end": { + "character": 11, + "line": 4 + }, + "start": { + "character": 10, + "line": 3 + } + } + }, + { + "newText": "\n\nfun_name() ->\n 1;\n bar(1) -> 2.", + "range": { + "end": { + "character": 12, + "line": 5 + }, + "start": { + "character": 12, + "line": 5 + } + } + } + ], + "textDocument": { + "uri": "file:///[..]/test_projects/end_to_end/assist_examples/src/head_mismatch.erl", + "version": 0 + } + } + ] + }, + "kind": "refactor.extract", + "title": "Extract into function" + }, + { + "edit": { + "documentChanges": [ + { + "edits": [ + { + "newText": "\n-export([bar/1]).\n", + "range": { + "end": { + "character": 0, + "line": 1 + }, + "start": { + "character": 0, + "line": 1 + } + } + } + ], + "textDocument": { + "uri": "file:///[..]/test_projects/end_to_end/assist_examples/src/head_mismatch.erl", + "version": 0 + } + } + ] + }, + "kind": "quickfix", + "title": "Export the function `bar/1`" + } + ]"#]], + ); +} + +#[test] +fn test_e2e_eqwalizer_module() { + let workspace_root = + AbsPathBuf::assert(Path::new(env!("CARGO_WORKSPACE_DIR")).join("test_projects/standard")); + + // Sanity check + assert!(std::fs::metadata(&workspace_root).is_ok()); + + diagnostic_project( + &workspace_root, + r"app_a/src/app_a.erl", + expect![[r#" + { + "diagnostics": [ + { + "code": "eqwalizer", + "message": "`'error'`.\nExpression has type: 'error'\nContext expected type: 'ok'\n See https://fb.me/eqwalizer_errors#incompatible_types", + "range": { + "end": { + "character": 7, + "line": 8 + }, + "start": { + "character": 4, + "line": 8 + } + }, + "severity": 1, + "source": "elp" + }, + { + "code": "eqwalizer", + "message": "`'error'`.\nExpression has type: 'error'\nContext expected type: 'ok'\n See https://fb.me/eqwalizer_errors#incompatible_types", + "range": { + "end": { + "character": 9, + "line": 12 + }, + "start": { + "character": 4, + "line": 12 + } + }, + "severity": 1, + "source": "elp" + }, + { + "code": "eqwalizer", + "message": "`'an_atom'`.\nExpression has type: 'an_atom'\nContext expected type: number()\n See https://fb.me/eqwalizer_errors#incompatible_types", + "range": { + "end": { + "character": 19, + "line": 16 + }, + "start": { + "character": 12, + "line": 16 + } + }, + "severity": 1, + "source": "elp" + }, + { + "code": "eqwalizer", + "message": "redundant fixme\n See https://fb.me/eqwalizer_errors#redundant_fixme", + "range": { + "end": { + "character": 21, + "line": 54 + }, + "start": { + "character": 4, + "line": 54 + } + }, + "severity": 1, + "source": "elp" + }, + { + "code": "eqwalizer", + "message": "`X`.\nExpression has type: #S{k_extra => term(), k_ok => term(), k_req1 => term(), k_req2 => term(), k_wrong1 => pid(), k_wrong2 => pid()}\nContext expected type: #S{k_ok => term(), k_req1 := atom(), k_req2 := atom(), k_req3 := atom(), k_wrong1 => atom(), k_wrong2 => atom()}\n\nThese associations do not match:\n\n #S{\n+ k_extra => ...\n- k_req1 := ...\n+ k_req1 => ...\n- k_req2 := ...\n+ k_req2 => ...\n- k_req3 := ...\n ...\n }\n See https://fb.me/eqwalizer_errors#incompatible_types", + "range": { + "end": { + "character": 5, + "line": 76 + }, + "start": { + "character": 4, + "line": 76 + } + }, + "severity": 1, + "source": "elp" + }, + { + "code": "eqwalizer", + "message": "`X`.\nExpression has type: id(#S{a := 'va', b := #S{c := #S{d => atom()}}})\nContext expected type: #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}}\n\n id(#S{a := 'va', b := #S{c := #S{d => atom()}}}) is not compatible with #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}}\n because\n at shape key 'b':\n #S{a := 'va', b := #S{c := #S{d => atom()}}} is not compatible with #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}}\n because\n at shape key 'c':\n #S{c := #S{d => atom()}} is not compatible with #S{c := id(#S{d := atom(), e := atom()})}\n because\n #S{d => atom()} is not compatible with id(#S{d := atom(), e := atom()})\n See https://fb.me/eqwalizer_errors#incompatible_types", + "range": { + "end": { + "character": 5, + "line": 100 + }, + "start": { + "character": 4, + "line": 100 + } + }, + "severity": 1, + "source": "elp" + }, + { + "code": "eqwalizer", + "message": "`X`.\nExpression has type: id(#S{a := 'va', b := #S{c := #S{d := pid(), e := pid()}}})\nContext expected type: #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}}\n\n id(#S{a := 'va', b := #S{c := #S{d := pid(), e := pid()}}}) is not compatible with #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}}\n because\n at shape key 'b':\n #S{a := 'va', b := #S{c := #S{d := pid(), e := pid()}}} is not compatible with #S{a := 'va', b := #S{c := id(#S{d := atom(), e := atom()})}}\n because\n at shape key 'c':\n #S{c := #S{d := pid(), e := pid()}} is not compatible with #S{c := id(#S{d := atom(), e := atom()})}\n because\n #S{d := pid(), e := pid()} is not compatible with id(#S{d := atom(), e := atom()})\n See https://fb.me/eqwalizer_errors#incompatible_types", + "range": { + "end": { + "character": 5, + "line": 123 + }, + "start": { + "character": 4, + "line": 123 + } + }, + "severity": 1, + "source": "elp" + } + ], + "uri": "file:///[..]/test_projects/standard/app_a/src/app_a.erl", + "version": 0 + }"#]], + ); +} + +// This used to fail because of trigerring eqwalizer for non-modules +// Now this fails with a timeout, since we never send down notifications +// if there's no diagnostics for a file. +// #[test] +// fn test_e2e_eqwalizer_header() { +// let workspace_root = +// AbsPathBuf::assert(Path::new(env!("CARGO_WORKSPACE_DIR")).join("test_projects/standard")); + +// // Sanity check +// assert!(std::fs::metadata(&workspace_root).is_ok()); + +// diagnostic_project( +// &workspace_root, +// r"app_a/include/app_a.hrl", +// expect![[r#" +// }"#]], +// ); +// } diff --git a/crates/elp/tests/slow-tests/support.rs b/crates/elp/tests/slow-tests/support.rs new file mode 100644 index 0000000000..7e5d74b967 --- /dev/null +++ b/crates/elp/tests/slow-tests/support.rs @@ -0,0 +1,414 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::cell::Cell; +use std::cell::RefCell; +use std::env; +use std::fs; +use std::sync::Once; +use std::thread; +use std::time::Duration; + +use crossbeam_channel::after; +use crossbeam_channel::select; +use crossbeam_channel::Receiver; +use elp::config::Config; +use elp::server::setup; +use elp_ide::diagnostics::DiagnosticCode; +use elp_ide::elp_ide_db::elp_base_db::AbsPathBuf; +use elp_log::Logger; +use expect_test::Expect; +use lsp_server::Connection; +use lsp_server::ErrorCode; +use lsp_server::Message; +use lsp_server::Notification; +use lsp_server::Request; +use lsp_types::notification::DidOpenTextDocument; +use lsp_types::notification::Exit; +use lsp_types::request::CodeActionRequest; +use lsp_types::request::Shutdown; +use lsp_types::CodeActionContext; +use lsp_types::CodeActionParams; +use lsp_types::DidOpenTextDocumentParams; +use lsp_types::PartialResultParams; +use lsp_types::Range; +use lsp_types::TextDocumentIdentifier; +use lsp_types::TextDocumentItem; +use lsp_types::Url; +use lsp_types::WorkDoneProgressParams; +use serde::Serialize; +use serde_json::json; +use serde_json::Value; +use tempfile::Builder; +use tempfile::TempDir; + +pub(crate) struct Project { + tmp_dir: TempDir, +} + +impl Project { + pub(crate) fn new() -> Project { + Project { + tmp_dir: Builder::new().prefix("elp_").tempdir().unwrap(), + } + } + + pub(crate) fn check_diagnostic( + self, + workspace_root: &AbsPathBuf, + module: &str, + expected_resp: Expect, + ) { + // Verify published diagnostic. + + let action = + |mock: TestServer, id: TextDocumentIdentifier| -> Value { mock.get_diagnostic(&id) }; + self.mock_lsp(workspace_root, module, action, expected_resp); + } + + pub(crate) fn check_code_action( + self, + workspace_root: &AbsPathBuf, + module: &str, + range: Range, + expected_resp: Expect, + ) { + // Verify code action ("Quick Fix" in the IDE). + + let action = |mock: TestServer, id: TextDocumentIdentifier| -> Value { + mock.send_request::(CodeActionParams { + text_document: id, + range, + context: CodeActionContext::default(), + partial_result_params: PartialResultParams::default(), + work_done_progress_params: WorkDoneProgressParams::default(), + }) + }; + self.mock_lsp(workspace_root, module, action, expected_resp); + } + + fn mock_lsp Value>( + self, + workspace_root: &AbsPathBuf, + module: &str, + action: F, + expected_resp: Expect, + ) { + static INIT: Once = Once::new(); + INIT.call_once(|| { + env_logger::builder() + .is_test(true) + .parse_env("ELP_LOG") + .try_init() + .unwrap(); + profile::init_from(crate::PROFILE); + }); + + let tmp_dir_path = AbsPathBuf::assert(self.tmp_dir.path().to_path_buf()); + + // Simpler version of elp::run_server ////////////////////////////////////// + // * Not handling sub-server + // * Using memory connection rather than stdio. + log::info!("test_server will start"); + let (connection, client) = Connection::memory(); + + // Mimic ServerSetup, without the handshake + // If the need for distinct config arises, move config in Project structure + // (as it is done by rust analyser). + let mut config = Config::new( + tmp_dir_path, + lsp_types::ClientCapabilities { + text_document: Some(lsp_types::TextDocumentClientCapabilities { + definition: Some(lsp_types::GotoCapability { + link_support: Some(true), + ..Default::default() + }), + code_action: Some(lsp_types::CodeActionClientCapabilities { + code_action_literal_support: Some( + lsp_types::CodeActionLiteralSupport::default(), + ), + ..Default::default() + }), + hover: Some(lsp_types::HoverClientCapabilities { + content_format: Some(vec![lsp_types::MarkupKind::Markdown]), + ..Default::default() + }), + ..Default::default() + }), + window: Some(lsp_types::WindowClientCapabilities { + // Disable progress messages, + // so we don't have to filter them. + work_done_progress: Some(false), + ..Default::default() + }), + experimental: Some(json!({ + // Necessary, since we'll wait for 'Running' notification. + // Also: closer to real-life usage. + "serverStatusNotification": true, + })), + ..Default::default() + }, + ); + config.ignore_diagnostic(DiagnosticCode::MissingCompileWarnMissingSpec); + + let handle = thread::spawn(|| { + let server = setup::setup_server(config, connection, Logger::default())?; + server.main_loop() + }); + + { + // We want to drop the mock at the end of this block to initiate shutdown sequence. + // Otherwise handle.join() would hang. + + let mock = TestServer::new(client, self.tmp_dir); + + let document = workspace_root.join(module); + let id = mock.doc_id(&document.as_path().display().to_string()); + + // Indicate the document was opened. + // - It mimicks what the IDE would do. + // - It is required for Eqwalizer diagnostics, which are + // only ran on explicitly opened documents. + mock.notification::(DidOpenTextDocumentParams { + text_document: TextDocumentItem::new( + id.uri.clone(), + "erlang".to_string(), + 0, + fs::read_to_string(document).unwrap(), + ), + }); + + mock.wait_until_workspace_is_loaded(); + + let actual = action(mock, id); + let actual_string = serde_json::to_string_pretty(&actual).unwrap(); + let actual_string = replace_paths(actual_string); + expected_resp.assert_eq(&actual_string); + } + + handle.join().unwrap().unwrap(); + } +} + +fn replace_paths(actual_string: String) -> String { + let to_replace = env!("CARGO_WORKSPACE_DIR"); + actual_string.replace(to_replace, "/[..]/") +} + +pub(crate) fn code_action_project( + workspace_root: &AbsPathBuf, + module: &str, + range: Range, + expected_resp: Expect, +) { + Project::new().check_code_action(workspace_root, module, range, expected_resp); +} + +pub(crate) fn diagnostic_project(workspace_root: &AbsPathBuf, module: &str, expected_resp: Expect) { + Project::new().check_diagnostic(workspace_root, module, expected_resp); +} + +// Bridge between (test) Project and real Server. +// It is called "Server" in rust analyser, +// yet it's principally mocking the LSP client. +pub(crate) struct TestServer { + req_id: Cell, + messages: RefCell>, + client: Connection, + /// XXX: remove the tempdir last + dir: TempDir, +} + +impl TestServer { + fn new(client: Connection, dir: TempDir) -> TestServer { + TestServer { + req_id: Cell::new(1), + messages: Default::default(), + client, + dir, + } + } + + pub(crate) fn doc_id(&self, rel_path: &str) -> TextDocumentIdentifier { + let path = self.dir.path().join(rel_path); + TextDocumentIdentifier { + uri: Url::from_file_path(path).unwrap(), + } + } + + pub(crate) fn notification(&self, params: N::Params) + where + N: lsp_types::notification::Notification, + N::Params: Serialize, + { + let r = Notification::new(N::METHOD.to_string(), params); + self.send_notification(r) + } + + /// Send request to elp server, and wait for response. + pub(crate) fn send_request(&self, params: R::Params) -> Value + where + R: lsp_types::request::Request, + R::Params: Serialize, + { + let id = self.req_id.get(); + self.req_id.set(id.wrapping_add(1)); + + let r = Request::new(id.into(), R::METHOD.to_string(), params); + self.send_request_(r) + } + fn send_request_(&self, r: Request) -> Value { + let id = r.id.clone(); + self.client.sender.send(r.clone().into()).unwrap(); + while let Some(msg) = self + .recv() + .unwrap_or_else(|Timeout| panic!("timeout waiting for response to request: {:?}", r)) + { + match msg { + Message::Request(req) => { + if req.method == "client/registerCapability" { + let params = req.params.to_string(); + if ["workspace/didChangeWatchedFiles", "textDocument/didSave"] + .iter() + .any(|&it| params.contains(it)) + { + continue; + } + } + continue; + } + Message::Notification(_) => {} + Message::Response(res) => { + assert_eq!(res.id, id); + if let Some(err) = res.error { + if err.code == ErrorCode::ContentModified as i32 { + // "elp still loading". + // wait for notification before sending first request. + log::error!("response: {:#?}", err); + std::thread::sleep(std::time::Duration::from_millis(100)); + continue; + } else { + panic!("error response: {:#?}", err); + } + } + return res.result.unwrap(); + } + } + } + panic!("no response for {:?}", r); + } + + pub(crate) fn wait_until_workspace_is_loaded(&self) { + // We wait until 'Running' phase, so that the request we want to test + // won't be answered by "elp still loading". + // ELP itself might still do other stuff for eqwalizer, which isn't an issue. + // On the contrary: the test is closer to real-life, with concurrent tasks. + while let Some(msg) = self + .recv() + .unwrap_or_else(|Timeout| panic!("timeout whilst waiting for workspace to load: ")) + { + match msg { + // Filter our other requests such as "client/registerCapability" + Message::Request(_req) => {} + Message::Response(res) => { + panic!("No request sent, didn't expect response: {:#?}", res); + } + Message::Notification(notif) => match notif.method.as_str() { + "elp/status" => { + let params = notif.params.to_string(); + if params.contains("Running") { + return; + } + } + "textDocument/publishDiagnostics" | "$/progress" => { + log::info!("======{:#?}", notif); + } + "telemetry/event" => { + log::info!("------{:#?}", notif); + } + _ => { + panic!("{:#?}", notif); + } + }, + } + } + unreachable!() + } + + pub(crate) fn get_diagnostic(&self, id: &TextDocumentIdentifier) -> Value { + let module = &id.uri.as_str(); + while let Some(msg) = self + .recv() + .unwrap_or_else(|Timeout| panic!("timeout whilst waiting for diagnostic: ")) + { + match msg { + // Filter our other requests such as "client/registerCapability" + Message::Request(_req) => {} + Message::Response(res) => { + panic!("No request sent, didn't expect response: {:#?}", res); + } + Message::Notification(notif) => { + match notif.method.as_str() { + "elp/event" => { + // Filter events such as "EqWAlizing completed" + continue; + } + "textDocument/publishDiagnostics" => { + if notif.params.to_string().contains(module) { + let diagnostics = ¬if.params["diagnostics"]; + match diagnostics { + Value::Array(_) => return notif.params, + _ => { + panic!("Unexpected diagnostic format: {:#?}", diagnostics) + } + } + } + // Filter (empty) diagnostics of other modules. + continue; + } + _ => { + // Filter irrelevant notifications. + continue; + } + } + } + } + } + unreachable!() + } + + fn recv(&self) -> Result, Timeout> { + let msg = recv_timeout(&self.client.receiver)?; + let msg = msg.map(|msg| { + self.messages.borrow_mut().push(msg.clone()); + msg + }); + Ok(msg) + } + fn send_notification(&self, not: Notification) { + self.client.sender.send(Message::Notification(not)).unwrap(); + } +} + +impl Drop for TestServer { + fn drop(&mut self) { + self.send_request::(()); + self.notification::(()); + } +} + +struct Timeout; + +fn recv_timeout(receiver: &Receiver) -> Result, Timeout> { + let timeout = Duration::from_secs(30); + select! { + recv(receiver) -> msg => Ok(msg.ok()), + recv(after(timeout)) -> _ => Err(Timeout), + } +} diff --git a/crates/elp_log/Cargo.toml b/crates/elp_log/Cargo.toml new file mode 100644 index 0000000000..52bd216e5e --- /dev/null +++ b/crates/elp_log/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "elp_log" +edition.workspace = true +version.workspace = true + +[dependencies] +crossbeam-channel.workspace = true +env_logger.workspace = true +fxhash.workspace = true +lazy_static.workspace = true +log.workspace = true +parking_lot.workspace = true +serde.workspace = true +serde_json.workspace = true + +[dev-dependencies] +expect-test.workspace = true +tempfile.workspace = true diff --git a/crates/elp_log/src/file.rs b/crates/elp_log/src/file.rs new file mode 100644 index 0000000000..37727ab114 --- /dev/null +++ b/crates/elp_log/src/file.rs @@ -0,0 +1,90 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::fs::File; +use std::io; +use std::io::BufWriter; +use std::io::Write; + +use log::Record; +use parking_lot::Mutex; + +use crate::Builder; +use crate::Filter; +use crate::ReconfigureLog; + +/// Simple logger that logs either to stderr or to a file, using `env_logger` +/// filter syntax. Amusingly, there's no crates.io crate that can do this and +/// only this. +/// +/// Taken from https://github.com/rust-lang/rust-analyzer/blob/81a9ad3672d547b2f5d265766bbb6c79909fb2da/crates/rust-analyzer/src/bin/logger.rs +pub struct FileLogger { + filter: Filter, + file: Option>>, + no_buffering: bool, +} + +impl FileLogger { + pub fn new(log_file: Option, no_buffering: bool, filter: Option<&str>) -> Self { + let filter = { + let mut builder = Builder::new(); + if let Some(filter) = filter { + builder.parse(filter); + } + builder.build() + }; + + let file = log_file.map(|it| Mutex::new(BufWriter::new(it))); + + Self { + filter, + file, + no_buffering, + } + } +} + +impl ReconfigureLog for FileLogger { + fn flush(&self) { + match &self.file { + Some(w) => { + let _ = w.lock().flush(); + } + None => { + let _ = io::stderr().flush(); + } + } + } + + fn filter(&self) -> &Filter { + &self.filter + } + + fn reconfigure(&mut self, mut filter: Builder) { + self.filter = filter.build(); + } + + fn write(&self, record: &Record) { + let formatted = format!("[{} {}] {}", record.level(), record.target(), record.args()); + let should_flush = match &self.file { + Some(w) => { + let _ = writeln!(w.lock(), "{}", formatted); + self.no_buffering + } + None => { + eprintln!("{}", formatted); + true // flush stderr unconditionally + } + }; + + if should_flush { + self.flush(); + } + } +} diff --git a/crates/elp_log/src/lib.rs b/crates/elp_log/src/lib.rs new file mode 100644 index 0000000000..f04a25daae --- /dev/null +++ b/crates/elp_log/src/lib.rs @@ -0,0 +1,236 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::fmt::Display; +use std::sync::Arc; +use std::time::Instant; + +pub use env_logger::filter::Builder; +pub use env_logger::filter::Filter; +use fxhash::FxHashMap; +use log::LevelFilter; +use log::Log; +use log::Metadata; +use log::Record; +use parking_lot::RwLock; + +mod file; +pub mod telemetry; + +pub use file::FileLogger; +use serde::Serialize; + +use crate::telemetry::send_with_duration; + +/// Trait similar to Log, but supports direct re-configuration through +/// env_logger-like filter +pub trait ReconfigureLog: Send + Sync { + fn filter(&self) -> &Filter; + + fn reconfigure(&mut self, filter: Builder); + + fn write(&self, record: &Record); + + fn flush(&self); +} + +#[derive(Default)] +struct Shared { + writers: FxHashMap>, +} + +impl Shared { + fn max_level(&self) -> LevelFilter { + self.writers + .values() + .map(|logger| logger.filter().filter()) + .max() + .unwrap_or(LevelFilter::Off) + } +} + +/// Re-configurable logger writer +#[derive(Default, Clone)] +pub struct Logger { + shared: Arc>, +} + +impl Logger { + pub fn register_logger>(&self, name: S, logger: Box) { + let mut shared = self.shared.write(); + shared.writers.insert(name.into(), logger); + + log::set_max_level(shared.max_level()); + } + + pub fn reconfigure(&self, name: &str, filter: Builder) { + let mut shared = self.shared.write(); + + if let Some(logger) = shared.writers.get_mut(name) { + logger.reconfigure(filter); + } + + log::set_max_level(shared.max_level()); + } + + pub fn install(&self) { + let max_level = self.shared.read().max_level(); + + let _ = + log::set_boxed_logger(Box::new(self.clone())).map(|()| log::set_max_level(max_level)); + } +} + +impl Log for Logger { + fn enabled(&self, metadata: &Metadata) -> bool { + self.shared + .read() + .writers + .values() + .any(|logger| logger.filter().enabled(metadata)) + } + + fn log(&self, record: &Record) { + for logger in self.shared.read().writers.values() { + if logger.filter().matches(record) { + logger.write(record); + } + } + } + + fn flush(&self) { + for logger in self.shared.read().writers.values() { + logger.flush(); + } + } +} + +/// Usage: `let _timer = timeit!(displayable&serializable)` +/// Logs elapsed time at INFO level when `drop`d. +/// **Always** use a named variable +/// so `drop` happens at the end of the function +#[macro_export] +macro_rules! timeit { + ($display:expr) => { + $crate::TimeIt::new(module_path!(), $display, false) + }; + ($($arg:tt)+) => { + $crate::TimeIt::new(module_path!(), format!($($arg)+), false) + }; +} + +/// Usage: `let _timer = timeit_with_telemetry!(displayable&serializable)` +/// Same as timeit!, but also send a LSP telemetry/event +#[macro_export] +macro_rules! timeit_with_telemetry { + ($display:expr) => { + $crate::TimeIt::new(module_path!(), $display, true) + }; + ($($arg:tt)+) => { + $crate::TimeIt::new(module_path!(), format!($($arg)+), true) + }; +} + +// inspired by rust-analyzer `timeit` +// https://github.com/rust-lang/rust-analyzer/blob/65a1538/crates/stdx/src/lib.rs#L18 +/// Logs the elapsed time when `drop`d +#[must_use = "logs the elapsed time when `drop`d"] +pub struct TimeIt +where + T: Display, + T: Serialize, + T: Clone, +{ + data: T, + module_path: &'static str, + instant: Option, + telemetry: bool, +} + +impl TimeIt +where + T: Display, + T: Serialize, + T: Clone, +{ + pub fn new(module_path: &'static str, data: T, telemetry: bool) -> Self { + TimeIt { + data, + module_path, + instant: Some(Instant::now()), + telemetry, + } + } +} + +impl Drop for TimeIt +where + T: Display, + T: Serialize, + T: Clone, +{ + fn drop(&mut self) { + if let Some(instant) = self.instant.take() { + let duration_ms = instant.elapsed().as_millis() as u32; + log::info!( + target: self.module_path, + "timeit '{}': {}ms", + self.data.clone(), + duration_ms + ); + if self.telemetry { + match serde_json::to_value(self.data.clone()) { + Ok(value) => send_with_duration(String::from("telemetry"), value, duration_ms), + Err(err) => log::warn!( + "Error serializing telemetry data. data: {}, err: {}", + self.data, + err + ), + } + } + } + } +} + +#[cfg(test)] +mod tests { + use std::env; + use std::io::Read; + + use tempfile::NamedTempFile; + + use super::*; + + #[test] + fn it_works() { + let mut file = NamedTempFile::new().unwrap(); + let log_file = file.reopen().unwrap(); + + let file_logger = FileLogger::new(Some(log_file), true, None); + + let logger = Logger::default(); + logger.register_logger("test", Box::new(file_logger)); + logger.install(); + + log::error!("This will be logged!"); + log::trace!("This won't be logged!"); + + let mut buf = String::new(); + file.read_to_string(&mut buf).unwrap(); + // When executing this test via buck2 the crate name is changed as part + // of the unittest rule generated. This ensures we are compatible with + // both buck2 and cargo. + let name = if env::var_os("BUCK2_DAEMON_UUID").is_some() { + "elp_log_unittest" + } else { + "elp_log" + }; + assert_eq!(format!("[ERROR {name}::tests] This will be logged!\n"), buf); + } +} diff --git a/crates/elp_log/src/telemetry.rs b/crates/elp_log/src/telemetry.rs new file mode 100644 index 0000000000..95ed3c7af2 --- /dev/null +++ b/crates/elp_log/src/telemetry.rs @@ -0,0 +1,98 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! A lightweight telemetry facade, inspired by the logging framework +//! used by Rust Analyzer: https://github.com/rust-lang/log +//! +//! This module provides a single telemetry API. + +//! If no telemetry implementation is selected, the facade falls back to +//! a “noop” implementation that ignores all telemetry messages. The overhead +//! in this case is very small - just an integer load, comparison and +//! jump. + +use lazy_static::lazy_static; +use serde::Deserialize; +use serde::Serialize; + +pub type TelemetryData = serde_json::Value; +pub type Duration = u32; +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TelemetryMessage { + #[serde(rename = "type")] + pub typ: String, + pub duration_ms: Option, + pub data: TelemetryData, +} + +pub type TelemetrySender = crossbeam_channel::Sender; +pub type TelemetryReceiver = crossbeam_channel::Receiver; + +lazy_static! { + static ref CHANNEL: (TelemetrySender, TelemetryReceiver) = crossbeam_channel::unbounded(); +} + +pub fn sender() -> &'static TelemetrySender { + &CHANNEL.0 +} + +pub fn receiver() -> &'static TelemetryReceiver { + &CHANNEL.1 +} + +fn build_message( + typ: String, + data: TelemetryData, + duration_ms: Option, +) -> TelemetryMessage { + TelemetryMessage { + // Note: the "type" field is required, otherwise the telemetry + // mapper in the vscode extension will not route the message + // to chronicle, and hence scuba. The value is mapped to the + //"extras.eventName" field. + typ, + duration_ms, + data, + } +} + +fn do_send(typ: String, data: serde_json::Value, duration: Option) { + let message = build_message(typ, data, duration); + let _ = sender().send(message); +} + +pub fn send(typ: String, data: serde_json::Value) { + do_send(typ, data, None); +} + +pub fn send_with_duration(typ: String, data: serde_json::Value, duration: Duration) { + do_send(typ, data, Some(duration)); +} + +#[cfg(test)] +mod tests { + use expect_test::expect; + + #[test] + fn it_works() { + let typ = String::from("telemetry"); + let data = serde_json::to_value("Hello telemetry!").unwrap(); + super::send(typ, data); + + let msg = super::receiver().try_recv().unwrap(); + expect![[r#" + TelemetryMessage { + typ: "telemetry", + duration_ms: None, + data: String("Hello telemetry!"), + } + "#]] + .assert_debug_eq(&msg); + } +} diff --git a/crates/eqwalizer/Cargo.toml b/crates/eqwalizer/Cargo.toml new file mode 100644 index 0000000000..475229a931 --- /dev/null +++ b/crates/eqwalizer/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "elp_eqwalizer" +edition.workspace = true +version.workspace = true + +[dependencies] +elp_base_db.workspace = true +elp_syntax.workspace = true + +anyhow.workspace = true +eetf.workspace = true +fxhash.workspace = true +lazy_static.workspace = true +log.workspace = true +parking_lot.workspace = true +salsa.workspace = true +serde_json.workspace = true +serde.workspace = true +serde_with.workspace = true +stdx.workspace = true +tempfile.workspace = true +timeout-readwrite.workspace = true diff --git a/crates/eqwalizer/build.rs b/crates/eqwalizer/build.rs new file mode 100644 index 0000000000..1e94b524fc --- /dev/null +++ b/crates/eqwalizer/build.rs @@ -0,0 +1,94 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::env; +use std::fs; +use std::path::Path; +use std::process::Command; + +fn main() { + let source_directory = Path::new("../../../eqwalizer/eqwalizer"); + let out_dir = env::var_os("OUT_DIR").unwrap(); + let eqwalizer_out_dir = Path::new("../../../../../buck-out/eqwalizer/scala-2.13"); + let dest_path = Path::new(&out_dir).join("eqwalizer"); + let extension; + + if let Some(path) = env::var_os("ELP_EQWALIZER_PATH") { + let from = Path::new(&path); + extension = from + .extension() + .unwrap_or_default() + .to_str() + .unwrap() + .to_string(); + fs::copy(from, dest_path).expect("Copying precompiled eqwalizer failed"); + } else { + extension = "".to_string(); + // Use the sbt wrapper on linux or otherwise require sbt to be installed + let sbt = fs::canonicalize(source_directory.join("./meta/sbt.sh")).unwrap(); + let output = Command::new(sbt) + .arg("assembly") + .current_dir(source_directory) + .env("EQWALIZER_USE_BUCK_OUT", "true") + .output() + .expect("failed to execute sbt assembly"); + + if !output.status.success() { + let stdout = + String::from_utf8(output.stdout).expect("valid utf8 output from sbt assembly"); + let stderr = + String::from_utf8(output.stderr).expect("valid utf8 output from sbt assembly"); + panic!( + "sbt assembly failed with stdout:\n{}\n\nstderr:\n{}", + stdout, stderr + ); + } + + let jar = fs::canonicalize(eqwalizer_out_dir.join("eqwalizer.jar")).unwrap(); + + let native_image = + fs::canonicalize(source_directory.join("./meta/native-image.sh")).unwrap(); + let image_path = fs::canonicalize(eqwalizer_out_dir) + .unwrap() + .join("eqwalizer"); + let output = Command::new(native_image) + .current_dir(source_directory) + .arg("-H:IncludeResources=application.conf") + .arg("--no-server") + .arg("--no-fallback") + .arg("-jar") + .arg(jar) + .arg(&image_path) + .output() + .expect("failed to execute native-image"); + + if !output.status.success() { + let stdout = + String::from_utf8(output.stdout).expect("valid utf8 output from native-image"); + let stderr = + String::from_utf8(output.stderr).expect("valid utf8 output from native-image"); + panic!( + "native-image failed with stdout:\n{}\n\nstderr:\n{}", + stdout, stderr + ); + } + + fs::copy(image_path, dest_path).expect("Copying fresh eqwalizer failed"); + + rerun_if_changed(source_directory.join("build.sbt")); + rerun_if_changed(source_directory.join("src")); + } + + println!("cargo:rustc-env=ELP_EQWALIZER_EXT={}", extension); + println!("cargo:rerun-if-env-changed=ELP_EQWALIZER_PATH"); +} + +fn rerun_if_changed(path: impl AsRef) { + println!("cargo:rerun-if-changed={}", path.as_ref().display()); +} diff --git a/crates/eqwalizer/src/ast/auto_import.rs b/crates/eqwalizer/src/ast/auto_import.rs new file mode 100644 index 0000000000..78d27e44ec --- /dev/null +++ b/crates/eqwalizer/src/ast/auto_import.rs @@ -0,0 +1,710 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use fxhash::FxHashSet; +use lazy_static::lazy_static; + +use crate::ast; + +lazy_static! { + static ref FUNS: FxHashSet = { + vec![ + ast::Id { + name: "abs".into(), + arity: 1, + }, + ast::Id { + name: "apply".into(), + arity: 2, + }, + ast::Id { + name: "apply".into(), + arity: 3, + }, + ast::Id { + name: "atom_to_binary".into(), + arity: 1, + }, + ast::Id { + name: "atom_to_binary".into(), + arity: 2, + }, + ast::Id { + name: "atom_to_list".into(), + arity: 1, + }, + ast::Id { + name: "binary_part".into(), + arity: 2, + }, + ast::Id { + name: "binary_part".into(), + arity: 3, + }, + ast::Id { + name: "binary_to_atom".into(), + arity: 1, + }, + ast::Id { + name: "binary_to_atom".into(), + arity: 2, + }, + ast::Id { + name: "binary_to_existing_atom".into(), + arity: 1, + }, + ast::Id { + name: "binary_to_existing_atom".into(), + arity: 2, + }, + ast::Id { + name: "binary_to_integer".into(), + arity: 1, + }, + ast::Id { + name: "binary_to_integer".into(), + arity: 2, + }, + ast::Id { + name: "binary_to_float".into(), + arity: 1, + }, + ast::Id { + name: "binary_to_list".into(), + arity: 1, + }, + ast::Id { + name: "binary_to_list".into(), + arity: 3, + }, + ast::Id { + name: "binary_to_term".into(), + arity: 1, + }, + ast::Id { + name: "binary_to_term".into(), + arity: 2, + }, + ast::Id { + name: "bitsize".into(), + arity: 1, + }, + ast::Id { + name: "bit_size".into(), + arity: 1, + }, + ast::Id { + name: "bitstring_to_list".into(), + arity: 1, + }, + ast::Id { + name: "byte_size".into(), + arity: 1, + }, + ast::Id { + name: "ceil".into(), + arity: 1, + }, + ast::Id { + name: "check_old_code".into(), + arity: 1, + }, + ast::Id { + name: "check_process_code".into(), + arity: 2, + }, + ast::Id { + name: "check_process_code".into(), + arity: 3, + }, + ast::Id { + name: "date".into(), + arity: 0, + }, + ast::Id { + name: "delete_module".into(), + arity: 1, + }, + ast::Id { + name: "demonitor".into(), + arity: 1, + }, + ast::Id { + name: "demonitor".into(), + arity: 2, + }, + ast::Id { + name: "disconnect_node".into(), + arity: 1, + }, + ast::Id { + name: "element".into(), + arity: 2, + }, + ast::Id { + name: "erase".into(), + arity: 0, + }, + ast::Id { + name: "erase".into(), + arity: 1, + }, + ast::Id { + name: "error".into(), + arity: 1, + }, + ast::Id { + name: "error".into(), + arity: 2, + }, + ast::Id { + name: "exit".into(), + arity: 1, + }, + ast::Id { + name: "exit".into(), + arity: 2, + }, + ast::Id { + name: "float".into(), + arity: 1, + }, + ast::Id { + name: "float_to_list".into(), + arity: 1, + }, + ast::Id { + name: "float_to_list".into(), + arity: 2, + }, + ast::Id { + name: "float_to_binary".into(), + arity: 1, + }, + ast::Id { + name: "float_to_binary".into(), + arity: 2, + }, + ast::Id { + name: "floor".into(), + arity: 1, + }, + ast::Id { + name: "garbage_collect".into(), + arity: 0, + }, + ast::Id { + name: "garbage_collect".into(), + arity: 1, + }, + ast::Id { + name: "garbage_collect".into(), + arity: 2, + }, + ast::Id { + name: "get".into(), + arity: 0, + }, + ast::Id { + name: "get".into(), + arity: 1, + }, + ast::Id { + name: "get_keys".into(), + arity: 0, + }, + ast::Id { + name: "get_keys".into(), + arity: 1, + }, + ast::Id { + name: "group_leader".into(), + arity: 0, + }, + ast::Id { + name: "group_leader".into(), + arity: 2, + }, + ast::Id { + name: "halt".into(), + arity: 0, + }, + ast::Id { + name: "halt".into(), + arity: 1, + }, + ast::Id { + name: "halt".into(), + arity: 2, + }, + ast::Id { + name: "hd".into(), + arity: 1, + }, + ast::Id { + name: "integer_to_binary".into(), + arity: 1, + }, + ast::Id { + name: "integer_to_binary".into(), + arity: 2, + }, + ast::Id { + name: "integer_to_list".into(), + arity: 1, + }, + ast::Id { + name: "integer_to_list".into(), + arity: 2, + }, + ast::Id { + name: "iolist_size".into(), + arity: 1, + }, + ast::Id { + name: "iolist_to_binary".into(), + arity: 1, + }, + ast::Id { + name: "is_alive".into(), + arity: 0, + }, + ast::Id { + name: "is_process_alive".into(), + arity: 1, + }, + ast::Id { + name: "is_atom".into(), + arity: 1, + }, + ast::Id { + name: "is_boolean".into(), + arity: 1, + }, + ast::Id { + name: "is_binary".into(), + arity: 1, + }, + ast::Id { + name: "is_bitstring".into(), + arity: 1, + }, + ast::Id { + name: "is_float".into(), + arity: 1, + }, + ast::Id { + name: "is_function".into(), + arity: 1, + }, + ast::Id { + name: "is_function".into(), + arity: 2, + }, + ast::Id { + name: "is_integer".into(), + arity: 1, + }, + ast::Id { + name: "is_list".into(), + arity: 1, + }, + ast::Id { + name: "is_map".into(), + arity: 1, + }, + ast::Id { + name: "is_map_key".into(), + arity: 2, + }, + ast::Id { + name: "is_number".into(), + arity: 1, + }, + ast::Id { + name: "is_pid".into(), + arity: 1, + }, + ast::Id { + name: "is_port".into(), + arity: 1, + }, + ast::Id { + name: "is_reference".into(), + arity: 1, + }, + ast::Id { + name: "is_tuple".into(), + arity: 1, + }, + ast::Id { + name: "is_record".into(), + arity: 2, + }, + ast::Id { + name: "is_record".into(), + arity: 3, + }, + ast::Id { + name: "length".into(), + arity: 1, + }, + ast::Id { + name: "link".into(), + arity: 1, + }, + ast::Id { + name: "list_to_atom".into(), + arity: 1, + }, + ast::Id { + name: "list_to_binary".into(), + arity: 1, + }, + ast::Id { + name: "list_to_bitstring".into(), + arity: 1, + }, + ast::Id { + name: "list_to_existing_atom".into(), + arity: 1, + }, + ast::Id { + name: "list_to_float".into(), + arity: 1, + }, + ast::Id { + name: "list_to_integer".into(), + arity: 1, + }, + ast::Id { + name: "list_to_integer".into(), + arity: 2, + }, + ast::Id { + name: "list_to_pid".into(), + arity: 1, + }, + ast::Id { + name: "list_to_port".into(), + arity: 1, + }, + ast::Id { + name: "list_to_ref".into(), + arity: 1, + }, + ast::Id { + name: "list_to_tuple".into(), + arity: 1, + }, + ast::Id { + name: "load_module".into(), + arity: 2, + }, + ast::Id { + name: "make_ref".into(), + arity: 0, + }, + ast::Id { + name: "map_size".into(), + arity: 1, + }, + ast::Id { + name: "map_get".into(), + arity: 2, + }, + ast::Id { + name: "max".into(), + arity: 2, + }, + ast::Id { + name: "min".into(), + arity: 2, + }, + ast::Id { + name: "module_loaded".into(), + arity: 1, + }, + ast::Id { + name: "monitor".into(), + arity: 2, + }, + ast::Id { + name: "monitor_node".into(), + arity: 2, + }, + ast::Id { + name: "node".into(), + arity: 0, + }, + ast::Id { + name: "node".into(), + arity: 1, + }, + ast::Id { + name: "nodes".into(), + arity: 0, + }, + ast::Id { + name: "nodes".into(), + arity: 1, + }, + ast::Id { + name: "now".into(), + arity: 0, + }, + ast::Id { + name: "open_port".into(), + arity: 2, + }, + ast::Id { + name: "pid_to_list".into(), + arity: 1, + }, + ast::Id { + name: "port_to_list".into(), + arity: 1, + }, + ast::Id { + name: "port_close".into(), + arity: 1, + }, + ast::Id { + name: "port_command".into(), + arity: 2, + }, + ast::Id { + name: "port_command".into(), + arity: 3, + }, + ast::Id { + name: "port_connect".into(), + arity: 2, + }, + ast::Id { + name: "port_control".into(), + arity: 3, + }, + ast::Id { + name: "pre_loaded".into(), + arity: 0, + }, + ast::Id { + name: "process_flag".into(), + arity: 2, + }, + ast::Id { + name: "process_flag".into(), + arity: 3, + }, + ast::Id { + name: "process_info".into(), + arity: 1, + }, + ast::Id { + name: "process_info".into(), + arity: 2, + }, + ast::Id { + name: "processes".into(), + arity: 0, + }, + ast::Id { + name: "purge_module".into(), + arity: 1, + }, + ast::Id { + name: "put".into(), + arity: 2, + }, + ast::Id { + name: "ref_to_list".into(), + arity: 1, + }, + ast::Id { + name: "register".into(), + arity: 2, + }, + ast::Id { + name: "registered".into(), + arity: 0, + }, + ast::Id { + name: "round".into(), + arity: 1, + }, + ast::Id { + name: "self".into(), + arity: 0, + }, + ast::Id { + name: "setelement".into(), + arity: 3, + }, + ast::Id { + name: "size".into(), + arity: 1, + }, + ast::Id { + name: "spawn".into(), + arity: 1, + }, + ast::Id { + name: "spawn".into(), + arity: 2, + }, + ast::Id { + name: "spawn".into(), + arity: 3, + }, + ast::Id { + name: "spawn".into(), + arity: 4, + }, + ast::Id { + name: "spawn_link".into(), + arity: 1, + }, + ast::Id { + name: "spawn_link".into(), + arity: 2, + }, + ast::Id { + name: "spawn_link".into(), + arity: 3, + }, + ast::Id { + name: "spawn_link".into(), + arity: 4, + }, + ast::Id { + name: "spawn_request".into(), + arity: 1, + }, + ast::Id { + name: "spawn_request".into(), + arity: 2, + }, + ast::Id { + name: "spawn_request".into(), + arity: 3, + }, + ast::Id { + name: "spawn_request".into(), + arity: 4, + }, + ast::Id { + name: "spawn_request".into(), + arity: 5, + }, + ast::Id { + name: "spawn_request_abandon".into(), + arity: 1, + }, + ast::Id { + name: "spawn_monitor".into(), + arity: 1, + }, + ast::Id { + name: "spawn_monitor".into(), + arity: 2, + }, + ast::Id { + name: "spawn_monitor".into(), + arity: 3, + }, + ast::Id { + name: "spawn_monitor".into(), + arity: 4, + }, + ast::Id { + name: "spawn_opt".into(), + arity: 2, + }, + ast::Id { + name: "spawn_opt".into(), + arity: 3, + }, + ast::Id { + name: "spawn_opt".into(), + arity: 4, + }, + ast::Id { + name: "spawn_opt".into(), + arity: 5, + }, + ast::Id { + name: "split_binary".into(), + arity: 2, + }, + ast::Id { + name: "statistics".into(), + arity: 1, + }, + ast::Id { + name: "term_to_binary".into(), + arity: 1, + }, + ast::Id { + name: "term_to_binary".into(), + arity: 2, + }, + ast::Id { + name: "term_to_iovec".into(), + arity: 1, + }, + ast::Id { + name: "term_to_iovec".into(), + arity: 2, + }, + ast::Id { + name: "throw".into(), + arity: 1, + }, + ast::Id { + name: "time".into(), + arity: 0, + }, + ast::Id { + name: "tl".into(), + arity: 1, + }, + ast::Id { + name: "trunc".into(), + arity: 1, + }, + ast::Id { + name: "tuple_size".into(), + arity: 1, + }, + ast::Id { + name: "tuple_to_list".into(), + arity: 1, + }, + ast::Id { + name: "unlink".into(), + arity: 1, + }, + ast::Id { + name: "unregister".into(), + arity: 1, + }, + ast::Id { + name: "whereis".into(), + arity: 1, + }, + ] + .into_iter() + .collect() + }; +} + +pub fn is_auto_imported(id: &ast::Id) -> bool { + return FUNS.contains(id); +} diff --git a/crates/eqwalizer/src/ast/binary_specifier.rs b/crates/eqwalizer/src/ast/binary_specifier.rs new file mode 100644 index 0000000000..13d20fd75f --- /dev/null +++ b/crates/eqwalizer/src/ast/binary_specifier.rs @@ -0,0 +1,24 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use serde::Serialize; + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub enum Specifier { + SignedIntegerSpecifier, + UnsignedIntegerSpecifier, + FloatSpecifier, + BinarySpecifier, + BytesSpecifier, + BitstringSpecifier, + BitsSpecifier, + Utf8Specifier, + Utf16Specifier, + Utf32Specifier, +} diff --git a/crates/eqwalizer/src/ast/compiler_macro.rs b/crates/eqwalizer/src/ast/compiler_macro.rs new file mode 100644 index 0000000000..1c7a20a748 --- /dev/null +++ b/crates/eqwalizer/src/ast/compiler_macro.rs @@ -0,0 +1,31 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::collections::HashSet; + +use lazy_static::lazy_static; + +use crate::ast; + +pub static FAKE_MODULE: &str = "$compiler_macro"; + +lazy_static! { + static ref FUNS: HashSet = { + vec![ast::Id { + name: "record_info".into(), + arity: 2, + }] + .into_iter() + .collect() + }; +} + +pub fn is_compiler_macro(id: &ast::Id) -> bool { + return FUNS.contains(id); +} diff --git a/crates/eqwalizer/src/ast/contractivity.rs b/crates/eqwalizer/src/ast/contractivity.rs new file mode 100644 index 0000000000..e6f4bfae89 --- /dev/null +++ b/crates/eqwalizer/src/ast/contractivity.rs @@ -0,0 +1,381 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! This module performs the second step of stubs validation +//! +//! It ensures that stubs are contractive, i.e., that the recursive type +//! aliases they define are all productive. +//! Broadly speaking, a recursive type is productive if all its infinite +//! expansions contain at least a type constructor (function type, tuple type, +//! etc. -- see function `is_producer` below) and produce a finite number +//! of distinct sub-terms. +//! +//! This property ensures that we can exhaustively and recursively compute +//! properties on recursive types in finite time, by stopping whenever we +//! visit an already-seen type. +//! +//! The main logic is function `is_foldable`, which checks whether a type +//! satisfies these properties, by recursively traversing it while keeping +//! the history of aliases seen so far. +//! Whenever we encounter a type alias that is already in the history, and +//! as long as we traversed a type constructor in the meantime, we know this +//! expansion path is contractive. +//! +//! Failure happens when we encounter a type alias that is not in the history, +//! but such that its expansion contains an embedding of an already-seen type. +//! Informally, this means that every expansion introduces a new, larger +//! sub-term. The function `is_he` (where "he" stands for "homeomorphic embedding") +//! below is responsible for checking whether a type embeds into another, +//! which can be done by "coupling" (the two types have the same top constructor) +//! or by "diving" (the first type embeds into one of the second's sub terms). +//! +//! Algorithm design by @ilyaklyuchnikov, see D30779530 for more information. + +use elp_base_db::ModuleName; +use elp_base_db::ProjectId; +use elp_syntax::SmolStr; +use fxhash::FxHashMap; + +use super::db::EqwalizerASTDatabase; +use super::form::InvalidForm; +use super::form::InvalidTypeDecl; +use super::form::TypeDecl; +use super::invalid_diagnostics::Invalid; +use super::invalid_diagnostics::NonProductiveRecursiveTypeAlias; +use super::stub::ModuleStub; +use super::subst::Subst; +use super::types::OpaqueType; +use super::types::Prop; +use super::types::Type; +use super::ContractivityCheckError; +use super::Id; +use super::RemoteId; + +fn is_he(s: &Type, t: &Type) -> Result { + Ok(he_by_diving(s, t)? || he_by_coupling(s, t)?) +} + +fn any_he<'a, I>(s: &Type, t: I) -> Result +where + I: Iterator, +{ + for ty in t { + if is_he(s, ty)? { + return Ok(true); + } + } + Ok(false) +} + +fn all_he<'a, I>(s: I, t: I) -> Result +where + I: Iterator, +{ + for (ty1, ty2) in s.zip(t) { + if !is_he(ty1, ty2)? { + return Ok(false); + } + } + Ok(true) +} + +fn he_by_diving(s: &Type, t: &Type) -> Result { + match t { + Type::FunType(ft) if !ft.forall.is_empty() => Err(ContractivityCheckError::NonEmptyForall), + Type::FunType(ft) => Ok(is_he(s, &ft.res_ty)? || any_he(s, ft.arg_tys.iter())?), + Type::AnyArityFunType(ft) => is_he(s, &ft.res_ty), + Type::TupleType(tt) => any_he(s, tt.arg_tys.iter()), + Type::ListType(lt) => is_he(s, <.t), + Type::UnionType(ut) => any_he(s, ut.tys.iter()), + Type::RemoteType(rt) => any_he(s, rt.arg_tys.iter()), + Type::OpaqueType(ot) => any_he(s, ot.arg_tys.iter()), + Type::DictMap(m) => Ok(is_he(s, &m.k_type)? || is_he(s, &m.v_type)?), + Type::ShapeMap(m) => { + let tys: Vec = m.props.iter().map(|p| p.tp().to_owned()).collect(); + any_he(s, tys.iter()) + } + Type::RefinedRecordType(rt) => any_he(s, rt.fields.values()), + _ => Ok(false), + } +} + +fn he_by_coupling(s: &Type, t: &Type) -> Result { + match (s, t) { + (Type::TupleType(tt1), Type::TupleType(tt2)) if tt1.arg_tys.len() == tt2.arg_tys.len() => { + all_he(tt1.arg_tys.iter(), tt2.arg_tys.iter()) + } + (Type::TupleType(_), _) => Ok(false), + (Type::FunType(ft1), Type::FunType(ft2)) if ft1.arg_tys.len() == ft2.arg_tys.len() => { + if !ft1.forall.is_empty() || !ft2.forall.is_empty() { + return Err(ContractivityCheckError::NonEmptyForall); + } + Ok(is_he(&ft1.res_ty, &ft2.res_ty)? && all_he(ft1.arg_tys.iter(), ft2.arg_tys.iter())?) + } + (Type::FunType(_), _) => Ok(false), + (Type::AnyArityFunType(ft1), Type::AnyArityFunType(ft2)) => is_he(&ft1.res_ty, &ft2.res_ty), + (Type::ListType(lt1), Type::ListType(lt2)) => is_he(<1.t, <2.t), + (Type::ListType(_), _) => Ok(false), + (Type::UnionType(ut1), Type::UnionType(ut2)) if ut1.tys.len() == ut2.tys.len() => { + all_he(ut1.tys.iter(), ut2.tys.iter()) + } + (Type::UnionType(_), _) => Ok(false), + (Type::RemoteType(rt1), Type::RemoteType(rt2)) if rt1.id == rt2.id => { + all_he(rt1.arg_tys.iter(), rt2.arg_tys.iter()) + } + (Type::RemoteType(_), _) => Ok(false), + (Type::OpaqueType(ot1), Type::OpaqueType(ot2)) if ot1.id == ot2.id => { + all_he(ot1.arg_tys.iter(), ot2.arg_tys.iter()) + } + (Type::OpaqueType(_), _) => Ok(false), + (Type::DictMap(m1), Type::DictMap(m2)) => { + Ok(is_he(&m1.k_type, &m2.k_type)? && is_he(&m1.v_type, &m2.v_type)?) + } + (Type::DictMap(_), _) => Ok(false), + (Type::ShapeMap(m1), Type::ShapeMap(m2)) if m1.props.len() == m2.props.len() => { + let mut props1 = m1.props.clone(); + let mut props2 = m2.props.clone(); + props1.sort_by_key(|p| p.key().to_owned()); + props2.sort_by_key(|p| p.key().to_owned()); + all_he_prop(props1.iter(), props2.iter()) + } + (Type::ShapeMap(_), _) => Ok(false), + (Type::RefinedRecordType(rt1), Type::RefinedRecordType(rt2)) + if rt1.rec_type == rt2.rec_type => + { + if rt1.fields.iter().any(|(f, _)| !rt2.fields.contains_key(f)) { + return Ok(false); + } + if rt2.fields.iter().any(|(f, _)| !rt1.fields.contains_key(f)) { + return Ok(false); + } + for key in rt1.fields.keys() { + if !is_he(rt1.fields.get(key).unwrap(), rt2.fields.get(key).unwrap())? { + return Ok(false); + } + } + Ok(true) + } + (Type::RefinedRecordType(_), _) => Ok(false), + (Type::VarType(vt1), Type::VarType(vt2)) if vt1 == vt2 => Ok(true), + (Type::VarType(_), _) => Ok(false), + (s, t) => Ok(s == t), + } +} + +fn all_he_prop<'a, I>(s: I, t: I) -> Result +where + I: Iterator, +{ + for (p1, p2) in s.zip(t) { + if !he_prop(p1, p2)? { + return Ok(false); + } + } + Ok(true) +} + +fn he_prop(s: &Prop, t: &Prop) -> Result { + match (s, t) { + (Prop::ReqProp(p1), Prop::ReqProp(p2)) if p1.key == p2.key => is_he(&p1.tp, &p2.tp), + (Prop::ReqProp(_), _) => Ok(false), + (Prop::OptProp(p1), Prop::OptProp(p2)) if p1.key == p2.key => is_he(&p1.tp, &p2.tp), + (Prop::OptProp(_), _) => Ok(false), + } +} + +pub struct StubContractivityChecker<'d> { + db: &'d dyn EqwalizerASTDatabase, + project_id: ProjectId, + module: SmolStr, +} + +impl StubContractivityChecker<'_> { + pub fn new<'d>( + db: &'d dyn EqwalizerASTDatabase, + project_id: ProjectId, + module: SmolStr, + ) -> StubContractivityChecker<'d> { + return StubContractivityChecker { + db, + project_id, + module, + }; + } + + fn check_type_decl( + &self, + stub: &mut ModuleStub, + t: &TypeDecl, + ) -> Result<(), ContractivityCheckError> { + if !self.is_contractive(&t.body)? { + stub.types.remove(&t.id); + stub.invalid_forms.push(self.to_invalid(t)); + } + Ok(()) + } + + fn check_opaque_decl( + &self, + stub: &mut ModuleStub, + t: &TypeDecl, + ) -> Result<(), ContractivityCheckError> { + if !self.is_contractive(&t.body)? { + stub.private_opaques.remove(&t.id); + stub.public_opaques.remove(&t.id); + stub.invalid_forms.push(self.to_invalid(t)); + } + Ok(()) + } + + fn to_invalid(&self, t: &TypeDecl) -> InvalidForm { + let diagnostics = + Invalid::NonProductiveRecursiveTypeAlias(NonProductiveRecursiveTypeAlias { + location: t.location.clone(), + name: t.id.to_string().into(), + }); + InvalidForm::InvalidTypeDecl(InvalidTypeDecl { + location: t.location.clone(), + id: t.id.clone(), + te: diagnostics, + }) + } + + fn is_contractive(&self, t: &Type) -> Result { + self.is_foldable(t, &vec![]) + } + + fn is_foldable( + &self, + ty: &Type, + history: &Vec<&Type>, + ) -> Result { + let mut produced = false; + for &t in history.iter().rev() { + if produced && t == ty { + return Ok(true); + } + produced = produced || self.is_producer(t)?; + } + let mut new_history = history.clone(); + new_history.push(ty); + match ty { + Type::FunType(ft) => { + if !ft.forall.is_empty() { + return Err(ContractivityCheckError::NonEmptyForall); + } + Ok(self.is_foldable(&ft.res_ty, &new_history)? + && self.all_foldable(ft.arg_tys.iter(), &new_history)?) + } + Type::AnyArityFunType(ft) => Ok(self.is_foldable(&ft.res_ty, &new_history)?), + Type::TupleType(tt) => Ok(self.all_foldable(tt.arg_tys.iter(), &new_history)?), + Type::ListType(lt) => Ok(self.is_foldable(<.t, &new_history)?), + Type::UnionType(ut) => Ok(self.all_foldable(ut.tys.iter(), &new_history)?), + Type::OpaqueType(ot) => Ok(self.all_foldable(ot.arg_tys.iter(), &new_history)?), + Type::DictMap(mt) => Ok(self.is_foldable(&mt.k_type, &new_history)? + && self.is_foldable(&mt.v_type, &new_history)?), + Type::ShapeMap(mt) => { + Ok(self.all_foldable(mt.props.iter().map(|p| p.tp()), &new_history)?) + } + Type::RefinedRecordType(rt) => Ok(self.all_foldable(rt.fields.values(), &new_history)?), + Type::RemoteType(rt) => { + for &t in history.iter() { + if he_by_coupling(t, ty)? { + return Ok(false); + } + } + match self.type_decl_body(&rt.id, &rt.arg_tys)? { + Some(typ) => Ok(self.is_foldable(&typ, &new_history)?), + None => Ok(true), + } + } + _ => Ok(true), + } + } + + fn all_foldable<'a, I>( + &self, + tys: I, + history: &Vec<&Type>, + ) -> Result + where + I: Iterator, + { + for ty in tys { + if !self.is_foldable(ty, history)? { + return Ok(false); + } + } + Ok(true) + } + + fn is_producer(&self, t: &Type) -> Result { + match t { + Type::FunType(_) + | Type::TupleType(_) + | Type::ListType(_) + | Type::OpaqueType(_) + | Type::DictMap(_) + | Type::ShapeMap(_) + | Type::RefinedRecordType(_) + | Type::AnyArityFunType(_) => Ok(true), + Type::RemoteType(_) => Ok(false), + Type::UnionType(_) => Ok(false), + _ => Err(ContractivityCheckError::UnexpectedType), + } + } + + fn type_decl_body( + &self, + id: &RemoteId, + args: &Vec, + ) -> Result, ContractivityCheckError> { + let local_id = Id { + name: id.name.clone(), + arity: id.arity, + }; + let stub = self + .db + .expanded_stub(self.project_id, ModuleName::new(id.module.as_str())) + .map_err(|_| ContractivityCheckError::UnexpectedID(id.clone()))?; + fn subst(decl: &TypeDecl, args: &Vec) -> Type { + let sub: FxHashMap = + decl.params.iter().map(|v| v.n).zip(args.iter()).collect(); + Subst { sub }.apply(decl.body.clone()) + } + Ok(stub + .types + .get(&local_id) + .map(|t| subst(t, args)) + .or_else(|| { + if self.module == id.module { + stub.private_opaques.get(&local_id).map(|t| subst(t, args)) + } else { + stub.public_opaques.get(&local_id).map(|_| { + Type::OpaqueType(OpaqueType { + id: id.clone(), + arg_tys: args.clone(), + }) + }) + } + })) + } + + pub fn check(&self, stub: &ModuleStub) -> Result { + let mut stub_result = stub.clone(); + stub.types + .iter() + .map(|(_, decl)| self.check_type_decl(&mut stub_result, decl)) + .collect::, _>>()?; + stub.private_opaques + .iter() + .map(|(_, decl)| self.check_opaque_decl(&mut stub_result, decl)) + .collect::, _>>()?; + Ok(stub_result) + } +} diff --git a/crates/eqwalizer/src/ast/convert.rs b/crates/eqwalizer/src/ast/convert.rs new file mode 100644 index 0000000000..6c2f3cfd31 --- /dev/null +++ b/crates/eqwalizer/src/ast/convert.rs @@ -0,0 +1,2215 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use eetf; +use eetf::Term; +use elp_syntax::SmolStr; +use fxhash::FxHashSet; + +use super::auto_import; +use super::binary_specifier::Specifier; +use super::compiler_macro; +use super::expr::AtomLit; +use super::expr::BComprehension; +use super::expr::BGenerate; +use super::expr::BinOp; +use super::expr::Binary; +use super::expr::BinaryElem; +use super::expr::Block; +use super::expr::Body; +use super::expr::Case; +use super::expr::Catch; +use super::expr::Cons; +use super::expr::DynCall; +use super::expr::DynRemoteFun; +use super::expr::DynRemoteFunArity; +use super::expr::Expr; +use super::expr::Filter; +use super::expr::FloatLit; +use super::expr::If; +use super::expr::IntLit; +use super::expr::LComprehension; +use super::expr::LGenerate; +use super::expr::Lambda; +use super::expr::LocalCall; +use super::expr::LocalFun; +use super::expr::MComprehension; +use super::expr::MGenerate; +use super::expr::MapCreate; +use super::expr::MapUpdate; +use super::expr::Match; +use super::expr::NilLit; +use super::expr::Qualifier; +use super::expr::Receive; +use super::expr::ReceiveWithTimeout; +use super::expr::RecordCreate; +use super::expr::RecordField; +use super::expr::RecordFieldGen; +use super::expr::RecordFieldNamed; +use super::expr::RecordIndex; +use super::expr::RecordSelect; +use super::expr::RecordUpdate; +use super::expr::RemoteCall; +use super::expr::RemoteFun; +use super::expr::StringLit; +use super::expr::TryCatchExpr; +use super::expr::TryOfCatchExpr; +use super::expr::Tuple; +use super::expr::UnOp; +use super::expr::Var; +use super::ext_types::AnyArityFunExtType; +use super::ext_types::AnyListExtType; +use super::ext_types::AnyMapExtType; +use super::ext_types::AtomLitExtType; +use super::ext_types::BinOpType; +use super::ext_types::BuiltinExtType; +use super::ext_types::ConstrainedFunType; +use super::ext_types::Constraint; +use super::ext_types::ExtProp; +use super::ext_types::ExtType; +use super::ext_types::FunExtType; +use super::ext_types::IntLitExtType; +use super::ext_types::ListExtType; +use super::ext_types::LocalExtType; +use super::ext_types::MapExtType; +use super::ext_types::OptBadExtProp; +use super::ext_types::OptExtProp; +use super::ext_types::RecordExtType; +use super::ext_types::RecordRefinedExtType; +use super::ext_types::RefinedField; +use super::ext_types::RemoteExtType; +use super::ext_types::ReqBadExtProp; +use super::ext_types::ReqExtProp; +use super::ext_types::TupleExtType; +use super::ext_types::UnOpType; +use super::ext_types::UnionExtType; +use super::ext_types::VarExtType; +use super::form::BehaviourAttr; +use super::form::CompileExportAllAttr; +use super::form::ElpMetadataAttr; +use super::form::EqwalizerNowarnFunctionAttr; +use super::form::EqwalizerUnlimitedRefinementAttr; +use super::form::ExportAttr; +use super::form::ExportTypeAttr; +use super::form::ExternalCallback; +use super::form::ExternalForm; +use super::form::ExternalFunSpec; +use super::form::ExternalOpaqueDecl; +use super::form::ExternalOptionalCallbacks; +use super::form::ExternalRecDecl; +use super::form::ExternalRecField; +use super::form::ExternalTypeDecl; +use super::form::FileAttr; +use super::form::Fixme; +use super::form::FunDecl; +use super::form::ImportAttr; +use super::form::ModuleAttr; +use super::form::TypingAttribute; +use super::guard::Guard; +use super::guard::Test; +use super::guard::TestAtom; +use super::guard::TestBinOp; +use super::guard::TestBinaryLit; +use super::guard::TestCall; +use super::guard::TestCons; +use super::guard::TestMapCreate; +use super::guard::TestMapUpdate; +use super::guard::TestNil; +use super::guard::TestNumber; +use super::guard::TestRecordCreate; +use super::guard::TestRecordField; +use super::guard::TestRecordFieldGen; +use super::guard::TestRecordFieldNamed; +use super::guard::TestRecordIndex; +use super::guard::TestRecordSelect; +use super::guard::TestString; +use super::guard::TestTuple; +use super::guard::TestUnOp; +use super::guard::TestVar; +use super::pat::Pat; +use super::pat::PatAtom; +use super::pat::PatBinOp; +use super::pat::PatBinary; +use super::pat::PatBinaryElem; +use super::pat::PatCons; +use super::pat::PatInt; +use super::pat::PatMap; +use super::pat::PatMatch; +use super::pat::PatNil; +use super::pat::PatNumber; +use super::pat::PatRecord; +use super::pat::PatRecordFieldNamed; +use super::pat::PatRecordIndex; +use super::pat::PatString; +use super::pat::PatTuple; +use super::pat::PatUnOp; +use super::pat::PatVar; +use super::pat::PatWild; +use super::types::Type; +use super::ConversionError; +use super::Id; +use super::RemoteId; +use crate::ast; +use crate::ast::expr::Clause; + +#[derive(Debug, Clone, PartialEq, Eq)] +struct Converter { + no_auto_imports: FxHashSet, + from_beam: bool, + filter_stub: bool, +} + +fn get_specifier(name: &str) -> Option { + match name { + "default" => Some(Specifier::UnsignedIntegerSpecifier), + "integer" => Some(Specifier::UnsignedIntegerSpecifier), + "float" => Some(Specifier::FloatSpecifier), + "binary" => Some(Specifier::BinarySpecifier), + "bytes" => Some(Specifier::BytesSpecifier), + "bitstring" => Some(Specifier::BitstringSpecifier), + "bits" => Some(Specifier::BitsSpecifier), + "utf8" => Some(Specifier::Utf8Specifier), + "utf16" => Some(Specifier::Utf16Specifier), + "utf32" => Some(Specifier::Utf32Specifier), + _ => None, + } +} + +impl Converter { + fn make_pos(&self, start: u32, end: u32) -> ast::Pos { + if self.from_beam { + ast::Pos::LineAndColumn(ast::LineAndColumn { + line: start, + column: end, + }) + } else { + ast::Pos::TextRange(ast::TextRange { + start_byte: start, + end_byte: end, + }) + } + } + + fn convert_pos(&self, term: &eetf::Term) -> Result { + match term { + Term::Tuple(tuple) => { + if let [Term::FixInteger(l), Term::FixInteger(c)] = &tuple.elements[..] { + return Ok(self.make_pos(l.value as u32, c.value as u32)); + } + } + Term::List(list) => { + if let [Term::Tuple(_), Term::Tuple(loc)] = &list.elements[..] { + if let [Term::Atom(eetf::Atom { name }), pos] = &loc.elements[..] { + if name == "location" { + return self.convert_pos(pos); + } + } + } + } + Term::FixInteger(l) => { + return Ok(self.make_pos(l.value as u32, l.value as u32 + 1)); + } + _ => (), + }; + return Err(ConversionError::InvalidLocation); + } + + fn convert_line(&self, line: &eetf::Term) -> Option { + match line { + Term::FixInteger(i) => Some(i.value as u32), + _ => None, + } + } + + fn convert_id(&self, id: &eetf::Term) -> Result { + match id { + Term::Tuple(tup) => { + if let [Term::Atom(name), Term::FixInteger(arity)] = &tup.elements[..] { + return Ok(ast::Id { + name: name.name.clone().into(), + arity: arity.value as u32, + }); + } + if let [Term::Atom(_), Term::Atom(name), Term::FixInteger(arity)] = + &tup.elements[..] + { + return Ok(ast::Id { + name: name.name.clone().into(), + arity: arity.value as u32, + }); + } + } + _ => (), + }; + return Err(ConversionError::InvalidID); + } + + fn convert_ids(&self, ids: &eetf::List) -> Result, ConversionError> { + ids.elements.iter().map(|id| self.convert_id(id)).collect() + } + + fn convert_varname(&self, v: &eetf::Term) -> Result { + if let Term::Tuple(var) = v { + if let [Term::Atom(v), _, Term::Atom(name)] = &var.elements[..] { + if v.name == "var" { + return Ok(name.name.clone().into()); + } + } + } + return Err(ConversionError::InvalidVarName); + } + + fn convert_name(&self, t: &eetf::Term) -> Result { + if let Term::Atom(atom) = t { + return Ok(atom.name.clone().into()); + } + return Err(ConversionError::InvalidName); + } + + fn convert_attribute( + &self, + kind: &eetf::Atom, + args: &eetf::Term, + location: ast::Pos, + ) -> Result, ConversionError> { + match (kind.name.as_str(), args) { + ("module", Term::Atom(name)) => { + return Ok(Some(ExternalForm::Module(ModuleAttr { + name: name.name.clone().into(), + location, + }))); + } + ("export", Term::List(ids)) => { + return Ok(Some(ExternalForm::Export(ExportAttr { + funs: self.convert_ids(ids)?, + location, + }))); + } + ("import", Term::Tuple(imports)) => { + if let [Term::Atom(m), Term::List(ids)] = &imports.elements[..] { + return Ok(Some(ExternalForm::Import(ImportAttr { + module: m.name.clone().into(), + funs: self.convert_ids(ids)?, + location, + }))); + } + return Ok(None); + } + ("export_type", Term::List(ids)) => { + return Ok(Some(ExternalForm::ExportType(ExportTypeAttr { + types: self.convert_ids(ids)?, + location, + }))); + } + ("record", Term::Tuple(rec)) => { + if let [Term::Atom(name), Term::List(fields)] = &rec.elements[..] { + return Ok(Some(ExternalForm::ExternalRecDecl(ExternalRecDecl { + name: name.name.clone().into(), + fields: fields + .elements + .iter() + .map(|f| self.convert_rec_field_form(f)) + .collect::, _>>()?, + location, + }))); + } + } + ("file", Term::Tuple(file)) => { + if let [Term::List(name), line] = &file.elements[..] { + if let Some(line) = self.convert_line(line) { + let filename: String = name + .elements + .iter() + .flat_map(|v| match v { + Term::FixInteger(i) => char::from_u32(i.value as u32), + _ => None, + }) + .collect(); + return Ok(Some(ExternalForm::File(FileAttr { + file: filename.into(), + start: line, + location, + }))); + } + } + } + ("elp_metadata", Term::List(prop_list)) => { + for prop in prop_list.elements.iter() { + if let Term::Tuple(prop) = prop { + if let [Term::Atom(tag), Term::List(raw_fixmes)] = &prop.elements[..] { + if tag.name == "eqwalizer_fixmes" { + let fixmes = raw_fixmes + .elements + .iter() + .map(|f| self.convert_fixme(f)) + .collect::, _>>()?; + return Ok(Some(ExternalForm::ElpMetadata(ElpMetadataAttr { + location, + fixmes, + }))); + } + } + } + } + } + ("behaviour" | "behavior", Term::Atom(name)) => { + return Ok(Some(ExternalForm::Behaviour(BehaviourAttr { + location, + name: name.name.clone().into(), + }))); + } + ("type", Term::Tuple(decl)) => { + if let [Term::Atom(n), body, Term::List(vs)] = &decl.elements[..] { + let id = ast::Id { + name: n.name.clone().into(), + arity: vs.elements.len() as u32, + }; + let body = self.convert_type(body)?; + let params = vs + .elements + .iter() + .map(|v| self.convert_varname(v)) + .collect::, _>>()?; + return Ok(Some(ExternalForm::ExternalTypeDecl(ExternalTypeDecl { + location, + id, + params, + body, + }))); + } + } + ("opaque", Term::Tuple(decl)) => { + if let [Term::Atom(n), body, Term::List(vs)] = &decl.elements[..] { + let id = ast::Id { + name: n.name.clone().into(), + arity: vs.elements.len() as u32, + }; + let body = self.convert_type(body)?; + let params = vs + .elements + .iter() + .map(|v| self.convert_varname(v)) + .collect::, _>>()?; + return Ok(Some(ExternalForm::ExternalOpaqueDecl(ExternalOpaqueDecl { + location, + id, + params, + body, + }))); + } + } + ("spec", Term::Tuple(spec)) => { + if let [fun_id, Term::List(types)] = &spec.elements[..] { + let id = self.convert_id(fun_id)?; + let types = types + .elements + .iter() + .map(|s| self.convert_fun_spec(s)) + .collect::, _>>()?; + return Ok(Some(ExternalForm::ExternalFunSpec(ExternalFunSpec { + location, + id, + types, + }))); + } + } + ("callback", Term::Tuple(cb)) => { + if let [fun_id, Term::List(types)] = &cb.elements[..] { + let id = self.convert_id(fun_id)?; + let types = types + .elements + .iter() + .map(|s| self.convert_fun_spec(s)) + .collect::, _>>()?; + return Ok(Some(ExternalForm::ExternalCallback(ExternalCallback { + location, + id, + types, + }))); + } + } + ("optional_callbacks", Term::List(ids)) => { + let ids = self.convert_ids(ids)?; + return Ok(Some(ExternalForm::ExternalOptionalCallbacks( + ExternalOptionalCallbacks { location, ids }, + ))); + } + ("compile", Term::List(flags)) => { + if flags.elements.iter().any(|f| self.is_export_all(f)) { + return Ok(Some(ExternalForm::CompileExportAll(CompileExportAllAttr { + location, + }))); + } + } + ("compile", Term::Atom(flag)) => { + if flag.name == "export_all" { + return Ok(Some(ExternalForm::CompileExportAll(CompileExportAllAttr { + location, + }))); + } + } + ("typing", Term::List(elems)) => { + let names = elems + .elements + .iter() + .map(|elem| self.convert_name(elem)) + .collect::, _>>()?; + return Ok(Some(ExternalForm::TypingAttribute(TypingAttribute { + location, + names, + }))); + } + ("eqwalizer", Term::Tuple(args)) => { + if let [Term::Atom(pragma), args] = &args.elements[..] { + if pragma.name == "nowarn_function" { + let id = self.convert_id(args)?; + return Ok(Some(ExternalForm::EqwalizerNowarnFunction( + EqwalizerNowarnFunctionAttr { location, id }, + ))); + } + if pragma.name == "unlimited_refinement" { + let id = self.convert_id(args)?; + return Ok(Some(ExternalForm::EqwalizerUnlimitedRefinement( + EqwalizerUnlimitedRefinementAttr { location, id }, + ))); + } + } + } + _ => (), + }; + return Ok(None); + } + + fn convert_fixme(&self, fixme: &eetf::Term) -> Result { + if let Term::Tuple(data) = fixme { + if let [ + Term::FixInteger(comment_start), + Term::FixInteger(comment_end), + Term::FixInteger(suppression_start), + Term::FixInteger(suppression_end), + Term::Atom(ignore), + ] = &data.elements[..] + { + let comment = ast::TextRange { + start_byte: comment_start.value as u32, + end_byte: comment_end.value as u32, + }; + let suppression = ast::TextRange { + start_byte: suppression_start.value as u32, + end_byte: suppression_end.value as u32, + }; + let is_ignore = ignore.name == "true"; + return Ok(Fixme { + comment, + suppression, + is_ignore, + }); + } + } + return Err(ConversionError::InvalidFixme); + } + + fn is_export_all(&self, flag: &eetf::Term) -> bool { + if let Term::Atom(flag) = flag { + return flag.name == "export_all"; + } + return false; + } + + fn convert_function( + &self, + name: &eetf::Atom, + arity: u32, + clauses: &eetf::List, + location: ast::Pos, + ) -> Result { + let id = ast::Id { + name: name.name.clone().into(), + arity, + }; + let clauses = clauses + .elements + .iter() + .map(|c| self.convert_clause(c)) + .collect::, _>>()?; + return Ok(ExternalForm::FunDecl(FunDecl { + id, + clauses, + location, + })); + } + + fn convert_clauses(&self, clauses: &eetf::List) -> Result, ConversionError> { + clauses + .elements + .iter() + .map(|clause| self.convert_clause(clause)) + .collect() + } + + fn convert_clause(&self, clause: &eetf::Term) -> Result { + if let Term::Tuple(cl) = clause { + if let [ + Term::Atom(tag), + pos, + Term::List(pats), + Term::List(guards), + Term::List(exprs), + ] = &cl.elements[..] + { + if tag.name == "clause" { + let location = self.convert_pos(pos)?; + let pats = pats + .elements + .iter() + .map(|p| self.convert_pat(p)) + .collect::, _>>()?; + let guards = guards + .elements + .iter() + .map(|g| self.convert_guard(g)) + .collect::, _>>()?; + let exprs = self.convert_exprs(exprs)?; + return Ok(Clause { + pats, + guards, + body: Body { exprs }, + location, + }); + } + } + } + return Err(ConversionError::InvalidClause); + } + + fn convert_rec_field_form( + &self, + field: &eetf::Term, + ) -> Result { + if let Term::Tuple(field) = field { + match &field.elements[..] { + [Term::Atom(kind), _pos, name_lit] if kind.name == "record_field" => { + let name = self.convert_atom_lit(name_lit)?; + return Ok(ExternalRecField { + name, + tp: None, + default_value: None, + }); + } + [Term::Atom(kind), _pos, name_lit, expr] if kind.name == "record_field" => { + let name = self.convert_atom_lit(name_lit)?; + let default_value = self.convert_expr(expr)?; + return Ok(ExternalRecField { + name, + tp: None, + default_value: Some(default_value), + }); + } + [Term::Atom(kind), field, ty] if kind.name == "typed_record_field" => { + let untyped_field = self.convert_rec_field_form(field)?; + let tp = self.convert_type(ty)?; + return Ok(ExternalRecField { + tp: Some(tp), + ..untyped_field + }); + } + _ => (), + } + } + return Err(ConversionError::InvalidRecordField); + } + + fn convert_atom_lit(&self, atom: &eetf::Term) -> Result { + if let Term::Tuple(data) = atom { + if let [Term::Atom(kind), _, Term::Atom(val)] = &data.elements[..] { + if kind.name == "atom" { + return Ok(val.name.clone().into()); + } + } + } + return Err(ConversionError::InvalidAtomLit); + } + + fn convert_int_lit(&self, atom: &eetf::Term) -> Result { + if let Term::Tuple(data) = atom { + if let [Term::Atom(kind), _, Term::FixInteger(val)] = &data.elements[..] { + if kind.name == "integer" { + return Ok(val.value); + } + } + } + return Err(ConversionError::InvalidIntLit); + } + + fn convert_cons(&self, expr: &eetf::Term) -> Result { + let mut exprs = vec![]; + let mut expr = expr; + loop { + if let Term::Tuple(tuple) = expr { + if let [Term::Atom(kind), pos, args @ ..] = &tuple.elements[..] { + match (kind.name.as_str(), args) { + ("cons", [he, te]) => { + let location = self.convert_pos(pos)?; + let h = self.convert_expr(he)?; + expr = te; + exprs.push((location, h)); + continue; + } + _ => { + let end = self.convert_expr(expr)?; + return Ok(exprs.into_iter().rev().fold(end, |t, (location, h)| { + Expr::Cons(Cons { + location, + h: Box::new(h), + t: Box::new(t), + }) + })); + } + } + } + } + return Err(ConversionError::InvalidExpr); + } + } + + fn convert_exprs(&self, exprs: &eetf::List) -> Result, ConversionError> { + exprs + .elements + .iter() + .map(|expr| self.convert_expr(expr)) + .collect() + } + + fn convert_expr(&self, expr: &eetf::Term) -> Result { + if let Term::Tuple(tuple) = expr { + if let [Term::Atom(kind), pos, args @ ..] = &tuple.elements[..] { + let location = self.convert_pos(pos)?; + match (kind.name.as_str(), args) { + ("match", [pat, exp]) => { + let pat = self.convert_pat(pat)?; + let expr = self.convert_expr(exp)?; + return Ok(Expr::Match(Match { + location, + pat, + expr: Box::new(expr), + })); + } + ("var", [Term::Atom(name)]) => { + return Ok(Expr::Var(Var { + location, + n: name.name.clone().into(), + })); + } + ("tuple", [Term::List(exps)]) => { + let elems = self.convert_exprs(exps)?; + return Ok(Expr::Tuple(Tuple { location, elems })); + } + ("nil", []) => { + return Ok(Expr::NilLit(NilLit { location })); + } + ("cons", _) => { + return self.convert_cons(expr); + } + ("bin", [Term::List(bin_elems)]) => { + let elems = bin_elems + .elements + .iter() + .map(|e| self.convert_binary_elem(e)) + .collect::, _>>()?; + return Ok(Expr::Binary(Binary { location, elems })); + } + ("op", [Term::Atom(op), e1, e2]) => { + let arg_1 = self.convert_expr(e1)?; + let arg_2 = self.convert_expr(e2)?; + return Ok(Expr::BinOp(BinOp { + location, + op: op.name.clone().into(), + arg_1: Box::new(arg_1), + arg_2: Box::new(arg_2), + })); + } + ("op", [Term::Atom(op), e]) => { + let arg = self.convert_expr(e)?; + return Ok(Expr::UnOp(UnOp { + location, + op: op.name.clone().into(), + arg: Box::new(arg), + })); + } + ("record", [Term::Atom(name), Term::List(fields)]) => { + return Ok(Expr::RecordCreate(RecordCreate { + location, + rec_name: name.name.clone().into(), + fields: fields + .elements + .iter() + .map(|f| self.convert_rec_field_expr(f)) + .collect::, _>>()?, + })); + } + ("record", [expr, Term::Atom(name), Term::List(fields)]) => { + return Ok(Expr::RecordUpdate(RecordUpdate { + location, + expr: Box::new(self.convert_expr(expr)?), + rec_name: name.name.clone().into(), + fields: fields + .elements + .iter() + .map(|f| match self.convert_rec_field_expr(f) { + Ok(RecordField::RecordFieldNamed(field)) => Ok(field), + _ => Err(ConversionError::InvalidRecordUpdateField), + }) + .collect::, ConversionError>>()?, + })); + } + ("record_index", [Term::Atom(name), field]) => { + return Ok(Expr::RecordIndex(RecordIndex { + location, + rec_name: name.name.clone().into(), + field_name: self.convert_atom_lit(field)?, + })); + } + ("record_field", [expr, Term::Atom(name), field]) => { + return Ok(Expr::RecordSelect(RecordSelect { + location, + expr: Box::new(self.convert_expr(expr)?), + rec_name: name.name.clone().into(), + field_name: self.convert_atom_lit(field)?, + })); + } + ("map", [Term::List(assocs)]) => { + return Ok(Expr::MapCreate(MapCreate { + location, + kvs: assocs + .elements + .iter() + .map(|kv| self.convert_create_kv(kv)) + .collect::, _>>()?, + })); + } + ("map", [expr, Term::List(assocs)]) => { + let map = self.convert_expr(expr)?; + return Ok(Expr::MapUpdate(MapUpdate { + location, + map: Box::new(map), + kvs: assocs + .elements + .iter() + .map(|kv| self.convert_kv(kv)) + .collect::, _>>()?, + })); + } + ("catch", [expr]) => { + return Ok(Expr::Catch(Catch { + location, + expr: Box::new(self.convert_expr(expr)?), + })); + } + ("call", [expr, Term::List(args)]) => { + let args: Vec = self.convert_exprs(args)?; + let arity = args.len() as u32; + if let Term::Tuple(call) = expr { + match &call.elements[..] { + [Term::Atom(remote), _, m, f] if remote.name == "remote" => { + match (self.convert_atom_lit(m), self.convert_atom_lit(f)) { + (Ok(m), Ok(f)) => { + let id = RemoteId { + module: m, + name: f, + arity, + }; + return Ok(Expr::RemoteCall(RemoteCall { + location, + id, + args, + })); + } + _ => (), + } + } + [Term::Atom(atom), _, Term::Atom(fname)] if atom.name == "atom" => { + let local_id = Id { + name: fname.name.clone().into(), + arity, + }; + if compiler_macro::is_compiler_macro(&local_id) { + let remote_id = RemoteId { + module: compiler_macro::FAKE_MODULE.into(), + name: fname.name.clone().into(), + arity, + }; + return Ok(Expr::RemoteCall(RemoteCall { + location, + id: remote_id, + args, + })); + } else if auto_import::is_auto_imported(&local_id) + && !self.no_auto_imports.contains(&local_id) + { + let remote_id = RemoteId { + module: "erlang".into(), + name: fname.name.clone().into(), + arity, + }; + return Ok(Expr::RemoteCall(RemoteCall { + location, + id: remote_id, + args, + })); + } else { + return Ok(Expr::LocalCall(LocalCall { + location, + id: local_id, + args, + })); + } + } + _ => (), + } + } + return Ok(Expr::DynCall(DynCall { + location, + f: Box::new(self.convert_expr(expr)?), + args, + })); + } + ("lc", [template, Term::List(qualifiers)]) => { + return Ok(Expr::LComprehension(LComprehension { + location, + template: Box::new(self.convert_expr(template)?), + qualifiers: qualifiers + .elements + .iter() + .map(|q| self.convert_qualifier(q)) + .collect::, _>>()?, + })); + } + ("bc", [template, Term::List(qualifiers)]) => { + return Ok(Expr::BComprehension(BComprehension { + location, + template: Box::new(self.convert_expr(template)?), + qualifiers: qualifiers + .elements + .iter() + .map(|q| self.convert_qualifier(q)) + .collect::, _>>()?, + })); + } + ("mc", [template, Term::List(qualifiers)]) => { + let (k_template, v_template) = self.convert_create_kv(template)?; + return Ok(Expr::MComprehension(MComprehension { + location, + k_template: Box::new(k_template), + v_template: Box::new(v_template), + qualifiers: qualifiers + .elements + .iter() + .map(|q| self.convert_qualifier(q)) + .collect::, _>>()?, + })); + } + ("block", [Term::List(exps)]) => { + return Ok(Expr::Block(Block { + location, + body: Body { + exprs: self.convert_exprs(exps)?, + }, + })); + } + ("if", [Term::List(clauses)]) => { + return Ok(Expr::If(If { + location, + clauses: self.convert_clauses(clauses)?, + })); + } + ("case", [expr, Term::List(clauses)]) => { + return Ok(Expr::Case(Case { + location, + expr: Box::new(self.convert_expr(expr)?), + clauses: self.convert_clauses(clauses)?, + })); + } + ( + "try", + [ + Term::List(body), + Term::List(try_clauses), + Term::List(catch_clauses), + Term::List(after), + ], + ) => { + let try_body = Body { + exprs: self.convert_exprs(body)?, + }; + let try_clauses: Vec = self.convert_clauses(try_clauses)?; + let catch_clauses = self.convert_clauses(catch_clauses)?; + let after: Vec = self.convert_exprs(after)?; + let after_body = if after.is_empty() { + None + } else { + Some(Body { exprs: after }) + }; + if try_clauses.is_empty() { + return Ok(Expr::TryCatchExpr(TryCatchExpr { + location, + try_body, + catch_clauses, + after_body, + })); + } else { + return Ok(Expr::TryOfCatchExpr(TryOfCatchExpr { + location, + try_clauses, + try_body, + catch_clauses, + after_body, + })); + } + } + ("receive", [Term::List(clauses)]) => { + return Ok(Expr::Receive(Receive { + location, + clauses: self.convert_clauses(clauses)?, + })); + } + ("receive", [Term::List(clauses), timeout, Term::List(defaults)]) => { + let timeout_exprs = self.convert_exprs(defaults)?; + let timeout_body = Body { + exprs: timeout_exprs, + }; + return Ok(Expr::ReceiveWithTimeout(ReceiveWithTimeout { + location, + clauses: self.convert_clauses(clauses)?, + timeout: Box::new(self.convert_expr(timeout)?), + timeout_body, + })); + } + ("fun", [Term::Tuple(decl)]) => match &decl.elements[..] { + [Term::Atom(kind), Term::List(clauses)] if kind.name == "clauses" => { + return Ok(Expr::Lambda(Lambda { + location, + clauses: self.convert_clauses(clauses)?, + name: None, + })); + } + [Term::Atom(kind), Term::Atom(name), Term::FixInteger(arity)] + if kind.name == "function" => + { + let local_id = Id { + name: name.name.clone().into(), + arity: arity.value as u32, + }; + if auto_import::is_auto_imported(&local_id) + && !self.no_auto_imports.contains(&local_id) + { + let remote_id = RemoteId { + module: "erlang".into(), + name: name.name.clone().into(), + arity: arity.value as u32, + }; + return Ok(Expr::RemoteFun(RemoteFun { + location, + id: remote_id, + })); + } else { + return Ok(Expr::LocalFun(LocalFun { + location, + id: local_id, + })); + } + } + [Term::Atom(kind), module, name, arity] if kind.name == "function" => { + match ( + self.convert_atom_lit(module), + self.convert_atom_lit(name), + self.convert_int_lit(arity), + ) { + (Ok(module), Ok(name), Ok(arity)) => { + let remote_id = RemoteId { + module, + name, + arity: arity as u32, + }; + return Ok(Expr::RemoteFun(RemoteFun { + location, + id: remote_id, + })); + } + _ => { + return Ok(Expr::DynRemoteFunArity(DynRemoteFunArity { + location, + module: Box::new(self.convert_expr(module)?), + name: Box::new(self.convert_expr(name)?), + arity: Box::new(self.convert_expr(arity)?), + })); + } + } + } + _ => (), + }, + ("named_fun", [Term::Atom(name), Term::List(clauses)]) => { + return Ok(Expr::Lambda(Lambda { + location, + clauses: self.convert_clauses(clauses)?, + name: Some(name.name.clone().into()), + })); + } + ("atom", [Term::Atom(value)]) => { + return Ok(Expr::AtomLit(AtomLit { + location, + s: value.name.clone().into(), + })); + } + ("float", [Term::Float(_)]) => { + return Ok(Expr::FloatLit(FloatLit { location })); + } + ("char" | "integer", [Term::BigInteger(_)]) => { + return Ok(Expr::IntLit(IntLit { + location, + value: None, + })); + } + ("char" | "integer", [Term::FixInteger(value)]) => { + return Ok(Expr::IntLit(IntLit { + location, + value: Some(value.value), + })); + } + ("string", [Term::List(elems)]) if elems.is_nil() => { + return Ok(Expr::StringLit(StringLit { + location, + empty: true, + })); + } + ("string", [_]) => { + return Ok(Expr::StringLit(StringLit { + location, + empty: false, + })); + } + ("remote", [module, name]) => { + return Ok(Expr::DynRemoteFun(DynRemoteFun { + location, + module: Box::new(self.convert_expr(module)?), + name: Box::new(self.convert_expr(name)?), + })); + } + _ => (), + } + } + } + return Err(ConversionError::InvalidExpr); + } + + fn convert_create_kv(&self, kv: &eetf::Term) -> Result<(Expr, Expr), ConversionError> { + if let Term::Tuple(kv) = kv { + if let [Term::Atom(atom_assoc), _, exp1, exp2] = &kv.elements[..] { + if atom_assoc.name == "map_field_assoc" { + return Ok((self.convert_expr(exp1)?, self.convert_expr(exp2)?)); + } + } + } + return Err(ConversionError::InvalidMapAssoc); + } + + fn convert_kv(&self, kv: &eetf::Term) -> Result<(Expr, Expr), ConversionError> { + if let Term::Tuple(kv) = kv { + if let [_, _, exp1, exp2] = &kv.elements[..] { + return Ok((self.convert_expr(exp1)?, self.convert_expr(exp2)?)); + } + } + return Err(ConversionError::InvalidKV); + } + + fn convert_rec_field_expr(&self, field: &eetf::Term) -> Result { + if let Term::Tuple(field) = field { + if let [Term::Atom(atom_field), _, name, exp] = &field.elements[..] { + if atom_field.name == "record_field" { + match self.convert_rec_field_name(name)? { + Some(name) => { + return Ok(RecordField::RecordFieldNamed(RecordFieldNamed { + name, + value: self.convert_expr(exp)?, + })); + } + None => { + return Ok(RecordField::RecordFieldGen(RecordFieldGen { + value: self.convert_expr(exp)?, + })); + } + } + } + } + } + return Err(ConversionError::InvalidRecordFieldExpr); + } + + fn convert_rec_field_name( + &self, + term: &eetf::Term, + ) -> Result, ConversionError> { + if let Term::Tuple(field) = term { + match &field.elements[..] { + [Term::Atom(atom), _, Term::Atom(val)] if atom.name == "atom" => { + return Ok(Some(val.name.clone().into())); + } + [Term::Atom(var), _, Term::Atom(val)] if var.name == "var" && val.name == "_" => { + return Ok(None); + } + _ => (), + } + } + return Err(ConversionError::InvalidRecordFieldName); + } + + fn convert_binary_elem(&self, elem: &eetf::Term) -> Result { + if let Term::Tuple(be) = elem { + if let [Term::Atom(atom_be), pos, elem, size, spec] = &be.elements[..] { + let location = self.convert_pos(pos)?; + let specifier = self.convert_specifier(spec); + let size = { + match size { + Term::Atom(a) if a.name == "default" => None, + e => Some(self.convert_expr(e)?), + } + }; + if atom_be.name == "bin_element" { + return Ok(BinaryElem { + location, + size, + specifier, + expr: self.convert_expr(elem)?, + }); + } + } + } + return Err(ConversionError::InvalidBinaryElem); + } + + fn convert_pat(&self, pat: &eetf::Term) -> Result { + if let Term::Tuple(pat) = pat { + if let [Term::Atom(kind), pos, args @ ..] = &pat.elements[..] { + let location = self.convert_pos(pos)?; + match (kind.name.as_str(), args) { + ("match", [pat1, pat2]) => { + return Ok(Pat::PatMatch(PatMatch { + location, + pat: Box::new(self.convert_pat(pat1)?), + arg: Box::new(self.convert_pat(pat2)?), + })); + } + ("var", [Term::Atom(v)]) if v.name == "_" => { + return Ok(Pat::PatWild(PatWild { location })); + } + ("var", [Term::Atom(v)]) => { + return Ok(Pat::PatVar(PatVar { + location, + n: v.name.clone().into(), + })); + } + ("tuple", [Term::List(pats)]) => { + return Ok(Pat::PatTuple(PatTuple { + location, + elems: pats + .elements + .iter() + .map(|p| self.convert_pat(p)) + .collect::, _>>()?, + })); + } + ("nil", []) => { + return Ok(Pat::PatNil(PatNil { location })); + } + ("cons", [hpat, tpat]) => { + return Ok(Pat::PatCons(PatCons { + location, + h: Box::new(self.convert_pat(hpat)?), + t: Box::new(self.convert_pat(tpat)?), + })); + } + ("atom", [Term::Atom(v)]) => { + return Ok(Pat::PatAtom(PatAtom { + location, + s: v.name.clone().into(), + })); + } + ("float", [_]) => { + return Ok(Pat::PatNumber(PatNumber { location })); + } + ("char" | "integer", [Term::BigInteger(_)]) => { + return Ok(Pat::PatInt(PatInt { location })); + } + ("char" | "integer", [Term::FixInteger(_)]) => { + return Ok(Pat::PatInt(PatInt { location })); + } + ("string", [_]) => { + return Ok(Pat::PatString(PatString { location })); + } + ("bin", [Term::List(bin_elems)]) => { + return Ok(Pat::PatBinary(PatBinary { + location, + elems: bin_elems + .elements + .iter() + .map(|be| self.convert_pat_binary_elem(be)) + .collect::, _>>()?, + })); + } + ("op", [Term::Atom(op), pat1, pat2]) => { + return Ok(Pat::PatBinOp(PatBinOp { + location, + op: op.name.clone().into(), + arg_1: Box::new(self.convert_pat(pat1)?), + arg_2: Box::new(self.convert_pat(pat2)?), + })); + } + ("op", [Term::Atom(op), pat]) => { + return Ok(Pat::PatUnOp(PatUnOp { + location, + op: op.name.clone().into(), + arg: Box::new(self.convert_pat(pat)?), + })); + } + ("record", [Term::Atom(name), Term::List(fields)]) => { + let fields_named = fields + .elements + .iter() + .map(|f| self.convert_pat_record_field(f)) + .collect::, _>>()? + .into_iter() + .flatten() + .collect(); + let gen = fields + .elements + .iter() + .map(|f| self.convert_pat_record_field_gen(f)) + .collect::, _>>()? + .into_iter() + .flatten() + .next() + .map(|pat| Box::new(pat)); + return Ok(Pat::PatRecord(PatRecord { + location, + rec_name: name.name.clone().into(), + fields: fields_named, + gen, + })); + } + ("record_index", [Term::Atom(name), field_name]) => { + return Ok(Pat::PatRecordIndex(PatRecordIndex { + location, + rec_name: name.name.clone().into(), + field_name: self.convert_atom_lit(field_name)?, + })); + } + ("map", [Term::List(kvs)]) => { + return Ok(Pat::PatMap(PatMap { + location, + kvs: kvs + .elements + .iter() + .map(|kv| self.convert_pat_kv(kv)) + .collect::, _>>()?, + })); + } + _ => (), + } + } + } + return Err(ConversionError::InvalidPattern); + } + + fn convert_pat_binary_elem(&self, pat: &eetf::Term) -> Result { + if let Term::Tuple(pat) = pat { + match &pat.elements[..] { + [Term::Atom(at_bin_element), pos, elem, esize, specifier] + if at_bin_element.name == "bin_element" => + { + let size = { + match esize { + Term::Atom(a) if a.name == "default" => None, + expr => Some(self.convert_expr(expr)?), + } + }; + let specifier = self.convert_specifier(specifier); + let location = self.convert_pos(pos)?; + return Ok(PatBinaryElem { + location, + pat: self.convert_pat(elem)?, + size, + specifier, + }); + } + _ => (), + } + } + return Err(ConversionError::InvalidPatBinaryElem); + } + + fn convert_pat_record_field( + &self, + pat: &eetf::Term, + ) -> Result, ConversionError> { + if let Term::Tuple(elems) = pat { + match &elems.elements[..] { + [Term::Atom(at_field), _, name, pat] if at_field.name == "record_field" => { + let pat = self.convert_pat(pat)?; + return Ok(self + .convert_rec_field_name(name)? + .map(|name| PatRecordFieldNamed { name, pat })); + } + _ => (), + } + } + return Err(ConversionError::InvalidPatRecordFieldNamed); + } + + fn convert_pat_record_field_gen( + &self, + pat: &eetf::Term, + ) -> Result, ConversionError> { + if let Term::Tuple(elems) = pat { + match &elems.elements[..] { + [Term::Atom(at_field), _, name, pat] if at_field.name == "record_field" => { + if self.convert_rec_field_name(name)?.is_none() { + return Ok(Some(self.convert_pat(pat)?)); + } else { + return Ok(None); + } + } + _ => (), + } + } + return Err(ConversionError::InvalidPatRecordFieldGen); + } + + fn convert_pat_kv(&self, pat: &eetf::Term) -> Result<(Test, Pat), ConversionError> { + if let Term::Tuple(elems) = pat { + match &elems.elements[..] { + [Term::Atom(kind), _, test, pat] + if kind.name == "map_field_exact" || kind.name == "map_field_assoc" => + { + return Ok((self.convert_test(test)?, self.convert_pat(pat)?)); + } + _ => (), + } + } + return Err(ConversionError::InvalidKVPattern); + } + + fn convert_guard(&self, guard: &eetf::Term) -> Result { + if let Term::List(tests) = guard { + return Ok(Guard { + tests: tests + .elements + .iter() + .map(|t| self.convert_test(t)) + .collect::, _>>()?, + }); + } + return Err(ConversionError::InvalidGuard); + } + + fn convert_test(&self, test: &eetf::Term) -> Result { + if let Term::Tuple(elems) = test { + if let [Term::Atom(kind), pos, args @ ..] = &elems.elements[..] { + let location = self.convert_pos(pos)?; + match (kind.name.as_str(), args) { + ("var", [Term::Atom(name)]) => { + return Ok(Test::TestVar(TestVar { + location, + v: name.name.clone().into(), + })); + } + ("tuple", [Term::List(tests)]) => { + let tests = tests + .elements + .iter() + .map(|t| self.convert_test(t)) + .collect::, _>>()?; + return Ok(Test::TestTuple(TestTuple { + location, + elems: tests, + })); + } + ("nil", []) => { + return Ok(Test::TestNil(TestNil { location })); + } + ("cons", [h, t]) => { + return Ok(Test::TestCons(TestCons { + location, + h: Box::new(self.convert_test(h)?), + t: Box::new(self.convert_test(t)?), + })); + } + ("bin", [_]) => { + return Ok(Test::TestBinaryLit(TestBinaryLit { location })); + } + ("op", [Term::Atom(op), arg1, arg2]) => { + return Ok(Test::TestBinOp(TestBinOp { + location, + op: op.name.clone().into(), + arg_1: Box::new(self.convert_test(arg1)?), + arg_2: Box::new(self.convert_test(arg2)?), + })); + } + ("op", [Term::Atom(op), arg1]) => { + return Ok(Test::TestUnOp(TestUnOp { + location, + op: op.name.clone().into(), + arg: Box::new(self.convert_test(arg1)?), + })); + } + ("record", [Term::Atom(name), Term::List(fields)]) => { + let tests = fields + .elements + .iter() + .map(|f| self.convert_test_record_field(f)) + .collect::, _>>()?; + return Ok(Test::TestRecordCreate(TestRecordCreate { + location, + rec_name: name.name.clone().into(), + fields: tests, + })); + } + ("record_index", [Term::Atom(name), field]) => { + let field_name = self.convert_atom_lit(field)?; + return Ok(Test::TestRecordIndex(TestRecordIndex { + location, + rec_name: name.name.clone().into(), + field_name, + })); + } + ("record_field", [test, Term::Atom(name), field]) => { + let field_name = self.convert_atom_lit(field)?; + let test = self.convert_test(test)?; + return Ok(Test::TestRecordSelect(TestRecordSelect { + location, + rec: Box::new(test), + rec_name: name.name.clone().into(), + field_name, + })); + } + ("map", [Term::List(kvs)]) => { + let tests = kvs + .elements + .iter() + .map(|kv| self.convert_test_kv(kv)) + .collect::, _>>()?; + return Ok(Test::TestMapCreate(TestMapCreate { + location, + kvs: tests, + })); + } + ("map", [t, Term::List(kvs)]) => { + let map = self.convert_test(t)?; + let kvs = kvs + .elements + .iter() + .map(|kv| self.convert_test_kv(kv)) + .collect::, _>>()?; + return Ok(Test::TestMapUpdate(TestMapUpdate { + location, + map: Box::new(map), + kvs, + })); + } + ("call", [Term::Tuple(expr), Term::List(args)]) => { + if let [Term::Atom(remote), _, module, fname] = &expr.elements[..] { + if remote.name == "remote" { + let module = self.convert_atom_lit(module)?; + if module == "erlang" { + let fname = self.convert_atom_lit(fname)?; + let id = Id { + name: fname, + arity: args.elements.len() as u32, + }; + let args = args + .elements + .iter() + .map(|t| self.convert_test(t)) + .collect::, _>>()?; + return Ok(Test::TestCall(TestCall { location, id, args })); + } + } + } + if let [Term::Atom(atom), _, Term::Atom(fname)] = &expr.elements[..] { + if atom.name == "atom" { + let id = Id { + name: fname.name.clone().into(), + arity: args.elements.len() as u32, + }; + let args = args + .elements + .iter() + .map(|t| self.convert_test(t)) + .collect::, _>>()?; + return Ok(Test::TestCall(TestCall { location, id, args })); + } + } + } + ("atom", [Term::Atom(value)]) => { + return Ok(Test::TestAtom(TestAtom { + location, + s: value.name.clone().into(), + })); + } + ("float", [_]) => { + return Ok(Test::TestNumber(TestNumber { + location, + lit: None, + })); + } + ("char" | "integer", [Term::BigInteger(_)]) => { + return Ok(Test::TestNumber(TestNumber { + location, + lit: None, + })); + } + ("char" | "integer", [Term::FixInteger(v)]) => { + return Ok(Test::TestNumber(TestNumber { + location, + lit: Some(v.value), + })); + } + ("string", [_]) => { + return Ok(Test::TestString(TestString { location })); + } + _ => (), + } + } + } + return Err(ConversionError::InvalidTest); + } + + fn convert_test_record_field( + &self, + term: &eetf::Term, + ) -> Result { + if let Term::Tuple(elems) = term { + if let [Term::Atom(kind), _, name, val] = &elems.elements[..] { + if kind.name == "record_field" { + match self.convert_rec_field_name(name)? { + Some(field_name) => { + return Ok(TestRecordField::TestRecordFieldNamed( + TestRecordFieldNamed { + name: field_name, + value: self.convert_test(val)?, + }, + )); + } + None => { + return Ok(TestRecordField::TestRecordFieldGen(TestRecordFieldGen { + value: self.convert_test(val)?, + })); + } + } + } + } + } + return Err(ConversionError::InvalidRecordFieldTest); + } + + fn convert_test_kv(&self, term: &eetf::Term) -> Result<(Test, Test), ConversionError> { + if let Term::Tuple(elems) = term { + if let [_, _, t1, t2] = &elems.elements[..] { + return Ok((self.convert_test(t1)?, self.convert_test(t2)?)); + } + } + return Err(ConversionError::InvalidKVTest); + } + + fn convert_qualifier(&self, term: &eetf::Term) -> Result { + if let Term::Tuple(elems) = term { + match &elems.elements[..] { + [Term::Atom(kind), _, pat, exp] if kind.name == "generate" => { + return Ok(Qualifier::LGenerate(LGenerate { + pat: self.convert_pat(pat)?, + expr: self.convert_expr(exp)?, + })); + } + [Term::Atom(kind), _, pat, exp] if kind.name == "b_generate" => { + return Ok(Qualifier::BGenerate(BGenerate { + pat: self.convert_pat(pat)?, + expr: self.convert_expr(exp)?, + })); + } + [Term::Atom(kind), _, Term::Tuple(m_elems), exp] if kind.name == "m_generate" => { + if let [Term::Atom(m_kind), _, k_pat, v_pat] = &m_elems.elements[..] { + if m_kind.name == "map_field_exact" { + return Ok(Qualifier::MGenerate(MGenerate { + k_pat: self.convert_pat(k_pat)?, + v_pat: self.convert_pat(v_pat)?, + expr: self.convert_expr(exp)?, + })); + } + } + } + _ => (), + } + } + return Ok(Qualifier::Filter(Filter { + expr: self.convert_expr(term)?, + })); + } + + fn convert_specifier(&self, term: &eetf::Term) -> Specifier { + let unsigned_spec = { + match term { + Term::List(specs) => specs + .elements + .iter() + .flat_map(|term| match term { + Term::Atom(a) => get_specifier(a.name.as_str()), + _ => None, + }) + .next() + .unwrap_or(Specifier::UnsignedIntegerSpecifier), + _ => Specifier::UnsignedIntegerSpecifier, + } + }; + let signed = { + match term { + Term::List(specs) => specs.elements.iter().any(|term| match term { + Term::Atom(a) => a.name == "signed", + _ => false, + }), + _ => false, + } + }; + let spec = { + if signed && unsigned_spec == Specifier::UnsignedIntegerSpecifier { + Specifier::SignedIntegerSpecifier + } else { + unsigned_spec + } + }; + return spec; + } + + fn convert_fun_spec(&self, spec: &eetf::Term) -> Result { + if let Term::Tuple(spec) = spec { + if let [Term::Atom(ty), pos, Term::Atom(kind), Term::List(decl)] = &spec.elements[..] { + if ty.name != "type" { + return Err(ConversionError::InvalidFunSpec); + } + if kind.name == "fun" { + if let [Term::Tuple(dom), result] = &decl.elements[..] { + if let [Term::Atom(ty), pos, Term::Atom(kind), Term::List(args)] = + &dom.elements[..] + { + if ty.name != "type" || kind.name != "product" { + return Err(ConversionError::InvalidFunSpec); + } + let location = self.convert_pos(pos)?; + let res_ty = self.convert_type(result)?; + let arg_tys = args + .elements + .iter() + .map(|t| self.convert_type(t)) + .collect::, _>>()?; + let ty = FunExtType { + location: location.clone(), + arg_tys, + res_ty: Box::new(res_ty), + }; + return Ok(ConstrainedFunType { + location, + ty, + constraints: vec![], + }); + } + } + } else if kind.name == "bounded_fun" { + let location = self.convert_pos(pos)?; + if let [ft, Term::List(constraints)] = &decl.elements[..] { + let fun_type = self.convert_type(ft)?; + let constraints = constraints + .elements + .iter() + .map(|c| self.convert_constraint(c)) + .collect::, _>>()?; + if let ExtType::FunExtType(ty) = fun_type { + return Ok(ConstrainedFunType { + location, + ty, + constraints, + }); + } + } + } + } + } + return Err(ConversionError::InvalidFunSpec); + } + + fn convert_constraint(&self, cons: &eetf::Term) -> Result { + if let Term::Tuple(cons) = cons { + if let [Term::Atom(ty), pos, Term::Atom(cs), Term::List(decl)] = &cons.elements[..] { + if let [is_sub, Term::List(vt)] = &decl.elements[..] { + if let [v, t] = &vt.elements[..] { + if ty.name == "type" + && cs.name == "constraint" + && self.convert_atom_lit(is_sub)? == "is_subtype".to_string() + { + let location = self.convert_pos(pos)?; + let t_var = self.convert_varname(v)?; + let ty = self.convert_type(t)?; + return Ok(Constraint { + location, + t_var, + ty, + }); + } + } + } + } + } + return Err(ConversionError::InvalidFunConstraint); + } + + fn convert_prop_type(&self, prop: &eetf::Term) -> Result { + if let Term::Tuple(prop) = prop { + if let [Term::Atom(ty), pos, Term::Atom(kind), Term::List(kv)] = &prop.elements[..] { + if ty.name != "type" { + return Err(ConversionError::InvalidPropType); + } + let location = self.convert_pos(pos)?; + if let [kt, vt] = &kv.elements[..] { + let key_type = self.convert_type(kt)?; + let val_type = self.convert_type(vt)?; + if kind.name == "map_field_assoc" { + match key_type { + ExtType::AtomLitExtType(_) => { + return Ok(ExtProp::OptExtProp(OptExtProp { + location, + key: key_type, + tp: val_type, + })); + } + _ => { + return Ok(ExtProp::OptBadExtProp(OptBadExtProp { + location, + key: key_type, + tp: val_type, + })); + } + } + } else if kind.name == "map_field_exact" { + match key_type { + ExtType::AtomLitExtType(_) => { + return Ok(ExtProp::ReqExtProp(ReqExtProp { + location, + key: key_type, + tp: val_type, + })); + } + _ => { + return Ok(ExtProp::ReqBadExtProp(ReqBadExtProp { + location, + key: key_type, + tp: val_type, + })); + } + } + } + } + } + } + return Err(ConversionError::InvalidPropType); + } + + fn convert_refined_field(&self, field: &eetf::Term) -> Result { + if let Term::Tuple(field) = field { + match &field.elements[..] { + [ + Term::Atom(atom_ty), + _, + Term::Atom(atom_field_ty), + Term::List(field), + ] if atom_ty.name == "type" && atom_field_ty.name == "field_type" => { + if let [name_lit, e_type] = &field.elements[..] { + return Ok(RefinedField { + label: self.convert_atom_lit(name_lit)?, + ty: self.convert_type(e_type)?, + }); + } + } + _ => (), + } + } + return Err(ConversionError::InvalidRecordRefinedField); + } + + fn convert_type(&self, ty: &eetf::Term) -> Result { + if let Term::Tuple(ty) = ty { + if let [Term::Atom(kind), pos, def @ ..] = &ty.elements[..] { + let location = self.convert_pos(pos)?; + match (kind.name.as_str(), &def[..]) { + ("ann_type", [Term::List(ty)]) => { + if let [_, tp] = &ty.elements[..] { + return self.convert_type(tp); + } + } + ("atom", [Term::Atom(val)]) => { + return Ok(ExtType::AtomLitExtType(AtomLitExtType { + location, + atom: val.name.clone().into(), + })); + } + ("type", [Term::Atom(fun), Term::List(ty)]) + if fun.name == "fun" && !ty.is_nil() => + { + if let [Term::Tuple(dom), res_ty] = &ty.elements[..] { + let res_ty = self.convert_type(res_ty)?; + if let [Term::Atom(dom_ty), _, Term::Atom(dom_kind), args @ ..] = + &dom.elements[..] + { + if dom_ty.name == "type" && dom_kind.name == "any" { + return Ok(ExtType::AnyArityFunExtType(AnyArityFunExtType { + location, + res_ty: Box::new(res_ty), + })); + } + if dom_ty.name == "type" && dom_kind.name == "product" { + if let [Term::List(args)] = &args[..] { + let arg_tys = args + .elements + .iter() + .map(|a| self.convert_type(a)) + .collect::, _>>()?; + return Ok(ExtType::FunExtType(FunExtType { + location, + arg_tys, + res_ty: Box::new(res_ty), + })); + } + } + } + } + } + ("type", [Term::Atom(kind), Term::List(range)]) if kind.name == "range" => { + if let [_range_first, _range_last] = &range.elements[..] { + return Ok(ExtType::int_ext_type(location)); + } + } + ("type", [Term::Atom(kind), def]) if kind.name == "map" => match def { + Term::Atom(a) if a.name == "any" => { + return Ok(ExtType::AnyMapExtType(AnyMapExtType { location })); + } + Term::List(assoc) if assoc.elements.is_empty() => { + return Ok(ExtType::MapExtType(MapExtType { + props: Vec::new(), + location, + })); + } + Term::List(assoc) if assoc.elements.len() == 1 => { + let hd = assoc.elements.first().unwrap(); + if let Term::Tuple(field) = hd { + if let [ + Term::Atom(prop_ty), + prop_pos, + Term::Atom(prop_kind), + Term::List(kv), + ] = &field.elements[..] + { + if prop_ty.name == "type" + && prop_kind.name == "map_field_assoc" + && kv.elements.len() == 2 + { + let prop_pos = self.convert_pos(prop_pos)?; + let key_type = + self.convert_type(kv.elements.get(0).unwrap())?; + let val_type = + self.convert_type(kv.elements.get(1).unwrap())?; + let prop = OptExtProp { + location: prop_pos, + key: key_type, + tp: val_type, + }; + return Ok(ExtType::MapExtType(MapExtType { + props: vec![ExtProp::OptExtProp(prop)], + location, + })); + } + } + } + return Ok(ExtType::MapExtType(MapExtType { + props: vec![self.convert_prop_type(&hd)?], + location, + })); + } + Term::List(assoc) => { + return Ok(ExtType::MapExtType(MapExtType { + props: assoc + .elements + .iter() + .map(|ty| self.convert_prop_type(ty)) + .collect::, _>>()?, + location, + })); + } + _ => (), + }, + ("type", [Term::Atom(kind), Term::List(decl)]) if kind.name == "record" => { + if let [record_name, field_tys @ ..] = &decl.elements[..] { + let record_name = self.convert_atom_lit(record_name)?; + if field_tys.is_empty() { + return Ok(ExtType::RecordExtType(RecordExtType { + location, + name: record_name, + })); + } else { + let refined_fields = field_tys + .iter() + .map(|ty| self.convert_refined_field(ty)) + .collect::, _>>()?; + return Ok(ExtType::RecordRefinedExtType(RecordRefinedExtType { + location, + name: record_name, + refined_fields, + })); + } + } + } + ("remote_type", [Term::List(decl)]) => { + if let [module, name, Term::List(args)] = &decl.elements[..] { + let module = self.convert_atom_lit(module)?; + let name = self.convert_atom_lit(name)?; + let id = RemoteId { + module, + name, + arity: args.elements.len() as u32, + }; + let args = args + .elements + .iter() + .map(|ty| self.convert_type(ty)) + .collect::, _>>()?; + return Ok(ExtType::RemoteExtType(RemoteExtType { + location, + id, + args, + })); + } + } + ("user_type", [Term::Atom(name), Term::List(params)]) => { + let id = Id { + name: name.name.clone().into(), + arity: params.elements.len() as u32, + }; + let args = params + .elements + .iter() + .map(|ty| self.convert_type(ty)) + .collect::, _>>()?; + return Ok(ExtType::LocalExtType(LocalExtType { location, id, args })); + } + ("integer", [Term::BigInteger(_)]) => { + return Ok(ExtType::IntLitExtType(IntLitExtType { location })); + } + ("char", [Term::BigInteger(_)]) => { + return Ok(ExtType::char_ext_type(location)); + } + ("integer", [Term::FixInteger(_)]) => { + return Ok(ExtType::IntLitExtType(IntLitExtType { location })); + } + ("char", [Term::FixInteger(_)]) => { + return Ok(ExtType::char_ext_type(location)); + } + ("op", [Term::Atom(op), _]) => { + return Ok(ExtType::UnOpType(UnOpType { + location, + op: op.name.clone().into(), + })); + } + ("op", [Term::Atom(op), _, _]) => { + return Ok(ExtType::BinOpType(BinOpType { + location, + op: op.name.clone().into(), + })); + } + ("type", [Term::Atom(kind), Term::Atom(param)]) + if kind.name == "tuple" && param.name == "any" => + { + return Ok(ExtType::tuple_ext_type(location)); + } + ("type", [Term::Atom(kind), Term::List(params)]) if kind.name == "tuple" => { + let arg_tys = params + .elements + .iter() + .map(|ty| self.convert_type(ty)) + .collect::, _>>()?; + return Ok(ExtType::TupleExtType(TupleExtType { location, arg_tys })); + } + ("type", [Term::Atom(kind), Term::List(params)]) if kind.name == "union" => { + let tys = params + .elements + .iter() + .map(|ty| self.convert_type(ty)) + .collect::, _>>()?; + return Ok(ExtType::UnionExtType(UnionExtType { location, tys })); + } + ("var", [Term::Atom(var)]) if var.name == "_" => { + return Ok(ExtType::any_ext_type(location)); + } + ("var", [Term::Atom(var)]) => { + return Ok(ExtType::VarExtType(VarExtType { + location, + name: var.name.clone().into(), + })); + } + ("type", [Term::Atom(kind), Term::List(args)]) + if (kind.name == "list" || kind.name == "nonempty_list") => + { + if args.is_nil() { + return Ok(ExtType::AnyListExtType(AnyListExtType { location })); + } else if args.elements.len() == 1 { + let t = self.convert_type(args.elements.first().unwrap())?; + return Ok(ExtType::ListExtType(ListExtType { + location, + t: Box::new(t), + })); + } + } + ("type", [Term::Atom(kind), Term::List(args)]) + if (kind.name == "maybe_improper_list" + || kind.name == "nonempty_improper_list" + || kind.name == "nonempty_maybe_improper_list") + && args.elements.len() == 2 => + { + let t = self.convert_type(args.elements.first().unwrap())?; + return Ok(ExtType::ListExtType(ListExtType { + location, + t: Box::new(t), + })); + } + ("type", [Term::Atom(kind), Term::List(args)]) if args.is_nil() => { + let builtin = Type::builtin_type(kind.name.as_str()); + if builtin.is_none() { + return Err(ConversionError::UnknownBuiltin( + kind.name.clone().into(), + 0, + )); + } else { + return Ok(ExtType::BuiltinExtType(BuiltinExtType { + location, + name: kind.name.clone().into(), + })); + } + } + ("type", [Term::Atom(kind), Term::List(params)]) if kind.name == "binary" => { + if params.elements.len() == 1 || params.elements.len() == 2 { + return Ok(ExtType::binary_ext_type(location)); + } + } + ("type", [Term::Atom(name), Term::List(params)]) => { + let arity = params.elements.len(); + return Err(ConversionError::UnknownBuiltin( + name.name.clone().into(), + arity, + )); + } + _ => (), + } + } + } + return Err(ConversionError::InvalidType); + } + + fn convert_form(&self, term: &eetf::Term) -> Result, ConversionError> { + if let Term::Tuple(tuple) = term { + if let [Term::Atom(atom), _] = &tuple.elements[..] { + if atom.name == "eof" { + return Ok(None); + } + } + if let [Term::Atom(attr), pos, Term::Atom(kind), args] = &tuple.elements[..] { + if attr.name == "attribute" { + let pos = self.convert_pos(pos)?; + return self.convert_attribute(kind, args, pos); + } + } + if let [ + Term::Atom(fun), + pos, + Term::Atom(name), + Term::FixInteger(arity), + Term::List(clauses), + ] = &tuple.elements[..] + { + if fun.name == "function" { + if !self.filter_stub { + let pos = self.convert_pos(pos)?; + let arity = arity.value as u32; + return Ok(Some(self.convert_function(name, arity, clauses, pos)?)); + } else { + return Ok(None); + } + } + } + } + return Err(ConversionError::InvalidForm); + } + + fn extract_no_auto_import(&self, term: &eetf::Term) -> Option> { + if let Term::Tuple(tuple) = term { + if let [Term::Atom(attr), _, Term::Atom(kind), Term::Tuple(args)] = &tuple.elements[..] + { + if attr.name == "attribute" && kind.name == "compile" { + if let [Term::Atom(no_auto), Term::List(ids)] = &args.elements[..] { + if no_auto.name == "no_auto_import" { + return Some( + ids.elements + .iter() + .flat_map(|id| self.convert_id(id)) + .collect(), + ); + } + } + } + } + } + return None; + } +} + +pub fn convert_forms( + term: &eetf::Term, + from_beam: bool, + filter_stub: bool, +) -> Result, ConversionError> { + if let Term::List(forms) = term { + let dummy_converter = Converter { + no_auto_imports: FxHashSet::default(), + from_beam, + filter_stub, + }; + let no_auto_imports: FxHashSet = forms + .elements + .iter() + .flat_map(|f| dummy_converter.extract_no_auto_import(f)) + .flatten() + .collect(); + let converter = Converter { + no_auto_imports, + from_beam, + filter_stub, + }; + return Ok(forms + .elements + .iter() + .map(|f| converter.convert_form(f)) + .collect::, _>>()? + .into_iter() + .flatten() + .collect()); + } + return Err(ConversionError::InvalidForms); +} diff --git a/crates/eqwalizer/src/ast/convert_types.rs b/crates/eqwalizer/src/ast/convert_types.rs new file mode 100644 index 0000000000..b49f032906 --- /dev/null +++ b/crates/eqwalizer/src/ast/convert_types.rs @@ -0,0 +1,459 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_syntax::SmolStr; +use fxhash::FxHashMap; + +use super::ext_types::ConstrainedFunType; +use super::ext_types::ExtProp; +use super::ext_types::ExtType; +use super::ext_types::FunExtType; +use super::form::Callback; +use super::form::ExternalCallback; +use super::form::ExternalFunSpec; +use super::form::ExternalOpaqueDecl; +use super::form::ExternalRecDecl; +use super::form::ExternalRecField; +use super::form::ExternalTypeDecl; +use super::form::FunSpec; +use super::form::InvalidConvertTypeInRecDecl; +use super::form::OpaqueTypeDecl; +use super::form::OverloadedFunSpec; +use super::form::RecDecl; +use super::form::RecField; +use super::form::TypeDecl; +use super::invalid_diagnostics::Invalid; +use super::invalid_diagnostics::TypeVarInRecordField; +use super::types::AnyArityFunType; +use super::types::AtomLitType; +use super::types::DictMap; +use super::types::FunType; +use super::types::ListType; +use super::types::OptProp; +use super::types::Prop; +use super::types::RecordType; +use super::types::RefinedRecordType; +use super::types::RemoteType; +use super::types::ReqProp; +use super::types::ShapeMap; +use super::types::TupleType; +use super::types::Type; +use super::types::UnionType; +use super::types::VarType; +use super::TypeConversionError; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TypeConverter { + module: SmolStr, + in_rec_decl: bool, +} + +impl TypeConverter { + pub fn new(module: SmolStr) -> Self { + TypeConverter { + module, + in_rec_decl: false, + } + } + + fn enter_rec_decl(&self) -> Self { + TypeConverter { + module: self.module.clone(), + in_rec_decl: true, + } + } + + pub fn convert_spec(&self, spec: ExternalFunSpec) -> Result { + let ty = self.convert_cft(spec.types.into_iter().next().unwrap())?; + Ok(FunSpec { + location: spec.location, + id: spec.id, + ty, + }) + } + + pub fn convert_overloaded_spec( + &self, + spec: ExternalFunSpec, + ) -> Result { + let tys = spec + .types + .into_iter() + .map(|ty| self.convert_cft(ty)) + .collect::>()?; + Ok(OverloadedFunSpec { + location: spec.location, + id: spec.id, + tys, + }) + } + + pub fn convert_callback(&self, cb: ExternalCallback) -> Result { + let tys = cb + .types + .into_iter() + .map(|ty| self.convert_cft(ty)) + .collect::>()?; + Ok(Callback { + location: cb.location, + id: cb.id, + tys, + }) + } + + fn convert_cft(&self, cft: ConstrainedFunType) -> Result { + let var_names = self.collect_var_names_in_fun_type(&cft.ty); + let subst = self.collect_substitution(&var_names); + // Invalid declarations should only appear when converting record declarations + let ft = self + .convert_fun_type(&subst, cft.ty)? + .map_err(|e| TypeConversionError::ErrorInFunType(e))?; + Ok(FunType { + forall: subst.into_values().collect(), + ..ft + }) + } + + pub fn convert_rec_decl( + &self, + decl: ExternalRecDecl, + ) -> Result, TypeConversionError> { + let new_context = self.enter_rec_decl(); + let result = decl + .fields + .into_iter() + .map(|field| new_context.convert_rec_field(field)) + .collect::, _>, _>>()?; + match result { + Ok(fields) => { + let refinable = fields.iter().any(|field| field.refinable); + Ok(Ok(RecDecl { + name: decl.name, + fields, + refinable, + location: decl.location, + })) + } + Err(e) => Ok(Err(InvalidConvertTypeInRecDecl { + name: decl.name, + te: e, + location: decl.location, + })), + } + } + + fn is_refinable(&self, field: &ExternalRecField) -> bool { + if let Some(ExtType::RemoteExtType(rt)) = &field.tp { + rt.id.module == "eqwalizer" && rt.id.name == "refinable" && rt.id.arity == 1 + } else { + false + } + } + + fn convert_rec_field( + &self, + field: ExternalRecField, + ) -> Result, TypeConversionError> { + let refinable = self.is_refinable(&field); + let tp = { + if let Some(typ) = field.tp { + match self.convert_type(&FxHashMap::default(), typ)? { + Ok(ty) => Some(ty), + Err(invalid) => return Ok(Err(invalid)), + } + } else { + None + } + }; + Ok(Ok(RecField { + name: field.name, + tp, + default_value: field.default_value, + refinable, + })) + } + + pub fn convert_type_decl( + &self, + decl: ExternalTypeDecl, + ) -> Result { + let sub = self.collect_substitution(&decl.params); + let params = self.collect_vars(&decl.params); + let result = self.convert_type(&sub, decl.body)?; + // Invalid declarations should only appear when converting record declarations + let body = result.map_err(|e| TypeConversionError::ErrorInTypeDecl(e))?; + Ok(TypeDecl { + id: decl.id, + params, + body, + location: decl.location, + }) + } + + pub fn convert_opaque_decl_public(&self, decl: ExternalOpaqueDecl) -> OpaqueTypeDecl { + OpaqueTypeDecl { + location: decl.location, + id: decl.id, + } + } + + pub fn convert_opaque_private( + &self, + decl: ExternalOpaqueDecl, + ) -> Result { + let sub = self.collect_substitution(&decl.params); + let params = self.collect_vars(&decl.params); + let result = self.convert_type(&sub, decl.body)?; + // Invalid declarations should only appear when converting record declarations + let body = result.map_err(|e| TypeConversionError::ErrorInTypeDecl(e))?; + Ok(TypeDecl { + id: decl.id, + params, + body, + location: decl.location, + }) + } + + fn collect_substitution(&self, vars: &Vec) -> FxHashMap { + vars.iter() + .enumerate() + .map(|(n, name)| (name.clone(), n as u32)) + .collect() + } + + fn collect_vars(&self, vars: &Vec) -> Vec { + vars.iter() + .enumerate() + .map(|(n, name)| VarType { + n: n as u32, + name: name.clone(), + }) + .collect() + } + + fn convert_fun_type( + &self, + sub: &FxHashMap, + ty: FunExtType, + ) -> Result, TypeConversionError> { + let args_conv = self.convert_types(sub, ty.arg_tys)?; + let res_conv = self.convert_type(sub, *ty.res_ty)?; + Ok(args_conv.and_then(|arg_tys| { + res_conv.map(|res_ty| FunType { + forall: vec![], + arg_tys, + res_ty: Box::new(res_ty), + }) + })) + } + + fn convert_types( + &self, + sub: &FxHashMap, + tys: Vec, + ) -> Result, Invalid>, TypeConversionError> { + tys.into_iter() + .map(|ty| self.convert_type(sub, ty)) + .collect::, _>, _>>() + } + + fn convert_type( + &self, + sub: &FxHashMap, + ty: ExtType, + ) -> Result, TypeConversionError> { + match ty { + ExtType::AtomLitExtType(atom) => { + Ok(Ok(Type::AtomLitType(AtomLitType { atom: atom.atom }))) + } + ExtType::FunExtType(ft) => self + .convert_fun_type(sub, ft) + .map(|res| res.map(|ty| Type::FunType(ty))), + ExtType::AnyArityFunExtType(ft) => { + Ok(self.convert_type(sub, *ft.res_ty)?.map(|res_ty| { + Type::AnyArityFunType(AnyArityFunType { + res_ty: Box::new(res_ty), + }) + })) + } + ExtType::TupleExtType(tys) => Ok(self + .convert_types(sub, tys.arg_tys)? + .map(|arg_tys| Type::TupleType(TupleType { arg_tys }))), + ExtType::ListExtType(ty) => Ok(self + .convert_type(sub, *ty.t)? + .map(|t| Type::ListType(ListType { t: Box::new(t) }))), + ExtType::UnionExtType(tys) => Ok(self + .convert_types(sub, tys.tys)? + .map(|tys| Type::UnionType(UnionType { tys }))), + ExtType::RemoteExtType(ty) + if ty.id.module == "eqwalizer" + && ty.id.name == "refinable" + && ty.id.arity == 1 + && ty.args.len() == 1 => + { + self.convert_type(sub, ty.args.into_iter().next().unwrap()) + } + ExtType::RemoteExtType(ty) => Ok(self + .convert_types(sub, ty.args)? + .map(|arg_tys| Type::RemoteType(RemoteType { id: ty.id, arg_tys }))), + ExtType::VarExtType(var) => match sub.get(&var.name) { + Some(id) => Ok(Ok(Type::VarType(VarType { + n: id.clone(), + name: var.name, + }))), + None if self.in_rec_decl => { + Ok(Err(Invalid::TypeVarInRecordField(TypeVarInRecordField { + location: var.location, + name: var.name, + }))) + } + None => Err(TypeConversionError::UnexpectedVariable(var.name)), + }, + ExtType::RecordExtType(ty) => Ok(Ok(Type::RecordType(RecordType { + name: ty.name, + module: self.module.clone(), + }))), + ExtType::RecordRefinedExtType(ty) => { + let rec_type = RecordType { + name: ty.name, + module: self.module.clone(), + }; + let fields = ty + .refined_fields + .into_iter() + .map(|field| { + self.convert_type(sub, field.ty) + .map(|res| res.map(|map| (field.label, map))) + }) + .collect::, _>, _>>()?; + Ok(fields + .map(|fields| Type::RefinedRecordType(RefinedRecordType { rec_type, fields }))) + } + ExtType::MapExtType(ty) => { + let is_shape = { + ty.props.iter().all(|prop| { + if let ExtType::AtomLitExtType(_) = prop.key() { + prop.is_ok() + } else { + false + } + }) + }; + if is_shape { + let props = ty + .props + .into_iter() + .map(|prop| self.to_shape_prop(sub, prop)) + .collect::, _>, _>>()?; + Ok(props.map(|props| Type::ShapeMap(ShapeMap { props }))) + } else { + let prop = ty + .props + .into_iter() + .next() + .ok_or(TypeConversionError::UnexpectedEmptyMap)?; + let (prop_k, prop_t) = prop.to_pair(); + let key = self.convert_type(sub, prop_k)?; + let value = self.convert_type(sub, prop_t)?; + Ok(key.and_then(|k_type| { + value.map(|v_type| { + Type::DictMap(DictMap { + k_type: Box::new(k_type), + v_type: Box::new(v_type), + }) + }) + })) + } + } + ExtType::BuiltinExtType(ty) => Ok(Ok(Type::builtin_type(ty.name.as_str()) + .ok_or(TypeConversionError::UnknownBuiltin(ty.name.into(), 0))?)), + ExtType::IntLitExtType(_) => Ok(Ok(Type::NumberType)), + ExtType::UnOpType(_) | ExtType::BinOpType(_) => Ok(Ok(Type::NumberType)), + ExtType::LocalExtType(_) | ExtType::AnyMapExtType(_) | ExtType::AnyListExtType(_) => { + Err(TypeConversionError::UnexpectedType) + } + } + } + + fn to_shape_prop( + &self, + sub: &FxHashMap, + prop: ExtProp, + ) -> Result, TypeConversionError> { + match prop { + ExtProp::ReqExtProp(p) => { + if let ExtType::AtomLitExtType(key) = p.key { + return Ok(self + .convert_type(sub, p.tp)? + .map(|tp| Prop::ReqProp(ReqProp { key: key.atom, tp }))); + } + } + ExtProp::OptExtProp(p) => { + if let ExtType::AtomLitExtType(key) = p.key { + return Ok(self + .convert_type(sub, p.tp)? + .map(|tp| Prop::OptProp(OptProp { key: key.atom, tp }))); + } + } + _ => (), + } + Err(TypeConversionError::UnexpectedShapeProp) + } + + fn collect_var_names_in_fun_type(&self, ty: &FunExtType) -> Vec { + [ + self.collect_var_names(&ty.res_ty), + self.collect_all_var_names(&ty.arg_tys), + ] + .concat() + } + + fn collect_var_names(&self, ty: &ExtType) -> Vec { + match ty { + ExtType::VarExtType(var) => vec![var.name.clone()], + ExtType::FunExtType(ft) => self.collect_var_names_in_fun_type(ft), + ExtType::AnyArityFunExtType(ft) => self.collect_var_names(&ft.res_ty), + ExtType::TupleExtType(ty) => self.collect_all_var_names(ty.arg_tys.as_ref()), + ExtType::UnionExtType(ty) => self.collect_all_var_names(ty.tys.as_ref()), + ExtType::LocalExtType(ty) => self.collect_all_var_names(ty.args.as_ref()), + ExtType::RemoteExtType(ty) => self.collect_all_var_names(ty.args.as_ref()), + ExtType::RecordRefinedExtType(ty) => ty + .refined_fields + .iter() + .flat_map(|field| self.collect_var_names(&field.ty)) + .collect(), + ExtType::MapExtType(ty) => ty + .props + .iter() + .flat_map(|prop| { + [ + self.collect_var_names(prop.key()), + self.collect_var_names(prop.tp()), + ] + .concat() + }) + .collect(), + ExtType::ListExtType(ty) => self.collect_var_names(&ty.t), + ExtType::AtomLitExtType(_) + | ExtType::RecordExtType(_) + | ExtType::BuiltinExtType(_) + | ExtType::IntLitExtType(_) + | ExtType::AnyMapExtType(_) + | ExtType::UnOpType(_) + | ExtType::BinOpType(_) + | ExtType::AnyListExtType(_) => vec![], + } + } + + fn collect_all_var_names(&self, tys: &Vec) -> Vec { + tys.iter() + .flat_map(|ty| self.collect_var_names(ty)) + .collect() + } +} diff --git a/crates/eqwalizer/src/ast/db.rs b/crates/eqwalizer/src/ast/db.rs new file mode 100644 index 0000000000..6d970b117c --- /dev/null +++ b/crates/eqwalizer/src/ast/db.rs @@ -0,0 +1,275 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::sync::Arc; + +use elp_base_db::AbsPathBuf; +use elp_base_db::AppType; +use elp_base_db::ModuleName; +use elp_base_db::ProjectId; +use elp_base_db::SourceDatabase; +use fxhash::FxHashSet; + +use super::contractivity::StubContractivityChecker; +use super::expand::StubExpander; +use super::stub::ModuleStub; +use super::trans_valid::TransitiveChecker; +use super::variance_check::VarianceChecker; +use super::Error; +use super::Id; +use super::AST; + +pub trait EqwalizerErlASTStorage { + fn get_erl_ast_bytes( + &self, + project_id: ProjectId, + module: ModuleName, + ) -> Result>, Error>; + fn get_erl_stub_bytes( + &self, + project_id: ProjectId, + module: ModuleName, + ) -> Result>, Error>; +} + +#[salsa::query_group(EqwalizerASTDatabaseStorage)] +pub trait EqwalizerASTDatabase: EqwalizerErlASTStorage + SourceDatabase { + fn from_beam(&self, project_id: ProjectId, module: ModuleName) -> bool; + + fn converted_ast(&self, project_id: ProjectId, module: ModuleName) -> Result, Error>; + fn converted_ast_bytes( + &self, + project_id: ProjectId, + module: ModuleName, + ) -> Result>, Error>; + fn converted_stub(&self, project_id: ProjectId, module: ModuleName) -> Result, Error>; + fn converted_stub_bytes( + &self, + project_id: ProjectId, + module: ModuleName, + ) -> Result>, Error>; + + fn type_ids( + &self, + project_id: ProjectId, + module: ModuleName, + ) -> Result>, Error>; + + fn expanded_stub( + &self, + project_id: ProjectId, + module: ModuleName, + ) -> Result, Error>; + fn expanded_stub_bytes( + &self, + project_id: ProjectId, + module: ModuleName, + ) -> Result>, Error>; + + fn contractive_stub( + &self, + project_id: ProjectId, + module: ModuleName, + ) -> Result, Error>; + fn contractive_stub_bytes( + &self, + project_id: ProjectId, + module: ModuleName, + ) -> Result>, Error>; + + fn covariant_stub( + &self, + project_id: ProjectId, + module: ModuleName, + ) -> Result, Error>; + fn covariant_stub_bytes( + &self, + project_id: ProjectId, + module: ModuleName, + ) -> Result>, Error>; + + fn transitive_stub( + &self, + project_id: ProjectId, + module: ModuleName, + ) -> Result, Error>; + fn transitive_stub_bytes( + &self, + project_id: ProjectId, + module: ModuleName, + ) -> Result>, Error>; +} + +fn from_beam(db: &dyn EqwalizerASTDatabase, project_id: ProjectId, module: ModuleName) -> bool { + if let Some(file_id) = db.module_index(project_id).file_for_module(&module) { + let source_root = db.file_source_root(file_id); + if let Some(app) = db.app_data(source_root) { + return app.app_type == AppType::Otp; + } + } + false +} + +fn converted_ast( + db: &dyn EqwalizerASTDatabase, + project_id: ProjectId, + module: ModuleName, +) -> Result, Error> { + let ast = db.get_erl_ast_bytes(project_id, module)?; + super::from_bytes(&ast).map(|conv_ast| Arc::new(conv_ast)) +} + +fn converted_ast_bytes( + db: &dyn EqwalizerASTDatabase, + project_id: ProjectId, + module: ModuleName, +) -> Result>, Error> { + db.converted_ast(project_id, module) + .map(|ast| Arc::new(super::to_bytes(&ast))) +} + +fn converted_stub( + db: &dyn EqwalizerASTDatabase, + project_id: ProjectId, + module: ModuleName, +) -> Result, Error> { + if db.from_beam(project_id, module.to_owned()) { + if let Some(beam_path) = beam_path(db, project_id, module.to_owned()) { + if let Ok(beam_contents) = std::fs::read(&beam_path) { + super::from_beam(&beam_contents).map(|conv_ast| Arc::new(conv_ast)) + } else { + Err(Error::BEAMNotFound(beam_path.into())) + } + } else { + Err(Error::ModuleNotFound(module.as_str().into())) + } + } else { + let stub = db.get_erl_stub_bytes(project_id, module)?; + super::from_bytes(&stub).map(|conv_stub| Arc::new(conv_stub)) + } +} + +fn converted_stub_bytes( + db: &dyn EqwalizerASTDatabase, + project_id: ProjectId, + module: ModuleName, +) -> Result>, Error> { + db.converted_stub(project_id, module) + .map(|ast| Arc::new(super::to_bytes(&ast))) +} + +fn beam_path( + db: &dyn EqwalizerASTDatabase, + project_id: ProjectId, + module: ModuleName, +) -> Option { + let file_id = db.module_index(project_id).file_for_module(&module)?; + let source_root = db.file_source_root(file_id); + let app = db.app_data(source_root)?; + let ebin = app.ebin_path.as_ref()?; + let filename = format!("{}.beam", module.as_str()); + Some(ebin.join(filename)) +} + +fn type_ids( + db: &dyn EqwalizerASTDatabase, + project_id: ProjectId, + module: ModuleName, +) -> Result>, Error> { + db.converted_stub(project_id, module) + .map(|ast| Arc::new(super::type_ids(&ast))) +} + +fn expanded_stub( + db: &dyn EqwalizerASTDatabase, + project_id: ProjectId, + module: ModuleName, +) -> Result, Error> { + let stub = db.converted_stub(project_id, module.clone())?; + let mut expander = StubExpander::new(db, project_id, true, module.as_str().into(), &stub); + expander + .expand(stub.to_vec()) + .map(|()| Arc::new(expander.stub)) + .map_err(|e| Error::TypeConversionError(e)) +} + +fn expanded_stub_bytes( + db: &dyn EqwalizerASTDatabase, + project_id: ProjectId, + module: ModuleName, +) -> Result>, Error> { + db.expanded_stub(project_id, module) + .map(|stub| Arc::new(stub.to_bytes())) +} + +fn contractive_stub( + db: &dyn EqwalizerASTDatabase, + project_id: ProjectId, + module: ModuleName, +) -> Result, Error> { + let stub = db.expanded_stub(project_id, module.clone())?; + let checker = StubContractivityChecker::new(db, project_id, module.as_str().into()); + checker + .check(&stub) + .map(|stub| Arc::new(stub)) + .map_err(|e| Error::ContractivityError(e)) +} + +fn contractive_stub_bytes( + db: &dyn EqwalizerASTDatabase, + project_id: ProjectId, + module: ModuleName, +) -> Result>, Error> { + db.contractive_stub(project_id, module) + .map(|stub| Arc::new(stub.to_bytes())) +} + +fn covariant_stub( + db: &dyn EqwalizerASTDatabase, + project_id: ProjectId, + module: ModuleName, +) -> Result, Error> { + let stub = db.contractive_stub(project_id, module.clone())?; + let checker = VarianceChecker::new(db, project_id); + checker + .check(&stub) + .map(|stub| Arc::new(stub)) + .map_err(|e| Error::VarianceCheckError(e)) +} + +fn covariant_stub_bytes( + db: &dyn EqwalizerASTDatabase, + project_id: ProjectId, + module: ModuleName, +) -> Result>, Error> { + db.covariant_stub(project_id, module) + .map(|stub| Arc::new(stub.to_bytes())) +} + +fn transitive_stub( + db: &dyn EqwalizerASTDatabase, + project_id: ProjectId, + module: ModuleName, +) -> Result, Error> { + let stub = db.covariant_stub(project_id, module.clone())?; + let mut checker = TransitiveChecker::new(db, project_id, module.as_str().clone().into()); + checker + .check(&stub) + .map(|stub| Arc::new(stub)) + .map_err(|e| Error::TransitiveCheckError(e)) +} + +fn transitive_stub_bytes( + db: &dyn EqwalizerASTDatabase, + project_id: ProjectId, + module: ModuleName, +) -> Result>, Error> { + db.transitive_stub(project_id, module) + .map(|stub| Arc::new(stub.to_bytes())) +} diff --git a/crates/eqwalizer/src/ast/expand.rs b/crates/eqwalizer/src/ast/expand.rs new file mode 100644 index 0000000000..6aa44a8b3e --- /dev/null +++ b/crates/eqwalizer/src/ast/expand.rs @@ -0,0 +1,793 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! This module contains the first phase of stubs validation +//! +//! It ensures that type ids point to types that exist, and expand local ids +//! to remote ids by adding the module they are defined in. +//! +//! It also performs several checks on types, such as ensuring that the same +//! type variable does not appear twice in the parameters of a type. + +use elp_base_db::ModuleName; +use elp_base_db::ProjectId; +use elp_syntax::SmolStr; +use fxhash::FxHashMap; +use fxhash::FxHashSet; + +use super::convert_types::TypeConverter; +use super::db::EqwalizerASTDatabase; +use super::ext_types::AnyArityFunExtType; +use super::ext_types::ConstrainedFunType; +use super::ext_types::ExtProp; +use super::ext_types::ExtType; +use super::ext_types::FunExtType; +use super::ext_types::ListExtType; +use super::ext_types::MapExtType; +use super::ext_types::OptExtProp; +use super::ext_types::RecordRefinedExtType; +use super::ext_types::RefinedField; +use super::ext_types::RemoteExtType; +use super::ext_types::ReqExtProp; +use super::ext_types::TupleExtType; +use super::ext_types::UnionExtType; +use super::ext_types::VarExtType; +use super::form::ExternalCallback; +use super::form::ExternalForm; +use super::form::ExternalFunSpec; +use super::form::ExternalOpaqueDecl; +use super::form::ExternalRecDecl; +use super::form::ExternalRecField; +use super::form::ExternalTypeDecl; +use super::form::InvalidForm; +use super::form::InvalidFunSpec; +use super::form::InvalidRecDecl; +use super::form::InvalidTypeDecl; +use super::form::TypeDecl; +use super::invalid_diagnostics::BadMapKey; +use super::invalid_diagnostics::Invalid; +use super::invalid_diagnostics::RecursiveConstraint; +use super::invalid_diagnostics::RepeatedTyVarInTyDecl; +use super::invalid_diagnostics::TyVarWithMultipleConstraints; +use super::invalid_diagnostics::UnboundTyVarInTyDecl; +use super::invalid_diagnostics::UnknownId; +use super::stub::ModuleStub; +use super::types::Type; +use super::Id; +use super::LineAndColumn; +use super::RemoteId; +use super::TextRange; +use super::TypeConversionError; +use super::AST; +use crate::ast; + +struct Expander<'d> { + module: SmolStr, + approximate: bool, + project_id: ProjectId, + db: &'d dyn EqwalizerASTDatabase, +} + +impl Expander<'_> { + fn expand_fun_spec( + &self, + fun_spec: ExternalFunSpec, + ) -> Result { + match self.expand_cfts(fun_spec.types) { + Ok(types) => Ok(ExternalFunSpec { types, ..fun_spec }), + Err(te) => Err(InvalidFunSpec { + location: fun_spec.location, + id: fun_spec.id, + te, + }), + } + } + + fn expand_cfts( + &self, + cfts: Vec, + ) -> Result, Invalid> { + cfts.into_iter().map(|cft| self.expand_cft(cft)).collect() + } + + fn expand_cft(&self, cft: ConstrainedFunType) -> Result { + let ft = { + if cft.constraints.is_empty() { + cft.ty + } else { + self.check_multiply_constrained_type_var(&cft)?; + let subst: FxHashMap = cft + .constraints + .into_iter() + .map(|c| (c.t_var, c.ty)) + .collect(); + let arg_tys = cft + .ty + .arg_tys + .into_iter() + .map(|ty| self.expand_constraints(ty, &subst, &FxHashSet::default())) + .collect::, _>>()?; + let res_ty = + self.expand_constraints(*cft.ty.res_ty, &subst, &FxHashSet::default())?; + FunExtType { + arg_tys, + res_ty: Box::new(res_ty), + location: cft.ty.location, + } + } + }; + let arg_tys = self.expand_types(ft.arg_tys)?; + let res_ty = Box::new(self.expand_type(*ft.res_ty)?); + let exp_ft = FunExtType { + arg_tys, + res_ty, + location: ft.location, + }; + Ok(ConstrainedFunType { + location: cft.location, + ty: exp_ft, + constraints: vec![], + }) + } + + fn expand_callback(&self, cb: ExternalCallback) -> Result { + match self.expand_cfts(cb.types) { + Ok(types) => Ok(ExternalCallback { types, ..cb }), + Err(te) => Err(InvalidFunSpec { + location: cb.location, + id: cb.id, + te, + }), + } + } + + fn expand_type_decl( + &self, + decl: ExternalTypeDecl, + ) -> Result { + let result = self + .validate_type_vars(&decl.location, &decl.body, &decl.params) + .and_then(|()| self.expand_type(decl.body)); + match result { + Ok(body) => Ok(ExternalTypeDecl { body, ..decl }), + Err(te) => Err(InvalidTypeDecl { + id: decl.id, + te, + location: decl.location, + }), + } + } + + fn expand_opaque_decl( + &self, + decl: ExternalOpaqueDecl, + ) -> Result { + let result = self + .validate_type_vars(&decl.location, &decl.body, &decl.params) + .and_then(|()| self.expand_type(decl.body)); + match result { + Ok(body) => Ok(ExternalOpaqueDecl { body, ..decl }), + Err(te) => Err(InvalidTypeDecl { + id: decl.id, + te, + location: decl.location, + }), + } + } + + fn validate_type_vars( + &self, + pos: &ast::Pos, + body: &ExtType, + params: &Vec, + ) -> Result<(), Invalid> { + self.check_repeated_type_param(pos, params)?; + body.visit(&|ty| match ty { + ExtType::VarExtType(ty_var) => self.check_unbound_type_var(pos, params, ty_var), + _ => Ok(()), + })?; + Ok(()) + } + + fn check_repeated_type_param( + &self, + pos: &ast::Pos, + params: &Vec, + ) -> Result<(), Invalid> { + let mut names = FxHashSet::default(); + for name in params { + if names.contains(name) { + return Err(Invalid::RepeatedTyVarInTyDecl(RepeatedTyVarInTyDecl { + location: pos.clone(), + name: name.clone(), + })); + } + names.insert(name); + } + Ok(()) + } + + fn check_multiply_constrained_type_var(&self, cft: &ConstrainedFunType) -> Result<(), Invalid> { + let mut names = FxHashSet::default(); + let vars: Vec<&SmolStr> = cft.constraints.iter().map(|c| &c.t_var).collect(); + for name in vars { + if names.contains(name) { + return Err(Invalid::TyVarWithMultipleConstraints( + TyVarWithMultipleConstraints { + location: cft.location.clone(), + n: name.clone(), + }, + )); + } + names.insert(name); + } + Ok(()) + } + + fn check_unbound_type_var( + &self, + pos: &ast::Pos, + params: &Vec, + ty_var: &VarExtType, + ) -> Result<(), Invalid> { + if !params.contains(&ty_var.name) { + Err(Invalid::UnboundTyVarInTyDecl(UnboundTyVarInTyDecl { + location: pos.clone(), + name: ty_var.name.clone(), + })) + } else { + Ok(()) + } + } + + fn expand_rec_decl(&self, decl: ExternalRecDecl) -> Result { + let fields = decl + .fields + .into_iter() + .map(|field| self.expand_rec_field(field)) + .collect::, _>>(); + match fields { + Ok(fields) => Ok(ExternalRecDecl { fields, ..decl }), + Err(te) => Err(InvalidRecDecl { + location: decl.location, + name: decl.name, + te, + }), + } + } + + fn expand_types(&self, ts: Vec) -> Result, Invalid> { + ts.into_iter().map(|t| self.expand_type(t)).collect() + } + + fn expand_type(&self, t: ExtType) -> Result { + match t { + ExtType::LocalExtType(ty) => { + let id = RemoteId { + module: self.module.clone(), + name: ty.id.name, + arity: ty.id.arity, + }; + let expanded_params = self.expand_types(ty.args)?; + Ok(ExtType::RemoteExtType(RemoteExtType { + id, + args: expanded_params, + location: ty.location, + })) + } + ExtType::RemoteExtType(ty) => { + let local_id = Id { + name: ty.id.name.clone(), + arity: ty.id.arity, + }; + let is_defined = self + .db + .type_ids(self.project_id, ModuleName::new(ty.id.module.as_str())) + .map(|ids| ids.contains(&local_id)) + .unwrap_or(false); + if !is_defined { + Err(Invalid::UnknownId(UnknownId { + location: ty.location, + id: ty.id, + })) + } else { + let expanded_params = self.expand_types(ty.args)?; + Ok(ExtType::RemoteExtType(RemoteExtType { + id: ty.id, + args: expanded_params, + location: ty.location, + })) + } + } + ExtType::FunExtType(ty) => Ok(ExtType::FunExtType(FunExtType { + location: ty.location, + arg_tys: self.expand_types(ty.arg_tys)?, + res_ty: Box::new(self.expand_type(*ty.res_ty)?), + })), + ExtType::AnyArityFunExtType(ty) => { + Ok(ExtType::AnyArityFunExtType(AnyArityFunExtType { + location: ty.location, + res_ty: Box::new(self.expand_type(*ty.res_ty)?), + })) + } + ExtType::TupleExtType(ty) => Ok(ExtType::TupleExtType(TupleExtType { + location: ty.location, + arg_tys: self.expand_types(ty.arg_tys)?, + })), + ExtType::ListExtType(ty) => Ok(ExtType::ListExtType(ListExtType { + location: ty.location, + t: Box::new(self.expand_type(*ty.t)?), + })), + ExtType::AnyListExtType(ty) => Ok(ExtType::ListExtType(ListExtType { + location: ty.location.clone(), + t: Box::new(ExtType::eqwalizer_dynamic(ty.location)), + })), + ExtType::UnionExtType(ty) => Ok(ExtType::UnionExtType(UnionExtType { + location: ty.location, + tys: self.expand_types(ty.tys)?, + })), + ExtType::MapExtType(ty) => { + if self.approximate && ty.props.iter().any(|prop| !prop.is_ok()) { + Ok(ExtType::MapExtType(MapExtType { + location: ty.location.clone(), + props: vec![ExtProp::OptExtProp(OptExtProp { + location: ty.location.clone(), + key: ExtType::eqwalizer_dynamic(ty.location.clone()), + tp: ExtType::eqwalizer_dynamic(ty.location), + })], + })) + } else { + let props = ty + .props + .into_iter() + .map(|prop| self.expand_prop(prop)) + .collect::, _>>()?; + Ok(ExtType::MapExtType(MapExtType { + location: ty.location, + props, + })) + } + } + ExtType::AnyMapExtType(ty) => Ok(ExtType::MapExtType(MapExtType { + location: ty.location.clone(), + props: vec![ExtProp::OptExtProp(OptExtProp { + location: ty.location.clone(), + key: ExtType::eqwalizer_dynamic(ty.location.clone()), + tp: ExtType::eqwalizer_dynamic(ty.location), + })], + })), + ExtType::RecordRefinedExtType(ty) => { + Ok(ExtType::RecordRefinedExtType(RecordRefinedExtType { + location: ty.location, + name: ty.name, + refined_fields: ty + .refined_fields + .into_iter() + .map(|field| self.expand_refined_record_field(field)) + .collect::, _>>()?, + })) + } + ExtType::VarExtType(_) + | ExtType::BuiltinExtType(_) + | ExtType::IntLitExtType(_) + | ExtType::AtomLitExtType(_) + | ExtType::RecordExtType(_) + | ExtType::UnOpType(_) + | ExtType::BinOpType(_) => Ok(t), + } + } + + fn expand_prop(&self, prop: ExtProp) -> Result { + match prop { + ExtProp::ReqExtProp(prop) => Ok(ExtProp::ReqExtProp(ReqExtProp { + location: prop.location, + key: self.expand_type(prop.key)?, + tp: self.expand_type(prop.tp)?, + })), + ExtProp::ReqBadExtProp(prop) => Err(Invalid::BadMapKey(BadMapKey { + location: prop.location, + })), + ExtProp::OptExtProp(prop) => Ok(ExtProp::OptExtProp(OptExtProp { + location: prop.location, + key: self.expand_type(prop.key)?, + tp: self.expand_type(prop.tp)?, + })), + ExtProp::OptBadExtProp(prop) => Err(Invalid::BadMapKey(BadMapKey { + location: prop.location, + })), + } + } + + fn expand_refined_record_field(&self, field: RefinedField) -> Result { + Ok(RefinedField { + label: field.label, + ty: self.expand_type(field.ty)?, + }) + } + + fn expand_all_constraints( + &self, + ts: Vec, + sub: &FxHashMap, + stack: &FxHashSet, + ) -> Result, Invalid> { + ts.into_iter() + .map(|t| self.expand_constraints(t, sub, stack)) + .collect() + } + + fn expand_constraints( + &self, + t: ExtType, + sub: &FxHashMap, + stack: &FxHashSet, + ) -> Result { + match t { + ExtType::LocalExtType(ty) => { + let id = RemoteId { + module: self.module.clone(), + name: ty.id.name, + arity: ty.id.arity, + }; + let expanded_params = self.expand_all_constraints(ty.args, sub, stack)?; + Ok(ExtType::RemoteExtType(RemoteExtType { + id, + args: expanded_params, + location: ty.location, + })) + } + ExtType::RemoteExtType(ty) => Ok(ExtType::RemoteExtType(RemoteExtType { + location: ty.location, + id: ty.id, + args: self.expand_all_constraints(ty.args, sub, stack)?, + })), + ExtType::FunExtType(ty) => Ok(ExtType::FunExtType(FunExtType { + location: ty.location, + arg_tys: self.expand_all_constraints(ty.arg_tys, sub, stack)?, + res_ty: Box::new(self.expand_constraints(*ty.res_ty, sub, stack)?), + })), + ExtType::AnyArityFunExtType(ty) => { + Ok(ExtType::AnyArityFunExtType(AnyArityFunExtType { + location: ty.location, + res_ty: Box::new(self.expand_constraints(*ty.res_ty, sub, stack)?), + })) + } + ExtType::TupleExtType(ty) => Ok(ExtType::TupleExtType(TupleExtType { + location: ty.location, + arg_tys: self.expand_all_constraints(ty.arg_tys, sub, stack)?, + })), + ExtType::ListExtType(ty) => Ok(ExtType::ListExtType(ListExtType { + location: ty.location, + t: Box::new(self.expand_constraints(*ty.t, sub, stack)?), + })), + ExtType::UnionExtType(ty) => Ok(ExtType::UnionExtType(UnionExtType { + location: ty.location, + tys: self.expand_all_constraints(ty.tys, sub, stack)?, + })), + ExtType::MapExtType(ty) => { + if self.approximate && ty.props.iter().any(|prop| !prop.is_ok()) { + Ok(ExtType::MapExtType(MapExtType { + location: ty.location.clone(), + props: vec![ExtProp::OptExtProp(OptExtProp { + location: ty.location.clone(), + key: ExtType::eqwalizer_dynamic(ty.location.clone()), + tp: ExtType::eqwalizer_dynamic(ty.location), + })], + })) + } else { + let props = ty + .props + .into_iter() + .map(|prop| self.expand_prop_constraint(prop, sub, stack)) + .collect::, _>>()?; + Ok(ExtType::MapExtType(MapExtType { + location: ty.location, + props, + })) + } + } + ExtType::RecordRefinedExtType(ty) => { + Ok(ExtType::RecordRefinedExtType(RecordRefinedExtType { + location: ty.location, + name: ty.name, + refined_fields: ty + .refined_fields + .into_iter() + .map(|field| self.expand_refined_record_field_constraint(field, sub, stack)) + .collect::, _>>()?, + })) + } + ExtType::VarExtType(ty) => { + if stack.contains(&ty.name) { + Err(Invalid::RecursiveConstraint(RecursiveConstraint { + location: ty.location, + n: ty.name, + })) + } else { + if let Some(tp) = sub.get(&ty.name) { + let mut stack2 = stack.clone(); + stack2.insert(ty.name); + self.expand_constraints(tp.clone(), sub, &stack2) + } else { + Ok(ExtType::VarExtType(ty)) + } + } + } + ExtType::BuiltinExtType(_) + | ExtType::IntLitExtType(_) + | ExtType::AtomLitExtType(_) + | ExtType::RecordExtType(_) + | ExtType::AnyMapExtType(_) + | ExtType::UnOpType(_) + | ExtType::BinOpType(_) + | ExtType::AnyListExtType(_) => Ok(t), + } + } + + fn expand_prop_constraint( + &self, + prop: ExtProp, + sub: &FxHashMap, + stack: &FxHashSet, + ) -> Result { + match prop { + ExtProp::ReqExtProp(prop) => Ok(ExtProp::ReqExtProp(ReqExtProp { + location: prop.location, + key: self.expand_constraints(prop.key, sub, stack)?, + tp: self.expand_constraints(prop.tp, sub, stack)?, + })), + ExtProp::ReqBadExtProp(prop) => Err(Invalid::BadMapKey(BadMapKey { + location: prop.location, + })), + ExtProp::OptExtProp(prop) => Ok(ExtProp::OptExtProp(OptExtProp { + location: prop.location, + key: self.expand_constraints(prop.key, sub, stack)?, + tp: self.expand_constraints(prop.tp, sub, stack)?, + })), + ExtProp::OptBadExtProp(prop) => Err(Invalid::BadMapKey(BadMapKey { + location: prop.location, + })), + } + } + + fn expand_refined_record_field_constraint( + &self, + field: RefinedField, + sub: &FxHashMap, + stack: &FxHashSet, + ) -> Result { + Ok(RefinedField { + label: field.label, + ty: self.expand_constraints(field.ty, sub, stack)?, + }) + } + + fn expand_rec_field(&self, field: ExternalRecField) -> Result { + let tp = { + if let Some(tp) = field.tp { + Some(self.expand_type(tp)?) + } else { + None + } + }; + Ok(ExternalRecField { tp, ..field }) + } +} + +pub struct StubExpander<'d> { + expander: Expander<'d>, + type_converter: TypeConverter, + pub stub: ModuleStub, + module_file: SmolStr, + current_file: SmolStr, +} + +impl StubExpander<'_> { + pub fn new<'d>( + db: &'d dyn EqwalizerASTDatabase, + project_id: ProjectId, + approximate: bool, + module: SmolStr, + ast: &AST, + ) -> StubExpander<'d> { + let expander = Expander { + module: module.clone(), + approximate, + db, + project_id, + }; + let type_converter = TypeConverter::new(module.clone()); + let stub = ModuleStub { + module, + ..ModuleStub::default() + }; + let module_file = ast + .into_iter() + .find_map(|form| match form { + ExternalForm::File(f) => Some(f.file.clone()), + _ => None, + }) + .unwrap(); + let current_file = module_file.clone(); + return StubExpander { + expander, + type_converter, + stub, + module_file, + current_file, + }; + } + + fn add_type_decl(&mut self, t: ExternalTypeDecl) -> Result<(), TypeConversionError> { + match self.expander.expand_type_decl(t) { + Ok(decl) => { + let decl = self.type_converter.convert_type_decl(decl)?; + self.stub.types.insert(decl.id.clone(), decl); + } + Err(invalid) => { + if self.current_file == self.module_file { + self.stub + .invalid_forms + .push(InvalidForm::InvalidTypeDecl(invalid)); + } + } + } + Ok(()) + } + + fn add_opaque_decl(&mut self, t: ExternalOpaqueDecl) -> Result<(), TypeConversionError> { + match self.expander.expand_opaque_decl(t) { + Ok(decl) => { + let public_decl = self.type_converter.convert_opaque_decl_public(decl.clone()); + self.stub + .public_opaques + .insert(public_decl.id.clone(), public_decl); + let opaque_decl = self.type_converter.convert_opaque_private(decl)?; + self.stub + .private_opaques + .insert(opaque_decl.id.clone(), opaque_decl); + } + Err(invalid) => { + if self.current_file == self.module_file { + self.stub + .invalid_forms + .push(InvalidForm::InvalidTypeDecl(invalid)); + } + } + } + Ok(()) + } + + fn add_record_decl(&mut self, t: ExternalRecDecl) -> Result<(), TypeConversionError> { + match self.expander.expand_rec_decl(t) { + Ok(decl) => match self.type_converter.convert_rec_decl(decl)? { + Ok(decl) => { + self.stub.records.insert(decl.name.clone(), decl); + } + Err(invalid) => { + if self.current_file == self.module_file { + self.stub + .invalid_forms + .push(InvalidForm::InvalidConvertTypeInRecDecl(invalid)); + } + } + }, + Err(invalid) => { + if self.current_file == self.module_file { + self.stub + .invalid_forms + .push(InvalidForm::InvalidRecDecl(invalid)); + } + } + } + Ok(()) + } + + fn add_spec(&mut self, t: ExternalFunSpec) -> Result<(), TypeConversionError> { + match self.expander.expand_fun_spec(t) { + Ok(decl) => { + if decl.types.len() == 1 { + let spec = self.type_converter.convert_spec(decl)?; + self.stub.specs.insert(spec.id.clone(), spec); + } else { + let spec = self.type_converter.convert_overloaded_spec(decl)?; + self.stub.overloaded_specs.insert(spec.id.clone(), spec); + } + } + Err(invalid) => { + if self.current_file == self.module_file { + self.stub + .invalid_forms + .push(InvalidForm::InvalidFunSpec(invalid)); + } + } + } + Ok(()) + } + + fn add_callback(&mut self, cb: ExternalCallback) -> Result<(), TypeConversionError> { + match self.expander.expand_callback(cb) { + Ok(cb) => { + let cb = self.type_converter.convert_callback(cb)?; + self.stub.callbacks.push(cb); + } + Err(invalid) => { + if self.current_file == self.module_file { + self.stub + .invalid_forms + .push(InvalidForm::InvalidFunSpec(invalid)); + } + } + } + Ok(()) + } + + fn add_extra_types(&mut self) -> () { + match self.stub.module.as_str() { + "erlang" => { + let pos: ast::Pos = { + if self + .expander + .db + .from_beam(self.expander.project_id, ModuleName::new("erlang")) + { + LineAndColumn::fake().into() + } else { + TextRange::fake().into() + } + }; + Type::builtin_type_aliases("erlang") + .into_iter() + .for_each(|name| { + let body = Type::builtin_type_alias_body(&name).unwrap(); + let id = ast::Id { name, arity: 0 }; + let decl = TypeDecl { + location: pos.clone(), + id: id.clone(), + params: vec![], + body, + }; + self.stub.types.insert(id, decl); + }) + } + _ => (), + } + } + + pub fn expand(&mut self, ast: AST) -> Result<(), TypeConversionError> { + ast.into_iter() + .map(|form| match form { + ExternalForm::File(f) => Ok(self.current_file = f.file), + ExternalForm::Export(e) => Ok(self.stub.exports.extend(e.funs)), + ExternalForm::Import(i) => Ok(i.funs.into_iter().for_each(|id| { + self.stub.imports.insert(id, i.module.clone()); + })), + ExternalForm::ExportType(e) => Ok(self.stub.export_types.extend(e.types)), + ExternalForm::ExternalTypeDecl(d) => self.add_type_decl(d), + ExternalForm::ExternalOpaqueDecl(d) => self.add_opaque_decl(d), + ExternalForm::ExternalFunSpec(s) => self.add_spec(s), + ExternalForm::ExternalRecDecl(r) => self.add_record_decl(r), + ExternalForm::Behaviour(_) => Ok(()), + ExternalForm::ExternalCallback(cb) => self.add_callback(cb), + ExternalForm::ExternalOptionalCallbacks(ocb) => { + Ok(self.stub.optional_callbacks.extend(ocb.ids)) + } + ExternalForm::Module(_) + | ExternalForm::CompileExportAll(_) + | ExternalForm::FunDecl(_) + | ExternalForm::ElpMetadata(_) + | ExternalForm::EqwalizerUnlimitedRefinement(_) + | ExternalForm::EqwalizerNowarnFunction(_) + | ExternalForm::TypingAttribute(_) => Ok(()), + }) + .collect::>()?; + self.add_extra_types(); + Ok(()) + } +} diff --git a/crates/eqwalizer/src/ast/expr.rs b/crates/eqwalizer/src/ast/expr.rs new file mode 100644 index 0000000000..5924853823 --- /dev/null +++ b/crates/eqwalizer/src/ast/expr.rs @@ -0,0 +1,378 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use ast::binary_specifier; +use ast::guard; +use ast::pat; +use elp_syntax::SmolStr; +use serde::Serialize; + +use crate::ast; + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub enum Expr { + Var(Var), + AtomLit(AtomLit), + IntLit(IntLit), + FloatLit(FloatLit), + Block(Block), + Match(Match), + Tuple(Tuple), + StringLit(StringLit), + NilLit(NilLit), + Cons(Cons), + Case(Case), + If(If), + LocalCall(LocalCall), + DynCall(DynCall), + RemoteCall(RemoteCall), + LocalFun(LocalFun), + RemoteFun(RemoteFun), + DynRemoteFun(DynRemoteFun), + DynRemoteFunArity(DynRemoteFunArity), + Lambda(Lambda), + UnOp(UnOp), + BinOp(BinOp), + LComprehension(LComprehension), + BComprehension(BComprehension), + MComprehension(MComprehension), + Binary(Binary), + Catch(Catch), + TryCatchExpr(TryCatchExpr), + TryOfCatchExpr(TryOfCatchExpr), + Receive(Receive), + ReceiveWithTimeout(ReceiveWithTimeout), + RecordCreate(RecordCreate), + RecordUpdate(RecordUpdate), + RecordSelect(RecordSelect), + RecordIndex(RecordIndex), + MapCreate(MapCreate), + MapUpdate(MapUpdate), +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct Var { + pub location: ast::Pos, + pub n: SmolStr, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct AtomLit { + pub location: ast::Pos, + pub s: SmolStr, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct IntLit { + pub location: ast::Pos, + pub value: Option, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct FloatLit { + pub location: ast::Pos, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct Block { + pub location: ast::Pos, + pub body: Body, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct Body { + pub exprs: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct Match { + pub location: ast::Pos, + pub pat: pat::Pat, + pub expr: Box, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct Tuple { + pub location: ast::Pos, + pub elems: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct StringLit { + pub location: ast::Pos, + pub empty: bool, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct NilLit { + pub location: ast::Pos, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct Cons { + pub location: ast::Pos, + pub h: Box, + pub t: Box, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct Case { + pub location: ast::Pos, + pub expr: Box, + pub clauses: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct If { + pub location: ast::Pos, + pub clauses: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct LocalCall { + pub location: ast::Pos, + pub id: ast::Id, + pub args: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct DynCall { + pub location: ast::Pos, + pub f: Box, + pub args: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct RemoteCall { + pub location: ast::Pos, + pub id: ast::RemoteId, + pub args: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct LocalFun { + pub location: ast::Pos, + pub id: ast::Id, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct RemoteFun { + pub location: ast::Pos, + pub id: ast::RemoteId, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct DynRemoteFun { + pub location: ast::Pos, + pub module: Box, + pub name: Box, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct DynRemoteFunArity { + pub location: ast::Pos, + pub module: Box, + pub name: Box, + pub arity: Box, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct Lambda { + pub location: ast::Pos, + pub clauses: Vec, + pub name: Option, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct UnOp { + pub location: ast::Pos, + pub op: SmolStr, + pub arg: Box, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct BinOp { + pub location: ast::Pos, + pub op: SmolStr, + pub arg_1: Box, + pub arg_2: Box, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct LComprehension { + pub location: ast::Pos, + pub template: Box, + pub qualifiers: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct BComprehension { + pub location: ast::Pos, + pub template: Box, + pub qualifiers: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct MComprehension { + pub location: ast::Pos, + pub k_template: Box, + pub v_template: Box, + pub qualifiers: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct Binary { + pub location: ast::Pos, + pub elems: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct Catch { + pub location: ast::Pos, + pub expr: Box, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct TryCatchExpr { + pub location: ast::Pos, + pub try_body: Body, + pub catch_clauses: Vec, + pub after_body: Option, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct TryOfCatchExpr { + pub location: ast::Pos, + pub try_body: Body, + pub try_clauses: Vec, + pub catch_clauses: Vec, + pub after_body: Option, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct Receive { + pub location: ast::Pos, + pub clauses: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct ReceiveWithTimeout { + pub location: ast::Pos, + pub clauses: Vec, + pub timeout: Box, + pub timeout_body: Body, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct RecordCreate { + pub location: ast::Pos, + pub rec_name: SmolStr, + pub fields: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct RecordUpdate { + pub location: ast::Pos, + pub expr: Box, + pub rec_name: SmolStr, + pub fields: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct RecordSelect { + pub location: ast::Pos, + pub expr: Box, + pub rec_name: SmolStr, + pub field_name: SmolStr, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct RecordIndex { + pub location: ast::Pos, + pub rec_name: SmolStr, + pub field_name: SmolStr, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct MapCreate { + pub location: ast::Pos, + pub kvs: Vec<(Expr, Expr)>, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct MapUpdate { + pub location: ast::Pos, + pub map: Box, + pub kvs: Vec<(Expr, Expr)>, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct Clause { + pub location: ast::Pos, + pub pats: Vec, + pub guards: Vec, + pub body: Body, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct BinaryElem { + pub location: ast::Pos, + pub expr: Expr, + pub size: Option, + pub specifier: binary_specifier::Specifier, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub enum RecordField { + RecordFieldNamed(RecordFieldNamed), + RecordFieldGen(RecordFieldGen), +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct RecordFieldNamed { + pub name: SmolStr, + pub value: Expr, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct RecordFieldGen { + pub value: Expr, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub enum Qualifier { + LGenerate(LGenerate), + BGenerate(BGenerate), + MGenerate(MGenerate), + Filter(Filter), +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct LGenerate { + pub pat: pat::Pat, + pub expr: Expr, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct BGenerate { + pub pat: pat::Pat, + pub expr: Expr, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct MGenerate { + pub k_pat: pat::Pat, + pub v_pat: pat::Pat, + pub expr: Expr, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct Filter { + pub expr: Expr, +} diff --git a/crates/eqwalizer/src/ast/ext_types.rs b/crates/eqwalizer/src/ast/ext_types.rs new file mode 100644 index 0000000000..aae01394df --- /dev/null +++ b/crates/eqwalizer/src/ast/ext_types.rs @@ -0,0 +1,320 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_syntax::SmolStr; +use serde::Serialize; + +use super::RemoteId; +use crate::ast; + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub enum ExtType { + AtomLitExtType(AtomLitExtType), + FunExtType(FunExtType), + AnyArityFunExtType(AnyArityFunExtType), + TupleExtType(TupleExtType), + ListExtType(ListExtType), + AnyListExtType(AnyListExtType), + UnionExtType(UnionExtType), + LocalExtType(LocalExtType), + RemoteExtType(RemoteExtType), + BuiltinExtType(BuiltinExtType), + IntLitExtType(IntLitExtType), + UnOpType(UnOpType), + BinOpType(BinOpType), + VarExtType(VarExtType), + RecordExtType(RecordExtType), + RecordRefinedExtType(RecordRefinedExtType), + MapExtType(MapExtType), + AnyMapExtType(AnyMapExtType), +} +impl ExtType { + pub fn int_ext_type(location: ast::Pos) -> ExtType { + return ExtType::BuiltinExtType(BuiltinExtType { + location, + name: "integer".into(), + }); + } + + pub fn any_ext_type(location: ast::Pos) -> ExtType { + return ExtType::BuiltinExtType(BuiltinExtType { + location, + name: "any".into(), + }); + } + + pub fn char_ext_type(location: ast::Pos) -> ExtType { + return ExtType::BuiltinExtType(BuiltinExtType { + location, + name: "char".into(), + }); + } + + pub fn tuple_ext_type(location: ast::Pos) -> ExtType { + return ExtType::BuiltinExtType(BuiltinExtType { + location, + name: "tuple".into(), + }); + } + + pub fn binary_ext_type(location: ast::Pos) -> ExtType { + return ExtType::BuiltinExtType(BuiltinExtType { + location, + name: "binary".into(), + }); + } + + pub fn eqwalizer_dynamic(location: ast::Pos) -> ExtType { + let id = RemoteId { + module: "eqwalizer".into(), + name: "dynamic".into(), + arity: 0, + }; + return ExtType::RemoteExtType(RemoteExtType { + location, + id, + args: vec![], + }); + } + + pub fn visit(&self, f: &dyn Fn(&ExtType) -> Result<(), T>) -> Result<(), T> { + f(self)?; + match self { + ExtType::FunExtType(ty) => ty + .res_ty + .visit(f) + .and_then(|()| ty.arg_tys.iter().map(|ty| ty.visit(f)).collect()), + ExtType::AnyArityFunExtType(ty) => ty.res_ty.visit(f), + ExtType::TupleExtType(ty) => ty.arg_tys.iter().map(|ty| ty.visit(f)).collect(), + ExtType::UnionExtType(ty) => ty.tys.iter().map(|ty| ty.visit(f)).collect(), + ExtType::MapExtType(ty) => ty + .props + .iter() + .map(|prop| prop.key().visit(f).and_then(|()| prop.tp().visit(f))) + .collect(), + ExtType::ListExtType(ty) => ty.t.visit(f), + ExtType::RecordRefinedExtType(ty) => ty + .refined_fields + .iter() + .map(|field| field.ty.visit(f)) + .collect(), + ExtType::RemoteExtType(ty) => ty.args.iter().map(|ty| ty.visit(f)).collect(), + ExtType::AtomLitExtType(_) + | ExtType::VarExtType(_) + | ExtType::RecordExtType(_) + | ExtType::AnyMapExtType(_) + | ExtType::LocalExtType(_) + | ExtType::BuiltinExtType(_) + | ExtType::IntLitExtType(_) + | ExtType::UnOpType(_) + | ExtType::BinOpType(_) + | ExtType::AnyListExtType(_) => Ok(()), + } + } +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct AtomLitExtType { + pub location: ast::Pos, + pub atom: SmolStr, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct FunExtType { + pub location: ast::Pos, + pub arg_tys: Vec, + pub res_ty: Box, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct AnyArityFunExtType { + pub location: ast::Pos, + pub res_ty: Box, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct TupleExtType { + pub location: ast::Pos, + pub arg_tys: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct ListExtType { + pub location: ast::Pos, + pub t: Box, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct AnyListExtType { + pub location: ast::Pos, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct UnionExtType { + pub location: ast::Pos, + pub tys: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct LocalExtType { + pub location: ast::Pos, + pub id: ast::Id, + pub args: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct RemoteExtType { + pub location: ast::Pos, + pub id: ast::RemoteId, + pub args: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct BuiltinExtType { + pub location: ast::Pos, + pub name: SmolStr, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct IntLitExtType { + pub location: ast::Pos, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct UnOpType { + pub location: ast::Pos, + pub op: SmolStr, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct BinOpType { + pub location: ast::Pos, + pub op: SmolStr, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct VarExtType { + pub location: ast::Pos, + pub name: SmolStr, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct RecordExtType { + pub location: ast::Pos, + pub name: SmolStr, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct RecordRefinedExtType { + pub location: ast::Pos, + pub name: SmolStr, + pub refined_fields: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct MapExtType { + pub location: ast::Pos, + pub props: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct AnyMapExtType { + pub location: ast::Pos, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct ConstrainedFunType { + pub location: ast::Pos, + pub ty: FunExtType, + pub constraints: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct Constraint { + pub location: ast::Pos, + pub t_var: SmolStr, + pub ty: ExtType, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct RefinedField { + pub label: SmolStr, + pub ty: ExtType, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub enum ExtProp { + ReqExtProp(ReqExtProp), + ReqBadExtProp(ReqBadExtProp), + OptExtProp(OptExtProp), + OptBadExtProp(OptBadExtProp), +} +impl ExtProp { + pub fn key(&self) -> &ExtType { + match self { + ExtProp::ReqExtProp(p) => &p.key, + ExtProp::ReqBadExtProp(p) => &p.key, + ExtProp::OptExtProp(p) => &p.key, + ExtProp::OptBadExtProp(p) => &p.key, + } + } + + pub fn tp(&self) -> &ExtType { + match self { + ExtProp::ReqExtProp(p) => &p.tp, + ExtProp::ReqBadExtProp(p) => &p.tp, + ExtProp::OptExtProp(p) => &p.tp, + ExtProp::OptBadExtProp(p) => &p.tp, + } + } + + pub fn to_pair(self) -> (ExtType, ExtType) { + match self { + ExtProp::ReqExtProp(p) => (p.key, p.tp), + ExtProp::ReqBadExtProp(p) => (p.key, p.tp), + ExtProp::OptExtProp(p) => (p.key, p.tp), + ExtProp::OptBadExtProp(p) => (p.key, p.tp), + } + } + + pub fn is_ok(&self) -> bool { + match self { + ExtProp::ReqExtProp(_) | ExtProp::OptExtProp(_) => true, + ExtProp::ReqBadExtProp(_) | ExtProp::OptBadExtProp(_) => false, + } + } +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct ReqExtProp { + pub location: ast::Pos, + pub key: ExtType, + pub tp: ExtType, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct ReqBadExtProp { + pub location: ast::Pos, + pub key: ExtType, + pub tp: ExtType, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct OptExtProp { + pub location: ast::Pos, + pub key: ExtType, + pub tp: ExtType, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct OptBadExtProp { + pub location: ast::Pos, + pub key: ExtType, + pub tp: ExtType, +} diff --git a/crates/eqwalizer/src/ast/form.rs b/crates/eqwalizer/src/ast/form.rs new file mode 100644 index 0000000000..181518741e --- /dev/null +++ b/crates/eqwalizer/src/ast/form.rs @@ -0,0 +1,278 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use ast::expr; +use ast::ext_types; +use ast::types; +use elp_syntax::SmolStr; +use serde::Serialize; + +use super::invalid_diagnostics::Invalid; +use crate::ast; + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub enum InternalForm { + Module(ModuleAttr), + Export(ExportAttr), + Import(ImportAttr), + ExportType(ExportTypeAttr), + FunDecl(FunDecl), + File(FileAttr), + ElpMetadata(ElpMetadataAttr), + Behaviour(BehaviourAttr), + EqwalizerNowarnFunction(EqwalizerNowarnFunctionAttr), + EqwalizerUnlimitedRefinement(EqwalizerUnlimitedRefinementAttr), + FunSpec(FunSpec), + OverloadedFunSpec(OverloadedFunSpec), + Callback(Callback), + RecDecl(RecDecl), + OpaqueTypeDecl(OpaqueTypeDecl), + TypeDecl(TypeDecl), + InvalidForm(InvalidForm), +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub enum ExternalForm { + Module(ModuleAttr), + CompileExportAll(CompileExportAllAttr), + Export(ExportAttr), + Import(ImportAttr), + ExportType(ExportTypeAttr), + FunDecl(FunDecl), + File(FileAttr), + ElpMetadata(ElpMetadataAttr), + Behaviour(BehaviourAttr), + EqwalizerNowarnFunction(EqwalizerNowarnFunctionAttr), + EqwalizerUnlimitedRefinement(EqwalizerUnlimitedRefinementAttr), + TypingAttribute(TypingAttribute), + ExternalTypeDecl(ExternalTypeDecl), + ExternalOpaqueDecl(ExternalOpaqueDecl), + ExternalFunSpec(ExternalFunSpec), + ExternalCallback(ExternalCallback), + ExternalOptionalCallbacks(ExternalOptionalCallbacks), + ExternalRecDecl(ExternalRecDecl), +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub enum InvalidForm { + InvalidTypeDecl(InvalidTypeDecl), + InvalidFunSpec(InvalidFunSpec), + InvalidRecDecl(InvalidRecDecl), + InvalidConvertTypeInRecDecl(InvalidConvertTypeInRecDecl), +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct ModuleAttr { + pub location: ast::Pos, + pub name: SmolStr, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct ExportAttr { + pub location: ast::Pos, + pub funs: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct ImportAttr { + pub location: ast::Pos, + pub module: SmolStr, + pub funs: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct ExportTypeAttr { + pub location: ast::Pos, + pub types: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct FunDecl { + pub location: ast::Pos, + pub id: ast::Id, + pub clauses: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct FileAttr { + pub location: ast::Pos, + pub file: SmolStr, + pub start: u32, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct ElpMetadataAttr { + pub location: ast::Pos, + pub fixmes: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct Fixme { + pub comment: ast::TextRange, + pub suppression: ast::TextRange, + pub is_ignore: bool, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct BehaviourAttr { + pub location: ast::Pos, + pub name: SmolStr, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct EqwalizerNowarnFunctionAttr { + pub location: ast::Pos, + pub id: ast::Id, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct EqwalizerUnlimitedRefinementAttr { + pub location: ast::Pos, + pub id: ast::Id, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct FunSpec { + pub location: ast::Pos, + pub id: ast::Id, + pub ty: types::FunType, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct OverloadedFunSpec { + pub location: ast::Pos, + pub id: ast::Id, + pub tys: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct Callback { + pub location: ast::Pos, + pub id: ast::Id, + pub tys: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct RecDecl { + pub location: ast::Pos, + pub name: SmolStr, + pub fields: Vec, + pub refinable: bool, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct RecField { + pub name: SmolStr, + pub tp: Option, + pub default_value: Option, + pub refinable: bool, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct OpaqueTypeDecl { + pub location: ast::Pos, + pub id: ast::Id, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct TypeDecl { + pub location: ast::Pos, + pub id: ast::Id, + pub params: Vec, + pub body: types::Type, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct CompileExportAllAttr { + pub location: ast::Pos, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct TypingAttribute { + pub location: ast::Pos, + pub names: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct ExternalTypeDecl { + pub location: ast::Pos, + pub id: ast::Id, + pub params: Vec, + pub body: ext_types::ExtType, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct ExternalOpaqueDecl { + pub location: ast::Pos, + pub id: ast::Id, + pub params: Vec, + pub body: ext_types::ExtType, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct ExternalFunSpec { + pub location: ast::Pos, + pub id: ast::Id, + pub types: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct ExternalCallback { + pub location: ast::Pos, + pub id: ast::Id, + pub types: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct ExternalOptionalCallbacks { + pub location: ast::Pos, + pub ids: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct ExternalRecDecl { + pub location: ast::Pos, + pub name: SmolStr, + pub fields: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct ExternalRecField { + pub name: SmolStr, + pub tp: Option, + pub default_value: Option, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct InvalidTypeDecl { + pub location: ast::Pos, + pub id: ast::Id, + pub te: Invalid, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct InvalidFunSpec { + pub location: ast::Pos, + pub id: ast::Id, + pub te: Invalid, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct InvalidRecDecl { + pub location: ast::Pos, + pub name: SmolStr, + pub te: Invalid, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct InvalidConvertTypeInRecDecl { + pub location: ast::Pos, + pub name: SmolStr, + pub te: Invalid, +} diff --git a/crates/eqwalizer/src/ast/guard.rs b/crates/eqwalizer/src/ast/guard.rs new file mode 100644 index 0000000000..ba6f655f69 --- /dev/null +++ b/crates/eqwalizer/src/ast/guard.rs @@ -0,0 +1,158 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_syntax::SmolStr; +use serde::Serialize; + +use crate::ast; + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct Guard { + pub tests: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub enum Test { + TestVar(TestVar), + TestAtom(TestAtom), + TestNumber(TestNumber), + TestTuple(TestTuple), + TestString(TestString), + TestNil(TestNil), + TestCons(TestCons), + TestCall(TestCall), + TestRecordCreate(TestRecordCreate), + TestRecordSelect(TestRecordSelect), + TestRecordIndex(TestRecordIndex), + TestMapCreate(TestMapCreate), + TestMapUpdate(TestMapUpdate), + TestUnOp(TestUnOp), + TestBinOp(TestBinOp), + TestBinaryLit(TestBinaryLit), +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct TestVar { + pub location: ast::Pos, + pub v: SmolStr, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct TestAtom { + pub location: ast::Pos, + pub s: SmolStr, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct TestNumber { + pub location: ast::Pos, + pub lit: Option, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct TestTuple { + pub location: ast::Pos, + pub elems: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct TestString { + pub location: ast::Pos, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct TestNil { + pub location: ast::Pos, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct TestCons { + pub location: ast::Pos, + pub h: Box, + pub t: Box, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct TestCall { + pub location: ast::Pos, + pub id: ast::Id, + pub args: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct TestRecordCreate { + pub location: ast::Pos, + pub rec_name: SmolStr, + pub fields: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct TestRecordSelect { + pub location: ast::Pos, + pub rec: Box, + pub rec_name: SmolStr, + pub field_name: SmolStr, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct TestRecordIndex { + pub location: ast::Pos, + pub rec_name: SmolStr, + pub field_name: SmolStr, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct TestMapCreate { + pub location: ast::Pos, + pub kvs: Vec<(Test, Test)>, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct TestMapUpdate { + pub location: ast::Pos, + pub map: Box, + pub kvs: Vec<(Test, Test)>, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct TestUnOp { + pub location: ast::Pos, + pub op: SmolStr, + pub arg: Box, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct TestBinOp { + pub location: ast::Pos, + pub op: SmolStr, + pub arg_1: Box, + pub arg_2: Box, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct TestBinaryLit { + pub location: ast::Pos, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub enum TestRecordField { + TestRecordFieldNamed(TestRecordFieldNamed), + TestRecordFieldGen(TestRecordFieldGen), +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct TestRecordFieldNamed { + pub name: SmolStr, + pub value: Test, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct TestRecordFieldGen { + pub value: Test, +} diff --git a/crates/eqwalizer/src/ast/invalid_diagnostics.rs b/crates/eqwalizer/src/ast/invalid_diagnostics.rs new file mode 100644 index 0000000000..f6ecbbfcb5 --- /dev/null +++ b/crates/eqwalizer/src/ast/invalid_diagnostics.rs @@ -0,0 +1,90 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_syntax::SmolStr; +use serde::Serialize; + +use super::types::Type; +use crate::ast; + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub enum Invalid { + UnknownId(UnknownId), + RecursiveConstraint(RecursiveConstraint), + TyVarWithMultipleConstraints(TyVarWithMultipleConstraints), + TypeVarInRecordField(TypeVarInRecordField), + UnboundTyVarInTyDecl(UnboundTyVarInTyDecl), + RepeatedTyVarInTyDecl(RepeatedTyVarInTyDecl), + NonProductiveRecursiveTypeAlias(NonProductiveRecursiveTypeAlias), + TransitiveInvalid(TransitiveInvalid), + AliasWithNonCovariantParam(AliasWithNonCovariantParam), + BadMapKey(BadMapKey), +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct UnknownId { + pub location: ast::Pos, + pub id: ast::RemoteId, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct RecursiveConstraint { + pub location: ast::Pos, + pub n: SmolStr, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct TyVarWithMultipleConstraints { + pub location: ast::Pos, + pub n: SmolStr, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct TypeVarInRecordField { + pub location: ast::Pos, + pub name: SmolStr, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct UnboundTyVarInTyDecl { + pub location: ast::Pos, + pub name: SmolStr, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct RepeatedTyVarInTyDecl { + pub location: ast::Pos, + pub name: SmolStr, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct NonProductiveRecursiveTypeAlias { + pub location: ast::Pos, + pub name: SmolStr, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct TransitiveInvalid { + pub location: ast::Pos, + pub name: SmolStr, + pub references: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct AliasWithNonCovariantParam { + pub location: ast::Pos, + pub name: SmolStr, + pub type_var: SmolStr, + pub exps: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct BadMapKey { + pub location: ast::Pos, +} diff --git a/crates/eqwalizer/src/ast/mod.rs b/crates/eqwalizer/src/ast/mod.rs new file mode 100644 index 0000000000..f510f74045 --- /dev/null +++ b/crates/eqwalizer/src/ast/mod.rs @@ -0,0 +1,340 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::fmt; +use std::io::BufRead; +use std::io::Cursor; +use std::io::Read; +use std::path::PathBuf; + +use eetf; +use eetf::Term; +use elp_syntax::SmolStr; +use fxhash::FxHashSet; +use serde::Serialize; +use serde_with::SerializeDisplay; + +use self::form::ExternalForm; +use self::invalid_diagnostics::Invalid; + +pub mod auto_import; +pub mod binary_specifier; +pub mod compiler_macro; +pub mod contractivity; +pub mod convert; +pub mod convert_types; +pub mod db; +pub mod expand; +pub mod expr; +pub mod ext_types; +pub mod form; +pub mod guard; +pub mod invalid_diagnostics; +pub mod pat; +pub mod stub; +pub mod subst; +pub mod trans_valid; +pub mod types; +pub mod variance_check; + +pub type AST = Vec; + +#[derive(SerializeDisplay, Debug, Clone, PartialEq, Eq, Hash)] +pub struct Id { + pub name: SmolStr, + pub arity: u32, +} + +impl fmt::Display for Id { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}/{}", self.name, self.arity) + } +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq, Hash)] +pub struct RemoteId { + pub module: SmolStr, + pub name: SmolStr, + pub arity: u32, +} + +impl fmt::Display for RemoteId { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}:{}/{}", self.module, self.name, self.arity) + } +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub enum Pos { + TextRange(TextRange), + LineAndColumn(LineAndColumn), +} +impl From for Pos { + fn from(x: LineAndColumn) -> Self { + Pos::LineAndColumn(x) + } +} +impl From for Pos { + fn from(x: TextRange) -> Self { + Pos::TextRange(x) + } +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct TextRange { + pub start_byte: u32, + pub end_byte: u32, +} +impl TextRange { + pub fn fake() -> Self { + TextRange { + start_byte: 0, + end_byte: 100, + } + } +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct LineAndColumn { + pub line: u32, + pub column: u32, +} +impl LineAndColumn { + pub fn fake() -> Self { + LineAndColumn { + line: 1, + column: 100, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Error { + ParseError, + DecodeError(String), + ModuleNotFound(String), + BEAMNotFound(PathBuf), + InvalidBEAM, + ConversionError(ConversionError), + TypeConversionError(TypeConversionError), + ContractivityError(ContractivityCheckError), + VarianceCheckError(VarianceCheckError), + TransitiveCheckError(TransitiveCheckError), +} + +impl From for Error { + fn from(err: eetf::DecodeError) -> Self { + let message = err.to_string(); + Error::DecodeError(message) + } +} + +impl From for Error { + fn from(err: ConversionError) -> Self { + Error::ConversionError(err) + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let message: String = match self { + Error::DecodeError(msg) => { + format!("EETF decoding failed with {}", msg) + } + err => format!("{:?}", err), + }; + write!(f, "eqWAlizer error:\n{}", message) + } +} + +impl std::error::Error for Error {} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ConversionError { + InvalidForm, + InvalidDecode, + InvalidLocation, + InvalidID, + InvalidVarName, + InvalidName, + InvalidFixme, + InvalidClause, + InvalidRecordField, + InvalidAtomLit, + InvalidIntLit, + InvalidExpr, + InvalidRecordUpdateField, + InvalidMapAssoc, + InvalidKV, + InvalidRecordFieldExpr, + InvalidRecordFieldName, + InvalidBinaryElem, + InvalidPattern, + InvalidPatBinaryElem, + InvalidPatRecordFieldGen, + InvalidPatRecordFieldNamed, + InvalidKVPattern, + InvalidGuard, + InvalidTest, + InvalidRecordFieldTest, + InvalidKVTest, + InvalidFunSpec, + InvalidFunConstraint, + InvalidPropType, + InvalidRecordRefinedField, + InvalidType, + InvalidForms, + UnknownBuiltin(String, usize), +} + +impl fmt::Display for ConversionError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let message: String = match self { + ConversionError::UnknownBuiltin(name, arity) => { + format!("unknown builtin {}/{}", name, arity) + } + // All other cases are variants without parameters, + // printing their name is enough info to debug + err => format!("{:?}", err), + }; + write!(f, "eqWAlizer AST conversion failed with {}", message) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TypeConversionError { + ErrorInFunType(Invalid), + ErrorInTypeDecl(Invalid), + UnexpectedVariable(SmolStr), + UnexpectedEmptyMap, + UnexpectedType, + UnknownBuiltin(String, usize), + UnexpectedShapeProp, +} + +impl fmt::Display for TypeConversionError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let message: String = match self { + TypeConversionError::UnknownBuiltin(name, arity) => { + format!("unknown builtin {}/{}", name, arity) + } + err => format!("{:?}", err), + }; + write!(f, "eqWAlizer stub expansion failed with {}", message) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ContractivityCheckError { + UnexpectedType, + UnexpectedID(RemoteId), + NonEmptyForall, +} + +impl fmt::Display for ContractivityCheckError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let message: String = match self { + ContractivityCheckError::UnexpectedID(rid) => { + format!("unknown ID {}", rid) + } + err => format!("{:?}", err), + }; + write!(f, "eqWAlizer contractivity check failed with {}", message) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum VarianceCheckError { + UnexpectedID(RemoteId), +} + +impl fmt::Display for VarianceCheckError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let message: String = match self { + VarianceCheckError::UnexpectedID(rid) => { + format!("unknown ID {}", rid) + } + }; + write!(f, "eqWAlizer variance check failed with {}", message) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TransitiveCheckError { + UnexpectedOpaqueType, +} + +impl fmt::Display for TransitiveCheckError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let message: String = format!("{:?}", self); + write!(f, "eqWAlizer transitive check failed with {}", message) + } +} + +pub fn from_bytes(bytes: &Vec) -> Result { + let term = eetf::Term::decode(Cursor::new(bytes))?; + if let Term::Tuple(res) = term { + if let [Term::Atom(ok), forms, _] = &res.elements[..] { + if ok.name == "ok" { + return Ok(convert::convert_forms(forms, false, false)?); + } + } + } + return Err(Error::ConversionError(ConversionError::InvalidDecode)); +} + +pub fn from_beam(bytes: &Vec) -> Result { + let mut cursor = Cursor::new(bytes); + let mut buf: [u8; 4] = [0; 4]; + let mut tag: [u8; 4] = [0; 4]; + + // "FOR1" + cursor.read(&mut buf).map_err(|_| Error::InvalidBEAM)?; + if &buf != b"FOR1" { + return Err(Error::InvalidBEAM); + } + // length + cursor.read(&mut buf).map_err(|_| Error::InvalidBEAM)?; + // BEAM + cursor.read(&mut buf).map_err(|_| Error::InvalidBEAM)?; + if &buf != b"BEAM" { + return Err(Error::InvalidBEAM); + } + while (bytes.len() as u64) - cursor.position() > 8 { + cursor.read(&mut tag).map_err(|_| Error::InvalidBEAM)?; + cursor.read(&mut buf).map_err(|_| Error::InvalidBEAM)?; + let length = u32::from_be_bytes(buf); + if &tag == b"Dbgi" || &tag == b"Abst" { + let t1 = Term::decode(&mut cursor)?; + if let Term::Tuple(terms) = t1 { + if let Term::Tuple(terms) = &terms.elements[2] { + let ast = &terms.elements[0]; + return Ok(convert::convert_forms(ast, true, true)?); + } + } + } else { + cursor.consume(((length + 3) & !3) as usize); + } + } + return Err(Error::InvalidBEAM); +} + +pub fn type_ids(ast: &AST) -> FxHashSet { + ast.into_iter() + .filter_map(|form| match form { + ExternalForm::ExternalTypeDecl(d) => Some(d.id.clone()), + ExternalForm::ExternalOpaqueDecl(d) => Some(d.id.clone()), + _ => None, + }) + .collect() +} + +pub fn to_bytes(ast: &AST) -> Vec { + return serde_json::to_vec(ast).unwrap(); +} diff --git a/crates/eqwalizer/src/ast/pat.rs b/crates/eqwalizer/src/ast/pat.rs new file mode 100644 index 0000000000..0c4dde7853 --- /dev/null +++ b/crates/eqwalizer/src/ast/pat.rs @@ -0,0 +1,149 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use ast::binary_specifier; +use ast::expr; +use ast::guard; +use elp_syntax::SmolStr; +use serde::Serialize; + +use crate::ast; + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub enum Pat { + PatWild(PatWild), + PatMatch(PatMatch), + PatTuple(PatTuple), + PatString(PatString), + PatNil(PatNil), + PatCons(PatCons), + PatInt(PatInt), + PatNumber(PatNumber), + PatAtom(PatAtom), + PatVar(PatVar), + PatRecord(PatRecord), + PatRecordIndex(PatRecordIndex), + PatUnOp(PatUnOp), + PatBinOp(PatBinOp), + PatBinary(PatBinary), + PatMap(PatMap), +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct PatWild { + pub location: ast::Pos, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct PatMatch { + pub location: ast::Pos, + pub pat: Box, + pub arg: Box, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct PatTuple { + pub location: ast::Pos, + pub elems: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct PatString { + pub location: ast::Pos, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct PatNil { + pub location: ast::Pos, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct PatCons { + pub location: ast::Pos, + pub h: Box, + pub t: Box, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct PatInt { + pub location: ast::Pos, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct PatNumber { + pub location: ast::Pos, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct PatAtom { + pub location: ast::Pos, + pub s: SmolStr, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct PatVar { + pub location: ast::Pos, + pub n: SmolStr, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct PatRecord { + pub location: ast::Pos, + pub rec_name: SmolStr, + pub fields: Vec, + pub gen: Option>, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct PatRecordIndex { + pub location: ast::Pos, + pub rec_name: SmolStr, + pub field_name: SmolStr, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct PatUnOp { + pub location: ast::Pos, + pub op: SmolStr, + pub arg: Box, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct PatBinOp { + pub location: ast::Pos, + pub op: SmolStr, + pub arg_1: Box, + pub arg_2: Box, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct PatBinary { + pub location: ast::Pos, + pub elems: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct PatBinaryElem { + pub location: ast::Pos, + pub pat: Pat, + pub size: Option, + pub specifier: binary_specifier::Specifier, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct PatRecordFieldNamed { + pub name: SmolStr, + pub pat: Pat, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct PatMap { + pub location: ast::Pos, + pub kvs: Vec<(guard::Test, Pat)>, +} diff --git a/crates/eqwalizer/src/ast/stub.rs b/crates/eqwalizer/src/ast/stub.rs new file mode 100644 index 0000000000..4dde5903cd --- /dev/null +++ b/crates/eqwalizer/src/ast/stub.rs @@ -0,0 +1,45 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_syntax::SmolStr; +use fxhash::FxHashMap; +use fxhash::FxHashSet; +use serde::Serialize; + +use super::form::Callback; +use super::form::FunSpec; +use super::form::InvalidForm; +use super::form::OpaqueTypeDecl; +use super::form::OverloadedFunSpec; +use super::form::RecDecl; +use super::form::TypeDecl; +use super::Id; + +#[derive(Serialize, Debug, Clone, PartialEq, Eq, Default)] +pub struct ModuleStub { + pub module: SmolStr, + pub exports: FxHashSet, + pub imports: FxHashMap, + pub export_types: FxHashSet, + pub private_opaques: FxHashMap, + pub public_opaques: FxHashMap, + pub types: FxHashMap, + pub specs: FxHashMap, + pub overloaded_specs: FxHashMap, + pub records: FxHashMap, + pub callbacks: Vec, + pub optional_callbacks: FxHashSet, + pub invalid_forms: Vec, +} + +impl ModuleStub { + pub fn to_bytes(&self) -> Vec { + return serde_json::to_vec(self).unwrap(); + } +} diff --git a/crates/eqwalizer/src/ast/subst.rs b/crates/eqwalizer/src/ast/subst.rs new file mode 100644 index 0000000000..9e981565b6 --- /dev/null +++ b/crates/eqwalizer/src/ast/subst.rs @@ -0,0 +1,112 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use fxhash::FxHashMap; + +use super::types::AnyArityFunType; +use super::types::DictMap; +use super::types::FunType; +use super::types::ListType; +use super::types::OpaqueType; +use super::types::OptProp; +use super::types::Prop; +use super::types::RefinedRecordType; +use super::types::RemoteType; +use super::types::ReqProp; +use super::types::ShapeMap; +use super::types::TupleType; +use super::types::Type; +use super::types::UnionType; + +pub struct Subst<'a> { + pub sub: FxHashMap, +} + +impl<'a> Subst<'a> { + pub fn apply(&self, t: Type) -> Type { + match t { + Type::FunType(ft) => { + let subst = self.subtract(&ft.forall); + Type::FunType(FunType { + forall: ft.forall, + arg_tys: subst.apply_all(ft.arg_tys), + res_ty: Box::new(subst.apply(*ft.res_ty)), + }) + } + Type::AnyArityFunType(ft) => Type::AnyArityFunType(AnyArityFunType { + res_ty: Box::new(self.apply(*ft.res_ty)), + }), + Type::TupleType(tt) => Type::TupleType(TupleType { + arg_tys: self.apply_all(tt.arg_tys), + }), + Type::ListType(lt) => Type::ListType(ListType { + t: Box::new(self.apply(*lt.t)), + }), + Type::UnionType(ut) => Type::UnionType(UnionType { + tys: self.apply_all(ut.tys), + }), + Type::RemoteType(rt) => Type::RemoteType(RemoteType { + id: rt.id, + arg_tys: self.apply_all(rt.arg_tys), + }), + Type::OpaqueType(ot) => Type::OpaqueType(OpaqueType { + id: ot.id, + arg_tys: self.apply_all(ot.arg_tys), + }), + Type::VarType(n) => { + if let Some(&typ) = self.sub.get(&n.n) { + typ.to_owned() + } else { + Type::VarType(n) + } + } + Type::ShapeMap(m) => Type::ShapeMap(ShapeMap { + props: m.props.into_iter().map(|p| self.apply_prop(p)).collect(), + }), + Type::DictMap(m) => Type::DictMap(DictMap { + k_type: Box::new(self.apply(*m.k_type)), + v_type: Box::new(self.apply(*m.v_type)), + }), + Type::RefinedRecordType(rt) => Type::RefinedRecordType(RefinedRecordType { + rec_type: rt.rec_type, + fields: rt + .fields + .into_iter() + .map(|(k, v)| (k, self.apply(v))) + .collect(), + }), + _ => t, + } + } + + fn apply_all(&self, ts: Vec) -> Vec { + ts.into_iter().map(|t| self.apply(t)).collect() + } + + fn apply_prop(&self, prop: Prop) -> Prop { + match prop { + Prop::OptProp(p) => Prop::OptProp(OptProp { + key: p.key, + tp: self.apply(p.tp), + }), + Prop::ReqProp(p) => Prop::ReqProp(ReqProp { + key: p.key, + tp: self.apply(p.tp), + }), + } + } + + fn subtract(&self, vars: &Vec) -> Subst { + let mut sub = self.sub.to_owned(); + vars.iter().for_each(|v| { + sub.remove(v); + }); + Subst { sub } + } +} diff --git a/crates/eqwalizer/src/ast/trans_valid.rs b/crates/eqwalizer/src/ast/trans_valid.rs new file mode 100644 index 0000000000..f8f4851340 --- /dev/null +++ b/crates/eqwalizer/src/ast/trans_valid.rs @@ -0,0 +1,415 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! This module performs the fourth and last step of stubs validation +//! +//! It ensures that declarations are transitively valid by propagating +//! all invalid declarations. I.e., if a type t1 depends on a type t2 +//! and t2 is invalid, then t1 will be tagged as invalid. + +use elp_base_db::ModuleName; +use elp_base_db::ProjectId; +use elp_syntax::SmolStr; +use fxhash::FxHashMap; +use fxhash::FxHashSet; + +use super::db::EqwalizerASTDatabase; +use super::form::Callback; +use super::form::FunSpec; +use super::form::InvalidForm; +use super::form::InvalidFunSpec; +use super::form::InvalidRecDecl; +use super::form::InvalidTypeDecl; +use super::form::OpaqueTypeDecl; +use super::form::OverloadedFunSpec; +use super::form::RecDecl; +use super::form::TypeDecl; +use super::invalid_diagnostics::Invalid; +use super::invalid_diagnostics::TransitiveInvalid; +use super::stub::ModuleStub; +use super::types::Type; +use super::Id; +use super::RemoteId; +use super::TransitiveCheckError; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +enum Ref { + RidRef(RemoteId), + RecRef(SmolStr, SmolStr), +} + +impl Ref { + fn module(&self) -> &SmolStr { + match self { + Ref::RidRef(rid) => &rid.module, + Ref::RecRef(module, _) => module, + } + } +} + +pub struct TransitiveChecker<'d> { + db: &'d dyn EqwalizerASTDatabase, + project_id: ProjectId, + module: SmolStr, + in_progress: FxHashSet, + invalid_refs: FxHashMap>, +} + +impl TransitiveChecker<'_> { + pub fn new<'d>( + db: &'d dyn EqwalizerASTDatabase, + project_id: ProjectId, + module: SmolStr, + ) -> TransitiveChecker<'d> { + return TransitiveChecker { + db, + project_id, + module, + in_progress: FxHashSet::default(), + invalid_refs: FxHashMap::default(), + }; + } + + fn show_invalids(&mut self, rref: &Ref) -> Vec { + self.invalid_refs + .get(&rref) + .unwrap() + .iter() + .map(|inv| self.show(inv)) + .collect() + } + + fn check_type_decl( + &mut self, + stub: &mut ModuleStub, + t: &TypeDecl, + ) -> Result<(), TransitiveCheckError> { + let rref = Ref::RidRef(RemoteId { + module: self.module.clone(), + name: t.id.name.clone(), + arity: t.id.arity, + }); + if !self.is_valid(&rref)? { + let invalids = self.show_invalids(&rref); + let diag = Invalid::TransitiveInvalid(TransitiveInvalid { + location: t.location.clone(), + name: t.id.to_string().into(), + references: invalids, + }); + stub.types.remove(&t.id); + stub.invalid_forms + .push(InvalidForm::InvalidTypeDecl(InvalidTypeDecl { + location: t.location.clone(), + id: t.id.clone(), + te: diag, + })) + } + Ok(()) + } + + fn check_private_opaque_decl( + &mut self, + stub: &mut ModuleStub, + t: &TypeDecl, + ) -> Result<(), TransitiveCheckError> { + let rref = Ref::RidRef(RemoteId { + module: self.module.clone(), + name: t.id.name.clone(), + arity: t.id.arity, + }); + if !self.is_valid(&rref)? { + let invalids = self.show_invalids(&rref); + let diag = Invalid::TransitiveInvalid(TransitiveInvalid { + location: t.location.clone(), + name: t.id.to_string().into(), + references: invalids, + }); + stub.private_opaques.remove(&t.id); + stub.invalid_forms + .push(InvalidForm::InvalidTypeDecl(InvalidTypeDecl { + location: t.location.clone(), + id: t.id.clone(), + te: diag, + })) + } + Ok(()) + } + + fn check_public_opaque_decl( + &mut self, + stub: &mut ModuleStub, + t: &OpaqueTypeDecl, + ) -> Result<(), TransitiveCheckError> { + let rref = Ref::RidRef(RemoteId { + module: self.module.clone(), + name: t.id.name.clone(), + arity: t.id.arity, + }); + if !self.is_valid(&rref)? { + stub.public_opaques.remove(&t.id); + } + Ok(()) + } + + fn check_spec( + &mut self, + stub: &mut ModuleStub, + spec: &FunSpec, + ) -> Result<(), TransitiveCheckError> { + let mut invalids = FxHashSet::default(); + self.collect_invalid_references( + &mut invalids, + &self.module.clone(), + &Type::FunType(spec.ty.to_owned()), + )?; + if !invalids.is_empty() { + let references = invalids.iter().map(|rref| self.show(rref)).collect(); + let diag = Invalid::TransitiveInvalid(TransitiveInvalid { + location: spec.location.clone(), + name: spec.id.to_string().into(), + references, + }); + stub.specs.remove(&spec.id); + stub.invalid_forms + .push(InvalidForm::InvalidFunSpec(InvalidFunSpec { + location: spec.location.clone(), + id: spec.id.clone(), + te: diag, + })) + } + Ok(()) + } + + fn check_record_decl( + &mut self, + stub: &mut ModuleStub, + t: &RecDecl, + ) -> Result<(), TransitiveCheckError> { + let rref = Ref::RecRef(self.module.clone(), t.name.clone()); + if !self.is_valid(&rref)? { + let invalids = self.show_invalids(&rref); + let diag = Invalid::TransitiveInvalid(TransitiveInvalid { + location: t.location.clone(), + name: t.name.clone(), + references: invalids, + }); + stub.records.remove(&t.name); + stub.invalid_forms + .push(InvalidForm::InvalidRecDecl(InvalidRecDecl { + location: t.location.clone(), + name: t.name.clone(), + te: diag, + })) + } + Ok(()) + } + + fn check_overloaded_spec( + &mut self, + stub: &mut ModuleStub, + spec: &OverloadedFunSpec, + ) -> Result<(), TransitiveCheckError> { + let mut invalids = FxHashSet::default(); + for ty in spec.tys.iter() { + self.collect_invalid_references( + &mut invalids, + &self.module.clone(), + &Type::FunType(ty.to_owned()), + )?; + } + if !invalids.is_empty() { + let references = invalids.iter().map(|rref| self.show(rref)).collect(); + let diag = Invalid::TransitiveInvalid(TransitiveInvalid { + location: spec.location.clone(), + name: spec.id.to_string().into(), + references, + }); + stub.overloaded_specs.remove(&spec.id); + stub.invalid_forms + .push(InvalidForm::InvalidFunSpec(InvalidFunSpec { + location: spec.location.clone(), + id: spec.id.clone(), + te: diag, + })) + } + Ok(()) + } + + fn check_callback( + &mut self, + stub: &mut ModuleStub, + cb: &Callback, + ) -> Result<(), TransitiveCheckError> { + let mut filtered_tys = vec![]; + for ty in cb.tys.iter() { + let mut invalids = FxHashSet::default(); + self.collect_invalid_references( + &mut invalids, + &self.module.clone(), + &Type::FunType(ty.to_owned()), + )?; + if invalids.is_empty() { + filtered_tys.push(ty.clone()) + } + } + let new_cb = Callback { + location: cb.location.clone(), + id: cb.id.clone(), + tys: filtered_tys, + }; + stub.callbacks.push(new_cb); + Ok(()) + } + + fn is_valid(&mut self, rref: &Ref) -> Result { + if self.in_progress.contains(rref) { + return Ok(true); + } + if let Some(invs) = self.invalid_refs.get(rref) { + return Ok(invs.is_empty()); + } + self.in_progress.insert(rref.clone()); + let mut invalids = FxHashSet::default(); + match self + .db + .covariant_stub(self.project_id, ModuleName::new(rref.module().as_str())) + { + Ok(stub) => match rref { + Ref::RidRef(rid) => { + let id = Id { + name: rid.name.clone(), + arity: rid.arity, + }; + match stub.types.get(&id) { + Some(tdecl) => self.collect_invalid_references( + &mut invalids, + &rid.module, + &tdecl.body, + )?, + None => match stub.private_opaques.get(&id) { + Some(tdecl) => self.collect_invalid_references( + &mut invalids, + &rid.module, + &tdecl.body, + )?, + None => { + invalids.insert(rref.clone()); + } + }, + } + } + Ref::RecRef(module, rec_name) => match stub.records.get(rec_name) { + Some(rdecl) => { + for field in rdecl.fields.iter() { + if let Some(ty) = &field.tp { + self.collect_invalid_references(&mut invalids, module, ty)?; + } + } + } + None => { + invalids.insert(rref.clone()); + } + }, + }, + Err(_) => { + invalids.insert(rref.clone()); + } + }; + let has_invalids = invalids.is_empty(); + self.in_progress.remove(rref); + self.invalid_refs.insert(rref.clone(), invalids); + Ok(has_invalids) + } + + fn collect_invalid_references( + &mut self, + refs: &mut FxHashSet, + module: &SmolStr, + ty: &Type, + ) -> Result<(), TransitiveCheckError> { + match ty { + Type::RemoteType(rt) => { + for arg in rt.arg_tys.iter() { + self.collect_invalid_references(refs, module, arg)?; + } + let rref = Ref::RidRef(rt.id.clone()); + if !self.is_valid(&rref)? { + refs.insert(rref); + } + } + Type::OpaqueType(_) => { + return Err(TransitiveCheckError::UnexpectedOpaqueType); + } + Type::RecordType(rt) => { + let rref = Ref::RecRef(module.clone(), rt.name.clone()); + if !self.is_valid(&rref)? { + refs.insert(rref); + } + } + Type::RefinedRecordType(rt) => { + let rref = Ref::RecRef(module.clone(), rt.rec_type.name.clone()); + for (_, ty) in rt.fields.iter() { + self.collect_invalid_references(refs, module, ty)?; + } + if !self.is_valid(&rref)? { + refs.insert(rref); + } + } + ty => ty.visit_children(&mut |ty| self.collect_invalid_references(refs, module, ty))?, + } + Ok(()) + } + + fn show(&self, rref: &Ref) -> SmolStr { + match rref { + Ref::RidRef(rid) if rid.module == self.module => Id { + name: rid.name.clone(), + arity: rid.arity, + } + .to_string() + .into(), + Ref::RidRef(rid) => rid.to_string().into(), + Ref::RecRef(_, name) => format!("#{}{{}}", name).into(), + } + } + + pub fn check(&mut self, stub: &ModuleStub) -> Result { + let mut stub_result = stub.clone(); + stub_result.callbacks = vec![]; + stub.types + .iter() + .map(|(_, decl)| self.check_type_decl(&mut stub_result, decl)) + .collect::, _>>()?; + stub.private_opaques + .iter() + .map(|(_, decl)| self.check_private_opaque_decl(&mut stub_result, decl)) + .collect::, _>>()?; + stub.public_opaques + .iter() + .map(|(_, decl)| self.check_public_opaque_decl(&mut stub_result, decl)) + .collect::, _>>()?; + stub.records + .iter() + .map(|(_, decl)| self.check_record_decl(&mut stub_result, decl)) + .collect::, _>>()?; + stub.specs + .iter() + .map(|(_, spec)| self.check_spec(&mut stub_result, spec)) + .collect::, _>>()?; + stub.overloaded_specs + .iter() + .map(|(_, spec)| self.check_overloaded_spec(&mut stub_result, spec)) + .collect::, _>>()?; + stub.callbacks + .iter() + .map(|cb| self.check_callback(&mut stub_result, cb)) + .collect::, _>>()?; + Ok(stub_result) + } +} diff --git a/crates/eqwalizer/src/ast/types.rs b/crates/eqwalizer/src/ast/types.rs new file mode 100644 index 0000000000..6fe285221c --- /dev/null +++ b/crates/eqwalizer/src/ast/types.rs @@ -0,0 +1,381 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_syntax::SmolStr; +use fxhash::FxHashMap; +use serde::Serialize; + +use crate::ast; + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub enum Type { + AtomLitType(AtomLitType), + AnyFunType, + FunType(FunType), + AnyArityFunType(AnyArityFunType), + AnyTupleType, + TupleType(TupleType), + NilType, + ListType(ListType), + UnionType(UnionType), + RemoteType(RemoteType), + OpaqueType(OpaqueType), + VarType(VarType), + RecordType(RecordType), + RefinedRecordType(RefinedRecordType), + DictMap(DictMap), + ShapeMap(ShapeMap), + BinaryType, + AnyType, + AtomType, + DynamicType, + NoneType, + PidType, + PortType, + ReferenceType, + NumberType, +} +impl Type { + pub const fn atom_lit_type(lit: SmolStr) -> Type { + return Type::AtomLitType(AtomLitType { atom: lit }); + } + + pub const FALSE_TYPE: Type = Type::atom_lit_type(SmolStr::new_inline("false")); + + pub const TRUE_TYPE: Type = Type::atom_lit_type(SmolStr::new_inline("true")); + + pub const CHAR_TYPE: Type = Type::NumberType; + + pub const BYTE_TYPE: Type = Type::NumberType; + + pub const FLOAT_TYPE: Type = Type::NumberType; + + pub const UNDEFINED: Type = Type::atom_lit_type(SmolStr::new_inline("undefined")); + + pub fn exn_class_type() -> Type { + return Type::UnionType(UnionType { + tys: vec![ + Type::atom_lit_type(SmolStr::new_inline("error")), + Type::atom_lit_type(SmolStr::new_inline("exit")), + Type::atom_lit_type(SmolStr::new_inline("throw")), + ], + }); + } + + pub fn cls_exn_stack_type() -> Type { + return Type::UnionType(UnionType { + tys: vec![ + Type::exn_class_type(), + Type::AnyType, + Type::ListType(ListType { + t: Box::new(Type::AnyType), + }), + ], + }); + } + + pub fn cls_exn_stack_type_dynamic() -> Type { + return Type::UnionType(UnionType { + tys: vec![ + Type::exn_class_type(), + Type::DynamicType, + Type::ListType(ListType { + t: Box::new(Type::DynamicType), + }), + ], + }); + } + + pub fn builtin_type_aliases(module: &str) -> Vec { + match module { + "erlang" => vec![ + "string".into(), + "boolean".into(), + "timeout".into(), + "identifier".into(), + "mfa".into(), + "iolist".into(), + "iodata".into(), + ], + _ => vec![], + } + } + + pub fn builtin_type_alias(name: &str) -> Option { + match name { + "string" | "boolean" | "timeout" | "identifier" | "mfa" | "iolist" | "iodata" => { + let id = ast::RemoteId { + module: "erlang".into(), + name: name.into(), + arity: 0, + }; + return Some(RemoteType { + id, + arg_tys: vec![], + }); + } + _ => { + return None; + } + } + } + + pub fn builtin_type_alias_body(name: &str) -> Option { + match name { + "string" => Some(Type::ListType(ListType { + t: Box::new(Type::CHAR_TYPE), + })), + "boolean" => Some(Type::UnionType(UnionType { + tys: vec![Type::FALSE_TYPE, Type::TRUE_TYPE], + })), + "timeout" => Some(Type::UnionType(UnionType { + tys: vec![ + Type::AtomLitType(AtomLitType { + atom: "infinity".into(), + }), + Type::NumberType, + ], + })), + "identifier" => Some(Type::UnionType(UnionType { + tys: vec![Type::PidType, Type::PortType, Type::ReferenceType], + })), + "mfa" => Some(Type::TupleType(TupleType { + arg_tys: vec![Type::AtomType, Type::AtomType, Type::NumberType], + })), + "iolist" => Some(Type::ListType(ListType { + t: Box::new(Type::UnionType(UnionType { + tys: vec![ + Type::BYTE_TYPE, + Type::BinaryType, + Type::RemoteType(Type::builtin_type_alias("iolist").unwrap()), + ], + })), + })), + "iodata" => Some(Type::UnionType(UnionType { + tys: vec![ + Type::RemoteType(Type::builtin_type_alias("iolist").unwrap()), + Type::BinaryType, + ], + })), + _ => None, + } + } + + pub fn string_type() -> Type { + return Type::RemoteType(Type::builtin_type_alias("string").unwrap()); + } + + pub fn boolean_type() -> Type { + return Type::RemoteType(Type::builtin_type_alias("boolean").unwrap()); + } + + pub fn builtin_type(name: &str) -> Option { + match name { + "any" | "term" => { + return Some(Type::AnyType); + } + "atom" | "module" | "node" => { + return Some(Type::AtomType); + } + "binary" | "bitstring" | "nonempty_binary" | "nonempty_bitstring" => { + return Some(Type::BinaryType); + } + "byte" => { + return Some(Type::BYTE_TYPE); + } + "char" => { + return Some(Type::CHAR_TYPE); + } + "float" => { + return Some(Type::FLOAT_TYPE); + } + "fun" | "function" => { + return Some(Type::AnyFunType); + } + "maybe_improper_list" | "nonempty_maybe_improper_list" => { + return Some(Type::ListType(ListType { + t: Box::new(Type::AnyType), + })); + } + "pos_integer" | "neg_integer" | "non_neg_integer" | "integer" | "number" | "arity" => { + return Some(Type::NumberType); + } + "nil" => { + return Some(Type::NilType); + } + "none" | "no_return" => { + return Some(Type::NoneType); + } + "pid" => { + return Some(Type::PidType); + } + "port" => { + return Some(Type::PortType); + } + "reference" => { + return Some(Type::ReferenceType); + } + "tuple" => { + return Some(Type::AnyTupleType); + } + "nonempty_string" => { + return Some(Type::string_type()); + } + "dynamic" => { + return Some(Type::DynamicType); + } + _ => { + return Type::builtin_type_alias(name).map(|rt| Type::RemoteType(rt)); + } + } + } + + pub fn visit_children(&self, f: &mut dyn FnMut(&Type) -> Result<(), T>) -> Result<(), T> { + match self { + Type::FunType(ty) => { + f(&ty.res_ty).and_then(|()| ty.arg_tys.iter().map(|ty| f(ty)).collect()) + } + Type::AnyArityFunType(ty) => f(&ty.res_ty), + Type::TupleType(ty) => ty.arg_tys.iter().map(|ty| f(ty)).collect(), + Type::UnionType(ty) => ty.tys.iter().map(|ty| f(ty)).collect(), + Type::RemoteType(ty) => ty.arg_tys.iter().map(|ty| f(ty)).collect(), + Type::OpaqueType(ty) => ty.arg_tys.iter().map(|ty| f(ty)).collect(), + Type::ShapeMap(ty) => ty.props.iter().map(|prop| f(prop.tp())).collect(), + Type::DictMap(ty) => f(&ty.v_type).and_then(|()| f(&ty.k_type)), + Type::ListType(ty) => f(&ty.t), + Type::RefinedRecordType(ty) => ty.fields.iter().map(|(_, ty)| f(ty)).collect(), + Type::AtomLitType(_) + | Type::AnyType + | Type::AnyFunType + | Type::AnyTupleType + | Type::AtomType + | Type::NilType + | Type::RecordType(_) + | Type::VarType(_) + | Type::BinaryType + | Type::NoneType + | Type::DynamicType + | Type::PidType + | Type::PortType + | Type::ReferenceType + | Type::NumberType => Ok(()), + } + } + + pub fn traverse(&self, f: &mut dyn FnMut(&Type) -> Result<(), T>) -> Result<(), T> { + f(self)?; + self.visit_children(&mut |ty| ty.traverse(f)) + } +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct AtomLitType { + pub atom: SmolStr, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct FunType { + pub forall: Vec, + pub arg_tys: Vec, + pub res_ty: Box, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct AnyArityFunType { + pub res_ty: Box, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct TupleType { + pub arg_tys: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct ListType { + pub t: Box, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct UnionType { + pub tys: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct RemoteType { + pub id: ast::RemoteId, + pub arg_tys: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct OpaqueType { + pub id: ast::RemoteId, + pub arg_tys: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct VarType { + pub n: u32, + pub name: SmolStr, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct RecordType { + pub name: SmolStr, + pub module: SmolStr, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct RefinedRecordType { + pub rec_type: RecordType, + pub fields: FxHashMap, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct DictMap { + pub k_type: Box, + pub v_type: Box, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct ShapeMap { + pub props: Vec, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub enum Prop { + ReqProp(ReqProp), + OptProp(OptProp), +} +impl Prop { + pub fn key(&self) -> &SmolStr { + match self { + Prop::ReqProp(req) => &req.key, + Prop::OptProp(opt) => &opt.key, + } + } + + pub fn tp(&self) -> &Type { + match self { + Prop::ReqProp(req) => &req.tp, + Prop::OptProp(opt) => &opt.tp, + } + } +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct ReqProp { + pub key: SmolStr, + pub tp: Type, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq)] +pub struct OptProp { + pub key: SmolStr, + pub tp: Type, +} diff --git a/crates/eqwalizer/src/ast/variance_check.rs b/crates/eqwalizer/src/ast/variance_check.rs new file mode 100644 index 0000000000..2df06bcecd --- /dev/null +++ b/crates/eqwalizer/src/ast/variance_check.rs @@ -0,0 +1,417 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! This module performs the third step of stubs validation +//! +//! It ensures that all opaque types are covariant, and finds a witness of +//! a contravariant expansion otherwise. +//! +//! This is done according to the following steps: +//! 1. Iterate over all opaque type declarations of a stub +//! 2. Iterate over all parameters of the declaration +//! 3. For each parameter, recursively traverse the type declaration while +//! keeping track of the variance, and expand non-opaques along the way. +//! 4. When a contravariant occurrence of the parameter is found, go up the +//! stack and rebuild the original type once for every non-opaque +//! expanded during the descent. +//! +//! This has the effect of producing a list of types, where the first one +//! is the original type declaration, each successive one is an expansion +//! of its predecessor, and the final one contains a contravariant occurrence +//! of a type parameter. +//! +//! The implementation is clone and allocation heavy, but only if a +//! contravariant expansion is found, which should not happen. + +use elp_base_db::ModuleName; +use elp_base_db::ProjectId; +use fxhash::FxHashMap; + +use super::db::EqwalizerASTDatabase; +use super::form::InvalidForm; +use super::form::InvalidTypeDecl; +use super::form::TypeDecl; +use super::invalid_diagnostics::AliasWithNonCovariantParam; +use super::invalid_diagnostics::Invalid; +use super::stub::ModuleStub; +use super::subst::Subst; +use super::types::AnyArityFunType; +use super::types::DictMap; +use super::types::FunType; +use super::types::ListType; +use super::types::OpaqueType; +use super::types::OptProp; +use super::types::Prop; +use super::types::RefinedRecordType; +use super::types::RemoteType; +use super::types::ReqProp; +use super::types::ShapeMap; +use super::types::TupleType; +use super::types::Type; +use super::types::UnionType; +use super::types::VarType; +use super::Id; +use super::RemoteId; +use super::VarianceCheckError; + +pub struct VarianceChecker<'d> { + db: &'d dyn EqwalizerASTDatabase, + project_id: ProjectId, +} + +impl VarianceChecker<'_> { + pub fn new<'d>(db: &'d dyn EqwalizerASTDatabase, project_id: ProjectId) -> VarianceChecker<'d> { + return VarianceChecker { db, project_id }; + } + + fn check_opaque_decl( + &self, + stub: &mut ModuleStub, + t: &TypeDecl, + ) -> Result<(), VarianceCheckError> { + if let Some((ty_var, expansion)) = self.expands_to_contravariant(t)? { + let invalid = self.to_invalid(t, &ty_var, expansion); + stub.invalid_forms.push(invalid); + stub.private_opaques.remove(&t.id); + } + Ok(()) + } + + fn expands_to_contravariant( + &self, + decl: &TypeDecl, + ) -> Result)>, VarianceCheckError> { + for tv in &decl.params { + let expansion = self.find_contravariant_expansion(&decl.body, tv, true, &vec![])?; + if expansion.len() > 0 { + return Ok(Some((tv.clone(), expansion))); + } + } + return Ok(None); + } + + fn find_contravariant_expansion_in_tys( + &self, + tys: &mut I, + tv: &VarType, + positive: bool, + history: &Vec<&RemoteType>, + ) -> Result>, VarianceCheckError> + where + I: Iterator, + { + if let Some(ty) = tys.next() { + let expansion = self.find_contravariant_expansion(&ty, tv, positive, history)?; + if expansion.len() > 0 { + Ok(expansion + .into_iter() + .map(|ty| { + let mut tys2: Vec = tys.collect(); + tys2.insert(0, ty); + tys2 + }) + .collect()) + } else { + Ok(self + .find_contravariant_expansion_in_tys(tys, tv, positive, history)? + .into_iter() + .map(|mut exp| { + exp.insert(0, ty.clone()); + exp + }) + .collect()) + } + } else { + Ok(vec![]) + } + } + + fn find_contravariant_expansion_in_props( + &self, + props: &mut I, + tv: &VarType, + positive: bool, + history: &Vec<&RemoteType>, + ) -> Result>, VarianceCheckError> + where + I: Iterator, + { + if let Some(prop) = props.next() { + let expansion = self.find_contravariant_expansion(prop.tp(), tv, positive, history)?; + if expansion.len() > 0 { + Ok(expansion + .into_iter() + .map(|tp| { + let new_prop = match &prop { + Prop::OptProp(op) => Prop::OptProp(OptProp { + key: op.key.clone(), + tp, + }), + Prop::ReqProp(rp) => Prop::ReqProp(ReqProp { + key: rp.key.clone(), + tp, + }), + }; + let mut props2: Vec = props.collect(); + props2.insert(0, new_prop); + props2 + }) + .collect()) + } else { + Ok(self + .find_contravariant_expansion_in_props(props, tv, positive, history)? + .into_iter() + .map(|mut exp| { + exp.insert(0, prop.clone()); + exp + }) + .collect()) + } + } else { + Ok(vec![]) + } + } + + fn find_contravariant_expansion( + &self, + ty: &Type, + tv: &VarType, + positive: bool, + history: &Vec<&RemoteType>, + ) -> Result, VarianceCheckError> { + let tv_absent = ty + .traverse(&mut |t| match t { + Type::VarType(v) if v == tv => Err(()), + _ => Ok(()), + }) + .is_ok(); + if tv_absent { + return Ok(vec![]); + } + match ty { + Type::VarType(_) if !positive => Ok(vec![ty.clone()]), + Type::RemoteType(rt) => { + if history.iter().any(|&t| t == rt) { + return Ok(vec![]); + } + let mut new_history = history.clone(); + new_history.push(rt); + if let Some(tbody) = self.type_decl_body(&rt.id, &rt.arg_tys)? { + let mut exps = + self.find_contravariant_expansion(&tbody, tv, positive, &new_history)?; + if !exps.is_empty() { + exps.insert(0, ty.clone()); + } + Ok(exps) + } else { + Ok(vec![]) + } + } + Type::FunType(ft) => { + let arg_exps = self.find_contravariant_expansion_in_tys( + &mut ft.arg_tys.clone().into_iter(), + tv, + !positive, + history, + )?; + if arg_exps.is_empty() { + Ok(self + .find_contravariant_expansion(&ft.res_ty, tv, positive, history)? + .into_iter() + .map(|t| { + Type::FunType(FunType { + forall: ft.forall.clone(), + arg_tys: ft.arg_tys.clone(), + res_ty: Box::new(t), + }) + }) + .collect()) + } else { + Ok(arg_exps + .into_iter() + .map(|ts| { + Type::FunType(FunType { + forall: ft.forall.clone(), + arg_tys: ts, + res_ty: ft.res_ty.clone(), + }) + }) + .collect()) + } + } + Type::AnyArityFunType(ft) => Ok(self + .find_contravariant_expansion(&ft.res_ty, tv, positive, history)? + .into_iter() + .map(|t| { + Type::AnyArityFunType(AnyArityFunType { + res_ty: Box::new(t), + }) + }) + .collect()), + Type::TupleType(tt) => Ok(self + .find_contravariant_expansion_in_tys( + &mut tt.arg_tys.clone().into_iter(), + tv, + positive, + history, + )? + .into_iter() + .map(|arg_tys| Type::TupleType(TupleType { arg_tys })) + .collect()), + Type::ListType(lt) => Ok(self + .find_contravariant_expansion(<.t, tv, positive, history)? + .into_iter() + .map(|t| Type::ListType(ListType { t: Box::new(t) })) + .collect()), + Type::UnionType(ut) => Ok(self + .find_contravariant_expansion_in_tys( + &mut ut.tys.clone().into_iter(), + tv, + positive, + history, + )? + .into_iter() + .map(|tys| Type::UnionType(UnionType { tys })) + .collect()), + Type::OpaqueType(ot) => Ok(self + .find_contravariant_expansion_in_tys( + &mut ot.arg_tys.clone().into_iter(), + tv, + positive, + history, + )? + .into_iter() + .map(|arg_tys| { + Type::OpaqueType(OpaqueType { + id: ot.id.clone(), + arg_tys, + }) + }) + .collect()), + Type::RefinedRecordType(rt) => { + for (name, ty) in rt.fields.iter() { + let ty_exps = self.find_contravariant_expansion(ty, tv, positive, history)?; + if !ty_exps.is_empty() { + return Ok(ty_exps + .into_iter() + .map(|field_ty| { + let mut new_fields = rt.fields.clone(); + new_fields.insert(name.clone(), field_ty); + new_fields + }) + .map(|fields| { + Type::RefinedRecordType(RefinedRecordType { + rec_type: rt.rec_type.clone(), + fields, + }) + }) + .collect()); + } + } + Ok(vec![]) + } + Type::DictMap(dt) => { + let k_exps = + self.find_contravariant_expansion(&dt.k_type, tv, positive, history)?; + if k_exps.is_empty() { + let v_exps = + self.find_contravariant_expansion(&dt.v_type, tv, positive, history)?; + Ok(v_exps + .into_iter() + .map(|v_type| { + Type::DictMap(DictMap { + k_type: dt.k_type.clone(), + v_type: Box::new(v_type), + }) + }) + .collect()) + } else { + Ok(k_exps + .into_iter() + .map(|k_type| { + Type::DictMap(DictMap { + k_type: Box::new(k_type), + v_type: dt.v_type.clone(), + }) + }) + .collect()) + } + } + Type::ShapeMap(mt) => Ok(self + .find_contravariant_expansion_in_props( + &mut mt.props.clone().into_iter(), + tv, + positive, + history, + )? + .into_iter() + .map(|props| Type::ShapeMap(ShapeMap { props })) + .collect()), + Type::VarType(_) + | Type::AtomLitType(_) + | Type::AnyFunType + | Type::AnyTupleType + | Type::NilType + | Type::RecordType(_) + | Type::BinaryType + | Type::AnyType + | Type::AtomType + | Type::DynamicType + | Type::NoneType + | Type::PidType + | Type::PortType + | Type::ReferenceType + | Type::NumberType => Ok(vec![]), + } + } + + fn to_invalid(&self, t: &TypeDecl, ty_var: &VarType, expansion: Vec) -> InvalidForm { + let diagnostics = Invalid::AliasWithNonCovariantParam(AliasWithNonCovariantParam { + type_var: ty_var.name.clone(), + location: t.location.clone(), + name: t.id.to_string().into(), + exps: expansion, + }); + InvalidForm::InvalidTypeDecl(InvalidTypeDecl { + location: t.location.clone(), + id: t.id.clone(), + te: diagnostics, + }) + } + + fn type_decl_body( + &self, + id: &RemoteId, + args: &Vec, + ) -> Result, VarianceCheckError> { + let local_id = Id { + name: id.name.clone(), + arity: id.arity, + }; + let stub = self + .db + .contractive_stub(self.project_id, ModuleName::new(id.module.as_str())) + .map_err(|_| VarianceCheckError::UnexpectedID(id.clone()))?; + fn subst(decl: &TypeDecl, args: &Vec) -> Type { + let sub: FxHashMap = + decl.params.iter().map(|v| v.n).zip(args.iter()).collect(); + Subst { sub }.apply(decl.body.clone()) + } + Ok(stub.types.get(&local_id).map(|t| subst(t, args))) + } + + pub fn check(&self, stub: &ModuleStub) -> Result { + let mut stub_result = stub.clone(); + stub.private_opaques + .iter() + .map(|(_, decl)| self.check_opaque_decl(&mut stub_result, decl)) + .collect::, _>>()?; + Ok(stub_result) + } +} diff --git a/crates/eqwalizer/src/ipc.rs b/crates/eqwalizer/src/ipc.rs new file mode 100644 index 0000000000..9df04cd94b --- /dev/null +++ b/crates/eqwalizer/src/ipc.rs @@ -0,0 +1,156 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::io::BufRead; +use std::io::BufReader; +use std::io::BufWriter; +use std::io::Write; +use std::process::ChildStdin; +use std::process::ChildStdout; +use std::process::Command; +use std::process::Stdio; +use std::time::Duration; + +use anyhow::Context; +use anyhow::Result; +use fxhash::FxHashMap; +use serde::Deserialize; +use serde::Serialize; +use stdx::JodChild; +use timeout_readwrite::TimeoutReader; +use timeout_readwrite::TimeoutWriter; + +use crate::EqwalizerDiagnostic; + +#[derive(Deserialize, Debug)] +pub enum EqWAlizerASTFormat { + RawForms, + ConvertedForms, + RawStub, + ConvertedStub, + ExpandedStub, + ContractiveStub, + CovariantStub, + TransitiveStub, +} + +#[derive(Deserialize, Debug)] +#[serde(tag = "tag", content = "content")] +pub enum MsgFromEqWAlizer { + EnteringModule { + module: String, + }, + GetAstBytes { + module: String, + format: EqWAlizerASTFormat, + }, + EqwalizingStart { + module: String, + }, + EqwalizingDone { + module: String, + }, + Dependencies { + modules: Vec, + }, + Done { + diagnostics: FxHashMap>, + }, +} + +#[derive(Serialize, Debug)] +#[serde(tag = "tag", content = "content")] +pub enum MsgToEqWAlizer { + ELPEnteringModule, + ELPExitingModule, + GetAstBytesReply { ast_bytes_len: u32 }, + CannotCompleteRequest, +} + +pub struct IpcHandle { + writer: BufWriter>, + reader: BufReader>, + _child_for_drop: JodChild, +} + +const WRITE_TIMEOUT: Duration = Duration::from_secs(5); +const READ_TIMEOUT: Duration = Duration::from_secs(240); + +impl IpcHandle { + pub fn from_command(cmd: &mut Command) -> Result { + cmd.stdin(Stdio::piped()) + .stdout(Stdio::piped()) + // for debugging purposes + .stderr(Stdio::inherit()); + + let mut child = cmd.spawn()?; + let stdin = child + .stdin + .take() + .context("failed to get stdin for eqwalizer process")?; + let stdout = child + .stdout + .take() + .context("failed to get stdout for eqwalizer process")?; + + let _child_for_drop = JodChild(child); + let writer = BufWriter::new(TimeoutWriter::new(stdin, WRITE_TIMEOUT)); + let reader = BufReader::new(TimeoutReader::new(stdout, READ_TIMEOUT)); + + Ok(Self { + writer, + reader, + _child_for_drop, + }) + } + + pub fn receive(&mut self) -> Result { + let buf = self.receive_line().context("receiving message")?; + let deserialized = + serde_json::from_str(&buf).expect("failed to parse stdout from eqwalizer"); + Ok(deserialized) + } + + pub fn receive_newline(&mut self) -> Result<()> { + let _ = self.receive_line().context("receiving newline")?; + Ok(()) + } + + pub fn send(&mut self, msg: &MsgToEqWAlizer) -> Result<()> { + let msg = serde_json::to_string(msg).expect("failed to serialize msg to eqwalizer"); + writeln!(self.writer, "{}", msg).with_context(|| format!("writing message: {:?}", msg))?; + self.writer + .flush() + .with_context(|| format!("flushing message: {:?}", msg))?; + Ok(()) + } + + pub fn send_bytes(&mut self, msg: &[u8]) -> Result<()> { + // Don't exceed pipe buffer size on Mac or Linux + // https://unix.stackexchange.com/a/11954/147568 + let chunk_size = 65_536; + for (idx, chunk) in msg.chunks(chunk_size).enumerate() { + self.writer + .write_all(chunk) + .with_context(|| format!("writing bytes chunk {} of size {}", idx, chunk.len()))?; + } + self.writer + .flush() + .with_context(|| format!("flushing bytes of size {}", msg.len()))?; + Ok(()) + } + + fn receive_line(&mut self) -> Result { + let mut buf = String::new(); + self.reader + .read_line(&mut buf) + .context("failed read_line from eqwalizer stdout")?; + Ok(buf) + } +} diff --git a/crates/eqwalizer/src/lib.rs b/crates/eqwalizer/src/lib.rs new file mode 100644 index 0000000000..c23a8e59ec --- /dev/null +++ b/crates/eqwalizer/src/lib.rs @@ -0,0 +1,603 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::env; +use std::ffi::OsString; +use std::fmt; +use std::fs; +use std::io::Write; +use std::marker::PhantomData; +use std::ops::Deref; +use std::ops::DerefMut; +use std::os::unix::prelude::PermissionsExt; +use std::path::Path; +use std::path::PathBuf; +use std::process::Command; +use std::process::ExitStatus; +use std::sync::Arc; +use std::time::Instant; + +use anyhow::Context; +use anyhow::Result; +use ast::form::ExternalForm; +use ast::Error; +use elp_base_db::ModuleName; +use elp_base_db::ProjectId; +use elp_syntax::TextRange; +use fxhash::FxHashMap; +use parking_lot::Mutex; +use serde::Deserialize; +use serde::Serialize; +use tempfile::Builder; +use tempfile::TempPath; + +pub mod ipc; +use ipc::IpcHandle; +use ipc::MsgFromEqWAlizer; +use ipc::MsgToEqWAlizer; + +use crate::ipc::EqWAlizerASTFormat; + +pub mod ast; + +// Bundle file with command to make sure it's not removed too early +#[derive(Clone)] +pub struct Eqwalizer { + cmd: OsString, + args: Vec, + pub shell: bool, + // Used only for the Drop implementation + _file: Option>, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum EqwalizerDiagnostics { + Diagnostics(FxHashMap>), + NoAst { module: String }, + Error(String), +} + +impl Default for EqwalizerDiagnostics { + fn default() -> Self { + EqwalizerDiagnostics::Diagnostics(Default::default()) + } +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct EqwalizerDiagnostic { + #[serde(deserialize_with = "deserialize_text_range")] + pub range: TextRange, + pub message: String, + pub uri: String, + pub code: String, + #[serde(rename(deserialize = "expressionOrNull"))] + pub expression: Option, + #[serde(rename(deserialize = "explanationOrNull"))] + pub explanation: Option, +} + +impl EqwalizerDiagnostics { + pub fn combine(mut self, other: &Self) -> Self { + match &mut self { + EqwalizerDiagnostics::NoAst { .. } => self, + EqwalizerDiagnostics::Error(_) => self, + EqwalizerDiagnostics::Diagnostics(diags) => match other { + EqwalizerDiagnostics::Diagnostics(other_diags) => { + diags.extend( + other_diags + .into_iter() + .map(|(k, v)| (k.to_string(), v.to_vec())), + ); + self + } + EqwalizerDiagnostics::Error(_) => other.clone(), + EqwalizerDiagnostics::NoAst { .. } => other.clone(), + }, + } + } +} + +#[derive(Serialize, Debug, PartialEq, Eq, Clone)] +pub struct EqwalizerStats { + ignores: u32, + fixmes: u32, + nowarn: u32, +} + +pub trait DbApi { + fn eqwalizing_start(&self, module: String) -> (); + fn eqwalizing_done(&self, module: String) -> (); + fn set_module_ipc_handle(&self, module: ModuleName, handle: Arc>) -> (); + fn module_ipc_handle(&self, module: ModuleName) -> Option>>; +} + +#[salsa::query_group(EqwalizerDiagnosticsDatabaseStorage)] +pub trait EqwalizerDiagnosticsDatabase: ast::db::EqwalizerASTDatabase + DbApi { + fn module_diagnostics( + &self, + project_id: ProjectId, + module: String, + ) -> (Arc, Instant); + + fn compute_eqwalizer_stats( + &self, + project_id: ProjectId, + module: ModuleName, + ) -> Option>; +} + +fn deserialize_text_range<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + #[derive(Deserialize)] + struct RawTextRange { + start: u32, + end: u32, + } + + let range = RawTextRange::deserialize(deserializer)?; + // Temporary for T148094436 + let _pctx = stdx::panic_context::enter(format!("\neqwalizer::deserialize_text_range")); + Ok(TextRange::new(range.start.into(), range.end.into())) +} + +impl Default for Eqwalizer { + fn default() -> Self { + let env = env::var("ELP_EQWALIZER_PATH"); + let (path, ext, temp_file) = if let Ok(path) = env { + let path = PathBuf::from(path); + let ext = path + .extension() + .unwrap_or_default() + .to_str() + .unwrap() + .to_string(); + (path, ext, None) + } else { + let extension = env!("ELP_EQWALIZER_EXT").to_string(); + let eqwalizer_src = include_bytes!(concat!(env!("OUT_DIR"), "/eqwalizer")); + let mut temp_file = Builder::new() + .prefix("eqwalizer") + .tempfile() + .expect("can't create eqwalizer temp executable"); + temp_file + .write_all(eqwalizer_src) + .expect("can't create eqwalizer temp executable"); + + let temp_file = temp_file.into_temp_path(); + + let mut perm = fs::metadata(&temp_file) + .expect("can't create eqwalizer temp executable") + .permissions(); + perm.set_mode(0o755); + fs::set_permissions(&temp_file, perm).expect("can't create eqwalizer temp executable"); + + (temp_file.to_path_buf(), extension, Some(temp_file)) + }; + + let (cmd, args) = match ext.as_str() { + "jar" => ( + "java".into(), + vec!["-Xss20M".into(), "-jar".into(), path.into()], + ), + "" => (path.into(), vec![]), + _ => panic!("Unknown eqwalizer executable {:?}", path), + }; + + Self { + cmd, + args, + shell: false, + _file: temp_file.map(Arc::new), + } + } +} + +impl Eqwalizer { + // Return a smart pointer to bundle lifetime with the temp file's lifetime + pub fn cmd<'file>(&'file self) -> CommandProxy<'file> { + let mut cmd = Command::new(&self.cmd); + cmd.args(&self.args); + CommandProxy::new(cmd) + } + + pub fn typecheck( + &self, + build_info_path: &Path, + db: &dyn EqwalizerDiagnosticsDatabase, + project_id: ProjectId, + modules: Vec<&str>, + ) -> EqwalizerDiagnostics { + let mut cmd = self.cmd(); + cmd.arg("ipc"); + cmd.args(modules); + cmd.env("EQWALIZER_IPC", "true"); + cmd.env("EQWALIZER_USE_ELP_CONVERTED_AST", "true"); + if self.shell { + cmd.env("EQWALIZER_ELP_SHELL", "true"); + } + add_env(&mut cmd, build_info_path, None); + + if self.shell { + match shell_typecheck(cmd, db, project_id) { + Ok(diags) => diags, + Err(err) => EqwalizerDiagnostics::Error(format!("{}", err)), + } + } else { + match do_typecheck(cmd, db, project_id) { + Ok(diags) => diags, + Err(err) => EqwalizerDiagnostics::Error(format!("{}", err)), + } + } + } + + pub fn passthrough( + &self, + args: &[String], + build_info_path: &Path, + elp_ast_dir: &Path, + ) -> Result { + let mut cmd = self.cmd(); + cmd.args(args); + add_env(&mut cmd, build_info_path, Some(elp_ast_dir)); + cmd.status() + .with_context(|| "Error in eqwalizer passthrough") + } +} + +fn do_typecheck( + mut cmd: CommandProxy, + db: &dyn EqwalizerDiagnosticsDatabase, + project_id: ProjectId, +) -> Result { + let mut handle = IpcHandle::from_command(&mut cmd) + .with_context(|| format!("starting eqWAlizer process: {:?}", cmd))?; + let _pctx = stdx::panic_context::enter(format!("\neqWAlizing with command: {:?}", cmd)); + loop { + db.unwind_if_cancelled(); + match handle.receive()? { + MsgFromEqWAlizer::GetAstBytes { module, format } => { + log::debug!( + "received from eqwalizer: GetAstBytes for module {} (format = {:?})", + module, + format + ); + let module_name = ModuleName::new(&module); + let ast = { + match format { + EqWAlizerASTFormat::RawForms => { + db.get_erl_ast_bytes(project_id, module_name) + } + EqWAlizerASTFormat::ConvertedForms => { + db.converted_ast_bytes(project_id, module_name) + } + EqWAlizerASTFormat::RawStub => { + db.get_erl_stub_bytes(project_id, module_name) + } + EqWAlizerASTFormat::ConvertedStub => { + db.converted_stub_bytes(project_id, module_name) + } + EqWAlizerASTFormat::ExpandedStub => { + db.expanded_stub_bytes(project_id, module_name) + } + EqWAlizerASTFormat::ContractiveStub => { + db.contractive_stub_bytes(project_id, module_name) + } + EqWAlizerASTFormat::CovariantStub => { + db.covariant_stub_bytes(project_id, module_name) + } + EqWAlizerASTFormat::TransitiveStub => { + db.transitive_stub_bytes(project_id, module_name) + } + } + }; + match ast { + Ok(ast_bytes) => { + log::debug!( + "sending to eqwalizer: GetAstBytesReply for module {}", + module + ); + let ast_bytes_len = ast_bytes.len().try_into()?; + let reply = &MsgToEqWAlizer::GetAstBytesReply { ast_bytes_len }; + handle.send(reply)?; + handle.receive_newline()?; + handle.send_bytes(&ast_bytes)?; + } + Err(Error::ModuleNotFound(_)) => { + log::debug!( + "module not found, sending to eqwalizer: empty GetAstBytesReply for module {}", + module + ); + let ast_bytes_len = 0; + let reply = &MsgToEqWAlizer::GetAstBytesReply { ast_bytes_len }; + handle.send(reply)?; + handle.receive_newline()?; + } + Err(Error::ParseError) => { + log::debug!( + "parse error, sending to eqwalizer: CannotCompleteRequest for module {}", + module + ); + let reply = &MsgToEqWAlizer::CannotCompleteRequest; + handle.send(reply)?; + return Ok(EqwalizerDiagnostics::NoAst { module }); + } + Err(err) => { + log::debug!( + "error {} sending to eqwalizer: CannotCompleteRequest for module {}", + err, + module + ); + let reply = &MsgToEqWAlizer::CannotCompleteRequest; + handle.send(reply)?; + return Ok(EqwalizerDiagnostics::Error(err.to_string())); + } + } + } + MsgFromEqWAlizer::EqwalizingStart { module } => db.eqwalizing_start(module), + MsgFromEqWAlizer::EqwalizingDone { module } => db.eqwalizing_done(module), + MsgFromEqWAlizer::Done { diagnostics } => { + log::debug!( + "received from eqwalizer: Done with diagnostics length {}", + diagnostics.len() + ); + return Ok(EqwalizerDiagnostics::Diagnostics(diagnostics)); + } + msg => { + log::warn!( + "received unexpected message from eqwalizer, ignoring: {:?}", + msg + ) + } + } + } +} + +fn shell_typecheck( + mut cmd: CommandProxy, + db: &dyn EqwalizerDiagnosticsDatabase, + project_id: ProjectId, +) -> Result { + // Never cache the results of this function + db.salsa_runtime().report_untracked_read(); + let handle = Arc::new(Mutex::new( + IpcHandle::from_command(&mut cmd) + .with_context(|| format!("starting eqWAlizer process: {:?}", cmd))?, + )); + let mut diagnostics = EqwalizerDiagnostics::default(); + loop { + db.unwind_if_cancelled(); + let msg = handle.lock().receive()?; + match msg { + MsgFromEqWAlizer::EnteringModule { module } => { + db.set_module_ipc_handle(ModuleName::new(&module), handle.clone()); + let diags = db.module_diagnostics(project_id, module).0; + handle.lock().send(&MsgToEqWAlizer::ELPExitingModule)?; + diagnostics = diagnostics.combine(&diags); + } + MsgFromEqWAlizer::Done { .. } => { + return Ok(diagnostics); + } + msg => { + log::warn!( + "received unexpected message from eqwalizer, ignoring: {:?}", + msg + ) + } + } + } +} + +fn module_diagnostics( + db: &dyn EqwalizerDiagnosticsDatabase, + project_id: ProjectId, + module: String, +) -> (Arc, Instant) { + // A timestamp is added to the return value to force Salsa to store new + // diagnostics, and not attempt to back-date them if they are equal to + // the memoized ones. + let timestamp = Instant::now(); + match get_module_diagnostics(db, project_id, module) { + Ok(diag) => (Arc::new(diag), timestamp), + Err(err) => ( + Arc::new(EqwalizerDiagnostics::Error(format!("{}", err))), + timestamp, + ), + } +} + +fn get_module_diagnostics( + db: &dyn EqwalizerDiagnosticsDatabase, + project_id: ProjectId, + module: String, +) -> Result { + let handle_mutex = db + .module_ipc_handle(ModuleName::new(&module)) + .ok_or(anyhow::Error::msg(format!( + "no eqWAlizer handle for module {}", + module + )))?; + let mut handle = handle_mutex.lock(); + handle.send(&MsgToEqWAlizer::ELPEnteringModule)?; + loop { + db.unwind_if_cancelled(); + match handle.receive()? { + MsgFromEqWAlizer::GetAstBytes { module, format } => { + log::debug!( + "received from eqwalizer: GetAstBytes for module {} (format = {:?})", + module, + format + ); + let module_name = ModuleName::new(&module); + let ast = { + match format { + EqWAlizerASTFormat::RawForms => { + db.get_erl_ast_bytes(project_id, module_name) + } + EqWAlizerASTFormat::ConvertedForms => { + db.converted_ast_bytes(project_id, module_name) + } + EqWAlizerASTFormat::RawStub => { + db.get_erl_stub_bytes(project_id, module_name) + } + EqWAlizerASTFormat::ConvertedStub => { + db.converted_stub_bytes(project_id, module_name) + } + EqWAlizerASTFormat::ExpandedStub => { + db.expanded_stub_bytes(project_id, module_name) + } + EqWAlizerASTFormat::ContractiveStub => { + db.contractive_stub_bytes(project_id, module_name) + } + EqWAlizerASTFormat::CovariantStub => { + db.covariant_stub_bytes(project_id, module_name) + } + EqWAlizerASTFormat::TransitiveStub => { + db.transitive_stub_bytes(project_id, module_name) + } + } + }; + match ast { + Ok(ast_bytes) => { + log::debug!( + "sending to eqwalizer: GetAstBytesReply for module {}", + module + ); + let ast_bytes_len = ast_bytes.len().try_into()?; + let reply = &MsgToEqWAlizer::GetAstBytesReply { ast_bytes_len }; + handle.send(reply)?; + handle.receive_newline()?; + handle.send_bytes(&ast_bytes)?; + } + Err(Error::ModuleNotFound(_)) => { + log::debug!( + "module not found, sending to eqwalizer: empty GetAstBytesReply for module {}", + module + ); + let ast_bytes_len = 0; + let reply = &MsgToEqWAlizer::GetAstBytesReply { ast_bytes_len }; + handle.send(reply)?; + handle.receive_newline()?; + } + Err(Error::ParseError) => { + log::debug!( + "parse error, sending to eqwalizer: CannotCompleteRequest for module {}", + module + ); + let reply = &MsgToEqWAlizer::CannotCompleteRequest; + handle.send(reply)?; + return Ok(EqwalizerDiagnostics::NoAst { module }); + } + Err(err) => { + log::debug!( + "error {} sending to eqwalizer: CannotCompleteRequest for module {}", + err, + module + ); + let reply = &MsgToEqWAlizer::CannotCompleteRequest; + handle.send(reply)?; + return Ok(EqwalizerDiagnostics::Error(err.to_string())); + } + } + } + MsgFromEqWAlizer::EqwalizingStart { module } => db.eqwalizing_start(module), + MsgFromEqWAlizer::EqwalizingDone { module } => db.eqwalizing_done(module), + MsgFromEqWAlizer::Done { diagnostics } => { + log::debug!( + "received from eqwalizer: Done with diagnostics length {}", + diagnostics.len() + ); + return Ok(EqwalizerDiagnostics::Diagnostics(diagnostics)); + } + MsgFromEqWAlizer::Dependencies { modules } => { + modules.iter().for_each(|module| { + let module = ModuleName::new(&module); + _ = db.transitive_stub_bytes(project_id, module); + }); + } + msg => { + log::warn!( + "received unexpected message from eqwalizer, ignoring: {:?}", + msg + ) + } + } + } +} + +fn compute_eqwalizer_stats( + db: &dyn EqwalizerDiagnosticsDatabase, + project_id: ProjectId, + module: ModuleName, +) -> Option> { + let ast = db.converted_ast(project_id, module).ok()?; + let mut fixmes = 0; + let mut ignores = 0; + let mut nowarn = 0; + for form in ast.to_vec() { + match form { + ExternalForm::ElpMetadata(meta) => { + for fixme in meta.fixmes { + if fixme.is_ignore { + ignores += 1 + } else { + fixmes += 1 + } + } + } + ExternalForm::EqwalizerNowarnFunction(_) => nowarn += 1, + _ => (), + } + } + if fixmes == 0 && ignores == 0 && nowarn == 0 { + return None; + } + Some(Arc::new(EqwalizerStats { + fixmes, + ignores, + nowarn, + })) +} + +fn add_env(cmd: &mut Command, build_info_path: &Path, elp_ast_dir: Option<&Path>) { + cmd.env("EQWALIZER_BUILD_INFO", build_info_path); + if let Some(elp_ast_dir) = elp_ast_dir { + cmd.env("EQWALIZER_ELP_AST_DIR", elp_ast_dir); + } +} + +/// This ensures the enclosed Command struct won't outlive the related temp file +pub struct CommandProxy<'file>(Command, PhantomData<&'file TempPath>); + +impl<'file> CommandProxy<'file> { + pub fn new(cmd: Command) -> Self { + Self(cmd, PhantomData) + } +} + +impl<'file> Deref for CommandProxy<'file> { + type Target = Command; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'file> DerefMut for CommandProxy<'file> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl fmt::Debug for CommandProxy<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self.0) + } +} diff --git a/crates/erlang_service/Cargo.toml b/crates/erlang_service/Cargo.toml new file mode 100644 index 0000000000..ce54b8bbcc --- /dev/null +++ b/crates/erlang_service/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "elp_erlang_service" +edition.workspace = true +version.workspace = true + +[dependencies] +anyhow.workspace = true +crossbeam-channel.workspace = true +eetf.workspace = true +fxhash.workspace = true +jod-thread.workspace = true +log.workspace = true +parking_lot.workspace = true +regex.workspace = true +stdx.workspace = true +tempfile.workspace = true +text-size.workspace = true + +[dev-dependencies] +env_logger.workspace = true +expect-test.workspace = true +lazy_static.workspace = true diff --git a/crates/erlang_service/build.rs b/crates/erlang_service/build.rs new file mode 100644 index 0000000000..bdfdde5cdb --- /dev/null +++ b/crates/erlang_service/build.rs @@ -0,0 +1,53 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::env; +use std::fs; +use std::path::Path; +use std::process::Command; + +fn main() { + let source_directory = "../../erlang_service"; + let out_dir = env::var_os("OUT_DIR").unwrap(); + let dest_dir = Path::new(&out_dir).join("erlang_service"); + fs::create_dir_all(&dest_dir).unwrap(); + + if let Some(path) = env::var_os("ELP_PARSE_SERVER_ESCRIPT_PATH") { + fs::copy(path, dest_dir.join("erlang_service")) + .expect("Copying precompiled erlang service escript failed"); + } else { + let profile = env::var("PROFILE").unwrap(); + let output = Command::new("rebar3") + .arg("escriptize") + .env("REBAR_PROFILE", &profile) + .env("REBAR_BASE_DIR", &dest_dir) + .current_dir(source_directory) + .output() + .expect("failed to execute rebar3 escriptize"); + + if !output.status.success() { + let stdout = + String::from_utf8(output.stdout).expect("valid utf8 output from rebar3 escriptize"); + let stderr = + String::from_utf8(output.stderr).expect("valid utf8 output from rebar3 escriptize"); + panic!( + "rebar3 escriptize failed with stdout:\n{}\n\nstderr:\n{}", + stdout, stderr + ); + } + + let source = dest_dir.join(profile).join("bin").join("erlang_service"); + fs::copy(source, dest_dir.join("erlang_service")).unwrap(); + + println!("cargo:rerun-if-changed={}/rebar.config", source_directory); + println!("cargo:rerun-if-changed={}/src", source_directory); + } + + println!("cargo:rerun-if-env-changed=ELP_PARSE_SERVER_ESCRIPT_PATH"); +} diff --git a/crates/erlang_service/fixtures/edoc_errors.erl b/crates/erlang_service/fixtures/edoc_errors.erl new file mode 100644 index 0000000000..f38ea2bb35 --- /dev/null +++ b/crates/erlang_service/fixtures/edoc_errors.erl @@ -0,0 +1,11 @@ +%% @doc This module contains a backtick, +%% which causes `EDoc to crash and burn +-module(edoc_errors). + +%% @doc This is function 1 +one() -> + 1. + +%% @docc This is function two, containing a warning +two() -> + 2. diff --git a/crates/erlang_service/fixtures/edoc_errors.expected b/crates/erlang_service/fixtures/edoc_errors.expected new file mode 100644 index 0000000000..569392b47f --- /dev/null +++ b/crates/erlang_service/fixtures/edoc_errors.expected @@ -0,0 +1,16 @@ +MODULE_DOC + + +FUNCTION_DOCS +{} + +EDOC_DIAGNOSTICS +[ + DocDiagnostic { + severity: "error", + line: 2, + message: "`-quote ended unexpectedly at line 2", + }, +] + + diff --git a/crates/erlang_service/fixtures/edoc_warnings.erl b/crates/erlang_service/fixtures/edoc_warnings.erl new file mode 100644 index 0000000000..09f78278cc --- /dev/null +++ b/crates/erlang_service/fixtures/edoc_warnings.erl @@ -0,0 +1,10 @@ +%% @docc This module contains some Edoc warnings +-module(edoc_warnings). + +%% @doc This is function 1 +one() -> + 1. + +%% @docc This is function two, containing an incorrect tag +two() -> + 2. diff --git a/crates/erlang_service/fixtures/edoc_warnings.expected b/crates/erlang_service/fixtures/edoc_warnings.expected new file mode 100644 index 0000000000..b33ff5a592 --- /dev/null +++ b/crates/erlang_service/fixtures/edoc_warnings.expected @@ -0,0 +1,26 @@ +MODULE_DOC + + +FUNCTION_DOCS +{ + ( + "one", + 0, + ): "This is function 1\n\n\n\n\n\n", +} + +EDOC_DIAGNOSTICS +[ + DocDiagnostic { + severity: "warning", + line: 1, + message: "tag @docc not recognized.", + }, + DocDiagnostic { + severity: "warning", + line: 8, + message: "tag @docc not recognized.", + }, +] + + diff --git a/crates/erlang_service/fixtures/error.erl b/crates/erlang_service/fixtures/error.erl new file mode 100644 index 0000000000..7df4df6eec --- /dev/null +++ b/crates/erlang_service/fixtures/error.erl @@ -0,0 +1,26 @@ +-module(error). + +-spec unrecognized1() -> atom. +unrecognized1() -> + %$:eqwalizer: cast(Key) + Key = get(key), + Key. + +-spec unrecognized2() -> atom. +unrecognized2() -> + %$eqwalizer:cast(Key)::dynamic() + Key = get(key), + Key. + +-spec unrecognized3() -> atom. +unrecognized3() -> + %$eqwalizer:cast(Key) + %$eqwalizer: ::dynamic() + Key = get(key), + Key. + +-spec unrecognized4() -> atom. +unrecognized4() -> + %$eqwalizer: handle(Key) + Key = get(key), + Key. diff --git a/crates/erlang_service/fixtures/error.expected b/crates/erlang_service/fixtures/error.expected new file mode 100644 index 0000000000..d323442e7f --- /dev/null +++ b/crates/erlang_service/fixtures/error.expected @@ -0,0 +1,153 @@ +AST +{attribute,{0,0},file,{"fixtures/error.erl",0}}. +{attribute,{0,14},module,error}. +{attribute,{17,46}, + spec, + {{unrecognized1,0}, + [{type,{36,46}, + 'fun', + [{type,{36,46},product,[]},{atom,{42,46},atom}]}]}}. +{function,{48,122}, + unrecognized1,0, + [{clause,{48,122}, + [],[], + [{match,{103,113}, + {var,{99,102},'Key'}, + {call,{105,113}, + {atom,{105,108},get}, + [{atom,{109,112},key}]}}, + {var,{119,122},'Key'}]}]}. +{attribute,{125,154}, + spec, + {{unrecognized2,0}, + [{type,{144,154}, + 'fun', + [{type,{144,154},product,[]},{atom,{150,154},atom}]}]}}. +{function,{156,239}, + unrecognized2,0, + [{clause,{156,239}, + [],[], + [{match,{220,230}, + {var,{216,219},'Key'}, + {call,{222,230}, + {atom,{222,225},get}, + [{atom,{226,229},key}]}}, + {var,{236,239},'Key'}]}]}. +{attribute,{242,271}, + spec, + {{unrecognized3,0}, + [{type,{261,271}, + 'fun', + [{type,{261,271},product,[]},{atom,{267,271},atom}]}]}}. +{function,{273,374}, + unrecognized3,0, + [{clause,{273,374}, + [],[], + [{match,{355,365}, + {var,{351,354},'Key'}, + {call,{357,365}, + {atom,{357,360},get}, + [{atom,{361,364},key}]}}, + {var,{371,374},'Key'}]}]}. +{attribute,{377,406}, + spec, + {{unrecognized4,0}, + [{type,{396,406}, + 'fun', + [{type,{396,406},product,[]},{atom,{402,406},atom}]}]}}. +{function,{408,483}, + unrecognized4,0, + [{clause,{408,483}, + [],[], + [{match,{464,474}, + {var,{460,463},'Key'}, + {call,{466,474}, + {atom,{466,469},get}, + [{atom,{470,473},key}]}}, + {var,{480,483},'Key'}]}]}. +{eof,{485,485}}. + + +STUB +{attribute,{0,0},file,{"fixtures/error.erl",0}}. +{attribute,{0,14},module,error}. +{attribute,{17,46}, + spec, + {{unrecognized1,0}, + [{type,{36,46}, + 'fun', + [{type,{36,46},product,[]},{atom,{42,46},atom}]}]}}. +{attribute,{125,154}, + spec, + {{unrecognized2,0}, + [{type,{144,154}, + 'fun', + [{type,{144,154},product,[]},{atom,{150,154},atom}]}]}}. +{attribute,{242,271}, + spec, + {{unrecognized3,0}, + [{type,{261,271}, + 'fun', + [{type,{261,271},product,[]},{atom,{267,271},atom}]}]}}. +{attribute,{377,406}, + spec, + {{unrecognized4,0}, + [{type,{396,406}, + 'fun', + [{type,{396,406},product,[]},{atom,{402,406},atom}]}]}}. + + +WARNINGS +[ + ParseError { + path: "fixtures/error.erl", + location: Some( + Normal( + TextRange( + 48..122, + ), + ), + ), + msg: "function unrecognized1/0 is unused", + code: "L1230", + }, + ParseError { + path: "fixtures/error.erl", + location: Some( + Normal( + TextRange( + 156..239, + ), + ), + ), + msg: "function unrecognized2/0 is unused", + code: "L1230", + }, + ParseError { + path: "fixtures/error.erl", + location: Some( + Normal( + TextRange( + 273..374, + ), + ), + ), + msg: "function unrecognized3/0 is unused", + code: "L1230", + }, + ParseError { + path: "fixtures/error.erl", + location: Some( + Normal( + TextRange( + 408..483, + ), + ), + ), + msg: "function unrecognized4/0 is unused", + code: "L1230", + }, +] + +ERRORS +[] diff --git a/crates/erlang_service/fixtures/error_attr.erl b/crates/erlang_service/fixtures/error_attr.erl new file mode 100644 index 0000000000..08766e593a --- /dev/null +++ b/crates/erlang_service/fixtures/error_attr.erl @@ -0,0 +1,7 @@ +-module(error_attr). + +-error("alamakota"). + +-error("two", "terms"). + +-error(NotATerm). diff --git a/crates/erlang_service/fixtures/error_attr.expected b/crates/erlang_service/fixtures/error_attr.expected new file mode 100644 index 0000000000..b6d88d0e40 --- /dev/null +++ b/crates/erlang_service/fixtures/error_attr.expected @@ -0,0 +1,56 @@ +AST +{attribute,{0,0},file,{"fixtures/error_attr.erl",0}}. +{attribute,{0,19},module,error_attr}. +{error,{{23,28},elp_epp,{error,"alamakota"}}}. +{error,{{45,50},elp_epp,{bad,error}}}. +{error,{{70,75},elp_epp,{bad,error}}}. +{eof,{87,87}}. + + +STUB +{attribute,{0,0},file,{"fixtures/error_attr.erl",0}}. +{attribute,{0,19},module,error_attr}. + + +WARNINGS +[] + +ERRORS +[ + ParseError { + path: "fixtures/error_attr.erl", + location: Some( + Normal( + TextRange( + 23..28, + ), + ), + ), + msg: "-error(\"alamakota\").", + code: "E1522", + }, + ParseError { + path: "fixtures/error_attr.erl", + location: Some( + Normal( + TextRange( + 45..50, + ), + ), + ), + msg: "badly formed 'error'", + code: "E1501", + }, + ParseError { + path: "fixtures/error_attr.erl", + location: Some( + Normal( + TextRange( + 70..75, + ), + ), + ), + msg: "badly formed 'error'", + code: "E1501", + }, +] diff --git a/crates/erlang_service/fixtures/misplaced_comment_error.erl b/crates/erlang_service/fixtures/misplaced_comment_error.erl new file mode 100644 index 0000000000..0857b0e96d --- /dev/null +++ b/crates/erlang_service/fixtures/misplaced_comment_error.erl @@ -0,0 +1,16 @@ +-module(misplaced_comment_error). + +bad_hint_arg() -> + get( + %$eqwalizer: cast(Key) :: dynamic() + key + ). + +bad_hint_comprehension() -> + L0 = [1, 2, 3], + L1 = [ + X || X <- L0, + %$eqwalizer: cast(X) :: pos_integer() + X > 1 + ], + L1. diff --git a/crates/erlang_service/fixtures/misplaced_comment_error.expected b/crates/erlang_service/fixtures/misplaced_comment_error.expected new file mode 100644 index 0000000000..dc6ca88f0b --- /dev/null +++ b/crates/erlang_service/fixtures/misplaced_comment_error.expected @@ -0,0 +1,81 @@ +AST +{attribute,{0,0},file,{"fixtures/misplaced_comment_error.erl",0}}. +{attribute,{0,32},module,misplaced_comment_error}. +{function,{35,111}, + bad_hint_arg,0, + [{clause,{35,111}, + [],[], + [{call,{55,111}, + {atom,{55,58},get}, + [{atom,{104,107},key}]}]}]}. +{function, + {114,248}, + bad_hint_comprehension,0, + [{clause, + {114,248}, + [],[], + [{match, + {147,158}, + {var,{144,146},'L0'}, + {cons, + {149,158}, + {integer,{150,151},1}, + {cons, + {151,158}, + {integer,{153,154},2}, + {cons, + {154,158}, + {integer,{156,157},3}, + {nil,{157,158}}}}}}, + {match, + {165,242}, + {var,{162,164},'L1'}, + {lc,{167,242}, + {var,{173,174},'X'}, + [{generate, + {178,185}, + {var,{178,179},'X'}, + {var,{183,185},'L0'}}, + {op,{233,238}, + '>', + {var,{233,234},'X'}, + {integer,{237,238},1}}]}}, + {var,{246,248},'L1'}]}]}. +{eof,{250,250}}. + + +STUB +{attribute,{0,0},file,{"fixtures/misplaced_comment_error.erl",0}}. +{attribute,{0,32},module,misplaced_comment_error}. + + +WARNINGS +[ + ParseError { + path: "fixtures/misplaced_comment_error.erl", + location: Some( + Normal( + TextRange( + 35..111, + ), + ), + ), + msg: "function bad_hint_arg/0 is unused", + code: "L1230", + }, + ParseError { + path: "fixtures/misplaced_comment_error.erl", + location: Some( + Normal( + TextRange( + 114..248, + ), + ), + ), + msg: "function bad_hint_comprehension/0 is unused", + code: "L1230", + }, +] + +ERRORS +[] diff --git a/crates/erlang_service/fixtures/regular.erl b/crates/erlang_service/fixtures/regular.erl new file mode 100644 index 0000000000..ed30c41c54 --- /dev/null +++ b/crates/erlang_service/fixtures/regular.erl @@ -0,0 +1 @@ +-module(regular). diff --git a/crates/erlang_service/fixtures/regular.expected b/crates/erlang_service/fixtures/regular.expected new file mode 100644 index 0000000000..965d3fbf8b --- /dev/null +++ b/crates/erlang_service/fixtures/regular.expected @@ -0,0 +1,16 @@ +AST +{attribute,{0,0},file,{"fixtures/regular.erl",0}}. +{attribute,{0,16},module,regular}. +{eof,{18,18}}. + + +STUB +{attribute,{0,0},file,{"fixtures/regular.erl",0}}. +{attribute,{0,16},module,regular}. + + +WARNINGS +[] + +ERRORS +[] diff --git a/crates/erlang_service/fixtures/structured_comment.erl b/crates/erlang_service/fixtures/structured_comment.erl new file mode 100644 index 0000000000..9cb8103c81 --- /dev/null +++ b/crates/erlang_service/fixtures/structured_comment.erl @@ -0,0 +1,9 @@ +-module(structured_comment). + +test(X) -> + %$eqwalizer: dynamic(X) + X. + +test2(X) -> + %$eqwalizer: X :: dynamic() + X. diff --git a/crates/erlang_service/fixtures/structured_comment.expected b/crates/erlang_service/fixtures/structured_comment.expected new file mode 100644 index 0000000000..6e0fcb19fa --- /dev/null +++ b/crates/erlang_service/fixtures/structured_comment.expected @@ -0,0 +1,47 @@ +AST +{attribute,{0,0},file,{"fixtures/structured_comment.erl",0}}. +{attribute,{0,27},module,structured_comment}. +{function,{30,74}, + test,1, + [{clause,{30,74},[{var,{35,36},'X'}],[],[{var,{73,74},'X'}]}]}. +{function,{77,126}, + test2,1, + [{clause,{77,126},[{var,{83,84},'X'}],[],[{var,{125,126},'X'}]}]}. +{eof,{128,128}}. + + +STUB +{attribute,{0,0},file,{"fixtures/structured_comment.erl",0}}. +{attribute,{0,27},module,structured_comment}. + + +WARNINGS +[ + ParseError { + path: "fixtures/structured_comment.erl", + location: Some( + Normal( + TextRange( + 30..74, + ), + ), + ), + msg: "function test/1 is unused", + code: "L1230", + }, + ParseError { + path: "fixtures/structured_comment.erl", + location: Some( + Normal( + TextRange( + 77..126, + ), + ), + ), + msg: "function test2/1 is unused", + code: "L1230", + }, +] + +ERRORS +[] diff --git a/crates/erlang_service/fixtures/unused_record.erl b/crates/erlang_service/fixtures/unused_record.erl new file mode 100644 index 0000000000..7c02192897 --- /dev/null +++ b/crates/erlang_service/fixtures/unused_record.erl @@ -0,0 +1,3 @@ +-module(unused_record). + +-record(my_record, {my_field}). diff --git a/crates/erlang_service/fixtures/unused_record.expected b/crates/erlang_service/fixtures/unused_record.expected new file mode 100644 index 0000000000..a841c32dbc --- /dev/null +++ b/crates/erlang_service/fixtures/unused_record.expected @@ -0,0 +1,35 @@ +AST +{attribute,{0,0},file,{"fixtures/unused_record.erl",0}}. +{attribute,{0,22},module,unused_record}. +{attribute,{25,55}, + record, + {my_record,[{record_field,{45,53},{atom,{45,53},my_field}}]}}. +{eof,{57,57}}. + + +STUB +{attribute,{0,0},file,{"fixtures/unused_record.erl",0}}. +{attribute,{0,22},module,unused_record}. +{attribute,{25,55}, + record, + {my_record,[{record_field,{45,53},{atom,{45,53},my_field}}]}}. + + +WARNINGS +[ + ParseError { + path: "fixtures/unused_record.erl", + location: Some( + Normal( + TextRange( + 25..55, + ), + ), + ), + msg: "record my_record is unused", + code: "L1260", + }, +] + +ERRORS +[] diff --git a/crates/erlang_service/fixtures/unused_record_in_header.expected b/crates/erlang_service/fixtures/unused_record_in_header.expected new file mode 100644 index 0000000000..0763421d68 --- /dev/null +++ b/crates/erlang_service/fixtures/unused_record_in_header.expected @@ -0,0 +1,20 @@ +AST +{attribute,{0,0},file,{"fixtures/unused_record_in_header.hrl",0}}. +{attribute,{0,30}, + record, + {my_record,[{record_field,{20,28},{atom,{20,28},my_field}}]}}. +{eof,{32,32}}. + + +STUB +{attribute,{0,0},file,{"fixtures/unused_record_in_header.hrl",0}}. +{attribute,{0,30}, + record, + {my_record,[{record_field,{20,28},{atom,{20,28},my_field}}]}}. + + +WARNINGS +[] + +ERRORS +[] diff --git a/crates/erlang_service/fixtures/unused_record_in_header.hrl b/crates/erlang_service/fixtures/unused_record_in_header.hrl new file mode 100644 index 0000000000..a4500981dc --- /dev/null +++ b/crates/erlang_service/fixtures/unused_record_in_header.hrl @@ -0,0 +1 @@ +-record(my_record, {my_field}). diff --git a/crates/erlang_service/src/lib.rs b/crates/erlang_service/src/lib.rs new file mode 100644 index 0000000000..3f94622d9d --- /dev/null +++ b/crates/erlang_service/src/lib.rs @@ -0,0 +1,815 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::io::BufRead; +use std::io::BufReader; +use std::io::BufWriter; +use std::io::Read; +use std::io::Write; +use std::iter; +use std::path::PathBuf; +use std::process::Child; +use std::process::ChildStdin; +use std::process::ChildStdout; +use std::process::Command; +use std::process::Stdio; +use std::sync::Arc; + +use anyhow::anyhow; +use anyhow::Context; +use anyhow::Result; +use crossbeam_channel::bounded; +use crossbeam_channel::Receiver; +use crossbeam_channel::Sender; +use eetf::pattern; +use fxhash::FxHashMap; +use jod_thread::JoinHandle; +use parking_lot::Mutex; +use regex::Regex; +use stdx::JodChild; +use tempfile::Builder; +use tempfile::TempPath; +use text_size::TextRange; + +// Location information of a warning/error may come in different flavors: +// * Eqwalizer: byte offset for range. +// * Default parser: line, column for start. +// Note: Using byte offset everywhere would complicate the code, +// since conversion requires access to source file and its encoding. + +/// Start location, as returned by erl parser (see erl_anno:location()). +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] +pub struct StartLocation { + pub line: u32, + pub column: u32, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub enum Location { + TextRange(TextRange), + StartLocation(StartLocation), +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub enum DiagnosticLocation { + Normal(Location), + Included { + directive_location: TextRange, // Location of include directive in the file compiled + error_location: TextRange, // Location of the error in the included file + }, +} + +/// This struct ensures proper shutdown sequence +/// +/// Struct fields are dropped in definition order, meaning we: +/// * first stop sending requests +/// * then stop accepting responses +/// * then shutdown the child process +/// * finally delete the executable +#[derive(Debug)] +struct SharedState { + _writer_for_drop: JoinHandle, + _reader_for_drop: JoinHandle, + _child_for_drop: JodChild, + _file_for_drop: TempPath, +} + +#[derive(Clone, Debug)] +pub struct Connection { + sender: Sender, + _for_drop: Arc, +} + +#[derive(Debug, Clone)] +pub enum CompileOption { + Includes(Vec), + Macros(Vec), + ParseTransforms(Vec), + ElpMetadata(eetf::Term), +} + +impl Into for CompileOption { + fn into(self) -> eetf::Term { + match self { + CompileOption::Includes(includes) => { + let paths = eetf::List::from( + includes + .into_iter() + .map(|path| path_into_list(path).into()) + .collect::>(), + ); + eetf::Tuple::from(vec![eetf::Atom::from("includes").into(), paths.into()]).into() + } + CompileOption::Macros(macros) => { + let macros = eetf::List::from(macros); + eetf::Tuple::from(vec![eetf::Atom::from("macros").into(), macros.into()]).into() + } + CompileOption::ParseTransforms(transforms) => { + let transforms = eetf::List::from(transforms); + let parse_transforms = eetf::Atom::from("parse_transforms"); + eetf::Tuple::from(vec![parse_transforms.into(), transforms.into()]).into() + } + CompileOption::ElpMetadata(elp_metadata) => { + let label = eetf::Atom::from("elp_metadata"); + eetf::Tuple::from(vec![label.into(), elp_metadata]).into() + } + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Format { + OffsetEtf, + Text, +} + +#[derive(Debug, Clone)] +pub struct ParseRequest { + pub options: Vec, + pub path: PathBuf, + pub format: Format, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum DocOrigin { + /// Get docs by running edoc on the Erlang source file. + /// Preferable, since it doesn't require an out-of-band build step + /// to get up-to-date BEAM files with embedded docs or `.chunk` files + /// beside them. + Edoc, + /// Get docs via the EEP-48 standardised method from BEAM files. + /// Required for some dependencies which don't use standard edocs in + /// comments, but who store docs in the relevant chunk of beam files + /// which we can assume are up-to-date, e.g. OTP. + Eep48, +} + +#[derive(Debug, Clone)] +pub struct DocRequest { + pub doc_origin: DocOrigin, + /// No matter the doc origin, **we give the path for the source file + /// here**. If the origin is EEP-48, erlang_service will resolve it to + /// the appropriate BEAM file itself. + pub src_path: PathBuf, +} + +#[derive(Debug, Clone)] +enum Request { + ParseRequest(ParseRequest, Sender>), + AddCodePath(Vec), + DocRequest(DocRequest, Sender>), +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ParseError { + pub path: PathBuf, + pub location: Option, + pub msg: String, + pub code: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct DocDiagnostic { + pub severity: String, + pub line: u32, + pub message: String, +} + +pub type ParseResult = RawParseResult; +type UndecodedParseResult = RawParseResult; +type RawNameArity = (String, u32); +type RawMarkdown = String; + +pub type DocResult = RawModuleDoc; + +#[derive(Debug)] +pub struct RawModuleDoc { + pub module_doc: RawMarkdown, + pub function_docs: FxHashMap, + pub diagnostics: Vec, +} + +#[derive(Debug)] +enum Reply { + ParseReply(Result), + DocReply(Result), +} + +enum ResponseSender { + ParseResponseSender(Sender>), + DocResponseSender(Sender>), +} + +impl ResponseSender { + fn send_exn(&self, e: anyhow::Error) { + match self { + ResponseSender::ParseResponseSender(r) => r.send(Result::Err(e)).unwrap(), + ResponseSender::DocResponseSender(r) => r.send(Result::Err(e)).unwrap(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct RawParseResult { + pub ast: Arc>, + pub stub: Arc>, + pub errors: Vec, + pub warnings: Vec, +} + +impl UndecodedParseResult { + pub fn decode(self) -> Result { + let errors = decode_errors(&self.errors).with_context(|| "when decoding errors")?; + let warnings = decode_errors(&self.warnings).with_context(|| "when decoding warnings")?; + + Ok(ParseResult { + ast: self.ast, + stub: self.stub, + errors, + warnings, + }) + } +} + +impl ParseResult { + pub fn error(error: ParseError) -> Self { + Self { + ast: Arc::default(), + stub: Arc::default(), + errors: vec![error], + warnings: Vec::default(), + } + } + + pub fn is_ok(&self) -> bool { + self.errors.is_empty() + } +} + +impl Connection { + pub fn start() -> Result { + let escript_src = + include_bytes!(concat!(env!("OUT_DIR"), "/erlang_service/erlang_service")); + let mut escript = Builder::new().prefix("erlang_service").tempfile()?; + escript.write_all(escript_src)?; + + let mut cmd = Command::new("escript"); + cmd.arg(escript.path()); + + cmd.stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()); + + let mut proc = cmd.spawn()?; + let escript = escript.into_temp_path(); + + let (sender, writer, reader) = stdio_transport(&mut proc); + + Ok(Connection { + sender, + _for_drop: Arc::new(SharedState { + _file_for_drop: escript, + _child_for_drop: JodChild(proc), + _writer_for_drop: writer, + _reader_for_drop: reader, + }), + }) + } + + pub fn request_parse(&self, request_in: ParseRequest) -> ParseResult { + let (sender, receiver) = bounded::>(0); + let path = request_in.path.clone(); + let request = Request::ParseRequest(request_in.clone(), sender); + self.sender.send(request).unwrap(); + match receiver.recv().unwrap() { + Result::Ok(result) => match result.decode() { + Result::Ok(result) => result, + Err(error) => { + log::error!("Decoding parse result failed: {:?}", error); + ParseResult::error(ParseError { + path, + location: None, + msg: format!("Could not parse, error: {}", error.to_string()), + code: "L0001".to_string(), + }) + } + }, + Err(error) => { + log::error!( + "Erlang service crashed for: {:?}, error: {:?}", + request_in, + error + ); + ParseResult::error(ParseError { + path, + location: None, + msg: format!("Could not parse, error: {}", error.to_string()), + code: "L0002".to_string(), + }) + } + } + } + + pub fn request_doc(&self, request: DocRequest) -> Result { + let (sender, receiver) = bounded::>(0); + let request = Request::DocRequest(request, sender); + self.sender.send(request.clone()).unwrap(); + match receiver.recv().unwrap() { + Result::Ok(result) => Result::Ok(result), + Err(error) => { + log::error!( + "Erlang service crashed for: {:?}, error: {:?}", + request.clone(), + error + ); + Err(format!( + "Erlang service crash when trying to load docs: {:?}", + request + )) + } + } + } + + pub fn add_code_path(&self, paths: Vec) { + let request = Request::AddCodePath(paths); + self.sender.send(request).unwrap(); + } +} + +fn stdio_transport(proc: &mut Child) -> (Sender, JoinHandle, JoinHandle) { + let instream = BufWriter::new(proc.stdin.take().unwrap()); + let mut outstream = BufReader::new(proc.stdout.take().unwrap()); + + let inflight = Arc::new(Mutex::new(FxHashMap::default())); + + let (writer_sender, writer_receiver) = bounded::(0); + let writer = jod_thread::spawn({ + let inflight = inflight.clone(); + move || match writer_run(writer_receiver, instream, inflight) { + Result::Ok(()) => {} + Err(err) => log::error!("writer failed with {}", err), + } + }); + + let reader = jod_thread::spawn({ + move || match reader_run(&mut outstream, inflight) { + Result::Ok(()) => {} + Err(err) => { + let mut buf = vec![0; 512]; + let _ = outstream.read(&mut buf); + let remaining = String::from_utf8_lossy(&buf); + log::error!( + "reader failed with {}\nremaining data:\n\n{}", + err, + remaining + ); + } + } + }); + + (writer_sender, writer, reader) +} + +fn reader_run( + outstream: &mut BufReader, + inflight: Arc>>, +) -> Result<()> { + let mut line_buf = String::new(); + loop { + line_buf.clear(); + outstream.read_line(&mut line_buf)?; + let parts = line_buf.split_ascii_whitespace().collect::>(); + if parts.is_empty() { + break; + } + + let id: usize = parts[1].parse()?; + let size: usize = parts[2].parse()?; + + let sender = inflight.lock().remove(&id).unwrap(); + + match parts[0] { + "REPLY" => { + let reply = decode_segments(outstream, &mut line_buf, size)?; + send_reply(sender, reply)?; + } + "EXCEPTION" => { + let mut buf = vec![0; size]; + outstream.read_exact(&mut buf)?; + let resp = String::from_utf8(buf).unwrap(); + let error = anyhow!("{}", resp); + sender.send_exn(error); + } + _ => { + log::error!("Unrecognised message: {}", line_buf); + break; + } + } + } + + fn decode_segments( + outstream: &mut BufReader, + line_buf: &mut String, + num: usize, + ) -> Result { + let mut ast = Vec::new(); + let mut stub = Vec::new(); + let mut warnings = Vec::new(); + let mut errors = Vec::new(); + + let mut function_docs = FxHashMap::default(); + let mut module_doc = Vec::new(); + let mut edoc_diagnostics = Vec::new(); + + let mut is_doc = false; + + let function_doc_regex = + Regex::new(r"^(?P\S+) (?P\d+) (?P(?s).*)$").unwrap(); + + let doc_diagnostic_regex = + Regex::new(r"^(?P\S+) (?P\d+) (?P.*)$").unwrap(); + + for _ in 0..num { + line_buf.clear(); + outstream.read_line(line_buf)?; + let parts = line_buf.split_ascii_whitespace().collect::>(); + if parts.len() == 0 { + break; + } + let size: usize = parts[1].parse()?; + let mut buf = vec![0; size]; + + outstream.read_exact(&mut buf)?; + + match parts[0] { + "AST" => ast = buf, + "STUB" => stub = buf, + "WARNINGS" => warnings = buf, + "ERRORS" => errors = buf, + "MODULE_DOC" => { + is_doc = true; + module_doc = buf + } + "FUNCTION_DOC" => { + is_doc = true; + let text = match String::from_utf8(buf) { + Ok(text) => text, + Err(err) => { + log::warn!("Failed UTF-8 conversion in FUNCTION_DOC: {err}"); + // Fall back to lossy latin1 loading of files. + // This should only affect files from yaws, and + // possibly OTP that are latin1 encoded. + let contents = err.into_bytes(); + contents.into_iter().map(|byte| byte as char).collect() + } + }; + if let Some(caps) = function_doc_regex.captures(&text) { + let name = caps.name("name").unwrap().as_str().to_string(); + let arity = caps.name("arity").unwrap().as_str().parse::()?; + let doc = caps.name("doc").unwrap().as_str().to_string(); + function_docs.insert((name, arity), doc); + } else { + log::error!("Could not capture in FUNCTION_DOC: {text}"); + } + } + "EDOC_DIAGNOSTIC" => { + let text = match String::from_utf8(buf) { + Ok(text) => text, + Err(err) => { + log::warn!("Failed UTF-8 conversion in EDOC_DIAGNOSTIC: {err}"); + // Fall back to lossy latin1 loading of files. + // This should only affect files from yaws, and + // possibly OTP that are latin1 encoded. + let contents = err.into_bytes(); + contents.into_iter().map(|byte| byte as char).collect() + } + }; + if let Some(caps) = doc_diagnostic_regex.captures(&text) { + let severity = caps.name("severity").unwrap().as_str().to_string(); + let line = caps.name("line").unwrap().as_str().parse::()?; + let message = caps.name("message").unwrap().as_str().to_string(); + edoc_diagnostics.push(DocDiagnostic { + severity, + line, + message, + }); + } else { + log::error!("Could not capture in EDOC_DIAGNOSTICS: {text}"); + } + } + _ => { + log::error!("Unrecognised segment: {}", line_buf); + break; + } + } + } + if is_doc { + let module_doc_str = String::from_utf8(module_doc).unwrap(); + Ok(Reply::DocReply(Ok(RawModuleDoc { + module_doc: module_doc_str, + function_docs, + diagnostics: edoc_diagnostics, + }))) + } else { + Ok(Reply::ParseReply(Ok(UndecodedParseResult { + ast: Arc::new(ast), + stub: Arc::new(stub), + warnings, + errors, + }))) + } + } + + Ok(()) +} + +fn send_reply(sender: ResponseSender, reply: Reply) -> Result<()> { + match (sender, reply) { + (ResponseSender::ParseResponseSender(s), Reply::ParseReply(r)) => { + Result::Ok(s.send(r).unwrap()) + } + (ResponseSender::DocResponseSender(s), Reply::DocReply(r)) => { + Result::Ok(s.send(r).unwrap()) + } + (ResponseSender::ParseResponseSender(_), Reply::DocReply(_)) => Result::Err(anyhow!( + "erlang_service response mismatch: Got a doc reply when expecting a parse reply" + )), + (ResponseSender::DocResponseSender(_), Reply::ParseReply(_)) => Result::Err(anyhow!( + "erlang_service response mismatch: Got a parse reply when expecting a doc reply" + )), + } +} + +fn writer_run( + receiver: Receiver, + mut instream: BufWriter, + inflight: Arc>>, +) -> Result<()> { + let mut counter = 0; + receiver.into_iter().try_for_each(|request| match request { + Request::ParseRequest(request, sender) => { + counter += 1; + inflight + .lock() + .insert(counter, ResponseSender::ParseResponseSender(sender)); + let tag = request.tag(); + let bytes = request.encode(counter); + writeln!(instream, "{} {}", tag, bytes.len())?; + instream.write_all(&bytes)?; + instream.flush() + } + Request::AddCodePath(paths) => { + writeln!(instream, "ADD_PATHS {}", paths.len())?; + for path in paths { + writeln!(instream, "{}", path.display())?; + } + Result::Ok(()) + } + Request::DocRequest(request, sender) => { + counter += 1; + inflight + .lock() + .insert(counter, ResponseSender::DocResponseSender(sender)); + let tag = request.tag(); + let bytes = request.encode(counter); + writeln!(instream, "{} {}", tag, bytes.len())?; + instream.write_all(&bytes)?; + instream.flush() + } + })?; + instream.write_all(b"EXIT\n")?; + Ok(()) +} + +fn decode_errors(buf: &[u8]) -> Result> { + if buf.len() == 0 { + return Ok(vec![]); + } + + eetf::Term::decode(buf)? + .as_match(pattern::VarList(( + pattern::Str(pattern::Unicode), + pattern::Or(( + (pattern::U32, pattern::U32), // Normal location + pattern::FixList(((pattern::U32, pattern::U32), (pattern::U32, pattern::U32))), // Location in include file + "none", + )), + pattern::Str(pattern::Unicode), // message + pattern::Str(pattern::Unicode), // code + ))) + .map_err(|err| anyhow!("Failed to decode errors: {:?}", err)) + .map(|res| { + res.into_iter() + .map(|(path, position, msg, code)| ParseError { + path: path.into(), + location: match position { + pattern::Union3::A((a, b)) => { + // Temporary for T148094436 + let _pctx = stdx::panic_context::enter(format!("\ndecode_errors1")); + Some(DiagnosticLocation::Normal(Location::TextRange( + TextRange::new(a.into(), b.into()), + ))) + } + pattern::Union3::B(((a, b), (c, d))) => { + // Temporary for T148094436 + let _pctx = stdx::panic_context::enter(format!("\ndecode_errors2")); + Some(DiagnosticLocation::Included { + directive_location: TextRange::new(a.into(), b.into()), + error_location: TextRange::new(c.into(), d.into()), + }) + } + pattern::Union3::C(_) => None, + }, + msg, + code, + }) + .collect() + }) +} + +impl ParseRequest { + fn tag(&self) -> &'static str { + match self.format { + Format::OffsetEtf { .. } => "COMPILE", + Format::Text => "TEXT", + } + } + + fn encode(self, id: usize) -> Vec { + let location = eetf::Atom::from("offset").into(); + let location_tuple = + eetf::Tuple::from(vec![eetf::Atom::from("location").into(), location]).into(); + let options = self + .options + .into_iter() + .map(|option| option.into()) + .chain(iter::once(location_tuple)) + .collect::>(); + let tuple = eetf::Tuple::from(vec![ + eetf::BigInteger::from(id).into(), + path_into_list(self.path).into(), + eetf::List::from(options).into(), + ]); + let mut buf = Vec::new(); + eetf::Term::from(tuple).encode(&mut buf).unwrap(); + buf + } +} + +impl DocRequest { + fn tag(&self) -> &'static str { + match self.doc_origin { + DocOrigin::Edoc => "DOC_EDOC", + DocOrigin::Eep48 => "DOC_EEP48", + } + } + + fn encode(self, id: usize) -> Vec { + let tuple = eetf::Tuple::from(vec![ + eetf::BigInteger::from(id).into(), + path_into_list(self.src_path).into(), + ]); + let mut buf = Vec::new(); + eetf::Term::from(tuple).encode(&mut buf).unwrap(); + buf + } +} + +#[cfg(unix)] +fn path_into_list(path: PathBuf) -> eetf::List { + use std::os::unix::prelude::OsStringExt; + path.into_os_string() + .into_vec() + .into_iter() + .map(|byte| eetf::FixInteger::from(byte).into()) + .collect::>() + .into() +} + +#[cfg(test)] +mod tests { + use std::str; + + use expect_test::expect_file; + use expect_test::ExpectFile; + use lazy_static::lazy_static; + + use super::*; + + #[test] + fn regular_module() { + expect_module( + "fixtures/regular.erl".into(), + expect_file!["../fixtures/regular.expected"], + vec![], + ); + } + + #[test] + fn structured_comment() { + expect_module( + "fixtures/structured_comment.erl".into(), + expect_file!["../fixtures/structured_comment.expected"], + vec![], + ); + } + + #[test] + fn errors() { + expect_module( + "fixtures/error.erl".into(), + expect_file!["../fixtures/error.expected"], + vec![], + ); + + expect_module( + "fixtures/misplaced_comment_error.erl".into(), + expect_file!["../fixtures/misplaced_comment_error.expected"], + vec![], + ); + } + + #[test] + fn warnings() { + expect_module( + "fixtures/error_attr.erl".into(), + expect_file!["../fixtures/error_attr.expected"], + vec![], + ); + } + + #[test] + fn unused_record() { + expect_module( + "fixtures/unused_record.erl".into(), + expect_file!["../fixtures/unused_record.expected"], + vec![], + ); + } + + #[test] + fn unused_record_in_header() { + expect_module( + "fixtures/unused_record_in_header.hrl".into(), + expect_file!["../fixtures/unused_record_in_header.expected"], + vec![], + ); + } + + #[test] + fn edoc_warnings() { + expect_docs( + "fixtures/edoc_warnings.erl".into(), + expect_file!["../fixtures/edoc_warnings.expected"], + ); + } + + #[test] + fn edoc_errors() { + expect_docs( + "fixtures/edoc_errors.erl".into(), + expect_file!["../fixtures/edoc_errors.expected"], + ); + } + + fn expect_module(path: PathBuf, expected: ExpectFile, options: Vec) { + lazy_static! { + static ref CONN: Connection = Connection::start().unwrap(); + } + let request = ParseRequest { + options, + path, + format: Format::Text, + }; + let response = CONN.request_parse(request); + let ast = str::from_utf8(&response.ast).unwrap(); + let stub = str::from_utf8(&response.stub).unwrap(); + let actual = format!( + "AST\n{}\n\nSTUB\n{}\n\nWARNINGS\n{:#?}\n\nERRORS\n{:#?}\n", + ast, stub, &response.warnings, &response.errors + ); + expected.assert_eq(&actual); + } + + fn expect_docs(path: PathBuf, expected: ExpectFile) { + lazy_static! { + static ref CONN: Connection = Connection::start().unwrap(); + } + let request = DocRequest { + doc_origin: DocOrigin::Edoc, + src_path: path, + }; + let response = CONN.request_doc(request).unwrap(); + let actual = format!( + "MODULE_DOC\n{}\n\nFUNCTION_DOCS\n{:#?}\n\nEDOC_DIAGNOSTICS\n{:#?}\n\n\n", + &response.module_doc, &response.function_docs, &response.diagnostics + ); + expected.assert_eq(&actual); + } +} diff --git a/crates/hir/Cargo.toml b/crates/hir/Cargo.toml new file mode 100644 index 0000000000..de2fbe27c8 --- /dev/null +++ b/crates/hir/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "hir" +edition.workspace = true +version.workspace = true + +[dependencies] +elp_base_db.workspace = true +elp_syntax.workspace = true +triple_accel.workspace = true +either.workspace = true +fxhash.workspace = true +itertools.workspace = true +la-arena.workspace = true +lazy_static.workspace = true +log.workspace = true +profile.workspace = true +regex.workspace = true +stdx.workspace = true + +[dev-dependencies] +expect-test.workspace = true diff --git a/crates/hir/src/body.rs b/crates/hir/src/body.rs new file mode 100644 index 0000000000..222c8584df --- /dev/null +++ b/crates/hir/src/body.rs @@ -0,0 +1,576 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::ops::Index; +use std::sync::Arc; + +use elp_base_db::FileId; +use elp_syntax::ast; +use elp_syntax::AstNode; +use elp_syntax::AstPtr; +use elp_syntax::TextRange; +use fxhash::FxHashMap; +use la_arena::Arena; +use la_arena::ArenaMap; + +use crate::db::MinDefDatabase; +use crate::db::MinInternDatabase; +use crate::expr::ClauseId; +use crate::fold::ExprCallBack; +use crate::fold::PatCallBack; +use crate::AnyExprId; +use crate::AnyExprRef; +use crate::Attribute; +use crate::AttributeId; +use crate::Callback; +use crate::CallbackId; +use crate::Clause; +use crate::CompileOption; +use crate::CompileOptionId; +use crate::DefineId; +use crate::Expr; +use crate::ExprId; +use crate::FoldCtx; +use crate::FormList; +use crate::Function; +use crate::FunctionId; +use crate::InFile; +use crate::Pat; +use crate::PatId; +use crate::RecordFieldBody; +use crate::RecordId; +use crate::ResolvedMacro; +use crate::Spec; +use crate::SpecId; +use crate::SpecSig; +use crate::Strategy; +use crate::Term; +use crate::TermId; +use crate::TypeAlias; +use crate::TypeAliasId; +use crate::TypeExpr; +use crate::TypeExprId; +use crate::Var; + +mod lower; +mod pretty; +pub mod scope; + +#[cfg(test)] +mod tests; +mod tree_print; + +#[derive(Debug, PartialEq, Eq, Default)] +pub struct Body { + pub exprs: Arena, + pub pats: Arena, + pub type_exprs: Arena, + pub terms: Arena, +} + +/// A wrapper around `Body` that indexes the macro expansion points +#[derive(Debug, PartialEq, Eq)] +pub struct UnexpandedIndex<'a>(pub &'a Body); + +#[derive(Debug, PartialEq, Eq)] +pub struct FunctionBody { + pub body: Arc, + pub clauses: Arena, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct TypeBody { + pub body: Arc, + pub vars: Vec, + pub ty: TypeExprId, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct SpecBody { + pub body: Arc, + pub sigs: Vec, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct RecordBody { + pub body: Arc, + pub fields: Vec, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct AttributeBody { + pub body: Arc, + pub value: TermId, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct DefineBody { + pub body: Arc, + pub expr: ExprId, +} + +impl Body { + fn shrink_to_fit(&mut self) { + // Exhaustive match to require handling new fields. + let Body { + exprs, + pats, + type_exprs, + terms, + } = self; + exprs.shrink_to_fit(); + pats.shrink_to_fit(); + type_exprs.shrink_to_fit(); + terms.shrink_to_fit(); + } + + pub fn print_any_expr(&self, db: &dyn MinInternDatabase, expr: AnyExprId) -> String { + match expr { + AnyExprId::Expr(expr_id) => pretty::print_expr(db, self, expr_id), + AnyExprId::Pat(pat_id) => pretty::print_pat(db, self, pat_id), + AnyExprId::TypeExpr(type_id) => pretty::print_type(db, self, type_id), + AnyExprId::Term(term_id) => pretty::print_term(db, self, term_id), + } + } + + pub fn tree_print_any_expr(&self, db: &dyn MinInternDatabase, expr: AnyExprId) -> String { + match expr { + AnyExprId::Expr(expr_id) => tree_print::print_expr(db, self, expr_id), + AnyExprId::Pat(pat_id) => tree_print::print_pat(db, self, pat_id), + AnyExprId::TypeExpr(type_id) => tree_print::print_type(db, self, type_id), + AnyExprId::Term(term_id) => tree_print::print_term(db, self, term_id), + } + } + + pub fn get_any(&self, id: AnyExprId) -> AnyExprRef<'_> { + match id { + AnyExprId::Expr(expr_id) => AnyExprRef::Expr(&self[expr_id]), + AnyExprId::Pat(pat_id) => AnyExprRef::Pat(&self[pat_id]), + AnyExprId::TypeExpr(type_id) => AnyExprRef::TypeExpr(&self[type_id]), + AnyExprId::Term(term_id) => AnyExprRef::Term(&self[term_id]), + } + } + + pub fn expr_id(&self, expr: &Expr) -> Option { + self.exprs + .iter() + .find_map(|(k, v)| if v == expr { Some(k.clone()) } else { None }) + } + + pub fn fold_expr<'a, T>( + &self, + strategy: Strategy, + expr_id: ExprId, + initial: T, + for_expr: ExprCallBack<'a, T>, + for_pat: PatCallBack<'a, T>, + ) -> T { + FoldCtx::fold_expr(&self, strategy, expr_id, initial, for_expr, for_pat) + } + + pub fn fold_pat<'a, T>( + &self, + strategy: Strategy, + pat_id: PatId, + initial: T, + for_expr: ExprCallBack<'a, T>, + for_pat: PatCallBack<'a, T>, + ) -> T { + FoldCtx::fold_pat(&self, strategy, pat_id, initial, for_expr, for_pat) + } +} + +impl FunctionBody { + pub(crate) fn function_body_with_source_query( + db: &dyn MinDefDatabase, + function_id: InFile, + ) -> (Arc, Arc) { + let form_list = db.file_form_list(function_id.file_id); + let function = &form_list[function_id.value]; + let function_ast = function.form_id.get(&function_id.file_syntax(db.upcast())); + + let mut ctx = lower::Ctx::new(db, function_id.file_id); + ctx.set_function_info(&function.name); + let (body, source_map) = ctx.lower_function(&function_ast); + (Arc::new(body), Arc::new(source_map)) + } + + pub fn print(&self, db: &dyn MinInternDatabase, form: &Function) -> String { + pretty::print_function(db, self, form) + } + + pub fn tree_print(&self, db: &dyn MinInternDatabase) -> String { + tree_print::print_function(db, self) + } +} + +impl TypeBody { + pub(crate) fn type_body_with_source_query( + db: &dyn MinDefDatabase, + type_alias_id: InFile, + ) -> (Arc, Arc) { + let form_list = db.file_form_list(type_alias_id.file_id); + let ctx = lower::Ctx::new(db, type_alias_id.file_id); + let source = type_alias_id.file_syntax(db.upcast()); + let (body, source_map) = match form_list[type_alias_id.value] { + TypeAlias::Regular { form_id, .. } => ctx.lower_type_alias(&form_id.get(&source)), + TypeAlias::Opaque { form_id, .. } => ctx.lower_opaque_type_alias(&form_id.get(&source)), + }; + (Arc::new(body), Arc::new(source_map)) + } + + pub fn print(&self, db: &dyn MinInternDatabase, form: &TypeAlias) -> String { + pretty::print_type_alias(db, self, form) + } + + pub fn tree_print(&self, db: &dyn MinInternDatabase, form: &TypeAlias) -> String { + tree_print::print_type_alias(db, self, form) + } +} + +impl DefineBody { + pub(crate) fn define_body_with_source_query( + db: &dyn MinDefDatabase, + define_id: InFile, + ) -> Option<(Arc, Arc)> { + let form_list = db.file_form_list(define_id.file_id); + let source = define_id.file_syntax(db.upcast()); + let define = &form_list[define_id.value]; + let define_ast = define.form_id.get(&source); + let (body, source_map) = + lower::Ctx::new(db, define_id.file_id).lower_define(&define_ast)?; + Some((Arc::new(body), Arc::new(source_map))) + } +} + +pub enum SpecOrCallback { + Spec(Spec), + Callback(Callback), +} + +impl SpecBody { + pub(crate) fn spec_body_with_source_query( + db: &dyn MinDefDatabase, + spec_id: InFile, + ) -> (Arc, Arc) { + let form_list = db.file_form_list(spec_id.file_id); + let spec_ast = form_list[spec_id.value] + .form_id + .get(&spec_id.file_syntax(db.upcast())); + + let (body, source_map) = lower::Ctx::new(db, spec_id.file_id).lower_spec(&spec_ast); + (Arc::new(body), Arc::new(source_map)) + } + + pub(crate) fn callback_body_with_source_query( + db: &dyn MinDefDatabase, + callback_id: InFile, + ) -> (Arc, Arc) { + let form_list = db.file_form_list(callback_id.file_id); + let callback_ast = form_list[callback_id.value] + .form_id + .get(&callback_id.file_syntax(db.upcast())); + + let (body, source_map) = + lower::Ctx::new(db, callback_id.file_id).lower_callback(&callback_ast); + (Arc::new(body), Arc::new(source_map)) + } + + pub fn print(&self, db: &dyn MinInternDatabase, form: SpecOrCallback) -> String { + pretty::print_spec(db, self, form) + } +} + +impl RecordBody { + pub(crate) fn record_body_with_source_query( + db: &dyn MinDefDatabase, + record_id: InFile, + ) -> (Arc, Arc) { + let form_list = db.file_form_list(record_id.file_id); + let record = &form_list[record_id.value]; + let record_ast = record.form_id.get(&record_id.file_syntax(db.upcast())); + + let (body, source_map) = + lower::Ctx::new(db, record_id.file_id).lower_record(record, &record_ast); + (Arc::new(body), Arc::new(source_map)) + } + + pub fn print( + &self, + db: &dyn MinInternDatabase, + form_list: &FormList, + record_id: RecordId, + ) -> String { + let form = &form_list[record_id]; + pretty::print_record(db, self, form, form_list) + } +} + +pub enum AnyAttribute { + CompileOption(CompileOption), + Attribute(Attribute), +} + +impl AttributeBody { + pub(crate) fn attribute_body_with_source_query( + db: &dyn MinDefDatabase, + attribute_id: InFile, + ) -> (Arc, Arc) { + let form_list = db.file_form_list(attribute_id.file_id); + let attribute_ast = form_list[attribute_id.value] + .form_id + .get(&attribute_id.file_syntax(db.upcast())); + + let (body, source_map) = + lower::Ctx::new(db, attribute_id.file_id).lower_attribute(&attribute_ast); + (Arc::new(body), Arc::new(source_map)) + } + + pub(crate) fn compile_body_with_source_query( + db: &dyn MinDefDatabase, + attribute_id: InFile, + ) -> (Arc, Arc) { + let form_list = db.file_form_list(attribute_id.file_id); + let attribute_ast = form_list[attribute_id.value] + .form_id + .get(&attribute_id.file_syntax(db.upcast())); + + let (body, source_map) = + lower::Ctx::new(db, attribute_id.file_id).lower_compile(&attribute_ast); + (Arc::new(body), Arc::new(source_map)) + } + + pub fn print(&self, db: &dyn MinInternDatabase, form: AnyAttribute) -> String { + pretty::print_attribute(db, self, &form) + } + + pub fn tree_print(&self, db: &dyn MinInternDatabase, form: AnyAttribute) -> String { + tree_print::print_attribute(db, self, &form) + } +} + +impl Index for FunctionBody { + type Output = Clause; + + fn index(&self, index: ClauseId) -> &Self::Output { + &self.clauses[index] + } +} + +impl<'a> Index for UnexpandedIndex<'a> { + type Output = Expr; + + fn index(&self, index: ExprId) -> &Self::Output { + // Do not "look through" macro expansion + &self.0.exprs[index] + } +} + +impl Index for Body { + type Output = Expr; + + fn index(&self, index: ExprId) -> &Self::Output { + // "look through" macro expansion. + match &self.exprs[index] { + Expr::MacroCall { expansion, args: _ } => &self.exprs[*expansion], + expr => expr, + } + } +} + +impl<'a> Index for UnexpandedIndex<'a> { + type Output = Pat; + + fn index(&self, index: PatId) -> &Self::Output { + // Do not "look through" macro expansion + &self.0.pats[index] + } +} + +impl Index for Body { + type Output = Pat; + + fn index(&self, index: PatId) -> &Self::Output { + // "look through" macro expansion. + match &self.pats[index] { + Pat::MacroCall { expansion, args: _ } => &self.pats[*expansion], + pat => pat, + } + } +} + +impl<'a> Index for UnexpandedIndex<'a> { + type Output = TypeExpr; + + fn index(&self, index: TypeExprId) -> &Self::Output { + // Do not "look through" macro expansion + &self.0.type_exprs[index] + } +} + +impl Index for Body { + type Output = TypeExpr; + + fn index(&self, index: TypeExprId) -> &Self::Output { + // "look through" macro expansion. + match &self.type_exprs[index] { + TypeExpr::MacroCall { expansion, args: _ } => &self.type_exprs[*expansion], + type_expr => type_expr, + } + } +} + +impl<'a> Index for UnexpandedIndex<'a> { + type Output = Term; + + fn index(&self, index: TermId) -> &Self::Output { + // Do not "look through" macro expansion + &self.0.terms[index] + } +} + +impl Index for Body { + type Output = Term; + + fn index(&self, index: TermId) -> &Self::Output { + // "look through" macro expansion. + match &self.terms[index] { + Term::MacroCall { expansion, args: _ } => &self.terms[*expansion], + term => term, + } + } +} + +pub type ExprSource = InFileAstPtr; + +pub type MacroSource = InFileAstPtr; + +#[derive(Clone, PartialEq, Eq, Debug, Hash)] +pub struct InFileAstPtr(InFile>) +where + T: AstNode, + InFile>: Copy; + +impl Copy for InFileAstPtr +where + T: AstNode + std::clone::Clone, + InFile>: Copy, +{ +} + +impl InFileAstPtr { + pub fn new(file_id: FileId, ptr: AstPtr) -> InFileAstPtr { + Self(InFile::new(file_id, ptr)) + } + + fn from_infile(in_file: InFile<&T>) -> InFileAstPtr { + InFileAstPtr::new(in_file.file_id, AstPtr::new(in_file.value)) + } + + pub fn file_id(&self) -> FileId { + self.0.file_id + } + + // Restrict access to the bare AstPtr. This allows us to prevent + // calls to AstPtr::to_node with the incorrect SourceFile ast. + // Because HIR expands macros, when walking the HIR ast you can + // easily have an original ast item originating from a different + // file, and it will then panic. + pub(crate) fn value(&self) -> AstPtr { + self.0.value + } + + pub fn to_node(&self, parse: &InFile) -> Option { + if self.0.file_id == parse.file_id { + Some(self.0.value.to_node(&parse.value.syntax())) + } else { + None + } + } + + pub(crate) fn range(&self) -> TextRange { + self.0.value.syntax_node_ptr().range() + } + + #[cfg(test)] + pub(crate) fn syntax_ptr_string(&self) -> String { + format!("{:?}", self.0.value.syntax_node_ptr()) + } +} + +/// A form body together with the mapping from syntax nodes to HIR expression +/// IDs. This is needed to go from e.g. a position in a file to the HIR +/// expression containing it; but for static analysis, got definition, etc., +/// we want to operate on a structure that is agnostic to the actual positions +/// of expressions in the file, so that we don't recompute results whenever some +/// whitespace is typed, or unrelated details in the file change. +/// +/// One complication here is that, due to macro expansion, a single `Body` might +/// be spread across several files. So, for each ExprId and PatId, we record +/// both the FileId and the position inside the file. +#[derive(Default, Debug, Eq, PartialEq)] +pub struct BodySourceMap { + expr_map: FxHashMap, + expr_map_back: ArenaMap, + pat_map: FxHashMap, + pat_map_back: ArenaMap, + type_expr_map: FxHashMap, + type_expr_map_back: ArenaMap, + term_map: FxHashMap, + term_map_back: ArenaMap, + macro_map: FxHashMap, +} + +impl BodySourceMap { + pub fn expr_id(&self, expr: InFile<&ast::Expr>) -> Option { + self.expr_map.get(&InFileAstPtr::from_infile(expr)).copied() + } + + pub fn expr(&self, expr_id: ExprId) -> Option { + self.expr_map_back.get(expr_id).copied() + } + + pub fn pat_id(&self, expr: InFile<&ast::Expr>) -> Option { + self.pat_map.get(&InFileAstPtr::from_infile(expr)).copied() + } + + pub fn pat(&self, pat_id: PatId) -> Option { + self.pat_map_back.get(pat_id).copied() + } + + pub fn type_expr_id(&self, expr: InFile<&ast::Expr>) -> Option { + self.type_expr_map + .get(&InFileAstPtr::from_infile(expr)) + .copied() + } + + pub fn term_id(&self, expr: InFile<&ast::Expr>) -> Option { + self.term_map.get(&InFileAstPtr::from_infile(expr)).copied() + } + + pub fn any_id(&self, expr: InFile<&ast::Expr>) -> Option { + let ptr = InFileAstPtr::from_infile(expr); + let expr_id = self.expr_map.get(&ptr).copied().map(AnyExprId::Expr); + expr_id + .or_else(|| self.pat_map.get(&ptr).copied().map(AnyExprId::Pat)) + .or_else(|| { + self.type_expr_map + .get(&ptr) + .copied() + .map(AnyExprId::TypeExpr) + }) + .or_else(|| self.term_map.get(&ptr).copied().map(AnyExprId::Term)) + } + + pub fn resolved_macro(&self, call: InFile<&ast::MacroCallExpr>) -> Option { + self.macro_map + .get(&InFileAstPtr::from_infile(call)) + .copied() + } +} diff --git a/crates/hir/src/body/lower.rs b/crates/hir/src/body/lower.rs new file mode 100644 index 0000000000..8b86b79905 --- /dev/null +++ b/crates/hir/src/body/lower.rs @@ -0,0 +1,2390 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::iter; +use std::sync::Arc; + +use either::Either; +use elp_base_db::FileId; +use elp_syntax::ast; +use elp_syntax::ast::ExprMax; +use elp_syntax::ast::MacroCallArgs; +use elp_syntax::ast::MacroDefReplacement; +use elp_syntax::ast::MapOp; +use elp_syntax::unescape; +use elp_syntax::AstPtr; +use fxhash::FxHashMap; + +use super::InFileAstPtr; +use crate::db::MinDefDatabase; +use crate::expr::MaybeExpr; +use crate::known; +use crate::macro_exp; +use crate::macro_exp::BuiltInMacro; +use crate::name::AsName; +use crate::Atom; +use crate::AttributeBody; +use crate::BinarySeg; +use crate::Body; +use crate::BodySourceMap; +use crate::CRClause; +use crate::CallTarget; +use crate::CatchClause; +use crate::Clause; +use crate::ComprehensionBuilder; +use crate::ComprehensionExpr; +use crate::DefineBody; +use crate::DefineId; +use crate::Expr; +use crate::ExprId; +use crate::ExprSource; +use crate::FunType; +use crate::FunctionBody; +use crate::IfClause; +use crate::InFile; +use crate::ListType; +use crate::Literal; +use crate::MacroName; +use crate::Name; +use crate::NameArity; +use crate::Pat; +use crate::PatId; +use crate::ReceiveAfter; +use crate::Record; +use crate::RecordBody; +use crate::RecordFieldBody; +use crate::ResolvedMacro; +use crate::SpecBody; +use crate::SpecSig; +use crate::Term; +use crate::TermId; +use crate::TypeBody; +use crate::TypeExpr; +use crate::TypeExprId; +use crate::Var; + +struct MacroStackEntry { + name: MacroName, + file_id: FileId, + var_map: FxHashMap, + parent_id: usize, +} + +pub struct Ctx<'a> { + db: &'a dyn MinDefDatabase, + original_file_id: FileId, + macro_stack: Vec, + macro_stack_id: usize, + function_info: Option<(Atom, u32)>, + body: Body, + source_map: BodySourceMap, +} + +#[derive(Debug)] +enum MacroReplacement { + BuiltIn(BuiltInMacro), + Ast(ast::MacroDefReplacement), + BuiltInArgs(BuiltInMacro, MacroCallArgs), + AstArgs(ast::MacroDefReplacement, MacroCallArgs), +} + +impl<'a> Ctx<'a> { + pub fn new(db: &'a dyn MinDefDatabase, file_id: FileId) -> Self { + Self { + db, + original_file_id: file_id, + macro_stack: vec![MacroStackEntry { + name: MacroName::new(Name::MISSING, None), + file_id, + var_map: FxHashMap::default(), + parent_id: 0, + }], + macro_stack_id: 0, + function_info: None, + body: Body::default(), + source_map: BodySourceMap::default(), + } + } + + pub fn set_function_info(&mut self, info: &NameArity) { + let name = self.db.atom(info.name().clone()); + let arity = info.arity(); + self.function_info = Some((name, arity)); + } + + fn finish(mut self) -> (Arc, BodySourceMap) { + // Verify macro expansion state + let entry = self.macro_stack.pop().expect("BUG: macro stack empty"); + assert_eq!(entry.file_id, self.original_file_id); + assert_eq!(entry.parent_id, 0); + assert!(entry.var_map.is_empty()); + assert!(self.macro_stack.is_empty()); + + self.body.shrink_to_fit(); + (Arc::new(self.body), self.source_map) + } + + pub fn lower_function(mut self, function: &ast::FunDecl) -> (FunctionBody, BodySourceMap) { + let clauses = function + .clauses() + .flat_map(|clause| self.lower_clause_or_macro(clause)) + .collect(); + let (body, source_map) = self.finish(); + + (FunctionBody { body, clauses }, source_map) + } + + pub fn lower_type_alias(self, type_alias: &ast::TypeAlias) -> (TypeBody, BodySourceMap) { + self.do_lower_type_alias(type_alias.name(), type_alias.ty()) + } + + pub fn lower_opaque_type_alias(self, type_alias: &ast::Opaque) -> (TypeBody, BodySourceMap) { + self.do_lower_type_alias(type_alias.name(), type_alias.ty()) + } + + fn do_lower_type_alias( + mut self, + name: Option, + ty: Option, + ) -> (TypeBody, BodySourceMap) { + let vars = name + .and_then(|name| name.args()) + .iter() + .flat_map(|args| args.args()) + .map(|var| self.db.var(var.as_name())) + .collect(); + let ty = self.lower_optional_type_expr(ty); + let (body, source_map) = self.finish(); + + (TypeBody { body, vars, ty }, source_map) + } + + pub fn lower_record( + mut self, + record: &Record, + ast: &ast::RecordDecl, + ) -> (RecordBody, BodySourceMap) { + let fields = record + .fields + .clone() + .zip(ast.fields()) + .map(|(field_id, field)| { + let expr = field + .expr() + .and_then(|field| field.expr()) + .map(|expr| self.lower_expr(&expr)); + let ty = field + .ty() + .and_then(|field| field.expr()) + .map(|expr| self.lower_type_expr(&expr)); + RecordFieldBody { field_id, expr, ty } + }) + .collect(); + + let (body, source_map) = self.finish(); + (RecordBody { body, fields }, source_map) + } + + pub fn lower_spec(mut self, spec: &ast::Spec) -> (SpecBody, BodySourceMap) { + let sigs = self.lower_sigs(spec.sigs()); + let (body, source_map) = self.finish(); + (SpecBody { body, sigs }, source_map) + } + + pub fn lower_callback(mut self, callback: &ast::Callback) -> (SpecBody, BodySourceMap) { + let sigs = self.lower_sigs(callback.sigs()); + let (body, source_map) = self.finish(); + (SpecBody { body, sigs }, source_map) + } + + fn lower_sigs(&mut self, sigs: impl Iterator) -> Vec { + sigs.map(|sig| { + let args = sig + .args() + .iter() + .flat_map(|args| args.args()) + .map(|arg| self.lower_type_expr(&arg)) + .collect(); + let result = self.lower_optional_type_expr(sig.ty()); + let guards = sig + .guard() + .iter() + .flat_map(|guards| guards.guards()) + .flat_map(|guard| { + let ty = self.lower_optional_type_expr(guard.ty()); + let var = self.db.var(guard.var()?.var()?.as_name()); + Some((var, ty)) + }) + .collect(); + SpecSig { + args, + result, + guards, + } + }) + .collect() + } + + pub fn lower_attribute(mut self, attr: &ast::WildAttribute) -> (AttributeBody, BodySourceMap) { + let value = self.lower_optional_term(attr.value()); + let (body, source_map) = self.finish(); + (AttributeBody { body, value }, source_map) + } + + pub fn lower_define(mut self, define: &ast::PpDefine) -> Option<(DefineBody, BodySourceMap)> { + let replacement = define.replacement()?; + match replacement { + MacroDefReplacement::Expr(expr) => { + let expr = self.lower_expr(&expr); + let (body, source_map) = self.finish(); + Some((DefineBody { body, expr }, source_map)) + } + _ => None, + } + } + + pub fn lower_compile( + mut self, + attr: &ast::CompileOptionsAttribute, + ) -> (AttributeBody, BodySourceMap) { + let value = self.lower_optional_term(attr.options()); + let (body, source_map) = self.finish(); + (AttributeBody { body, value }, source_map) + } + + fn lower_clause_or_macro( + &mut self, + clause: ast::FunctionOrMacroClause, + ) -> impl Iterator { + match clause { + ast::FunctionOrMacroClause::FunctionClause(clause) => { + Either::Left(self.lower_clause(&clause).into_iter()) + } + ast::FunctionOrMacroClause::MacroCallExpr(call) => { + Either::Right( + self.resolve_macro(&call, |this, _source, replacement| { + match replacement { + MacroReplacement::Ast( + ast::MacroDefReplacement::ReplacementFunctionClauses(clauses), + ) => clauses + .clauses() + .flat_map(|clause| this.lower_clause_or_macro(clause)) + .collect(), + // no built-in macro makes sense in this place + MacroReplacement::Ast(_) | MacroReplacement::BuiltIn(_) => vec![], + // args make no sense here + MacroReplacement::AstArgs(_, _) + | MacroReplacement::BuiltInArgs(_, _) => vec![], + } + }) + .into_iter() + .flatten(), + ) + } + } + } + + fn lower_clause(&mut self, clause: &ast::FunctionClause) -> Option { + let pats = clause + .args() + .iter() + .flat_map(|args| args.args()) + .map(|pat| self.lower_pat(&pat)) + .collect(); + let guards = self.lower_guards(clause.guard()); + let exprs = self.lower_clause_body(clause.body()); + + Some(Clause { + pats, + guards, + exprs, + }) + } + + fn lower_optional_pat(&mut self, expr: Option) -> PatId { + if let Some(expr) = &expr { + self.lower_pat(expr) + } else { + self.alloc_pat(Pat::Missing, None) + } + } + + fn lower_pat(&mut self, expr: &ast::Expr) -> PatId { + match expr { + ast::Expr::ExprMax(expr_max) => self.lower_pat_max(expr_max, expr), + ast::Expr::AnnType(ann) => { + let _ = self.lower_optional_pat(ann.ty()); + self.alloc_pat(Pat::Missing, Some(expr)) + } + ast::Expr::BinaryOpExpr(binary_op) => { + let lhs = self.lower_optional_pat(binary_op.lhs()); + let rhs = self.lower_optional_pat(binary_op.rhs()); + if let Some((op, _)) = binary_op.op() { + self.alloc_pat(Pat::BinaryOp { lhs, op, rhs }, Some(expr)) + } else { + self.alloc_pat(Pat::Missing, Some(expr)) + } + } + ast::Expr::Call(call) => { + let _ = self.lower_optional_pat(call.expr()); + let _ = call + .args() + .iter() + .flat_map(|args| args.args()) + .for_each(|expr| { + let _ = self.lower_pat(&expr); + }); + self.alloc_pat(Pat::Missing, Some(expr)) + } + ast::Expr::CatchExpr(catch) => { + let _ = self.lower_optional_pat(catch.expr()); + self.alloc_pat(Pat::Missing, Some(expr)) + } + ast::Expr::Dotdotdot(_) => self.alloc_pat(Pat::Missing, Some(expr)), + ast::Expr::MapExpr(map) => { + let fields = map + .fields() + .flat_map(|field| { + let key = self.lower_optional_expr(field.key()); + let value = self.lower_optional_pat(field.value()); + if let Some((ast::MapOp::Exact, _)) = field.op() { + Some((key, value)) + } else { + None + } + }) + .collect(); + self.alloc_pat(Pat::Map { fields }, Some(expr)) + } + ast::Expr::MapExprUpdate(update) => { + let _ = self.lower_optional_pat(update.expr().map(Into::into)); + let _ = update.fields().for_each(|field| { + let _ = self.lower_optional_expr(field.key()); + let _ = self.lower_optional_expr(field.value()); + }); + self.alloc_pat(Pat::Missing, Some(expr)) + } + ast::Expr::MatchExpr(mat) => { + let lhs = self.lower_optional_pat(mat.lhs()); + let rhs = self.lower_optional_pat(mat.rhs()); + self.alloc_pat(Pat::Match { lhs, rhs }, Some(expr)) + } + ast::Expr::Pipe(pipe) => { + let _ = self.lower_optional_pat(pipe.lhs()); + let _ = self.lower_optional_pat(pipe.rhs()); + self.alloc_pat(Pat::Missing, Some(expr)) + } + ast::Expr::RangeType(range) => { + let _ = self.lower_optional_pat(range.lhs()); + let _ = self.lower_optional_pat(range.rhs()); + self.alloc_pat(Pat::Missing, Some(expr)) + } + ast::Expr::RecordExpr(record) => { + let name = record.name().and_then(|n| self.resolve_name(n.name()?)); + let fields = record + .fields() + .flat_map(|field| { + let value = + self.lower_optional_pat(field.expr().and_then(|expr| expr.expr())); + let name = self.resolve_name(field.name()?)?; + Some((name, value)) + }) + .collect(); + if let Some(name) = name { + self.alloc_pat(Pat::Record { name, fields }, Some(expr)) + } else { + self.alloc_pat(Pat::Missing, Some(expr)) + } + } + ast::Expr::RecordFieldExpr(field) => { + let _ = self.lower_optional_pat(field.expr().map(Into::into)); + self.alloc_pat(Pat::Missing, Some(expr)) + } + ast::Expr::RecordIndexExpr(index) => { + let name = index.name().and_then(|n| self.resolve_name(n.name()?)); + let field = index.field().and_then(|n| self.resolve_name(n.name()?)); + if let (Some(name), Some(field)) = (name, field) { + self.alloc_pat(Pat::RecordIndex { name, field }, Some(expr)) + } else { + self.alloc_pat(Pat::Missing, Some(expr)) + } + } + ast::Expr::RecordUpdateExpr(update) => { + let _ = self.lower_optional_pat(update.expr().map(Into::into)); + let _ = update + .fields() + .flat_map(|field| field.expr()?.expr()) + .for_each(|expr| { + let _ = self.lower_pat(&expr); + }); + self.alloc_pat(Pat::Missing, Some(expr)) + } + ast::Expr::Remote(remote) => { + let _ = self.lower_optional_pat( + remote + .module() + .and_then(|module| module.module()) + .map(Into::into), + ); + let _ = self.lower_optional_pat(remote.fun().map(Into::into)); + self.alloc_pat(Pat::Missing, Some(expr)) + } + ast::Expr::UnaryOpExpr(unary_op) => { + let operand = self.lower_optional_pat(unary_op.operand()); + if let Some((op, _)) = unary_op.op() { + self.alloc_pat(Pat::UnaryOp { pat: operand, op }, Some(expr)) + } else { + self.alloc_pat(Pat::Missing, Some(expr)) + } + } + ast::Expr::CondMatchExpr(cond) => { + self.lower_optional_pat(cond.lhs()); + self.lower_optional_pat(cond.rhs()); + self.alloc_pat(Pat::Missing, Some(expr)) + } + } + } + + fn lower_pat_max(&mut self, expr_max: &ast::ExprMax, expr: &ast::Expr) -> PatId { + match expr_max { + ast::ExprMax::AnonymousFun(fun) => { + let _ = fun.clauses().for_each(|clause| { + let _ = clause + .args() + .iter() + .flat_map(|args| args.args()) + .for_each(|pat| { + let _ = self.lower_pat(&pat); + }); + let _ = self.lower_guards(clause.guard()); + let _ = self.lower_clause_body(clause.body()); + }); + self.alloc_pat(Pat::Missing, Some(expr)) + } + ast::ExprMax::Atom(atom) => { + let atom = self.db.atom(atom.as_name()); + self.alloc_pat(Pat::Literal(Literal::Atom(atom)), Some(expr)) + } + ast::ExprMax::Binary(bin) => { + let segs = bin + .elements() + .flat_map(|element| self.lower_bin_element(&element, Self::lower_optional_pat)) + .collect(); + self.alloc_pat(Pat::Binary { segs }, Some(expr)) + } + ast::ExprMax::BinaryComprehension(bc) => { + let _ = self.lower_optional_pat(bc.expr().map(Into::into)); + let _ = self.lower_lc_exprs(bc.lc_exprs()); + self.alloc_pat(Pat::Missing, Some(expr)) + } + ast::ExprMax::BlockExpr(block) => { + let _ = block.exprs().for_each(|expr| { + self.lower_expr(&expr); + }); + self.alloc_pat(Pat::Missing, Some(expr)) + } + ast::ExprMax::CaseExpr(case) => { + let _ = self.lower_optional_pat(case.expr()); + let _ = case + .clauses() + .flat_map(|clause| self.lower_cr_clause(clause)) + .last(); + self.alloc_pat(Pat::Missing, Some(expr)) + } + ast::ExprMax::Char(char) => { + let value = lower_char(char).map_or(Pat::Missing, Pat::Literal); + self.alloc_pat(value, Some(expr)) + } + ast::ExprMax::Concatables(concat) => { + let value = lower_concat(concat).map_or(Pat::Missing, Pat::Literal); + self.alloc_pat(value, Some(expr)) + } + ast::ExprMax::ExternalFun(fun) => { + let _ = self.lower_optional_pat( + fun.module() + .and_then(|module| module.name()) + .map(Into::into), + ); + let _ = self.lower_optional_pat(fun.fun().map(Into::into)); + let _ = self.lower_optional_pat( + fun.arity().and_then(|arity| arity.value()).map(Into::into), + ); + self.alloc_pat(Pat::Missing, Some(expr)) + } + ast::ExprMax::Float(float) => { + let value = lower_float(float).map_or(Pat::Missing, Pat::Literal); + self.alloc_pat(value, Some(expr)) + } + ast::ExprMax::FunType(fun) => { + if let Some(sig) = fun.sig() { + let _ = self.lower_optional_pat(sig.ty()); + let _ = sig + .args() + .iter() + .flat_map(|args| args.args()) + .for_each(|pat| { + let _ = self.lower_pat(&pat); + }); + } + self.alloc_pat(Pat::Missing, Some(expr)) + } + ast::ExprMax::IfExpr(if_expr) => { + let _ = if_expr.clauses().for_each(|clause| { + let _ = self.lower_guards(clause.guard()); + let _ = self.lower_clause_body(clause.body()); + }); + self.alloc_pat(Pat::Missing, Some(expr)) + } + ast::ExprMax::Integer(int) => { + let value = lower_int(int).map_or(Pat::Missing, Pat::Literal); + self.alloc_pat(value, Some(expr)) + } + ast::ExprMax::InternalFun(fun) => { + let _ = self.lower_optional_pat(fun.fun().map(Into::into)); + let _ = self.lower_optional_pat( + fun.arity().and_then(|arity| arity.value()).map(Into::into), + ); + self.alloc_pat(Pat::Missing, Some(expr)) + } + ast::ExprMax::List(list) => { + let (pats, tail) = self.lower_list( + list, + |this| this.alloc_pat(Pat::Missing, None), + |this, expr| this.lower_pat(expr), + ); + self.alloc_pat(Pat::List { pats, tail }, Some(expr)) + } + ast::ExprMax::ListComprehension(lc) => { + let _ = self.lower_optional_pat(lc.expr()); + let _ = self.lower_lc_exprs(lc.lc_exprs()); + self.alloc_pat(Pat::Missing, Some(expr)) + } + ast::ExprMax::MacroCallExpr(call) => self + .resolve_macro(call, |this, source, replacement| match replacement { + MacroReplacement::BuiltIn(built_in) => this + .lower_built_in_macro(built_in) + .map(|literal| { + let pat_id = this.alloc_pat(Pat::Literal(literal), Some(expr)); + this.record_pat_source(pat_id, source); + pat_id + }), + MacroReplacement::Ast(ast::MacroDefReplacement::Expr(macro_expr)) => { + let pat_id = this.lower_pat(¯o_expr); + this.record_pat_source(pat_id, source); + Some(pat_id) + } + MacroReplacement::Ast(_) + // calls are not allowed in patterns + | MacroReplacement::BuiltInArgs(_, _) + | MacroReplacement::AstArgs(_, _) => None, + }) + .flatten() + .map(|expansion| { + let args = call + .args() + .iter() + .flat_map(|args| args.args()) + .map(|expr| self.lower_optional_expr(expr.expr())) + .collect(); + let expr_id = self.alloc_pat(Pat::MacroCall { expansion, args }, Some(expr)); + expr_id + }) + .unwrap_or_else(|| { + let _ = call + .args() + .iter() + .flat_map(|args| args.args()) + .for_each(|expr| { + let _ = self.lower_optional_pat(expr.expr()); + let _ = self.lower_optional_pat(expr.guard()); + }); + self.alloc_pat(Pat::Missing, Some(expr)) + }), + ast::ExprMax::MacroString(_) => self.alloc_pat(Pat::Missing, Some(expr)), + ExprMax::MapComprehension(map_comp) => { + self.lower_optional_pat(map_comp.expr().and_then(|mf| mf.key())); + self.lower_optional_pat(map_comp.expr().and_then(|mf| mf.value())); + let _ = self.lower_lc_exprs(map_comp.lc_exprs()); + self.alloc_pat(Pat::Missing, Some(expr)) + } + ast::ExprMax::MaybeExpr(maybe) => { + let _ = maybe.exprs().for_each(|expr| { + self.lower_expr(&expr); + }); + let _ = maybe + .clauses() + .flat_map(|clause| self.lower_cr_clause(clause)) + .last(); + self.alloc_pat(Pat::Missing, Some(expr)) + } + ast::ExprMax::ParenExpr(paren_expr) => { + if let Some(expr) = paren_expr.expr() { + let pat_id = self.lower_pat(&expr); + let ptr = AstPtr::new(&expr); + let source = InFileAstPtr::new(self.curr_file_id(), ptr); + self.record_pat_source(pat_id, source); + pat_id + } else { + self.alloc_pat(Pat::Missing, Some(expr)) + } + } + ast::ExprMax::ReceiveExpr(receive) => { + let _ = receive + .clauses() + .flat_map(|clause| self.lower_cr_clause(clause)) + .last(); + let _ = receive.after().map(|after| { + let _ = self.lower_optional_expr(after.expr()); + let _ = self.lower_clause_body(after.body()); + }); + self.alloc_pat(Pat::Missing, Some(expr)) + } + ast::ExprMax::String(str) => { + let value = lower_str(str).map_or(Pat::Missing, Pat::Literal); + self.alloc_pat(value, Some(expr)) + } + ast::ExprMax::TryExpr(try_expr) => { + let _ = try_expr.exprs().for_each(|expr| { + self.lower_pat(&expr); + }); + let _ = try_expr + .clauses() + .flat_map(|clause| self.lower_cr_clause(clause)) + .last(); + let _ = try_expr.catch().for_each(|clause| { + let _ = clause + .class() + .and_then(|class| class.class()) + .map(|class| self.lower_pat(&class.into())); + let _ = self.lower_optional_pat(clause.pat().map(Into::into)); + let _ = clause + .stack() + .and_then(|stack| stack.class()) + .map(|var| self.lower_pat(&ast::Expr::ExprMax(ast::ExprMax::Var(var)))); + let _ = self.lower_guards(clause.guard()); + let _ = self.lower_clause_body(clause.body()); + }); + let _ = try_expr + .after() + .iter() + .flat_map(|after| after.exprs()) + .for_each(|expr| { + self.lower_pat(&expr); + }); + self.alloc_pat(Pat::Missing, Some(expr)) + } + ast::ExprMax::Tuple(tup) => { + let pats = tup.expr().map(|expr| self.lower_pat(&expr)).collect(); + self.alloc_pat(Pat::Tuple { pats }, Some(expr)) + } + ast::ExprMax::Var(var) => self + .resolve_var(var, |this, expr| this.lower_optional_pat(expr.expr())) + .unwrap_or_else(|var| self.alloc_pat(Pat::Var(var), Some(expr))), + } + } + + fn lower_optional_expr(&mut self, expr: Option) -> ExprId { + if let Some(expr) = &expr { + self.lower_expr(expr) + } else { + self.alloc_expr(Expr::Missing, None) + } + } + + fn lower_expr(&mut self, expr: &ast::Expr) -> ExprId { + match expr { + ast::Expr::ExprMax(expr_max) => self.lower_expr_max(expr_max, expr), + ast::Expr::AnnType(ann) => { + let _ = self.lower_optional_expr(ann.ty()); + self.alloc_expr(Expr::Missing, Some(expr)) + } + ast::Expr::BinaryOpExpr(binary_op) => { + let lhs = self.lower_optional_expr(binary_op.lhs()); + let rhs = self.lower_optional_expr(binary_op.rhs()); + if let Some((op, _)) = binary_op.op() { + self.alloc_expr(Expr::BinaryOp { lhs, op, rhs }, Some(expr)) + } else { + self.alloc_expr(Expr::Missing, Some(expr)) + } + } + ast::Expr::Call(call) => { + let target = self.lower_call_target(call.expr()); + let args = call + .args() + .iter() + .flat_map(|args| args.args()) + .map(|expr| self.lower_expr(&expr)) + .collect(); + self.alloc_expr(Expr::Call { target, args }, Some(expr)) + } + ast::Expr::CatchExpr(catch) => { + let value = self.lower_optional_expr(catch.expr()); + self.alloc_expr(Expr::Catch { expr: value }, Some(expr)) + } + ast::Expr::Dotdotdot(_) => self.alloc_expr(Expr::Missing, Some(expr)), + ast::Expr::MapExpr(map) => { + let fields = map + .fields() + .flat_map(|field| { + let key = self.lower_optional_expr(field.key()); + let value = self.lower_optional_expr(field.value()); + if let Some((ast::MapOp::Assoc, _)) = field.op() { + Some((key, value)) + } else { + None + } + }) + .collect(); + self.alloc_expr(Expr::Map { fields }, Some(expr)) + } + ast::Expr::MapExprUpdate(update) => { + let base = self.lower_optional_expr(update.expr().map(Into::into)); + let fields = update + .fields() + .flat_map(|field| { + let key = self.lower_optional_expr(field.key()); + let value = self.lower_optional_expr(field.value()); + Some((key, field.op()?.0, value)) + }) + .collect(); + self.alloc_expr(Expr::MapUpdate { expr: base, fields }, Some(expr)) + } + ast::Expr::MatchExpr(mat) => { + let lhs = self.lower_optional_pat(mat.lhs()); + let rhs = self.lower_optional_expr(mat.rhs()); + self.alloc_expr(Expr::Match { lhs, rhs }, Some(expr)) + } + ast::Expr::Pipe(pipe) => { + let _ = self.lower_optional_expr(pipe.lhs()); + let _ = self.lower_optional_expr(pipe.rhs()); + self.alloc_expr(Expr::Missing, Some(expr)) + } + ast::Expr::RangeType(range) => { + let _ = self.lower_optional_expr(range.lhs()); + let _ = self.lower_optional_expr(range.rhs()); + self.alloc_expr(Expr::Missing, Some(expr)) + } + ast::Expr::RecordExpr(record) => { + let name = record.name().and_then(|n| self.resolve_name(n.name()?)); + let fields = record + .fields() + .flat_map(|field| { + let value = + self.lower_optional_expr(field.expr().and_then(|expr| expr.expr())); + let name = self.resolve_name(field.name()?)?; + Some((name, value)) + }) + .collect(); + if let Some(name) = name { + self.alloc_expr(Expr::Record { name, fields }, Some(expr)) + } else { + self.alloc_expr(Expr::Missing, Some(expr)) + } + } + ast::Expr::RecordFieldExpr(field) => { + let base = self.lower_optional_expr(field.expr().map(Into::into)); + let name = field.name().and_then(|n| self.resolve_name(n.name()?)); + let field = field.field().and_then(|n| self.resolve_name(n.name()?)); + if let (Some(name), Some(field)) = (name, field) { + self.alloc_expr( + Expr::RecordField { + expr: base, + name, + field, + }, + Some(expr), + ) + } else { + self.alloc_expr(Expr::Missing, Some(expr)) + } + } + ast::Expr::RecordIndexExpr(index) => { + let name = index.name().and_then(|n| self.resolve_name(n.name()?)); + let field = index.field().and_then(|n| self.resolve_name(n.name()?)); + if let (Some(name), Some(field)) = (name, field) { + self.alloc_expr(Expr::RecordIndex { name, field }, Some(expr)) + } else { + self.alloc_expr(Expr::Missing, Some(expr)) + } + } + ast::Expr::RecordUpdateExpr(update) => { + let base = self.lower_optional_expr(update.expr().map(Into::into)); + let name = update.name().and_then(|n| self.resolve_name(n.name()?)); + let fields = update + .fields() + .flat_map(|field| { + let value = + self.lower_optional_expr(field.expr().and_then(|expr| expr.expr())); + let name = self.resolve_name(field.name()?)?; + Some((name, value)) + }) + .collect(); + if let Some(name) = name { + self.alloc_expr( + Expr::RecordUpdate { + expr: base, + name, + fields, + }, + Some(expr), + ) + } else { + self.alloc_expr(Expr::Missing, Some(expr)) + } + } + ast::Expr::Remote(remote) => { + let _ = self.lower_optional_expr( + remote + .module() + .and_then(|module| module.module()) + .map(Into::into), + ); + let _ = self.lower_optional_expr(remote.fun().map(Into::into)); + self.alloc_expr(Expr::Missing, Some(expr)) + } + ast::Expr::UnaryOpExpr(unary_op) => { + let operand = self.lower_optional_expr(unary_op.operand()); + if let Some((op, _)) = unary_op.op() { + self.alloc_expr(Expr::UnaryOp { expr: operand, op }, Some(expr)) + } else { + self.alloc_expr(Expr::Missing, Some(expr)) + } + } + ast::Expr::CondMatchExpr(cond) => { + self.lower_optional_pat(cond.lhs()); + self.lower_optional_expr(cond.rhs()); + self.alloc_expr(Expr::Missing, Some(expr)) + } + } + } + + fn lower_call_target(&mut self, expr: Option) -> CallTarget { + match expr.as_ref() { + Some(ast::Expr::ExprMax(ast::ExprMax::ParenExpr(paren))) => { + self.lower_call_target(paren.expr()) + } + Some(ast::Expr::Remote(remote)) => CallTarget::Remote { + module: self.lower_optional_expr( + remote + .module() + .and_then(|module| module.module()) + .map(ast::Expr::ExprMax), + ), + name: self.lower_optional_expr(remote.fun().map(ast::Expr::ExprMax)), + }, + Some(ast::Expr::ExprMax(ast::ExprMax::MacroCallExpr(call))) => self + .resolve_macro(call, |this, source, replacement| match replacement { + MacroReplacement::BuiltIn(built_in) => { + this.lower_built_in_macro(built_in).map(|literal| { + let name = this.alloc_expr(Expr::Literal(literal), None); + this.record_expr_source(name, source); + CallTarget::Local { name } + }) + } + MacroReplacement::Ast(ast::MacroDefReplacement::Expr(expr)) => { + Some(this.lower_call_target(Some(expr))) + } + MacroReplacement::Ast(_) => None, + // This would mean double parens in the call - invalid + MacroReplacement::BuiltInArgs(_, _) | MacroReplacement::AstArgs(_, _) => None, + }) + .flatten() + .unwrap_or_else(|| { + let _ = call + .args() + .iter() + .flat_map(|args| args.args()) + .for_each(|expr| { + let _ = self.lower_optional_expr(expr.expr()); + let _ = self.lower_optional_expr(expr.guard()); + }); + CallTarget::Local { + name: self.alloc_expr(Expr::Missing, expr.as_ref()), + } + }), + Some(expr) => CallTarget::Local { + name: self.lower_expr(expr), + }, + None => CallTarget::Local { + name: self.alloc_expr(Expr::Missing, None), + }, + } + } + + fn lower_expr_max(&mut self, expr_max: &ast::ExprMax, expr: &ast::Expr) -> ExprId { + match expr_max { + ast::ExprMax::AnonymousFun(fun) => { + let mut name = None; + let clauses = fun + .clauses() + .map(|clause| { + if let Some(found_name) = clause.name() { + name = Some(self.lower_pat(&found_name.into())); + } + let pats = clause + .args() + .iter() + .flat_map(|args| args.args()) + .map(|pat| self.lower_pat(&pat)) + .collect(); + let guards = self.lower_guards(clause.guard()); + let exprs = self.lower_clause_body(clause.body()); + Clause { + pats, + guards, + exprs, + } + }) + .collect(); + self.alloc_expr(Expr::Closure { clauses, name }, Some(expr)) + } + ast::ExprMax::Atom(atom) => { + let atom = self.db.atom(atom.as_name()); + self.alloc_expr(Expr::Literal(Literal::Atom(atom)), Some(expr)) + } + ast::ExprMax::Binary(bin) => { + let segs = bin + .elements() + .flat_map(|element| self.lower_bin_element(&element, Self::lower_optional_expr)) + .collect(); + self.alloc_expr(Expr::Binary { segs }, Some(expr)) + } + ast::ExprMax::BinaryComprehension(bc) => { + let value = self.lower_optional_expr(bc.expr().map(Into::into)); + let builder = ComprehensionBuilder::Binary(value); + let exprs = self.lower_lc_exprs(bc.lc_exprs()); + self.alloc_expr(Expr::Comprehension { builder, exprs }, Some(expr)) + } + ast::ExprMax::BlockExpr(block) => { + let exprs = block.exprs().map(|expr| self.lower_expr(&expr)).collect(); + self.alloc_expr(Expr::Block { exprs }, Some(expr)) + } + ast::ExprMax::CaseExpr(case) => { + let value = self.lower_optional_expr(case.expr()); + let clauses = case + .clauses() + .flat_map(|clause| self.lower_cr_clause(clause)) + .collect(); + self.alloc_expr( + Expr::Case { + expr: value, + clauses, + }, + Some(expr), + ) + } + ast::ExprMax::Char(char) => { + let value = lower_char(char).map_or(Expr::Missing, Expr::Literal); + self.alloc_expr(value, Some(expr)) + } + ast::ExprMax::Concatables(concat) => { + let value = lower_concat(concat).map_or(Expr::Missing, Expr::Literal); + self.alloc_expr(value, Some(expr)) + } + ast::ExprMax::ExternalFun(fun) => { + let target = CallTarget::Remote { + module: self.lower_optional_expr( + fun.module() + .and_then(|module| module.name()) + .map(Into::into), + ), + name: self.lower_optional_expr(fun.fun().map(Into::into)), + }; + let arity = self.lower_optional_expr( + fun.arity().and_then(|arity| arity.value()).map(Into::into), + ); + self.alloc_expr(Expr::CaptureFun { target, arity }, Some(expr)) + } + ast::ExprMax::Float(float) => { + let value = lower_float(float).map_or(Expr::Missing, Expr::Literal); + self.alloc_expr(value, Some(expr)) + } + ast::ExprMax::FunType(fun) => { + if let Some(sig) = fun.sig() { + let _ = self.lower_optional_expr(sig.ty()); + let _ = sig + .args() + .iter() + .flat_map(|args| args.args()) + .for_each(|pat| { + let _ = self.lower_expr(&pat); + }); + } + self.alloc_expr(Expr::Missing, Some(expr)) + } + ast::ExprMax::IfExpr(if_expr) => { + let clauses = if_expr + .clauses() + .map(|clause| { + let guards = self.lower_guards(clause.guard()); + let exprs = self.lower_clause_body(clause.body()); + IfClause { guards, exprs } + }) + .collect(); + self.alloc_expr(Expr::If { clauses }, Some(expr)) + } + ast::ExprMax::Integer(int) => { + let value = lower_int(int).map_or(Expr::Missing, Expr::Literal); + self.alloc_expr(value, Some(expr)) + } + ast::ExprMax::InternalFun(fun) => { + let target = CallTarget::Local { + name: self.lower_optional_expr(fun.fun().map(Into::into)), + }; + let arity = self.lower_optional_expr( + fun.arity().and_then(|arity| arity.value()).map(Into::into), + ); + self.alloc_expr(Expr::CaptureFun { target, arity }, Some(expr)) + } + ast::ExprMax::List(list) => { + let (exprs, tail) = self.lower_list( + list, + |this| this.alloc_expr(Expr::Missing, None), + |this, expr| this.lower_expr(expr), + ); + self.alloc_expr(Expr::List { exprs, tail }, Some(expr)) + } + ast::ExprMax::ListComprehension(lc) => { + let value = self.lower_optional_expr(lc.expr()); + let builder = ComprehensionBuilder::List(value); + let exprs = self.lower_lc_exprs(lc.lc_exprs()); + self.alloc_expr(Expr::Comprehension { builder, exprs }, Some(expr)) + } + ast::ExprMax::MacroCallExpr(call) => self + .resolve_macro(call, |this, source, replacement| match replacement { + MacroReplacement::BuiltIn(built_in) => { + this.lower_built_in_macro(built_in).map(|literal| { + let expr_id = this.alloc_expr(Expr::Literal(literal), None); + this.record_expr_source(expr_id, source); + expr_id + }) + } + MacroReplacement::Ast(ast::MacroDefReplacement::Expr(macro_expr)) => { + let expr_id = this.lower_expr(¯o_expr); + this.record_expr_source(expr_id, source); + Some(expr_id) + } + MacroReplacement::Ast(_) => None, + MacroReplacement::BuiltInArgs(built_in, args) => { + let name = this + .lower_built_in_macro(built_in) + .map(|literal| this.alloc_expr(Expr::Literal(literal), None)) + .unwrap_or_else(|| this.alloc_expr(Expr::Missing, None)); + let target = CallTarget::Local { name }; + let args = args + .args() + .map(|expr| this.lower_optional_expr(expr.expr())) + .collect(); + let expr_id = this.alloc_expr(Expr::Call { target, args }, None); + this.record_expr_source(expr_id, source); + Some(expr_id) + } + MacroReplacement::AstArgs( + ast::MacroDefReplacement::Expr(replacement), + args, + ) => { + let target = this.lower_call_target(Some(replacement)); + let args = args + .args() + .map(|expr| this.lower_optional_expr(expr.expr())) + .collect(); + let expr_id = this.alloc_expr(Expr::Call { target, args }, None); + this.record_expr_source(expr_id, source); + Some(expr_id) + } + MacroReplacement::AstArgs(_, _) => None, + }) + .flatten() + .map(|expansion| { + let args = call + .args() + .iter() + .flat_map(|args| args.args()) + .map(|expr| self.lower_optional_expr(expr.expr())) + .collect(); + let expr_id = self.alloc_expr(Expr::MacroCall { expansion, args }, Some(expr)); + expr_id + }) + .unwrap_or_else(|| { + let _ = call + .args() + .iter() + .flat_map(|args| args.args()) + .for_each(|expr| { + let _ = self.lower_optional_expr(expr.expr()); + let _ = self.lower_optional_expr(expr.guard()); + }); + self.alloc_expr(Expr::Missing, Some(expr)) + }), + ast::ExprMax::MacroString(_) => self.alloc_expr(Expr::Missing, Some(expr)), + ast::ExprMax::ParenExpr(paren_expr) => { + if let Some(paren_expr) = paren_expr.expr() { + let expr_id = self.lower_expr(&paren_expr); + let ptr = AstPtr::new(expr); + let source = InFileAstPtr::new(self.curr_file_id(), ptr); + self.record_expr_source(expr_id, source); + expr_id + } else { + self.alloc_expr(Expr::Missing, Some(expr)) + } + } + ast::ExprMax::ReceiveExpr(receive) => { + let clauses = receive + .clauses() + .flat_map(|clause| self.lower_cr_clause(clause)) + .collect(); + let after = receive.after().map(|after| { + let timeout = self.lower_optional_expr(after.expr()); + let exprs = self.lower_clause_body(after.body()); + ReceiveAfter { timeout, exprs } + }); + self.alloc_expr(Expr::Receive { clauses, after }, Some(expr)) + } + ast::ExprMax::String(str) => { + let value = lower_str(str).map_or(Expr::Missing, Expr::Literal); + self.alloc_expr(value, Some(expr)) + } + ast::ExprMax::TryExpr(try_expr) => { + let exprs = try_expr + .exprs() + .map(|expr| self.lower_expr(&expr)) + .collect(); + let of_clauses = try_expr + .clauses() + .flat_map(|clause| self.lower_cr_clause(clause)) + .collect(); + let catch_clauses = try_expr + .catch() + .map(|clause| { + let class = clause + .class() + .and_then(|class| class.class()) + .map(|class| self.lower_pat(&class.into())); + let reason = self.lower_optional_pat(clause.pat().map(Into::into)); + let stack = clause + .stack() + .and_then(|stack| stack.class()) + .map(|var| self.lower_pat(&ast::Expr::ExprMax(ast::ExprMax::Var(var)))); + let guards = self.lower_guards(clause.guard()); + let exprs = self.lower_clause_body(clause.body()); + CatchClause { + class, + reason, + stack, + guards, + exprs, + } + }) + .collect(); + let after = try_expr + .after() + .iter() + .flat_map(|after| after.exprs()) + .map(|expr| self.lower_expr(&expr)) + .collect(); + self.alloc_expr( + Expr::Try { + exprs, + of_clauses, + catch_clauses, + after, + }, + Some(expr), + ) + } + ast::ExprMax::Tuple(tup) => { + let exprs = tup.expr().map(|expr| self.lower_expr(&expr)).collect(); + self.alloc_expr(Expr::Tuple { exprs }, Some(expr)) + } + ast::ExprMax::Var(var) => self + .resolve_var(var, |this, expr| this.lower_optional_expr(expr.expr())) + .unwrap_or_else(|var| self.alloc_expr(Expr::Var(var), Some(expr))), + ast::ExprMax::MaybeExpr(maybe) => { + let exprs = maybe + .exprs() + .map(|expr| self.lower_maybe_expr(&expr)) + .collect(); + + let else_clauses = maybe + .clauses() + .flat_map(|clause| self.lower_cr_clause(clause)) + .collect(); + self.alloc_expr( + Expr::Maybe { + exprs, + else_clauses, + }, + Some(expr), + ) + } + ast::ExprMax::MapComprehension(map_comp) => { + let key = self.lower_optional_expr(map_comp.expr().and_then(|mf| mf.key())); + let value = self.lower_optional_expr(map_comp.expr().and_then(|mf| mf.value())); + let exprs = self.lower_lc_exprs(map_comp.lc_exprs()); + let comp_expr = match map_comp.expr().and_then(|mf| mf.op()) { + Some((MapOp::Assoc, _)) => Expr::Comprehension { + builder: ComprehensionBuilder::Map(key, value), + exprs, + }, + _ => Expr::Missing, + }; + + self.alloc_expr(comp_expr, Some(expr)) + } + } + } + + fn lower_maybe_expr(&mut self, expr: &ast::Expr) -> MaybeExpr { + match expr { + ast::Expr::CondMatchExpr(cond) => { + let pat_id = self.lower_optional_pat(cond.lhs()); + let expr_id = self.lower_optional_expr(cond.rhs()); + self.alloc_expr(Expr::Missing, Some(expr)); + MaybeExpr::Cond { + lhs: pat_id, + rhs: expr_id, + } + } + ast::Expr::ExprMax(ast::ExprMax::ParenExpr(paren)) => match paren.expr() { + Some(paren_expr) => self.lower_maybe_expr(&paren_expr), + None => MaybeExpr::Expr(self.alloc_expr(Expr::Missing, None)), + }, + e => MaybeExpr::Expr(self.lower_expr(e)), + } + } + + fn lower_list( + &mut self, + list: &ast::List, + make_missing: impl Fn(&mut Self) -> Id, + lower: impl Fn(&mut Self, &ast::Expr) -> Id, + ) -> (Vec, Option) { + let mut tail = None; + let mut ids = vec![]; + + for expr in list.exprs() { + if let ast::Expr::Pipe(pipe) = &expr { + let id = pipe + .lhs() + .map(|expr| lower(self, &expr)) + .unwrap_or_else(|| make_missing(self)); + ids.push(id); + + if let Some(tail) = tail { + // TODO: add error + ids.push(tail) + } + tail = pipe.rhs().map(|expr| lower(self, &expr)); + } else { + ids.push(lower(self, &expr)); + } + } + + (ids, tail) + } + + fn lower_bin_element( + &mut self, + element: &ast::BinElement, + lower: fn(&mut Self, Option) -> Id, + ) -> Option> { + let elem = lower(self, element.element().map(Into::into)); + let size = element + .size() + .and_then(|size| size.size()) + .map(|expr| self.lower_expr(&expr.into())); + + let mut unit = None; + let tys = element + .types() + .iter() + .flat_map(|types| types.types()) + .flat_map(|ty| match ty { + ast::BitType::Name(name) => self.resolve_name(name), + ast::BitType::BitTypeUnit(ty_unit) => { + unit = ty_unit.size().and_then(|unit| self.resolve_arity(unit)); + None + } + }) + .collect(); + + Some(BinarySeg { + elem, + size, + unit, + tys, + }) + } + + fn lower_cr_clause(&mut self, clause: ast::CrClauseOrMacro) -> impl Iterator { + match clause { + ast::CrClauseOrMacro::CrClause(clause) => { + let pat = self.lower_optional_pat(clause.pat()); + let guards = self.lower_guards(clause.guard()); + let exprs = self.lower_clause_body(clause.body()); + Either::Left(Some(CRClause { pat, guards, exprs }).into_iter()) + } + ast::CrClauseOrMacro::MacroCallExpr(call) => { + Either::Right( + self.resolve_macro(&call, |this, _source, replacement| { + match replacement { + MacroReplacement::Ast( + ast::MacroDefReplacement::ReplacementCrClauses(clauses), + ) => clauses + .clauses() + .flat_map(|clause| this.lower_cr_clause(clause)) + .collect(), + // no built-in macro makes sense in this place + MacroReplacement::Ast(_) | MacroReplacement::BuiltIn(_) => vec![], + // args make no sense here + MacroReplacement::AstArgs(_, _) + | MacroReplacement::BuiltInArgs(_, _) => vec![], + } + }) + .into_iter() + .flatten(), + ) + } + } + } + + fn lower_guards(&mut self, guards: Option) -> Vec> { + guards + .iter() + .flat_map(|guard| guard.clauses()) + .map(|clause| clause.exprs().map(|expr| self.lower_expr(&expr)).collect()) + .collect() + } + + fn lower_clause_body(&mut self, body: Option) -> Vec { + body.iter() + .flat_map(|body| body.exprs()) + .map(|expr| self.lower_expr(&expr)) + .collect() + } + + fn lower_lc_exprs(&mut self, exprs: Option) -> Vec { + exprs + .iter() + .flat_map(|exprs| exprs.exprs()) + .map(|expr| match expr { + ast::LcExpr::Expr(expr) => ComprehensionExpr::Expr(self.lower_expr(&expr)), + ast::LcExpr::BGenerator(bin_gen) => { + let pat = self.lower_optional_pat(bin_gen.lhs()); + let expr = self.lower_optional_expr(bin_gen.rhs()); + ComprehensionExpr::BinGenerator { pat, expr } + } + ast::LcExpr::Generator(list_gen) => { + let pat = self.lower_optional_pat(list_gen.lhs()); + let expr = self.lower_optional_expr(list_gen.rhs()); + ComprehensionExpr::ListGenerator { pat, expr } + } + ast::LcExpr::MapGenerator(map_gen) => { + let key = self.lower_optional_pat(map_gen.lhs().and_then(|mf| mf.key())); + let value = self.lower_optional_pat(map_gen.lhs().and_then(|mf| mf.value())); + let expr = self.lower_optional_expr(map_gen.rhs()); + ComprehensionExpr::MapGenerator { key, value, expr } + } + }) + .collect() + } + + fn lower_optional_type_expr(&mut self, expr: Option) -> TypeExprId { + if let Some(expr) = &expr { + self.lower_type_expr(expr) + } else { + self.alloc_type_expr(TypeExpr::Missing, None) + } + } + + fn lower_type_expr(&mut self, expr: &ast::Expr) -> TypeExprId { + match expr { + ast::Expr::ExprMax(expr_max) => self.lower_type_expr_max(expr_max, expr), + ast::Expr::AnnType(ann) => { + let ty = self.lower_optional_type_expr(ann.ty()); + if let Some(var) = ann.var().and_then(|var| var.var()) { + let var = self.db.var(var.as_name()); + self.alloc_type_expr(TypeExpr::AnnType { var, ty }, Some(expr)) + } else { + self.alloc_type_expr(TypeExpr::Missing, Some(expr)) + } + } + ast::Expr::BinaryOpExpr(binary_op) => { + let lhs = self.lower_optional_type_expr(binary_op.lhs()); + let rhs = self.lower_optional_type_expr(binary_op.rhs()); + if let Some((op, _)) = binary_op.op() { + self.alloc_type_expr(TypeExpr::BinaryOp { lhs, op, rhs }, Some(expr)) + } else { + self.alloc_type_expr(TypeExpr::Missing, Some(expr)) + } + } + ast::Expr::Call(call) => { + let target = self.lower_type_call_target(call.expr()); + let args = call + .args() + .iter() + .flat_map(|args| args.args()) + .map(|expr| self.lower_type_expr(&expr)) + .collect(); + self.alloc_type_expr(TypeExpr::Call { target, args }, Some(expr)) + } + ast::Expr::CatchExpr(catch) => { + let _ = self.lower_optional_type_expr(catch.expr()); + self.alloc_type_expr(TypeExpr::Missing, Some(expr)) + } + ast::Expr::Dotdotdot(_) => self.alloc_type_expr(TypeExpr::Missing, Some(expr)), + ast::Expr::MapExpr(map) => { + let fields = map + .fields() + .flat_map(|field| { + let key = self.lower_optional_type_expr(field.key()); + let value = self.lower_optional_type_expr(field.value()); + Some((key, field.op()?.0, value)) + }) + .collect(); + self.alloc_type_expr(TypeExpr::Map { fields }, Some(expr)) + } + ast::Expr::MapExprUpdate(update) => { + let _ = self.lower_optional_type_expr(update.expr().map(Into::into)); + update.fields().for_each(|field| { + let _ = self.lower_optional_type_expr(field.key()); + let _ = self.lower_optional_type_expr(field.value()); + }); + self.alloc_type_expr(TypeExpr::Missing, Some(expr)) + } + ast::Expr::MatchExpr(mat) => { + let _ = self.lower_optional_type_expr(mat.lhs()); + let _ = self.lower_optional_type_expr(mat.rhs()); + self.alloc_type_expr(TypeExpr::Missing, Some(expr)) + } + ast::Expr::Pipe(pipe) => { + let mut pipe = pipe.clone(); + let mut types = vec![self.lower_optional_type_expr(pipe.lhs())]; + while let Some(ast::Expr::Pipe(next)) = pipe.rhs() { + types.push(self.lower_optional_type_expr(next.lhs())); + pipe = next; + } + types.push(self.lower_optional_type_expr(pipe.rhs())); + self.alloc_type_expr(TypeExpr::Union { types }, Some(expr)) + } + ast::Expr::RangeType(range) => { + let lhs = self.lower_optional_type_expr(range.lhs()); + let rhs = self.lower_optional_type_expr(range.rhs()); + self.alloc_type_expr(TypeExpr::Range { lhs, rhs }, Some(expr)) + } + ast::Expr::RecordExpr(record) => { + let name = record.name().and_then(|n| self.resolve_name(n.name()?)); + let fields = record + .fields() + .flat_map(|field| { + let ty = + self.lower_optional_type_expr(field.ty().and_then(|expr| expr.expr())); + let name = self.resolve_name(field.name()?)?; + Some((name, ty)) + }) + .collect(); + if let Some(name) = name { + self.alloc_type_expr(TypeExpr::Record { name, fields }, Some(expr)) + } else { + self.alloc_type_expr(TypeExpr::Missing, Some(expr)) + } + } + ast::Expr::RecordFieldExpr(field) => { + let _ = self.lower_optional_type_expr(field.expr().map(Into::into)); + self.alloc_type_expr(TypeExpr::Missing, Some(expr)) + } + ast::Expr::RecordIndexExpr(_index) => { + self.alloc_type_expr(TypeExpr::Missing, Some(expr)) + } + ast::Expr::RecordUpdateExpr(update) => { + let _ = self.lower_optional_type_expr(update.expr().map(Into::into)); + update.fields().for_each(|field| { + let _ = field.expr().iter().for_each(|field_expr| { + self.lower_optional_type_expr(field_expr.expr()); + }); + }); + self.alloc_type_expr(TypeExpr::Missing, Some(expr)) + } + ast::Expr::Remote(remote) => { + let _ = self.lower_optional_type_expr( + remote + .module() + .and_then(|module| module.module()) + .map(Into::into), + ); + let _ = self.lower_optional_type_expr(remote.fun().map(Into::into)); + self.alloc_type_expr(TypeExpr::Missing, Some(expr)) + } + ast::Expr::UnaryOpExpr(unary_op) => { + let operand = self.lower_optional_type_expr(unary_op.operand()); + if let Some((op, _)) = unary_op.op() { + self.alloc_type_expr( + TypeExpr::UnaryOp { + type_expr: operand, + op, + }, + Some(expr), + ) + } else { + self.alloc_type_expr(TypeExpr::Missing, Some(expr)) + } + } + ast::Expr::CondMatchExpr(cond) => { + let _ = self.lower_optional_type_expr(cond.lhs()); + let _ = self.lower_optional_type_expr(cond.rhs()); + self.alloc_type_expr(TypeExpr::Missing, Some(expr)) + } + } + } + + fn lower_type_call_target(&mut self, expr: Option) -> CallTarget { + match expr.as_ref() { + Some(ast::Expr::ExprMax(ast::ExprMax::ParenExpr(paren))) => { + self.lower_type_call_target(paren.expr()) + } + Some(ast::Expr::Remote(remote)) => CallTarget::Remote { + module: self.lower_optional_type_expr( + remote + .module() + .and_then(|module| module.module()) + .map(ast::Expr::ExprMax), + ), + name: self.lower_optional_type_expr(remote.fun().map(ast::Expr::ExprMax)), + }, + Some(ast::Expr::ExprMax(ast::ExprMax::MacroCallExpr(call))) => self + .resolve_macro(call, |this, source, replacement| match replacement { + MacroReplacement::BuiltIn(built_in) => { + this.lower_built_in_macro(built_in).map(|literal| { + let name = this.alloc_type_expr(TypeExpr::Literal(literal), None); + this.record_type_source(name, source); + CallTarget::Local { name } + }) + } + MacroReplacement::Ast(ast::MacroDefReplacement::Expr(expr)) => { + Some(this.lower_type_call_target(Some(expr))) + } + MacroReplacement::Ast(_) => None, + // This would mean double parens in the call - invalid + MacroReplacement::BuiltInArgs(_, _) | MacroReplacement::AstArgs(_, _) => None, + }) + .flatten() + .unwrap_or_else(|| { + let _ = call + .args() + .iter() + .flat_map(|args| args.args()) + .for_each(|expr| { + let _ = self.lower_optional_type_expr(expr.expr()); + let _ = self.lower_optional_type_expr(expr.guard()); + }); + CallTarget::Local { + name: self.alloc_type_expr(TypeExpr::Missing, expr.as_ref()), + } + }), + Some(expr) => CallTarget::Local { + name: self.lower_type_expr(expr), + }, + None => CallTarget::Local { + name: self.alloc_type_expr(TypeExpr::Missing, None), + }, + } + } + + fn lower_type_expr_max(&mut self, expr_max: &ast::ExprMax, expr: &ast::Expr) -> TypeExprId { + match expr_max { + ast::ExprMax::AnonymousFun(_fun) => self.alloc_type_expr(TypeExpr::Missing, Some(expr)), + ast::ExprMax::Atom(atom) => { + let atom = self.db.atom(atom.as_name()); + self.alloc_type_expr(TypeExpr::Literal(Literal::Atom(atom)), Some(expr)) + } + ast::ExprMax::Binary(_bin) => self.alloc_type_expr(TypeExpr::Missing, Some(expr)), + ast::ExprMax::BinaryComprehension(_bc) => { + self.alloc_type_expr(TypeExpr::Missing, Some(expr)) + } + ast::ExprMax::BlockExpr(_block) => self.alloc_type_expr(TypeExpr::Missing, Some(expr)), + ast::ExprMax::CaseExpr(_case) => self.alloc_type_expr(TypeExpr::Missing, Some(expr)), + ast::ExprMax::Char(char) => { + let value = lower_char(char).map_or(TypeExpr::Missing, TypeExpr::Literal); + self.alloc_type_expr(value, Some(expr)) + } + ast::ExprMax::Concatables(_concat) => { + self.alloc_type_expr(TypeExpr::Missing, Some(expr)) + } + ast::ExprMax::ExternalFun(_fun) => self.alloc_type_expr(TypeExpr::Missing, Some(expr)), + ast::ExprMax::Float(float) => { + let value = lower_float(float).map_or(TypeExpr::Missing, TypeExpr::Literal); + self.alloc_type_expr(value, Some(expr)) + } + ast::ExprMax::FunType(fun) => match fun.sig() { + None => self.alloc_type_expr(TypeExpr::Fun(FunType::Any), Some(expr)), + Some(sig) => { + let result = self.lower_optional_type_expr(sig.ty()); + let mut params = Vec::new(); + let has_dot_dot_dot = + sig.args().iter().flat_map(|args| args.args()).any(|param| { + params.push(self.lower_type_expr(¶m)); + matches!(param, ast::Expr::Dotdotdot(_)) + }); + let fun = if has_dot_dot_dot { + FunType::AnyArgs { result } + } else { + FunType::Full { params, result } + }; + self.alloc_type_expr(TypeExpr::Fun(fun), Some(expr)) + } + }, + ast::ExprMax::IfExpr(_if_expr) => self.alloc_type_expr(TypeExpr::Missing, Some(expr)), + ast::ExprMax::Integer(int) => { + let value = lower_int(int).map_or(TypeExpr::Missing, TypeExpr::Literal); + self.alloc_type_expr(value, Some(expr)) + } + ast::ExprMax::InternalFun(_fun) => self.alloc_type_expr(TypeExpr::Missing, Some(expr)), + ast::ExprMax::List(list) => { + let ty = list.exprs().fold(ListType::Empty, |ty, expr| { + let elem = self.lower_type_expr(&expr); + match ty { + ListType::Empty => ListType::Regular(elem), + ListType::Regular(elem) if matches!(expr, ast::Expr::Dotdotdot(_)) => { + ListType::NonEmpty(elem) + } + other => other, + } + }); + self.alloc_type_expr(TypeExpr::List(ty), Some(expr)) + } + ast::ExprMax::ListComprehension(_lc) => { + self.alloc_type_expr(TypeExpr::Missing, Some(expr)) + } + ast::ExprMax::MacroCallExpr(call) => self + .resolve_macro(call, |this, source, replacement| match replacement { + MacroReplacement::BuiltIn(built_in) => { + this.lower_built_in_macro(built_in).map(|literal| { + let type_id = this.alloc_type_expr(TypeExpr::Literal(literal), None); + this.record_type_source(type_id, source); + type_id + }) + } + MacroReplacement::Ast(ast::MacroDefReplacement::Expr(macro_expr)) => { + let type_id = this.lower_type_expr(¯o_expr); + this.record_type_source(type_id, source); + Some(type_id) + } + MacroReplacement::Ast(_) => None, + MacroReplacement::BuiltInArgs(built_in, args) => { + let name = this + .lower_built_in_macro(built_in) + .map(|literal| this.alloc_type_expr(TypeExpr::Literal(literal), None)) + .unwrap_or_else(|| this.alloc_type_expr(TypeExpr::Missing, None)); + let target = CallTarget::Local { name }; + let args = args + .args() + .map(|expr| this.lower_optional_type_expr(expr.expr())) + .collect(); + let type_id = this.alloc_type_expr(TypeExpr::Call { target, args }, None); + this.record_type_source(type_id, source); + Some(type_id) + } + MacroReplacement::AstArgs( + ast::MacroDefReplacement::Expr(replacement), + args, + ) => { + let target = this.lower_type_call_target(Some(replacement)); + let args = args + .args() + .map(|expr| this.lower_optional_type_expr(expr.expr())) + .collect(); + let type_id = this.alloc_type_expr(TypeExpr::Call { target, args }, None); + this.record_type_source(type_id, source); + Some(type_id) + } + MacroReplacement::AstArgs(_, _) => None, + }) + .flatten() + .map(|expansion| { + let args = call + .args() + .iter() + .flat_map(|args| args.args()) + .map(|expr| self.lower_optional_expr(expr.expr())) + .collect(); + let expr_id = + self.alloc_type_expr(TypeExpr::MacroCall { expansion, args }, Some(expr)); + expr_id + }) + .unwrap_or_else(|| { + let _ = call + .args() + .iter() + .flat_map(|args| args.args()) + .for_each(|expr| { + let _ = self.lower_optional_type_expr(expr.expr()); + let _ = self.lower_optional_type_expr(expr.guard()); + }); + self.alloc_type_expr(TypeExpr::Missing, Some(expr)) + }), + ast::ExprMax::MacroString(_) => self.alloc_type_expr(TypeExpr::Missing, Some(expr)), + ast::ExprMax::ParenExpr(paren_expr) => { + if let Some(expr) = paren_expr.expr() { + let type_expr_id = self.lower_type_expr(&expr); + let ptr = AstPtr::new(&expr); + let source = InFileAstPtr::new(self.curr_file_id(), ptr); + self.record_type_source(type_expr_id, source); + type_expr_id + } else { + self.alloc_type_expr(TypeExpr::Missing, Some(expr)) + } + } + ast::ExprMax::ReceiveExpr(_receive) => { + self.alloc_type_expr(TypeExpr::Missing, Some(expr)) + } + ast::ExprMax::String(str) => { + let value = lower_str(str).map_or(TypeExpr::Missing, TypeExpr::Literal); + self.alloc_type_expr(value, Some(expr)) + } + ast::ExprMax::TryExpr(_try_expr) => self.alloc_type_expr(TypeExpr::Missing, Some(expr)), + ast::ExprMax::Tuple(tup) => { + let args = tup.expr().map(|expr| self.lower_type_expr(&expr)).collect(); + self.alloc_type_expr(TypeExpr::Tuple { args }, Some(expr)) + } + ast::ExprMax::Var(var) => self + .resolve_var(var, |this, expr| this.lower_optional_type_expr(expr.expr())) + .unwrap_or_else(|var| self.alloc_type_expr(TypeExpr::Var(var), Some(expr))), + ast::ExprMax::MaybeExpr(maybe) => { + maybe.exprs().for_each(|expr| { + self.lower_expr(&expr); + }); + + maybe + .clauses() + .flat_map(|clause| self.lower_cr_clause(clause)) + .last(); + self.alloc_type_expr(TypeExpr::Missing, Some(expr)) + } + ExprMax::MapComprehension(_mc) => self.alloc_type_expr(TypeExpr::Missing, Some(expr)), + } + } + + fn lower_optional_term(&mut self, expr: Option) -> TermId { + if let Some(expr) = &expr { + self.lower_term(expr) + } else { + self.alloc_term(Term::Missing, None) + } + } + + fn lower_term(&mut self, expr: &ast::Expr) -> TermId { + match expr { + ast::Expr::ExprMax(expr_max) => self.lower_term_max(expr_max, expr), + ast::Expr::AnnType(ann) => { + let _ = self.lower_optional_term(ann.ty()); + self.alloc_term(Term::Missing, Some(expr)) + } + ast::Expr::BinaryOpExpr(binary_op) => { + // Interpreting foo/1 as {foo, 1} + let lhs = self.lower_optional_term(binary_op.lhs()); + let rhs = self.lower_optional_term(binary_op.rhs()); + if matches!( + binary_op.op(), + Some((ast::BinaryOp::ArithOp(ast::ArithOp::FloatDiv), _)) + ) && matches!(self.body[lhs], Term::Literal(Literal::Atom(_))) + && matches!(self.body[rhs], Term::Literal(Literal::Integer(_))) + { + let exprs = vec![lhs, rhs]; + self.alloc_term(Term::Tuple { exprs }, Some(expr)) + } else { + self.alloc_term(Term::Missing, Some(expr)) + } + } + ast::Expr::Call(call) => { + let _ = self.lower_optional_term(call.expr()); + let _ = call + .args() + .iter() + .flat_map(|args| args.args()) + .for_each(|expr| { + let _ = self.lower_term(&expr); + }); + self.alloc_term(Term::Missing, Some(expr)) + } + ast::Expr::CatchExpr(catch) => { + let _ = self.lower_optional_term(catch.expr()); + self.alloc_term(Term::Missing, Some(expr)) + } + ast::Expr::Dotdotdot(_) => self.alloc_term(Term::Missing, Some(expr)), + ast::Expr::MapExpr(map) => { + let fields = map + .fields() + .flat_map(|field| { + let key = self.lower_optional_term(field.key()); + let value = self.lower_optional_term(field.value()); + if let Some((ast::MapOp::Assoc, _)) = field.op() { + Some((key, value)) + } else { + None + } + }) + .collect(); + self.alloc_term(Term::Map { fields }, Some(expr)) + } + ast::Expr::MapExprUpdate(update) => { + let _ = self.lower_optional_term(update.expr().map(Into::into)); + update.fields().for_each(|field| { + let _ = self.lower_optional_term(field.key()); + let _ = self.lower_optional_term(field.value()); + }); + self.alloc_term(Term::Missing, Some(expr)) + } + ast::Expr::MatchExpr(mat) => { + let _ = self.lower_optional_term(mat.lhs()); + let _ = self.lower_optional_term(mat.rhs()); + self.alloc_term(Term::Missing, Some(expr)) + } + ast::Expr::Pipe(pipe) => { + let _ = self.lower_optional_term(pipe.lhs()); + let _ = self.lower_optional_term(pipe.rhs()); + self.alloc_term(Term::Missing, Some(expr)) + } + ast::Expr::RangeType(range) => { + let _ = self.lower_optional_term(range.lhs()); + let _ = self.lower_optional_term(range.rhs()); + self.alloc_term(Term::Missing, Some(expr)) + } + ast::Expr::RecordExpr(record) => { + record.fields().for_each(|field| { + let _ = self.lower_optional_term(field.ty().and_then(|expr| expr.expr())); + }); + self.alloc_term(Term::Missing, Some(expr)) + } + ast::Expr::RecordFieldExpr(field) => { + let _ = self.lower_optional_term(field.expr().map(Into::into)); + self.alloc_term(Term::Missing, Some(expr)) + } + ast::Expr::RecordIndexExpr(_index) => self.alloc_term(Term::Missing, Some(expr)), + ast::Expr::RecordUpdateExpr(update) => { + let _ = self.lower_optional_term(update.expr().map(Into::into)); + update.fields().for_each(|field| { + let _ = self.lower_optional_term(field.expr().and_then(|expr| expr.expr())); + let _ = self.lower_optional_term(field.ty().and_then(|expr| expr.expr())); + }); + self.alloc_term(Term::Missing, Some(expr)) + } + ast::Expr::Remote(remote) => { + let _ = self.lower_optional_term( + remote + .module() + .and_then(|module| module.module()) + .map(Into::into), + ); + let _ = self.lower_optional_term(remote.fun().map(Into::into)); + self.alloc_term(Term::Missing, Some(expr)) + } + ast::Expr::UnaryOpExpr(unary_op) => { + let term = self.lower_optional_term(unary_op.operand()); + match unary_op.op() { + Some((ast::UnaryOp::Plus, _)) => { + self.alloc_term(self.body[term].clone(), Some(expr)) + } + Some((ast::UnaryOp::Minus, _)) => { + if let Term::Literal(literal) = &self.body[term] { + let value = literal.negate().map_or(Term::Missing, Term::Literal); + self.alloc_term(value, Some(expr)) + } else { + self.alloc_term(Term::Missing, Some(expr)) + } + } + _ => self.alloc_term(Term::Missing, Some(expr)), + } + } + ast::Expr::CondMatchExpr(cond) => { + let _ = self.lower_optional_term(cond.lhs()); + let _ = self.lower_optional_term(cond.rhs()); + self.alloc_term(Term::Missing, Some(expr)) + } + } + } + + fn lower_term_max(&mut self, expr_max: &ast::ExprMax, expr: &ast::Expr) -> TermId { + match expr_max { + ast::ExprMax::AnonymousFun(_fun) => self.alloc_term(Term::Missing, Some(expr)), + ast::ExprMax::Atom(atom) => { + let atom = self.db.atom(atom.as_name()); + self.alloc_term(Term::Literal(Literal::Atom(atom)), Some(expr)) + } + ast::ExprMax::Binary(bin) => { + let value = bin + .elements() + .fold(Term::Binary(Vec::new()), |acc, element| { + if let Some(seg) = + self.lower_bin_element(&element, Self::lower_optional_term) + { + match acc { + Term::Binary(mut vec) => { + // TODO: process size & unit & types + if seg.size.is_none() + && seg.unit.is_none() + && seg.tys.is_empty() + { + match &self.body[seg.elem] { + Term::Literal(Literal::Char(ch)) => { + vec.push(*ch as u8); + Term::Binary(vec) + } + Term::Literal(Literal::Integer(int)) => { + vec.push(*int as u8); + Term::Binary(vec) + } + Term::Literal(Literal::String(str)) => { + vec.extend(str.chars().map(|ch| ch as u8)); + Term::Binary(vec) + } + _ => Term::Missing, + } + } else { + Term::Missing + } + } + _ => Term::Missing, + } + } else { + acc + } + }); + + self.alloc_term(value, Some(expr)) + } + ast::ExprMax::BinaryComprehension(_bc) => self.alloc_term(Term::Missing, Some(expr)), + ast::ExprMax::BlockExpr(_block) => self.alloc_term(Term::Missing, Some(expr)), + ast::ExprMax::CaseExpr(_case) => self.alloc_term(Term::Missing, Some(expr)), + ast::ExprMax::Char(char) => { + let value = lower_char(char).map_or(Term::Missing, Term::Literal); + self.alloc_term(value, Some(expr)) + } + ast::ExprMax::Concatables(concat) => { + let value = lower_concat(concat).map_or(Term::Missing, Term::Literal); + self.alloc_term(value, Some(expr)) + } + ast::ExprMax::ExternalFun(fun) => { + let module = self.lower_optional_term( + fun.module() + .and_then(|module| module.name()) + .map(Into::into), + ); + let name = self.lower_optional_term(fun.fun().map(Into::into)); + let arity = self.lower_optional_term( + fun.arity().and_then(|arity| arity.value()).map(Into::into), + ); + if let ( + Term::Literal(Literal::Atom(module)), + Term::Literal(Literal::Atom(name)), + Term::Literal(Literal::Integer(arity)), + ) = (&self.body[module], &self.body[name], &self.body[arity]) + { + if let Ok(arity) = (*arity).try_into() { + let term = Term::CaptureFun { + module: *module, + name: *name, + arity, + }; + self.alloc_term(term, Some(expr)) + } else { + self.alloc_term(Term::Missing, Some(expr)) + } + } else { + self.alloc_term(Term::Missing, Some(expr)) + } + } + ast::ExprMax::Float(float) => { + let value = lower_float(float).map_or(Term::Missing, Term::Literal); + self.alloc_term(value, Some(expr)) + } + ast::ExprMax::FunType(fun) => { + if let Some(sig) = fun.sig() { + let _ = self.lower_optional_term(sig.ty()); + let _ = sig + .args() + .iter() + .flat_map(|args| args.args()) + .for_each(|pat| { + let _ = self.lower_term(&pat); + }); + } + self.alloc_term(Term::Missing, Some(expr)) + } + ast::ExprMax::IfExpr(_if_expr) => self.alloc_term(Term::Missing, Some(expr)), + ast::ExprMax::Integer(int) => { + let value = lower_int(int).map_or(Term::Missing, Term::Literal); + self.alloc_term(value, Some(expr)) + } + ast::ExprMax::InternalFun(_fun) => self.alloc_term(Term::Missing, Some(expr)), + ast::ExprMax::List(list) => { + let (exprs, tail) = self.lower_list( + list, + |this| this.alloc_term(Term::Missing, None), + |this, expr| this.lower_term(expr), + ); + self.alloc_term(Term::List { exprs, tail }, Some(expr)) + } + ast::ExprMax::ListComprehension(_lc) => self.alloc_term(Term::Missing, Some(expr)), + ast::ExprMax::MacroCallExpr(call) => self + .resolve_macro(call, |this, source, replacement| match replacement { + MacroReplacement::BuiltIn(built_in) => { + this.lower_built_in_macro(built_in).map(|literal| { + let term_id = this.alloc_term(Term::Literal(literal), None); + this.record_term_source(term_id, source); + term_id + }) + } + MacroReplacement::Ast(ast::MacroDefReplacement::Expr(macro_expr)) => { + let term_id = this.lower_term(¯o_expr); + this.record_term_source(term_id, source); + Some(term_id) + } + _ => None, + }) + .flatten() + .map(|expansion| { + let args = call + .args() + .iter() + .flat_map(|args| args.args()) + .map(|expr| self.lower_optional_expr(expr.expr())) + .collect(); + let expr_id = self.alloc_term(Term::MacroCall { expansion, args }, Some(expr)); + expr_id + }) + .unwrap_or_else(|| { + let _ = call + .args() + .iter() + .flat_map(|args| args.args()) + .for_each(|expr| { + let _ = self.lower_optional_term(expr.expr()); + let _ = self.lower_optional_term(expr.guard()); + }); + self.alloc_term(Term::Missing, Some(expr)) + }), + ast::ExprMax::MacroString(_) => self.alloc_term(Term::Missing, Some(expr)), + ast::ExprMax::ParenExpr(paren_expr) => { + if let Some(expr) = paren_expr.expr() { + self.lower_term(&expr) + } else { + self.alloc_term(Term::Missing, Some(expr)) + } + } + ast::ExprMax::ReceiveExpr(_receive) => self.alloc_term(Term::Missing, Some(expr)), + ast::ExprMax::String(str) => { + let value = lower_str(str).map_or(Term::Missing, Term::Literal); + self.alloc_term(value, Some(expr)) + } + ast::ExprMax::TryExpr(_try_expr) => self.alloc_term(Term::Missing, Some(expr)), + ast::ExprMax::Tuple(tup) => { + let exprs = tup.expr().map(|expr| self.lower_term(&expr)).collect(); + self.alloc_term(Term::Tuple { exprs }, Some(expr)) + } + ast::ExprMax::Var(var) => self + .resolve_var(var, |this, expr| this.lower_optional_term(expr.expr())) + .unwrap_or_else(|_var| self.alloc_term(Term::Missing, Some(expr))), + ast::ExprMax::MaybeExpr(maybe_expr) => { + maybe_expr.exprs().for_each(|expr| { + self.lower_expr(&expr); + }); + + maybe_expr + .clauses() + .flat_map(|clause| self.lower_cr_clause(clause)) + .last(); + self.alloc_term(Term::Missing, Some(expr)) + } + ExprMax::MapComprehension(_mc) => self.alloc_term(Term::Missing, Some(expr)), + } + } + + fn lower_built_in_macro(&mut self, built_in: BuiltInMacro) -> Option { + match built_in { + // This is a bit of a hack, but allows us not to depend on the file system + // It somewhat replicates the behaviour of -deterministic option + BuiltInMacro::FILE => { + let form_list = self.db.file_form_list(self.original_file_id); + form_list + .module_attribute() + .map(|attr| Literal::String(format!("{}.erl", attr.name))) + } + BuiltInMacro::FUNCTION_NAME => self.function_info.map(|(name, _)| Literal::Atom(name)), + BuiltInMacro::FUNCTION_ARITY => self + .function_info + .map(|(_, arity)| Literal::Integer(arity as i128)), + // Dummy value, we don't want to depend on the exact position + BuiltInMacro::LINE => Some(Literal::Integer(0)), + BuiltInMacro::MODULE => { + let form_list = self.db.file_form_list(self.original_file_id); + form_list + .module_attribute() + .map(|attr| Literal::Atom(self.db.atom(attr.name.clone()))) + } + BuiltInMacro::MODULE_STRING => { + let form_list = self.db.file_form_list(self.original_file_id); + form_list + .module_attribute() + .map(|attr| Literal::String(attr.name.to_string())) + } + BuiltInMacro::MACHINE => Some(Literal::Atom(self.db.atom(known::ELP))), + // Dummy value, must be an integer + BuiltInMacro::OTP_RELEASE => Some(Literal::Integer(2000)), + } + } + + fn resolve_name(&mut self, name: ast::Name) -> Option { + let expr_id = self.lower_expr(&name.into()); + if let Expr::Literal(Literal::Atom(atom)) = self.body[expr_id] { + Some(atom) + } else { + None + } + } + + fn resolve_arity(&mut self, arity: ast::ArityValue) -> Option { + let expr_id = self.lower_expr(&arity.into()); + if let Expr::Literal(Literal::Integer(int)) = self.body[expr_id] { + Some(int) + } else { + None + } + } + + fn resolve_macro( + &mut self, + call: &ast::MacroCallExpr, + cb: impl FnOnce(&mut Self, ExprSource, MacroReplacement) -> R, + ) -> Option { + let name = macro_exp::macro_name(call)?; + if self.macro_stack().any(|entry| entry.name == name) { + return None; + } + + let source = InFileAstPtr::new(self.curr_file_id(), AstPtr::new(call).cast().unwrap()); + + match self.db.resolve_macro(self.original_file_id, name.clone()) { + Some(res @ ResolvedMacro::BuiltIn(built_in)) => { + self.record_macro_resolution(call, res); + Some(cb(self, source, MacroReplacement::BuiltIn(built_in))) + } + Some(res @ ResolvedMacro::User(def_idx)) => { + self.record_macro_resolution(call, res); + self.enter_macro(name, def_idx, call.args(), |this, replacement| { + cb(this, source, MacroReplacement::Ast(replacement)) + }) + } + None => { + let name = name.with_arity(None); + let args = call.args()?; + let res = self.db.resolve_macro(self.original_file_id, name.clone())?; + self.record_macro_resolution(call, res); + match res { + ResolvedMacro::BuiltIn(built_in) => Some(cb( + self, + source, + MacroReplacement::BuiltInArgs(built_in, args), + )), + ResolvedMacro::User(def_idx) => { + self.enter_macro(name, def_idx, None, |this, replacement| { + cb(this, source, MacroReplacement::AstArgs(replacement, args)) + }) + } + } + } + } + } + + fn enter_macro( + &mut self, + name: MacroName, + def_idx: InFile, + args: Option, + cb: impl FnOnce(&mut Self, ast::MacroDefReplacement) -> R, + ) -> Option { + let form_list = self.db.file_form_list(def_idx.file_id); + let define_form_id = form_list[def_idx.value].form_id; + let source = self.db.parse(def_idx.file_id); + let define = define_form_id.get(&source.tree()); + let replacement = define.replacement()?; + + let var_map = if let Some(args) = args { + define + .args() + .zip(args.args()) + .map(|(var, arg)| (self.db.var(var.as_name()), arg)) + .collect() + } else { + FxHashMap::default() + }; + let new_stack_id = self.macro_stack.len(); + self.macro_stack.push(MacroStackEntry { + name, + file_id: def_idx.file_id, + var_map, + parent_id: self.macro_stack_id, + }); + self.macro_stack_id = new_stack_id; + + let ret = cb(self, replacement); + + let entry = self.macro_stack.pop().expect("BUG: missing stack entry"); + self.macro_stack_id = entry.parent_id; + + Some(ret) + } + + fn macro_stack(&self) -> impl Iterator { + iter::successors(Some(&self.macro_stack[self.macro_stack_id]), |entry| { + if entry.parent_id != 0 { + Some(&self.macro_stack[entry.parent_id]) + } else { + None + } + }) + } + + fn resolve_var( + &mut self, + var: &ast::Var, + cb: impl FnOnce(&mut Self, ast::MacroExpr) -> R, + ) -> Result { + let var = self.db.var(var.as_name()); + let entry = &self.macro_stack[self.macro_stack_id]; + if let Some(expr) = entry.var_map.get(&var).cloned() { + let curr_stack_id = self.macro_stack_id; + self.macro_stack_id = entry.parent_id; + + let ret = cb(self, expr); + + self.macro_stack_id = curr_stack_id; + + Ok(ret) + } else { + Err(var) + } + } + + fn alloc_expr(&mut self, expr: Expr, source: Option<&ast::Expr>) -> ExprId { + let expr_id = self.body.exprs.alloc(expr); + if let Some(source) = source { + let ptr = AstPtr::new(source); + let source = InFileAstPtr::new(self.curr_file_id(), ptr); + self.record_expr_source(expr_id, source); + } + expr_id + } + + fn record_expr_source(&mut self, expr_id: ExprId, source: ExprSource) { + self.source_map.expr_map.insert(source, expr_id); + self.source_map.expr_map_back.insert(expr_id, source); + } + + fn alloc_pat(&mut self, expr: Pat, source: Option<&ast::Expr>) -> PatId { + let pat_id = self.body.pats.alloc(expr); + if let Some(source) = source { + let ptr = AstPtr::new(source); + let source = InFileAstPtr::new(self.curr_file_id(), ptr); + self.record_pat_source(pat_id, source); + } + pat_id + } + + fn record_pat_source(&mut self, pat_id: PatId, source: ExprSource) { + self.source_map.pat_map.insert(source, pat_id); + self.source_map.pat_map_back.insert(pat_id, source); + } + + fn alloc_type_expr(&mut self, type_expr: TypeExpr, source: Option<&ast::Expr>) -> TypeExprId { + let type_expr_id = self.body.type_exprs.alloc(type_expr); + if let Some(source) = source { + let ptr = AstPtr::new(source); + let source = InFileAstPtr::new(self.curr_file_id(), ptr); + self.record_type_source(type_expr_id, source); + } + type_expr_id + } + + fn record_type_source(&mut self, type_id: TypeExprId, source: ExprSource) { + self.source_map.type_expr_map.insert(source, type_id); + self.source_map.type_expr_map_back.insert(type_id, source); + } + + fn alloc_term(&mut self, term: Term, source: Option<&ast::Expr>) -> TermId { + let term_id = self.body.terms.alloc(term); + if let Some(source) = source { + let ptr = AstPtr::new(source); + let source = InFileAstPtr::new(self.curr_file_id(), ptr); + self.record_term_source(term_id, source); + } + term_id + } + + fn record_term_source(&mut self, term_id: TermId, source: ExprSource) { + self.source_map.term_map.insert(source, term_id); + self.source_map.term_map_back.insert(term_id, source); + } + + fn record_macro_resolution(&mut self, call: &ast::MacroCallExpr, res: ResolvedMacro) { + let ptr = AstPtr::new(call); + let source = InFileAstPtr::new(self.curr_file_id(), ptr); + self.source_map.macro_map.insert(source, res); + } + + fn curr_file_id(&self) -> FileId { + self.macro_stack[self.macro_stack_id].file_id + } +} + +fn lower_char(char: &ast::Char) -> Option { + unescape::unescape_string(&char.text()) + .and_then(|str| str.chars().next()) + .map(Literal::Char) +} + +fn lower_float(float: &ast::Float) -> Option { + let float: f64 = float.text().parse().ok()?; + Some(Literal::Float(float.to_bits())) +} + +fn lower_raw_int(int: &ast::Integer) -> Option { + let text = int.text(); + if text.contains('_') { + let str = text.replace('_', ""); + str.parse().ok() + } else { + text.parse().ok() + } +} + +fn lower_int(int: &ast::Integer) -> Option { + lower_raw_int(int).map(Literal::Integer) +} + +fn lower_str(str: &ast::String) -> Option { + Some(Literal::String( + unescape::unescape_string(&str.text())?.to_string(), + )) +} + +fn lower_concat(concat: &ast::Concatables) -> Option { + let mut buf = String::new(); + + for concatable in concat.elems() { + // TODO: macro resolution + match concatable { + ast::Concatable::MacroCallExpr(_) => return None, + ast::Concatable::MacroString(_) => return None, + ast::Concatable::String(str) => buf.push_str(&unescape::unescape_string(&str.text())?), + ast::Concatable::Var(_) => return None, + } + } + + Some(Literal::String(buf)) +} diff --git a/crates/hir/src/body/pretty.rs b/crates/hir/src/body/pretty.rs new file mode 100644 index 0000000000..df9aa8ef6c --- /dev/null +++ b/crates/hir/src/body/pretty.rs @@ -0,0 +1,841 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::fmt; +use std::fmt::Write as _; +use std::str; + +use super::SpecOrCallback; +use crate::db::MinInternDatabase; +use crate::expr::MaybeExpr; +use crate::AnyAttribute; +use crate::AttributeBody; +use crate::BinarySeg; +use crate::Body; +use crate::CallTarget; +use crate::Clause; +use crate::ComprehensionBuilder; +use crate::ComprehensionExpr; +use crate::Expr; +use crate::ExprId; +use crate::FormList; +use crate::FunType; +use crate::Function; +use crate::FunctionBody; +use crate::ListType; +use crate::Literal; +use crate::Pat; +use crate::PatId; +use crate::Record; +use crate::RecordBody; +use crate::RecordFieldBody; +use crate::SpecBody; +use crate::SpecSig; +use crate::Term; +use crate::TermId; +use crate::TypeAlias; +use crate::TypeBody; +use crate::TypeExpr; +use crate::TypeExprId; +use crate::Var; + +pub fn print_function(db: &dyn MinInternDatabase, body: &FunctionBody, form: &Function) -> String { + let mut printer = Printer::new(db, &body.body); + + let mut sep = ""; + for (_idx, clause) in body.clauses.iter() { + write!(printer, "{}", sep).unwrap(); + sep = ";\n"; + printer.print_clause(clause, form.name.name()).unwrap(); + } + write!(printer, ".").unwrap(); + + printer.to_string() +} + +pub fn print_type_alias(db: &dyn MinInternDatabase, body: &TypeBody, form: &TypeAlias) -> String { + let mut printer = Printer::new(db, &body.body); + + match form { + TypeAlias::Regular { .. } => write!(printer, "-type ").unwrap(), + TypeAlias::Opaque { .. } => write!(printer, "-opaque ").unwrap(), + } + + printer + .print_args(form.name().name(), &body.vars, |this, &var| { + write!(this, "{}", db.lookup_var(var)) + }) + .unwrap(); + write!(printer, " :: ").unwrap(); + printer.print_type(&printer.body[body.ty]).unwrap(); + write!(printer, ".").unwrap(); + + printer.to_string() +} + +pub fn print_spec(db: &dyn MinInternDatabase, body: &SpecBody, form: SpecOrCallback) -> String { + let mut printer = Printer::new(db, &body.body); + + match form { + SpecOrCallback::Spec(spec) => writeln!(printer, "-spec {}", spec.name.name()).unwrap(), + SpecOrCallback::Callback(cb) => writeln!(printer, "-callback {}", cb.name.name()).unwrap(), + } + printer.indent_level += 1; + + let mut sep = ""; + for sig in &body.sigs { + write!(printer, "{}", sep).unwrap(); + sep = ";\n"; + printer.print_sig(sig).unwrap(); + } + + write!(printer, ".").unwrap(); + + printer.to_string() +} + +pub fn print_record( + db: &dyn MinInternDatabase, + body: &RecordBody, + form: &Record, + form_list: &FormList, +) -> String { + let mut printer = Printer::new(db, &body.body); + + write!(printer, "-record({}, {{", form.name).unwrap(); + printer.indent_level += 1; + + let mut sep = "\n"; + for field in &body.fields { + write!(printer, "{}", sep).unwrap(); + sep = ",\n"; + printer.print_field(field, form_list).unwrap(); + } + + printer.indent_level -= 1; + write!(printer, "\n}}).").unwrap(); + + printer.to_string() +} + +pub fn print_attribute( + db: &dyn MinInternDatabase, + body: &AttributeBody, + form: &AnyAttribute, +) -> String { + let mut printer = Printer::new(db, &body.body); + + match form { + AnyAttribute::CompileOption(_) => write!(printer, "-compile(").unwrap(), + AnyAttribute::Attribute(attr) => write!(printer, "-{}(", attr.name).unwrap(), + } + printer.print_term(&printer.body[body.value]).unwrap(); + write!(printer, ").").unwrap(); + + printer.to_string() +} + +pub fn print_expr(db: &dyn MinInternDatabase, body: &Body, expr: ExprId) -> String { + let mut printer = Printer::new(db, body); + printer.print_expr(&body[expr]).unwrap(); + printer.to_string() +} + +pub fn print_pat(db: &dyn MinInternDatabase, body: &Body, pat: PatId) -> String { + let mut printer = Printer::new(db, body); + printer.print_pat(&body[pat]).unwrap(); + printer.to_string() +} + +pub fn print_type(db: &dyn MinInternDatabase, body: &Body, ty: TypeExprId) -> String { + let mut printer = Printer::new(db, body); + printer.print_type(&body[ty]).unwrap(); + printer.to_string() +} + +pub fn print_term(db: &dyn MinInternDatabase, body: &Body, term: TermId) -> String { + let mut printer = Printer::new(db, body); + printer.print_term(&body[term]).unwrap(); + printer.to_string() +} + +struct Printer<'a> { + db: &'a dyn MinInternDatabase, + body: &'a Body, + buf: String, + indent_level: usize, + needs_indent: bool, +} + +impl<'a> Printer<'a> { + fn new(db: &'a dyn MinInternDatabase, body: &'a Body) -> Self { + Printer { + db, + body, + buf: String::new(), + indent_level: 0, + needs_indent: true, + } + } + + fn to_string(mut self) -> String { + self.buf.truncate(self.buf.trim_end().len()); + self.buf.push('\n'); + self.buf + } + + fn print_clause(&mut self, clause: &Clause, name: &str) -> fmt::Result { + write!(self, "{}(", name)?; + let mut sep = ""; + for pat in clause.pats.iter().map(|pat_id| &self.body[*pat_id]) { + write!(self, "{}", sep)?; + sep = ", "; + self.print_pat(pat)?; + } + write!(self, ")")?; + + self.print_guards(&clause.guards, true)?; + self.print_clause_body(&clause.exprs) + } + + fn print_sig(&mut self, sig: &SpecSig) -> fmt::Result { + self.print_args("", &sig.args, |this, ty| this.print_type(&self.body[*ty]))?; + write!(self, " -> ")?; + self.print_type(&self.body[sig.result])?; + self.print_type_guards(&sig.guards) + } + + fn print_field(&mut self, body: &RecordFieldBody, form_list: &FormList) -> fmt::Result { + write!(self, "{}", form_list[body.field_id].name)?; + if let Some(expr) = body.expr { + write!(self, " = ")?; + self.print_expr(&self.body[expr])?; + } + if let Some(ty) = body.ty { + write!(self, " :: ")?; + self.print_type(&self.body[ty])?; + } + Ok(()) + } + + fn print_pat(&mut self, pat: &Pat) -> fmt::Result { + match pat { + Pat::Missing => write!(self, "[missing]"), + Pat::Literal(lit) => self.print_literal(lit), + Pat::Var(var) => write!(self, "{}", self.db.lookup_var(*var)), + Pat::Tuple { pats } => self.print_seq(pats, None, "{", "}", ",", |this, pat| { + this.print_pat(&this.body[*pat]) + }), + Pat::List { pats, tail } => { + self.print_seq(pats, tail.as_ref(), "[", "]", ",", |this, pat| { + this.print_pat(&this.body[*pat]) + }) + } + Pat::Match { lhs, rhs } => { + self.print_pat(&self.body[*lhs])?; + write!(self, " = ")?; + self.print_pat(&self.body[*rhs]) + } + Pat::UnaryOp { pat, op } => { + write!(self, "({} ", op)?; + self.print_pat(&self.body[*pat])?; + write!(self, ")") + } + Pat::BinaryOp { lhs, rhs, op } => { + write!(self, "(")?; + self.print_pat(&self.body[*lhs])?; + write!(self, " {} ", op)?; + self.print_pat(&self.body[*rhs])?; + write!(self, ")") + } + Pat::Map { fields } => { + self.print_seq(fields, None, "#{", "}", ",", |this, (key, value)| { + this.print_expr(&this.body[*key])?; + write!(this, " := ")?; + this.print_pat(&this.body[*value]) + }) + } + Pat::RecordIndex { name, field } => { + write!( + self, + "#{}.{}", + self.db.lookup_atom(*name), + self.db.lookup_atom(*field) + ) + } + Pat::Record { name, fields } => { + write!(self, "#{}", self.db.lookup_atom(*name))?; + self.print_seq(fields, None, "{", "}", ",", |this, (key, val)| { + write!(this, "{} = ", this.db.lookup_atom(*key))?; + this.print_pat(&this.body[*val]) + }) + } + Pat::Binary { segs } => self.print_seq(segs, None, "<<", ">>", ",", |this, seg| { + this.print_bin_segment(seg, |this, pat| this.print_pat(&this.body[pat])) + }), + Pat::MacroCall { expansion, args: _ } => self.print_pat(&self.body[*expansion]), + } + } + + fn print_guards(&mut self, guards: &[Vec], when_nested: bool) -> fmt::Result { + if !guards.is_empty() { + if when_nested { + self.indent_level += 1; + writeln!(self, " when")?; + } + let mut sep = ""; + for guard_clause in guards { + write!(self, "{}", sep)?; + sep = ";\n"; + let mut sep = ""; + for expr in guard_clause { + write!(self, "{}", sep)?; + sep = ",\n"; + self.print_expr(&self.body[*expr])?; + } + } + if when_nested { + self.indent_level -= 1; + write!(self, "\n->") + } else { + write!(self, " ->") + } + } else { + write!(self, " ->") + } + } + + fn print_type_guards(&mut self, guards: &[(Var, TypeExprId)]) -> fmt::Result { + if !guards.is_empty() { + self.indent_level += 1; + write!(self, "\nwhen ")?; + let mut sep = ""; + for (var, ty) in guards { + write!(self, "{}{} :: ", sep, self.db.lookup_var(*var))?; + sep = ", "; + self.print_type(&self.body[*ty])?; + } + self.indent_level += 1; + } + Ok(()) + } + + fn print_clause_body(&mut self, exprs: &[ExprId]) -> fmt::Result { + self.indent_level += 1; + let mut sep = ""; + for expr_id in exprs { + writeln!(self, "{}", sep)?; + sep = ","; + self.print_expr(&self.body[*expr_id])?; + } + self.indent_level -= 1; + Ok(()) + } + + fn print_expr(&mut self, expr: &Expr) -> fmt::Result { + match expr { + Expr::Missing => write!(self, "[missing]"), + Expr::Literal(lit) => self.print_literal(lit), + Expr::Var(var) => write!(self, "{}", self.db.lookup_var(*var)), + Expr::Tuple { exprs } => self.print_seq(exprs, None, "{", "}", ",", |this, expr| { + this.print_expr(&this.body[*expr]) + }), + Expr::List { exprs, tail } => { + self.print_seq(exprs, tail.as_ref(), "[", "]", ",", |this, expr| { + this.print_expr(&this.body[*expr]) + }) + } + Expr::Match { lhs, rhs } => { + self.print_pat(&self.body[*lhs])?; + write!(self, " = ")?; + self.print_expr(&self.body[*rhs]) + } + Expr::UnaryOp { expr, op } => { + write!(self, "({} ", op)?; + self.print_expr(&self.body[*expr])?; + write!(self, ")") + } + Expr::BinaryOp { lhs, rhs, op } => { + write!(self, "(")?; + self.print_expr(&self.body[*lhs])?; + write!(self, " {} ", op)?; + self.print_expr(&self.body[*rhs])?; + write!(self, ")") + } + Expr::Map { fields } => { + self.print_seq(fields, None, "#{", "}", ",", |this, (key, value)| { + this.print_expr(&this.body[*key])?; + write!(this, " => ")?; + this.print_expr(&this.body[*value]) + }) + } + Expr::MapUpdate { expr, fields } => { + self.print_expr(&self.body[*expr])?; + self.print_seq(fields, None, "#{", "}", ",", |this, (key, op, value)| { + this.print_expr(&this.body[*key])?; + write!(this, " {} ", op)?; + this.print_expr(&this.body[*value]) + }) + } + Expr::RecordIndex { name, field } => { + write!( + self, + "#{}.{}", + self.db.lookup_atom(*name), + self.db.lookup_atom(*field) + ) + } + Expr::Record { name, fields } => { + write!(self, "#{}", self.db.lookup_atom(*name))?; + self.print_seq(fields, None, "{", "}", ",", |this, (key, val)| { + write!(this, "{} = ", this.db.lookup_atom(*key))?; + this.print_expr(&this.body[*val]) + }) + } + Expr::RecordUpdate { expr, name, fields } => { + self.print_expr(&self.body[*expr])?; + write!(self, "#{}", self.db.lookup_atom(*name))?; + self.print_seq(fields, None, "{", "}", ",", |this, (key, val)| { + write!(this, "{} = ", this.db.lookup_atom(*key))?; + this.print_expr(&this.body[*val]) + }) + } + Expr::RecordField { expr, name, field } => { + self.print_expr(&self.body[*expr])?; + write!( + self, + "#{}.{}", + self.db.lookup_atom(*name), + self.db.lookup_atom(*field) + ) + } + Expr::Binary { segs } => self.print_seq(segs, None, "<<", ">>", ",", |this, seg| { + this.print_bin_segment(seg, |this, expr| this.print_expr(&this.body[expr])) + }), + Expr::Catch { expr } => { + write!(self, "(catch ")?; + self.print_expr(&self.body[*expr])?; + write!(self, ")") + } + Expr::Block { exprs } => { + self.print_seq(exprs, None, "begin", "end", ",", |this, expr| { + this.print_expr(&this.body[*expr]) + }) + } + Expr::Case { expr, clauses } => { + write!(self, "case ")?; + self.print_expr(&self.body[*expr])?; + self.print_seq(clauses, None, " of", "end", ";", |this, clause| { + this.print_pat(&this.body[clause.pat])?; + this.print_guards(&clause.guards, true)?; + this.print_clause_body(&clause.exprs) + }) + } + Expr::Receive { clauses, after } => { + self.print_seq(clauses, None, "receive", "", ";", |this, clause| { + this.print_pat(&this.body[clause.pat])?; + this.print_guards(&clause.guards, true)?; + this.print_clause_body(&clause.exprs) + })?; + if let Some(after) = after { + write!(self, "after ")?; + self.print_expr(&self.body[after.timeout])?; + write!(self, " ->")?; + self.print_clause_body(&after.exprs)?; + write!(self, "\nend") + } else { + write!(self, "end") + } + } + Expr::MacroCall { expansion, args: _ } => self.print_expr(&self.body[*expansion]), + Expr::Call { target, args } => { + self.print_call_target(target, |this, expr| this.print_expr(&this.body[*expr]))?; + self.print_seq(args, None, "(", ")", ",", |this, expr| { + this.print_expr(&this.body[*expr]) + }) + } + Expr::CaptureFun { target, arity } => { + write!(self, "fun ")?; + self.print_call_target(target, |this, expr| this.print_expr(&this.body[*expr]))?; + write!(self, "/")?; + self.print_expr(&self.body[*arity]) + } + Expr::If { clauses } => { + self.print_seq(clauses, None, "if", "end", ";", |this, clause| { + this.print_guards(&clause.guards, false)?; + this.print_clause_body(&clause.exprs) + }) + } + Expr::Try { + exprs, + of_clauses, + catch_clauses, + after, + } => { + self.print_seq(exprs, None, "try", "", ",", |this, expr| { + this.print_expr(&this.body[*expr]) + })?; + if !of_clauses.is_empty() { + self.print_seq(of_clauses, None, "of", "", ";", |this, clause| { + this.print_pat(&this.body[clause.pat])?; + this.print_guards(&clause.guards, true)?; + this.print_clause_body(&clause.exprs) + })?; + } + if !catch_clauses.is_empty() { + self.print_seq(catch_clauses, None, "catch", "", ";", |this, clause| { + if let Some(class) = clause.class { + this.print_pat(&this.body[class])?; + write!(this, ":")?; + } + this.print_pat(&this.body[clause.reason])?; + if let Some(stack) = clause.stack { + write!(this, ":")?; + this.print_pat(&this.body[stack])?; + } + this.print_guards(&clause.guards, true)?; + this.print_clause_body(&clause.exprs) + })?; + } + if !after.is_empty() { + self.print_seq(after, None, "after", "", ",", |this, expr| { + this.print_expr(&this.body[*expr]) + })?; + } + write!(self, "end") + } + Expr::Comprehension { builder, exprs } => { + let (start, end) = match builder { + ComprehensionBuilder::List(_) => ("[", "]"), + ComprehensionBuilder::Binary(_) => ("<<", ">>"), + ComprehensionBuilder::Map(_, _) => ("#{", "}"), + }; + writeln!(self, "{}", start)?; + self.indent_level += 1; + match builder { + ComprehensionBuilder::List(expr) => self.print_expr(&self.body[*expr])?, + ComprehensionBuilder::Binary(expr) => self.print_expr(&self.body[*expr])?, + ComprehensionBuilder::Map(expr1, expr2) => { + self.print_expr(&self.body[*expr1])?; + write!(self, " => ")?; + self.print_expr(&self.body[*expr2])? + } + } + self.indent_level -= 1; + writeln!(self)?; + self.print_seq(exprs, None, "||", end, ",", |this, expr| match expr { + ComprehensionExpr::BinGenerator { pat, expr } => { + this.print_pat(&this.body[*pat])?; + write!(this, " <= ")?; + this.print_expr(&this.body[*expr]) + } + ComprehensionExpr::ListGenerator { pat, expr } => { + this.print_pat(&this.body[*pat])?; + write!(this, " <- ")?; + this.print_expr(&this.body[*expr]) + } + ComprehensionExpr::Expr(expr) => this.print_expr(&this.body[*expr]), + ComprehensionExpr::MapGenerator { key, value, expr } => { + this.print_pat(&this.body[*key])?; + write!(this, " := ")?; + this.print_pat(&this.body[*value])?; + write!(this, " <- ")?; + this.print_expr(&this.body[*expr]) + } + }) + } + Expr::Closure { clauses, name } => { + let name_str = if let Some(pat_id) = name { + print_pat(self.db, self.body, *pat_id) + } else { + "".to_string() + }; + let name_str = name_str.trim(); + self.print_seq(clauses, None, "fun", "end", ";", |this, clause| { + this.print_clause(clause, name_str) + }) + } + Expr::Maybe { + exprs, + else_clauses, + } => { + self.print_seq(exprs, None, "maybe", "", ",", |this, expr| match expr { + MaybeExpr::Cond { lhs, rhs } => { + this.print_pat(&this.body[*lhs])?; + write!(this, " ?= ")?; + this.print_expr(&this.body[*rhs]) + } + MaybeExpr::Expr(expr_id) => this.print_expr(&this.body[*expr_id]), + })?; + if !else_clauses.is_empty() { + self.print_seq(else_clauses, None, "else", "", ";", |this, clause| { + this.print_pat(&this.body[clause.pat])?; + this.print_guards(&clause.guards, true)?; + this.print_clause_body(&clause.exprs) + })?; + } + write!(self, "end") + } + } + } + + fn print_type(&mut self, ty: &TypeExpr) -> fmt::Result { + match ty { + TypeExpr::Missing => write!(self, "[missing]"), + TypeExpr::Literal(lit) => self.print_literal(lit), + TypeExpr::Var(var) => write!(self, "{}", self.db.lookup_var(*var)), + TypeExpr::Tuple { args } => self.print_seq(args, None, "{", "}", ",", |this, ty| { + this.print_type(&this.body[*ty]) + }), + TypeExpr::List(list) => { + write!(self, "[")?; + match list { + ListType::Empty => {} + ListType::Regular(ty) => { + self.print_type(&self.body[*ty])?; + } + ListType::NonEmpty(ty) => { + self.print_type(&self.body[*ty])?; + write!(self, ", ...")?; + } + } + write!(self, "]") + } + TypeExpr::Map { fields } => { + self.print_seq(fields, None, "#{", "}", ",", |this, (key, op, value)| { + this.print_type(&this.body[*key])?; + write!(this, " {} ", op)?; + this.print_type(&this.body[*value]) + }) + } + TypeExpr::Record { name, fields } => { + write!(self, "#{}", self.db.lookup_atom(*name))?; + self.print_seq(fields, None, "{", "}", ",", |this, (key, val)| { + write!(this, "{} :: ", this.db.lookup_atom(*key))?; + this.print_type(&this.body[*val]) + }) + } + TypeExpr::Fun(fun) => { + write!(self, "fun(")?; + match fun { + FunType::Any => {} + FunType::AnyArgs { result } => { + write!(self, "(...) -> ")?; + self.print_type(&self.body[*result])?; + } + FunType::Full { params, result } => { + self.print_args("", params, |this, ty| this.print_type(&this.body[*ty]))?; + write!(self, " -> ")?; + self.print_type(&self.body[*result])?; + } + } + write!(self, ")") + } + TypeExpr::UnaryOp { type_expr, op } => { + write!(self, "({} ", op)?; + self.print_type(&self.body[*type_expr])?; + write!(self, ")") + } + TypeExpr::AnnType { var, ty } => { + write!(self, "({} ", self.db.lookup_var(*var))?; + write!(self, " :: ")?; + self.print_type(&self.body[*ty])?; + write!(self, ")") + } + TypeExpr::BinaryOp { lhs, rhs, op } => { + write!(self, "(")?; + self.print_type(&self.body[*lhs])?; + write!(self, " {} ", op)?; + self.print_type(&self.body[*rhs])?; + write!(self, ")") + } + TypeExpr::Range { lhs, rhs } => { + write!(self, "(")?; + self.print_type(&self.body[*lhs])?; + write!(self, "..")?; + self.print_type(&self.body[*rhs])?; + write!(self, ")") + } + TypeExpr::Union { types } => self.print_seq(types, None, "(", ")", " |", |this, ty| { + this.print_type(&self.body[*ty]) + }), + TypeExpr::Call { target, args } => { + self.print_call_target(target, |this, ty| this.print_type(&this.body[*ty]))?; + self.print_seq(args, None, "(", ")", ",", |this, ty| { + this.print_type(&this.body[*ty]) + }) + } + TypeExpr::MacroCall { expansion, args: _ } => self.print_type(&self.body[*expansion]), + } + } + + fn print_term(&mut self, term: &Term) -> fmt::Result { + match term { + Term::Missing => write!(self, "[missing]"), + Term::Literal(lit) => self.print_literal(lit), + Term::Tuple { exprs } => self.print_seq(exprs, None, "{", "}", ",", |this, term| { + this.print_term(&this.body[*term]) + }), + Term::List { exprs, tail } => { + self.print_seq(exprs, tail.as_ref(), "[", "]", ",", |this, term| { + this.print_term(&this.body[*term]) + }) + } + Term::Map { fields } => { + self.print_seq(fields, None, "#{", "}", ",", |this, (key, value)| { + this.print_term(&this.body[*key])?; + write!(this, " => ")?; + this.print_term(&this.body[*value]) + }) + } + Term::Binary(bin) => { + if let Ok(str) = str::from_utf8(bin) { + write!(self, "<<{:?}/utf8>>", str) + } else { + write!(self, "<<")?; + let mut sep = ""; + for byte in bin { + write!(self, "{}{}", sep, byte)?; + sep = ", "; + } + write!(self, ">>") + } + } + Term::CaptureFun { + module, + name, + arity, + } => { + write!( + self, + "fun {}:{}/{}", + self.db.lookup_atom(*module), + self.db.lookup_atom(*name), + arity + ) + } + Term::MacroCall { expansion, args: _ } => self.print_term(&self.body[*expansion]), + } + } + + fn print_call_target( + &mut self, + target: &CallTarget, + print: impl Fn(&mut Self, &T) -> fmt::Result, + ) -> fmt::Result { + match target { + CallTarget::Local { name } => print(self, name), + CallTarget::Remote { module, name } => { + print(self, module)?; + write!(self, ":")?; + print(self, name) + } + } + } + + fn print_literal(&mut self, lit: &Literal) -> fmt::Result { + match lit { + Literal::String(string) => write!(self, "{:?}", string), + Literal::Char(char) => write!(self, "${}", char), + Literal::Atom(atom) => write!(self, "'{}'", self.db.lookup_atom(*atom)), + Literal::Integer(int) => write!(self, "{}", int), + Literal::Float(float) => write!(self, "{}", f64::from_bits(*float)), + } + } + + fn print_seq( + &mut self, + exprs: &[T], + tail: Option<&T>, + start: &str, + end: &str, + base_sep: &str, + print: impl Fn(&mut Self, &T) -> fmt::Result, + ) -> fmt::Result { + if exprs.is_empty() && tail.is_none() { + write!(self, "{}{}", start, end) + } else { + write!(self, "{}", start)?; + self.indent_level += 1; + let mut sep = ""; + for expr in exprs { + writeln!(self, "{}", sep)?; + sep = base_sep; + print(self, expr)?; + } + if let Some(tail) = tail { + write!(self, "\n| ")?; + print(self, tail)?; + } + self.indent_level -= 1; + write!(self, "\n{}", end) + } + } + + fn print_args( + &mut self, + name: &str, + exprs: &[T], + print: impl Fn(&mut Self, &T) -> fmt::Result, + ) -> fmt::Result { + write!(self, "{}(", name)?; + let mut sep = ""; + for expr in exprs { + write!(self, "{}", sep)?; + sep = ", "; + print(self, expr)?; + } + write!(self, ")") + } + + fn print_bin_segment( + &mut self, + seg: &BinarySeg, + print: fn(&mut Self, T) -> fmt::Result, + ) -> fmt::Result { + print(self, seg.elem)?; + if let Some(size) = seg.size { + write!(self, ":")?; + self.print_expr(&self.body[size])?; + } + if !seg.tys.is_empty() || seg.unit.is_some() { + write!(self, "/")?; + let mut sep = ""; + for &ty in &seg.tys { + write!(self, "{}{}", sep, self.db.lookup_atom(ty))?; + sep = "-"; + } + if let Some(unit) = seg.unit { + write!(self, "{}unit:{}", sep, unit)?; + } + } + Ok(()) + } +} + +impl<'a> fmt::Write for Printer<'a> { + fn write_str(&mut self, s: &str) -> fmt::Result { + for line in s.split_inclusive('\n') { + if self.needs_indent { + if !self.buf.ends_with('\n') { + self.buf.push('\n'); + } + for _ in 0..self.indent_level { + self.buf.push_str(" "); + } + self.needs_indent = false; + } + + self.buf.push_str(line); + self.needs_indent = line.ends_with('\n'); + } + + Ok(()) + } +} diff --git a/crates/hir/src/body/scope.rs b/crates/hir/src/body/scope.rs new file mode 100644 index 0000000000..05223af6ea --- /dev/null +++ b/crates/hir/src/body/scope.rs @@ -0,0 +1,801 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! Name resolution for expressions. +//! +//! This models what happens in elp_lint.erl +//! An overview prepared by Richard Carlsson can be found at +//! https://docs.google.com/document/d/1_qukz2RD5Bc5U4npfGwfzARJxF5feBLNiLKXsuVNy-g/edit +//! +//! We make the following assumptions in this code +//! +//! - We follow the process erlc does +//! +//! - We are not processing diagnostics. In some cases this means +//! that we compute a binding for a variable that will generate a +//! warning or error from the Erlang compiler. Examples are unsafe +//! variables escaping from a case clause, or bindings escaping from +//! the `catch` part of a `try .. catch`. +//! +//! We prioritise the ability of a user to navigate using +//! go-to-definition, even if the code is broken, as it may be a +//! transient state, and diagnostics from other sources will give +//! additional feedback. + +use std::ops::Index; +use std::ops::IndexMut; +use std::sync::Arc; + +use fxhash::FxHashMap; +use la_arena::Arena; +use la_arena::ArenaMap; +use la_arena::Idx; + +use super::UnexpandedIndex; +use crate::db::MinDefDatabase; +use crate::expr::ClauseId; +use crate::expr::MaybeExpr; +use crate::Body; +use crate::CRClause; +use crate::Clause; +use crate::ComprehensionBuilder; +use crate::ExprId; +use crate::FunctionBody; +use crate::FunctionId; +use crate::InFile; +use crate::Name; +use crate::PatId; +use crate::Var; + +pub type ScopeId = Idx; + +#[derive(Debug, PartialEq, Eq)] +pub struct FunctionScopes { + // Invariant: These are the same order as in FunctionBody + clause_scopes: ArenaMap, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct ExprScopes { + scopes: Arena, + scope_by_expr: FxHashMap, + scope_by_pat: FxHashMap, + // Interned value of the anonymous variable ('_'). + anonymous_var: Var, +} + +impl Index for ExprScopes { + type Output = ScopeData; + + fn index(&self, index: ScopeId) -> &Self::Output { + &self.scopes[index] + } +} + +impl IndexMut for ExprScopes { + fn index_mut(&mut self, index: ScopeId) -> &mut ScopeData { + &mut self.scopes[index] + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct ScopeData { + parent: Option, + entries: ScopeEntries, +} + +type ScopeEntryMap = FxHashMap>; +#[derive(Debug, PartialEq, Eq, Default, Clone)] +pub struct ScopeEntries { + data: ScopeEntryMap, +} + +impl ScopeEntries { + fn insert(&mut self, name: Var, pats: Vec) { + self.data + .entry(name) + .and_modify(|v| v.extend(pats.clone())) + .or_insert_with(|| pats.clone()); + } + + pub fn lookup(&self, name: &Var) -> Option<&Vec> { + self.data.get(name) + } + + pub fn names(&self) -> impl Iterator + '_ { + self.data.keys().copied() + } +} + +/// See https://fburl.com/code/otca3wbd for the equivalent data +/// structures in elp_lint.erl. We track the elp_lint state in a +/// `ScopeEntry`, and usage in `VarTable`. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum VarUsage { + Used, + UnUsed, +} + +/// Equivalent to elp_lint VarTable. +#[derive(Debug, Default, Clone)] +struct VarTable { + vars: FxHashMap, +} + +impl VarTable { + // For a variable in a pattern: + // - If it is not in the bound set, it is new and is added to the set (as yet unused) + // - If it is already in the bound set, it is simply marked as used + fn new_in_pattern(&mut self, var: Var) { + self.vars + .entry(var) + .and_modify(|e| *e = VarUsage::Used) + .or_insert(VarUsage::UnUsed); + } + + fn set_used(&mut self, var: &Var) { + self.vars.insert(*var, VarUsage::Used); + } + + fn is_new(&self, var: &Var) -> bool { + match self.vars.get(var) { + Some(v) => *v == VarUsage::UnUsed, + None => true, + } + } + + fn merge(&mut self, other: &VarTable) { + for (k, v) in &other.vars { + self.vars + .entry(*k) + .and_modify(|v2| { + if *v2 == VarUsage::Used || *v == VarUsage::Used { + *v2 = VarUsage::Used; + } else { + *v2 = VarUsage::UnUsed; + } + }) + .or_insert(*v); + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AddBinding { + Always, + IfUnused, +} + +impl FunctionScopes { + pub(crate) fn function_scopes_query( + db: &dyn MinDefDatabase, + function_id: InFile, + ) -> Arc { + let function_body = db.function_body(function_id); + let anonymous_var = db.var(Name::ANONYMOUS); + let clause_scopes = function_body + .clauses + .iter() + .map(|(idx, clause)| { + ( + idx, + ExprScopes::for_clause(&function_body, clause, anonymous_var), + ) + }) + .collect(); + Arc::new(FunctionScopes { clause_scopes }) + } + + pub(crate) fn get(&self, clause: ClauseId) -> Option { + self.clause_scopes.get(clause).cloned() + } +} + +impl ExprScopes { + fn for_clause(body: &FunctionBody, clause: &Clause, anonymous_var: Var) -> ExprScopes { + let mut scopes = ExprScopes { + scopes: Arena::default(), + scope_by_expr: FxHashMap::default(), + scope_by_pat: FxHashMap::default(), + anonymous_var, + }; + let mut root = scopes.root_scope(); + let mut vt = VarTable::default(); + scopes.add_params_bindings(&body.body, &mut root, &clause.pats, &mut vt); + for exprs in &clause.guards { + for expr_id in exprs { + compute_expr_scopes(*expr_id, &body.body, &mut scopes, &mut root, &mut vt); + } + } + for expr_id in &clause.exprs { + compute_expr_scopes(*expr_id, &body.body, &mut scopes, &mut root, &mut vt); + } + scopes + } + + pub fn entries(&self, scope: ScopeId) -> &ScopeEntries { + &self.scopes[scope].entries + } + + pub fn scope_chain(&self, scope: Option) -> impl Iterator + '_ { + std::iter::successors(scope, move |&scope| self.scopes[scope].parent) + } + + pub fn scope_for_expr(&self, expr_id: ExprId) -> Option { + self.scope_by_expr.get(&expr_id).copied() + } + + pub fn scope_for_pat(&self, pat_id: PatId) -> Option { + self.scope_by_pat.get(&pat_id).copied() + } + + fn root_scope(&mut self) -> ScopeId { + self.scopes.alloc(ScopeData { + parent: None, + entries: ScopeEntries::default(), + }) + } + + fn new_scope(&mut self, parent: ScopeId) -> ScopeId { + self.scopes.alloc(ScopeData { + parent: Some(parent), + entries: ScopeEntries::default(), + }) + } + + fn set_scope_expr(&mut self, node: ExprId, scope: ScopeId) { + self.scope_by_expr.insert(node, scope); + } + + fn set_scope_pat(&mut self, node: PatId, scope: ScopeId) { + self.scope_by_pat.insert(node, scope); + } + + fn add_bindings( + &mut self, + body: &Body, + scope: &mut ScopeId, + pat: PatId, + vt: &mut VarTable, + add_bindings: AddBinding, + ) { + self.set_scope_pat(pat, *scope); + let pattern = &body[pat]; + match pattern { + crate::Pat::Missing => {} + crate::Pat::Literal(_) => {} + crate::Pat::Var(var) => { + if var != &self.anonymous_var { + vt.new_in_pattern(*var); + if vt.is_new(var) { + vt.set_used(var); + if add_bindings == AddBinding::IfUnused { + self.scopes[*scope].entries.insert(*var, vec![pat]); + } + } + if add_bindings != AddBinding::IfUnused { + self.scopes[*scope].entries.insert(*var, vec![pat]); + } + } + } + crate::Pat::Match { lhs, rhs } => { + self.add_bindings(body, scope, *lhs, vt, add_bindings); + self.add_bindings(body, scope, *rhs, vt, add_bindings); + } + crate::Pat::Tuple { pats } => { + for pat in pats { + self.add_bindings(body, scope, *pat, vt, add_bindings) + } + } + crate::Pat::List { pats, tail } => { + for pat in pats { + self.add_bindings(body, scope, *pat, vt, add_bindings) + } + tail.map(|pat| self.add_bindings(body, scope, pat, vt, add_bindings)); + } + crate::Pat::Binary { segs } => { + for seg in segs { + if let Some(expr) = seg.size { + compute_expr_scopes(expr, body, self, scope, vt); + } + self.add_bindings(body, scope, seg.elem, vt, add_bindings) + } + } + crate::Pat::UnaryOp { pat, op: _ } => { + self.add_bindings(body, scope, *pat, vt, add_bindings); + } + crate::Pat::BinaryOp { lhs, rhs, op: _ } => { + self.add_bindings(body, scope, *lhs, vt, add_bindings); + self.add_bindings(body, scope, *rhs, vt, add_bindings); + } + crate::Pat::Record { name: _, fields } => { + for (_, pat) in fields { + self.add_bindings(body, scope, *pat, vt, add_bindings); + } + } + crate::Pat::RecordIndex { name: _, field: _ } => {} + crate::Pat::Map { fields } => { + for (expr, pat) in fields { + compute_expr_scopes(*expr, body, self, scope, vt); + self.add_bindings(body, scope, *pat, vt, add_bindings); + } + } + crate::Pat::MacroCall { expansion, args } => { + self.add_bindings(body, scope, *expansion, vt, add_bindings); + for arg in args { + compute_expr_scopes(*arg, body, self, scope, vt); + } + } + }; + } + + fn add_params_bindings( + &mut self, + body: &Body, + scope: &mut ScopeId, + params: &[PatId], + vt: &mut VarTable, + ) { + params + .iter() + .for_each(|pat| self.add_bindings(body, scope, *pat, vt, AddBinding::IfUnused)); + } +} + +fn compute_expr_scopes( + expr: ExprId, + body: &Body, + scopes: &mut ExprScopes, + scope: &mut ScopeId, + vt: &mut VarTable, +) { + scopes.set_scope_expr(expr, *scope); + match &UnexpandedIndex(&body)[expr] { + crate::Expr::Missing => {} + crate::Expr::Literal(_) => {} + crate::Expr::Var(_) => {} + crate::Expr::Match { lhs, rhs } => { + compute_expr_scopes(*rhs, body, scopes, scope, vt); + scopes.add_bindings(body, scope, *lhs, vt, AddBinding::IfUnused); + } + crate::Expr::Tuple { exprs } => { + for expr in exprs { + compute_expr_scopes(*expr, body, scopes, scope, vt); + } + } + crate::Expr::List { exprs, tail } => { + for expr in exprs { + compute_expr_scopes(*expr, body, scopes, scope, vt); + } + if let Some(tail) = tail { + compute_expr_scopes(*tail, body, scopes, scope, vt); + } + } + crate::Expr::Binary { segs } => { + for seg in segs { + compute_expr_scopes(seg.elem, body, scopes, scope, vt); + if let Some(size) = seg.size { + compute_expr_scopes(size, body, scopes, scope, vt); + } + } + } + crate::Expr::UnaryOp { expr, op: _ } => { + compute_expr_scopes(*expr, body, scopes, scope, vt); + } + crate::Expr::BinaryOp { lhs, rhs, op: _ } => { + // TODO: deal with `(X = 1) + (Y = 2)` binding both sides, and exporting values + compute_expr_scopes(*lhs, body, scopes, scope, vt); + compute_expr_scopes(*rhs, body, scopes, scope, vt); + } + crate::Expr::Record { name: _, fields } => { + for (_, expr) in fields { + compute_expr_scopes(*expr, body, scopes, scope, vt); + } + } + crate::Expr::RecordUpdate { + expr, + name: _, + fields, + } => { + compute_expr_scopes(*expr, body, scopes, scope, vt); + for (_, expr) in fields { + compute_expr_scopes(*expr, body, scopes, scope, vt); + } + } + crate::Expr::RecordIndex { name: _, field: _ } => {} + crate::Expr::RecordField { + expr, + name: _, + field: _, + } => { + compute_expr_scopes(*expr, body, scopes, scope, vt); + } + crate::Expr::Map { fields } => { + for (lhs, rhs) in fields { + compute_expr_scopes(*lhs, body, scopes, scope, vt); + compute_expr_scopes(*rhs, body, scopes, scope, vt); + } + } + crate::Expr::MapUpdate { expr, fields } => { + compute_expr_scopes(*expr, body, scopes, scope, vt); + for (lhs, _, rhs) in fields { + compute_expr_scopes(*lhs, body, scopes, scope, vt); + compute_expr_scopes(*rhs, body, scopes, scope, vt); + } + } + crate::Expr::Catch { expr } => { + compute_expr_scopes(*expr, body, scopes, scope, vt); + } + crate::Expr::MacroCall { expansion, args } => { + compute_expr_scopes(*expansion, body, scopes, scope, vt); + for arg in args { + compute_expr_scopes(*arg, body, scopes, scope, vt); + } + } + crate::Expr::Call { target, args } => { + match target { + crate::CallTarget::Local { name } => { + compute_expr_scopes(*name, body, scopes, scope, vt); + } + crate::CallTarget::Remote { module, name } => { + compute_expr_scopes(*module, body, scopes, scope, vt); + compute_expr_scopes(*name, body, scopes, scope, vt); + } + } + for arg in args { + compute_expr_scopes(*arg, body, scopes, scope, vt); + } + } + crate::Expr::Comprehension { builder, exprs } => { + let mut sub_vt = vt.clone(); + let scope = &mut scopes.new_scope(*scope); + for expr in exprs { + match expr { + crate::ComprehensionExpr::BinGenerator { pat, expr } => { + compute_expr_scopes(*expr, body, scopes, scope, &mut sub_vt); + *scope = scopes.new_scope(*scope); + scopes.add_bindings(body, scope, *pat, &mut sub_vt, AddBinding::Always); + } + crate::ComprehensionExpr::ListGenerator { pat, expr } => { + compute_expr_scopes(*expr, body, scopes, scope, &mut sub_vt); + *scope = scopes.new_scope(*scope); + scopes.add_bindings(body, scope, *pat, &mut sub_vt, AddBinding::Always); + } + crate::ComprehensionExpr::Expr(expr) => { + compute_expr_scopes(*expr, body, scopes, scope, &mut sub_vt) + } + crate::ComprehensionExpr::MapGenerator { key, value, expr } => { + compute_expr_scopes(*expr, body, scopes, scope, &mut sub_vt); + *scope = scopes.new_scope(*scope); + scopes.add_bindings(body, scope, *key, &mut sub_vt, AddBinding::Always); + scopes.add_bindings(body, scope, *value, &mut sub_vt, AddBinding::Always); + } + }; + } + match builder { + ComprehensionBuilder::List(expr) => { + compute_expr_scopes(*expr, body, scopes, scope, &mut sub_vt) + } + ComprehensionBuilder::Binary(expr) => { + compute_expr_scopes(*expr, body, scopes, scope, &mut sub_vt) + } + ComprehensionBuilder::Map(key, value) => { + compute_expr_scopes(*key, body, scopes, scope, &mut sub_vt); + compute_expr_scopes(*value, body, scopes, scope, &mut sub_vt) + } + } + } + crate::Expr::Block { exprs } => { + for expr in exprs { + compute_expr_scopes(*expr, body, scopes, scope, vt); + } + } + crate::Expr::If { clauses } => { + let if_scopes: Vec<_> = clauses + .iter() + .map(|clause| { + let mut scope = scopes.new_scope(*scope); + let mut sub_vt = vt.clone(); + for guard_exprs in &clause.guards { + for guard in guard_exprs { + compute_expr_scopes(*guard, body, scopes, &mut scope, &mut sub_vt); + } + } + for expr in &clause.exprs { + compute_expr_scopes(*expr, body, scopes, &mut scope, &mut sub_vt); + } + (scope, sub_vt) + }) + .collect(); + vt.merge(&add_exported_scopes(scopes, scope, &if_scopes)); + } + crate::Expr::Case { expr, clauses } => { + compute_expr_scopes(*expr, body, scopes, scope, vt); + let clause_scopes = compute_clause_scopes(clauses, body, scopes, scope, vt); + vt.merge(&add_exported_scopes(scopes, scope, &clause_scopes)); + } + crate::Expr::Receive { clauses, after } => { + let mut clause_scopes = compute_clause_scopes(clauses, body, scopes, scope, vt); + if let Some(ra) = after { + let mut sub_vt = vt.clone(); + let mut scope = scopes.new_scope(*scope); + compute_expr_scopes(ra.timeout, body, scopes, &mut scope, &mut sub_vt); + clause_scopes.push((scope, sub_vt)); + let mut sub_vt2 = vt.clone(); + let mut scope = scopes.new_scope(scope); + for expr in &ra.exprs { + compute_expr_scopes(*expr, body, scopes, &mut scope, &mut sub_vt2); + } + clause_scopes.push((scope, sub_vt2)); + }; + vt.merge(&add_exported_scopes(scopes, scope, &clause_scopes)); + } + crate::Expr::Try { + exprs, + of_clauses, + catch_clauses, + after, + } => { + // From elp_lint: The only exports we allow are from the exprs to the of_clauses. + let mut expr_vt = vt.clone(); + for expr in exprs { + compute_expr_scopes(*expr, body, scopes, scope, &mut expr_vt); + } + let mut clause_scopes = compute_clause_scopes(of_clauses, body, scopes, scope, vt); + for clause in catch_clauses { + let mut sub_vt = vt.clone(); + let mut scope = scopes.new_scope(*scope); + if let Some(class) = clause.class { + scopes.add_bindings(body, &mut scope, class, &mut sub_vt, AddBinding::IfUnused); + } + scopes.add_bindings( + body, + &mut scope, + clause.reason, + &mut sub_vt, + AddBinding::IfUnused, + ); + if let Some(stack) = clause.stack { + scopes.add_bindings(body, &mut scope, stack, &mut sub_vt, AddBinding::IfUnused); + } + for guard in &clause.guards { + for expr in guard { + compute_expr_scopes(*expr, body, scopes, &mut scope, &mut sub_vt); + } + } + for expr in &clause.exprs { + compute_expr_scopes(*expr, body, scopes, &mut scope, &mut sub_vt); + } + clause_scopes.push((scope, sub_vt)); + } + + let mut sub_vt2 = vt.clone(); + for expr in after { + compute_expr_scopes(*expr, body, scopes, scope, &mut sub_vt2); + } + vt.merge(&add_exported_scopes(scopes, scope, &clause_scopes)); + } + crate::Expr::CaptureFun { target, arity } => { + match target { + crate::CallTarget::Local { name } => { + compute_expr_scopes(*name, body, scopes, scope, vt); + } + crate::CallTarget::Remote { module, name } => { + compute_expr_scopes(*module, body, scopes, scope, vt); + compute_expr_scopes(*name, body, scopes, scope, vt); + } + }; + compute_expr_scopes(*arity, body, scopes, scope, vt); + } + crate::Expr::Closure { clauses, name } => { + if let Some(name) = name { + scopes.add_bindings(body, scope, *name, vt, AddBinding::Always); + } + for clause in clauses.iter() { + let mut sub_vt = vt.clone(); + let mut scope = scopes.new_scope(*scope); + scopes.add_params_bindings(body, &mut scope, &clause.pats, &mut sub_vt); + + for guards in &clause.guards { + for guard in guards { + scope = scopes.new_scope(scope); + compute_expr_scopes(*guard, body, scopes, &mut scope, &mut sub_vt); + } + } + for expr in &clause.exprs { + compute_expr_scopes(*expr, body, scopes, &mut scope, &mut sub_vt); + } + } + } + crate::Expr::Maybe { + exprs, + else_clauses, + } => { + let mut expr_vt = vt.clone(); + let mut expr_scope = scopes.new_scope(*scope); + + for expr in exprs { + match expr { + MaybeExpr::Cond { lhs, rhs } => { + compute_expr_scopes(*rhs, body, scopes, &mut expr_scope, vt); + scopes.add_bindings(body, scope, *lhs, vt, AddBinding::IfUnused); + } + MaybeExpr::Expr(expr) => { + compute_expr_scopes(*expr, body, scopes, &mut expr_scope, &mut expr_vt); + } + } + } + + let clause_scopes = compute_clause_scopes(else_clauses, body, scopes, scope, vt); + vt.merge(&add_exported_scopes(scopes, scope, &clause_scopes)); + } + } +} + +fn compute_clause_scopes( + clauses: &[CRClause], + body: &Body, + scopes: &mut ExprScopes, + scope: &mut ScopeId, + vt: &mut VarTable, +) -> Vec<(ScopeId, VarTable)> { + clauses + .iter() + .map(|clause| { + let mut sub_vt = vt.clone(); + let mut scope = scopes.new_scope(*scope); + scopes.add_bindings( + body, + &mut scope, + clause.pat, + &mut sub_vt, + AddBinding::IfUnused, + ); + for guards in &clause.guards { + for guard in guards { + scope = scopes.new_scope(scope); + compute_expr_scopes(*guard, body, scopes, &mut scope, &mut sub_vt); + } + } + for expr in &clause.exprs { + compute_expr_scopes(*expr, body, scopes, &mut scope, &mut sub_vt); + } + (scope, sub_vt) + }) + .collect() +} + +fn add_exported_scopes( + scopes: &mut ExprScopes, + current_scope_id: &mut ScopeId, + clause_scopes: &[(ScopeId, VarTable)], +) -> VarTable { + let mut scope_info: FxHashMap> = FxHashMap::default(); + let mut ret_vt = VarTable::default(); + for (clause_scope, vt) in clause_scopes { + let scope = &scopes[*clause_scope]; + for (name, pats) in &scope.entries.data { + scope_info + .entry(*name) + .and_modify(|v| v.extend(pats)) + .or_insert_with(|| pats.clone()); + } + ret_vt.merge(vt); + } + + // And add them to a new scope to be valid after this point in the source + *current_scope_id = scopes.new_scope(*current_scope_id); + let current_scope = &mut scopes[*current_scope_id]; + for (name, pats) in scope_info { + current_scope.entries.insert(name, pats); + } + ret_vt +} + +#[cfg(test)] +mod tests { + use elp_base_db::assert_eq_text; + use elp_base_db::fixture::extract_offset; + use elp_base_db::fixture::WithFixture; + use elp_base_db::FileId; + use elp_base_db::SourceDatabase; + use elp_syntax::algo::find_node_at_offset; + use elp_syntax::ast; + use elp_syntax::AstNode; + + use crate::db::MinDefDatabase; + use crate::db::MinInternDatabase; + use crate::test_db::TestDB; + use crate::FunctionId; + use crate::InFile; + use crate::Semantic; + + // Return the first function found in the test fixture + fn find_function(db: &TestDB, file_id: FileId) -> FunctionId { + let forms = db.file_form_list(file_id); + + let function = forms.forms().iter().find_map(|form| match form { + crate::FormIdx::Function(f) => Some(f), + _ => None, + }); + *function.unwrap() + } + + #[track_caller] + fn do_check(elp_fixture: &str, expected: &[&str]) { + let (offset, code) = extract_offset(elp_fixture); + let code = { + let mut buf = String::new(); + let off: usize = offset.into(); + buf.push_str(&code[..off]); + buf.push_str("M~arker"); + buf.push_str(&code[off..]); + buf + }; + + let (db, position) = TestDB::with_position(&code); + let sema = Semantic::new(&db); + let file_id = position.file_id; + let offset = position.offset; + + let file_syntax = db.parse(file_id).syntax_node(); + let var: ast::Var = find_node_at_offset(&file_syntax, offset).unwrap(); + let marker = ast::Expr::ExprMax(ast::ExprMax::Var(var.clone())); + let function = find_function(&db, file_id); + + let scopes = db.function_scopes(InFile { + file_id, + value: function, + }); + let clause_id = sema.find_enclosing_function_clause(var.syntax()).unwrap(); + + let (_body, source_map) = db.function_body_with_source(InFile::new(file_id, function)); + + let expr_id = source_map + .expr_id(InFile { + file_id: file_id.into(), + value: &marker, + }) + .unwrap(); + let clause_scope = scopes.get(clause_id).unwrap(); + let scope = clause_scope.scope_for_expr(expr_id); + + let actual: _ = clause_scope + .scope_chain(scope) + .flat_map(|scope| clause_scope.entries(scope).names()) + .map(|it| db.lookup_var(it).as_str().to_string()) + .collect::>() + .join("\n"); + let expected = expected.join("\n"); + assert_eq_text!(&expected, &actual); + } + + #[test] + fn test_function_param_scope() { + do_check( + r" + f(Bar, Baz) -> + ~. + ", + &["Bar", "Baz"], + ); + } + + #[test] + fn test_simple_assign() { + do_check( + r" + f() -> + X = 1, + ~. + ", + &["X"], + ); + } +} diff --git a/crates/hir/src/body/tests.rs b/crates/hir/src/body/tests.rs new file mode 100644 index 0000000000..efa80ff491 --- /dev/null +++ b/crates/hir/src/body/tests.rs @@ -0,0 +1,2052 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_base_db::fixture::WithFixture; +use expect_test::expect; +use expect_test::Expect; + +use crate::db::MinDefDatabase; +use crate::test_db::TestDB; +use crate::AnyAttribute; +use crate::FormIdx; +use crate::InFile; +use crate::SpecOrCallback; + +#[track_caller] +fn check(ra_fixture: &str, expect: Expect) { + let (db, file_id) = TestDB::with_single_file(ra_fixture); + let form_list = db.file_form_list(file_id); + let pretty = form_list + .forms() + .iter() + .flat_map(|&form_idx| match form_idx { + FormIdx::Function(function_id) => { + let function = &form_list[function_id]; + let body = db.function_body(InFile::new(file_id, function_id)); + Some(body.print(&db, function)) + } + FormIdx::TypeAlias(type_alias_id) => { + let type_alias = &form_list[type_alias_id]; + let body = db.type_body(InFile::new(file_id, type_alias_id)); + Some(body.print(&db, type_alias)) + } + FormIdx::Spec(spec_id) => { + let spec = SpecOrCallback::Spec(form_list[spec_id].clone()); + let body = db.spec_body(InFile::new(file_id, spec_id)); + Some(body.print(&db, spec)) + } + FormIdx::Callback(callback_id) => { + let spec = SpecOrCallback::Callback(form_list[callback_id].clone()); + let body = db.callback_body(InFile::new(file_id, callback_id)); + Some(body.print(&db, spec)) + } + FormIdx::Record(record_id) => { + let body = db.record_body(InFile::new(file_id, record_id)); + Some(body.print(&db, &form_list, record_id)) + } + FormIdx::Attribute(attribute_id) => { + let attribute = AnyAttribute::Attribute(form_list[attribute_id].clone()); + let body = db.attribute_body(InFile::new(file_id, attribute_id)); + Some(body.print(&db, attribute)) + } + FormIdx::CompileOption(attribute_id) => { + let attribute = AnyAttribute::CompileOption(form_list[attribute_id].clone()); + let body = db.compile_body(InFile::new(file_id, attribute_id)); + Some(body.print(&db, attribute)) + } + _ => None, + }) + .collect::>() + .join(""); + expect.assert_eq(pretty.trim_start()); +} + +#[test] +fn simple() { + check( + r#" +foo(ok) -> ok. +"#, + expect![[r#" + foo('ok') -> + 'ok'. + "#]], + ); +} + +#[test] +fn char() { + check( + r#" +foo($a) -> $b. +"#, + expect![[r#" + foo($a) -> + $b. + "#]], + ); +} + +#[test] +fn float() { + check( + r#" +foo(0.1) -> 1.2. +"#, + expect![[r#" + foo(0.1) -> + 1.2. + "#]], + ); +} + +#[test] +fn integer() { + check( + r#" +foo(42) -> 4_2. +"#, + expect![[r#" + foo(42) -> + 42. + "#]], + ); +} + +#[test] +fn string() { + check( + r#" +foo("") -> "abc\s\x61". +"#, + expect![[r#" + foo("") -> + "abc a". + "#]], + ); +} + +#[test] +fn concat() { + check( + r#" +foo("" "") -> "a" "b" "c" "\141". +"#, + expect![[r#" + foo("") -> + "abca". + "#]], + ); +} + +#[test] +fn var() { + check( + r#" +foo(Foo) -> Foo. +"#, + expect![[r#" + foo(Foo) -> + Foo. + "#]], + ); +} + +#[test] +fn tuple() { + check( + r#" +foo({a, b}) -> {1, 2, 3}. +"#, + expect![[r#" + foo({ + 'a', + 'b' + }) -> + { + 1, + 2, + 3 + }. + "#]], + ); +} + +#[test] +fn list() { + check( + r#" +foo([a, b]) -> [1, 2, 3]. +"#, + expect![[r#" + foo([ + 'a', + 'b' + ]) -> + [ + 1, + 2, + 3 + ]. + "#]], + ); + + check( + r#" +foo([a | b]) -> [1, 2 | 3]. +"#, + expect![[r#" + foo([ + 'a' + | 'b' + ]) -> + [ + 1, + 2 + | 3 + ]. + "#]], + ); +} + +#[test] +fn r#match() { + check( + r#" +foo(A = B) -> A = B. +"#, + expect![[r#" + foo(A = B) -> + A = B. + "#]], + ); +} + +#[test] +fn unary_op() { + check( + r#" +foo(+ A, -B) -> bnot A, not C. +"#, + expect![[r#" + foo((+ A), (- B)) -> + (bnot A), + (not C). + "#]], + ); +} + +#[test] +fn binary_op() { + check( + r#" +foo(A ++ B, C + D) -> E andalso F, G ! H. +"#, + expect![[r#" + foo((A ++ B), (C + D)) -> + (E andalso F), + (G ! H). + "#]], + ); +} + +#[test] +fn map() { + check( + r#" +foo(#{1 + 2 := 3 + 4}) -> #{a => b}. +"#, + expect![[r##" + foo(#{ + (1 + 2) := (3 + 4) + }) -> + #{ + 'a' => 'b' + }. + "##]], + ); +} + +#[test] +fn map_update() { + check( + r#" +foo() -> #{a => b}#{a := b, c => d}. +"#, + expect![[r##" + foo() -> + #{ + 'a' => 'b' + }#{ + 'a' := 'b', + 'c' => 'd' + }. + "##]], + ); +} + +#[test] +fn record_index() { + check( + r#" +foo(#record.field) -> #record.field. +"#, + expect![[r##" + foo(#record.field) -> + #record.field. + "##]], + ); +} + +#[test] +fn record() { + check( + r#" +foo1(#record{field = 1}) -> #record{field = A + B}. +foo2(#record{field}) -> #record{field = }. +"#, + expect![[r##" + foo1(#record{ + field = 1 + }) -> + #record{ + field = (A + B) + }. + + foo2(#record{ + field = [missing] + }) -> + #record{ + field = [missing] + }. + "##]], + ); +} + +#[test] +fn record_update() { + check( + r#" +foo1() -> Expr#record{field = undefined}. +foo2() -> Expr#record{field = ok, missing = }. +"#, + expect![[r##" + foo1() -> + Expr#record{ + field = 'undefined' + }. + + foo2() -> + Expr#record{ + field = 'ok', + missing = [missing] + }. + "##]], + ); +} + +#[test] +fn record_field() { + check( + r#" +foo() -> Expr#record.field. +"#, + expect![[r##" + foo() -> + Expr#record.field. + "##]], + ); +} + +#[test] +fn binary() { + check( + r#" +foo(<>) -> <<+1/integer-little-unit:8>>. +"#, + expect![[r#" + foo(<< + Size, + Data:Size/binary + >>) -> + << + (+ 1)/integer-little-unit:8 + >>. + "#]], + ); +} + +#[test] +fn catch() { + check( + r#" +foo() -> catch 1 + 2. +"#, + expect![[r#" + foo() -> + (catch (1 + 2)). + "#]], + ); +} + +#[test] +fn begin_block() { + check( + r#" +foo() -> begin 1, 2 end. +"#, + expect![[r#" + foo() -> + begin + 1, + 2 + end. + "#]], + ); +} + +#[test] +fn case() { + check( + r#" +foo() -> + case 1 + 2 of + X when X andalso true; X <= 100, X >= 5 -> ok; + _ -> error + end. +"#, + expect![[r#" + foo() -> + case (1 + 2) of + X when + (X andalso 'true'); + (X < 100), + (X >= 5) + -> + 'ok'; + _ -> + 'error' + end. + "#]], + ); +} + +#[test] +fn receive() { + check( + r#" +foo() -> + receive + ok when true -> ok; + _ -> error + after Timeout -> timeout + end. +"#, + expect![[r#" + foo() -> + receive + 'ok' when + 'true' + -> + 'ok'; + _ -> + 'error' + after Timeout -> + 'timeout' + end. + "#]], + ); +} + +#[test] +fn call() { + check( + r#" +foo() -> + foo(), + foo:bar(A). +"#, + expect![[r#" + foo() -> + 'foo'(), + 'foo':'bar'( + A + ). + "#]], + ); +} + +#[test] +fn capture_fun() { + check( + r#" +foo() -> + fun foo/1, + fun mod:foo/1, + fun Mod:Foo/Arity. +"#, + expect![[r#" + foo() -> + fun 'foo'/1, + fun 'mod':'foo'/1, + fun Mod:Foo/Arity. + "#]], + ); +} + +#[test] +fn if_expr() { + check( + r#" +foo() -> + if is_atom(X) -> ok; + true -> error + end. +"#, + expect![[r#" + foo() -> + if + 'is_atom'( + X + ) -> + 'ok'; + 'true' -> + 'error' + end. + "#]], + ); +} + +#[test] +fn try_expr() { + check( + r#" +foo() -> + try 1, 2 of + _ -> ok + catch + Pat when true -> ok; + error:undef:Stack -> Stack + after + ok + end. +"#, + expect![[r#" + foo() -> + try + 1, + 2 + of + _ -> + 'ok' + catch + Pat when + 'true' + -> + 'ok'; + 'error':'undef':Stack -> + Stack + after + 'ok' + end. + "#]], + ); +} + +#[test] +fn comprehensions() { + check( + r#" +foo() -> + [X || X <- List, X >= 5], + << Byte || <> <= Bytes, Byte >= 5>>, + #{KK => VV || KK := VV <- Map}. +"#, + expect![[r#" + foo() -> + [ + X + || + X <- List, + (X >= 5) + ], + << + Byte + || + << + Byte + >> <= Bytes, + (Byte >= 5) + >>, + #{ + KK => VV + || + KK := VV <- Map + }. + "#]], + ); +} + +#[test] +fn fun() { + check( + r#" +foo() -> + fun (ok) -> ok; + (error) -> error + end, + fun Named() -> Named() end. +"#, + expect![[r#" + foo() -> + fun + ('ok') -> + 'ok'; + ('error') -> + 'error' + end, + fun + Named() -> + Named() + end. + "#]], + ); +} + +#[test] +fn parens() { + check( + r#" +foo((ok), ()) -> + (ok), + (). +"#, + expect![[r#" + foo('ok', [missing]) -> + 'ok', + [missing]. + "#]], + ); +} + +#[test] +fn invalid_ann_type() { + check( + r#" +foo(A :: {}) -> A :: {}. +"#, + expect![[r#" + foo([missing]) -> + [missing]. + "#]], + ); +} + +#[test] +fn invalid_dotdotdot() { + check( + r#" +foo(...) -> .... +"#, + expect![[r#" + foo([missing]) -> + [missing]. + "#]], + ); +} + +#[test] +fn invalid_call() { + check( + r#" +foo(bar()) -> ok. +"#, + expect![[r#" + foo([missing]) -> + 'ok'. + "#]], + ); +} + +#[test] +fn invalid_catch() { + check( + r#" +foo(catch 1) -> ok. +"#, + expect![[r#" + foo([missing]) -> + 'ok'. + "#]], + ); +} + +#[test] +fn invalid_map_update() { + check( + r#" +foo(X#{}) -> ok. +"#, + expect![[r#" + foo([missing]) -> + 'ok'. + "#]], + ); +} + +#[test] +fn invalid_pipe() { + check( + r#" +foo(X | Y) -> X | Y. +"#, + expect![[r#" + foo([missing]) -> + [missing]. + "#]], + ); +} + +#[test] +fn invalid_range() { + check( + r#" +foo(X..Y) -> X..Y. +"#, + expect![[r#" + foo([missing]) -> + [missing]. + "#]], + ); +} + +#[test] +fn invalid_record_field() { + check( + r#" +foo(X#foo.bar) -> ok. +"#, + expect![[r#" + foo([missing]) -> + 'ok'. + "#]], + ); +} + +#[test] +fn invalid_record_update() { + check( + r#" +foo(X#foo{}) -> ok. +"#, + expect![[r#" + foo([missing]) -> + 'ok'. + "#]], + ); +} + +#[test] +fn invalid_remote() { + check( + r#" +foo(a:b) -> a:b. +"#, + expect![[r#" + foo([missing]) -> + [missing]. + "#]], + ); +} + +#[test] +fn invalid_fun() { + check( + r#" +foo(fun() -> ok end) -> ok. +"#, + expect![[r#" + foo([missing]) -> + 'ok'. + "#]], + ); +} + +#[test] +fn invalid_comprehension() { + check( + r#" +foo(<>, [Byte || Byte <- List]]) -> ok. +"#, + expect![[r#" + foo([missing], [missing]) -> + 'ok'. + "#]], + ); +} + +#[test] +fn invalid_block() { + check( + r#" +foo(begin foo end) -> ok. +"#, + expect![[r#" + foo([missing]) -> + 'ok'. + "#]], + ); +} + +#[test] +fn invalid_case() { + check( + r#" +foo(case X of _ -> ok end) -> ok. +"#, + expect![[r#" + foo([missing]) -> + 'ok'. + "#]], + ); +} + +#[test] +fn invalid_capture() { + check( + r#" +foo(fun erlang:self/0, fun foo/2) -> ok. +"#, + expect![[r#" + foo([missing], [missing]) -> + 'ok'. + "#]], + ); +} + +#[test] +fn invalid_fun_type() { + check( + r#" +foo(fun()) -> fun(). +"#, + expect![[r#" + foo([missing]) -> + [missing]. + "#]], + ); +} + +#[test] +fn invalid_if() { + check( + r#" +foo(if true -> ok end) -> ok. +"#, + expect![[r#" + foo([missing]) -> + 'ok'. + "#]], + ); +} + +#[test] +fn invalid_macro_string() { + check( + r#" +foo(??X) -> ??X. +"#, + expect![[r#" + foo([missing]) -> + [missing]. + "#]], + ); +} + +#[test] +fn invalid_receive() { + check( + r#" +foo(receive _ -> ok after X -> timeout end) -> ok. +"#, + expect![[r#" + foo([missing]) -> + 'ok'. + "#]], + ); +} + +#[test] +fn invalid_try() { + check( + r#" +foo(try 1 of _ -> ok catch _ -> error end) -> ok. +"#, + expect![[r#" + foo([missing]) -> + 'ok'. + "#]], + ); +} + +#[test] +fn invalid_concat() { + check( + r#" +foo("a" B "c") -> "a" B "c". +"#, + expect![[r#" + foo([missing]) -> + [missing]. + "#]], + ); +} + +#[test] +fn invalid_macro_case_clause() { + check( + r#" +foo() -> + case X of + ?MACRO(); + ok -> ok + end. +"#, + expect![[r#" + foo() -> + case X of + 'ok' -> + 'ok' + end. + "#]], + ); +} + +#[test] +fn macro_exprs() { + check( + r#" +foo(?MACRO()) -> ?MACRO(). +"#, + expect![[r#" + foo([missing]) -> + [missing]. + "#]], + ); +} + +#[test] +fn simple_type() { + check( + r#" +-type foo() :: ok. +"#, + expect![[r#" + -type foo() :: 'ok'. + "#]], + ); +} + +#[test] +fn simple_opaque() { + check( + r#" +-opaque foo() :: ok. +"#, + expect![[r#" + -opaque foo() :: 'ok'. + "#]], + ); +} + +#[test] +fn unary_op_type() { + check( + r#" +-type foo() :: -1. +"#, + expect![[r#" + -type foo() :: (- 1). + "#]], + ); +} + +#[test] +fn binary_op_type() { + check( + r#" +-type foo() :: 1 + 1. +"#, + expect![[r#" + -type foo() :: (1 + 1). + "#]], + ); +} + +#[test] +fn ann_type() { + check( + r#" +-type foo() :: A :: any(). +"#, + expect![[r#" + -type foo() :: (A :: 'any'()). + "#]], + ); +} + +#[test] +fn list_type() { + check( + r#" +-type foo() :: [foo]. +-type bar() :: [bar, ...]. +"#, + expect![[r#" + -type foo() :: ['foo']. + + -type bar() :: ['bar', ...]. + "#]], + ); +} + +#[test] +fn tuple_type() { + check( + r#" +-type foo() :: {a, b, c}. +"#, + expect![[r#" + -type foo() :: { + 'a', + 'b', + 'c' + }. + "#]], + ); +} + +#[test] +fn range_type() { + check( + r#" +-type foo() :: 1..100. +"#, + expect![[r#" + -type foo() :: (1..100). + "#]], + ); +} + +#[test] +fn map_type() { + check( + r#" +-type foo() :: #{a => b, c := d}. +"#, + expect![[r##" + -type foo() :: #{ + 'a' => 'b', + 'c' := 'd' + }. + "##]], + ); +} + +#[test] +fn fun_type() { + check( + r#" +-type foo1() :: fun(). +-type foo2() :: fun(() -> ok). +-type foo3() :: fun((a, b) -> ok). +-type foo4() :: fun((...) -> ok). +"#, + expect![[r#" + -type foo1() :: fun(). + + -type foo2() :: fun(() -> 'ok'). + + -type foo3() :: fun(('a', 'b') -> 'ok'). + + -type foo4() :: fun((...) -> 'ok'). + "#]], + ); +} + +#[test] +fn union_type() { + check( + r#" +-type foo1() :: a | b. +-type foo2() :: a | b | c. +-type foo3() :: (a | b) | c. +"#, + expect![[r#" + -type foo1() :: ( + 'a' | + 'b' + ). + + -type foo2() :: ( + 'a' | + 'b' | + 'c' + ). + + -type foo3() :: ( + ( + 'a' | + 'b' + ) | + 'c' + ). + "#]], + ); +} + +#[test] +fn var_type() { + check( + r#" +-type foo(A) :: A. +"#, + expect![[r#" + -type foo(A) :: A. + "#]], + ); +} + +#[test] +fn call_type() { + check( + r#" +-type local(A) :: local(A | integer()). +-type remote(A) :: module:remote(A | integer()). +"#, + expect![[r#" + -type local(A) :: 'local'( + ( + A | + 'integer'() + ) + ). + + -type remote(A) :: 'module':'remote'( + ( + A | + 'integer'() + ) + ). + "#]], + ); +} + +#[test] +fn record_type() { + check( + r#" +-type foo1() :: #record{}. +-type foo2(B) :: #record{a :: integer(), b :: B}. +-type foo3() :: #record{a ::}. +"#, + expect![[r##" + -type foo1() :: #record{}. + + -type foo2(B) :: #record{ + a :: 'integer'(), + b :: B + }. + + -type foo3() :: #record{ + a :: [missing] + }. + "##]], + ); +} + +#[test] +fn invalid_type() { + check( + r#" +-type foo() :: catch 1. +"#, + expect![[r#" + -type foo() :: [missing]. + "#]], + ); +} + +#[test] +fn simple_spec() { + check( + r#" +-spec foo() -> ok. +"#, + expect![[r#" + -spec foo + () -> 'ok'. + "#]], + ); +} + +#[test] +fn simple_callback() { + check( + r#" +-callback foo() -> ok. +"#, + expect![[r#" + -callback foo + () -> 'ok'. + "#]], + ); +} + +#[test] +fn multi_sig_spec() { + check( + r#" +-spec foo(atom()) -> atom(); + (integer()) -> integer(). +"#, + expect![[r#" + -spec foo + ('atom'()) -> 'atom'(); + ('integer'()) -> 'integer'(). + "#]], + ); +} + +#[test] +fn ann_var_spec() { + check( + r#" +-spec foo(A :: any()) -> ok. +"#, + expect![[r#" + -spec foo + ((A :: 'any'())) -> 'ok'. + "#]], + ); +} + +#[test] +fn guarded_spec() { + check( + r#" +-spec foo(A) -> A + when A :: any(). +"#, + expect![[r#" + -spec foo + (A) -> A + when A :: 'any'(). + "#]], + ); +} + +#[test] +fn record_definition() { + check( + r#" +-record(foo, {}). +-record(foo, {field}). +-record(foo, {field = value}). +-record(foo, {field :: type}). +-record(foo, {field = value :: type}). +"#, + expect![[r#" + -record(foo, { + }). + + -record(foo, { + field + }). + + -record(foo, { + field = 'value' + }). + + -record(foo, { + field :: 'type' + }). + + -record(foo, { + field = 'value' :: 'type' + }). + "#]], + ); +} + +#[test] +fn simple_term() { + check( + r#" +-foo(ok). +-missing_value(). +"#, + expect![[r#" + -foo('ok'). + + -missing_value([missing]). + "#]], + ); +} + +#[test] +fn tuple_term() { + check( + r#" +-foo({1, 2, ok, "abc"}). +"#, + expect![[r#" + -foo({ + 1, + 2, + 'ok', + "abc" + }). + "#]], + ); +} + +#[test] +fn list_term() { + check( + r#" +-foo([]). +-bar([1, 2]). +-baz([1 | 2]). +"#, + expect![[r#" + -foo([]). + + -bar([ + 1, + 2 + ]). + + -baz([ + 1 + | 2 + ]). + "#]], + ); +} + +#[test] +fn map_term() { + check( + r#" +-foo(#{1 => 2}). +"#, + expect![[r##" + -foo(#{ + 1 => 2 + }). + "##]], + ); +} + +#[test] +fn fun_term() { + check( + r#" +-foo(fun erlang:is_integer/1). +"#, + expect![[r#" + -foo(fun erlang:is_integer/1). + "#]], + ); +} + +#[test] +fn unary_op_term() { + check( + r#" +-foo(-(1)). +-foo(-(1.5)). +-foo(-$a). +-foo(+1). +-foo(+1.5). +-foo(+$a). +-foo(-atom). +"#, + expect![[r#" + -foo(-1). + + -foo(-1.5). + + -foo(-97). + + -foo(1). + + -foo(1.5). + + -foo($a). + + -foo([missing]). + "#]], + ); +} + +#[test] +fn binary_op_term() { + check( + r#" +-foo(foo/1). +-compile({inline, [foo/1]}). +-compile({a/a, 1/1}). +"#, + expect![[r#" + -foo({ + 'foo', + 1 + }). + + -compile({ + 'inline', + [ + { + 'foo', + 1 + } + ] + }). + + -compile({ + [missing], + [missing] + }). + "#]], + ); +} + +#[test] +fn binary_term() { + check( + r#" +-foo(<<"abc">>). +-bar(<<"abc", "def">>). +-baz(<<$a, $b, $c>>). +-foobar(<<1, 2, 3, -1>>). +"#, + expect![[r#" + -foo(<<"abc"/utf8>>). + + -bar(<<"abcdef"/utf8>>). + + -baz(<<"abc"/utf8>>). + + -foobar(<<1, 2, 3, 255>>). + "#]], + ); +} + +#[test] +fn expand_macro_function_clause() { + check( + r#" +-define(CLAUSE, foo(_) -> ok). + +foo(1) -> 1; +?CLAUSE. +"#, + expect![[r#" + foo(1) -> + 1; + foo(_) -> + 'ok'. + "#]], + ); +} + +#[test] +fn expand_macro_expr() { + check( + r#" +-define(EXPR, 1 + 2). + +foo() -> ?EXPR. +"#, + expect![[r#" + foo() -> + (1 + 2). + "#]], + ); +} + +#[test] +fn expand_macro_var_in_expr() { + check( + r#" +-define(EXPR(X), 1 + X). + +foo() -> ?EXPR(2). +"#, + expect![[r#" + foo() -> + (1 + 2). + "#]], + ); +} + +#[test] +fn expand_macro_function() { + check( + r#" +-define(NAME, name). + +foo() -> ?NAME(2). +"#, + expect![[r#" + foo() -> + 'name'( + 2 + ). + "#]], + ); +} + +#[test] +fn expand_macro_remote_function() { + check( + r#" +-define(NAME, module:name). + +foo() -> ?NAME(2). +"#, + expect![[r#" + foo() -> + 'module':'name'( + 2 + ). + "#]], + ); +} + +#[test] +fn expand_macro_pat() { + check( + r#" +-define(PAT, [_]). + +foo(?PAT) -> ok. +"#, + expect![[r#" + foo([ + _ + ]) -> + 'ok'. + "#]], + ); +} + +#[test] +fn expand_macro_var_in_pat() { + check( + r#" +-define(PAT(X), [X]). + +foo(?PAT(_)) -> ok. +"#, + expect![[r#" + foo([ + _ + ]) -> + 'ok'. + "#]], + ); +} + +#[test] +fn expand_macro_type() { + check( + r#" +-define(TY, a | b). + +-type foo() :: ?TY. +"#, + expect![[r#" + -type foo() :: ( + 'a' | + 'b' + ). + "#]], + ); +} + +#[test] +fn expand_macro_type_call() { + check( + r#" +-define(NAME, name). + +-type foo() :: ?NAME(). +"#, + expect![[r#" + -type foo() :: 'name'(). + "#]], + ); +} + +#[test] +fn expand_macro_remote_type() { + check( + r#" +-define(NAME, module:name). + +-type foo() :: ?NAME(). +"#, + expect![[r#" + -type foo() :: 'module':'name'(). + "#]], + ); +} + +#[test] +fn expand_macro_var_in_type() { + check( + r#" +-define(TY(X), a | X). + +-type foo() :: ?TY(b). +"#, + expect![[r#" + -type foo() :: ( + 'a' | + 'b' + ). + "#]], + ); +} + +#[test] +fn expand_macro_term() { + check( + r#" +-define(TERM, [0, 1]). + +-foo(?TERM). +"#, + expect![[r#" + -foo([ + 0, + 1 + ]). + "#]], + ); +} + +#[test] +fn expand_macro_var_in_term() { + check( + r#" +-define(TERM(X), [0, X]). + +-foo(?TERM(1)). +"#, + expect![[r#" + -foo([ + 0, + 1 + ]). + "#]], + ); +} + +#[test] +fn expand_macro_cr_clause() { + check( + r#" +-define(CLAUSE(Pat, Expr), Pat -> Expr). + +foo() -> + case bar() of + ?CLAUSE(ok, ok); + ?CLAUSE(_, error) + end. +"#, + expect![[r#" + foo() -> + case 'bar'() of + 'ok' -> + 'ok'; + _ -> + 'error' + end. + "#]], + ); +} + +#[test] +fn expand_macro_arity() { + check( + r#" +-define(ARITY(X), X). + +foo() -> + fun local/?ARITY(1), + fun remote:function/?ARITY(2). +"#, + expect![[r#" + foo() -> + fun 'local'/1, + fun 'remote':'function'/2. + "#]], + ); +} + +#[test] +fn expand_macro_name() { + check( + r#" +-define(NAME, name). + +foo() -> + #?NAME{?NAME = ?NAME}. +"#, + expect![[r#" + foo() -> + #name{ + name = 'name' + }. + "#]], + ); +} + +#[test] +fn expand_nested_macro() { + check( + r#" +-define(M1(A, B), {m1, ?M2(A, ?M3(B))}). +-define(M2(B, A), {m2, B, ?M3(A)}). +-define(M3(A), {m3, A}). + +foo() -> + ?M1(1, 2), + ?M1(A, B). +"#, + expect![[r#" + foo() -> + { + 'm1', + { + 'm2', + 1, + { + 'm3', + { + 'm3', + 2 + } + } + } + }, + { + 'm1', + { + 'm2', + A, + { + 'm3', + { + 'm3', + B + } + } + } + }. + "#]], + ); +} + +#[test] +fn expand_built_in_function_name() { + check( + r#" +foo(?FUNCTION_NAME) -> ?FUNCTION_NAME. +"#, + expect![[r#" + foo('foo') -> + 'foo'. + "#]], + ); + + check( + r#" +foo() -> ?FUNCTION_NAME(). +"#, + expect![[r#" + foo() -> + 'foo'(). + "#]], + ); +} + +#[test] +fn expand_built_in_function_arity() { + check( + r#" +foo(?FUNCTION_ARITY) -> ?FUNCTION_ARITY. +"#, + expect![[r#" + foo(1) -> + 1. + "#]], + ); +} + +#[test] +fn expand_built_in_line() { + check( + r#" +-type foo() :: ?LINE. + +foo(?LINE) -> ?LINE. +"#, + expect![[r#" + -type foo() :: 0. + + foo(0) -> + 0. + "#]], + ); +} + +#[test] +fn expand_built_in_machine() { + check( + r#" +-type foo() :: ?MACHINE. + +foo(?MACHINE) -> ?MACHINE. +"#, + expect![[r#" + -type foo() :: 'ELP'. + + foo('ELP') -> + 'ELP'. + "#]], + ); +} + +#[test] +fn expand_built_in_otp_release() { + check( + r#" +-type foo() :: ?OTP_RELEASE. + +foo(?OTP_RELEASE) -> ?OTP_RELEASE. +"#, + expect![[r#" + -type foo() :: 2000. + + foo(2000) -> + 2000. + "#]], + ); +} + +#[test] +fn expand_built_in_module_no_attribute() { + check( + r#" +-type foo() :: ?MODULE. + +foo(?MODULE) -> ?MODULE. +"#, + expect![[r#" + -type foo() :: [missing]. + + foo([missing]) -> + [missing]. + "#]], + ); +} + +#[test] +fn expand_built_in_module() { + check( + r#" +-module(foobar). + +-type foo() :: ?MODULE. + +foo(?MODULE) -> ?MODULE. +"#, + expect![[r#" + -type foo() :: 'foobar'. + + foo('foobar') -> + 'foobar'. + "#]], + ); +} + +#[test] +fn expand_built_in_module_string() { + check( + r#" +-module(foobar). + +-type foo() :: ?MODULE_STRING. + +foo(?MODULE_STRING) -> ?MODULE_STRING. +"#, + expect![[r#" + -type foo() :: "foobar". + + foo("foobar") -> + "foobar". + "#]], + ); +} + +#[test] +fn expand_built_in_file() { + check( + r#" +-module(foobar). + +-type foo() :: ?FILE. + +foo(?FILE) -> ?FILE. +"#, + expect![[r#" + -type foo() :: "foobar.erl". + + foo("foobar.erl") -> + "foobar.erl". + "#]], + ); +} + +#[test] +fn expand_recursive_macro() { + check( + r#" +-define(FOO, ?FOO). + +foo(?FOO) -> ?FOO. +"#, + expect![[r#" + foo([missing]) -> + [missing]. + "#]], + ); +} + +#[test] +fn expand_mutually_recursive_macro() { + check( + r#" +-define(FOO, ?BAR). +-define(BAR, ?FOO). + +foo(?FOO) -> ?BAR. +"#, + expect![[r#" + foo([missing]) -> + [missing]. + "#]], + ); +} + +#[test] +fn expand_aparently_argument_recursive_macro() { + check( + r#" +-define(FOO(X), X). + +foo(?FOO(?FOO(1))) -> ?FOO(?FOO(1)). +"#, + expect![[r#" + foo(1) -> + 1. + "#]], + ); + + check( + r#" +-define(FOO(X), ?BAR(X)). +-define(BAR(X), X). + +foo(?FOO(?BAR(?FOO(?BAR(1))))) -> ?FOO(?BAR(?FOO(?BAR(1)))). +"#, + expect![[r#" + foo(1) -> + 1. + "#]], + ); +} + +#[test] +fn maybe_simple() { + check( + r#" +foo() -> +maybe + {ok, A} ?= a(), + true = A >= 0, + {ok, B} ?= b(), + A + B +end."#, + expect![[r#" + foo() -> + maybe + { + 'ok', + A + } ?= 'a'(), + 'true' = (A >= 0), + { + 'ok', + B + } ?= 'b'(), + (A + B) + end. + "#]], + ); +} + +#[test] +fn maybe_else() { + check( + r#" +foo() -> +maybe + {ok, A} ?= a(), + true = A >= 0, + A +else + error -> error; + Other when Other == 0 -> error +end."#, + expect![[r#" + foo() -> + maybe + { + 'ok', + A + } ?= 'a'(), + 'true' = (A >= 0), + A + else + 'error' -> + 'error'; + Other when + (Other == 0) + -> + 'error' + end. + "#]], + ); +} diff --git a/crates/hir/src/body/tree_print.rs b/crates/hir/src/body/tree_print.rs new file mode 100644 index 0000000000..8fae2fbd41 --- /dev/null +++ b/crates/hir/src/body/tree_print.rs @@ -0,0 +1,2634 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! Print a representation of the HIR AST for a given entry point into +//! the Body. + +use std::fmt; +use std::fmt::Write as _; +use std::str; + +use crate::db::MinInternDatabase; +use crate::expr::MaybeExpr; +use crate::AnyAttribute; +use crate::AttributeBody; +use crate::BinarySeg; +use crate::Body; +use crate::CRClause; +use crate::CallTarget; +use crate::CatchClause; +use crate::Clause; +use crate::ComprehensionBuilder; +use crate::ComprehensionExpr; +use crate::Expr; +use crate::ExprId; +use crate::FunType; +use crate::FunctionBody; +use crate::ListType; +use crate::Literal; +use crate::Pat; +use crate::PatId; +use crate::Term; +use crate::TermId; +use crate::TypeAlias; +use crate::TypeExpr; +use crate::TypeExprId; + +pub(crate) fn print_expr(db: &dyn MinInternDatabase, body: &Body, expr: ExprId) -> String { + let mut printer = Printer::new(db, body); + printer.print_expr(&body[expr]); + printer.to_string() +} + +pub(crate) fn print_pat(db: &dyn MinInternDatabase, body: &Body, pat: PatId) -> String { + let mut printer = Printer::new(db, body); + printer.print_pat(&body[pat]); + printer.to_string() +} + +pub(crate) fn print_type(db: &dyn MinInternDatabase, body: &Body, ty: TypeExprId) -> String { + let mut printer = Printer::new(db, body); + printer.print_type(&body[ty]); + printer.to_string() +} + +pub(crate) fn print_term(db: &dyn MinInternDatabase, body: &Body, term: TermId) -> String { + let mut printer = Printer::new(db, body); + printer.print_term(&body[term]); + printer.to_string() +} + +pub(crate) fn print_attribute( + db: &dyn MinInternDatabase, + body: &AttributeBody, + form: &AnyAttribute, +) -> String { + let mut printer = Printer::new(db, &body.body); + + match form { + AnyAttribute::CompileOption(_) => writeln!(printer, "-compile(").unwrap(), + AnyAttribute::Attribute(attr) => writeln!(printer, "-{}(", attr.name).unwrap(), + } + + printer.indent(); + printer.print_term(&printer.body[body.value]); + writeln!(printer, "").ok(); + printer.dedent(); + write!(printer, ").").ok(); + + printer.to_string() +} + +pub(crate) fn print_function(db: &dyn MinInternDatabase, body: &FunctionBody) -> String { + let mut printer = Printer::new(db, &body.body); + + let mut sep = ""; + for (_idx, clause) in body.clauses.iter() { + write!(printer, "{}", sep).ok(); + sep = ";\n"; + printer.print_clause(clause); + } + write!(printer, ".").ok(); + + printer.to_string() +} + +pub(crate) fn print_type_alias( + db: &dyn MinInternDatabase, + body: &crate::TypeBody, + form: &crate::TypeAlias, +) -> String { + let mut printer = Printer::new(db, &body.body); + + match form { + TypeAlias::Regular { .. } => write!(printer, "-type ").unwrap(), + TypeAlias::Opaque { .. } => write!(printer, "-opaque ").unwrap(), + } + + printer.print_args(form.name().name(), &body.vars, |this, &var| { + write!(this, "{}", db.lookup_var(var)).ok(); + }); + write!(printer, " :: ").ok(); + printer.print_type(&printer.body[body.ty]); + write!(printer, ".").ok(); + + printer.to_string() +} + +struct Printer<'a> { + db: &'a dyn MinInternDatabase, + body: &'a Body, + buf: String, + indent_level: usize, + needs_indent: bool, +} + +impl<'a> Printer<'a> { + fn new(db: &'a dyn MinInternDatabase, body: &'a Body) -> Self { + Printer { + db, + body, + buf: String::new(), + indent_level: 0, + needs_indent: true, + } + } + + fn to_string(mut self) -> String { + self.buf.truncate(self.buf.trim_end().len()); + self.buf.push('\n'); + self.buf + } + + fn indent(&mut self) { + self.indent_level += 1; + } + + fn dedent(&mut self) { + self.indent_level -= 1; + } + + fn print_expr(&mut self, expr: &Expr) { + match expr { + Expr::Missing => { + write!(self, "Expr::Missing").ok(); + } + Expr::Literal(lit) => { + write!(self, "Literal(").ok(); + self.print_literal(lit); + write!(self, ")").ok(); + } + Expr::Var(var) => { + write!(self, "Expr::Var({})", self.db.lookup_var(*var)).ok(); + } + Expr::Match { lhs, rhs } => { + self.print_herald("Expr::Match", &mut |this| { + this.print_labelled("lhs", true, &mut |this| { + this.print_pat(&this.body[*lhs]); + }); + this.print_labelled("rhs", true, &mut |this| { + this.print_expr(&this.body[*rhs]); + }); + }); + } + Expr::Tuple { exprs } => { + self.print_herald("Expr::Tuple", &mut |this| { + exprs.iter().for_each(|expr_id| { + this.print_expr(&this.body[*expr_id]); + writeln!(this, ",").ok(); + }); + }); + } + Expr::List { exprs, tail } => { + self.print_herald("Expr::List", &mut |this| { + this.print_labelled("exprs", false, &mut |this| { + exprs.iter().for_each(|expr_id| { + this.print_expr(&this.body[*expr_id]); + writeln!(this, ",").ok(); + }); + }); + + this.print_labelled("tail", false, &mut |this| { + if let Some(expr_id) = tail { + this.print_expr(&this.body[*expr_id]); + writeln!(this, ",").ok(); + } + }); + }); + } + Expr::Binary { segs } => { + self.print_herald("Expr::Binary", &mut |this| { + segs.iter().for_each(|seg| { + this.print_bin_segment(seg, |this, expr| { + this.print_expr(&this.body[expr]); + }); + writeln!(this, "").ok(); + }); + }); + } + Expr::UnaryOp { expr, op } => { + self.print_herald("Expr::UnaryOp", &mut |this| { + this.print_expr(&this.body[*expr]); + writeln!(this, "").ok(); + writeln!(this, "{:?},", op).ok(); + }); + } + Expr::BinaryOp { lhs, rhs, op } => { + self.print_herald("Expr::BinaryOp", &mut |this| { + this.print_labelled("lhs", true, &mut |this| this.print_expr(&this.body[*lhs])); + this.print_labelled("rhs", true, &mut |this| this.print_expr(&this.body[*rhs])); + this.print_labelled("op", true, &mut |this| { + write!(this, "{:?},", op).ok(); + }); + }); + } + Expr::Record { name, fields } => { + self.print_herald("Expr::Record", &mut |this| { + writeln!(this, "name: Atom('{}')", this.db.lookup_atom(*name)).ok(); + this.print_labelled("fields", false, &mut |this| { + fields.iter().for_each(|(name, expr_id)| { + writeln!(this, "Atom('{}'):", this.db.lookup_atom(*name)).ok(); + this.indent(); + this.print_expr(&this.body[*expr_id]); + writeln!(this, ",").ok(); + this.dedent(); + }); + }); + }); + } + Expr::RecordUpdate { expr, name, fields } => { + self.print_herald("Expr::RecordUpdate", &mut |this| { + this.print_labelled("expr", true, &mut |this| { + this.print_expr(&this.body[*expr]) + }); + writeln!(this, "name: Atom('{}')", this.db.lookup_atom(*name)).ok(); + this.print_labelled("fields", false, &mut |this| { + fields.iter().for_each(|(name, expr_id)| { + writeln!(this, "Atom('{}'):", this.db.lookup_atom(*name)).ok(); + this.indent(); + this.print_expr(&this.body[*expr_id]); + writeln!(this, ",").ok(); + this.dedent(); + }); + }); + }); + } + Expr::RecordIndex { name, field } => { + self.print_herald("Expr::RecordIndex", &mut |this| { + writeln!(this, "name: Atom('{}')", this.db.lookup_atom(*name)).ok(); + writeln!(this, "field: Atom('{}')", this.db.lookup_atom(*field)).ok(); + }); + } + Expr::RecordField { expr, name, field } => { + self.print_herald("Expr::RecordField", &mut |this| { + this.print_labelled("expr", true, &mut |this| { + this.print_expr(&this.body[*expr]) + }); + writeln!(this, "name: Atom('{}')", this.db.lookup_atom(*name)).ok(); + writeln!(this, "field: Atom('{}')", this.db.lookup_atom(*field)).ok(); + }); + } + Expr::Map { fields } => { + self.print_herald("Expr::Map", &mut |this| { + fields.iter().for_each(|(name, value)| { + this.print_expr(&this.body[*name]); + writeln!(this, ",").ok(); + this.print_expr(&this.body[*value]); + writeln!(this, ",").ok(); + }); + }); + } + Expr::MapUpdate { expr, fields } => { + self.print_herald("Expr::MapUpdate", &mut |this| { + this.print_labelled("expr", true, &mut |this| { + this.print_expr(&this.body[*expr]) + }); + this.print_labelled("fields", false, &mut |this| { + fields.iter().for_each(|(name, op, value)| { + this.print_expr(&this.body[*name]); + this.indent(); + writeln!(this, "").ok(); + writeln!(this, "{:?}", op).ok(); + this.print_expr(&this.body[*value]); + writeln!(this, ",").ok(); + this.dedent(); + }); + }); + }); + } + Expr::Catch { expr } => { + self.print_herald("Expr::Catch", &mut |this| { + this.print_labelled("expr", true, &mut |this| { + this.print_expr(&this.body[*expr]) + }); + }); + } + Expr::MacroCall { + expansion: _, + args: _, + } => todo!(), + Expr::Call { target, args } => { + self.print_herald("Expr::Call", &mut |this| { + this.print_labelled("target", true, &mut |this1| { + this1.print_call_target(target, |this, expr| { + this.print_expr(&this.body[*expr]) + }); + }); + this.print_labelled("args", false, &mut |this| { + args.iter().for_each(|expr_id| { + this.print_expr(&this.body[*expr_id]); + writeln!(this, ",").ok(); + }); + }); + }); + } + Expr::Comprehension { builder, exprs } => { + self.print_herald("Expr::Comprehension", &mut |this| { + this.print_labelled("builder", true, &mut |this| { + match builder { + ComprehensionBuilder::List(expr) => { + this.print_herald("ComprehensionBuilder::List", &mut |this| { + this.print_expr(&this.body[*expr]); + writeln!(this, "").ok(); + }); + } + ComprehensionBuilder::Binary(expr) => { + this.print_herald("ComprehensionBuilder::Binary", &mut |this| { + this.print_expr(&this.body[*expr]); + writeln!(this, "").ok(); + }); + } + ComprehensionBuilder::Map(expr1, expr2) => { + this.print_herald("ComprehensionBuilder::Map", &mut |this| { + this.print_expr(&this.body[*expr1]); + writeln!(this, "").ok(); + writeln!(this, "=>").ok(); + this.print_expr(&this.body[*expr2]); + writeln!(this, "").ok(); + }); + } + }; + }); + + this.print_labelled("exprs", false, &mut |this| { + exprs.iter().for_each(|expr| { + match expr { + ComprehensionExpr::BinGenerator { pat, expr } => { + this.print_herald( + "ComprehensionExpr::BinGenerator", + &mut |this| { + this.print_pat(&this.body[*pat]); + writeln!(this, "").ok(); + this.print_expr(&this.body[*expr]); + writeln!(this, "").ok(); + }, + ); + } + ComprehensionExpr::ListGenerator { pat, expr } => { + this.print_herald( + "ComprehensionExpr::ListGenerator", + &mut |this| { + this.print_pat(&this.body[*pat]); + writeln!(this, "").ok(); + this.print_expr(&this.body[*expr]); + writeln!(this, "").ok(); + }, + ); + } + ComprehensionExpr::Expr(expr) => { + this.print_herald("ComprehensionExpr::Expr", &mut |this| { + this.print_expr(&this.body[*expr]); + writeln!(this, "").ok(); + }); + } + ComprehensionExpr::MapGenerator { key, value, expr } => { + this.print_herald( + "ComprehensionExpr::MapGenerator", + &mut |this| { + this.print_pat(&this.body[*key]); + writeln!(this, " :=").ok(); + this.print_pat(&this.body[*value]); + writeln!(this, " <-").ok(); + this.print_expr(&this.body[*expr]); + writeln!(this, "").ok(); + }, + ); + } + }; + writeln!(this, ",").ok(); + }); + }); + }); + } + Expr::Block { exprs } => { + self.print_herald("Expr::Block", &mut |this| { + exprs.iter().for_each(|expr_id| { + this.print_expr(&this.body[*expr_id]); + writeln!(this, ",").ok(); + }); + }); + } + Expr::If { clauses } => { + self.print_herald("Expr::If", &mut |this| { + clauses.iter().for_each(|clause| { + this.print_herald("IfClause", &mut |this| { + this.print_labelled("guards", false, &mut |this| { + this.print_guards(&clause.guards) + }); + this.print_labelled("exprs", false, &mut |this| { + this.print_exprs(&clause.exprs) + }); + }); + writeln!(this, "").ok(); + }); + }); + } + Expr::Case { expr, clauses } => { + self.print_herald("Expr::Case", &mut |this| { + this.print_labelled("expr", true, &mut |this| { + this.print_expr(&this.body[*expr]) + }); + this.print_labelled("clauses", false, &mut |this| { + clauses.iter().for_each(|clause| { + this.print_cr_clause(clause); + }); + }); + }); + } + Expr::Receive { clauses, after } => { + self.print_herald("Expr::Receive", &mut |this| { + this.print_labelled("clauses", false, &mut |this| { + clauses.iter().for_each(|clause| { + this.print_cr_clause(clause); + }); + }); + + this.print_labelled("after", false, &mut |this| { + if let Some(after) = after { + this.print_herald("ReceiveAfter", &mut |this| { + this.print_labelled("timeout", true, &mut |this| { + this.print_expr(&this.body[after.timeout]) + }); + + this.print_labelled("exprs", false, &mut |this| { + after.exprs.iter().for_each(|expr_id| { + this.print_expr(&this.body[*expr_id]); + writeln!(this, ",").ok(); + }); + }); + }); + } + }); + }); + } + Expr::Try { + exprs, + of_clauses, + catch_clauses, + after, + } => { + self.print_herald("Expr::Try", &mut |this| { + this.print_labelled("exprs", false, &mut |this| { + exprs.iter().for_each(|expr_id| { + this.print_expr(&this.body[*expr_id]); + writeln!(this, ",").ok(); + }); + }); + + this.print_labelled("of_clauses", false, &mut |this| { + of_clauses.iter().for_each(|clause| { + this.print_cr_clause(clause); + }); + }); + this.print_labelled("catch_clauses", false, &mut |this| { + catch_clauses.iter().for_each(|clause| { + this.print_catch_clause(clause); + writeln!(this, ",").ok(); + }); + }); + + this.print_labelled("after", false, &mut |this| { + after.iter().for_each(|expr_id| { + this.print_expr(&this.body[*expr_id]); + writeln!(this, ",").ok(); + }); + }); + }); + } + Expr::CaptureFun { target, arity } => { + self.print_herald("Expr::CaptureFun", &mut |this| { + this.print_labelled("target", true, &mut |this1| { + this1.print_call_target(target, |this, expr| { + this.print_expr(&this.body[*expr]) + }); + }); + + this.print_labelled("arity", true, &mut |this| { + this.print_expr(&this.body[*arity]) + }); + }); + } + Expr::Closure { clauses, name } => { + self.print_herald("Expr::Closure", &mut |this| { + this.print_labelled("clauses", false, &mut |this| { + clauses.iter().for_each(|clause| { + this.print_clause(clause); + writeln!(this, ",").ok(); + }); + }); + this.print_labelled("name", false, &mut |this| { + if let Some(name) = name { + this.print_pat(&this.body[*name]); + writeln!(this, "").ok(); + } + }); + }); + } + Expr::Maybe { + exprs, + else_clauses, + } => { + self.print_herald("Expr::Maybe", &mut |this| { + this.print_labelled("exprs", false, &mut |this| { + exprs.iter().for_each(|expr| { + this.print_maybe_expr(expr); + writeln!(this, ",").ok(); + }); + }); + this.print_labelled("else_clauses", false, &mut |this| { + else_clauses.iter().for_each(|clause| { + this.print_cr_clause(clause); + }); + }); + }); + } + } + } + + fn print_pat(&mut self, pat: &Pat) { + match pat { + Pat::Missing => { + write!(self, "Pat::Missing").ok(); + } + Pat::Literal(lit) => { + write!(self, "Literal(").ok(); + self.print_literal(lit); + write!(self, ")").ok(); + } + Pat::Var(var) => { + write!(self, "Pat::Var({})", self.db.lookup_var(*var)).ok(); + } + Pat::Match { lhs, rhs } => { + self.print_herald("Pat::Match", &mut |this| { + this.print_labelled("lhs", true, &mut |this| this.print_pat(&this.body[*lhs])); + this.print_labelled("rhs", true, &mut |this| this.print_pat(&this.body[*rhs])); + }); + } + Pat::Tuple { pats } => { + self.print_herald("Pat::Tuple", &mut |this| { + pats.iter().for_each(|pat_id| { + this.print_pat(&this.body[*pat_id]); + writeln!(this, ",").ok(); + }); + }); + } + Pat::List { pats, tail } => { + self.print_herald("Pat::List", &mut |this| { + this.print_labelled("exprs", false, &mut |this| { + pats.iter().for_each(|pat_id| { + this.print_pat(&this.body[*pat_id]); + writeln!(this, ",").ok(); + }); + }); + + this.print_labelled("tail", false, &mut |this| { + if let Some(pat_id) = tail { + this.print_pat(&this.body[*pat_id]); + writeln!(this, ",").ok(); + } + }); + }); + } + Pat::Binary { segs } => { + self.print_herald("Pat::Binary", &mut |this| { + segs.iter().for_each(|seg| { + this.print_bin_segment(seg, |this, pat| this.print_pat(&this.body[pat])); + writeln!(this, "").ok(); + }); + }); + } + Pat::UnaryOp { pat, op } => { + self.print_herald("Pat::UnaryOp", &mut |this| { + this.print_pat(&this.body[*pat]); + writeln!(this, "").ok(); + writeln!(this, "{:?},", op).ok(); + }); + } + Pat::BinaryOp { lhs, rhs, op } => { + self.print_herald("Pat::BinaryOp", &mut |this| { + this.print_labelled("lhs", true, &mut |this| this.print_pat(&this.body[*lhs])); + this.print_labelled("rhs", true, &mut |this| this.print_pat(&this.body[*rhs])); + this.print_labelled("op", true, &mut |this| { + write!(this, "{:?},", op).ok(); + }); + }); + } + Pat::Record { name, fields } => { + self.print_herald("Pat::Record", &mut |this| { + writeln!(this, "name: Atom('{}')", this.db.lookup_atom(*name)).ok(); + this.print_labelled("fields", false, &mut |this| { + fields.iter().for_each(|(name, pat_id)| { + writeln!(this, "Atom('{}'):", this.db.lookup_atom(*name)).ok(); + this.indent(); + this.print_pat(&this.body[*pat_id]); + writeln!(this, ",").ok(); + this.dedent(); + }); + }); + }); + } + Pat::RecordIndex { name, field } => { + self.print_herald("Pat::RecordIndex", &mut |this| { + writeln!(this, "name: Atom('{}')", this.db.lookup_atom(*name)).ok(); + writeln!(this, "field: Atom('{}')", this.db.lookup_atom(*field)).ok(); + }); + } + Pat::Map { fields } => { + self.print_herald("Pat::Map", &mut |this| { + fields.iter().for_each(|(name, value)| { + this.print_expr(&this.body[*name]); + writeln!(this, ",").ok(); + this.print_pat(&this.body[*value]); + writeln!(this, ",").ok(); + }); + }); + } + Pat::MacroCall { + expansion: _, + args: _, + } => todo!(), + } + } + + fn print_term(&mut self, term: &Term) { + match term { + Term::Missing => { + write!(self, "Term::Missing").ok(); + } + Term::Literal(lit) => { + write!(self, "Literal(").ok(); + self.print_literal(lit); + write!(self, ")").ok(); + } + Term::Binary(bin) => { + self.print_herald("Term::Binary", &mut |this| { + if let Ok(str) = str::from_utf8(bin) { + writeln!(this, "<<{:?}/utf8>>", str).ok(); + } else { + write!(this, "<<").ok(); + let mut sep = ""; + for byte in bin { + write!(this, "{}{}", sep, byte).ok(); + sep = ", "; + } + writeln!(this, ">>").ok(); + } + }); + } + Term::Tuple { exprs } => { + self.print_herald("Term::Tuple", &mut |this| { + exprs.iter().for_each(|term_id| { + this.print_term(&this.body[*term_id]); + writeln!(this, ",").ok(); + }); + }); + } + Term::List { exprs, tail } => { + self.print_herald("Term::List", &mut |this| { + this.print_labelled("exprs", false, &mut |this| { + exprs.iter().for_each(|term_id| { + this.print_term(&this.body[*term_id]); + writeln!(this, ",").ok(); + }); + }); + + this.print_labelled("tail", false, &mut |this| { + if let Some(term_id) = tail { + this.print_term(&this.body[*term_id]); + writeln!(this, ",").ok(); + } + }); + }); + } + Term::Map { fields } => { + self.print_herald("Term::Map", &mut |this| { + fields.iter().for_each(|(name, value)| { + this.print_term(&this.body[*name]); + write!(this, " => ").ok(); + this.print_term(&this.body[*value]); + writeln!(this, ",").ok(); + }); + }); + } + Term::CaptureFun { + module, + name, + arity, + } => { + self.print_herald("Term::CaptureFun", &mut |this| { + write!( + this, + "fun {}:{}/{}", + this.db.lookup_atom(*module), + this.db.lookup_atom(*name), + arity + ) + .ok(); + }); + } + Term::MacroCall { + expansion: _, + args: _, + } => todo!(), + } + } + + fn print_type(&mut self, ty: &TypeExpr) { + match ty { + TypeExpr::AnnType { var, ty } => { + self.print_herald("TypeExpr::AnnType", &mut |this| { + this.print_labelled("var", true, &mut |this| { + write!(this, "{}", this.db.lookup_var(*var)).ok(); + }); + this.print_labelled("ty", true, &mut |this| this.print_type(&this.body[*ty])); + }); + } + TypeExpr::BinaryOp { lhs, rhs, op } => { + self.print_herald("TypeExpr::BinaryOp", &mut |this| { + this.print_labelled("lhs", true, &mut |this| this.print_type(&this.body[*lhs])); + this.print_labelled("rhs", true, &mut |this| this.print_type(&this.body[*rhs])); + this.print_labelled("op", true, &mut |this| { + write!(this, "{:?},", op).ok(); + }); + }); + } + TypeExpr::Call { target, args } => { + self.print_herald("TypeExpr::Call", &mut |this| { + this.print_labelled("target", true, &mut |this1| { + this1 + .print_call_target(target, |this, ty| this.print_type(&this.body[*ty])); + }); + this.print_labelled("args", false, &mut |this| { + args.iter().for_each(|ty| { + this.print_type(&this.body[*ty]); + writeln!(this, ",").ok(); + }); + }); + }); + } + TypeExpr::Fun(fun) => { + self.print_herald("TypeExpr::Fun", &mut |this| match fun { + FunType::Any => { + writeln!(this, "FunType::Any").ok(); + } + FunType::AnyArgs { result } => { + this.print_herald("FunType::AnyArgs", &mut |this| { + this.print_labelled("result", true, &mut |this| { + this.print_type(&this.body[*result]); + }); + }); + } + FunType::Full { params, result } => { + this.print_herald("FunType::Full", &mut |this| { + this.print_labelled("params", false, &mut |this| { + params.iter().for_each(|ty| { + this.print_type(&this.body[*ty]); + writeln!(this, ",").ok(); + }); + }); + this.print_labelled("result", true, &mut |this| { + this.print_type(&this.body[*result]); + }); + }); + } + }); + } + TypeExpr::List(list) => { + self.print_herald("TypeExpr::Fun", &mut |this| match list { + ListType::Empty => { + writeln!(this, "ListType::Empty").ok(); + } + ListType::Regular(ty) => { + this.print_herald("ListType::Regular", &mut |this| { + this.print_type(&this.body[*ty]); + }); + } + ListType::NonEmpty(ty) => { + this.print_herald("ListType::NonEmpty", &mut |this| { + this.print_type(&this.body[*ty]); + }); + } + }); + } + TypeExpr::Literal(lit) => { + write!(self, "Literal(").ok(); + self.print_literal(lit); + write!(self, ")").ok(); + } + TypeExpr::Map { fields } => { + self.print_herald("TypeExpr::Map", &mut |this| { + fields.iter().for_each(|(name, op, value)| { + this.print_type(&this.body[*name]); + this.indent(); + writeln!(this, "").ok(); + writeln!(this, "{:?}", op).ok(); + this.print_type(&this.body[*value]); + writeln!(this, ",").ok(); + this.dedent(); + }); + }); + } + TypeExpr::Missing => { + write!(self, "TypeExpr::Missing").ok(); + } + TypeExpr::Union { types } => { + self.print_herald("TypeExpr::Union", &mut |this| { + types.iter().for_each(|ty| { + this.print_type(&this.body[*ty]); + writeln!(this, ",").ok(); + }); + }); + } + TypeExpr::Range { lhs, rhs } => { + self.print_herald("TypeExpr::Range", &mut |this| { + this.print_labelled("lhs", true, &mut |this| { + this.print_type(&this.body[*lhs]); + }); + this.print_labelled("rhs", true, &mut |this| { + this.print_type(&this.body[*rhs]); + }); + }); + } + TypeExpr::Record { name, fields } => { + self.print_herald("TypeExpr::Record", &mut |this| { + writeln!(this, "name: Atom('{}')", this.db.lookup_atom(*name)).ok(); + this.print_labelled("fields", false, &mut |this| { + fields.iter().for_each(|(name, ty)| { + writeln!(this, "Atom('{}'):", this.db.lookup_atom(*name)).ok(); + this.indent(); + this.print_type(&this.body[*ty]); + writeln!(this, ",").ok(); + this.dedent(); + }); + }); + }); + } + TypeExpr::Tuple { args } => { + self.print_herald("TypeExpr::Tuple", &mut |this| { + args.iter().for_each(|ty| { + this.print_type(&this.body[*ty]); + writeln!(this, ",").ok(); + }); + }); + } + TypeExpr::UnaryOp { type_expr, op } => { + self.print_herald("TypeExpr::UnaryOp", &mut |this| { + this.print_type(&this.body[*type_expr]); + writeln!(this, "").ok(); + writeln!(this, "{:?},", op).ok(); + }); + } + TypeExpr::Var(var) => { + write!(self, "TypeExpr::Var({})", self.db.lookup_var(*var)).ok(); + } + TypeExpr::MacroCall { + expansion: _, + args: _, + } => todo!(), + } + } + + fn print_literal(&mut self, lit: &Literal) { + match lit { + Literal::String(string) => write!(self, "String({:?})", string), + Literal::Char(char) => write!(self, "Char(${})", char), + Literal::Atom(atom) => write!(self, "Atom('{}')", self.db.lookup_atom(*atom)), + Literal::Integer(int) => write!(self, "Integer({})", int), + Literal::Float(float) => write!(self, "Float({})", f64::from_bits(*float)), + } + .ok(); + } + + fn print_clause(&mut self, clause: &Clause) { + self.print_herald("Clause", &mut |this| { + this.print_labelled("pats", false, &mut |this| { + for pat in clause.pats.iter().map(|pat_id| &this.body[*pat_id]) { + this.print_pat(pat); + writeln!(this, ",").ok(); + } + }); + this.print_labelled("guards", false, &mut |this| { + this.print_guards(&clause.guards); + }); + this.print_labelled("exprs", false, &mut |this| { + this.print_exprs(&clause.exprs); + }); + }); + } + + fn print_cr_clause(&mut self, clause: &CRClause) { + self.print_herald("CRClause", &mut |this| { + this.print_labelled("pat", true, &mut |this| { + this.print_pat(&this.body[clause.pat]); + }); + this.print_labelled("guards", false, &mut |this| { + this.print_guards(&clause.guards); + }); + this.print_labelled("exprs", false, &mut |this| { + this.print_exprs(&clause.exprs); + }); + }); + writeln!(self, "").ok(); + } + + fn print_catch_clause(&mut self, clause: &CatchClause) { + self.print_herald("CatchClause", &mut |this| { + this.print_labelled("class", false, &mut |this| { + if let Some(class) = clause.class { + this.print_pat(&this.body[class]); + writeln!(this, "").ok(); + } + }); + this.print_labelled("reason", true, &mut |this| { + this.print_pat(&this.body[clause.reason]); + }); + this.print_labelled("stack", false, &mut |this| { + if let Some(stack) = clause.stack { + this.print_pat(&this.body[stack]); + writeln!(this, "").ok(); + } + }); + this.print_labelled("guards", false, &mut |this| { + this.print_guards(&clause.guards); + }); + this.print_labelled("exprs", false, &mut |this| { + this.print_exprs(&clause.exprs); + }); + }); + } + + fn print_guards(&mut self, guards: &[Vec]) { + if !guards.is_empty() { + for guard_clause in guards { + self.print_labelled("guard", false, &mut |this| { + for expr in guard_clause { + this.print_expr(&this.body[*expr]); + writeln!(this, ",").ok(); + } + }); + } + } + } + + fn print_exprs(&mut self, exprs: &[ExprId]) { + for expr_id in exprs { + self.print_expr(&self.body[*expr_id]); + writeln!(self, ",").ok(); + } + } + + fn print_herald(&mut self, label: &str, print: &mut dyn FnMut(&mut Self)) { + writeln!(self, "{label} {{").ok(); + self.indent(); + print(self); + self.dedent(); + write!(self, "}}").ok(); + } + + fn print_herald_parens(&mut self, label: &str, print: &mut dyn FnMut(&mut Self)) { + writeln!(self, "{label}(").ok(); + self.indent(); + print(self); + self.dedent(); + write!(self, ")").ok(); + } + + fn print_labelled( + &mut self, + label: &str, + final_newline: bool, + print: &mut dyn FnMut(&mut Self), + ) { + writeln!(self, "{}", label).ok(); + self.indent(); + print(self); + if final_newline { + writeln!(self, "").ok(); + } + self.dedent(); + } + + fn print_bin_segment(&mut self, seg: &BinarySeg, print: fn(&mut Self, T)) { + self.print_herald("BinarySeg", &mut |this| { + writeln!(this, "elem").ok(); + this.indent(); + print(this, seg.elem); + writeln!(this, "").ok(); + this.dedent(); + writeln!(this, "size").ok(); + this.indent(); + if let Some(size) = seg.size { + writeln!(this, "Some(").ok(); + this.print_expr(&this.body[size]); + writeln!(this, ")").ok(); + } else { + writeln!(this, "None").ok(); + } + this.dedent(); + writeln!(this, "tys").ok(); + this.indent(); + for &ty in &seg.tys { + writeln!(this, "{},", this.db.lookup_atom(ty)).ok(); + } + this.dedent(); + writeln!(this, "unit").ok(); + this.indent(); + if let Some(unit) = seg.unit { + writeln!(this, "{}", unit).ok(); + }; + this.dedent(); + }); + } + + fn print_call_target(&mut self, target: &CallTarget, print: impl Fn(&mut Self, &T)) { + match target { + CallTarget::Local { name } => { + self.print_herald("CallTarget::Local", &mut |this| { + print(this, name); + writeln!(this, "").ok(); + }); + } + CallTarget::Remote { module, name } => { + self.print_herald("CallTarget::Remote", &mut |this| { + print(this, module); + writeln!(this, "").ok(); + print(this, name); + writeln!(this, "").ok(); + }); + } + } + } + + fn print_maybe_expr(&mut self, expr: &MaybeExpr) { + match expr { + MaybeExpr::Cond { lhs, rhs } => { + self.print_herald("MaybeExpr::Cond", &mut |this| { + this.print_labelled("lhs", true, &mut |this| this.print_pat(&this.body[*lhs])); + this.print_labelled("rhs", true, &mut |this| this.print_expr(&this.body[*rhs])); + }); + } + MaybeExpr::Expr(expr) => { + self.print_herald_parens("MaybeExpr::Expr", &mut |this| { + this.print_expr(&this.body[*expr]); + writeln!(this, "").ok(); + }); + } + } + } + + fn print_args(&mut self, name: &str, exprs: &[T], print: impl Fn(&mut Self, &T)) { + write!(self, "{}(", name).ok(); + let mut sep = ""; + for expr in exprs { + write!(self, "{}", sep).ok(); + sep = ", "; + print(self, expr); + } + write!(self, ")").ok(); + } +} + +impl<'a> fmt::Write for Printer<'a> { + fn write_str(&mut self, s: &str) -> fmt::Result { + for line in s.split_inclusive('\n') { + if self.needs_indent { + if !self.buf.ends_with('\n') { + self.buf.push('\n'); + } + for _ in 0..self.indent_level { + self.buf.push_str(" "); + } + self.needs_indent = false; + } + + self.buf.push_str(line); + self.needs_indent = line.ends_with('\n'); + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use elp_base_db::fixture::WithFixture; + use expect_test::expect; + use expect_test::Expect; + + use crate::db::MinDefDatabase; + use crate::test_db::TestDB; + use crate::AnyAttribute; + use crate::FormIdx; + use crate::InFile; + use crate::SpecOrCallback; + + #[track_caller] + fn check(fixture: &str, expect: Expect) { + let (db, file_id) = TestDB::with_single_file(fixture); + let form_list = db.file_form_list(file_id); + let pretty = form_list + .forms() + .iter() + .flat_map(|&form_idx| -> Option { + match form_idx { + FormIdx::Function(function_id) => { + let body = db.function_body(InFile::new(file_id, function_id)); + Some(body.tree_print(&db)) + } + FormIdx::TypeAlias(type_alias_id) => { + let type_alias = &form_list[type_alias_id]; + let body = db.type_body(InFile::new(file_id, type_alias_id)); + Some(body.tree_print(&db, type_alias)) + } + FormIdx::Spec(spec_id) => { + let spec = SpecOrCallback::Spec(form_list[spec_id].clone()); + let body = db.spec_body(InFile::new(file_id, spec_id)); + Some(body.print(&db, spec)) + } + FormIdx::Callback(callback_id) => { + let spec = SpecOrCallback::Callback(form_list[callback_id].clone()); + let body = db.callback_body(InFile::new(file_id, callback_id)); + Some(body.print(&db, spec)) + } + FormIdx::Record(record_id) => { + let body = db.record_body(InFile::new(file_id, record_id)); + Some(body.print(&db, &form_list, record_id)) + } + FormIdx::Attribute(attribute_id) => { + let attribute = AnyAttribute::Attribute(form_list[attribute_id].clone()); + let body = db.attribute_body(InFile::new(file_id, attribute_id)); + Some(body.print(&db, attribute)) + } + FormIdx::CompileOption(attribute_id) => { + let attribute = + AnyAttribute::CompileOption(form_list[attribute_id].clone()); + let body = db.compile_body(InFile::new(file_id, attribute_id)); + Some(body.tree_print(&db, attribute)) + } + _ => None, + } + }) + .collect::>() + .join(""); + expect.assert_eq(pretty.trim_start()); + } + + #[test] + fn term_via_attribute_tuple() { + check( + r#" + -compile({blah}). + "#, + expect![[r#" + -compile( + Term::Tuple { + Literal(Atom('blah')), + } + ). + "#]], + ); + } + + #[test] + fn term_via_attribute_list() { + check( + r#" + -compile(["blah",foo|$b]). + "#, + expect![[r#" + -compile( + Term::List { + exprs + Literal(String("blah")), + Literal(Atom('foo')), + tail + Literal(Char($b)), + } + ). + "#]], + ); + } + + #[test] + fn term_via_attribute_map() { + check( + r#" + -compile(#{ xx => 5.3, yy => 3}). + "#, + expect![[r#" + -compile( + Term::Map { + Literal(Atom('xx')) => Literal(Float(5.3)), + Literal(Atom('yy')) => Literal(Integer(3)), + } + ). + "#]], + ); + } + + #[test] + fn term_via_attribute_capture_fun() { + check( + r#" + -compile({fun foo/1, fun mod:foo/1}). + "#, + expect![[r#" + -compile( + Term::Tuple { + Term::Missing, + Term::CaptureFun { + fun mod:foo/1}, + } + ). + "#]], + ); + } + + #[test] + fn term_via_attribute_capture_binary() { + check( + r#" + -compile({<<"abc">>, + <<"abc", "def">>, + <<$a, $b, $c>>, + <<1, 2, 3, -1>> + }). + "#, + expect![[r#" + -compile( + Term::Tuple { + Term::Binary { + <<"abc"/utf8>> + }, + Term::Binary { + <<"abcdef"/utf8>> + }, + Term::Binary { + <<"abc"/utf8>> + }, + Term::Binary { + <<1, 2, 3, 255>> + }, + } + ). + "#]], + ); + } + + #[test] + fn expr_via_fun_tuple() { + check( + r#" + foo() -> {a, 1}. + "#, + expect![[r#" + Clause { + pats + guards + exprs + Expr::Tuple { + Literal(Atom('a')), + Literal(Integer(1)), + }, + }. + "#]], + ); + } + + #[test] + fn expr_via_fun_match() { + check( + r#" + foo() -> A = $b. + "#, + expect![[r#" + Clause { + pats + guards + exprs + Expr::Match { + lhs + Pat::Var(A) + rhs + Literal(Char($b)) + }, + }. + "#]], + ); + } + + #[test] + fn expr_via_fun_list() { + check( + r#" + foo() -> [A, b | 2.1]. + "#, + expect![[r#" + Clause { + pats + guards + exprs + Expr::List { + exprs + Expr::Var(A), + Literal(Atom('b')), + tail + Literal(Float(2.1)), + }, + }. + "#]], + ); + } + + #[test] + fn expr_via_fun_binary() { + check( + r#" + foo() -> <<+1/integer-little-unit:8>>. + "#, + expect![[r#" + Clause { + pats + guards + exprs + Expr::Binary { + BinarySeg { + elem + Expr::UnaryOp { + Literal(Integer(1)) + Plus, + } + size + None + tys + integer, + little, + unit + 8 + } + }, + }. + "#]], + ); + } + + #[test] + fn expr_via_fun_record() { + check( + r#" + foo() -> #record{field = 3, bar = 5 }. + "#, + expect![[r#" + Clause { + pats + guards + exprs + Expr::Record { + name: Atom('record') + fields + Atom('field'): + Literal(Integer(3)), + Atom('bar'): + Literal(Integer(5)), + }, + }. + "#]], + ); + } + + #[test] + fn expr_via_fun_record_update() { + check( + r#" + foo() -> Name#record{field = undefined}. + "#, + expect![[r#" + Clause { + pats + guards + exprs + Expr::RecordUpdate { + expr + Expr::Var(Name) + name: Atom('record') + fields + Atom('field'): + Literal(Atom('undefined')), + }, + }. + "#]], + ); + } + + #[test] + fn expr_via_fun_record_index() { + check( + r#" + foo() -> #rec.name. + "#, + expect![[r#" + Clause { + pats + guards + exprs + Expr::RecordIndex { + name: Atom('rec') + field: Atom('name') + }, + }. + "#]], + ); + } + + #[test] + fn expr_via_fun_record_field() { + check( + r#" + foo() -> Name#record.field. + "#, + expect![[r#" + Clause { + pats + guards + exprs + Expr::RecordField { + expr + Expr::Var(Name) + name: Atom('record') + field: Atom('field') + }, + }. + "#]], + ); + } + + #[test] + fn expr_via_fun_map() { + check( + r#" + foo() -> #{ foo => a + 3, bar => $v }. + "#, + expect![[r#" + Clause { + pats + guards + exprs + Expr::Map { + Literal(Atom('foo')), + Expr::BinaryOp { + lhs + Literal(Atom('a')) + rhs + Literal(Integer(3)) + op + ArithOp(Add), + }, + Literal(Atom('bar')), + Literal(Char($v)), + }, + }. + "#]], + ); + } + + #[test] + fn expr_via_fun_map_update() { + check( + r#" + foo() -> #{a => b}#{a := b, c => d}. + "#, + expect![[r#" + Clause { + pats + guards + exprs + Expr::MapUpdate { + expr + Expr::Map { + Literal(Atom('a')), + Literal(Atom('b')), + } + fields + Literal(Atom('a')) + Exact + Literal(Atom('b')), + Literal(Atom('c')) + Assoc + Literal(Atom('d')), + }, + }. + "#]], + ); + } + + #[test] + fn expr_via_fun_catch() { + check( + r#" + foo() -> catch 1 + 2. + "#, + expect![[r#" + Clause { + pats + guards + exprs + Expr::Catch { + expr + Expr::BinaryOp { + lhs + Literal(Integer(1)) + rhs + Literal(Integer(2)) + op + ArithOp(Add), + } + }, + }. + "#]], + ); + } + + #[test] + fn expr_via_fun_macrocall() { + // TODO: set optional with/without MacroCall printing. + check( + r#" + -define(EXPR(X), 1 + X). + foo() -> ?EXPR(2). + "#, + expect![[r#" + Clause { + pats + guards + exprs + Expr::BinaryOp { + lhs + Literal(Integer(1)) + rhs + Literal(Integer(2)) + op + ArithOp(Add), + }, + }. + "#]], + ); + } + + #[test] + fn expr_via_fun_call_mfa() { + check( + r#" + foo() -> baz:bar(3,X). + "#, + expect![[r#" + Clause { + pats + guards + exprs + Expr::Call { + target + CallTarget::Remote { + Literal(Atom('baz')) + Literal(Atom('bar')) + } + args + Literal(Integer(3)), + Expr::Var(X), + }, + }. + "#]], + ); + } + + #[test] + fn expr_via_fun_call_fa() { + check( + r#" + foo() -> bar(3,X). + "#, + expect![[r#" + Clause { + pats + guards + exprs + Expr::Call { + target + CallTarget::Local { + Literal(Atom('bar')) + } + args + Literal(Integer(3)), + Expr::Var(X), + }, + }. + "#]], + ); + } + + #[test] + fn expr_via_fun_comprehension() { + check( + r#" + foo() -> + [X || X <- List, X >= 5]. + "#, + expect![[r#" + Clause { + pats + guards + exprs + Expr::Comprehension { + builder + ComprehensionBuilder::List { + Expr::Var(X) + } + exprs + ComprehensionExpr::ListGenerator { + Pat::Var(X) + Expr::Var(List) + }, + ComprehensionExpr::Expr { + Expr::BinaryOp { + lhs + Expr::Var(X) + rhs + Literal(Integer(5)) + op + CompOp(Ord { ordering: Greater, strict: false }), + } + }, + }, + }. + "#]], + ); + } + + #[test] + fn expr_via_fun_comprehension_binary() { + check( + r#" + foo() -> + << Byte || <> <= Bytes, Byte >= 5>>. + "#, + expect![[r#" + Clause { + pats + guards + exprs + Expr::Comprehension { + builder + ComprehensionBuilder::Binary { + Expr::Var(Byte) + } + exprs + ComprehensionExpr::BinGenerator { + Pat::Binary { + BinarySeg { + elem + Pat::Var(Byte) + size + None + tys + unit + } + } + Expr::Var(Bytes) + }, + ComprehensionExpr::Expr { + Expr::BinaryOp { + lhs + Expr::Var(Byte) + rhs + Literal(Integer(5)) + op + CompOp(Ord { ordering: Greater, strict: false }), + } + }, + }, + }. + "#]], + ); + } + + #[test] + fn expr_via_fun_map_comprehension() { + check( + r#" + foo() -> + #{KK => VV || KK := VV <- Map}. + "#, + expect![[r#" + Clause { + pats + guards + exprs + Expr::Comprehension { + builder + ComprehensionBuilder::Map { + Expr::Var(KK) + => + Expr::Var(VV) + } + exprs + ComprehensionExpr::MapGenerator { + Pat::Var(KK) := + Pat::Var(VV) <- + Expr::Var(Map) + }, + }, + }. + "#]], + ); + } + + #[test] + fn expr_via_fun_block() { + check( + r#" + foo() -> begin 1, 2 end. + "#, + expect![[r#" + Clause { + pats + guards + exprs + Expr::Block { + Literal(Integer(1)), + Literal(Integer(2)), + }, + }. + "#]], + ); + } + + #[test] + fn expr_via_fun_if() { + check( + r#" + foo() -> + if is_atom(X) -> ok; + true -> error + end. + "#, + expect![[r#" + Clause { + pats + guards + exprs + Expr::If { + IfClause { + guards + guard + Expr::Call { + target + CallTarget::Local { + Literal(Atom('is_atom')) + } + args + Expr::Var(X), + }, + exprs + Literal(Atom('ok')), + } + IfClause { + guards + guard + Literal(Atom('true')), + exprs + Literal(Atom('error')), + } + }, + }. + "#]], + ); + } + + #[test] + fn expr_via_fun_case() { + check( + r#" + foo() -> + case 1 + 2 of + X when X andalso true; X <= 100, X >= 5 -> ok; + _ -> error + end. + "#, + expect![[r#" + Clause { + pats + guards + exprs + Expr::Case { + expr + Expr::BinaryOp { + lhs + Literal(Integer(1)) + rhs + Literal(Integer(2)) + op + ArithOp(Add), + } + clauses + CRClause { + pat + Pat::Var(X) + guards + guard + Expr::BinaryOp { + lhs + Expr::Var(X) + rhs + Literal(Atom('true')) + op + LogicOp(And { lazy: true }), + }, + guard + Expr::BinaryOp { + lhs + Expr::Var(X) + rhs + Literal(Integer(100)) + op + CompOp(Ord { ordering: Less, strict: true }), + }, + Expr::BinaryOp { + lhs + Expr::Var(X) + rhs + Literal(Integer(5)) + op + CompOp(Ord { ordering: Greater, strict: false }), + }, + exprs + Literal(Atom('ok')), + } + CRClause { + pat + Pat::Var(_) + guards + exprs + Literal(Atom('error')), + } + }, + }. + "#]], + ); + } + + #[test] + fn expr_via_fun_receive() { + check( + r#" + foo() -> + receive + ok when true -> ok; + _ -> error + after Timeout -> timeout + end. + "#, + expect![[r#" + Clause { + pats + guards + exprs + Expr::Receive { + clauses + CRClause { + pat + Literal(Atom('ok')) + guards + guard + Literal(Atom('true')), + exprs + Literal(Atom('ok')), + } + CRClause { + pat + Pat::Var(_) + guards + exprs + Literal(Atom('error')), + } + after + ReceiveAfter { + timeout + Expr::Var(Timeout) + exprs + Literal(Atom('timeout')), + }}, + }. + "#]], + ); + } + + #[test] + fn expr_via_fun_try() { + check( + r#" + foo() -> + try 1, 2 of + _ -> ok + catch + Pat when true -> ok; + error:undef:Stack -> Stack + after + ok + end. + "#, + expect![[r#" + Clause { + pats + guards + exprs + Expr::Try { + exprs + Literal(Integer(1)), + Literal(Integer(2)), + of_clauses + CRClause { + pat + Pat::Var(_) + guards + exprs + Literal(Atom('ok')), + } + catch_clauses + CatchClause { + class + reason + Pat::Var(Pat) + stack + guards + guard + Literal(Atom('true')), + exprs + Literal(Atom('ok')), + }, + CatchClause { + class + Literal(Atom('error')) + reason + Literal(Atom('undef')) + stack + Pat::Var(Stack) + guards + exprs + Expr::Var(Stack), + }, + after + Literal(Atom('ok')), + }, + }. + "#]], + ); + } + + #[test] + fn expr_via_fun_capture_fun() { + check( + r#" + foo() -> + fun foo/1, + fun mod:foo/1, + fun Mod:Foo/Arity. + "#, + expect![[r#" + Clause { + pats + guards + exprs + Expr::CaptureFun { + target + CallTarget::Local { + Literal(Atom('foo')) + } + arity + Literal(Integer(1)) + }, + Expr::CaptureFun { + target + CallTarget::Remote { + Literal(Atom('mod')) + Literal(Atom('foo')) + } + arity + Literal(Integer(1)) + }, + Expr::CaptureFun { + target + CallTarget::Remote { + Expr::Var(Mod) + Expr::Var(Foo) + } + arity + Expr::Var(Arity) + }, + }. + "#]], + ); + } + + #[test] + fn expr_via_fun_closure() { + check( + r#" + foo() -> + fun (ok) -> ok; + (error) -> error + end, + fun Named() -> Named() end. + "#, + expect![[r#" + Clause { + pats + guards + exprs + Expr::Closure { + clauses + Clause { + pats + Literal(Atom('ok')), + guards + exprs + Literal(Atom('ok')), + }, + Clause { + pats + Literal(Atom('error')), + guards + exprs + Literal(Atom('error')), + }, + name + }, + Expr::Closure { + clauses + Clause { + pats + guards + exprs + Expr::Call { + target + CallTarget::Local { + Expr::Var(Named) + } + args + }, + }, + name + Pat::Var(Named) + }, + }. + "#]], + ); + } + + #[test] + fn expr_via_fun_maybe() { + check( + r#" + foo() -> + maybe + {ok, A} ?= a(), + true = A >= 0, + A + else + error -> error; + Other when Other == 0 -> error + end. + "#, + expect![[r#" + Clause { + pats + guards + exprs + Expr::Maybe { + exprs + MaybeExpr::Cond { + lhs + Pat::Tuple { + Literal(Atom('ok')), + Pat::Var(A), + } + rhs + Expr::Call { + target + CallTarget::Local { + Literal(Atom('a')) + } + args + } + }, + MaybeExpr::Expr( + Expr::Match { + lhs + Literal(Atom('true')) + rhs + Expr::BinaryOp { + lhs + Expr::Var(A) + rhs + Literal(Integer(0)) + op + CompOp(Ord { ordering: Greater, strict: false }), + } + } + ), + MaybeExpr::Expr( + Expr::Var(A) + ), + else_clauses + CRClause { + pat + Literal(Atom('error')) + guards + exprs + Literal(Atom('error')), + } + CRClause { + pat + Pat::Var(Other) + guards + guard + Expr::BinaryOp { + lhs + Expr::Var(Other) + rhs + Literal(Integer(0)) + op + CompOp(Eq { strict: false, negated: false }), + }, + exprs + Literal(Atom('error')), + } + }, + }. + "#]], + ); + } + + #[test] + fn expr_via_fun_missing() { + check( + r#" + foo(a:b) -> a:b. + "#, + expect![[r#" + Clause { + pats + Pat::Missing, + guards + exprs + Expr::Missing, + }. + "#]], + ); + } + + #[test] + fn pat_via_fun_match() { + check( + r#" + foo(A = 4) -> ok. + "#, + expect![[r#" + Clause { + pats + Pat::Match { + lhs + Pat::Var(A) + rhs + Literal(Integer(4)) + }, + guards + exprs + Literal(Atom('ok')), + }. + "#]], + ); + } + + #[test] + fn pat_via_fun_list() { + check( + r#" + foo([A,4|X]) -> ok. + "#, + expect![[r#" + Clause { + pats + Pat::List { + exprs + Pat::Var(A), + Literal(Integer(4)), + tail + Pat::Var(X), + }, + guards + exprs + Literal(Atom('ok')), + }. + "#]], + ); + } + + #[test] + fn pat_via_fun_unary_op() { + check( + r#" + foo(X = +1) -> ok. + "#, + expect![[r#" + Clause { + pats + Pat::Match { + lhs + Pat::Var(X) + rhs + Pat::UnaryOp { + Literal(Integer(1)) + Plus, + } + }, + guards + exprs + Literal(Atom('ok')), + }. + "#]], + ); + } + + #[test] + fn pat_via_fun_binary_op() { + check( + r#" + foo(X + 4) -> ok. + "#, + expect![[r#" + Clause { + pats + Pat::BinaryOp { + lhs + Pat::Var(X) + rhs + Literal(Integer(4)) + op + ArithOp(Add), + }, + guards + exprs + Literal(Atom('ok')), + }. + "#]], + ); + } + + #[test] + fn pat_via_fun_record() { + check( + r#" + foo(#rec{f=X, g=Y}) -> ok. + "#, + expect![[r#" + Clause { + pats + Pat::Record { + name: Atom('rec') + fields + Atom('f'): + Pat::Var(X), + Atom('g'): + Pat::Var(Y), + }, + guards + exprs + Literal(Atom('ok')), + }. + "#]], + ); + } + + #[test] + fn pat_via_fun_record_index() { + check( + r#" + foo(#rec.f) -> ok. + "#, + expect![[r#" + Clause { + pats + Pat::RecordIndex { + name: Atom('rec') + field: Atom('f') + }, + guards + exprs + Literal(Atom('ok')), + }. + "#]], + ); + } + + #[test] + fn pat_via_fun_map() { + check( + r#" + foo(#{1 + 2 := 3 + 4}) -> #{a => b}. + "#, + expect![[r#" + Clause { + pats + Pat::Map { + Expr::BinaryOp { + lhs + Literal(Integer(1)) + rhs + Literal(Integer(2)) + op + ArithOp(Add), + }, + Pat::BinaryOp { + lhs + Literal(Integer(3)) + rhs + Literal(Integer(4)) + op + ArithOp(Add), + }, + }, + guards + exprs + Expr::Map { + Literal(Atom('a')), + Literal(Atom('b')), + }, + }. + "#]], + ); + } + + #[test] + fn type_binary_op() { + check( + r#" + -type foo() :: 1 + 1. + "#, + expect![[r#" + -type foo() :: TypeExpr::BinaryOp { + lhs + Literal(Integer(1)) + rhs + Literal(Integer(1)) + op + ArithOp(Add), + }. + "#]], + ); + } + + #[test] + fn type_call() { + check( + r#" + -type local(A) :: local(A | integer()). + -type remote(A) :: module:remote(A | integer()). + "#, + expect![[r#" + -type local(A) :: TypeExpr::Call { + target + CallTarget::Local { + Literal(Atom('local')) + } + args + TypeExpr::Union { + TypeExpr::Var(A), + TypeExpr::Call { + target + CallTarget::Local { + Literal(Atom('integer')) + } + args + }, + }, + }. + + -type remote(A) :: TypeExpr::Call { + target + CallTarget::Remote { + Literal(Atom('module')) + Literal(Atom('remote')) + } + args + TypeExpr::Union { + TypeExpr::Var(A), + TypeExpr::Call { + target + CallTarget::Local { + Literal(Atom('integer')) + } + args + }, + }, + }. + "#]], + ); + } + + #[test] + fn type_fun() { + check( + r#" + -type foo1() :: fun(). + -type foo2() :: fun(() -> ok). + -type foo3() :: fun((a, b) -> ok). + -type foo4() :: fun((...) -> ok). + "#, + expect![[r#" + -type foo1() :: TypeExpr::Fun { + FunType::Any + }. + + -type foo2() :: TypeExpr::Fun { + FunType::Full { + params + result + Literal(Atom('ok')) + }}. + + -type foo3() :: TypeExpr::Fun { + FunType::Full { + params + Literal(Atom('a')), + Literal(Atom('b')), + result + Literal(Atom('ok')) + }}. + + -type foo4() :: TypeExpr::Fun { + FunType::AnyArgs { + result + Literal(Atom('ok')) + }}. + "#]], + ); + } + + #[test] + fn type_list() { + check( + r#" + -type foo() :: [foo]. + -type bar() :: [bar, ...]. + -type baz() :: []. + "#, + expect![[r#" + -type foo() :: TypeExpr::Fun { + ListType::Regular { + Literal(Atom('foo'))}}. + + -type bar() :: TypeExpr::Fun { + ListType::NonEmpty { + Literal(Atom('bar'))}}. + + -type baz() :: TypeExpr::Fun { + ListType::Empty + }. + "#]], + ); + } + + #[test] + fn type_map() { + check( + r#" + -type foo() :: #{a => b, c := d}. + "#, + expect![[r#" + -type foo() :: TypeExpr::Map { + Literal(Atom('a')) + Assoc + Literal(Atom('b')), + Literal(Atom('c')) + Exact + Literal(Atom('d')), + }. + "#]], + ); + } + + #[test] + fn type_range() { + check( + r#" + -type foo() :: 1..100. + "#, + expect![[r#" + -type foo() :: TypeExpr::Range { + lhs + Literal(Integer(1)) + rhs + Literal(Integer(100)) + }. + "#]], + ); + } + + #[test] + fn type_record() { + check( + r#" + -type foo1() :: #record{}. + -type foo2(B) :: #record{a :: integer(), b :: B}. + -type foo3() :: #record{a ::}. + "#, + expect![[r#" + -type foo1() :: TypeExpr::Record { + name: Atom('record') + fields + }. + + -type foo2(B) :: TypeExpr::Record { + name: Atom('record') + fields + Atom('a'): + TypeExpr::Call { + target + CallTarget::Local { + Literal(Atom('integer')) + } + args + }, + Atom('b'): + TypeExpr::Var(B), + }. + + -type foo3() :: TypeExpr::Record { + name: Atom('record') + fields + Atom('a'): + TypeExpr::Missing, + }. + "#]], + ); + } + + #[test] + fn type_tuple() { + check( + r#" + -type foo() :: {a, b, c}. + "#, + expect![[r#" + -type foo() :: TypeExpr::Tuple { + Literal(Atom('a')), + Literal(Atom('b')), + Literal(Atom('c')), + }. + "#]], + ); + } + + #[test] + fn type_unary_op() { + check( + r#" + -type foo() :: -1. + "#, + expect![[r#" + -type foo() :: TypeExpr::UnaryOp { + Literal(Integer(1)) + Minus, + }. + "#]], + ); + } + + #[test] + fn type_ann_type() { + check( + r#" + -type foo() :: A :: any(). + "#, + expect![[r#" + -type foo() :: TypeExpr::AnnType { + var + A + ty + TypeExpr::Call { + target + CallTarget::Local { + Literal(Atom('any')) + } + args + } + }. + "#]], + ); + } +} diff --git a/crates/hir/src/db.rs b/crates/hir/src/db.rs new file mode 100644 index 0000000000..197608aaa2 --- /dev/null +++ b/crates/hir/src/db.rs @@ -0,0 +1,180 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::sync::Arc; + +use elp_base_db::salsa; +use elp_base_db::FileId; +use elp_base_db::SourceDatabase; +use elp_base_db::Upcast; +use elp_syntax::ast; +use fxhash::FxHashMap; + +use crate::body::scope::FunctionScopes; +use crate::body::DefineBody; +use crate::edoc; +use crate::edoc::EdocHeader; +use crate::include; +pub use crate::intern::MinInternDatabase; +pub use crate::intern::MinInternDatabaseStorage; +use crate::macro_exp; +use crate::macro_exp::MacroResolution; +use crate::AttributeBody; +use crate::AttributeId; +use crate::BodySourceMap; +use crate::CallbackId; +use crate::CompileOptionId; +use crate::DefMap; +use crate::DefineId; +use crate::FormList; +use crate::FunctionBody; +use crate::FunctionId; +use crate::InFile; +use crate::InFileAstPtr; +use crate::IncludeAttributeId; +use crate::MacroName; +use crate::RecordBody; +use crate::RecordId; +use crate::ResolvedMacro; +use crate::SpecBody; +use crate::SpecId; +use crate::TypeAliasId; +use crate::TypeBody; + +#[salsa::query_group(MinDefDatabaseStorage)] +pub trait MinDefDatabase: + MinInternDatabase + Upcast + SourceDatabase + Upcast +{ + #[salsa::invoke(FormList::file_form_list_query)] + fn file_form_list(&self, file_id: FileId) -> Arc; + + #[salsa::invoke(FunctionBody::function_body_with_source_query)] + fn function_body_with_source( + &self, + function_id: InFile, + ) -> (Arc, Arc); + + #[salsa::invoke(RecordBody::record_body_with_source_query)] + fn record_body_with_source( + &self, + record_id: InFile, + ) -> (Arc, Arc); + + #[salsa::invoke(SpecBody::spec_body_with_source_query)] + fn spec_body_with_source(&self, spec_id: InFile) + -> (Arc, Arc); + + #[salsa::invoke(SpecBody::callback_body_with_source_query)] + fn callback_body_with_source( + &self, + callback_id: InFile, + ) -> (Arc, Arc); + + #[salsa::invoke(TypeBody::type_body_with_source_query)] + fn type_body_with_source( + &self, + type_alias_id: InFile, + ) -> (Arc, Arc); + + #[salsa::invoke(AttributeBody::attribute_body_with_source_query)] + fn attribute_body_with_source( + &self, + attribute_id: InFile, + ) -> (Arc, Arc); + + #[salsa::invoke(AttributeBody::compile_body_with_source_query)] + fn compile_body_with_source( + &self, + attribute_id: InFile, + ) -> (Arc, Arc); + + #[salsa::invoke(DefineBody::define_body_with_source_query)] + fn define_body_with_source( + &self, + define_id: InFile, + ) -> Option<(Arc, Arc)>; + + // Projection queries to stop recomputation if structure didn't change, even if positions did + fn function_body(&self, function_id: InFile) -> Arc; + fn type_body(&self, type_alias_id: InFile) -> Arc; + fn spec_body(&self, spec_id: InFile) -> Arc; + fn callback_body(&self, callback_id: InFile) -> Arc; + fn record_body(&self, record_id: InFile) -> Arc; + fn attribute_body(&self, attribute_id: InFile) -> Arc; + fn compile_body(&self, attribute_id: InFile) -> Arc; + fn define_body(&self, define_id: InFile) -> Option>; + + #[salsa::invoke(FunctionScopes::function_scopes_query)] + fn function_scopes(&self, fun: InFile) -> Arc; + + #[salsa::invoke(include::resolve)] + fn resolve_include(&self, include_id: InFile) -> Option; + + #[salsa::invoke(macro_exp::resolve_query)] + fn resolve_macro(&self, file_id: FileId, name: MacroName) -> Option; + + #[salsa::invoke(edoc::file_edoc_comments_query)] + fn file_edoc_comments( + &self, + file_id: FileId, + ) -> Option, EdocHeader>>; + + // Helper query to run the recursive resolution algorithm + #[salsa::cycle(macro_exp::recover_cycle)] + #[salsa::invoke(macro_exp::local_resolve_query)] + fn local_resolve_macro(&self, file_id: FileId, name: MacroName) -> MacroResolution; + + #[salsa::cycle(DefMap::recover_cycle)] + #[salsa::invoke(DefMap::def_map_query)] + fn def_map(&self, file_id: FileId) -> Arc; + + // Helper query to compute only local data, avoids recomputation of header data, + // if only local information changed + #[salsa::invoke(DefMap::local_def_map_query)] + fn local_def_map(&self, file_id: FileId) -> Arc; +} + +fn function_body(db: &dyn MinDefDatabase, function_id: InFile) -> Arc { + db.function_body_with_source(function_id).0 +} + +fn type_body(db: &dyn MinDefDatabase, type_alias_id: InFile) -> Arc { + db.type_body_with_source(type_alias_id).0 +} + +fn spec_body(db: &dyn MinDefDatabase, spec_id: InFile) -> Arc { + db.spec_body_with_source(spec_id).0 +} + +fn callback_body(db: &dyn MinDefDatabase, callback_id: InFile) -> Arc { + db.callback_body_with_source(callback_id).0 +} + +fn record_body(db: &dyn MinDefDatabase, record_id: InFile) -> Arc { + db.record_body_with_source(record_id).0 +} + +fn attribute_body( + db: &dyn MinDefDatabase, + attribute_id: InFile, +) -> Arc { + db.attribute_body_with_source(attribute_id).0 +} + +fn compile_body( + db: &dyn MinDefDatabase, + attribute_id: InFile, +) -> Arc { + db.compile_body_with_source(attribute_id).0 +} + +fn define_body(db: &dyn MinDefDatabase, define_id: InFile) -> Option> { + db.define_body_with_source(define_id) + .map(|(body, _source)| body) +} diff --git a/crates/hir/src/def_map.rs b/crates/hir/src/def_map.rs new file mode 100644 index 0000000000..7f49f9d8f9 --- /dev/null +++ b/crates/hir/src/def_map.rs @@ -0,0 +1,760 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! A lookup map for definitions in a module/file. +//! +//! DefMap represents definitions of various constructs in a file - +//! functions, records, types, in a way that is easy to lookup. +//! It represents the state as Erlang compiler sees it - after include resolution. +//! +//! They are constructed recursively and separately for all headers and modules - +//! this makes sure that we need to do a minimal amount of re-computation on changes. + +use std::sync::Arc; + +use elp_base_db::FileId; +use elp_syntax::ast; +use elp_syntax::match_ast; +use elp_syntax::AstNode; +use fxhash::FxHashMap; +use fxhash::FxHashSet; +use profile::Count; + +use crate::db::MinDefDatabase; +use crate::form_list::DeprecatedAttribute; +use crate::form_list::DeprecatedFa; +use crate::known; +use crate::module_data::SpecDef; +use crate::module_data::SpecdFunctionDef; +use crate::name::AsName; +use crate::CallbackDef; +use crate::DefineDef; +use crate::File; +use crate::FormIdx; +use crate::FunctionDef; +use crate::InFile; +use crate::MacroName; +use crate::Name; +use crate::NameArity; +use crate::OptionalCallbacks; +use crate::PPDirective; +use crate::RecordDef; +use crate::TypeAliasDef; + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct DefMap { + _c: Count, + included: FxHashSet, + functions: FxHashMap, + specs: FxHashMap, + exported_functions: FxHashSet, + deprecated: Deprecated, + optional_callbacks: FxHashSet, + imported_functions: FxHashMap, + types: FxHashMap, + exported_types: FxHashSet, + records: FxHashMap, + callbacks: FxHashMap, + macros: FxHashMap, + export_all: bool, + pub parse_transform: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct Deprecated { + all: bool, + functions: FxHashSet, + fa: FxHashSet, +} + +impl Deprecated { + fn is_deprecated(&self, fa: &NameArity) -> bool { + if self.all { + return true; + } + if self.functions.contains(fa.name()) { + return true; + } + if self.fa.contains(fa) { + return true; + } + return false; + } + + fn shrink_to_fit(&mut self) { + self.functions.shrink_to_fit(); + self.fa.shrink_to_fit(); + } +} + +impl DefMap { + pub(crate) fn local_def_map_query(db: &dyn MinDefDatabase, file_id: FileId) -> Arc { + let mut def_map = Self::default(); + let file = File { file_id }; + + // Type (and to some extent function) definitions & export attributes + // can come in any order, we initially store types/functions as unexported, non-deprecated + // and later fix it up. + // We do this both for the local & complete query to have some export information + // in case we fall back to local-only data in cycles. + + let form_list = db.file_form_list(file_id); + for &form in form_list.forms() { + match form { + FormIdx::Function(idx) => { + let function = form_list[idx].clone(); + def_map.functions.insert( + function.name.clone(), + FunctionDef { + file, + exported: false, + deprecated: false, + function, + function_id: idx, + }, + ); + } + FormIdx::Export(idx) => { + for export_id in form_list[idx].entries.clone() { + def_map + .exported_functions + .insert(form_list[export_id].name.clone()); + } + } + FormIdx::Import(idx) => { + for import_id in form_list[idx].entries.clone() { + let module = form_list[idx].from.clone(); + def_map + .imported_functions + .insert(form_list[import_id].name.clone(), module); + } + } + FormIdx::TypeAlias(idx) => { + let type_alias = form_list[idx].clone(); + def_map.types.insert( + type_alias.name().clone(), + TypeAliasDef { + file, + exported: false, + type_alias, + }, + ); + } + FormIdx::TypeExport(idx) => { + for export_id in form_list[idx].entries.clone() { + def_map + .exported_types + .insert(form_list[export_id].name.clone()); + } + } + FormIdx::Callback(idx) => { + let callback = form_list[idx].clone(); + def_map.callbacks.insert( + callback.name.clone(), + CallbackDef { + file, + callback, + optional: false, + }, + ); + } + FormIdx::Record(idx) => { + let record = form_list[idx].clone(); + def_map + .records + .insert(record.name.clone(), RecordDef { file, record }); + } + FormIdx::PPDirective(idx) => { + if let PPDirective::Define(define) = &form_list[idx] { + let define = form_list[*define].clone(); + def_map + .macros + .insert(define.name.clone(), DefineDef { file, define }); + } + } + FormIdx::CompileOption(idx) => { + let option = &form_list[idx]; + let source = db.parse(file_id); + let ast_option = option.form_id.get(&source.tree()); + if let Some(options) = ast_option.options() { + // Blindly search for any atom with value `export_all`, or `parse_transform`. + options.syntax().descendants().for_each(|n| { + match_ast! { + match n { + ast::Atom(a) => { + if a.as_name() == known::export_all { + def_map.export_all = true; + } + if a.as_name() == known::parse_transform { + def_map.parse_transform = true; + } + }, + _ => {}, + } + } + }); + } + } + FormIdx::Spec(idx) => { + let spec = form_list[idx].clone(); + def_map.specs.insert( + spec.name.clone(), + SpecDef { + file, + spec, + spec_id: idx, + }, + ); + } + //https://github.com/erlang/otp/blob/69aa665f3f48a59f83ad48dea63fdf1476d1d46a/lib/stdlib/src/erl_lint.erl#L1123 + FormIdx::DeprecatedAttribute(idx) => match &form_list[idx] { + DeprecatedAttribute::Module { .. } => { + def_map.deprecated.all = true; + } + DeprecatedAttribute::Fa { fa, .. } => { + Self::def_map_deprecated_attr(&mut def_map, &fa); + } + DeprecatedAttribute::Fas { fas, .. } => { + for fa in fas { + Self::def_map_deprecated_attr(&mut def_map, &fa); + } + } + }, + FormIdx::OptionalCallbacks(idx) => { + let OptionalCallbacks { + entries, + cond: _, + form_id: _, + } = &form_list[idx]; + entries.clone().into_iter().for_each(|fa| { + def_map + .optional_callbacks + .insert(form_list[fa].name.clone()); + }); + } + _ => {} + } + } + + def_map.fixup_exports(); + def_map.fixup_deprecated(); + def_map.fixup_callbacks(); + def_map.shrink_to_fit(); + + Arc::new(def_map) + } + + fn def_map_deprecated_attr(def_map: &mut DefMap, fa: &DeprecatedFa) { + if fa.name == "_" { + def_map.deprecated.all = true; + } + match fa.arity { + Some(arity) => def_map + .deprecated + .fa + .insert(NameArity::new(fa.name.clone(), arity)), + None => def_map.deprecated.functions.insert(fa.name.clone()), + }; + } + + pub(crate) fn def_map_query(db: &dyn MinDefDatabase, file_id: FileId) -> Arc { + let local = db.local_def_map(file_id); + let form_list = db.file_form_list(file_id); + + let mut remote = Self::default(); + + form_list + .includes() + .filter_map(|(idx, _)| db.resolve_include(InFile::new(file_id, idx))) + // guard against naive cycles of headers including themselves + .filter(|&included_file_id| included_file_id != file_id) + .map(|included_file_id| (included_file_id, db.def_map(included_file_id))) + .for_each(|(file_id, def_map)| { + remote.included.insert(file_id); + remote.merge(&def_map) + }); + + // Small optimisation for a case where we have no headers or headers don't contain defintitions + // we're inrested in - should be hit frequently in headers themselves + if remote.is_empty() { + local + } else { + remote.merge(&local); + remote.fixup_exports(); + remote.fixup_deprecated(); + Arc::new(remote) + } + } + + // This handles the case of headers accidentally forming other cycles. + // Return just the local def map in such cases, not resolving nested includes at all + pub(crate) fn recover_cycle( + db: &dyn MinDefDatabase, + _cycle: &[String], + file_id: &FileId, + ) -> Arc { + db.local_def_map(*file_id) + } + + pub fn get_function(&self, name: &NameArity) -> Option<&FunctionDef> { + self.functions.get(name) + } + + pub fn is_deprecated(&self, name: &NameArity) -> bool { + self.deprecated.is_deprecated(name) + } + + pub fn get_spec(&self, name: &NameArity) -> Option<&SpecDef> { + self.specs.get(name) + } + + pub fn get_specd_function(&self, name: &NameArity) -> Option { + let (spec_def, function_def) = Option::zip( + self.get_spec(name).cloned(), + self.get_function(name).cloned(), + )?; + Some(SpecdFunctionDef { + spec_def, + function_def, + }) + } + + pub fn get_exported_functions(&self) -> &FxHashSet { + &self.exported_functions + } + + pub fn is_function_exported(&self, name: &NameArity) -> bool { + self.exported_functions.contains(name) + } + + pub fn get_functions(&self) -> &FxHashMap { + &self.functions + } + + pub fn get_specs(&self) -> &FxHashMap { + &self.specs + } + + pub fn get_specd_functions(&self) -> FxHashMap { + let functions = self.get_functions(); + let specs = self.get_specs(); + let name_arities = functions.keys().into_iter().chain(specs.keys().into_iter()); + name_arities + .filter_map(|na| { + specs.get(na).zip(functions.get(na)).map(|(s, f)| { + ( + na.clone(), + SpecdFunctionDef { + spec_def: s.clone(), + function_def: f.clone(), + }, + ) + }) + }) + .collect() + } + + pub fn get_imports(&self) -> &FxHashMap { + &self.imported_functions + } + + pub fn get_functions_in_scope(&self) -> impl Iterator { + self.get_imports().keys().chain(self.get_functions().keys()) + } + + // TODO: tweak API T127375780 + pub fn get_types(&self) -> &FxHashMap { + &self.types + } + + pub fn get_type(&self, name: &NameArity) -> Option<&TypeAliasDef> { + self.types.get(name) + } + + // TODO: tweak API T127375780 + pub fn get_exported_types(&self) -> &FxHashSet { + &self.exported_types + } + + pub fn get_records(&self) -> &FxHashMap { + &self.records + } + + pub fn get_record(&self, name: &Name) -> Option<&RecordDef> { + self.records.get(name) + } + + pub fn get_macros(&self) -> &FxHashMap { + &self.macros + } + + pub fn get_callbacks(&self) -> &FxHashMap { + &self.callbacks + } + + pub fn get_callback(&self, name: &NameArity) -> Option<&CallbackDef> { + self.callbacks.get(name) + } + + pub fn is_callback_optional(&self, name: &NameArity) -> bool { + self.optional_callbacks.contains(name) + } + + pub fn get_included_files(&self) -> impl Iterator + '_ { + self.included.iter().copied() + } + + fn is_empty(&self) -> bool { + self.included.is_empty() + && self.functions.is_empty() + && self.exported_functions.is_empty() + && self.types.is_empty() + && self.exported_types.is_empty() + && self.records.is_empty() + && self.callbacks.is_empty() + && self.macros.is_empty() + } + + fn merge(&mut self, other: &Self) { + self.included.extend(other.included.iter().cloned()); + self.functions.extend( + other + .functions + .iter() + .map(|(name, def)| (name.clone(), def.clone())), + ); + self.specs.extend( + other + .specs + .iter() + .map(|(name, def)| (name.clone(), def.clone())), + ); + self.exported_functions + .extend(other.exported_functions.iter().cloned()); + self.types.extend( + other + .types + .iter() + .map(|(name, def)| (name.clone(), def.clone())), + ); + self.exported_types + .extend(other.exported_types.iter().cloned()); + self.records.extend( + other + .records + .iter() + .map(|(name, def)| (name.clone(), def.clone())), + ); + self.callbacks.extend( + other + .callbacks + .iter() + .map(|(name, def)| (name.clone(), def.clone())), + ); + self.macros.extend( + other + .macros + .iter() + .map(|(name, def)| (name.clone(), def.clone())), + ); + self.deprecated.all |= other.deprecated.all; + self.deprecated + .functions + .extend(other.deprecated.functions.iter().cloned()); + self.deprecated + .fa + .extend(other.deprecated.fa.iter().cloned()); + } + + fn fixup_exports(&mut self) { + if self.export_all { + self.exported_functions = self.functions.keys().cloned().collect() + } + + for (name, fun_def) in self.functions.iter_mut() { + fun_def.exported |= self.export_all || self.exported_functions.contains(name) + } + + for (name, type_def) in self.types.iter_mut() { + type_def.exported |= self.exported_types.contains(name) + } + } + + fn fixup_deprecated(&mut self) { + for (name, fun_def) in self.functions.iter_mut() { + fun_def.deprecated |= self.deprecated.is_deprecated(name) + } + } + + fn fixup_callbacks(&mut self) { + for (name, callback) in self.callbacks.iter_mut() { + callback.optional |= self.optional_callbacks.contains(name); + } + } + + fn shrink_to_fit(&mut self) { + // Exhaustive match to require handling new fields. + let Self { + _c: _, + included, + functions, + specs, + deprecated, + exported_functions, + imported_functions, + types, + exported_types, + records, + callbacks, + macros, + export_all: _, + parse_transform: _, + optional_callbacks, + } = self; + + included.shrink_to_fit(); + functions.shrink_to_fit(); + specs.shrink_to_fit(); + exported_functions.shrink_to_fit(); + imported_functions.shrink_to_fit(); + types.shrink_to_fit(); + exported_types.shrink_to_fit(); + optional_callbacks.shrink_to_fit(); + records.shrink_to_fit(); + callbacks.shrink_to_fit(); + macros.shrink_to_fit(); + deprecated.shrink_to_fit(); + } +} + +#[cfg(test)] +mod tests { + use elp_base_db::fixture::WithFixture; + use expect_test::expect; + use expect_test::Expect; + + use super::*; + use crate::test_db::TestDB; + use crate::TypeAlias; + + fn check_functions(fixture: &str, expect: Expect) { + let (db, files) = TestDB::with_many_files(fixture); + let file_id = files[0]; + let def_map = db.def_map(file_id); + let mut resolved = def_map + .functions + .values() + .map(|def| { + format!( + "fun {} exported: {}", + def.function.name, + def.exported && def_map.exported_functions.contains(&def.function.name) + ) + }) + .chain(def_map.types.values().map(|def| match &def.type_alias { + TypeAlias::Regular { name, .. } => { + format!("-type {} exported: {}", name, def.exported) + } + TypeAlias::Opaque { name, .. } => { + format!("-opaque {} exported: {}", name, def.exported) + } + })) + .collect::>() + .join("\n"); + resolved.push('\n'); + expect.assert_eq(&resolved); + } + + fn check_callbacks(fixture: &str, expect: Expect) { + let (db, files) = TestDB::with_many_files(fixture); + let file_id = files[0]; + let def_map = db.def_map(file_id); + let mut resolved = def_map + .callbacks + .values() + .map(|def| { + format!( + "callback {} optional: {}", + def.callback.name, + def.optional && def_map.optional_callbacks.contains(&def.callback.name) + ) + }) + .collect::>() + .join("\n"); + resolved.push('\n'); + expect.assert_eq(&resolved); + } + + #[test] + fn exported_functions() { + check_functions( + r#" +-export([foo/1]). + +foo(_) -> ok. +bar() -> ok. +"#, + expect![[r#" + fun bar/0 exported: false + fun foo/1 exported: true + "#]], + ) + } + + #[test] + fn export_all_1() { + check_functions( + r#" +-compile(export_all). + +foo(_) -> ok. +bar() -> ok. +"#, + expect![[r#" + fun bar/0 exported: true + fun foo/1 exported: true + "#]], + ) + } + + #[test] + fn export_all_2() { + check_functions( + r#" +-compile({export_all}). + +foo(_) -> ok. +bar() -> ok. +"#, + expect![[r#" + fun bar/0 exported: true + fun foo/1 exported: true + "#]], + ) + } + + #[test] + fn export_all_3() { + check_functions( + r#" +-compile([export_all]). + +foo(_) -> ok. +bar() -> ok. +"#, + expect![[r#" + fun bar/0 exported: true + fun foo/1 exported: true + "#]], + ) + } + + #[test] + fn export_all_4() { + check_functions( + r#" +-compile([brief,export_all]). + +foo(_) -> ok. +bar() -> ok. +"#, + expect![[r#" + fun bar/0 exported: true + fun foo/1 exported: true + "#]], + ) + } + + #[test] + fn exported_types() { + check_functions( + r#" +-export_type([foo/1]). + +-type foo(A) :: ok. +-opaque bar() :: ok. +"#, + expect![[r#" + -opaque bar/0 exported: false + -type foo/1 exported: true + "#]], + ) + } + + #[test] + fn exported_types_post_definition() { + check_functions( + r#" +-opaque foo(A) :: ok. +-type bar() :: ok. + +-export_type([foo/1]). +"#, + expect![[r#" + -type bar/0 exported: false + -opaque foo/1 exported: true + "#]], + ) + } + + #[test] + fn exported_types_from_header() { + check_functions( + r#" +//- /module.erl +-include("header.hrl"). + +-export_type([foo/1]). +//- /header.hrl +-type foo(A) :: ok. +-type bar() :: ok. +"#, + expect![[r#" + -type bar/0 exported: false + -type foo/1 exported: true + "#]], + ) + } + + #[test] + fn export_functions_in_header() { + check_functions( + r#" +//- /module.erl +-include("header.hrl"). + +foo(_) -> ok. +bar() -> ok. +//- /header.hrl +-export([foo/1]). +"#, + expect![[r#" + fun bar/0 exported: false + fun foo/1 exported: true + "#]], + ) + } + + #[test] + fn optional_callback() { + check_callbacks( + r#" + //- /module.erl + -callback optional() -> ok. + -callback init(term()) -> ok. + -optional_callbacks([ + init/1 + ])."#, + expect![[r#" + callback optional/0 optional: false + callback init/1 optional: true + "#]], + ) + } +} diff --git a/crates/hir/src/diagnostics.rs b/crates/hir/src/diagnostics.rs new file mode 100644 index 0000000000..2dea9d8ff9 --- /dev/null +++ b/crates/hir/src/diagnostics.rs @@ -0,0 +1,36 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::fmt; + +use elp_syntax::TextRange; + +#[derive(Debug, PartialEq, Eq)] +pub struct Diagnostic { + pub location: TextRange, + pub message: DiagnosticMessage, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum DiagnosticMessage { + VarNameOutsideMacro, +} + +impl fmt::Display for DiagnosticMessage { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + DiagnosticMessage::VarNameOutsideMacro => { + write!( + f, + "using variable instead of an atom name is allowed only inside -define" + ) + } + } + } +} diff --git a/crates/hir/src/edoc.rs b/crates/hir/src/edoc.rs new file mode 100644 index 0000000000..ee4489b905 --- /dev/null +++ b/crates/hir/src/edoc.rs @@ -0,0 +1,417 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! Extract edoc comments from a file +//! +//! From https://www.erlang.org/doc/apps/edoc/chapter.html#introduction +//! Quote +//! EDoc lets you write the documentation of an Erlang program as comments +//! in the source code itself, using tags on the form "@Name ...". A +//! source file does not have to contain tags for EDoc to generate its +//! documentation, but without tags the result will only contain the basic +//! available information that can be extracted from the module. + +//! A tag must be the first thing on a comment line, except for leading +//! '%' characters and whitespace. The comment must be between program +//! declarations, and not on the same line as any program text. All the +//! following text - including consecutive comment lines - up until the +//! end of the comment or the next tagged line, is taken as the content of +//! the tag. + +//! Tags are associated with the nearest following program construct "of +//! significance" (the module name declaration and function +//! definitions). Other constructs are ignored. +//! End Quote + +use elp_base_db::FileId; +use elp_syntax::ast; +use elp_syntax::AstNode; +use elp_syntax::AstPtr; +use elp_syntax::SyntaxKind; +use elp_syntax::SyntaxNode; +use elp_syntax::TextRange; +use fxhash::FxHashMap; +use lazy_static::lazy_static; +use regex::Regex; + +use crate::db::MinDefDatabase; +use crate::InFileAstPtr; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EdocHeader { + form: InFileAstPtr, + tags: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EdocTag { + name: String, + comments: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EdocComment { + range: TextRange, + source: String, + syntax: InFileAstPtr, +} + +impl EdocHeader { + pub fn text_ranges(&self) -> Vec { + self.tags.iter().flat_map(|tag| tag.text_ranges()).collect() + } + + pub fn comments(&self) -> Vec> { + self.tags.iter().flat_map(|tag| tag.comments()).collect() + } + + pub fn params(&self) -> FxHashMap { + lazy_static! { + static ref RE: Regex = Regex::new(r"^%+\s+@param ([^\s]+)\s+(.*)$").unwrap(); + } + let mut res = FxHashMap::default(); + for source in self.sources_by_tag("param".to_string()) { + match RE.captures(&source) { + Some(captures) => { + if captures.len() == 3 { + res.insert(captures[1].to_string(), captures[2].to_string()); + } + } + None => (), + } + } + res + } + + pub fn sources_by_tag(&self, name: String) -> Vec { + self.tags + .iter() + .filter(|tag| tag.name == name) + .flat_map(|tag| tag.sources()) + .collect() + } +} + +impl EdocTag { + pub fn text_ranges(&self) -> Vec { + self.comments.iter().map(|comment| comment.range).collect() + } + pub fn comments(&self) -> Vec> { + self.comments.iter().map(|comment| comment.syntax).collect() + } + pub fn sources(&self) -> Vec { + self.comments + .iter() + .map(|comment| comment.source.clone()) + .collect() + } +} + +pub fn file_edoc_comments_query( + db: &dyn MinDefDatabase, + file_id: FileId, +) -> Option, EdocHeader>> { + let source = db.parse(file_id).tree(); + let mut res = FxHashMap::default(); + source.forms().into_iter().for_each(|f| { + if is_significant(f.syntax()) { + let mut comments: Vec<_> = prev_form_nodes(f.syntax()) + .filter(|syntax| { + syntax.kind() == elp_syntax::SyntaxKind::COMMENT && only_comment_on_line(syntax) + }) + .filter_map(ast::Comment::cast) + .collect(); + comments.reverse(); + if let Some(edoc) = + edoc_from_comments(InFileAstPtr::new(file_id, AstPtr::new(&f)), &comments) + { + res.insert(edoc.form, edoc); + } + } + }); + Some(res) +} + +fn edoc_from_comments( + form: InFileAstPtr, + comments: &Vec, +) -> Option { + let tags = comments + .into_iter() + .skip_while(|c| contains_edoc_tag(&c.syntax().text().to_string()).is_none()) + .map(|c| { + ( + contains_edoc_tag(&c.syntax().text().to_string()), + EdocComment { + range: c.syntax().text_range(), + source: c.syntax().text().to_string(), + syntax: InFileAstPtr::new(form.file_id(), AstPtr::new(c)), + }, + ) + }) + .fold(Vec::default(), |mut acc, (tag, comment)| { + if let Some(name) = tag { + acc.push(EdocTag { + name, + comments: vec![comment], + }); + } else { + if !&acc.is_empty() { + let mut t = acc[0].clone(); + t.comments.push(comment); + acc[0] = EdocTag { + name: t.name, + comments: t.comments, + } + } + } + acc + }); + if tags.is_empty() { + None + } else { + Some(EdocHeader { form, tags }) + } +} + +/// An edoc comment must be alone on a line, it cannot come after +/// code. +fn only_comment_on_line(comment: &SyntaxNode) -> bool { + // We check for a positive "other" found on the same line, in case + // the comment is the first line of the file, which will not have + // a preceding newline. + let mut node = comment + .siblings_with_tokens(elp_syntax::Direction::Prev) + .skip(1); // Starts with itself + + loop { + if let Some(node) = node.next() { + if let Some(tok) = node.into_token() { + if tok.kind() == SyntaxKind::WHITESPACE { + if tok.text().contains('\n') { + return true; + } + } + } else { + return false; + } + } else { + return true; + }; + } +} + +fn prev_form_nodes(syntax: &SyntaxNode) -> impl Iterator { + syntax + .siblings_with_tokens(elp_syntax::Direction::Prev) + .skip(1) // Starts with itself + .filter_map(|node_or_token| node_or_token.into_node()) + .take_while(|node| !is_significant(node)) +} + +/// Significant forms for edoc are functions and module declarations. +fn is_significant(node: &SyntaxNode) -> bool { + node.kind() == SyntaxKind::FUN_DECL || node.kind() == SyntaxKind::MODULE_ATTRIBUTE +} + +/// Check if the given comment starts with an edoc tag. +/// A tag must be the first thing on a comment line, except for leading +/// '%' characters and whitespace. +fn contains_edoc_tag(comment: &str) -> Option { + lazy_static! { + static ref RE: Regex = Regex::new(r"^%+\s+@([^\s]+).*$").unwrap(); + } + RE.captures_iter(comment).next().map(|c| c[1].to_string()) +} + +#[cfg(test)] +mod tests { + use elp_base_db::fixture::WithFixture; + use elp_syntax::ast; + use expect_test::expect; + use expect_test::Expect; + use fxhash::FxHashMap; + + use super::contains_edoc_tag; + use super::file_edoc_comments_query; + use super::EdocComment; + use super::EdocHeader; + use super::EdocTag; + use crate::test_db::TestDB; + use crate::InFileAstPtr; + + fn test_print(edoc: FxHashMap, EdocHeader>) -> String { + let mut buf = String::default(); + edoc.iter().for_each(|(_k, EdocHeader { form, tags })| { + buf.push_str(format!("{}\n", form.syntax_ptr_string()).as_str()); + tags.iter().for_each(|EdocTag { name, comments }| { + buf.push_str(format!(" {}\n", name).as_str()); + comments.iter().for_each( + |EdocComment { + range, + source, + syntax: _, + }| { + buf.push_str(format!(" {:?}: \"{}\"\n", range, source).as_str()); + }, + ); + }); + }); + buf + } + + #[track_caller] + fn check(fixture: &str, expected: Expect) { + let (db, fixture) = TestDB::with_fixture(fixture); + let file_id = fixture.files[0]; + let edocs = file_edoc_comments_query(&db, file_id); + expected.assert_eq(&test_print(edocs.unwrap())) + } + + #[test] + fn test_contains_annotation() { + expect![[r#" + Some( + "foo", + ) + "#]] + .assert_debug_eq(&contains_edoc_tag("%% @foo bar")); + } + + #[test] + fn edoc_1() { + check( + r#" + %% @doc blah + %% @param Foo ${2:Argument description} + %% @param Arg2 ${3:Argument description} + %% @returns ${4:Return description} + foo(Foo, some_atom) -> ok. +"#, + expect![[r#" + SyntaxNodePtr { range: 130..156, kind: FUN_DECL } + doc + 0..12: "%% @doc blah" + param + 13..52: "%% @param Foo ${2:Argument description}" + param + 53..93: "%% @param Arg2 ${3:Argument description}" + returns + 94..129: "%% @returns ${4:Return description}" + "#]], + ) + } + + #[test] + fn edoc_2() { + check( + r#" + %% Just a normal comment + %% @param Foo ${2:Argument description} + %% Does not have a tag + %% @returns ${4:Return description} + foo(Foo, some_atom) -> ok. +"#, + expect![[r#" + SyntaxNodePtr { range: 124..150, kind: FUN_DECL } + param + 25..64: "%% @param Foo ${2:Argument description}" + 65..87: "%% Does not have a tag" + returns + 88..123: "%% @returns ${4:Return description}" + "#]], + ) + } + + #[test] + fn edoc_must_be_alone_on_line() { + check( + r#" + bar() -> ok. %% @doc not an edoc comment + %% @tag is an edoc comment + foo(Foo, some_atom) -> ok. +"#, + expect![[r#" + SyntaxNodePtr { range: 68..94, kind: FUN_DECL } + tag + 41..67: "%% @tag is an edoc comment" + "#]], + ) + } + + #[test] + fn edoc_ignores_insignificant_forms() { + check( + r#" + %% @tag is an edoc comment + -compile(warn_missing_spec). + -include_lib("stdlib/include/assert.hrl"). + -define(X,3). + -export([foo/2]). + -import(erlang, []). + -type a_type() :: typ | false. + -export_type([a_type/0]). + -behaviour(gen_server). + -callback do_it(Typ :: a_type()) -> ok. + -spec foo(Foo :: type1(), type2()) -> ok. + -opaque client() :: #client{}. + -type client2() :: #client2{}. + -optional_callbacks([do_it/1]). + -record(state, {profile}). + -wild(attr). + %% Part of the same edoc + foo(Foo, some_atom) -> ok. +"#, + expect![[r#" + SyntaxNodePtr { range: 474..500, kind: FUN_DECL } + tag + 0..26: "%% @tag is an edoc comment" + 449..473: "%% Part of the same edoc" + "#]], + ) + } + + #[test] + fn edoc_module_attribute() { + check( + r#" + %% @tag is an edoc comment + -module(foo). +"#, + expect![[r#" + SyntaxNodePtr { range: 27..40, kind: MODULE_ATTRIBUTE } + tag + 0..26: "%% @tag is an edoc comment" + "#]], + ) + } + + #[test] + fn edoc_multiple() { + check( + r#" + %% @foo is an edoc comment + -module(foo). + + %% @doc fff is ... + fff() -> ok. + + %% @doc This will be ignored +"#, + expect![[r#" + SyntaxNodePtr { range: 61..73, kind: FUN_DECL } + doc + 42..60: "%% @doc fff is ..." + SyntaxNodePtr { range: 27..40, kind: MODULE_ATTRIBUTE } + foo + 0..26: "%% @foo is an edoc comment" + "#]], + ) + } +} diff --git a/crates/hir/src/expr.rs b/crates/hir/src/expr.rs new file mode 100644 index 0000000000..29b895a66e --- /dev/null +++ b/crates/hir/src/expr.rs @@ -0,0 +1,553 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_base_db::FileId; +pub use elp_syntax::ast::BinaryOp; +pub use elp_syntax::ast::MapOp; +pub use elp_syntax::ast::UnaryOp; +use elp_syntax::SmolStr; +use la_arena::Idx; + +use crate::sema; +use crate::Atom; +use crate::Body; +use crate::FunctionDef; +use crate::InFunctionBody; +use crate::RecordFieldId; +use crate::Semantic; +use crate::Var; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum AnyExprId { + Expr(ExprId), + Pat(PatId), + TypeExpr(TypeExprId), + Term(TermId), +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum AnyExprRef<'a> { + Expr(&'a Expr), + Pat(&'a Pat), + TypeExpr(&'a TypeExpr), + Term(&'a Term), +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum Literal { + String(String), + Char(char), + Atom(Atom), + Integer(i128), // TODO: bigints + Float(u64), // FIXME: f64 is not Eq +} + +impl Literal { + pub fn negate(&self) -> Option { + match self { + Literal::String(_) => None, + Literal::Atom(_) => None, + // Weird, but allowed https://github.com/erlang/otp/blob/09c601fa2183d4c545791ebcd68f869a5ab912a4/lib/stdlib/src/erl_parse.yrl#L1432 + Literal::Char(ch) => Some(Literal::Integer(-(*ch as i128))), + Literal::Integer(int) => Some(Literal::Integer(-int)), + Literal::Float(bits) => Some(Literal::Float((-f64::from_bits(*bits)).to_bits())), + } + } +} + +pub type ExprId = Idx; + +#[derive(Debug, Clone, Eq, PartialEq)] +/// A regular Erlang expression +pub enum Expr { + /// This is produced if the syntax tree does not have a required + /// expression piece, or it was in some way invalid + Missing, + Literal(Literal), + Var(Var), + Match { + lhs: PatId, + rhs: ExprId, + }, + Tuple { + exprs: Vec, + }, + List { + exprs: Vec, + tail: Option, + }, + Binary { + segs: Vec>, + }, + UnaryOp { + expr: ExprId, + op: UnaryOp, + }, + BinaryOp { + lhs: ExprId, + rhs: ExprId, + op: BinaryOp, + }, + Record { + name: Atom, + fields: Vec<(Atom, ExprId)>, + }, + RecordUpdate { + expr: ExprId, + name: Atom, + fields: Vec<(Atom, ExprId)>, + }, + RecordIndex { + name: Atom, + field: Atom, + }, + RecordField { + expr: ExprId, + name: Atom, + field: Atom, + }, + Map { + fields: Vec<(ExprId, ExprId)>, + }, + MapUpdate { + expr: ExprId, + fields: Vec<(ExprId, MapOp, ExprId)>, + }, + Catch { + expr: ExprId, + }, + MacroCall { + // This constructor captures the point a macro is expanded + // into an expression. This allows us to separately track the + // arguments, for things like highlight related, or unused + // function arguments. + expansion: ExprId, + args: Vec, + }, + Call { + target: CallTarget, + args: Vec, + }, + Comprehension { + builder: ComprehensionBuilder, + exprs: Vec, + }, + Block { + exprs: Vec, + }, + If { + clauses: Vec, + }, + Case { + expr: ExprId, + clauses: Vec, + }, + Receive { + clauses: Vec, + after: Option, + }, + Try { + exprs: Vec, + of_clauses: Vec, + catch_clauses: Vec, + after: Vec, + }, + CaptureFun { + target: CallTarget, + arity: ExprId, + }, + Closure { + clauses: Vec, + name: Option, + }, + Maybe { + exprs: Vec, + else_clauses: Vec, + }, +} + +impl Expr { + pub fn as_atom(&self) -> Option { + match self { + Expr::Literal(Literal::Atom(atom)) => Some(*atom), + _ => None, + } + } + + pub fn as_var(&self) -> Option { + match self { + Expr::Var(var) => Some(*var), + _ => None, + } + } + + pub fn list_length(&self) -> Option { + match &self { + Expr::List { exprs, tail } => { + // Deal with a simple list only. + if tail.is_some() { + None + } else { + Some(exprs.len()) + } + } + _ => None, + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum MaybeExpr { + Cond { lhs: PatId, rhs: ExprId }, + Expr(ExprId), +} + +pub type ClauseId = Idx; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Clause { + pub pats: Vec, + pub guards: Vec>, + pub exprs: Vec, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct CRClause { + pub pat: PatId, + pub guards: Vec>, + pub exprs: Vec, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct IfClause { + pub guards: Vec>, + pub exprs: Vec, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct CatchClause { + pub class: Option, + pub reason: PatId, + pub stack: Option, + pub guards: Vec>, + pub exprs: Vec, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct RecordFieldBody { + pub field_id: RecordFieldId, + pub expr: Option, + pub ty: Option, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct ReceiveAfter { + pub timeout: ExprId, + pub exprs: Vec, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum CallTarget { + Local { name: Id }, + Remote { module: Id, name: Id }, +} + +impl CallTarget { + pub fn resolve_call( + &self, + arity: u32, + sema: &Semantic, + file_id: FileId, + body: &Body, + ) -> Option { + sema::to_def::resolve_call_target(sema, self, arity, file_id, body) + } + + pub fn label(&self, arity: u32, sema: &Semantic, body: &Body) -> Option { + match self { + CallTarget::Local { name } => { + let name = sema.db.lookup_atom(body[*name].as_atom()?); + Some(SmolStr::new(format!("{name}/{arity}"))) + } + CallTarget::Remote { module, name } => { + let name = sema.db.lookup_atom(body[*name].as_atom()?); + let module = sema.db.lookup_atom(body[*module].as_atom()?); + Some(SmolStr::new(format!("{module}:{name}/{arity}",))) + } + } + } + + pub fn label_short(&self, sema: &Semantic, body: &Body) -> Option { + match self { + CallTarget::Local { name } => { + let name = sema.db.lookup_atom(body[*name].as_atom()?); + Some(SmolStr::new(format!("{name}"))) + } + CallTarget::Remote { module, name } => { + let name = sema.db.lookup_atom(body[*name].as_atom()?); + let module = sema.db.lookup_atom(body[*module].as_atom()?); + Some(SmolStr::new(format!("{module}:{name}",))) + } + } + } + + pub fn is_module_fun( + &self, + sema: &Semantic, + def_fb: &InFunctionBody<&FunctionDef>, + module_name: crate::Name, + fun_name: crate::Name, + ) -> bool { + match self { + CallTarget::Local { name: _ } => false, + CallTarget::Remote { module, name } => { + sema.is_atom_named(&def_fb[*module], module_name) + && sema.is_atom_named(&def_fb[*name], fun_name) + } + } + } +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum ComprehensionBuilder { + List(ExprId), + Binary(ExprId), + Map(ExprId, ExprId), +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum ComprehensionExpr { + BinGenerator { + pat: PatId, + expr: ExprId, + }, + ListGenerator { + pat: PatId, + expr: ExprId, + }, + MapGenerator { + key: PatId, + value: PatId, + expr: ExprId, + }, + Expr(ExprId), +} + +pub type PatId = Idx; + +#[derive(Debug, Clone, Eq, PartialEq)] +/// A regular Erlang pattern +pub enum Pat { + Missing, + Literal(Literal), + Var(Var), + Match { + lhs: PatId, + rhs: PatId, + }, + Tuple { + pats: Vec, + }, + List { + pats: Vec, + tail: Option, + }, + Binary { + segs: Vec>, + }, + UnaryOp { + pat: PatId, + op: UnaryOp, + }, + BinaryOp { + lhs: PatId, + rhs: PatId, + op: BinaryOp, + }, + Record { + name: Atom, + fields: Vec<(Atom, PatId)>, + }, + RecordIndex { + name: Atom, + field: Atom, + }, + /// map keys in patterns are allowed to be a subset of expressions + Map { + fields: Vec<(ExprId, PatId)>, + }, + MacroCall { + // This constructor captures the point a macro is expanded + // into an expression. This allows us to separately track the + // arguments, for things like highlight related, or unused + // function arguments. + expansion: PatId, + args: Vec, + }, +} + +impl Pat { + pub fn as_var(&self) -> Option { + match self { + Pat::Var(var) => Some(*var), + _ => None, + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct BinarySeg { + pub elem: Val, + pub size: Option, + // TODO we might want to normalise this, but it's pretty complex + // See logic in https://github.com/erlang/otp/blob/master/lib/stdlib/src/erl_bits.erl + pub tys: Vec, + pub unit: Option, +} + +impl BinarySeg { + pub fn with_value(&self, value: U) -> BinarySeg { + BinarySeg { + elem: value, + size: self.size, + tys: self.tys.clone(), + unit: self.unit, + } + } + + pub fn map U, U>(self, f: F) -> BinarySeg { + BinarySeg { + elem: f(self.elem), + size: self.size, + tys: self.tys, + unit: self.unit, + } + } +} + +pub type TermId = Idx; + +#[derive(Debug, Clone, Eq, PartialEq)] +/// A limited expression translated as a constant term, e.g. in module attributes +pub enum Term { + Missing, + Literal(Literal), + Binary(Vec), + Tuple { + exprs: Vec, + }, + List { + exprs: Vec, + tail: Option, + }, + Map { + fields: Vec<(TermId, TermId)>, + }, + CaptureFun { + module: Atom, + name: Atom, + arity: u32, + }, + MacroCall { + // This constructor captures the point a macro is expanded + // into an expression. This allows us to separately track the + // arguments, for things like highlight related, or unused + // function arguments. + expansion: TermId, + args: Vec, + }, +} + +pub type TypeExprId = Idx; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum TypeExpr { + AnnType { + var: Var, + ty: TypeExprId, + }, + BinaryOp { + lhs: TypeExprId, + rhs: TypeExprId, + op: BinaryOp, + }, + Call { + target: CallTarget, + args: Vec, + }, + Fun(FunType), + List(ListType), + Literal(Literal), + Map { + fields: Vec<(TypeExprId, MapOp, TypeExprId)>, + }, + Missing, + Union { + types: Vec, + }, + Range { + lhs: TypeExprId, + rhs: TypeExprId, + }, + Record { + name: Atom, + fields: Vec<(Atom, TypeExprId)>, + }, + Tuple { + args: Vec, + }, + UnaryOp { + type_expr: TypeExprId, + op: UnaryOp, + }, + Var(Var), + MacroCall { + // This constructor captures the point a macro is expanded + // into an expression. This allows us to separately track the + // arguments, for things like highlight related, or unused + // function arguments. + expansion: TypeExprId, + args: Vec, + }, +} + +impl TypeExpr { + pub fn as_atom(&self) -> Option { + match self { + TypeExpr::Literal(Literal::Atom(atom)) => Some(*atom), + _ => None, + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum FunType { + Any, + AnyArgs { + result: TypeExprId, + }, + Full { + params: Vec, + result: TypeExprId, + }, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum ListType { + Empty, + Regular(TypeExprId), + NonEmpty(TypeExprId), +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct SpecSig { + pub args: Vec, + pub result: TypeExprId, + pub guards: Vec<(Var, TypeExprId)>, +} diff --git a/crates/hir/src/fold.rs b/crates/hir/src/fold.rs new file mode 100644 index 0000000000..defe37fef1 --- /dev/null +++ b/crates/hir/src/fold.rs @@ -0,0 +1,939 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! Ability to traverse over the hir ast computing a result + +use std::ops::Index; + +use crate::body::UnexpandedIndex; +use crate::expr::MaybeExpr; +use crate::Body; +use crate::CRClause; +use crate::CallTarget; +use crate::Clause; +use crate::ComprehensionBuilder; +use crate::ComprehensionExpr; +use crate::Expr; +use crate::ExprId; +use crate::Pat; +use crate::PatId; +use crate::Term; +use crate::TermId; +use crate::TypeExpr; +use crate::TypeExprId; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum On { + Entry, + Exit, +} + +#[derive(Debug)] +pub struct ExprCallBackCtx { + pub on: On, + pub in_macro: Option, + pub expr_id: ExprId, + pub expr: Expr, +} + +#[derive(Debug)] +pub struct PatCallBackCtx { + pub on: On, + pub in_macro: Option, + pub pat_id: PatId, + pub pat: Pat, +} + +#[derive(Debug)] +pub struct TermCallBackCtx { + pub on: On, + pub in_macro: Option, + pub term_id: TermId, + pub term: Term, +} + +pub type ExprCallBack<'a, T> = &'a mut dyn FnMut(T, ExprCallBackCtx) -> T; +pub type PatCallBack<'a, T> = &'a mut dyn FnMut(T, PatCallBackCtx) -> T; +pub type TermCallBack<'a, T> = &'a mut dyn FnMut(T, TermCallBackCtx) -> T; + +fn noop_expr_callback(acc: T, _ctx: ExprCallBackCtx) -> T { + acc +} +fn noop_pat_callback(acc: T, _ctx: PatCallBackCtx) -> T { + acc +} +fn noop_term_callback(acc: T, _ctx: TermCallBackCtx) -> T { + acc +} + +pub struct FoldCtx<'a, T> { + body: &'a FoldBody<'a>, + strategy: Strategy, + macro_stack: Vec, + for_expr: ExprCallBack<'a, T>, + for_pat: PatCallBack<'a, T>, + for_term: TermCallBack<'a, T>, +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum Strategy { + TopDown, + BottomUp, + Both, +} + +#[derive(Debug)] +pub enum FoldBody<'a> { + Body(&'a Body), + UnexpandedIndex(UnexpandedIndex<'a>), +} + +impl<'a, T> FoldCtx<'a, T> { + pub fn fold_expr( + body: &'a Body, + strategy: Strategy, + expr_id: ExprId, + initial: T, + for_expr: ExprCallBack<'a, T>, + for_pat: PatCallBack<'a, T>, + ) -> T { + FoldCtx { + body: &FoldBody::Body(body), + strategy, + macro_stack: Vec::default(), + for_expr, + for_pat, + for_term: &mut noop_term_callback, + } + .do_fold_expr(expr_id, initial) + } + + pub fn fold_pat( + body: &'a Body, + strategy: Strategy, + pat_id: PatId, + initial: T, + for_expr: ExprCallBack<'a, T>, + for_pat: PatCallBack<'a, T>, + ) -> T { + FoldCtx { + body: &FoldBody::Body(body), + strategy, + macro_stack: Vec::default(), + for_expr, + for_pat, + for_term: &mut noop_term_callback, + } + .do_fold_pat(pat_id, initial) + } + + fn in_macro(&self) -> Option { + if let Some(expr_id) = self.macro_stack.first() { + Some(*expr_id) + } else { + None + } + } + + pub fn fold_expr_foldbody( + body: &'a FoldBody<'a>, + strategy: Strategy, + expr_id: ExprId, + initial: T, + for_expr: ExprCallBack<'a, T>, + for_pat: PatCallBack<'a, T>, + ) -> T { + FoldCtx { + body, + strategy, + macro_stack: Vec::default(), + for_expr, + for_pat, + for_term: &mut noop_term_callback, + } + .do_fold_expr(expr_id, initial) + } + + pub fn fold_term( + body: &'a Body, + strategy: Strategy, + term_id: TermId, + initial: T, + for_term: TermCallBack<'a, T>, + ) -> T { + FoldCtx { + body: &FoldBody::Body(body), + strategy, + macro_stack: Vec::default(), + for_expr: &mut noop_expr_callback, + for_pat: &mut noop_pat_callback, + for_term, + } + .do_fold_term(term_id, initial) + } + + // ----------------------------------------------------------------- + + fn do_fold_expr(&mut self, expr_id: ExprId, initial: T) -> T { + let expr = &self.body[expr_id]; + let ctx = ExprCallBackCtx { + on: On::Entry, + in_macro: self.in_macro(), + expr_id, + expr: expr.clone(), + }; + let acc = match self.strategy { + Strategy::TopDown | Strategy::Both => (self.for_expr)(initial, ctx), + _ => initial, + }; + let r = match expr { + crate::Expr::Missing => acc, + crate::Expr::Literal(_) => acc, + crate::Expr::Var(_) => acc, + crate::Expr::Match { lhs, rhs } => { + let r = self.do_fold_pat(*lhs, acc); + self.do_fold_expr(*rhs, r) + } + crate::Expr::Tuple { exprs } => self.fold_exprs(exprs, acc), + crate::Expr::List { exprs, tail } => { + let r = self.fold_exprs(exprs, acc); + if let Some(expr_id) = tail { + self.do_fold_expr(*expr_id, r) + } else { + r + } + } + crate::Expr::Binary { segs } => segs.iter().fold(acc, |acc, binary_seg| { + let mut r = self.do_fold_expr(binary_seg.elem, acc); + if let Some(expr_id) = binary_seg.size { + r = self.do_fold_expr(expr_id, r); + } + r + }), + crate::Expr::UnaryOp { expr, op: _ } => self.do_fold_expr(*expr, acc), + crate::Expr::BinaryOp { lhs, rhs, op: _ } => { + let r = self.do_fold_expr(*lhs, acc); + self.do_fold_expr(*rhs, r) + } + crate::Expr::Record { name: _, fields } => fields + .iter() + .fold(acc, |acc, (_, field)| self.do_fold_expr(*field, acc)), + crate::Expr::RecordUpdate { + expr, + name: _, + fields, + } => { + let r = self.do_fold_expr(*expr, acc); + fields + .iter() + .fold(r, |acc, (_, field)| self.do_fold_expr(*field, acc)) + } + crate::Expr::RecordIndex { name: _, field: _ } => acc, + crate::Expr::RecordField { + expr, + name: _, + field: _, + } => self.do_fold_expr(*expr, acc), + crate::Expr::Map { fields } => fields.iter().fold(acc, |acc, (k, v)| { + let r = self.do_fold_expr(*k, acc); + self.do_fold_expr(*v, r) + }), + crate::Expr::MapUpdate { expr, fields } => { + let r = self.do_fold_expr(*expr, acc); + fields.iter().fold(r, |acc, (lhs, _op, rhs)| { + let r = self.do_fold_expr(*lhs, acc); + self.do_fold_expr(*rhs, r) + }) + } + crate::Expr::Catch { expr } => self.do_fold_expr(*expr, acc), + crate::Expr::MacroCall { expansion, args: _ } => { + self.macro_stack.push(expr_id); + let r = self.do_fold_expr(*expansion, acc); + self.macro_stack.pop(); + r + } + crate::Expr::Call { target, args } => { + let r = match target { + CallTarget::Local { name } => self.do_fold_expr(*name, acc), + CallTarget::Remote { module, name } => { + let r = self.do_fold_expr(*module, acc); + self.do_fold_expr(*name, r) + } + }; + args.iter().fold(r, |acc, arg| self.do_fold_expr(*arg, acc)) + } + crate::Expr::Comprehension { builder, exprs } => match builder { + ComprehensionBuilder::List(expr) => self.fold_comprehension(expr, exprs, acc), + ComprehensionBuilder::Binary(expr) => self.fold_comprehension(expr, exprs, acc), + ComprehensionBuilder::Map(key, value) => { + let r = self.fold_comprehension(key, exprs, acc); + self.fold_comprehension(value, exprs, r) + } + }, + crate::Expr::Block { exprs } => exprs + .iter() + .fold(acc, |acc, expr_id| self.do_fold_expr(*expr_id, acc)), + crate::Expr::If { clauses } => clauses.iter().fold(acc, |acc, clause| { + let r = clause.guards.iter().fold(acc, |acc, exprs| { + exprs + .iter() + .fold(acc, |acc, expr| self.do_fold_expr(*expr, acc)) + }); + clause + .exprs + .iter() + .fold(r, |acc, expr| self.do_fold_expr(*expr, acc)) + }), + crate::Expr::Case { expr, clauses } => { + let r = self.do_fold_expr(*expr, acc); + self.fold_cr_clause(clauses, r) + } + crate::Expr::Receive { clauses, after } => { + let mut r = self.fold_cr_clause(clauses, acc); + if let Some(after) = after { + r = self.do_fold_expr(after.timeout, r); + r = self.fold_exprs(&after.exprs, r); + }; + r + } + crate::Expr::Try { + exprs, + of_clauses, + catch_clauses, + after, + } => { + let r = exprs + .iter() + .fold(acc, |acc, expr| self.do_fold_expr(*expr, acc)); + let mut r = self.fold_cr_clause(of_clauses, r); + r = catch_clauses.iter().fold(r, |acc, clause| { + let mut r = acc; + if let Some(pat_id) = clause.class { + r = self.do_fold_pat(pat_id, r); + } + r = self.do_fold_pat(clause.reason, r); + if let Some(pat_id) = clause.stack { + r = self.do_fold_pat(pat_id, r); + } + + r = clause + .guards + .iter() + .fold(r, |acc, exprs| self.fold_exprs(exprs, acc)); + clause + .exprs + .iter() + .fold(r, |acc, expr| self.do_fold_expr(*expr, acc)) + }); + after + .iter() + .fold(r, |acc, expr| self.do_fold_expr(*expr, acc)) + } + crate::Expr::CaptureFun { target, arity } => { + let r = match target { + CallTarget::Local { name } => self.do_fold_expr(*name, acc), + CallTarget::Remote { module, name } => { + let r = self.do_fold_expr(*module, acc); + self.do_fold_expr(*name, r) + } + }; + self.do_fold_expr(*arity, r) + } + crate::Expr::Closure { clauses, name: _ } => clauses.iter().fold( + acc, + |acc, + Clause { + pats, + guards, + exprs, + }| { + let mut r = pats + .iter() + .fold(acc, |acc, pat_id| self.do_fold_pat(*pat_id, acc)); + r = guards + .iter() + .fold(r, |acc, exprs| self.fold_exprs(exprs, acc)); + self.fold_exprs(&exprs, r) + }, + ), + Expr::Maybe { + exprs, + else_clauses, + } => { + let r = exprs.iter().fold(acc, |acc, expr| match expr { + MaybeExpr::Cond { lhs, rhs } => { + let r = self.do_fold_pat(*lhs, acc); + self.do_fold_expr(*rhs, r) + } + MaybeExpr::Expr(expr) => self.do_fold_expr(*expr, acc), + }); + self.fold_cr_clause(else_clauses, r) + } + }; + match self.strategy { + Strategy::BottomUp | Strategy::Both => { + let ctx = ExprCallBackCtx { + on: On::Exit, + in_macro: self.in_macro(), + expr_id, + expr: expr.clone(), + }; + (self.for_expr)(r, ctx) + } + _ => r, + } + } + + fn do_fold_pat(&mut self, pat_id: PatId, initial: T) -> T { + let pat = &self.body[pat_id]; + let ctx = PatCallBackCtx { + on: On::Entry, + in_macro: self.in_macro(), + pat_id, + pat: pat.clone(), + }; + let acc = match self.strategy { + Strategy::TopDown | Strategy::Both => (self.for_pat)(initial, ctx), + _ => initial, + }; + let r = match &pat { + crate::Pat::Missing => acc, + crate::Pat::Literal(_) => acc, + crate::Pat::Var(_) => acc, + crate::Pat::Match { lhs, rhs } => { + let r = self.do_fold_pat(*lhs, acc); + self.do_fold_pat(*rhs, r) + } + crate::Pat::Tuple { pats } => self.fold_pats(pats, acc), + crate::Pat::List { pats, tail } => { + let mut r = self.fold_pats(pats, acc); + if let Some(pat_id) = tail { + r = self.do_fold_pat(*pat_id, r); + }; + r + } + crate::Pat::Binary { segs } => segs.iter().fold(acc, |acc, binary_seg| { + let mut r = self.do_fold_pat(binary_seg.elem, acc); + if let Some(expr_id) = binary_seg.size { + r = self.do_fold_expr(expr_id, r); + } + r + }), + crate::Pat::UnaryOp { pat, op: _ } => self.do_fold_pat(*pat, acc), + crate::Pat::BinaryOp { lhs, rhs, op: _ } => { + let r = self.do_fold_pat(*lhs, acc); + self.do_fold_pat(*rhs, r) + } + crate::Pat::Record { name: _, fields } => fields + .iter() + .fold(acc, |acc, (_, field)| self.do_fold_pat(*field, acc)), + crate::Pat::RecordIndex { name: _, field: _ } => acc, + crate::Pat::Map { fields } => fields.iter().fold(acc, |acc, (k, v)| { + let r = self.do_fold_expr(*k, acc); + self.do_fold_pat(*v, r) + }), + crate::Pat::MacroCall { expansion, args } => { + let r = self.do_fold_pat(*expansion, acc); + args.iter().fold(r, |acc, arg| self.do_fold_expr(*arg, acc)) + } + }; + + match self.strategy { + Strategy::BottomUp | Strategy::Both => { + let ctx = PatCallBackCtx { + on: On::Exit, + in_macro: self.in_macro(), + pat_id, + pat: pat.clone(), + }; + (self.for_pat)(r, ctx) + } + _ => r, + } + } + + fn fold_exprs(&mut self, exprs: &[ExprId], initial: T) -> T { + exprs + .iter() + .fold(initial, |acc, expr_id| self.do_fold_expr(*expr_id, acc)) + } + + fn fold_pats(&mut self, pats: &[PatId], initial: T) -> T { + pats.iter() + .fold(initial, |acc, expr_id| self.do_fold_pat(*expr_id, acc)) + } + + fn fold_cr_clause(&mut self, clauses: &[CRClause], initial: T) -> T { + clauses.iter().fold(initial, |acc, clause| { + let mut r = self.do_fold_pat(clause.pat, acc); + r = clause.guards.iter().fold(r, |acc, exprs| { + exprs + .iter() + .fold(acc, |acc, expr| self.do_fold_expr(*expr, acc)) + }); + clause + .exprs + .iter() + .fold(r, |acc, expr| self.do_fold_expr(*expr, acc)) + }) + } + + fn fold_comprehension(&mut self, expr: &ExprId, exprs: &[ComprehensionExpr], initial: T) -> T { + let r = self.do_fold_expr(*expr, initial); + exprs + .iter() + .fold(r, |acc, comprehension_expr| match comprehension_expr { + ComprehensionExpr::BinGenerator { pat, expr } => { + let r = self.do_fold_pat(*pat, acc); + self.do_fold_expr(*expr, r) + } + ComprehensionExpr::ListGenerator { pat, expr } => { + let r = self.do_fold_pat(*pat, acc); + self.do_fold_expr(*expr, r) + } + ComprehensionExpr::Expr(expr) => self.do_fold_expr(*expr, acc), + ComprehensionExpr::MapGenerator { key, value, expr } => { + let r = self.do_fold_pat(*key, acc); + let r = self.do_fold_pat(*value, r); + self.do_fold_expr(*expr, r) + } + }) + } + + pub fn do_fold_term(&mut self, term_id: TermId, initial: T) -> T { + let term = &self.body[term_id]; + let ctx = TermCallBackCtx { + on: On::Entry, + in_macro: self.in_macro(), + term_id, + term: term.clone(), + }; + let acc = match self.strategy { + Strategy::TopDown | Strategy::Both => (self.for_term)(initial, ctx), + _ => initial, + }; + let r = match &term { + crate::Term::Missing => acc, + crate::Term::Literal(_) => acc, + crate::Term::Binary(_) => acc, // Limited translation of binaries in terms + crate::Term::Tuple { exprs } => self.do_fold_terms(exprs, acc), + crate::Term::List { exprs, tail } => { + let r = self.do_fold_terms(exprs, acc); + if let Some(term_id) = tail { + self.do_fold_term(*term_id, r) + } else { + r + } + } + crate::Term::Map { fields } => fields.iter().fold(acc, |acc, (k, v)| { + let r = self.do_fold_term(*k, acc); + self.do_fold_term(*v, r) + }), + crate::Term::CaptureFun { + module: _, + name: _, + arity: _, + } => acc, + crate::Term::MacroCall { expansion, args: _ } => { + let r = self.do_fold_term(*expansion, acc); + // We ignore the args for now + r + } + }; + match self.strategy { + Strategy::BottomUp | Strategy::Both => { + let ctx = TermCallBackCtx { + on: On::Exit, + in_macro: self.in_macro(), + term_id, + term: term.clone(), + }; + (self.for_term)(r, ctx) + } + _ => r, + } + } + + fn do_fold_terms(&mut self, terms: &[TermId], initial: T) -> T { + terms + .iter() + .fold(initial, |acc, expr_id| self.do_fold_term(*expr_id, acc)) + } +} + +// --------------------------------------------------------------------- +// Index impls FoldBody + +impl<'a> Index for FoldBody<'a> { + type Output = Expr; + + fn index(&self, index: ExprId) -> &Self::Output { + match self { + FoldBody::Body(body) => body.index(index), + FoldBody::UnexpandedIndex(body) => body.index(index), + } + } +} + +impl<'a> Index for FoldBody<'a> { + type Output = Pat; + + fn index(&self, index: PatId) -> &Self::Output { + match self { + FoldBody::Body(body) => body.index(index), + FoldBody::UnexpandedIndex(body) => body.index(index), + } + } +} + +impl<'a> Index for FoldBody<'a> { + type Output = TypeExpr; + + fn index(&self, index: TypeExprId) -> &Self::Output { + match self { + FoldBody::Body(body) => body.index(index), + FoldBody::UnexpandedIndex(body) => body.index(index), + } + } +} + +impl<'a> Index for FoldBody<'a> { + type Output = Term; + + fn index(&self, index: TermId) -> &Self::Output { + match self { + FoldBody::Body(body) => body.index(index), + FoldBody::UnexpandedIndex(body) => body.index(index), + } + } +} + +// --------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use elp_base_db::fixture::WithFixture; + use elp_syntax::algo; + use elp_syntax::ast; + use elp_syntax::AstNode; + use expect_test::expect; + use expect_test::Expect; + use la_arena::Idx; + use la_arena::RawIdx; + + use super::FoldBody; + use crate::body::UnexpandedIndex; + use crate::expr::ClauseId; + use crate::fold::FoldCtx; + use crate::fold::Strategy; + use crate::sema::WithMacros; + use crate::test_db::TestDB; + use crate::AnyExprRef; + use crate::Atom; + use crate::Expr; + use crate::FunctionBody; + use crate::InFile; + use crate::Literal; + use crate::Pat; + use crate::Semantic; + use crate::Term; + use crate::TypeExpr; + + fn to_atom(sema: &Semantic<'_>, ast: InFile<&ast::Atom>) -> Option { + let (body, body_map) = sema.find_body(ast.file_id, ast.value.syntax())?; + let expr = ast.map(|atom| ast::Expr::from(ast::ExprMax::from(atom.clone()))); + let any_expr_id = body_map.any_id(expr.as_ref())?; + let atom = match body.get_any(any_expr_id) { + AnyExprRef::Expr(Expr::Literal(Literal::Atom(atom))) => atom, + AnyExprRef::Pat(Pat::Literal(Literal::Atom(atom))) => atom, + AnyExprRef::TypeExpr(TypeExpr::Literal(Literal::Atom(atom))) => atom, + AnyExprRef::Term(Term::Literal(Literal::Atom(atom))) => atom, + _ => return None, + }; + + Some(atom.clone()) + } + + #[test] + fn traverse_expr() { + let fixture_str = r#" +bar() -> + begin + A = B + 3, + [A|A], + Y = ~A, + catch A, + begin + A, + Y = 6 + end, + A + end. +"#; + + let (db, file_id, range_or_offset) = TestDB::with_range_or_offset(fixture_str); + let sema = Semantic::new(&db); + let offset = match range_or_offset { + elp_base_db::fixture::RangeOrOffset::Range(_) => panic!(), + elp_base_db::fixture::RangeOrOffset::Offset(o) => o, + }; + let in_file = sema.parse(file_id); + let source_file = in_file.value; + let ast_var = algo::find_node_at_offset::(source_file.syntax(), offset).unwrap(); + + let (body, body_map) = FunctionBody::function_body_with_source_query( + &db, + InFile { + file_id, + value: Idx::from_raw(RawIdx::from(0)), + }, + ); + + let expr = ast::Expr::ExprMax(ast::ExprMax::Var(ast_var.clone())); + let expr_id = body_map + .expr_id(InFile { + file_id, + value: &expr, + }) + .unwrap(); + let expr = &body.body[expr_id]; + let hir_var = match expr { + crate::Expr::Var(v) => v, + _ => panic!(), + }; + let idx = ClauseId::from_raw(RawIdx::from(0)); + let r: u32 = FoldCtx::fold_expr( + &body.body, + Strategy::TopDown, + body.clauses[idx].exprs[0], + 0, + &mut |acc, ctx| match ctx.expr { + crate::Expr::Var(v) => { + if &v == hir_var { + acc + 1 + } else { + acc + } + } + _ => acc, + }, + &mut |acc, ctx| match ctx.pat { + crate::Pat::Var(v) => { + if &v == hir_var { + acc + 1 + } else { + acc + } + } + _ => acc, + }, + ); + + // There are 7 occurrences of the Var "A" in the code example + expect![[r#" + 7 + "#]] + .assert_debug_eq(&r); + expect![[r#" + Var { + syntax: VAR@51..52 + VAR@51..52 "A" + , + } + "#]] + .assert_debug_eq(&ast_var); + } + + #[test] + fn traverse_term() { + let fixture_str = r#" +-compile([{f~oo,bar},[baz, {foo}]]). +"#; + + let (db, file_id, range_or_offset) = TestDB::with_range_or_offset(fixture_str); + let sema = Semantic::new(&db); + let offset = match range_or_offset { + elp_base_db::fixture::RangeOrOffset::Range(_) => panic!(), + elp_base_db::fixture::RangeOrOffset::Offset(o) => o, + }; + let in_file = sema.parse(file_id); + let source_file = in_file.value; + let ast_atom = + algo::find_node_at_offset::(source_file.syntax(), offset).unwrap(); + let hir_atom = to_atom(&sema, InFile::new(file_id, &ast_atom)).unwrap(); + + let form_list = sema.db.file_form_list(file_id); + let (idx, _) = form_list.compile_attributes().next().unwrap(); + let compiler_options = sema.db.compile_body(InFile::new(file_id, idx)); + let r = FoldCtx::fold_term( + &compiler_options.body, + Strategy::TopDown, + compiler_options.value, + 0, + &mut |acc, ctx| match &ctx.term { + crate::Term::Literal(Literal::Atom(atom)) => { + if atom == &hir_atom { + acc + 1 + } else { + acc + } + } + _ => acc, + }, + ); + + // There are 2 occurrences of the atom 'foo' in the code example + expect![[r#" + 2 + "#]] + .assert_debug_eq(&r); + expect![[r#" + Atom { + syntax: ATOM@11..14 + ATOM@11..14 "foo" + , + } + "#]] + .assert_debug_eq(&ast_atom); + } + + #[track_caller] + fn check_macros( + with_macros: WithMacros, + fixture_str: &str, + tree_expect: Expect, + r_expect: Expect, + ) { + let (db, file_id, range_or_offset) = TestDB::with_range_or_offset(fixture_str); + let sema = Semantic::new(&db); + let offset = match range_or_offset { + elp_base_db::fixture::RangeOrOffset::Range(_) => panic!(), + elp_base_db::fixture::RangeOrOffset::Offset(o) => o, + }; + let in_file = sema.parse(file_id); + let source_file = in_file.value; + let ast_atom = + algo::find_node_at_offset::(source_file.syntax(), offset).unwrap(); + let hir_atom = to_atom(&sema, InFile::new(file_id, &ast_atom)).unwrap(); + + let form_list = sema.db.file_form_list(file_id); + let (idx, _) = form_list.functions().next().unwrap(); + let compiler_options = sema.db.function_body(InFile::new(file_id, idx)); + + let idx = ClauseId::from_raw(RawIdx::from(0)); + + let fold_body = if with_macros == WithMacros::Yes { + FoldBody::UnexpandedIndex(UnexpandedIndex(&compiler_options.body)) + } else { + FoldBody::Body(&compiler_options.body) + }; + let r = FoldCtx::fold_expr_foldbody( + &fold_body, + Strategy::TopDown, + compiler_options.clauses[idx].exprs[0], + (0, 0), + &mut |(in_macro, not_in_macro), ctx| match ctx.expr { + crate::Expr::Literal(Literal::Atom(atom)) => { + if atom == hir_atom { + if ctx.in_macro.is_some() { + (in_macro + 1, not_in_macro) + } else { + (in_macro, not_in_macro + 1) + } + } else { + (in_macro, not_in_macro) + } + } + _ => (in_macro, not_in_macro), + }, + &mut |(in_macro, not_in_macro), ctx| match ctx.pat { + _ => (in_macro, not_in_macro), + }, + ); + tree_expect.assert_eq(&compiler_options.tree_print(&db)); + + r_expect.assert_debug_eq(&r); + } + + #[test] + fn macro_aware() { + check_macros( + WithMacros::Yes, + r#" + -define(AA(X), {X,foo}). + bar() -> + begin %% clause.exprs[0] + ?AA(f~oo), + {foo} + end. + "#, + expect![[r#" + + Clause { + pats + guards + exprs + Expr::Block { + Expr::Tuple { + Literal(Atom('foo')), + Literal(Atom('foo')), + }, + Expr::Tuple { + Literal(Atom('foo')), + }, + }, + }. + "#]], + expect![[r#" + ( + 2, + 1, + ) + "#]], + ) + } + + #[test] + fn ignore_macros() { + check_macros( + WithMacros::No, + r#" + -define(AA(X), {X,foo}). + bar() -> + begin %% clause.exprs[0] + ?AA(f~oo), + {foo} + end. + "#, + expect![[r#" + + Clause { + pats + guards + exprs + Expr::Block { + Expr::Tuple { + Literal(Atom('foo')), + Literal(Atom('foo')), + }, + Expr::Tuple { + Literal(Atom('foo')), + }, + }, + }. + "#]], + expect![[r#" + ( + 0, + 3, + ) + "#]], + ) + } +} diff --git a/crates/hir/src/form_list.rs b/crates/hir/src/form_list.rs new file mode 100644 index 0000000000..d5147e1236 --- /dev/null +++ b/crates/hir/src/form_list.rs @@ -0,0 +1,716 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! A simplified AST that only contains forms. +//! +//! This is the primary IR used throughout `hir`. +//! +//! `FormList`s are built per `FileId`, from the syntax tree of the parsed file. This means that +//! they are location-independent: they don't know which macros are active or which module or header +//! they belong to, since those concepts don't exist at this level (a single `FormList` might be part +//! of multiple modules, or might be included into the same module twice via `-include`). +//! +//! One important purpose of this layer is to provide an "invalidation barrier" for incremental +//! computations: when typing inside a form body, the `FormList` of the modified file is typically +//! unaffected, so we don't have to recompute name resolution results or form data (see `data.rs`). +//! +//! TODO: The `FormList` for the currently open file can be displayed by using the VS Code command +//! "ELP: Debug FormList". +//! +//! Compared to erlc's architecture, `FormList` has properties represented both +//! in EEP (like `-define`), Erlang Abstract Forms (like functions), +//! but also logically extracted from wildcard attributes (like `-on_load`). +//! Many syntax-level Erlang features are already desugared to simpler forms in the `FormList`, +//! but name resolution has not yet been performed. `FormList`s are per-file, while Abstract Forms are +//! per module, because we are interested in incrementally computing it. +//! +//! The representation of items in the `FormList` should generally mirror the surface syntax: it is +//! usually a bad idea to desugar a syntax-level construct to something that is structurally +//! different here. Name resolution needs to be able to process attributes and expand macros +//! and having a 1-to-1 mapping between syntax and the `FormList` avoids introducing subtle bugs. +//! +//! In general, any item in the `FormList` stores its `FormId`, which allows mapping it back to its +//! surface syntax. + +// Note: +// We use `FormId` to form a stable reference to a form in a file from +// other files. e.g. for types, remote calls, etc. But we also use +// the form list for navigation when working on the file itself. The +// crucial difference in these two cases is that for the latter we can +// rely on the `SyntaxNode` of a form being the same, as it is the same +// file. This allows us to speed up operations by caching a map from +// the form `SyntaxNode` to the `Form` itself. + +use std::ops::Index; +use std::sync::Arc; + +use elp_base_db::FileId; +use elp_syntax::ast; +use elp_syntax::AstPtr; +use elp_syntax::SmolStr; +use fxhash::FxHashMap; +use la_arena::Arena; +use la_arena::Idx; +use la_arena::IdxRange; +use profile::Count; + +use crate::db::MinDefDatabase; +use crate::Diagnostic; +use crate::MacroName; +use crate::Name; +use crate::NameArity; + +mod form_id; +mod lower; +mod pretty; +#[cfg(test)] +mod tests; + +pub use form_id::FormId; + +#[derive(Debug, Eq, PartialEq)] +pub struct FormList { + _c: Count, + data: Box, + forms: Vec, + diagnostics: Vec, + // Map from the range of a form to its index + map_back: FxHashMap, FormIdx>, +} + +impl FormList { + pub(crate) fn file_form_list_query(db: &dyn MinDefDatabase, file_id: FileId) -> Arc { + let _p = profile::span("file_form_list_query").detail(|| format!("{:?}", file_id)); + let syntax = db.parse(file_id).tree(); + let ctx = lower::Ctx::new(db, &syntax); + Arc::new(ctx.lower_forms()) + } + + pub fn forms(&self) -> &[FormIdx] { + &self.forms + } + + pub(crate) fn data(&self) -> &FormListData { + &*self.data + } + + pub fn includes(&self) -> impl Iterator { + self.data.includes.iter() + } + + pub fn functions(&self) -> impl Iterator { + self.data.functions.iter() + } + + pub fn exports(&self) -> impl Iterator { + self.data.exports.iter() + } + + pub fn specs(&self) -> impl Iterator { + self.data.specs.iter() + } + + pub fn attributes(&self) -> impl Iterator { + self.data.attributes.iter() + } + + pub fn pp_stack(&self) -> &Arena { + &self.data.pp_directives + } + + /// Returns the first -module attribute in the file + pub fn module_attribute(&self) -> Option<&ModuleAttribute> { + self.data + .module_attribute + .iter() + .next() + .map(|(_idx, attr)| attr) + } + + /// Returns the -behaviour attributes in the file + pub fn behaviour_attributes(&self) -> impl Iterator { + self.data.behaviours.iter() + } + + /// Returns the -callback attribute in the file + pub fn callback_attributes(&self) -> impl Iterator { + self.data.callbacks.iter() + } + + /// Returns an iterator over the -compile attributes in the file + pub fn compile_attributes(&self) -> impl Iterator { + self.data.compile_options.iter() + } + + pub fn find_form(&self, form: &ast::Form) -> Option { + self.map_back.get(&AstPtr::new(form)).copied() + } + + pub fn pretty_print(&self) -> String { + pretty::print(self) + } +} + +#[derive(Debug, Default, Eq, PartialEq)] +pub(crate) struct FormListData { + // Even though only one is allowed, in the syntax we might + // have many due to errors or conditional compilation + module_attribute: Arena, + includes: Arena, + functions: Arena, + pub defines: Arena, + pub pp_directives: Arena, + pp_conditions: Arena, + exports: Arena, + imports: Arena, + type_exports: Arena, + behaviours: Arena, + type_aliases: Arena, + specs: Arena, + callbacks: Arena, + optional_callbacks: Arena, + records: Arena, + attributes: Arena, + compile_options: Arena, + record_fields: Arena, + fa_entries: Arena, + deprecates: Arena, +} + +impl FormListData { + fn shrink_to_fit(&mut self) { + // Exhaustive match to require handling new fields. + let FormListData { + module_attribute, + includes, + functions, + defines, + pp_directives, + pp_conditions, + exports, + imports, + type_exports, + behaviours, + type_aliases, + specs, + callbacks, + optional_callbacks, + records, + attributes, + compile_options, + record_fields, + fa_entries, + deprecates, + } = self; + module_attribute.shrink_to_fit(); + includes.shrink_to_fit(); + functions.shrink_to_fit(); + defines.shrink_to_fit(); + pp_directives.shrink_to_fit(); + pp_conditions.shrink_to_fit(); + exports.shrink_to_fit(); + imports.shrink_to_fit(); + type_exports.shrink_to_fit(); + type_aliases.shrink_to_fit(); + behaviours.shrink_to_fit(); + specs.shrink_to_fit(); + callbacks.shrink_to_fit(); + optional_callbacks.shrink_to_fit(); + records.shrink_to_fit(); + compile_options.shrink_to_fit(); + attributes.shrink_to_fit(); + record_fields.shrink_to_fit(); + fa_entries.shrink_to_fit(); + deprecates.shrink_to_fit(); + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum FormIdx { + ModuleAttribute(ModuleAttributeId), + Function(FunctionId), + PPDirective(PPDirectiveId), + PPCondition(PPConditionId), + Export(ExportId), + Import(ImportId), + TypeExport(TypeExportId), + Behaviour(BehaviourId), + TypeAlias(TypeAliasId), + Spec(SpecId), + Callback(CallbackId), + OptionalCallbacks(OptionalCallbacksId), + Record(RecordId), + Attribute(AttributeId), + CompileOption(CompileOptionId), + DeprecatedAttribute(DeprecatedAttributeId), +} + +pub type ModuleAttributeId = Idx; +pub type IncludeAttributeId = Idx; +pub type FunctionId = Idx; +pub type DefineId = Idx; +pub type PPDirectiveId = Idx; +pub type PPConditionId = Idx; +pub type ExportId = Idx; +pub type ImportId = Idx; +pub type TypeExportId = Idx; +pub type BehaviourId = Idx; +pub type TypeAliasId = Idx; +pub type SpecId = Idx; +pub type CallbackId = Idx; +pub type OptionalCallbacksId = Idx; +pub type RecordId = Idx; +pub type AttributeId = Idx; +pub type CompileOptionId = Idx; +pub type RecordFieldId = Idx; +pub type FaEntryId = Idx; +pub type DeprecatedAttributeId = Idx; + +impl Index for FormList { + type Output = ModuleAttribute; + + fn index(&self, index: ModuleAttributeId) -> &Self::Output { + &self.data.module_attribute[index] + } +} + +impl Index for FormList { + type Output = IncludeAttribute; + + fn index(&self, index: IncludeAttributeId) -> &Self::Output { + &self.data.includes[index] + } +} + +impl Index for FormList { + type Output = Function; + + fn index(&self, index: FunctionId) -> &Self::Output { + &self.data.functions[index] + } +} + +impl Index for FormList { + type Output = Define; + + fn index(&self, index: DefineId) -> &Self::Output { + &self.data.defines[index] + } +} + +impl Index for FormList { + type Output = PPDirective; + + fn index(&self, index: PPDirectiveId) -> &Self::Output { + &self.data.pp_directives[index] + } +} + +impl Index for FormList { + type Output = PPCondition; + + fn index(&self, index: PPConditionId) -> &Self::Output { + &self.data.pp_conditions[index] + } +} + +impl Index for FormList { + type Output = Export; + + fn index(&self, index: ExportId) -> &Self::Output { + &self.data.exports[index] + } +} + +impl Index for FormList { + type Output = Import; + + fn index(&self, index: ImportId) -> &Self::Output { + &self.data.imports[index] + } +} + +impl Index for FormList { + type Output = TypeExport; + + fn index(&self, index: TypeExportId) -> &Self::Output { + &self.data.type_exports[index] + } +} + +impl Index for FormList { + type Output = Behaviour; + + fn index(&self, index: BehaviourId) -> &Self::Output { + &self.data.behaviours[index] + } +} + +impl Index for FormList { + type Output = TypeAlias; + + fn index(&self, index: TypeAliasId) -> &Self::Output { + &self.data.type_aliases[index] + } +} + +impl Index for FormList { + type Output = Spec; + + fn index(&self, index: SpecId) -> &Self::Output { + &self.data.specs[index] + } +} + +impl Index for FormList { + type Output = Callback; + + fn index(&self, index: CallbackId) -> &Self::Output { + &self.data.callbacks[index] + } +} + +impl Index for FormList { + type Output = OptionalCallbacks; + + fn index(&self, index: OptionalCallbacksId) -> &Self::Output { + &self.data.optional_callbacks[index] + } +} + +impl Index for FormList { + type Output = Record; + + fn index(&self, index: RecordId) -> &Self::Output { + &self.data.records[index] + } +} + +impl Index for FormList { + type Output = Attribute; + + fn index(&self, index: AttributeId) -> &Self::Output { + &self.data.attributes[index] + } +} + +impl Index for FormList { + type Output = CompileOption; + + fn index(&self, index: CompileOptionId) -> &Self::Output { + &self.data.compile_options[index] + } +} + +impl Index for FormList { + type Output = RecordField; + + fn index(&self, index: RecordFieldId) -> &Self::Output { + &self.data.record_fields[index] + } +} + +impl Index for FormList { + type Output = FaEntry; + + fn index(&self, index: FaEntryId) -> &Self::Output { + &self.data.fa_entries[index] + } +} + +impl Index for FormList { + type Output = DeprecatedAttribute; + + fn index(&self, index: DeprecatedAttributeId) -> &Self::Output { + &self.data.deprecates[index] + } +} + +/// -module +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct ModuleAttribute { + pub name: Name, + pub cond: Option, + pub form_id: FormId, +} + +/// -include and -include_lib +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum IncludeAttribute { + Include { + path: SmolStr, + cond: Option, + form_id: FormId, + }, + IncludeLib { + path: SmolStr, + cond: Option, + form_id: FormId, + }, +} + +impl IncludeAttribute { + pub fn form_id(&self) -> FormId { + match self { + IncludeAttribute::Include { form_id, .. } => form_id.upcast(), + IncludeAttribute::IncludeLib { form_id, .. } => form_id.upcast(), + } + } +} + +/// -deprecated +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum DeprecatedAttribute { + Module { + cond: Option, + form_id: FormId, + }, + Fa { + fa: DeprecatedFa, + cond: Option, + form_id: FormId, + }, + Fas { + fas: Vec, + cond: Option, + form_id: FormId, + }, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct DeprecatedFa { + pub name: Name, + pub arity: Option, + pub desc: Option, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum DeprecatedDesc { + Str(SmolStr), + Atom(SmolStr), +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Function { + pub name: NameArity, + pub param_names: Vec, + pub cond: Option, + pub form_id: FormId, +} + +/// -define, -undef, -include, and -include_lib +/// +/// These form a stack we can use for macro resolution +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum PPDirective { + Define(DefineId), + Undef { + name: Name, + cond: Option, + form_id: FormId, + }, + Include(IncludeAttributeId), +} + +impl PPDirective { + pub fn as_include(&self) -> Option { + match self { + PPDirective::Define(_) => None, + PPDirective::Undef { .. } => None, + PPDirective::Include(idx) => Some(*idx), + } + } + + pub fn as_define(&self) -> Option { + match self { + PPDirective::Define(idx) => Some(*idx), + PPDirective::Undef { .. } => None, + PPDirective::Include(_) => None, + } + } + + pub fn form_id(&self, form_list: &FormList) -> FormId { + match self { + PPDirective::Define(define) => form_list[*define].form_id.upcast(), + PPDirective::Undef { form_id, .. } => form_id.upcast(), + PPDirective::Include(include) => form_list[*include].form_id(), + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Define { + pub name: MacroName, + pub cond: Option, + pub form_id: FormId, +} + +/// -ifdef, -ifndef, -elsif, -end, and -endif +/// +/// Every form has an index into a pre-processor condition +/// it was defined in. This can be evaluated to understand if +/// the form is "active" or not. +/// -elif, -else, and -endif conditions refer to the previous +/// -elif, -if, -ifdef, or -ifndef. Evaluating the entire preceding chain +/// can answer, if this condition is "active" or not. +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum PPCondition { + Ifdef { + cond: Option, + name: Name, + form_id: FormId, + }, + Ifndef { + cond: Option, + name: Name, + form_id: FormId, + }, + If { + cond: Option, + form_id: FormId, + }, + Else { + prev: PPConditionId, + form_id: FormId, + }, + Elif { + prev: PPConditionId, + form_id: FormId, + }, + Endif { + prev: PPConditionId, + form_id: FormId, + }, +} + +impl PPCondition { + pub fn form_id(&self) -> FormId { + match self { + PPCondition::Ifdef { form_id, .. } => form_id.upcast(), + PPCondition::Ifndef { form_id, .. } => form_id.upcast(), + PPCondition::If { form_id, .. } => form_id.upcast(), + PPCondition::Else { form_id, .. } => form_id.upcast(), + PPCondition::Elif { form_id, .. } => form_id.upcast(), + PPCondition::Endif { form_id, .. } => form_id.upcast(), + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Export { + pub entries: IdxRange, + pub cond: Option, + pub form_id: FormId, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Import { + pub from: Name, + pub entries: IdxRange, + pub cond: Option, + pub form_id: FormId, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct TypeExport { + pub entries: IdxRange, + pub cond: Option, + pub form_id: FormId, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Behaviour { + pub name: Name, + pub cond: Option, + pub form_id: FormId, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum TypeAlias { + Regular { + name: NameArity, + cond: Option, + form_id: FormId, + }, + Opaque { + name: NameArity, + cond: Option, + form_id: FormId, + }, +} + +impl TypeAlias { + pub fn form_id(&self) -> FormId { + match self { + TypeAlias::Regular { form_id, .. } => form_id.upcast(), + TypeAlias::Opaque { form_id, .. } => form_id.upcast(), + } + } + + pub fn name(&self) -> &NameArity { + match self { + TypeAlias::Regular { name, .. } => name, + TypeAlias::Opaque { name, .. } => name, + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Spec { + pub name: NameArity, + pub cond: Option, + pub form_id: FormId, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Callback { + pub name: NameArity, + pub cond: Option, + pub form_id: FormId, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct OptionalCallbacks { + pub entries: IdxRange, + pub cond: Option, + pub form_id: FormId, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Record { + pub name: Name, + pub fields: IdxRange, + pub cond: Option, + pub form_id: FormId, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct CompileOption { + pub cond: Option, + pub form_id: FormId, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Attribute { + pub name: Name, + pub cond: Option, + pub form_id: FormId, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct RecordField { + pub name: Name, + pub idx: u32, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct FaEntry { + pub name: NameArity, + pub idx: u32, +} diff --git a/crates/hir/src/form_list/form_id.rs b/crates/hir/src/form_list/form_id.rs new file mode 100644 index 0000000000..4c634d8380 --- /dev/null +++ b/crates/hir/src/form_list/form_id.rs @@ -0,0 +1,113 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! `FormId` allows to create stable IDs for forms in a file. +//! +//! Specifically, it uses the sequential position of the form in the file +//! as the ID. That way, ids don't change unless the set of forms itself changes. + +use std::any::type_name; +use std::fmt; +use std::hash::Hash; +use std::hash::Hasher; +use std::marker::PhantomData; + +use elp_base_db::FileId; +use elp_syntax::ast; +use elp_syntax::AstNode; +use elp_syntax::SyntaxNodePtr; +use fxhash::FxHashMap; + +use crate::db::MinDefDatabase; + +#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] +struct RawId(u32); + +/// `FormId` points to an AST node encoding type & position of the node. +/// Use `FormId::get()` to reconstitute the AST node +pub struct FormId { + raw: RawId, + _ty: PhantomData N>, +} + +impl Clone for FormId { + fn clone(&self) -> FormId { + *self + } +} +impl Copy for FormId {} + +impl PartialEq for FormId { + fn eq(&self, other: &Self) -> bool { + self.raw == other.raw + } +} +impl Eq for FormId {} +impl Hash for FormId { + fn hash(&self, hasher: &mut H) { + self.raw.hash(hasher); + } +} + +impl fmt::Debug for FormId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "FormId::<{}>({})", type_name::(), self.raw.0) + } +} + +impl FormId { + pub fn get(&self, source_file: &ast::SourceFile) -> N { + source_file + .forms() + .nth(self.raw.0 as usize) + .and_then(|form| N::cast(form.syntax().clone())) + .unwrap() + } + + pub fn get_ast(&self, db: &dyn MinDefDatabase, file_id: FileId) -> N { + let parsed = db.parse(file_id); + self.get(&parsed.tree()) + } +} + +impl> FormId { + pub fn upcast(self) -> FormId { + FormId { + raw: self.raw, + _ty: PhantomData, + } + } +} + +// Temporary struct used during form lowering for resolving ids +#[derive(Debug, Default, Clone)] +pub struct FormIdMap { + arena: FxHashMap, +} + +impl FormIdMap { + pub fn from_source_file(source_file: &ast::SourceFile) -> Self { + let mut map = Self::default(); + for (id, form) in source_file.forms().enumerate() { + let ptr = SyntaxNodePtr::new(form.syntax()); + let raw_id = RawId(id.try_into().unwrap()); + map.arena.insert(ptr, raw_id); + } + map + } + + #[allow(unused)] + pub fn get_id(&self, node: &N) -> FormId { + let raw_id = self.arena.get(&SyntaxNodePtr::new(node.syntax())).unwrap(); + FormId { + raw: *raw_id, + _ty: PhantomData, + } + } +} diff --git a/crates/hir/src/form_list/lower.rs b/crates/hir/src/form_list/lower.rs new file mode 100644 index 0000000000..80fda9a7bd --- /dev/null +++ b/crates/hir/src/form_list/lower.rs @@ -0,0 +1,669 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_syntax::ast; +use elp_syntax::ast::DeprecatedFunArity; +use elp_syntax::ast::Desc; +use elp_syntax::unescape; +use elp_syntax::AstNode; +use elp_syntax::AstPtr; +use elp_syntax::SmolStr; +use elp_syntax::SyntaxNode; +use fxhash::FxHashMap; +use la_arena::Idx; +use la_arena::IdxRange; +use la_arena::RawIdx; +use profile::Count; + +use super::form_id::FormIdMap; +use super::FormIdx; +use super::FormListData; +use crate::db::MinDefDatabase; +use crate::form_list::DeprecatedAttribute; +use crate::form_list::DeprecatedDesc; +use crate::form_list::DeprecatedFa; +use crate::macro_exp::MacroExpCtx; +use crate::name::AsName; +use crate::Attribute; +use crate::Behaviour; +use crate::Callback; +use crate::CompileOption; +use crate::Define; +use crate::Diagnostic; +use crate::DiagnosticMessage; +use crate::Export; +use crate::FaEntry; +use crate::FaEntryId; +use crate::FormList; +use crate::Function; +use crate::Import; +use crate::IncludeAttribute; +use crate::MacroName; +use crate::ModuleAttribute; +use crate::Name; +use crate::NameArity; +use crate::OptionalCallbacks; +use crate::PPCondition; +use crate::PPConditionId; +use crate::PPDirective; +use crate::Record; +use crate::RecordField; +use crate::RecordFieldId; +use crate::Spec; +use crate::TypeAlias; +use crate::TypeExport; + +pub struct Ctx<'a> { + db: &'a dyn MinDefDatabase, + source_file: &'a ast::SourceFile, + id_map: FormIdMap, + map_back: FxHashMap, FormIdx>, + data: Box, + diagnostics: Vec, + conditions: Vec, +} + +impl<'a> Ctx<'a> { + pub fn new(db: &'a dyn MinDefDatabase, source_file: &'a ast::SourceFile) -> Self { + Self { + db, + source_file, + id_map: FormIdMap::from_source_file(source_file), + map_back: FxHashMap::default(), + data: Box::default(), + diagnostics: Vec::new(), + conditions: Vec::new(), + } + } + + pub fn lower_forms(mut self) -> FormList { + let forms = self + .source_file + .forms() + .flat_map(|form| { + let idx = match &form { + ast::Form::ModuleAttribute(module_attr) => self.lower_module_attr(module_attr), + ast::Form::FunDecl(function) => self.lower_function(function), + ast::Form::PreprocessorDirective(pp) => self.lower_pp_directive(pp), + ast::Form::BehaviourAttribute(behaviour) => self.lower_behaviour(behaviour), + ast::Form::Callback(callback) => self.lower_callback(callback), + ast::Form::CompileOptionsAttribute(copt) => self.lower_compile(copt), + ast::Form::ExportAttribute(export) => self.lower_export(export), + ast::Form::ExportTypeAttribute(export) => self.lower_type_export(export), + ast::Form::FileAttribute(_) => None, + ast::Form::ImportAttribute(import) => self.lower_import(import), + ast::Form::Opaque(opaque) => self.lower_opaque(opaque), + ast::Form::OptionalCallbacksAttribute(cbs) => { + self.lower_optional_callbacks(cbs) + } + ast::Form::RecordDecl(record) => self.lower_record(record), + ast::Form::Spec(spec) => self.lower_spec(spec), + ast::Form::TypeAlias(alias) => self.lower_type_alias(alias), + ast::Form::WildAttribute(attribute) => self.lower_attribute(attribute), + ast::Form::DeprecatedAttribute(deprecated_attr) => { + self.lower_deprecated_attr(deprecated_attr) + } + }?; + self.map_back.insert(AstPtr::new(&form), idx); + Some(idx) + }) + .collect(); + + self.data.shrink_to_fit(); + FormList { + _c: Count::default(), + data: self.data, + forms, + diagnostics: self.diagnostics, + map_back: self.map_back, + } + } + + fn lower_deprecated_attr( + &mut self, + deprecated_attr: &ast::DeprecatedAttribute, + ) -> Option { + let cond = self.conditions.last().copied(); + match deprecated_attr.attr() { + None => None, + Some(ast::DeprecatedDetails::DeprecatedFa(fa)) => { + let fa = self.lower_deprecated_fa(&fa)?; + let form_id = self.id_map.get_id(deprecated_attr); + let fa_attr = DeprecatedAttribute::Fa { fa, form_id, cond }; + Some(FormIdx::DeprecatedAttribute( + self.data.deprecates.alloc(fa_attr), + )) + } + Some(ast::DeprecatedDetails::DeprecatedFas(attrs)) => { + let fas = attrs + .fa() + .flat_map(|fa| self.lower_deprecated_fa(&fa)) + .collect(); + let form_id = self.id_map.get_id(deprecated_attr); + let fas_attr = DeprecatedAttribute::Fas { fas, form_id, cond }; + Some(FormIdx::DeprecatedAttribute( + self.data.deprecates.alloc(fas_attr), + )) + } + Some(ast::DeprecatedDetails::DeprecatedModule(module)) => { + if module.module()?.text()? == "module" { + let form_id = self.id_map.get_id(deprecated_attr); + let module = DeprecatedAttribute::Module { form_id, cond }; + Some(FormIdx::DeprecatedAttribute( + self.data.deprecates.alloc(module), + )) + } else { + None + } + } + } + } + + fn lower_deprecated_fa(&mut self, fa: &ast::DeprecatedFa) -> Option { + let name = fa.fun()?.as_name(); + let arity = match fa.arity()? { + DeprecatedFunArity::Integer(i) => Some(u32::from(i)), + DeprecatedFunArity::DeprecatedWildcard(_) => None, //'_' + }; + let desc = match fa.desc() { + None => None, + Some(desc) => match desc.desc() { + None => None, + Some(Desc::MultiString(desc_str)) => { + if let Some(desc) = lower_string_like(&desc_str) { + Some(DeprecatedDesc::Str(SmolStr::new(desc))) + } else { + None + } + } + //https://www.erlang.org/doc/man/xref.html + //next_version or eventually + //we don't use it so far + Some(Desc::Atom(atom)) => Some(DeprecatedDesc::Atom(atom.as_name().raw())), + }, + }; + Some(DeprecatedFa { name, arity, desc }) + } + + fn lower_module_attr(&mut self, module_attr: &ast::ModuleAttribute) -> Option { + let cond = self.conditions.last().copied(); + let name = self.resolve_name(&module_attr.name()?); + let form_id = self.id_map.get_id(module_attr); + let res = ModuleAttribute { + name, + cond, + form_id, + }; + Some(FormIdx::ModuleAttribute( + self.data.module_attribute.alloc(res), + )) + } + + fn lower_function(&mut self, function: &ast::FunDecl) -> Option { + let cond = self.conditions.last().copied(); + let (name, args) = function.clauses().find_map(|clause| match clause { + ast::FunctionOrMacroClause::FunctionClause(clause) => { + Some((clause.name()?, clause.args()?)) + } + // TODO: macro expansion + ast::FunctionOrMacroClause::MacroCallExpr(_) => None, + })?; + + let name = self.resolve_name(&name); + let param_names: Vec = args + .args() + .enumerate() + .map( + |(i, param)| match ast::Var::cast(param.syntax().to_owned()) { + Some(var) if var.as_name() != "_" => var.as_name(), + _ => Name::arg(i + 1), + }, + ) + .collect(); + let arity = args.args().count().try_into().ok()?; + let name = NameArity::new(name, arity); + + let form_id = self.id_map.get_id(function); + let res = Function { + name, + param_names, + cond, + form_id, + }; + Some(FormIdx::Function(self.data.functions.alloc(res))) + } + + fn lower_pp_directive(&mut self, pp: &ast::PreprocessorDirective) -> Option { + match pp { + ast::PreprocessorDirective::PpInclude(include) => self.lower_include(include), + ast::PreprocessorDirective::PpIncludeLib(include) => self.lower_include_lib(include), + ast::PreprocessorDirective::PpDefine(define) => self.lower_define(define), + ast::PreprocessorDirective::PpUndef(undef) => self.lower_undef(undef), + ast::PreprocessorDirective::PpElif(elif) => self.lower_elif(elif), + ast::PreprocessorDirective::PpElse(pp_else) => self.lower_else(pp_else), + ast::PreprocessorDirective::PpEndif(endif) => self.lower_endif(endif), + ast::PreprocessorDirective::PpIf(pp_id) => self.lower_if(pp_id), + ast::PreprocessorDirective::PpIfdef(ifdef) => self.lower_ifdef(ifdef), + ast::PreprocessorDirective::PpIfndef(ifndef) => self.lower_ifndef(ifndef), + } + } + + fn lower_include(&mut self, include: &ast::PpInclude) -> Option { + let cond = self.conditions.last().copied(); + let path: String = include + .file() + .flat_map(|detail| match detail { + ast::IncludeDetail::String(str) => Some(String::from(str)), + // TODO: macro expansion + ast::IncludeDetail::MacroCallExpr(_) => None, + }) + .collect(); + let form_id = self.id_map.get_id(include); + let res = IncludeAttribute::Include { + path: SmolStr::new(path), + cond, + form_id, + }; + self.alloc_include(res) + } + + fn lower_include_lib(&mut self, include: &ast::PpIncludeLib) -> Option { + let cond = self.conditions.last().copied(); + let path: String = include + .file() + .flat_map(|detail| match detail { + ast::IncludeDetail::String(str) => Some(String::from(str)), + // TODO: macro expansion + ast::IncludeDetail::MacroCallExpr(_) => None, + }) + .collect(); + let form_id = self.id_map.get_id(include); + let res = IncludeAttribute::IncludeLib { + path: SmolStr::new(path), + cond, + form_id, + }; + self.alloc_include(res) + } + + fn alloc_include(&mut self, res: IncludeAttribute) -> Option { + let include_idx = self.data.includes.alloc(res); + let idx = self + .data + .pp_directives + .alloc(PPDirective::Include(include_idx)); + Some(FormIdx::PPDirective(idx)) + } + + fn lower_define(&mut self, define: &ast::PpDefine) -> Option { + let cond = self.conditions.last().copied(); + let definition = define.lhs()?; + let name = definition.name()?.as_name(); + let arity = definition + .args() + .and_then(|args| args.args().count().try_into().ok()); + let name = MacroName::new(name, arity); + let form_id = self.id_map.get_id(define); + let res = Define { + name, + cond, + form_id, + }; + let define_idx = self.data.defines.alloc(res); + let idx = self + .data + .pp_directives + .alloc(PPDirective::Define(define_idx)); + Some(FormIdx::PPDirective(idx)) + } + + fn lower_undef(&mut self, undef: &ast::PpUndef) -> Option { + let cond = self.conditions.last().copied(); + let name = undef.name()?.as_name(); + let form_id = self.id_map.get_id(undef); + let res = PPDirective::Undef { + name, + cond, + form_id, + }; + Some(FormIdx::PPDirective(self.data.pp_directives.alloc(res))) + } + + fn lower_ifdef(&mut self, ifdef: &ast::PpIfdef) -> Option { + let cond = self.conditions.last().copied(); + let name = ifdef.name()?.as_name(); + let form_id = self.id_map.get_id(ifdef); + let res = PPCondition::Ifdef { + cond, + name, + form_id, + }; + let id = self.data.pp_conditions.alloc(res); + self.conditions.push(id); + Some(FormIdx::PPCondition(id)) + } + + fn lower_ifndef(&mut self, ifndef: &ast::PpIfndef) -> Option { + let cond = self.conditions.last().copied(); + let name = ifndef.name()?.as_name(); + let form_id = self.id_map.get_id(ifndef); + let res = PPCondition::Ifndef { + cond, + name, + form_id, + }; + let id = self.data.pp_conditions.alloc(res); + self.conditions.push(id); + Some(FormIdx::PPCondition(id)) + } + + fn lower_endif(&mut self, endif: &ast::PpEndif) -> Option { + let prev = self.conditions.pop()?; + let form_id = self.id_map.get_id(endif); + let res = PPCondition::Endif { prev, form_id }; + Some(FormIdx::PPCondition(self.data.pp_conditions.alloc(res))) + } + + fn lower_if(&mut self, pp_if: &ast::PpIf) -> Option { + let cond = self.conditions.last().copied(); + let form_id = self.id_map.get_id(pp_if); + let res = PPCondition::If { cond, form_id }; + let id = self.data.pp_conditions.alloc(res); + self.conditions.push(id); + Some(FormIdx::PPCondition(id)) + } + + fn lower_elif(&mut self, pp_elif: &ast::PpElif) -> Option { + let prev = self.conditions.pop()?; + let form_id = self.id_map.get_id(pp_elif); + let res = PPCondition::Elif { prev, form_id }; + let id = self.data.pp_conditions.alloc(res); + self.conditions.push(id); + Some(FormIdx::PPCondition(id)) + } + + fn lower_else(&mut self, pp_else: &ast::PpElse) -> Option { + let prev = self.conditions.pop()?; + let form_id = self.id_map.get_id(pp_else); + let res = PPCondition::Else { prev, form_id }; + let id = self.data.pp_conditions.alloc(res); + self.conditions.push(id); + Some(FormIdx::PPCondition(id)) + } + + fn lower_export(&mut self, export: &ast::ExportAttribute) -> Option { + let cond = self.conditions.last().copied(); + let entries = self.lower_fa_entries(export.funs()); + let form_id = self.id_map.get_id(export); + let res = Export { + entries, + cond, + form_id, + }; + Some(FormIdx::Export(self.data.exports.alloc(res))) + } + + fn lower_import(&mut self, import: &ast::ImportAttribute) -> Option { + // Import attribute with missing name is still useful - it defines local functions + let from = import + .module() + .map(|name| self.resolve_name(&name)) + .unwrap_or(Name::MISSING); + let entries = self.lower_fa_entries(import.funs()); + let cond = self.conditions.last().copied(); + let form_id = self.id_map.get_id(import); + let res = Import { + from, + entries, + cond, + form_id, + }; + Some(FormIdx::Import(self.data.imports.alloc(res))) + } + + fn lower_type_export(&mut self, export: &ast::ExportTypeAttribute) -> Option { + let cond = self.conditions.last().copied(); + let entries = self.lower_fa_entries(export.types()); + let form_id = self.id_map.get_id(export); + let res = TypeExport { + entries, + cond, + form_id, + }; + Some(FormIdx::TypeExport(self.data.type_exports.alloc(res))) + } + + fn lower_fa_entries(&mut self, entries: impl Iterator) -> IdxRange { + let mut entries = entries + .enumerate() + .filter_map(|(idx, field)| self.lower_fa_entry(field, idx as u32)); + + if let Some(first) = entries.next() { + let last = entries.last().unwrap_or(first); + IdxRange::new_inclusive(first..=last) + } else { + let zero = Idx::from_raw(RawIdx::from(0)); + IdxRange::new(zero..zero) + } + } + + fn lower_fa_entry(&mut self, entry: ast::Fa, idx: u32) -> Option { + let name = self.resolve_name(&entry.fun()?); + let arity = self.resolve_arity(&entry.arity()?.value()?)?; + let name = NameArity::new(name, arity); + let res = FaEntry { name, idx }; + Some(self.data.fa_entries.alloc(res)) + } + + fn lower_behaviour(&mut self, behaviour: &ast::BehaviourAttribute) -> Option { + let cond = self.conditions.last().copied(); + let form_id = self.id_map.get_id(behaviour); + let name = self.resolve_name(&behaviour.name()?); + let res = Behaviour { + name, + cond, + form_id, + }; + Some(FormIdx::Behaviour(self.data.behaviours.alloc(res))) + } + + fn lower_type_alias(&mut self, alias: &ast::TypeAlias) -> Option { + let cond = self.conditions.last().copied(); + let type_name = alias.name()?; + let name = self.resolve_name(&type_name.name()?); + let arity = type_name.args()?.args().count().try_into().ok()?; + let name = NameArity::new(name, arity); + + let form_id = self.id_map.get_id(alias); + let res = TypeAlias::Regular { + name, + cond, + form_id, + }; + Some(FormIdx::TypeAlias(self.data.type_aliases.alloc(res))) + } + + fn lower_opaque(&mut self, opaque: &ast::Opaque) -> Option { + let cond = self.conditions.last().copied(); + let type_name = opaque.name()?; + let name = self.resolve_name(&type_name.name()?); + let arity = type_name.args()?.args().count().try_into().ok()?; + let name = NameArity::new(name, arity); + + let form_id = self.id_map.get_id(opaque); + let res = TypeAlias::Opaque { + name, + cond, + form_id, + }; + Some(FormIdx::TypeAlias(self.data.type_aliases.alloc(res))) + } + + fn lower_optional_callbacks( + &mut self, + cbs: &ast::OptionalCallbacksAttribute, + ) -> Option { + let cond = self.conditions.last().copied(); + let entries = self.lower_fa_entries(cbs.callbacks()); + let form_id = self.id_map.get_id(cbs); + let res = OptionalCallbacks { + entries, + cond, + form_id, + }; + Some(FormIdx::OptionalCallbacks( + self.data.optional_callbacks.alloc(res), + )) + } + + fn lower_spec(&mut self, spec: &ast::Spec) -> Option { + let cond = self.conditions.last().copied(); + if spec.module().is_some() { + return None; + } + let name = self.resolve_name(&spec.fun()?); + let args = spec.sigs().find_map(|sig| sig.args())?; + let arity = args.args().count().try_into().ok()?; + let name = NameArity::new(name, arity); + + let form_id = self.id_map.get_id(spec); + let res = Spec { + name, + cond, + form_id, + }; + Some(FormIdx::Spec(self.data.specs.alloc(res))) + } + + fn lower_callback(&mut self, callback: &ast::Callback) -> Option { + let cond = self.conditions.last().copied(); + if callback.module().is_some() { + return None; + } + let name = self.resolve_name(&callback.fun()?); + let args = callback.sigs().find_map(|sig| sig.args())?; + let arity = args.args().count().try_into().ok()?; + let name = NameArity::new(name, arity); + + let form_id = self.id_map.get_id(callback); + let res = Callback { + name, + cond, + form_id, + }; + Some(FormIdx::Callback(self.data.callbacks.alloc(res))) + } + + fn lower_record(&mut self, record: &ast::RecordDecl) -> Option { + let cond = self.conditions.last().copied(); + let name = self.resolve_name(&record.name()?); + + let mut fields = record + .fields() + .enumerate() + .filter_map(|(idx, field)| self.lower_record_field(field, idx as u32)); + + let fields = if let Some(first) = fields.next() { + let last = fields.last().unwrap_or(first); + IdxRange::new_inclusive(first..=last) + } else { + let zero = Idx::from_raw(RawIdx::from(0)); + IdxRange::new(zero..zero) + }; + + let form_id = self.id_map.get_id(record); + let res = Record { + name, + fields, + cond, + form_id, + }; + Some(FormIdx::Record(self.data.records.alloc(res))) + } + + fn lower_record_field(&mut self, field: ast::RecordField, idx: u32) -> Option { + let name = self.resolve_name(&field.name()?); + let res = RecordField { name, idx }; + Some(self.data.record_fields.alloc(res)) + } + + fn lower_compile(&mut self, copt: &ast::CompileOptionsAttribute) -> Option { + let cond = self.conditions.last().copied(); + let form_id = self.id_map.get_id(copt); + let res = CompileOption { cond, form_id }; + Some(FormIdx::CompileOption(self.data.compile_options.alloc(res))) + } + + fn lower_attribute(&mut self, attribute: &ast::WildAttribute) -> Option { + let cond = self.conditions.last().copied(); + let name = self.resolve_name(&attribute.name()?.name()?); + let form_id = self.id_map.get_id(attribute); + let res = Attribute { + cond, + form_id, + name, + }; + Some(FormIdx::Attribute(self.data.attributes.alloc(res))) + } + + fn resolve_name(&mut self, name: &ast::Name) -> Name { + match name { + ast::Name::Atom(atom) => atom.as_name(), + ast::Name::Var(var) => { + self.add_diagnostic(var.syntax(), DiagnosticMessage::VarNameOutsideMacro); + Name::MISSING + } + ast::Name::MacroCallExpr(macro_call) => { + let exp_ctx = MacroExpCtx::new(&self.data, self.db); + exp_ctx + .expand_atom(macro_call, self.source_file) + .map_or(Name::MISSING, |atom| atom.as_name()) + } + } + } + + fn resolve_arity(&mut self, arity: &ast::ArityValue) -> Option { + // TODO: macro resolution + match arity { + ast::ArityValue::Integer(int) => { + let text = int.text(); + if text.contains('_') { + let str = text.replace('_', ""); + str.parse().ok() + } else { + text.parse().ok() + } + } + ast::ArityValue::MacroCallExpr(_) => None, + ast::ArityValue::Var(_) => None, + } + } + + fn add_diagnostic(&mut self, node: &SyntaxNode, message: DiagnosticMessage) { + self.diagnostics.push(Diagnostic { + location: node.text_range(), + message, + }); + } +} + +fn lower_string_like(concat: &ast::MultiString) -> Option { + let mut buf = String::new(); + + for concatable in concat.elems() { + // TODO: macro resolution + match concatable { + ast::StringLike::MacroCallExpr(_) => return None, + ast::StringLike::MacroString(_) => return None, + ast::StringLike::String(str) => buf.push_str(&unescape::unescape_string(&str.text())?), + } + } + + Some(buf) +} diff --git a/crates/hir/src/form_list/pretty.rs b/crates/hir/src/form_list/pretty.rs new file mode 100644 index 0000000000..66489bb7a4 --- /dev/null +++ b/crates/hir/src/form_list/pretty.rs @@ -0,0 +1,434 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::fmt; +use std::fmt::Formatter; +use std::fmt::Write as _; + +use la_arena::IdxRange; +use la_arena::RawIdx; + +use crate::form_list::DeprecatedAttribute; +use crate::form_list::DeprecatedDesc; +use crate::form_list::DeprecatedFa; +use crate::Attribute; +use crate::Behaviour; +use crate::Callback; +use crate::CompileOption; +use crate::Define; +use crate::Export; +use crate::FaEntry; +use crate::FormIdx; +use crate::FormList; +use crate::Function; +use crate::Import; +use crate::IncludeAttribute; +use crate::ModuleAttribute; +use crate::OptionalCallbacks; +use crate::PPCondition; +use crate::PPConditionId; +use crate::PPDirective; +use crate::Record; +use crate::Spec; +use crate::TypeAlias; +use crate::TypeExport; + +pub fn print(forms: &FormList) -> String { + let mut printer = Printer { + forms, + buf: String::new(), + }; + + for &form in forms.forms() { + printer.print_form(form).unwrap(); + } + + printer.buf.truncate(printer.buf.trim_end().len()); + printer.buf.push('\n'); + printer.buf +} + +struct Printer<'a> { + forms: &'a FormList, + buf: String, +} + +impl<'a> Printer<'a> { + pub fn print_form(&mut self, form: FormIdx) -> fmt::Result { + match form { + FormIdx::ModuleAttribute(idx) => self.print_module_attribute(&self.forms[idx])?, + FormIdx::Function(idx) => self.print_function(&self.forms[idx])?, + FormIdx::PPDirective(idx) => self.print_pp_directive(&self.forms[idx])?, + FormIdx::PPCondition(idx) => self.print_pp_condition(&self.forms[idx], idx)?, + FormIdx::Export(idx) => self.print_export(&self.forms[idx])?, + FormIdx::Import(idx) => self.print_import(&self.forms[idx])?, + FormIdx::TypeExport(idx) => self.print_type_export(&self.forms[idx])?, + FormIdx::Behaviour(idx) => self.print_behaviour(&self.forms[idx])?, + FormIdx::TypeAlias(idx) => self.print_type_alias(&self.forms[idx])?, + FormIdx::OptionalCallbacks(idx) => self.print_optional_callbacks(&self.forms[idx])?, + FormIdx::Spec(idx) => self.print_spec(&self.forms[idx])?, + FormIdx::Callback(idx) => self.print_callback(&self.forms[idx])?, + FormIdx::Record(idx) => self.print_record(&self.forms[idx])?, + FormIdx::CompileOption(idx) => self.print_compile(&self.forms[idx])?, + FormIdx::Attribute(idx) => self.print_attribute(&self.forms[idx])?, + FormIdx::DeprecatedAttribute(idx) => self.print_deprecated(&self.forms[idx])?, + } + writeln!(self) + } + + fn print_module_attribute(&mut self, module_attribute: &ModuleAttribute) -> fmt::Result { + writeln!( + self, + "-module({}). %% cond: {:?}", + module_attribute.name, + raw_cond(&module_attribute.cond) + ) + } + + fn print_include(&mut self, include: &IncludeAttribute) -> fmt::Result { + match include { + IncludeAttribute::Include { + path, + cond, + form_id: _, + } => { + writeln!(self, "-include({:?}). %% cond: {:?}", path, raw_cond(cond)) + } + IncludeAttribute::IncludeLib { + path, + cond, + form_id: _, + } => { + writeln!( + self, + "-inlcude_lib({:?}). %% cond: {:?}", + path, + raw_cond(cond) + ) + } + } + } + + fn print_function(&mut self, function: &Function) -> fmt::Result { + let args = BlankArgs(function.name.arity()); + writeln!( + self, + "{}({}) -> .... %% cond: {:?}", + function.name.name(), + args, + raw_cond(&function.cond) + ) + } + + fn print_pp_directive(&mut self, pp: &PPDirective) -> fmt::Result { + match pp { + PPDirective::Define(idx) => self.print_define(&self.forms[*idx]), + PPDirective::Undef { + name, + cond, + form_id: _, + } => { + writeln!(self, "-undef({}). %% cond: {:?}", name, raw_cond(cond)) + } + PPDirective::Include(idx) => self.print_include(&self.forms[*idx]), + } + } + + fn print_define(&mut self, define: &Define) -> fmt::Result { + if let Some(arity) = define.name.arity() { + writeln!( + self, + "-define({}({}), ...). %% cond: {:?}", + define.name.name(), + BlankArgs(arity), + raw_cond(&define.cond) + ) + } else { + writeln!( + self, + "-define({}, ...). %% cond: {:?}", + define.name.name(), + raw_cond(&define.cond) + ) + } + } + + fn print_pp_condition(&mut self, pp: &PPCondition, idx: PPConditionId) -> fmt::Result { + match pp { + PPCondition::Ifdef { + cond, + name, + form_id: _, + } => writeln!( + self, + "-ifdef({}). %% {:?}, cond: {:?}", + name, + idx.into_raw(), + raw_cond(cond) + ), + PPCondition::Ifndef { + cond, + name, + form_id: _, + } => writeln!( + self, + "-ifndef({}). %% {:?}, cond: {:?}", + name, + idx.into_raw(), + raw_cond(cond) + ), + PPCondition::Endif { prev, form_id: _ } => { + writeln!(self, "-endif. %% prev: {:?}", prev.into_raw()) + } + PPCondition::If { cond, form_id: _ } => { + writeln!( + self, + "-if(...). %% {:?}, cond: {:?}", + idx.into_raw(), + raw_cond(cond) + ) + } + PPCondition::Elif { prev, form_id: _ } => { + writeln!( + self, + "-elif(...). %% {:?}, prev: {:?}", + idx.into_raw(), + prev.into_raw() + ) + } + PPCondition::Else { prev, form_id: _ } => { + writeln!( + self, + "-else. %% {:?}, prev: {:?}", + idx.into_raw(), + prev.into_raw() + ) + } + } + } + + fn print_export(&mut self, export: &Export) -> fmt::Result { + write!(self, "-export(")?; + self.print_entries(&export.entries, export.cond) + } + + fn print_import(&mut self, import: &Import) -> fmt::Result { + write!(self, "-import({}, ", import.from)?; + self.print_entries(&import.entries, import.cond) + } + + fn print_type_export(&mut self, export: &TypeExport) -> fmt::Result { + write!(self, "-export_type(")?; + self.print_entries(&export.entries, export.cond) + } + + fn print_entries( + &mut self, + entries: &IdxRange, + cond: Option, + ) -> fmt::Result { + if entries.is_empty() { + writeln!(self, "[]). %% cond: {:?}", raw_cond(&cond)) + } else { + writeln!(self, "[ %% cond: {:?}", raw_cond(&cond))?; + let mut sep = ""; + for entry_id in entries.clone() { + write!(self, "{} {}", sep, self.forms[entry_id].name)?; + sep = ",\n"; + } + writeln!(self, "\n]).") + } + } + + fn print_behaviour(&mut self, behaviour: &Behaviour) -> fmt::Result { + writeln!( + self, + "-behaviour({}). %% cond: {:?}", + behaviour.name, + raw_cond(&behaviour.cond) + ) + } + + fn print_type_alias(&mut self, alias: &TypeAlias) -> fmt::Result { + let (attr, name, cond) = match alias { + TypeAlias::Regular { + name, + cond, + form_id: _, + } => ("type", name, cond), + TypeAlias::Opaque { + name, + cond, + form_id: _, + } => ("opaque", name, cond), + }; + let args = BlankArgs(name.arity()); + let name = name.name(); + + writeln!( + self, + "-{} {}({}) :: .... %% cond: {:?}", + attr, + name, + args, + raw_cond(cond) + ) + } + + fn print_optional_callbacks(&mut self, cbs: &OptionalCallbacks) -> fmt::Result { + write!(self, "-optional_callbacks(")?; + self.print_entries(&cbs.entries, cbs.cond) + } + + fn print_spec(&mut self, spec: &Spec) -> fmt::Result { + let args = BlankArgs(spec.name.arity()); + let name = spec.name.name(); + writeln!( + self, + "-spec {}({}) -> .... %% cond: {:?}", + name, + args, + raw_cond(&spec.cond) + ) + } + + fn print_callback(&mut self, callback: &Callback) -> fmt::Result { + let args = BlankArgs(callback.name.arity()); + let name = callback.name.name(); + writeln!( + self, + "-callback {}({}) -> .... %% cond: {:?}", + name, + args, + raw_cond(&callback.cond) + ) + } + + fn print_record(&mut self, record: &Record) -> fmt::Result { + if record.fields.is_empty() { + writeln!( + self, + "-record({}, {{}}). %% cond: {:?}", + record.name, + raw_cond(&record.cond) + ) + } else { + writeln!( + self, + "-record({}, {{ %% cond: {:?}", + record.name, + raw_cond(&record.cond) + )?; + let mut sep = ""; + for field_id in record.fields.clone() { + write!(self, "{} {}", sep, self.forms[field_id].name)?; + sep = ",\n"; + } + writeln!(self, "\n}}).") + } + } + + fn print_compile(&mut self, compile: &CompileOption) -> fmt::Result { + writeln!( + self, + "-compile(...). %% cond: {:?}", + raw_cond(&compile.cond) + ) + } + + fn print_attribute(&mut self, attribute: &Attribute) -> fmt::Result { + writeln!( + self, + "-{}(...). %% cond: {:?}", + attribute.name, + raw_cond(&attribute.cond) + ) + } + fn print_deprecated(&mut self, attribute: &DeprecatedAttribute) -> fmt::Result { + match attribute { + DeprecatedAttribute::Module { cond, .. } => { + writeln!(self, "-deprecated(module). %% cond: {:?}", raw_cond(&cond)) + } + DeprecatedAttribute::Fa { fa, cond, .. } => { + writeln!(self, "-deprecated({}). %% cond: {:?}", fa, raw_cond(&cond)) + } + DeprecatedAttribute::Fas { fas, cond, .. } => { + writeln!( + self, + "-deprecated({}). %% cond: {:?}", + DeprecatedFas(fas), + raw_cond(&cond) + ) + } + } + } +} + +impl fmt::Display for DeprecatedDesc { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + DeprecatedDesc::Str(s) => write!(f, "\"{}\"", s), + DeprecatedDesc::Atom(s) => fmt::Display::fmt(s, f), + } + } +} + +impl fmt::Display for DeprecatedFa { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let mut name = &self.name.as_str(); + if name == &"_" { + name = &"'_'"; + } + match (&self.arity, &self.desc) { + (Some(arity), Some(desc)) => write!(f, "{{{}, {}, {}}}", name, arity, desc), + (Some(arity), None) => write!(f, "{{{}, {}}}", name, arity), + (None, Some(desc)) => write!(f, "{{{}, '_', {}}}", name, desc), + (None, None) => write!(f, "{{{}, '_'}}", name), + } + } +} + +struct DeprecatedFas<'a>(&'a Vec); + +impl<'a> fmt::Display for DeprecatedFas<'a> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "[")?; + for (i, fa) in self.0.iter().enumerate() { + if i > 0 { + write!(f, ",")?; + } + write!(f, "{}", fa)?; + } + write!(f, "]") + } +} + +struct BlankArgs(u32); + +impl fmt::Display for BlankArgs { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut join = ""; + for _ in 0..self.0 { + write!(f, "{}_", join)?; + join = ", " + } + Ok(()) + } +} + +fn raw_cond(cond: &Option) -> Option { + cond.map(|cond| cond.into_raw()) +} + +impl<'a> fmt::Write for Printer<'a> { + fn write_str(&mut self, s: &str) -> fmt::Result { + self.buf.push_str(s); + Ok(()) + } +} diff --git a/crates/hir/src/form_list/tests.rs b/crates/hir/src/form_list/tests.rs new file mode 100644 index 0000000000..e20931f742 --- /dev/null +++ b/crates/hir/src/form_list/tests.rs @@ -0,0 +1,476 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_base_db::fixture::WithFixture; +use expect_test::expect; +use expect_test::Expect; + +use crate::db::MinDefDatabase; +use crate::test_db::TestDB; + +fn check(ra_fixture: &str, expect: Expect) { + let (db, file_id) = TestDB::with_single_file(ra_fixture); + let form_list = db.file_form_list(file_id); + let pretty = form_list.pretty_print(); + expect.assert_eq(pretty.trim_start()); +} + +#[test] +fn empty() { + check(r#""#, expect![[r#""#]]); +} + +#[test] +fn module_attribute() { + check( + r#" +-module( 'foo' ). +"#, + expect![[r#" + -module(foo). %% cond: None + "#]], + ) +} + +#[test] +fn include_attribute() { + check( + r#" +-include("foo.hrl"). +-include_lib("kernel" "/include/file.hrl"). +"#, + expect![[r#" + -include("foo.hrl"). %% cond: None + + -inlcude_lib("kernel/include/file.hrl"). %% cond: None + "#]], + ) +} + +#[test] +fn function() { + check( + r#" +fun1() -> ok. +fun2(1, 2, 3) -> error; +fun2(A, B, C) -> ok. +"#, + expect![[r#" + fun1() -> .... %% cond: None + + fun2(_, _, _) -> .... %% cond: None + "#]], + ) +} + +#[test] +fn define_undef() { + check( + r#" +-define(FOO, 1). +-define(BAR(), 2). +-define(BAZ(X), 3). +-undef(XXX). +"#, + expect![[r#" + -define(FOO, ...). %% cond: None + + -define(BAR(), ...). %% cond: None + + -define(BAZ(_), ...). %% cond: None + + -undef(XXX). %% cond: None + "#]], + ) +} + +#[test] +fn macro_function() { + check( + r#" +-define(FOO, foo). +-define(INCOMPATIBLE, 1 + 1). +?FOO() -> ok. +?UNDEFINED() -> ok. +?INCOMPATIBLE() -> ok. +"#, + expect![[r#" + -define(FOO, ...). %% cond: None + + -define(INCOMPATIBLE, ...). %% cond: None + + foo() -> .... %% cond: None + + [missing name]() -> .... %% cond: None + + [missing name]() -> .... %% cond: None + "#]], + ) +} + +#[test] +fn var_function() { + check( + r#" +VAR() -> ok. +"#, + expect![[r#" + [missing name]() -> .... %% cond: None + "#]], + ) +} + +#[test] +fn ifdef() { + check( + r#" +-ifdef(FOO). +-ifdef(bar). +-endif. +-endif. +"#, + expect![[r#" + -ifdef(FOO). %% 0, cond: None + + -ifdef(bar). %% 1, cond: Some(0) + + -endif. %% prev: 1 + + -endif. %% prev: 0 + "#]], + ) +} + +#[test] +fn ifndef() { + check( + r#" +-ifndef(FOO). +-ifndef(bar). +-endif. +-endif. +"#, + expect![[r#" + -ifndef(FOO). %% 0, cond: None + + -ifndef(bar). %% 1, cond: Some(0) + + -endif. %% prev: 1 + + -endif. %% prev: 0 + "#]], + ) +} + +#[test] +fn pp_if() { + check( + r#" +-if(?FOO > 0). +-if(?bar < 100). +-endif. +-endif. +"#, + expect![[r#" + -if(...). %% 0, cond: None + + -if(...). %% 1, cond: Some(0) + + -endif. %% prev: 1 + + -endif. %% prev: 0 + "#]], + ) +} + +#[test] +fn pp_elif() { + check( + r#" +-if(?FOO > 0). +-elif(?bar < 100). +-elif(?bar < 1). +-endif. +"#, + expect![[r#" + -if(...). %% 0, cond: None + + -elif(...). %% 1, prev: 0 + + -elif(...). %% 2, prev: 1 + + -endif. %% prev: 2 + "#]], + ) +} + +#[test] +fn pp_else() { + check( + r#" +-if(?FOO > 0). +-else. +foo() -> ok. +-endif. +"#, + expect![[r#" + -if(...). %% 0, cond: None + + -else. %% 1, prev: 0 + + foo() -> .... %% cond: Some(1) + + -endif. %% prev: 1 + "#]], + ) +} + +#[test] +fn export() { + check( + r#" +-export([]). +-export([foo/1, bar/0]). +"#, + expect![[r#" + -export([]). %% cond: None + + -export([ %% cond: None + foo/1, + bar/0 + ]). + "#]], + ) +} + +#[test] +fn import() { + check( + r#" +-import(, []). +-import(foo, []). +-import(foo, [foo/1]). +"#, + expect![[r#" + -import([missing name], []). %% cond: None + + -import(foo, []). %% cond: None + + -import(foo, [ %% cond: None + foo/1 + ]). + "#]], + ) +} + +#[test] +fn type_export() { + check( + r#" +-export_type([]). +-export_type([foo/1]). +"#, + expect![[r#" + -export_type([]). %% cond: None + + -export_type([ %% cond: None + foo/1 + ]). + "#]], + ) +} + +#[test] +fn behaviour() { + check( + r#" +-behaviour(foo). +-behavior(foo). +"#, + expect![[r#" + -behaviour(foo). %% cond: None + + -behaviour(foo). %% cond: None + "#]], + ) +} + +#[test] +fn type_alias() { + check( + r#" +-type foo() :: ok. +-type bar(A) :: ok. +"#, + expect![[r#" + -type foo() :: .... %% cond: None + + -type bar(_) :: .... %% cond: None + "#]], + ) +} + +#[test] +fn opaque() { + check( + r#" +-opaque foo() :: ok. +-opaque bar(A) :: ok. +"#, + expect![[r#" + -opaque foo() :: .... %% cond: None + + -opaque bar(_) :: .... %% cond: None + "#]], + ) +} + +#[test] +fn optional_callbacks() { + check( + r#" +-optional_callbacks([]). +-optional_callbacks([foo/1]). +"#, + expect![[r#" + -optional_callbacks([]). %% cond: None + + -optional_callbacks([ %% cond: None + foo/1 + ]). + "#]], + ) +} + +#[test] +fn specs() { + check( + r#" +-spec bar() -> ok. +-spec foo(integer()) -> integer(); + (float()) -> float(). +"#, + expect![[r#" + -spec bar() -> .... %% cond: None + + -spec foo(_) -> .... %% cond: None + "#]], + ) +} + +#[test] +fn callbacks() { + check( + r#" +-callback bar() -> ok. +-callback foo(integer()) -> integer(); + (float()) -> float(). +"#, + expect![[r#" + -callback bar() -> .... %% cond: None + + -callback foo(_) -> .... %% cond: None + "#]], + ) +} + +#[test] +fn record() { + check( + r#" +-record(foo, {}). +"#, + expect![[r#" + -record(foo, {}). %% cond: None + "#]], + ) +} + +#[test] +fn record_fields() { + check( + r#" +-record(foo, {a, b = b, c :: c, d = d :: d}). +"#, + expect![[r#" + -record(foo, { %% cond: None + a, + b, + c, + d + }). + "#]], + ) +} + +#[test] +fn compile_option() { + check( + r#" +-compile([export_all]). +"#, + expect![[r#" + -compile(...). %% cond: None + "#]], + ) +} + +#[test] +fn attributes() { + check( + r#" +-'foobar'(a). +-attribute([]). +"#, + expect![[r#" + -foobar(...). %% cond: None + + -attribute(...). %% cond: None + "#]], + ) +} + +#[test] +fn deprecated() { + check( + r#" + -deprecated(module). + -deprecated({foo, 1, "desc"}). + -deprecated({foo, '_', "desc"}). + -deprecated({foo, '_', atom}). + -deprecated({foo, '_'}). + -deprecated({foo, 1}). + -deprecated({'_', '_'}). + -deprecated({'_', '_', atom}). + -deprecated({'_', '_', "desc"}). + -deprecated([{foo, 1, "desc"}, {foo, '_', "desc"}, {foo, '_', atom}, {foo, '_'}, {foo, 1}, {'_', '_'}, {'_', '_', atom}, {'_', '_', "desc"}]). +"#, + expect![[r#" + -deprecated(module). %% cond: None + + -deprecated({foo, 1, "desc"}). %% cond: None + + -deprecated({foo, '_', "desc"}). %% cond: None + + -deprecated({foo, '_', atom}). %% cond: None + + -deprecated({foo, '_'}). %% cond: None + + -deprecated({foo, 1}). %% cond: None + + -deprecated({'_', '_'}). %% cond: None + + -deprecated({'_', '_', atom}). %% cond: None + + -deprecated({'_', '_', "desc"}). %% cond: None + + -deprecated([{foo, 1, "desc"},{foo, '_', "desc"},{foo, '_', atom},{foo, '_'},{foo, 1},{'_', '_'},{'_', '_', atom},{'_', '_', "desc"}]). %% cond: None + "#]], + ) +} diff --git a/crates/hir/src/include.rs b/crates/hir/src/include.rs new file mode 100644 index 0000000000..33184c89fd --- /dev/null +++ b/crates/hir/src/include.rs @@ -0,0 +1,185 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::sync::Arc; + +use elp_base_db::FileId; +use elp_base_db::SourceRoot; +use elp_base_db::SourceRootId; + +use crate::db::MinDefDatabase; +use crate::InFile; +use crate::IncludeAttribute; +use crate::IncludeAttributeId; + +struct IncludeCtx<'a> { + db: &'a dyn MinDefDatabase, + source_root_id: SourceRootId, + source_root: Arc, + file_id: FileId, +} + +pub(crate) fn resolve( + db: &dyn MinDefDatabase, + include_id: InFile, +) -> Option { + IncludeCtx::new(db, include_id.file_id).resolve(include_id.value) +} + +impl<'a> IncludeCtx<'a> { + fn new(db: &'a dyn MinDefDatabase, file_id: FileId) -> Self { + let source_root_id = db.file_source_root(file_id); + let source_root = db.source_root(source_root_id); + Self { + db, + file_id, + source_root_id, + source_root, + } + } + + fn resolve(&self, id: IncludeAttributeId) -> Option { + let form_list = self.db.file_form_list(self.file_id); + let (path, file_id) = match &form_list[id] { + IncludeAttribute::Include { path, .. } => (path, self.resolve_include(path)), + IncludeAttribute::IncludeLib { path, .. } => (path, self.resolve_include_lib(path)), + }; + if file_id.is_none() { + let module_str = if let Some(module_attribute) = form_list.module_attribute() { + module_attribute.name.as_str().to_string() + } else { + format!("{:?}", self.file_id) + }; + log::warn!("Unable to resolve \"{path}\" in '{module_str}'"); + } + file_id + } + + fn resolve_include(&self, path: &str) -> Option { + self.resolve_relative(path) + .or_else(|| self.resolve_local(path)) + } + + fn resolve_include_lib(&self, path: &str) -> Option { + self.resolve_include(path) + .or_else(|| self.resolve_remote(path)) + } + + fn resolve_relative(&self, path: &str) -> Option { + self.source_root.relative_path(self.file_id, path) + } + + fn resolve_local(&self, path: &str) -> Option { + let app_data = self.db.app_data(self.source_root_id)?; + app_data.include_path.iter().find_map(|include| { + let name = include.join(path); + self.source_root.file_for_path(&name.into()) + }) + } + + fn resolve_remote(&self, path: &str) -> Option { + let app_data = self.db.app_data(self.source_root_id)?; + let project_data = self.db.project_data(app_data.project_id); + + let (app_name, path) = path.split_once('/')?; + let source_root_id = project_data.app_roots.get(app_name)?; + let source_root = self.db.source_root(source_root_id); + let target_app_data = self.db.app_data(source_root_id)?; + let path = target_app_data.dir.join(path); + source_root.file_for_path(&path.into()) + } +} + +#[cfg(test)] +mod tests { + use elp_base_db::fixture::WithFixture; + use elp_base_db::SourceDatabase; + use expect_test::expect; + use expect_test::Expect; + + use super::*; + use crate::test_db::TestDB; + + fn check(ra_fixture: &str, expect: Expect) { + let (db, files) = TestDB::with_many_files(ra_fixture); + let file_id = files[0]; + let form_list = db.file_form_list(file_id); + let mut resolved = form_list + .includes() + .map(|(idx, include)| { + let resolved = db + .resolve_include(InFile::new(file_id, idx)) + .unwrap_or_else(|| panic!("unresolved include: {:?}", include)); + let resolved_path = db + .source_root(db.file_source_root(resolved)) + .path_for_file(&resolved) + .unwrap() + .clone(); + (include, resolved_path) + }) + .map(|(include, resolved)| match include { + IncludeAttribute::Include { path, .. } => { + format!("-include({:?}). % => {}", path, resolved) + } + IncludeAttribute::IncludeLib { path, .. } => { + format!("-include_lib({:?}). % => {}", path, resolved) + } + }) + .collect::>() + .join("\n"); + resolved.push('\n'); + expect.assert_eq(&resolved); + } + + #[test] + fn relative() { + check( + r#" +//- /src/module.erl +-include("header.hrl"). +-include_lib("header.hrl"). +//- /src/header.hrl +"#, + expect![[r#" + -include("header.hrl"). % => /src/header.hrl + -include_lib("header.hrl"). % => /src/header.hrl + "#]], + ) + } + + #[test] + fn include_path() { + check( + r#" +//- /src/module.erl include_path:/include +-include("header.hrl"). +-include_lib("header.hrl"). +//- /include/header.hrl +"#, + expect![[r#" + -include("header.hrl"). % => /include/header.hrl + -include_lib("header.hrl"). % => /include/header.hrl + "#]], + ) + } + + #[test] + fn lib() { + check( + r#" +//- /main/src/module.erl app:main +-include_lib("another/include/header.hrl"). +//- /another-app/include/header.hrl app:another +"#, + expect![[r#" + -include_lib("another/include/header.hrl"). % => /another-app/include/header.hrl + "#]], + ) + } +} diff --git a/crates/hir/src/intern.rs b/crates/hir/src/intern.rs new file mode 100644 index 0000000000..329a97ac1a --- /dev/null +++ b/crates/hir/src/intern.rs @@ -0,0 +1,53 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_base_db::salsa; + +use crate::Name; + +#[salsa::query_group(MinInternDatabaseStorage)] +pub trait MinInternDatabase { + #[salsa::interned] + fn atom(&self, name: Name) -> Atom; + + #[salsa::interned] + fn var(&self, name: Name) -> Var; +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Atom(salsa::InternId); + +impl salsa::InternKey for Atom { + fn from_intern_id(v: salsa::InternId) -> Self { + Atom(v) + } + + fn as_intern_id(&self) -> salsa::InternId { + self.0 + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Var(salsa::InternId); + +impl salsa::InternKey for Var { + fn from_intern_id(v: salsa::InternId) -> Self { + Var(v) + } + + fn as_intern_id(&self) -> salsa::InternId { + self.0 + } +} + +impl Var { + pub fn as_string(&self, db: &dyn MinInternDatabase) -> String { + db.lookup_var(*self).to_string() + } +} diff --git a/crates/hir/src/lib.rs b/crates/hir/src/lib.rs new file mode 100644 index 0000000000..08dfb9d8b9 --- /dev/null +++ b/crates/hir/src/lib.rs @@ -0,0 +1,188 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_base_db::FileId; +use elp_base_db::SourceDatabase; +use elp_syntax::ast; + +mod body; +pub mod db; +mod def_map; +mod diagnostics; +pub mod edoc; +mod expr; +mod fold; +mod form_list; +mod include; +mod intern; +mod macro_exp; +mod module_data; +mod name; +pub mod resolver; +mod sema; +mod test_db; + +pub use body::AnyAttribute; +pub use body::AttributeBody; +pub use body::Body; +pub use body::BodySourceMap; +pub use body::DefineBody; +pub use body::ExprSource; +pub use body::FunctionBody; +pub use body::InFileAstPtr; +pub use body::RecordBody; +pub use body::SpecBody; +pub use body::SpecOrCallback; +pub use body::TypeBody; +pub use def_map::DefMap; +pub use diagnostics::Diagnostic; +pub use diagnostics::DiagnosticMessage; +pub use expr::AnyExprId; +pub use expr::AnyExprRef; +pub use expr::BinarySeg; +pub use expr::CRClause; +pub use expr::CallTarget; +pub use expr::CatchClause; +pub use expr::Clause; +pub use expr::ComprehensionBuilder; +pub use expr::ComprehensionExpr; +pub use expr::Expr; +pub use expr::ExprId; +pub use expr::FunType; +pub use expr::IfClause; +pub use expr::ListType; +pub use expr::Literal; +pub use expr::MapOp; +pub use expr::Pat; +pub use expr::PatId; +pub use expr::ReceiveAfter; +pub use expr::RecordFieldBody; +pub use expr::SpecSig; +pub use expr::Term; +pub use expr::TermId; +pub use expr::TypeExpr; +pub use expr::TypeExprId; +pub use fold::FoldCtx; +pub use fold::On; +pub use fold::Strategy; +pub use form_list::Attribute; +pub use form_list::AttributeId; +pub use form_list::Behaviour; +pub use form_list::BehaviourId; +pub use form_list::Callback; +pub use form_list::CallbackId; +pub use form_list::CompileOption; +pub use form_list::CompileOptionId; +pub use form_list::Define; +pub use form_list::DefineId; +pub use form_list::Export; +pub use form_list::ExportId; +pub use form_list::FaEntry; +pub use form_list::FaEntryId; +pub use form_list::FormId; +pub use form_list::FormIdx; +pub use form_list::FormList; +pub use form_list::Function; +pub use form_list::FunctionId; +pub use form_list::Import; +pub use form_list::ImportId; +pub use form_list::IncludeAttribute; +pub use form_list::IncludeAttributeId; +pub use form_list::ModuleAttribute; +pub use form_list::ModuleAttributeId; +pub use form_list::OptionalCallbacks; +pub use form_list::OptionalCallbacksId; +pub use form_list::PPCondition; +pub use form_list::PPConditionId; +pub use form_list::PPDirective; +pub use form_list::PPDirectiveId; +pub use form_list::Record; +pub use form_list::RecordField; +pub use form_list::RecordFieldId; +pub use form_list::RecordId; +pub use form_list::Spec; +pub use form_list::SpecId; +pub use form_list::TypeAlias; +pub use form_list::TypeAliasId; +pub use form_list::TypeExport; +pub use form_list::TypeExportId; +pub use intern::Atom; +pub use intern::Var; +pub use macro_exp::ResolvedMacro; +pub use module_data::CallbackDef; +pub use module_data::DefineDef; +pub use module_data::File; +pub use module_data::FileKind; +pub use module_data::FunctionDef; +pub use module_data::Module; +pub use module_data::RecordDef; +pub use module_data::RecordFieldDef; +pub use module_data::SpecDef; +pub use module_data::SpecdFunctionDef; +pub use module_data::TypeAliasDef; +pub use module_data::TypeAliasSource; +pub use module_data::VarDef; +pub use name::known; +pub use name::MacroName; +pub use name::Name; +pub use name::NameArity; +pub use sema::CallDef; +pub use sema::DefinitionOrReference; +pub use sema::FaDef; +pub use sema::InFunctionBody; +pub use sema::ScopeAnalysis; +pub use sema::Semantic; + +/// `InFile` stores a value of `T` inside a particular file. +/// +/// Typical usages are: +/// +/// * `InFile` -- syntax node in a file +/// * `InFile` -- ast node in a file +/// * `InFile` -- offset in a file +/// * `InFile` -- `-include` in a file +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] +pub struct InFile { + pub file_id: FileId, + pub value: T, +} + +impl InFile { + pub fn new(file_id: FileId, value: T) -> InFile { + InFile { file_id, value } + } + + pub fn with_value(&self, value: U) -> InFile { + InFile::new(self.file_id, value) + } + + pub fn map U, U>(self, f: F) -> InFile { + InFile::new(self.file_id, f(self.value)) + } + + pub fn as_ref(&self) -> InFile<&T> { + self.with_value(&self.value) + } + + pub fn file_syntax(&self, db: &dyn SourceDatabase) -> ast::SourceFile { + db.parse(self.file_id).tree() + } +} + +impl InFile<&T> { + pub fn cloned(&self) -> InFile { + self.with_value(self.value.clone()) + } +} + +impl InFile> { + pub fn transpose(self) -> Option> { + self.value.map(|value| InFile::new(self.file_id, value)) + } +} diff --git a/crates/hir/src/macro_exp.rs b/crates/hir/src/macro_exp.rs new file mode 100644 index 0000000000..c0ddacfbd6 --- /dev/null +++ b/crates/hir/src/macro_exp.rs @@ -0,0 +1,485 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_base_db::FileId; +use elp_syntax::ast; + +use crate::db::MinDefDatabase; +use crate::form_list::FormListData; +use crate::known; +use crate::name::AsName; +use crate::Define; +use crate::DefineId; +use crate::InFile; +use crate::MacroName; +use crate::PPDirective; + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +#[allow(non_camel_case_types)] +pub enum BuiltInMacro { + FILE, + FUNCTION_NAME, + FUNCTION_ARITY, + LINE, + MODULE, + MODULE_STRING, + MACHINE, + OTP_RELEASE, +} + +impl BuiltInMacro { + pub fn name(&self) -> MacroName { + let name = match self { + BuiltInMacro::FILE => known::FILE, + BuiltInMacro::FUNCTION_NAME => known::FUNCTION_NAME, + BuiltInMacro::FUNCTION_ARITY => known::FUNCTION_ARITY, + BuiltInMacro::LINE => known::LINE, + BuiltInMacro::MODULE => known::MODULE, + BuiltInMacro::MODULE_STRING => known::MODULE_STRING, + BuiltInMacro::MACHINE => known::MACHINE, + BuiltInMacro::OTP_RELEASE => known::OTP_RELEASE, + }; + MacroName::new(name, None) + } +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +pub enum ResolvedMacro { + BuiltIn(BuiltInMacro), + User(InFile), +} + +impl ResolvedMacro { + pub fn name(&self, db: &dyn MinDefDatabase) -> MacroName { + match self { + ResolvedMacro::BuiltIn(built_in) => built_in.name(), + ResolvedMacro::User(def) => { + let form_list = db.file_form_list(def.file_id); + form_list[def.value].name.clone() + } + } + } +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub enum MacroResolution { + Resolved(InFile), + Undef, + Unresolved, +} + +pub(crate) fn resolve_query( + db: &dyn MinDefDatabase, + file_id: FileId, + name: MacroName, +) -> Option { + if let Some(value) = resolve_built_in(&name) { + return value.map(ResolvedMacro::BuiltIn); + } + + match db.local_resolve_macro(file_id, name) { + MacroResolution::Resolved(resolved) => Some(ResolvedMacro::User(resolved)), + MacroResolution::Undef => None, + MacroResolution::Unresolved => None, + } +} + +fn resolve_built_in(name: &MacroName) -> Option> { + let built_in = match name.name().as_str() { + "FILE" => Some(BuiltInMacro::FILE), + "FUNCTION_NAME" => Some(BuiltInMacro::FUNCTION_NAME), + "FUNCTION_ARITY" => Some(BuiltInMacro::FUNCTION_ARITY), + "LINE" => Some(BuiltInMacro::LINE), + "MODULE" => Some(BuiltInMacro::MODULE), + "MODULE_STRING" => Some(BuiltInMacro::MODULE_STRING), + "MACHINE" => Some(BuiltInMacro::MACHINE), + "OTP_RELEASE" => Some(BuiltInMacro::OTP_RELEASE), + _ => None, + }; + + if built_in.is_some() { + if name.arity().is_none() { + return Some(built_in); + } else { + return Some(None); + } + } + + None +} + +pub(crate) fn local_resolve_query( + db: &dyn MinDefDatabase, + file_id: FileId, + name: MacroName, +) -> MacroResolution { + let form_list = db.file_form_list(file_id); + + for (_idx, directive) in form_list.pp_stack().iter().rev() { + match directive { + PPDirective::Define(idx) => { + let define = &form_list[*idx]; + if define.name == name { + return MacroResolution::Resolved(InFile::new(file_id, *idx)); + } + } + PPDirective::Undef { + name: undefed, + cond: _, + form_id: _, + } if undefed == name.name() => { + return MacroResolution::Undef; + } + PPDirective::Undef { .. } => {} + PPDirective::Include(idx) => { + if let Some(resolved) = db.resolve_include(InFile::new(file_id, *idx)) { + match db.local_resolve_macro(resolved, name.clone()) { + MacroResolution::Resolved(resolved) => { + return MacroResolution::Resolved(resolved); + } + MacroResolution::Undef => return MacroResolution::Undef, + MacroResolution::Unresolved => {} + } + } + } + } + } + + MacroResolution::Unresolved +} + +// This handles the case of headers accidentally forming cycles during macro resolution. +pub(crate) fn recover_cycle( + _db: &dyn MinDefDatabase, + _cycle: &[String], + _file_id: &FileId, + _name: &MacroName, +) -> MacroResolution { + MacroResolution::Unresolved +} + +pub struct MacroExpCtx<'a> { + _db: &'a dyn MinDefDatabase, + form_list: &'a FormListData, +} + +impl<'a> MacroExpCtx<'a> { + pub(crate) fn new(form_list: &'a FormListData, db: &'a dyn MinDefDatabase) -> Self { + MacroExpCtx { form_list, _db: db } + } + + pub fn expand_atom( + &self, + macro_call: &ast::MacroCallExpr, + source_file: &ast::SourceFile, + ) -> Option { + match self.find_replacement(macro_call, source_file)? { + ast::MacroDefReplacement::Expr(ast::Expr::ExprMax(ast::ExprMax::Atom(atom))) => { + Some(atom) + } + ast::MacroDefReplacement::Expr(_) => None, + ast::MacroDefReplacement::ReplacementCrClauses(_) => None, + ast::MacroDefReplacement::ReplacementFunctionClauses(_) => None, + ast::MacroDefReplacement::ReplacementGuardAnd(_) => None, + ast::MacroDefReplacement::ReplacementGuardOr(_) => None, + ast::MacroDefReplacement::ReplacementParens(_) => None, + } + } + + pub fn find_define(&self, macro_call: &ast::MacroCallExpr) -> Option<&Define> { + let target = macro_name(macro_call)?; + + for (_idx, directive) in self.form_list.pp_directives.iter().rev() { + // TODO: evaluate conditions + match directive { + PPDirective::Define(idx) => { + let define = &self.form_list.defines[*idx]; + if define.name == target { + return Some(define); + } + } + PPDirective::Undef { + name, + cond: _, + form_id: _, + } if name == target.name() => { + // TODO: diagnostic it was explicitly undefed + return None; + } + PPDirective::Undef { .. } => {} + // TODO: recurse into includes + PPDirective::Include { .. } => return None, + } + } + + // TODO: diagnostic no definition found + None + } + + pub fn find_defines_by_name(&self, name: &ast::MacroName) -> Vec<&Define> { + let target = name.as_name(); + let mut defines = vec![]; + + for (_idx, directive) in self.form_list.pp_directives.iter().rev() { + // TODO: evaluate conditions + match directive { + PPDirective::Define(idx) => { + let define = &self.form_list.defines[*idx]; + if define.name.name() == &target { + defines.push(define); + } + } + PPDirective::Undef { .. } => {} + // TODO: recurse into includes + PPDirective::Include { .. } => break, + } + } + + defines + } + + fn find_replacement( + &self, + call: &ast::MacroCallExpr, + source_file: &ast::SourceFile, + ) -> Option { + self.find_define(call)? + .form_id + .get(source_file) + .replacement() + } +} + +pub fn macro_name(macro_call: &ast::MacroCallExpr) -> Option { + let name = macro_call.name()?.as_name(); + let arity = macro_call + .args() + .and_then(|args| args.args().count().try_into().ok()); + Some(MacroName::new(name, arity)) +} + +#[cfg(test)] +mod tests { + use elp_base_db::fixture::ChangeFixture; + use elp_base_db::fixture::WithFixture; + use elp_base_db::FileRange; + use elp_base_db::SourceDatabase; + use elp_syntax::algo; + use elp_syntax::ast; + use elp_syntax::AstNode; + + use super::*; + use crate::test_db::TestDB; + use crate::DefineDef; + use crate::File; + + fn resolve_macro(fixture: &str) -> (Option, TestDB, ChangeFixture) { + let (db, fixture) = TestDB::with_fixture(fixture); + let position = fixture.position(); + + let parsed = db.parse(position.file_id); + assert!( + parsed.errors().is_empty(), + "test must not contain parse errors, got: {:?}", + parsed.errors() + ); + + let macro_call = + algo::find_node_at_offset::(&parsed.syntax_node(), position.offset) + .expect("macro call marked with ~ not found"); + let name = macro_name(¯o_call).unwrap(); + + (db.resolve_macro(position.file_id, name), db, fixture) + } + + #[track_caller] + fn check_built_in(fixture: &str, expected: BuiltInMacro) { + let (resolved, _db, _fixture) = resolve_macro(fixture); + + assert_eq!(resolved, Some(ResolvedMacro::BuiltIn(expected))); + } + + #[track_caller] + fn check_user(fixture: &str) { + let (resolved, db, fixture) = resolve_macro(fixture); + let annos = fixture.annotations(&db); + assert_eq!(annos.len(), 1); + let (expected_range, _) = annos[0]; + + let resolved = match resolved.expect("failed to resolve macro") { + ResolvedMacro::BuiltIn(built_in) => panic!( + "expected to resolve to a custom macro, got {:?} instead", + built_in + ), + ResolvedMacro::User(def) => def, + }; + let def = DefineDef { + file: File { + file_id: resolved.file_id, + }, + define: db.file_form_list(resolved.file_id)[resolved.value].clone(), + }; + let found_range = FileRange { + file_id: resolved.file_id, + range: def.source(&db).syntax().text_range(), + }; + + assert_eq!(expected_range, found_range); + } + + #[test] + fn test_line() { + check_built_in( + r#" +-define(LINE, ignored). +bar() -> ?~LINE. +"#, + BuiltInMacro::LINE, + ); + } + + #[test] + fn test_line_paren() { + let (resolved, _db, _fixture) = resolve_macro( + r#" +-define(LINE, ignored). +bar() -> ?~LINE(). +"#, + ); + assert_eq!(resolved, None); + } + + #[test] + fn test_file() { + check_built_in( + r#" +-define(FILE, ignored). +bar() -> ?~FILE. +"#, + BuiltInMacro::FILE, + ); + } + + #[test] + fn test_function_name() { + check_built_in( + r#" +-define(FUNCTION_NAME, ignored). +bar() -> ?~FUNCTION_NAME. +"#, + BuiltInMacro::FUNCTION_NAME, + ); + } + + #[test] + fn test_function_arity() { + check_built_in( + r#" +-define(FUNCTION_ARITY, ignored). +bar() -> ?~FUNCTION_ARITY. +"#, + BuiltInMacro::FUNCTION_ARITY, + ); + } + + #[test] + fn test_module() { + check_built_in( + r#" +-define(MODULE, ignored). +bar() -> ?~MODULE. +"#, + BuiltInMacro::MODULE, + ); + } + + #[test] + fn test_module_string() { + check_built_in( + r#" +-define(MODULE_STRING, ignored). +bar() -> ?~MODULE_STRING. +"#, + BuiltInMacro::MODULE_STRING, + ); + } + + #[test] + fn test_machine() { + check_built_in( + r#" +-define(MACHINE, ignored). +bar() -> ?~MACHINE. +"#, + BuiltInMacro::MACHINE, + ); + } + + #[test] + fn test_otp_release() { + check_built_in( + r#" +-define(OTP_RELEASE, ignored). +bar() -> ?~OTP_RELEASE. +"#, + BuiltInMacro::OTP_RELEASE, + ); + } + + #[test] + fn test_resolve_in_include() { + check_user( + r#" +//- /src/include.hrl +-define(MACRO, wrong). +-define(MACRO(_), wrong). + -define(MACRO(), right). +%% ^^^^^^^^^^^^^^^^^^^^^^^^ + +//- /src/main.erl +-module(main). + +-include("include.hrl"). + +foo() -> ?~MACRO(). +"#, + ); + } + + #[test] + fn test_resolve_with_undef() { + check_user( + r#" +-define(MACRO, wrong). +-undef(MACRO). + -define(MACRO, right). +%% ^^^^^^^^^^^^^^^^^^^^^^ + +foo() -> ?~MACRO. +"#, + ); + } + + #[test] + fn test_recursive_fails_cleanly() { + let (resolved, _db, _fixture) = resolve_macro( + r#" +//- /src/include.hrl +-define(FOO, _). +-include("include.hrl"). + +//- /src/main.erl +-module(main). +-include("include.hrl"). +foo() -> ?~FOO. +"#, + ); + assert_eq!(resolved, None); + } +} diff --git a/crates/hir/src/module_data.rs b/crates/hir/src/module_data.rs new file mode 100644 index 0000000000..9b978f59a3 --- /dev/null +++ b/crates/hir/src/module_data.rs @@ -0,0 +1,350 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::sync::Arc; + +use elp_base_db::FileId; +use elp_base_db::SourceDatabase; +use elp_syntax::ast; +use elp_syntax::AstNode; +use elp_syntax::AstPtr; +use elp_syntax::SmolStr; +use elp_syntax::SyntaxNode; + +use crate::db::MinDefDatabase; +use crate::db::MinInternDatabase; +use crate::edoc::EdocHeader; +use crate::Callback; +use crate::DefMap; +use crate::Define; +use crate::Function; +use crate::FunctionId; +use crate::InFile; +use crate::InFileAstPtr; +use crate::InFunctionBody; +use crate::ModuleAttribute; +use crate::Name; +use crate::NameArity; +use crate::Record; +use crate::RecordField; +use crate::Spec; +use crate::SpecId; +use crate::TypeAlias; +use crate::Var; + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum FileKind { + Module, + Header, + Other, +} + +/// Represents an erlang file - header or module +#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] +pub struct File { + pub file_id: FileId, +} + +impl File { + pub fn source(&self, db: &dyn SourceDatabase) -> ast::SourceFile { + db.parse(self.file_id).tree() + } + + pub fn kind(&self, db: &dyn SourceDatabase) -> FileKind { + let source_root = db.source_root(db.file_source_root(self.file_id)); + let ext = source_root + .path_for_file(&self.file_id) + .and_then(|path| path.name_and_extension()) + .and_then(|(_name, ext)| ext); + match ext { + Some("erl") => FileKind::Module, + Some("hrl") => FileKind::Header, + _ => FileKind::Other, + } + } + + pub fn name(&self, db: &dyn SourceDatabase) -> SmolStr { + let source_root = db.source_root(db.file_source_root(self.file_id)); + if let Some((name, Some(ext))) = source_root + .path_for_file(&self.file_id) + .and_then(|path| path.name_and_extension()) + { + SmolStr::new(format!("{}.{}", name, ext)) + } else { + SmolStr::new_inline("unknown") + } + } + + pub fn def_map(&self, db: &dyn MinDefDatabase) -> Arc { + db.def_map(self.file_id) + } +} + +/// Represents a module definition +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Module { + pub file: File, +} + +impl Module { + pub fn module_attribute(&self, db: &dyn MinDefDatabase) -> Option { + let forms = db.file_form_list(self.file.file_id); + forms.module_attribute().map(|a| a.clone()) + } + + pub fn name(&self, db: &dyn MinDefDatabase) -> Name { + let attr = self.module_attribute(db); + attr.map_or(Name::MISSING, |attr| attr.name) + } + + pub fn is_in_otp(&self, db: &dyn MinDefDatabase) -> bool { + is_in_otp(self.file.file_id, db) + } +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct FunctionDef { + pub file: File, + pub exported: bool, + pub deprecated: bool, + pub function: Function, + pub function_id: FunctionId, +} + +impl FunctionDef { + pub fn source(&self, db: &dyn SourceDatabase) -> ast::FunDecl { + let source_file = self.file.source(db); + self.function.form_id.get(&source_file) + } + + pub fn in_function_body( + &self, + db: &dyn MinDefDatabase, + value: T, + ) -> crate::InFunctionBody { + let function_body = db.function_body(InFile::new(self.file.file_id, self.function_id)); + InFunctionBody::new( + function_body, + InFile::new(self.file.file_id, self.function_id), + None, + value, + ) + } + + pub fn is_in_otp(&self, db: &dyn MinDefDatabase) -> bool { + is_in_otp(self.file.file_id, db) + } + + pub fn edoc_comments(&self, db: &dyn MinDefDatabase) -> Option { + let form = InFileAstPtr::new( + self.file.file_id, + AstPtr::new(&ast::Form::FunDecl(self.source(db.upcast()))), + ); + let file_edoc = db.file_edoc_comments(form.file_id())?; + file_edoc.get(&form).cloned() + } +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct SpecDef { + pub file: File, + pub spec: Spec, + pub spec_id: SpecId, +} + +impl SpecDef { + pub fn source(&self, db: &dyn SourceDatabase) -> ast::Spec { + let source_file = self.file.source(db); + self.spec.form_id.get(&source_file) + } +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct SpecdFunctionDef { + pub spec_def: SpecDef, + pub function_def: FunctionDef, +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct RecordDef { + pub file: File, + pub record: Record, +} + +impl RecordDef { + pub fn source(&self, db: &dyn SourceDatabase) -> ast::RecordDecl { + let source_file = self.file.source(db); + self.record.form_id.get(&source_file) + } + + pub fn fields( + &self, + db: &dyn MinDefDatabase, + ) -> impl Iterator + '_ { + let forms = db.file_form_list(self.file.file_id); + self.record.fields.clone().map(move |f| { + ( + forms[f].name.clone(), + RecordFieldDef { + record: self.clone(), + field: forms[f].clone(), + }, + ) + }) + } + + pub fn field_names(&self, db: &dyn MinDefDatabase) -> impl Iterator { + let forms = db.file_form_list(self.file.file_id); + self.record + .fields + .clone() + .map(move |f| forms[f].name.clone()) + } + + pub fn find_field_by_id(&self, db: &dyn MinDefDatabase, id: usize) -> Option { + let forms = db.file_form_list(self.file.file_id); + let field = self.record.fields.clone().nth(id)?; + Some(RecordFieldDef { + record: self.clone(), + field: forms[field].clone(), + }) + } + + pub fn find_field(&self, db: &dyn MinDefDatabase, name: &Name) -> Option { + let forms = db.file_form_list(self.file.file_id); + let field = self + .record + .fields + .clone() + .find(|&field| &forms[field].name == name)?; + Some(RecordFieldDef { + record: self.clone(), + field: forms[field].clone(), + }) + } +} + +/// Represents a record field definition in a particular record +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct RecordFieldDef { + pub record: RecordDef, + pub field: RecordField, +} + +impl RecordFieldDef { + pub fn source(&self, db: &dyn SourceDatabase) -> ast::RecordField { + let record = self.record.source(db); + record.fields().nth(self.field.idx as usize).unwrap() + } +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct TypeAliasDef { + pub file: File, + pub exported: bool, + pub type_alias: TypeAlias, +} + +pub enum TypeAliasSource { + Regular(ast::TypeAlias), + Opaque(ast::Opaque), +} + +impl TypeAliasDef { + pub fn source(&self, db: &dyn SourceDatabase) -> TypeAliasSource { + let source_file = self.file.source(db); + match self.type_alias { + TypeAlias::Opaque { form_id, .. } => TypeAliasSource::Opaque(form_id.get(&source_file)), + TypeAlias::Regular { form_id, .. } => { + TypeAliasSource::Regular(form_id.get(&source_file)) + } + } + } + + pub fn name(&self) -> &NameArity { + match &self.type_alias { + TypeAlias::Regular { name, .. } => name, + TypeAlias::Opaque { name, .. } => name, + } + } +} + +impl TypeAliasSource { + pub fn syntax(&self) -> &SyntaxNode { + match self { + TypeAliasSource::Regular(type_alias) => type_alias.syntax(), + TypeAliasSource::Opaque(opaque) => opaque.syntax(), + } + } + + pub fn type_name(&self) -> Option { + match self { + TypeAliasSource::Regular(type_alias) => type_alias.name(), + TypeAliasSource::Opaque(opaque) => opaque.name(), + } + } +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct CallbackDef { + pub file: File, + pub optional: bool, + pub callback: Callback, +} + +impl CallbackDef { + pub fn source(&self, db: &dyn SourceDatabase) -> ast::Callback { + let source_file = self.file.source(db); + self.callback.form_id.get(&source_file) + } +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct DefineDef { + pub file: File, + pub define: Define, +} + +impl DefineDef { + pub fn source(&self, db: &dyn SourceDatabase) -> ast::PpDefine { + let source_file = self.file.source(db); + self.define.form_id.get(&source_file) + } +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct VarDef { + pub file: File, + // Restrict access to the crate only, so we can ensure it is + // reconstituted against the correct source. + pub(crate) var: AstPtr, + pub hir_var: Var, +} + +impl VarDef { + pub fn source(&self, db: &dyn SourceDatabase) -> ast::Var { + let source_file = self.file.source(db); + self.var.to_node(source_file.syntax()) + } + + pub fn name(&self, db: &dyn MinInternDatabase) -> Name { + db.lookup_var(self.hir_var).clone() + } +} + +fn is_in_otp(file_id: FileId, db: &dyn MinDefDatabase) -> bool { + let source_root_id = db.file_source_root(file_id); + match db.app_data(source_root_id) { + Some(app_data) => { + let project_id = app_data.project_id; + db.project_data(project_id).otp_project_id == Some(project_id) + } + None => false, + } +} diff --git a/crates/hir/src/name.rs b/crates/hir/src/name.rs new file mode 100644 index 0000000000..0024ec74ac --- /dev/null +++ b/crates/hir/src/name.rs @@ -0,0 +1,236 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! See [`Name`]. + +use std::borrow::Cow; +use std::fmt; +use std::ops::Deref; + +use elp_base_db::to_quoted_string; +use elp_syntax::ast; +use elp_syntax::unescape; +use elp_syntax::SmolStr; + +/// `Name` is a wrapper around string, in Erlang abstract forms represented +/// as raw atoms, which is used in hir for both references and declarations +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Name(SmolStr); + +impl fmt::Display for Name { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +impl Name { + /// A fake name for things missing in the source code. + /// + /// Ideally, we want a `gensym` semantics for missing names -- each missing + /// name is equal only to itself. It's not clear how to implement this in + /// salsa though, so we punt on that bit for a moment. + pub const MISSING: Self = Self::new_inline("[missing name]"); + /// Ditto for anonymous vars + pub const ANONYMOUS: Self = Self::new_inline("_"); + + /// Note: this is private to make creating name from random string hard. + const fn new(text: SmolStr) -> Name { + Name(text) + } + + /// Note: The one time it's okay to make a Name from an arbitrary string + /// is when reading it from the wire when talking to erlang_service + pub fn from_erlang_service(text: &str) -> Name { + Name(SmolStr::new(text)) + } + + /// Shortcut to create inline plain text name + const fn new_inline(text: &str) -> Name { + Name::new(SmolStr::new_inline(text)) + } + + pub fn raw(&self) -> SmolStr { + self.0.clone() + } + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn to_quoted_string(&self) -> String { + if self == &Self::MISSING { + self.to_string() + } else { + to_quoted_string(&self.as_str()) + } + } + + /// Resolve a name from the text of token. + /// + /// This replicates the atom normalisation done in the Erlang parser + fn resolve(raw_text: &str) -> Name { + let escaped = unescape::unescape_string(raw_text).unwrap_or(Cow::Borrowed(raw_text)); + Name::new(escaped.into()) + } + + pub(super) fn arg(argument_index_starting_from_one: usize) -> Name { + Self::new(SmolStr::new(format!( + "Arg{argument_index_starting_from_one}" + ))) + } +} + +impl Deref for Name { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl PartialEq for Name { + fn eq(&self, other: &str) -> bool { + self.0 == other + } +} + +impl<'a> PartialEq<&'a str> for Name { + fn eq(&self, other: &&'a str) -> bool { + self.0 == *other + } +} + +/// `NameArity` is a wrapper around `Name` with arity attached, +/// this is used frequently in Erlang for identifying various language elements +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct NameArity(Name, u32); + +impl fmt::Display for NameArity { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}/{}", self.0.to_quoted_string(), self.1) + } +} + +impl NameArity { + pub const fn new(name: Name, arity: u32) -> NameArity { + NameArity(name, arity) + } + + pub fn name(&self) -> &Name { + &self.0 + } + + pub fn arity(&self) -> u32 { + self.1 + } +} + +/// `MacroName` is a wrapper around `Name` with optional arity attached, +/// this is used in Erlang for identifying macros +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct MacroName(Name, Option); + +impl fmt::Display for MacroName { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(arity) = self.1 { + write!(f, "{}/{}", self.0, arity) + } else { + write!(f, "{}", self.0) + } + } +} + +impl MacroName { + pub const fn new(name: Name, arity: Option) -> MacroName { + MacroName(name, arity) + } + + pub fn name(&self) -> &Name { + &self.0 + } + + pub fn arity(&self) -> Option { + self.1 + } + + pub fn with_arity(self, arity: Option) -> Self { + MacroName(self.0, arity) + } +} + +// There's explicitly no conversion from `ast::Name` +// It represents names before macro resolution, which should be +// processed to obtain the final name - plain atom + +pub trait AsName { + fn as_name(&self) -> Name; +} + +impl AsName for ast::Atom { + fn as_name(&self) -> Name { + Name::resolve(&self.raw_text()) + } +} + +impl AsName for ast::Var { + fn as_name(&self) -> Name { + Name::new(self.text().into()) + } +} + +impl AsName for ast::MacroName { + fn as_name(&self) -> Name { + match self { + ast::MacroName::Atom(atom) => atom.as_name(), + ast::MacroName::Var(var) => var.as_name(), + } + } +} + +pub mod known { + macro_rules! known_names { + ($($ident:ident),* $(,)?) => { + $( + #[allow(bad_style)] + pub const $ident: super::Name = + super::Name::new_inline(stringify!($ident)); + )* + }; + } + + known_names!( + // predefined macros + FILE, + FUNCTION_NAME, + FUNCTION_ARITY, + LINE, + MODULE, + MODULE_STRING, + MACHINE, + OTP_RELEASE, + // predefined values + ELP, + // known atoms + erlang, + apply, + export_all, + parse_transform, + // Common Test framework + all, + group, + groups, + init_per_suite, + end_per_suite, + testcase, + warn_missing_spec, + nowarn_missing_spec, + warn_missing_spec_all, + nowarn_missing_spec_all, + ); +} diff --git a/crates/hir/src/resolver.rs b/crates/hir/src/resolver.rs new file mode 100644 index 0000000000..63cb15deb3 --- /dev/null +++ b/crates/hir/src/resolver.rs @@ -0,0 +1,56 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! Name resolution façade. + +use fxhash::FxHashSet; + +use crate::body::scope::ExprScopes; +use crate::body::scope::ScopeId; +use crate::ExprId; +use crate::PatId; +use crate::Var; + +pub type Resolution = (Var, Vec); + +#[derive(Debug, Clone)] +pub struct Resolver { + pub scopes: ExprScopes, +} + +impl Resolver { + pub fn new(expr_scopes: ExprScopes) -> Resolver { + Resolver { + scopes: expr_scopes, + } + } + + pub fn resolve_pat_id(&self, var: &Var, pat_id: PatId) -> Option<&Vec> { + let scope = self.scopes.scope_for_pat(pat_id)?; + self.resolve_var_in_scope(var, scope) + } + + pub fn resolve_expr_id(&self, var: &Var, expr_id: ExprId) -> Option<&Vec> { + let scope = self.scopes.scope_for_expr(expr_id)?; + self.resolve_var_in_scope(var, scope) + } + + pub fn resolve_var_in_scope(&self, var: &Var, scope: ScopeId) -> Option<&Vec> { + self.scopes + .scope_chain(Some(scope)) + .find_map(|scope| self.scopes.entries(scope).lookup(var)) + } + + pub fn all_vars_in_scope(&self, scope: ScopeId) -> FxHashSet { + self.scopes + .scope_chain(Some(scope)) + .flat_map(|scope| self.scopes.entries(scope).names()) + .collect() + } +} diff --git a/crates/hir/src/sema.rs b/crates/hir/src/sema.rs new file mode 100644 index 0000000000..420dbce37b --- /dev/null +++ b/crates/hir/src/sema.rs @@ -0,0 +1,1222 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::cell::RefCell; +use std::iter::FromIterator; +use std::ops::Index; +use std::sync::Arc; +use std::vec::IntoIter; + +use elp_base_db::FileId; +use elp_base_db::ModuleIndex; +use elp_base_db::ModuleName; +use elp_syntax::ast; +use elp_syntax::AstNode; +use elp_syntax::SyntaxNode; +use elp_syntax::TextRange; +use fxhash::FxHashMap; +use fxhash::FxHashSet; +use la_arena::RawIdx; + +use self::find::FindForm; +pub use self::to_def::CallDef; +pub use self::to_def::DefinitionOrReference; +pub use self::to_def::FaDef; +use self::to_def::ToDef; +use crate::body::scope::ScopeId; +use crate::body::UnexpandedIndex; +use crate::db::MinDefDatabase; +use crate::edoc::EdocHeader; +use crate::expr::ClauseId; +use crate::fold::ExprCallBack; +use crate::fold::ExprCallBackCtx; +use crate::fold::FoldBody; +use crate::fold::FoldCtx; +use crate::fold::PatCallBack; +use crate::fold::PatCallBackCtx; +use crate::fold::Strategy; +pub use crate::intern::MinInternDatabase; +pub use crate::intern::MinInternDatabaseStorage; +use crate::resolver::Resolution; +use crate::resolver::Resolver; +use crate::Body; +use crate::BodySourceMap; +use crate::CRClause; +use crate::Clause; +use crate::DefMap; +use crate::Expr; +use crate::ExprId; +use crate::File; +use crate::FormIdx; +use crate::FunctionBody; +use crate::FunctionId; +use crate::InFile; +use crate::InFileAstPtr; +use crate::Literal; +use crate::MacroName; +use crate::Module; +use crate::Name; +use crate::PPDirective; +use crate::Pat; +use crate::PatId; +use crate::SpecId; +use crate::Term; +use crate::TermId; +use crate::TypeExpr; +use crate::TypeExprId; +use crate::Var; +use crate::VarDef; + +mod find; +pub(crate) mod to_def; + +pub struct ModuleIter(Arc); + +impl IntoIterator for ModuleIter { + type Item = ModuleName; + + type IntoIter = IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.all_modules().into_iter() + } +} + +/// Primary API to get Semantic information from HIR +pub struct Semantic<'db> { + pub db: &'db dyn MinDefDatabase, +} + +impl<'db> Semantic<'db> { + pub fn new(db: &'db Db) -> Self { + Self { db } + } +} + +impl<'db> Semantic<'db> { + pub fn parse(&self, file_id: FileId) -> InFile { + InFile::new(file_id, self.db.parse(file_id).tree()) + } + + pub fn def_map(&self, file_id: FileId) -> Arc { + self.db.def_map(file_id) + } + + pub fn to_def(&self, ast: InFile<&T>) -> Option { + ToDef::to_def(self, ast) + } + + pub fn to_expr(&self, expr: InFile<&ast::Expr>) -> Option> { + let function_id = self.find_enclosing_function(expr.file_id, expr.value.syntax())?; + let (body, body_map) = self + .db + .function_body_with_source(expr.with_value(function_id)); + let expr_id = &body_map.expr_id(expr)?; + Some(InFunctionBody { + body, + function_id: expr.with_value(function_id), + body_map: Some(body_map).into(), + value: *expr_id, + }) + } + + pub fn to_function_body(&self, function_id: InFile) -> InFunctionBody<()> { + let (body, body_map) = self.db.function_body_with_source(function_id); + InFunctionBody { + body, + function_id, + body_map: Some(body_map).into(), + value: (), + } + } + + pub fn resolve_module_names(&self, from_file: FileId) -> Option { + let source_root_id = self.db.file_source_root(from_file); + let project_id = self.db.app_data(source_root_id)?.project_id; + let module_index = self.db.module_index(project_id); + Some(ModuleIter(module_index)) + } + + pub fn module_name(&self, file_id: FileId) -> Option { + let source_root_id = self.db.file_source_root(file_id); + let project_id = self.db.app_data(source_root_id)?.project_id; + let module_index = self.db.module_index(project_id); + let module_name = module_index.module_for_file(file_id)?; + Some(module_name.clone()) + } + + pub fn resolve_module_name(&self, file_id: FileId, name: &str) -> Option { + let source_root_id = self.db.file_source_root(file_id); + let project_id = self.db.app_data(source_root_id)?.project_id; + let module_index = self.db.module_index(project_id); + let module_file_id = module_index.file_for_module(name)?; + Some(Module { + file: File { + file_id: module_file_id, + }, + }) + } + + pub fn file_edoc_comments( + &self, + file_id: FileId, + ) -> Option, EdocHeader>> { + self.db.file_edoc_comments(file_id) + } + + pub fn form_edoc_comments(&self, form: InFileAstPtr) -> Option { + let file_edoc = self.file_edoc_comments(form.file_id())?; + file_edoc.get(&form).cloned() + } + + pub fn resolve_var_to_pats(&self, var_in: InFile<&ast::Var>) -> Option> { + let function_id = self.find_enclosing_function(var_in.file_id, var_in.value.syntax())?; + let clause_id = self.find_enclosing_function_clause(var_in.value.syntax())?; + let resolver = self.clause_resolver(var_in.with_value(function_id), clause_id)?; + let expr = ast::Expr::ExprMax(ast::ExprMax::Var(var_in.value.clone())); + if let Some(expr_id) = resolver.expr_id_ast(self.db, var_in.with_value(&expr)) { + let var = resolver[expr_id].as_var()?; + resolver.value.resolve_expr_id(&var, expr_id).cloned() + } else { + let pat_id = resolver.pat_id_ast(self.db, var_in.with_value(&expr))?; + let var = resolver[pat_id].as_var()?; + resolver.value.resolve_pat_id(&var, pat_id).cloned() + } + } + + pub fn expand(&self, call: InFile<&ast::MacroCallExpr>) -> Option<(MacroName, String)> { + let (body, body_source) = self.find_body(call.file_id, call.value.syntax())?; + let name = body_source.resolved_macro(call)?.name(self.db); + let expr = ast::Expr::cast(call.value.syntax().clone())?; + let any_expr_id = body_source.any_id(call.with_value(&expr))?; + Some((name, body.print_any_expr(self.db.upcast(), any_expr_id))) + } + + pub fn scope_for(&self, var_in: InFile<&ast::Var>) -> Option<(Resolver, ScopeId)> { + let function_id = self.find_enclosing_function(var_in.file_id, var_in.value.syntax())?; + let clause_id = self.find_enclosing_function_clause(var_in.value.syntax())?; + let resolver = self.clause_resolver(var_in.with_value(function_id), clause_id)?; + let expr = ast::Expr::ExprMax(ast::ExprMax::Var(var_in.value.clone())); + if let Some(expr_id) = resolver.expr_id_ast(self.db, var_in.with_value(&expr)) { + let scope = resolver.value.scopes.scope_for_expr(expr_id)?; + Some((resolver.value, scope)) + } else { + let pat_id = resolver.pat_id_ast(self.db, var_in.with_value(&expr))?; + let scope = resolver.value.scopes.scope_for_pat(pat_id)?; + Some((resolver.value, scope)) + } + } + + pub fn find_body( + &self, + file_id: FileId, + syntax: &SyntaxNode, + ) -> Option<(Arc, Arc)> { + let form = syntax.ancestors().find_map(ast::Form::cast)?; + let form_list = self.db.file_form_list(file_id); + let form = form_list.find_form(&form)?; + match form { + FormIdx::Function(fun) => { + let (body, map) = self.db.function_body_with_source(InFile::new(file_id, fun)); + Some((body.body.clone(), map)) + } + FormIdx::Record(record) => { + let (body, map) = self + .db + .record_body_with_source(InFile::new(file_id, record)); + Some((body.body.clone(), map)) + } + FormIdx::Spec(spec) => { + let (body, map) = self.db.spec_body_with_source(InFile::new(file_id, spec)); + Some((body.body.clone(), map)) + } + FormIdx::Callback(cb) => { + let (body, map) = self.db.callback_body_with_source(InFile::new(file_id, cb)); + Some((body.body.clone(), map)) + } + FormIdx::TypeAlias(alias) => { + let (body, map) = self.db.type_body_with_source(InFile::new(file_id, alias)); + Some((body.body.clone(), map)) + } + FormIdx::Attribute(attr) => { + let (body, map) = self + .db + .attribute_body_with_source(InFile::new(file_id, attr)); + Some((body.body.clone(), map)) + } + FormIdx::CompileOption(attr) => { + let (body, map) = self.db.compile_body_with_source(InFile::new(file_id, attr)); + Some((body.body.clone(), map)) + } + FormIdx::PPDirective(pp) => match form_list[pp] { + PPDirective::Define(define) => self + .db + .define_body_with_source(InFile::new(file_id, define)) + .map(|(body, map)| (body.body.clone(), map)), + _ => None, + }, + _ => None, + } + } + + fn find_form(&self, ast: InFile<&T>) -> Option { + FindForm::find(self, ast) + } + + pub fn find_enclosing_function( + &self, + file_id: FileId, + syntax: &SyntaxNode, + ) -> Option { + let form = syntax.ancestors().find_map(ast::Form::cast)?; + let form_list = self.db.file_form_list(file_id); + let form = form_list.find_form(&form)?; + match form { + FormIdx::Function(fun) => Some(fun), + _ => None, + } + } + + pub fn find_enclosing_function_clause(&self, syntax: &SyntaxNode) -> Option { + // ClauseId's are allocated sequentially. Find the one we need. + let fun = syntax.ancestors().find_map(ast::FunDecl::cast)?; + let idx = fun.clauses().enumerate().find_map(|(idx, clause)| { + if clause + .syntax() + .text_range() + .contains(syntax.text_range().start()) + { + Some(idx) + } else { + None + } + })?; + Some(ClauseId::from_raw(RawIdx::from(idx as u32))) + } + + pub fn find_enclosing_spec(&self, file_id: FileId, syntax: &SyntaxNode) -> Option { + let form = syntax.ancestors().find_map(ast::Form::cast)?; + let form_list = self.db.file_form_list(file_id); + let form = form_list.find_form(&form)?; + match form { + FormIdx::Spec(fun) => Some(fun), + _ => None, + } + } + + pub fn vardef_source(&self, def: &VarDef) -> ast::Var { + def.source(self.db.upcast()) + } + + /// Return the free and bound variables in a given ast expression. + pub fn free_vars_ast(&self, file_id: FileId, expr: &ast::Expr) -> Option { + let function_id = self.find_enclosing_function(file_id, expr.syntax())?; + let infile_function_id = InFile::new(file_id, function_id); + + let (body, source_map) = self.db.function_body_with_source(infile_function_id); + let expr_id_in = source_map.expr_id(InFile { + file_id, + value: &expr, + })?; + self.free_vars(&InFunctionBody { + body, + function_id: infile_function_id, + body_map: Some(source_map).into(), + value: expr_id_in, + }) + } + + /// Return the free and bound variables in a given expression. + pub fn free_vars(&self, expr: &InFunctionBody) -> Option { + let function = expr.function_id; + let scopes = self.db.function_scopes(function); + let expr_id_in = expr.value; + let clause_id = expr.clause_id(&expr_id_in)?; + let clause_scopes = scopes.get(clause_id)?; + let resolver = Resolver::new(clause_scopes); + + let inside_pats = FoldCtx::fold_expr( + &expr.body.body, + Strategy::TopDown, + expr_id_in, + FxHashSet::default(), + &mut |acc, _| acc, + &mut |mut acc, ctx| { + acc.insert(ctx.pat_id.clone()); + acc + }, + ); + + let update_vars = |mut analysis: ScopeAnalysis, var_id: Var, defs: Option<&Vec>| { + if let Some(defs) = defs { + let (inside, outside): (Vec, Vec) = + defs.iter().partition(|pat_id| inside_pats.contains(pat_id)); + if !outside.is_empty() { + analysis.free.insert((var_id, outside)); + }; + if !inside.is_empty() { + analysis.bound.insert((var_id, inside)); + }; + analysis + } else { + analysis + } + }; + + Some(FoldCtx::fold_expr( + &expr.body.body, + Strategy::TopDown, + expr_id_in.clone(), + ScopeAnalysis::new(), + &mut |defs, ctx| match ctx.expr { + Expr::Var(var_id) => { + update_vars(defs, var_id, resolver.resolve_expr_id(&var_id, ctx.expr_id)) + } + _ => defs, + }, + &mut |defs, ctx| match ctx.pat { + Pat::Var(var_id) => { + update_vars(defs, var_id, resolver.resolve_pat_id(&var_id, ctx.pat_id)) + } + _ => defs, + }, + )) + } + + /// Wrap the `Resolver` for the function clause containing the + /// `syntax` in an `InFunctionBody`. + pub fn function_clause_resolver( + &self, + file_id: FileId, + syntax: &SyntaxNode, + ) -> Option> { + let function_id = self.find_enclosing_function(file_id, syntax)?; + let clause_id = self.find_enclosing_function_clause(syntax)?; + self.clause_resolver(InFile::new(file_id, function_id), clause_id) + } + + pub fn clause_resolver( + &self, + function_id: InFile, + clause_id: ClauseId, + ) -> Option> { + let body = self.db.function_body(function_id); + let scopes = self.db.function_scopes(function_id); + let clause_scopes = scopes.get(clause_id)?; + let resolver = Resolver::new(clause_scopes); + Some(InFunctionBody { + body, + function_id, + body_map: None.into(), // We may not need it, do not get it now + value: resolver, + }) + } + + pub fn find_vars_in_clause_ast(&self, expr: &InFile<&ast::Expr>) -> Option> { + let clause = self.find_enclosing_function_clause(&expr.value.syntax())?; + let in_function = self.to_expr(*expr)?; + ScopeAnalysis::clause_vars_in_scope(&self, &in_function.with_value(&in_function[clause])) + } + + /// Find all other variables within the function clause that resolve + /// to the one given. + pub fn find_local_usages(&self, var: InFile<&ast::Var>) -> Option> { + // TODO: replace this function with the appropriate one when the + // highlight usages feature exists. T128835148 + let var_resolved = self.resolve_var_to_pats(var)?; + let mut resolved_set = FxHashSet::from_iter(var_resolved); + let clause = var + .value + .syntax() + .ancestors() + .find_map(ast::FunctionClause::cast)?; + + // We first extend the resolved_set to the widest one that + // includes the current variable resolution. This ensures + // that if we are looking at a variable in one leg of a case + // clause, and it has equivalents in another leg, then these + // are also found. + clause + .syntax() + .descendants() + .filter_map(ast::Var::cast) + .for_each(|v| { + if let Some(ds) = self.resolve_var_to_pats(InFile::new(var.file_id, &v)) { + let ds_set = FxHashSet::from_iter(ds); + if resolved_set.intersection(&ds_set).next().is_some() { + resolved_set.extend(ds_set); + } + } + }); + + // Then we actually check for any variables that resolve to it. + let vars: Vec<_> = clause + .syntax() + .descendants() + .filter_map(ast::Var::cast) + .filter_map(|v| { + if let Some(ds) = self.resolve_var_to_pats(InFile::new(var.file_id, &v)) { + // We have resolved a candidate Var. + // Check that it resolves to the one we are looking for + + // We may be in an arm of a case, receive, + // try, and so we will only find one + // definition. So check for an intersection + // with the whole. + + if resolved_set + .intersection(&FxHashSet::from_iter(ds)) + .next() + .is_some() + { + Some(v) + } else { + None + } + } else { + None + } + }) + .collect(); + + if vars.is_empty() { None } else { Some(vars) } + } + + pub fn fold_function<'a, T>( + &self, + function_id: InFile, + initial: T, + for_expr: FunctionExprCallBack<'a, T>, + for_pat: FunctionPatCallBack<'a, T>, + ) -> T { + let function_body = self.db.function_body(function_id); + fold_function_body( + WithMacros::No, + &function_body, + Strategy::TopDown, + initial, + for_expr, + for_pat, + ) + } + + pub fn fold_clause<'a, T>( + &'a self, + function_id: InFile, + clause_id: ClauseId, + initial: T, + for_expr: ExprCallBack<'a, T>, + for_pat: PatCallBack<'a, T>, + ) -> T { + let function_body = self.db.function_body(function_id); + function_body[clause_id] + .exprs + .iter() + .fold(initial, |acc_inner, expr_id| { + FoldCtx::fold_expr( + &function_body.body, + Strategy::TopDown, + *expr_id, + acc_inner, + for_expr, + for_pat, + ) + }) + } + + pub fn bound_vars_in_pattern_diagnostic( + &self, + file_id: FileId, + ) -> FxHashSet<(InFile, PatId, ast::Var)> { + let def_map = self.def_map(file_id); + let mut res = FxHashSet::default(); + for (_name, def) in def_map.get_functions() { + if def.file.file_id == file_id { + let function_id = InFile::new(file_id, def.function_id); + + self.fold_function( + function_id, + (), + &mut |acc, clause_id, ctx| { + if let Some(mut resolver) = self.clause_resolver(function_id, clause_id) { + let mut bound_vars = + BoundVarsInPat::new(&self, &mut resolver, file_id, &mut res); + match ctx.expr { + Expr::Match { lhs, rhs: _ } => { + bound_vars.report_any_bound_vars(&lhs) + } + Expr::Case { expr: _, clauses } => { + bound_vars.cr_clauses(&clauses); + } + Expr::Try { + exprs: _, + of_clauses, + catch_clauses, + after: _, + } => { + bound_vars.cr_clauses(&of_clauses); + catch_clauses.iter().for_each(|clause| { + bound_vars.report_any_bound_vars(&clause.reason); + }) + } + _ => {} + } + }; + acc + }, + &mut |acc, _, _| acc, + ); + } + } + res + } + + fn bound_vars_in_pat( + &self, + pat_id: &PatId, + resolver: &mut InFunctionBody, + file_id: FileId, + ) -> FxHashSet<(InFile, PatId, ast::Var)> { + let parse = self.parse(file_id); + let body_map = &resolver.get_body_map(self.db); + FoldCtx::fold_pat( + &resolver.body.body, + Strategy::TopDown, + *pat_id, + FxHashSet::default(), + &mut |acc, _| acc, + &mut |mut acc, ctx| { + match &resolver[ctx.pat_id] { + Pat::Var(var) => { + if let Some(pat_ids) = resolver.value.resolve_pat_id(&var, ctx.pat_id) { + pat_ids.iter().for_each(|def_pat_id| { + if &ctx.pat_id != def_pat_id { + if let Some(pat_ptr) = body_map.pat(ctx.pat_id) { + if let Some(pat_ast) = pat_ptr.to_node(&parse) { + match pat_ast { + ast::Expr::ExprMax(ast::ExprMax::Var(var)) => { + if var.syntax().text() != "_" { + acc.insert(( + resolver.function_id, + ctx.pat_id.clone(), + var, + )); + } + } + _ => {} + } + }; + } + } + }); + } + } + _ => {} + }; + acc + }, + ) + } + + pub fn is_atom_named(&self, expr: &Expr, known_atom: crate::Name) -> bool { + match expr { + Expr::Literal(Literal::Atom(atom)) => self.db.lookup_atom(*atom) == known_atom, + _ => false, + } + } +} + +pub type FunctionExprCallBack<'a, T> = &'a mut dyn FnMut(T, ClauseId, ExprCallBackCtx) -> T; +pub type FunctionPatCallBack<'a, T> = &'a mut dyn FnMut(T, ClauseId, PatCallBackCtx) -> T; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum WithMacros { + Yes, + No, +} + +fn fold_function_body<'a, T>( + with_macros: WithMacros, + function_body: &FunctionBody, + strategy: Strategy, + initial: T, + for_expr: FunctionExprCallBack<'a, T>, + for_pat: FunctionPatCallBack<'a, T>, +) -> T { + function_body + .clauses + .iter() + .fold(initial, |acc, (clause_id, clause)| { + clause.exprs.iter().fold(acc, |acc_inner, expr_id| { + let fold_body = if with_macros == WithMacros::Yes { + FoldBody::UnexpandedIndex(UnexpandedIndex(&function_body.body)) + } else { + FoldBody::Body(&function_body.body) + }; + FoldCtx::fold_expr_foldbody( + &fold_body, + strategy, + *expr_id, + acc_inner, + &mut |acc, ctx| for_expr(acc, clause_id, ctx), + &mut |acc, ctx| for_pat(acc, clause_id, ctx), + ) + }) + }) +} + +// --------------------------------------------------------------------- + +struct BoundVarsInPat<'a> { + sema: &'a Semantic<'a>, + resolver: &'a mut InFunctionBody, + file_id: FileId, + res: &'a mut FxHashSet<(InFile, PatId, ast::Var)>, +} + +impl<'a> BoundVarsInPat<'a> { + fn new( + sema: &'a Semantic<'a>, + resolver: &'a mut InFunctionBody, + file_id: FileId, + res: &'a mut FxHashSet<(InFile, PatId, ast::Var)>, + ) -> Self { + BoundVarsInPat { + sema, + resolver, + file_id, + res, + } + } + + fn report_any_bound_vars(&mut self, pat_id: &PatId) { + let bound_vars = self + .sema + .bound_vars_in_pat(&pat_id, self.resolver, self.file_id); + bound_vars.into_iter().for_each(|v| { + self.res.insert(v); + }); + } + + fn cr_clauses(&mut self, clauses: &[CRClause]) { + clauses + .iter() + .for_each(|clause| self.report_any_bound_vars(&clause.pat)) + } +} + +// --------------------------------------------------------------------- + +#[derive(Debug)] +pub struct ScopeAnalysis { + pub free: FxHashSet, + pub bound: FxHashSet, +} + +impl ScopeAnalysis { + pub fn new() -> Self { + Self { + free: FxHashSet::default(), + bound: FxHashSet::default(), + } + } + + pub fn clause_vars_in_scope( + sema: &Semantic, + clause: &InFunctionBody<&Clause>, + ) -> Option> { + let acc = FxHashSet::default(); + let x = clause.value.exprs.iter().fold(acc, |mut acc, expr| { + let mut analyzer = ScopeAnalysis::new(); + analyzer.walk_expr(sema, &clause.with_value(*expr)); + analyzer.update_scope_analysis(&mut acc); + acc + }); + Some(x) + } + + pub fn update_scope_analysis(&self, acc: &mut FxHashSet) { + acc.extend(self.bound.iter().map(|(v, _p)| v)); + acc.extend(self.free.iter().map(|(v, _p)| v)); + } + + /// Process an expression in the current scope context, updating + /// the free and bound vars + pub fn walk_ast_expr(&mut self, sema: &Semantic, file_id: FileId, expr: ast::Expr) { + if let Some(scopes) = sema.free_vars_ast(file_id, &expr) { + self.callback(scopes.free, scopes.bound); + } + } + + /// Process an expression in the current scope context, updating + /// the free and bound vars + pub fn walk_expr(&mut self, sema: &Semantic, expr: &InFunctionBody) { + if let Some(scopes) = sema.free_vars(expr) { + self.callback(scopes.free, scopes.bound); + } + } + + fn callback(&mut self, free: FxHashSet, bound: FxHashSet) { + // If any of the free variables are already bound, remove them. + let (free, _rest): (FxHashSet, FxHashSet) = + free.into_iter().partition(|v| !self.bound.contains(v)); + self.free.extend(free); + self.bound.extend(bound); + } +} + +#[derive(Debug, Clone)] +pub struct InFunctionBody { + body: Arc, + function_id: InFile, + // cache body_map if we already have it when wrapping the value. + // This field should go away once we fully use the hir API only + body_map: RefCell>>, + pub value: T, +} + +impl InFunctionBody { + pub fn new( + body: Arc, + function_id: InFile, + body_map: Option>, + value: T, + ) -> InFunctionBody { + InFunctionBody { + body, + function_id, + body_map: body_map.into(), + value, + } + } + + pub fn as_ref(&self) -> InFunctionBody<&T> { + self.with_value(&self.value) + } + + pub fn with_value(&self, value: U) -> InFunctionBody { + InFunctionBody { + body: self.body.clone(), + function_id: self.function_id, + body_map: self.body_map.clone(), + value, + } + } + + pub fn file_id(&self) -> FileId { + self.function_id.file_id + } + + pub fn get_body_map(&self, db: &dyn MinDefDatabase) -> Arc { + if let Some(body_map) = &self.body_map.borrow().as_ref() { + //return explicitly here because borrow is still held in else statement + //https://stackoverflow.com/questions/30243606/why-is-a-borrow-still-held-in-the-else-block-of-an-if-let + return Arc::clone(body_map); + } + let (_body, body_map) = db.function_body_with_source(self.function_id); + *self.body_map.borrow_mut() = Some(body_map.clone()); + body_map + } + + pub fn expr_id(&self, expr: &Expr) -> Option { + self.body.body.expr_id(expr) + } + + pub fn expr_id_ast(&self, db: &dyn MinDefDatabase, expr: InFile<&ast::Expr>) -> Option { + self.get_body_map(db).expr_id(expr) + } + + pub fn pat_id_ast(&self, db: &dyn MinDefDatabase, expr: InFile<&ast::Expr>) -> Option { + self.get_body_map(db).pat_id(expr) + } + + pub fn expr_from_id( + &mut self, + db: &dyn MinDefDatabase, + expr: InFile<&ast::Expr>, + ) -> Option { + let expr_id = self.expr_id_ast(db, expr)?; + Some(self.body.body[expr_id].clone()) + } + + pub fn clause_id(&self, expr_id: &ExprId) -> Option { + // Can we have a reverse lookup for this instead? + // Or go to syntax, parent, clause id + let idx = self.body.clauses.iter().find_map(|(idx, clause)| { + if clause.exprs.iter().any(|expr| { + FoldCtx::fold_expr( + &self.body.body, + Strategy::TopDown, + *expr, + false, + &mut |acc, ctx| acc || expr_id == &ctx.expr_id, + &mut |acc, _| acc, + ) + }) { + Some(idx) + } else { + None + } + })?; + Some(idx) + } + + pub fn clauses(&self) -> impl Iterator { + self.body.clauses.iter() + } + + pub fn body(&self) -> Arc { + self.body.body.clone() + } + + pub fn fold_expr<'a, R>( + &self, + strategy: Strategy, + expr_id: ExprId, + initial: R, + for_expr: ExprCallBack<'a, R>, + for_pat: PatCallBack<'a, R>, + ) -> R { + FoldCtx::fold_expr( + &self.body.body, + strategy, + expr_id, + initial, + for_expr, + for_pat, + ) + } + + pub fn fold_pat<'a, R>( + &self, + strategy: Strategy, + pat_id: PatId, + initial: R, + for_expr: ExprCallBack<'a, R>, + for_pat: PatCallBack<'a, R>, + ) -> R { + FoldCtx::fold_pat( + &self.body.body, + strategy, + pat_id, + initial, + for_expr, + for_pat, + ) + } + + pub fn fold_function<'a, R>( + &self, + initial: R, + for_expr: FunctionExprCallBack<'a, R>, + for_pat: FunctionPatCallBack<'a, R>, + ) -> R { + fold_function_body( + WithMacros::No, + &self.body, + Strategy::TopDown, + initial, + for_expr, + for_pat, + ) + } + + pub fn fold_function_with_macros<'a, R>( + &self, + strategy: Strategy, + initial: R, + for_expr: FunctionExprCallBack<'a, R>, + for_pat: FunctionPatCallBack<'a, R>, + ) -> R { + fold_function_body( + WithMacros::Yes, + &self.body, + strategy, + initial, + for_expr, + for_pat, + ) + } + + pub fn range_for_expr(&self, db: &dyn MinDefDatabase, expr_id: ExprId) -> Option { + let body_map = self.get_body_map(db); + let ast = body_map.expr(expr_id)?; + Some(ast.range()) + } + + pub fn range_for_pat(&mut self, db: &dyn MinDefDatabase, pat_id: PatId) -> Option { + let body_map = self.get_body_map(db); + let ast = body_map.pat(pat_id)?; + Some(ast.range()) + } + + pub fn as_atom_name(&self, db: &dyn MinDefDatabase, expr: &ExprId) -> Option { + Some(db.lookup_atom(self[*expr].as_atom()?)) + } +} + +impl Index for InFunctionBody { + type Output = Clause; + + fn index(&self, index: ClauseId) -> &Self::Output { + &self.body[index] + } +} + +impl Index for InFunctionBody { + type Output = Expr; + + fn index(&self, index: ExprId) -> &Self::Output { + &self.body.body[index] + } +} + +impl Index for InFunctionBody { + type Output = Pat; + + fn index(&self, index: PatId) -> &Self::Output { + &self.body.body[index] + } +} + +impl Index for InFunctionBody { + type Output = TypeExpr; + + fn index(&self, index: TypeExprId) -> &Self::Output { + &self.body.body[index] + } +} + +impl Index for InFunctionBody { + type Output = Term; + + fn index(&self, index: TermId) -> &Self::Output { + &self.body.body[index] + } +} + +#[cfg(test)] +mod tests { + use elp_base_db::fixture::WithFixture; + use elp_base_db::SourceDatabase; + use elp_syntax::algo::find_node_at_offset; + use elp_syntax::ast; + use elp_syntax::AstNode; + use expect_test::expect; + use expect_test::Expect; + use itertools::Itertools; + + use crate::test_db::TestDB; + use crate::InFile; + use crate::Semantic; + + #[track_caller] + fn check_local_usages(fixture_before: &str, expect: Expect) { + let (db, position) = TestDB::with_position(fixture_before); + let sema = Semantic::new(&db); + + let file_syntax = db.parse(position.file_id).syntax_node(); + let var: ast::Var = find_node_at_offset(&file_syntax, position.offset).unwrap(); + let usages = sema + .find_local_usages(InFile { + file_id: position.file_id, + value: &var, + }) + .unwrap(); + expect.assert_debug_eq(&usages); + } + + #[test] + fn test_find_local_usages_1() { + check_local_usages( + r#"testz() -> + case rand:uniform(2) of + 1 -> + Z = 1; + 2 -> + ~Z = 2; + Z -> + ok + end, + Z."#, + expect![[r#" + [ + Var { + syntax: VAR@109..110 + VAR@109..110 "Z" + , + }, + Var { + syntax: VAR@171..172 + VAR@171..172 "Z" + , + }, + Var { + syntax: VAR@201..202 + VAR@201..202 "Z" + , + }, + Var { + syntax: VAR@279..280 + VAR@279..280 "Z" + , + }, + ] + "#]], + ) + } + + #[test] + fn test_find_local_usages_2() { + check_local_usages( + r#"main() -> + Y = 5, + AssertIs5 = fun (X) -> + ~Y = X, + erlang:display(Y) + end, + AssertIs5(2), + erlang:display(Y), + ok."#, + expect![[r#" + [ + Var { + syntax: VAR@29..30 + VAR@29..30 "Y" + , + }, + Var { + syntax: VAR@101..102 + VAR@101..102 "Y" + , + }, + Var { + syntax: VAR@146..147 + VAR@146..147 "Y" + , + }, + Var { + syntax: VAR@240..241 + VAR@240..241 "Y" + , + }, + ] + "#]], + ) + } + + #[track_caller] + fn check_bound_var_in_pattern(fixture: &str) { + let (db, fixture) = TestDB::with_fixture(fixture); + let annotations = fixture.annotations(&db); + let expected: Vec<_> = annotations + .iter() + .map(|(fr, _)| fr.range) + .sorted_by(|a, b| a.start().cmp(&b.start())) + .collect(); + let file_id = fixture.files[0]; + let sema = Semantic::new(&db); + let vars = sema.bound_vars_in_pattern_diagnostic(file_id); + let ranges: Vec<_> = vars + .iter() + .map(|(_, _, v)| v.syntax().text_range()) + .sorted_by(|a, b| a.start().cmp(&b.start())) + .collect(); + assert_eq!(expected, ranges); + } + + #[test] + fn bound_variable_in_pattern_1() { + check_bound_var_in_pattern( + r#" + f(Var1) -> + Var1 = 1. + %% ^^^^ "#, + ) + } + + #[test] + fn bound_variable_in_pattern_2() { + check_bound_var_in_pattern( + r#" + f(Var1) -> + Var2 = 1."#, + ) + } + + #[test] + fn bound_variable_in_pattern_3() { + check_bound_var_in_pattern( + r#" + g(Var2) -> + case a:b() of + {ok, Var2} -> ok; + %% ^^^^ + _ -> error + end."#, + ) + } + + #[test] + fn bound_variable_in_pattern_4() { + check_bound_var_in_pattern( + r#" + h(Var3, Var4) -> + try a:b() of + {New, Var3} -> + %% ^^^^ + New + catch Var4 -> + %% ^^^^ + error + end."#, + ) + } + + #[test] + fn bound_variable_in_pattern_5() { + check_bound_var_in_pattern( + r#" + fun_expr(New) -> + fun(New, Var5) -> + Var5 = New + %% ^^^^ + end."#, + ) + } + + #[test] + fn bound_variable_in_pattern_6() { + check_bound_var_in_pattern( + r#" + named_fun_expr() -> + fun F(New, Var6) -> + New = Var6, + %% ^^^ + F = Var6 + %% ^ + end."#, + ) + } + + #[test] + fn bound_variable_in_pattern_not_underscore() { + check_bound_var_in_pattern( + // Do not report for '_' + r#" + test4(L) -> + [H | _] = lists:map( + fun app_a_mod2:id/1, + L), + _ = atom_to_list(H), + {H}. + "#, + ) + } +} diff --git a/crates/hir/src/sema/find.rs b/crates/hir/src/sema/find.rs new file mode 100644 index 0000000000..0af6796faa --- /dev/null +++ b/crates/hir/src/sema/find.rs @@ -0,0 +1,308 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_base_db::FileId; +use elp_syntax::ast; +use elp_syntax::AstNode; + +use crate::Behaviour; +use crate::Callback; +use crate::Define; +use crate::Export; +use crate::FormIdx; +use crate::Import; +use crate::InFile; +use crate::IncludeAttributeId; +use crate::ModuleAttribute; +use crate::OptionalCallbacks; +use crate::Record; +use crate::Semantic; +use crate::Spec; +use crate::TypeAlias; +use crate::TypeExport; + +pub trait FindForm: Clone { + type Form; + + fn find(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option; +} + +impl FindForm for ast::ModuleAttribute { + type Form = ModuleAttribute; + + fn find(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let source_file = sema.parse(ast.file_id).value; + let form_list = sema.db.file_form_list(ast.file_id); + let form_idx = form_list.find_form(&ast::Form::ModuleAttribute(ast.value.clone()))?; + let form = match form_idx { + FormIdx::ModuleAttribute(idx) => { + if &form_list[idx].form_id.get(&source_file) == ast.value { + Some(idx) + } else { + None + } + } + _ => None, + }?; + Some(form_list[form].clone()) + } +} + +impl FindForm for ast::TypeName { + type Form = TypeAlias; + + fn find(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let ast_form = ast::Form::cast(ast.value.syntax().parent()?)?; + let source_file = sema.parse(ast.file_id).value; + let form_list = sema.db.file_form_list(ast.file_id); + let form_idx = form_list.find_form(&ast_form)?; + let form = match form_idx { + FormIdx::TypeAlias(idx) => { + if &form_list[idx].form_id().get(&source_file) == &ast_form { + Some(idx) + } else { + None + } + } + _ => None, + }?; + Some(form_list[form].clone()) + } +} + +impl FindForm for ast::RecordDecl { + type Form = Record; + + fn find(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let source_file = sema.parse(ast.file_id).value; + let form_list = sema.db.file_form_list(ast.file_id); + let form_idx = form_list.find_form(&ast::Form::RecordDecl(ast.value.clone()))?; + let form = match form_idx { + FormIdx::Record(idx) => { + if &form_list[idx].form_id.get(&source_file) == ast.value { + Some(idx) + } else { + None + } + } + _ => None, + }?; + Some(form_list[form].clone()) + } +} + +impl FindForm for ast::Callback { + type Form = Callback; + + fn find(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let source_file = sema.parse(ast.file_id).value; + let form_list = sema.db.file_form_list(ast.file_id); + let form_idx = form_list.find_form(&ast::Form::Callback(ast.value.clone()))?; + let form = match form_idx { + FormIdx::Callback(idx) => { + if &form_list[idx].form_id.get(&source_file) == ast.value { + Some(idx) + } else { + None + } + } + _ => None, + }?; + Some(form_list[form].clone()) + } +} + +impl FindForm for ast::PpDefine { + type Form = Define; + + fn find(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let source_file = sema.parse(ast.file_id).value; + let form_list = sema.db.file_form_list(ast.file_id); + let form_idx = form_list.find_form(&ast::Form::cast(ast.value.syntax().clone())?)?; + let form = match form_idx { + FormIdx::PPDirective(idx) => { + let idx = form_list[idx].as_define()?; + if &form_list[idx].form_id.get(&source_file) == ast.value { + Some(idx) + } else { + None + } + } + _ => None, + }?; + Some(form_list[form].clone()) + } +} + +impl FindForm for ast::BehaviourAttribute { + type Form = Behaviour; + + fn find(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let source_file = sema.parse(ast.file_id).value; + let form_list = sema.db.file_form_list(ast.file_id); + let form_idx = form_list.find_form(&ast::Form::BehaviourAttribute(ast.value.clone()))?; + let form = match form_idx { + FormIdx::Behaviour(idx) => { + if &form_list[idx].form_id.get(&source_file) == ast.value { + Some(idx) + } else { + None + } + } + _ => None, + }?; + Some(form_list[form].clone()) + } +} + +impl FindForm for ast::ImportAttribute { + type Form = Import; + + fn find(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let source_file = sema.parse(ast.file_id).value; + let form_list = sema.db.file_form_list(ast.file_id); + let form_idx = form_list.find_form(&ast::Form::ImportAttribute(ast.value.clone()))?; + let form = match form_idx { + FormIdx::Import(idx) => { + if &form_list[idx].form_id.get(&source_file) == ast.value { + Some(idx) + } else { + None + } + } + _ => None, + }?; + Some(form_list[form].clone()) + } +} + +impl FindForm for ast::ExportAttribute { + type Form = Export; + + fn find(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let source_file = sema.parse(ast.file_id).value; + let form_list = sema.db.file_form_list(ast.file_id); + let form_idx = form_list.find_form(&ast::Form::ExportAttribute(ast.value.clone()))?; + let form = match form_idx { + FormIdx::Export(idx) => { + if &form_list[idx].form_id.get(&source_file) == ast.value { + Some(idx) + } else { + None + } + } + _ => None, + }?; + Some(form_list[form].clone()) + } +} + +impl FindForm for ast::ExportTypeAttribute { + type Form = TypeExport; + + fn find(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let source_file = sema.parse(ast.file_id).value; + let form_list = sema.db.file_form_list(ast.file_id); + let form_idx = form_list.find_form(&ast::Form::ExportTypeAttribute(ast.value.clone()))?; + let form = match form_idx { + FormIdx::TypeExport(idx) => { + if &form_list[idx].form_id.get(&source_file) == ast.value { + Some(idx) + } else { + None + } + } + _ => None, + }?; + Some(form_list[form].clone()) + } +} + +impl FindForm for ast::Spec { + type Form = Spec; + + fn find(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let source_file = sema.parse(ast.file_id).value; + let form_list = sema.db.file_form_list(ast.file_id); + let form_idx = form_list.find_form(&ast::Form::Spec(ast.value.clone()))?; + let form = match form_idx { + FormIdx::Spec(idx) => { + if &form_list[idx].form_id.get(&source_file) == ast.value { + Some(idx) + } else { + None + } + } + _ => None, + }?; + Some(form_list[form].clone()) + } +} + +impl FindForm for ast::OptionalCallbacksAttribute { + type Form = OptionalCallbacks; + + fn find(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let source_file = sema.parse(ast.file_id).value; + let form_list = sema.db.file_form_list(ast.file_id); + let form_idx = + form_list.find_form(&ast::Form::OptionalCallbacksAttribute(ast.value.clone()))?; + let form = match form_idx { + FormIdx::OptionalCallbacks(idx) => { + if &form_list[idx].form_id.get(&source_file) == ast.value { + Some(idx) + } else { + None + } + } + _ => None, + }?; + Some(form_list[form].clone()) + } +} + +impl FindForm for ast::PpInclude { + type Form = IncludeAttributeId; + + fn find(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let form = ast::Form::cast(ast.value.syntax().clone())?; + find_any_include(sema, ast.file_id, &form) + } +} + +impl FindForm for ast::PpIncludeLib { + type Form = IncludeAttributeId; + + fn find(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let form = ast::Form::cast(ast.value.syntax().clone())?; + find_any_include(sema, ast.file_id, &form) + } +} + +fn find_any_include( + sema: &Semantic<'_>, + file_id: FileId, + ast_form: &ast::Form, +) -> Option { + let source_file = sema.parse(file_id).value; + let form_list = sema.db.file_form_list(file_id); + let form_idx = form_list.find_form(ast_form)?; + let form = match form_idx { + FormIdx::PPDirective(idx) => { + let include_idx = form_list[idx].as_include()?; + if &form_list[include_idx].form_id().get(&source_file) == ast_form { + Some(include_idx) + } else { + None + } + } + _ => None, + }?; + Some(form) +} diff --git a/crates/hir/src/sema/to_def.rs b/crates/hir/src/sema/to_def.rs new file mode 100644 index 0000000000..05f1d599be --- /dev/null +++ b/crates/hir/src/sema/to_def.rs @@ -0,0 +1,750 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_base_db::FileId; +use elp_syntax::ast; +use elp_syntax::match_ast; +use elp_syntax::AstNode; + +use crate::known; +use crate::macro_exp; +use crate::macro_exp::MacroExpCtx; +use crate::resolver::Resolver; +use crate::AnyExprRef; +use crate::Body; +use crate::CallTarget; +use crate::CallbackDef; +use crate::DefineDef; +use crate::Expr; +use crate::ExprId; +use crate::File; +use crate::FunctionDef; +use crate::InFile; +use crate::Literal; +use crate::Module; +use crate::Name; +use crate::NameArity; +use crate::Pat; +use crate::RecordDef; +use crate::RecordFieldDef; +use crate::ResolvedMacro; +use crate::Semantic; +use crate::Term; +use crate::TypeAliasDef; +use crate::TypeExpr; +use crate::VarDef; + +pub trait ToDef: Clone { + type Def; + + fn to_def(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option; +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum DefinitionOrReference { + Definition(Definition), + Reference(Reference), +} + +impl DefinitionOrReference { + pub fn to_reference(self) -> Option { + match self { + DefinitionOrReference::Definition(_) => None, + DefinitionOrReference::Reference(reference) => Some(reference), + } + } +} + +// --------------------------------------------------------------------- + +impl ToDef for ast::Atom { + type Def = Module; + + fn to_def(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let (body, body_map) = sema.find_body(ast.file_id, ast.value.syntax())?; + let file_id = ast.file_id; + let expr = ast.map(|atom| ast::Expr::from(ast::ExprMax::from(atom.clone()))); + let any_expr_id = body_map.any_id(expr.as_ref())?; + let atom = match body.get_any(any_expr_id) { + AnyExprRef::Expr(Expr::Literal(Literal::Atom(atom))) => atom, + AnyExprRef::Pat(Pat::Literal(Literal::Atom(atom))) => atom, + AnyExprRef::TypeExpr(TypeExpr::Literal(Literal::Atom(atom))) => atom, + AnyExprRef::Term(Term::Literal(Literal::Atom(atom))) => atom, + _ => return None, + }; + let name = sema.db.lookup_atom(*atom); + let def = resolve_module_name(sema, file_id, &name)?; + + Some(def) + } +} + +// --------------------------------------------------------------------- + +impl ToDef for ast::BehaviourAttribute { + type Def = Module; + + fn to_def(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let attr = sema.find_form(ast)?; + let def = resolve_module_name(sema, ast.file_id, &attr.name)?; + + Some(def) + } +} + +impl ToDef for ast::ImportAttribute { + type Def = Module; + + fn to_def(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let attr = sema.find_form(ast)?; + let def = resolve_module_name(sema, ast.file_id, &attr.from)?; + Some(def) + } +} + +// --------------------------------------------------------------------- + +#[derive(Debug)] +pub enum CallDef { + Function(FunctionDef), + Type(TypeAliasDef), +} + +impl ToDef for ast::Remote { + type Def = CallDef; + + fn to_def(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let call = ast::Call::cast(ast.value.syntax().parent()?)?; + ToDef::to_def(sema, ast.with_value(&call)) + } +} + +impl ToDef for ast::Call { + type Def = CallDef; + + fn to_def(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let (body, body_map) = sema.find_body(ast.file_id, ast.value.syntax())?; + let file_id = ast.file_id; + let expr = ast.map(|call| ast::Expr::from(call.clone())); + let any_expr_id = body_map.any_id(expr.as_ref())?; + let def = match body.get_any(any_expr_id) { + AnyExprRef::Expr(Expr::Call { target, args }) => { + let arity = args.len().try_into().ok()?; + resolve_call_target(sema, target, arity, file_id, &body).map(CallDef::Function) + } + AnyExprRef::TypeExpr(TypeExpr::Call { target, args }) => { + let arity = args.len().try_into().ok()?; + let (file_id, type_expr) = match target { + CallTarget::Local { name } => (ast.file_id, *name), + CallTarget::Remote { module, name } => { + let module = sema.db.lookup_atom(body[*module].as_atom()?); + ( + resolve_module_name(sema, ast.file_id, &module)? + .file + .file_id, + *name, + ) + } + }; + + let name = sema.db.lookup_atom(body[type_expr].as_atom()?); + let name = NameArity::new(name, arity); + sema.db + .def_map(file_id) + .get_types() + .get(&name) + .cloned() + .map(CallDef::Type) + } + _ => None, + }?; + Some(def) + } +} + +// --------------------------------------------------------------------- + +impl ToDef for ast::InternalFun { + type Def = FunctionDef; + + fn to_def(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let expr = ast.map(|fun| ast::Expr::from(ast::ExprMax::from(fun.clone()))); + resolve_capture(sema, expr) + } +} + +impl ToDef for ast::ExternalFun { + type Def = FunctionDef; + + fn to_def(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let expr = ast.map(|fun| ast::Expr::from(ast::ExprMax::from(fun.clone()))); + resolve_capture(sema, expr) + } +} + +// --------------------------------------------------------------------- + +impl ToDef for ast::Spec { + type Def = FunctionDef; + + fn to_def(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let attr = sema.find_form(ast)?; + // We ignore the `module` field - this is old syntax that is + // not even fully supported in OTP and it's only allowed to + // take the value of ?MODULE + sema.db + .def_map(ast.file_id) + .get_function(&attr.name) + .cloned() + } +} + +// --------------------------------------------------------------------- + +impl ToDef for ast::RecordName { + type Def = RecordDef; + + fn to_def(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let record_expr = ast::Expr::cast(ast.value.syntax().parent()?)?; + let (record, _) = resolve_record(sema, ast.file_id, &record_expr, None)?; + sema.db + .def_map(ast.file_id) + .get_records() + .get(&record) + .cloned() + } +} + +// --------------------------------------------------------------------- + +impl ToDef for ast::RecordFieldName { + type Def = RecordFieldDef; + + fn to_def(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let record_expr = ast::Expr::cast(ast.value.syntax().parent()?)?; + let (record, field_name) = resolve_record(sema, ast.file_id, &record_expr, None)?; + let field_name = field_name?; + sema.db + .def_map(ast.file_id) + .get_records() + .get(&record)? + .find_field(sema.db, &field_name) + } +} + +// --------------------------------------------------------------------- + +impl ToDef for ast::RecordField { + type Def = DefinitionOrReference; + + fn to_def(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let (idx, expr) = match_ast! { + match (ast.value.syntax().parent()?) { + ast::RecordExpr(rec) => (rec.fields().position(|def| &def == ast.value)?, rec.into()), + ast::RecordUpdateExpr(rec) => (rec.fields().position(|def| &def == ast.value)?, rec.into()), + ast::RecordDecl(rec) => { + let idx = rec.fields().position(|def| def == *ast.value)?; + let record = ToDef::to_def(sema, ast.with_value(&rec))?; + return Some(DefinitionOrReference::Definition(record.find_field_by_id(sema.db, idx)?)); + }, + _ => return None, + } + }; + let (record, field_name) = resolve_record(sema, ast.file_id, &expr, Some(idx))?; + let field_name = field_name?; + let def = sema + .db + .def_map(ast.file_id) + .get_records() + .get(&record)? + .find_field(sema.db, &field_name)?; + + Some(DefinitionOrReference::Reference(def)) + } +} + +// --------------------------------------------------------------------- + +impl ToDef for ast::MacroCallExpr { + type Def = DefineDef; + + fn to_def(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let name = macro_exp::macro_name(ast.value)?; + let resolved = match sema.db.resolve_macro(ast.file_id, name.clone()) { + Some(ResolvedMacro::User(resolved)) => resolved, + Some(ResolvedMacro::BuiltIn(_)) => return None, + None => { + let name = name.with_arity(None); + match sema.db.resolve_macro(ast.file_id, name) { + Some(ResolvedMacro::User(resolved)) => resolved, + _ => return None, + } + } + }; + let form_list = sema.db.file_form_list(resolved.file_id); + let define = form_list[resolved.value].clone(); + let file = File { + file_id: resolved.file_id, + }; + Some(DefineDef { file, define }) + } +} + +// --------------------------------------------------------------------- + +impl ToDef for ast::PpInclude { + type Def = File; + + fn to_def(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let idx = sema.find_form(ast)?; + let def = sema + .db + .resolve_include(ast.with_value(idx)) + .map(|file_id| File { file_id })?; + Some(def) + } +} + +// --------------------------------------------------------------------- + +impl ToDef for ast::PpIncludeLib { + type Def = File; + + fn to_def(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let idx = sema.find_form(ast)?; + let def = sema + .db + .resolve_include(ast.with_value(idx)) + .map(|file_id| File { file_id })?; + Some(def) + } +} + +// --------------------------------------------------------------------- + +pub enum FaDef { + Function(FunctionDef), + Type(TypeAliasDef), + Callback(CallbackDef), +} + +impl ToDef for ast::Fa { + type Def = FaDef; + + fn to_def(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let form_list = sema.db.file_form_list(ast.file_id); + let parent = ast.value.syntax().parent()?; + match_ast! { + match (parent) { + ast::ExportAttribute(attr) => { + let export_attr = sema.find_form(ast.with_value(&attr))?; + let idx = attr.funs().position(|fa| &fa == ast.value)? as u32; + let entry = export_attr + .entries + .clone() + .find(|&entry| form_list[entry].idx == idx)?; + sema.db + .def_map(ast.file_id) + .get_function(&form_list[entry].name) + .cloned() + .map(FaDef::Function) + }, + ast::ImportAttribute(attr) => { + let import_attr = sema.find_form(ast.with_value(&attr))?; + let idx = attr.funs().position(|fa| &fa == ast.value)? as u32; + let entry = import_attr + .entries + .clone() + .find(|&entry| form_list[entry].idx == idx)?; + let imported_module = ToDef::to_def(sema, InFile::new(ast.file_id, &attr))?; + sema.db + .def_map(imported_module.file.file_id) + .get_function(&form_list[entry].name) + .cloned() + .map(FaDef::Function) + }, + ast::ExportTypeAttribute(attr) => { + let export_attr = sema.find_form(ast.with_value(&attr))?; + let idx = attr.types().position(|fa| &fa == ast.value)? as u32; + let entry = export_attr + .entries + .clone() + .find(|&entry| form_list[entry].idx == idx)?; + sema.db + .def_map(ast.file_id) + .get_types() + .get(&form_list[entry].name) + .cloned() + .map(FaDef::Type) + }, + ast::OptionalCallbacksAttribute(attr) => { + let optional_callbacks = sema.find_form(ast.with_value(&attr))?; + let idx = attr.callbacks().position(|fa| &fa == ast.value)? as u32; + let entry = optional_callbacks + .entries + .clone() + .find(|&entry| form_list[entry].idx == idx)?; + sema.db + .def_map(ast.file_id) + .get_callbacks() + .get(&form_list[entry].name) + .cloned() + .map(FaDef::Callback) + }, + _ => return None, + } + } + } +} + +// --------------------------------------------------------------------- + +impl ToDef for ast::MacroName { + type Def = Vec; + + fn to_def(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let form_list = sema.db.file_form_list(ast.file_id); + let ctx = MacroExpCtx::new(form_list.data(), sema.db); + let defines = ctx.find_defines_by_name(ast.value); + let file = File { + file_id: ast.file_id, + }; + + let resolved: Vec<_> = defines + .into_iter() + .map(|define| DefineDef { + file, + define: define.clone(), + }) + .collect(); + + if resolved.is_empty() { + None + } else { + Some(resolved) + } + } +} + +// --------------------------------------------------------------------- + +impl ToDef for ast::Var { + type Def = DefinitionOrReference>; + + fn to_def(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let function_id = sema.find_enclosing_function(ast.file_id, ast.value.syntax())?; + let (body, body_map) = sema + .db + .function_body_with_source(ast.with_value(function_id)); + let clause_id = sema.find_enclosing_function_clause(ast.value.syntax())?; + let scopes = sema.db.function_scopes(InFile { + file_id: ast.file_id, + value: function_id, + }); + let clause_scopes = scopes.get(clause_id)?; + let resolver = Resolver::new(clause_scopes.clone()); + let expr = ast::Expr::ExprMax(ast::ExprMax::Var(ast.value.clone())); + let (var, original_pat_id, pat_ids) = + if let Some(expr_id) = body_map.expr_id(ast.with_value(&expr)) { + let var = body.body[expr_id].as_var()?; + (var, None, resolver.resolve_expr_id(&var, expr_id)?) + } else { + let pat_id = body_map.pat_id(ast.with_value(&expr))?; + let var = body.body[pat_id].as_var()?; + (var, Some(pat_id), resolver.resolve_pat_id(&var, pat_id)?) + }; + let mut self_def = false; + let mut resolved = pat_ids + .iter() + .filter_map(|&pat_id| { + let var_def = body_map.pat(pat_id)?; + let def = VarDef { + file: File { + file_id: var_def.file_id(), + }, + var: var_def.value().cast()?, + hir_var: var, + }; + if Some(pat_id) == original_pat_id { + self_def = true; + }; + Some(def) + }) + .collect::>(); + if self_def { + assert!(resolved.len() == 1); + // swap_remove dance necessary to take ownership of element without copying + Some(DefinitionOrReference::Definition(resolved.swap_remove(0))) + } else if !resolved.is_empty() { + Some(DefinitionOrReference::Reference(resolved)) + } else { + None + } + } +} + +// --------------------------------------------------------------------- + +pub(crate) fn resolve_module_expr( + sema: &Semantic<'_>, + body: &Body, + file_id: FileId, + expr_id: ExprId, +) -> Option { + let name = sema.db.lookup_atom(body[expr_id].as_atom()?); + resolve_module_name(sema, file_id, &name) +} + +pub fn resolve_module_name(sema: &Semantic<'_>, file_id: FileId, name: &str) -> Option { + let source_root_id = sema.db.file_source_root(file_id); + let project_id = sema.db.app_data(source_root_id)?.project_id; + let module_index = sema.db.module_index(project_id); + let module_file_id = module_index.file_for_module(name)?; + Some(Module { + file: File { + file_id: module_file_id, + }, + }) +} + +pub(crate) fn resolve_call_target( + sema: &Semantic<'_>, + target: &CallTarget, + arity: u32, + file_id: FileId, + body: &Body, +) -> Option { + let (is_local, file_id, fun_expr) = match target { + CallTarget::Local { name } => (true, file_id, *name), + CallTarget::Remote { module, name } => ( + false, + resolve_module_expr(sema, body, file_id, *module)? + .file + .file_id, + *name, + ), + }; + + let name = sema.db.lookup_atom(body[fun_expr].as_atom()?); + let name_arity = NameArity::new(name, arity); + if let Some(def) = sema.db.def_map(file_id).get_function(&name_arity).cloned() { + Some(def) + } else { + let module_name = sema + .db + .def_map(file_id) + .get_imports() + .get(&name_arity)? + .clone(); + let module = resolve_module_name(sema, file_id, &module_name)?; + let def = sema + .db + .def_map(module.file.file_id) + .get_function(&name_arity) + .cloned()?; + // Check that the definition comes from the module in the import attribute + if is_local || def.file.file_id == file_id { + Some(def) + } else { + None + } + } +} + +fn resolve_capture(sema: &Semantic<'_>, fun: InFile) -> Option { + let (body, body_map) = sema.find_body(fun.file_id, fun.value.syntax())?; + let expr_id = body_map.expr_id(fun.as_ref())?; + let (target, arity) = match &body[expr_id] { + Expr::CaptureFun { target, arity } => (target, arity), + _ => return None, + }; + let arity = match body[*arity] { + Expr::Literal(Literal::Integer(int)) => int.try_into().ok()?, + _ => return None, + }; + resolve_call_target(sema, target, arity, fun.file_id, &body) +} + +fn resolve_record( + sema: &Semantic<'_>, + file_id: FileId, + expr: &ast::Expr, + idx: Option, +) -> Option<(Name, Option)> { + let (body, body_map) = sema.find_body(file_id, expr.syntax())?; + let expr = InFile::new(file_id, expr); + let any_expr_id = body_map.any_id(expr)?; + let (name, field) = match body.get_any(any_expr_id) { + AnyExprRef::Expr(Expr::Record { name, fields }) => { + (*name, idx.and_then(|idx| Some(fields.get(idx)?.0))) + } + AnyExprRef::Expr(Expr::RecordUpdate { + expr: _, + name, + fields, + }) => (*name, idx.and_then(|idx| Some(fields.get(idx)?.0))), + AnyExprRef::Expr(Expr::RecordIndex { name, field }) => (*name, Some(*field)), + AnyExprRef::Expr(Expr::RecordField { + expr: _, + name, + field, + }) => (*name, Some(*field)), + AnyExprRef::Pat(Pat::Record { name, fields }) => { + (*name, idx.and_then(|idx| Some(fields.get(idx)?.0))) + } + AnyExprRef::Pat(Pat::RecordIndex { name, field }) => (*name, Some(*field)), + AnyExprRef::TypeExpr(TypeExpr::Record { name, fields }) => { + (*name, idx.and_then(|idx| Some(fields.get(idx)?.0))) + } + _ => return None, + }; + Some(( + sema.db.lookup_atom(name), + field.map(|name| sema.db.lookup_atom(name)), + )) +} + +// --------------------------------------------------------------------- + +impl ToDef for ast::ModuleAttribute { + type Def = Module; + + fn to_def(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let form = sema.find_form(ast)?; + resolve_module_name(sema, ast.file_id, &form.name) + } +} + +impl ToDef for ast::TypeName { + type Def = TypeAliasDef; + + fn to_def(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let form = sema.find_form(ast)?; + sema.db.def_map(ast.file_id).get_type(form.name()).cloned() + } +} + +impl ToDef for ast::RecordDecl { + type Def = RecordDef; + + fn to_def(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let form = sema.find_form(ast)?; + sema.db.def_map(ast.file_id).get_record(&form.name).cloned() + } +} + +impl ToDef for ast::FunctionClause { + type Def = FunctionDef; + + fn to_def(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let form_list = sema.db.file_form_list(ast.file_id); + let idx = sema.find_enclosing_function(ast.file_id, ast.value.syntax())?; + let name = &form_list[idx].name; + sema.db.def_map(ast.file_id).get_function(name).cloned() + } +} + +impl ToDef for ast::MacroLhs { + type Def = DefineDef; + + fn to_def(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let define = ast::PpDefine::cast(ast.value.syntax().parent()?)?; + let form = sema.find_form(ast.with_value(&define))?; + Some(DefineDef { + file: File { + file_id: ast.file_id, + }, + define: form, + }) + } +} + +impl ToDef for ast::Callback { + type Def = CallbackDef; + + fn to_def(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let form = sema.find_form(ast)?; + sema.db + .def_map(ast.file_id) + .get_callback(&form.name) + .cloned() + } +} + +impl ToDef for ast::ExprArgs { + type Def = CallDef; + + /// Looking specifically to pull out a function definition from an + /// `apply/2` or `apply/3` call. + fn to_def(sema: &Semantic<'_>, ast: InFile<&Self>) -> Option { + let (body, body_map) = sema.find_body(ast.file_id, ast.value.syntax())?; + let call = ast::Expr::cast(ast.value.syntax().parent()?.clone())?; + let expr = ast.with_value(call); + let expr_id = body_map.expr_id(expr.as_ref())?; + let def = match &body[expr_id] { + Expr::Call { target, args } => match target { + CallTarget::Local { name } => { + look_for_apply_call(sema, ast.file_id, None, *name, args, &body) + } + CallTarget::Remote { module, name } => { + look_for_apply_call(sema, ast.file_id, Some(*module), *name, args, &body) + } + }, + _ => None, + }?; + Some(def) + } +} + +fn look_for_apply_call( + sema: &Semantic, + file_id: FileId, + module: Option, + fun: ExprId, + args: &[ExprId], + body: &Body, +) -> Option { + if let Some(module) = module { + let atom = body[module].as_atom()?; + if sema.db.lookup_atom(atom) != known::erlang { + return None; + } + }; + let atom = body[fun].as_atom()?; + if sema.db.lookup_atom(atom) == known::apply { + if args.len() == 2 { + // apply/2 + let arity = arity_from_apply_args(args[1], &body)?; + let apply_target = CallTarget::Local { + name: args[0].clone(), + }; + resolve_call_target(sema, &apply_target, arity, file_id, &body).map(CallDef::Function) + } else if args.len() == 3 { + // apply/3 + let arity = arity_from_apply_args(args[2], &body)?; + let apply_target = CallTarget::Remote { + module: args[0].clone(), + name: args[1].clone(), + }; + resolve_call_target(sema, &apply_target, arity, file_id, &body).map(CallDef::Function) + } else { + None + } + } else { + None + } +} + +/// The apply call has a last parameter being a list of arguments. +/// Given the `ExprId` of this parameter, return the length of the +/// list. +fn arity_from_apply_args(args: ExprId, body: &Body) -> Option { + // Deal with a simple list only. + body[args].list_length().map(|l| l as u32) +} diff --git a/crates/hir/src/test_db.rs b/crates/hir/src/test_db.rs new file mode 100644 index 0000000000..9572765c65 --- /dev/null +++ b/crates/hir/src/test_db.rs @@ -0,0 +1,62 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! Database used for testing `hir`. + +use std::fmt; +use std::panic; +use std::sync::Arc; + +use elp_base_db::salsa; +use elp_base_db::FileId; +use elp_base_db::FileLoader; +use elp_base_db::FileLoaderDelegate; +use elp_base_db::SourceDatabase; +use elp_base_db::Upcast; + +use crate::db::MinInternDatabase; + +#[salsa::database( + elp_base_db::SourceDatabaseExtStorage, + elp_base_db::SourceDatabaseStorage, + crate::db::MinDefDatabaseStorage, + crate::db::MinInternDatabaseStorage +)] +#[derive(Default)] +pub(crate) struct TestDB { + storage: salsa::Storage, +} + +impl Upcast for TestDB { + fn upcast(&self) -> &(dyn SourceDatabase + 'static) { + self + } +} + +impl Upcast for TestDB { + fn upcast(&self) -> &(dyn MinInternDatabase + 'static) { + self + } +} + +impl salsa::Database for TestDB {} + +impl fmt::Debug for TestDB { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TestDB").finish() + } +} + +impl panic::RefUnwindSafe for TestDB {} + +impl FileLoader for TestDB { + fn file_text(&self, file_id: FileId) -> Arc { + FileLoaderDelegate(self).file_text(file_id) + } +} diff --git a/crates/ide/Cargo.toml b/crates/ide/Cargo.toml new file mode 100644 index 0000000000..7ef364ae92 --- /dev/null +++ b/crates/ide/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "elp_ide" +edition.workspace = true +version.workspace = true + +[dependencies] +elp_ide_assists.workspace = true +elp_ide_completion.workspace = true +elp_ide_db.workspace = true +elp_project_model.workspace = true +elp_syntax.workspace = true +hir.workspace = true + +anyhow.workspace = true +fxhash.workspace = true +imara-diff.workspace = true +itertools.workspace = true +lazy_static.workspace = true +log.workspace = true +profile.workspace = true +regex.workspace = true +smallvec.workspace = true +stdx.workspace = true +strsim.workspace = true +strum.workspace = true +strum_macros.workspace = true +text-edit.workspace = true +triple_accel.workspace = true + +[dev-dependencies] +env_logger.workspace = true +expect-test.workspace = true diff --git a/crates/ide/src/annotations.rs b/crates/ide/src/annotations.rs new file mode 100644 index 0000000000..53219a92fb --- /dev/null +++ b/crates/ide/src/annotations.rs @@ -0,0 +1,101 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_ide_db::elp_base_db::FileId; +use elp_ide_db::RootDatabase; +use elp_syntax::TextRange; + +use crate::runnables::runnables; +use crate::runnables::Runnable; + +// Feature: Annotations +// +// Provides user with annotations above items (e.g. for running tests) +// +#[derive(Debug)] +pub struct Annotation { + pub range: TextRange, + pub kind: AnnotationKind, +} + +#[derive(Debug)] +pub enum AnnotationKind { + Runnable(Runnable), +} + +pub(crate) fn annotations(db: &RootDatabase, file_id: FileId) -> Vec { + let mut annotations = Vec::default(); + + for runnable in runnables(db, file_id) { + let range = runnable.nav.range(); + annotations.push(Annotation { + range, + kind: AnnotationKind::Runnable(runnable), + }); + } + annotations +} + +#[cfg(test)] +mod tests { + use elp_ide_db::elp_base_db::FileRange; + use stdx::trim_indent; + + use crate::fixture; + use crate::AnnotationKind; + + #[track_caller] + fn check(fixture: &str) { + let (analysis, pos, mut annotations) = fixture::annotations(trim_indent(fixture).as_str()); + let actual_annotations = analysis.annotations(pos.file_id).unwrap(); + let mut actual = Vec::new(); + for annotation in actual_annotations { + match annotation.kind { + AnnotationKind::Runnable(runnable) => { + let file_id = runnable.nav.file_id; + let range = runnable.nav.focus_range.unwrap(); + let text = runnable.nav.name; + actual.push((FileRange { file_id, range }, text.to_string())); + } + } + } + let cmp = |(frange, text): &(FileRange, String)| { + (frange.file_id, frange.range.start(), text.clone()) + }; + actual.sort_by_key(cmp); + annotations.sort_by_key(cmp); + assert_eq!(actual, annotations); + } + + #[test] + fn annotations_no_suite() { + check( + r#" +-module(main). +~ +main() -> + ok. + "#, + ); + } + + #[test] + fn annotations_suite() { + check( + r#" +//- /main_SUITE.erl + ~ + -module(main_SUITE). +%% ^^^^^^^^^^^^^^^^^^^^ main_SUITE +main() -> + ok. + "#, + ); + } +} diff --git a/crates/ide/src/call_hierarchy.rs b/crates/ide/src/call_hierarchy.rs new file mode 100644 index 0000000000..6570223fad --- /dev/null +++ b/crates/ide/src/call_hierarchy.rs @@ -0,0 +1,395 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_ide_db::elp_base_db::FilePosition; +use elp_ide_db::FxIndexMap; +use elp_ide_db::RootDatabase; +use elp_syntax::algo; +use elp_syntax::ast::{self}; +use elp_syntax::AstNode; +use elp_syntax::SmolStr; +use elp_syntax::TextRange; +use hir::Expr; +use hir::InFile; +use hir::Semantic; + +use crate::handlers::goto_definition; +use crate::handlers::references; +use crate::navigation_target::ToNav; +use crate::NavigationTarget; +use crate::RangeInfo; + +#[derive(Debug, Clone)] +pub struct CallItem { + pub target: NavigationTarget, + pub ranges: Vec, +} + +pub(crate) fn call_hierarchy_prepare( + db: &RootDatabase, + position: FilePosition, +) -> Option>> { + goto_definition::goto_definition(db, position) +} + +pub(crate) fn incoming_calls(db: &RootDatabase, position: FilePosition) -> Option> { + let sema = Semantic::new(db); + let mut calls = CallLocations::default(); + let search_result = references::find_all_refs(&sema, position); + let references = search_result?.first()?.references.clone(); + + for (file_id, ranges) in references { + let source_file = sema.parse(file_id); + let syntax = source_file.value.syntax(); + let form_list = sema.db.file_form_list(file_id); + + for range in ranges { + if let Some(call) = algo::find_node_at_offset::(syntax, range.start()) { + let enclosing_function_id = sema.find_enclosing_function(file_id, call.syntax())?; + let enclosing_function_name = &form_list[enclosing_function_id].name; + let def_map = sema.def_map(file_id); + let enclosing_function_def = def_map.get_function(enclosing_function_name)?; + let mut enclosing_function_nav = enclosing_function_def.to_nav(db); + if file_id != position.file_id { + if let Some(module_name) = sema.module_name(file_id) { + enclosing_function_nav.name = SmolStr::new(format!( + "{}:{}", + module_name.as_str(), + enclosing_function_nav.name + )) + } + } + calls.add(enclosing_function_nav, range); + } + } + } + + Some(calls.into_items()) +} + +pub(crate) fn outgoing_calls(db: &RootDatabase, position: FilePosition) -> Option> { + let sema = Semantic::new(db); + let mut calls = CallLocations::default(); + let file_id = position.file_id; + let source_file = sema.parse(file_id); + let syntax = source_file.value.syntax(); + if let Some(function) = algo::find_node_at_offset::(syntax, position.offset) { + let function_id_idx = sema.find_enclosing_function(file_id, function.syntax())?; + let function_id = InFile::new(file_id, function_id_idx); + let function_body = sema.to_function_body(function_id); + sema.fold_function( + function_id, + (), + &mut |acc, _clause_id, ctx| { + match &ctx.expr { + Expr::Call { target, args } => { + let arity = args.len() as u32; + let body = &function_body.body(); + if let Some(call_def) = target.resolve_call(arity, &sema, file_id, body) { + let mut nav = call_def.to_nav(db); + if let Some(label) = target.label(arity, &sema, &body) { + nav.name = label + } + if let Some(expr) = &function_body.get_body_map(db).expr(ctx.expr_id) { + if let Some(node) = expr.to_node(&source_file) { + if let Some(call) = algo::find_node_at_offset::( + &node.syntax(), + node.syntax().text_range().start(), + ) { + if let Some(expr) = call.expr() { + let range = expr.syntax().text_range(); + calls.add(nav.clone(), range); + } + } + } + } + } + } + _ => (), + } + acc + }, + &mut |acc, _, _| acc, + ) + } + Some(calls.into_items()) +} + +#[derive(Default)] +struct CallLocations { + funcs: FxIndexMap>, +} + +impl CallLocations { + fn add(&mut self, target: NavigationTarget, range: TextRange) { + self.funcs.entry(target).or_default().push(range); + } + + fn into_items(self) -> Vec { + self.funcs + .into_iter() + .map(|(target, ranges)| CallItem { target, ranges }) + .collect() + } +} + +#[cfg(test)] +mod tests { + + use crate::tests::check_call_hierarchy; + + #[test] + fn test_call_hierarchy_on_ref() { + check_call_hierarchy( + r#" + callee() -> +%% ^^^^^^ + ok. + caller() -> + cal~lee(). + "#, + r#" + cal~lee() -> + ok. + caller() -> +%% ^^^^^^ from: caller/0 + callee(). + %% ^^^^^^ from_range: caller/0 + "#, + r#" + cal~lee() -> + ok. + caller() -> + callee(). + "#, + ); + } + + #[test] + fn test_call_hierarchy_on_def() { + check_call_hierarchy( + r#" + call~ee() -> + %% ^^^^^^ + ok. + caller() -> + callee(). + "#, + r#" + call~ee() -> + ok. + caller() -> + %% ^^^^^^ from: caller/0 + callee(). + %% ^^^^^^ from_range: caller/0 + "#, + r#" + call~ee() -> + ok. + caller() -> + callee(). + "#, + ) + } + + #[test] + fn test_call_hierarchy_multiple_calls_same_function() { + check_call_hierarchy( + r#" + callee() -> ok. + %% ^^^^^^ + caller() -> + call~ee(), + callee(). + "#, + r#" + cal~lee() -> ok. + caller() -> + %% ^^^^^^ from: caller/0 + callee(), + %% ^^^^^^ from_range: caller/0 + callee(). + %% ^^^^^^ from_range: caller/0 + "#, + r#" + cal~lee() -> ok. + caller() -> + callee(), + callee(). + "#, + ); + } + + #[test] + fn test_call_hierarchy_multiple_calls_different_function() { + check_call_hierarchy( + r#" + callee() -> ok. + %% ^^^^^^ + caller1() -> + call~ee(). + caller2() -> + callee(). + "#, + r#" + cal~lee() -> ok. + caller1() -> + %% ^^^^^^^ from: caller1/0 + callee(). + %% ^^^^^^ from_range: caller1/0 + caller2() -> + %% ^^^^^^^ from: caller2/0 + callee(). + %% ^^^^^^ from_range: caller2/0 + "#, + r#" + cal~lee() -> ok. + caller1() -> + callee(). + caller2() -> + callee(). + "#, + ); + } + + #[test] + fn test_call_hierarchy_recursive() { + check_call_hierarchy( + r#" + fact(1) -> 1; + %% ^^^^ + fact(N) -> N * fa~ct(N-1). + "#, + r#" + f~act(1) -> 1; + %% ^^^^ from: fact/1 + fact(N) -> N * fact(N-1). + %% ^^^^ from_range: fact/1 + "#, + r#" + f~act(1) -> 1; + %% ^^^^ to: fact/1 + fact(N) -> N * fact(N-1). + %% ^^^^ from_range: fact/1 + "#, + ) + } + + #[test] + fn test_call_hierarchy_different_files() { + check_call_hierarchy( + r#" + //- /src/a.erl + -module(a). + caller() -> + b:calle~e(). + //- /src/b.erl + -module(b). + -export([callee/0]). + callee() -> ok. + %% ^^^^^^ + "#, + r#" + //- /src/a.erl + -module(a). + caller() -> + %% ^^^^^^ from: a:caller/0 + b:callee(). + %% ^^^^^^ from_range: a:caller/0 + //- /src/b.erl + -module(b). + -export([callee/0]). + cal~lee() -> ok. + "#, + r#" + //- /src/a.erl + -module(a). + caller() -> + b:callee(). + //- /src/b.erl + -module(b). + -export([callee/0]). + cal~lee() -> ok. + "#, + ); + } + + #[test] + fn test_call_hierarchy_outgoing() { + check_call_hierarchy( + r#" + -module(main). + callee() -> + ok. + call~er() -> + %% ^^^^^^ + callee(), + callee(). + "#, + r#" + -module(main). + callee() -> + ok. + call~er() -> + callee(), + callee(). + "#, + r#" + -module(main). + callee() -> + %% ^^^^^^ to: callee/0 + ok. + call~er() -> + callee(), + %% ^^^^^^ from_range: callee/0 + callee(). + %% ^^^^^^ from_range: callee/0 + "#, + ); + } + + #[test] + fn test_call_hierarchy_outgoing_fully_qualified() { + check_call_hierarchy( + r#" + //- /src/a.erl + -module(a). + cal~ler() -> + %% ^^^^^^ + b:callee(). + //- /src/b.erl + -module(b). + -export([callee/0]). + callee() -> ok. + "#, + r#" + //- /src/a.erl + -module(a). + cal~ler() -> + b:callee(). + //- /src/b.erl + -module(b). + -export([callee/0]). + callee() -> ok. + "#, + r#" + //- /src/a.erl + -module(a). + ca~ller() -> + b:callee(). + %% ^^^^^^^^ from_range: b:callee/0 + //- /src/b.erl + -module(b). + -export([callee/0]). + callee() -> ok. + %% ^^^^^^ to: b:callee/0 + "#, + ); + } +} diff --git a/crates/ide/src/codemod_helpers.rs b/crates/ide/src/codemod_helpers.rs new file mode 100644 index 0000000000..8250c814fa --- /dev/null +++ b/crates/ide/src/codemod_helpers.rs @@ -0,0 +1,532 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::iter; + +use elp_syntax::ast; +use elp_syntax::ast::in_erlang_module; +use elp_syntax::AstNode; +use elp_syntax::SmolStr; +use elp_syntax::SyntaxElement; +use elp_syntax::SyntaxKind; +use elp_syntax::TextRange; +use fxhash::FxHashMap; +use hir::Body; +use hir::CallTarget; +use hir::Expr; +use hir::ExprId; +use hir::FunctionDef; +use hir::InFile; +use hir::InFunctionBody; +use hir::On; +use hir::Semantic; +use hir::Strategy; + +use crate::diagnostics::Diagnostic; + +// Given an expression that represents a statement, return a text range that covers +// the statement in full. This means: +// +// - If the expression is followed by a comma, include the comma in the range, and all +// whitespace following the comma +// - If the expression is not followed by a comma, but is preceded by a comma, include +// the preceding comma in the range, and all whitespace before the comma +// - Otherwise, we just use the range of the expression +// +// We typically want to have the statement range in order to be able to delete the statement, +// remove an element from a list, etc. +pub(crate) fn statement_range(expr: &ast::Expr) -> TextRange { + let node = expr.syntax(); + let node_range = node.text_range(); + + let elements_to_the_right = iter::successors(node.next_sibling_or_token(), |n| { + (*n).next_sibling_or_token() + }); + let mut searching_for_comma = true; + let final_node_range; + + let mut node_range_right = node_range; + for element in elements_to_the_right { + if let Some(t) = &SyntaxElement::into_token(element) { + match t.kind() { + SyntaxKind::WHITESPACE => node_range_right = t.text_range(), + SyntaxKind::ANON_COMMA if searching_for_comma => { + node_range_right = t.text_range(); + searching_for_comma = false + } + _ => break, + } + } else { + break; + } + } + + if !searching_for_comma { + final_node_range = node_range_right; + } else { + // We didn't find a trailing comma, so let's see if there is a preceding comma + + let elements_to_the_left = iter::successors(node.prev_sibling_or_token(), |n| { + (*n).prev_sibling_or_token() + }); + + let mut node_range_left = node_range; + for element in elements_to_the_left { + if let Some(t) = &SyntaxElement::into_token(element) { + match t.kind() { + SyntaxKind::WHITESPACE => node_range_left = t.text_range(), + SyntaxKind::ANON_COMMA if searching_for_comma => { + node_range_left = t.text_range(); + searching_for_comma = false + } + _ => break, + } + } else { + break; + } + } + + if !searching_for_comma { + final_node_range = node_range_left; + } else { + // We didn't find a trailing nor preceding comma, this is a singleton + final_node_range = node_range_left.cover(node_range_right); + } + } + + node_range.cover(final_node_range) +} + +pub(crate) fn var_name_starts_with_underscore(var: &ast::Var) -> bool { + var.syntax().to_string().starts_with("_") +} + +pub(crate) fn is_only_place_where_var_is_defined(sema: &Semantic, var: InFile<&ast::Var>) -> bool { + check_is_only_place_where_var_is_defined(sema, var).is_some() +} + +pub(crate) fn var_has_no_references(sema: &Semantic, var: InFile<&ast::Var>) -> bool { + check_var_has_no_references(sema, var).is_some() +} + +pub(crate) fn check_is_only_place_where_var_is_defined( + sema: &Semantic, + var: InFile<&ast::Var>, +) -> Option<()> { + let usages = sema.find_local_usages(var)?; + let num_definitions = usages + .iter() + .filter(|v| sema.to_def(var.with_value(*v)).map_or(false, is_definition)) + .count(); + if num_definitions == 1 { Some(()) } else { None } +} + +pub(crate) fn check_var_has_no_references(sema: &Semantic, var: InFile<&ast::Var>) -> Option<()> { + let usages = sema.find_local_usages(var)?; + let num_definitions = usages + .iter() + .filter(|v| { + sema.to_def(var.with_value(*v)) + .map_or(false, |dor| !is_definition(dor)) + }) + .count(); + if num_definitions == 0 { Some(()) } else { None } +} + +pub(crate) fn check_var_has_references(sema: &Semantic, var: InFile<&ast::Var>) -> Option<()> { + match check_var_has_no_references(sema, var) { + Some(()) => None, + None => Some(()), + } +} + +fn is_definition(def: hir::DefinitionOrReference) -> bool { + match def { + hir::DefinitionOrReference::Definition { .. } => true, + hir::DefinitionOrReference::Reference { .. } => false, + } +} + +#[derive(Debug, Clone)] +struct FunctionMatcher<'a, T> { + labels_full: FxHashMap, (&'a FunctionMatch, &'a T)>, + labels_mf: FxHashMap, (&'a FunctionMatch, &'a T)>, + labels_m: FxHashMap, (&'a FunctionMatch, &'a T)>, +} + +impl<'a, T> FunctionMatcher<'a, T> { + fn new(call: &'a [(&'a FunctionMatch, T)]) -> FunctionMatcher<'a, T> { + let mut labels_full: FxHashMap, (&FunctionMatch, &T)> = + FxHashMap::default(); + let mut labels_mf: FxHashMap, (&FunctionMatch, &T)> = FxHashMap::default(); + let mut labels_m: FxHashMap, (&FunctionMatch, &T)> = FxHashMap::default(); + call.into_iter().for_each(|(c, t)| match c { + FunctionMatch::MFA(mfa) => { + if mfa.module == "erlang" && in_erlang_module(&mfa.name, mfa.arity as usize) { + labels_full.insert(Some(mfa.label().into()), (*c, t)); + labels_full.insert(Some(mfa.short_label().into()), (*c, t)); + } else { + labels_full.insert(Some(mfa.label().into()), (*c, t)); + } + } + FunctionMatch::MF { module, name } => { + let label = format!("{}:{}", module, name); + labels_mf.insert(Some(label.into()), (*c, t)); + } + FunctionMatch::M { module } => { + labels_m.insert(Some(module.into()), (*c, t)); + } + }); + FunctionMatcher { + labels_full, + labels_mf, + labels_m, + } + } + + fn get_match( + &self, + target: &CallTarget, + args: &Vec, + sema: &Semantic, + body: &Body, + ) -> Option<(&'a FunctionMatch, &'a T)> { + self.labels_full + .get(&target.label(args.len() as u32, sema, body)) + .copied() + .or_else(|| self.labels_mf.get(&target.label_short(sema, body)).copied()) + .or_else(|| match target { + CallTarget::Local { name: _ } => None, + CallTarget::Remote { module, name: _ } => { + let name = sema.db.lookup_atom(body[*module].as_atom()?); + let label = Some(SmolStr::new(format!("{name}"))); + self.labels_m.get(&label).copied() + } + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FunctionMatch { + MFA(MFA), + #[allow(dead_code)] + MF { + module: String, + name: String, + }, + #[allow(dead_code)] + M { + module: String, + }, +} + +impl FunctionMatch { + pub fn mfa(m: &str, f: &str, arity: u32) -> FunctionMatch { + FunctionMatch::MFA(MFA { + module: m.into(), + name: f.into(), + arity, + }) + } + + #[allow(dead_code)] + pub fn mfas(m: &str, f: &str, arity: Vec) -> Vec { + arity + .into_iter() + .map(|a| { + FunctionMatch::MFA(MFA { + module: m.into(), + name: f.into(), + arity: a, + }) + }) + .collect() + } + + #[allow(dead_code)] + pub fn mf(m: &str, f: &str) -> FunctionMatch { + FunctionMatch::MF { + module: m.into(), + name: f.into(), + } + } + + #[allow(dead_code)] + pub fn m(m: &str) -> FunctionMatch { + FunctionMatch::M { module: m.into() } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MFA { + pub module: String, + pub name: String, + pub arity: u32, +} + +impl MFA { + pub fn new(m: &str, n: &str, arity: u32) -> MFA { + MFA { + module: m.into(), + name: n.into(), + arity, + } + } + + pub fn label(&self) -> String { + format!("{}:{}/{}", self.module, self.name, self.arity) + } + + pub fn short_label(&self) -> String { + format!("{}/{}", self.name, self.arity) + } +} + +/// Check a specific call instance, and return the contents of a +/// diagnostic if needed. +pub type CheckCall<'a, T> = &'a dyn Fn( + &FunctionMatch, + &T, + &CallTarget, + &[ExprId], + &InFunctionBody<&FunctionDef>, +) -> Option; + +pub(crate) fn find_call_in_function( + diags: &mut Vec, + sema: &Semantic, + def: &FunctionDef, + call: &[(&FunctionMatch, T)], + check_call: CheckCall, + make_diag: impl FnOnce( + &Semantic, + &mut InFunctionBody<&FunctionDef>, + &CallTarget, + &[ExprId], + &str, + TextRange, + ) -> Option + + Copy, +) -> Option<()> { + let mut def_fb = def.in_function_body(sema.db, def); + let matcher = FunctionMatcher::new(call); + def_fb.clone().fold_function_with_macros( + Strategy::Both, + (), + &mut |acc, _, ctx| { + match ctx.expr { + Expr::Call { target, args } => { + if ctx.on == On::Entry { + if let Some((mfa, t)) = + matcher.get_match(&target, &args, sema, &def_fb.body()) + { + if let Some(match_descr) = check_call(mfa, t, &target, &args, &def_fb) { + // Got one. + let call_expr_id = if let Some(expr_id) = ctx.in_macro { + expr_id + } else { + ctx.expr_id + }; + if let Some(range) = &def_fb.range_for_expr(sema.db, call_expr_id) { + if let Some(diag) = make_diag( + sema, + &mut def_fb, + &target, + &args, + &match_descr, + range.clone(), + ) { + diags.push(diag) + } + } + } + } + } + } + _ => {} + }; + acc + }, + &mut |acc, _, _| acc, + ); + Some(()) +} + +// --------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use elp_ide_db::elp_base_db::FileId; + use fxhash::FxHashSet; + use hir::FunctionDef; + use hir::Semantic; + + use super::find_call_in_function; + use super::FunctionMatch; + use crate::diagnostics::Diagnostic; + use crate::diagnostics::DiagnosticCode; + use crate::diagnostics::DiagnosticsConfig; + use crate::diagnostics::Severity; + use crate::tests::check_diagnostics_with_config; + + fn check_functions( + diags: &mut Vec, + sema: &Semantic, + file_id: FileId, + match_spec: &Vec>, + ) { + sema.def_map(file_id) + .get_functions() + .iter() + .for_each(|(_arity, def)| check_function(diags, sema, def, match_spec.clone())); + } + + fn check_function( + diags: &mut Vec, + sema: &Semantic, + def: &FunctionDef, + match_spec: Vec>, + ) { + let matches = match_spec + .into_iter() + .flatten() + .collect::>(); + + process_matches(diags, sema, def, &matches); + } + + fn process_matches( + diags: &mut Vec, + sema: &Semantic, + def: &FunctionDef, + bad: &[FunctionMatch], + ) { + let mfas = bad.iter().map(|b| (b, ())).collect::>(); + find_call_in_function( + diags, + sema, + def, + &mfas, + &move |_mfa, _, _target, _args, _def_fb| Some("Diagnostic Message".to_string()), + move |_sema, mut _def_fb, __target, _args, extra_info, range| { + let diag = Diagnostic::new( + DiagnosticCode::AdHoc("test".to_string()), + extra_info, + range.clone(), + ) + .severity(Severity::Warning); + Some(diag) + }, + ); + } + + #[track_caller] + fn check_adhoc_function_match(match_spec: &Vec>, fixture: &str) { + check_diagnostics_with_config( + DiagnosticsConfig::new( + false, + FxHashSet::default(), + vec![&|acc, sema, file_id, _ext| check_functions(acc, sema, file_id, match_spec)], + ) + .disable(DiagnosticCode::MissingCompileWarnMissingSpec), + fixture, + ); + } + + // ----------------------------------------------------------------- + + #[test] + fn find_call_in_function_1() { + check_adhoc_function_match( + &vec![FunctionMatch::mfas("foo", "fire_bombs", vec![1, 2])], + r#" + -module(main). + + bar(Config) -> + foo:fire_bombs(Config), + %% ^^^^^^^^^^^^^^^^^^^^^^ warning: Diagnostic Message + foo:fire_bombs(Config, zz). + %% ^^^^^^^^^^^^^^^^^^^^^^^^^^ warning: Diagnostic Message + "#, + ) + } + + #[test] + fn find_call_in_function_erlang_module() { + check_adhoc_function_match( + &vec![FunctionMatch::mfas("erlang", "spawn", vec![2, 4])], + r#" + -module(main). + + bar(Node) -> + erlang:spawn(fun() -> ok end), + spawn(fun() -> ok end), + erlang:spawn(Node, fun() -> ok end), + %% ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ warning: Diagnostic Message + spawn(Node, fun() -> ok end), + %% ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ warning: Diagnostic Message + erlang:spawn(Node, mod, fff, []), + %% ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ warning: Diagnostic Message + spawn(Node, mod, fff, []). + %% ^^^^^^^^^^^^^^^^^^^^^^^^^ warning: Diagnostic Message + "#, + ) + } + + #[test] + fn find_call_in_function_erlang_module_2() { + check_adhoc_function_match( + // Not actually in the erlang module + &vec![FunctionMatch::mfas("erlang", "spawn", vec![0])], + r#" + -module(main). + + bar() -> + erlang:spawn(), + %% ^^^^^^^^^^^^^^ warning: Diagnostic Message + spawn(). + "#, + ) + } + + #[test] + fn find_call_module_function() { + check_adhoc_function_match( + &vec![vec![FunctionMatch::mf("foo", "bar")]], + r#" + -module(main). + + bar() -> + foo:bar(), + %% ^^^^^^^^^ warning: Diagnostic Message + foo:bar(x), + %% ^^^^^^^^^^ warning: Diagnostic Message + foo:bar(x,y). + %% ^^^^^^^^^^^^ warning: Diagnostic Message + "#, + ) + } + + #[test] + fn find_call_module_only() { + check_adhoc_function_match( + &vec![vec![FunctionMatch::m("foo")]], + r#" + -module(main). + + bar() -> + foo:bar(), + %% ^^^^^^^^^ warning: Diagnostic Message + baz:bar(x), + foo:florgle(x,y). + %% ^^^^^^^^^^^^^^^^ warning: Diagnostic Message + "#, + ) + } +} diff --git a/crates/ide/src/common_test.rs b/crates/ide/src/common_test.rs new file mode 100644 index 0000000000..a27eaad40b --- /dev/null +++ b/crates/ide/src/common_test.rs @@ -0,0 +1,656 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +// This module implements native support for the Common Test testing framework in ELP. +// The main use case is to provide code lenses so that users can run testcases +// directly from the IDE. +// +// In a Common Test suite, tests are defined via a callback function: `all/0`. +// Tests can also be grouped together and groups definitions are provided via an +// additional callback function: `groups/0`. +// +// For more information about Common Test and the structure of a test suite, +// please see: +// +// * https://www.erlang.org/doc/apps/common_test/introduction.html +// * https://www.erlang.org/doc/man/ct_suite.html +// +// We currently parse test and group definitions, without evaluating those functions. +// This means that, for the time being, only tests specified as literals are supported. +// This limitation can be solved by leveraging the Erlang Service in ELP to evaluate +// those functions before processing them. + +use elp_ide_db::elp_base_db::FileId; +use elp_syntax::AstNode; +use elp_syntax::TextRange; +use fxhash::FxHashMap; +use fxhash::FxHashSet; +use hir::known; +use hir::Body; +use hir::Expr; +use hir::ExprId; +use hir::FunctionDef; +use hir::InFile; +use hir::InFunctionBody; +use hir::Literal; +use hir::Name; +use hir::NameArity; +use hir::Semantic; +use lazy_static::lazy_static; + +use crate::diagnostics::Diagnostic; +use crate::diagnostics::DiagnosticCode; +use crate::diagnostics::Severity; +use crate::navigation_target::ToNav; +use crate::runnables::RunnableKind; +use crate::Runnable; + +const SUFFIX: &str = "_SUITE"; + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub enum GroupName { + NoGroup, // Used to allow tests to run outside of a group + Name(Name), +} + +impl GroupName { + pub fn name(&self) -> String { + match self { + GroupName::NoGroup => "".to_string(), + GroupName::Name(name) => name.to_string(), + } + } +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +struct GroupDef { + name: Name, + content: Vec, +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +enum TestDef { + TestName(Name), + GroupName(Name), + GroupDef(Name, Vec), +} + +pub fn unreachable_test(diagnostics: &mut Vec, sema: &Semantic, file_id: FileId) { + let exported_test_ranges = exported_test_ranges(sema, file_id); + match runnable_names(sema, file_id) { + Ok(runnable_names) => { + for (name, range) in exported_test_ranges { + if !runnable_names.contains(&name) { + let d = Diagnostic::new( + DiagnosticCode::UnreachableTest, + format!("Unreachable test ({name})"), + range, + ) + .severity(Severity::Warning) + .with_ignore_fix(file_id); + diagnostics.push(d); + } + } + } + Err(_) => (), + } +} + +fn runnable_names(sema: &Semantic, file_id: FileId) -> Result, ()> { + runnables(sema, file_id).map(|runnables| { + runnables + .into_iter() + .filter_map(|runnable| match runnable.kind { + RunnableKind::Test { name, .. } => Some(name), + RunnableKind::Suite => None, + }) + .collect() + }) +} + +fn exported_test_ranges(sema: &Semantic, file_id: FileId) -> FxHashMap { + let mut res = FxHashMap::default(); + let def_map = sema.db.def_map(file_id); + let functions = def_map.get_functions(); + for (name_arity, def) in functions { + if def.exported { + if !KNOWN_FUNCTIONS_ARITY_1.contains(name_arity) { + if let Some(name) = def.source(sema.db.upcast()).name() { + if name_arity.arity() == 1 { + res.insert(name_arity.clone(), name.syntax().text_range()); + } + } + } + } + } + res +} + +lazy_static! { + static ref KNOWN_FUNCTIONS_ARITY_1: FxHashSet = { + let mut res = FxHashSet::default(); + for name in vec![known::end_per_suite, known::init_per_suite, known::group] { + res.insert(NameArity::new(name.clone(), 1)); + } + res + }; +} + +// Populate the list of runnables for a Common Test test suite +pub fn runnables(sema: &Semantic, file_id: FileId) -> Result, ()> { + let mut res = Vec::new(); + if let Some(module_name) = sema.module_name(file_id) { + if module_name.ends_with(SUFFIX) { + // Add a runnable for the entire test suite + if let Some(suite_runnable) = suite_to_runnable(sema, file_id) { + res.push(suite_runnable); + } + // Parse and expand the content of the groups/0 function + let groups = groups(sema, file_id)?; + // Parse the content of the all/0 function + let all = all(sema, file_id)?; + // Finally produce the list of runnables + let test_defs = Vec::from_iter(all); + runnables_for_test_defs( + &mut res, + sema, + file_id, + &test_defs, + FxHashSet::default(), + &groups, + ); + } + } + Ok(res) +} + +fn runnables_for_test_defs( + res: &mut Vec, + sema: &Semantic, + file_id: FileId, + test_defs: &Vec, + group_names: FxHashSet, + group_defs: &FxHashMap, +) { + for test_def in test_defs { + match test_def { + TestDef::TestName(testcase_name) => { + if group_names.is_empty() { + if let Some(runnable) = + runnable(sema, file_id, testcase_name.clone(), GroupName::NoGroup) + { + res.push(runnable); + } + } else { + for group_name in group_names.clone() { + if let Some(runnable) = + runnable(sema, file_id, testcase_name.clone(), group_name) + { + res.push(runnable); + } + } + } + } + TestDef::GroupName(group_name) => { + if !group_names.contains(&GroupName::Name(group_name.clone())) { + runnables_for_group_def( + res, + sema, + file_id, + &group_name, + group_names.clone(), + group_defs, + ) + } + } + TestDef::GroupDef(group_name, group_test_defs) => { + if !group_names.contains(&GroupName::Name(group_name.clone())) { + let mut new_group_names = group_names.clone(); + new_group_names.insert(GroupName::Name(group_name.clone())); + runnables_for_test_defs( + res, + sema, + file_id, + group_test_defs, + new_group_names, + group_defs, + ) + } + } + } + } +} + +fn runnables_for_group_def( + res: &mut Vec, + sema: &Semantic, + file_id: FileId, + group_name: &Name, + group_names: FxHashSet, + group_defs: &FxHashMap, +) { + match group_defs.get(group_name) { + Some(GroupDef { name, content }) => { + let mut new_group_names = group_names.clone(); + new_group_names.insert(GroupName::Name(name.clone())); + runnables_for_test_defs(res, sema, file_id, content, new_group_names, group_defs) + } + None => (), + } +} + +// A testcase is runnable if: +// * A corresponding function with arity 1 exists +// * That function is exported +fn runnable( + sema: &Semantic, + file_id: FileId, + name: Name, + group_name: GroupName, +) -> Option { + let def_map = sema.def_map(file_id); + let name_arity = NameArity::new(name, 1); + let def = def_map.get_function(&name_arity)?; + if def.exported { + def_to_runnable(sema, def, group_name) + } else { + None + } +} + +// Return a runnable for the given test suite +fn suite_to_runnable(sema: &Semantic, file_id: FileId) -> Option { + let suite = sema.module_name(file_id)?.to_string(); + let module = sema.resolve_module_name(file_id, suite.as_str())?; + let nav = module.to_nav(sema.db); + Some(Runnable { + nav, + kind: RunnableKind::Suite, + }) +} + +// Return a runnable for the given function definition +fn def_to_runnable(sema: &Semantic, def: &FunctionDef, group: GroupName) -> Option { + let nav = def.to_nav(sema.db); + let app_name = sema.db.file_app_name(def.file.file_id)?; + let suite = sema.module_name(def.file.file_id)?.to_string(); + let name = def.function.name.clone(); + let kind = RunnableKind::Test { + name: name.clone(), + app_name, + suite, + case: name.name().to_string(), + group, + }; + Some(Runnable { nav, kind }) +} + +// Extract the test definitions from the content of the all/0 function +fn all(sema: &Semantic, file_id: FileId) -> Result, ()> { + let mut res = FxHashSet::default(); + + if let Some(expr) = top_level_expression(sema, file_id, known::all, 0) { + let body = expr.body(); + match &body[expr.value] { + Expr::List { exprs, tail: _ } => { + for expr_id in exprs { + parse_test_definition(&mut res, sema, &body, *expr_id)? + } + } + _ => return Err(()), + } + } + + Ok(res) +} + +// Parse each entry from the `all/0` function. +// See https://www.erlang.org/doc/man/ct_suite.html#Module:all-0 for details +fn parse_test_definition( + res: &mut FxHashSet, + sema: &Semantic, + body: &Body, + expr_id: ExprId, +) -> Result<(), ()> { + match &body[expr_id] { + Expr::Literal(Literal::Atom(testcase_name)) => { + let testcase_name = sema.db.lookup_atom(*testcase_name); + res.insert(TestDef::TestName(testcase_name)); + } + Expr::Tuple { exprs } => match exprs[..] { + [first, second] => match (&body[first], &body[second]) { + ( + Expr::Literal(Literal::Atom(group_tag)), + Expr::Literal(Literal::Atom(group_name)), + ) => { + if sema.db.lookup_atom(*group_tag) == known::group { + // {group, Group} + let group_name = sema.db.lookup_atom(*group_name); + res.insert(TestDef::GroupName(group_name)); + } + } + _ => return Err(()), + }, + [first, second, _third] => match (&body[first], &body[second]) { + (Expr::Literal(Literal::Atom(first)), Expr::Literal(Literal::Atom(second))) => { + if sema.db.lookup_atom(*first) == known::group { + // {group, Group, _Properties} + let group_name = sema.db.lookup_atom(*second); + res.insert(TestDef::GroupName(group_name)); + } else if sema.db.lookup_atom(*first) == known::testcase { + // {testcase, Testcase, _Properties} + let testcase_name = sema.db.lookup_atom(*second); + res.insert(TestDef::TestName(testcase_name)); + } + } + _ => return Err(()), + }, + [first, second, _third, _fourth] => { + match (&body[first], &body[second]) { + (Expr::Literal(Literal::Atom(first)), Expr::Literal(Literal::Atom(second))) => { + if sema.db.lookup_atom(*first) == known::group { + // {group, Group, _Properties, _SubGroupsProperties} + let group_name = sema.db.lookup_atom(*second); + res.insert(TestDef::GroupName(group_name)); + } + } + _ => return Err(()), + } + } + _ => return Err(()), + }, + _ => return Err(()), + }; + Ok(()) +} + +// Given a function expressed in terms of name and arity, return the top-level expression for that function. +// This is used to parse the content of the all/0 and groups/0 functions. +// The function must be exported. +fn top_level_expression( + sema: &Semantic, + file_id: FileId, + name: Name, + arity: u32, +) -> Option> { + let def_map = sema.def_map(file_id); + let exported_functions = def_map.get_exported_functions(); + let name_arity = exported_functions.get(&NameArity::new(name, arity))?; + let body = def_map.get_function(name_arity)?; + let function_id = InFile::new(file_id, body.function_id); + let function_body = sema.to_function_body(function_id); + let (_clause_idx, clause) = function_body.clauses().next()?; + let expr_id = clause.exprs.first()?; + Some(function_body.with_value(*expr_id)) +} + +// Extract the group definitions from the content of the groups/0 function. +fn groups(sema: &Semantic, file_id: FileId) -> Result, ()> { + let mut res = FxHashMap::default(); + + if let Some(expr) = top_level_expression(sema, file_id, known::groups, 0) { + let body = expr.body(); + match &body[expr.value] { + Expr::List { exprs, tail: _ } => { + for expr_id in exprs { + match parse_group(sema, &body, *expr_id) { + Some(group_def) => { + res.insert(group_def.name.clone(), group_def); + } + None => return Err(()), + } + } + } + _ => return Err(()), + } + } + + Ok(res) +} + +// Parse each entry from the `groups/0` function. +// See https://www.erlang.org/doc/man/ct_suite.html#Module:groups-0 for details +fn parse_group(sema: &Semantic, body: &Body, expr_id: ExprId) -> Option { + match &body[expr_id] { + Expr::Tuple { exprs } => match exprs[..] { + [group_name, _properties, group_content] => { + let group_name = &body[group_name].as_atom()?; + let group_name = sema.db.lookup_atom(*group_name); + let group_content = parse_group_content(sema, body, group_content).ok()?; + Some(GroupDef { + name: group_name, + content: group_content, + }) + } + _ => None, + }, + _ => None, + } +} + +fn parse_group_content( + sema: &Semantic, + body: &Body, + group_content: ExprId, +) -> Result, ()> { + let mut res = Vec::new(); + match &body[group_content] { + Expr::List { exprs, tail: _ } => { + for expr in exprs { + parse_group_content_entry(&mut res, sema, body.clone(), *expr)? + } + } + _ => return Err(()), + }; + Ok(res) +} + +fn parse_group_content_entry( + res: &mut Vec, + sema: &Semantic, + body: &Body, + expr_id: ExprId, +) -> Result<(), ()> { + match &body[expr_id] { + Expr::Literal(Literal::Atom(testcase_name)) => { + let testcase_name = sema.db.lookup_atom(*testcase_name); + res.push(TestDef::TestName(testcase_name)); + } + Expr::Tuple { exprs } => match exprs[..] { + [group_tag, group_name] => match (&body[group_tag], &body[group_name]) { + ( + Expr::Literal(Literal::Atom(group_tag)), + Expr::Literal(Literal::Atom(group_name)), + ) => { + if sema.db.lookup_atom(*group_tag) == known::group { + let group_name = sema.db.lookup_atom(*group_name); + res.push(TestDef::GroupName(group_name)); + } + } + _ => return Err(()), + }, + [group_name, _properties, group_content] => { + if let Some(group_name) = &body[group_name].as_atom() { + let group_name = sema.db.lookup_atom(*group_name); + let content = parse_group_content(sema, body, group_content)?; + res.push(TestDef::GroupDef(group_name, content)) + } + } + _ => return Err(()), + }, + _ => return Err(()), + }; + Ok(()) +} + +#[cfg(test)] +mod tests { + + use crate::diagnostics::DiagnosticCode; + use crate::diagnostics::DiagnosticsConfig; + use crate::tests::check_diagnostics_with_config; + use crate::tests::check_fix; + + #[track_caller] + pub(crate) fn check_diagnostics(ra_fixture: &str) { + let config = + DiagnosticsConfig::default().disable(DiagnosticCode::MissingCompileWarnMissingSpec); + check_diagnostics_with_config(config, ra_fixture) + } + + #[test] + fn test_unreachable_test() { + check_diagnostics( + r#" +//- /my_app/test/my_SUITE.erl + -module(my_SUITE). + -export([all/0]). + -export([a/1, b/1]). + all() -> [a]. + a(_Config) -> + ok. + b(_Config) -> +%% ^ 💡 warning: Unreachable test (b/1) + ok. + "#, + ); + } + + #[test] + fn test_unreachable_test_init_end() { + check_diagnostics( + r#" +//- /my_app/test/my_SUITE.erl + -module(my_SUITE). + -export([all/0]). + -export([init_per_suite/1, end_per_suite/1]). + -export([a/1, b/1]). + all() -> [a]. + init_per_suite(Config) -> Config. + end_per_suite(_Config) -> ok. + a(_Config) -> + ok. + b(_Config) -> +%% ^ 💡 warning: Unreachable test (b/1) + ok. + "#, + ); + } + + #[test] + fn test_unreachable_test_unparsable_all() { + check_diagnostics( + r#" +//- /my_app/test/my_SUITE.erl + -module(my_SUITE). + -export([all/0]). + -export([init_per_suite/1, end_per_suite/1]). + -export([a/1, b/1]). + all() -> do_all(). + do_all() -> [a]. + init_per_suite(Config) -> Config. + end_per_suite(_Config) -> ok. + a(_Config) -> + ok. + b(_Config) -> + ok. + "#, + ); + } + + #[test] + fn test_unreachable_test_ignore() { + check_diagnostics( + r#" +//- /my_app/test/my_SUITE.erl + -module(my_SUITE). + -export([all/0]). + -export([a/1, b/1, c/1]). + all() -> [a]. + a(_Config) -> + ok. + % elp:ignore W0008 + b(_Config) -> + ok. + c(_Config) -> +%% ^ 💡 warning: Unreachable test (c/1) + ok. + "#, + ); + } + #[test] + fn test_unreachable_test_ignore_by_label() { + check_diagnostics( + r#" +//- /my_app/test/my_SUITE.erl + -module(my_SUITE). + -export([all/0]). + -export([a/1, b/1, c/1]). + all() -> [a]. + a(_Config) -> + ok. + % elp:ignore unreachable_test + b(_Config) -> + ok. + c(_Config) -> +%% ^ 💡 warning: Unreachable test (c/1) + ok. + "#, + ); + } + + #[test] + fn test_unreachable_test_fix() { + check_diagnostics( + r#" + //- /my_app/test/my_SUITE.erl + -module(my_SUITE). + -export([all/0]). + -export([a/1, b/1, c/1]). + all() -> [a]. + a(_Config) -> + ok. + b(_Config) -> + %% ^ 💡 warning: Unreachable test (b/1) + ok. + c(_Config) -> + %% ^ 💡 warning: Unreachable test (c/1) + ok. + "#, + ); + check_fix( + r#" +//- /my_app/test/my_SUITE.erl +-module(my_SUITE). +-export([all/0]). +-export([a/1, b/1, c/1]). +all() -> [a]. +a(_Config) -> + ok. +b(_Config) -> + ok. +c~(_Config) -> + ok. + "#, + r#" +-module(my_SUITE). +-export([all/0]). +-export([a/1, b/1, c/1]). +all() -> [a]. +a(_Config) -> + ok. +b(_Config) -> + ok. +% elp:ignore W0008 (unreachable_test) +c(_Config) -> + ok. + "#, + ); + } +} diff --git a/crates/ide/src/diagnostics.rs b/crates/ide/src/diagnostics.rs new file mode 100644 index 0000000000..8d68734897 --- /dev/null +++ b/crates/ide/src/diagnostics.rs @@ -0,0 +1,1349 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::collections::BTreeSet; +use std::fmt; +use std::str::FromStr; + +use elp_ide_assists::AssistId; +use elp_ide_assists::AssistKind; +use elp_ide_db::assists::Assist; +use elp_ide_db::docs::DocDatabase; +use elp_ide_db::elp_base_db::FileId; +use elp_ide_db::erlang_service; +use elp_ide_db::erlang_service::DiagnosticLocation; +use elp_ide_db::erlang_service::Location; +use elp_ide_db::erlang_service::ParseError; +use elp_ide_db::erlang_service::StartLocation; +use elp_ide_db::label::Label; +use elp_ide_db::source_change::SourceChange; +use elp_ide_db::ErlAstDatabase; +use elp_ide_db::LineCol; +use elp_ide_db::LineIndex; +use elp_ide_db::LineIndexDatabase; +use elp_syntax::algo; +use elp_syntax::ast; +use elp_syntax::ast::AstNode; +use elp_syntax::Direction; +use elp_syntax::NodeOrToken; +use elp_syntax::Parse; +use elp_syntax::SyntaxKind; +use elp_syntax::SyntaxNode; +use elp_syntax::TextRange; +use elp_syntax::TextSize; +use fxhash::FxHashMap; +use fxhash::FxHashSet; +use hir::db::MinDefDatabase; +use hir::InFile; +use hir::Semantic; +use lazy_static::lazy_static; +use regex::Regex; +use strum::IntoEnumIterator; +use strum_macros::EnumIter; +use text_edit::TextEdit; + +use crate::common_test; +// @fb-only: use crate::meta_only::MetaOnlyDiagnosticCode; +use crate::RootDatabase; +use crate::SourceDatabase; + +mod application_env; +mod effect_free_statement; +mod head_mismatch; +// @fb-only: mod meta_only; +mod missing_compile_warn_missing_spec; +mod misspelled_attribute; +mod module_mismatch; +mod mutable_variable; +mod redundant_assignment; +mod replace_call; +mod trivial_match; +mod unused_function_args; +mod unused_include; +mod unused_macro; +mod unused_record_field; + +#[derive(Debug, Clone)] +// For the doc please refer to +// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/ +pub struct Diagnostic { + pub message: String, + pub range: TextRange, + pub severity: Severity, + pub experimental: bool, + pub fixes: Option>, + pub related_info: Option>, + pub code: DiagnosticCode, +} + +impl Diagnostic { + pub(crate) fn new( + code: DiagnosticCode, + message: impl Into, + range: TextRange, + ) -> Diagnostic { + let message = message.into(); + Diagnostic { + code, + message, + range, + severity: Severity::Error, + experimental: false, + fixes: None, + related_info: None, + } + } + + pub(crate) fn with_related( + mut self, + related_info: Option>, + ) -> Diagnostic { + self.related_info = related_info; + self + } + + fn error(code: DiagnosticCode, range: TextRange, message: String) -> Self { + Self::new(code, message, range).severity(Severity::Error) + } + + fn warning(code: DiagnosticCode, range: TextRange, message: String) -> Self { + Self::new(code, message, range).severity(Severity::Warning) + } + + pub(crate) fn severity(mut self, severity: Severity) -> Diagnostic { + self.severity = severity; + self + } + + pub(crate) fn with_fixes(mut self, fixes: Option>) -> Diagnostic { + self.fixes = fixes; + self + } + + pub(crate) fn experimental(mut self) -> Diagnostic { + self.experimental = true; + self + } + + pub(crate) fn should_be_ignored(&self, line_index: &LineIndex, source: &SyntaxNode) -> bool { + match prev_line_comment_text(&line_index, source, self.range.start()) { + Some(comment) => comment_contains_ignore_code(&comment, &self.code), + None => false, + } + } + + pub(crate) fn with_ignore_fix(mut self, file_id: FileId) -> Diagnostic { + let mut builder = TextEdit::builder(); + let text = format!( + "% elp:ignore {} ({})\n", + self.code.as_code(), + self.code.as_label() + ); + builder.insert(self.range.start(), text); + let edit = builder.finish(); + let source_change = SourceChange::from_text_edit(file_id, edit); + let ignore_fix = Assist { + id: AssistId("ignore_problem", AssistKind::QuickFix), + label: Label::new("Ignore problem"), + group: None, + target: self.range, + source_change: Some(source_change), + user_input: None, + }; + match &mut self.fixes { + Some(fixes) => fixes.push(ignore_fix), + None => self.fixes = Some(vec![ignore_fix]), + }; + self + } + + pub fn print(&self, line_index: &LineIndex) -> String { + let start = line_index.line_col(self.range.start()); + let end = line_index.line_col(self.range.end()); + format!( + "{}:{}-{}:{}::[{:?}] [{}] {}", + start.line, + start.col_utf16, + end.line, + end.col_utf16, + self.severity, + self.code, + self.message + ) + } +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Ignore { + pub codes: Vec, + pub suppression_range: TextRange, +} + +#[derive(Debug, Clone)] +pub struct RelatedInformation { + pub range: TextRange, + pub message: String, +} + +impl fmt::Display for Diagnostic { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "({}, {:?} {:?} {:?})", + self.message, self.range, self.severity, self.code + ) + } +} + +#[derive(Debug, Copy, Clone)] +pub enum Severity { + Error, + Warning, + // `WeakWarning` maps onto a Notice warning when used in the LSP + // environment, and in VS Code this means it does not show up in + // the problems pane, has an unobtrusive underline, but does show + // up on hover if the cursor is placed on it. + WeakWarning, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, EnumIter)] +// pub struct DiagnosticCode(pub String); +pub enum DiagnosticCode { + DefaultCodeForEnumIter, + HeadMismatch, + MissingModule, + ModuleMismatch, + UnusedInclude, + BoundVarInPattern, + UnusedMacro, + UnusedRecordField, + MutableVarBug, + SyntaxError, + Missing(String), + StatementHasNoEffect, + TrivialMatch, + UnusedFunctionArg, + RedundantAssignment, + UnreachableTest, + ApplicationGetEnv, + MissingCompileWarnMissingSpec, + MisspelledAttribute, + + // Wrapper for erlang service diagnostic codes + ErlangService(String), + // Used for ad-hoc diagnostics via lints/codemods + AdHoc(String), + // @fb-only: MetaOnly(MetaOnlyDiagnosticCode), +} + +impl Default for DiagnosticCode { + fn default() -> Self { + DiagnosticCode::DefaultCodeForEnumIter + } +} + +impl DiagnosticCode { + pub fn as_code(&self) -> String { + match self { + DiagnosticCode::DefaultCodeForEnumIter => "DEFAULT-UNUSED-CONSTRUCTOR".to_string(), + DiagnosticCode::MissingModule => "L1201".to_string(), + DiagnosticCode::UnusedInclude => "L1500".to_string(), // Unused file + DiagnosticCode::HeadMismatch => "P1700".to_string(), // "head-mismatch" + DiagnosticCode::SyntaxError => "P1711".to_string(), + DiagnosticCode::BoundVarInPattern => "W0000".to_string(), + DiagnosticCode::ModuleMismatch => "W0001".to_string(), // "module-mismatch" + DiagnosticCode::UnusedMacro => "W0002".to_string(), // "unused-macro" + DiagnosticCode::UnusedRecordField => "W0003".to_string(), // unused-record-field + DiagnosticCode::Missing(_) => "W0004".to_string(), // epp had missing_comma and missing_parenthesis + DiagnosticCode::MutableVarBug => "W0005".to_string(), // mutable-variable + DiagnosticCode::StatementHasNoEffect => "W0006".to_string(), // statement-has-no-effect + DiagnosticCode::TrivialMatch => "W0007".to_string(), // trivial-match + DiagnosticCode::UnreachableTest => "W0008".to_string(), + DiagnosticCode::RedundantAssignment => "W0009".to_string(), // redundant-assignment + DiagnosticCode::UnusedFunctionArg => "W0010".to_string(), // unused-function-arg + DiagnosticCode::ApplicationGetEnv => "W0011".to_string(), // application_get_env + DiagnosticCode::MissingCompileWarnMissingSpec => "W0012".to_string(), + DiagnosticCode::MisspelledAttribute => "W0013".to_string(), // misspelled-attribute + DiagnosticCode::ErlangService(c) => c.to_string(), + DiagnosticCode::AdHoc(c) => format!("ad-hoc: {c}").to_string(), + // @fb-only: DiagnosticCode::MetaOnly(c) => c.as_code(), + } + } + + pub fn as_label(&self) -> String { + match self { + DiagnosticCode::DefaultCodeForEnumIter => "DEFAULT-UNUSED-CONSTRUCTOR".to_string(), + DiagnosticCode::MissingModule => "missing_module".to_string(), + DiagnosticCode::UnusedInclude => "unused_include".to_string(), + DiagnosticCode::HeadMismatch => "head_mismatch".to_string(), + DiagnosticCode::SyntaxError => "syntax_error".to_string(), + DiagnosticCode::BoundVarInPattern => "bound_var_in_pattern".to_string(), + DiagnosticCode::ModuleMismatch => "module_mismatch".to_string(), + DiagnosticCode::UnusedMacro => "unused_macro".to_string(), + DiagnosticCode::UnusedRecordField => "unused_record_field".to_string(), + DiagnosticCode::Missing(_) => "missing_comma_or_parenthesis".to_string(), + DiagnosticCode::MutableVarBug => "mutable_variable_bug".to_string(), + DiagnosticCode::StatementHasNoEffect => "statement_has_no_effect".to_string(), + DiagnosticCode::TrivialMatch => "trivial_match".to_string(), + DiagnosticCode::UnusedFunctionArg => "unused_function_arg".to_string(), + DiagnosticCode::RedundantAssignment => "redundant_assignment".to_string(), + DiagnosticCode::UnreachableTest => "unreachable_test".to_string(), + DiagnosticCode::MissingCompileWarnMissingSpec => { + // Match the name in the original + "compile-warn-missing-spec".to_string() + } + DiagnosticCode::ApplicationGetEnv => "application_get_env".to_string(), + DiagnosticCode::MisspelledAttribute => "misspelled_attribute".to_string(), + DiagnosticCode::ErlangService(c) => c.to_string(), + DiagnosticCode::AdHoc(c) => format!("ad-hoc: {c}").to_string(), + // @fb-only: DiagnosticCode::MetaOnly(c) => c.as_label(), + } + } + + pub fn maybe_from_string(s: &String) -> Option { + if let Some(r) = DIAGNOSTIC_CODE_LOOKUPS.get(s) { + Some(r.clone()) + } else { + // Look for ErlangService and AdHoc + if let Some(code) = Self::is_adhoc(s) { + Some(DiagnosticCode::AdHoc(code)) + } else { + // Last resort, an ErlangService one. + // This is broad, so it can expand easily + if let Some(code) = Self::is_erlang_service(s) { + Some(DiagnosticCode::ErlangService(code)) + } else { + None + } + } + } + } + + /// Check if the diagnostic label is for an AdHoc one. + fn is_adhoc(s: &str) -> Option { + // Looking for something like "ad-hoc: ad-hoc-title-1" + lazy_static! { + static ref RE: Regex = Regex::new(r"^ad-hoc: ([^\s]+)$").unwrap(); + } + RE.captures_iter(s).next().map(|c| c[1].to_string()) + } + + /// Check if the diagnostic label is for an ErlangService one. + fn is_erlang_service(s: &str) -> Option { + // Looing for something like "L0008" + lazy_static! { + static ref RE: Regex = Regex::new(r"^([A-Z]+[0-9]{4})$").unwrap(); + } + RE.captures_iter(s).next().map(|c| c[1].to_string()) + } +} + +lazy_static! { + static ref DIAGNOSTIC_CODE_LOOKUPS: FxHashMap = { + let mut res = FxHashMap::default(); + for code in DiagnosticCode::iter() { + res.insert(code.as_code(), code.clone()); + res.insert(code.as_label(), code.clone()); + } + res + }; +} + +impl FromStr for DiagnosticCode { + type Err = String; + fn from_str(s: &str) -> Result { + if let Some(code) = DiagnosticCode::maybe_from_string(&s.to_string()) { + Ok(code) + } else { + Err(format!("Unknown DiagnosticCode: '{s}'")) + } + } +} +impl fmt::Display for DiagnosticCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_code()) + } +} + +pub trait AdhocSemanticDiagnostics: + Fn(&mut Vec, &Semantic, FileId, Option<&str>) -> () + std::panic::RefUnwindSafe + Sync +{ +} +impl AdhocSemanticDiagnostics for F where + F: Fn(&mut Vec, &Semantic, FileId, Option<&str>) -> () + + std::panic::RefUnwindSafe + + Sync +{ +} + +#[derive(Default, Clone)] +pub struct DiagnosticsConfig<'a> { + pub disable_experimental: bool, + disabled: FxHashSet, + pub adhoc_semantic_diagnostics: Vec<&'a dyn AdhocSemanticDiagnostics>, +} + +impl<'a> DiagnosticsConfig<'a> { + pub fn new( + disable_experimental: bool, + disabled: FxHashSet, + adhoc_semantic_diagnostics: Vec<&'a dyn AdhocSemanticDiagnostics>, + ) -> DiagnosticsConfig<'a> { + DiagnosticsConfig { + disable_experimental, + disabled, + adhoc_semantic_diagnostics, + } + } + + pub fn disable(mut self, code: DiagnosticCode) -> DiagnosticsConfig<'a> { + self.disabled.insert(code); + self + } +} + +pub fn diagnostics( + db: &RootDatabase, + config: &DiagnosticsConfig, + file_id: FileId, + include_generated: bool, +) -> Vec { + lazy_static! { + static ref EXTENSIONS: Vec = vec!["erl".to_string(), "hrl".to_string(),]; + }; + let parse = db.parse(file_id); + let root_id = db.file_source_root(file_id); + let root = db.source_root(root_id); + let path = root.path_for_file(&file_id).unwrap(); + + let ext = path.name_and_extension().unwrap_or_default().1; + let report_diagnostics = EXTENSIONS.iter().any(|it| Some(it.as_str()) == ext); + + let mut res = Vec::new(); + + if report_diagnostics { + let is_erl_module = matches!(path.name_and_extension(), Some((_, Some("erl")))); + let sema = Semantic::new(db); + + if is_erl_module { + no_module_definition_diagnostic(&mut res, &parse); + if include_generated || !db.is_generated(file_id) { + unused_include::unused_includes(&sema, db, &mut res, file_id); + } + let is_test_suite = match path.name_and_extension() { + Some((name, _)) => name.ends_with("_SUITE"), + _ => false, + }; + if is_test_suite { + common_test::unreachable_test(&mut res, &sema, file_id) + } + } + + res.append(&mut form_missing_separator_diagnostics(&parse)); + + config + .adhoc_semantic_diagnostics + .iter() + .for_each(|f| f(&mut res, &sema, file_id, ext)); + semantic_diagnostics(&mut res, &sema, file_id, ext, config.disable_experimental); + syntax_diagnostics(db, &parse, &mut res, file_id); + + res.extend(parse.errors().iter().take(128).map(|err| { + Diagnostic::error( + DiagnosticCode::SyntaxError, + err.range(), + format!("Syntax Error: {}", err), + ) + })); + } + let line_index = db.file_line_index(file_id); + res.retain(|d| { + !config.disabled.contains(&d.code) + && !(config.disable_experimental && d.experimental) + && !d.should_be_ignored(&line_index, &parse.syntax_node()) + }); + + res +} + +pub fn semantic_diagnostics( + res: &mut Vec, + sema: &Semantic, + file_id: FileId, + ext: Option<&str>, + disable_experimental: bool, +) { + // TODO: disable this check when T151727890 and T151605845 are resolved + if !disable_experimental { + unused_function_args::unused_function_args(res, sema, file_id); + redundant_assignment::redundant_assignment(res, sema, file_id); + trivial_match::trivial_match(res, sema, file_id); + } + unused_macro::unused_macro(res, sema, file_id, ext); + unused_record_field::unused_record_field(res, sema, file_id, ext); + mutable_variable::mutable_variable_bug(res, sema, file_id); + effect_free_statement::effect_free_statement(res, sema, file_id); + application_env::application_env(res, sema, file_id); + // @fb-only: meta_only::diagnostics(res, sema, file_id); + missing_compile_warn_missing_spec::missing_compile_warn_missing_spec(res, sema, file_id); +} + +pub fn syntax_diagnostics( + db: &RootDatabase, + parse: &Parse, + res: &mut Vec, + file_id: FileId, +) { + misspelled_attribute::misspelled_attribute(res, db, file_id); + for node in parse.tree().syntax().descendants() { + head_mismatch::head_mismatch(res, file_id, &node); + module_mismatch::module_mismatch(res, db, file_id, &node); + } +} + +pub fn filter_diagnostics(diagnostics: Vec, code: DiagnosticCode) -> Vec { + diagnostics.into_iter().filter(|d| d.code == code).collect() +} + +fn no_module_definition_diagnostic( + diagnostics: &mut Vec, + parse: &Parse, +) { + let mut report = |range| { + diagnostics.push(Diagnostic { + message: "no module definition".to_string(), + range, + severity: Severity::Error, + experimental: false, + fixes: None, + related_info: None, + code: DiagnosticCode::MissingModule, + }); + }; + for form in parse.tree().forms() { + match form { + ast::Form::PreprocessorDirective(_) => { + continue; // skip any directives + } + ast::Form::FileAttribute(_) => { + continue; // skip + } + ast::Form::ModuleAttribute(_) => { + break; + } + other_form => { + report(other_form.syntax().text_range()); + break; + } + } + } +} + +fn form_missing_separator_diagnostics(parse: &Parse) -> Vec { + parse + .tree() + .forms() + .into_iter() + .flat_map(|form: ast::Form| match form { + ast::Form::ExportAttribute(f) => { + check_missing_sep(f.funs(), SyntaxKind::ANON_COMMA, ",", "missing_comma") + } + ast::Form::ExportTypeAttribute(f) => { + check_missing_sep(f.types(), SyntaxKind::ANON_COMMA, ",", "missing_comma") + } + ast::Form::FunDecl(f) => { + check_missing_sep(f.clauses(), SyntaxKind::ANON_SEMI, ";", "missing_semi") + } + ast::Form::ImportAttribute(f) => { + check_missing_sep(f.funs(), SyntaxKind::ANON_COMMA, ",", "missing_comma") + } + ast::Form::RecordDecl(f) => record_decl_check_missing_comma(f), + ast::Form::TypeAlias(f) => { + let args = f + .name() + .and_then(|name| name.args()) + .into_iter() + .flat_map(|args| args.args()); + check_missing_sep(args, SyntaxKind::ANON_COMMA, ",", "missing_comma") + } + ast::Form::Opaque(f) => { + let args = f + .name() + .and_then(|name| name.args()) + .into_iter() + .flat_map(|args| args.args()); + check_missing_sep(args, SyntaxKind::ANON_COMMA, ",", "missing_comma") + } + _ => vec![], + }) + .collect() +} + +fn check_missing_sep( + nodes: impl Iterator, + separator: SyntaxKind, + item: &'static str, + code: &'static str, +) -> Vec { + let mut diagnostics = vec![]; + + for node in nodes.skip(1) { + let syntax = node.syntax(); + if let Some(previous) = non_whitespace_sibling_or_token(syntax, Direction::Prev) { + if previous.kind() != separator { + diagnostics.push(make_missing_diagnostic( + previous.text_range(), + item, + code.to_string(), + )) + } + } + } + + diagnostics +} + +fn record_decl_check_missing_comma(record: ast::RecordDecl) -> Vec { + if let Some(name) = record.name() { + if let Some(next) = non_whitespace_sibling_or_token(name.syntax(), Direction::Next) { + if next.kind() != SyntaxKind::ANON_COMMA { + return vec![make_missing_diagnostic( + name.syntax().text_range(), + ",", + "missing_comma".to_string(), + )]; + } + } + } + + vec![] +} + +fn comment_contains_ignore_code(comment: &str, code: &DiagnosticCode) -> bool { + let pattern = "% elp:ignore"; + match comment.find(pattern) { + Some(start) => { + let comment = comment[start.into()..].to_string(); + comment + .split_whitespace() + .any(|code_str| match DiagnosticCode::from_str(code_str) { + Ok(code_comment) => *code == code_comment, + Err(_) => false, + }) + } + _ => false, + } +} + +fn prev_line(line_index: &LineIndex, current_line: u32) -> Option { + match current_line { + 0 => None, + _ => line_index.line_at(current_line as usize - 1), + } +} + +fn prev_line_comment_text( + line_index: &LineIndex, + source: &SyntaxNode, + offset: TextSize, +) -> Option { + let current_line = line_index.line_col(offset).line; + let prev_line = prev_line(line_index, current_line)?; + // Temporary for T153426323 + let _pctx = stdx::panic_context::enter(format!("\nprev_line_comment_text")); + let token = source.token_at_offset(prev_line).left_biased()?; + Some( + token + .siblings_with_tokens(elp_syntax::Direction::Next) + .filter(|node| node.kind() == SyntaxKind::COMMENT) + .next()? + .as_node()? + .text() + .to_string(), + ) +} + +fn non_whitespace_sibling_or_token(node: &SyntaxNode, dir: Direction) -> Option { + node.siblings_with_tokens(dir) + .skip(1) // starts with self + .filter(|node| node.kind() != SyntaxKind::WHITESPACE && node.kind() != SyntaxKind::COMMENT) + .next() +} + +fn make_missing_diagnostic(range: TextRange, item: &'static str, code: String) -> Diagnostic { + let message = format!("Missing '{}'", item); + Diagnostic { + message, + range, + severity: Severity::Warning, + experimental: false, + fixes: None, + related_info: None, + code: DiagnosticCode::Missing(code), + } +} + +pub fn erlang_service_diagnostics( + db: &RootDatabase, + file_id: FileId, +) -> Vec<(FileId, Vec)> { + // Use the same format as eqwalizer, so we can re-use the salsa cache entry + let format = erlang_service::Format::OffsetEtf; + + let res = db.module_ast(file_id, format); + + // We use a BTreeSet of a tuple because neither ParseError nor + // Diagnostic nor TextRange has an Ord instance + let mut error_info: BTreeSet<(FileId, TextSize, TextSize, String, String)> = + BTreeSet::default(); + let mut warning_info: BTreeSet<(FileId, TextSize, TextSize, String, String)> = + BTreeSet::default(); + + res.errors + .iter() + .filter_map(|d| parse_error_to_diagnostic_info(db, file_id, d)) + .for_each(|val| { + error_info.insert(val); + }); + res.warnings + .iter() + .filter_map(|d| parse_error_to_diagnostic_info(db, file_id, d)) + .for_each(|val| { + warning_info.insert(val); + }); + + let diags: Vec<(FileId, Diagnostic)> = error_info + .into_iter() + .map(|(file_id, start, end, code, msg)| { + // Temporary for T148094436 + let _pctx = stdx::panic_context::enter(format!("\nerlang_service_diagnostics:1")); + ( + file_id, + Diagnostic::new( + DiagnosticCode::ErlangService(code), + msg, + TextRange::new(start, end), + ) + .severity(Severity::Error), + ) + }) + .chain( + warning_info + .into_iter() + .map(|(file_id, start, end, code, msg)| { + // Temporary for T148094436 + let _pctx = + stdx::panic_context::enter(format!("\nerlang_service_diagnostics:2")); + ( + file_id, + Diagnostic::new( + DiagnosticCode::ErlangService(code), + msg, + TextRange::new(start, end), + ) + .severity(Severity::Warning), + ) + }), + ) + .collect(); + + // Remove diagnostics already reported by ELP + let diags: Vec<(FileId, Diagnostic)> = diags + .into_iter() + .filter(|(_, d)| is_implemented_in_elp(&d.message)) + .collect(); + if diags.len() == 0 { + // If there are no diagnostics reported, return an empty list + // against the `file_id` to clear the list of diagnostics for + // the file. + vec![(file_id, vec![])] + } else { + let mut diags_map: FxHashMap> = FxHashMap::default(); + diags.into_iter().for_each(|(file_id, diag)| { + diags_map + .entry(file_id) + .and_modify(|existing| existing.push(diag.clone())) + .or_insert(vec![diag.clone()]); + }); + diags_map + .into_iter() + .map(|(file_id, ds)| (file_id, ds)) + .collect() + } +} + +pub fn edoc_diagnostics(db: &RootDatabase, file_id: FileId) -> Vec<(FileId, Vec)> { + // We use a BTreeSet of a tuple because neither ParseError nor + // Diagnostic nor TextRange has an Ord instance + let mut error_info: BTreeSet<(FileId, TextSize, TextSize, String, String)> = + BTreeSet::default(); + let mut warning_info: BTreeSet<(FileId, TextSize, TextSize, String, String)> = + BTreeSet::default(); + + // If the file cannot be parsed, it does not really make sense to run EDoc, + // so let's return early. + // Use the same format as eqwalizer, so we can re-use the salsa cache entry. + let format = erlang_service::Format::OffsetEtf; + let ast = db.module_ast(file_id, format); + if !ast.is_ok() { + return vec![]; + }; + + let res = db.file_doc(file_id); + let line_index = db.file_line_index(file_id); + let code = "EDOC000".to_string(); + + res.diagnostics.iter().for_each(|d| { + // While line number in EDoc diagnostics are 1 based, + // EDoc can return some error messages for the entire module with + // a default location of 0. + // We normalize it to 1, so it can be correctly displayed on the first line of the module. + // See: https://github.com/erlang/otp/blob/f9e367c1992735164b0e6c96881c35a30890aed2/lib/edoc/src/edoc.erl#L778-L782 + let line = if d.line == 0 { 1 } else { d.line }; + let start = line_index + .safe_offset(LineCol { + line: line - 1, + col_utf16: 0, + }) + .unwrap_or(TextSize::from(0)); + let end = line_index + .safe_offset(LineCol { line, col_utf16: 0 }) + .unwrap_or(TextSize::from(0)); + let message = &d.message; + let val = (file_id, start, end, code.clone(), message.clone()); + match d.severity.as_str() { + "error" => { + error_info.insert(val); + } + "warning" => { + warning_info.insert(val); + } + _ => (), + } + }); + + let diags: Vec<(FileId, Diagnostic)> = error_info + .into_iter() + .map(|(file_id, start, end, code, msg)| { + // Temporary for T148094436 + let _pctx = stdx::panic_context::enter(format!("\nedoc_diagnostics:1")); + ( + file_id, + Diagnostic::new( + DiagnosticCode::ErlangService(code), + msg, + TextRange::new(start, end), + ) + .severity(Severity::WeakWarning), + ) + }) + .chain( + warning_info + .into_iter() + .map(|(file_id, start, end, code, msg)| { + // Temporary for T148094436 + let _pctx = stdx::panic_context::enter(format!("\nedoc_diagnostics:2")); + ( + file_id, + Diagnostic::new( + DiagnosticCode::ErlangService(code), + msg, + TextRange::new(start, end), + ) + .severity(Severity::WeakWarning), + ) + }), + ) + .collect(); + + if diags.len() == 0 { + // If there are no diagnostics reported, return an empty list + // against the `file_id` to clear the list of diagnostics for + // the file. + vec![(file_id, vec![])] + } else { + let mut diags_map: FxHashMap> = FxHashMap::default(); + diags.into_iter().for_each(|(file_id, diag)| { + diags_map + .entry(file_id) + .and_modify(|existing| existing.push(diag.clone())) + .or_insert(vec![diag.clone()]); + }); + diags_map + .into_iter() + .map(|(file_id, ds)| (file_id, ds)) + .collect() + } +} + +/// Match the message part of the diagnostics produced by erlang_ls or +/// the erlang_service but already implemented natively in ELP +pub fn is_implemented_in_elp(message: &String) -> bool { + match message.as_str() { + "head mismatch" => false, + "no module definition" => false, + _ => true, + } +} + +fn parse_error_to_diagnostic_info( + db: &RootDatabase, + file_id: FileId, + parse_error: &ParseError, +) -> Option<(FileId, TextSize, TextSize, String, String)> { + match parse_error.location { + Some(DiagnosticLocation::Included { + directive_location, + error_location, + }) => { + // This diagnostic belongs to the file included at the + // `directive_location. + if let Some(included_file_id) = + included_file_file_id(db, file_id, Location::TextRange(directive_location)) + { + Some(( + included_file_id, + error_location.start(), + error_location.end(), + parse_error.code.clone(), + parse_error.msg.clone(), + )) + } else { + None + } + } + Some(DiagnosticLocation::Normal(Location::TextRange(range))) => { + let default_range = ( + file_id, + range.start(), + range.end(), + parse_error.code.clone(), + parse_error.msg.clone(), + ); + match parse_error.code.as_str() { + // For certain warnings, OTP returns a diagnostic for the entire definition of a function or record. + // That can be very verbose and distracting, so we try restricting the range to the function/record name only. + "L1230" | "L1309" => match function_name_range(db, file_id, range) { + Some(name_range) => Some(( + file_id, + name_range.start(), + name_range.end(), + parse_error.code.clone(), + parse_error.msg.clone(), + )), + None => Some(default_range), + }, + "L1260" => match record_name_range(db, file_id, range) { + Some(name_range) => Some(( + file_id, + name_range.start(), + name_range.end(), + parse_error.code.clone(), + parse_error.msg.clone(), + )), + None => Some(default_range), + }, + _ => Some(default_range), + } + } + Some(DiagnosticLocation::Normal(Location::StartLocation(StartLocation { + line: _, + column: _, + }))) => { + log::error!( + "Expecting TextRange, erlang_service provided Location: {:?}", + parse_error.location + ); + Some(( + file_id, + TextSize::default(), + TextSize::default(), + parse_error.code.clone(), + parse_error.msg.clone(), + )) + } + None => Some(( + file_id, + TextSize::default(), + TextSize::default(), + parse_error.code.clone(), + parse_error.msg.clone(), + )), + } +} + +fn function_name_range(db: &RootDatabase, file_id: FileId, range: TextRange) -> Option { + let sema = Semantic::new(db); + let source_file = sema.parse(file_id); + let function = + algo::find_node_at_offset::(source_file.value.syntax(), range.start())?; + Some(function.name()?.syntax().text_range()) +} + +fn record_name_range(db: &RootDatabase, file_id: FileId, range: TextRange) -> Option { + let sema = Semantic::new(db); + let source_file = sema.parse(file_id); + let record = + algo::find_node_at_offset::(source_file.value.syntax(), range.start())?; + Some(record.name()?.syntax().text_range()) +} + +/// For an error in an included file, find the include directive, work +/// out what include file it refers to, get its FileId +pub fn included_file_file_id( + db: &RootDatabase, + file_id: FileId, + directive_location: Location, +) -> Option { + let line_index = db.file_line_index(file_id); + + let directive_range = location_range(directive_location, &line_index); + let parsed = db.parse(file_id); + let form_list = db.file_form_list(file_id); + let include = form_list.includes().find_map(|(idx, include)| { + let form = include.form_id().get(&parsed.tree()); + if form.syntax().text_range().contains(directive_range.start()) { + db.resolve_include(InFile::new(file_id, idx)) + } else { + None + } + })?; + Some(include) +} + +fn location_range(location: Location, line_index: &LineIndex) -> TextRange { + match location { + Location::TextRange(range) => range, + Location::StartLocation(StartLocation { line, column }) => { + let line_col = LineCol { + line, + col_utf16: column, + }; + // Temporary for T147609435 + let _pctx = stdx::panic_context::enter(format!("\ndiagnostics::location_range")); + let pos = line_index.offset(line_col); + TextRange::new(pos, pos) + } + } +} + +// --------------------------------------------------------------------- + +// To run the tests via cargo +// cargo test --package elp_ide --lib +#[cfg(test)] +mod tests { + use elp_syntax::ast; + use expect_test::expect; + + use super::*; + use crate::codemod_helpers::FunctionMatch; + use crate::codemod_helpers::MFA; + use crate::tests::check_diagnostics; + use crate::tests::check_diagnostics_with_config; + + #[test] + fn fun_decl_missing_semi_no_warning() { + let text = concat!("foo(2)->3."); + + let parsed = ast::SourceFile::parse_text(text); + let d = form_missing_separator_diagnostics(&parsed); + assert_eq!(format!("{:?}", d), "[]") + } + + #[test] + fn fun_decl_missing_semi_no_warning_2() { + let text = concat!("foo(1)->2;\n", "foo(2)->3."); + + let parsed = ast::SourceFile::parse_text(text); + let d = form_missing_separator_diagnostics(&parsed); + assert_eq!(format!("{:?}", d), "[]") + } + + #[test] + fn fun_decl_missing_semi() { + check_diagnostics( + r#" + -module(main). + foo(1)->2 +%% ^^^^^^^^^ warning: Missing ';' + foo(2)->3. +"#, + ); + } + + #[test] + fn export_attribute_missing_comma() { + check_diagnostics( + r#" +-module(main). +-export([foo/0 bar/1]). + %% ^^^^^ warning: Missing ',' +"#, + ); + } + + #[test] + fn export_type_attribute_missing_comma() { + check_diagnostics( + r#" +-module(main). +-export_type([foo/0 bar/1]). + %% ^^^^^ warning: Missing ',' +"#, + ); + } + + #[test] + fn import_attribute_missing_comma() { + check_diagnostics( + r#" +-module(main). +-import(bb, [foo/0 bar/1]). + %% ^^^^^ warning: Missing ',' +"#, + ); + } + + #[test] + fn type_decl_missing_comma() { + check_diagnostics( + r#" +-module(main). +-type foo(A B) :: [A,B]. + %% ^ warning: Missing ',' +"#, + ); + } + + #[test] + fn record_decl_missing_comma() { + check_diagnostics( + r#" +-module(main). +-record(foo {f1, f2 = 3}). + %% ^^^ warning: Missing ',' +main(X) -> + {X#foo.f1, X#foo.f2}. +"#, + ); + } + + #[test] + fn record_decl_no_warning() { + check_diagnostics( + r#" +-module(main). +-define(NAME, name). +-record(?NAME, {}). +"#, + ) + } + + // #[test] + // fn define_type_missing_comma() { + // let mut parser = Parser::new(); + // let text = concat!("-define(foo, [?F1, ?F2])."); + + // let source_fn = |range: Range| text[range.start..range.end].to_string(); + // let parsed = Arc::new(to_sourcefile(&parser.parse(&text), &source_fn)); + // let d = crate::diagnostics::form_missing_separator_diagnostics(parsed); + // assert_eq!( + // format!("{:?}", d), + // "[Diagnostic { message: \"Missing ','\", range: 8..11, severity: Warning, code: Some(DiagnosticCode(\"missing_comma\")) }]" + // ) + // } + + #[test] + fn fun_decl_module_decl_ok() { + check_diagnostics( + r#" +-file("main.erl",1). +-define(baz,4). +-module(main). +foo(2)->?baz. +"#, + ); + } + + #[test] + fn fun_decl_module_decl_missing() { + check_diagnostics( + r#" + -file("foo.erl",1). + -define(baz,4). + foo(2)->?baz. +%%^^^^^^^^^^^^^ error: no module definition +"#, + ); + } + + #[test] + fn fun_decl_module_decl_missing_2() { + check_diagnostics( + r#" + baz(1)->4. +%%^^^^^^^^^^ error: no module definition + foo(2)->3. +"#, + ); + } + + #[test] + fn fun_decl_module_decl_after_preprocessor() { + check_diagnostics( + r#" +-ifndef(snmpm_net_if_mt). +-module(main). +-endif. +baz(1)->4. +"#, + ); + } + + #[test] + fn filter_diagnostics() { + let diag1 = "head mismatch".to_string(); + let diag2 = "no module definition".to_string(); + let diagk = "another diagnostic".to_string(); + let diags = vec![diag1, diag2, diagk.clone()]; + assert_eq!( + diags + .into_iter() + .filter(|d| is_implemented_in_elp(&d)) + .collect::>(), + vec![diagk] + ); + } + + #[test] + fn filter_experimental() { + let mut config = DiagnosticsConfig { + disable_experimental: false, + disabled: FxHashSet::default(), + adhoc_semantic_diagnostics: vec![&|acc, sema, file_id, _ext| { + replace_call::replace_call_site( + &FunctionMatch::MFA(MFA { + module: "foo".into(), + name: "bar".into(), + arity: 0, + }), + replace_call::Replacement::UseOk, + acc, + sema, + file_id, + ) + }], + }; + config + .disabled + .insert(DiagnosticCode::MissingCompileWarnMissingSpec); + check_diagnostics_with_config( + DiagnosticsConfig { + disable_experimental: false, + ..config.clone() + }, + r#" + //- /src/main.erl + -module(main). + + do_foo() -> + X = foo:bar(), + %% ^^^^^^^^^ 💡 weak: 'foo:bar/0' called + X. + //- /src/foo.erl + -module(foo). + "#, + ); + check_diagnostics_with_config( + DiagnosticsConfig { + disable_experimental: true, + ..config.clone() + }, + r#" + -module(main). + + do_foo() -> + X = foo:bar(), + X. + "#, + ) + } + + #[test] + fn from_string_1() { + let strings = vec!["W0008", "unreachable_test"]; + let codes = strings + .iter() + .map(|s| DiagnosticCode::maybe_from_string(&s.to_string())) + .collect::>(); + expect![[r#" + [ + Some( + UnreachableTest, + ), + Some( + UnreachableTest, + ), + ] + "#]] + .assert_debug_eq(&codes); + } + + #[test] + fn from_string_2() { + let strings = vec![ + DiagnosticCode::AdHoc("ad-hoc-title-1".to_string()).as_label(), + DiagnosticCode::AdHoc("ad-hoc-title-2".to_string()).as_code(), + ]; + let codes = strings + .iter() + .map(|s| DiagnosticCode::maybe_from_string(&s.to_string())) + .collect::>(); + expect![[r#" + [ + Some( + AdHoc( + "ad-hoc-title-1", + ), + ), + Some( + AdHoc( + "ad-hoc-title-2", + ), + ), + ] + "#]] + .assert_debug_eq(&codes); + } + + #[test] + fn from_string_3() { + let strings = vec!["C1000", "L1213"]; + let codes = strings + .iter() + .map(|s| DiagnosticCode::maybe_from_string(&s.to_string())) + .collect::>(); + expect![[r#" + [ + Some( + ErlangService( + "C1000", + ), + ), + Some( + ErlangService( + "L1213", + ), + ), + ] + "#]] + .assert_debug_eq(&codes); + } +} diff --git a/crates/ide/src/diagnostics/application_env.rs b/crates/ide/src/diagnostics/application_env.rs new file mode 100644 index 0000000000..aedd53cf38 --- /dev/null +++ b/crates/ide/src/diagnostics/application_env.rs @@ -0,0 +1,263 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +// Diagnostic: application-env +// +// Diagnostic for unsafe usages of an applications environment. +// The originial motivation and discussion is in T107133234 + +use elp_ide_db::elp_base_db::FileId; +use hir::ExprId; +use hir::FunctionDef; +use hir::Semantic; + +use super::Diagnostic; +use crate::codemod_helpers::find_call_in_function; +use crate::codemod_helpers::FunctionMatch; +use crate::diagnostics::DiagnosticCode; +use crate::diagnostics::Severity; + +pub(crate) fn application_env(diags: &mut Vec, sema: &Semantic, file_id: FileId) { + sema.def_map(file_id) + .get_functions() + .iter() + .for_each(|(_arity, def)| check_function(diags, sema, def)); +} + +#[derive(Debug, Clone)] +pub(crate) struct BadEnvCall { + mfa: FunctionMatch, + action: BadEnvCallAction, +} + +impl BadEnvCall { + pub(crate) fn new( + m: &str, + f: &str, + arity: Vec, + action: BadEnvCallAction, + ) -> Vec { + arity + .into_iter() + .map(|a| BadEnvCall { + mfa: FunctionMatch::mfa(m.into(), f.into(), a), + action: action.clone(), + }) + .collect() + } +} + +#[derive(Debug, Clone)] +pub(crate) enum BadEnvCallAction { + AppArg(usize), // Zero-based argument index + /// Matches a config argument of the form {tag, {Arg0, Arg1, ...}} + /// The `tag` must match, and we check the `index`th part of the + /// second tuple element + /// e.g. `our_mod:a_fun(Context, Location, [..., {cfg, {Application, Flag}}, ...], Format, Args) + // @oss-only #[allow(dead_code)] + OptionsArg { + /// Which argument contains the list of options to be checked + arg_index: usize, + /// The option tag we are looking for + tag: String, + }, +} + +pub(crate) fn check_function(diags: &mut Vec, sema: &Semantic, def: &FunctionDef) { + let bad_matches = vec![BadEnvCall::new( + "application", + "get_env", + vec![2, 3], + BadEnvCallAction::AppArg(0), + )] + .into_iter() + .flatten() + .collect::>(); + + process_badmatches(diags, sema, def, &bad_matches); +} + +pub(crate) fn process_badmatches( + diags: &mut Vec, + sema: &Semantic, + def: &FunctionDef, + bad: &[BadEnvCall], +) { + let mfas = bad.iter().map(|b| (&b.mfa, &b.action)).collect::>(); + find_call_in_function( + diags, + sema, + def, + &mfas, + &move |_mfa, action, _target, args, def_fb| match action { + BadEnvCallAction::AppArg(arg_index) => { + let arg = args.get(*arg_index)?; + check_valid_application(sema, def_fb, arg, def) + } + BadEnvCallAction::OptionsArg { arg_index, tag } => { + let arg = args.get(*arg_index)?; + match &def_fb[*arg] { + hir::Expr::List { exprs, tail: _ } => { + exprs.iter().find_map(|expr| match &def_fb[*expr] { + hir::Expr::Tuple { exprs } => { + let key = exprs.get(0)?; + let val = exprs.get(1)?; + let key_name = def_fb.as_atom_name(sema.db, &key)?; + if tag == key_name.as_str() { + if let hir::Expr::Tuple { exprs } = &def_fb[*val] { + let app = exprs.get(0)?; + check_valid_application(sema, def_fb, &app, def) + } else { + None + } + } else { + None + } + } + _ => None, + }) + } + _ => None, + } + } + }, + move |_sema, mut _def_fb, _target, _call_id, extra_info, range| { + let diag = + Diagnostic::new(DiagnosticCode::ApplicationGetEnv, extra_info, range.clone()) + .severity(Severity::Warning); + Some(diag) + }, + ); +} + +fn check_valid_application( + sema: &Semantic, + def_fb: &hir::InFunctionBody<&FunctionDef>, + arg: &ExprId, + def: &FunctionDef, +) -> Option { + let arg_name = def_fb.as_atom_name(sema.db, arg)?; + let form_list = sema.db.file_form_list(def.file.file_id); + // We need the app from the calling function location. + let app = sema.db.file_app_name(def_fb.file_id())?; + + if arg_name.as_str() == app.as_str() { + None + } else { + let module_attribute = form_list.module_attribute()?; + let module = module_attribute.name.clone(); + Some(format!( + "module `{module}` belongs to app `{app}`, but reads env for `{arg_name}`" + )) + } +} + +#[cfg(test)] +mod tests { + use crate::diagnostics::DiagnosticCode; + use crate::diagnostics::DiagnosticsConfig; + use crate::tests::check_diagnostics_with_config; + + #[track_caller] + pub(crate) fn check_diagnostics(ra_fixture: &str) { + let mut config = DiagnosticsConfig::default(); + config + .disabled + .insert(DiagnosticCode::MissingCompileWarnMissingSpec); + check_diagnostics_with_config(config, ra_fixture) + } + + #[test] + fn get_env_basic() { + check_diagnostics( + r#" + //- /my_app/src/main.erl app:my_app + -module(main). + + get_mine() -> + application:get_env(misc, key). + %% ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ warning: module `main` belongs to app `my_app`, but reads env for `misc` + + get_mine3() -> + application:get_env(misc, key, def). + %% ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ warning: module `main` belongs to app `my_app`, but reads env for `misc` + + //- /my_app/src/application.erl + -module(application). + -export([get_env/2, get_env/3]). + get_env(App,Key) -> {App,Key}. + get_env(App,Key,Def) -> {App,Key,Def}. + "#, + ) + } + + #[test] + fn get_env_2() { + // TODO: the Scala version also reports for the header file + // (https://fburl.com/code/ohea18zy), but that was introduced + // by D37262963 when tweaking the Glean indexing. We + // currently do not have any functions defined in header files + // for WASERVER + check_diagnostics( + r#" + //- /misc/src/app_env.erl app:misc + -module(app_env). + + -compile([export_all, nowarn_export_all]). + + -include_lib("misc/include/my_header.hrl"). + + get_key_dynamic(App) -> + application:get_env(App, key). + + get_mine() -> + application:get_env(misc, key). + + steal() -> + application:get_env(debug, key). + %% ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ warning: module `app_env` belongs to app `misc`, but reads env for `debug` + + //- /misc/src/application.erl app:misc + -module(application). + -export([get_env/2, get_env/3]). + get_env(App,Key) -> {App,Key}. + get_env(App,Key,Def) -> {App,Key,Def}. + + //- /misc/include/my_header.hrl + get_check_key() -> + application:get_env(check, key). + "#, + ) + } + + #[test] + fn get_env_macro() { + check_diagnostics( + r#" + //- /my_app/src/main.erl app:my_app + -module(main). + + -include("my_app/include/my_header.hrl"). + + get_mine() -> + ?get(misc, key). + %% ^^^^^^^^^^^^^^^ warning: module `main` belongs to app `my_app`, but reads env for `misc` + + //- /my_app/include/my_header.hrl app:my_app + -define(get(K,V), application:get_env(K,V)). + + //- /my_app/src/application.erl app:my_app + -module(application). + -export([get_env/2]). + get_env(App,Key) -> {App,Key}. + + "#, + ) + } +} diff --git a/crates/ide/src/diagnostics/effect_free_statement.rs b/crates/ide/src/diagnostics/effect_free_statement.rs new file mode 100644 index 0000000000..a32dc38420 --- /dev/null +++ b/crates/ide/src/diagnostics/effect_free_statement.rs @@ -0,0 +1,491 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! Lint/fix: effect_free_statement +//! +//! Return a diagnostic if a statement is just a literal or a variable, and +//! offer to remove the statement as a fix. +//! + +use std::iter; + +use elp_ide_db::elp_base_db::FileId; +use elp_ide_db::source_change::SourceChange; +use elp_syntax::ast; +use elp_syntax::AstNode; +use elp_syntax::SyntaxElement; +use elp_syntax::SyntaxKind; +use hir::Expr; +use hir::ExprId; +use hir::FunctionDef; +use hir::InFunctionBody; +use hir::Semantic; +use text_edit::TextEdit; + +use super::Diagnostic; +use super::Severity; +use crate::codemod_helpers::statement_range; +use crate::diagnostics::DiagnosticCode; +use crate::fix; + +pub(crate) fn effect_free_statement(diags: &mut Vec, sema: &Semantic, file_id: FileId) { + sema.def_map(file_id) + .get_functions() + .iter() + .for_each(|(_arity, def)| { + if def.file.file_id == file_id { + let source_file = sema.parse(file_id); + + let def_fb = def.in_function_body(sema.db, def); + let body_map = def_fb.get_body_map(sema.db); + def_fb.fold_function( + (), + &mut |_acc, _, ctx| { + if let Some(in_file_ast_ptr) = body_map.expr(ctx.expr_id) { + if let Some(expr_ast) = in_file_ast_ptr.to_node(&source_file) { + if is_statement(&expr_ast) + && !is_macro_usage(&expr_ast) + && has_no_effect(&def_fb, &ctx.expr_id) + && is_followed_by(SyntaxKind::ANON_COMMA, &expr_ast) + { + diags.push(make_diagnostic(file_id, &expr_ast)); + } + } + } + }, + &mut |_acc, _, _| (), + ); + } + }); +} + +fn has_no_effect(def_fb: &InFunctionBody<&FunctionDef>, expr_id: &ExprId) -> bool { + let expr = &def_fb[*expr_id]; + match expr { + Expr::Missing => false, + + Expr::Literal(_) => true, + Expr::Binary { .. } => true, + Expr::Var(_) => true, + Expr::CaptureFun { .. } => true, + Expr::Closure { .. } => true, + + Expr::Tuple { exprs } => exprs + .iter() + .all(|list_elem| has_no_effect(def_fb, list_elem)), + + Expr::List { exprs, .. } => exprs + .iter() + .all(|list_elem| has_no_effect(def_fb, list_elem)), + + Expr::Map { fields } => fields + .iter() + .all(|(k, v)| has_no_effect(def_fb, k) && has_no_effect(def_fb, v)), + Expr::MapUpdate { .. } => { + // Side-effect: may throw if not a map + false + } + Expr::Comprehension { .. } => { + // Side-effect: may throw due to generators types + false + } + + Expr::Record { fields, .. } => fields + .iter() + .all(|(_key, value)| has_no_effect(def_fb, value)), + Expr::RecordUpdate { .. } | Expr::RecordField { .. } => { + // Side-effect: may throw (e.g. expr not a record) + false + } + Expr::RecordIndex { .. } => true, + + Expr::UnaryOp { .. } | Expr::BinaryOp { .. } => { + // Side-effect: may throw + false + } + Expr::MacroCall { expansion, args: _ } => has_no_effect(def_fb, expansion), + Expr::Call { .. } | Expr::Receive { .. } => false, + + Expr::Block { exprs } => exprs.iter().all(|stmt| has_no_effect(def_fb, stmt)), + Expr::Catch { expr } => has_no_effect(def_fb, expr), + Expr::Try { of_clauses, .. } if !of_clauses.is_empty() => { + // Side-effect: may fail to match + false + } + Expr::Try { exprs, after, .. } => { + // NB. if exprs has no effect, nothing can throw + // so there can be no match errors in catch_clauses + exprs.iter().all(|stmt| has_no_effect(def_fb, stmt)) + && after.iter().all(|stmt| has_no_effect(def_fb, stmt)) + } + Expr::Match { .. } | Expr::If { .. } | Expr::Case { .. } | Expr::Maybe { .. } => { + // Side-effects: + // - binding of variables + // - may fail to match + false + } + } +} + +fn is_statement(expr: &ast::Expr) -> bool { + let syntax = expr.syntax(); + match syntax.parent() { + Some(parent) => match parent.kind() { + SyntaxKind::CLAUSE_BODY => true, + SyntaxKind::BLOCK_EXPR => true, + SyntaxKind::TRY_EXPR => true, + SyntaxKind::CATCH_EXPR => true, + SyntaxKind::TRY_AFTER => true, + _ => false, + }, + _ => false, + } +} + +fn is_macro_usage(expr: &ast::Expr) -> bool { + let syntax = expr.syntax(); + syntax.kind() == SyntaxKind::MACRO_CALL_EXPR +} + +fn is_followed_by(expected_kind: SyntaxKind, expr: &ast::Expr) -> bool { + let node = expr.syntax(); + let elements = iter::successors(node.next_sibling_or_token(), |n| { + (*n).next_sibling_or_token() + }); + for element in elements { + if let Some(t) = &SyntaxElement::into_token(element) { + let kind = t.kind(); + if kind != SyntaxKind::WHITESPACE { + return kind == expected_kind; + } + } + } + return false; +} + +fn remove_statement(expr: &ast::Expr) -> Option { + let range = statement_range(expr); + + let mut edit_builder = TextEdit::builder(); + edit_builder.delete(range); + Some(edit_builder.finish()) +} + +fn make_diagnostic(file_id: FileId, expr: &ast::Expr) -> Diagnostic { + let node = expr.syntax(); + let range = node.text_range(); + let diag = Diagnostic::new( + DiagnosticCode::StatementHasNoEffect, + "this statement has no effect", + range, + ) + .severity(Severity::Warning); + + if let Some(statement_removal) = remove_statement(expr) { + diag.with_fixes(Some(vec![fix( + "remove_statement", + "Remove redundant statement", + SourceChange::from_text_edit(file_id, statement_removal), + range, + )])) + } else { + diag + } +} + +#[cfg(test)] +mod tests { + + use crate::tests::check_diagnostics; + use crate::tests::check_fix; + + #[test] + fn check_removes_comma_and_other_stuff() { + check_fix( + r#" + -module(main). + test_foo(_Config) -> + do_something(), + ~ok, + do_something_else(), + ok. + do_something() -> ok. + "#, + r#" + -module(main). + test_foo(_Config) -> + do_something(), + do_something_else(), + ok. + do_something() -> ok. + "#, + ); + } + + #[test] + fn remove_useless_atom() { + check_diagnostics( + r#" + -module(main). + test_foo(_Config) -> + do_something(), + ok, + %%% ^^ 💡 warning: this statement has no effect + do_something_else(), + bar, + %%% ^^^ 💡 warning: this statement has no effect + ok. + do_something() -> ok. + "#, + ); + } + + #[test] + fn remove_useless_var() { + check_diagnostics( + r#" + -module(main). + test_foo(_Config) -> + X = 42, + X, + %%% ^ 💡 warning: this statement has no effect + ok. + "#, + ); + } + + #[test] + fn remove_useless_literals() { + check_diagnostics( + r#" + -module(main). + test_foo(_Config) -> + do_something(), + 42, + %%% ^^ 💡 warning: this statement has no effect + 41.9999, + %%% ^^^^^^^ 💡 warning: this statement has no effect + do_something_else(), + "foo", + %%% ^^^^^ 💡 warning: this statement has no effect + <<"foo">>, + %%% ^^^^^^^^^ 💡 warning: this statement has no effect + 'A', + %%% ^^^ 💡 warning: this statement has no effect + ok. + do_something() -> 42. + "#, + ); + } + + #[test] + fn remove_useless_lambda() { + check_diagnostics( + r#" + -module(main). + test_foo(_Config) -> + do_something(), + fun() -> do_something() end, + %%% ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 💡 warning: this statement has no effect + F = fun() -> do_something() end, + F(), + fun do_something/0, + %%% ^^^^^^^^^^^^^^^^^^ 💡 warning: this statement has no effect + fun erlang:length/1, + %%% ^^^^^^^^^^^^^^^^^^^ 💡 warning: this statement has no effect + ok. + do_something() -> 42. + "#, + ); + } + + #[test] + fn remove_under_parens() { + check_diagnostics( + r#" + -module(main). + test_foo(_Config) -> + (do_something()), + (blah), + %%% ^^^^^^ 💡 warning: this statement has no effect + ok. + do_something() -> (42). + "#, + ); + } + + #[test] + fn remove_useless_blocks() { + check_diagnostics( + r#" + -module(main). + test_foo(_Config) -> + do_something(), + begin 42, blah, ("foo") end, + %%% ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 💡 warning: this statement has no effect + %%% ^^ 💡 warning: this statement has no effect + %%% ^^^^ 💡 warning: this statement has no effect + begin + do_something(), + blah, + %%% ^^^^ 💡 warning: this statement has no effect + ok + end, + ok. + do_something() -> (42). + "#, + ); + } + + #[test] + fn remove_useless_lists() { + check_diagnostics( + r#" + -module(main). + test_foo(_Config) -> + do_something(), + [42, blah, ("foo")], + %%% ^^^^^^^^^^^^^^^^^^^ 💡 warning: this statement has no effect + [42, do_something(), blah], + [], + %%% ^^ 💡 warning: this statement has no effect + ok. + do_something() -> []. + "#, + ); + } + + #[test] + fn remove_useless_tuples() { + check_diagnostics( + r#" + -module(main). + test_foo(_Config) -> + do_something(), + {42, [blah], {"foo"}}, + %%% ^^^^^^^^^^^^^^^^^^^^^ 💡 warning: this statement has no effect + {42, do_something(), blah}, + {}, + %%% ^^ 💡 warning: this statement has no effect + ok. + do_something() -> []. + "#, + ); + } + + #[test] + fn remove_useless_record_operations() { + check_diagnostics( + r#" + -module(main). + -record(person, {name, age}). + test_foo(P) -> + do_something(), + #person{name="Bob", age=42}, + %%% ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 💡 warning: this statement has no effect + #person{name=get_name(), age=42}, + P#person{name="Alice"}, + #person.name, + %%% ^^^^^^^^^^^^ 💡 warning: this statement has no effect + P#person.name, + ok. + get_name() -> "bob". + "#, + ); + } + + #[test] + fn remove_useless_map_operations() { + check_diagnostics( + r#" + -module(main). + test_foo(P) -> + do_something(), + #{name => "Bob", age => 42}, + %%% ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 💡 warning: this statement has no effect + #{name => get_name(), age => 42}, + #{get_key() => "Bob", age => 42}, + P#{name=>"Alice"}, + ok. + get_name() -> "bob". + get_key() -> name. + "#, + ); + } + + #[test] + fn remove_useless_try_catch_operations() { + check_diagnostics( + r#" + -module(main). + test_foo(_P) -> + catch do_something(), + catch ok, + %%% ^^^^^^^^ 💡 warning: this statement has no effect + try does, nothing catch _ -> do_stuff() end, + %%% ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 💡 warning: this statement has no effect + %%% ^^^^ 💡 warning: this statement has no effect + try + does_nothing + of _ -> ok + catch _ -> not_ok + end, + try + do_something() + after + ok + end, + try does, nothing after blah, ok end, + %%% ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 💡 warning: this statement has no effect + %%% ^^^^ 💡 warning: this statement has no effect + %%% ^^^^ 💡 warning: this statement has no effect + try + does, nothing + %%% ^^^^ 💡 warning: this statement has no effect + of _ -> foo, bar + %%% ^^^ 💡 warning: this statement has no effect + catch + _ -> 42, not_ok + %%% ^^ 💡 warning: this statement has no effect + after + [1,2,3], + %%% ^^^^^^^ 💡 warning: this statement has no effect + ok + end, + ok. + "#, + ); + } + + #[test] + fn ignore_stuff_introduced_from_macros() { + check_diagnostics( + r#" +//- /include/foo.hrl include_path:/include + -define(included_noop(X), noop). + +//- /src/foo.erl + -module(foo). + -include("foo.hrl"). + -define(noop(X), another_noop). + -define(also_noop, yet_another_noop). + + blah() -> + noop, + %%% ^^^^ 💡 warning: this statement has no effect + do_something(), + ?included_noop(42), + do_something(), + ?noop(42), + ?also_noop, + ok. + "#, + ) + } +} diff --git a/crates/ide/src/diagnostics/head_mismatch.rs b/crates/ide/src/diagnostics/head_mismatch.rs new file mode 100644 index 0000000000..55720664df --- /dev/null +++ b/crates/ide/src/diagnostics/head_mismatch.rs @@ -0,0 +1,404 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::hash::Hash; + +use elp_ide_db::elp_base_db::FileId; +use elp_ide_db::source_change::SourceChange; +use elp_syntax::ast; +use elp_syntax::ast::AstNode; +use elp_syntax::syntax_node::SyntaxNode; +use elp_syntax::TextRange; +use fxhash::FxHashMap; +use text_edit::TextEdit; + +use super::DiagnosticCode; +use crate::diagnostics::RelatedInformation; +use crate::fix; +use crate::Diagnostic; + +// Diagnostic: head-mismatch (P1700) +// +// Diagnostic for mismatches between the clauses of a function declaration. + +pub(crate) fn head_mismatch( + acc: &mut Vec, + file_id: FileId, + node: &SyntaxNode, +) -> Option<()> { + head_mismatch_fundecl(acc, file_id, node); + head_mismatch_anonymous_fun(acc, file_id, node); + Some(()) +} + +pub(crate) fn head_mismatch_fundecl( + acc: &mut Vec, + file_id: FileId, + node: &SyntaxNode, +) -> Option<()> { + let f = ast::FunDecl::cast(node.clone())?; + let heads: Vec = fundecl_heads(f); + Name {}.validate_fundecl_attr(file_id, &heads, acc); + Arity {}.validate_fundecl_attr(file_id, &heads, acc); + Some(()) +} + +pub(crate) fn head_mismatch_anonymous_fun( + acc: &mut Vec, + file_id: FileId, + node: &SyntaxNode, +) -> Option<()> { + let f = ast::AnonymousFun::cast(node.clone())?; + let heads: Vec = anonymous_fun_heads(f); + Name {}.validate_fundecl_attr(file_id, &heads, acc); + Arity {}.validate_fundecl_attr(file_id, &heads, acc); + Some(()) +} + +type HeadInfo = (String, TextRange, usize, TextRange); + +trait Validate +where + A: Eq, + A: Hash, + A: Clone, + A: std::fmt::Display, +{ + fn get_attr(self, head: &HeadInfo) -> A; + fn get_loc(self, head: &HeadInfo) -> TextRange; + fn make_diagnostic( + self, + file_id: FileId, + attr: &A, + hattr: &A, + attr_loc: TextRange, + ref_loc: TextRange, + ) -> Diagnostic; + + // Actually does the work + fn validate_fundecl_attr( + self, + file_id: FileId, + heads: &[HeadInfo], + errors: &mut Vec, + ) -> Option<()> + where + Self: Sized, + Self: Copy, + { + // Find the mismatched arity with the lowest number of locations. + + // 1. Create a map of names to locations + let mut attrs: FxHashMap> = FxHashMap::default(); + + if attrs.len() == 1 { + // Only one attr in the funDecl, nothing more to be done + return Some(()); + } + + for head in heads { + let attr = self.get_attr(head); + let attr_loc = self.get_loc(head); + match attrs.get(&attr) { + None => { + attrs.insert(attr.clone(), vec![attr_loc.clone()]); + } + Some(ranges) => { + let mut ranges = ranges.clone(); + ranges.push(attr_loc.clone()); + attrs.insert(attr.clone(), ranges); + } + } + } + + // 2. Find the attrs with the highest count. On a tie, take the + // one occuring earliest in the file + let mut highest = None; + for (attr, locations) in attrs { + match highest { + None => highest = Some((attr.clone(), locations.clone())), + Some((ref _cur_attr, ref cur_highest)) => { + if locations.len() == cur_highest.len() { + // Keep the one closet to the beginning + let mut locs = locations.clone(); + let mut cur = cur_highest.clone(); + locs.sort_by(|a, b| a.start().cmp(&b.start())); + cur.sort_by(|a, b| a.start().cmp(&b.start())); + if locs[0].start() < cur[0].start() { + highest = Some((attr.clone(), locations.clone())); + } + } else if locations.len() > cur_highest.len() { + highest = Some((attr.clone(), locations.clone())); + } + } + } + } + + // 3. Report mismatch for all not highest, against earliest + // occurrence of highest + let (hattr, hlocs) = highest?; + let mut hlocs = hlocs.clone(); + hlocs.sort_by(|a, b| a.start().cmp(&b.start())); + let ref_loc = hlocs[0]; + for head in heads { + let attr = self.get_attr(head); + let attr_loc = self.get_loc(head); + if hattr != attr { + errors.push(self.make_diagnostic(file_id, &attr, &hattr, attr_loc, ref_loc)); + } + } + + None + } +} + +#[derive(Copy, Clone)] +struct Name {} +#[derive(Copy, Clone)] +struct Arity {} + +impl Validate for Name { + fn get_attr(self, head: &HeadInfo) -> String { + head.0.clone() + } + + fn get_loc(self, head: &HeadInfo) -> TextRange { + head.1.clone() + } + + fn make_diagnostic( + self, + file_id: FileId, + attr: &String, + hattr: &String, + attr_loc: TextRange, + ref_loc: TextRange, + ) -> Diagnostic { + let mut edit_builder = TextEdit::builder(); + edit_builder.delete(attr_loc); + edit_builder.insert(attr_loc.start(), hattr.clone()); + let edit = edit_builder.finish(); + + Diagnostic::new( + super::DiagnosticCode::HeadMismatch, + format!("head mismatch '{}' vs '{}'", attr, hattr), + attr_loc, + ) + .with_related(Some(vec![RelatedInformation { + range: ref_loc, + message: "Mismatched clause name".to_string(), + }])) + .with_fixes(Some(vec![fix( + "fix_head_mismatch", + "Fix head mismatch", + SourceChange::from_text_edit(file_id, edit), + attr_loc, + )])) + } +} + +impl Validate for Arity { + fn get_attr(self, head: &HeadInfo) -> usize { + head.2.clone() + } + + fn get_loc(self, head: &HeadInfo) -> TextRange { + head.3.clone() + } + + fn make_diagnostic( + self, + _file_id: FileId, + attr: &usize, + hattr: &usize, + attr_loc: TextRange, + ref_loc: TextRange, + ) -> Diagnostic { + Diagnostic::new( + DiagnosticCode::HeadMismatch, + format!("head arity mismatch {} vs {}", attr, hattr), + attr_loc, + ) + .with_related(Some(vec![RelatedInformation { + range: ref_loc, + message: "Mismatched clause".to_string(), + }])) + } +} + +fn fundecl_heads(fun_decl: ast::FunDecl) -> Vec { + fun_decl + .clauses() + .flat_map(|clause| match clause { + ast::FunctionOrMacroClause::FunctionClause(clause) => Some(clause), + ast::FunctionOrMacroClause::MacroCallExpr(_) => None, + }) + .flat_map(|clause| { + let name = match clause.name()? { + ast::Name::Atom(name) => name, + ast::Name::MacroCallExpr(_) | ast::Name::Var(_) => return None, + }; + let clause_name = name.text()?.to_string(); + let clause_arity = clause.args()?.args().count(); + Some(( + clause_name, + name.syntax().text_range(), + clause_arity, + clause.syntax().text_range(), + )) + }) + .collect() +} + +fn anonymous_fun_heads(fun: ast::AnonymousFun) -> Vec { + fun.clauses() + .flat_map(|clause| { + let clause_name = match clause.name() { + Some(n) => n.text().to_string(), + None => "".to_string(), + }; + let name_location = match clause.name() { + Some(n) => n.syntax().text_range(), + None => clause.syntax().text_range(), + }; + let clause_arity = clause.args()?.args().count(); + Some(( + clause_name, + name_location, + clause_arity, + clause.syntax().text_range(), + )) + }) + .collect() +} + +// To run the tests via cargo +// cargo test --package elp_ide --lib +#[cfg(test)] +mod tests { + use crate::tests::check_diagnostics; + use crate::tests::check_fix; + + // The followings tests exercice head_mismatch function indirectly. + + #[test] + fn test_head_mismatch() { + check_diagnostics( + r#" + -module(main). + foo(0) -> 1; + boo(1) -> 2. + %% ^^^ 💡 error: head mismatch 'boo' vs 'foo' + "#, + ); + check_fix( + r#" + -module(main). + foo(0) -> 1; + bo~o(1) -> 2. + "#, + r#" + -module(main). + foo(0) -> 1; + foo(1) -> 2. + "#, + ); + } + + #[test] + fn test_head_no_mismatch() { + // No head mismatch. + check_diagnostics( + r#" + -module(main). + foo(0) -> 1; + foo(1) -> 2. + "#, + ) + } + + #[test] + fn test_head_mismatch_majority() { + check_diagnostics( + r#" + -module(main). + foo(0) -> 1; + %% ^^^ 💡 error: head mismatch 'foo' vs 'boo' + boo(1) -> 2; + boo(2) -> 3. + "#, + ); + check_fix( + r#" + -module(main). + fo~o(0) -> 1; + boo(1) -> 2; + boo(2) -> 3. + "#, + r#" + -module(main). + boo(0) -> 1; + boo(1) -> 2; + boo(2) -> 3. + "#, + ); + } + + #[test] + fn test_head_mismatch_arity() { + check_diagnostics( + r#" + -module(main). + foo(0) -> 1; + foo(1,0) -> 2. + %% ^^^^^^^^^^^^^ error: head arity mismatch 2 vs 1 + "#, + ); + } + + #[test] + fn test_head_mismatch_arity_majority() { + check_diagnostics( + r#" + -module(main). + foo(2,0) -> 3; + foo(0) -> 1; + %% ^^^^^^^^^^^ error: head arity mismatch 1 vs 2 + foo(1,0) -> 2. + "#, + ); + } + + #[test] + fn test_head_mismatch_quoted_atom() { + check_diagnostics( + r#" + -module(main). + foo(0) -> 1; + 'foo'(1) -> 2. + "#, + ); + } + + #[test] + fn test_head_mismatch_syntax_error() { + check_diagnostics( + r#" + -module(main). + foo() -> + F = fun + (0) -> ok; + A(N) -> ok + %% ^ 💡 error: head mismatch 'A' vs '' + end, + F(). + "#, + ); + } +} diff --git a/crates/ide/src/diagnostics/missing_compile_warn_missing_spec.rs b/crates/ide/src/diagnostics/missing_compile_warn_missing_spec.rs new file mode 100644 index 0000000000..059af0ed69 --- /dev/null +++ b/crates/ide/src/diagnostics/missing_compile_warn_missing_spec.rs @@ -0,0 +1,386 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! Lint/fix: missing_compile_warn_missing_spec +//! +//! Return a diagnostic if a the file does not have +//! `warn_missing_spec(_all)` in a compile attribute +//! Add this as a fix. +//! + +use elp_ide_assists::helpers::add_compile_option; +use elp_ide_db::elp_base_db::FileId; +use elp_ide_db::elp_base_db::SourceDatabase; +use elp_ide_db::source_change::SourceChangeBuilder; +use elp_syntax::AstNode; +use fxhash::FxHashSet; +use hir::known; +use hir::FoldCtx; +use hir::InFile; +use hir::Literal; +use hir::Name; +use hir::Semantic; +use hir::Strategy; +use hir::Term; +use lazy_static::lazy_static; +use regex::Regex; +use text_edit::TextRange; + +use super::Diagnostic; +use crate::fix; + +pub(crate) fn missing_compile_warn_missing_spec( + diags: &mut Vec, + sema: &Semantic, + file_id: FileId, +) { + if sema.db.is_generated(file_id) || Some(false) == is_in_src_dir(sema.db.upcast(), file_id) { + return; + } + let form_list = sema.db.file_form_list(file_id); + if form_list.compile_attributes().next().is_none() { + report_diagnostic(sema, None, file_id, diags); + } + let attributes = form_list + .compile_attributes() + .map(|(idx, compile_attribute)| { + let co = sema.db.compile_body(InFile::new(file_id, idx)); + let is_present = FoldCtx::fold_term( + &co.body, + Strategy::TopDown, + co.value, + false, + &mut |acc, ctx| match &ctx.term { + Term::Literal(Literal::Atom(atom)) => { + let name = sema.db.lookup_atom(*atom); + if MISSING_SPEC_OPTIONS.contains(&name) { + true + } else { + acc + } + } + _ => acc, + }, + ); + (is_present, compile_attribute) + }) + .collect::>(); + if !attributes.iter().any(|(present, _)| *present) { + // Report on first compile attribute only + if let Some((_, compile_attribute)) = attributes.iter().next() { + let range = compile_attribute + .form_id + .get_ast(sema.db, file_id) + .syntax() + .text_range(); + report_diagnostic(sema, Some(range), file_id, diags) + } + } +} + +lazy_static! { + static ref MISSING_SPEC_OPTIONS: FxHashSet = { + let mut res = FxHashSet::default(); + for name in vec![ + known::warn_missing_spec, + known::nowarn_missing_spec, + known::warn_missing_spec_all, + known::nowarn_missing_spec_all, + ] { + res.insert(name); + } + res + }; +} + +fn is_in_src_dir(db: &dyn SourceDatabase, file_id: FileId) -> Option { + let root_id = db.file_source_root(file_id); + let root = db.source_root(root_id); + let path = root.path_for_file(&file_id)?.as_path()?.as_ref().to_str()?; + Some(is_srcdir_path(path)) +} + +fn is_srcdir_path(s: &str) -> bool { + lazy_static! { + static ref RE: Regex = Regex::new(r"^.*/erl/[^/]+/src/.*\.erl$").unwrap(); + } + RE.is_match(s) +} + +fn report_diagnostic( + sema: &Semantic, + range: Option, + file_id: FileId, + diags: &mut Vec, +) { + let range = range.unwrap_or(TextRange::empty(0.into())); + + let mut builder = SourceChangeBuilder::new(file_id); + add_compile_option(sema, file_id, "warn_missing_spec", None, &mut builder); + let edit = builder.finish(); + let d = Diagnostic::new( + crate::diagnostics::DiagnosticCode::MissingCompileWarnMissingSpec, + format!( + "Please add \"-compile(warn_missing_spec).\" or \"-compile(warn_missing_spec_all).\" to the module. If exported functions are not all specced, they need to be specced." + ), + range, + ).with_fixes(Some(vec![fix("add_warn_missing_spec", + "Add compile option 'warn_missing_spec'", + edit, range)])); + diags.push(d); +} + +#[cfg(test)] +mod tests { + + use crate::diagnostics::DiagnosticsConfig; + use crate::tests::check_diagnostics_with_config; + use crate::tests::check_fix_with_config; + + #[track_caller] + pub(crate) fn check_fix(fixture_before: &str, fixture_after: &str) { + let config = DiagnosticsConfig::default(); + check_fix_with_config(config, fixture_before, fixture_after) + } + + #[track_caller] + pub(crate) fn check_diagnostics(fixture: &str) { + let config = DiagnosticsConfig::default(); + check_diagnostics_with_config(config, fixture) + } + + #[test] + fn no_compile_attribute() { + check_diagnostics( + r#" + //- /erl/my_app/src/main.erl + %% <<< 💡 error: Please add "-compile(warn_missing_spec)." or "-compile(warn_missing_spec_all)." to the module. If exported functions are not all specced, they need to be specced. + + -module(main). + + "#, + ) + } + + #[test] + fn compile_attribute_missing_setting() { + check_diagnostics( + r#" + //- /erl/my_app/src/main.erl + -module(main). + + -compile([export_all, nowarn_export_all]). + %% ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 💡 error: Please add "-compile(warn_missing_spec)." or "-compile(warn_missing_spec_all)." to the module. If exported functions are not all specced, they need to be specced. + + "#, + ) + } + + #[test] + fn warn_missing_spec_ok() { + check_diagnostics( + r#" + //- /erl/my_app/src/main.erl + -module(main). + + -compile(warn_missing_spec). + + "#, + ) + } + + #[test] + fn nowarn_missing_spec_ok() { + check_diagnostics( + r#" + //- /erl/my_app/src/main.erl + -module(main). + + -compile(nowarn_missing_spec). + + "#, + ) + } + + #[test] + fn warn_missing_spec_all_ok() { + check_diagnostics( + r#" + //- /erl/my_app/src/main.erl + -module(main). + + -compile(warn_missing_spec_all). + + "#, + ) + } + + #[test] + fn nowarn_missing_spec_all_ok() { + check_diagnostics( + r#" + //- /erl/my_app/src/main.erl + -module(main). + + -compile(nowarn_missing_spec_all). + + "#, + ) + } + + #[test] + fn more_than_one_compile_attribute_1() { + check_diagnostics( + r#" + //- /erl/my_app/src/main.erl + -module(main). + + -compile(warn_missing_spec). + -compile([export_all, nowarn_export_all]). + "#, + ) + } + + #[test] + fn more_than_one_compile_attribute_2() { + check_diagnostics( + r#" + //- /erl/my_app/src/main.erl + -module(main). + + -compile(export_all). + %% ^^^^^^^^^^^^^^^^^^^^^ 💡 error: Please add "-compile(warn_missing_spec)." or "-compile(warn_missing_spec_all)." to the module. If exported functions are not all specced, they need to be specced. + -compile(nowarn_export_all). + "#, + ) + } + + #[test] + fn more_than_one_compile_attribute_3() { + check_diagnostics( + r#" + //- /erl/my_app/src/main.erl + -module(main). + -compile({nowarn_deprecated_function, {erlang,get_stacktrace,0}}). + -compile([ + warn_missing_spec_all, + export_all, + nowarn_export_all + ]). + + "#, + ) + } + + #[test] + fn not_in_generated_file() { + check_diagnostics( + r#" + //- /erl/my_app/src/main.erl + %% -*- coding: utf-8 -*- + %% Automatically generated, do not edit + %% @generated from blah + %% To generate, see targets and instructions in local Makefile + %% Version source: git + -module(main). + -eqwalizer(ignore). + + "#, + ) + } + + #[test] + fn not_in_test_or_extra_file() { + check_diagnostics( + r#" + //- /erl/my_app/test/my_SUITE.erl extra:test + -module(my_SUITE). + -export([all/0]). + -export([a/1]). + all() -> [a]. + a(_Config) -> + ok. + "#, + ) + } + + #[test] + fn applies_fix_no_attribute() { + check_fix( + r#" + //- /erl/my_app/src/main.erl + ~-module(main). + + %% a comment"#, + r#" + -module(main). + + -compile([warn_missing_spec]). + + %% a comment"#, + ); + } + + #[test] + fn applies_fix_existing_attribute_list() { + check_fix( + r#" + //- /erl/my_app/src/main.erl + -module(main). + + -c~ompile([export_all, nowarn_export_all]). + + "#, + r#" + -module(main). + + -compile([export_all, nowarn_export_all, warn_missing_spec]). + + "#, + ); + } + + #[test] + fn applies_fix_existing_attribute_atom() { + check_fix( + r#" + //- /erl/my_app/src/main.erl + -module(main). + + -c~ompile(export_all). + + "#, + r#" + -module(main). + + -compile([export_all, warn_missing_spec]). + + "#, + ); + } + + #[test] + fn applies_fix_existing_attribute_tuple() { + check_fix( + r#" + //- /erl/my_app/src/main.erl + -module(main). + + -c~ompile({foo, bar}). + + "#, + r#" + -module(main). + + -compile([{foo, bar}, warn_missing_spec]). + + "#, + ); + } +} diff --git a/crates/ide/src/diagnostics/misspelled_attribute.rs b/crates/ide/src/diagnostics/misspelled_attribute.rs new file mode 100644 index 0000000000..e6073e912c --- /dev/null +++ b/crates/ide/src/diagnostics/misspelled_attribute.rs @@ -0,0 +1,225 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_ide_db::elp_base_db::FileId; +use elp_ide_db::source_change::SourceChange; +use elp_syntax::ast::AstNode; +use elp_syntax::ast::WildAttribute; +use hir::db::MinDefDatabase; +use hir::Attribute; +use text_edit::TextEdit; + +use super::Diagnostic; +use crate::diagnostics::RelatedInformation; +use crate::fix; +use crate::TextRange; +use crate::TextSize; + +// Diagnostic: misspelled_attribute +// +// Finds attributes with names similar to "known" attributes, +// and suggests replacing them +// +// ``` +// -include_lob("/foo/bar/baz.hrl"). +// ``` +// -> +// ``` +// -include_lib("/foo/bar/baz.hrl"). +// ``` +pub(crate) fn misspelled_attribute( + diagnostics: &mut Vec, + db: &dyn MinDefDatabase, + file_id: FileId, +) { + let form_list = db.file_form_list(file_id); + let potential_misspellings = form_list.attributes().filter_map(|(id, attr)| { + looks_like_misspelling(attr).map(|suggested_rename| (id, attr, suggested_rename)) + }); + potential_misspellings.for_each(|(_id, attr, suggested_rename)| { + let parsed_file = db.parse(file_id); + let attr_form = attr.form_id.get(&parsed_file.tree()); + diagnostics.push(make_diagnostic(file_id, attr, attr_form, suggested_rename)) + }) +} + +const KNOWN_ATTRIBUTES: &[&str] = &[ + "include_lib", + "export_type", + "behaviour", + "callback", + "dialyzer", + "behavior", + "on_load", + "include", + "feature", + "compile", + "record", + "oncall", + "module", + "import", + "export", + "define", + "opaque", + "typing", + "author", + "type", + "spec", + "nifs", + "file", + "vsn", +]; + +fn looks_like_misspelling(attr: &Attribute) -> Option<&str> { + let mut suggestions: Vec<(&str, f64)> = KNOWN_ATTRIBUTES + .iter() + .filter(|&known| &attr.name != known) + .filter(|&known| { + let close_enough: usize = std::cmp::max(1, std::cmp::min(3, attr.name.len() / 3)); + triple_accel::levenshtein::rdamerau(attr.name.as_str().as_bytes(), known.as_bytes()) + <= u32::try_from(close_enough).unwrap() + }) + .map(|&known| (known, strsim::jaro_winkler(&attr.name, known))) + .collect::>(); + suggestions.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap()); + suggestions + .first() + .map(|(suggestion, _similarity)| suggestion) + .copied() +} + +fn make_diagnostic( + file_id: FileId, + attr: &Attribute, + attr_form: WildAttribute, + suggested_rename: &str, +) -> Diagnostic { + // Includes the '-', e.g. "-dialyzer" from `-dialyzer([]).`, but we don't + // want to apply another `.name()`, because for attributes with special + // meanings like `-record(foo, ...).` we would get "foo" + let attr_name_range_with_hyphen = attr_form.name().unwrap().syntax().text_range(); + // Temporary for T148094436 + let _pctx = stdx::panic_context::enter(format!("\nmisspelled_attribute::make_diagnostic")); + let attr_name_range = TextRange::new( + attr_name_range_with_hyphen + .start() + .checked_add(TextSize::of('-').into()) + .unwrap(), + attr_name_range_with_hyphen.end(), + ); + + let edit = TextEdit::replace(attr_name_range, suggested_rename.to_string()); + + Diagnostic::new( + super::DiagnosticCode::MisspelledAttribute, + format!( + "misspelled attribute, saw '{}' but expected '{}'", + attr.name, suggested_rename + ), + attr_name_range, + ) + .with_related(Some(vec![RelatedInformation { + range: attr_name_range, + message: "Misspelled attribute".to_string(), + }])) + .with_fixes(Some(vec![fix( + "fix_misspelled_attribute", + format!("Change misspelled attribute to '{}'", suggested_rename).as_str(), + SourceChange::from_text_edit(file_id, edit), + attr_name_range, + )])) +} + +// To run the tests via cargo +// cargo test --package elp_ide --lib +#[cfg(test)] +mod tests { + use fxhash::FxHashSet; + + use crate::diagnostics::DiagnosticCode; + use crate::diagnostics::DiagnosticsConfig; + use crate::fixture; + use crate::tests::check_diagnostics; + use crate::tests::check_fix; + + #[test] + fn test_can_find_and_fix_misspelling() { + check_diagnostics( + r#" + -module(main). + -dyalizer({nowarn_function, f/0}). + %%% ^^^^^^^^ 💡 error: misspelled attribute, saw 'dyalizer' but expected 'dialyzer' + "#, + ); + check_fix( + r#" + -module(main). + -dyalize~r({nowarn_function, f/0}). + "#, + r#" + -module(main). + -dialyzer({nowarn_function, f/0}). + "#, + ); + } + + #[test] + fn test_can_ignore_valid_spelling() { + let (analysis, position, _) = fixture::annotations( + r#" + -module(main). + -di~alyzer({nowarn_function, f/0}). + "#, + ); + let mut config = DiagnosticsConfig { + disable_experimental: true, + disabled: FxHashSet::default(), + adhoc_semantic_diagnostics: vec![], + }; + config + .disabled + .insert(DiagnosticCode::MissingCompileWarnMissingSpec); + let diags = analysis + .diagnostics(&config, position.file_id, true) + .unwrap(); + assert!( + diags.is_empty(), + "didn't expect diagnostic errors in files: {:?}", + diags + ); + } + + #[test] + fn test_does_not_consider_the_names_of_records() { + let (analysis, position, _) = fixture::annotations( + r#" + -module(main). + -re~cord(dyalizer, {field = "foo"}). + + f(#dyalizer{field = Bar}) -> Bar. + "#, + ); + let mut config = DiagnosticsConfig { + disable_experimental: true, + disabled: FxHashSet::default(), + adhoc_semantic_diagnostics: vec![], + }; + config + .disabled + .insert(DiagnosticCode::MissingCompileWarnMissingSpec); + let diags = analysis + .diagnostics(&config, position.file_id, true) + .unwrap(); + assert!( + diags.is_empty(), + "didn't expect diagnostic errors in files: {:?}", + diags + ); + } +} diff --git a/crates/ide/src/diagnostics/module_mismatch.rs b/crates/ide/src/diagnostics/module_mismatch.rs new file mode 100644 index 0000000000..22b1c69fd1 --- /dev/null +++ b/crates/ide/src/diagnostics/module_mismatch.rs @@ -0,0 +1,101 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +// Diagnostic: module-mismatch +// +// Diagnostic for mismatches between the module attribute name and the path of the given file + +use elp_ide_assists::Assist; +use elp_ide_db::elp_base_db::FileId; +use elp_ide_db::elp_base_db::SourceDatabase; +use elp_ide_db::source_change::SourceChange; +use elp_ide_db::RootDatabase; +use elp_syntax::ast; +use elp_syntax::AstNode; +use elp_syntax::SyntaxNode; +use elp_syntax::TextRange; +use text_edit::TextEdit; + +use crate::fix; +use crate::Diagnostic; + +pub(crate) fn module_mismatch( + acc: &mut Vec, + db: &RootDatabase, + file_id: FileId, + node: &SyntaxNode, +) -> Option<()> { + let module_name = ast::ModuleAttribute::cast(node.clone())?.name()?; + let root_id = db.file_source_root(file_id); + let root = db.source_root(root_id); + let path = root.path_for_file(&file_id).unwrap(); + let filename = path.name_and_extension().unwrap_or_default().0; + let loc = module_name.syntax().text_range(); + if module_name.text()? != filename { + let d = Diagnostic::new( + crate::diagnostics::DiagnosticCode::ModuleMismatch, + format!("Module name ({module_name}) does not match file name ({filename})"), + loc, + ) + .with_fixes(Some(vec![rename_module_to_match_filename( + file_id, loc, filename, + )])); + acc.push(d); + }; + Some(()) +} + +fn rename_module_to_match_filename(file_id: FileId, loc: TextRange, filename: &str) -> Assist { + let mut builder = TextEdit::builder(); + builder.replace(loc, filename.to_string()); + let edit = builder.finish(); + fix( + "rename_module_to_match_filename", + &format!("Rename module to: {filename}"), + SourceChange::from_text_edit(file_id, edit), + loc, + ) +} + +#[cfg(test)] +mod tests { + + use crate::tests::check_diagnostics; + use crate::tests::check_fix; + + #[test] + fn test_module_mismatch() { + check_diagnostics( + r#" +//- /src/foo.erl +-module(bar). +%% ^^^ 💡 error: Module name (bar) does not match file name (foo) +"#, + ); + check_fix( + r#" +//- /src/foo.erl +-module(b~ar). +"#, + r#" +-module(foo). +"#, + ) + } + + #[test] + fn test_module_mismatch_correct() { + check_diagnostics( + r#" +//- /src/foo.erl +-module(foo). + "#, + ); + } +} diff --git a/crates/ide/src/diagnostics/mutable_variable.rs b/crates/ide/src/diagnostics/mutable_variable.rs new file mode 100644 index 0000000000..5351f69326 --- /dev/null +++ b/crates/ide/src/diagnostics/mutable_variable.rs @@ -0,0 +1,123 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +// Diagnostic: mutable-variable +// +// Diagnostic for detecting OTP mutable variable bug +// https://github.com/erlang/otp/issues/6873 +// +// We are looking for a chain of match expressions where the +// constituent elements are already bound. +// +// ```erlang +// test() -> +// Zero = 0, +// One = 1, +// +// Result = One = Zero, +// ^^^^^^^^^^^^^^^^^^^ +// ``` +// + +use elp_ide_db::elp_base_db::FileId; +use fxhash::FxHashMap; +use fxhash::FxHashSet; +use hir::Expr; +use hir::FunctionId; +use hir::PatId; +use hir::Semantic; + +use crate::diagnostics::DiagnosticCode; +use crate::Diagnostic; + +pub(crate) fn mutable_variable_bug( + diags: &mut Vec, + sema: &Semantic, + file_id: FileId, +) -> Option<()> { + let mut bound_vars_by_function: FxHashMap> = FxHashMap::default(); + let bound_vars = sema.bound_vars_in_pattern_diagnostic(file_id); + bound_vars.iter().for_each(|(function_id, pat_id, _var)| { + bound_vars_by_function + .entry(function_id.value) + .and_modify(|vars| { + vars.insert(pat_id); + }) + .or_insert_with(|| { + let mut vars = FxHashSet::default(); + vars.insert(pat_id); + vars + }); + }); + sema.def_map(file_id) + .get_functions() + .iter() + .for_each(|(_arity, def)| { + if def.file.file_id == file_id { + if let Some(bound_vars) = bound_vars_by_function.get(&def.function_id) { + let def_fb = def.in_function_body(sema.db, def); + def_fb.fold_function( + (), + &mut |acc, _clause_id, ctx| { + match ctx.expr { + Expr::Match { lhs: _, rhs } => match &def_fb[rhs] { + Expr::Match { lhs, rhs: _ } => { + if bound_vars.contains(lhs) { + if let Some(range) = + def_fb.range_for_expr(sema.db, ctx.expr_id) + { + diags.push(Diagnostic::new( + DiagnosticCode::MutableVarBug, + "Possible mutable variable bug", + range, + )); + } + } + } + _ => {} + }, + _ => {} + }; + acc + }, + &mut |acc, _, _| acc, + ); + } + } + }); + + Some(()) +} + +#[cfg(test)] +mod tests { + + use crate::tests::check_diagnostics; + + #[test] + fn mutable_variable_1() { + check_diagnostics( + r#" +//- /src/test.erl +-module(test). + +-export([test/0]). + +test() -> + Zero = 0, + One = 1, + + Result = One = Zero, +%% ^^^^^^^^^^^^^^^^^^^ error: Possible mutable variable bug + + Result. +"#, + ); + } +} diff --git a/crates/ide/src/diagnostics/redundant_assignment.rs b/crates/ide/src/diagnostics/redundant_assignment.rs new file mode 100644 index 0000000000..bdfd5c9c32 --- /dev/null +++ b/crates/ide/src/diagnostics/redundant_assignment.rs @@ -0,0 +1,195 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! Lint/fix: redundant_assignment +//! +//! Return a diagnostic whenever we have A = B, with A unbound, and offer to inline +//! A as a fix. +//! + +use elp_ide_db::elp_base_db::FileId; +use elp_ide_db::source_change::SourceChange; +use elp_syntax::ast; +use hir::BodySourceMap; +use hir::Expr; +use hir::ExprId; +use hir::FunctionDef; +use hir::InFile; +use hir::InFunctionBody; +use hir::Pat; +use hir::PatId; +use hir::Semantic; + +use super::Diagnostic; +use super::Severity; +use crate::codemod_helpers::check_is_only_place_where_var_is_defined; +use crate::codemod_helpers::check_var_has_references; +use crate::diagnostics::DiagnosticCode; +use crate::fix; + +pub(crate) fn redundant_assignment(diags: &mut Vec, sema: &Semantic, file_id: FileId) { + sema.def_map(file_id) + .get_functions() + .iter() + .for_each(|(_arity, def)| { + if def.file.file_id == file_id { + process_matches(diags, sema, def) + } + }); +} + +fn process_matches(diags: &mut Vec, sema: &Semantic, def: &FunctionDef) { + let mut def_fb = def.in_function_body(sema.db, def); + def_fb.clone().fold_function( + (), + &mut |_acc, _, ctx| match ctx.expr { + Expr::Match { lhs, rhs } => match &def_fb[lhs] { + Pat::Var(_) => match &def_fb[rhs] { + Expr::Var(_) => { + let cloned_lhs = lhs.clone(); + let cloned_rhs = rhs.clone(); + if let Some(diag) = is_var_assignment_to_unused_var( + &sema, + &mut def_fb, + def.file.file_id, + ctx.expr_id, + cloned_lhs, + cloned_rhs, + ) { + diags.push(diag); + } + } + + _ => {} + }, + + _ => (), + }, + _ => (), + }, + &mut |_acc, _, _| (), + ); +} + +fn is_var_assignment_to_unused_var( + sema: &Semantic, + def_fb: &mut InFunctionBody<&FunctionDef>, + file_id: FileId, + expr_id: ExprId, + lhs: PatId, + rhs: ExprId, +) -> Option { + let source_file = sema.parse(file_id); + let body_map = def_fb.get_body_map(sema.db); + + let rhs_name = body_map.expr(rhs)?.to_node(&source_file)?.to_string(); + + let renamings = try_rename_usages(&sema, &body_map, &source_file, lhs, rhs_name)?; + + let range = def_fb.range_for_expr(sema.db, expr_id)?; + + let diag = Diagnostic::new( + DiagnosticCode::RedundantAssignment, + "assignment is redundant", + range, + ) + .severity(Severity::WeakWarning) + .with_fixes(Some(vec![fix( + "remove_redundant_assignment", + "Use right-hand of assignment everywhere", + renamings, + range, + )])); + + Some(diag) +} + +fn try_rename_usages( + sema: &Semantic, + body_map: &BodySourceMap, + source_file: &InFile, + pat_id: PatId, + new_name: String, +) -> Option { + let infile_ast_ptr = body_map.pat(pat_id)?; + let ast_node = infile_ast_ptr.to_node(&source_file)?; + if let ast::Expr::ExprMax(ast::ExprMax::Var(ast_var)) = ast_node { + let infile_ast_var = InFile::new(source_file.file_id, &ast_var); + let def = sema.to_def(infile_ast_var)?; + + let () = check_is_only_place_where_var_is_defined(sema, infile_ast_var)?; + let () = check_var_has_references(sema, infile_ast_var)?; // otherwise covered by trivial-match + + if let hir::DefinitionOrReference::Definition(var_def) = def { + let sym_def = elp_ide_db::SymbolDefinition::Var(var_def); + return sym_def + .rename( + &sema, + &|_| new_name.clone(), + elp_ide_db::rename::SafetyChecks::No, + ) + .ok(); + } + } + None +} + +#[cfg(test)] +mod tests { + + use crate::tests::check_diagnostics; + use crate::tests::check_fix; + + #[test] + fn can_fix_lhs_is_var() { + check_fix( + r#" + -module(main). + + do_foo() -> + X = 42, + ~Y = X, + bar(Y), + Y. + "#, + r#" + -module(main). + + do_foo() -> + X = 42, + X = X, + bar(X), + X. + "#, + ) + } + + #[test] + fn produces_diagnostic_lhs_is_var() { + check_diagnostics( + r#" + -module(main). + + do_foo() -> + X = 42, + Y = X, + %%% ^^^^^ 💡 weak: assignment is redundant + bar(Y), + Z = Y, + %%% ^^^^^ 💡 weak: assignment is redundant + g(Z), + case Y of + [A] -> C = A; + B -> C = B + end, + C. + "#, + ) + } +} diff --git a/crates/ide/src/diagnostics/replace_call.rs b/crates/ide/src/diagnostics/replace_call.rs new file mode 100644 index 0000000000..d62f6c8641 --- /dev/null +++ b/crates/ide/src/diagnostics/replace_call.rs @@ -0,0 +1,598 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! Lint/fix: replace_call +//! +//! Return a diagnostic if a given (noop) function is used, +//! and a fix to replace it by something else. +//! + +use elp_ide_db::elp_base_db::FileId; +use elp_ide_db::source_change::SourceChange; +use elp_syntax::ast; +use elp_syntax::TextRange; +use hir::Expr; +use hir::ExprId; +use hir::FunctionDef; +use hir::InFunctionBody; +use hir::Literal; +use hir::Semantic; +use text_edit::TextEdit; + +use super::Diagnostic; +use super::Severity; +use crate::codemod_helpers::find_call_in_function; +use crate::codemod_helpers::statement_range; +use crate::codemod_helpers::CheckCall; +use crate::codemod_helpers::FunctionMatch; +use crate::codemod_helpers::MFA; +use crate::diagnostics::DiagnosticCode; +use crate::fix; + +#[allow(dead_code)] +pub fn replace_call_site( + mfa: &FunctionMatch, + replacement: Replacement, + acc: &mut Vec, + sema: &Semantic, + file_id: FileId, +) { + replace_call_site_if_args_match( + mfa, + &|_mfa, _, _target, _args, _def_fb| Some("".to_string()), + replacement, + acc, + sema, + file_id, + ) +} + +pub fn replace_call_site_if_args_match( + fm: &FunctionMatch, + args_match: CheckCall<()>, + replacement: Replacement, + acc: &mut Vec, + sema: &Semantic, + file_id: FileId, +) { + sema.def_map(file_id) + .get_functions() + .iter() + .for_each(|(_arity, def)| { + if def.file.file_id == file_id { + find_call_in_function( + acc, + sema, + def, + &vec![(fm, ())], + &args_match, + move |sema, mut def_fb, target, args, extra_info, range| { + let mfa_str = target + .label(args.len() as u32, sema, &def_fb.body())? + .to_string(); + let sep = if extra_info.len() > 0 { " " } else { "" }; + let diag = Diagnostic::new( + DiagnosticCode::AdHoc(mfa_str.clone()), + format!("'{}' called{}{}", &mfa_str, &sep, &extra_info), + range.clone(), + ) + .severity(Severity::WeakWarning) + .experimental(); + if let Some(edit) = + replace_call(replacement, sema, &mut def_fb, file_id, args, &range) + { + Some(diag.with_fixes(Some(vec![fix( + "replace_call_site", + &format!("Replace call to '{:?}'", &mfa_str), + SourceChange::from_text_edit(file_id, edit), + range, + )]))) + } else { + Some(diag) + } + }, + ); + } + }); +} + +#[allow(dead_code)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Replacement { + UseOk, + UseCallArg(u32), +} + +fn replace_call( + replacement: Replacement, + sema: &Semantic, + def_fb: &mut InFunctionBody<&FunctionDef>, + file_id: FileId, + args: &[ExprId], + call_loc: &TextRange, +) -> Option { + let opt_replacement_str = match replacement { + Replacement::UseOk => Some("ok".to_string()), + Replacement::UseCallArg(n) => { + let &nth = args.get(n as usize)?; + + let body_map = def_fb.get_body_map(sema.db); + let source_file = sema.parse(file_id); + + let nth_str = body_map.expr(nth)?.to_node(&source_file)?.to_string(); + Some(nth_str) + } + }; + opt_replacement_str.map(|replacement_str| { + let mut edit_builder = TextEdit::builder(); + edit_builder.replace(*call_loc, replacement_str); + edit_builder.finish() + }) +} + +#[allow(dead_code)] +pub fn remove_fun_ref_from_list( + mfa: &MFA, + diags: &mut Vec, + sema: &Semantic, + file_id: FileId, +) { + let mfa_str = &mfa.label(); + sema.def_map(file_id) + .get_functions() + .iter() + .for_each(|(_arity, def)| { + if def.file.file_id == file_id { + let def_fb = def.in_function_body(sema.db, def); + def_fb.clone().fold_function( + (), + &mut |_acc, _, ctx| { + let matches = + match_fun_ref_in_list_in_call_arg(mfa, sema, &def_fb, &ctx.expr_id); + matches.iter().for_each(|matched_funref_id| { + if let Some(range) = + def_fb.clone().range_for_expr(sema.db, *matched_funref_id) + { + let body_map = def_fb.clone().get_body_map(sema.db); + + if let Some(in_file_ast_ptr) = body_map.expr(*matched_funref_id) { + let source_file = sema.parse(file_id); + if let Some(list_elem_ast) = + in_file_ast_ptr.to_node(&source_file) + { + if let Some(statement_removal) = + remove_statement(&list_elem_ast) + { + let diag = Diagnostic::new( + DiagnosticCode::AdHoc(mfa_str.into()), + format!("'{}' ref found", &mfa_str), + range.clone(), + ) + .severity(Severity::WeakWarning) + .experimental() + .with_fixes(Some(vec![fix( + "remove_fun_ref_from_list", + "Remove noop fun ref from list", + SourceChange::from_text_edit( + file_id, + statement_removal, + ), + range, + )])); + diags.push(diag); + } + } + } + } + }); + }, + &mut |acc, _, _| acc, + ); + } + }) +} + +fn match_fun_ref_in_list_in_call_arg( + funref_mfa: &MFA, + sema: &Semantic, + def_fb: &InFunctionBody<&FunctionDef>, + expr_id: &ExprId, +) -> Vec { + let mut result = vec![]; + let expr = &def_fb[*expr_id]; + match expr { + Expr::Call { args, .. } => args.iter().for_each(|arg_id| { + let arg = &def_fb[*arg_id]; + match arg { + Expr::List { exprs, .. } => exprs.iter().for_each(|list_elem_id| { + let list_elem = &def_fb[*list_elem_id]; + match list_elem { + Expr::CaptureFun { + target, + arity: arity_expr_id, + } => { + if let Expr::Literal(Literal::Integer(arity)) = &def_fb[*arity_expr_id] + { + let target_label = + target.label(*arity as u32, sema, &def_fb.body()); + let funref_label = &funref_mfa.label(); + if target_label == Some(funref_label.into()) { + result.push(*list_elem_id); + } + } + } + _ => {} + } + }), + _ => {} + } + }), + _ => {} + } + return result; +} + +fn remove_statement(expr: &ast::Expr) -> Option { + let range = statement_range(expr); + + let mut edit_builder = TextEdit::builder(); + edit_builder.delete(range); + Some(edit_builder.finish()) +} + +#[cfg(test)] +mod tests { + + use hir::Expr; + use hir::Literal; + + use super::*; + use crate::tests::check_diagnostics_with_config; + use crate::tests::check_fix_with_config; + use crate::DiagnosticsConfig; + + #[test] + fn check_fix_remove_call_use_ok() { + let mut config = DiagnosticsConfig { + adhoc_semantic_diagnostics: vec![&|acc, sema, file_id, _ext| { + replace_call_site( + &FunctionMatch::MFA(MFA { + module: "foo".into(), + name: "fire_bombs".into(), + arity: 1, + }), + Replacement::UseOk, + acc, + sema, + file_id, + ) + }], + ..DiagnosticsConfig::default() + }; + config + .disabled + .insert(DiagnosticCode::MissingCompileWarnMissingSpec); + + check_fix_with_config( + config, + r#" + //- /src/blah_SUITE.erl + -module(blah_SUITE). + + end_per_suite(Config) -> + ~foo:fire_bombs(Config). + //- /src/foo.erl + -module(foo). + fire_bombs(Config) -> + boom. + "#, + r#" + -module(blah_SUITE). + + end_per_suite(Config) -> + ok. + "#, + ) + } + + #[test] + fn check_fix_remove_call_use_call_args() { + let mut config = DiagnosticsConfig { + adhoc_semantic_diagnostics: vec![&|acc, sema, file_id, _ext| { + replace_call_site( + &FunctionMatch::MFA(MFA { + module: "foo".into(), + name: "fire_bombs".into(), + arity: 2, + }), + Replacement::UseCallArg(1), + acc, + sema, + file_id, + ) + }], + ..DiagnosticsConfig::default() + }; + config + .disabled + .insert(DiagnosticCode::MissingCompileWarnMissingSpec); + + check_fix_with_config( + config, + r#" + //- /src/blah_SUITE.erl + -module(blah_SUITE). + + end_per_suite(Config) -> + Message = ~foo:fire_bombs(Config, qwerty), + transmit(Message). + //- /src/foo.erl + -module(foo). + fire_bombs(Config, MissilesCode) -> + boom. + "#, + r#" + -module(blah_SUITE). + + end_per_suite(Config) -> + Message = qwerty, + transmit(Message). + "#, + ) + } + + #[test] + fn check_remove_call_site_if_args_match_uses_match() { + let mut config = DiagnosticsConfig { + adhoc_semantic_diagnostics: vec![&|acc, sema, file_id, _ext| { + replace_call_site_if_args_match( + &FunctionMatch::MFA(MFA { + module: "foo".into(), + name: "fire_bombs".into(), + arity: 2, + }), + &|_, _, _target, args, def_fb| match &def_fb[args[1]] { + Expr::Literal(Literal::Integer(42)) => Some("with 42".to_string()), + _ => None, + }, + Replacement::UseOk, + acc, + sema, + file_id, + ) + }], + ..DiagnosticsConfig::default() + }; + config + .disabled + .insert(DiagnosticCode::MissingCompileWarnMissingSpec); + + check_diagnostics_with_config( + config, + r#" + //- /src/blah_SUITE.erl + -module(blah_SUITE). + + end_per_suite(Config) -> + foo:fire_bombs(Config, 44), + foo:fire_bombs(Config, 43), + foo:fire_bombs(Config, 42), + %%% ^^^^^^^^^^^^^^^^^^^^^^^^^^ 💡 weak: 'foo:fire_bombs/2' called with 42 + foo:fire_bombs(Config, 41), + foo:fire_bombs(Config, 40). + //- /src/foo.erl + -module(foo). + "#, + ) + } + + #[test] + fn check_fix_remove_fun_ref_from_list_first() { + let mut config = DiagnosticsConfig { + adhoc_semantic_diagnostics: vec![&|acc, sema, file_id, _ext| { + remove_fun_ref_from_list( + &MFA { + module: "foo".into(), + name: "fire_bombs".into(), + arity: 1, + }, + acc, + sema, + file_id, + ) + }], + ..DiagnosticsConfig::default() + }; + config + .disabled + .insert(DiagnosticCode::MissingCompileWarnMissingSpec); + + check_fix_with_config( + config, + r#" + //- /src/blah_SUITE.erl + -module(blah_SUITE). + + end_per_suite(Config) -> + some:clearner_helper([ + f~un foo:fire_bombs/1, + fun foo:regret_it/1, + fun foo:too_late_now_think_twice_next_time/2 + ], Config). + //- /src/foo.erl + -module(foo). + -export([fire_bombs/1, regret_it/1, too_late_now_think_twice_next_time/2]). + fire_bombs(Config) -> boom. + regret_it(Config) -> oops. + too_late_now_think_twice_next_time(Config) -> 'oh well'. + "#, + r#" + -module(blah_SUITE). + + end_per_suite(Config) -> + some:clearner_helper([ + fun foo:regret_it/1, + fun foo:too_late_now_think_twice_next_time/2 + ], Config). + "#, + ); + } + + #[test] + fn check_fix_remove_fun_ref_from_list_middle() { + let mut config = DiagnosticsConfig { + adhoc_semantic_diagnostics: vec![&|acc, sema, file_id, _ext| { + remove_fun_ref_from_list( + &MFA { + module: "foo".into(), + name: "fire_bombs".into(), + arity: 1, + }, + acc, + sema, + file_id, + ) + }], + ..DiagnosticsConfig::default() + }; + config + .disabled + .insert(DiagnosticCode::MissingCompileWarnMissingSpec); + + check_fix_with_config( + config, + r#" + //- /src/blah_SUITE.erl + -module(blah_SUITE). + + end_per_suite(Config) -> + some:clearner_helper([ + fun foo:regret_it/1, + f~un foo:fire_bombs/1, + fun foo:too_late_now_think_twice_next_time/2 + ], Config). + //- /src/foo.erl + -module(foo). + -export([fire_bombs/1, regret_it/1, too_late_now_think_twice_next_time/2]). + fire_bombs(Config) -> boom. + regret_it(Config) -> oops. + too_late_now_think_twice_next_time(Config) -> 'oh well'. + "#, + r#" + -module(blah_SUITE). + + end_per_suite(Config) -> + some:clearner_helper([ + fun foo:regret_it/1, + fun foo:too_late_now_think_twice_next_time/2 + ], Config). + "#, + ); + } + + #[test] + fn check_fix_remove_fun_ref_from_list_last() { + let mut config = DiagnosticsConfig { + adhoc_semantic_diagnostics: vec![&|acc, sema, file_id, _ext| { + remove_fun_ref_from_list( + &MFA { + module: "foo".into(), + name: "fire_bombs".into(), + arity: 1, + }, + acc, + sema, + file_id, + ) + }], + ..DiagnosticsConfig::default() + }; + config + .disabled + .insert(DiagnosticCode::MissingCompileWarnMissingSpec); + + check_fix_with_config( + config, + r#" + //- /src/blah_SUITE.erl + -module(blah_SUITE). + + end_per_suite(Config) -> + some:clearner_helper([ + fun foo:regret_it/1, + fun foo:too_late_now_think_twice_next_time/2, + f~un foo:fire_bombs/1 + ], Config). + //- /src/foo.erl + -module(foo). + -export([fire_bombs/1, regret_it/1, too_late_now_think_twice_next_time/2]). + fire_bombs(Config) -> boom. + regret_it(Config) -> oops. + too_late_now_think_twice_next_time(Config) -> 'oh well'. + "#, + r#" + -module(blah_SUITE). + + end_per_suite(Config) -> + some:clearner_helper([ + fun foo:regret_it/1, + fun foo:too_late_now_think_twice_next_time/2 + ], Config). + "#, + ); + } + + #[test] + fn check_fix_remove_fun_ref_from_list_singleton() { + let mut config = DiagnosticsConfig { + adhoc_semantic_diagnostics: vec![&|acc, sema, file_id, _ext| { + remove_fun_ref_from_list( + &MFA { + module: "foo".into(), + name: "fire_bombs".into(), + arity: 1, + }, + acc, + sema, + file_id, + ) + }], + ..DiagnosticsConfig::default() + }; + + config + .disabled + .insert(DiagnosticCode::MissingCompileWarnMissingSpec); + + check_fix_with_config( + config, + r#" + //- /src/blah_SUITE.erl + -module(blah_SUITE). + + end_per_suite(Config) -> + some:clearner_helper([ + f~un foo:fire_bombs/1 + ], Config). + //- /src/foo.erl + -module(foo). + -export([fire_bombs/1, regret_it/1, too_late_now_think_twice_next_time/2]). + fire_bombs(Config) -> boom. + regret_it(Config) -> oops. + too_late_now_think_twice_next_time(Config) -> 'oh well'. + "#, + r#" + -module(blah_SUITE). + + end_per_suite(Config) -> + some:clearner_helper([], Config). + "#, + ); + } +} diff --git a/crates/ide/src/diagnostics/trivial_match.rs b/crates/ide/src/diagnostics/trivial_match.rs new file mode 100644 index 0000000000..0a75facf1c --- /dev/null +++ b/crates/ide/src/diagnostics/trivial_match.rs @@ -0,0 +1,471 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! Lint/fix: trivial_match +//! +//! Return a diagnostic if a match will trivially always succeed and offer to +//! remove the lhs as a fix. +//! + +use std::collections::HashMap; + +use elp_ide_db::elp_base_db::FileId; +use elp_ide_db::source_change::SourceChange; +use elp_syntax::ast; +use elp_syntax::SourceFile; +use elp_syntax::TextRange; +use hir::BinarySeg; +use hir::BodySourceMap; +use hir::Expr; +use hir::ExprId; +use hir::FunctionDef; +use hir::InFile; +use hir::InFunctionBody; +use hir::Literal; +use hir::Pat; +use hir::PatId; +use hir::Semantic; +use text_edit::TextEdit; + +use super::Diagnostic; +use super::Severity; +use crate::codemod_helpers::is_only_place_where_var_is_defined; +use crate::codemod_helpers::var_has_no_references; +use crate::codemod_helpers::var_name_starts_with_underscore; +use crate::diagnostics::DiagnosticCode; +use crate::fix; + +pub(crate) fn trivial_match(diags: &mut Vec, sema: &Semantic, file_id: FileId) { + sema.def_map(file_id) + .get_functions() + .iter() + .for_each(|(_arity, def)| { + if def.file.file_id == file_id { + process_matches(diags, sema, def) + } + }); +} + +fn process_matches(diags: &mut Vec, sema: &Semantic, def: &FunctionDef) { + let def_fb = def.in_function_body(sema.db, def); + let body_map = def_fb.get_body_map(sema.db); + let source_file = sema.parse(def.file.file_id); + + def_fb.fold_function( + (), + &mut |_acc, _, ctx| { + let expr = ctx.expr; + match expr { + Expr::Match { lhs, rhs } => { + let rhs = &rhs.clone(); + if matches_trivially(&sema, &def_fb, &body_map, &source_file, &lhs, &rhs) { + if let Some(range) = &def_fb.range_for_expr(sema.db, ctx.expr_id) { + let rhs_ast = body_map + .expr(*rhs) + .and_then(|infile_ast_ptr| infile_ast_ptr.to_node(&source_file)); + diags.push(make_diagnostic(def.file.file_id, range, rhs_ast)); + } + } + } + _ => (), + } + }, + &mut |_acc, _, _| (), + ); +} + +fn matches_trivially( + sema: &Semantic, + def_fb: &InFunctionBody<&FunctionDef>, + body_map: &BodySourceMap, + source_file: &InFile, + pat_id: &PatId, + expr_id: &ExprId, +) -> bool { + let pat = &def_fb[*pat_id]; + let expr = &def_fb[*expr_id]; + match pat { + Pat::Missing => false, + + Pat::Literal(l) => match expr { + Expr::Literal(r) => l == r, + _ => false, + }, + Pat::Var(l) => { + let ast_node = body_map + .pat(*pat_id) + .and_then(|infile_ast_ptr| infile_ast_ptr.to_node(&source_file)); + + if let Some(ast::Expr::ExprMax(ast::ExprMax::Var(ast_var))) = ast_node { + let infile_ast_var = InFile::new(source_file.file_id, &ast_var); + + if !var_name_starts_with_underscore(&ast_var) + && is_only_place_where_var_is_defined(sema, infile_ast_var) + && var_has_no_references(sema, infile_ast_var) + { + // RHS defines a variable, so this will always match. Moreover, the the variable is + // never used, so we can safely remover it. + return true; + } + } + + match expr { + Expr::Var(r) => l == r, + _ => false, + } + } + + Pat::Match { .. } => false, + + Pat::Tuple { pats } => match expr { + Expr::Tuple { exprs } if pats.len() == exprs.len() => pats + .iter() + .zip(exprs.iter()) + .all(|(p, e)| matches_trivially(sema, def_fb, body_map, source_file, p, e)), + _ => false, + }, + + Pat::List { pats, tail: None } => match expr { + Expr::List { exprs, tail: None } if pats.len() == exprs.len() => pats + .iter() + .zip(exprs.iter()) + .all(|(p, e)| matches_trivially(sema, def_fb, body_map, source_file, p, e)), + _ => false, + }, + Pat::List { .. } => false, + + Pat::Record { + name: pat_name, + fields: pat_fields, + } => match expr { + Expr::Record { + name: expr_name, + fields: expr_fields, + } => match {} { + _ if pat_name != expr_name => false, + _ => { + let pat_fields_map = pat_fields.iter().map(|p| *p).collect::>(); + let expr_fields_map = expr_fields.iter().map(|p| *p).collect::>(); + pat_fields_map.iter().all(|(field, pat_val)| { + if let Some(expr_val) = expr_fields_map.get(field) { + matches_trivially( + sema, + def_fb, + body_map, + source_file, + &pat_val, + expr_val, + ) + } else { + false + } + }) + } + }, + _ => false, + }, + Pat::RecordIndex { .. } => false, + + Pat::Map { fields: pat_fields } => match { expr } { + Expr::Map { + fields: expr_fields, + } => { + let pat_fields_map = pat_fields + .iter() + .filter_map(|(field, val)| { + let lit = as_literal(def_fb, field)?; + Some((lit, val)) + }) + .collect::>(); + + // We only handle maps with literals as keys, so ensure no + // key got lost in the translation + if pat_fields_map.len() < pat_fields.len() { + false + } else { + let expr_fields_map = expr_fields + .iter() + .filter_map(|(field, val)| { + let lit = as_literal(def_fb, field)?; + Some((lit, val)) + }) + .collect::>(); + + pat_fields_map.iter().all(|(field, pat_val)| { + if let Some(expr_val) = expr_fields_map.get(field) { + matches_trivially( + sema, + def_fb, + body_map, + source_file, + &pat_val, + expr_val, + ) + } else { + false + } + }) + } + } + _ => false, + }, + + Pat::Binary { segs: pat_segs } => match expr { + Expr::Binary { segs: expr_segs } => { + let trivial_seg = BinarySeg { + elem: {}, + size: None, + tys: vec![], + unit: None, + }; + pat_segs + .iter() + .zip(expr_segs.iter()) + .all(|(pat_seg, expr_seg)| { + pat_seg.with_value({}) == trivial_seg + && expr_seg.with_value({}) == trivial_seg + && matches_trivially( + sema, + def_fb, + body_map, + source_file, + &pat_seg.elem, + &expr_seg.elem, + ) + }) + } + + _ => false, + }, + + Pat::UnaryOp { .. } | Pat::BinaryOp { .. } => false, + Pat::MacroCall { expansion, args: _ } => { + matches_trivially(sema, def_fb, body_map, source_file, expansion, expr_id) + } + } +} + +fn as_literal(def_fb: &InFunctionBody<&FunctionDef>, expr_id: &ExprId) -> Option { + let expr = &def_fb[*expr_id]; + match expr { + Expr::Literal(lit) => Some(lit.clone()), + _ => None, + } +} + +fn make_diagnostic( + file_id: FileId, + range: &TextRange, + maybe_replacement: Option, +) -> Diagnostic { + let diag = Diagnostic::new(DiagnosticCode::TrivialMatch, "match is redundant", *range) + .severity(Severity::Warning); + + if let Some(replacement_ast) = maybe_replacement { + let replacement_str = replacement_ast.to_string(); + let mut edit_builder = TextEdit::builder(); + edit_builder.replace(*range, replacement_str); + let edit = edit_builder.finish(); + + diag.with_fixes(Some(vec![fix( + "remove_redundant_match", + "Remove match", + SourceChange::from_text_edit(file_id, edit), + *range, + )])) + } else { + diag + } +} + +#[cfg(test)] +mod tests { + + use crate::tests::check_diagnostics; + use crate::tests::check_fix; + + #[test] + fn can_fix() { + check_fix( + r#" + -module(main). + + do_foo() -> + X = ~42 = 42, + ok. + "#, + r#" + -module(main). + + do_foo() -> + X = 42, + ok. + "#, + ); + check_fix( + r#" + -module(main). + + do_foo() -> + X = ~foo(bar), + ok. + "#, + r#" + -module(main). + + do_foo() -> + foo(bar), + ok. + "#, + ) + } + + #[test] + fn trivial_lit_matches() { + check_diagnostics( + r#" + -module(main). + + do_foo() -> + 42 = 42, + %%% ^^^^^^^ 💡 warning: match is redundant + 42 = 43, + "blah" = "blah", + %%% ^^^^^^^^^^^^^^^ 💡 warning: match is redundant + "blah" = "bleh", + 'x' = 'x', + %%% ^^^^^^^^^ 💡 warning: match is redundant + 'x' = 'X', + true = true, + %%% ^^^^^^^^^^^ 💡 warning: match is redundant + true = false, + ok. + "#, + ) + } + + #[test] + fn trivial_var_matches() { + check_diagnostics( + r#" + -module(main). + + do_foo() -> + X = 42, + Y = 42, + X = X, + %%% ^^^^^ 💡 warning: match is redundant + X = Y, + {Z} = {Y}, + %%% ^^^^^^^^^ 💡 warning: match is redundant + [W, ok] = [ok, ok], + %%% ^^^^^^^^^^^^^^^^^^ 💡 warning: match is redundant + [_W, ok] = [ok, ok], + ok. + "#, + ) + } + + #[test] + fn trivial_binary_matches() { + check_diagnostics( + r#" + -module(main). + + do_foo() -> + X = 42, + <<"foo", 42>> = <<"foo", 42>>, + %%% ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 💡 warning: match is redundant + <<"foo", X>> = <<"foo", X>>, + %%% ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 💡 warning: match is redundant + <<"foo", Y>> = <<"foo", 42>>, + Y. + "#, + ) + } + #[test] + fn trivial_tuple_matches() { + check_diagnostics( + r#" + -module(main). + + do_foo() -> + X = 42, + {X, "foo", {foo, bar}} = {X, "foo", {foo, bar}}, + %%% ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 💡 warning: match is redundant + {X, foo} = {X, bar}, + {X, "foo", {foo, bar}} = {X, "foo", {foo, pub}}, + {X, "foo", {foo, bar}} = {X, "foo", {foo, bar, hey}}, + {} = {}, + %%% ^^^^^^^ 💡 warning: match is redundant + ok. + "#, + ) + } + + #[test] + fn trivial_list_matches() { + check_diagnostics( + r#" + -module(main). + + do_foo() -> + X = 42, + [X, ["foo"], [foo, bar]] = [X, ["foo"], [foo, bar]], + %%% ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^💡 warning: match is redundant + [X, foo] = [X, bar], + [X, "foo", [foo, bar]] = [X, "foo", [foo, pub]], + [X, "foo", [foo, bar]] = [X, "foo", [foo, bar, hey]], + [] = [], + %%% ^^^^^^^ 💡 warning: match is redundant + ok. + "#, + ) + } + + #[test] + fn trivial_record_matches() { + check_diagnostics( + r#" + -module(main). + + -record(person, {name, age}). + + do_foo() -> + #person{name = "Joe", age = 42} = #person{age = 42, name = "Joe"}, + %%% ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 💡 warning: match is redundant + #person{name = "Joe", age = 43} = #person{age = 42, name = "Joe"}, + #person{name = "Joe"} = #person{age = 42, name = "Joe"}, + %%% ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 💡 warning: match is redundant + #person{age = 42} = #person{age = 42, name = "Joe"}, + %%% ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 💡 warning: match is redundant + ok. + "#, + ) + } + + #[test] + fn trivial_maps_matches() { + check_diagnostics( + r#" + -module(main). + + do_foo() -> + #{name := "Joe", age := 42} = #{age => 42, name => "Joe"}, + %%% ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 💡 warning: match is redundant + #{name := "Joe", age := 43} = #{age => 42, name => "Joe"}, + #{name := "Joe"} = #{age => 42, name => "Joe"}, + %%% ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 💡 warning: match is redundant + #{age := 42} = #{age => 42, name => "Joe"}, + %%% ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 💡 warning: match is redundant + ok. + "#, + ) + } +} diff --git a/crates/ide/src/diagnostics/unused_function_args.rs b/crates/ide/src/diagnostics/unused_function_args.rs new file mode 100644 index 0000000000..0f8b3852ee --- /dev/null +++ b/crates/ide/src/diagnostics/unused_function_args.rs @@ -0,0 +1,289 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! Lint/fix: unused_function_args +//! +//! Return a diagnostic if a function has an argument that is not used in +//! the function body, and offer to add an underscore to the name as fix. +//! + +use std::collections::HashMap; +use std::collections::HashSet; + +use elp_ide_db::elp_base_db::FileId; +use elp_ide_db::source_change::SourceChange; +use elp_syntax::ast; +use hir::Clause; +use hir::FunctionDef; +use hir::InFile; +use hir::InFunctionBody; +use hir::PatId; +use hir::Semantic; +use hir::Strategy; +use text_edit::TextEdit; +use text_edit::TextRange; + +use super::Diagnostic; +use super::Severity; +use crate::diagnostics::DiagnosticCode; +use crate::fix; + +pub(crate) fn unused_function_args(diags: &mut Vec, sema: &Semantic, file_id: FileId) { + sema.def_map(file_id) + .get_functions() + .iter() + .for_each(|(_arity, def)| { + if def.file.file_id != file_id { + return; + } + let source_file = sema.parse(file_id); + + let mut def_fb = def.in_function_body(sema.db, def); + let body_map = def_fb.get_body_map(sema.db); + + for (_clause_id, clause @ Clause { pats, .. }) in def_fb.clone().clauses() { + let mut unused_vars_with_wrong_name = HashMap::new(); + + for clause_arg_pat_id in pats.iter() { + def_fb.fold_pat( + Strategy::TopDown, + *clause_arg_pat_id, + (), + &mut |(), _| {}, + &mut |(), ctx| { + if let Some(var) = ctx.pat.as_var() { + if is_unused_var( + &sema, + &def_fb, + &body_map, + &source_file, + &ctx.pat_id, + ) { + let var_name = var.as_string(sema.db.upcast()); + if !var_name.starts_with("_") { + unused_vars_with_wrong_name.insert(ctx.pat_id, var_name); + } + } + } + }, + ); + } + + if !unused_vars_with_wrong_name.is_empty() { + if let Some(replacements) = pick_new_unused_var_names( + &sema, + &def_fb, + &clause, + &unused_vars_with_wrong_name, + ) { + for (pat_id, new_name) in replacements.iter() { + if let Some(range) = def_fb.range_for_pat(sema.db, *pat_id) { + diags.push(make_diagnostic(file_id, range, new_name.clone())); + } + } + } + } + } + }); +} + +fn is_unused_var( + sema: &Semantic, + def_fb: &InFunctionBody<&FunctionDef>, + body_map: &hir::BodySourceMap, + source_file: &InFile, + pat_id: &PatId, +) -> bool { + match &def_fb[*pat_id].as_var() { + Some(_var) => { + if let Some(infile_ast_ptr) = body_map.pat(*pat_id) { + if let Some(ast::Expr::ExprMax(ast::ExprMax::Var(ast_var))) = + infile_ast_ptr.to_node(&source_file) + { + let infile_ast_var = InFile::new(source_file.file_id, &ast_var); + if let Some(var_usages) = sema.find_local_usages(infile_ast_var) { + return var_usages.len() == 1; + } + } + } + false + } + _ => false, + } +} + +// Pick a new name for the unused vars, that start with an underscore. Ensure no clash with existing names. +fn pick_new_unused_var_names( + sema: &Semantic, + def_fb: &InFunctionBody<&FunctionDef>, + clause: &Clause, + unused_var_names: &HashMap, +) -> Option> { + let clause_vars = hir::ScopeAnalysis::clause_vars_in_scope(&sema, &def_fb.with_value(clause))?; + let unused_vars: HashSet = HashSet::from_iter( + unused_var_names + .keys() + .filter_map(|pat_id| def_fb[*pat_id].as_var()), + ); + let other_ignored_var_names: HashSet = + HashSet::from_iter(clause_vars.iter().filter_map(|v| { + if unused_vars.contains(v) { + None + } else { + let vname = v.as_string(sema.db.upcast()); + if !vname.starts_with("_") { + None + } else { + Some(vname) + } + } + })); + let result = unused_var_names + .iter() + .map(|(k, v)| { + let mut new_var_name = format!("_{}", v); + while other_ignored_var_names.contains(&new_var_name) { + new_var_name = format!("_{}", new_var_name); + } + (*k, new_var_name) + }) + .collect(); + Some(result) +} + +fn make_diagnostic(file_id: FileId, range: TextRange, new_name: String) -> Diagnostic { + let mut edit_builder = TextEdit::builder(); + edit_builder.replace(range, new_name); + let edit = edit_builder.finish(); + + Diagnostic::new( + DiagnosticCode::UnusedFunctionArg, + "this variable is unused", + range, + ) + .severity(Severity::Warning) + // Marking as EXPERIMENTAL since it currently conflicts with handlers::ignore_variable https://fburl.com/code/rkm52yfj + .experimental() + .with_fixes(Some(vec![fix( + "prefix_with_underscore", + "Prefix variable with an underscore", + SourceChange::from_text_edit(file_id, edit), + range, + )])) +} + +#[cfg(test)] +mod tests { + + use crate::tests::check_diagnostics; + use crate::tests::check_fix; + + #[test] + fn check_diagnostic_unused_unprefixed_variables() { + check_diagnostics( + r#" + -module(main). + do_something(Unused, Used, _AlsoUsed, AlsoUnused, _UnusedButOk) -> + %%% ^^^^^^ 💡 warning: this variable is unused + %%% ^^^^^^^^^^ 💡 warning: this variable is unused + + case Used of + undefined -> ok; + _Unused -> do_something_else(_AlsoUsed) + end. + "#, + ); + } + + #[test] + fn check_diagnostic_unused_unprefixed_variables_inside_patterns() { + check_diagnostics( + r#" + -module(main). + do_something([Unused | Used], #{foo := {_AlsoUsed, AlsoUnused, _UnusedButOk}}) -> + %%% ^^^^^^ 💡 warning: this variable is unused + %%% ^^^^^^^^^^ 💡 warning: this variable is unused + + case Used of + undefined -> ok; + _Unused -> do_something_else(_AlsoUsed) + end. + "#, + ); + } + + #[test] + fn check_prefixes_unused_unprefixed_variables() { + check_fix( + r#" + -module(main). + do_something(U~nused, Used, _AlsoUsed, _UnusedButOk) -> + case Used of + undefined -> ok; + _SomethingElseUnused -> do_something_else(_AlsoUsed) + end. + "#, + r#" + -module(main). + do_something(_Unused, Used, _AlsoUsed, _UnusedButOk) -> + case Used of + undefined -> ok; + _SomethingElseUnused -> do_something_else(_AlsoUsed) + end. + "#, + ); + } + + #[test] + fn check_prefixes_unused_unprefixed_variables_avoiding_captures() { + check_fix( + r#" + -module(main). + do_something(Un~used, Used, _AlsoUsed, _UnusedButOk) -> + case Used of + undefined -> ok; + _Unused -> do_something_else(_AlsoUsed) + end. + "#, + r#" + -module(main). + do_something(__Unused, Used, _AlsoUsed, _UnusedButOk) -> + case Used of + undefined -> ok; + _Unused -> do_something_else(_AlsoUsed) + end. + "#, + ); + } + + #[test] + fn argument_used_in_macro_1() { + check_diagnostics( + r#" + -module(main). + -define(a_macro(Expr), ok). + %% -define(a_macro(Expr), {Expr}). + get_aclink_state_test_helper(Args) -> + ?a_macro(Args). + "#, + ); + } + + #[test] + fn more_than_one_clause() { + check_diagnostics( + r#" + -module(main). + foo(Args) -> {foo, Args}; + foo(Args2) -> ok. + %% ^^^^^ 💡 warning: this variable is unused + "#, + ); + } +} diff --git a/crates/ide/src/diagnostics/unused_include.rs b/crates/ide/src/diagnostics/unused_include.rs new file mode 100644 index 0000000000..dde3423678 --- /dev/null +++ b/crates/ide/src/diagnostics/unused_include.rs @@ -0,0 +1,427 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +// Diagnostic: unused include (L1500) +// +// Return a warning if nothing is used from an include file + +use elp_ide_db::elp_base_db::FileId; +use elp_ide_db::source_change::SourceChange; +use elp_ide_db::SearchScope; +use elp_ide_db::SymbolDefinition; +use elp_syntax::ast::AstNode; +use fxhash::FxHashMap; +use fxhash::FxHashSet; +use hir::db::MinDefDatabase; +use hir::InFile; +use hir::IncludeAttribute; +use hir::Semantic; +use text_edit::TextEdit; + +use super::Diagnostic; +use crate::diagnostics::DiagnosticCode; +use crate::diagnostics::Severity; +use crate::fix; + +pub(crate) fn unused_includes( + sema: &Semantic, + db: &dyn MinDefDatabase, + diagnostics: &mut Vec, + file_id: FileId, +) { + let form_list = db.file_form_list(file_id); + let mut cache = Default::default(); + for (include_idx, attr) in form_list.includes() { + let in_file = InFile::new(file_id, include_idx); + if let Some(include_file_id) = db.resolve_include(in_file) { + if is_file_used(sema, db, include_file_id, file_id, &mut cache) { + continue; + } + + let path = match attr { + IncludeAttribute::Include { path, .. } => path, + IncludeAttribute::IncludeLib { path, .. } => path, + }; + + let source_file = db.parse(file_id); + let inc_text_rage = attr + .form_id() + .get(&source_file.tree()) + .syntax() + .text_range(); + + let mut edit_builder = TextEdit::builder(); + edit_builder.delete(inc_text_rage.clone()); + let edit = edit_builder.finish(); + + let diagnostic = Diagnostic::new( + DiagnosticCode::UnusedInclude, + format!("Unused file: {}", path), + inc_text_rage.clone(), + ) + .severity(Severity::Warning) + .with_fixes(Some(vec![fix( + "remove_unused_include", + "Remove unused include", + SourceChange::from_text_edit(file_id, edit), + inc_text_rage, + )])); + + log::debug!("Found unused include {:?}", path); + + diagnostics.push(diagnostic); + } + } +} + +fn is_file_used( + sema: &Semantic, + db: &dyn MinDefDatabase, + include_file_id: FileId, + target: FileId, + cache: &mut FxHashMap, +) -> bool { + if let Some(used) = cache.get(&include_file_id) { + return *used; + } + + let mut todo = FxHashSet::default(); + todo.insert(include_file_id); + let scope = SearchScope::single_file(target, None); + while let Some(file_id) = todo.iter().next().cloned() { + todo.remove(&file_id); + + let list = db.file_form_list(file_id); + for (include_idx, _) in list.includes() { + let in_file = InFile::new(file_id, include_idx); + if let Some(include_file_id) = db.resolve_include(in_file) { + match cache.get(&include_file_id) { + None => todo.insert(include_file_id), + Some(true) => return true, + _ => false, + }; + } + } + + let def_map = db.local_def_map(file_id); + if def_map.parse_transform { + cache.insert(file_id, true); + return true; + } + if !def_map.get_callbacks().is_empty() { + cache.insert(file_id, true); + return true; + } + + if !def_map.get_exported_functions().is_empty() { + cache.insert(file_id, true); + return true; + } + + if !def_map.get_exported_types().is_empty() { + cache.insert(file_id, true); + return true; + } + + //TODO use find usages for that after it will work + if !def_map.get_imports().is_empty() { + cache.insert(file_id, true); + return true; + } + + for (_, fun_def) in def_map.get_functions() { + if SymbolDefinition::Function(fun_def.clone()) + .usages(&sema) + .set_scope(&scope) + .at_least_one() + { + cache.insert(file_id, true); + return true; + } + } + + for (_, type_def) in def_map.get_types() { + if SymbolDefinition::Type(type_def.clone()) + .usages(&sema) + .set_scope(&scope) + .at_least_one() + { + cache.insert(file_id, true); + return true; + } + } + + for (_, record_def) in def_map.get_records() { + if SymbolDefinition::Record(record_def.clone()) + .usages(&sema) + .set_scope(&scope) + .at_least_one() + { + cache.insert(file_id, true); + return true; + } + } + + for (_, macro_def) in def_map.get_macros() { + if SymbolDefinition::Define(macro_def.clone()) + .usages(&sema) + .set_scope(&scope) + .at_least_one() + { + cache.insert(file_id, true); + return true; + } + } + + cache.insert(file_id, false); + } + + return false; +} + +#[cfg(test)] +mod tests { + + use crate::tests::check_diagnostics; + + #[test] + fn optimise_includes_unused_include_with_macro() { + check_diagnostics( + r#" +//- /include/foo.hrl include_path:/include + -define(FOO,3). +//- /src/foo.erl + -module(foo). + -include("foo.hrl"). +%%^^^^^^^^^^^^^^^^^^^^ 💡 warning: Unused file: foo.hrl + "#, + ); + } + + #[test] + fn optimise_includes_used_include_with_macro() { + check_diagnostics( + r#" +//- /include/foo.hrl include_path:/include + -define(FOO,3). +//- /src/foo.erl + -module(foo). + -include("foo.hrl"). + foo() -> ?FOO. + "#, + ); + } + + #[test] + fn optimise_includes_unused_include_with_function() { + check_diagnostics( + r#" +//- /include/foo.hrl include_path:/include + foo() -> bar. +//- /src/foo.erl + -module(foo). + -include("foo.hrl"). +%%^^^^^^^^^^^^^^^^^^^^ 💡 warning: Unused file: foo.hrl + "#, + ); + } + + #[test] + fn optimise_includes_used_include_with_function() { + check_diagnostics( + r#" +//- /include/foo.hrl include_path:/include + foo() -> bar. +//- /src/foo.erl + -module(foo). + -include("foo.hrl"). + baz() -> foo(). + "#, + ); + } + + #[test] + fn optimise_includes_unused_include_with_type() { + check_diagnostics( + r#" +//- /include/foo.hrl include_path:/include + -type orddict(Key, Val) :: [{Key, Val}]. +//- /src/foo.erl + -module(foo). + -include("foo.hrl"). +%%^^^^^^^^^^^^^^^^^^^^ 💡 warning: Unused file: foo.hrl + "#, + ); + } + + #[test] + fn optimise_includes_used_include_with_type() { + check_diagnostics( + r#" +//- /include/foo.hrl include_path:/include + -type orddict(Key, Val) :: [{Key, Val}]. +//- /src/bar.erl + -module(bar). + -include("foo.hrl"). + -spec foo() -> orddict(integer(), integer()). + foo() -> orddict(1, 2). + "#, + ); + } + + #[test] + fn optimise_includes_unused_include_with_record() { + check_diagnostics( + r#" +//- /include/foo.hrl include_path:/include + -record(person, {name :: string(), height :: pos_integer()}). +//- /src/foo.erl + -module(foo). + -include("foo.hrl"). +%%^^^^^^^^^^^^^^^^^^^^ 💡 warning: Unused file: foo.hrl + "#, + ); + } + + #[test] + fn optimise_includes_used_include_exported_type() { + check_diagnostics( + r#" +//- /include/foo.hrl include_path:/include + -type orddict(Key, Val) :: [{Key, Val}]. + -export_type([orddict/2]). + +//- /src/foo.erl + -module(foo). + -include("foo.hrl"). + "#, + ); + } + + #[test] + fn optimise_includes_used_include_exported_function() { + check_diagnostics( + r#" +//- /include/foo.hrl include_path:/include + foo() -> bar. + -export([foo/0]). + +//- /src/foo.erl + -module(foo). + -include("foo.hrl"). + "#, + ); + } + + #[test] + fn optimise_includes_used_include_callback() { + check_diagnostics( + r#" +//- /include/foo.hrl include_path:/include + -callback terminate() -> 'ok'. + +//- /src/foo.erl + -module(foo). + -include("foo.hrl"). + "#, + ); + } + + #[test] + fn optimise_includes_used_include_import() { + check_diagnostics( + r#" +//- /include/foo.hrl include_path:/include + -import(lists, [all/2]). + +//- /src/foo.erl + -module(foo). + -include("foo.hrl"). + "#, + ); + } + + #[test] + fn optimise_includes_used_include_bug() { + check_diagnostics( + r#" +//- /include/foo.hrl include_path:/include +-define(line,). +lol(A, _B) -> A. +-define(enum, lol). + +//- /src/foo.erl + -module(foo). + -include("foo.hrl"). + bar() -> ?enum(1, [a,b]). + "#, + ); + } + + #[test] + fn optimise_includes_used_transitive() { + check_diagnostics( + r#" +//- /include/header0.hrl include_path:/include + bar() -> ok. +//- /include/header1.hrl include_path:/include + -include("header0.hrl"). +//- /include/header2.hrl include_path:/include + -include("header1.hrl"). + +//- /src/foo.erl + -module(foo). + -include("header2.hrl"). + + foo() -> bar(). + "#, + ); + } + + #[test] + fn optimise_includes_used_remote_transitive() { + check_diagnostics( + r#" +//- /kernel/include/logger.hrl include_path:/include app:kernel + -define(LOG_WARNING, true). + +//- /include/do_log.hrl include_path:/include app:lol + + -include_lib("kernel/include/logger.hrl"). + + +//- /src/foo.erl app:lol + -module(foo). + -include("do_log.hrl"). + + get_all_logs(_LogsDirectory) -> + ?LOG_WARNING. +"#, + ); + } + + #[test] + fn used_for_parse_transform() { + check_diagnostics( + r#" +//- /stdlib/include/ms_transform.hrl include_path:/include app:stdlib + -compile({parse_transform,ms_transform}). + + +//- /src/foo.erl app:lol + -module(foo). + -include_lib("stdlib/include/ms_transform.hrl"). + + select_all() -> + Match = ets:fun2ms(fun(#corp{login = L, props = P}) when L /= 'unknown' -> {L, P} end), + Match. +"#, + ); + } +} diff --git a/crates/ide/src/diagnostics/unused_macro.rs b/crates/ide/src/diagnostics/unused_macro.rs new file mode 100644 index 0000000000..69741a0d77 --- /dev/null +++ b/crates/ide/src/diagnostics/unused_macro.rs @@ -0,0 +1,193 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +// Diagnostic: unused-macro +// +// Return a warning if a macro defined in an .erl file has no references to it + +use elp_ide_assists::Assist; +use elp_ide_db::elp_base_db::FileId; +use elp_ide_db::source_change::SourceChange; +use elp_ide_db::SymbolDefinition; +use elp_syntax::AstNode; +use elp_syntax::SyntaxKind; +use elp_syntax::TextRange; +use elp_syntax::TextSize; +use hir::Semantic; +use text_edit::TextEdit; + +use crate::diagnostics::DiagnosticCode; +use crate::fix; +use crate::Diagnostic; + +pub(crate) fn unused_macro( + acc: &mut Vec, + sema: &Semantic, + file_id: FileId, + ext: Option<&str>, +) -> Option<()> { + if Some("erl") == ext { + let def_map = sema.def_map(file_id); + for (name, def) in def_map.get_macros() { + // Only run the check for macros defined in the local module, + // not in the included files. + if def.file.file_id == file_id { + if !SymbolDefinition::Define(def.clone()) + .usages(&sema) + .at_least_one() + { + let source = def.source(sema.db.upcast()); + let macro_syntax = source.syntax(); + // If after the macro there's a new line, drop it + let next_token = macro_syntax.last_token()?.next_token()?; + let macro_range = if next_token.kind() == SyntaxKind::WHITESPACE + && next_token.text().starts_with("\n") + { + let start = macro_syntax.text_range().start(); + let end = macro_syntax.text_range().end() + TextSize::from(1); + // Temporary for T148094436 + let _pctx = + stdx::panic_context::enter(format!("\ndiagnostics::unused_macro")); + TextRange::new(start, end) + } else { + macro_syntax.text_range() + }; + let name_range = source.name()?.syntax().text_range(); + let d = make_diagnostic(file_id, macro_range, name_range, &name.to_string()); + acc.push(d); + } + } + } + } + Some(()) +} + +fn make_diagnostic( + file_id: FileId, + macro_range: TextRange, + name_range: TextRange, + name: &str, +) -> Diagnostic { + Diagnostic::warning( + DiagnosticCode::UnusedMacro, + name_range, + format!("Unused macro ({name})"), + ) + .with_fixes(Some(vec![delete_unused_macro(file_id, macro_range, name)])) +} + +fn delete_unused_macro(file_id: FileId, range: TextRange, name: &str) -> Assist { + let mut builder = TextEdit::builder(); + builder.delete(range); + let edit = builder.finish(); + fix( + "delete_unused_macro", + &format!("Delete unused macro ({name})"), + SourceChange::from_text_edit(file_id, edit), + range, + ) +} + +#[cfg(test)] +mod tests { + + use crate::tests::check_diagnostics; + use crate::tests::check_fix; + + #[test] + fn test_unused_macro() { + check_diagnostics( + r#" +-module(main). +-define(MEANING_OF_LIFE, 42). + %% ^^^^^^^^^^^^^^^ 💡 warning: Unused macro (MEANING_OF_LIFE) + "#, + ); + check_fix( + r#" +-module(main). +-define(MEA~NING_OF_LIFE, 42). + "#, + r#" +-module(main). + "#, + ) + } + + #[test] + fn test_unused_macro_not_applicable() { + check_diagnostics( + r#" +-module(main). +-define(MEANING_OF_LIFE, 42). +main() -> + ?MEANING_OF_LIFE. + "#, + ); + } + + #[test] + fn test_unused_macro_not_applicable_for_hrl_file() { + check_diagnostics( + r#" +//- /include/foo.hrl +-define(MEANING_OF_LIFE, 42). + "#, + ); + } + + #[test] + fn test_unused_macro_with_arg() { + check_diagnostics( + r#" +-module(main). +-define(USED_MACRO, used_macro). +-define(UNUSED_MACRO, unused_macro). + %% ^^^^^^^^^^^^ 💡 warning: Unused macro (UNUSED_MACRO) +-define(UNUSED_MACRO_WITH_ARG(C), C). + %% ^^^^^^^^^^^^^^^^^^^^^ 💡 warning: Unused macro (UNUSED_MACRO_WITH_ARG/1) + +main() -> + ?MOD:foo(), + ?USED_MACRO. + "#, + ); + } + + #[test] + fn test_unused_macro_dynamic_call() { + // Ported from issue #1021 in Erlang LS + check_diagnostics( + r#" +-module(main). +-define(MOD, module). %% MOD +main() -> + ?MOD:foo(). + "#, + ); + } + + #[test] + fn test_unused_macro_include() { + check_diagnostics( + r#" +//- /src/foo.hrl +-define(A, a). +-define(B, b). +//- /src/foo.erl +-module(foo). +-include("foo.hrl"). +-define(BAR, 42). + %% ^^^ 💡 warning: Unused macro (BAR) +main() -> + ?A. + "#, + ); + } +} diff --git a/crates/ide/src/diagnostics/unused_record_field.rs b/crates/ide/src/diagnostics/unused_record_field.rs new file mode 100644 index 0000000000..c8715a8ca9 --- /dev/null +++ b/crates/ide/src/diagnostics/unused_record_field.rs @@ -0,0 +1,137 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +// Diagnostic: unused-record-field +// +// Return a warning if a record field defined in an .erl file has no references to it + +use elp_ide_db::elp_base_db::FileId; +use elp_ide_db::SymbolDefinition; +use elp_syntax::AstNode; +use elp_syntax::TextRange; +use hir::Semantic; + +use crate::diagnostics::DiagnosticCode; +use crate::Diagnostic; + +pub(crate) fn unused_record_field( + acc: &mut Vec, + sema: &Semantic, + file_id: FileId, + ext: Option<&str>, +) -> Option<()> { + if Some("erl") == ext { + let def_map = sema.def_map(file_id); + for (name, def) in def_map.get_records() { + // Only run the check for records defined in the local module, + // not in the included files. + if def.file.file_id == file_id { + for (field_name, field_def) in def.fields(sema.db) { + if !SymbolDefinition::RecordField(field_def.clone()) + .usages(&sema) + .at_least_one() + { + let combined_name = format!("{name}.{field_name}"); + let range = field_def.source(sema.db.upcast()).syntax().text_range(); + let d = make_diagnostic(range, &combined_name); + acc.push(d); + } + } + } + } + } + Some(()) +} + +fn make_diagnostic(name_range: TextRange, name: &str) -> Diagnostic { + Diagnostic::warning( + DiagnosticCode::UnusedRecordField, + name_range, + format!("Unused record field ({name})"), + ) +} + +#[cfg(test)] +mod tests { + + use crate::tests::check_diagnostics; + + #[test] + fn test_unused_record_field() { + check_diagnostics( + r#" +-module(main). + +-export([main/1]). + +-record(used_field, {field_a, field_b = 42}). +-record(unused_field, {field_c, field_d}). + %% ^^^^^^^ warning: Unused record field (unused_field.field_d) + +main(#used_field{field_a = A, field_b = B}) -> + {A, B}; +main(R) -> + R#unused_field.field_c. + "#, + ); + } + + #[test] + fn test_unused_record_field_not_applicable() { + check_diagnostics( + r#" +-module(main). +-record(used_field, {field_a, field_b = 42}). + +main(#used_field{field_a = A} = X) -> + {A, X#used_field.field_b}. + "#, + ); + } + + #[test] + fn test_unused_record_field_not_applicable_for_hrl_file() { + check_diagnostics( + r#" +//- /include/foo.hrl +-record(unused_record, {field_a, field_b}). + "#, + ); + } + + #[test] + fn test_unused_record_field_include() { + check_diagnostics( + r#" +//- /include/foo.hrl +-record(unused_record, {field_a, field_b}). +//- /src/foo.erl +-module(foo). +-include("foo.hrl"). +main(#used_field{field_a = A}) -> + {A, B}. + "#, + ); + } + + #[test] + fn test_unused_record_field_nested() { + check_diagnostics( + r#" +-module(main). +-record(a, {a1, a2}). + %% ^^ warning: Unused record field (a.a2) +-record(b, {b1, b2}). + %% ^^ warning: Unused record field (b.b1) +main(#a{a1 = #b{b2 = B2}} = A) -> + {A, B2}. + "#, + ); + } +} diff --git a/crates/ide/src/diff.rs b/crates/ide/src/diff.rs new file mode 100644 index 0000000000..1f572fa2b8 --- /dev/null +++ b/crates/ide/src/diff.rs @@ -0,0 +1,237 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::fmt::Display; +use std::fmt::Write; +use std::hash::Hash; +use std::ops::Range; + +use imara_diff::diff; +use imara_diff::intern::InternedInput; +use imara_diff::intern::Interner; +use imara_diff::intern::Token; +use imara_diff::Algorithm; +use imara_diff::Sink; + +pub fn diff_from_textedit(before: &str, after: &str) -> (Vec, Option) { + let input = InternedInput::new(before, after); + let (unified, diff_range) = diff(Algorithm::Histogram, &input, DiffRangeBuilder::new(&input)); + (diff_range, Some(unified)) +} + +// --------------------------------------------------------------------- +// Based on [`UnifiedDiffBuilder`](imara_diff::UnifiedDiffBuilder), +// with additional output of the changed ranges + +pub struct DiffRangeBuilder<'a, W, T> +where + W: Write, + T: Hash + Eq + Display, +{ + before: &'a [Token], + after: &'a [Token], + interner: &'a Interner, + + pos: u32, + before_hunk_start: u32, + after_hunk_start: u32, + before_hunk_len: u32, + after_hunk_len: u32, + + buffer: String, + dst: W, + + out: Vec, +} + +impl<'a, T> DiffRangeBuilder<'a, String, T> +where + T: Hash + Eq + Display, +{ + pub fn new(input: &'a InternedInput) -> Self { + Self { + before_hunk_start: 0, + after_hunk_start: 0, + before_hunk_len: 0, + after_hunk_len: 0, + buffer: String::with_capacity(8), + dst: String::new(), + interner: &input.interner, + before: &input.before, + after: &input.after, + pos: 0, + out: Vec::default(), + } + } +} + +#[derive(Debug)] +pub struct DiffRange { + pub after_start: u32, +} + +impl<'a, W, T> DiffRangeBuilder<'a, W, T> +where + W: Write, + T: Hash + Eq + Display, +{ + /// Create a new `UnifiedDiffBuilder` for the given `input`, + /// that will writes it output to the provided implementation of [`Write`](std::fmt::Write). + pub fn with_writer(input: &'a InternedInput, writer: W) -> Self { + Self { + before_hunk_start: 0, + after_hunk_start: 0, + before_hunk_len: 0, + after_hunk_len: 0, + buffer: String::with_capacity(8), + dst: writer, + interner: &input.interner, + before: &input.before, + after: &input.after, + pos: 0, + out: Vec::default(), + } + } + + fn print_tokens(&mut self, tokens: &[Token], prefix: char) { + for &token in tokens { + writeln!(&mut self.buffer, "{prefix}{}", self.interner[token]).unwrap(); + } + } + + fn flush(&mut self) { + if self.before_hunk_len == 0 && self.after_hunk_len == 0 { + return; + } + + let end = (self.pos + 3).min(self.before.len() as u32); + self.update_pos(end, end, None); + + writeln!( + &mut self.dst, + "@@ -{},{} +{},{} @@", + self.before_hunk_start + 1, + self.before_hunk_len, + self.after_hunk_start + 1, + self.after_hunk_len, + ) + .unwrap(); + write!(&mut self.dst, "{}", &self.buffer).unwrap(); + self.buffer.clear(); + self.before_hunk_len = 0; + self.after_hunk_len = 0 + } + + fn update_pos(&mut self, print_to: u32, move_to: u32, after: Option>) { + self.print_tokens(&self.before[self.pos as usize..print_to as usize], ' '); + let len = print_to - self.pos; + self.pos = move_to; + self.before_hunk_len += len; + self.after_hunk_len += len; + + if let Some(after) = after { + self.out.push(DiffRange { + after_start: after.start, + }); + } + } +} + +impl Sink for DiffRangeBuilder<'_, W, T> +where + W: Write, + T: Hash + Eq + Display, +{ + type Out = (W, Vec); + + fn process_change(&mut self, before: Range, after: Range) { + if before.start - self.pos > 6 { + self.flush(); + self.pos = before.start - 3; + self.before_hunk_start = self.pos; + self.after_hunk_start = after.start - 3; + } + self.update_pos(before.start, before.end, Some(after.clone())); + self.before_hunk_len += before.end - before.start; + self.after_hunk_len += after.end - after.start; + self.print_tokens( + &self.before[before.start as usize..before.end as usize], + '-', + ); + self.print_tokens(&self.after[after.start as usize..after.end as usize], '+'); + } + + fn finish(mut self) -> Self::Out { + self.flush(); + (self.dst, self.out) + } +} + +// --------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use expect_test::expect; + + use super::diff_from_textedit; + + #[test] + fn diff_provides_correct_change_range() { + let before = r#" + line 1 + line 2 + line 3 + line 4 + line 5 + line 6 + "#; + let after = r#" + line 1 + line 3 + line 4 + line 5a + line 5b + line 6 + "#; + + let (diff, unified) = diff_from_textedit(&before, &after); + expect![[r#" + [ + DiffRange { + after_start: 2, + }, + DiffRange { + after_start: 4, + }, + ] + "#]] + .assert_debug_eq(&diff); + expect![[r#" + @@ -1,8 +1,8 @@ + + line 1 + - line 2 + line 3 + line 4 + - line 5 + + line 5a + + line 5b + line 6 + + "#]] + .assert_eq( + &unified + .unwrap() + .split("\n") + .map(str::trim_end) + .collect::>() + .join("\n"), + ); + } +} diff --git a/crates/ide/src/doc_links.rs b/crates/ide/src/doc_links.rs new file mode 100644 index 0000000000..e8abca4076 --- /dev/null +++ b/crates/ide/src/doc_links.rs @@ -0,0 +1,151 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_ide_db::elp_base_db::FilePosition; +use elp_ide_db::RootDatabase; +use elp_ide_db::SymbolClass; +use elp_ide_db::SymbolDefinition; +use elp_syntax::AstNode; +use hir::InFile; +use hir::Semantic; + +const OTP_BASE_URL: &str = "https://erlang.org"; + +/// Retrieve a link to documentation for the given symbol. +pub(crate) fn external_docs(db: &RootDatabase, position: &FilePosition) -> Option> { + let sema = Semantic::new(db); + let source_file = sema.parse(position.file_id); + + // Temporary for T153426323 + let _pctx = stdx::panic_context::enter(format!("\nexternal_docs")); + let token = source_file + .value + .syntax() + .token_at_offset(position.offset) + .left_biased()?; + + let doc_links = SymbolClass::classify(&sema, InFile::new(position.file_id, token))? + .into_iter() + .filter_map(|def| doc_links(&sema, def)) + .flatten() + .collect(); + Some(doc_links) +} + +fn doc_links(sema: &Semantic, def: SymbolDefinition) -> Option> { + match def { + SymbolDefinition::Module(module) => { + if module.is_in_otp(sema.db) { + let url = format!("{}/doc/man/{}.html", OTP_BASE_URL, module.name(sema.db)); + Some(vec![url]) + } else { + None + } + } + SymbolDefinition::Function(function_def) => { + if function_def.is_in_otp(sema.db) { + let module_name = sema.module_name(function_def.file.file_id)?; + let url = format!( + "{}/doc/man/{}.html#{}-{}", + OTP_BASE_URL, + module_name.as_str(), + function_def.function.name.name(), + function_def.function.name.arity() + ); + Some(vec![url]) + } else { + None + } + } + _ => None, + } +} + +#[cfg(test)] +mod tests { + use crate::fixture; + + fn check(fixture: &str, expected_links: Vec<&str>) { + let (analysis, position) = fixture::position(fixture); + let actual_links = analysis.external_docs(position).ok().unwrap().unwrap(); + assert_eq!(actual_links, expected_links); + } + + #[test] + fn otp_module_doc_links() { + check( + r#" +//- /opt/lib/stdlib-3.17/src/lists.erl otp_app:/opt/lib/stdlib-3.17 +-module(lists). +-export([reverse/1]). +reverse([]) -> []. + +//- /src/two.erl +-module(two). +a() -> + list~s:reverse([]). + "#, + vec!["https://erlang.org/doc/man/lists.html"], + ) + } + + #[test] + fn non_otp_module_doc_links() { + check( + r#" +//- /src/one.erl +-module(one). +-export([reverse/1]). +reverse([]) -> []. + +//- /src/two.erl +-module(two). +a() -> + on~e:reverse([]). + "#, + vec![], + ) + } + + #[test] + fn otp_function_doc_links() { + check( + r#" +//- /opt/lib/stdlib-3.17/src/lists.erl otp_app:/opt/lib/stdlib-3.17 +-module(lists). +-export([reverse/1]). +reverse([]) -> []. + +//- /src/two.erl +-module(two). +a() -> + lists:rev~erse([]). + "#, + vec!["https://erlang.org/doc/man/lists.html#reverse-1"], + ) + } + + #[test] + fn non_otp_function_doc_links() { + check( + r#" +//- /src/one.erl +-module(one). +-export([reverse/1]). +reverse([]) -> []. + +//- /src/two.erl +-module(two). +a() -> + one:rev~erse([]). + "#, + vec![], + ) + } +} diff --git a/crates/ide/src/document_symbols.rs b/crates/ide/src/document_symbols.rs new file mode 100644 index 0000000000..23bcf2b9f2 --- /dev/null +++ b/crates/ide/src/document_symbols.rs @@ -0,0 +1,362 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::fmt; + +use elp_ide_db::elp_base_db::FileId; +use elp_ide_db::RootDatabase; +use elp_ide_db::SymbolKind; +use elp_syntax::ast::FunctionOrMacroClause; +use elp_syntax::AstNode; +use elp_syntax::TextRange; +use hir::db::MinDefDatabase; +use hir::DefineDef; +use hir::FunctionDef; +use hir::Name; +use hir::RecordDef; +use hir::Semantic; +use hir::TypeAliasDef; + +#[derive(Clone, PartialEq, Eq, Hash, Debug)] +pub struct DocumentSymbol { + pub name: String, + pub kind: SymbolKind, + pub range: TextRange, + pub selection_range: TextRange, + pub deprecated: bool, + pub detail: Option, + pub children: Option>, +} + +impl fmt::Display for DocumentSymbol { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut s = format!("{:?} | {}", self.kind, self.name); + match &self.detail { + None => (), + Some(detail) => s.push_str(format!(" | {}", detail).as_str()), + }; + if self.deprecated { + s.push_str(" | deprecated") + } + write!(f, "{s}") + } +} + +pub trait ToDocumentSymbol { + fn to_document_symbol(&self, db: &dyn MinDefDatabase) -> DocumentSymbol; +} + +impl ToDocumentSymbol for FunctionDef { + fn to_document_symbol(&self, db: &dyn MinDefDatabase) -> DocumentSymbol { + let source = self.source(db.upcast()); + let range = source.syntax().text_range(); + let mut children = Vec::new(); + for clause in source.clauses() { + let function_name = self.function.name.to_string(); + let function_name_no_arity = self.function.name.name().to_string(); + let clause_name = match &clause { + FunctionOrMacroClause::FunctionClause(clause) => match clause.args() { + None => Name::MISSING.to_string(), + Some(args) => args.to_string(), + }, + FunctionOrMacroClause::MacroCallExpr(_) => Name::MISSING.to_string(), + }; + let range = clause.syntax().text_range(); + let selection_range = match &clause { + FunctionOrMacroClause::FunctionClause(clause) => match clause.name() { + None => range, + Some(name) => name.syntax().text_range(), + }, + FunctionOrMacroClause::MacroCallExpr(_) => range, + }; + let symbol = DocumentSymbol { + name: format!("{function_name_no_arity}{clause_name}"), + kind: SymbolKind::Function, + range, + selection_range, + deprecated: self.deprecated, + detail: Some(function_name), + children: None, + }; + children.push(symbol); + } + let selection_range = children.first().map_or(range, |c| c.selection_range); + let children = if children.len() > 0 { + Some(children) + } else { + None + }; + DocumentSymbol { + name: self.function.name.to_string(), + kind: SymbolKind::Function, + range, + selection_range, + deprecated: self.deprecated, + detail: None, + children, + } + } +} + +impl ToDocumentSymbol for TypeAliasDef { + fn to_document_symbol(&self, db: &dyn MinDefDatabase) -> DocumentSymbol { + let source = self.source(db.upcast()); + let range = source.syntax().text_range(); + let selection_range = match &source.type_name() { + None => range, + Some(name) => name.syntax().text_range(), + }; + DocumentSymbol { + name: self.name().to_string(), + kind: SymbolKind::Type, + range, + selection_range, + deprecated: false, + detail: None, + children: None, + } + } +} + +impl ToDocumentSymbol for RecordDef { + fn to_document_symbol(&self, db: &dyn MinDefDatabase) -> DocumentSymbol { + let source = self.source(db.upcast()); + let range = source.syntax().text_range(); + let selection_range = match &source.name() { + None => range, + Some(name) => name.syntax().text_range(), + }; + DocumentSymbol { + name: self.record.name.to_string(), + kind: SymbolKind::Record, + range, + selection_range, + deprecated: false, + detail: None, + children: None, + } + } +} + +impl ToDocumentSymbol for DefineDef { + fn to_document_symbol(&self, db: &dyn MinDefDatabase) -> DocumentSymbol { + let source = self.source(db.upcast()); + let range = source.syntax().text_range(); + let selection_range = if let Some(lhs) = &source.lhs() { + lhs.syntax().text_range() + } else { + range + }; + DocumentSymbol { + name: self.define.name.to_string(), + kind: SymbolKind::Define, + range, + selection_range, + deprecated: false, + detail: None, + children: None, + } + } +} + +// Feature: Document Symbols +// +// Provides a list of the symbols defined in the file. Can be used to +// +// * fuzzy search symbol in a file (super useful) +// * draw breadcrumbs to describe the context around the cursor +// * draw outline of the file +// +// |=== +// | Editor | Shortcut +// +// | VS Code | kbd:[Ctrl+Shift+O] +// |=== +pub(crate) fn document_symbols(db: &RootDatabase, file_id: FileId) -> Vec { + let sema = Semantic::new(db); + let def_map = sema.def_map(file_id); + + let mut res = Vec::new(); + + for (name, def) in def_map.get_functions() { + if def.file.file_id == file_id { + let mut symbol = def.to_document_symbol(db); + if def_map.is_deprecated(name) { + symbol.deprecated = true; + } + res.push(symbol); + } + } + for (_name, def) in def_map.get_records() { + if def.file.file_id == file_id { + res.push(def.to_document_symbol(db)); + } + } + for (_name, def) in def_map.get_macros() { + if def.file.file_id == file_id { + res.push(def.to_document_symbol(db)); + } + } + for (_name, def) in def_map.get_types() { + if def.file.file_id == file_id { + res.push(def.to_document_symbol(db)); + } + } + + res.sort_by(|a, b| a.range.start().cmp(&b.range.start())); + + res +} + +#[cfg(test)] +mod tests { + + use elp_ide_db::elp_base_db::FileRange; + + use crate::fixture; + + fn check(fixture: &str) { + let (analysis, pos, mut expected) = fixture::annotations(fixture); + let file_id = pos.file_id; + let symbols = analysis.document_symbols(file_id).unwrap(); + + let mut actual = Vec::new(); + for symbol in symbols { + actual.push(( + FileRange { + file_id, + range: symbol.selection_range, + }, + symbol.to_string(), + )); + if let Some(children) = symbol.children { + for child in children { + actual.push(( + FileRange { + file_id, + range: child.selection_range, + }, + child.to_string(), + )) + } + } + } + actual.sort_by_key(|(file_range, _)| file_range.range.start()); + expected.sort_by_key(|(file_range, _)| file_range.range.start()); + + assert_eq!( + expected, actual, + "\nExpected:\n{expected:#?}\n\nActual:\n{actual:#?}" + ) + } + + #[test] + fn test_file_structure() { + check( + r#"~ + -module(file_structure_test). + + -export([ a/1, b/0, c/0]). + + -record(my_first_record, {my_integer :: my_integer(), my_atom :: atom() }). +%% ^^^^^^^^^^^^^^^ Record | my_first_record + -record(my_second_record, {my_list :: [] }). +%% ^^^^^^^^^^^^^^^^ Record | my_second_record + -type my_integer() :: integer(). +%% ^^^^^^^^^^^^ Type | my_integer/0 + + -define(MEANING_OF_LIFE, 42). +%% ^^^^^^^^^^^^^^^ Define | MEANING_OF_LIFE + -define(MEANING_OF_LIFE(X), X). % You are the owner of your own destiny. +%% ^^^^^^^^^^^^^^^^^^ Define | MEANING_OF_LIFE/1 + + a(_) -> a. +%% ^ Function | a/1 +%% ^ Function | a(_) | a/1 + b() -> b. +%% ^ Function | b/0 +%% ^ Function | b() | b/0 + + c() -> +%% ^ Function | c/0 +%% ^ Function | c() | c/0 + a(), + b(), + ok. + + ?MEANING_OF_LIFE(X, Y) -> +%% ^^^^^^^^^^^^^^^^ Function | [missing name]/2 +%% ^^^^^^^^^^^^^^^^ Function | [missing name](X, Y) | [missing name]/2 + X + Y. +"#, + ); + } + + #[test] + fn test_deprecated_function() { + check( + r#"~ + -module(main). + -export([ a/1, b/0]). + -deprecated({a, 1}). + a(_) -> a. +%% ^ Function | a/1 | deprecated +%% ^ Function | a(_) | a/1 | deprecated + b() -> b. +%% ^ Function | b/0 +%% ^ Function | b() | b/0 +"#, + ); + } + + #[test] + fn test_multiple_clauses() { + check( + r#"~ + -module(main). + -export([ a/1, b/0]). + -deprecated({a, 1}). + a(1) -> 1; +%% ^ Function | a/1 | deprecated +%% ^ Function | a(1) | a/1 | deprecated + a(2) -> 2. +%% ^ Function | a(2) | a/1 | deprecated + b() -> b. +%% ^ Function | b/0 +%% ^ Function | b() | b/0 +"#, + ); + } + + #[test] + fn test_header_file() { + check( + r#" +//- /header.hrl + -define(INCLUDED_MACRO, included). + -record(included_record, {my_field :: integer()}). + -type included_type() :: integer(). + included_function() -> ok. + +//- /main.erl + -module(main).~ + -include("header.hrl"). + -define(LOCAL_MACRO, local). +%% ^^^^^^^^^^^ Define | LOCAL_MACRO + -record(included_record, {my_field :: integer()}). +%% ^^^^^^^^^^^^^^^ Record | included_record + -type local_type() :: integer(). +%% ^^^^^^^^^^^^ Type | local_type/0 + local_function() -> ok. +%% ^^^^^^^^^^^^^^ Function | local_function/0 +%% ^^^^^^^^^^^^^^ Function | local_function() | local_function/0 +"#, + ); + } +} diff --git a/crates/ide/src/expand_macro.rs b/crates/ide/src/expand_macro.rs new file mode 100644 index 0000000000..a612e4685d --- /dev/null +++ b/crates/ide/src/expand_macro.rs @@ -0,0 +1,586 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_ide_db::helpers::pick_best_token; +use elp_ide_db::RootDatabase; +use elp_syntax::ast; +use elp_syntax::AstNode; +use elp_syntax::SyntaxKind; +use hir::Semantic; + +use crate::FilePosition; + +#[derive(Debug)] +pub struct ExpandedMacro { + pub name: String, + pub expansion: String, +} + +// Feature: Expand Macro Recursively +// +// Shows the full macro expansion of the macro at current cursor. +// +// |=== +// | Editor | Action Name +// +// | VS Code | **Erlang: Expand Macro** +// |=== +// +pub(crate) fn expand_macro(db: &RootDatabase, position: FilePosition) -> Option { + let sema = Semantic::new(db); + let source_file = sema.parse(position.file_id); + + // Temporary for T153426323 + let _pctx = stdx::panic_context::enter(format!("\nexpand_macro")); + let tok = pick_best_token( + source_file.value.syntax().token_at_offset(position.offset), + |kind| match kind { + SyntaxKind::ATOM => 1, + SyntaxKind::VAR => 1, + _ => 0, + }, + )?; + + tok.parent_ancestors().find_map(|node| { + let mac = ast::MacroCallExpr::cast(node)?; + let (name, expansion) = sema.expand(source_file.with_value(&mac))?; + Some(ExpandedMacro { + name: name.to_string(), + expansion, + }) + }) +} + +#[cfg(test)] +mod tests { + use expect_test::expect; + use expect_test::Expect; + + use crate::fixture; + + fn check(elp_fixture: &str, expect: Expect) { + let (analysis, pos) = fixture::position(elp_fixture); + + let expansion = match analysis.expand_macro(pos).unwrap() { + Some(it) => format!("{}{}", it.name, it.expansion), + None => "***EXPANSION FAILED***".to_string(), + }; + expect.assert_eq(&expansion); + } + + #[test] + fn macro_expand_line_macro() { + check( + r#" +-module(foo). +-bar() -> ?L~INE. +"#, + expect![[r#" + LINE + 0 + "#]], + ); + } + + #[test] + fn macro_expand_file_macro() { + check( + r#" +-module(foo). +-bar() -> ?F~ILE. +"#, + expect![[r#" + FILE + "foo.erl" + "#]], + ); + } + + #[test] + fn macro_expand_constant_macro() { + check( + r#" +-module(foo). +-define(FOO,foo). +bar() -> ?F~OO. +"#, + expect![[r#" + FOO + 'foo' + "#]], + ); + } + + #[test] + fn macro_expand_constant_macro2() { + check( + r#" +-module(foo). +-define(FOO,foo + 1). +bar() -> ?F~OO. +"#, + expect![[r#" + FOO + ('foo' + 1) + "#]], + ); + } + + #[test] + fn macro_expand_simple_param_macro() { + check( + r#" +-module(foo). +-define(FOO(X),foo+X). +bar() -> ?F~OO(4). +"#, + expect![[r#" + FOO/1 + ('foo' + 4) + "#]], + ); + } + + #[test] + fn macro_expand_simple_param_macro2() { + check( + r#" +-module(foo). +-define(FOO(X,Y),foo+X+Y + 1). +bar() -> ?F~OO(4,5). +"#, + expect![[r#" + FOO/2 + ((('foo' + 4) + 5) + 1) + "#]], + ); + } + + #[test] + fn macro_expand_simple_param_macro3() { + check( + r#" +-module(foo). +-define(FOO(X,Y),foo+X+Y + 1). +bar() -> ?F~OO(4,(baz(42))). +"#, + expect![[r#" + FOO/2 + ((('foo' + 4) + 'baz'( + 42 + )) + 1) + "#]], + ); + } + + #[test] + fn macro_expand_param_macro_args_mismatch() { + check( + r#" +-module(foo). +-define(FOO(X),foo+X). +bar() -> ?F~OO(4,5). +"#, + expect!["***EXPANSION FAILED***"], + ); + } + + #[test] + fn macro_expand_missing_macro() { + check( + r#" +-module(foo). +bar() -> ?F~OO. +"#, + expect!["***EXPANSION FAILED***"], + ); + } + + #[test] + fn macro_expand_recursive_fail() { + check( + r#" +-module(foo). +-define(BAZ, ?BAZ). +-define(FOO(X),foo+X+?BAZ). +bar() -> ?F~OO(4). +"#, + expect![[r#" + FOO/1 + (('foo' + 4) + [missing]) + "#]], + ); + } + + #[test] + fn macro_expand_recursive() { + check( + r#" +-module(foo). +-define(BAZ, baz). +-define(FOO(X),foo+X+?BAZ). +bar() -> ?F~OO(4). +"#, + expect![[r#" + FOO/1 + (('foo' + 4) + 'baz') + "#]], + ); + } + + #[test] + fn macro_expand_recursive_multiple_fail() { + check( + r#" +-module(foo). +-define(BAZ, ?BAR(6)). +-define(BAR(X), ?FOO(X)). +-define(FOO(X),foo+X+?BAZ). +bar() -> ?F~OO(4). +"#, + expect![[r#" + FOO/1 + (('foo' + 4) + [missing]) + "#]], + ); + } + + #[test] + fn macro_expand_recursive_multiple() { + check( + r#" +-module(foo). +-define(BAZ, ?BAR(6)). +-define(BAR(X), foo(X)). +-define(FOO(X),foo+X+?BAZ). +bar() -> ?F~OO(4). +"#, + expect![[r#" + FOO/1 + (('foo' + 4) + 'foo'( + 6 + )) + "#]], + ); + } + + #[test] + fn macro_expand_comment_in_rhs() { + check( + r#" +-module(foo). +-define(FOO,[ +%% comment +1, +2 +]). +bar() -> ?F~OO. +"#, + expect![[r#" + FOO + [ + 1, + 2 + ] + "#]], + ); + } + + #[test] + fn macro_expand_wtf() { + check( + r#" +-module(foo). + +-define(VAL, val). +-define(ALL, all). +-define(assertA(PARAM_A, Type, Expected), begin + ((fun() -> + {ok, Actual} = lookup_mod:get(a_mod:get_val(PARAM_A), Type), + ?assertEqual(length(Expected), length(Actual)), + DebugComment = [{actual, Actual}, {expected, Expected}], + SortFun = fun(A, B) -> maps:get(code, A) =< maps:get(code, B) end, + lists:foreach( + fun({ExpectedVal, ActualVal}) -> + ?assertEqual(maps:get(code, ExpectedVal), maps:get(code, ActualVal), DebugComment), + ?assertEqual(maps:get(key_val, ExpectedVal, undefined), maps:get(key_val, ActualVal, undefined)), + ExpectedType = + case Type of + ?ALL -> + maps:get(type, ExpectedVal, missing_expected_type); + _ -> + Type + end, + ?assertEqual(ExpectedType, maps:get(type, ActualVal)), + ?assert(maps:is_key(ctime, ActualVal)) + end, + lists:zip(lists:sort(SortFun, Expected), lists:sort(SortFun, Actual)) + ) + end)()) +end). +baz() -> + ?asser~tA(Alice, ?VAL, []), + ok. +"#, + expect![[r#" + assertA/3 + begin + fun + () -> + { + 'ok', + Actual + } = 'lookup_mod':'get'( + 'a_mod':'get_val'( + Alice + ), + 'val' + ), + [missing], + DebugComment = [ + { + 'actual', + Actual + }, + { + 'expected', + [] + } + ], + SortFun = fun + (A, B) -> + ('maps':'get'( + 'code', + A + ) =< 'maps':'get'( + 'code', + B + )) + end, + 'lists':'foreach'( + fun + ({ + ExpectedVal, + ActualVal + }) -> + [missing], + [missing], + ExpectedType = case 'val' of + 'all' -> + 'maps':'get'( + 'type', + ExpectedVal, + 'missing_expected_type' + ); + _ -> + 'val' + end, + [missing], + [missing] + end, + 'lists':'zip'( + 'lists':'sort'( + SortFun, + [] + ), + 'lists':'sort'( + SortFun, + Actual + ) + ) + ) + end() + end + "#]], + ); + } + + #[test] + fn macro_expand_wtf2() { + check( + r#" +-module(foo). + +-define(WA_QR_TYPE_MESSAGE, qr_type_message). +-define(WA_QR_TYPE_ALL, qr_type_all). +-define(assertQrs(WID, Type, ExpectedQrs), + ExpectedType = + case Type of + ?WA_QR_TYPE_ALL -> + maps:get(type, ExpectedQr, missing_expected_type); + _ -> + Type + end, +). +baz() -> + ?asser~tQrs(AliceWID, ?WA_QR_TYPE_MESSAGE, []), + ok. +"#, + expect![[r#" + assertQrs/3 + ExpectedType = case 'qr_type_message' of + 'qr_type_all' -> + 'maps':'get'( + 'type', + ExpectedQr, + 'missing_expected_type' + ); + _ -> + 'qr_type_message' + end + "#]], + ); + } + + #[test] + fn macro_expand_case() { + check( + r#" +-module(foo). + +-define(FOO(X), + case X of + 1 -> + one; + _ -> + X + end). +baz() -> + ?F~OO(3), + ok. +"#, + expect![[r#" + FOO/1 + case 3 of + 1 -> + 'one'; + _ -> + 3 + end + "#]], + ); + } + + #[test] + fn macro_expand_include_file() { + check( + r#" +//- /include/foo.hrl include_path:/include +-define(FOO,3). +//- /src/foo.erl +-module(foo). +-include("foo.hrl"). +bar() -> ?F~OO. +"#, + expect![[r#" + FOO + 3 + "#]], + ); + } + + #[test] + fn macro_expand_no_param_macro_1() { + check( + r#" +-module(foo). +-define(C, m:f). +f() -> + ?~C(). +"#, + expect![[r#" + C + 'm':'f'() + "#]], + ); + } + #[test] + fn macro_expand_no_param_macro_2() { + check( + r#" +-module(foo). +-define(C, m:f). +f() -> + ?~C(1,2). +"#, + expect![[r#" + C + 'm':'f'( + 1, + 2 + ) + "#]], + ); + } + + #[test] + fn macro_expand_empty_param_macro() { + check( + r#" +-module(foo). +-define(F0(), c). +f() -> + ?~?F0. +"#, + expect!["***EXPANSION FAILED***"], + ); + } + + #[test] + fn macro_expand_multiple_arities() { + check( + r#" +-module(foo). +-define(C(), 0). +-define(C(X), X). +-define(C(X,Y), X+Y). +f() -> + ?~C(1,2). +"#, + expect![[r#" + C/2 + (1 + 2) + "#]], + ); + } + + #[test] + fn macro_expand_no_param_macro() { + check( + r#" +-module(foo). +-export([get_partition/1]). +-define(HASH_FUN, wa_pg2:hash). +get_partition(Who) -> + ?~HASH_FUN(Who, 5). +"#, + expect![[r#" + HASH_FUN + 'wa_pg2':'hash'( + Who, + 5 + ) + "#]], + ); + } + + #[test] + fn macro_expand_outside_expressions() { + check( + r#" + +-module(foo). +-define(ARITY, 0). +-export([bar/?A~RITY]). +bar() -> ok. +"#, + expect!["***EXPANSION FAILED***"], + ); + } +} diff --git a/crates/ide/src/extend_selection.rs b/crates/ide/src/extend_selection.rs new file mode 100644 index 0000000000..1e7373ebb6 --- /dev/null +++ b/crates/ide/src/extend_selection.rs @@ -0,0 +1,373 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_ide_db::RootDatabase; +use elp_syntax::ast::AstNode; +use elp_syntax::Direction; +use elp_syntax::NodeOrToken; +use elp_syntax::SyntaxKind::*; +use elp_syntax::SyntaxKind::{self}; +use elp_syntax::SyntaxNode; +use elp_syntax::SyntaxToken; +use elp_syntax::TextRange; +use elp_syntax::TextSize; +use elp_syntax::TokenAtOffset; +use hir::Semantic; + +use crate::FileRange; + +// Feature: Expand and Shrink Selection +// +// Extends or shrinks the current selection to the encompassing syntactic construct +// (expression, function, module, etc). It works with multiple cursors. +// +// |=== +// | Editor | Shortcut +// +// | VS Code | kbd:[Alt+Shift+→], kbd:[Alt+Shift+←] +// |=== +pub(crate) fn extend_selection(db: &RootDatabase, frange: FileRange) -> TextRange { + let sema = Semantic::new(db); + let source_file = sema.parse(frange.file_id); + try_extend_selection(source_file.value.syntax(), frange).unwrap_or(frange.range) +} + +fn try_extend_selection(root: &SyntaxNode, frange: FileRange) -> Option { + let range = frange.range; + + let string_kinds = [COMMENT, STRING, BINARY]; + let list_kinds = [ + ANONYMOUS_FUN, + BIT_TYPE_LIST, + BLOCK_EXPR, + CALLBACK, + CASE_EXPR, + CLAUSE_BODY, + CONCATABLES, + EXPORT_ATTRIBUTE, + EXPORT_TYPE_ATTRIBUTE, + EXPR_ARGS, + FUN_DECL, + GUARD_CLAUSE, + GUARD, + IF_EXPR, + IMPORT_ATTRIBUTE, + LC_EXPRS, + LIST, + MACRO_CALL_ARGS, + MAP_EXPR_UPDATE, + MAP_EXPR, + OPTIONAL_CALLBACKS_ATTRIBUTE, + PP_INCLUDE_LIB, + PP_INCLUDE, + RECEIVE_EXPR, + RECORD_DECL, + RECORD_EXPR, + RECORD_UPDATE_EXPR, + REPLACEMENT_CR_CLAUSES, + REPLACEMENT_FUNCTION_CLAUSES, + REPLACEMENT_GUARD_AND, + REPLACEMENT_GUARD_OR, + SOURCE_FILE, + SPEC, + TRY_AFTER, + TRY_EXPR, + TUPLE, + TYPE_GUARDS, + VAR_ARGS, + ]; + + if range.is_empty() { + let offset = range.start(); + // Temporary for T153426323 + let _pctx = stdx::panic_context::enter(format!("\ntry_extend_selection")); + let mut leaves = root.token_at_offset(offset); + if leaves.clone().all(|it| it.kind() == WHITESPACE) { + return Some(extend_ws(root, leaves.next()?, offset)); + } + let leaf_range = match leaves { + TokenAtOffset::None => return None, + TokenAtOffset::Single(l) => { + if string_kinds.contains(&l.kind()) { + extend_single_word_in_comment_or_string(&l, offset) + .unwrap_or_else(|| l.text_range()) + } else { + l.text_range() + } + } + TokenAtOffset::Between(l, r) => pick_best(l, r).text_range(), + }; + return Some(leaf_range); + }; + let node = match root.covering_element(range) { + NodeOrToken::Token(token) => { + if token.text_range() != range { + return Some(token.text_range()); + } + token.parent()? + } + NodeOrToken::Node(node) => node, + }; + + if node.text_range() != range { + return Some(node.text_range()); + } + + let node = shallowest_node(&node); + + if node.parent().map(|n| list_kinds.contains(&n.kind())) == Some(true) { + if let Some(range) = extend_list_item(&node) { + return Some(range); + } + } + + node.parent().map(|it| it.text_range()) +} + +/// Find the shallowest node with same range, which allows us to traverse siblings. +fn shallowest_node(node: &SyntaxNode) -> SyntaxNode { + node.ancestors() + .take_while(|n| n.text_range() == node.text_range()) + .last() + .unwrap() +} + +fn extend_single_word_in_comment_or_string( + leaf: &SyntaxToken, + offset: TextSize, +) -> Option { + let text: &str = leaf.text(); + let cursor_position: u32 = (offset - leaf.text_range().start()).into(); + + let (before, after) = text.split_at(cursor_position as usize); + + fn non_word_char(c: char) -> bool { + !(c.is_alphanumeric() || c == '_') + } + + let start_idx = before.rfind(non_word_char)? as u32; + let end_idx = after.find(non_word_char).unwrap_or_else(|| after.len()) as u32; + + let from: TextSize = (start_idx + 1).into(); + let to: TextSize = (cursor_position + end_idx).into(); + + // Temporary for T148094436 + let _pctx = stdx::panic_context::enter(format!("\nextend_single_word_in_comment_or_string")); + let range = TextRange::new(from, to); + if range.is_empty() { + None + } else { + Some(range + leaf.text_range().start()) + } +} + +fn extend_ws(root: &SyntaxNode, ws: SyntaxToken, offset: TextSize) -> TextRange { + let ws_text = ws.text(); + // Temporary for T148094436 + let _pctx = stdx::panic_context::enter(format!("\nextend_ws")); + let suffix = TextRange::new(offset, ws.text_range().end()) - ws.text_range().start(); + let prefix = TextRange::new(ws.text_range().start(), offset) - ws.text_range().start(); + let ws_suffix = &ws_text[suffix]; + let ws_prefix = &ws_text[prefix]; + if ws_text.contains('\n') && !ws_suffix.contains('\n') { + if let Some(node) = ws.next_sibling_or_token() { + let start = match ws_prefix.rfind('\n') { + Some(idx) => ws.text_range().start() + TextSize::from((idx + 1) as u32), + None => node.text_range().start(), + }; + let end = if root.text().char_at(node.text_range().end()) == Some('\n') { + node.text_range().end() + TextSize::of('\n') + } else { + node.text_range().end() + }; + return TextRange::new(start, end); + } + } + ws.text_range() +} + +fn pick_best(l: SyntaxToken, r: SyntaxToken) -> SyntaxToken { + return if priority(&r) > priority(&l) { r } else { l }; + fn priority(n: &SyntaxToken) -> usize { + match n.kind() { + WHITESPACE => 0, + _ => 1, + } + } +} + +/// Extend list item selection to include nearby delimiter and whitespace. +fn extend_list_item(node: &SyntaxNode) -> Option { + fn is_single_line_ws(node: &SyntaxToken) -> bool { + node.kind() == WHITESPACE && !node.text().contains('\n') + } + + fn nearby_delimiter( + delimiter_kind: SyntaxKind, + node: &SyntaxNode, + dir: Direction, + ) -> Option { + node.siblings_with_tokens(dir) + .skip(1) + .find(|node| match node { + NodeOrToken::Node(_) => true, + NodeOrToken::Token(it) => !is_single_line_ws(it), + }) + .and_then(|it| it.into_token()) + .filter(|node| node.kind() == delimiter_kind) + } + + let delimiter = SyntaxKind::ANON_COMMA; + + if let Some(delimiter_node) = nearby_delimiter(delimiter, node, Direction::Next) { + // Include any following whitespace when delimiter is after list item. + let final_node = delimiter_node + .next_sibling_or_token() + .and_then(|it| it.into_token()) + .filter(is_single_line_ws) + .unwrap_or(delimiter_node); + + // Temporary for T148094436 + let _pctx = stdx::panic_context::enter(format!("\nextend_list_item1")); + return Some(TextRange::new( + node.text_range().start(), + final_node.text_range().end(), + )); + } + if let Some(delimiter_node) = nearby_delimiter(delimiter, node, Direction::Prev) { + // Temporary for T148094436 + let _pctx = stdx::panic_context::enter(format!("\nextend_list_item2")); + return Some(TextRange::new( + delimiter_node.text_range().start(), + node.text_range().end(), + )); + } + + None +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::fixture; + + fn do_check(before: &str, afters: &[&str]) { + let (analysis, position) = fixture::position(before); + let before = analysis.file_text(position.file_id).unwrap(); + let range = TextRange::empty(position.offset); + let mut frange = FileRange { + file_id: position.file_id, + range, + }; + + for &after in afters { + frange.range = analysis.extend_selection(frange).unwrap(); + let actual = &before[frange.range]; + assert_eq!(after, actual); + } + } + + #[test] + fn test_extend_selection_arith_expression() { + do_check( + r#"foo() -> ~1 + 1"#, + &["1", "1 + 1", "-> 1 + 1", "foo() -> 1 + 1"], + ); + } + + #[test] + fn test_extend_selection_function_args() { + do_check(r#"foo(X~) -> ok."#, &["X", "(X)", "foo(X) -> ok"]); + do_check( + r#"foo(X, ~Y) -> ok."#, + &["Y", ", Y", "(X, Y)", "foo(X, Y) -> ok", "foo(X, Y) -> ok."], + ); + do_check( + r#"foo(X, ~Y :: integer()) -> ok."#, + &[ + "Y", + "Y ::", + "Y :: integer()", + ", Y :: integer()", + "(X, Y :: integer())", + "foo(X, Y :: integer()) -> ok", + "foo(X, Y :: integer()) -> ok.", + ], + ); + do_check( + r#"foo({X, ~Y :: integer(), Z}) -> ok."#, + &[ + "Y", + "Y ::", + "Y :: integer()", + "Y :: integer(), ", + "{X, Y :: integer(), Z}", + "({X, Y :: integer(), Z})", + "foo({X, Y :: integer(), Z}) -> ok", + "foo({X, Y :: integer(), Z}) -> ok.", + ], + ); + } + + #[test] + fn test_extend_selection_tuples() { + do_check( + r#"foo() -> {1, t~wo, "three"}"#, + &["two", "two, ", "{1, two, \"three\"}"], + ); + } + + #[test] + fn test_extend_selection_lists() { + do_check( + r#"foo() -> [1, tw~o, "three"]"#, + &["two", "two, ", "[1, two, \"three\"]"], + ); + } + + #[test] + fn test_extend_selection_strings() { + do_check( + r#" +-module(strings). + +some_strings() -> + X = "abc", %% Look, a comment! + Y = "123" + "4~56" + "789". +"#, + &[ + "456", + "\"456\"", + "\"123\"\n \"456\"\n \"789\"", + "Y = \"123\"\n \"456\"\n \"789\"", + "->\n X = \"abc\", %% Look, a comment!\n Y = \"123\"\n \"456\"\n \"789\"", + ], + ); + } + + #[test] + fn test_extend_guards() { + do_check( + r#" +-module(guards). + +foo(X) when is_in~teger(X) andalso not is_boolean(X) -> + 42. +"#, + &[ + "is_integer", + "is_integer(X)", + "is_integer(X) andalso not is_boolean(X)", + "foo(X) when is_integer(X) andalso not is_boolean(X) ->\n 42", + ], + ); + } +} diff --git a/crates/ide/src/fixture.rs b/crates/ide/src/fixture.rs new file mode 100644 index 0000000000..8787bc1ecf --- /dev/null +++ b/crates/ide/src/fixture.rs @@ -0,0 +1,67 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! Utilities for creating `Analysis` instances for tests. + +use elp_ide_db::elp_base_db::fixture::WithFixture; +use elp_ide_db::elp_base_db::fixture::CURSOR_MARKER; +use elp_ide_db::elp_base_db::FileId; +use elp_ide_db::elp_base_db::FileRange; +use elp_ide_db::elp_base_db::SourceDatabase; +use elp_ide_db::RootDatabase; + +use crate::Analysis; +use crate::AnalysisHost; +use crate::FilePosition; + +/// Creates analysis from a single file fixture, returns the file id +pub(crate) fn single_file(fixture: &str) -> (Analysis, FileId) { + let (db, file_id) = RootDatabase::with_single_file(fixture); + let host = AnalysisHost { db }; + (host.analysis(), file_id) +} + +/// Creates analysis from a multi-file fixture, returns position marked with the [`CURSOR_MARKER`] +pub(crate) fn position(fixture: &str) -> (Analysis, FilePosition) { + let (db, position) = RootDatabase::with_position(fixture); + let host = AnalysisHost { db }; + (host.analysis(), position) +} + +/// Creates analysis from a multi-file fixture +pub(crate) fn multi_file(fixture: &str) -> Analysis { + let (db, _) = RootDatabase::with_fixture(fixture); + let host = AnalysisHost { db }; + host.analysis() +} + +/// Creates analysis from a multi-file fixture, returns first position marked with [`CURSOR_MARKER`] +/// and annotations marked with sequence of %% ^^^ +pub fn annotations(fixture: &str) -> (Analysis, FilePosition, Vec<(FileRange, String)>) { + let (db, fixture) = RootDatabase::with_fixture(fixture); + let (file_id, range_or_offset) = fixture + .file_position + .expect(&format!("expected a marker ({})", CURSOR_MARKER)); + let offset = range_or_offset.expect_offset(); + + let annotations = fixture.annotations(&db); + let analysis = AnalysisHost { db }.analysis(); + (analysis, FilePosition { file_id, offset }, annotations) +} + +pub fn check_no_parse_errors(analysis: &Analysis, file_id: FileId) -> Option<()> { + // Check that we have a syntactically valid starting point + let text = analysis.file_text(file_id).ok()?; + let parse = analysis.db.parse(file_id); + let errors = parse.errors(); + if !errors.is_empty() { + assert_eq!(format!("{:?}\nin\n{text}", errors), ""); + }; + Some(()) +} diff --git a/crates/ide/src/folding_ranges.rs b/crates/ide/src/folding_ranges.rs new file mode 100644 index 0000000000..fd58050d51 --- /dev/null +++ b/crates/ide/src/folding_ranges.rs @@ -0,0 +1,129 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_ide_db::elp_base_db::FileId; +use elp_ide_db::RootDatabase; +use elp_syntax::AstNode; +use elp_syntax::TextRange; +use hir::Semantic; + +#[derive(Debug, PartialEq, Eq)] +pub enum FoldKind { + Function, + Record, +} + +#[derive(Debug)] +pub struct Fold { + pub range: TextRange, + pub kind: FoldKind, +} + +// Feature: Folding +// +// Defines folding regions for functions. +pub(crate) fn folding_ranges(db: &RootDatabase, file_id: FileId) -> Vec { + let mut folds = Vec::new(); + let sema = Semantic::new(db); + let def_map = sema.def_map(file_id); + // Functions + for (_name, def) in def_map.get_functions() { + folds.push(Fold { + kind: FoldKind::Function, + range: def.source(db).syntax().text_range(), + }) + } + // Records + for (_name, def) in def_map.get_records() { + folds.push(Fold { + kind: FoldKind::Record, + range: def.source(db).syntax().text_range(), + }) + } + folds +} + +#[cfg(test)] +mod tests { + use elp_ide_db::elp_base_db::fixture::extract_tags; + + use super::*; + use crate::fixture; + + fn check(fixture: &str) { + let (ranges, fixture) = extract_tags(fixture.trim_start(), "fold"); + let (analysis, file_id) = fixture::single_file(&fixture); + let mut folds = analysis.folding_ranges(file_id).unwrap_or_default(); + folds.sort_by_key(|fold| (fold.range.start(), fold.range.end())); + + assert_eq!( + folds.len(), + ranges.len(), + "The amount of folds is different than the expected amount" + ); + + for (fold, (range, attr)) in folds.iter().zip(ranges.into_iter()) { + assert_eq!( + fold.range.start(), + range.start(), + "mismatched start of folding ranges" + ); + assert_eq!( + fold.range.end(), + range.end(), + "mismatched end of folding ranges" + ); + + let kind = match fold.kind { + FoldKind::Function | FoldKind::Record => "region", + }; + assert_eq!(kind, &attr.unwrap()); + } + } + + #[test] + fn test_function() { + check( + r#" +-module(my_module). +one() -> + ok. +"#, + ) + } + + #[test] + fn test_record() { + check( + r#" +-module(my_module). +-record(my_record, {a :: integer(), b :: binary()}). +"#, + ) + } + + #[test] + fn test_records_and_functions() { + check( + r#" +-module(my_module). + +-record(my_record, {a :: integer(), + b :: binary()}). + +one() -> + ok. + +two() -> + ok, + ok. +"#, + ); + } +} diff --git a/crates/ide/src/handlers/get_docs.rs b/crates/ide/src/handlers/get_docs.rs new file mode 100644 index 0000000000..da3087dbc7 --- /dev/null +++ b/crates/ide/src/handlers/get_docs.rs @@ -0,0 +1,31 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_ide_db::docs::Doc; +use elp_ide_db::elp_base_db::FilePosition; +use elp_ide_db::elp_base_db::FileRange; +use elp_ide_db::find_best_token; +use elp_ide_db::RootDatabase; +use hir::Semantic; + +pub(crate) fn get_doc_at_position( + db: &RootDatabase, + position: FilePosition, +) -> Option<(Doc, FileRange)> { + let sema = Semantic::new(db); + let docs = elp_ide_db::docs::Documentation::new(db, &sema); + let token = find_best_token(&sema, position)?; + + let range = FileRange { + file_id: token.file_id, + range: token.value.text_range(), + }; + let doc = Doc::from_reference(&docs, &token); + doc.map(|d| (d, range)) +} diff --git a/crates/ide/src/handlers/goto_definition.rs b/crates/ide/src/handlers/goto_definition.rs new file mode 100644 index 0000000000..e7630a12ab --- /dev/null +++ b/crates/ide/src/handlers/goto_definition.rs @@ -0,0 +1,3458 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_ide_db::elp_base_db::FilePosition; +use elp_ide_db::find_best_token; +use elp_ide_db::RootDatabase; +use elp_ide_db::SymbolClass; +use hir::Semantic; + +use crate::navigation_target::NavigationTarget; +use crate::navigation_target::ToNav; +use crate::RangeInfo; + +pub(crate) fn goto_definition( + db: &RootDatabase, + position: FilePosition, +) -> Option>> { + let sema = Semantic::new(db); + let token = find_best_token(&sema, position)?; + let targets = SymbolClass::classify(&sema, token.clone())? + .into_iter() + .map(|def| def.to_nav(db)) + .collect(); + Some(RangeInfo::new(token.value.text_range(), targets)) +} + +#[cfg(test)] +mod tests { + use crate::fixture; + use crate::tests::check_navs; + use crate::tests::check_no_parse_errors; + + #[track_caller] + fn check_expect_parse_error(fixture: &str) { + check_worker(fixture, false) + } + + #[track_caller] + fn check(fixture: &str) { + check_worker(fixture, true) + } + + #[track_caller] + fn check_worker(fixture: &str, check_parse_error: bool) { + let (analysis, position, expected) = fixture::annotations(fixture); + if check_parse_error { + check_no_parse_errors(&analysis, position.file_id); + } + + let navs = analysis + .goto_definition(position) + .unwrap() + .expect("no definition found") + .info; + + if navs.is_empty() { + panic!("got some with empty navs!"); + } + + check_navs(navs, expected); + } + + fn check_unresolved(fixture: &str) { + let (analysis, position) = fixture::position(fixture); + check_no_parse_errors(&analysis, position.file_id); + + match analysis.goto_definition(position).unwrap() { + Some(navs) if !navs.info.is_empty() => { + panic!("didn't expect this to resolve anywhere: {:?}", navs) + } + Some(_) => { + panic!("got some with empty navs!"); + } + None => {} + } + } + + #[test] + fn module_name() { + check( + r#" +//- /src/main.erl +-module(main). + +foo() -> anoth~er. + +//- /src/another.erl + -module(another). +%%^^^^^^^^^^^^^^^^^ +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +foo(anoth~er) -> ok. + +//- /src/another.erl + -module(another). +%%^^^^^^^^^^^^^^^^^ +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +-type x() :: anoth~er. + +//- /src/another.erl + -module(another). +%%^^^^^^^^^^^^^^^^^ +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +-compile({parse_transform, anoth~er}). + +//- /src/another.erl + -module(another). +%%^^^^^^^^^^^^^^^^^ +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +-attr(anoth~er). + +//- /src/another.erl + -module(another). +%%^^^^^^^^^^^^^^^^^ +"#, + ); + } + + #[test] + fn module_name_inside_otp() { + check( + r#" +//- /src/main.erl +-module(main). + +foo() -> l~ists. + +//- /opt/lib/stdlib-3.17/src/lists.erl otp_app:/opt/lib/stdlib-3.17 + -module(lists). +%%^^^^^^^^^^^^^^^ +"#, + ); + } + + #[test] + fn module_name_no_module_attr() { + check( + r#" +//- /src/main.erl +-module(main). + +foo() -> anoth~er. + +//- /src/another.erl +%%^file +foo(1) -> ok. +bar(2) -> ok. +"#, + ) + } + + #[test] + fn module_name_unresolved() { + check_unresolved( + r#" +//- /src/main.erl +-module(main). + +foo() -> anoth~er. +"#, + ) + } + + #[test] + fn local_call() { + check( + r#" +//- /src/main.erl +-module(main). + +foo() -> b~ar(). + + bar() -> ok. +%%^^^ +"#, + ) + } + + #[test] + fn local_call_from_record_def() { + check( + r#" +//- /src/main.erl +-module(main). + +-record(test, {init = i~nit() :: atom()}). + + init() -> #test.init. +%%^^^^ +"#, + ) + } + + #[test] + fn local_call_to_header() { + check( + r#" +//- /src/main.erl +-module(main). +-include("header.hrl"). + +foo() -> b~ar(). + +//- /src/header.hrl + bar() -> ok. +%%^^^ +"#, + ) + } + + #[test] + fn local_call_to_inner_header() { + check( + r#" +//- /src/main.erl +-module(main). +-include("header1.hrl"). +foo() -> b~ar(). + +//- /src/header1.hrl +-include("header2.hrl"). + +//- /src/header2.hrl + bar() -> ok. +%%^^^ +"#, + ) + } + + #[test] + fn cyclic_header() { + check( + r#" +//- /src/main.erl +-module(main). +-include("header.hrl"). +foo() -> b~ar(). + +//- /src/header.hrl +-include("header.hrl"). + + bar() -> ok. +%%^^^ +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). +-include("header1.hrl"). +foo() -> b~ar(). + +//- /src/header1.hrl +-include("header2.hrl"). + + bar() -> ok. +%%^^^ + +//- /src/header2.hrl +-include("header1.hrl"). +"#, + ); + + check_unresolved( + r#" +//- /src/main.erl +-module(main). +-include("header1.hrl"). +foo() -> b~ar(). +baz() -> ko(). + +//- /src/header1.hrl +-include("header2.hrl"). + ko() -> err. + +//- /src/header2.hrl +-include("header1.hrl"). + + bar() -> ok. +"#, + ); + } + + #[test] + fn local_type_alias() { + // BinaryOpExpr + check( + r#" +//- /src/main.erl +-module(main). + +-type foo() :: number(). +%% ^^^^^ +-type bar() :: f~oo() + atom(). +"#, + ); + + // Call + check( + r#" +//- /src/main.erl +-module(main). + +-type foo() :: number(). +%% ^^^^^ +-type bar() :: f~oo(). +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +-type foo() :: number(). +%% ^^^^^ +-opaque bar() :: f~oo(). +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +-type foo() :: number(). +%% ^^^^^ +-type id(X) :: X. +-type bar() :: id(f~oo()). +"#, + ); + + // MapExpr + check( + r#" +//- /src/main.erl +-module(main). + +-type foo() :: number(). +%% ^^^^^ +-opaque bar() :: #{atom() => f~oo()}. +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +-type foo() :: number(). +%% ^^^^^ +-type id(X) :: X. +-type bar() :: #{id(f~oo()) => atom()} | []. +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +-type foo() :: number(). +%% ^^^^^ +-type id(X) :: X. +-type bar() :: #{id(f~oo()) => foo()}. +"#, + ); + + // Pipe + check( + r#" +//- /src/main.erl +-module(main). + +-type foo() :: number(). +%% ^^^^^ +-opaque bar() :: binary() | f~oo(). +"#, + ); + + // RangeType + check( + r#" +//- /src/main.erl +-module(main). + +-type foo() :: 1. +%% ^^^^^ +-type bar() :: f~oo() .. 3. +"#, + ); + + // Record + check( + r#" +//- /src/main.erl +-module(main). + +-type foo() :: number(). +%% ^^^^^ +-record(rec, {foo}). +-type rec() :: #rec{foo :: f~oo()}. +"#, + ); + + // UnaryOpExpr + check( + r#" +//- /src/main.erl +-module(main). + +-type foo() :: number(). +%% ^^^^^ +-type bar() :: - f~oo(). +"#, + ); + + // FunType + check( + r#" +//- /src/main.erl +-module(main). + +-type foo() :: number(). +%% ^^^^^ +-type bar() :: fun((f~oo()) -> foo()). +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +-type foo() :: number(). +%% ^^^^^ +-type bar() :: fun((...) -> f~oo()). +"#, + ); + + // List + check( + r#" +//- /src/main.erl +-module(main). + +-type foo() :: number(). +%% ^^^^^ +-type bar() :: [fun((f~oo()) -> foo())]. +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +-type foo() :: number(). +%% ^^^^^ +-type bar() :: [fun((f~oo()) -> foo()), ...]. +"#, + ); + + // Paren + check( + r#" +//- /src/main.erl +-module(main). + +-type foo() :: number(). +%% ^^^^^ +-type bar(A) :: ([fun((f~oo()) -> A), ...]). +"#, + ); + + // Tuple + check( + r#" +//- /src/main.erl +-module(main). + +-type foo() :: number(). +%% ^^^^^ +-type bar(A) :: {f~oo(), A}. +"#, + ); + } + + #[test] + fn local_type_to_header() { + check( + r#" +//- /src/main.erl +-module(main). +-include("header.hrl"). + +-type bar() :: f~oo(). + +//- /src/header.hrl +-type foo() :: number(). +%% ^^^^^ +"#, + ); + } + + #[test] + fn remote_type_alias() { + check( + r#" +//- /src/main.erl +-module(main). + +-type foo() :: number(). +%% ^^^^^ +-type bar() :: main:f~oo(). +"#, + ); + + check( + r#" +//- /src/mod1.erl +-module(mod1). +-type foo() :: number(). +%% ^^^^^ + +//- /src/mod2.erl +-module(mod2). +-type bar() :: mod1:f~oo(). +"#, + ); + + check( + r#" +//- /src/mod1.erl +-module(mod1). + +-type foo() :: number(). +%% ^^^^^ + +//- /src/mod2.erl +-module(mod2). +-opaque bar() :: binary() | mod1:f~oo(). +"#, + ); + + check( + r#" +//- /src/mod1.erl +-module(mod1). + +-type foo() :: number(). +%% ^^^^^ + +//- /src/mod2.erl +-module(mod2). +-opaque bar() :: #{atom() => mod1:f~oo()}. +"#, + ); + } + + #[test] + fn remote_opaque_to_header() { + check( + r#" +//- /src/main.erl +-module(main). +-include("header.hrl"). + +-type bar() :: main:f~oo(). + +//- /src/header.hrl +-type foo() :: number(). +%% ^^^^^ +"#, + ); + } + + #[test] + fn local_opaque() { + check( + r#" +//- /src/main.erl +-module(main). + +-opaque foo() :: number(). +%% ^^^^^ +-type bar() :: f~oo(). +"#, + ) + } + + #[test] + fn remote_opaque() { + check( + r#" +//- /src/mod1.erl +-module(mod1). +-opaque foo() :: number(). +%% ^^^^^ + +//- /src/mod2.erl +-module(mod2). +-type bar() :: mod1:f~oo(). +"#, + ) + } + + #[test] + fn local_type_alias_from_record_def() { + check( + r#" +//- /src/main.erl +-module(main). + +-type foo(X) :: [X]. +%% ^^^^^^ + +-record(foo_container, {foo :: f~oo(ok)}). +main() -> #foo_container.foo. +"#, + ) + } + + #[test] + fn local_type_alias_from_spec() { + check( + r#" +//- /src/main.erl +-module(main). + +-type foo(X) :: [X]. +%% ^^^^^^ + +-spec bar(f~oo(atom())) -> foo(pid()). +bar(X) -> X. +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +-type foo(X) :: [X]. +%% ^^^^^^ + +-spec bar(foo(atom())) -> atom() | f~oo(pid()). +bar(X) -> X. +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +-type foo() :: []. +%% ^^^^^ + +-spec bar() -> XX when XX :: f~oo(). +bar() -> ok. +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +-type foo() :: []. +%% ^^^^^ + +-spec bar(In :: f~oo()) -> Out :: foo(). +bar(X) -> X. +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +-type foo() :: []. +%% ^^^^^ + +-spec bar(In :: foo()) -> Out :: f~oo(). +bar(X) -> X. +"#, + ); + } + + #[test] + fn local_type_alias_from_callback() { + check( + r#" +//- /src/main.erl +-module(main). + +-type foo(X) :: [X]. +%% ^^^^^^ + +-callback bar(f~oo(atom())) -> foo(pid()). +"#, + ); + } + + #[test] + fn record_from_spec() { + check( + r#" +//- /src/main.erl +-module(main). + +-record(foo, {}). +%% ^^^ + +-spec bar(#f~oo{}) -> pid(). +bar(X) -> X. +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +-record(foo, {}). +%% ^^^ + +-spec bar(atom()) -> atom() | #f~oo{}. +bar(X) -> X. +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +-type foo() :: []. +%% ^^^^^ + +-spec bar() -> XX when XX :: f~oo(). +bar() -> ok. +"#, + ); + } + + #[test] + fn remote_call() { + check( + r#" +//- /src/main.erl +-module(main). + +foo() -> main:b~ar(). + + bar() -> ok. +%%^^^ +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +foo() -> another:b~ar(). + +//- /src/another.erl +-module(another). + bar() -> ok. +%%^^^ +"#, + ); + + check_unresolved( + r#" +//- /src/main.erl +-module(main). + +foo() -> Another:b~ar(). +"#, + ) + } + + #[test] + fn remote_call_to_header() { + check( + r#" +//- /src/main.erl +-module(main). +-include("header.hrl"). + +foo() -> main:b~ar(). + +//- /src/header.hrl + bar() -> ok. +%%^^^ +"#, + ); + } + + #[test] + fn remote_call_module() { + check( + r#" +//- /src/main.erl +-module(main). + +foo() -> a~nother:bar(). + +//- /src/another.erl + -module(another). +%%^^^^^^^^^^^^^^^^^ +"#, + ) + } + + #[test] + fn behaviour_attribute() { + check( + r#" +//- /src/main.erl +-module(main). +-behaviour(a~nother). + +//- /src/another.erl + -module(another). +%%^^^^^^^^^^^^^^^^^ +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). +-behavior(a~nother). + +//- /src/another.erl + -module(another). +%%^^^^^^^^^^^^^^^^^ +"#, + ) + } + + #[test] + fn import_attribute() { + check( + r#" +//- /src/main.erl +-module(main). + +-import(a~nother, []). + +//- /src/another.erl + -module(another). +%%^^^^^^^^^^^^^^^^^ +"#, + ) + } + + #[test] + fn internal_fun() { + check( + r#" +//- /src/main.erl +-module(main). + +foo() -> fun a~nother/0. + + another() -> ok. +%%^^^^^^^ +"#, + ) + } + + #[test] + fn external_fun() { + check( + r#" +//- /src/main.erl +-module(main). + +foo() -> fun another:f~oo/0. + +//- /src/another.erl +-module(another). + + foo() -> ok. +%%^^^ +"#, + ) + } + + #[test] + fn external_fun_module() { + check( + r#" +//- /src/main.erl +-module(main). + +foo() -> fun a~nother:foo/0. + +//- /src/another.erl + -module(another). +%%^^^^^^^^^^^^^^^^^ +"#, + ) + } + + #[test] + fn spec() { + check( + r#" +//- /src/main.erl +-module(main). + +-spec f~oo() -> ok. + foo() -> ok. +%%^^^ +"#, + ) + } + + #[test] + fn record_name() { + check( + r#" +//- /src/main.erl +-module(main). + +-record(rec, {}). +%% ^^^ + +foo() -> #r~ec{}. +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +-record(rec, {}). +%% ^^^ + +foo() -> Expr#r~ec{}. +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +-record(rec, {}). +%% ^^^ + +foo() -> Expr#r~ec.field. +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +-record(rec, {}). +%% ^^^ + +foo() -> #r~ec.field. +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +-record(rec, {}). +%% ^^^ + +foo(#r~ec{}) -> ok. +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +-record(rec, {}). +%% ^^^ + +foo(#r~ec.field) -> ok. +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +-record(rec, {}). +%% ^^^ + +-type rec() :: #r~ec{}. +"#, + ); + } + + #[test] + fn record_name_to_header() { + check( + r#" +//- /src/main.erl +-module(main). +-include("header.hrl"). + +foo() -> #r~ec{}. + +//- /src/header.hrl +-record(rec, {}). +%% ^^^ +"#, + ); + } + + #[test] + fn record_field_name() { + check( + r#" +//- /src/main.erl +-module(main). + +-record(rec, {field}). +%% ^^^^^ + +foo() -> Expr#rec.f~ield. +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +-record(rec, {field}). +%% ^^^^^ + +foo() -> #rec.f~ield. +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +-record(rec, {field}). +%% ^^^^^ + +foo(#rec.f~ield) -> ok. +"#, + ); + } + + #[test] + fn record_field() { + check( + r#" +//- /src/main.erl +-module(main). + +-record(rec, {field}). +%% ^^^^^ + +foo() -> #rec{f~ield = ok}. +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +-record(rec, {field}). +%% ^^^^^ + +foo(Expr) -> Expr#rec{f~ield = ok}. +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +-record(rec, {field}). +%% ^^^^^ + +foo(#rec{f~ield = ok}) -> ok. +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +-record(rec, {field}). +%% ^^^^^ + +-type rec() :: #rec{f~ield :: ok}. +"#, + ); + } + + #[test] + fn record_field_to_header() { + check( + r#" +//- /src/main.erl +-module(main). +-include("header.hrl"). + +foo() -> #rec{field1 = 1, f~ield3 = ok, field2 = ""}. + +//- /src/header.hrl +-record(rec, {field1, field2, field3}). +%% ^^^^^^ +"#, + ); + } + + #[test] + fn export_entry() { + check( + r#" +//- /src/main.erl +-module(main). + +-export([f~oo/1]). + + foo(_) -> ok. +%%^^^ +"#, + ) + } + + #[test] + fn import_entry() { + check( + r#" +//- /src/main.erl +-module(main). + +-import(another, [f~oo/1]). + +//- /src/another.erl +-module(another). + + foo(_) -> ok. +%%^^^ +"#, + ) + } + + #[test] + fn export_type_entry() { + check( + r#" +//- /src/main.erl +-module(main). + +-export_type([f~oo/1]). + +-type foo(_) :: ok. +%% ^^^^^^ +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +-export_type([f~oo/1]). + +-opaque foo(_) :: ok. +%% ^^^^^^ +"#, + ); + } + + #[test] + fn optional_callbacks_entry() { + check( + r#" +//- /src/main.erl +-module(main). + +-optional_callbacks([f~oo/1]). + +-callback foo(integer()) -> integer(). +%% ^^^ +"#, + ); + } + + #[test] + fn optional_callbacks_entry_to_header() { + check( + r#" +//- /src/main.erl +-module(main). +-include("header.hrl"). + +-optional_callbacks([f~oo/1]). + +//- /src/header.hrl +-callback foo(integer()) -> integer(). +%% ^^^ +"#, + ); + } + + #[test] + fn macro_call() { + check( + r#" +//- /src/main.erl +-module(main). + +-define(FOO, 1). +%% ^^^ + +foo() -> ?F~OO. +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +-define(FOO(), 1). +%% ^^^^^ + +foo(?F~OO()) -> ok. +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +-define(FOO(X), X). +%% ^^^^^^ + +-type foo() :: ?F~OO(integer()). +"#, + ); + } + + #[test] + fn macro_name() { + check( + r#" +//- /src/main.erl +-module(main). + +-define(FOO, 1). +%% ^^^ + +-ifdef(F~OO). +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +-define(FOO(), 1). +%% ^^^^^ + +-ifndef(F~OO). +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +-define(FOO(X), X). +%% ^^^^^^ + +-undef(F~OO). +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +-define(foo, 1). +%% ^^^ +-define(foo(), 2). +%% ^^^^^ +-define(foo(X), 3). +%% ^^^^^^ + +-ifndef(f~oo). +"#, + ); + } + + #[test] + fn macro_undef() { + check_unresolved( + r#" +//- /src/main.erl +-module(main). + +-define(FOO, 1). + +-undef(FOO). + +foo() -> ?F~OO. +"#, + ); + + check_unresolved( + r#" +//- /src/main.erl +-module(main). + +-define(foo, 1). +-define(foo(), 2). + +-undef(foo). + +foo() -> ?f~oo(). +"#, + ); + } + + #[test] + fn macro_in_header() { + check( + r#" +//- /src/include.hrl + +-define(FOO, 1). +%% ^^^ + +//- /src/main.erl +-module(main). + +-include("include.hrl"). + +foo() -> ?F~OO. +"#, + ); + + check( + r#" +//- /src/include.hrl + +-define(FOO(X), X). +%% ^^^^^^ + +//- /src/main.erl +-module(main). + +-include("include.hrl"). + +foo() -> ?F~OO(1). +"#, + ); + } + + #[test] + fn include() { + check( + r#" +//- /src/main.erl +-module(main). + +-include("h~eader.hrl"). +//- /src/header.hrl +%% ^file +-import(lists, [all/2]). +"#, + ); + + check( + r#" +//- /src/main.erl include_path:/include +-module(main). + +-include("h~eader.hrl"). +//- /include/header.hrl +%% ^file +-import(lists, [all/2]). +"#, + ); + } + + #[test] + fn include_lib() { + check( + r#" +//- /main/src/main.erl app:main +-module(main). + +-include_lib("a~nother/include/header.hrl"). +//- /another-app/include/header.hrl app:another +%% ^file +-import(lists, [all/2]). +"#, + ); + } + + #[test] + fn var() { + check( + r#" +//- /main/src/main.erl +-module(main). + +foo(Var) -> V~ar. +%% ^^^ +"#, + ); + + check( + r#" +//- /main/src/main.erl +-module(main). + +foo() -> + Var = 1, +%% ^^^ + V~ar. +"#, + ); + } + + #[test] + fn var_self() { + check( + r#" +//- /main/src/main.erl +-module(main). + +foo() -> + V~ar = 1, +%% ^^^ + Var. +"#, + ); + } + + #[test] + fn case() { + check_expect_parse_error( + r#" +//- /main/src/main.erl +-module(main). + +bar(X) -> + Var = 3, +%% ^^^ + case X of + V~ar -> none + end. +"#, + ); + + check_expect_parse_error( + r#" +//- /main/src/main.erl +-module(main). + +bar(X) -> + Var = 3, +%% ^^^ + case X of + {V~ar, 6} -> none + end. +"#, + ); + } + + #[test] + fn case2() { + check( + r#" +//- /main/src/main.erl +-module(main). + +bar(X) -> + case X of + {Var, 5} -> Var; + {Var, 6} -> V~ar +%% ^^^ + end. +"#, + ); + } + + #[test] + fn case3() { + check( + r#" +//- /main/src/main.erl +-module(main). + +bar(X) -> + case X of + {Var, 5} -> Var; + {V~ar, 6} -> Var +%% ^^^ + end. +"#, + ); + } + + #[test] + fn case4() { + check( + r#" +//- /main/src/main.erl +-module(main). + +bar(XX) -> + case XX of + 1 -> ZZ = 1, YY = 0, g(YY); +%% ^^ + 2 -> ZZ = 2 +%% ^^ + end, + Z~Z. +"#, + ); + } + + #[test] + fn case5() { + check( + r#" +//- /main/src/main.erl +-module(main). + +bar(XX) -> + case XX of + 1 -> ZZ = 1, YY = 0; +%% ^^ + 2 -> ZZ = 2 + end, + g(ZZ), + Y~Y. +"#, + ); + } + + #[test] + fn case6() { + check_unresolved( + r#" +//- /main/src/main.erl +-module(main). + +bar(XX) -> + case XX of + 1 -> ZZ = 1, g(ZZ); + 2 -> Z~Z + end. +"#, + ); + } + + #[test] + fn anonymous_fun_1() { + check( + r#" +//- /main/src/main.erl +-module(main). + +bar() -> + F = fun + (X, true) -> XX = X+1, X~X; +%% ^^ + (X, _) -> XX = X+1, XX + end, + F(42, true). +"#, + ); + } + + #[test] + fn anonymous_fun_2() { + check_expect_parse_error( + r#" +//- /main/src/main.erl +-module(main). + +bar() -> + XX = 1, +%% ^^ + F = fun + (X, true) -> XX = X+1, X~X; + (X, _) -> XX = X+1, XX + end, + ok. +"#, + ); + } + + #[test] + fn binary_comprehension1() { + check( + r#" +//- /main/src/main.erl +-module(main). + +bar(Bytes) -> + Byte = 1, + << B~yte || <> <= Bytes, Byte >= 5>>, +%% ^^^^ + Byte. +"#, + ); + } + + #[test] + fn binary_comprehension2() { + check( + r#" +//- /main/src/main.erl +-module(main). + +bar(Bytes) -> + By~te = 1, +%% ^^^^ + << Byte || <> <= Bytes, Byte >= 5>>, + Byte. +"#, + ); + } + + #[test] + fn binary_comprehension3() { + check( + r#" +//- /main/src/main.erl +-module(main). + +bar(Bytes) -> + Byte = 1, +%% ^^^^ + << Byte || <> <= Bytes, Byte >= 5>>, + By~te. +"#, + ); + } + + #[test] + fn binary_comprehension_chained() { + check( + r#" +//- /main/src/main.erl +-module(main). + +bar(Bytes) -> + BB = 1, + << BB || <> <= Bytes, <> <= By~te>>, +%% ^^^^ + BB. +"#, + ); + } + + #[test] + fn list_comprehension1() { + check( + r#" +//- /main/src/main.erl +-module(main). + +bar(List) -> + XX = 1, + [XX || XX <- List, X~X >= 5], +%% ^^ + XX. +"#, + ); + } + + #[test] + fn list_comprehension2() { + check( + r#" +//- /main/src/main.erl +-module(main). + +bar(List) -> + X~X = 1, +%% ^^ + [XX || XX <- List, XX >= 5], + XX. +"#, + ); + } + + #[test] + fn tuple1() { + check( + r#" +//- /main/src/main.erl +-module(main). + +bar(List) -> + XX = 1, +%% ^^ + {X~X,List}. +"#, + ); + } + + #[test] + fn tuple2() { + check( + r#" +//- /main/src/main.erl +-module(main). + +bar() -> + {XX = 1, 2}, +%% ^^ + X~X. +"#, + ); + } + + #[test] + fn list1() { + check( + r#" +//- /main/src/main.erl +-module(main). + +bar(List) -> + XX = 1, +%% ^^ + [2,X~X,4|List]. +"#, + ); + } + + #[test] + fn list2() { + check( + r#" +//- /main/src/main.erl +-module(main). + +bar(_List) -> + XX = [1], +%% ^^ + [2,XX,4|X~X]. +"#, + ); + } + + #[test] + fn list3() { + check( + r#" +//- /main/src/main.erl +-module(main). + +bar() -> + [2,XX = 1,4], +%% ^^ + X~X. +"#, + ); + } + + #[test] + fn unary_op1() { + check( + r#" +//- /main/src/main.erl +-module(main). + +bar(_List) -> + XX = 1, +%% ^^ + -X~X. +"#, + ); + } + + #[test] + fn record() { + check( + r#" +//- /main/src/main.erl +-module(main). + +bar(_List) -> + XX = 1, +%% ^^ + #record{field = X~X}. +"#, + ); + } + + #[test] + fn record_update1() { + check( + r#" +//- /main/src/main.erl +-module(main). + +bar(List) -> + XX = 1, +%% ^^ + List#record{field = X~X}. +"#, + ); + } + + #[test] + fn record_update2() { + check( + r#" +//- /main/src/main.erl +-module(main). + +bar(List) -> +%% ^^^^ + XX = 1, + Li~st#record{field = XX}. +"#, + ); + } + + #[test] + fn record_field_expr() { + check( + r#" +//- /main/src/main.erl +-module(main). + +bar(List) -> +%% ^^^^ + XX = 1, + g(XX), + Li~st#record.field. +"#, + ); + } + + #[test] + fn record_map1() { + check( + r#" +//- /main/src/main.erl +-module(main). + +bar(_List) -> + XX = 1, +%% ^^ + #{foo => X~X}. +"#, + ); + } + + #[test] + fn record_map2() { + check( + r#" +//- /main/src/main.erl +-module(main). + +bar(_List) -> + XX = 1, +%% ^^ + #{X~X => 1}. +"#, + ); + } + + #[test] + fn record_map_update1() { + check( + r#" +//- /main/src/main.erl +-module(main). + +bar(List) -> + XX = 1, +%% ^^ + List#{foo => X~X}. +"#, + ); + } + + #[test] + fn record_map_update2() { + check( + r#" +//- /main/src/main.erl +-module(main). + +bar(List) -> + XX = 1, +%% ^^ + List#{X~X => 1}. +"#, + ); + } + + #[test] + fn record_map_update3() { + check( + r#" +//- /main/src/main.erl +-module(main). + +bar(List) -> +%% ^^^^ + XX = foo, + Li~st#{XX => 1}. +"#, + ); + } + + #[test] + fn catch() { + check( + r#" +//- /main/src/main.erl +-module(main). + +bar(_List) -> + XX = foo, +%% ^^ + catch X~X. +"#, + ); + } + + #[test] + fn binary1() { + check( + r#" +//- /main/src/main.erl +-module(main). + +bar(_List) -> + XX = 1, +%% ^^ + <<2,X~X,4>>. +"#, + ); + } + + #[test] + fn binary2() { + check( + r#" +//- /main/src/main.erl +-module(main). + +bar(List) -> + XX = 1, +%% ^^ + <<2,List:X~X,4>>. +"#, + ); + } + + #[test] + fn binary3() { + check( + r#" +//- /main/src/main.erl +-module(main). + +bar(List) -> + <> = List. +%% ^^^^ +"#, + ); + } + + #[test] + fn binary4() { + check( + r#" +//- /main/src/main.erl +-module(main). + +bar(List) -> + <> = List. +%% ^^^^ +"#, + ); + } + + #[test] + fn call1() { + check( + r#" +//- /main/src/main.erl +-module(main). + +bar(FunName) -> +%% ^^^^^^^ + Fun~Name(). +"#, + ); + } + + #[test] + fn call2() { + check( + r#" +//- /main/src/module.erl + -module(module). +%% ^^^^^^^^^^^^^^^^ + +bar(FunName) -> + mo~dule:FunName(). +"#, + ); + } + + #[test] + fn call3() { + check( + r#" +//- /main/src/module.erl +-module(module). + +bar(FunName) -> +%% ^^^^^^^ + module:Fun~Name(). +"#, + ); + } + + #[test] + fn call4() { + check( + r#" +//- /main/src/module.erl +-module(module). + +bar(ModName) -> +%% ^^^^^^^ + Mod~Name:foo(). +"#, + ); + } + + #[test] + fn call5() { + check( + r#" +//- /main/src/module.erl +-module(module). + +bar(FunName) -> +%% ^^^^^^^ + module:foo(Fun~Name). +"#, + ); + } + + #[test] + fn block1() { + check( + r#" +//- /main/src/module.erl +-module(module). + +bar(FunName) -> +%% ^^^^^^^ + begin + foo(Fu~nName) + end. +"#, + ); + } + + #[test] + fn block2() { + check( + r#" +//- /main/src/module.erl +-module(module). + +bar() -> + begin + XX = 1 +%% ^^ + end, + X~X. +"#, + ); + } + + #[test] + fn if1() { + check( + r#" +//- /main/src/module.erl +-module(module). + +bar(XX) -> +%% ^^ + if + X~X -> ok; + true -> not_ok + end. +"#, + ); + } + + #[test] + fn if2() { + check( + r#" + //- /main/src/module.erl + -module(module). + + bar(XX) -> + if + XX-> YY = 1, ZZ = 2, g(ZZ); + %% ^^ + true -> YY = 0 + %% ^^ + end, + Y~Y. + "#, + ); + } + + #[test] + fn if3() { + check( + r#" + //- /main/src/module.erl + -module(module). + + bar(XX) -> + if + XX-> YY = 1, ZZ = 2; + %% ^^ + true -> YY = 0 + end, + foo(YY), + Z~Z. + "#, + ); + } + + #[test] + fn if4() { + check( + r#" + //- /main/src/module.erl + -module(module). + + bar(XX) -> + %% ^^ + if + X~X-> YY = 1, ZZ = 2, g(ZZ); + true -> YY = 0 + end, + YY. + "#, + ); + } + + #[test] + fn receive1() { + check( + r#" + //- /main/src/module.erl + -module(module). + + bar(XX) -> + %% ^^ + receive + X~X-> YY = 1, ZZ = 2, g(ZZ); + true -> YY = 0 + after TimeOut -> timeout + end, + YY. + "#, + ); + } + + #[test] + fn receive2() { + check( + r#" + //- /main/src/module.erl + -module(module). + + bar(XX) -> + receive + XX-> YY = 1, ZZ = 2, g(ZZ); + %% ^^ + true -> YY = 0 + %% ^^ + after TimeOut -> timeout + end, + Y~Y. + "#, + ); + } + + #[test] + fn receive3() { + check( + r#" + //- /main/src/module.erl + -module(module). + + bar(XX) -> + receive + XX-> YY = 1, ZZ = 2; + %% ^^ + true -> YY = 0 + after TimeOut -> timeout + end, + g(YY), + Z~Z. + "#, + ); + } + + #[test] + fn receive4() { + check( + r#" + //- /main/src/module.erl + -module(module). + + bar(XX) -> + TimeOut = 45, + %%^^^^^^^ + receive + XX-> YY = 1, ZZ = 2; + true -> YY = 0 + after Tim~eOut -> timeout + end, + g(YY), + ZZ. + "#, + ); + } + + #[test] + fn receive5() { + check( + r#" + //- /main/src/module.erl + -module(module). + + bar(XX) -> + TimeOut = 45, + receive + XX-> YY = 1, ZZ = 2; + %% ^^ + true -> YY = 0 + after TimeOut -> ZZ = 4 + %% ^^ + end, + g(YY), + Z~Z. + "#, + ); + } + + #[test] + fn receive6() { + check( + r#" + //- /main/src/module.erl + -module(module). + + bar() -> + receive + after XX = 1 -> ok + %% ^^ + end, + X~X. + "#, + ); + } + + #[test] + fn try1() { + check_expect_parse_error( + r#" + //- /main/src/module.erl + -module(module). + + bar(XX) -> + %% ^^ + try 1, 2 of + X~X -> ok + catch + YY when true -> ok; + error:undef:Stack -> Stack + after + ok + end. + "#, + ); + } + + #[test] + fn try2() { + check_expect_parse_error( + r#" + //- /main/src/module.erl + -module(module). + + bar(XX) -> + try 1, 2 of + XX -> YY = 1, ok + %% ^^ + catch + Cond when true -> ok; + error:undef:Stack -> Stack + after + ok + end, + Y~Y. + "#, + ); + } + + #[test] + fn try3() { + check_expect_parse_error( + r#" + //- /main/src/module.erl + -module(module). + + bar(XX) -> + try 1, 2 of + XX -> YY = 1, ok + catch + Cond when true -> ok; + %% ^^^^ + error:undef:Stack -> Stack + after + ok + end, + %% This will generate an error diagnostic for an unsafe usage, but we resolve anyway + Co~nd. + "#, + ); + } + + #[test] + fn try4() { + check_expect_parse_error( + r#" + //- /main/src/module.erl + -module(module). + + bar(XX) -> + try 1, 2 of + XX -> YY = 1, ok + catch + Cond when true -> ok; + error:undef:Stack -> Sta~ck + %% ^^^^^ + after + ok + end, + Cond. + "#, + ); + } + + #[test] + fn try5() { + check_expect_parse_error( + r#" + //- /main/src/module.erl + -module(module). + + bar(XX) -> + try 1, 2 of + XX -> YY = 1, ok + catch + Cond when true -> ok; + error:undef:Stack -> Stack + %% ^^^^^ + after + ok + end, + %% This will generate an error diagnostic for an unsafe usage, but we resolve anyway + St~ack. + "#, + ); + } + + #[test] + fn try6() { + check_expect_parse_error( + r#" + //- /main/src/module.erl + -module(module). + + bar(XX) -> + try 1, 2 of + XX -> YY = 1, ok + catch + Cond when true -> ok; + error:Undef:Stack -> Stack + %% ^^^^^ + after + ok + end, + %% This will generate an error diagnostic for an unsafe usage, but we resolve anyway + Und~ef. + "#, + ); + } + + #[test] + fn try7() { + check_expect_parse_error( + r#" + //- /main/src/module.erl + -module(module). + + bar(XX) -> + try 1, 2 of + XX -> YY = 1, ok + catch + Cond when true -> ok; + Error:Undef:Stack -> Stack + %% ^^^^^ + after + ok + end, + %% This will generate an error diagnostic for an unsafe usage, but we resolve anyway + Err~or. + "#, + ); + } + + #[test] + fn try8() { + check_expect_parse_error( + r#" + //- /main/src/module.erl + -module(module). + + bar(XX) -> + %% ^^ + try 1, 2 of + XX -> YY = 1, ok + catch + Cond when X~X -> ok; + error:undef:Stack -> Stack + after + ok + end, + %% This will generate an error diagnostic for an unsafe usage, but we resolve anyway + Stack. + "#, + ); + } + + #[test] + fn try9() { + check_expect_parse_error( + r#" + //- /main/src/module.erl + -module(module). + + bar(XX) -> + try ZZ = 1, 2 of + %% ^^ + XX -> YY = 1, ok + catch + Cond when XX -> ok; + error:undef:Stack -> Stack + after + ok + end, + Z~Z. + "#, + ); + } + + #[test] + fn capture_fun1() { + check( + r#" + //- /main/src/module.erl + -module(module). + + bar(XX) -> + %% ^^ + YY = fun modu:fun/X~X, + YY. + "#, + ); + } + + #[test] + fn capture_fun2() { + check( + r#" + //- /main/src/module.erl + -module(module). + + bar(XX) -> + %% ^^ + YY = fun modu:X~X/0, + YY. + "#, + ); + } + + #[test] + fn capture_fun3() { + check( + r#" + //- /main/src/module.erl + -module(module). + + bar(XX) -> + %% ^^ + YY = fun X~X:foo/0, + YY. + "#, + ); + } + + #[test] + fn capture_fun4() { + check( + r#" + //- /main/src/module.erl + -module(module). + + bar(XX) -> + %% ^^ + YY = fun X~X/0, + YY. + "#, + ); + } + + #[test] + fn pat_match1() { + check( + r#" + //- /main/src/module.erl + -module(module). + + foo(AA = BB) -> + %% ^^ + A~A + BB. + "#, + ); + } + + #[test] + fn pat_match2() { + check( + r#" + //- /main/src/module.erl + -module(module). + + foo(AA = BB) -> + %% ^^ + AA + B~B. + "#, + ); + } + + #[test] + fn pat_list1() { + check( + r#" + //- /main/src/module.erl + -module(module). + + foo([AA, BB]) -> + %% ^^ + A~A + BB. + "#, + ); + } + + #[test] + fn pat_list2() { + check( + r#" + //- /main/src/module.erl + -module(module). + + foo([AA | BB]) -> + %% ^^ + AA + B~B. + "#, + ); + } + + #[test] + fn pat_unary_op1() { + check( + r#" + //- /main/src/module.erl + -module(module). + + foo(AA = not BB) -> + %% ^^ + AA + B~B. + "#, + ); + } + + #[test] + fn pat_binary_op1() { + check( + r#" + //- /main/src/module.erl + -module(module). + + foo(AA andalso BB) -> + %% ^^ + A~A + BB. + "#, + ); + } + + #[test] + fn pat_binary_op2() { + check( + r#" + //- /main/src/module.erl + -module(module). + + foo(AA andalso BB) -> + %% ^^ + AA + B~B. + "#, + ); + } + + #[test] + fn pat_record1() { + check( + r#" + //- /main/src/module.erl + -module(module). + + foo(#record{field = AA}) -> + %% ^^ + A~A. + "#, + ); + } + + #[test] + fn pat_map1() { + check( + r#" + //- /main/src/module.erl + -module(module). + + foo(#{foo := BB}) -> + %% ^^ + [AA] = B~B. + "#, + ); + } + + #[test] + fn macro_module_name() { + check( + r#" +//- /src/main.erl +-module(main). + + foo() -> +%% ^^^ + ?MODULE:f~oo(). +"#, + ); + + check( + r#" +//- /src/main.erl +-module(main). + +-define(ANOTHER, another). +foo() -> + ?ANOTHER:f~oo() . + +//- /src/another.erl +-module(another). + + foo() -> ok. +%% ^^^ + "#, + ); + } + + #[test] + fn anonymous_fun_as_variable_1() { + check_expect_parse_error( + r#" + //- /main/src/module.erl + -module(module). + + main(_) -> + fun FF() -> + %% ^^ + case rand:uniform(2) of + 1 -> F~F(); + _ -> ok + end + end(). + "#, + ); + } + + #[test] + fn anonymous_fun_as_variable_2() { + check_expect_parse_error( + r#" + //- /main/src/module.erl + -module(module). + + main(_) -> + fun FF() -> + %% ^^ + {_, _} = catch F~F = 3 + end(). + "#, + ); + } + + #[test] + fn test_class_variable() { + check_expect_parse_error( + r#" +//- /main/src/module.erl +-module(module). + +main(_) -> + Ty = error, +%% ^^ + try ok + catch + T~y:_ -> ok + end. +"#, + ) + } + + #[test] + fn variable_in_guard_1() { + check( + r#" + //- /main/src/module.erl + -module(module). + + foo(LowerBound, UpperBound) + %% ^^^^^^^^^^ + when LowerB~ound < UpperBound, is_integer(LowerBound), is_integer(UpperBound) -> + Range = UpperBound - LowerBound, + LowerBound + foo(Range). + "#, + ); + } + + #[test] + fn single_clause_for_variable() { + check( + r#" + //- /main/src/module.erl + -module(module). + + foo(0) -> + X~0 = 1, + %% ^^ + X0; + foo(Y) -> + [X0] = Y, + X0. + "#, + ); + } + + #[test] + fn define_func_include_file() { + check( + r#" +//- /include/foo.hrl include_path:/include +-define(enum, m:f). +%% ^^^^ +//- /src/foo.erl + -module(foo). + -include("foo.hrl"). + + bar() -> ?e~num(1, [a,b]). + "#, + ); + } + + // Unresolved with atom being an identifier and not "just" an expression + // navigating to a module + mod unresolved_atoms_with_special_meaning { + use super::*; + + #[test] + fn module_attribute() { + check( + r#" +//- /src/main.erl + -module(ma~in). +%%^^^^^^^^^^^^^^ + +//- /src/another.erl + -module(another). +"#, + ) + } + + #[test] + fn fa() { + check_unresolved( + r#" +//- /src/main.erl +-module(main). + +-export([a~nother/1]). + +//- /src/another.erl + -module(another). +"#, + ); + + check_unresolved( + r#" +//- /src/main.erl +-module(main). + +-import(x, [a~nother/1]). + +//- /src/another.erl + -module(another). +"#, + ) + } + + #[test] + fn type_name() { + check( + r#" +//- /src/main.erl +-module(main). + +-type a~nother() :: _. +%% ^^^^^^^^^ + +//- /src/another.erl + -module(another). +"#, + ) + } + + #[test] + fn record_decl() { + check( + r#" +//- /src/main.erl +-module(main). + +-record(a~nother, {}). +%% ^^^^^^^ + +//- /src/another.erl + -module(another). +"#, + ) + } + + #[test] + fn spec() { + check_unresolved( + r#" +//- /src/main.erl +-module(main). + +-spec a~nother() -> _. + +//- /src/another.erl + -module(another). +"#, + ) + } + + #[test] + fn callback() { + check( + r#" +//- /src/main.erl +-module(main). + +-callback a~nother() -> _. +%% ^^^^^^^ + +//- /src/another.erl + -module(another). +"#, + ) + } + + #[test] + fn attr_name() { + check_unresolved( + r#" +//- /src/main.erl +-module(main). + +-a~nother(ok). + +//- /src/another.erl + -module(another). +"#, + ) + } + + #[test] + fn function_clause() { + check( + r#" +//- /src/main.erl +-module(main). + + a~nother() -> ok. +%%^^^^^^^ +//- /src/another.erl + -module(another). +"#, + ) + } + + #[test] + fn bit_type_list() { + check_unresolved( + r#" +//- /src/main.erl +-module(main). + +foo() -> <<1/a~nother>>. + +//- /src/another.erl + -module(another). +"#, + ) + } + + #[test] + fn record_name() { + check_unresolved( + r#" +//- /src/main.erl +-module(main). + +foo() -> #a~nother{}. + +//- /src/another.erl + -module(another). +"#, + ) + } + + #[test] + fn record_field_name() { + check_unresolved( + r#" +//- /src/main.erl +-module(main). + +foo() -> #x.a~nother. + +//- /src/another.erl + -module(another). +"#, + ) + } + + #[test] + fn record_field() { + check_unresolved( + r#" +//- /src/main.erl +-module(main). + +foo() -> #x{a~nother = ok}. + +//- /src/another.erl + -module(another). +"#, + ) + } + + #[test] + fn external_fun() { + check_unresolved( + r#" +//- /src/main.erl +-module(main). + +foo() -> fun foo:a~nother/0. + +//- /src/another.erl + -module(another). +"#, + ) + } + + #[test] + fn internal_fun() { + check_unresolved( + r#" +//- /src/main.erl +-module(main). + +foo() -> fun a~nother/0. + +//- /src/another.erl + -module(another). +"#, + ) + } + + #[test] + fn try_class() { + check_unresolved( + r#" +//- /src/main.erl +-module(main). + +foo() -> try ok catch a~nother:_ -> ok end. + +//- /src/another.erl + -module(another). +"#, + ) + } + + #[test] + fn macro_lhs() { + check( + r#" +//- /src/main.erl +-module(main). + +-define(a~nother, 4). +%% ^^^^^^^ +foo() -> ?another. + +//- /src/another.erl + -module(another). +"#, + ) + } + + #[test] + fn macro_call_expr() { + check_unresolved( + r#" +//- /src/main.erl +-module(main). + +foo() -> ?a~nother(). + +//- /src/another.erl + -module(another). +"#, + ) + } + + #[test] + fn pp_undef() { + check_unresolved( + r#" +//- /src/main.erl +-module(main). + +-undef(a~nother). + +//- /src/another.erl + -module(another). +"#, + ) + } + + #[test] + fn pp_ifdef() { + check_unresolved( + r#" +//- /src/main.erl +-module(main). + +-ifdef(a~nother). + +//- /src/another.erl + -module(another). +"#, + ) + } + + #[test] + fn pp_ifndef() { + check_unresolved( + r#" +//- /src/main.erl +-module(main). + +-ifndef(a~nother). + +//- /src/another.erl + -module(another). +"#, + ) + } + + #[test] + fn remote() { + check_unresolved( + r#" +//- /src/main.erl +-module(main). + +foo() -> foo:a~nother(). + +//- /src/another.erl + -module(another). +"#, + ) + } + + #[test] + fn call() { + check_unresolved( + r#" +//- /src/main.erl +-module(main). + +foo() -> a~nother(). + +//- /src/another.erl + -module(another). +"#, + ) + } + } + + mod maybe { + use super::*; + + #[test] + fn maybe() { + check( + r#" +//- /main/src/module.erl +-module(module). + +foo() -> + maybe + {ok, AA} = ok_a(), + %% ^^ + {ok, BB} = ok_b(), + {A~A, BB} + end. + "#, + ); + } + + #[test] + fn maybe_cond_match_simple() { + check( + r#" +//- /main/src/module.erl +-module(module). + +foo() -> + maybe + {ok, AA} ?= ok_a(), + %% ^^ + {ok, BB} = ok_b(), + {A~A, BB} + end. + "#, + ); + } + + #[test] + fn maybe_cond_match() { + check( + r#" +//- /main/src/module.erl +-module(module). + +foo() -> + maybe + AA = 1, + %% ^^ + {ok, AA} ?= ok_a(), + {ok, BB} = ok_b(), + {A~A, BB} + end. + "#, + ); + } + + #[test] + fn maybe_else() { + check( + r#" +//- /main/src/module.erl +-module(module). + +foo() -> + maybe + A = 1, + A + else + Err -> + %% ^^^ + {error, E~rr} + end. + "#, + ); + } + + #[test] + fn maybe_unresolved() { + check_unresolved( + r#" +//- /main/src/module.erl +-module(module). + +foo() -> + maybe + AA = 1, + AA + else + 2 -> {error, A~A} + end. + "#, + ); + } + } + mod map_comp { + use super::*; + + #[test] + fn simple_go_to_key() { + check( + r#" +//- /main/src/module.erl +-module(module). + +foo(Map) -> + #{K~K => VV + || KK := VV <- Map}. +%% ^^ + "#, + ); + } + + #[test] + fn simple_go_to_val() { + check( + r#" +//- /main/src/module.erl +-module(module). + +foo() -> + #{KK => V~V + || KK := VV <- #{1 => 2, 3 => 4}}. +%% ^^ + "#, + ); + } + + #[test] + fn go_to_key_in_filter() { + check( + r#" +//- /main/src/module.erl +-module(module). + +foo() -> + #{KK => V + || KK := V <- #{1 => 2, 3 => 4}, K~K > 1}. +%% ^^ + "#, + ); + } + + #[test] + fn go_to_key_with_list_generator() { + check( + r#" +//- /main/src/module.erl +-module(module). + +foo() -> + #{KK => K~K + 1 + || KK <- [1, 2, 3]}. +%% ^^ + "#, + ); + } + + #[test] + fn go_to_val_with_binary_generator() { + check( + r#" +//- /main/src/module.erl +-module(module). + +foo() -> + #{KK => V~V + 1 + || <> <= <<1, 2, 3, 4>>}. +%% ^^ + "#, + ); + } + + #[test] + fn list_comp_with_map_generator() { + check( + r#" +//- /main/src/module.erl +-module(module). + +foo() -> + [{K, V~V} + || K := VV <- #{1 => 2, 3 => 4}, K>1]. +%% ^^ + "#, + ); + } + } + + #[test] + fn local_call_from_macro_rhs() { + check( + r#" + //- /src/main.erl + -module(main). + -export([main/0]). + -define(MY_MACRO(), my_f~unction()). + main() -> ?MY_MACRO(). + my_function() -> ok. + %% ^^^^^^^^^^^ +"#, + ) + } + + #[test] + fn remote_call_from_macro_rhs() { + check( + r#" + //- /src/main.erl + -module(main). + -export([main/0]). + -define(MY_MACRO(), ?MODULE:my_f~unction()). + main() -> ?MY_MACRO(). + my_function() -> ok. + %% ^^^^^^^^^^^ +"#, + ) + } + + #[test] + fn remote_call_from_included_macro_rhs() { + check( + r#" + //- /include/main.hrl + -define(MY_MACRO(), main:my_f~unction()). + //- /src/main.erl + -module(main). + -export([main/0]). + my_function() -> ok. + %% ^^^^^^^^^^^ +"#, + ) + } +} diff --git a/crates/ide/src/handlers/mod.rs b/crates/ide/src/handlers/mod.rs new file mode 100644 index 0000000000..92ced985c1 --- /dev/null +++ b/crates/ide/src/handlers/mod.rs @@ -0,0 +1,12 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +pub mod get_docs; +pub mod goto_definition; +pub mod references; diff --git a/crates/ide/src/handlers/references.rs b/crates/ide/src/handlers/references.rs new file mode 100644 index 0000000000..ff7f3a53fd --- /dev/null +++ b/crates/ide/src/handlers/references.rs @@ -0,0 +1,644 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! This module implements a reference search. +//! First, the element at the cursor position must be either an `ast::Atom` +//! or `ast::Var`. We first try to resolve it as if it was a definition of a symbol +//! (e.g. module attribute), and if that fails, we try to treat it as a reference. +//! Either way, we obtain element's HIR. +//! After that, we collect files that might contain references and look +//! for text occurrences of the identifier. If there's an `ast::Name` +//! at the index that the match starts at and its tree parent is +//! resolved to the search element SymbolDefinition, we get a reference. + +use elp_ide_db::elp_base_db::FileId; +use elp_ide_db::find_best_token; +use elp_ide_db::SymbolClass; +use elp_ide_db::SymbolDefinition; +use elp_syntax::AstNode; +use elp_syntax::TextRange; +use fxhash::FxHashMap; +use hir::Semantic; + +use crate::FilePosition; +use crate::NavigationTarget; +use crate::ToNav; + +#[derive(Debug, Clone)] +pub struct ReferenceSearchResult { + pub declaration: NavigationTarget, + pub references: FxHashMap>, +} + +// Feature: Find All References +// +// Shows all references of the item at the cursor location +// +// |=== +// | Editor | Shortcut +// +// | VS Code | kbd:[Shift+Alt+F12] +// |=== +pub(crate) fn find_all_refs( + sema: &Semantic<'_>, + position: FilePosition, +) -> Option> { + let _p = profile::span("find_all_refs"); + let search = move |def: SymbolDefinition| { + let declaration = def.to_nav(sema.db); + let usages = match def { + SymbolDefinition::Function(_) => def.usages(sema).direct_only().all(), + _ => def.usages(sema).all(), + }; + + let references = usages + .into_iter() + .map(|(file_id, refs)| { + ( + file_id, + refs.into_iter() + .map(|name| name.syntax().text_range()) + .collect(), + ) + }) + .collect(); + + ReferenceSearchResult { + declaration, + references, + } + }; + + let token = find_best_token(sema, position)?; + + match SymbolClass::classify(sema, token)? { + SymbolClass::Definition(def) => Some(vec![search(def)]), + SymbolClass::Reference { refs, typ: _ } => Some(refs.into_iter().map(search).collect()), + } +} + +#[cfg(test)] +mod tests { + use elp_ide_db::elp_base_db::FileRange; + + use crate::fixture; + use crate::tests::check_file_ranges; + + fn check(fixture: &str) { + let (analysis, pos, mut annos) = fixture::annotations(fixture); + if let Ok(Some(resolved)) = analysis.find_all_refs(pos) { + for res in resolved { + let def_name = match annos + .iter() + .position(|(range, _)| range == &res.declaration.file_range()) + { + Some(idx) => annos.remove(idx).1, + None => panic!( + "definition not found for:\n{:#?}\nsearching:\n{:#?}", + res, annos + ), + }; + let key = def_name + .strip_prefix("def") + .expect("malformed definition key"); + + let expected = take_by(&mut annos, |(_, name)| name == key); + let found_ranges = res + .references + .into_iter() + .flat_map(|(file_id, ranges)| { + ranges + .into_iter() + .map(move |range| FileRange { file_id, range }) + }) + .collect(); + check_file_ranges(found_ranges, expected) + } + } + assert!(annos.is_empty()); + + fn take_by(vec: &mut Vec, by: impl Fn(&T) -> bool) -> Vec { + let mut res = vec![]; + let mut i = 0; + + while i < vec.len() { + if by(&vec[i]) { + let found = vec.swap_remove(i); + res.push(found); + } else { + i += 1; + } + } + + res + } + } + + #[test] + fn test_module_expr() { + check( + r#" +//- /src/main.erl + -module(main). +%%^^^^^^^^^^^^^^def + +main() -> main~. +%% ^^^^ +"#, + ); + } + + #[test] + fn test_module_type() { + check( + r#" +//- /src/main.erl + -module(main). +%%^^^^^^^^^^^^^^def + +-type main() :: main~. +%% ^^^^ +"#, + ); + } + + #[test] + fn test_module_from_definition() { + check( + r#" +//- /src/main.erl + -module(main~). +%%^^^^^^^^^^^^^^def + +//- /src/type.erl +-type main() :: main. +%% ^^^^ + +//- /src/function.erl +main() -> main. +%% ^^^^ +"#, + ); + } + + #[test] + fn test_module_across_files() { + check( + r#" +//- /src/another.erl + -module(another). +%%^^^^^^^^^^^^^^^^^def + +//- /src/main.erl +foo() -> another~. +%% ^^^^^^^ + +//- /src/third.erl +foo() -> another. +%% ^^^^^^^ +"#, + ); + } + + #[test] + fn test_type() { + check( + r#" +//- /src/main.erl +-type foo~() :: foo. +%% ^^^^^def + +-type bar() :: {foo(), baz(), foo(integer())}. +%% ^^^ +"#, + ); + + check( + r#" +//- /src/main.erl +-type foo() :: foo. +%% ^^^^^def + +-type bar() :: {foo~(), baz(), foo(integer())}. +%% ^^^ +"#, + ); + + check( + r#" +//- /src/main.erl +-export_type([foo/0]). +%% ^^^ + +-type foo() :: foo. +%% ^^^^^def + +-type bar() :: foo~(). +%% ^^^ + +//- /src/another.erl +-type bar() :: {main:foo(), baz(), main:foo(integer())}. +%% ^^^ +"#, + ) + } + + #[test] + fn test_record() { + check( + r#" +//- /src/main.erl +-record(foo~, {}). +%% ^^^def + +-type foo() :: #foo{}. +%% ^^^ + + +foo(#foo{}) -> +%% ^^^ + #foo{}, +%% ^^^ + Var#foo{}, +%% ^^^ + Var#foo.field, +%% ^^^ + #foo.field. +%% ^^^ +"#, + ); + + check( + r#" +//- /src/main.erl +-record(foo, {}). +%% ^^^def + +-type foo() :: #foo~{}. +%% ^^^ + +foo() -> #foo{}. +%% ^^^ +"#, + ); + + check( + r#" +//- /src/main.hrl +-record(foo, {}). +%% ^^^def + +//- /src/main.erl +-include("main.hrl"). +-type foo() :: #foo~{}. +%% ^^^ + +//- /src/another.erl +-include("main.hrl"). + +foo() -> #foo{}. +%% ^^^ + +//- /src/different_record.erl +-record(foo, {}). + +should_not_match() -> #foo{}. +"#, + ); + } + + #[test] + fn test_record_field() { + check( + r#" +//- /src/main.erl +-record(foo, {a, b~}). +%% ^def + +-type foo() :: #foo{b :: integer()}. +%% ^ + +foo(#foo{a = _, b = _}) -> +%% ^ + #foo{a = 1, b = 2}, +%% ^ + Var#foo{b = 3}, +%% ^ + Var#foo.b, +%% ^ + #foo.b. +%% ^ +"#, + ); + + check( + r#" +//- /src/main.erl +-record(foo, {a}). +%% ^def + +-type foo() :: #foo{a = 1}. +%% ^ + +foo() -> #foo{a~ = 2}. +%% ^ +"#, + ); + + check( + r#" +//- /src/main.hrl +-record(foo, {a}). +%% ^def + +//- /src/main.erl +-include("main.hrl"). +-type foo() :: #foo{a~ :: integer()}. +%% ^ + +//- /src/another.erl +-include("main.hrl"). + +foo() -> #foo{a = 1}. +%% ^ + +//- /src/different_record.erl +-record(foo, {a}). + +should_not_match() -> #foo{a = 1}. +"#, + ); + } + + #[test] + fn test_function() { + check( + r#" +//- /src/main.erl + +-export([foo/0]). + + foo~() -> ok. +%%^^^def + +bar() -> foo(). +%% ^^^ +baz() -> foo(1). + +//- /src/another.erl + +-import(main, [foo/0]). +"#, + ); + + check( + r#" +//- /src/main.erl + +-export([foo/0]). + + foo() -> ok. +%%^^^def + +bar() -> foo(). +%% ^^^ +baz() -> foo(1). + +//- /src/another.erl + +-import(main, [foo/0]). + +baz() -> main:foo~(). +%% ^^^ +"#, + ); + + check( + r#" +-export([foo/0]). + +-spec foo~() -> ok. + foo() -> ok. +%%^^^def +"#, + ); + + // Finding reference works from any clause, + // the first clause is considered the "definition" + check( + r#" +-export([foo/1]). + + foo~(1) -> ok; +%%^^^def +foo(2) -> error. +"#, + ); + + check( + r#" +-export([foo/1]). + + foo(1) -> ok; +%%^^^def +foo~(2) -> error. +"#, + ); + } + + #[test] + fn test_functions_import_1() { + check( + r#" +//- /foo/src/main.erl app:foo +-module(main). +-import(another, [baz/1]). +foo() -> + ba~z(3). +%% ^^^ +//- /foo/src/another.erl app:foo + -module(another). + -export([baz/1]). + baz(0) -> zero. +%% ^^^def +"#, + ); + } + + #[test] + fn test_functions_import_2() { + check( + r#" +//- /foo/src/main.erl app:foo +-module(main). +foo() -> another:imp~orted(). + +//- /foo/src/another.erl app:foo + -module(another). + -import(baz, [imported/0]). + +//- /foo/src/baz.erl app:foo + -module(baz). + + -export([imported/0]). + + imported() -> ok. + +"#, + ); + } + + #[test] + fn test_macro() { + check( + r#" +//- /src/main.erl +-define(FOO~(X), X). +%% ^^^^^^def + +-type foo() :: ?FOO(integer()). +%% ^^^ + +foo(?FOO(_), FOO) -> FOO, ?FOO(1). +%% ^^^ +%% ^^^ + +-ifdef(FOO). +%% ^^^ +-ifndef(FOO). +%% ^^^ +"#, + ); + + check( + r#" +//- /src/main.hrl +-define(FOO, 1). +%% ^^^def + +//- /src/main.erl +-include("main.hrl"). + +foo() -> ?FOO~. +%% ^^^ + +//- /src/another.erl +-include("main.hrl"). + +-type foo() :: ?FOO. +%% ^^^ + +//- /src/no_include.erl +foo() -> ?FOO. + +//- /src/another_macro.erl +-define(FOO, 2). + +foo() -> ?FOO. +"#, + ); + } + + #[test] + fn test_var() { + check( + r#" +foo(Var~) -> +%% ^^^def + Var. +%% ^^^ +"#, + ); + + check( + r#" +foo(Var) -> +%% ^^^def + Var~, +%% ^^^ + Var. +%% ^^^ +"#, + ); + + check( + r#" +foo() -> + case {} of + Var -> ok; +%% ^^^def1 + _ -> + Var = 1 +%% ^^^def2 + end, + Var~. +%% ^^^1 +%% ^^^2 +"#, + ); + } + + #[test] + fn test_callback() { + check( + r#" +-callback foo~(integer()) -> ok. +%% ^^^def +-callback foo() -> ok. + +-optional_callbacks([foo/1]). +%% ^^^ +-optional_callbacks([foo/0]). +"#, + ); + } + + #[test] + fn test_headers() { + check( + r#" +//- /foo/include/main.hrl app:foo include_path:/foo/include +%% ^file def +//- /foo/src/main.erl app:foo +-include("main.hrl~"). +%% ^^^^^^^^^^ +//- /bar/src/another.erl app:bar +-include_lib("foo/include/main.hrl"). +%% ^^^^^^^^^^^^^^^^^^^^^^ +"#, + ); + } + + #[test] + fn test_functions_only() { + // T137651044: Reference results for function should only include functions + check( + r#" +//- /foo/src/main.erl app:foo +-module(main). +-import(another, [baz/1]). +foo() -> + baz(3), +%% ^^^ + another:b~az(5). +%% ^^^ +//- /foo/src/another.erl app:foo + -module(another). + -export([baz/1]). + -spec baz(any()) -> any(). + baz(0) -> zero; +%% ^^^def + baz(_) -> ok. + + other() -> baz(2). +%% ^^^ +"#, + ); + } +} diff --git a/crates/ide/src/highlight_related.rs b/crates/ide/src/highlight_related.rs new file mode 100644 index 0000000000..bea85e6643 --- /dev/null +++ b/crates/ide/src/highlight_related.rs @@ -0,0 +1,1040 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_ide_db::elp_base_db::FilePosition; +use elp_ide_db::find_best_token; +use elp_ide_db::ReferenceCategory; +use elp_ide_db::SearchScope; +use elp_ide_db::SymbolClass; +use elp_ide_db::SymbolDefinition; +use elp_syntax::ast; +use elp_syntax::AstNode; +use elp_syntax::NodeOrToken; +use elp_syntax::TextRange; +use hir::Semantic; + +use crate::navigation_target::ToNav; + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct HighlightedRange { + pub range: TextRange, + pub category: Option, +} + +// Feature: Highlight Related +// +// Highlights constructs related to the thing under the cursor. +// +pub(crate) fn highlight_related( + sema: &Semantic, + position: FilePosition, +) -> Option> { + let _p = profile::span("highlight_related"); + find_local_refs(sema, position) +} + +/// This function is based on `references::find_all_refs()` but limits +/// its search to the current file, does not request direct only, and +/// returns highlight results +fn find_local_refs(sema: &Semantic<'_>, position: FilePosition) -> Option> { + let _p = profile::span("find_local_refs"); + let search = move |def: SymbolDefinition| -> Vec { + let declaration = def.to_nav(sema.db); + let (ref_category, decl_category) = match def { + SymbolDefinition::Var(_) => ( + Some(ReferenceCategory::Read), + Some(ReferenceCategory::Write), + ), + _ => (None, None), + }; + let file_scope = SearchScope::single_file(position.file_id, None); + let usages = def.usages(sema).set_scope(&file_scope).all(); + + let mut references: Vec<_> = usages + .into_iter() + .flat_map(|(file_id, refs)| { + if file_id == position.file_id { + refs.into_iter() + .map(|name| HighlightedRange { + range: name.syntax().text_range(), + category: ref_category, + }) + .collect::>() + } else { + Vec::default() + } + }) + .collect(); + if declaration.file_id == position.file_id { + references.push(HighlightedRange { + range: declaration.focus_range.unwrap_or(declaration.full_range), + category: decl_category, + }); + }; + + references + }; + + let token = find_best_token(sema, position)?; + match SymbolClass::classify(sema, token.clone()) { + Some(SymbolClass::Definition(def)) => Some(search(def)), + Some(SymbolClass::Reference { refs, typ: _ }) => { + Some(refs.into_iter().flat_map(search).collect()) + } + None => { + let atom = ast::Atom::cast(token.value.parent()?)?; + let escaped_name = atom.text(); + // Simply find all matching atoms in the file, using normalised names. + // Possibly limit it to +- X lines, since it is for display in a viewport? + // Possibly make use of the search.rs functionality? But that needs a SymbolDefinition + let source = sema.parse(position.file_id); + let references: Vec<_> = source + .value + .syntax() + .descendants_with_tokens() + .filter_map(|n| match n { + NodeOrToken::Node(n) => { + let atom = ast::Atom::cast(n)?; + if atom.text() == escaped_name { + Some(HighlightedRange { + range: atom.syntax().text_range(), + category: None, + }) + } else { + None + } + } + NodeOrToken::Token(_) => None, + }) + .collect(); + + Some(references) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::fixture; + + #[track_caller] + fn check(fixture_str: &str) { + let (analysis, pos, annotations) = fixture::annotations(fixture_str); + fixture::check_no_parse_errors(&analysis, pos.file_id); + + let hls = analysis.highlight_related(pos).unwrap().unwrap_or_default(); + + let mut expected = annotations + .into_iter() + .map(|(r, access)| (r.range, (!access.is_empty()).then_some(access))) + .collect::>(); + + let mut actual = hls + .into_iter() + .map(|hl| { + ( + hl.range, + hl.category.map(|it| { + match it { + ReferenceCategory::Read => "read", + ReferenceCategory::Write => "write", + } + .to_string() + }), + ) + }) + .collect::>(); + actual.sort_by_key(|(range, _)| range.start()); + expected.sort_by_key(|(range, _)| range.start()); + + assert_eq!(expected, actual); + } + + // Note: Test names are based on those in erlang_ls + #[test] + fn application_local() { + check( + r#" + -module(main). + -export([ function_a/0, function_b/0, function_g/1 ]). + %% ^^^^^^^^^^ + + -define(MACRO_A, macro_a). + -define(MACRO_A(X), erlang:display(X)). + + function_a() -> + fun~ction_b(), + %% ^^^^^^^^^^ + #record_a{}. + + function_b() -> + %% ^^^^^^^^^^ + ?MACRO_A. + + function_g(X) -> + F = fun function_b/0, + %% ^^^^^^^^^^ + G = { }, + {F, G}. + +"#, + ); + } + + #[test] + fn application_remote_module() { + check( + r#" + //- /src/main.erl + -module(main). + -export([ function_c/0, function_g/1 ]). + + function_c() -> + code_n~avigation_extra:do(test), + %% ^^^^^^^^^^^^^^^^^^^^^ + A = #record_a{ field_a = a }, + _X = A#record_a.field_a, _Y = A#record_a.field_a, + length([1, 2, 3]). + + function_g(X) -> + F = fun function_b/0, + G = {fun code_navigation_extra:do/1 }, + %% ^^^^^^^^^^^^^^^^^^^^^ + {F, G}. + + + //- /src/code_navigation_extra.erl + -module(code_navigation_extra). + -export([do/1]). + do(X) -> ok. +"#, + ); + } + + #[test] + fn application_remote_function() { + check( + r#" + //- /src/main.erl + -module(main). + -export([ function_c/0, function_g/1 ]). + + function_c() -> + code_navigation_extra:d~o(test), + %% ^^ + A = #record_a{ field_a = a }, + _X = A#record_a.field_a, _Y = A#record_a.field_a, + length([1, 2, 3]). + + function_g(X) -> + F = fun function_b/0, + G = {fun code_navigation_extra:do/1 }, + %% ^^ + {F, G}. + + + //- /src/code_navigation_extra.erl + -module(code_navigation_extra). + -export([do/1]). + do(X) -> ok. +"#, + ); + } + + #[test] + fn application_imported() { + check( + r#" + //- /src/main.erl + -module(main). + -export([ function_c/0, function_g/1 ]). + -import(lists, [length/1]). + %% ^^^^^^ + + function_c() -> + code_navigation_extra:do(test), + A = #record_a{ field_a = a }, + _X = A#record_a.field_a, _Y = A#record_a.field_a, + len~gth([1, 2, 3]). + %% ^^^^^^ + + function_g(X) -> + F = fun function_b/0, + G = {fun code_navigation_extra:do/1 }, + {F, G}. + + + //- /opt/lib/stdlib-3.17/src/lists.erl otp_app:/opt/lib/stdlib-3.17 + -module(lists). + -export([length/1]). + length(X) -> ok. +"#, + ); + } + + #[test] + fn function_definition() { + check( + r#" + -module(main). + -export([ function_a/0, function_b/0, function_g/1 ]). + %% ^^^^^^^^^^ + + -define(MACRO_A, macro_a). + -define(MACRO_A(X), erlang:display(X)). + + function_a() -> + function_b(), + %% ^^^^^^^^^^ + #record_a{}. + + funct~ion_b() -> + %% ^^^^^^^^^^ + ?MACRO_A. + + function_g(X) -> + F = fun function_b/0, + %% ^^^^^^^^^^ + G = { }, + {F, G}. + +"#, + ); + } + + #[test] + fn fun_local() { + check( + r#" + -module(main). + -export([ function_a/0, function_b/0, function_g/1 ]). + %% ^^^^^^^^^^ + + -define(MACRO_A, macro_a). + -define(MACRO_A(X), erlang:display(X)). + + function_a() -> + function_b(), + %% ^^^^^^^^^^ + #record_a{}. + + function_b() -> + %% ^^^^^^^^^^ + ?MACRO_A. + + function_g(X) -> + F = fun func~tion_b/0, + %% ^^^^^^^^^^ + G = { }, + {F, G}. + +"#, + ); + } + + #[test] + fn fun_remote_module() { + check( + r#" + //- /src/main.erl + -module(main). + -export([ function_c/0, function_g/1 ]). + + function_c() -> + code_navigation_extra:do(test), + %% ^^^^^^^^^^^^^^^^^^^^^ + A = #record_a{ field_a = a }, + _X = A#record_a.field_a, _Y = A#record_a.field_a, + length([1, 2, 3]). + + function_g(X) -> + F = fun function_b/0, + G = {fun code_navig~ation_extra:do/1 }, + %% ^^^^^^^^^^^^^^^^^^^^^ + {F, G}. + + + //- /src/code_navigation_extra.erl + -module(code_navigation_extra). + -export([do/1]). + do(X) -> ok. +"#, + ); + } + + #[test] + fn fun_remote_function() { + check( + r#" + //- /src/main.erl + -module(main). + -export([ function_c/0, function_g/1 ]). + + function_c() -> + code_navigation_extra:do(test), + %% ^^ + A = #record_a{ field_a = a }, + _X = A#record_a.field_a, _Y = A#record_a.field_a, + length([1, 2, 3]). + + function_g(X) -> + F = fun function_b/0, + G = {fun code_navigation_extra:d~o/1 }, + %% ^^ + {F, G}. + + + //- /src/code_navigation_extra.erl + -module(code_navigation_extra). + -export([do/1]). + do(X) -> ok. +"#, + ); + } + + #[test] + fn atom() { + check( + r#" + -module(main). + -export([ function_a/0, function_b/0, function_g/1 ]). + + -record(record_a, {field_a, field_b, 'Field C'}). + %% ^^^^^^^ + + -define(MACRO_A, macro_a). + -define(MACRO_A(X), erlang:display(X)). + + function_a() -> + function_b(), + #record_a{}. + + function_c() -> + code_navigation_extra:do(test), + A = #record_a{ field_a = a }, + %% ^^^^^^^ + _X = A#record_a.field_a, _Y = A#record_a.field_a, + %% ^^^^^^^ + %% ^^^^^^^ + length([1, 2, 3]). + + + function_b() -> + ?MACRO_A. + + function_g(X) -> + F = fun function_b/0, + G = { }, + {F, G}. + + %% atom highlighting and completion includes record fields + function_o() -> + {fie~ld_a, incl}. + %% ^^^^^^^ + + %% [#1052] ?MODULE macro as record name + -record(?MODULE, {field_a, field_b}). + %% ^^^^^^^ + + -spec function_q() -> {#?MODULE{field_a :: integer()}, any()}. + %% ^^^^^^^ + function_q() -> + X = #?MODULE{}, + {X#?MODULE{field_a = 42}, X#?MODULE.field_a}. + %% ^^^^^^^ + %% ^^^^^^^ + + +"#, + ); + } + + #[test] + fn quoted_atom_1() { + check( + r#" + -module(main). + -export([ 'PascalCaseFunction'/1 ]). + %% ^^^^^^^^^^^^^^^^^^^^ + + %% quoted atoms + -spec 'PascalCaseFunction'(T) -> 'Code.Navigation.Elixirish':'Type'(T). + %% ^^^^^^^^^^^^^^^^^^^^ + 'PascalC~aseFunction'(R) -> + %% ^^^^^^^^^^^^^^^^^^^^ + _ = R#record_a.'Field C', + F = fun 'Code.Navigation.Elixirish':do/1, + F('Atom with whitespaces, "double quotes" and even some \'single quotes\''). + +"#, + ); + } + + #[test] + fn quoted_atom_2() { + check( + r#" + -module(main). + -export([ 'PascalCaseFunction'/1 ]). + + %% quoted atoms + -spec 'PascalCaseFunction'(T) -> 'Code.Navigation.Elixirish':'Type'(T). + 'PascalCaseFunction'(R) -> + _ = R#record_a.'Field C', + F = fun 'Code.Navigation.Elixirish':do/1, + F('Atom with whi~tespaces, "double quotes" and even some \'single quotes\''). + %% ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +"#, + ); + } + + #[test] + fn quoted_atom_3() { + check( + r#" + -module(main). + -export([ 'PascalCaseFunction'/1 ]). + + -record(record_a, {field_a, field_b, 'Field C'}). + %% ^^^^^^^^^ + + %% quoted atoms + -spec 'PascalCaseFunction'(T) -> 'Code.Navigation.Elixirish':'Type'(T). + 'PascalCaseFunction'(R) -> + _ = R#record_a.'Fie~ld C', + %% ^^^^^^^^^ + F = fun 'Code.Navigation.Elixirish':do/1, + F('Atom with whitespaces, "double quotes" and even some \'single quotes\''). + +"#, + ); + } + + #[test] + fn quoted_atom_4() { + check( + r#" + -module(main). + -export([ 'PascalCaseFunction'/1 ]). + + -record(record_a, {field_a, field_b, 'Field C'}). + + %% quoted atoms + -spec 'PascalCaseFunction'(T) -> 'Code.Navigation.Elixirish':'Type'(T). + %% ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 'PascalCaseFunction'(R) -> + _ = R#record_a.'Field C', + F = fun 'Code.Naviga~tion.Elixirish':do/1, + %% ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + F('Atom with whitespaces, "double quotes" and even some \'single quotes\''). + +"#, + ); + } + + #[test] + fn quoted_atom_5() { + check( + r#" + -module(main). + -export([ 'PascalCaseFunction'/1 ]). + + -record(record_a, {field_a, field_b, 'Field C'}). + + %% quoted atoms + -spec 'PascalCaseFunction'(T) -> 'Code.Na~vigation.Elixirish':'Type'(T). + %% ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 'PascalCaseFunction'(R) -> + _ = R#record_a.'Field C', + F = fun 'Code.Navigation.Elixirish':do/1, + %% ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + F('Atom with whitespaces, "double quotes" and even some \'single quotes\''). + +"#, + ); + } + + #[test] + fn quoted_atom_6() { + check( + r#" + -module(main). + -export([ 'PascalCaseFunction'/1 ]). + + -record(record_a, {field_a, field_b, 'Field C'}). + + %% quoted atoms + -spec 'PascalCaseFunction'(T) -> 'Code.Na~vigation.Elixirish':'Type'(T). + %% ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 'PascalCaseFunction'(R) -> + _ = R#record_a.'Field C', + F = fun 'Code.Navigation.Elixirish':do/1, + %% ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + F('Atom with whitespaces, "double quotes" and even some \'single quotes\''). + +"#, + ); + } + + #[test] + fn record_expr() { + check( + r#" + -module(main). + -export([ 'PascalCaseFunction'/1 ]). + + -record(record_a, {field_a, field_b, 'Field C'}). + %% ^^^^^^^^ + + function_a() -> + function_b(), + #rec~ord_a{}. + %% ^^^^^^^^ + + function_c() -> + code_navigation_extra:do(test), + A = #record_a{ field_a = a }, + %% ^^^^^^^^ + _X = A#record_a.field_a, _Y = A#record_a.field_a, + %% ^^^^^^^^ + %% ^^^^^^^^ + length([1, 2, 3]). + + %% quoted atoms + -spec 'PascalCaseFunction'(T) -> 'Code.Navigation.Elixirish':'Type'(T). + 'PascalCaseFunction'(R) -> + _ = R#record_a.'Field C', + %% ^^^^^^^^ + F = fun 'Code.Navigation.Elixirish':do/1, + F('Atom with whitespaces, "double quotes" and even some \'single quotes\''). + +"#, + ); + } + + #[test] + fn record_access() { + check( + r#" + -module(main). + -export([ 'PascalCaseFunction'/1 ]). + + -record(record_a, {field_a, field_b, 'Field C'}). + %% ^^^^^^^^ + + function_a() -> + function_b(), + #record_a{}. + %% ^^^^^^^^ + + function_c() -> + code_navigation_extra:do(test), + A = #record_a{ field_a = a }, + %% ^^^^^^^^ + _X = A#re~cord_a.field_a, _Y = A#record_a.field_a, + %% ^^^^^^^^ + %% ^^^^^^^^ + length([1, 2, 3]). + + %% quoted atoms + -spec 'PascalCaseFunction'(T) -> 'Code.Navigation.Elixirish':'Type'(T). + 'PascalCaseFunction'(R) -> + _ = R#record_a.'Field C', + %% ^^^^^^^^ + F = fun 'Code.Navigation.Elixirish':do/1, + F('Atom with whitespaces, "double quotes" and even some \'single quotes\''). + +"#, + ); + } + + #[test] + fn record_def() { + check( + r#" + -module(main). + -export([ 'PascalCaseFunction'/1 ]). + + -record(rec~ord_a, {field_a, field_b, 'Field C'}). + %% ^^^^^^^^ + + function_a() -> + function_b(), + #record_a{}. + %% ^^^^^^^^ + + function_c() -> + code_navigation_extra:do(test), + A = #record_a{ field_a = a }, + %% ^^^^^^^^ + _X = A#record_a.field_a, _Y = A#record_a.field_a, + %% ^^^^^^^^ + %% ^^^^^^^^ + length([1, 2, 3]). + + %% quoted atoms + -spec 'PascalCaseFunction'(T) -> 'Code.Navigation.Elixirish':'Type'(T). + 'PascalCaseFunction'(R) -> + _ = R#record_a.'Field C', + %% ^^^^^^^^ + F = fun 'Code.Navigation.Elixirish':do/1, + F('Atom with whitespaces, "double quotes" and even some \'single quotes\''). + +"#, + ); + } + + #[test] + fn record_field() { + check( + r#" + -module(main). + -export([ 'PascalCaseFunction'/1 ]). + + -record(record_a, {fie~ld_a, field_b, 'Field C'}). + %% ^^^^^^^ + + function_a() -> + function_b(), + #record_a{}. + + function_c() -> + code_navigation_extra:do(test), + A = #record_a{ field_a = a }, + %% ^^^^^^^ + _X = A#record_a.field_a, _Y = A#record_a.field_a, + %% ^^^^^^^ + %% ^^^^^^^ + length([1, 2, 3]). + + %% quoted atoms + -spec 'PascalCaseFunction'(T) -> 'Code.Navigation.Elixirish':'Type'(T). + 'PascalCaseFunction'(R) -> + _ = R#record_a.'Field C', + F = fun 'Code.Navigation.Elixirish':do/1, + F('Atom with whitespaces, "double quotes" and even some \'single quotes\''). + +"#, + ); + } + + #[test] + fn export() { + // Not duplicating this functionality from erlang_ls, it does not make sense. + check( + r#" + -module(main). + -expo~rt([ 'PascalCaseFunction'/1 ]). + + %% quoted atoms + -spec 'PascalCaseFunction'(T) -> 'Code.Navigation.Elixirish':'Type'(T). + 'PascalCaseFunction'(R) -> + _ = R#record_a.'Field C', + F = fun 'Code.Navigation.Elixirish':do/1, + F('Atom with whitespaces, "double quotes" and even some \'single quotes\''). + +"#, + ); + } + + #[test] + fn export_entry() { + check( + r#" + -module(main). + -export([ function_a/0, func~tion_b/0, function_g/1 ]). + %% ^^^^^^^^^^ + + -define(MACRO_A, macro_a). + -define(MACRO_A(X), erlang:display(X)). + + function_a() -> + function_b(), + %% ^^^^^^^^^^ + #record_a{}. + + function_b() -> + %% ^^^^^^^^^^ + ?MACRO_A. + + function_g(X) -> + F = fun function_b/0, + %% ^^^^^^^^^^ + G = { }, + {F, G}. + +"#, + ); + } + + #[test] + fn export_type_entry() { + check( + r#" + -module(main). + + -type type_a() :: atom(). + %% ^^^^^^^^ + + -export_type([ typ~e_a/0 ]). + %% ^^^^^^ + +"#, + ); + } + + #[test] + fn import() { + // Not duplicating this functionality from erlang_ls, it does not make sense. + // And is broken in erlang_ls + check( + r#" + + //- /src/main.erl + -module(main). + -imp~ort(lists, [length/1]). + + foo() -> length([1]). + + //- /opt/lib/stdlib-3.17/src/lists.erl otp_app:/opt/lib/stdlib-3.17 + -module(lists). + -export([length/1]). + length(X) -> ok. +"#, + ); + } + + #[test] + fn import_entry() { + check( + r#" + + //- /src/main.erl + -module(main). + -import(lists, [le~ngth/1]). + %% ^^^^^^ + + foo() -> length([1]). + %% ^^^^^^ + + //- /opt/lib/stdlib-3.17/src/lists.erl otp_app:/opt/lib/stdlib-3.17 + -module(lists). + -export([length/1]). + length(X) -> ok. +"#, + ); + } + + #[test] + fn type_def() { + check( + r#" + //- /src/main.erl + -module(main). + -type ty~pe_a() :: any(). + %% ^^^^^^^^ + + -spec function_h() -> type_a() | undefined_type_a() | file:fd(). + %% ^^^^^^ + function_h() -> + function_i(). + +"#, + ); + } + + #[test] + fn type_application_module() { + check( + r#" + //- /src/main.erl + -module(main). + -type type_a() :: any(). + + -spec function_h() -> type_a() | undefined_type_a() | fi~le:fd(). + %% ^^^^ + function_h() -> + function_i(). + +"#, + ); + } + + #[test] + fn type_application_type() { + check( + r#" + //- /src/main.erl + -module(main). + -type type_a() :: any(). + + -spec function_h() -> type_a() | undefined_type_a() | file:f~d(). + %% ^^ + function_h() -> + function_i(). + +"#, + ); + } + + #[test] + fn opaque() { + check( + r#" + //- /src/main.erl + -module(main). + -opaque opaque_t~ype_a() :: atom(). + %% ^^^^^^^^^^^^^^^ + + -export_type([ opaque_type_a/0 ]). + %% ^^^^^^^^^^^^^ + + -type user_type_a() :: type_a() | opaque_type_a(). + %% ^^^^^^^^^^^^^ + +"#, + ); + } + + #[test] + fn macro_define() { + check( + r#" + //- /src/main.erl + -module(main). + -define(MA~CRO_A, macro_a). + %% ^^^^^^^ + -define(MACRO_A(X), erlang:display(X)). + + function_b() -> + ?MACRO_A. + %% ^^^^^^^ + + function_d() -> + ?MACRO_A(d). + + %% [#333] Record field accessors assumed to be atoms + function_k() -> + X#included_record_a.?MACRO_A, + %% ^^^^^^^ + <<"foo:">>. +"#, + ); + } + + #[test] + fn macro_use() { + check( + r#" + //- /src/main.erl + -module(main). + -define(MACRO_A, macro_a). + %% ^^^^^^^ + -define(MACRO_A(X), erlang:display(X)). + + function_b() -> + ?MAC~RO_A. + %% ^^^^^^^ + + function_d() -> + ?MACRO_A(d). + + %% [#333] Record field accessors assumed to be atoms + function_k() -> + X#included_record_a.?MACRO_A, + %% ^^^^^^^ + <<"foo:">>. +"#, + ); + } + + #[test] + fn spec() { + // Not duplicating this functionality from erlang_ls, it does not make sense. + // So we duplicate just the type, not the whole attribute + check( + r#" + + //- /src/main.erl + -module(main). + -spec func~tion_h() -> type_a() | undefined_type_a() | file:fd(). + %% ^^^^^^^^^^ + +"#, + ); + } + + #[test] + fn behaviour() { + // Not duplicating this functionality from erlang_ls, it does not make sense. + // So we duplicate just the name, not the whole attribute + check( + r#" + + //- /src/main.erl + -module(main). + -behaviour(behav~iour_a). + %% ^^^^^^^^^^^ + +"#, + ); + } + + #[test] + fn callback() { + // Not duplicating this functionality from erlang_ls, it does not make sense. + // So we duplicate just the name, not the whole attribute + check( + r#" + + //- /src/main.erl + -module(main). + + -callback rena~me_me(any()) -> ok. + %% ^^^^^^^^^ + +"#, + ); + } + + #[test] + fn local_variables_1() { + check( + r#" + //- /src/main.erl + -module(main). + + foo(X~X,YY) -> + %% ^^write + XX + YY. + %% ^^read + +"#, + ); + } + + #[test] + fn argument_used_in_macro() { + check( + r#" + //- /src/main.erl + -module(main). + + -define(a_macro(Expr), ok). + get_aclink_state_test_helper(Ar~gs) -> + %% ^^^^write + ?a_macro(Args). + %% ^^^^read + +"#, + ); + } +} diff --git a/crates/ide/src/inlay_hints.rs b/crates/ide/src/inlay_hints.rs new file mode 100644 index 0000000000..6203608a00 --- /dev/null +++ b/crates/ide/src/inlay_hints.rs @@ -0,0 +1,250 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::fmt::{self}; + +use elp_ide_db::elp_base_db::FileId; +use elp_ide_db::elp_base_db::FileRange; +use elp_ide_db::RootDatabase; +use elp_syntax::TextRange; +use hir::Semantic; +use itertools::Itertools; +use smallvec::smallvec; +use smallvec::SmallVec; +mod param_name; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct InlayHintsConfig { + pub parameter_hints: bool, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum InlayKind { + Parameter, +} + +#[derive(Debug)] +pub struct InlayHint { + /// The text range this inlay hint applies to. + pub range: TextRange, + /// The kind of this inlay hint. This is used to determine side and padding of the hint for + /// rendering purposes. + pub kind: InlayKind, + /// The actual label to show in the inlay hint. + pub label: InlayHintLabel, +} + +#[derive(Debug)] +pub enum InlayTooltip { + String(String), + Markdown(String), +} + +#[derive(Default)] +pub struct InlayHintLabel { + pub parts: SmallVec<[InlayHintLabelPart; 1]>, +} + +impl InlayHintLabel { + pub fn simple( + s: impl Into, + tooltip: Option, + linked_location: Option, + ) -> InlayHintLabel { + InlayHintLabel { + parts: smallvec![InlayHintLabelPart { + text: s.into(), + linked_location, + tooltip, + }], + } + } + + pub fn prepend_str(&mut self, s: &str) { + match &mut *self.parts { + [ + InlayHintLabelPart { + text, + linked_location: None, + tooltip: None, + }, + .., + ] => text.insert_str(0, s), + _ => self.parts.insert( + 0, + InlayHintLabelPart { + text: s.into(), + linked_location: None, + tooltip: None, + }, + ), + } + } + + pub fn append_str(&mut self, s: &str) { + match &mut *self.parts { + [ + .., + InlayHintLabelPart { + text, + linked_location: None, + tooltip: None, + }, + ] => text.push_str(s), + _ => self.parts.push(InlayHintLabelPart { + text: s.into(), + linked_location: None, + tooltip: None, + }), + } + } +} + +impl From for InlayHintLabel { + fn from(s: String) -> Self { + Self { + parts: smallvec![InlayHintLabelPart { + text: s, + linked_location: None, + tooltip: None, + }], + } + } +} + +impl From<&str> for InlayHintLabel { + fn from(s: &str) -> Self { + Self { + parts: smallvec![InlayHintLabelPart { + text: s.into(), + linked_location: None, + tooltip: None, + }], + } + } +} + +impl fmt::Display for InlayHintLabel { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.parts.iter().map(|part| &part.text).format("")) + } +} + +impl fmt::Debug for InlayHintLabel { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_list().entries(&self.parts).finish() + } +} + +pub struct InlayHintLabelPart { + pub text: String, + /// Source location represented by this label part. The client will use this to fetch the part's + /// hover tooltip, and Ctrl+Clicking the label part will navigate to the definition the location + /// refers to (not necessarily the location itself). + /// When setting this, no tooltip must be set on the containing hint, or VS Code will display + /// them both. + pub linked_location: Option, + /// The tooltip to show when hovering over the inlay hint, this may invoke other actions like + /// hover requests to show. + pub tooltip: Option, +} + +impl fmt::Debug for InlayHintLabelPart { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self { + text, + linked_location: None, + tooltip: None, + } => text.fmt(f), + Self { + text, + linked_location, + tooltip, + } => f + .debug_struct("InlayHintLabelPart") + .field("text", text) + .field("linked_location", linked_location) + .field( + "tooltip", + &tooltip.as_ref().map_or("", |it| match it { + InlayTooltip::String(it) | InlayTooltip::Markdown(it) => it, + }), + ) + .finish(), + } + } +} + +// Feature: Inlay Hints +// +// ELP shows additional information inline with the source code. +// Editors usually render this using read-only virtual text snippets interspersed with code. +// +// Available hints are: +// +// * names of function arguments +pub(crate) fn inlay_hints( + db: &RootDatabase, + file_id: FileId, + range_limit: Option, + config: &InlayHintsConfig, +) -> Vec { + let _p = profile::span("inlay_hints"); + let sema = Semantic::new(db); + + let mut acc = Vec::new(); + + param_name::hints(&mut acc, &sema, config, file_id, range_limit); + + acc +} + +#[cfg(test)] +mod tests { + use elp_ide_db::elp_base_db::fixture::extract_annotations; + use itertools::Itertools; + + use crate::fixture; + use crate::inlay_hints::InlayHintsConfig; + + pub(super) const DISABLED_CONFIG: InlayHintsConfig = InlayHintsConfig { + parameter_hints: false, + }; + + #[track_caller] + pub(super) fn check_with_config(config: InlayHintsConfig, fixture: &str) { + let (analysis, file_id) = fixture::single_file(fixture); + let mut expected = extract_annotations(&analysis.file_text(file_id).unwrap()); + let inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap(); + let actual = inlay_hints + .into_iter() + .map(|it| (it.range, it.label.to_string())) + .sorted_by_key(|(range, _)| range.start()) + .collect::>(); + expected.sort_by_key(|(range, _)| range.start()); + + assert_eq!( + expected, actual, + "\nExpected:\n{expected:#?}\n\nActual:\n{actual:#?}" + ); + } + + #[test] + fn hints_disabled() { + check_with_config( + InlayHintsConfig { ..DISABLED_CONFIG }, + r#" +-module(main). +sum(A, B) -> A + B. +main() -> _X = sum(1, 2). +"#, + ); + } +} diff --git a/crates/ide/src/inlay_hints/param_name.rs b/crates/ide/src/inlay_hints/param_name.rs new file mode 100644 index 0000000000..aedabbc43e --- /dev/null +++ b/crates/ide/src/inlay_hints/param_name.rs @@ -0,0 +1,233 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_ide_db::elp_base_db::FileId; +use elp_syntax::TextRange; +use hir::db::MinInternDatabase; +use hir::Expr; +use hir::InFile; +use hir::Name; +use hir::Semantic; + +use crate::InlayHint; +use crate::InlayHintLabel; +use crate::InlayHintsConfig; +use crate::InlayKind; + +pub(super) fn hints( + res: &mut Vec, + sema: &Semantic, + config: &InlayHintsConfig, + file_id: FileId, + range_limit: Option, +) -> Option<()> { + if !config.parameter_hints { + return None; + } + let def_map = sema.def_map(file_id); + for (_name, def) in def_map.get_functions() { + if def.file.file_id == file_id { + let def_fb = def.in_function_body(sema.db, def); + let function_id = InFile::new(file_id, def.function_id); + let function_body = sema.to_function_body(function_id); + sema.fold_function( + function_id, + (), + &mut |acc, _clause_id, ctx| { + match ctx.expr { + Expr::Call { target, args } => { + let arity = args.len() as u32; + let body = &function_body.body(); + if let Some(call_def) = target.resolve_call(arity, &sema, file_id, body) + { + let param_names = call_def.function.param_names; + for (param_name, arg) in param_names.iter().zip(args) { + if should_hint( + sema.db.upcast(), + param_name, + &function_body[arg], + ) { + if let Some(arg_range) = def_fb.range_for_expr(sema.db, arg) + { + if range_limit.is_none() + || range_limit.unwrap().contains_range(arg_range) + { + let hint = InlayHint { + range: arg_range, + kind: InlayKind::Parameter, + label: InlayHintLabel::simple( + param_name.as_str(), + None, + None, + ), + }; + res.push(hint); + } + } + } + } + } + } + _ => {} + } + acc + }, + &mut |acc, _, _| acc, + ); + } + } + Some(()) +} + +fn should_hint(db: &dyn MinInternDatabase, param_name: &Name, expr: &Expr) -> bool { + if let Some(var) = expr.as_var() { + var.as_string(db) != param_name.as_str() + } else { + true + } +} + +#[cfg(test)] +mod tests { + use crate::inlay_hints::tests::check_with_config; + use crate::inlay_hints::tests::DISABLED_CONFIG; + use crate::inlay_hints::InlayHintsConfig; + + #[track_caller] + fn check_params(fixture: &str) { + check_with_config( + InlayHintsConfig { + parameter_hints: true, + ..DISABLED_CONFIG + }, + fixture, + ); + } + + #[test] + fn param_hints_basic() { + check_params( + r#" +-module(main). +-compile(export_all). +sum(A, B) -> A + B. +main() -> sum(1, + %% ^A + 2 + %% ^B + ). +}"#, + ); + } + + #[test] + fn param_hints_variables_same_name() { + check_params( + r#" +-module(main). +-compile(export_all). +sum(A, B) -> A + B. +main() -> + A = 1, + B = 2, + sum(A, + B + ). +}"#, + ); + } + + #[test] + fn param_hints_variables_different_name() { + check_params( + r#" +-module(main). +-compile(export_all). +sum(A, B) -> A + B. +main() -> + A = 1, + X = 2, + sum(A, + X + %% ^B + ). +}"#, + ); + } + + #[test] + fn param_hints_variables_expression() { + check_params( + r#" +-module(main). +-compile(export_all). +sum(A, B) -> A + B. +main() -> + X = 2, + sum(1 * (3 - 2), + %% ^^^^^^^^^^^A + X + %% ^B + ). +}"#, + ); + } + + #[test] + fn param_hints_variables_multiple_calls() { + check_params( + r#" +-module(main). +-compile(export_all). +sum(A, B) -> A + B. +main() -> + X = 2, + sum(1 * (3 - 2), + %% ^^^^^^^^^^^A + X + %% ^B + ), + sum(X, + %% ^A + X + %% ^B + ). +}"#, + ); + } + + #[test] + fn param_hints_variables_wrong_arity() { + check_params( + r#" +-module(main). +-compile(export_all). +sum(A, B) -> A + B. +main() -> + A = 1, + sum(A). +}"#, + ); + } + + #[test] + fn param_hints_variables_missing_param() { + check_params( + r#" +-module(main). +-compile(export_all). +sum(A, B) -> A + B. +main() -> + A = 1, + B = 2, + sum(A, ). +}"#, + ); + } +} diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs new file mode 100644 index 0000000000..d3bdd80394 --- /dev/null +++ b/crates/ide/src/lib.rs @@ -0,0 +1,585 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::error::Error; +use std::sync::Arc; + +use anyhow::Result; +use call_hierarchy::CallItem; +use diagnostics::Diagnostic; +use diagnostics::DiagnosticsConfig; +use elp_ide_assists::Assist; +use elp_ide_assists::AssistConfig; +use elp_ide_assists::AssistId; +use elp_ide_assists::AssistKind; +use elp_ide_assists::AssistResolveStrategy; +use elp_ide_completion::Completion; +use elp_ide_db::assists::AssistContextDiagnostic; +use elp_ide_db::assists::AssistUserInput; +use elp_ide_db::docs::Doc; +use elp_ide_db::elp_base_db::salsa; +use elp_ide_db::elp_base_db::salsa::ParallelDatabase; +use elp_ide_db::elp_base_db::Change; +use elp_ide_db::elp_base_db::FileId; +use elp_ide_db::elp_base_db::FilePosition; +use elp_ide_db::elp_base_db::FileRange; +use elp_ide_db::elp_base_db::ModuleIndex; +use elp_ide_db::elp_base_db::ModuleName; +use elp_ide_db::elp_base_db::ProjectData; +use elp_ide_db::elp_base_db::ProjectId; +use elp_ide_db::elp_base_db::SourceDatabase; +use elp_ide_db::elp_base_db::SourceDatabaseExt; +use elp_ide_db::erlang_service::ParseResult; +use elp_ide_db::label::Label; +use elp_ide_db::rename::RenameError; +use elp_ide_db::source_change::SourceChange; +use elp_ide_db::Eqwalizer; +use elp_ide_db::EqwalizerDatabase; +use elp_ide_db::EqwalizerDiagnostics; +use elp_ide_db::EqwalizerStats; +use elp_ide_db::ErlAstDatabase; +use elp_ide_db::Includes; +use elp_ide_db::LineIndex; +use elp_ide_db::LineIndexDatabase; +use elp_ide_db::RootDatabase; +use elp_project_model::AppName; +use elp_project_model::AppType; +use elp_syntax::algo::ancestors_at_offset; +use elp_syntax::ast; +use elp_syntax::AstNode; +use expand_macro::ExpandedMacro; +use handlers::get_docs; +use handlers::goto_definition; +use handlers::references; +use hir::db::MinDefDatabase; +use hir::DefMap; +use hir::File; +use hir::Module; +use hir::Semantic; +use navigation_target::ToNav; + +mod annotations; +mod call_hierarchy; +mod codemod_helpers; +mod common_test; +mod doc_links; +mod document_symbols; +mod expand_macro; +mod extend_selection; +mod folding_ranges; +mod handlers; +mod inlay_hints; +mod navigation_target; +mod rename; +mod runnables; +mod signature_help; +mod syntax_highlighting; + +#[cfg(test)] +mod fixture; +#[cfg(test)] +mod tests; + +pub mod diagnostics; +pub mod diff; +mod highlight_related; +// @fb-only: mod meta_only; + +pub use annotations::Annotation; +pub use annotations::AnnotationKind; +pub use common_test::GroupName; +pub use document_symbols::DocumentSymbol; +pub use elp_ide_assists; +pub use elp_ide_completion; +pub use elp_ide_db; +pub use elp_ide_db::erlang_service; +pub use elp_syntax::TextRange; +pub use elp_syntax::TextSize; +pub use folding_ranges::Fold; +pub use folding_ranges::FoldKind; +pub use handlers::references::ReferenceSearchResult; +pub use highlight_related::HighlightedRange; +pub use inlay_hints::InlayHint; +pub use inlay_hints::InlayHintLabel; +pub use inlay_hints::InlayHintLabelPart; +pub use inlay_hints::InlayHintsConfig; +pub use inlay_hints::InlayKind; +pub use inlay_hints::InlayTooltip; +pub use navigation_target::NavigationTarget; +pub use runnables::Runnable; +pub use runnables::RunnableKind; +pub use signature_help::SignatureHelp; +pub use syntax_highlighting::tags::Highlight; +pub use syntax_highlighting::tags::HlMod; +pub use syntax_highlighting::tags::HlMods; +pub use syntax_highlighting::tags::HlTag; +pub use syntax_highlighting::HighlightConfig; +pub use syntax_highlighting::HlRange; + +pub type Cancellable = Result; + +/// Info associated with a text range. +#[derive(Debug)] +pub struct RangeInfo { + pub range: TextRange, + pub info: T, +} + +impl RangeInfo { + pub fn new(range: TextRange, info: T) -> RangeInfo { + RangeInfo { range, info } + } +} + +/// `AnalysisHost` stores the current state of the world. +#[derive(Debug, Default)] +pub struct AnalysisHost { + db: RootDatabase, +} + +impl AnalysisHost { + /// Returns a snapshot of the current state, which you can query for + /// semantic information. + pub fn analysis(&self) -> Analysis { + Analysis { + db: self.db.snapshot(), + } + } + + /// Trigger cancellations on all Analysis forked from the current database + pub fn request_cancellation(&mut self) { + self.db.request_cancellation(); + } + + pub fn raw_database(&self) -> &RootDatabase { + &self.db + } + pub fn raw_database_mut(&mut self) -> &mut RootDatabase { + &mut self.db + } + + /// Applies changes to the current state of the world. If there are + /// outstanding snapshots, they will be canceled. + pub fn apply_change(&mut self, change: Change) { + self.db.apply_change(change) + } +} + +/// Analysis is a snapshot of a world state at a moment in time. It is the main +/// entry point for asking semantic information about the world. When the world +/// state is advanced using `AnalysisHost::apply_change` method, all existing +/// `Analysis` are canceled (most method return `Err(Canceled)`). +#[derive(Debug)] +pub struct Analysis { + db: salsa::Snapshot, +} + +// As a general design guideline, `Analysis` API are intended to be independent +// from the language server protocol. That is, when exposing some functionality +// we should think in terms of "what API makes most sense" and not in terms of +// "what types LSP uses". We have at least 2 consumers of the API - LSP and CLI +impl Analysis { + /// Gets the file's `LineIndex`: data structure to convert between absolute + /// offsets and line/column representation. + pub fn line_index(&self, file_id: FileId) -> Cancellable> { + self.with_db(|db| db.file_line_index(file_id)) + } + + /// Computes the set of diagnostics for the given file. + pub fn diagnostics( + &self, + config: &DiagnosticsConfig, + file_id: FileId, + include_generated: bool, + ) -> Cancellable> { + self.with_db(|db| diagnostics::diagnostics(db, config, file_id, include_generated)) + } + + /// Computes the set of eqwalizer diagnostics for the given file. + pub fn eqwalizer_diagnostics( + &self, + project_id: ProjectId, + file_ids: Vec, + ) -> Cancellable> { + self.with_db(|db| db.eqwalizer_diagnostics(project_id, file_ids)) + } + + pub fn eqwalizer_stats( + &self, + project_id: ProjectId, + file_id: FileId, + ) -> Cancellable>> { + self.with_db(|db| db.eqwalizer_stats(project_id, file_id)) + } + + /// Computes the set of EDoc diagnostics for the given file. + pub fn edoc_diagnostics(&self, file_id: FileId) -> Cancellable)>> { + self.with_db(|db| diagnostics::edoc_diagnostics(db, file_id)) + } + + /// Computes the set of parse server diagnostics for the given file. + pub fn erlang_service_diagnostics( + &self, + file_id: FileId, + ) -> Cancellable)>> { + self.with_db(|db| diagnostics::erlang_service_diagnostics(db, file_id)) + } + + /// Low-level access to eqwalizer + pub fn eqwalizer(&self) -> &Eqwalizer { + self.db.eqwalizer() + } + + /// eqwalizer is enabled if: + /// - the app (the module belongs to) has `.eqwalizer` marker in the roof + /// - or the module has `-typing([eqwalizer]).` pragma + /// - or the whole project has `enable_all=true` in its `.elp.toml` file + pub fn is_eqwalizer_enabled( + &self, + file_id: FileId, + include_generated: bool, + ) -> Cancellable { + self.with_db(|db| db.is_eqwalizer_enabled(file_id, include_generated)) + } + + /// ETF for the module's abstract forms + pub fn module_ast( + &self, + file_id: FileId, + format: erlang_service::Format, + ) -> Cancellable> { + self.with_db(|db| db.module_ast(file_id, format)) + } + + pub fn project_id(&self, file_id: FileId) -> Cancellable> { + self.with_db(|db| Some(db.app_data(db.file_source_root(file_id))?.project_id)) + } + + pub fn project_data(&self, file_id: FileId) -> Cancellable>> { + self.with_db(|db| { + Some(db.project_data(db.app_data(db.file_source_root(file_id))?.project_id)) + }) + } + + /// Returns module name + pub fn module_name(&self, file_id: FileId) -> Cancellable> { + self.with_db(|db| { + let app_data = db.app_data(db.file_source_root(file_id))?; + db.module_index(app_data.project_id) + .module_for_file(file_id) + .cloned() + }) + } + + pub fn module_index(&self, project_id: ProjectId) -> Cancellable> { + self.with_db(|db| db.module_index(project_id)) + } + + pub fn module_file_id( + &self, + project_id: ProjectId, + module: &str, + ) -> Cancellable> { + self.with_db(|db| db.module_index(project_id).file_for_module(module)) + } + + pub fn expand_macro(&self, position: FilePosition) -> Cancellable> { + self.with_db(|db| expand_macro::expand_macro(db, position)) + } + + /// Selects the next syntactic nodes encompassing the range. + pub fn extend_selection(&self, frange: FileRange) -> Cancellable { + self.with_db(|db| extend_selection::extend_selection(db, frange)) + } + + /// Returns a list of symbols in the file. Useful to draw a + /// file outline. + pub fn document_symbols(&self, file_id: FileId) -> Cancellable> { + self.with_db(|db| document_symbols::document_symbols(db, file_id)) + } + + /// Returns the contents of a file + pub fn file_text(&self, file_id: FileId) -> Cancellable> { + self.with_db(|db| db.file_text(file_id)) + } + + /// Returns the app_type for a file + pub fn file_app_name(&self, file_id: FileId) -> Cancellable> { + self.with_db(|db| db.file_app_name(file_id)) + } + + /// Returns the app_type for a file + pub fn file_app_type(&self, file_id: FileId) -> Cancellable> { + self.with_db(|db| db.file_app_type(file_id)) + } + + /// Convenience function to return assists + quick fixes for diagnostics + pub fn assists_with_fixes( + &self, + assist_config: &AssistConfig, + diagnostics_config: &DiagnosticsConfig, + resolve: AssistResolveStrategy, + frange: FileRange, + context_diagnostics: &[AssistContextDiagnostic], + user_input: Option, + ) -> Cancellable> { + let include_fixes = match &assist_config.allowed { + Some(it) => it + .iter() + .any(|&it| it == AssistKind::None || it == AssistKind::QuickFix), + None => true, + }; + + self.with_db(|db| { + let diagnostic_assists = if include_fixes { + diagnostics::diagnostics(db, diagnostics_config, frange.file_id, false) + .into_iter() + .flat_map(|it| it.fixes.unwrap_or_default()) + .filter(|it| it.target.intersect(frange.range).is_some()) + .collect() + } else { + Vec::new() + }; + let assists = elp_ide_assists::assists( + db, + assist_config, + resolve, + frange, + &context_diagnostics, + user_input, + ); + + let mut res = diagnostic_assists; + res.extend(assists.into_iter()); + + res + }) + } + + pub fn is_generated(&self, file_id: FileId) -> Cancellable { + self.with_db(|db| db.is_generated(file_id)) + } + + pub fn is_test_suite_or_test_helper(&self, file_id: FileId) -> Cancellable> { + self.with_db(|db| db.is_test_suite_or_test_helper(file_id)) + } + + /// Search symbols. Only module names are currently supported. + pub fn symbol_search( + &self, + project_id: ProjectId, + query: &str, + ) -> Cancellable> { + const LIMIT: i32 = 128; + self.with_db(|db| { + let module_index = self.module_index(project_id).unwrap(); + let mut total = 0; + module_index + .all_modules() + .iter() + .filter_map(|name: &ModuleName| { + if total <= LIMIT && name.as_str().contains(query) { + let file_id = module_index.file_for_module(name)?; + let module = Module { + file: File { file_id }, + }; + total += 1; + Some(module.to_nav(db)) + } else { + None + } + }) + .collect() + }) + } + + pub fn goto_definition( + &self, + position: FilePosition, + ) -> Cancellable>>> { + self.with_db(|db| goto_definition::goto_definition(db, position)) + } + + /// Returns the docs for the symbol at the given position + pub fn get_docs_at_position( + &self, + position: FilePosition, + ) -> Cancellable> { + self.with_db(|db| get_docs::get_doc_at_position(db, position)) + } + + /// Finds all usages of the reference at point. + pub fn find_all_refs( + &self, + position: FilePosition, + ) -> Cancellable>> { + self.with_db(|db| references::find_all_refs(&Semantic::new(db), position)) + } + + pub fn completions( + &self, + position: FilePosition, + trigger_character: Option, + ) -> Cancellable> { + self.with_db(|db| elp_ide_completion::completions(db, position, trigger_character)) + } + + pub fn resolved_includes(&self, file_id: FileId) -> Cancellable> { + self.with_db(|db| db.resolved_includes(file_id)) + } + + /// Returns the edit required to rename the thing at the position to the new + /// name. + pub fn rename( + &self, + position: FilePosition, + new_name: &str, + ) -> Cancellable> { + self.with_db(|db| rename::rename(db, position, new_name)) + } + + /// Returns the set of folding ranges. + pub fn folding_ranges(&self, file_id: FileId) -> Cancellable> { + self.with_db(|db| folding_ranges::folding_ranges(db, file_id)) + } + + /// Computes call hierarchy candidates for the given file position. + pub fn call_hierarchy_prepare( + &self, + position: FilePosition, + ) -> Cancellable>>> { + self.with_db(|db| call_hierarchy::call_hierarchy_prepare(db, position)) + } + + /// Computes incoming calls for the given file position. + pub fn incoming_calls(&self, position: FilePosition) -> Cancellable>> { + self.with_db(|db| call_hierarchy::incoming_calls(db, position)) + } + + /// Computes outgoing calls for the given file position. + pub fn outgoing_calls(&self, position: FilePosition) -> Cancellable>> { + self.with_db(|db| call_hierarchy::outgoing_calls(db, position)) + } + + /// Computes parameter information at the given position. + pub fn signature_help( + &self, + position: FilePosition, + ) -> Cancellable, Option)>> { + self.with_db(|db| signature_help::signature_help(db, position)) + } + + /// Returns a list of the places in the file where type hints can be displayed. + pub fn inlay_hints( + &self, + config: &InlayHintsConfig, + file_id: FileId, + range: Option, + ) -> Cancellable> { + self.with_db(|db| inlay_hints::inlay_hints(db, file_id, range, config)) + } + + /// Computes syntax highlighting for the given file + pub fn highlight(&self, file_id: FileId) -> Cancellable> { + self.with_db(|db| syntax_highlighting::highlight(db, file_id, None)) + } + + /// Computes all ranges to highlight for a given item in a file. + pub fn highlight_related( + &self, + position: FilePosition, + ) -> Cancellable>> { + self.with_db(|db| highlight_related::highlight_related(&Semantic::new(db), position)) + } + + /// Computes syntax highlighting for the given file range. + pub fn highlight_range(&self, frange: FileRange) -> Cancellable> { + self.with_db(|db| syntax_highlighting::highlight(db, frange.file_id, Some(frange.range))) + } + + pub fn annotations(&self, file_id: FileId) -> Cancellable> { + self.with_db(|db| annotations::annotations(db, file_id)) + } + + pub fn runnables(&self, file_id: FileId) -> Cancellable> { + self.with_db(|db| runnables::runnables(db, file_id)) + } + + /// Return URL(s) for the documentation of the symbol under the cursor. + pub fn external_docs(&self, position: FilePosition) -> Cancellable>> { + self.with_db(|db| doc_links::external_docs(db, &position)) + } + + /// Return TextRange for the form enclosing the given position + pub fn enclosing_text_range(&self, position: FilePosition) -> Cancellable> { + self.with_db(|db| { + let sema = Semantic::new(db); + let source = sema.parse(position.file_id); + let syntax = source.value.syntax(); + let form = ancestors_at_offset(syntax, position.offset).find_map(ast::Form::cast)?; + Some(form.syntax().text_range()) + }) + } + + pub fn def_map(&self, file_id: FileId) -> Cancellable> { + self.with_db(|db| db.def_map(file_id)) + } + + /// Performs an operation on the database that may be canceled. + /// + /// ELP needs to be able to answer semantic questions about the + /// code while the code is being modified. A common problem is that a + /// long-running query is being calculated when a new change arrives. + /// + /// We can't just apply the change immediately: this will cause the pending + /// query to see inconsistent state (it will observe an absence of + /// repeatable read). So what we do is we **cancel** all pending queries + /// before applying the change. + /// + /// Salsa implements cancellation by unwinding with a special value and + /// catching it on the API boundary. + fn with_db(&self, f: F) -> Cancellable + where + F: FnOnce(&RootDatabase) -> T + std::panic::UnwindSafe, + { + salsa::Cancelled::catch(|| f(&self.db)) + } +} + +impl Clone for Analysis { + fn clone(&self) -> Self { + Analysis { + db: self.db.snapshot(), + } + } +} + +pub fn is_cancelled(e: &(dyn Error + 'static)) -> bool { + e.downcast_ref::().is_some() +} + +// --------------------------------------------------------------------- + +fn fix(id: &'static str, label: &str, source_change: SourceChange, target: TextRange) -> Assist { + let mut res = unresolved_fix(id, label, target); + res.source_change = Some(source_change); + res +} + +fn unresolved_fix(id: &'static str, label: &str, target: TextRange) -> Assist { + assert!(!id.contains(' ')); + Assist { + id: AssistId(id, AssistKind::QuickFix), + label: Label::new(label), + group: None, + target, + source_change: None, + user_input: None, + } +} diff --git a/crates/ide/src/navigation_target.rs b/crates/ide/src/navigation_target.rs new file mode 100644 index 0000000000..2e52accb22 --- /dev/null +++ b/crates/ide/src/navigation_target.rs @@ -0,0 +1,248 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! See [`NavigationTarget`]. + +use std::fmt; + +use elp_ide_db::elp_base_db::FileId; +use elp_ide_db::elp_base_db::FileRange; +use elp_ide_db::SymbolDefinition; +use elp_ide_db::SymbolKind; +use elp_syntax::ast; +use elp_syntax::AstNode; +use elp_syntax::SmolStr; +use elp_syntax::TextRange; +use hir::db::MinDefDatabase; + +/// `NavigationTarget` represents an element in the editor's UI which you can +/// click on to navigate to a particular piece of code. +/// +/// Typically, a `NavigationTarget` corresponds to some element in the source +/// code, like a function or a record, but this is not strictly required. +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct NavigationTarget { + pub file_id: FileId, + /// Range which encompasses the whole element. + /// + /// Should include body, doc comments, attributes, etc. + /// + /// Clients should use this range to answer "is the cursor inside the + /// element?" question. + pub full_range: TextRange, + /// A "most interesting" range within the `full_range`. + /// + /// Typically, `full_range` is the whole syntax node, including doc + /// comments, and `focus_range` is the range of the identifier. + /// + /// Clients should place the cursor on this range when navigating to this target. + pub focus_range: Option, + pub name: SmolStr, + pub kind: SymbolKind, +} + +impl fmt::Debug for NavigationTarget { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut f = f.debug_struct("NavigationTarget"); + f.field("file_id", &self.file_id); + f.field("full_range", &self.full_range); + f.field("name", &self.name); + if let Some(focus_range) = &self.focus_range { + f.field("focus_range", focus_range); + } + f.finish() + } +} + +impl NavigationTarget { + /// Returns either the focus range, or the full range, if not available + /// anchored to the file_id + pub fn range(&self) -> TextRange { + self.focus_range.unwrap_or(self.full_range) + } + + /// Returns either the focus range, or the full range, if not available + /// anchored to the file_id + pub fn file_range(&self) -> FileRange { + FileRange { + file_id: self.file_id, + range: self.range(), + } + } +} + +pub trait ToNav { + fn to_nav(&self, db: &dyn MinDefDatabase) -> NavigationTarget; +} + +impl ToNav for SymbolDefinition { + fn to_nav(&self, db: &dyn MinDefDatabase) -> NavigationTarget { + match self { + SymbolDefinition::Module(it) => it.to_nav(db), + SymbolDefinition::Function(it) => it.to_nav(db), + SymbolDefinition::Record(it) => it.to_nav(db), + SymbolDefinition::RecordField(it) => it.to_nav(db), + SymbolDefinition::Type(it) => it.to_nav(db), + SymbolDefinition::Callback(it) => it.to_nav(db), + SymbolDefinition::Define(it) => it.to_nav(db), + SymbolDefinition::Header(it) => it.to_nav(db), + SymbolDefinition::Var(it) => it.to_nav(db), + } + } +} + +impl ToNav for hir::Module { + fn to_nav(&self, db: &dyn MinDefDatabase) -> NavigationTarget { + let file_id = self.file.file_id; + let source = self.file.source(db.upcast()); + let full_range = source.syntax().text_range(); + let attr = self.module_attribute(db); + let focus_range = attr + .as_ref() + .map(|attr| attr.form_id.get(&source).syntax().text_range()); + NavigationTarget { + file_id, + full_range, + focus_range, + name: self.name(db).raw(), + kind: SymbolKind::Module, + } + } +} + +impl ToNav for hir::FunctionDef { + fn to_nav(&self, db: &dyn MinDefDatabase) -> NavigationTarget { + let file_id = self.file.file_id; + let source = self.source(db.upcast()); + let full_range = source.syntax().text_range(); + let focus_range = source.clauses().find_map(|clause| match clause { + ast::FunctionOrMacroClause::FunctionClause(clause) => { + clause.name().map(|name| name.syntax().text_range()) + } + ast::FunctionOrMacroClause::MacroCallExpr(_) => None, + }); + let arity = self.function.name.arity(); + let name = self.function.name.name().raw(); + NavigationTarget { + file_id, + full_range, + focus_range, + name: SmolStr::new(format!("{name}/{arity}")), + kind: SymbolKind::Function, + } + } +} + +impl ToNav for hir::RecordDef { + fn to_nav(&self, db: &dyn MinDefDatabase) -> NavigationTarget { + let file_id = self.file.file_id; + let source = self.source(db.upcast()); + let full_range = source.syntax().text_range(); + let focus_range = source.name().map(|name| name.syntax().text_range()); + NavigationTarget { + file_id, + full_range, + focus_range, + name: self.record.name.raw(), + kind: SymbolKind::Record, + } + } +} + +impl ToNav for hir::RecordFieldDef { + fn to_nav(&self, db: &dyn MinDefDatabase) -> NavigationTarget { + let file_id = self.record.file.file_id; + let source = self.source(db.upcast()); + let full_range = source.syntax().text_range(); + let focus_range = source.name().map(|name| name.syntax().text_range()); + NavigationTarget { + file_id, + full_range, + focus_range, + name: self.field.name.raw(), + kind: SymbolKind::RecordField, + } + } +} + +impl ToNav for hir::TypeAliasDef { + fn to_nav(&self, db: &dyn MinDefDatabase) -> NavigationTarget { + let file_id = self.file.file_id; + let source = self.source(db.upcast()); + let full_range = source.syntax().text_range(); + let focus_range = source.type_name().map(|name| name.syntax().text_range()); + NavigationTarget { + file_id, + full_range, + focus_range, + name: self.type_alias.name().name().raw(), + kind: SymbolKind::Type, + } + } +} + +impl ToNav for hir::CallbackDef { + fn to_nav(&self, db: &dyn MinDefDatabase) -> NavigationTarget { + let file_id = self.file.file_id; + let source = self.source(db.upcast()); + let full_range = source.syntax().text_range(); + let focus_range = source.fun().map(|name| name.syntax().text_range()); + NavigationTarget { + file_id, + full_range, + focus_range, + name: self.callback.name.name().raw(), + kind: SymbolKind::Callback, + } + } +} + +impl ToNav for hir::DefineDef { + fn to_nav(&self, db: &dyn MinDefDatabase) -> NavigationTarget { + let file_id = self.file.file_id; + let source = self.source(db.upcast()); + let full_range = source.syntax().text_range(); + let focus_range = source.lhs().map(|lhs| lhs.syntax().text_range()); + NavigationTarget { + file_id, + full_range, + focus_range, + name: self.define.name.name().raw(), + kind: SymbolKind::Define, + } + } +} + +impl ToNav for hir::File { + fn to_nav(&self, db: &dyn MinDefDatabase) -> NavigationTarget { + let source = self.source(db.upcast()); + let full_range = source.syntax().text_range(); + NavigationTarget { + file_id: self.file_id, + full_range, + focus_range: None, + name: self.name(db.upcast()), + kind: SymbolKind::File, + } + } +} + +impl ToNav for hir::VarDef { + fn to_nav(&self, db: &dyn MinDefDatabase) -> NavigationTarget { + let source = self.source(db.upcast()); + let full_range = source.syntax().text_range(); + NavigationTarget { + file_id: self.file.file_id, + full_range, + focus_range: None, + name: self.name(db.upcast()).raw(), + kind: SymbolKind::Variable, + } + } +} diff --git a/crates/ide/src/rename.rs b/crates/ide/src/rename.rs new file mode 100644 index 0000000000..ea9e60927b --- /dev/null +++ b/crates/ide/src/rename.rs @@ -0,0 +1,1025 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! Renaming functionality. + +use elp_ide_db::elp_base_db::FilePosition; +use elp_ide_db::rename::format_err; +use elp_ide_db::rename::rename_error; +use elp_ide_db::rename::RenameError; +use elp_ide_db::rename::RenameResult; +use elp_ide_db::rename::SafetyChecks; +use elp_ide_db::source_change::SourceChange; +use elp_ide_db::ReferenceClass; +use elp_ide_db::RootDatabase; +use elp_ide_db::SymbolClass; +use elp_ide_db::SymbolDefinition; +use elp_syntax::algo; +use elp_syntax::ast; +use elp_syntax::AstNode; +use elp_syntax::SyntaxNode; +use hir::InFile; +use hir::Semantic; + +// Feature: Rename +// +// Renames the item below the cursor and all of its references +// +// |=== +// | Editor | Shortcut +// +// | VS Code | kbd:[F2] +// |=== +// +pub(crate) fn rename( + db: &RootDatabase, + position: FilePosition, + new_name: &str, +) -> RenameResult { + let sema = Semantic::new(db); + let file_id = position.file_id; + let source_file = sema.parse(file_id); + let syntax = source_file.value.syntax(); + let new_name = new_name.trim(); + + let defs = find_definitions(&sema, syntax, position)?; + + let ops: RenameResult> = defs + .iter() + .map(|def| def.rename(&sema, &|_| new_name.to_string(), SafetyChecks::Yes)) + .collect(); + + ops?.into_iter() + .reduce(|acc, elem| acc.merge(elem)) + .ok_or_else(|| format_err!("No references found at position")) +} + +fn find_definitions( + sema: &Semantic, + syntax: &SyntaxNode, + position: FilePosition, +) -> RenameResult> { + let symbols = + if let Some(name_like) = algo::find_node_at_offset::(syntax, position.offset) { + let res = match &name_like { + ast::Name::Var(var) => { + let def = sema.to_def::(InFile { + file_id: position.file_id, + value: var, + }); + if let Some(defs) = def { + match defs { + hir::DefinitionOrReference::Definition(def) => { + Some(Ok(vec![SymbolDefinition::Var(def)])) + } + hir::DefinitionOrReference::Reference(defs) => Some(Ok(defs + .into_iter() + .map(|def| SymbolDefinition::Var(def)) + .collect::>())), + } + } else { + None + } + } + ast::Name::Atom(atom) => { + if let Some(token) = atom.syntax().first_token() { + let location = InFile { + file_id: position.file_id, + value: token.clone(), + }; + match SymbolClass::classify(sema, location) { + Some(SymbolClass::Definition(def)) => Some(Ok(vec![def])), + Some(SymbolClass::Reference { refs, typ: _ }) => match refs { + ReferenceClass::Definition(def) => Some(Ok(vec![def])), + ReferenceClass::MultiVar(defs) => Some(Ok(defs + .into_iter() + .map(|def| SymbolDefinition::Var(def)) + .collect::>())), + ReferenceClass::MultiMacro(_) => None, + }, + None => None, + } + } else { + None + } + } + ast::Name::MacroCallExpr(_) => None, + }; + res + } else { + rename_error!("No references found at position") + }; + + if let Some(res) = symbols { + res + } else { + rename_error!("No references found at position") + } +} + +#[cfg(test)] +mod tests { + use elp_ide_db::elp_base_db::assert_eq_text; + use elp_ide_db::elp_base_db::test_fixture::trim_indent; + use text_edit::TextEdit; + + use crate::fixture; + + #[track_caller] + fn check(new_name: &str, fixture_before: &str, fixture_after_str: &str) { + let fixture_after_str = &trim_indent(fixture_after_str); + let analysis_after = fixture::multi_file(fixture_after_str); + + let (analysis, position) = fixture::position(fixture_before); + let rename_result = analysis + .rename(position, new_name) + .unwrap_or_else(|err| panic!("Rename to '{}' was cancelled: {}", new_name, err)); + match rename_result { + Ok(source_change) => { + for edit in source_change.source_file_edits { + let mut text_edit_builder = TextEdit::builder(); + let file_id = edit.0; + for indel in edit.1.into_iter() { + text_edit_builder.replace(indel.delete, indel.insert); + } + let mut result = analysis.file_text(file_id).unwrap().to_string(); + let edit = text_edit_builder.finish(); + edit.apply(&mut result); + let expected = analysis_after.file_text(file_id).unwrap().to_string(); + assert_eq_text!(&*expected, &*result); + } + } + Err(err) => { + if fixture_after_str.starts_with("error:") { + let error_message = fixture_after_str + .chars() + .into_iter() + .skip("error:".len()) + .collect::(); + assert_eq!(error_message.trim(), err.to_string()); + } else { + panic!("Rename to '{}' failed unexpectedly: {}", new_name, err) + } + } + }; + } + + #[test] + fn test_rename_var_1() { + check("Y", r#"main() -> I~ = 1."#, r#"main() -> Y = 1."#); + } + + #[test] + fn test_rename_var_2() { + check( + "Y", + r#"main() -> + I~ = 1, + I + 2."#, + r#"main() -> + Y = 1, + Y + 2."#, + ); + } + + #[test] + fn test_rename_var_3() { + check( + "Y", + r#"main(X) -> + case X of + 1 -> Z = 2; + 2 -> Z = 3 + end, + ~Z + 2."#, + r#"main(X) -> + case X of + 1 -> Y = 2; + 2 -> Y = 3 + end, + Y + 2."#, + ); + } + + #[test] + fn test_rename_var_4() { + check( + "Y", + r#"testz() -> + case rand:uniform(2) of + 1 -> + Z = 1; + 2 -> + ~Z = 2; + Z -> + ok + end, + Z."#, + r#"testz() -> + case rand:uniform(2) of + 1 -> + Y = 1; + 2 -> + Y = 2; + Y -> + ok + end, + Y."#, + ); + } + + #[test] + fn test_rename_var_5() { + check( + "YY", + r#"main(_) -> + Y = 5, + AssertIs5 = fun (X) -> + ~Y = X, + erlang:display(Y) + end, + AssertIs5(2), + erlang:display(Y), + ok."#, + r#"main(_) -> + YY = 5, + AssertIs5 = fun (X) -> + YY = X, + erlang:display(YY) + end, + AssertIs5(2), + erlang:display(YY), + ok."#, + ); + } + + #[test] + fn test_rename_var_6() { + check( + "ZZ", + r#"main(_) -> + Z = 2, + case 3 of + 3 -> ~Z = 2 + end."#, + r#"main(_) -> + ZZ = 2, + case 3 of + 3 -> ZZ = 2 + end."#, + ); + } + + #[test] + fn test_rename_var_7() { + check( + "Y", + r#"main() -> + I = 1, + I~ + 2."#, + r#"main() -> + Y = 1, + Y + 2."#, + ); + } + + #[test] + fn test_rename_var_name_clash_1() { + check( + "Y", + r#"main(Y) -> + I~ = 1, + I + Y."#, + r#"error: Name 'Y' already in scope"#, + ); + } + + #[test] + fn test_rename_var_but_not_shadowed() { + check( + "Z", + r#"triples( Self, X, Y, none )-> + [ Result || Result = { X~, Y, _} <- Self ]."#, + r#"triples( Self, X, Y, none )-> + [ Result || Result = { Z, Y, _} <- Self ]."#, + ); + } + + // ----------------------------------------------------------------- + + #[test] + fn test_rename_local_function_no_calls() { + check( + "new_fun", + r#"trip~les( Self, X, Y, none )-> + [ Result || Result = { X, Y, _} <- Self ]."#, + r#"new_fun( Self, X, Y, none )-> + [ Result || Result = { X, Y, _} <- Self ]."#, + ); + } + + #[test] + fn test_rename_local_function_with_calls_1() { + check( + "new_fun", + r#"fo~o() -> ok. + bar() -> foo()."#, + r#"new_fun() -> ok. + bar() -> new_fun()."#, + ); + } + + #[test] + fn test_rename_local_function_with_calls_2() { + check( + "new_fun", + r#"fo~o() -> ok. + bar() -> baz(),foo()."#, + r#"new_fun() -> ok. + bar() -> baz(),new_fun()."#, + ); + } + + #[test] + fn test_rename_local_function_with_calls_3() { + check( + "new_fun", + r#"fo~o(0) -> 0; + foo(X) -> foo(X - 1). + bar() -> foo(3)."#, + r#"new_fun(0) -> 0; + new_fun(X) -> new_fun(X - 1). + bar() -> new_fun(3)."#, + ); + } + + #[test] + fn test_rename_local_function_with_calls_4() { + check( + "new_fun", + r#"test1() -> + ok. + foo() -> te~st1()."#, + r#"new_fun() -> + ok. + foo() -> new_fun()."#, + ); + } + + #[test] + fn test_rename_local_function_fails_name_clash_1() { + check( + "new_fun", + r#"fo~o() -> ok. + new_fun() -> ok."#, + r#"error: Function 'new_fun/0' already in scope"#, + ); + } + + #[test] + fn test_rename_local_function_fails_name_clash_2() { + check( + "foo", + r#"foo() -> ok. + b~ar() -> ok."#, + r#"error: Function 'foo/0' already in scope"#, + ); + } + + #[test] + fn test_rename_local_function_fails_name_clash_checks_arity() { + check( + "new_fun", + r#"fo~o() -> ok. + new_fun(X) -> ok."#, + r#"new_fun() -> ok. + new_fun(X) -> ok."#, + ); + } + + #[test] + fn test_rename_local_function_fails_name_clash_imported_function() { + check( + "new_fun", + r#"-import(bar, [new_fun/0]). + fo~o() -> ok."#, + r#"error: Function 'new_fun/0' already in scope"#, + ); + } + + #[test] + fn test_rename_local_function_fails_name_clash_erlang_function() { + check( + "alias", + r#"fo~o() -> ok."#, + r#"error: Function 'alias/0' already in scope"#, + ); + } + + #[test] + fn test_rename_local_function_also_name_in_macro() { + check( + "new", + r#"-define(FOO, foo). + fo~o() -> ok. + bar() -> ?FOO()"#, + r#"-define(FOO, foo). + new() -> ok. + bar() -> ?FOO()"#, + ); + } + + #[test] + fn test_rename_local_var_trims_surrounding_spaces() { + check(" Aaa ", r#"foo() -> V~ar = 3."#, r#"foo() -> Aaa = 3."#); + } + + #[test] + fn test_rename_local_function_trims_surrounding_spaces() { + check(" aaa ", r#"fo~o() -> Var = 3."#, r#"aaa() -> Var = 3."#); + } + + #[test] + fn test_rename_local_var_fails_invalid_var_name() { + check( + "aaa", + r#"foo() -> V~ar = 3."#, + r#"error: Invalid new variable name: 'aaa'"#, + ); + } + + #[test] + fn test_rename_local_function_fails_invalid_function_name_1() { + check( + "Foo", + r#"fo~o() -> ok."#, + r#"error: Invalid new function name: 'Foo'"#, + ); + } + + #[test] + fn test_rename_local_function_fails_invalid_function_name_2() { + check( + "TT TTT", + r#"fo~o() -> ok."#, + r#"error: Invalid new function name: 'TT TTT'"#, + ); + } + + #[test] + fn test_rename_local_function_fails_invalid_function_name_3() { + check( + "TTT", + r#"fo~o() -> ok."#, + r#"error: Invalid new function name: 'TTT'"#, + ); + } + + #[test] + fn test_rename_var_d39578003_case_5() { + check( + "_G", + r#"main(_) -> + fun F() -> + case rand:uniform(2) of + 1 -> F(); + _ -> ok + end, + {_, _} = catch ~F = 3 + end()."#, + r#"main(_) -> + fun _G() -> + case rand:uniform(2) of + 1 -> _G(); + _ -> ok + end, + {_, _} = catch _G = 3 + end()."#, + ); + } + + #[test] + fn test_rename_in_spawn_1() { + check( + "new_name", + r#"foo() -> + Pid = erlang:spawn(fun noop_~group_leader/0), + ok. + + noop_group_leader() -> ok."#, + r#"foo() -> + Pid = erlang:spawn(fun new_name/0), + ok. + + new_name() -> ok."#, + ); + } + + #[test] + fn test_rename_in_spawn_2() { + check( + "new_name", + r#"foo() -> + Pid = erlang:spawn(fun noop_group_leader/0), + ok. + + noop_group_~leader() -> ok."#, + r#"foo() -> + Pid = erlang:spawn(fun new_name/0), + ok. + + new_name() -> ok."#, + ); + } + + #[test] + fn test_rename_in_apply_1() { + check( + "new_name", + r#" + //- /src/baz.erl + -module(baz). + foo() -> + apply(?MODULE, bar, []). + + b~ar() -> + ok."#, + r#" + -module(baz). + foo() -> + apply(?MODULE, new_name, []). + + new_name() -> + ok."#, + ); + } + + #[test] + fn test_rename_in_apply_2() { + check( + "new_name", + r#" + //- /src/baz.erl + -module(baz). + foo() -> + apply(baz, b~ar, []). + bar() -> + ok."#, + r#" + -module(baz). + foo() -> + apply(baz, new_name, []). + new_name() -> + ok."#, + ); + } + + #[test] + fn test_rename_in_apply_args_1() { + check( + "new_name", + r#" + //- /src/baz.erl + -module(baz). + foo() -> + apply(baz, b~ar, [1]). + bar(X) -> + ok. + bar() -> + ok."#, + r#" + -module(baz). + foo() -> + apply(baz, new_name, [1]). + new_name(X) -> + ok. + bar() -> + ok."#, + ); + } + + #[test] + fn test_rename_in_apply_args_2() { + check( + "new_name", + r#" + //- /src/baz.erl + -module(baz). + foo(XS) -> + apply(baz, b~ar, [1|XS]). + bar(X) -> + ok. + bar() -> + ok."#, + r#"error: No references found at position"#, + ); + } + + #[test] + fn test_rename_in_apply_3() { + check( + "new_name", + r#" + //- /src/baz.erl + -module(baz). + foo() -> + X = bar(), + apply(b~ar, []). + bar() -> + ok."#, + r#" + -module(baz). + foo() -> + X = new_name(), + apply(new_name, []). + new_name() -> + ok."#, + ); + } + + #[test] + fn test_rename_in_apply_4() { + check( + "new_name", + r#" + //- /src/baz.erl + -module(baz). + foo() -> + erlang:apply(?MODULE, b~ar, []), + erlang:apply(bar, []). + bar() -> + ok."#, + r#" + -module(baz). + foo() -> + erlang:apply(?MODULE, new_name, []), + erlang:apply(new_name, []). + new_name() -> + ok."#, + ); + } + + #[test] + fn test_rename_in_apply_5() { + check( + "new_name", + r#" + //- /src/baz.erl + -module(baz). + foo() -> + other_mod:apply(b~ar, []). + bar() -> + ok."#, + r#"error: No references found at position"#, + ); + } + + #[test] + fn test_rename_function_with_spec_1() { + check( + "new_name", + r#" + -spec foo() -> ok. + fo~o() -> + ok."#, + r#" + -spec new_name() -> ok. + new_name() -> + ok."#, + ); + } + + #[test] + fn test_rename_function_with_spec_2() { + check( + "new_name", + r#" + -spec f~oo() -> ok. + foo() -> + ok."#, + r#" + -spec new_name() -> ok. + new_name() -> + ok."#, + ); + } + + #[test] + fn test_rename_function_with_spec_3() { + check( + "new_name", + r#" + -spec f~oo(any()) -> ok. + foo(1) -> ok; + foo(_) -> oops."#, + r#" + -spec new_name(any()) -> ok. + new_name(1) -> ok; + new_name(_) -> oops."#, + ); + } + + #[test] + fn test_rename_underscore_1() { + check( + "NewName", + r#" + foo() -> + ~_ = foo(), + ok."#, + r#"error: No references found at position"#, + ); + } + + #[test] + fn test_rename_underscore_2() { + check( + "NewName", + r#" + foo(X) -> + ~_ = foo(1), + _ = foo(2), + X."#, + r#"error: No references found at position"#, + ); + } + + #[test] + fn test_rename_case_1() { + check( + "XX", + r#" + foo(X) -> + X2 = case X of + [X~0, X1] -> X0 + X1; + [] -> X0 = 1 + end, + X0 + X2."#, + r#" + foo(X) -> + X2 = case X of + [XX, X1] -> XX + X1; + [] -> XX = 1 + end, + XX + X2."#, + ); + } + + #[test] + fn test_rename_case_2() { + check( + "XX", + r#" + foo() -> + X~0 = 42, + X1 = X0 + 1, + [ X0 || X0 <- [X1] ]."#, + r#" + foo() -> + XX = 42, + X1 = XX + 1, + [ X0 || X0 <- [X1] ]."#, + ); + } + + #[test] + fn test_rename_case_3() { + check( + "XX", + r#" + foo() -> + [ X1 || X1 <- [X~0 = 2] ], + X1 = 3."#, + r#" + foo() -> + [ X1 || X1 <- [XX = 2] ], + X1 = 3."#, + ); + } + + #[test] + fn rename_export_function() { + check( + "new_name", + r#" + //- /src/baz.erl + -module(baz). + -export([foo/0]). + foo() -> ok. + bar() -> f~oo(). + + //- /src/bar.erl + -module(bar). + another_fun() -> + baz:foo(), + ok. + "#, + r#" + //- /src/baz.erl + -module(baz). + -export([new_name/0]). + new_name() -> ok. + bar() -> new_name(). + + //- /src/bar.erl + -module(bar). + another_fun() -> + baz:new_name(), + ok. + "#, + ); + } + + #[test] + fn rename_export_function_2() { + check( + "new_name", + r#" + //- /src/baz.erl + -module(baz). + -export([foo/0]). + foo() -> ok. + bar() -> f~oo(). + + //- /src/bar.erl + -module(bar). + -import(baz, [foo/0]). + another_fun() -> + foo(), + ok. + "#, + r#" + //- /src/baz.erl + -module(baz). + -export([new_name/0]). + new_name() -> ok. + bar() -> new_name(). + + //- /src/bar.erl + -module(bar). + -import(baz, [new_name/0]). + another_fun() -> + new_name(), + ok. + "#, + ); + } + + #[test] + fn rename_export_function_fails() { + check( + "new_name", + r#" + //- /src/baz.erl + -module(baz). + -export([foo/0]). + foo() -> ok. + bar() -> f~oo(). + + //- /src/bar.erl + -module(bar). + -import(baz, [foo/0]). + another_fun() -> + foo(), + ok. + new_name() -> ok. + "#, + r#"error: Function 'new_name/0' already in scope in module 'bar'"#, + ); + } + + #[test] + fn rename_function_in_include() { + // We do not have functions defined in our header files, but + // confirm it does the right thing anyway + check( + "new_name", + r#" + //- /src/main.hrl + %% main.hrl + -spec bar() -> ok. + bar() -> ok. + + //- /src/main.erl + %% main.erl + -include("main.hrl"). + baz() -> ba~r(). + + //- /src/another.erl + %% another.erl + -include("main.hrl"). + + foo() -> bar(). + + //- /src/different_bar.erl + %% different_bar.erl + bar() -> different. + + should_not_match() -> bar(). + "#, + r#" + //- /src/main.hrl + %% main.hrl + -spec new_name() -> ok. + new_name() -> ok. + + //- /src/main.erl + %% main.erl + -include("main.hrl"). + baz() -> new_name(). + + //- /src/another.erl + %% another.erl + -include("main.hrl"). + + foo() -> new_name(). + + //- /src/different_bar.erl + %% different_bar.erl + bar() -> different. + + should_not_match() -> bar(). + "#, + ); + } + + #[test] + fn test_rename_in_macro_rhs_1() { + check( + "new_name", + r#" + -define(BAR(X), foo(X)). + baz() -> ?BAR(3). + fo~o(X) -> + X."#, + r#" + -define(BAR(X), new_name(X)). + baz() -> ?BAR(3). + new_name(X) -> + X."#, + ); + } + + #[test] + fn rename_with_macro() { + check( + "NewName", + r#" + //- /src/main.hrl + %% main.hrl + -define(config,test_server:lookup_config). + + //- /src/main.erl + %% main.erl + -include("main.hrl"). + start_apps(Config) -> + PrivDir = ?config(priv_dir, Config), + {ok, A~pps} = {ok, [foo]}, + [{apps, Apps}]. + "#, + r#" + //- /src/main.hrl + %% main.hrl + -define(config,test_server:lookup_config). + + //- /src/main.erl + %% main.erl + -include("main.hrl"). + start_apps(Config) -> + PrivDir = ?config(priv_dir, Config), + {ok, NewName} = {ok, [foo]}, + [{apps, NewName}]. + "#, + ); + } + + // Document that at the moment there are a few corner cases + // where we incorrectly rename type definitions within a macro. + // See T157498333 + #[test] + fn rename_function_with_macro_type() { + check( + "newFoo", + r#" + -module(main). + -define(TY, foo()). + -spec x(?TY) -> ok. + x(_) -> foo(). + fo~o() -> ok. + "#, + r#" + -module(main). + -define(TY, newFoo()). + -spec x(?TY) -> ok. + x(_) -> newFoo(). + newFoo() -> ok. + "#, + ); + } +} diff --git a/crates/ide/src/runnables.rs b/crates/ide/src/runnables.rs new file mode 100644 index 0000000000..0c018f9d42 --- /dev/null +++ b/crates/ide/src/runnables.rs @@ -0,0 +1,418 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_ide_db::elp_base_db::FileId; +use elp_ide_db::RootDatabase; +use elp_project_model::AppName; +use hir::NameArity; +use hir::Semantic; + +use crate::common_test; +use crate::NavigationTarget; + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct Runnable { + pub nav: NavigationTarget, + pub kind: RunnableKind, +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub enum RunnableKind { + Test { + name: NameArity, + app_name: AppName, + suite: String, + case: String, + group: common_test::GroupName, + }, + Suite, +} + +impl Runnable { + pub fn label(&self, _target: Option) -> String { + match &self.kind { + RunnableKind::Test { .. } => format!("test"), + RunnableKind::Suite => format!("test"), + } + } + pub fn id(&self) -> String { + match &self.kind { + RunnableKind::Test { + suite, case, group, .. + } => { + let group = group.name(); + format!("{suite} - {group}.{case}") + } + RunnableKind::Suite => "".to_string(), + } + } + pub fn regex(&self) -> String { + match &self.kind { + RunnableKind::Test { + app_name, + suite, + case, + group, + .. + } => { + let group = group.name(); + format!("{app_name}:{suite} - {group}.{case}$") + } + RunnableKind::Suite => "".to_string(), + } + } + pub fn buck2_args(&self, target: String) -> Vec { + let mut args = Vec::new(); + match &self.kind { + RunnableKind::Test { .. } => { + args.push(target); + args.push("--".to_string()); + args.push("--regex".to_string()); + args.push(self.regex()); + args.push("--print-passing-details".to_string()); + args.push("--run-disabled".to_string()); + } + RunnableKind::Suite => { + args.push(target); + args.push("--".to_string()); + args.push("--print-passing-details".to_string()); + args.push("--run-disabled".to_string()); + } + } + args + } + + // The Unicode variation selector is appended to the play button to avoid that + // the play symbol is transformed into an emoji + pub fn run_title(&self) -> String { + match &self.kind { + RunnableKind::Test { group, .. } => match group { + common_test::GroupName::NoGroup => String::from(format!("▶\u{fe0e} Run Test")), + common_test::GroupName::Name(name) => { + String::from(format!("▶\u{fe0e} Run Test (in {})", name)) + } + }, + RunnableKind::Suite => String::from(format!("▶\u{fe0e} Run All Tests")), + } + } + pub fn debug_title(&self) -> String { + match &self.kind { + RunnableKind::Test { group, .. } => match group { + common_test::GroupName::NoGroup => String::from(format!("▶\u{fe0e} Debug")), + common_test::GroupName::Name(name) => { + String::from(format!("▶\u{fe0e} Debug (in {})", name)) + } + }, + RunnableKind::Suite => String::from(format!("▶\u{fe0e} Debug")), + } + } +} + +// Feature: Run +// +// Shows a popup suggesting to run a test **at the current cursor +// location**. Super useful for repeatedly running just a single test. Do bind this +// to a shortcut! +// +// |=== +// | Editor | Action Name +// +// | VS Code | **ELP: Run** +// |=== +pub(crate) fn runnables(db: &RootDatabase, file_id: FileId) -> Vec { + let sema = Semantic::new(db); + match common_test::runnables(&sema, file_id) { + Ok(runnables) => runnables, + Err(_) => Vec::new(), + } +} + +#[cfg(test)] +mod tests { + + use elp_ide_db::elp_base_db::FileRange; + use stdx::trim_indent; + + use crate::fixture; + + #[track_caller] + fn check_runnables(fixture: &str) { + let (analysis, pos, mut annotations) = fixture::annotations(trim_indent(fixture).as_str()); + let runnables = analysis.runnables(pos.file_id).unwrap(); + let mut actual = Vec::new(); + for runnable in runnables { + let file_id = runnable.nav.file_id; + let range = runnable.nav.focus_range.unwrap(); + // Remove all non-ascii character to avoid repeating Unicode variation selectors in every test + let text = trim_indent( + runnable + .run_title() + .replace(|c: char| !c.is_ascii(), "") + .as_str(), + ); + actual.push((FileRange { file_id, range }, text)); + } + let cmp = |(frange, text): &(FileRange, String)| { + (frange.file_id, frange.range.start(), text.clone()) + }; + actual.sort_by_key(cmp); + annotations.sort_by_key(cmp); + assert_eq!(actual, annotations); + } + + #[test] + fn runnables_no_suite() { + check_runnables( + r#" + //- /my_app/src/main.erl + ~ + -module(main). + -export([all/]). + main() -> + ok. + "#, + ); + } + + #[test] + fn runnables_suite() { + check_runnables( + r#" + //- /my_app/test/my_common_test_SUITE.erl + ~ + -module(my_common_test_SUITE). + %% ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Run All Tests + -export([all/0, groups/0]). + -export([a/1, b/1, c/1]). + all() -> [a, b, {group, gc1}]. + groups() -> [{gc1, [], [c, {gc2, [], [d]}]}]. + a(_Config) -> + %% ^ Run Test + ok. + b(_Config) -> + %% ^ Run Test + ok. + c(_Config) -> + %% ^ Run Test (in gc1) + ok. + "#, + ); + } + + #[test] + fn runnables_suite_not_exported() { + check_runnables( + r#" + //- /my_app/test/my_common_test_SUITE.erl + ~ + -module(my_common_test_SUITE). + %% ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Run All Tests + -export([all/0, groups/0]). + -export([a/1]). + all() -> [a, b]. + a(_Config) -> + %% ^ Run Test + ok. + b(_Config) -> + ok. + "#, + ); + } + + #[test] + fn runnables_nested_groups() { + check_runnables( + r#" + //- /my_app/test/nested_SUITE.erl + ~ + -module(nested_SUITE). + %% ^^^^^^^^^^^^^^^^^^^^^^ Run All Tests + + -export([all/0, groups/0]). + -export([tc1/1, tc2/1, tc3/1, tc4/1, tc5/1]). + all() -> + [tc1, {group, g1}, {group, g2, [], [{sg21, []}]}]. + + groups() -> + [ {g1, [], [tc2, {group, g3}]} + , {g2, [], [tc3, {group, sg21}]} + , {g3, [], [tc4]} + , {sg21, [], [tc5]} + ]. + + tc1(_) -> + %% ^^^ Run Test + ok. + tc2(_) -> + %% ^^^ Run Test (in g1) + ok. + tc3(_) -> + %% ^^^ Run Test (in g2) + ok. + tc4(_) -> + %% ^^^ Run Test (in g1) + %% ^^^ Run Test (in g3) + ok. + tc5(_) -> + %% ^^^ Run Test (in g2) + %% ^^^ Run Test (in sg21) + ok. +"#, + ); + } + + #[test] + fn runnables_suite_recursive_groups() { + check_runnables( + r#" + //- /my_app/test/my_common_test_SUITE.erl + ~ + -module(my_common_test_SUITE). + %% ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Run All Tests + -export([all/0, groups/0]). + -export([a/1, b/1]). + all() -> [{group, ga}, {group, gb}]. + groups() -> [{ga, [], [a, {group, gb}]}, + {gb, [], [{group, ga}]} + ]. + a(_Config) -> + %% ^ Run Test (in ga) + %% ^ Run Test (in ga) + %% ^ Run Test (in gb) + ok. + b(_Config) -> + ok. + "#, + ); + } + + #[test] + fn runnables_suite_otp_example_1() { + check_runnables( + r#" + //- /my_app/test/otp_SUITE.erl + ~ + -module(otp_SUITE). + %% ^^^^^^^^^^^^^^^^^^^ Run All Tests + -compile(export_all). + groups() -> [{group1, [parallel], [test1a,test1b]}, + {group2, [shuffle,sequence], [test2a,test2b,test2c]}]. + all() -> [testcase1, {group,group1}, {testcase,testcase2,[{repeat,10}]}, {group,group2}]. + testcase1(_) -> ok. + %% ^^^^^^^^^ Run Test + testcase2(_) -> ok. + %% ^^^^^^^^^ Run Test + test1a(_) -> ok. + %% ^^^^^^ Run Test (in group1) + test1b(_) -> ok. + %% ^^^^^^ Run Test (in group1) + test2a(_) -> ok. + %% ^^^^^^ Run Test (in group2) + test2b(_) -> ok. + %% ^^^^^^ Run Test (in group2) + test2c(_) -> ok. + %% ^^^^^^ Run Test (in group2) + "#, + ); + } + + #[test] + fn runnables_suite_otp_example_2() { + check_runnables( + r#" + //- /my_app/test/otp_SUITE.erl + ~ + -module(otp_SUITE). + %% ^^^^^^^^^^^^^^^^^^^ Run All Tests + -compile(export_all). + groups() -> [{tests1, [], [{tests2, [], [t2a,t2b]}, {tests3, [], [t3a,t3b]}]}]. + all() ->[{group, tests1, default, [{tests2, [parallel]}]}, + {group, tests1, default, [{tests2, [shuffle,{repeat,10}]}]}]. + t2a(_) -> ok. + %% ^^^ Run Test (in tests1) + %% ^^^ Run Test (in tests2) + t2b(_) -> ok. + %% ^^^ Run Test (in tests1) + %% ^^^ Run Test (in tests2) + t3a(_) -> ok. + %% ^^^ Run Test (in tests1) + %% ^^^ Run Test (in tests3) + t3b(_) -> ok. + %% ^^^ Run Test (in tests1) + %% ^^^ Run Test (in tests3) + "#, + ); + } + + #[test] + fn runnables_suite_otp_example_3() { + check_runnables( + r#" + //- /my_app/test/otp_SUITE.erl + ~ + -module(otp_SUITE). + %% ^^^^^^^^^^^^^^^^^^^ Run All Tests + -compile(export_all). + groups() -> [{tests1, [], [{tests2, [], [t2a,t2b]}, + {tests3, [], [t3a,t3b]}]}]. + all() -> + [{group, tests1, default, [{tests2, [parallel]}, + {tests3, default}]}, + {group, tests1, default, [{tests2, [shuffle,{repeat,10}]}, + {tests3, default}]}]. + t2a(_) -> ok. + %% ^^^ Run Test (in tests1) + %% ^^^ Run Test (in tests2) + t2b(_) -> ok. + %% ^^^ Run Test (in tests1) + %% ^^^ Run Test (in tests2) + t3a(_) -> ok. + %% ^^^ Run Test (in tests1) + %% ^^^ Run Test (in tests3) + t3b(_) -> ok. + %% ^^^ Run Test (in tests1) + %% ^^^ Run Test (in tests3) + "#, + ); + } + + #[test] + fn runnables_suite_otp_example_4() { + check_runnables( + r#" + //- /my_app/test/otp_SUITE.erl + ~ + -module(otp_SUITE). + %% ^^^^^^^^^^^^^^^^^^^ Run All Tests + -compile(export_all). + groups() -> + [{tests1, [], [{group, tests2}]}, + {tests2, [], [{group, tests3}]}, + {tests3, [{repeat,2}], [t3a,t3b,t3c]}]. + + all() -> + [{group, tests1, default, + [{tests2, default, + [{tests3, [parallel,{repeat,100}]}]}]}]. + t3a(_) -> ok. + %% ^^^ Run Test (in tests1) + %% ^^^ Run Test (in tests2) + %% ^^^ Run Test (in tests3) + t3b(_) -> ok. + %% ^^^ Run Test (in tests1) + %% ^^^ Run Test (in tests2) + %% ^^^ Run Test (in tests3) + t3c(_) -> ok. + %% ^^^ Run Test (in tests1) + %% ^^^ Run Test (in tests2) + %% ^^^ Run Test (in tests3) + "#, + ); + } +} diff --git a/crates/ide/src/signature_help.rs b/crates/ide/src/signature_help.rs new file mode 100644 index 0000000000..3a7fa2885b --- /dev/null +++ b/crates/ide/src/signature_help.rs @@ -0,0 +1,646 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! This module provides primitives for showing type and function parameter information when editing +//! a call or use-site. + +use elp_ide_db::elp_base_db::FileId; +use elp_ide_db::elp_base_db::FilePosition; +use elp_ide_db::find_best_token; +use elp_ide_db::RootDatabase; +use elp_syntax::algo; +use elp_syntax::ast; +use elp_syntax::AstNode; +use elp_syntax::TextRange; +use elp_syntax::TextSize; +use fxhash::FxHashMap; +use hir::CallTarget; +use hir::FunctionDef; +use hir::InFile; +use hir::Name; +use hir::Semantic; +use itertools::Itertools; +use stdx::format_to; + +use crate::handlers::get_docs::get_doc_at_position; + +/// Contains information about an item signature as seen from a use site. +/// +/// This includes the "active parameter", which is the parameter whose value is currently being +/// edited. +#[derive(Debug)] +pub struct SignatureHelp { + pub function_doc: Option, + pub parameters_doc: FxHashMap, + pub signature: String, + pub active_parameter: Option, + parameters: Vec, +} + +impl SignatureHelp { + pub fn parameter_labels(&self) -> impl Iterator + '_ { + self.parameters.iter().map(move |&it| &self.signature[it]) + } + + pub fn parameter_ranges(&self) -> &[TextRange] { + &self.parameters + } + + fn push_param(&mut self, param: &str) { + if !self.signature.ends_with('(') { + self.signature.push_str(", "); + } + let start = TextSize::of(&self.signature); + self.signature.push_str(param); + let end = TextSize::of(&self.signature); + // Temporary for T148094436 + let _pctx = stdx::panic_context::enter(format!("\nSignatureHelp::push_param")); + self.parameters.push(TextRange::new(start, end)) + } +} + +/// Computes parameter information for the given position. +pub(crate) fn signature_help( + db: &RootDatabase, + position: FilePosition, +) -> Option<(Vec, Option)> { + let sema = Semantic::new(db); + let source_file = sema.parse(position.file_id); + let syntax = source_file.value.syntax(); + let token = find_best_token(&sema, position)?.value; + let call = algo::find_node_at_offset::(syntax, position.offset)?; + let call_expr = sema.to_expr(InFile::new( + position.file_id, + &ast::Expr::Call(call.clone()), + ))?; + let active_parameter = match call.args() { + Some(args) => { + let param = args + .args() + .take_while(|arg| arg.syntax().text_range().end() <= token.text_range().start()) + .count(); + Some(param) + } + None => None, + }; + + let mut res = Vec::new(); + + match &call_expr[call_expr.value] { + hir::Expr::Call { target, args } => { + let arity = args.len() as u32; + match target { + CallTarget::Local { name } => { + let fun_atom = &call_expr[name.clone()].as_atom()?; + let fun_name = sema.db.lookup_atom(*fun_atom); + signature_help_for_call( + &mut res, + sema, + db, + position.file_id, + None, + fun_name, + arity, + active_parameter, + ) + } + CallTarget::Remote { module, name } => { + let module_atom = &call_expr[module.clone()].as_atom()?; + let module_name = sema.db.lookup_atom(*module_atom); + let fun_atom = &call_expr[name.clone()].as_atom()?; + let fun_name = sema.db.lookup_atom(*fun_atom); + let module = + sema.resolve_module_name(position.file_id, module_name.as_str())?; + signature_help_for_call( + &mut res, + sema, + db, + module.file.file_id, + Some(module_name), + fun_name, + arity, + active_parameter, + ) + } + } + } + _ => (), + }; + + Some((res, active_parameter)) +} + +fn signature_help_for_call( + res: &mut Vec, + sema: Semantic, + db: &RootDatabase, + file_id: FileId, + module_name: Option, + fun_name: Name, + arity: u32, + active_parameter: Option, +) { + let def_map = sema.def_map(file_id); + let functions = def_map + .get_functions_in_scope() + .filter(|&name_arity| { + *name_arity.name() == fun_name + && name_arity.arity() >= arity + && (module_name.is_none() || def_map.is_function_exported(name_arity)) + }) + .sorted(); + for name_arity in functions { + match def_map.get_function(name_arity) { + Some(def) => { + let help = build_signature_help( + db, + &sema, + file_id, + def, + active_parameter, + module_name.clone(), + &fun_name, + ); + res.push(help); + } + None => { + // Function could be imported + if let Some(module_name) = def_map.get_imports().get(name_arity) { + if let Some(module) = sema.resolve_module_name(file_id, module_name) { + let def_map = sema.def_map(module.file.file_id); + if let Some(def) = def_map.get_function(name_arity) { + let help = build_signature_help( + db, + &sema, + module.file.file_id, + def, + active_parameter, + Some(module_name.clone()), + &fun_name, + ); + res.push(help) + } + } + } + } + } + } +} + +fn build_signature_help( + db: &RootDatabase, + sema: &Semantic, + file_id: FileId, + def: &FunctionDef, + active_parameter: Option, + module_name: Option, + fun_name: &Name, +) -> SignatureHelp { + let function_doc = get_function_doc(db, &sema, file_id, def); + let parameters_doc = get_parameters_doc(db, def); + let mut help = SignatureHelp { + function_doc, + parameters_doc, + signature: String::new(), + parameters: vec![], + active_parameter, + }; + match &module_name { + Some(m) => format_to!(help.signature, "{m}:{fun_name}("), + None => format_to!(help.signature, "{fun_name}("), + } + let parameters = &def.function.param_names; + for parameter in parameters { + help.push_param(parameter); + } + help.signature.push(')'); + help +} + +fn get_parameters_doc(db: &RootDatabase, def: &FunctionDef) -> FxHashMap { + match def.edoc_comments(db) { + Some(edoc_header) => edoc_header.params(), + None => FxHashMap::default(), + } +} + +fn get_function_doc( + db: &RootDatabase, + sema: &Semantic, + file_id: FileId, + def: &FunctionDef, +) -> Option { + let position = FilePosition { + file_id, + offset: def.source(sema.db.upcast()).syntax().text_range().start(), + }; + let (doc, _file_range) = get_doc_at_position(db, position)?; + Some(doc.markdown_text().to_string()) +} + +#[cfg(test)] +mod tests { + use std::iter; + + use elp_ide_db::elp_base_db::fixture::WithFixture; + use expect_test::expect; + use expect_test::Expect; + use stdx::format_to; + + use crate::RootDatabase; + + fn check(fixture: &str, expect: Expect) { + let (db, position) = RootDatabase::with_position(fixture); + let sig_help = crate::signature_help::signature_help(&db, position); + let actual = match sig_help { + Some((sig_help, _active_parameter)) => { + let mut rendered = String::new(); + for sh in sig_help { + if let Some(spec) = &sh.function_doc { + format_to!(rendered, "{}\n------\n", spec.as_str()); + } + format_to!(rendered, "{}\n", sh.signature); + let mut offset = 0; + for (i, range) in sh.parameter_ranges().iter().enumerate() { + let is_active = sh.active_parameter == Some(i); + + let start = u32::from(range.start()); + let gap = start.checked_sub(offset).unwrap_or_else(|| { + panic!("parameter ranges out of order: {:?}", sh.parameter_ranges()) + }); + rendered.extend(iter::repeat(' ').take(gap as usize)); + let param_text = &sh.signature[*range]; + let width = param_text.chars().count(); + let marker = if is_active { '^' } else { '-' }; + rendered.extend(iter::repeat(marker).take(width)); + offset += gap + u32::from(range.len()); + } + if !sh.parameter_ranges().is_empty() { + format_to!(rendered, "\n"); + } + if !sh.parameters_doc.is_empty() { + format_to!(rendered, "------\n"); + for (param_name, param_desc) in &sh.parameters_doc { + format_to!(rendered, "{param_name}: {param_desc}\n"); + } + } + format_to!(rendered, "======\n"); + } + rendered + } + None => String::new(), + }; + expect.assert_eq(&actual); + } + + #[test] + fn test_fn_signature_local_two_args() { + check( + r#" +-module(main). + +-spec add(integer(), integer()) -> integer(). +add(This, That) -> + add(This, That, 0). + +-spec add(integer(), integer(), integer()) -> integer(). +add(This, That, Extra) -> + This + That + Extra. + +main() -> + add(~, That). +"#, + expect![[r#" + ```erlang + -spec add(integer(), integer()) -> integer(). + ``` + ------ + add(This, That) + ^^^^ ---- + ====== + ```erlang + -spec add(integer(), integer(), integer()) -> integer(). + ``` + ------ + add(This, That, Extra) + ^^^^ ---- ----- + ====== + "#]], + ); + check( + r#" +-module(main). + +-spec add(integer(), integer()) -> integer(). +add(This, That) -> + add(This, That, 0). + +-spec add(integer(), integer(), integer()) -> integer(). +add(This, That, Extra) -> + This + That + Extra. + +main() -> + add(This~). +"#, + expect![[r#" + ```erlang + -spec add(integer(), integer()) -> integer(). + ``` + ------ + add(This, That) + ^^^^ ---- + ====== + ```erlang + -spec add(integer(), integer(), integer()) -> integer(). + ``` + ------ + add(This, That, Extra) + ^^^^ ---- ----- + ====== + "#]], + ); + check( + r#" +-module(main). + +-spec add(integer(), integer()) -> integer(). +add(This, That) -> + add(This, That, 0). + +-spec add(integer(), integer(), integer()) -> integer(). +add(This, That, Extra) -> + This + That + Extra. + +main() -> + add(This, ~). +"#, + expect![[r#" + ```erlang + -spec add(integer(), integer()) -> integer(). + ``` + ------ + add(This, That) + ---- ^^^^ + ====== + ```erlang + -spec add(integer(), integer(), integer()) -> integer(). + ``` + ------ + add(This, That, Extra) + ---- ^^^^ ----- + ====== + "#]], + ); + } + + #[test] + fn test_fn_signature_remote_two_args() { + check( + r#" +//- /one.erl +-module(one). + +-compile(export_all). + +-spec add(integer(), integer()) -> integer(). +add(This, That) -> + add(This, That, 0). + +-spec add(integer(), integer(), integer()) -> integer(). +add(This, That, Extra) -> + This + That + Extra. + +//- /two.erl +-module(two). + +main() -> + one:add(~, That). +"#, + expect![[r#" + ```erlang + -spec add(integer(), integer()) -> integer(). + ``` + ------ + one:add(This, That) + ^^^^ ---- + ====== + ```erlang + -spec add(integer(), integer(), integer()) -> integer(). + ``` + ------ + one:add(This, That, Extra) + ^^^^ ---- ----- + ====== + "#]], + ); + check( + r#" +//- /one.erl +-module(one). + +-compile(export_all). + +-spec add(integer(), integer()) -> integer(). +add(This, That) -> + add(This, That, 0). + +-spec add(integer(), integer(), integer()) -> integer(). +add(This, That, Extra) -> + This + That + Extra. + +//- /two.erl +-module(two). + +main() -> + one:add(This~). +"#, + expect![[r#" + ```erlang + -spec add(integer(), integer()) -> integer(). + ``` + ------ + one:add(This, That) + ^^^^ ---- + ====== + ```erlang + -spec add(integer(), integer(), integer()) -> integer(). + ``` + ------ + one:add(This, That, Extra) + ^^^^ ---- ----- + ====== + "#]], + ); + check( + r#" +//- /one.erl +-module(one). + +-compile(export_all). + +-spec add(integer(), integer()) -> integer(). +add(This, That) -> + add(This, That, 0). + +-spec add(integer(), integer(), integer()) -> integer(). +add(This, That, Extra) -> + This + That + Extra. + +//- /two.erl +-module(two). + +main() -> + one:add(This, ~). +"#, + expect![[r#" + ```erlang + -spec add(integer(), integer()) -> integer(). + ``` + ------ + one:add(This, That) + ---- ^^^^ + ====== + ```erlang + -spec add(integer(), integer(), integer()) -> integer(). + ``` + ------ + one:add(This, That, Extra) + ---- ^^^^ ----- + ====== + "#]], + ); + } + + // Due to the way the current grammar currently works, this is + // currently not returning any results since the cursor is not + // identified as part of the EXPR_ARGS. + // In practice, this should not be an issue since VS Code + // auto-completes parentheses. + #[test] + fn test_fn_signature_unclosed_call() { + check( + r#" +-module(main). + +-compile(export_all). + +-spec add(integer(), integer()) -> integer(). +add(This, That) -> + add(This, That, 0). + +-spec add(integer(), integer(), integer()) -> integer(). +add(This, That, Extra) -> + This + That + Extra. + +main() -> + main:add(~ +"#, + expect![""], + ); + } + + #[test] + fn test_fn_signature_doc() { + check( + r#" +-module(main). + +-compile(export_all). + +%% @doc +%% Add This to That +%% @param This The first thing +%% @param That The second thing +%% @returns The sum of This and That plus 0 +%% @end +-spec add(integer(), integer()) -> integer(). +add(This, That) -> + add(This, That, 0). + +%% @doc +%% Add This to That, including an extra +%% @param This The first thing +%% @param That The second thing +%% @param Extra Something more +%% @returns The sum of This and That plus the Extra +%% @end +-spec add(integer(), integer(), integer()) -> integer(). +add(This, That, Extra) -> + This + That + Extra. + +main() -> + main:add(This, ~) +"#, + expect![[r#" + ```erlang + -spec add(integer(), integer()) -> integer(). + ``` + ------ + main:add(This, That) + ---- ^^^^ + ------ + That: The second thing + This: The first thing + ====== + ```erlang + -spec add(integer(), integer(), integer()) -> integer(). + ``` + ------ + main:add(This, That, Extra) + ---- ^^^^ ----- + ------ + Extra: Something more + That: The second thing + This: The first thing + ====== + "#]], + ); + } + + #[test] + fn test_fn_signature_local_imported() { + check( + r#" +//- /one.erl +-module(one). +-compile(export_all). + +-spec add(integer(), integer()) -> integer(). +add(This, That) -> + add(This, That, 0). + +-spec add(integer(), integer(), integer()) -> integer(). +add(This, That, Extra) -> + This + That + Extra. + +//- /two.erl +-module(two). +-import(one, [add/2, add/3]). +main() -> + add(~, That). +"#, + expect![[r#" + ```erlang + -spec add(integer(), integer()) -> integer(). + ``` + ------ + one:add(This, That) + ^^^^ ---- + ====== + ```erlang + -spec add(integer(), integer(), integer()) -> integer(). + ``` + ------ + one:add(This, That, Extra) + ^^^^ ---- ----- + ====== + "#]], + ); + } +} diff --git a/crates/ide/src/syntax_highlighting.rs b/crates/ide/src/syntax_highlighting.rs new file mode 100644 index 0000000000..3df2ddded2 --- /dev/null +++ b/crates/ide/src/syntax_highlighting.rs @@ -0,0 +1,365 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +mod highlights; +pub(crate) mod tags; + +use elp_ide_db::elp_base_db::FileId; +use elp_ide_db::RootDatabase; +use elp_ide_db::SymbolKind; +use elp_syntax::ast; +use elp_syntax::AstNode; +use elp_syntax::NodeOrToken; +use elp_syntax::TextRange; +use hir::CallTarget; +use hir::DefMap; +use hir::Expr; +use hir::ExprId; +use hir::InFile; +use hir::InFunctionBody; +use hir::NameArity; +use hir::Semantic; + +use self::highlights::Highlights; +use self::tags::Highlight; +use crate::HlMod; +use crate::HlTag; + +#[derive(Debug, Clone, Copy)] +pub struct HlRange { + pub range: TextRange, + pub highlight: Highlight, + pub binding_hash: Option, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct HighlightConfig {} + +// Feature: Semantic Syntax Highlighting +// +// ELP highlights some code semantically. +// +// Initially this is just used for bound variables in patterns + +pub(crate) fn highlight( + db: &RootDatabase, + file_id: FileId, + range_to_highlight: Option, +) -> Vec { + let _p = profile::span("highlight"); + let sema = Semantic::new(db); + + // Determine the root based on the given range. + let (root, range_to_highlight) = { + let source_file = sema.parse(file_id); + let source_file = source_file.value.syntax(); + match range_to_highlight { + Some(range) => { + let node = match source_file.covering_element(range) { + NodeOrToken::Node(it) => it, + NodeOrToken::Token(it) => it.parent().unwrap_or_else(|| source_file.clone()), + }; + (node, range) + } + None => (source_file.clone(), source_file.text_range()), + } + }; + + let mut hl = highlights::Highlights::new(root.text_range()); + bound_vars_in_pattern_highlight(&sema, file_id, range_to_highlight, &mut hl); + functions_highlight(&sema, file_id, range_to_highlight, &mut hl); + deprecated_func_highlight(&sema, file_id, range_to_highlight, &mut hl); + hl.to_vec() +} + +fn bound_vars_in_pattern_highlight( + sema: &Semantic, + file_id: FileId, + range_to_highlight: TextRange, + hl: &mut Highlights, +) { + let highlight_bound = HlTag::Symbol(SymbolKind::Variable) | HlMod::Bound; + + let bound_var_ranges = sema.bound_vars_in_pattern_diagnostic(file_id); + bound_var_ranges.iter().for_each(|(_, _, var)| { + let range = var.syntax().text_range(); + // Element inside the viewport, need to highlight + if range_to_highlight.intersect(range).is_some() { + hl.add(HlRange { + range, + highlight: highlight_bound, + binding_hash: None, + }); + } + }); +} + +fn deprecated_func_highlight( + sema: &Semantic, + file_id: FileId, + range_to_highlight: TextRange, + hl: &mut Highlights, +) { + let def_map = sema.def_map(file_id); + let highlight = HlTag::Symbol(SymbolKind::Function) | HlMod::DeprecatedFunction; + for (_name, def) in def_map.get_functions() { + if def.file.file_id == file_id { + let function_id = InFile::new(file_id, def.function_id); + let function_body = sema.to_function_body(function_id); + sema.fold_function( + function_id, + (), + &mut |acc, _clause_id, ctx| { + match ctx.expr { + Expr::Call { target, args } => { + let arity = args.len() as u32; + match target { + CallTarget::Local { name } => { + if let Some(range) = find_deprecated_range( + sema, + &def_map, + &name, + arity, + range_to_highlight, + &function_body, + ) { + hl.add(HlRange { + range, + highlight, + binding_hash: None, + }) + } + } + CallTarget::Remote { module, name } => { + if let Some(file_id) = find_remote_module_file_id( + sema, + file_id, + &module, + &function_body, + ) { + let def_map = sema.def_map(file_id); + if let Some(range) = find_deprecated_range( + sema, + &def_map, + &name, + arity, + range_to_highlight, + &function_body, + ) { + hl.add(HlRange { + range, + highlight, + binding_hash: None, + }) + } + } + } + } + } + _ => {} + } + acc + }, + &mut |acc, _, _| acc, + ); + } + } +} + +fn find_deprecated_range( + sema: &Semantic, + def_map: &DefMap, + name: &ExprId, + arity: u32, + range_to_highlight: TextRange, + function_body: &InFunctionBody<()>, +) -> Option { + let fun_atom = &function_body[name.clone()].as_atom()?; + let range = function_body.range_for_expr(sema.db, *name)?; + if range_to_highlight.intersect(range).is_some() { + let name = sema.db.lookup_atom(*fun_atom); + if def_map.is_deprecated(&NameArity::new(name, arity)) { + return Some(range); + } + } + None +} + +fn find_remote_module_file_id( + sema: &Semantic, + file_id: FileId, + module: &ExprId, + function_body: &InFunctionBody<()>, +) -> Option { + let module_atom = &function_body[module.clone()].as_atom()?; + let module_name = sema.db.lookup_atom(*module_atom); + let module = sema.resolve_module_name(file_id, module_name.as_str())?; + Some(module.file.file_id) +} + +fn functions_highlight( + sema: &Semantic, + file_id: FileId, + range_to_highlight: TextRange, + hl: &mut Highlights, +) { + let def_map = sema.def_map(file_id); + for (_name, def) in def_map.get_functions() { + if def.file.file_id == file_id && (def.exported || def.deprecated) { + let fun_decl_ast = def.source(sema.db.upcast()); + + fun_decl_ast.clauses().for_each(|clause| match clause { + ast::FunctionOrMacroClause::FunctionClause(clause) => { + clause.name().map(|n| { + let range = n.syntax().text_range(); + + let highlight = match (def.exported, def.deprecated) { + (true, true) => { + HlTag::Symbol(SymbolKind::Function) + | HlMod::ExportedFunction + | HlMod::DeprecatedFunction + } + (false, true) => { + HlTag::Symbol(SymbolKind::Function) | HlMod::DeprecatedFunction + } + (true, false) => { + HlTag::Symbol(SymbolKind::Function) | HlMod::ExportedFunction + } + (false, false) => unreachable!("checked above"), + }; + + // Element inside the viewport, need to highlight + if range_to_highlight.intersect(range).is_some() { + hl.add(HlRange { + range, + highlight, + binding_hash: None, + }) + } + }); + } + ast::FunctionOrMacroClause::MacroCallExpr(_) => {} + }) + } + } +} + +#[cfg(test)] +mod tests { + use elp_base_db::fixture::WithFixture; + use elp_ide_db::elp_base_db; + use elp_ide_db::elp_base_db::fixture::extract_tags; + use elp_ide_db::RootDatabase; + use itertools::Itertools; + + use crate::syntax_highlighting::highlight; + use crate::HlTag; + + // These are tests of the specific modifier functionality. When + // we go all-in with semantic tokens, we can consider bringing + // over the RA test mechanism which compares an HTML file. + #[track_caller] + fn check_highlights(fixture: &str) { + let (ranges, fixture) = extract_tags(fixture.trim_start(), "tag"); + let range = if !ranges.is_empty() { + Some(ranges[0].0) + } else { + None + }; + + let (db, fixture) = RootDatabase::with_fixture(&fixture); + let annotations = fixture.annotations(&db); + let expected: Vec<_> = annotations + .into_iter() + .map(|(fr, tag)| (fr.range, tag)) + .sorted_by(|a, b| a.0.start().cmp(&b.0.start())) + .collect(); + + let file_id = fixture.files[0]; + let highlights = highlight(&db, file_id, range); + let ranges: Vec<_> = highlights + .iter() + .filter(|h| h.highlight != HlTag::None.into()) // Means with no modifiers either + .map(|h| { + let mods: Vec<_> = h.highlight.mods.iter().map(|m| format!("{m}")).collect(); + (h.range, mods.join(",")) + }) + .sorted_by(|a, b| a.0.start().cmp(&b.0.start())) + .collect(); + assert_eq!(expected, ranges); + } + + #[test] + fn highlights_1() { + check_highlights( + r#" + f(Var1) -> + Var1 = 1. + %% ^^^^bound "#, + ) + } + + #[test] + fn highlights_2() { + check_highlights( + r#" + -export([f/1]). + f(Var1) -> + %% ^exported_function + Var1 = 1. + %% ^^^^bound "#, + ) + } + + #[test] + fn deprecated_exported_highlight() { + check_highlights( + r#" + -deprecated([{f, 1}, {g, 1}]). + -export([g/1]). + f(1) -> 1; + %% ^deprecated_function + f(2) -> 2. + %% ^deprecated_function + g(3) -> 3. + %% ^exported_function,deprecated_function"#, + ) + } + + #[test] + fn deprecated_highlight() { + check_highlights( + r#" + //- /src/deprecated_highlight.erl + -module(deprecated_highlight). + -deprecated([{f, 1}]). + f(1) -> 1. + %% ^deprecated_function + ga(Num) -> f(Num). + %% ^deprecated_function"#, + ) + } + + #[test] + fn highlights_in_range() { + check_highlights( + r#" + -export([f/1]). + foo(X) -> ok. + f(Var1) -> + %% Not exported_function + + Var1 = 1. + %% ^^^^bound + + bar(Y) -> ok. + "#, + ) + } +} diff --git a/crates/ide/src/syntax_highlighting/highlights.rs b/crates/ide/src/syntax_highlighting/highlights.rs new file mode 100644 index 0000000000..d1f3b65c1a --- /dev/null +++ b/crates/ide/src/syntax_highlighting/highlights.rs @@ -0,0 +1,115 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! Collects a tree of highlighted ranges and flattens it. +use std::iter; + +use elp_syntax::TextRange; +use stdx::equal_range_by; + +use crate::HlRange; +use crate::HlTag; + +pub(super) struct Highlights { + root: Node, +} + +struct Node { + hl_range: HlRange, + nested: Vec, +} + +impl Highlights { + pub(super) fn new(range: TextRange) -> Highlights { + Highlights { + root: Node::new(HlRange { + range, + highlight: HlTag::None.into(), + binding_hash: None, + }), + } + } + + pub(super) fn add(&mut self, hl_range: HlRange) { + self.root.add(hl_range); + } + + pub(super) fn to_vec(&self) -> Vec { + let mut res = Vec::new(); + self.root.flatten(&mut res); + res + } +} + +impl Node { + fn new(hl_range: HlRange) -> Node { + Node { + hl_range, + nested: Vec::new(), + } + } + + fn add(&mut self, hl_range: HlRange) { + assert!(self.hl_range.range.contains_range(hl_range.range)); + + // Fast path + if let Some(last) = self.nested.last_mut() { + if last.hl_range.range.contains_range(hl_range.range) { + return last.add(hl_range); + } + if last.hl_range.range.end() <= hl_range.range.start() { + return self.nested.push(Node::new(hl_range)); + } + } + + let overlapping = equal_range_by(&self.nested, |n| { + TextRange::ordering(n.hl_range.range, hl_range.range) + }); + + if overlapping.len() == 1 + && self.nested[overlapping.start] + .hl_range + .range + .contains_range(hl_range.range) + { + return self.nested[overlapping.start].add(hl_range); + } + + let nested = self + .nested + .splice(overlapping.clone(), iter::once(Node::new(hl_range))) + .collect::>(); + self.nested[overlapping.start].nested = nested; + } + + fn flatten(&self, acc: &mut Vec) { + let mut start = self.hl_range.range.start(); + let mut nested = self.nested.iter(); + // Temporary for T148094436 + let _pctx = stdx::panic_context::enter(format!("\nhighlights::Node::flatten")); + loop { + let next = nested.next(); + let end = next.map_or(self.hl_range.range.end(), |it| it.hl_range.range.start()); + if start < end { + acc.push(HlRange { + range: TextRange::new(start, end), + highlight: self.hl_range.highlight, + binding_hash: self.hl_range.binding_hash, + }); + } + start = match next { + Some(child) => { + child.flatten(acc); + child.hl_range.range.end() + } + None => break, + } + } + } +} diff --git a/crates/ide/src/syntax_highlighting/tags.rs b/crates/ide/src/syntax_highlighting/tags.rs new file mode 100644 index 0000000000..9588b8e024 --- /dev/null +++ b/crates/ide/src/syntax_highlighting/tags.rs @@ -0,0 +1,176 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! Defines token tags we use for syntax highlighting. +//! A tag is not unlike a CSS class. + +use std::fmt; +use std::fmt::Write; +use std::ops; + +use elp_ide_db::SymbolKind; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Highlight { + pub tag: HlTag, + pub mods: HlMods, +} + +#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct HlMods(u32); + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum HlTag { + Symbol(SymbolKind), + + // For things which don't have a specific highlight. This is the + // default for anything we do not specifically set, and maps to VS Code `generic` type + None, +} + +// Don't forget to adjust the feature description in +// crates/ide/src/syntax_highlighting.rs. And make sure to use the +// lsp strings used when converting to the protocol in +// crates\elp\src\semantic_tokens.rs, not the names of the variants +// here. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[repr(u8)] +pub enum HlMod { + /// Bound variable in pattern. + Bound = 0, + // Local vs exported function name. + ExportedFunction, + DeprecatedFunction, +} + +impl HlTag { + fn as_str(self) -> &'static str { + match self { + HlTag::Symbol(symbol) => match symbol { + // Tied in to to_proto::symbol_kind + SymbolKind::File => "file", + SymbolKind::Module => "module", + SymbolKind::Function => "function", + SymbolKind::Record => "struct", + SymbolKind::RecordField => "struct", + SymbolKind::Type => "type_parameter", + SymbolKind::Define => "constant", + SymbolKind::Variable => "variable", + SymbolKind::Callback => "function", + }, + HlTag::None => "none", + } + } +} + +impl fmt::Display for HlTag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self.as_str(), f) + } +} + +impl HlMod { + const ALL: &'static [HlMod; 3] = &[ + HlMod::Bound, + HlMod::ExportedFunction, + HlMod::DeprecatedFunction, + ]; + + fn as_str(self) -> &'static str { + match self { + HlMod::Bound => "bound", + HlMod::ExportedFunction => "exported_function", + HlMod::DeprecatedFunction => "deprecated_function", + } + } + + fn mask(self) -> u32 { + 1 << (self as u32) + } +} + +impl fmt::Display for HlMod { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self.as_str(), f) + } +} + +impl fmt::Display for Highlight { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.tag.fmt(f)?; + for modifier in self.mods.iter() { + f.write_char('.')?; + modifier.fmt(f)?; + } + Ok(()) + } +} + +impl From for Highlight { + fn from(tag: HlTag) -> Highlight { + Highlight::new(tag) + } +} + +impl Highlight { + pub(crate) fn new(tag: HlTag) -> Highlight { + Highlight { + tag, + mods: HlMods::default(), + } + } + pub fn is_empty(&self) -> bool { + self.tag == HlTag::None && self.mods.is_empty() + } +} +impl ops::BitOr for HlTag { + type Output = Highlight; + + fn bitor(self, rhs: HlMod) -> Highlight { + Highlight::new(self) | rhs + } +} + +impl ops::BitOrAssign for HlMods { + fn bitor_assign(&mut self, rhs: HlMod) { + self.0 |= rhs.mask(); + } +} + +impl ops::BitOrAssign for Highlight { + fn bitor_assign(&mut self, rhs: HlMod) { + self.mods |= rhs; + } +} + +impl ops::BitOr for Highlight { + type Output = Highlight; + + fn bitor(mut self, rhs: HlMod) -> Highlight { + self |= rhs; + self + } +} + +impl HlMods { + pub fn is_empty(&self) -> bool { + self.0 == 0 + } + + pub fn contains(self, m: HlMod) -> bool { + self.0 & m.mask() == m.mask() + } + + pub fn iter(self) -> impl Iterator { + HlMod::ALL + .iter() + .copied() + .filter(move |it| self.0 & it.mask() == it.mask()) + } +} diff --git a/crates/ide/src/tests.rs b/crates/ide/src/tests.rs new file mode 100644 index 0000000000..2191032886 --- /dev/null +++ b/crates/ide/src/tests.rs @@ -0,0 +1,227 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +// To run the tests via cargo +// cargo test --package elp_ide --lib + +use elp_ide_db::elp_base_db::assert_eq_text; +use elp_ide_db::elp_base_db::fixture::extract_annotations; +use elp_ide_db::elp_base_db::fixture::WithFixture; +use elp_ide_db::elp_base_db::test_fixture::trim_indent; +use elp_ide_db::elp_base_db::FileId; +use elp_ide_db::elp_base_db::FileRange; +use elp_ide_db::elp_base_db::SourceDatabaseExt; +use elp_ide_db::RootDatabase; +use fxhash::FxHashSet; + +use crate::diagnostics; +use crate::diagnostics::DiagnosticCode; +use crate::diagnostics::Severity; +use crate::fixture; +use crate::Analysis; +use crate::DiagnosticsConfig; +use crate::NavigationTarget; + +/// Takes a multi-file input fixture with annotated cursor positions, +/// and checks that: +/// * a diagnostic is produced +/// * the first diagnostic fix trigger range touches the input cursor position +/// * that the contents of the file containing the cursor match `after` after the diagnostic fix is applied +#[track_caller] +pub(crate) fn check_fix(fixture_before: &str, fixture_after: &str) { + let config = + DiagnosticsConfig::default().disable(DiagnosticCode::MissingCompileWarnMissingSpec); + check_nth_fix(0, fixture_before, fixture_after, config); +} + +/// Like `check_fix` but with a custom DiagnosticsConfig +#[track_caller] +pub(crate) fn check_fix_with_config( + config: DiagnosticsConfig, + fixture_before: &str, + fixture_after: &str, +) { + check_nth_fix(0, fixture_before, fixture_after, config); +} + +#[track_caller] +fn check_nth_fix(nth: usize, fixture_before: &str, fixture_after: &str, config: DiagnosticsConfig) { + let after = trim_indent(fixture_after); + + let (db, file_position) = RootDatabase::with_position(fixture_before); + let diagnostic = diagnostics::diagnostics(&db, &config, file_position.file_id, true) + .pop() + .expect("no diagnostics"); + let fix = &diagnostic.fixes.expect("diagnostic misses fixes")[nth]; + let actual = { + let source_change = fix.source_change.as_ref().unwrap(); + let file_id = *source_change.source_file_edits.keys().next().unwrap(); + let mut actual = db.file_text(file_id).to_string(); + + for edit in source_change.source_file_edits.values() { + edit.apply(&mut actual); + } + actual + }; + assert!( + fix.target.contains_inclusive(file_position.offset), + "diagnostic fix range {:?} does not touch cursor position {:?}", + fix.target, + file_position.offset + ); + assert_eq_text!(&after, &actual); +} + +#[track_caller] +pub(crate) fn check_diagnostics(ra_fixture: &str) { + let config = + DiagnosticsConfig::default().disable(DiagnosticCode::MissingCompileWarnMissingSpec); + check_diagnostics_with_config(config, ra_fixture) +} + +#[track_caller] +pub(crate) fn check_diagnostics_with_config(config: DiagnosticsConfig, elp_fixture: &str) { + let (db, files) = RootDatabase::with_many_files(elp_fixture); + for file_id in files { + let diagnostics = diagnostics::diagnostics(&db, &config, file_id, true); + + let expected = extract_annotations(&*db.file_text(file_id)); + let mut actual = diagnostics + .into_iter() + .map(|d| { + let mut annotation = String::new(); + if let Some(fixes) = &d.fixes { + assert!(!fixes.is_empty()); + annotation.push_str("💡 ") + } + annotation.push_str(match d.severity { + Severity::Error => "error", + Severity::Warning => "warning", + Severity::WeakWarning => "weak", + }); + annotation.push_str(": "); + annotation.push_str(&d.message); + (d.range, annotation) + }) + .collect::>(); + actual.sort_by_key(|(range, _)| range.start()); + assert_eq!(expected, actual); + } +} + +#[track_caller] +pub fn check_no_parse_errors(analysis: &Analysis, file_id: FileId) { + let config = DiagnosticsConfig::new(true, FxHashSet::default(), vec![]) + .disable(DiagnosticCode::MissingCompileWarnMissingSpec); + let diags = analysis.diagnostics(&config, file_id, true).unwrap(); + assert!( + diags.is_empty(), + "didn't expect parse errors in files: {:?}", + diags + ); +} + +#[track_caller] +pub fn check_navs(navs: Vec, expected: Vec<(FileRange, String)>) { + let ranges = navs + .into_iter() + .map(|nav| nav.file_range()) + .collect::>(); + check_file_ranges(ranges, expected) +} + +pub fn check_file_ranges(mut ranges: Vec, expected: Vec<(FileRange, String)>) { + let cmp = |&FileRange { file_id, range }: &_| (file_id, range.start()); + let mut expected = expected + .into_iter() + .map(|(range, _)| range) + .collect::>(); + ranges.sort_by_key(cmp); + expected.sort_by_key(cmp); + assert_eq!(expected, ranges); +} + +#[track_caller] +pub fn check_call_hierarchy(prepare_fixture: &str, incoming_fixture: &str, outgoing_fixture: &str) { + check_call_hierarchy_prepare(prepare_fixture); + check_call_hierarchy_incoming_calls(incoming_fixture); + check_call_hierarchy_outgoing_calls(outgoing_fixture); +} + +fn check_call_hierarchy_prepare(fixture: &str) { + let (analysis, pos, mut annotations) = fixture::annotations(trim_indent(fixture).as_str()); + let mut navs = analysis.call_hierarchy_prepare(pos).unwrap().unwrap().info; + assert_eq!(navs.len(), 1); + assert_eq!(annotations.len(), 1); + let nav = navs.pop().unwrap(); + let (expected_range, _text) = annotations.pop().unwrap(); + let actual_range = FileRange { + file_id: nav.file_id, + range: nav.focus_range.unwrap(), + }; + assert_eq!(expected_range, actual_range); +} + +fn check_call_hierarchy_incoming_calls(fixture: &str) { + let (analysis, pos, mut expected) = fixture::annotations(trim_indent(fixture).as_str()); + let incoming_calls = analysis.incoming_calls(pos).unwrap().unwrap(); + let mut actual = Vec::new(); + for call in incoming_calls { + actual.push(( + FileRange { + file_id: call.target.file_id, + range: call.target.focus_range.unwrap(), + }, + format!("from: {}", call.target.name), + )); + for range in call.ranges { + actual.push(( + FileRange { + file_id: call.target.file_id, + range, + }, + format!("from_range: {}", call.target.name), + )); + } + } + let cmp = + |(frange, text): &(FileRange, String)| (frange.file_id, frange.range.start(), text.clone()); + actual.sort_by_key(cmp); + expected.sort_by_key(cmp); + assert_eq!(actual, expected); +} + +fn check_call_hierarchy_outgoing_calls(fixture: &str) { + let (analysis, pos, mut expected) = fixture::annotations(trim_indent(fixture).as_str()); + let outgoing_calls = analysis.outgoing_calls(pos).unwrap().unwrap(); + let mut actual = Vec::new(); + for call in outgoing_calls { + actual.push(( + FileRange { + file_id: call.target.file_id, + range: call.target.focus_range.unwrap(), + }, + format!("to: {}", call.target.name), + )); + for range in call.ranges { + actual.push(( + FileRange { + file_id: pos.file_id, + range, + }, + format!("from_range: {}", call.target.name), + )); + } + } + let cmp = + |(frange, text): &(FileRange, String)| (frange.file_id, frange.range.start(), text.clone()); + actual.sort_by_key(cmp); + expected.sort_by_key(cmp); + assert_eq!(actual, expected); +} diff --git a/crates/ide_assists/Cargo.toml b/crates/ide_assists/Cargo.toml new file mode 100644 index 0000000000..66288e6ce9 --- /dev/null +++ b/crates/ide_assists/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "elp_ide_assists" +edition.workspace = true +version.workspace = true + +[dependencies] +elp_ide_db.workspace = true +elp_syntax.workspace = true +hir.workspace = true + +cov-mark.workspace = true +fxhash.workspace = true +itertools.workspace = true +lazy_static.workspace = true +log.workspace = true +regex.workspace = true +stdx.workspace = true +text-edit.workspace = true + +[dev-dependencies] +expect-test.workspace = true diff --git a/crates/ide_assists/src/assist_config.rs b/crates/ide_assists/src/assist_config.rs new file mode 100644 index 0000000000..aff0e6d5c3 --- /dev/null +++ b/crates/ide_assists/src/assist_config.rs @@ -0,0 +1,24 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! Settings for tweaking assists. +//! +//! The fun thing here is `SnippetCap` -- this type can only be created in this +//! module, and we use to statically check that we only produce snippet +//! assists if we are allowed to. + +use elp_ide_db::helpers::SnippetCap; + +use crate::AssistKind; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct AssistConfig { + pub snippet_cap: Option, + pub allowed: Option>, +} diff --git a/crates/ide_assists/src/assist_context.rs b/crates/ide_assists/src/assist_context.rs new file mode 100644 index 0000000000..4e17f30a7e --- /dev/null +++ b/crates/ide_assists/src/assist_context.rs @@ -0,0 +1,347 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! See [`AssistContext`]. + +use elp_ide_db::assists::AssistContextDiagnostic; +use elp_ide_db::assists::AssistUserInput; +use elp_ide_db::elp_base_db::FileId; +use elp_ide_db::elp_base_db::FileRange; +use elp_ide_db::elp_base_db::SourceDatabase; +use elp_ide_db::label::Label; +use elp_ide_db::source_change::SourceChangeBuilder; +use elp_ide_db::RootDatabase; +use elp_ide_db::SymbolClass; +use elp_syntax::algo; +use elp_syntax::ast::AstNode; +use elp_syntax::Direction; +use elp_syntax::SourceFile; +use elp_syntax::SyntaxElement; +use elp_syntax::SyntaxKind; +use elp_syntax::SyntaxToken; +use elp_syntax::TextRange; +use elp_syntax::TextSize; +use elp_syntax::TokenAtOffset; +use fxhash::FxHashSet; +use hir::db::MinDefDatabase; +use hir::Body; +use hir::Expr; +use hir::ExprId; +use hir::FormId; +use hir::InFile; +use hir::InFileAstPtr; +use hir::Semantic; +use hir::Strategy; +use hir::TypeExprId; + +use crate::assist_config::AssistConfig; +use crate::Assist; +use crate::AssistId; +use crate::AssistKind; +use crate::AssistResolveStrategy; + +/// `AssistContext` allows to apply an assist or check if it could be applied. +/// +/// Assists use a somewhat over-engineered approach, given the current needs. +/// The assists workflow consists of two phases. In the first phase, a user asks +/// for the list of available assists. In the second phase, the user picks a +/// particular assist and it gets applied. +/// +/// There are two peculiarities here: +/// +/// * first, we ideally avoid computing more things then necessary to answer "is +/// assist applicable" in the first phase. +/// * second, when we are applying assist, we don't have a guarantee that there +/// weren't any changes between the point when user asked for assists and when +/// they applied a particular assist. So, when applying assist, we need to do +/// all the checks from scratch. +/// +/// To avoid repeating the same code twice for both "check" and "apply" +/// functions, we use an approach reminiscent of that of Django's function based +/// views dealing with forms. Each assist receives a runtime parameter, +/// `resolve`. It first check if an edit is applicable (potentially computing +/// info required to compute the actual edit). If it is applicable, and +/// `resolve` is `true`, it then computes the actual edit. +/// +/// So, to implement the original assists workflow, we can first apply each edit +/// with `resolve = false`, and then applying the selected edit again, with +/// `resolve = true` this time. +/// +/// Note, however, that we don't actually use such two-phase logic at the +/// moment, because the LSP API is pretty awkward in this place, and it's much +/// easier to just compute the edit eagerly :-) +pub(crate) struct AssistContext<'a> { + pub(crate) config: &'a AssistConfig, + pub(crate) sema: Semantic<'a>, + pub(crate) frange: FileRange, + pub(crate) diagnostics: &'a [AssistContextDiagnostic], + pub(crate) user_input: Option, + trimmed_range: TextRange, + source_file: SourceFile, +} + +impl<'a> AssistContext<'a> { + pub(crate) fn new( + db: &'a RootDatabase, + config: &'a AssistConfig, + frange: FileRange, + diagnostics: &'a [AssistContextDiagnostic], + user_input: Option, + ) -> AssistContext<'a> { + let source_file = db.parse(frange.file_id).tree(); + // Trim the selection to omit leading and trailing whitespace. + // In this context, comments are not included in the + // whitespace to be skipped, so any comments selected will be + // included in the reduced range. + let start = frange.range.start(); + let end = frange.range.end(); + // Temporary for T153426323 + let _pctx = stdx::panic_context::enter(format!("\nAssistContext::new start")); + let left = source_file.syntax().token_at_offset(start); + // Temporary for T153426323 + let _pctx = stdx::panic_context::enter(format!("\nAssistContext::new end")); + let right = source_file.syntax().token_at_offset(end); + let left = left + .right_biased() + .and_then(|t| algo::skip_whitespace_token(t, Direction::Next)); + let right = right + .left_biased() + .and_then(|t| algo::skip_whitespace_token(t, Direction::Prev)); + let left = left.map(|t| t.text_range().start().clamp(start, end)); + let right = right.map(|t| t.text_range().end().clamp(start, end)); + + let trimmed_range = match (left, right) { + (Some(left), Some(right)) if left <= right => { + // Temporary for T148094436 + let _pctx = stdx::panic_context::enter(format!("\nAssistContext::new")); + TextRange::new(left, right) + } + // Selection solely consists of whitespace so just fall back to the original + _ => frange.range, + }; + + AssistContext { + config, + sema: Semantic::new(db), + frange, + trimmed_range, + source_file, + diagnostics, + user_input, + } + } + + pub(crate) fn db(&self) -> &dyn MinDefDatabase { + self.sema.db + } + + // NB, this ignores active selection. + pub(crate) fn offset(&self) -> TextSize { + // Temporary for T153426323 + let _pctx = stdx::panic_context::enter(format!("\nAssistContext::offset")); + self.frange.range.start() + } + + pub(crate) fn token_at_offset(&self) -> TokenAtOffset { + // Temporary for T153426323 + let _pctx = stdx::panic_context::enter(format!("\nAssistContext::token_at_offset")); + self.source_file.syntax().token_at_offset(self.offset()) + } + + pub(crate) fn find_tokens_syntax_at_offset( + &self, + kinds: FxHashSet, + ) -> Option { + self.token_at_offset().find(|it| kinds.contains(&it.kind())) + } + + pub(crate) fn find_node_at_offset(&self) -> Option { + algo::find_node_at_offset(self.source_file.syntax(), self.offset()) + } + + pub(crate) fn find_node_at_custom_offset(&self, offset: TextSize) -> Option { + algo::find_node_at_offset(self.source_file.syntax(), offset) + } + + /// Returns the selected range trimmed for whitespace tokens, that is the range will be snapped + /// to the nearest enclosed token. + pub(crate) fn selection_trimmed(&self) -> TextRange { + self.trimmed_range + } + + pub(crate) fn file_id(&self) -> FileId { + self.frange.file_id + } + + pub(crate) fn has_empty_selection(&self) -> bool { + self.trimmed_range.is_empty() + } + + /// Returns the element covered by the selection range, this + /// excludes trailing whitespace in the selection. + pub(crate) fn covering_element(&self) -> SyntaxElement { + self.source_file + .syntax() + .covering_element(self.selection_trimmed()) + } + + pub(crate) fn classify_offset(&self) -> Option { + // Temporary for T153426323 + let _pctx = stdx::panic_context::enter(format!("\nAssistContext::classify_offset")); + let token = self + .source_file + .syntax() + .token_at_offset(self.offset()) + .right_biased()?; // offset is start of a range + SymbolClass::classify(&self.sema, InFile::new(self.file_id(), token)) + } + + pub(crate) fn form_ast(&self, form_id: FormId) -> T + where + T: AstNode, + { + form_id.get(&self.source_file) + } + + pub(crate) fn ast_ptr_get(&self, ptr: InFileAstPtr) -> Option + where + T: AstNode, + { + ptr.to_node(&InFile::new(self.file_id(), self.source_file.clone())) + } + + pub(crate) fn user_input_or(&self, get_default: impl FnOnce() -> String) -> String { + if let Some(user_input) = &self.user_input { + user_input.value.clone() + } else { + get_default() + } + } + + /// Given a list of `hir::ExprId`, create a set of function + /// arguments as a comma-separated string, with names derived from + /// the expressions in a user-meaningful way. + pub(crate) fn create_function_args(&self, args: &[ExprId], body: &Body) -> String { + args.iter() + .map(|arg| { + // IntelliJ seems to turn (numeric) + // literals into `N`, and then just smash + // the variables together. So a param + // called as `X + 1 + Y` becomes `XNY`. + let vars_and_literals = body.fold_expr( + Strategy::TopDown, + *arg, + Vec::default(), + &mut |mut acc, ctx| { + match &ctx.expr { + Expr::Var(var) => { + acc.push(var.as_string(self.db().upcast())); + } + Expr::Literal(_) => { + acc.push("N".to_string()); + } + _ => {} + }; + acc + }, + &mut |acc, _| acc, + ); + vars_and_literals.join("") + }) + .collect::>() + .join(", ") + } + + #[allow(dead_code)] // Used further up the stack, will delete then + pub(crate) fn create_function_args_from_types( + &self, + args: &[TypeExprId], + body: &Body, + ) -> String { + args.iter() + .enumerate() + .map(|(i, typ)| match &body[*typ] { + hir::TypeExpr::AnnType { var, ty: _ } => var.as_string(self.db().upcast()), + _ => format!("Arg{}", i + 1), + }) + .collect::>() + .join(",") + } +} + +pub(crate) struct Assists { + file: FileId, + resolve: AssistResolveStrategy, + buf: Vec, + allowed: Option>, +} + +impl Assists { + pub(crate) fn new(ctx: &AssistContext, resolve: AssistResolveStrategy) -> Assists { + Assists { + resolve, + file: ctx.frange.file_id, + buf: Vec::new(), + allowed: ctx.config.allowed.clone(), + } + } + + pub(crate) fn finish(mut self) -> Vec { + self.buf.sort_by_key(|assist| assist.target.len()); + self.buf + } + + pub(crate) fn add( + &mut self, + id: AssistId, + label: impl Into, + target: TextRange, + user_input: Option, + f: impl FnOnce(&mut SourceChangeBuilder), + ) -> Option<()> { + if !self.is_allowed(&id) { + return None; + } + let label = Label::new(label.into()); + let assist = Assist { + id, + label, + group: None, + target, + source_change: None, + user_input, + }; + self.add_impl(assist, f) + } + + fn add_impl( + &mut self, + mut assist: Assist, + f: impl FnOnce(&mut SourceChangeBuilder), + ) -> Option<()> { + let source_change = if self.resolve.should_resolve(&assist.id) { + let mut builder = SourceChangeBuilder::new(self.file); + f(&mut builder); + Some(builder.finish()) + } else { + None + }; + assist.source_change = source_change; + + self.buf.push(assist); + Some(()) + } + + fn is_allowed(&self, id: &AssistId) -> bool { + match &self.allowed { + Some(allowed) => allowed.iter().any(|kind| kind.contains(id.1)), + None => true, + } + } +} diff --git a/crates/ide_assists/src/handlers/add_edoc.rs b/crates/ide_assists/src/handlers/add_edoc.rs new file mode 100644 index 0000000000..bbaf7695ea --- /dev/null +++ b/crates/ide_assists/src/handlers/add_edoc.rs @@ -0,0 +1,213 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_ide_db::assists::AssistId; +use elp_ide_db::assists::AssistKind; +use elp_syntax::ast; +use elp_syntax::AstNode; +use elp_syntax::TextSize; + +use crate::helpers::prev_form_nodes; +use crate::AssistContext; +use crate::Assists; + +const DEFAULT_TEXT: &str = "{@link https://www.erlang.org/doc/apps/edoc/chapter.html EDoc Manual}"; +const ARG_TEXT: &str = "Argument description"; +const RETURN_TEXT: &str = "Return description"; + +// Assist: add_edoc +// +// Adds an edoc comment above a function, if it doesn't already have one. +// +// ``` +// foo(Arg1) -> ok. +// ``` +// -> +// ``` +// %% @doc Function description +// %% @param Arg1 Argument description +// %% @returns Description +// foo(Arg1) -> ok. +// ``` +pub(crate) fn add_edoc(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { + let name = match ctx.find_node_at_offset::()? { + ast::Name::Atom(name) => name, + ast::Name::MacroCallExpr(_) | ast::Name::Var(_) => return None, + }; + let clause = ast::FunctionClause::cast(name.syntax().parent()?)?; + let function = ast::FunDecl::cast(clause.syntax().parent()?)?; + + let already_has_edoc = prev_form_nodes(&function.syntax()) + .filter(|syntax| syntax.kind() == elp_syntax::SyntaxKind::COMMENT) + .filter_map(|comment| { + let text = comment.text(); + let at = text.find_char('@')?; + Some((text, at)) + }) + .any(|(text, at)| text.slice(at..(at + TextSize::of("@doc"))) == "@doc"); + if already_has_edoc { + return None; + } + + let insert = prev_form_nodes(&function.syntax()) + .filter_map(|form| ast::Spec::cast(form)) + .map(|spec| spec.syntax().text_range().start()) + .next() + .unwrap_or_else(|| function.syntax().text_range().start()); + let target = name.syntax().text_range(); + + acc.add( + AssistId("add_edoc", AssistKind::Generate), + "Add edoc comment", + target, + None, + |builder| { + let arg_names = clause + .args() + .into_iter() + .flat_map(|args| args.args()) + .enumerate() + .map(|(arg_idx, expr)| arg_name(arg_idx + 1, expr)); + + match ctx.config.snippet_cap { + Some(cap) => { + let mut snippet_idx = 1; + let header_snippet = format!("%% @doc ${{{}:{}}}\n", snippet_idx, DEFAULT_TEXT); + let args_snippets = arg_names + .map(|arg_name| { + snippet_idx += 1; + format!("%% @param {} ${{{}:{}}}\n", arg_name, snippet_idx, ARG_TEXT) + }) + .collect::(); + snippet_idx += 1; + let snippet = format!( + "{}{}%% @returns ${{{}:{}}}\n", + header_snippet, args_snippets, snippet_idx, RETURN_TEXT + ); + builder.edit_file(ctx.frange.file_id); + builder.insert_snippet(cap, insert, snippet); + } + None => { + let args_text = arg_names + .map(|arg_name| format!("%% @param {} {}\n", arg_name, ARG_TEXT)) + .collect::(); + let text = format!( + "%% @doc {}\n{}%% @returns {}\n", + DEFAULT_TEXT, args_text, RETURN_TEXT + ); + builder.edit_file(ctx.frange.file_id); + builder.insert(insert, text) + } + } + }, + ) +} + +pub fn arg_name(arg_idx: usize, expr: ast::Expr) -> String { + if let ast::Expr::ExprMax(ast::ExprMax::Var(var)) = expr { + var.text().to_string() + } else { + format!("Arg{}", arg_idx) + } +} + +#[cfg(test)] +mod tests { + use expect_test::expect; + + use super::*; + use crate::tests::*; + + #[test] + fn test_base_case() { + check_assist( + add_edoc, + "Add edoc comment", + r#" +~foo(Foo, some_atom) -> ok. +"#, + expect![[r#" + %% @doc ${1:{@link https://www.erlang.org/doc/apps/edoc/chapter.html EDoc Manual}} + %% @param Foo ${2:Argument description} + %% @param Arg2 ${3:Argument description} + %% @returns ${4:Return description} + foo(Foo, some_atom) -> ok. + "#]], + ) + } + + #[test] + fn test_with_spec() { + check_assist( + add_edoc, + "Add edoc comment", + r#" +-spec foo(x(), y()) -> ok. +~foo(Foo, some_atom) -> ok. +"#, + expect![[r#" + %% @doc ${1:{@link https://www.erlang.org/doc/apps/edoc/chapter.html EDoc Manual}} + %% @param Foo ${2:Argument description} + %% @param Arg2 ${3:Argument description} + %% @returns ${4:Return description} + -spec foo(x(), y()) -> ok. + foo(Foo, some_atom) -> ok. + "#]], + ) + } + + #[test] + fn test_previous_has_comment() { + check_assist( + add_edoc, + "Add edoc comment", + r#" +%% @doc bar +bar() -> ok. +~foo() -> ok. +"#, + expect![[r#" + %% @doc bar + bar() -> ok. + %% @doc ${1:{@link https://www.erlang.org/doc/apps/edoc/chapter.html EDoc Manual}} + %% @returns ${2:Return description} + foo() -> ok. + "#]], + ) + } + + #[test] + fn test_non_edoc_comment() { + check_assist( + add_edoc, + "Add edoc comment", + r#" +%% Some comment +~foo() -> ok. +"#, + expect![[r#" + %% Some comment + %% @doc ${1:{@link https://www.erlang.org/doc/apps/edoc/chapter.html EDoc Manual}} + %% @returns ${2:Return description} + foo() -> ok. + "#]], + ) + } + + #[test] + fn test_already_has_edoc() { + check_assist_not_applicable( + add_edoc, + r#" +%% @doc foo +~foo(Foo, some_atom) -> ok. +"#, + ); + } +} diff --git a/crates/ide_assists/src/handlers/add_format.rs b/crates/ide_assists/src/handlers/add_format.rs new file mode 100644 index 0000000000..1ed223a4b9 --- /dev/null +++ b/crates/ide_assists/src/handlers/add_format.rs @@ -0,0 +1,135 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::cmp::min; + +use elp_ide_db::assists::AssistId; +use elp_ide_db::assists::AssistKind; +use elp_syntax::ast; +use elp_syntax::AstNode; +use elp_syntax::TextSize; + +use crate::helpers::prev_form_nodes; +use crate::AssistContext; +use crate::Assists; + +const PRAGMA: &str = "@format"; +// The following prefix is used to prevent being recognized as an EDoc tag. +const PRAGMA_PREFIX: &str = "%% % "; + +// Assist: add_format +// +// Adds a @format pragma above the module attribute line, if the file doesn't contain a pragma already. +// The pragma is used to indicate that the current file is formatted using the +// default formatter (erlfmt). +// +// ``` +// -module(my_module). +// ``` +// -> +// ``` +// %% % @format +// -module(my_module). +// ``` +pub(crate) fn add_format(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { + let module = ctx.find_node_at_offset::()?; + let already_has_format = prev_form_nodes(&module.syntax()) + .filter_map(|comment| { + let text = comment.text(); + let at = text.find_char('@')?; + Some((text, at)) + }) + .any(|(text, at)| text.slice(at..min(text.len(), at + TextSize::of(PRAGMA))) == PRAGMA); + if already_has_format { + return None; + } + + let insert = module.syntax().text_range().start(); + let target = module.syntax().text_range(); + + acc.add( + AssistId("add_format", AssistKind::Generate), + "Add @format pragma", + target, + None, + |builder| { + let text = format!("{}{}\n", PRAGMA_PREFIX, PRAGMA); + builder.edit_file(ctx.frange.file_id); + builder.insert(insert, text) + }, + ) +} + +#[cfg(test)] +mod tests { + use expect_test::expect; + + use super::*; + use crate::tests::*; + + #[test] + fn test_add_format_pragma() { + check_assist( + add_format, + "Add @format pragma", + r#" +%% LICENSE +%% LICENSE +%% LICENSE +%% +-modu~le(my_module). +"#, + expect![[r#" + %% LICENSE + %% LICENSE + %% LICENSE + %% + %% % @format + -module(my_module). + "#]], + ) + } + + #[test] + fn test_module_already_has_format_pragma() { + check_assist_not_applicable( + add_format, + r#" +%% LICENSE +%% LICENSE +%% LICENSE +%% +%% % @format +%% +-modu~le(my_module). +"#, + ); + } + + #[test] + fn test_module_with_edoc() { + check_assist( + add_format, + "Add @format pragma", + r#" +%%%------------------------------------------------------------------- +%%% @doc +%%% My custom server +-mo~dule(custom_server). +"#, + expect![[r#" + %%%------------------------------------------------------------------- + %%% @doc + %%% My custom server + %% % @format + -module(custom_server). + "#]], + ) + } +} diff --git a/crates/ide_assists/src/handlers/add_impl.rs b/crates/ide_assists/src/handlers/add_impl.rs new file mode 100644 index 0000000000..2e3cad8826 --- /dev/null +++ b/crates/ide_assists/src/handlers/add_impl.rs @@ -0,0 +1,202 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_ide_db::assists::AssistId; +use elp_ide_db::assists::AssistKind; +use elp_syntax::ast; +use elp_syntax::ast::Spec; +use elp_syntax::AstNode; +use hir::InFile; +use hir::SpecdFunctionDef; + +use crate::AssistContext; +use crate::Assists; + +// Assist: add_impl +// +// Adds an implementation stub below a spec, if it doesn't already have one. +// +// ``` +// -spec foo(Arg1 :: arg1(), arg2()) -> return_type(). +// ``` +// -> +// ``` +// -spec foo(Arg1 :: arg1(), arg2()) -> return_type(). +// foo(Arg1, Arg2) -> +// error("not implemented"). +// ``` +pub(crate) fn add_impl(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { + let spec = ctx.find_node_at_offset::()?; + let spec_id = InFile::new( + ctx.file_id(), + ctx.sema.find_enclosing_spec(ctx.file_id(), spec.syntax())?, + ); + + let has_impl_already = ctx + .db() + .def_map(spec_id.file_id) + .get_specd_functions() + .iter() + .any(|(_, SpecdFunctionDef { spec_def, .. })| spec_def.spec_id == spec_id.value); + + if has_impl_already { + return None; + } + + let name = spec.fun()?; + let name_text = name.text()?; + let insert = spec.syntax().text_range().end(); + let target = name.syntax().text_range(); + + acc.add( + AssistId("add_impl", AssistKind::Generate), + "Add implementation", + target, + None, + |builder| { + let first_sig = spec.sigs().into_iter().next().unwrap(); + let arg_names = first_sig.args().map_or(Vec::new(), |args| { + args.args() + .into_iter() + .enumerate() + .map(|(arg_idx, expr)| arg_name(arg_idx + 1, expr)) + .collect() + }); + + match ctx.config.snippet_cap { + Some(cap) => { + let mut snippet_idx = 0; + let args_snippets = arg_names + .iter() + .map(|arg_name| { + snippet_idx += 1; + format!("${{{}:{}}}, ", snippet_idx, arg_name) + }) + .collect::(); + snippet_idx += 1; + let snippet = format!( + "\n{}({}) ->\n ${{{}:error(\"not implemented\").}}\n", + name_text, + args_snippets.trim_end_matches(", "), + snippet_idx + ); + builder.edit_file(ctx.frange.file_id); + builder.insert_snippet(cap, insert, snippet); + } + None => { + let args_text = arg_names + .iter() + .map(|arg_name| format!("{}, ", arg_name)) + .collect::(); + let text = format!( + "\n{}({}) ->\n error(\"not implemented\").\n", + name_text, + args_text.trim_end_matches(", ") + ); + builder.edit_file(ctx.frange.file_id); + builder.insert(insert, text) + } + } + }, + ) +} + +pub fn arg_name(arg_idx: usize, expr: ast::Expr) -> String { + // -spec f(A) -> ok. + // f(A) -> ok. + if let ast::Expr::ExprMax(ast::ExprMax::Var(var)) = expr { + var.text().to_string() + + // -spec f(A :: foo()) -> ok. + // f(A) -> ok. + } else if let ast::Expr::AnnType(ann) = expr { + ann.var() + .and_then(|var| var.var()) + .map(|var| var.text().to_string()) + .unwrap_or_else(|| format!("Arg{}", arg_idx)) + + // -spec f(bar()) -> ok. + // f(Arg1) -> ok. + } else { + format!("Arg{}", arg_idx) + } +} + +#[cfg(test)] +mod tests { + use expect_test::expect; + + use super::*; + use crate::tests::*; + + /// We use the "expect parse error" checks below for the cases that generate + /// snippets (https://code.visualstudio.com/docs/editor/userdefinedsnippets), + /// since the snippets themselves are not valid Erlang code, but Erlang code + /// templates consumed by the LSP client to enable quick edits of parameters. + + #[test] + fn test_base_case() { + check_assist_expect_parse_error( + add_impl, + "Add implementation", + r#" +-spec ~foo(Foo :: term(), some_atom) -> ok. +"#, + expect![[r#" + -spec foo(Foo :: term(), some_atom) -> ok. + foo(${1:Foo}, ${2:Arg2}) -> + ${3:error("not implemented").} + + "#]], + ) + } + + #[test] + fn test_previous_has_impl() { + check_assist_expect_parse_error( + add_impl, + "Add implementation", + r#" +-spec bar() -> ok. +bar() -> ok. +-spec ~foo() -> return_type(). +"#, + expect![[r#" + -spec bar() -> ok. + bar() -> ok. + -spec foo() -> return_type(). + foo() -> + ${1:error("not implemented").} + + "#]], + ) + } + + #[test] + fn test_already_has_impl_above() { + check_assist_not_applicable( + add_impl, + r#" +foo(Foo, some_atom) -> ok. +-spec ~foo(x(), y()) -> ok. + "#, + ); + } + + #[test] + fn test_already_has_impl_below() { + check_assist_not_applicable( + add_impl, + r#" +-spec ~foo(x(), y()) -> ok. +foo(Foo, some_atom) -> ok. + "#, + ); + } +} diff --git a/crates/ide_assists/src/handlers/add_spec.rs b/crates/ide_assists/src/handlers/add_spec.rs new file mode 100644 index 0000000000..1456dd032c --- /dev/null +++ b/crates/ide_assists/src/handlers/add_spec.rs @@ -0,0 +1,191 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_ide_db::assists::AssistId; +use elp_ide_db::assists::AssistKind; +use elp_ide_db::SymbolClass; +use elp_ide_db::SymbolDefinition; +use elp_syntax::ast; +use elp_syntax::AstNode; + +use crate::AssistContext; +use crate::Assists; + +// Assist: add_spec +// +// Adds a spec stub above a function, if it doesn't already have one. +// +// ``` +// foo(Arg1, some_atom) -> ok. +// ``` +// -> +// ``` +// -spec foo(Arg1 :: arg1(), arg2()) -> return_type(). +// foo(Arg1, some_atom) -> ok. +// ``` +pub(crate) fn add_spec(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { + let function_def = match ctx.classify_offset()? { + SymbolClass::Definition(SymbolDefinition::Function(fun_def)) => Some(fun_def), + _ => None, + }?; + + let has_spec_already = ctx + .sema + .def_map(ctx.file_id()) + .get_spec(&function_def.function.name) + .is_some(); + + if has_spec_already { + return None; + } + + let source = function_def.source(ctx.db().upcast()); + let name = source.name()?; + let name_text = name.text()?; + + let insert = source.syntax().text_range().start(); + let target = name.syntax().text_range(); + + acc.add( + AssistId("add_spec", AssistKind::Generate), + "Add spec stub", + target, + None, + |builder| { + let type_names = source + .clauses() + .find_map(|c| match c { + ast::FunctionOrMacroClause::FunctionClause(ref clause) => { + if c.syntax().text_range().contains(ctx.offset()) { + Some(clause.clone()) + } else { + None + } + } + ast::FunctionOrMacroClause::MacroCallExpr(_) => None, + }) + .unwrap() + .args() + .into_iter() + .flat_map(|args| args.args()) + .enumerate() + .map(|(arg_idx, expr)| type_name(arg_idx + 1, expr)); + + match ctx.config.snippet_cap { + Some(cap) => { + let mut snippet_idx = 0; + let types_snippets = type_names + .map(|arg_name| { + snippet_idx += 1; + format!("${{{}:{}}}, ", snippet_idx, arg_name) + }) + .collect::(); + snippet_idx += 1; + let snippet = format!( + "-spec {}({}) -> ${{{}:return_type()}}.\n", + name_text, + types_snippets.trim_end_matches(", "), + snippet_idx + ); + builder.edit_file(ctx.frange.file_id); + builder.insert_snippet(cap, insert, snippet); + } + None => { + let types_text = type_names + .map(|arg_name| format!("{}, ", arg_name)) + .collect::(); + let text = format!( + "-spec {}({}) -> return_type().\n", + name_text, + types_text.trim_end_matches(", ") + ); + builder.edit_file(ctx.frange.file_id); + builder.insert(insert, text) + } + } + }, + ) +} + +pub fn type_name(arg_idx: usize, expr: ast::Expr) -> String { + if let ast::Expr::ExprMax(ast::ExprMax::Var(var)) = expr { + format!("{} :: type{}()", var.text().to_string(), arg_idx) + } else { + format!("type{}()", arg_idx) + } +} + +#[cfg(test)] +mod tests { + use expect_test::expect; + + use super::*; + use crate::tests::*; + + /// We use the "expect parse error" checks below for the cases that generate + /// snippets (https://code.visualstudio.com/docs/editor/userdefinedsnippets), + /// since the snippets themselves are not valid Erlang code, but Erlang code + /// templates consumed by the LSP client to enable quick edits of parameters. + + #[test] + fn test_base_case() { + check_assist_expect_parse_error( + add_spec, + "Add spec stub", + r#" +~foo(Foo, some_atom) -> ok. +"#, + expect![[r#" + -spec foo(${1:Foo :: type1()}, ${2:type2()}) -> ${3:return_type()}. + foo(Foo, some_atom) -> ok. + "#]], + ) + } + + #[test] + fn test_previous_has_spec() { + check_assist_expect_parse_error( + add_spec, + "Add spec stub", + r#" +-spec bar() -> ok. +bar() -> ok. +f~oo() -> ok. +"#, + expect![[r#" + -spec bar() -> ok. + bar() -> ok. + -spec foo() -> ${1:return_type()}. + foo() -> ok. + "#]], + ) + } + + #[test] + fn test_already_has_spec_above() { + check_assist_not_applicable( + add_spec, + r#" +-spec foo(x(), y()) -> ok. +~foo(Foo, some_atom) -> ok. + "#, + ); + } + + #[test] + fn test_already_has_spec_below() { + check_assist_not_applicable( + add_spec, + r#" +~foo(Foo, some_atom) -> ok. +-spec foo(x(), y()) -> ok. + "#, + ); + } +} diff --git a/crates/ide_assists/src/handlers/bump_variables.rs b/crates/ide_assists/src/handlers/bump_variables.rs new file mode 100644 index 0000000000..71b87667f5 --- /dev/null +++ b/crates/ide_assists/src/handlers/bump_variables.rs @@ -0,0 +1,286 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use ast::AstNode; +use elp_ide_db::assists::AssistId; +use elp_ide_db::assists::AssistKind; +use elp_ide_db::rename::RenameResult; +use elp_ide_db::rename::SafetyChecks; +use elp_ide_db::source_change::SourceChange; +use elp_ide_db::ReferenceClass; +use elp_ide_db::SymbolClass; +use elp_ide_db::SymbolDefinition; +use elp_syntax::ast; +use fxhash::FxHashSet; +use hir::Expr; +use hir::InFile; +use hir::Pat; +use lazy_static::lazy_static; +use regex::Regex; + +use crate::AssistContext; +use crate::Assists; + +// Assist: bump_variables +// +// In a sequence of variable assignments, rename them to create a gap. +// +// ``` +// foo() -> +// X0 = 1, +// X1 = X0 +1, +// X2 = X1 + 4, +// X2. +// ``` +// -> +// ``` +// foo() -> +// X0 = 1, +// X2 = X0 +1, +// X3 = X2 + 4, +// X3. +// ``` +pub(crate) fn bump_variables(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { + let var: ast::Var = ctx.find_node_at_offset()?; + if let Some(number) = NumberedVar::from_var(&var.text().as_str()) { + let variable_name = var.text(); + let variable_range = var.syntax().text_range(); + // We are on a numbered variable. Check if there are any + // others with the same stem but a higher number, and + // referring to this one. + + let file_id = ctx.frange.file_id; + let function_id = ctx + .sema + .find_enclosing_function(ctx.file_id(), var.syntax())?; + let infile_function = InFile::new(ctx.file_id(), function_id); + let (_body, body_map) = ctx.db().function_body_with_source(infile_function); + let clause_id = ctx.sema.find_enclosing_function_clause(var.syntax())?; + let vars = ctx.sema.fold_clause( + infile_function, + clause_id, + FxHashSet::default(), + &mut |mut acc, ctx| match ctx.expr { + Expr::Var(v) => { + if let Some(expr) = body_map.expr(ctx.expr_id) { + if expr.file_id() == file_id { + acc.insert((v, expr)); + } + } + acc + } + _ => acc, + }, + &mut |mut acc, ctx| match ctx.pat { + Pat::Var(v) => { + if let Some(pat) = body_map.pat(ctx.pat_id) { + if pat.file_id() == file_id { + acc.insert((v, pat)); + } + } + acc + } + _ => acc, + }, + ); + let mut var_defs = Vec::default(); + vars.into_iter() + .filter_map(|(v, vs)| { + let var_name = ctx.db().lookup_var(v); + let nv = NumberedVar::from_var(var_name.as_str())?; + Some((vs, nv)) + }) + .filter(|(_vs, nv)| nv.base == number.base && nv.number >= number.number) + .for_each(|(vs, nv)| { + || -> Option<()> { + let token = ctx.ast_ptr_get(vs)?.syntax().first_token()?; + match SymbolClass::classify(&ctx.sema, InFile::new(ctx.file_id(), token))? { + SymbolClass::Definition(SymbolDefinition::Var(d)) => { + var_defs.push((SymbolDefinition::Var(d), nv)); + } + SymbolClass::Definition(_) => {} + SymbolClass::Reference { refs, typ: _ } => { + match refs { + ReferenceClass::Definition(SymbolDefinition::Var(d)) => { + var_defs.push((SymbolDefinition::Var(d), nv)); + } + _ => {} + }; + } + }; + Some(()) + }(); + }); + + // Note: taking naive approach here, assuming the numerical + // sequence is sane. So not analyzing the actual chain of + // assignments/usages + let rename_ops: RenameResult> = var_defs + .iter() + .map(|(def, nv)| def.rename(&ctx.sema, &|_| nv.bumped(), SafetyChecks::No)) + .collect(); + + let rename_op = match rename_ops { + Ok(ops) => ops.into_iter().reduce(|acc, elem| acc.merge(elem)), + Err(_) => None, + }; + + if let Some(edits) = rename_op { + let id = AssistId("bump_variables", AssistKind::QuickFix); + let message = format!("Bump variable `{variable_name}`"); + acc.add(id, message, variable_range, None, |builder| { + builder.apply_source_change(edits); + }); + } + } + Some(()) +} + +#[derive(Debug)] +struct NumberedVar { + base: String, + number: usize, +} + +impl NumberedVar { + fn from_var(var_str: &str) -> Option { + lazy_static! { + static ref RE: Regex = Regex::new(r"[0-9]+$").unwrap(); + } + let res = RE.find(var_str)?; + let number = &var_str[res.start()..res.end()]; + match number.parse::() { + Ok(number) => Some(NumberedVar { + base: var_str[0..res.start()].to_string(), + number, + }), + Err(_) => None, + } + } + + fn bumped(&self) -> String { + format!("{}{}", self.base, self.number + 1).to_string() + } +} + +#[cfg(test)] +mod tests { + use expect_test::expect; + + use super::*; + use crate::tests::*; + + #[test] + fn bump_vars_1() { + check_assist( + bump_variables, + "Bump variable `X1`", + r#" + foo() -> + X0 = 1, + X~1 = X0 +1, + X2 = X1 + 4, + X2. + "#, + expect![[r#" + foo() -> + X0 = 1, + X2 = X0 +1, + X3 = X2 + 4, + X3. + "#]], + ) + } + + #[test] + fn bump_vars_2() { + check_assist( + bump_variables, + "Bump variable `X0`", + r#" + foo(Y) -> + X~0 = 1, + case Y of + {ok, Val} -> X0; + {new, X1} -> X1 + end. + "#, + expect![[r#" + foo(Y) -> + X1 = 1, + case Y of + {ok, Val} -> X1; + {new, X2} -> X2 + end. + "#]], + ) + } + + #[test] + fn bump_vars_3() { + check_assist( + bump_variables, + "Bump variable `X0`", + r#" + foo(0) -> + X~0 = 1, + X1 = X0 + 2, + X1; + foo(Y) -> + X0 = 2, + X1 = X0 + Y, + X1. + "#, + expect![[r#" + foo(0) -> + X1 = 1, + X2 = X1 + 2, + X2; + foo(Y) -> + X0 = 2, + X1 = X0 + Y, + X1. + "#]], + ) + } + + #[test] + fn bump_vars_with_macro() { + check_assist( + bump_variables, + "Bump variable `X1`", + r#" + //- /src/blah.erl + -module(blah). + -include("inc.hrl"). + foo() -> + ?X~1 = 1, + X2 = X1 + 4, + ?assertEqual(X2,5), + X2. + //- /src/inc.hrl + -define(assertEqual(Expect, Expr), + begin + ((fun () -> + X__X = (Expect) + end)()) + end). + "#, + expect![[r#" + -module(blah). + -include("inc.hrl"). + foo() -> + ?X1 = 1, + X3 = X1 + 4, + ?assertEqual(X3,5), + X3. + "#]], + ) + } +} diff --git a/crates/ide_assists/src/handlers/create_function.rs b/crates/ide_assists/src/handlers/create_function.rs new file mode 100644 index 0000000000..2e9e2ff5de --- /dev/null +++ b/crates/ide_assists/src/handlers/create_function.rs @@ -0,0 +1,191 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_ide_db::assists::AssistContextDiagnosticCode; +use elp_ide_db::assists::AssistId; +use elp_ide_db::assists::AssistKind; +use elp_syntax::ast; +use elp_syntax::AstNode; +use elp_syntax::TextSize; +use hir::Expr; +use hir::InFile; + +use crate::AssistContext; +use crate::Assists; + +// Assist: create_function +// +// Create a local function if it does not exist +// +// ``` +// foo() -> bar(). +// ``` +// -> +// ``` +// foo() -> bar(). +// +// bar() -> +// erlang:error(not_implemented). +// +// ``` +pub(crate) fn create_function(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { + for d in ctx.diagnostics { + if let AssistContextDiagnosticCode::UndefinedFunction = d.code { + if ctx.classify_offset().is_none() { + let call: ast::Call = ctx.find_node_at_offset()?; + let function_id = ctx + .sema + .find_enclosing_function(ctx.file_id(), call.syntax())?; + + let call_expr = ctx + .sema + .to_expr(InFile::new(ctx.file_id(), &ast::Expr::Call(call.clone())))?; + if let Expr::Call { target, args } = &call_expr[call_expr.value] { + let (module_name, function_name) = match &target { + hir::CallTarget::Local { name } => { + let fun_atom = &call_expr[name.clone()].as_atom()?; + let fun_name = ctx.sema.db.lookup_atom(*fun_atom).to_string(); + (None, fun_name) + } + hir::CallTarget::Remote { module, name } => { + let module = &call_expr[module.clone()].as_atom()?; + let fun_atom = &call_expr[name.clone()].as_atom()?; + let fun_name = ctx.sema.db.lookup_atom(*fun_atom).to_string(); + (Some(module.clone()), fun_name) + } + }; + let function_arity = args.len(); + + let form_list = ctx.db().file_form_list(ctx.file_id()); + if let Some(module_name) = module_name { + let module_name = ctx.db().lookup_atom(module_name); + let module_attr = form_list.module_attribute()?; + if module_name != module_attr.name { + // Only inline qualified function calls if + // they refer to the module we are in. + // TODO: implied main module? + return None; + } + } + let function_args = ctx.create_function_args(args, &call_expr.body()); + + let enclosing_function = &form_list[function_id]; + + let function_range = ctx + .form_ast(enclosing_function.form_id) + .syntax() + .text_range(); + + let insert = TextSize::from(function_range.end() + TextSize::from(1)); + + let id = AssistId("create_function", AssistKind::QuickFix); + let message = format!("Create function `{function_name}/{function_arity}`"); + acc.add(id, message, function_range, None, |builder| { + let text = format!("\n{function_name}({function_args}) ->\n erlang:error(not_implemented).\n\n"); + builder.edit_file(ctx.frange.file_id); + builder.insert(insert, text) + }); + } + } + } + } + Some(()) +} + +#[cfg(test)] +mod tests { + use expect_test::expect; + + use super::*; + use crate::tests::*; + + #[test] + fn one_param_function() { + check_assist( + create_function, + "Create function `foo/1`", + r#" + -module(life). + + heavy_calculations(X) -> fo~o(X). + %% ^^^^^^ 💡 L1227: function foo/1 undefined +"#, + expect![[r#" + -module(life). + + heavy_calculations(X) -> foo(X). + + foo(X) -> + erlang:error(not_implemented). + + "#]], + ) + } + + #[test] + fn qualified_function_1() { + check_assist( + create_function, + "Create function `foo/2`", + r#" + //- /src/life.erl + -module(life). + + heavy_calculations(X) -> life:f~oo(X, X+1). + %% ^^^^^^^^^^^ 💡 L1227: function life:foo/2 undefined +"#, + expect![[r#" + -module(life). + + heavy_calculations(X) -> life:foo(X, X+1). + + foo(X, XN) -> + erlang:error(not_implemented). + + "#]], + ) + } + + #[test] + fn qualified_function_2() { + check_assist_not_applicable( + create_function, + r#" + //- /src/life.erl + -module(life). + + heavy_calculations(X) -> other:f~oo(X, X+1). + %% ^^^^^^^^^^^^ 💡 L1227: function other:foo/2 undefined +"#, + ) + } + + #[test] + fn macro_qualified_function() { + check_assist( + create_function, + "Create function `foo/0`", + r#" + -module(life). + + heavy_calculations(X) -> ?MODULE:fo~o(). + %% ^^^^^^^^^^^^^ 💡 L1227: function life:foo/0 undefined +"#, + expect![[r#" + -module(life). + + heavy_calculations(X) -> ?MODULE:foo(). + + foo() -> + erlang:error(not_implemented). + + "#]], + ) + } +} diff --git a/crates/ide_assists/src/handlers/delete_function.rs b/crates/ide_assists/src/handlers/delete_function.rs new file mode 100644 index 0000000000..7815c84645 --- /dev/null +++ b/crates/ide_assists/src/handlers/delete_function.rs @@ -0,0 +1,279 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_ide_db::assists::AssistContextDiagnosticCode; +use elp_ide_db::assists::AssistId; +use elp_ide_db::assists::AssistKind; +use elp_syntax::ast::HasArity; +use elp_syntax::ast::{self}; + +use crate::assist_context::AssistContext; +use crate::assist_context::Assists; +use crate::helpers::ranges_for_delete_function; + +// Assist: delete_function +// +// Delete a function, if deemed as unused. +// +// ``` +// -module(life). +// +// heavy_calculations(X) -> X. +// %% ^^^^^^^^^^^^^^^ 💡 L1230: Function heavy_calculations/1 is unused +// +// meaning() -> +// 42. +// ``` +// -> +// ``` +// -module(life). +// +// meaning() -> +// 42. +// ``` +pub(crate) fn delete_function(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { + for d in ctx.diagnostics { + if let AssistContextDiagnosticCode::UnusedFunction = d.code { + let function_declaration: ast::FunDecl = + ctx.find_node_at_custom_offset::(d.range.start())?; + let function_name = function_declaration.name()?; + let function_arity = function_declaration.arity_value()?; + let function_ranges = ranges_for_delete_function(ctx, &function_declaration)?; + + let id = AssistId("delete_function", AssistKind::QuickFix); + let message = format!("Remove the unused function `{function_name}/{function_arity}`"); + acc.add(id, message, function_ranges.function, None, |builder| { + builder.edit_file(ctx.frange.file_id); + function_ranges.delete(builder); + }); + } + } + Some(()) +} + +#[cfg(test)] +mod tests { + use expect_test::expect; + + use super::*; + use crate::tests::*; + + #[test] + fn test_delete_unused_function() { + check_assist( + delete_function, + "Remove the unused function `heavy_calculations/1`", + r#" + -module(life). + + heavy_cal~culations(X) -> + %% ^^^^^^^^^^^^^^^^ 💡 L1230: Function heavy_calculations/1 is unused + X. + + meaning() -> + 42. +"#, + expect![[r#" + -module(life). + + meaning() -> + 42. + "#]], + ) + } + + #[test] + fn test_delete_unused_function_multiple_clauses() { + check_assist( + delete_function, + "Remove the unused function `heavy_calculations/1`", + r#" + -module(life). + + heavy_cal~culations(0) -> + %% ^^^^^^^^^^^^^^^^ 💡 L1230: Function heavy_calculations/1 is unused + 0; + heavy_calculations(X) -> + X. + + meaning() -> + 42. +"#, + expect![[r#" + -module(life). + + meaning() -> + 42. + "#]], + ) + } + + #[test] + fn test_delete_unused_function_with_spec_1() { + check_assist( + delete_function, + "Remove the unused function `heavy_calculations/1`", + r#" + -module(life). + + -spec heavy_calculations(any()) -> any(). + heavy_cal~culations(X) -> + %% ^^^^^^^^^^^^^^^^ 💡 L1230: Function heavy_calculations/1 is unused + X. + + meaning() -> + 42. +"#, + expect![[r#" + -module(life). + + meaning() -> + 42. + "#]], + ) + } + + #[test] + fn test_delete_unused_function_with_spec_2() { + check_assist( + delete_function, + "Remove the unused function `heavy_calculations/1`", + r#" + -module(life). + + heavy_cal~culations(X) -> + %% ^^^^^^^^^^^^^^^^ 💡 L1230: Function heavy_calculations/1 is unused + X. + + meaning() -> + 42. + -spec heavy_calculations(any()) -> any(). +"#, + expect![[r#" + -module(life). + + meaning() -> + 42. + "#]], + ) + } + + #[test] + fn delete_unused_function_with_edoc_1() { + check_assist( + delete_function, + "Remove the unused function `heavy_calculations/1`", + r#" + -module(life). + + %% @doc some docs + heavy_cal~culations(X) -> + %% ^^^^^^^^^^^^^^^^ 💡 L1230: Function heavy_calculations/1 is unused + X. + + meaning() -> + 42. + -spec heavy_calculations(any()) -> any(). +"#, + expect![[r#" + -module(life). + + meaning() -> + 42. + "#]], + ) + } + + #[test] + fn delete_unused_function_with_edoc_2() { + check_assist( + delete_function, + "Remove the unused function `heavy_calculations/1`", + r#" + -module(life). + + %% @doc some docs + %% Continue the tag, then a gap + + heavy_cal~culations(X) -> + %% ^^^^^^^^^^^^^^^^ 💡 L1230: Function heavy_calculations/1 is unused + X. + + meaning() -> + 42. + -spec heavy_calculations(any()) -> any(). +"#, + expect![[r#" + -module(life). + + meaning() -> + 42. + "#]], + ) + } + + #[test] + fn delete_unused_function_with_edoc_3() { + check_assist( + delete_function, + "Remove the unused function `heavy_calculations/1`", + r#" + -module(life). + + %% This is not part of the edoc + %% @doc some docs + heavy_cal~culations(X) -> + %% ^^^^^^^^^^^^^^^^ 💡 L1230: Function heavy_calculations/1 is unused + X. + + meaning() -> + 42. + -spec heavy_calculations(any()) -> any(). +"#, + expect![[r#" + -module(life). + + %% This is not part of the edoc + meaning() -> + 42. + "#]], + ) + } + + #[test] + fn delete_unused_function_with_edoc_interspersed() { + check_assist( + delete_function, + "Remove the unused function `heavy_calculations/1`", + r#" + -module(life). + + %% This is not part of the edoc + %% @doc some docs + -type client2() :: #client2{}. + %% The above type does not stop this being part of the edoc + heavy_cal~culations(X) -> + %% ^^^^^^^^^^^^^^^^ 💡 L1230: Function heavy_calculations/1 is unused + X. + + meaning() -> + 42. + -spec heavy_calculations(any()) -> any(). +"#, + expect![[r#" + -module(life). + + %% This is not part of the edoc + -type client2() :: #client2{}. + meaning() -> + 42. + "#]], + ) + } +} diff --git a/crates/ide_assists/src/handlers/export_function.rs b/crates/ide_assists/src/handlers/export_function.rs new file mode 100644 index 0000000000..dab23b7f6a --- /dev/null +++ b/crates/ide_assists/src/handlers/export_function.rs @@ -0,0 +1,230 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_ide_db::assists::AssistId; +use elp_ide_db::assists::AssistKind; +use elp_ide_db::SymbolClass; +use elp_ide_db::SymbolDefinition; +use elp_syntax::AstNode; + +use crate::helpers; +use crate::AssistContext; +use crate::Assists; + +// Assist: export_function +// +// Export a function if it is unused +// +// ``` +// foo() -> ok. +// ``` +// -> +// ``` +// -export([foo/0]). +// foo() -> ok. +// ``` +pub(crate) fn export_function(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { + if let Some(SymbolClass::Definition(SymbolDefinition::Function(fun))) = ctx.classify_offset() { + let function_name_arity = fun.function.name; + let function_range = ctx.form_ast(fun.function.form_id).syntax().text_range(); + + if !fun.exported { + let id = AssistId("export_function", AssistKind::QuickFix); + let message = format!("Export the function `{function_name_arity}`"); + acc.add(id, message, function_range, None, |builder| { + helpers::ExportBuilder::new( + &ctx.sema, + ctx.file_id(), + &[function_name_arity], + builder, + ) + .finish(); + }); + } + } + Some(()) +} + +#[cfg(test)] +mod tests { + use expect_test::expect; + + use super::*; + use crate::tests::*; + + #[test] + fn export_with_module_header() { + check_assist( + export_function, + "Export the function `heavy_calculations/1`", + r#" + -module(life). + + heavy_cal~culations(X) -> X. +"#, + expect![[r#" + -module(life). + + -export([heavy_calculations/1]). + + heavy_calculations(X) -> X. + "#]], + ) + } + + #[test] + fn export_no_module_header() { + check_assist( + export_function, + "Export the function `heavy_calculations/1`", + r#" + heavy_cal~culations(X) -> X. +"#, + expect![[r#" + + -export([heavy_calculations/1]). + heavy_calculations(X) -> X. + "#]], + ) + } + + #[test] + fn already_exported_1() { + check_assist_not_applicable( + export_function, + r#" + -export([heavy_calculations/1]). + heavy_cal~culations(X) -> X. +"#, + ) + } + + #[test] + fn already_exported_2() { + check_assist_not_applicable( + export_function, + r#" + -compile([export_all]). + heavy_cal~culations(X) -> X. +"#, + ) + } + + #[test] + fn export_into_existing_export_if_only_one() { + check_assist( + export_function, + "Export the function `heavy_calculations/1`", + r#" + -module(life). + -export([foo/0]). + + heavy_cal~culations(X) -> X. + foo() -> ok. + "#, + expect![[r#" + -module(life). + -export([foo/0, heavy_calculations/1]). + + heavy_calculations(X) -> X. + foo() -> ok. + "#]], + ) + } + + #[test] + fn export_into_existing_empty_export() { + check_assist( + export_function, + "Export the function `heavy_calculations/1`", + r#" + -module(life). + -export([]). + + heavy_cal~culations(X) -> X. + foo() -> ok. + "#, + expect![[r#" + -module(life). + -export([heavy_calculations/1]). + + heavy_calculations(X) -> X. + foo() -> ok. + "#]], + ) + } + + #[test] + fn export_into_new_export_if_multiple_existing() { + check_assist( + export_function, + "Export the function `heavy_calculations/1`", + r#" + -module(life). + -export([foo/0]). + -export([bar/0]). + + heavy_cal~culations(X) -> X. + foo() -> ok. + bar() -> ok. + "#, + expect![[r#" + -module(life). + + -export([heavy_calculations/1]). + -export([foo/0]). + -export([bar/0]). + + heavy_calculations(X) -> X. + foo() -> ok. + bar() -> ok. + "#]], + ) + } + + #[test] + fn export_quoted_atom_function() { + check_assist( + export_function, + "Export the function `'Code.Navigation.Elixirish'/1`", + r#" + -module(life). + + 'Code.Navigation.Eli~xirish'(X) -> X. + "#, + expect![[r#" + -module(life). + + -export(['Code.Navigation.Elixirish'/1]). + + 'Code.Navigation.Elixirish'(X) -> X. + "#]], + ) + } + + #[test] + fn export_cursor_on_left_margin() { + check_assist( + export_function, + "Export the function `heavy_calculations/1`", + r#" +-module(life). + +~heavy_calculations(X) -> X. +"#, + expect![[r#" + -module(life). + + -export([heavy_calculations/1]). + + heavy_calculations(X) -> X. + "#]], + ) + } +} diff --git a/crates/ide_assists/src/handlers/extract_function.rs b/crates/ide_assists/src/handlers/extract_function.rs new file mode 100644 index 0000000000..75b72f577b --- /dev/null +++ b/crates/ide_assists/src/handlers/extract_function.rs @@ -0,0 +1,1305 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_ide_db::assists::AssistId; +use elp_ide_db::assists::AssistUserInput; +use elp_ide_db::assists::AssistUserInputType; +use elp_syntax::algo; +use elp_syntax::ast; +use elp_syntax::ast::edit::IndentLevel; +use elp_syntax::ast::AstChildren; +use elp_syntax::AstNode; +use elp_syntax::Direction; +use elp_syntax::NodeOrToken; +use elp_syntax::SyntaxKind; +use elp_syntax::SyntaxNode; +use elp_syntax::SyntaxToken; +use elp_syntax::TextRange; +use fxhash::FxHashSet; +use hir::resolver::Resolution; +use hir::ScopeAnalysis; +use hir::Var; +use itertools::Itertools; +use stdx::format_to; + +use crate::assist_context::AssistContext; +use crate::assist_context::Assists; +use crate::helpers::change_indent; +use crate::helpers::freshen_function_name; +use crate::helpers::DEFAULT_INDENT_STEP; + +// Assist: extract_function +// +// Extracts selected statements and comments into new function. +// +// ``` +// main() -> +// N = 1, +// ~M = N + 2, +// // calculate +// K = M + N,~ +// G = 3. +// } +// ``` +// -> +// ``` +// main() -> +// N = 1, +// fun_name(N), +// G = 3. +// } +// +// $0fun_name(N) -> +// M = N + 2, +// // calculate +// K = M + N. +// } +// ``` +pub(crate) fn extract_function(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> { + let range = ctx.selection_trimmed(); + if range.is_empty() { + return None; + } + + let node = ctx.covering_element(); + if node.kind() == SyntaxKind::COMMENT { + return None; + } + + let node = match node { + NodeOrToken::Node(n) => n, + NodeOrToken::Token(t) => t.parent()?, + }; + + let body = extraction_target(ctx, &node, range)?; + let insert_after = node_to_insert_after(&body)?; + let target_range = body.text_range(); + + acc.add( + AssistId("extract_function", crate::AssistKind::RefactorExtract), + "Extract into function", + target_range, + Some(AssistUserInput { + input_type: AssistUserInputType::Atom, + value: make_function_name(ctx), // Maybe just a constant, this is expensive + }), + move |builder| { + let locals = body.analyze(&ctx); + let outliving_locals = body.ret_values(ctx, &locals.bound); + let params = body.extracted_function_params(locals.free); + let name = freshen_function_name( + &ctx, + ctx.user_input_or(|| make_function_name(&ctx)), + params.len() as u32, + ); + let fun = Function { + name, + params, + body, + outliving_locals, + }; + + let new_indent = IndentLevel::from_node(&insert_after); + let old_indent = fun.body.indent_level(); + + builder.replace(target_range, make_call(ctx, &fun)); + let fn_def = fun.format(ctx, old_indent, new_indent); + let insert_offset = insert_after.text_range().end(); + + match ctx.config.snippet_cap { + Some(cap) => builder.insert_snippet(cap, insert_offset, fn_def), + None => builder.insert(insert_offset, fn_def), + }; + }, + ) +} + +fn make_function_name(ctx: &AssistContext<'_>) -> String { + let def_map = ctx.sema.def_map(ctx.file_id()); + let names_in_scope: FxHashSet<_> = def_map + .get_functions() + .keys() + .chain(def_map.get_imports().into_iter().map(|(na, _)| na)) + .map(|n| n.name().as_str().to_string()) + .collect(); + let default_name = "fun_name"; + + let mut name = default_name.to_string(); + let mut counter = 0; + while names_in_scope.contains(&name) { + counter += 1; + name = format!("{}{}", &default_name, counter) + } + + name +} + +/// Try to guess what user wants to extract +/// +/// We have basically have two cases: +/// * We want whole node, +/// Then we can use `ast::Expr` +/// * We want a few statements for a block. E.g. +/// ```erlang,no_run +/// foo() -> +/// M = 1, +/// ~ +/// N = 2, +/// K = 3, +/// K + N. +/// ~ +/// } +/// ``` +/// +fn extraction_target( + ctx: &AssistContext, + node: &SyntaxNode, + selection_range: TextRange, +) -> Option { + // Covering element returned the parent block of one or multiple + // expressions that have been selected + if let Some(expr_list) = ast::ClauseBody::cast(node.clone()) { + // Extract the full expressions. + return Some(FunctionBody::from_range_clause_body( + expr_list, + selection_range, + )); + } + if let Some(fun_decl) = ast::FunDecl::cast(node.clone()) { + // Extract the full expressions. + // There can only be one clause, else the FunDecl would not be its covering element. + if let Some(clause) = fun_decl.clauses().next() { + match clause { + ast::FunctionOrMacroClause::FunctionClause(_fun_clause) => { + return Some(FunctionBody::from_range_fun_decl(fun_decl, selection_range)); + } + ast::FunctionOrMacroClause::MacroCallExpr(_) => {} + }; + } + } + + let expr = ast::Expr::cast(node.clone())?; + // A node got selected fully + if node.text_range() == selection_range { + return FunctionBody::from_expr(ctx, expr); + } + + node.ancestors() + .find_map(ast::Expr::cast) + .and_then(|expr| FunctionBody::from_expr(ctx, expr)) +} + +#[derive(Debug)] +struct Function { + name: String, + params: Vec, + body: FunctionBody, + outliving_locals: Vec, +} + +#[derive(Debug)] +struct Param { + var: Var, +} + +#[derive(Debug)] +enum FunctionBody { + Expr(ast::Expr), + Span { + parent: SpanParent, + text_range: TextRange, + }, +} + +#[derive(Debug)] +enum SpanParent { + ClauseBody(ast::ClauseBody), + FunDecl(ast::FunDecl), +} + +impl SpanParent { + fn syntax(&self) -> &SyntaxNode { + match self { + SpanParent::ClauseBody(it) => it.syntax(), + SpanParent::FunDecl(it) => it.syntax(), + } + } + + fn exprs(&self) -> Option> { + match self { + SpanParent::ClauseBody(it) => Some(it.exprs()), + SpanParent::FunDecl(it) => match it.clauses().next()? { + ast::FunctionOrMacroClause::FunctionClause(it) => Some(it.body()?.exprs()), + ast::FunctionOrMacroClause::MacroCallExpr(_) => None, + }, + } + } +} + +impl FunctionBody { + fn from_expr(ctx: &AssistContext, expr: ast::Expr) -> Option { + if !valid_extraction(&expr.syntax()) { + return None; + } + ctx.sema + .find_enclosing_function(ctx.file_id(), expr.syntax()) + .map(|_| match expr { + expr => Self::Expr(expr), + }) + } + + fn from_range_clause_body(parent: ast::ClauseBody, selected: TextRange) -> FunctionBody { + let full_body = parent.syntax().children_with_tokens(); + + let text_range = full_body + // Harvest commonality + .filter(|it| ast::Expr::can_cast(it.kind()) || it.kind() == SyntaxKind::COMMENT) + .map(|element| element.text_range()) + .filter(|&range| overlaps(&selected, &range)) + .reduce(|acc, item| acc.cover(item)); + + Self::Span { + parent: SpanParent::ClauseBody(parent), + text_range: text_range.unwrap_or(selected), + } + } + + fn from_range_fun_decl(parent: ast::FunDecl, selected: TextRange) -> FunctionBody { + let parent = SpanParent::FunDecl(parent); + let text_range = FunctionBody::nodes_in_span_parent(&parent, &selected) + .iter() + // Harvest commonality + .map(|element| element.text_range()) + .filter(|&range| overlaps(&selected, &range)) + .reduce(|acc, item| acc.cover(item)); + + Self::Span { + parent, + text_range: text_range.unwrap_or(selected), + } + } + + fn nodes_in_span_parent(parent: &SpanParent, selected: &TextRange) -> Vec { + match parent { + SpanParent::ClauseBody(clause) => clause.syntax().children_with_tokens().collect_vec(), + SpanParent::FunDecl(fun_decl) => { + let full_body = fun_decl.syntax().children_with_tokens(); + + // We need the full FunDecl for comments between the end of + // the FunctionClause and the end of the FunDecl. But we must + // process the Exprs in the FunctionClause. Hence need an expansion step + full_body + .flat_map(|it| match &it { + NodeOrToken::Node(node) => { + if let Some(function_clause) = ast::FunctionClause::cast(node.clone()) { + match function_clause.body() { + Some(clause_body) => { + clause_body.syntax().children_with_tokens().collect_vec() + } + _ => vec![it], + } + } else { + vec![it] + } + } + NodeOrToken::Token(_) => vec![it], + }) + // Harvest commonality + .filter(|it| ast::Expr::can_cast(it.kind()) || it.kind() == SyntaxKind::COMMENT) + .filter(|it| overlaps(selected, &it.text_range())) + .collect_vec() + } + } + } + + fn node(&self) -> &SyntaxNode { + match self { + FunctionBody::Expr(e) => e.syntax(), + FunctionBody::Span { parent, .. } => parent.syntax(), + } + } + + fn indent_level(&self) -> IndentLevel { + match &self { + FunctionBody::Expr(expr) => IndentLevel::from_node(expr.syntax()), + FunctionBody::Span { parent, text_range } => { + if let Some(element) = FunctionBody::nodes_in_span_parent(parent, text_range) + .iter() + .find(|it| text_range.contains_range(it.text_range())) + { + match element { + NodeOrToken::Node(node) => IndentLevel::from_node(&node), + NodeOrToken::Token(t) => IndentLevel::from_token(&t), + } + } else { + IndentLevel(1) + } + } + } + } + + fn walk_expr(&self, ctx: &AssistContext, analyzer: &mut ScopeAnalysis) { + match self { + FunctionBody::Expr(expr) => { + analyzer.walk_ast_expr(&ctx.sema, ctx.file_id(), expr.clone()) + } + FunctionBody::Span { parent, text_range } => { + if let Some(exprs) = parent.exprs() { + exprs + .filter(|expr| text_range.contains_range(expr.syntax().text_range())) + .for_each(|expr: ast::Expr| { + analyzer.walk_ast_expr(&ctx.sema, ctx.file_id(), expr) + }); + } + } + } + } + + fn text_range(&self) -> TextRange { + match self { + FunctionBody::Expr(expr) => expr.syntax().text_range(), + &FunctionBody::Span { text_range, .. } => text_range, + } + } + + /// Analyzes a function body, returning the used local variables + /// that are referenced in it. + fn analyze(&self, ctx: &AssistContext) -> ScopeAnalysis { + let mut analyzer = ScopeAnalysis::new(); + self.walk_expr(ctx, &mut analyzer); + analyzer + } + + /// Local variables defined inside `body` that are accessed outside of it + fn ret_values<'a>( + &self, + ctx: &'a AssistContext<'_>, + locals_bound_in_body: &'a FxHashSet, + ) -> Vec { + match &self { + FunctionBody::Expr(expr) => { + let parent = expr + .syntax() + .ancestors() + .find_map(ast::ClauseBody::cast) + .and_then(|body| Some(SpanParent::ClauseBody(body))); + if let Some(parent) = parent { + calculate_ret_values( + &parent, + &expr.syntax().text_range(), + ctx, + locals_bound_in_body, + ) + } else { + vec![] + } + } + FunctionBody::Span { parent, text_range } => { + calculate_ret_values(parent, text_range, ctx, locals_bound_in_body) + } + } + } + + /// find variables that should be extracted as params + /// + /// Computes additional info that affects param type and mutability + fn extracted_function_params(&self, free: FxHashSet) -> Vec { + free.into_iter().map(|(var, _)| Param { var }).collect() + } +} + +fn calculate_ret_values<'a>( + parent: &SpanParent, + text_range: &TextRange, + ctx: &AssistContext, + locals_bound_in_body: &'a FxHashSet, +) -> Vec { + let trailing: Vec<_> = parent + .syntax() + .children_with_tokens() + .filter(|it| it.text_range().start() > text_range.end()) + .filter_map(|node_or_token| match &node_or_token { + NodeOrToken::Node(node) => ast::Expr::cast(node.clone()), + _ => None, + }) + .collect::>(); + let mut analyzer = ScopeAnalysis::new(); + trailing.into_iter().for_each(|expr: ast::Expr| { + analyzer.walk_ast_expr(&ctx.sema, ctx.file_id(), expr); + }); + locals_bound_in_body + .iter() + .filter_map(|local| { + if analyzer.free.contains(local) || analyzer.bound.contains(local) { + Some(local.0) + } else { + None + } + }) + .collect() +} + +/// Check whether the node is a valid expression which can be +/// extracted to a function. In general that's true for any +/// expression, but in some cases that would produce invalid code. +fn valid_extraction(node: &SyntaxNode) -> bool { + if let Some(n) = node.parent() { + match n.kind() { + SyntaxKind::RECORD_FIELD => false, + _ => true, + } + } else { + false + } +} +/// Check if there is any part in common of the two `TextRange`s +fn overlaps(range_a: &TextRange, range_b: &TextRange) -> bool { + range_a + .intersect(*range_b) + .filter(|it| !it.is_empty()) + .is_some() +} + +/// find where to put extracted function definition +/// +/// Function should be put right after returned node +fn node_to_insert_after(body: &FunctionBody) -> Option { + let node = body.node(); + let mut ancestors = node.ancestors().peekable(); + let mut last_ancestor = None; + while let Some(next_ancestor) = ancestors.next() { + match next_ancestor.kind() { + SyntaxKind::SOURCE_FILE => break, + _ => (), + } + last_ancestor = Some(next_ancestor); + } + last_ancestor +} + +fn make_call(ctx: &AssistContext<'_>, fun: &Function) -> String { + let args = fun + .params + .iter() + .map(|param| format!("{}", ctx.db().lookup_var(param.var))) + .collect::>() + .join(","); + + let mut buf = String::default(); + + match fun.outliving_locals.as_slice() { + [] => {} + [local] => { + format_to!(buf, "{} = ", local.as_string(ctx.db().upcast())) + } + vars => { + buf.push_str("{"); + let bindings = vars.iter().format_with(", ", |local, f| { + f(&format_args!("{}", local.as_string(ctx.db().upcast()))) + }); + format_to!(buf, "{}", bindings); + buf.push_str("} = "); + } + } + + format_to!(buf, "{}", fun.name); + format_to!(buf, "({})", args); + + // Check if the selected range ended with a comment, which + // occurred after a trailing comma in the original context + let insert_comma = ends_with_comma_then_trivia(fun.body.node(), fun.body.text_range()); + if insert_comma.is_some() { + buf.push(','); + } + + buf +} + +fn ends_with_comma_then_trivia(node: &SyntaxNode, range: TextRange) -> Option { + // Temporary for T153426323 + let _pctx = stdx::panic_context::enter(format!("\nends_with_comma_then_trivia")); + let end_tok = node.token_at_offset(range.end()); + let end_tok = end_tok + .left_biased() + .and_then(|t| algo::skip_trivia_token(t, Direction::Prev)); + end_tok.map_or(None, |it| { + if it.kind() == SyntaxKind::ANON_COMMA { + Some(it.text_range()) + } else { + None + } + }) +} + +impl Function { + fn make_param_list(&self, ctx: &AssistContext<'_>) -> String { + self.params + .iter() + .map(|param| format!("{}", ctx.db().lookup_var(param.var))) + .collect::>() + .join(",") + } + + fn format( + &self, + ctx: &AssistContext<'_>, + old_indent: IndentLevel, + new_indent: IndentLevel, + ) -> String { + let mut fn_def = String::new(); + let params = self.make_param_list(ctx); + let ret_ty = self.make_ret_ty(ctx); + let (body, ends_with_comment) = self.make_body(old_indent); + match ctx.config.snippet_cap { + Some(_) => format_to!(fn_def, "\n\n{}$0{}({}) ->", new_indent, self.name, params), + None => format_to!(fn_def, "\n\n{}{}({}) ->", new_indent, self.name, params), + } + fn_def.push_str(&body); + + if let Some(ret_ty) = ret_ty { + format_to!(fn_def, ",\n {}", ret_ty); + fn_def.push_str("."); + } else { + // If the body ends with a comment, put the final `.` on a new + // line. + if ends_with_comment { + fn_def.push_str("\n ."); + } else { + fn_def.push_str("."); + } + } + fn_def + } + + fn make_ret_ty(&self, ctx: &AssistContext<'_>) -> Option { + match self.outliving_locals.as_slice() { + [] => None, + [local] => Some(local.as_string(ctx.db().upcast())), + vars => Some(format!( + "{{{}}}", // open and closing braces, around vars + vars.iter() + .map(|v| v.as_string(ctx.db().upcast())) + .collect::>() + .join(", ") + )), + } + } + + fn make_body(&self, old_indent: IndentLevel) -> (String, bool) { + let mut fun_str = String::default(); + let delta_indent = DEFAULT_INDENT_STEP - old_indent.0 as i8; + + let mut last_tok = None; + let block = match &self.body { + FunctionBody::Expr(expr) => { + change_indent(DEFAULT_INDENT_STEP, format!("\n{}", expr.syntax())) + } + FunctionBody::Span { parent, text_range } => { + let mut parts = " ".repeat(old_indent.0 as usize); + parts = "\n".to_owned() + &parts; + + let has_comma = ends_with_comma_then_trivia(&self.body.node(), *text_range); + + tokens(parent.syntax()) + .filter(|it| { + text_range.contains_range(it.text_range()) + // Remove comma before comment, if it exists we have its range. + && has_comma.map_or(true, |comma_range| comma_range != it.text_range()) + }) + .for_each(|it| { + parts.push_str(it.text()); + last_tok = Some(it); + }); + change_indent(delta_indent, parts) + } + }; + + fun_str.push_str(&block); + ( + fun_str, + last_tok.map_or(false, |t| t.kind() == SyntaxKind::COMMENT), + ) + } +} + +fn tokens(node: &SyntaxNode) -> impl Iterator { + node.descendants_with_tokens() + .filter_map(|element| element.into_token()) + .filter_map(move |token| Some(token)) +} + +// --------------------------------------------------------------------- +#[cfg(test)] +mod tests { + use expect_test::expect; + + use super::*; + use crate::tests::check_assist; + use crate::tests::check_assist_not_applicable; + use crate::tests::check_assist_with_user_input; + + #[test] + fn no_args_from_binary_expr() { + check_assist( + extract_function, + "Extract into function", + r#" +foo() -> + foo(~1 + 1~). +"#, + expect![[r#" + foo() -> + foo(fun_name_edited()). + + $0fun_name_edited() -> + 1 + 1. + "#]], + ); + } + + #[test] + fn no_args_last_expr() { + check_assist( + extract_function, + "Extract into function", + r#" + foo() -> + K = 1, + ~M = 1, + M + 1~. + "#, + expect![[r#" + foo() -> + K = 1, + fun_name_edited(). + + $0fun_name_edited() -> + M = 1, + M + 1. + "#]], + ); + } + + #[test] + fn no_args_not_last_expr() { + check_assist( + extract_function, + "Extract into function", + r#" + foo() -> + K = 1, + ~foo(), + baz:bar()~, + K + 1. + "#, + expect![[r#" + foo() -> + K = 1, + fun_name_edited(), + K + 1. + + $0fun_name_edited() -> + foo(), + baz:bar(). + "#]], + ); + } + + #[test] + fn extract_with_args_no_return() { + check_assist( + extract_function, + "Extract into function", + r#" + foo() -> + K = 1, + M = 1, + ~M + 1~, + ok. + "#, + expect![[r#" + foo() -> + K = 1, + M = 1, + fun_name_edited(M), + ok. + + $0fun_name_edited(M) -> + M + 1. + "#]], + ); + } + + #[test] + fn extract_with_args_return_single() { + check_assist( + extract_function, + "Extract into function", + r#" + foo() -> + N = 1, + V = ~N * N,~ + V + 1. + "#, + expect![[r#" + foo() -> + N = 1, + V = fun_name_edited(N), + V + 1. + + $0fun_name_edited(N) -> + V = N * N, + V. + "#]], + ); + } + + #[test] + fn extract_with_args_return_multiple() { + check_assist( + extract_function, + "Extract into function", + r#" + foo() -> + N = 1, + ~V = N * N, + J = 1~, + V + J. + "#, + expect![[r#" + foo() -> + N = 1, + {V, J} = fun_name_edited(N), + V + J. + + $0fun_name_edited(N) -> + V = N * N, + J = 1, + {V, J}. + "#]], + ); + } + + #[test] + fn extract_with_args_return_what_is_needed() { + check_assist( + extract_function, + "Extract into function", + r#" + foo() -> + N = 1, + ~V = N * N, + J = 1~, + V + 3. + "#, + expect![[r#" + foo() -> + N = 1, + V = fun_name_edited(N), + V + 3. + + $0fun_name_edited(N) -> + V = N * N, + J = 1, + V. + "#]], + ); + } + + #[test] + fn extract_does_not_clash_name1() { + check_assist( + extract_function, + "Extract into function", + r#" + foo() -> + ~N = 1~. + + fun_name() -> ok. + "#, + expect![[r#" + foo() -> + fun_name1_edited(). + + $0fun_name1_edited() -> + N = 1. + + fun_name() -> ok. + "#]], + ); + } + + #[test] + fn extract_does_not_clash_name2() { + check_assist( + extract_function, + "Extract into function", + r#" + -import(bar, [fun_name/0]). + foo() -> + ~N = 1~. + "#, + expect![[r#" + -import(bar, [fun_name/0]). + foo() -> + fun_name1_edited(). + + $0fun_name1_edited() -> + N = 1. + "#]], + ); + } + + #[test] + fn extract_preserves_internal_comments() { + check_assist( + extract_function, + "Extract into function", + r#" + foo() -> + N = 1, + ~V = N * N, + %% A comment + J = 1~, + V + 3. + "#, + expect![[r#" + foo() -> + N = 1, + V = fun_name_edited(N), + V + 3. + + $0fun_name_edited(N) -> + V = N * N, + %% A comment + J = 1, + V. + "#]], + ); + } + + #[test] + fn in_comment_is_not_applicable() { + check_assist_not_applicable( + extract_function, + r#"main() -> 1 + %% ~comment~ + 1."#, + ); + } + + #[test] + fn extract_does_not_tear_comments_apart1() { + check_assist( + extract_function, + "Extract into function", + r#" + foo() -> + %% ~ comment1 + foo(), + foo(), + %% ~ comment2 + bar(). + "#, + expect![[r#" + foo() -> + fun_name_edited(), + bar(). + + $0fun_name_edited() -> + %% comment1 + foo(), + foo() + %% comment2 + . + "#]], + ); + } + + #[test] + fn extract_does_not_tear_comments_apart2() { + check_assist( + extract_function, + "Extract into function", + r#" + foo() -> + %% ~ comment1 + foo(), + foo() + %% ~ comment2 + . + "#, + expect![[r#" + foo() -> + fun_name_edited() + . + + $0fun_name_edited() -> + %% comment1 + foo(), + foo() + %% comment2 + . + "#]], + ); + } + + #[test] + fn extract_function_copies_comment_at_start() { + check_assist( + extract_function, + "Extract into function", + r#" + func()-> + I = 0, + ~%% comment here! + X = 0.~ + "#, + expect![[r#" + func()-> + I = 0, + fun_name_edited(). + + $0fun_name_edited() -> + %% comment here! + X = 0. + "#]], + ); + } + + #[test] + fn extract_function_copies_comment_at_end() { + check_assist( + extract_function, + "Extract into function", + r#" + func() -> + I = 0, + ~X = 0, + %% comment here!~ + foo(). + "#, + expect![[r#" + func() -> + I = 0, + fun_name_edited(), + foo(). + + $0fun_name_edited() -> + X = 0 + %% comment here! + . + "#]], + ); + } + #[test] + fn extract_function_copies_comment_indented() { + check_assist( + extract_function, + "Extract into function", + r#" + func() -> + I = 0, + ~X = 0, + begin + %% comment here! + A = 3 + end~. + "#, + expect![[r#" + func() -> + I = 0, + fun_name_edited(). + + $0fun_name_edited() -> + X = 0, + begin + %% comment here! + A = 3 + end. + "#]], + ); + } + + #[test] + fn extract_subject_of_case() { + check_assist( + extract_function, + "Extract into function", + r#" + foo() -> + X = 1, + case ~X + 3~ of + _ -> ok + end, + foo(). + "#, + expect![[r#" + foo() -> + X = 1, + case fun_name_edited(X) of + _ -> ok + end, + foo(). + + $0fun_name_edited(X) -> + X + 3. + "#]], + ); + } + + #[test] + fn extract_already_complex_return() { + check_assist( + extract_function, + "Extract into function", + r#" + foo() -> + X = 1, + ~{ok, Bar} = case X of + _ -> {ok, X+2} + end, + J = Bar + X + 2,~ + J + Bar. + "#, + expect![[r#" + foo() -> + X = 1, + {J, Bar} = fun_name_edited(X), + J + Bar. + + $0fun_name_edited(X) -> + {ok, Bar} = case X of + _ -> {ok, X+2} + end, + J = Bar + X + 2, + {J, Bar}. + "#]], + ); + } + + #[test] + fn extract_expression_in_case_arm() { + check_assist( + extract_function, + "Extract into function", + r#" + foo() -> + X = 1, + {ok, Bar} = case X of + _ -> {ok, ~X+2~} + end, + J = Bar + X + 2, + J + Bar. + "#, + expect![[r#" + foo() -> + X = 1, + {ok, Bar} = case X of + _ -> {ok, fun_name_edited(X)} + end, + J = Bar + X + 2, + J + Bar. + + $0fun_name_edited(X) -> + X+2. + "#]], + ); + } + + #[test] + fn extract_partial_block() { + check_assist( + extract_function, + "Extract into function", + r#" + foo() -> + M = 2, + N = 1, + V = M ~* N, + W = 3,~ + V + W. + "#, + expect![[r#" + foo() -> + M = 2, + N = 1, + {V, W} = fun_name_edited(N,M), + V + W. + + $0fun_name_edited(N,M) -> + V = M * N, + W = 3, + {V, W}. + "#]], + ); + } + + #[test] + fn extract_in_macro_def_rhs() { + check_assist_not_applicable( + extract_function, + r#" + -define(FOO(X), ~X+3~). + "#, + ); + } + + #[test] + fn extract_record_field_1() { + check_assist_not_applicable( + extract_function, + r#" + foo(X) -> + #?REQ_ARGS_STRUCT_NAME{ + ~request = Normal#?REQ_STRUCT_NAME{ + field_name = undefined + }~ + }. + "#, + ); + } + + #[test] + fn extract_record_field_2() { + check_assist_not_applicable( + extract_function, + r#" + foo(X) -> + #?REQ_ARGS_STRUCT_NAME{ + ~request~ = Normal#?REQ_STRUCT_NAME{ + field_name = undefined + } + }. + "#, + ); + } + + #[test] + fn user_input_fun_name() { + check_assist( + extract_function, + "Extract into function", + r#" +foo() -> + foo(~1 + 1~). +"#, + expect![[r#" + foo() -> + foo(fun_name_edited()). + + $0fun_name_edited() -> + 1 + 1. + "#]], + ); + } + + #[test] + fn check_new_name_is_safe() { + check_assist_with_user_input( + extract_function, + "Extract into function", + "foo", + r#" + foo() -> + foo(~1 + 1~). + "#, + expect![[r#" + foo() -> + foo(foo_0()). + + $0foo_0() -> + 1 + 1. + "#]], + ); + } + + #[test] + fn check_new_name_is_safe_checks_arity() { + check_assist_with_user_input( + extract_function, + "Extract into function", + "foo", + r#" + foo(X) -> + bar(~1 + 1~). + "#, + expect![[r#" + foo(X) -> + bar(foo()). + + $0foo() -> + 1 + 1. + "#]], + ); + } + + #[test] + fn check_new_name_is_safe_checks_erlang_auto() { + check_assist_with_user_input( + extract_function, + "Extract into function", + "date", + r#" + foo(X) -> + bar(~1 + 1~). + "#, + expect![[r#" + foo(X) -> + bar(date_0()). + + $0date_0() -> + 1 + 1. + "#]], + ); + } + + #[test] + fn underscores() { + check_assist( + extract_function, + "Extract into function", + r#" + f() -> + _ = 42, + ~{ok, _} = {ok, 42}~. + "#, + expect![[r#" + f() -> + _ = 42, + fun_name_edited(). + + $0fun_name_edited() -> + {ok, _} = {ok, 42}. + "#]], + ); + } + + #[test] + // T147302206 + fn trailing_comma() { + check_assist( + extract_function, + "Extract into function", + r#" + version(Application) -> + ~{ok, Version} = application:get_key(Application, vsn)~, + Version. + "#, + expect![[r#" + version(Application) -> + Version = fun_name_edited(Application), + Version. + + $0fun_name_edited(Application) -> + {ok, Version} = application:get_key(Application, vsn), + Version. + "#]], + ); + } +} diff --git a/crates/ide_assists/src/handlers/extract_variable.rs b/crates/ide_assists/src/handlers/extract_variable.rs new file mode 100644 index 0000000000..2391c676b1 --- /dev/null +++ b/crates/ide_assists/src/handlers/extract_variable.rs @@ -0,0 +1,341 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_ide_db::assists::AssistId; +use elp_ide_db::assists::AssistKind; +use elp_ide_db::assists::AssistUserInput; +use elp_ide_db::assists::AssistUserInputType; +use elp_syntax::ast; +use elp_syntax::AstNode; +use elp_syntax::NodeOrToken; +use elp_syntax::SyntaxKind; +use elp_syntax::SyntaxNode; +use hir::InFile; +use stdx::format_to; + +use crate::assist_context::AssistContext; +use crate::assist_context::Assists; +use crate::helpers::freshen_variable_name; +use crate::helpers::suggest_name_for_variable; + +// Assist: extract_variable +// +// Extracts subexpression into a variable. +// +// ``` +// foo() -> +// $0(1 + 2)$0 * 4. +// ``` +// -> +// ``` +// foo() -> +// $0VarName = (1 + 2), +// VarName * 4. +// ``` +// Note: $0 is the snippet language encoding of cursor ranges and positions. +pub(crate) fn extract_variable(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { + if ctx.has_empty_selection() { + return None; + } + + let node = match ctx.covering_element() { + NodeOrToken::Node(it) => it, + NodeOrToken::Token(it) if it.kind() == SyntaxKind::COMMENT => { + return None; + } + NodeOrToken::Token(it) => it.parent()?, + }; + let node = node + .ancestors() + .take_while(|anc| anc.text_range() == node.text_range()) + .last()?; + if !valid_extraction(&node) { + return None; + } + let to_extract = node + .descendants() + .take_while(|it| ctx.selection_trimmed().contains_range(it.text_range())) + .find_map(valid_target_expr)?; + + let anchor = Anchor::from(&to_extract)?; + let target = to_extract.syntax().text_range(); + let indent = anchor.syntax().prev_sibling_or_token()?.as_token()?.clone(); + acc.add( + AssistId("extract_variable", AssistKind::RefactorExtract), + "Extract into variable", + target, + Some(AssistUserInput { + input_type: AssistUserInputType::Variable, + value: suggest_name_for_variable(&to_extract, &ctx.sema), + }), + move |edit| { + let vars_in_clause = ctx + .sema + .find_vars_in_clause_ast(&InFile::new(ctx.file_id(), &to_extract)); + let var_name = freshen_variable_name( + &ctx.sema, + ctx.user_input_or(|| suggest_name_for_variable(&to_extract, &ctx.sema)), + &vars_in_clause, + ); + let expr_range = to_extract.syntax().text_range(); + + let mut buf = String::new(); + format_to!(buf, "{} = {}", var_name, to_extract.syntax()); + + buf.push(','); + + // We want to maintain the indent level, + // but we do not want to duplicate possible + // extra newlines in the indent block + let text = indent.text(); + if text.starts_with('\n') { + buf.push('\n'); + buf.push_str(text.trim_start_matches('\n')); + } else { + buf.push_str(text); + } + + edit.replace(expr_range, var_name.clone()); + let offset = anchor.syntax().text_range().start(); + match ctx.config.snippet_cap { + Some(cap) => { + let snip = buf.replace(&format!("{}", var_name), &format!("$0{}", var_name)); + edit.insert_snippet(cap, offset, snip) + } + None => edit.insert(offset, buf), + } + }, + ) +} + +/// Check whether the node is a valid expression which can be +/// extracted to a variable. In general that's true for any +/// expression, but in some cases that would produce invalid code. +fn valid_target_expr(node: SyntaxNode) -> Option { + ast::Expr::cast(node) +} + +/// Check whether the node is a valid expression which can be +/// extracted to a variable. In general that's true for any +/// expression, but in some cases that would produce invalid code. +fn valid_extraction(node: &SyntaxNode) -> bool { + match node.kind() { + SyntaxKind::RECORD_FIELD => false, + _ => true, + } +} + +#[derive(Debug)] +struct Anchor(SyntaxNode); + +impl Anchor { + fn from(to_extract: &ast::Expr) -> Option { + to_extract + .syntax() + .ancestors() + // Do not ascend beyond the current declaration + .take_while(|it| { + !ast::ClauseBody::can_cast(it.kind()) || ast::MacroCallExpr::can_cast(it.kind()) + }) + .find_map(|node| { + if ast::MacroCallExpr::can_cast(node.kind()) { + return None; + } + + if let Some(_expr) = node.parent().and_then(ast::ClauseBody::cast) { + return Some(Anchor(node)); + } + + None + }) + } + + fn syntax(&self) -> &SyntaxNode { + &self.0 + } +} + +#[cfg(test)] +mod tests { + use expect_test::expect; + + use super::*; + use crate::tests::check_assist; + use crate::tests::check_assist_not_applicable; + use crate::tests::check_assist_with_user_input; + + #[test] + fn test_extract_var_simple() { + check_assist( + extract_variable, + "Extract into variable", + r#" +foo() -> + ~(1 + 2)~ * 4. +"#, + expect![[r#" + foo() -> + $0VarNameEdited = (1 + 2), + VarNameEdited * 4. + "#]], + ); + } + + #[test] + fn test_extract_var_case_rhs() { + check_assist( + extract_variable, + "Extract into variable", + r#" +foo(X) -> + case X of + 1 -> ~3 + X~; + _ -> X + end. +"#, + expect![[r#" + foo(X) -> + case X of + 1 -> $0VarNameEdited = 3 + X, VarNameEdited; + _ -> X + end. + "#]], + ); + } + + #[test] + fn test_extract_var_case_expr() { + check_assist( + extract_variable, + "Extract into variable", + r#" +foo(X) -> + case ~X + 2~ of + 1 -> 3 + X; + _ -> X + end. +"#, + expect![[r#" + foo(X) -> + $0VarNameEdited = X + 2, + case VarNameEdited of + 1 -> 3 + X; + _ -> X + end. + "#]], + ); + } + + #[test] + fn test_extract_var_in_record_1() { + check_assist_not_applicable( + extract_variable, + r#" +foo(X) -> + #?REQ_ARGS_STRUCT_NAME{ + ~request = Normal#?REQ_STRUCT_NAME{ + field_name = undefined + }~ + }. +"#, + ); + } + + #[test] + fn test_extract_var_in_record_2() { + check_assist( + extract_variable, + "Extract into variable", + r#" + foo(X) -> + #?REQ_ARGS_STRUCT_NAME{ + request = Normal#?REQ_STRUCT_NAME{ + field_name = ~undefined~ + } + }."#, + expect![[r#" + foo(X) -> + $0VarNameEdited = undefined, + #?REQ_ARGS_STRUCT_NAME{ + request = Normal#?REQ_STRUCT_NAME{ + field_name = VarNameEdited + } + }."#]], + ); + } + + #[test] + fn test_extract_var_in_record_3() { + check_assist( + extract_variable, + "Extract into variable", + r#" + foo(X) -> + #?REQ_ARGS_STRUCT_NAME{ + request = ~Normal~#?REQ_STRUCT_NAME{ + field_name = undefined + } + }."#, + expect![[r#" + foo(X) -> + $0VarNameEdited = Normal, + #?REQ_ARGS_STRUCT_NAME{ + request = VarNameEdited#?REQ_STRUCT_NAME{ + field_name = undefined + } + }."#]], + ); + } + + #[test] + fn test_extract_var_in_record_4() { + check_assist( + extract_variable, + "Extract into variable", + r#" + foo(X) -> + #record_name{field_name = ~6~}."#, + expect![[r#" + foo(X) -> + $0VarNameEdited = 6, + #record_name{field_name = VarNameEdited}."#]], + ); + } + + #[test] + fn test_extract_var_in_record_5() { + check_assist( + extract_variable, + "Extract into variable", + r#" + foo(X) -> X#record_name.~field_name~."#, + expect!["foo(X) -> $0VarNameEdited = field_name, X#record_name.VarNameEdited."], + ); + } + + #[test] + fn check_new_name_is_safe() { + check_assist_with_user_input( + extract_variable, + "Extract into variable", + "NameClash", + r#" + foo() -> + NameClash = 3, + ~(1 + 2)~ * 4. + "#, + expect![[r#" + foo() -> + NameClash = 3, + $0NameClash0 = (1 + 2), + NameClash0 * 4. + "#]], + ); + } +} diff --git a/crates/ide_assists/src/handlers/flip_sep.rs b/crates/ide_assists/src/handlers/flip_sep.rs new file mode 100644 index 0000000000..57915e9a5e --- /dev/null +++ b/crates/ide_assists/src/handlers/flip_sep.rs @@ -0,0 +1,492 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_ide_db::assists::AssistId; +use elp_ide_db::assists::AssistKind; +use elp_syntax::algo::non_trivia_sibling; +use elp_syntax::Direction; +use elp_syntax::SyntaxKind; +use fxhash::FxHashSet; + +use crate::AssistContext; +use crate::Assists; + +// Assist: flip_sep +// +// Flips two items around a separator. +// +// ``` +// {{1, 2}~, {3, 4}}. +// ``` +// -> +// ``` +// {{3, 4}~, {1, 2}}. +// ``` +// +// ``` +// f(A~, B) -> ok. +// ``` +// -> +// ``` +// f(B, A) -> ok. +// ``` +pub(crate) fn flip_sep(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { + let pivot = ctx.find_tokens_syntax_at_offset(FxHashSet::from_iter([ + SyntaxKind::ANON_COMMA, + SyntaxKind::ANON_SEMI, + ]))?; + + let prev = non_trivia_sibling(pivot.clone().into(), Direction::Prev)?; + let next = non_trivia_sibling(pivot.clone().into(), Direction::Next)?; + + acc.add( + AssistId("flip_sep", AssistKind::RefactorRewrite), + "Flip around separator", + pivot.text_range(), + None, + |edit| { + edit.replace(prev.text_range(), next.to_string()); + edit.replace(next.text_range(), prev.to_string()); + }, + ) +} + +#[cfg(test)] +mod tests { + use expect_test::expect; + + use super::*; + use crate::tests::*; + + // --- Just two elements to swap --- + + #[test] + fn test_two_function_params() { + check_assist( + flip_sep, + "Flip around separator", + r#" +foo(Foo~, some_atom) -> ok. +"#, + expect![[r#" + foo(some_atom, Foo) -> ok. + "#]], + ) + } + + #[test] + fn test_two_function_args() { + check_assist( + flip_sep, + "Flip around separator", + r#" +foo(Foo~, some_atom) -> ok. +"#, + expect![[r#" + foo(some_atom, Foo) -> ok. + "#]], + ) + } + + #[test] + fn test_two_spec_params() { + check_assist( + flip_sep, + "Flip around separator", + r#" +-spec foo(Foo :: t()~, some_atom) -> ok. +"#, + expect![[r#" + -spec foo(some_atom, Foo :: t()) -> ok. + "#]], + ) + } + + #[test] + fn test_two_statements() { + check_assist( + flip_sep, + "Flip around separator", + r#" +foo(Foo, some_atom) -> + lists:reverse([1,2,3])~, + ok. +"#, + expect![[r#" + foo(Foo, some_atom) -> + ok, + lists:reverse([1,2,3]). + "#]], + ) + } + + #[test] + fn test_two_cases() { + check_assist( + flip_sep, + "Flip around separator", + r#" +foo(Foo) -> + case Foo of + a -> x~; + b -> y + end. +"#, + expect![[r#" + foo(Foo) -> + case Foo of + b -> y; + a -> x + end. + "#]], + ) + } + + #[test] + fn test_two_intersected_specs() { + check_assist( + flip_sep, + "Flip around separator", + r#" +-spec foo(Foo :: t(), some_atom) -> a~; (Bar :: r(), other_atom) -> b. +"#, + expect![[r#" + -spec foo(Bar :: r(), other_atom) -> b; (Foo :: t(), some_atom) -> a. + "#]], + ) + } + + #[test] + fn test_two_function_clauses() { + check_assist( + flip_sep, + "Flip around separator", + r#" +foo({X, Y}) -> X ++ Y~; +foo(XY) -> XY. +"#, + expect![[r#" + foo(XY) -> XY; + foo({X, Y}) -> X ++ Y. + "#]], + ) + } + + #[test] + fn test_two_tuple_elements() { + check_assist( + flip_sep, + "Flip around separator", + r#" +foo({X~, Y}) -> X ++ Y. +"#, + expect![[r#" + foo({Y, X}) -> X ++ Y. + "#]], + ) + } + + #[test] + fn test_two_list_elements() { + check_assist( + flip_sep, + "Flip around separator", + r#" +foo() -> [1~, 2]. +"#, + expect![[r#" + foo() -> [2, 1]. + "#]], + ) + } + + #[test] + fn test_comma_in_string_not_a_separator() { + check_assist_not_applicable( + flip_sep, + r#" +foo() -> + "This is not a pivot~, I think". + "#, + ); + } + + #[test] + fn test_semicolon_in_string_not_a_separator() { + check_assist_not_applicable( + flip_sep, + r#" +foo() -> + "This is not a pivot~; I think". + "#, + ); + } + + #[test] + fn test_comma_in_atom_not_a_separator() { + check_assist_not_applicable( + flip_sep, + r#" +foo() -> + 'quoted~,atom'. + "#, + ); + } + + #[test] + fn test_comma_in_semicolon_not_a_separator() { + check_assist_not_applicable( + flip_sep, + r#" +foo() -> + 'quoted~;atom'. + "#, + ); + } + + // --- Multiple elements, of which only two should be swapped --- + + #[test] + fn test_multiple_function_params() { + check_assist( + flip_sep, + "Flip around separator", + r#" +foo(Bar, Foo~, some_atom) -> ok. +"#, + expect![[r#" + foo(Bar, some_atom, Foo) -> ok. + "#]], + ) + } + + #[test] + fn test_multiple_function_args() { + check_assist( + flip_sep, + "Flip around separator", + r#" +foo(Bar, Foo~, some_atom) -> ok. +"#, + expect![[r#" + foo(Bar, some_atom, Foo) -> ok. + "#]], + ) + } + + #[test] + fn test_multiple_spec_params() { + check_assist( + flip_sep, + "Flip around separator", + r#" +-spec foo(Bar :: boolean(), Foo :: string()~, some_atom) -> ok. +"#, + expect![[r#" + -spec foo(Bar :: boolean(), some_atom, Foo :: string()) -> ok. + "#]], + ) + } + + #[test] + fn test_multiple_statements() { + check_assist( + flip_sep, + "Flip around separator", + r#" +foo(Foo, some_atom) -> + [a,b,c], + lists:reverse([1,2,3])~, + ok, + {error, "reason"}. +"#, + expect![[r#" + foo(Foo, some_atom) -> + [a,b,c], + ok, + lists:reverse([1,2,3]), + {error, "reason"}. + "#]], + ) + } + + #[test] + fn test_multiple_cases() { + check_assist( + flip_sep, + "Flip around separator", + r#" +foo(Foo) -> + case Foo of + {a,b} -> w; + a -> x~; + b -> y; + _ -> z + end. +"#, + expect![[r#" + foo(Foo) -> + case Foo of + {a,b} -> w; + b -> y; + a -> x; + _ -> z + end. + "#]], + ) + } + + #[test] + fn test_multiple_intersected_specs() { + check_assist( + flip_sep, + "Flip around separator", + r#" +-spec foo(Baz :: s(), another_atom) -> c; (Foo :: t(), some_atom) -> a~; (Bar :: r(), other_atom) -> b. +"#, + expect![[r#" + -spec foo(Baz :: s(), another_atom) -> c; (Bar :: r(), other_atom) -> b; (Foo :: t(), some_atom) -> a. + "#]], + ) + } + + #[test] + fn test_multiple_function_clauses() { + check_assist( + flip_sep, + "Flip around separator", + r#" +foo([X | Ys]) -> Ys; +foo({X, Y}) -> X ++ Y~; +foo(XY) -> XY. +"#, + expect![[r#" + foo([X | Ys]) -> Ys; + foo(XY) -> XY; + foo({X, Y}) -> X ++ Y. + "#]], + ) + } + + #[test] + fn test_multiple_tuple_elements() { + check_assist( + flip_sep, + "Flip around separator", + r#" +foo({W, X~, Y, Z}) -> W ++ X ++ Y ++ Z. +"#, + expect![[r#" + foo({W, Y, X, Z}) -> W ++ X ++ Y ++ Z. + "#]], + ) + } + + #[test] + fn test_multiple_list_elements() { + check_assist( + flip_sep, + "Flip around separator", + r#" +foo() -> [0, 1~, 2, 3]. +"#, + expect![[r#" + foo() -> [0, 2, 1, 3]. + "#]], + ) + } + + #[test] + fn test_multiple_binary_elements() { + check_assist( + flip_sep, + "Flip around separator", + r#" +foo() -> + A = 1, + B = 17, + C = 42, + D = 9, + <>. +"#, + expect![[r#" + foo() -> + A = 1, + B = 17, + C = 42, + D = 9, + <>. + "#]], + ); + } + + #[test] + fn test_list_comprehension_generators_and_filters_are_flippable() { + check_assist( + flip_sep, + "Flip around separator", + r#" +foo() -> + [ X || X <- [1,2,c,4], is_integer(X)~, X > 1 ]. +"#, + expect![[r#" + foo() -> + [ X || X <- [1,2,c,4], X > 1, is_integer(X) ]. + "#]], + ) + } + + #[test] + fn test_list_comprehension_values_are_flippable() { + check_assist( + flip_sep, + "Flip around separator", + r#" +foo() -> + [{X~, Y} || X <- [1,2,3], Y <- [a,b]]. +"#, + expect![[r#" + foo() -> + [{Y, X} || X <- [1,2,3], Y <- [a,b]]. + "#]], + ) + } + + #[test] + fn test_binary_comprehension_generators_and_filters_are_flippable() { + check_assist( + flip_sep, + "Flip around separator", + r#" +foo() -> + [ X || X <= <<1,2,3,4>>, is_integer(X)~, X > 1 ]. +"#, + expect![[r#" + foo() -> + [ X || X <= <<1,2,3,4>>, X > 1, is_integer(X) ]. + "#]], + ) + } + + #[test] + fn test_binary_comprehension_values_are_flippable() { + check_assist( + flip_sep, + "Flip around separator", + r#" +foo() -> + [ {X~, X + 1} || X <= <<1,2,3,4>>, is_integer(X), X > 1 ]. +"#, + expect![[r#" + foo() -> + [ {X + 1, X} || X <= <<1,2,3,4>>, is_integer(X), X > 1 ]. + "#]], + ) + } +} diff --git a/crates/ide_assists/src/handlers/ignore_variable.rs b/crates/ide_assists/src/handlers/ignore_variable.rs new file mode 100644 index 0000000000..92ad217189 --- /dev/null +++ b/crates/ide_assists/src/handlers/ignore_variable.rs @@ -0,0 +1,122 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_ide_db::assists::AssistContextDiagnosticCode; +use elp_ide_db::assists::AssistId; +use elp_ide_db::assists::AssistKind; +use elp_syntax::ast; +use elp_syntax::AstNode; + +use crate::assist_context::AssistContext; +use crate::assist_context::Assists; + +// Assist: ignore_variable +// +// Prepend an underscore to a variable +// +// ``` +// meaning_of_life() -> +// Thoughts = thinking(), +// 42. +// ``` +// -> +// ``` +// meaning_of_life() -> +// _Thoughts = thinking(), +// 42. +// ``` +pub(crate) fn ignore_variable(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { + for d in ctx.diagnostics { + if let AssistContextDiagnosticCode::UnusedVariable = d.code { + let var: ast::Var = ctx.find_node_at_custom_offset::(d.range.start())?; + let var_name = var.text(); + let var_range = var.syntax().text_range(); + acc.add( + AssistId("ignore_variable", AssistKind::QuickFix), + format!("Prefix the variable name with an underscore: `_{var_name}`"), + var_range, + None, + |builder| { + builder.edit_file(ctx.frange.file_id); + builder.insert(var.syntax().text_range().start(), "_") + }, + ); + } + } + Some(()) +} + +#[cfg(test)] +mod tests { + use expect_test::expect; + + use super::*; + use crate::tests::*; + + #[test] + fn test_ignore_unused_variable() { + check_assist( + ignore_variable, + "Prefix the variable name with an underscore: `_Thoughts`", + r#" +-module(my_module). +-export([meaning_of_life/0]). +meaning_of_life() -> + Th~oughts = thinking(), + %% ^^^^^^^^^ 💡 L1268: variable 'Thoughts' is unused + 42. +"#, + expect![[r#" + -module(my_module). + -export([meaning_of_life/0]). + meaning_of_life() -> + _Thoughts = thinking(), + 42. + "#]], + ) + } + + #[test] + fn test_ignore_already_unused_variable() { + check_assist_not_applicable( + ignore_variable, + r#" +-module(my_module). +-export([meaning_of_life/0]). +meaning_of_life() -> + _Thou~ghts = thinking(), + 42. +"#, + ); + } + + #[test] + fn test_unknown_diagnostic() { + check_assist( + ignore_variable, + "Prefix the variable name with an underscore: `_Thoughts`", + r#" +-module(my_module). + %% ^^^^^^^^^ 💡 X12345: Module name does not match file name +-export([meaning_of_life/0]). +meaning_of_life() -> + Th~oughts = thinking(), + %% ^^^^^^^^^ 💡 L1268: variable 'Thoughts' is unused + 42. +"#, + expect![[r#" + -module(my_module). + -export([meaning_of_life/0]). + meaning_of_life() -> + _Thoughts = thinking(), + 42. + "#]], + ) + } +} diff --git a/crates/ide_assists/src/handlers/implement_behaviour.rs b/crates/ide_assists/src/handlers/implement_behaviour.rs new file mode 100644 index 0000000000..764defb40a --- /dev/null +++ b/crates/ide_assists/src/handlers/implement_behaviour.rs @@ -0,0 +1,559 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::cmp::max; + +use elp_ide_db::assists::AssistId; +use elp_ide_db::assists::AssistKind; +use elp_syntax::ast::BehaviourAttribute; +use elp_syntax::AstNode; +use hir::Callback; +use hir::CallbackId; +use hir::InFile; +use hir::Module; +use hir::NameArity; +use text_edit::TextRange; +use text_edit::TextSize; + +use crate::assist_context::AssistContext; +use crate::assist_context::Assists; +use crate::helpers; + +// Assist: implement_behaviour +// +// Implement and export all callbacks when on behaviour attribute +// +// ``` +// -behaviour(gen_server). +// +// -> +// ``` +// -behaviour(gen_server). +// +// %% Callbacks for `gen_server` +// -export([init/1, handle_call/3, handle_cast/2]). +// +// init(Args) -> +// erlang:error(not_implemented). +// +// handle_call(Request,From,State) -> +// erlang:error(not_implemented). +// +// handle_cast(Request,State) -> +// erlang:error(not_implemented). +// ``` + +pub(crate) fn implement_behaviour(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { + let behaviour_ast = ctx.find_node_at_offset::()?; + let behaviour = ctx + .sema + .to_def(InFile::new(ctx.file_id(), &behaviour_ast))?; + let our_def_map = ctx.sema.def_map(ctx.file_id()); + let our_forms = ctx.sema.db.file_form_list(ctx.file_id()); + let module_def_map = ctx.sema.def_map(behaviour.file.file_id); + let behaviour_forms = ctx.sema.db.file_form_list(behaviour.file.file_id); + let mut existing_callback = None; + let mut existing_optional_callback = None; + let mut additions = Vec::default(); + let mut optional_additions = Vec::default(); + behaviour_forms + .callback_attributes() + .for_each(|(idx, callback)| { + let implemented = our_forms.functions().find(|(_, f)| f.name == callback.name); + match implemented { + Some((_idx, fun)) => { + if module_def_map.is_callback_optional(&callback.name) { + if existing_optional_callback.is_none() { + existing_optional_callback = Some(callback.name.clone()); + } + if !our_def_map.is_function_exported(&fun.name) { + optional_additions.push((idx, Kind::ExportOnly(&fun.name))); + } + } else { + if existing_callback.is_none() { + existing_callback = Some(callback.name.clone()); + } + if !our_def_map.is_function_exported(&fun.name) { + additions.push((idx, Kind::ExportOnly(&fun.name))); + } + } + } + None => { + if module_def_map.is_callback_optional(&callback.name) { + optional_additions.push((idx, Kind::CallBack(callback))); + } else { + additions.push((idx, Kind::CallBack(callback))); + } + } + }; + }); + + let attr_range = behaviour_ast.syntax().text_range(); + let export_range = our_forms.exports().last().map(|(_idx, export)| { + export + .form_id + .get_ast(ctx.sema.db, ctx.file_id()) + .syntax() + .text_range() + }); + let insert_start = match export_range { + Some(range) => max(range.end(), attr_range.end()), + None => attr_range.end(), + }; + let insert_at = TextSize::from(insert_start + TextSize::from(1)); + + let mut implement_callbacks = + ImplementCallbacks::new(ctx, &behaviour, attr_range, insert_at, acc); + + if additions.len() != 0 { + let id = AssistId("implement_callbacks", AssistKind::QuickFix); + let message = format!( + "Create callbacks for '{}'", + behaviour.name(ctx.sema.db).as_str() + ); + let comment = format!("Callbacks for `{}`", behaviour.name(ctx.db())); + implement_callbacks.add_assist(id, additions, message, comment, existing_callback); + } + + if optional_additions.len() != 0 { + let id = AssistId("implement_optional_callbacks", AssistKind::QuickFix); + let message = format!( + "Create optional callbacks for '{}'", + behaviour.name(ctx.sema.db) + ); + let comment = format!("Optional callbacks for `{}`", behaviour.name(ctx.db())); + implement_callbacks.add_assist( + id, + optional_additions, + message, + comment, + existing_optional_callback, + ); + } + Some(()) +} + +enum Kind<'a> { + CallBack(&'a Callback), + ExportOnly(&'a NameArity), +} + +struct ImplementCallbacks<'a> { + ctx: &'a AssistContext<'a>, + behaviour: &'a Module, + acc: &'a mut Assists, + attr_range: TextRange, + insert_at: TextSize, +} + +impl<'a> ImplementCallbacks<'a> { + fn new( + ctx: &'a AssistContext<'a>, + behaviour: &'a Module, + attr_range: TextRange, + insert_at: TextSize, + acc: &'a mut Assists, + ) -> ImplementCallbacks<'a> { + ImplementCallbacks { + ctx, + behaviour, + attr_range, + insert_at, + acc, + } + } + + fn add_assist( + &mut self, + id: AssistId, + additions: Vec<(CallbackId, Kind)>, + message: String, + comment: String, + existing_callback: Option, + ) { + let (funs, texts) = build_assist(self.ctx, self.behaviour, additions); + self.acc.add(id, message, self.attr_range, None, |builder| { + let mut export_builder = + helpers::ExportBuilder::new(&self.ctx.sema, self.ctx.file_id(), &funs, builder) + .insert_at(self.insert_at) + .with_comment(comment); + if let Some(existing) = existing_callback { + export_builder = export_builder.group_with(existing) + } + export_builder.finish(); + builder.edit_file(self.ctx.frange.file_id); + let mut text = texts.join("\n"); + text.push_str("\n"); + builder.insert(self.insert_at, text) + }); + } +} + +fn build_assist( + ctx: &AssistContext<'_>, + behaviour: &Module, + additions: Vec<(CallbackId, Kind)>, +) -> (Vec, Vec) { + let (funs, texts): (Vec, Vec) = additions + .into_iter() + .filter_map(|(idx, callback)| make_implementation(ctx, &behaviour, idx, callback)) + .fold((vec![], vec![]), |(mut funs, mut msgs), (fun, msg)| { + funs.push(fun); + if let Some(msg) = msg { + msgs.push(msg); + } + (funs, msgs) + }); + (funs, texts) +} + +fn make_implementation( + ctx: &AssistContext<'_>, + behaviour: &Module, + idx: CallbackId, + callback: Kind, +) -> Option<(NameArity, Option)> { + let callback_body = ctx + .sema + .db + .callback_body(InFile::new(behaviour.file.file_id, idx)); + + match callback { + Kind::CallBack(callback) => { + let function_name = callback.name.name(); + if let Some(sig) = callback_body.sigs.iter().next() { + let function_args = + ctx.create_function_args_from_types(&sig.args, &callback_body.body); + let addition = ( + callback.name.clone(), + Some(format!( + "\n{}({}) ->\n erlang:error(not_implemented).", + function_name, function_args + )), + ); + Some(addition) + } else { + None + } + } + Kind::ExportOnly(name) => Some((name.clone(), None)), + } +} + +// --------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use expect_test::expect; + + use super::*; + use crate::tests::*; + + #[test] + fn implement_behaviour_from_scratch() { + check_assist( + implement_behaviour, + "Create callbacks for 'supervisor'", + r#" + //- /src/main.erl + -module(main). + -behaviour(sup~ervisor). + + //- /opt/lib/stdlib-4.31/src/supervisor.erl otp_app:/opt/lib/stdlib-4.31 + -module(supervisor). + -callback init(Args :: term()) -> + {ok, {SupFlags :: sup_flags(), [ChildSpec :: child_spec()]}} + | ignore. + -module(lists). + -export([reverse/1]). + reverse([]) -> []. + "#, + expect![[r#" + -module(main). + -behaviour(supervisor). + + %% Callbacks for `supervisor` + -export([init/1]). + + init(Args) -> + erlang:error(not_implemented). + + "#]], + ) + } + + #[test] + fn implement_behaviour_from_scratch_2() { + check_assist( + implement_behaviour, + "Create callbacks for 'gen_server'", + r#" + //- /src/main.erl + -module(main). + -behaviour(gen_s~erver). + + existing_fun() -> ok. + + //- /opt/lib/stdlib-4.31/src/gen_server.erl otp_app:/opt/lib/stdlib-4.31 + -module(gen_server). + -callback init(Args :: term()) -> + {ok, State :: term()} | {ok, State :: term(), timeout() | hibernate | {continue, term()}} | + {stop, Reason :: term()} | ignore. + -callback handle_call(Request :: term(), From :: from(), + State :: term()) -> + {reply, Reply :: term(), NewState :: term()} | + {reply, Reply :: term(), NewState :: term(), timeout() | hibernate | {continue, term()}} | + {noreply, NewState :: term()} | + {noreply, NewState :: term(), timeout() | hibernate | {continue, term()}} | + {stop, Reason :: term(), Reply :: term(), NewState :: term()} | + {stop, Reason :: term(), NewState :: term()}. + "#, + expect![[r#" + -module(main). + -behaviour(gen_server). + + %% Callbacks for `gen_server` + -export([init/1, handle_call/3]). + + init(Args) -> + erlang:error(not_implemented). + + handle_call(Request,From,State) -> + erlang:error(not_implemented). + + existing_fun() -> ok. + + "#]], + ) + } + + #[test] + fn implement_balance_of_behaviour_callbacks_1() { + check_assist( + implement_behaviour, + "Create callbacks for 'my_behaviour'", + r#" + //- /src/main.erl + -module(main). + -behaviour(my_b~ehaviour). + -export([init/1]). + + init(_) -> already_done,ok. + + //- /src/my_behaviour.erl + -module(my_behaviour). + -callback init(Args :: term()) -> ok. + -callback another() -> ok. + "#, + expect![[r#" + -module(main). + -behaviour(my_behaviour). + -export([init/1, another/0]). + + another() -> + erlang:error(not_implemented). + + init(_) -> already_done,ok. + + "#]], + ) + } + + #[test] + fn implement_balance_of_behaviour_callbacks_2() { + check_assist( + implement_behaviour, + "Create callbacks for 'my_behaviour'", + r#" + //- /src/main.erl + -module(main). + -behaviour(my_b~ehaviour). + + -export([foo/0]). + + %% Callbacks for `my_behaviour` + -export([init/1]). + + init(_) -> already_done,ok. + + foo() -> ok. + + //- /src/my_behaviour.erl + -module(my_behaviour). + -callback init(Args :: term()) -> ok. + -callback another() -> ok. + "#, + expect![[r#" + -module(main). + -behaviour(my_behaviour). + + -export([foo/0]). + + %% Callbacks for `my_behaviour` + -export([init/1, another/0]). + + another() -> + erlang:error(not_implemented). + + init(_) -> already_done,ok. + + foo() -> ok. + + "#]], + ) + } + + #[test] + fn implement_balance_of_behaviour_callbacks_3() { + check_assist( + implement_behaviour, + "Create callbacks for 'my_behaviour'", + r#" + //- /src/main.erl + -module(main). + -export([foo/0]). + + -behaviour(my_b~ehaviour). + + init(_) -> already_done,ok. + + foo() -> ok. + + //- /src/my_behaviour.erl + -module(my_behaviour). + -callback init(Args :: term()) -> ok. + -callback another() -> ok. + "#, + expect![[r#" + -module(main). + -export([foo/0]). + + -behaviour(my_behaviour). + + %% Callbacks for `my_behaviour` + -export([init/1, another/0]). + + another() -> + erlang:error(not_implemented). + + init(_) -> already_done,ok. + + foo() -> ok. + + "#]], + ) + } + + #[test] + fn optional_callbacks_skipped() { + check_assist( + implement_behaviour, + "Create callbacks for 'my_behaviour'", + r#" + //- /src/main.erl + -module(main). + -behaviour(my_b~ehaviour). + + init(_) -> already_done,ok. + + //- /src/my_behaviour.erl + -module(my_behaviour). + -callback init(Args :: term()) -> ok. + -callback another() -> ok. + -callback optional() -> ok. + -optional_callbacks([ + init/1, optional/0 + ]). + "#, + expect![[r#" + -module(main). + -behaviour(my_behaviour). + + %% Callbacks for `my_behaviour` + -export([another/0]). + + another() -> + erlang:error(not_implemented). + + init(_) -> already_done,ok. + + "#]], + ) + } + + #[test] + fn optional_callbacks_assist() { + check_assist( + implement_behaviour, + "Create optional callbacks for 'my_behaviour'", + r#" + //- /src/main.erl + -module(main). + -behaviour(my_b~ehaviour). + + init(_) -> already_done,ok. + + //- /src/my_behaviour.erl + -module(my_behaviour). + -callback init(Args :: term()) -> ok. + -callback another() -> ok. + -callback optional() -> ok. + -optional_callbacks([ + init/1, optional/0 + ]). + "#, + expect![[r#" + -module(main). + -behaviour(my_behaviour). + + %% Optional callbacks for `my_behaviour` + -export([init/1, optional/0]). + + optional() -> + erlang:error(not_implemented). + + init(_) -> already_done,ok. + + "#]], + ) + } + + #[test] + fn existing_fun_missing_export() { + check_assist( + implement_behaviour, + "Create callbacks for 'my_behaviour'", + r#" + //- /src/main.erl + -module(main). + -behaviour(my_b~ehaviour). + + init(_) -> already_done,ok. + + //- /src/my_behaviour.erl + -module(my_behaviour). + -callback init(Args :: term()) -> ok. + -callback another() -> ok. + "#, + expect![[r#" + -module(main). + -behaviour(my_behaviour). + + %% Callbacks for `my_behaviour` + -export([init/1, another/0]). + + another() -> + erlang:error(not_implemented). + + init(_) -> already_done,ok. + + "#]], + ) + } +} diff --git a/crates/ide_assists/src/handlers/inline_function.rs b/crates/ide_assists/src/handlers/inline_function.rs new file mode 100644 index 0000000000..868858a86c --- /dev/null +++ b/crates/ide_assists/src/handlers/inline_function.rs @@ -0,0 +1,2039 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::iter::zip; + +use elp_ide_db::assists::AssistId; +use elp_ide_db::assists::AssistKind; +use elp_ide_db::elp_base_db::FileId; +use elp_ide_db::elp_base_db::FilePosition; +use elp_ide_db::elp_base_db::FileRange; +use elp_ide_db::find_best_token; +use elp_ide_db::rename::SafetyChecks; +use elp_ide_db::SearchScope; +use elp_ide_db::SymbolClass; +use elp_ide_db::SymbolDefinition; +use elp_syntax::ast; +use elp_syntax::ast::edit::IndentLevel; +use elp_syntax::ast::HasArity; +use elp_syntax::AstNode; +use elp_syntax::NodeOrToken; +use elp_syntax::SyntaxKind; +use elp_syntax::SyntaxNode; +use elp_syntax::SyntaxToken; +use elp_syntax::TextRange; +use elp_syntax::TextSize; +use fxhash::FxHashSet; +use hir::Clause; +use hir::FunctionDef; +use hir::InFile; +use hir::InFunctionBody; +use hir::Pat; +use hir::ScopeAnalysis; +use hir::Semantic; +use hir::Var; +use itertools::izip; +use text_edit::TextEdit; + +use crate::assist_context::AssistContext; +use crate::assist_context::Assists; +use crate::helpers::change_indent; +use crate::helpers::parens_needed; +use crate::helpers::ranges_for_delete_function; +use crate::helpers::simple_param_vars; +use crate::helpers::DEFAULT_INDENT_STEP; + +// Assist: inline_function +// +// Replaces the occurrence of a function call with its definition +// +// ``` +// foo(B) -> 3 + B. +// bar() -> foo(4). +// ``` +// -> +// ``` +// bar() -> 3 + 4. +// ``` +pub(crate) fn inline_function(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { + let InlineData { + fun, + delete_definition, + target, + references, + } = can_inline_function(ctx)?; + + let usages = SymbolDefinition::Function(fun.clone()).usages(&ctx.sema); + if !usages.at_least_one() { + return None; + } + + let is_recursive_fn = usages + .clone() + .set_scope(&SearchScope::file_range(FileRange { + file_id: fun.file.file_id, + range: fun.source(ctx.db().upcast()).syntax().text_range(), + })) + .at_least_one(); + if is_recursive_fn { + cov_mark::hit!(inline_function_recursive); + return None; + } + + if !is_safe(ctx, &fun, &references) { + cov_mark::hit!(inline_function_is_safe); + return None; + } + + acc.add( + AssistId("inline_function", AssistKind::RefactorInline), + "Inline function", + target, + None, + move |builder| { + let file_id = ctx.frange.file_id; + let ast_fun = fun.source(ctx.db().upcast()); + if delete_definition { + if let Some(delete_ranges) = ranges_for_delete_function(ctx, &ast_fun) { + delete_ranges.delete(builder); + } + } + for call in references { + let infile_ast_fun = InFile::new(fun.file.file_id, &ast_fun); + let function_body = ctx + .db() + .function_body(InFile::new(fun.file.file_id, fun.function_id)); + if ast_fun.clauses().count() == 1 { + if let Some(ast::FunctionOrMacroClause::FunctionClause(ast_clause)) = + ast_fun.clauses().next() + { + if let Some(clause) = function_body + .clauses + .iter() + .next() + .map(|(_, clause)| fun.in_function_body(ctx.db(), clause.clone())) + { + if let Some((range, replacement)) = inline_single_function_clause( + &ctx.sema, + file_id, + &infile_ast_fun, + &ast_clause, + &call, + &clause, + ) { + builder.replace(range, replacement) + } + } + }; + } else if let Some((range, replacement)) = + inline_function_as_case(&infile_ast_fun, &call) + { + builder.replace(range, replacement) + } + } + }, + ) +} + +/// Function has multiple clauses, convert them to a case statement +fn inline_function_as_case( + fun: &InFile<&ast::FunDecl>, + call: &ast::Call, +) -> Option<(TextRange, String)> { + let mut clauses = Vec::default(); + let end_idx = fun.value.clauses().count() - 1; + let params = params_text(&call.args()?)?; + clauses.push("".to_string()); + clauses.push(format!("case {params} of")); + for (idx, clause) in fun.value.clauses().enumerate() { + match clause { + ast::FunctionOrMacroClause::FunctionClause(clause) => { + // Turn the clause into a case + // foo(Args) -> bar; + // --> + // Args -> bar; + let params = params_text(&clause.args()?)?; + let guards = guards_text(clause.guard()).unwrap_or_default(); + let (body, _offset, body_indent) = clause_body_text_with_intro(&clause)?; + let delta_indent = body_indent.0 as i8; + let body = change_indent(delta_indent, body); + let case_clause = if idx != end_idx { + format!(" {params} {guards}{body};") + } else { + format!(" {params} {guards}{body}") + }; + clauses.push(case_clause); + } + ast::FunctionOrMacroClause::MacroCallExpr(_) => return None, + } + } + clauses.push("end".to_string()); + let old_indent = IndentLevel::from_node(call.syntax()); + let delta_indent = old_indent.0 as i8 + DEFAULT_INDENT_STEP; + let replacement_range = if clauses[0] == "" { + call_replacement_range(call) + } else { + call.syntax().text_range() + }; + Some(( + replacement_range, + change_indent(delta_indent, clauses.join("\n")), + )) +} + +/// When a call is replaced by something starting on a new line, extend +/// the range to exclude any trailing whitespace at the call site. +fn call_replacement_range(call: &ast::Call) -> TextRange { + fn get_start(call: &ast::Call) -> Option { + let call_range = call.syntax().text_range(); + let mut token = call.syntax().first_token()?.prev_token()?; + let mut start_pos = call_range.start(); + while token.kind() == SyntaxKind::WHITESPACE { + start_pos = token.text_range().start(); + token = token.prev_token()?; + } + // Temporary for T148094436 + let _pctx = + stdx::panic_context::enter(format!("\ninline_function::call_replacement_range")); + Some(TextRange::new(start_pos, call.syntax().text_range().end())) + } + + get_start(call).unwrap_or_else(|| call.syntax().text_range()) +} + +/// Inline a function having a single clause. +fn inline_single_function_clause( + sema: &Semantic, + file_id: FileId, + ast_fun: &InFile<&ast::FunDecl>, + ast_clause: &ast::FunctionClause, + call: &ast::Call, + clause: &InFunctionBody, +) -> Option<(TextRange, String)> { + if ast_clause.guard().is_some() { + inline_function_as_case(ast_fun, call) + } else { + if ast_clause.body()?.exprs().count() == 1 + && !has_vars_in_clause(sema, ast_fun.file_id, ast_clause) + { + inline_simple_function_clause(sema, file_id, ast_clause, call) + } else { + inline_single_function_clause_with_begin(ast_clause, call, clause) + } + } +} + +fn inline_single_function_clause_with_begin( + ast_clause: &ast::FunctionClause, + call: &ast::Call, + clause: &InFunctionBody, +) -> Option<(TextRange, String)> { + let (edited_text, _offset) = clause_body_text(ast_clause)?; + + let body_indent = IndentLevel(DEFAULT_INDENT_STEP as u8); + let mut final_text = String::default(); + final_text.push_str("\nbegin"); + + assign_params_for_begin(&mut final_text, body_indent, ast_clause, call, clause)?; + if edited_text.chars().next() != Some('\n') { + final_text.push_str((format!("\n{body_indent}")).as_str()); + final_text.push_str(&edited_text); + } else { + // final text starts on a new line, as a block. Adjust its + // indentation. + + let indent = edited_text + .chars() + .into_iter() + .skip(1) + .take_while(|c| *c == ' ') + .count(); + let delta_indent = body_indent.0 as i8 - indent as i8; + final_text.push_str(&change_indent(delta_indent, edited_text)); + } + final_text.push_str("\nend"); + + let old_indent = IndentLevel::from_node(call.syntax()); + let delta_indent = old_indent.0 as i8 + DEFAULT_INDENT_STEP; + let replacement_range = if final_text.chars().next() == Some('\n') { + call_replacement_range(call) + } else { + call.syntax().text_range() + }; + Some((replacement_range, change_indent(delta_indent, final_text))) +} + +fn assign_params_for_begin( + final_text: &mut String, + body_indent: IndentLevel, + ast_clause: &ast::FunctionClause, + call: &ast::Call, + clause: &InFunctionBody, +) -> Option<()> { + let arity = ast_clause.arity_value()?; + izip!( + call.args()?.args(), + ast_clause.args()?.args(), + &clause.value.pats + ) + .enumerate() + .for_each(|(idx, (val, var, pat))| { + let is_single_var = match &clause[*pat] { + Pat::Var(_) => true, + _ => false, + }; + if idx == 0 { + if let Some(leading_comments) = get_val_preceding_comments(val.syntax()) { + final_text.push_str((format!("\n{body_indent}{leading_comments}")).as_str()); + } + }; + + let var_str = var.syntax().text(); + let (has_comma, val, comments) = + if let Some((has_comma, comments)) = get_val_trailing_comments(val.syntax()) { + (has_comma, val, comments) + } else { + (false, val, "".to_string()) + }; + + if comments != "" || !is_single_var || (var_str.to_string() != val.to_string()) { + final_text.push_str(format!("\n{body_indent}{var_str} = ").as_str()); + + if idx == arity - 1 { + // last one. Will not have a comma, but we insert one before the comment + final_text.push_str(format!("{val},{comments}").as_str()); + } else { + final_text.push_str(format!("{val}{comments}").as_str()); + if !has_comma { + final_text.push_str(","); + } + } + } + }); + Some(()) +} + +fn get_val_trailing_comments(syntax: &SyntaxNode) -> Option<(bool, String)> { + let mut token = syntax.last_token()?.next_token()?; + let mut has_comma = false; + let mut has_comment = false; + let mut res = String::default(); + while token.kind() == SyntaxKind::COMMENT + || token.kind() == SyntaxKind::ANON_COMMA + || token.kind() == SyntaxKind::WHITESPACE + { + res.push_str(&token.text().to_string().clone()); + if token.kind() == SyntaxKind::ANON_COMMA { + has_comma = true; + } + if token.kind() == SyntaxKind::COMMENT { + has_comment = true; + } + token = token.next_token()?; + } + + if has_comment { + Some((has_comma, res.trim_end().to_string())) + } else { + None + } +} + +fn get_val_preceding_comments(syntax: &SyntaxNode) -> Option { + let mut token = syntax.first_token()?.prev_token()?; + let mut res = Vec::default(); + while token.kind() == SyntaxKind::COMMENT + || token.kind() == SyntaxKind::ANON_COMMA + || token.kind() == SyntaxKind::WHITESPACE + { + res.push(token.text().to_string().clone()); + token = token.prev_token()?; + } + res.reverse(); + let res = res.join("").trim().to_string(); + if res.is_empty() { None } else { Some(res) } +} + +fn inline_simple_function_clause( + sema: &Semantic, + file_id: FileId, + clause: &ast::FunctionClause, + call: &ast::Call, +) -> Option<(TextRange, String)> { + // We need to adjust all the edits to skip the start of the file + let (mut edited_text, offset) = clause_body_text(clause)?; + let mut changes = Vec::default(); + zip(call.args()?.args(), clause.args()?.args()).for_each(|(val, var)| { + let var_syntax = var.syntax(); + + let defs = if let Some(v) = ast::Var::cast(var_syntax.clone()) { + let def = sema.to_def::(InFile { file_id, value: &v }); + if let Some(defs) = def { + match defs { + hir::DefinitionOrReference::Definition(def) => { + Some(vec![SymbolDefinition::Var(def)]) + } + hir::DefinitionOrReference::Reference(defs) => Some( + defs.into_iter() + .map(SymbolDefinition::Var) + .collect::>(), + ), + } + } else { + None + } + } else { + None + }; + if let Some(defs) = defs { + let base_name = val.syntax().text().to_string(); + let parened_name = format!("({})", base_name); + for def in defs { + match def.rename( + sema, + &|mvar| { + param_substitution(mvar, base_name.clone(), parened_name.clone(), &val) + .unwrap_or_else(|| base_name.clone()) + }, + SafetyChecks::No, + ) { + Ok(change) => { + changes.push(change); + } + Err(err) => { + log::info!("got rename err: {err}"); + } + } + } + } + }); + // Build the final edit, accounting for the fact that we are only + // using the clause body for the replacement, so we need to apply + // an offset to each edit calculated from editing the whole + // function. + let mut builder = TextEdit::builder(); + changes.iter().for_each(|change| { + change.source_file_edits.values().for_each(|edit| { + if let Some(edit) = apply_offset(edit, offset) { + edit.iter().for_each(|(delete, insert)| { + builder.replace(*delete, insert.clone()); + }); + } + }) + }); + + let edit = builder.finish(); + edit.apply(&mut edited_text); + + let old_indent = IndentLevel::from_node(call.syntax()); + let delta_indent = old_indent.0 as i8 + DEFAULT_INDENT_STEP; + let replacement_range = if edited_text.chars().next() == Some('\n') { + call_replacement_range(call) + } else { + call.syntax().text_range() + }; + Some((replacement_range, change_indent(delta_indent, edited_text))) +} + +fn param_substitution( + mvar: Option<&ast::Name>, + base_name: String, + parened_name: String, + val: &ast::Expr, +) -> Option { + if let Some(ast::Name::Var(var)) = mvar { + let (_, needed) = parens_needed(val, var)?; + if needed { + Some(parened_name) + } else { + Some(base_name) + } + } else { + Some(base_name) + } +} + +fn has_vars_in_clause(sema: &Semantic, file_id: FileId, fun_clause: &ast::FunctionClause) -> bool { + let clause = || -> Option> { + let ast_expr = fun_clause.body()?.exprs().next()?; + let expr = sema.to_expr(InFile::new(file_id, &ast_expr))?; + let clause_id = sema.find_enclosing_function_clause(ast_expr.syntax())?; + let clause = &expr[clause_id]; + Some(expr.with_value(clause.clone())) + }(); + + if let Some(clause) = clause { + if let Some(vars) = ScopeAnalysis::clause_vars_in_scope(sema, &clause.as_ref()) { + !vars.is_empty() + } else { + true + } + } else { + true + } +} + +fn apply_offset(edit: &TextEdit, offset: TextSize) -> Option> { + // Temporary for T148094436 + let _pctx = stdx::panic_context::enter(format!("\ninline_function::apply_offset")); + Some( + edit.iter() + .filter_map(|te| { + Some(( + TextRange::new( + te.delete.start().checked_sub(offset)?, + te.delete.end().checked_sub(offset)?, + ), + te.insert.clone(), + )) + }) + .collect::>(), + ) +} + +/// Given the clause of a single-clause function, return the text in +/// its body. +fn clause_body_text_with_intro( + clause: &ast::FunctionClause, +) -> Option<(String, TextSize, IndentLevel)> { + // Get the text of the clause body, including any comments between + // the `->` and first expression, but excluding the `->` (ANON_DASH_GT). + let mut offset = None; + let token = clause.body()?.syntax().last_token()?.next_token()?; + let trailing_comments = get_trailing_comments(token)?; + let mut body_text = clause + .body()? + .syntax() + .descendants_with_tokens() + .skip(1) // Skip the entire node + .filter_map(|n| match n { + NodeOrToken::Token(t) => { + if offset.is_none() { + offset = Some(t.text_range().start()) + } + Some(t.text().to_string()) + } + NodeOrToken::Node(_) => None, + }) + .collect::>() + .join(""); + body_text.push_str(&trailing_comments); + + let indent = clause + .body()? + .syntax() + .descendants_with_tokens() + .skip(1) // Skip the entire node + .map(|n| n) + .skip_while(|n| match n { + NodeOrToken::Token(t) => t.kind() != SyntaxKind::WHITESPACE, + NodeOrToken::Node(_) => true, + }) + .skip(1) // skip the whitespace + .find_map(|n| match n { + NodeOrToken::Token(t) => Some(IndentLevel::from_token(&t)), + NodeOrToken::Node(_) => None, + }); + Some((body_text, offset?, indent?)) +} + +fn clause_body_text(clause: &ast::FunctionClause) -> Option<(String, TextSize)> { + // Get the text of the clause body, including any comments between + // the `->` and first expression, but excluding the `->` (ANON_DASH_GT). + let mut offset = None; + let token = clause.body()?.syntax().last_token()?.next_token()?; + let trailing_comments = get_trailing_comments(token)?; + let mut body_text = clause + .body()? + .syntax() + .descendants_with_tokens() + .skip(2) // Skip the entire node, and initial ANON_DASH_GT token + .skip_while(|n| match n { + NodeOrToken::Token(t) => { + if t.kind() == SyntaxKind::WHITESPACE { + !t.text().contains("\n") + } else { + false + } + } + NodeOrToken::Node(_) => true, + }) + .filter_map(|n| match n { + NodeOrToken::Token(t) => { + if offset.is_none() { + offset = Some(t.text_range().start()) + } + Some(t.text().to_string()) + } + NodeOrToken::Node(_) => None, + }) + .collect::>() + .join(""); + body_text.push_str(&trailing_comments); + + Some((body_text, offset?)) +} + +fn get_trailing_comments(mut token: SyntaxToken) -> Option { + let mut res = String::default(); + while token.kind() != SyntaxKind::ANON_DOT && token.kind() != SyntaxKind::ANON_SEMI { + res.push_str(&token.text().to_string().clone()); + token = token.next_token()?; + } + Some(res) +} + +fn params_text(args: &ast::ExprArgs) -> Option { + // As per grammar.js, ast::ExprArgs is always wrapped in parens + let mut args_str = args + .syntax() + .text() + .to_string() + .strip_prefix("(")? + .strip_suffix(")")? + .trim() + .to_string(); + + // If we have a trailing comment, add a newline. + // First, get last token, excluding trailing ')' + let mut token = args.syntax().last_token()?.prev_token()?; + while token.kind() == SyntaxKind::WHITESPACE { + token = token.prev_token()?; + } + if token.kind() == SyntaxKind::COMMENT { + args_str.push_str("\n"); + } + + if args.args().count() == 1 { + Some(args_str) + } else { + Some(format!( + "{{{}}}", // open and closing braces, around params + args_str + )) + } +} + +fn guards_text(guard: Option) -> Option { + let guard = guard?; + + // Step backwards to pick up the 'when' keyword, and any intervening whitespace/comments + let mut token = guard.syntax().first_token()?.prev_token()?; + let mut when = Vec::default(); + while token.kind() != SyntaxKind::ANON_WHEN { + when.push(token.text().to_string().clone()); + token = token.prev_token()?; + } + when.push(token.text().to_string().clone()); // Include 'when' token + when.reverse(); + + let guards = guard.syntax().text().to_string(); + when.push(guards); + + let mut trail = String::default(); + let mut token = guard.syntax().last_token()?.next_token()?; + while token.kind() != SyntaxKind::ANON_DASH_GT { + trail.push_str(&token.text().to_string().clone()); + token = token.next_token()?; + } + if trail.contains("\n") { + trail.push_str(" "); + } + when.push(trail); + Some(when.join("")) +} + +struct InlineData { + fun: FunctionDef, + delete_definition: bool, + target: TextRange, + references: Vec, +} + +fn can_inline_function(ctx: &AssistContext) -> Option { + let file_id = ctx.frange.file_id; + let token = find_best_token( + &ctx.sema, + FilePosition { + file_id, + offset: ctx.offset(), + }, + )?; + let target = SymbolClass::classify(&ctx.sema, token)?; + let def = match target { + SymbolClass::Definition(SymbolDefinition::Function(fun)) => { + Some(SymbolDefinition::Function(fun)) + } + SymbolClass::Reference { refs, typ: _ } => { + if let Some(SymbolDefinition::Function(fun)) = refs.into_iter().next() { + Some(SymbolDefinition::Function(fun)) + } else { + None + } + } + _ => None, + }?; + + let is_local_function = def.file().file_id == file_id; + + match &def { + SymbolDefinition::Function(fun) => { + let target = fun.source(ctx.db().upcast()).syntax().text_range(); + let usages = def.clone().usages(&ctx.sema).direct_only().all(); + let mut all_references = Vec::default(); + let mut selected_call_reference = Vec::default(); + usages.iter().for_each(|(_file_id, names)| { + for name_like in names { + if let Some(call) = get_call(name_like.syntax()) { + if let Some(name) = call.expr() { + if name.syntax().text_range().contains(ctx.offset()) { + selected_call_reference.push(call.clone()); + } + } + all_references.push(call.clone()); + }; + } + }); + let (references, delete_definition) = if selected_call_reference.is_empty() { + // Cursor must be on a spec or function definition, + // delete all references + (all_references, !fun.exported) + } else { + ( + selected_call_reference, + all_references.len() == 1 && !fun.exported, + ) + }; + if references.is_empty() { + None + } else { + Some(InlineData { + fun: fun.clone(), + delete_definition: is_local_function && delete_definition, + target, + references, + }) + } + } + _ => None, + } +} + +// AZ:TODO: use hir::Call instead +fn get_call(syntax: &SyntaxNode) -> Option { + if let Some(call) = ast::Call::cast(syntax.parent()?) { + Some(call) + } else { + ast::Call::cast(syntax.parent()?.parent()?) + } +} + +/// Check that all variables defined in the clauses of the `FunDecl` +/// are unused in the call location. This excludes single-variable +/// parameters. +fn is_safe(ctx: &AssistContext, fun: &FunctionDef, references: &[ast::Call]) -> bool { + fn check_is_safe( + ctx: &AssistContext, + fun: &FunctionDef, + references: &[ast::Call], + ) -> Option { + let function_body = ctx + .db() + .function_body(InFile::new(fun.file.file_id, fun.function_id)); + let fun_vars = function_body + .clauses + .iter() + .filter_map(|(_, clause)| { + ScopeAnalysis::clause_vars_in_scope( + &ctx.sema, + &fun.in_function_body(ctx.db(), clause), + ) + }) + .fold(FxHashSet::default(), move |mut acc, new: FxHashSet| { + acc.extend(new.into_iter()); + acc + }); + let simple_param_vars = function_body + .clauses + .iter() + .filter_map(|(_, clause)| simple_param_vars(&fun.in_function_body(ctx.db(), clause))) + .fold(FxHashSet::default(), move |mut acc, new: FxHashSet| { + acc.extend(new.into_iter()); + acc + }); + let fun_vars: FxHashSet = fun_vars + .difference(&simple_param_vars) + .map(|x| x.clone()) + .collect(); + + // At each call site, check that no param clashes with the fun_vars. + let file_id = ctx.file_id(); + let ok = references.iter().all(|call| { + // clause_vars is all the top-level vars in the function + // clause containing the call, before and after it, at the + // top level. + + let expr = ast::Expr::Call(call.clone()); + let clause_vars = || -> Option> { + let clause_id = ctx.sema.find_enclosing_function_clause(call.syntax())?; + let call_function = ctx.sema.to_expr(InFile::new(ctx.file_id(), &expr))?; + let clause = &call_function[clause_id]; + let clause_vars = ScopeAnalysis::clause_vars_in_scope( + &ctx.sema, + &call_function.with_value(&clause), + ); + clause_vars + }() + .unwrap_or_default(); + + // We also need to check in the actual scope we are + // currently in. e.g. a catch clause, where vars are not exported + if let Some(mut vars) = call_vars_in_scope(&ctx.sema, file_id, call) { + vars.extend(clause_vars.iter()); + vars.intersection(&fun_vars).count() == 0 + } else { + false + } + }); + Some(ok) + } + check_is_safe(ctx, fun, references).unwrap_or(false) +} + +fn call_vars_in_scope( + sema: &Semantic, + file_id: FileId, + call: &ast::Call, +) -> Option> { + let resolver = sema.function_clause_resolver(file_id, call.syntax())?; + let call_expr_id = resolver.expr_id_ast( + sema.db, + InFile::new(file_id, &ast::Expr::Call(call.clone())), + )?; + let scope = resolver.value.scopes.scope_for_expr(call_expr_id)?; + let vars = resolver.value.all_vars_in_scope(scope); + Some(vars) +} + +// --------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use expect_test::expect; + + use super::*; + use crate::tests::*; + + #[test] + fn test_inline_function_no_params_from_usage_1() { + check_assist( + inline_function, + "Inline function", + r#" + foo() -> ok. + bar() -> f~oo()."#, + expect![[r#" + bar() -> ok."#]], + ) + } + + #[test] + fn test_inline_function_no_params_from_definition_1() { + check_assist( + inline_function, + "Inline function", + r#" + fo~o() -> ok. + bar() -> foo()."#, + expect![[r#" + bar() -> ok."#]], + ) + } + + #[test] + fn test_inline_function_no_params_multiple_usage_1() { + check_assist( + inline_function, + "Inline function", + r#" + fo~o() -> ok. + baz(X) -> + Y = foo(), + X. + bar() -> foo()."#, + expect![[r#" + baz(X) -> + Y = ok, + X. + bar() -> ok."#]], + ) + } + + #[test] + fn test_inline_function_with_params_1() { + check_assist( + inline_function, + "Inline function", + r#" + foo(B) -> 3 + B. + bar() -> f~oo(4)."#, + expect![[r#" + bar() -> + begin + B = 4, + 3 + B + end."#]], + ) + } + + #[test] + fn test_inline_function_with_params_2() { + check_assist( + inline_function, + "Inline function", + r#" + foo(A,B) -> A + B. + bar() -> f~oo(35,4)."#, + expect![[r#" + bar() -> + begin + A = 35, + B = 4, + A + B + end."#]], + ) + } + + #[test] + fn test_inline_function_begin_1() { + check_assist( + inline_function, + "Inline function", + r#" + foo(A,B) -> A * B. + bar() -> f~oo(23 - 2,4)."#, + expect![[r#" + bar() -> + begin + A = 23 - 2, + B = 4, + A * B + end."#]], + ) + } + + #[test] + fn test_inline_function_begin_2() { + check_assist( + inline_function, + "Inline function", + r#" + foo(A,B) -> + X = 1, + A * B + X. + bar() -> f~oo(23 - 2,4)."#, + expect![[r#" + bar() -> + begin + A = 23 - 2, + B = 4, + X = 1, + A * B + X + end."#]], + ) + } + + #[test] + fn test_inline_function_constant_parameter_1() { + check_assist( + inline_function, + "Inline function", + r#" + foo(A,0) -> A+3. + bar() -> f~oo(4,0)."#, + expect![[r#" + bar() -> + begin + A = 4, + 0 = 0, + A+3 + end."#]], + ) + } + + #[test] + fn test_inline_function_multiple_clauses_1() { + check_assist( + inline_function, + "Inline function", + r#" + foo(0) -> 5; + foo(AA) -> AA * 3. + bar() -> f~oo(4)."#, + expect![[r#" + bar() -> + case 4 of + 0 -> 5; + AA -> AA * 3 + end."#]], + ) + } + + #[test] + fn test_inline_function_delete_original_1() { + check_assist( + inline_function, + "Inline function", + r#" + ok() -> ok. + + foo(0) -> 5; + foo(AA) -> AA * 3. + bar() -> f~oo(4)."#, + expect![[r#" + ok() -> ok. + + bar() -> + case 4 of + 0 -> 5; + AA -> AA * 3 + end."#]], + ) + } + + #[test] + fn test_inline_function_delete_original_2() { + check_assist( + inline_function, + "Inline function", + r#" + ok() -> ok. + + foo(AA) -> AA * 3. + bar() -> f~oo(4)."#, + expect![[r#" + ok() -> ok. + + bar() -> + begin + AA = 4, + AA * 3 + end."#]], + ) + } + + #[test] + fn test_inline_function_indentation_1() { + check_assist( + inline_function, + "Inline function", + r#" + baz(0) -> + Y = 3, + Y = 2; + baz(Z) -> + begin + Y = 4 + end, + Y + Z. + + foo() -> + XX = 3, + YY = ba~z(4), + YY."#, + expect![[r#" + foo() -> + XX = 3, + YY = + case 4 of + 0 -> + Y = 3, + Y = 2; + Z -> + begin + Y = 4 + end, + Y + Z + end, + YY."#]], + ) + } + + #[test] + fn test_inline_function_indentation_2() { + check_assist( + inline_function, + "Inline function", + r#" + baz(Z) -> + begin + Y = 4 + end, + Y + Z. + + foo() -> + XX = 3, + YY = ba~z(4), + YY."#, + expect![[r#" + foo() -> + XX = 3, + YY = + begin + Z = 4, + begin + Y = 4 + end, + Y + Z + end, + YY."#]], + ) + } + + #[test] + fn test_inline_function_indentation_3() { + check_assist( + inline_function, + "Inline function", + r#" + baz(Z) + -> begin + Y = 4 + end, + Y + Z. + + foo() -> + XX = 3, + YY = ba~z(4), + YY."#, + expect![[r#" + foo() -> + XX = 3, + YY = + begin + Z = 4, + begin + Y = 4 + end, + Y + Z + end, + YY."#]], + ) + } + + #[test] + fn test_inline_function_with_spec_1() { + check_assist( + inline_function, + "Inline function", + r#" + -spec baz() -> ok. + baz() -> ok. + + foo() -> + YY = ba~z()."#, + expect![[r#" + foo() -> + YY = ok."#]], + ) + } + + #[test] + fn test_inline_function_with_spec_2() { + check_assist( + inline_function, + "Inline function", + r#" + -spec b~az() -> ok. + baz() -> ok. + + foo() -> + YY = baz()."#, + expect![[r#" + foo() -> + YY = ok."#]], + ) + } + + #[test] + fn test_inline_function_with_comments_1() { + check_assist( + inline_function, + "Inline function", + r#" + foo() -> + YY = b~az(). + + baz() -> + %% a comment + ok."#, + expect![[r#" + foo() -> + YY = + %% a comment + ok. + + "#]], + ) + } + + #[test] + fn test_inline_function_with_comments_2() { + check_assist( + inline_function, + "Inline function", + r#" + foo(X) -> + YY = b~az(X). + + baz(Z) -> + %% a comment + 1 + Z."#, + expect![[r#" + foo(X) -> + YY = + begin + Z = X, + %% a comment + 1 + Z + end. + + "#]], + ) + } + + #[test] + fn test_inline_function_with_comments_3() { + check_assist( + inline_function, + "Inline function", + r#" + foo(X) -> + YY = b~az(X). + + baz(Z) -> %% a comment + 1 + Z."#, + expect![[r#" + foo(X) -> + YY = + begin + Z = X, + %% a comment + 1 + Z + end. + + "#]], + ) + } + + #[test] + fn test_inline_function_with_comments_4() { + check_assist( + inline_function, + "Inline function", + r#" + foo(X) -> + YY = b~az(X), + YY. + + baz(Z) -> %% a comment + 1 + Z."#, + expect![[r#" + foo(X) -> + YY = + begin + Z = X, + %% a comment + 1 + Z + end, + YY. + + "#]], + ) + } + + #[test] + fn test_inline_function_with_trailing_comments_1() { + check_assist( + inline_function, + "Inline function", + r#" + foo(X) -> + YY = b~az(X). + + baz(Z) -> + 1 + Z + % a comment + ."#, + expect![[r#" + foo(X) -> + YY = + begin + Z = X, + 1 + Z + % a comment + + end. + + "#]], + ) + } + + #[test] + fn test_inline_function_with_trailing_comments_2() { + check_assist( + inline_function, + "Inline function", + r#" + foo(X) -> + YY = b~az(X). + + baz(Z) -> + Y = 1, + Y + Z + % a comment + ."#, + expect![[r#" + foo(X) -> + YY = + begin + Z = X, + Y = 1, + Y + Z + % a comment + + end. + + "#]], + ) + } + + #[test] + fn test_inline_function_with_case_substitution_1() { + check_assist( + inline_function, + "Inline function", + r#" + foo(X) -> + YY = b~az(X). + + baz(Z) -> + Y = 1, + Y + Z."#, + expect![[r#" + foo(X) -> + YY = + begin + Z = X, + Y = 1, + Y + Z + end. + + "#]], + ) + } + + #[test] + fn test_inline_function_inline_all_from_function_def_1() { + check_assist( + inline_function, + "Inline function", + r#" + foo(X) -> baz(X). + bar(Y) -> 1 + baz(Y). + + b~az(Z) -> 1 + Z."#, + expect![[r#" + foo(X) -> + begin + Z = X, + 1 + Z + end. + bar(Y) -> 1 + + begin + Z = Y, + 1 + Z + end. + + "#]], + ) + } + + #[test] + fn test_inline_function_inline_one_from_function_usage_1() { + check_assist( + inline_function, + "Inline function", + r#" + foo(X) -> baz(X). + bar(Y) -> 1 + b~az(Y). + + baz(Z) -> 1 + Z."#, + expect![[r#" + foo(X) -> baz(X). + bar(Y) -> 1 + + begin + Z = Y, + 1 + Z + end. + + baz(Z) -> 1 + Z."#]], + ) + } + + #[test] + fn test_inline_function_inline_one_from_function_usage_2() { + check_assist( + inline_function, + "Inline function", + r#" + bar(Y) -> 1 + b~az(Y). + + baz(Z) -> 1 + Z."#, + expect![[r#" + bar(Y) -> 1 + + begin + Z = Y, + 1 + Z + end. + + "#]], + ) + } + + #[test] + fn test_inline_function_inline_do_not_delete_exported_1() { + check_assist( + inline_function, + "Inline function", + r#" + -export([baz/1]). + bar(Y) -> 1 + b~az(Y). + + baz(Z) -> 1 + Z."#, + expect![[r#" + -export([baz/1]). + bar(Y) -> 1 + + begin + Z = Y, + 1 + Z + end. + + baz(Z) -> 1 + Z."#]], + ) + } + + #[test] + fn test_inline_function_inline_do_not_delete_exported_2() { + check_assist( + inline_function, + "Inline function", + r#" + -export([baz/1]). + foo(X) -> baz(X). + bar(Y) -> 1 + baz(Y). + + b~az(Z) -> 1 + Z."#, + expect![[r#" + -export([baz/1]). + foo(X) -> + begin + Z = X, + 1 + Z + end. + bar(Y) -> 1 + + begin + Z = Y, + 1 + Z + end. + + baz(Z) -> 1 + Z."#]], + ) + } + + #[test] + fn test_inline_function_inline_do_not_inline_external_1() { + check_assist( + inline_function, + "Inline function", + r#" + //- /src/main.erl + -import(another, [baz/1]). + bar(Y) -> 1 + b~az(Y). + + //- /src/another.erl + -export([baz/1]). + baz(Z) -> 1 + Z."#, + expect![[r#" + -import(another, [baz/1]). + bar(Y) -> 1 + + begin + Z = Y, + 1 + Z + end. + + "#]], + ) + } + + #[test] + fn test_inline_function_inline_do_not_inline_external_2() { + check_assist( + inline_function, + "Inline function", + r#" + //- /src/main.erl + bar(Y) -> 1 + another:b~az(Y). + + //- /src/another.erl + -export([baz/1]). + baz(Z) -> 1 + Z."#, + expect![[r#" + bar(Y) -> 1 + + begin + Z = Y, + 1 + Z + end. + + "#]], + ) + } + + #[test] + fn test_inline_function_guards_1() { + check_assist( + inline_function, + "Inline function", + r#" + foo(X) -> b~az(X). + + baz(Z) when Z =:= 42 -> 42; + baz(Z) -> 1 + Z. + "#, + expect![[r#" + foo(X) -> + case X of + Z when Z =:= 42 -> 42; + Z -> 1 + Z + end. + + "#]], + ) + } + + #[test] + fn test_inline_function_guards_2() { + check_assist( + inline_function, + "Inline function", + r#" + foo(X) -> b~az(X). + + baz(Z) when Z =:= 42 -> 42. + "#, + expect![[r#" + foo(X) -> + case X of + Z when Z =:= 42 -> 42 + end. + + "#]], + ) + } + + #[test] + fn test_inline_function_guards_3() { + check_assist( + inline_function, + "Inline function", + r#" + foo(X) -> b~az(X). + + baz(Z) when Z =:= 42;Z =:= 0 -> 42. + "#, + expect![[r#" + foo(X) -> + case X of + Z when Z =:= 42;Z =:= 0 -> 42 + end. + + "#]], + ) + } + + #[test] + fn test_inline_function_guards_4() { + check_assist( + inline_function, + "Inline function", + r#" + foo(X) -> b~az(X). + + baz(Z) when A = Z,A =:= 42;Z =:= 0 -> 42. + "#, + expect![[r#" + foo(X) -> + case X of + Z when A = Z,A =:= 42;Z =:= 0 -> 42 + end. + + "#]], + ) + } + + #[test] + fn test_inline_function_guards_5() { + check_assist( + inline_function, + "Inline function", + r#" + foo(X) -> b~az(X). + + baz(Z) when + % this one + Z =:= 42; + % that one + Z =:= 0 + -> begin + 42 + end. + "#, + expect![[r#" + foo(X) -> + case X of + Z when + % this one + Z =:= 42; + % that one + Z =:= 0 + -> begin + 42 + end + end. + + "#]], + ) + } + + #[test] + fn test_inline_function_multiple_params_case_1() { + check_assist( + inline_function, + "Inline function", + r#" + foo(X) -> b~az(X, 5). + + baz(0, A) -> A + 3; + baz(Z, A) -> Z + A. + "#, + expect![[r#" + foo(X) -> + case {X, 5} of + {0, A} -> A + 3; + {Z, A} -> Z + A + end. + + "#]], + ) + } + + #[test] + fn test_inline_function_multiple_params_case_2() { + check_assist( + inline_function, + "Inline function", + r#" + foo(X) -> b~az(X,5). + + baz(Z,A) -> Z + A. + "#, + expect![[r#" + foo(X) -> + begin + Z = X, + A = 5, + Z + A + end. + + "#]], + ) + } + + #[test] + fn test_inline_function_complex_params_1() { + check_assist( + inline_function, + "Inline function", + r#" + foo(X) -> b~az(remote:with_side_effects(X + 1)). + + baz(Z) -> bar(Z) * Z. + "#, + expect![[r#" + foo(X) -> + begin + Z = remote:with_side_effects(X + 1), + bar(Z) * Z + end. + + "#]], + ) + } + + #[test] + fn test_inline_function_complex_params_2() { + check_assist( + inline_function, + "Inline function", + r#" + foo(X) -> b~az(X). + + baz(Z) -> bar(Z) * Z. + "#, + expect![[r#" + foo(X) -> + begin + Z = X, + bar(Z) * Z + end. + + "#]], + ) + } + + #[test] + fn test_inline_function_complex_params_3() { + check_assist( + inline_function, + "Inline function", + r#" + foo(X) -> b~az(X,with_side_effects(X + 1)). + + baz(Y,Z) -> bar(Z) * Z + Y. + "#, + expect![[r#" + foo(X) -> + begin + Y = X, + Z = with_side_effects(X + 1), + bar(Z) * Z + Y + end. + + "#]], + ) + } + + #[test] + fn test_inline_function_check_variable_name_clash_1() { + cov_mark::check!(inline_function_is_safe); + check_assist_not_applicable( + inline_function, + r#" + foo(X) -> + X = 1, + b~az(3, X). + + baz(A,B) -> + X = 1, + Y = 2, + (A + 1) * (B + Y). + "#, + ) + } + + #[test] + fn test_inline_function_check_variable_name_clash_2() { + cov_mark::check!(inline_function_is_safe); + check_assist_not_applicable( + inline_function, + r#" + foo(X) -> + Z = 1, + b~az(3, Z). + + baz(A,B) -> + X = 1, + Y = 2, + (A + 1) * (B + Y). + "#, + ) + } + + #[test] + fn test_inline_function_recursion_1() { + cov_mark::check!(inline_function_recursive); + check_assist_not_applicable( + inline_function, + r#" + foo(A) -> + b~az(A). + + baz(X) -> + case X of + 0 -> 0; + _ -> X + baz(X - 1) + end. + "#, + ) + } + + #[test] + fn test_inline_function_param_comments_1() { + check_assist( + inline_function, + "Inline function", + r#" + foo(A) -> + b~az(A, %% Use the parameter + 3). + + baz(X,Y) -> {X,Y}. + "#, + expect![[r#" + foo(A) -> + begin + X = A, %% Use the parameter + Y = 3, + {X,Y} + end. + + "#]], + ) + } + + #[test] + fn test_inline_function_param_comments_2() { + check_assist( + inline_function, + "Inline function", + r#" + foo(A) -> + b~az(A, + 3 %% Use the parameter + ). + + baz(X,Y) -> {X,Y}. + "#, + expect![[r#" + foo(A) -> + begin + X = A, + Y = 3, %% Use the parameter + {X,Y} + end. + + "#]], + ) + } + + #[test] + fn test_inline_function_param_comments_3() { + check_assist( + inline_function, + "Inline function", + r#" + foo(A) -> + b~az( + %% A is special + A, + 3 + ). + + baz(X,Y) -> {X,Y}. + "#, + expect![[r#" + foo(A) -> + begin + %% A is special + X = A, + Y = 3, + {X,Y} + end. + + "#]], + ) + } + + #[test] + fn test_inline_function_as_case_param_comments_1() { + check_assist( + inline_function, + "Inline function", + r#" + foo(X) -> + b~az(X, %% X is special + 5). + + baz(0, A) -> A + 3; + baz(Z, A) -> Z + A. + "#, + expect![[r#" + foo(X) -> + case {X, %% X is special + 5} of + {0, A} -> A + 3; + {Z, A} -> Z + A + end. + + "#]], + ) + } + + #[test] + fn test_inline_function_as_case_param_comments_2() { + check_assist( + inline_function, + "Inline function", + r#" + foo(0) -> 5; + foo(AA) -> AA * 3. + bar() -> f~oo( + %% Leading comment + 4 %% Trailing + )."#, + expect![[r#" + bar() -> + case %% Leading comment + 4 %% Trailing + of + 0 -> 5; + AA -> AA * 3 + end."#]], + ) + } + + #[test] + fn test_inline_function_as_case_function_expr() { + check_assist( + inline_function, + "Inline function", + r#" + foo(0) -> 5; + foo(AA) -> AA * 3. + bar() -> f~oo(baz(4))."#, + expect![[r#" + bar() -> + case baz(4) of + 0 -> 5; + AA -> AA * 3 + end."#]], + ) + } + + #[test] + fn test_inline_function_param_name_shadowing_1() { + check_assist( + inline_function, + "Inline function", + r#" + foo(X) -> X + 5. + bar(X) -> f~oo(X)."#, + expect![[r#" + bar(X) -> + begin + X + 5 + end."#]], + ) + } + + #[test] + fn test_inline_function_param_name_shadowing_2() { + check_assist( + inline_function, + "Inline function", + r#" + foo(X,Y) -> X + Y. + bar(X) -> f~oo(X,3)."#, + expect![[r#" + bar(X) -> + begin + Y = 3, + X + Y + end."#]], + ) + } + + #[test] + fn test_inline_function_param_name_shadowing_3() { + check_assist( + inline_function, + "Inline function", + r#" + foo(X,Y) -> X + Y. + bar(X) -> f~oo(X, + 3 % a comment + )."#, + expect![[r#" + bar(X) -> + begin + Y = 3, % a comment + X + Y + end."#]], + ) + } + + #[test] + fn test_inline_function_param_name_shadowing_4() { + check_assist( + inline_function, + "Inline function", + r#" + foo(X,Y) -> X + Y. + bar(X) -> f~oo(X, % another comment + 3 % a comment + )."#, + expect![[r#" + bar(X) -> + begin + X = X, % another comment + Y = 3, % a comment + X + Y + end."#]], + ) + } + + #[test] + fn test_inline_function_param_name_shadowing_case_1() { + check_assist( + inline_function, + "Inline function", + r#" + foo(0) -> 3; + foo(X) -> X + 5. + bar(X) -> f~oo(X)."#, + expect![[r#" + bar(X) -> + case X of + 0 -> 3; + X -> X + 5 + end."#]], + ) + } + + #[test] + fn test_inline_inner_function() { + // T153086784 + check_assist( + inline_function, + "Inline function", + r#" + simple_map(Fields) -> {map, Fields, #{}, []}. + struct(_Config) -> + Decode = simple_map(#{1 => {field, {struct, simp~le_map(#{1 => {nested, byte}})}}})."#, + expect![[r#" + simple_map(Fields) -> {map, Fields, #{}, []}. + struct(_Config) -> + Decode = simple_map(#{1 => {field, {struct, + begin + Fields = #{1 => {nested, byte}}, + {map, Fields, #{}, []} + end}}})."#]], + ) + } +} diff --git a/crates/ide_assists/src/handlers/inline_local_variable.rs b/crates/ide_assists/src/handlers/inline_local_variable.rs new file mode 100644 index 0000000000..70651c6525 --- /dev/null +++ b/crates/ide_assists/src/handlers/inline_local_variable.rs @@ -0,0 +1,612 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_ide_db::assists::AssistId; +use elp_ide_db::assists::AssistKind; +use elp_syntax::ast; +use elp_syntax::ast::AstNode; +use elp_syntax::TextRange; +use hir::db::MinDefDatabase; +use hir::InFile; +use hir::Semantic; + +use crate::assist_context::AssistContext; +use crate::assist_context::Assists; +use crate::helpers; +use crate::helpers::skip_trailing_separator; +use crate::helpers::skip_ws; + +// Assist: inline_local_variable +// +// Replaces the occurrence of a variable with its definition +// +// ``` +// A = 3 + B, +// foo(A). +// ``` +// -> +// ``` +// foo(3 + B). +// ``` +pub(crate) fn inline_local_variable(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { + let InlineData { + lhs_var, + match_expr, + delete_definition, + target, + references, + } = if let Some(data) = ctx + .find_node_at_offset::() + .and_then(|match_expr| inline_variable_definition(ctx, match_expr)) + { + Some(data) + } else if let Some(data) = ctx + .find_node_at_offset::() + .and_then(|var| inline_usage(ctx, var)) + { + Some(data) + } else { + None + }?; + + if !is_safe(ctx, lhs_var, &match_expr, &references) { + return None; + } + + let rhs = match_expr.rhs()?; + + acc.add( + AssistId("inline_local_variable", AssistKind::RefactorInline), + "Inline variable", + target, + None, + move |builder| { + let delete_range = delete_definition.then(|| { + let orig_range = match_expr.syntax().text_range(); + let start = match skip_ws(match_expr.syntax().prev_sibling_or_token()) { + Some(start) => start.start(), + None => orig_range.start(), + }; + let end = match skip_trailing_separator(match_expr.syntax()) { + Some(end) => end.end(), + None => orig_range.end(), + }; + // Temporary for T148094436 + let _pctx = stdx::panic_context::enter(format!("\ninline_local_variable")); + TextRange::new(start, end) + }); + + let init_str = rhs.syntax().text().to_string(); + let init_in_paren = format!("({})", &init_str); + if let Some(range) = delete_range { + builder.delete(range); + } + references + .into_iter() + .filter_map(|v| helpers::parens_needed(&rhs, &v)) + .for_each(|(range, should_wrap)| { + let replacement = if should_wrap { + &init_in_paren + } else { + &init_str + }; + builder.replace(range, replacement.clone()) + }) + }, + ) +} + +struct InlineData { + lhs_var: ast::Var, + match_expr: ast::MatchExpr, + delete_definition: bool, + target: TextRange, + references: Vec, +} + +fn inline_variable_definition( + ctx: &AssistContext, + match_expr: ast::MatchExpr, +) -> Option { + let lhs_var = match match_expr.lhs()? { + ast::Expr::ExprMax(ast::ExprMax::Var(v)) => v, + _ => return None, + }; + + // The finding of the candidate is noisy, may have some unrelated + // ancestor, check that the ranges are sane. + if !lhs_var + .syntax() + .text_range() + .contains_range(ctx.selection_trimmed()) + { + return None; + } + + let db = ctx.db(); + let references = find_local_usages(&ctx.sema, db, InFile::new(ctx.file_id(), &lhs_var))?; + + let target = lhs_var.syntax().text_range(); + Some(InlineData { + lhs_var, + match_expr, + delete_definition: true, + target, + references, + }) +} + +/// Inline the single reference indicated. Only remove the variable +/// definition if there are no other usages. +fn inline_usage(ctx: &AssistContext, var: ast::Var) -> Option { + // The finding of the candidate is noisy, may have some unrelated + // ancestor, check that the ranges are sane. + if !var + .syntax() + .text_range() + .contains_range(ctx.selection_trimmed()) + { + return None; + } + let db = ctx.db(); + let var_defs = ctx + .sema + .to_def(InFile::new(ctx.file_id(), &var))? + .to_reference()?; + let lhs_var = if var_defs.len() == 1 { + var_defs[0].source(db.upcast()) + } else { + return None; + }; + let match_expr = ast::MatchExpr::cast(lhs_var.syntax().parent()?)?; + + // Are trying to inline ourselves? + if lhs_var.syntax().text_range() == var.syntax().text_range() { + return None; + } + + let mut references = find_local_usages(&ctx.sema, db, InFile::new(ctx.file_id(), &lhs_var))?; + let delete_definition = references.len() == 1; + references.retain(|fref| fref.syntax().text_range() == var.syntax().text_range()); + + let target = lhs_var.syntax().text_range(); + Some(InlineData { + lhs_var, + match_expr, + delete_definition, + target, + references, + }) +} + +/// Find all other variables within the function clause that resolve +/// to the one given, but only if there is a single +/// resolution. Variables having multiple binding sites, as arising +/// from case clauses, cannot be inlined. +fn find_local_usages<'a>( + sema: &Semantic<'a>, + db: &dyn MinDefDatabase, + var: InFile<&ast::Var>, +) -> Option> { + // TODO: replace this function with the appropriate one when the + // highlight usages feature exists. T128835148 + let var_range = var.value.syntax().text_range(); + let clause = var + .value + .syntax() + .ancestors() + .find_map(ast::FunctionClause::cast)?; + + let vars: Vec<_> = clause + .syntax() + .descendants() + .filter_map(ast::Var::cast) + .filter_map(|v| { + let ds = sema.to_def(InFile::new(var.file_id, &v))?.to_reference()?; + if ds.len() == 1 { + // We have resolved a candidate Var. + // Check that it resolves to the one we are currently attempting to inline + // And that we have not found ourselves. + if ds[0].source(db.upcast()).syntax().text_range() == var_range + && v.syntax().text_range() != var_range + { + Some(v) + } else { + None + } + } else { + None + } + }) + .collect(); + + if vars.is_empty() { None } else { Some(vars) } +} + +/// Check that all free variables defined in the RHS of the `MatchExpr` +/// have the same bindings in the target location, in `references`. +fn is_safe( + ctx: &AssistContext, + lhs_var: ast::Var, + match_expr: &ast::MatchExpr, + references: &[ast::Var], +) -> bool { + // We care about two kinds of variables external to the + // `match_expr` rhs. + // 1. Ones that are free in the rhs expression and already bound + // in the surrounding context. These must be bound to the same + // value in the new location. + // 2. Ones that are at the top level in the rhs expression, and + // are bound in the expression. These must be unbound in the + // new location. + + // Use closure to allow use of `?` operator + if let Some(r) = || -> Option { + let rhs = match_expr.rhs()?; + let rhs_vars = ctx.sema.free_vars_ast(ctx.file_id(), &rhs)?; + + let (resolver_orig, scope_orig) = ctx.sema.scope_for(InFile { + file_id: ctx.file_id(), + value: &lhs_var, + })?; + + if references.iter().all(|var: &ast::Var| { + let var_in = InFile { + file_id: ctx.file_id(), + value: var, + }; + if let Some((resolver_var, scope_var)) = ctx.sema.scope_for(var_in) { + // XXX_orig is where the RHS was originally defined + // XXX_var is where we want to inline it + + // Check that all of `rhs_free_vars` have the same definition in both places + let free_ok = rhs_vars.free.iter().all(|(var, _defs)| { + resolver_orig.resolve_var_in_scope(var, scope_orig) + == resolver_var.resolve_var_in_scope(var, scope_var) + }); + + // Check that all of `rhs_bound_vars` are unbound in the new place + let bound_ok = rhs_vars.bound.iter().all(|(var, _defs)| { + resolver_var.resolve_var_in_scope(var, scope_var).is_none() + }); + free_ok && bound_ok + } else { + false + } + }) { + Some(true) + } else { + Some(false) + } + }() { + r + } else { + false + } +} + +// --------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use expect_test::expect; + + use super::*; + use crate::tests::*; + + #[test] + fn test_definition1() { + check_assist( + inline_local_variable, + "Inline variable", + r#" +bar() -> + ~A = 3 + B, + foo(A). +"#, + expect![[r#" + bar() -> + foo(3 + B). + "#]], + ) + } + + #[test] + fn test_definition2() { + check_assist( + inline_local_variable, + "Inline variable", + r#" +foo() -> + ~VarName = (1 + 2), + VarName * 4. +"#, + expect![[r#" + foo() -> + (1 + 2) * 4. + "#]], + ) + } + + #[test] + fn test_definition_case() { + check_assist( + inline_local_variable, + "Inline variable", + r#" +bar(X) -> + ~A = 3 + B, + case X of + A -> none + end. +"#, + expect![[r#" + bar(X) -> + case X of + 3 + B -> none + end. + "#]], + ) + } + + #[test] + fn test_shadowed_variable_free_vars() { + check_assist_not_applicable( + inline_local_variable, + r#" +bar() -> + B = 3, + ~A = begin + C = 2, + case B of + 3 -> 6; + _ -> C + end + end, + (fun() -> + C = 5, + foo(A) % Not applicable, C is already bound in the new location + end)(). +"#, + ) + } + + #[test] + fn test_usage_case() { + check_assist( + inline_local_variable, + "Inline variable", + r#" +baz(X) -> + V = 3, + A = case X of + ~V -> X + 4; + _ -> X + end, + A. +"#, + expect![[r#" + baz(X) -> + A = case X of + 3 -> X + 4; + _ -> X + end, + A. + "#]], + ) + } + + #[test] + fn test_inline_in_case1() { + check_assist_not_applicable( + inline_local_variable, + r#" + bar(XX) -> + case XX of + 1 -> + Z = 3; + 2 -> + ~Z = 4 + end, + foo(Z). + + "#, + ); + } + + #[test] + fn test_usage_one_only() { + check_assist( + inline_local_variable, + "Inline variable", + r#" +bar() -> + A = 3 + B, + foo(~A). +"#, + expect![[r#" + bar() -> + foo(3 + B). + "#]], + ) + } + + #[test] + fn test_usage_multiple() { + check_assist( + inline_local_variable, + "Inline variable", + r#" +bar() -> + A = 3 + B, + foo(~A), + bar(A). +"#, + expect![[r#" + bar() -> + A = 3 + B, + foo(3 + B), + bar(A). + "#]], + ) + } + + #[test] + fn test_inline_usage_in_case1() { + check_assist_not_applicable( + inline_local_variable, + r#" + bar() -> + case rand:uniform() of + 1 -> + Z = 3; + 2 -> + Z = 4 + end, + foo(~Z). + + "#, + ); + } + + #[test] + fn test_parens_needed1() { + check_assist( + inline_local_variable, + "Inline variable", + r#" +bar() -> + ~A = 3 + B, + X = A * 3. +"#, + expect![[r#" + bar() -> + X = (3 + B) * 3. + "#]], + ) + } + + #[test] + fn test_parens_needed2() { + check_assist( + inline_local_variable, + "Inline variable", + r#" +bar() -> + ~A = baz(4), + X = A * 3. +"#, + expect![[r#" + bar() -> + X = baz(4) * 3. + "#]], + ) + } + + #[test] + fn test_parens_needed3() { + check_assist( + inline_local_variable, + "Inline variable", + r#" +bar() -> + ~A = B#{ foo := 3}, + X = A * 3. +"#, + expect![[r##" + bar() -> + X = B#{ foo := 3} * 3. + "##]], + ) + } + + #[test] + fn test_parens_needed4() { + check_assist( + inline_local_variable, + "Inline variable", + r#" +bar() -> + ~A = baz, + X = A * 3. +"#, + expect![[r##" + bar() -> + X = baz * 3. + "##]], + ) + } + + #[test] + fn test_parens_needed_parent1() { + check_assist( + inline_local_variable, + "Inline variable", + r#" +bar() -> + ~A = B + 3, + foo(A,4). +"#, + expect![[r#" + bar() -> + foo(B + 3,4). + "#]], + ) + } + + #[test] + fn test_parens_needed_parent2() { + check_assist( + inline_local_variable, + "Inline variable", + r#" +bar() -> + ~A = B + 3, + [A|A], + Y = A, + catch A, + begin + A, + Y = 6 + end, + A. +"#, + expect![[r#" + bar() -> + [B + 3|B + 3], + Y = B + 3, + catch B + 3, + begin + B + 3, + Y = 6 + end, + B + 3. + "#]], + ) + } + + #[test] + fn test_surrounding_commas1() { + check_assist( + inline_local_variable, + "Inline variable", + r#" +bar() -> + ~A = 3 + B , + C = 5, + bar(A). +"#, + expect![[r#" + bar() -> + C = 5, + bar(3 + B). + "#]], + ) + } +} diff --git a/crates/ide_assists/src/helpers.rs b/crates/ide_assists/src/helpers.rs new file mode 100644 index 0000000000..e58d7db9bc --- /dev/null +++ b/crates/ide_assists/src/helpers.rs @@ -0,0 +1,547 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::iter; + +use elp_ide_db::elp_base_db::FileId; +use elp_ide_db::rename::is_safe_function; +use elp_ide_db::source_change::SourceChangeBuilder; +use elp_ide_db::ReferenceClass; +use elp_ide_db::SymbolClass; +use elp_ide_db::SymbolDefinition; +use elp_syntax::ast; +use elp_syntax::match_ast; +use elp_syntax::AstNode; +use elp_syntax::AstPtr; +use elp_syntax::NodeOrToken; +use elp_syntax::SourceFile; +use elp_syntax::SyntaxElement; +use elp_syntax::SyntaxKind; +use elp_syntax::SyntaxNode; +use elp_syntax::TextRange; +use fxhash::FxHashSet; +use hir::Clause; +use hir::CompileOption; +use hir::FormList; +use hir::InFileAstPtr; +use hir::InFunctionBody; +use hir::NameArity; +use hir::Semantic; +use hir::Var; +use text_edit::TextSize; + +use crate::assist_context::AssistContext; + +pub fn prev_form_nodes(syntax: &SyntaxNode) -> impl Iterator { + syntax + .siblings_with_tokens(elp_syntax::Direction::Prev) + .skip(1) // Starts with itself + .filter_map(|node_or_token| node_or_token.into_node()) + .take_while(|node| node.kind() != SyntaxKind::FUN_DECL) +} + +/// Use surrounding context to suggest a name for a new variable. +/// Defaults to simply `VarName` for now. +/// +/// **NOTE**: it is caller's responsibility to guarantee uniqueness of the name. +/// I.e. it doesn't look for names in scope. +pub(crate) fn suggest_name_for_variable(_expr: &ast::Expr, _sema: &Semantic) -> String { + "VarName".to_string() +} + +/// Given a variable name and vars in scope, return either the +/// original if it does not clash, or one with the smallest numeric suffix to be fresh. +pub(crate) fn freshen_variable_name( + sema: &Semantic, + var_name: String, + vars_in_clause: &Option>, +) -> String { + if let Some(vars_in_clause) = vars_in_clause { + let is_safe = |name: &String| -> bool { + vars_in_clause + .iter() + .all(|v| name != &v.as_string(sema.db.upcast())) + }; + if is_safe(&var_name) { + var_name + } else { + let mut i = 0; + loop { + let name = format!("{var_name}{i}").to_string(); + if is_safe(&name) { + return name; + } + i = i + 1; + } + } + } else { + var_name + } +} + +/// Given a function name/arity and FileId, return either the original if it +/// does not clash, or one with the smallest numeric suffix to be +/// fresh. +pub(crate) fn freshen_function_name(ctx: &AssistContext, name: String, arity: u32) -> String { + if is_safe_function(&ctx.sema, ctx.file_id(), &name, arity) { + name + } else { + let mut i = 0; + loop { + let candidate_name = format!("{name}_{i}").to_string(); + if is_safe_function(&ctx.sema, ctx.file_id(), &candidate_name, arity) { + return candidate_name; + } + i = i + 1; + } + } +} + +pub(crate) fn skip_ws(node: Option) -> Option { + node.and_then(SyntaxElement::into_token).and_then(|t| { + if t.kind() == SyntaxKind::WHITESPACE { + Some(t.text_range()) + } else { + None + } + }) +} + +pub(crate) fn skip_trailing_separator(node: &SyntaxNode) -> Option { + let elements = iter::successors(node.next_sibling_or_token(), |n| { + (*n).next_sibling_or_token() + }); + for element in elements { + if let Some(t) = &SyntaxElement::into_token(element) { + if t.kind() != SyntaxKind::WHITESPACE { + return Some(t.text_range()); + } + } + } + None +} + +pub(crate) fn find_next_token(node: &SyntaxNode, delimiter: SyntaxKind) -> Option { + node.children_with_tokens() + .filter_map(|it| it.into_token()) + .filter(|it| it.kind() == delimiter) + .next() + .map(|t| t.text_range()) +} + +pub(crate) fn skip_trailing_newline(node: &SyntaxNode) -> Option { + let elements = iter::successors(node.next_sibling_or_token(), |n| { + (*n).next_sibling_or_token() + }); + for element in elements { + if let Some(t) = &SyntaxElement::into_token(element) { + if t.kind() == SyntaxKind::WHITESPACE && t.text().contains("\n") { + return Some(t.text_range()); + } + } else { + return None; + } + } + None +} + +pub(crate) fn parens_needed(expr: &ast::Expr, var: &ast::Var) -> Option<(TextRange, bool)> { + let rhs_not_needed = matches!( + expr, + ast::Expr::ExprMax(ast::ExprMax::Atom(_)) + | ast::Expr::ExprMax(ast::ExprMax::Binary(_)) + | ast::Expr::ExprMax(ast::ExprMax::BinaryComprehension(_)) + | ast::Expr::ExprMax(ast::ExprMax::BlockExpr(_)) + | ast::Expr::ExprMax(ast::ExprMax::CaseExpr(_)) + | ast::Expr::ExprMax(ast::ExprMax::Char(_)) + | ast::Expr::ExprMax(ast::ExprMax::Float(_)) + | ast::Expr::ExprMax(ast::ExprMax::IfExpr(_)) + | ast::Expr::ExprMax(ast::ExprMax::Integer(_)) + | ast::Expr::ExprMax(ast::ExprMax::List(_)) + | ast::Expr::ExprMax(ast::ExprMax::ListComprehension(_)) + | ast::Expr::ExprMax(ast::ExprMax::MacroCallExpr(_)) + | ast::Expr::ExprMax(ast::ExprMax::MacroString(_)) + | ast::Expr::ExprMax(ast::ExprMax::ParenExpr(_)) + | ast::Expr::ExprMax(ast::ExprMax::ReceiveExpr(_)) + | ast::Expr::ExprMax(ast::ExprMax::String(_)) + | ast::Expr::ExprMax(ast::ExprMax::TryExpr(_)) + | ast::Expr::ExprMax(ast::ExprMax::Tuple(_)) + | ast::Expr::ExprMax(ast::ExprMax::Var(_)) + | ast::Expr::Call(_) + | ast::Expr::MapExpr(_) + | ast::Expr::MapExprUpdate(_) + | ast::Expr::RecordExpr(_) + | ast::Expr::RecordFieldExpr(_) + | ast::Expr::RecordIndexExpr(_) + | ast::Expr::RecordUpdateExpr(_), + ); + + let parent = var.syntax().parent()?; + let parent_not_needed = match_ast! { + match parent { + ast::ExprArgs(_) => true, + ast::Pipe(_) => true, + ast::ClauseBody(_) => true, + ast::CatchExpr(_) => true, + ast::MatchExpr(_) => true, + ast::BlockExpr(_) => true, + ast::CrClause(_) => true, + _ => false + } + }; + + Some(( + var.syntax().text_range(), + !(rhs_not_needed || parent_not_needed), + )) +} + +pub(crate) fn change_indent(delta_indent: i8, str: String) -> String { + let indent_str = " ".repeat(delta_indent.abs() as usize); + if str.contains("\n") { + // Only change indentation if the new string has more than one line. + str.split("\n") + .enumerate() + .map(|(idx, s)| { + if idx == 0 && s != "" { + // No leading newline, trim leading whitespace + s.trim_start().to_string() + } else { + if delta_indent >= 0 { + if s != "" { + format!("{}{}", indent_str, s) + } else { + s.to_owned() + } + } else { + if let Some(s) = s.strip_prefix(indent_str.as_str()) { + s.to_string() + } else { + // Do not lose useful characters, but remove all leading whitespace + s.trim_start().to_string() + } + } + } + }) + .map(|s| s.trim_end().to_string()) + .collect::>() + .join("\n") + } else { + str.trim_start().to_string() + } +} + +pub const DEFAULT_INDENT_STEP: i8 = 4; + +/// Any parameters to the `Clause` that are just a single variable. +pub(crate) fn simple_param_vars(clause: &InFunctionBody<&Clause>) -> Option> { + let mut acc = FxHashSet::default(); + clause.value.pats.iter().for_each(|p| match &clause[*p] { + hir::Pat::Var(v) => { + acc.insert(v.clone()); + } + _ => {} + }); + Some(acc) +} + +#[derive(Debug)] +pub(crate) struct FunctionRanges { + pub(crate) function: TextRange, + pub(crate) spec: Option, + pub(crate) edoc: Vec, +} + +impl FunctionRanges { + pub(crate) fn delete(&self, builder: &mut SourceChangeBuilder) { + builder.delete(self.function); + self.spec.into_iter().for_each(|range| { + builder.delete(range); + }); + self.edoc.iter().for_each(|range| { + builder.delete(*range); + }); + } +} + +pub(crate) fn ranges_for_delete_function( + ctx: &AssistContext, + ast_fun: &ast::FunDecl, +) -> Option { + // Look for a possible spec, and delete it too. + let function_def = match ctx.classify_offset()? { + SymbolClass::Definition(SymbolDefinition::Function(fun_def)) => Some(fun_def), + SymbolClass::Reference { refs, typ: _ } => match refs { + ReferenceClass::Definition(SymbolDefinition::Function(fun_def)) => Some(fun_def), + _ => None, + }, + _ => None, + }?; + + let def_map = ctx.sema.def_map(ctx.file_id()); + let spec = def_map.get_spec(&function_def.function.name); + + let edoc_comments: Vec> = if let Some(file_edoc) = + ctx.sema.form_edoc_comments(InFileAstPtr::new( + ctx.file_id(), + AstPtr::new(&ast::Form::FunDecl(ast_fun.clone())), + )) { + file_edoc.comments() + } else { + vec![] + }; + + let edoc = edoc_comments + .iter() + .filter_map(|c| { + let comment = ctx.ast_ptr_get(*c)?; + Some(extend_form_range_for_delete(comment.syntax())) + }) + .collect(); + + let spec_range = spec.map(|spec| { + let ast_spec = ctx.form_ast(spec.spec.form_id); + extend_form_range_for_delete(ast_spec.syntax()) + }); + + Some(FunctionRanges { + function: extend_form_range_for_delete(ast_fun.syntax()), + spec: spec_range, + edoc, + }) +} + +fn extend_form_range_for_delete(syntax: &SyntaxNode) -> TextRange { + let orig_range = syntax.text_range(); + let start = orig_range.start(); + let end = match skip_trailing_newline(syntax) { + Some(end) => end.end(), + None => orig_range.end(), + }; + // Temporary for T148094436 + let _pctx = stdx::panic_context::enter(format!("\nextend_form_range_for_delete")); + TextRange::new(start, end) +} + +// --------------------------------------------------------------------- + +pub fn add_compile_option<'a>( + sema: &'a Semantic<'a>, + file_id: FileId, + option: &str, + insert_at: Option, + builder: &'a mut SourceChangeBuilder, +) -> Option<()> { + let source = sema.parse(file_id).value; + let form_list = sema.db.file_form_list(file_id); + + builder.edit_file(file_id); + if form_list.compile_attributes().count() == 0 { + new_compile_attribute(&form_list, &source, option, insert_at, builder); + Some(()) + } else { + if form_list.compile_attributes().count() == 1 { + // One existing compile attribute, add the option to it. + let (_, co) = form_list.compile_attributes().next()?; + add_to_compile_attribute(co, &source, option, builder) + } else { + // Multiple, make a new one + new_compile_attribute(&form_list, &source, option, insert_at, builder); + Some(()) + } + } +} + +fn new_compile_attribute<'a>( + form_list: &FormList, + source: &SourceFile, + option: &str, + insert_at: Option, + builder: &'a mut SourceChangeBuilder, +) { + let insert = insert_at.unwrap_or_else(|| { + if let Some(module_attr) = form_list.module_attribute() { + let module_attr_range = module_attr.form_id.get(&source).syntax().text_range(); + TextSize::from(module_attr_range.end() + TextSize::from(1)) + } else { + TextSize::from(0) + } + }); + builder.insert(insert, format!("\n-compile([{option}]).\n")) +} + +fn add_to_compile_attribute<'a>( + co: &CompileOption, + source: &SourceFile, + option: &str, + builder: &'a mut SourceChangeBuilder, +) -> Option<()> { + let export_ast = co.form_id.get(source); + match &export_ast.options()? { + ast::Expr::ExprMax(ast::ExprMax::List(e)) => { + // Skip the trailing "]" + let mut r = e.syntax().text_range().end(); + r -= TextSize::from(1); + builder.insert(r, format!(", {option}")); + } + ast::Expr::ExprMax(ast::ExprMax::Atom(e)) => { + let r = e.syntax().text_range(); + builder.replace(r, format!("[{}, {option}]", e.syntax().text())); + } + ast::Expr::ExprMax(ast::ExprMax::Tuple(e)) => { + let r = e.syntax().text_range(); + builder.replace(r, format!("[{}, {option}]", e.syntax().text())); + } + _ => return None, + }; + Some(()) +} + +// --------------------------------------------------------------------- + +pub(crate) struct ExportBuilder<'a> { + sema: &'a Semantic<'a>, + file_id: FileId, + funs: &'a [NameArity], + // `group_with`: Add `funs` to the same export as this, if found. + // If it is added to the existing export, the comment is not used. + group_with: Option, + insert_at: Option, + with_comment: Option, + builder: &'a mut SourceChangeBuilder, +} + +impl<'a> ExportBuilder<'a> { + pub(crate) fn new( + sema: &'a Semantic<'a>, + file_id: FileId, + funs: &'a [NameArity], + builder: &'a mut SourceChangeBuilder, + ) -> ExportBuilder<'a> { + ExportBuilder { + sema, + file_id, + funs, + group_with: None, + insert_at: None, + with_comment: None, + builder, + } + } + + pub(crate) fn group_with(mut self, name: NameArity) -> ExportBuilder<'a> { + self.group_with = Some(name); + self + } + + pub(crate) fn insert_at(mut self, location: TextSize) -> ExportBuilder<'a> { + self.insert_at = Some(location); + self + } + + pub(crate) fn with_comment(mut self, comment: String) -> ExportBuilder<'a> { + self.with_comment = Some(comment); + self + } + + pub(crate) fn finish(&mut self) { + let source = self.sema.parse(self.file_id).value; + let form_list = self.sema.db.file_form_list(self.file_id); + let export_text = self + .funs + .iter() + .map(|function_name_arity| format!("{function_name_arity}")) + .collect::>() + .join(", "); + + let (insert, text) = if form_list.exports().count() == 0 { + self.new_export(form_list, source, export_text) + } else { + // Top priority: group_with + if let Some(group_with) = &self.group_with { + if let Some((insert, text)) = || -> Option<_> { + let (_, export) = form_list.exports().find(|(_, e)| { + e.entries + .clone() + .into_iter() + .any(|fa| &form_list[fa].name == group_with) + })?; + add_to_export(export, &source, &export_text) + }() { + (insert, text) + } else { + self.new_export(form_list, source, export_text) + } + } else { + if self.with_comment.is_some() { + // Preceding comment for export, always make a fresh one + self.new_export(form_list, source, export_text) + } else { + if let Some((insert, text)) = || -> Option<_> { + if form_list.exports().count() == 1 { + // One existing export, add the function to it. + + let (_, export) = form_list.exports().next()?; + add_to_export(export, &source, &export_text) + } else { + // Multiple + None + } + }() { + (insert, text) + } else { + // Zero or multiple existing exports, create a fresh one + self.new_export(form_list, source, export_text) + } + } + } + }; + + self.builder.edit_file(self.file_id); + self.builder.insert(insert, text) + } + + fn new_export( + &self, + form_list: std::sync::Arc, + source: elp_syntax::SourceFile, + export_text: String, + ) -> (TextSize, String) { + let insert = self.insert_at.unwrap_or_else(|| { + if let Some(module_attr) = form_list.module_attribute() { + let module_attr_range = module_attr.form_id.get(&source).syntax().text_range(); + TextSize::from(module_attr_range.end() + TextSize::from(1)) + } else { + TextSize::from(0) + } + }); + match &self.with_comment { + Some(comment) => ( + insert, + format!("\n%% {comment}\n-export([{export_text}]).\n"), + ), + None => (insert, format!("\n-export([{export_text}]).\n")), + } + } +} + +fn add_to_export( + export: &hir::Export, + source: &elp_syntax::SourceFile, + export_text: &String, +) -> Option<(TextSize, String)> { + let export_ast = export.form_id.get(source); + if let Some(fa) = export_ast.funs().last() { + Some((fa.syntax().text_range().end(), format!(", {export_text}"))) + } else { + // Empty export list + let range = find_next_token(export_ast.syntax(), SyntaxKind::ANON_LBRACK)?; + Some((range.end(), export_text.clone())) + } +} diff --git a/crates/ide_assists/src/lib.rs b/crates/ide_assists/src/lib.rs new file mode 100644 index 0000000000..a6549445f3 --- /dev/null +++ b/crates/ide_assists/src/lib.rs @@ -0,0 +1,113 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! `assists` crate provides a bunch of code assists, also known as code +//! actions (in LSP) or intentions (in IntelliJ). +//! +//! An assist is a micro-refactoring, which is automatically activated in +//! certain context. For example, if the cursor is over `,`, a "swap `,`" assist +//! becomes available. + +#[allow(unused)] +macro_rules! eprintln { + ($($tt:tt)*) => { stdx::eprintln!($($tt)*) }; +} + +mod assist_config; +mod assist_context; +pub mod helpers; +#[cfg(test)] +mod tests; + +// use hir::Semantics; +pub use assist_config::AssistConfig; +pub use elp_ide_db::assists::Assist; +use elp_ide_db::assists::AssistContextDiagnostic; +pub use elp_ide_db::assists::AssistId; +pub use elp_ide_db::assists::AssistKind; +pub use elp_ide_db::assists::AssistResolveStrategy; +use elp_ide_db::assists::AssistUserInput; +pub use elp_ide_db::assists::GroupLabel; +pub use elp_ide_db::assists::SingleResolve; +use elp_ide_db::elp_base_db::FileRange; +use elp_ide_db::RootDatabase; + +// use elp_syntax::TextRange; +pub(crate) use crate::assist_context::AssistContext; +pub(crate) use crate::assist_context::Assists; + +/// Return all the assists applicable at the given position. +pub fn assists( + db: &RootDatabase, + config: &AssistConfig, + resolve: AssistResolveStrategy, + range: FileRange, + context_diagnostics: &[AssistContextDiagnostic], + user_input: Option, +) -> Vec { + let ctx = AssistContext::new(db, config, range, context_diagnostics, user_input); + let mut acc = Assists::new(&ctx, resolve); + handlers::all().iter().for_each(|handler| { + handler(&mut acc, &ctx); + }); + acc.finish() +} + +mod handlers { + use crate::AssistContext; + use crate::Assists; + + pub(crate) type Handler = fn(&mut Assists, &AssistContext) -> Option<()>; + + mod add_edoc; + mod add_format; + mod add_impl; + mod add_spec; + mod bump_variables; + mod create_function; + mod delete_function; + mod export_function; + mod extract_function; + mod extract_variable; + mod flip_sep; + mod ignore_variable; + mod implement_behaviour; + mod inline_function; + mod inline_local_variable; + + pub(crate) fn all() -> &'static [Handler] { + &[ + // These are alphabetic for the foolish consistency + add_edoc::add_edoc, + add_format::add_format, + add_impl::add_impl, + add_spec::add_spec, + bump_variables::bump_variables, + create_function::create_function, + delete_function::delete_function, + export_function::export_function, + extract_function::extract_function, + extract_variable::extract_variable, + flip_sep::flip_sep, + ignore_variable::ignore_variable, + implement_behaviour::implement_behaviour, + inline_function::inline_function, + inline_local_variable::inline_local_variable, + // These are manually sorted for better priorities. By default, + // priority is determined by the size of the target range (smaller + // target wins). If the ranges are equal, position in this list is + // used as a tie-breaker. + // add_missing_impl_members::add_missing_impl_members, + // add_missing_impl_members::add_missing_default_members, + + // Are you sure you want to add new assist here, and not to the + // sorted list above? + ] + } +} diff --git a/crates/ide_assists/src/tests.rs b/crates/ide_assists/src/tests.rs new file mode 100644 index 0000000000..de50a6f553 --- /dev/null +++ b/crates/ide_assists/src/tests.rs @@ -0,0 +1,701 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::str::FromStr; + +use elp_ide_db::assists::AssistContextDiagnostic; +use elp_ide_db::assists::AssistContextDiagnosticCode; +use elp_ide_db::assists::AssistId; +use elp_ide_db::assists::AssistKind; +use elp_ide_db::assists::AssistUserInput; +use elp_ide_db::assists::AssistUserInputType; +use elp_ide_db::elp_base_db::fixture::extract_annotations; +use elp_ide_db::elp_base_db::fixture::remove_annotations; +use elp_ide_db::elp_base_db::fixture::WithFixture; +use elp_ide_db::elp_base_db::FileRange; +use elp_ide_db::elp_base_db::SourceDatabase; +use elp_ide_db::elp_base_db::SourceDatabaseExt; +use elp_ide_db::helpers::SnippetCap; +use elp_ide_db::source_change::FileSystemEdit; +use elp_ide_db::RootDatabase; +use elp_ide_db::SymbolClass; +use elp_ide_db::SymbolDefinition; +use elp_syntax::ast; +use elp_syntax::AstNode; +use elp_syntax::SourceFile; +use expect_test::expect; +use expect_test::Expect; +use hir::Expr; +use hir::InFile; +use stdx::format_to; + +use crate::handlers::Handler; +use crate::helpers; +use crate::AssistConfig; +use crate::AssistContext; +use crate::AssistResolveStrategy; +use crate::Assists; + +pub(crate) const TEST_CONFIG: AssistConfig = AssistConfig { + snippet_cap: SnippetCap::new(true), + allowed: None, +}; + +#[track_caller] +pub(crate) fn check_assist( + assist: Handler, + assist_label: &str, + fixture_before: &str, + fixture_after: Expect, +) { + check( + assist, + fixture_before, + ExpectedResult::After(fixture_after), + Some(assist_label), + true, + None, + ); +} + +#[track_caller] +pub(crate) fn check_assist_with_user_input( + assist: Handler, + assist_label: &str, + user_input: &str, + fixture_before: &str, + fixture_after: Expect, +) { + check( + assist, + fixture_before, + ExpectedResult::After(fixture_after), + Some(assist_label), + true, + Some(user_input), + ); +} + +#[track_caller] +// We allow dead code because this function is used while debugging tests +#[allow(dead_code)] +pub(crate) fn check_assist_expect_parse_error( + assist: Handler, + assist_label: &str, + fixture_before: &str, + fixture_after: Expect, +) { + check( + assist, + fixture_before, + ExpectedResult::After(fixture_after), + Some(assist_label), + false, + None, + ); +} + +#[track_caller] +pub(crate) fn check_assist_not_applicable(assist: Handler, ra_fixture: &str) { + check( + assist, + ra_fixture, + ExpectedResult::NotApplicable, + None, + true, + None, + ); +} + +enum ExpectedResult { + NotApplicable, + After(Expect), +} + +pub const SNIPPET_CURSOR_MARKER: &str = "$0"; + +#[track_caller] +fn check( + handler: Handler, + before: &str, + expected: ExpectedResult, + assist_label: Option<&str>, + check_parse_error: bool, + user_input: Option<&str>, +) { + let (db, file_with_caret_id, range_or_offset) = RootDatabase::with_range_or_offset(before); + + let frange = FileRange { + file_id: file_with_caret_id, + range: range_or_offset.into(), + }; + + let sema = &db; + let config = TEST_CONFIG; + let context_diagnostics = extract_annotations(&*db.file_text(file_with_caret_id)); + let mut diagnostics = vec![]; + for (range, text) in &context_diagnostics { + if let Some((code_and_bulb, message)) = text.split_once(":") { + if let Some(code_string) = code_and_bulb.strip_prefix("💡 ") { + if let Ok(code) = AssistContextDiagnosticCode::from_str(code_string) { + let d = AssistContextDiagnostic::new(code, message.trim().to_string(), *range); + diagnostics.push(d) + } + } + } + } + + // Check that the fixture is syntactically valid + if check_parse_error { + // Check that we have a syntactically valid starting point + let text = sema.file_text(frange.file_id); + let parse = sema.parse(frange.file_id); + let errors = parse.errors(); + if !errors.is_empty() { + assert_eq!(format!("{:?}\nin\n{text}", errors), ""); + } + } + + let mut ctx = AssistContext::new(&sema, &config, frange, &diagnostics, None); + let resolve = match expected { + _ => AssistResolveStrategy::All, + }; + let mut acc = Assists::new(&ctx, resolve.clone()); + handler(&mut acc, &ctx); + let mut res = acc.finish(); + if let Some(requested_user_input) = res.get(0).and_then(|a| a.user_input.clone()) { + let value = if let Some(input) = user_input { + input.to_string() + } else { + match requested_user_input.input_type { + AssistUserInputType::Variable => { + format!("{}Edited", requested_user_input.value).to_string() + } + AssistUserInputType::Atom => { + format!("{}_edited", requested_user_input.value).to_string() + } + } + }; + ctx.user_input = Some(AssistUserInput { + input_type: requested_user_input.input_type, + value, + }); + // Resolve the assist, with the edited result + let mut acc = Assists::new(&ctx, resolve); + handler(&mut acc, &ctx); + res = acc.finish(); + } + + let assist = match assist_label { + Some(label) => res + .clone() + .into_iter() + .find(|resolved| resolved.label == label), + None => res.clone().pop(), + }; + + match (assist, expected) { + (Some(assist), ExpectedResult::After(after)) => { + let source_change = assist + .source_change + .expect("Assist did not contain any source changes"); + assert!(!source_change.source_file_edits.is_empty()); + let skip_header = source_change.source_file_edits.len() == 1 + && source_change.file_system_edits.len() == 0; + + let mut buf = String::new(); + for (file_id, edit) in source_change.source_file_edits { + let mut text = db.file_text(file_id).as_ref().to_owned(); + edit.apply(&mut text); + if !skip_header { + let sr = db.file_source_root(file_id); + let sr = db.source_root(sr); + let path = sr.path_for_file(&file_id).unwrap(); + format_to!(buf, "//- {}\n", path) + } + buf.push_str(&text); + } + + for file_system_edit in source_change.file_system_edits { + if let FileSystemEdit::CreateFile { + dst, + initial_contents, + } = file_system_edit + { + let sr = db.file_source_root(dst.anchor); + let sr = db.source_root(sr); + let mut base = sr.path_for_file(&dst.anchor).unwrap().clone(); + base.pop(); + let created_file_path = format!("{}{}", base.to_string(), &dst.path[1..]); + format_to!(buf, "//- {}\n", created_file_path); + buf.push_str(&initial_contents); + } + } + + if check_parse_error { + // Check that we have introduced a syntactically valid result + let text = remove_annotations(Some(SNIPPET_CURSOR_MARKER), &buf); + let parse = SourceFile::parse_text(&text); + let errors = parse.errors(); + if !errors.is_empty() { + assert_eq!(format!("{:?}\nin\n{text}", errors), ""); + } + } + + after.assert_eq(&remove_annotations(None, &buf)); + } + + (Some(_), ExpectedResult::NotApplicable) => panic!("assist should not be applicable!"), + (None, ExpectedResult::After(_)) => { + match assist_label { + Some(label) => { + let all = res + .clone() + .iter() + .map(|resolved| resolved.label.clone()) + .collect::>(); + panic!("Expecting \"{}\", but not found in {:?}", label, all); + } + None => panic!("code action is not applicable"), + }; + } + (None, ExpectedResult::NotApplicable) => (), + }; +} + +#[test] +fn test_whitespace_skip1() { + let before = r#" +bar() -> + A %comment1 + ~ = 3 + B %% comment2 + ~ , + foo(A). +"#; + + let (db, frange) = RootDatabase::with_range(before); + let sema = &db; + let config = TEST_CONFIG; + let diagnostics = vec![]; + let ctx = AssistContext::new(&sema, &config, frange, &diagnostics, None); + expect![[r#" + FileRange { + file_id: FileId( + 0, + ), + range: 26..56, + } + "#]] + .assert_debug_eq(&ctx.frange); + expect![[r#" + 29..48 + "#]] + .assert_debug_eq(&ctx.selection_trimmed()); +} + +#[test] +fn test_whitespace_skip2() { + let before = r#" +bar() -> + A ~ %comment1 + = 3 + B ~%% comment2 + , + foo(A). +"#; + + let (db, frange) = RootDatabase::with_range(before); + let sema = &db; + let config = TEST_CONFIG; + let diagnostics = vec![]; + let ctx = AssistContext::new(&sema, &config, frange, &diagnostics, None); + expect![[r#" + FileRange { + file_id: FileId( + 0, + ), + range: 13..38, + } + "#]] + .assert_debug_eq(&ctx.frange); + expect![[r#" + 14..37 + "#]] + .assert_debug_eq(&ctx.selection_trimmed()); +} + +#[test] +fn test_function_args() { + let fixture = r#"heavy_calculations(X) -> ~life:foo(X, X+1)~."#; + + let (db, frange) = RootDatabase::with_range(fixture); + let sema = &db; + let config = TEST_CONFIG; + let diagnostics = vec![]; + let ctx = AssistContext::new(&sema, &config, frange, &diagnostics, None); + let call: ast::Call = ctx.find_node_at_offset().unwrap(); + let call_expr = ctx + .sema + .to_expr(InFile::new(ctx.file_id(), &ast::Expr::Call(call.clone()))) + .unwrap(); + if let Expr::Call { target: _, args } = &call_expr[call_expr.value] { + expect![[r#" + "X, XN" + "#]] + .assert_debug_eq(&ctx.create_function_args(args, &call_expr.body())); + } else { + panic!("Expecting Expr::Call"); + } +} + +#[test] +fn test_function_args_from_type() { + let fixture = r#" + -callback handle_call(~Request :: term(), + From :: from(), + State :: term()~) -> ok."#; + + let (db, frange) = RootDatabase::with_range(fixture); + let sema = &db; + let config = TEST_CONFIG; + let diagnostics = vec![]; + let ctx = AssistContext::new(&sema, &config, frange, &diagnostics, None); + let behaviour_forms = ctx.sema.db.file_form_list(ctx.file_id()); + if let Some((idx, _callback)) = behaviour_forms.callback_attributes().next() { + let callback_body = ctx.sema.db.callback_body(InFile::new(ctx.file_id(), idx)); + if let Some(sig) = callback_body.sigs.iter().next() { + expect![[r#" + "Request,From,State" + "#]] + .assert_debug_eq(&ctx.create_function_args_from_types(&sig.args, &callback_body.body)); + } else { + panic!("Expecting Sig"); + } + } else { + panic!("Expecting Callback"); + }; +} + +#[test] +fn export_no_pre_existing() { + fn export_function(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { + if let Some(SymbolClass::Definition(SymbolDefinition::Function(fun))) = + ctx.classify_offset() + { + let function_name_arity = fun.function.name; + let function_range = ctx.form_ast(fun.function.form_id).syntax().text_range(); + + if !fun.exported { + let id = AssistId("export_function", AssistKind::QuickFix); + let message = format!("Export the function `{function_name_arity}`"); + acc.add(id, message, function_range, None, |builder| { + helpers::ExportBuilder::new( + &ctx.sema, + ctx.file_id(), + &[function_name_arity], + builder, + ) + .finish(); + }); + } + } + Some(()) + } + check_assist( + export_function, + "Export the function `heavy_calculations/1`", + r#" + -module(life). + + heavy_cal~culations(X) -> X. + "#, + expect![[r#" + -module(life). + + -export([heavy_calculations/1]). + + heavy_calculations(X) -> X. + "#]], + ) +} + +#[test] +fn export_single_pre_existing() { + fn export_function(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { + if let Some(SymbolClass::Definition(SymbolDefinition::Function(fun))) = + ctx.classify_offset() + { + let function_name_arity = fun.function.name; + let function_range = ctx.form_ast(fun.function.form_id).syntax().text_range(); + + if !fun.exported { + let id = AssistId("export_function", AssistKind::QuickFix); + let message = format!("Export the function `{function_name_arity}`"); + acc.add(id, message, function_range, None, |builder| { + helpers::ExportBuilder::new( + &ctx.sema, + ctx.file_id(), + &[function_name_arity], + builder, + ) + .finish(); + }); + } + } + Some(()) + } + check_assist( + export_function, + "Export the function `heavy_calculations/1`", + r#" + -module(life). + + -export([foo/0]). + + heavy_cal~culations(X) -> X. + + foo() -> ok. + "#, + expect![[r#" + -module(life). + + -export([foo/0, heavy_calculations/1]). + + heavy_calculations(X) -> X. + + foo() -> ok. + "#]], + ) +} + +#[test] +fn export_single_pre_existing_with_comment() { + fn export_function(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { + if let Some(SymbolClass::Definition(SymbolDefinition::Function(fun))) = + ctx.classify_offset() + { + let function_name_arity = fun.function.name; + let function_range = ctx.form_ast(fun.function.form_id).syntax().text_range(); + + if !fun.exported { + let id = AssistId("export_function", AssistKind::QuickFix); + let message = format!("Export the function `{function_name_arity}`"); + acc.add(id, message, function_range, None, |builder| { + helpers::ExportBuilder::new( + &ctx.sema, + ctx.file_id(), + &[function_name_arity], + builder, + ) + .with_comment("header comment".to_string()) + .finish(); + }); + } + } + Some(()) + } + check_assist( + export_function, + "Export the function `heavy_calculations/1`", + r#" + -module(life). + + -export([foo/0]). + + heavy_cal~culations(X) -> X. + + foo() -> ok. + "#, + expect![[r#" + -module(life). + + %% header comment + -export([heavy_calculations/1]). + + -export([foo/0]). + + heavy_calculations(X) -> X. + + foo() -> ok. + "#]], + ) +} + +#[test] +fn export_single_group_with_overrides_comment() { + fn export_function(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { + if let Some(SymbolClass::Definition(SymbolDefinition::Function(fun))) = + ctx.classify_offset() + { + let function_name_arity = fun.function.name; + let function_range = ctx.form_ast(fun.function.form_id).syntax().text_range(); + + let forms = ctx.db().file_form_list(ctx.file_id()); + let (_, export) = forms.exports().next().unwrap(); + let fa = &export.entries.clone().into_iter().next().unwrap(); + let existing = forms[*fa].name.clone(); + + if !fun.exported { + let id = AssistId("export_function", AssistKind::QuickFix); + let message = format!("Export the function `{function_name_arity}`"); + acc.add(id, message, function_range, None, |builder| { + helpers::ExportBuilder::new( + &ctx.sema, + ctx.file_id(), + &[function_name_arity], + builder, + ) + .group_with(existing) + .with_comment("header comment".to_string()) + .finish(); + }); + } + } + Some(()) + } + check_assist( + export_function, + "Export the function `heavy_calculations/1`", + r#" + -module(life). + + -export([foo/0]). + + heavy_cal~culations(X) -> X. + + foo() -> ok. + "#, + expect![[r#" + -module(life). + + -export([foo/0, heavy_calculations/1]). + + heavy_calculations(X) -> X. + + foo() -> ok. + "#]], + ) +} + +#[test] +fn export_into_specific_pre_existing_1() { + fn export_function(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { + if let Some(SymbolClass::Definition(SymbolDefinition::Function(fun))) = + ctx.classify_offset() + { + let function_name_arity = fun.function.name; + let function_range = ctx.form_ast(fun.function.form_id).syntax().text_range(); + + let forms = ctx.db().file_form_list(ctx.file_id()); + let (_, export) = forms.exports().next().unwrap(); + let fa = &export.entries.clone().into_iter().next().unwrap(); + let existing = forms[*fa].name.clone(); + + if !fun.exported { + let id = AssistId("export_function", AssistKind::QuickFix); + let message = format!("Export the function `{function_name_arity}`"); + acc.add(id, message, function_range, None, |builder| { + helpers::ExportBuilder::new( + &ctx.sema, + ctx.file_id(), + &[function_name_arity], + builder, + ) + .group_with(existing) + .finish(); + }); + } + } + Some(()) + } + check_assist( + export_function, + "Export the function `heavy_calculations/1`", + r#" + -module(life). + + -export([foo/0]). + -export([bar/0]). + + heavy_cal~culations(X) -> X. + + foo() -> ok. + bar() -> ok. + "#, + expect![[r#" + -module(life). + + -export([foo/0, heavy_calculations/1]). + -export([bar/0]). + + heavy_calculations(X) -> X. + + foo() -> ok. + bar() -> ok. + "#]], + ) +} + +#[test] +fn export_into_specific_pre_existing_2() { + fn export_function(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { + if let Some(SymbolClass::Definition(SymbolDefinition::Function(fun))) = + ctx.classify_offset() + { + let function_name_arity = fun.function.name; + let function_range = ctx.form_ast(fun.function.form_id).syntax().text_range(); + + let forms = ctx.db().file_form_list(ctx.file_id()); + let (_, export) = forms.exports().skip(1).next().unwrap(); + let fa = &export.entries.clone().into_iter().next().unwrap(); + let existing = forms[*fa].name.clone(); + + if !fun.exported { + let id = AssistId("export_function", AssistKind::QuickFix); + let message = format!("Export the function `{function_name_arity}`"); + acc.add(id, message, function_range, None, |builder| { + helpers::ExportBuilder::new( + &ctx.sema, + ctx.file_id(), + &[function_name_arity], + builder, + ) + .group_with(existing) + .finish(); + }); + } + } + Some(()) + } + check_assist( + export_function, + "Export the function `heavy_calculations/1`", + r#" + -module(life). + + -export([foo/0]). + -export([bar/0]). + + heavy_cal~culations(X) -> X. + + foo() -> ok. + bar() -> ok. + "#, + expect![[r#" + -module(life). + + -export([foo/0]). + -export([bar/0, heavy_calculations/1]). + + heavy_calculations(X) -> X. + + foo() -> ok. + bar() -> ok. + "#]], + ) +} diff --git a/crates/ide_completion/Cargo.toml b/crates/ide_completion/Cargo.toml new file mode 100644 index 0000000000..4493106200 --- /dev/null +++ b/crates/ide_completion/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "elp_ide_completion" +edition.workspace = true +version.workspace = true + +[dependencies] +elp_base_db.workspace = true +elp_ide_db.workspace = true +elp_syntax.workspace = true +hir.workspace = true + +fxhash.workspace = true +lazy_static.workspace = true +log.workspace = true +lsp-types.workspace = true +stdx.workspace = true + +[dev-dependencies] +expect-test.workspace = true +serde_json.workspace = true diff --git a/crates/ide_completion/src/attributes.rs b/crates/ide_completion/src/attributes.rs new file mode 100644 index 0000000000..1ce818722f --- /dev/null +++ b/crates/ide_completion/src/attributes.rs @@ -0,0 +1,288 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use crate::Args; +use crate::Completion; +use crate::Contents; +use crate::DoneFlag; +use crate::Kind; + +pub(crate) fn add_completions( + acc: &mut Vec, + Args { + sema, + previous_tokens, + file_position, + trigger, + .. + }: &Args, +) -> DoneFlag { + use elp_syntax::SyntaxKind as K; + let default = vec![]; + let previous_tokens: &[_] = previous_tokens.as_ref().unwrap_or(&default); + match previous_tokens { + // -behavior(behavior_name_prefix~ + [ + .., + (K::ANON_DASH, _), + (K::ANON_BEHAVIOR | K::ANON_BEHAVIOUR, _), + (K::ANON_LPAREN, _), + (K::ATOM, behavior_name_prefix), + ] if trigger.is_none() => || -> _ { + let modules = sema.resolve_module_names(file_position.file_id)?; + let completions = modules.into_iter().filter_map(|m| { + if m.starts_with(behavior_name_prefix.text()) { + let module = sema.resolve_module_name(file_position.file_id, &m)?; + let def_map = sema.def_map(module.file.file_id); + if def_map.get_callbacks().is_empty() { + None + } else { + Some(Completion { + label: m.to_string(), + kind: Kind::Behavior, + contents: Contents::SameAsLabel, + position: None, + sort_text: None, + deprecated: false, + }) + } + } else { + None + } + }); + + acc.extend(completions); + Some(true) + }() + .unwrap_or_default(), + + [.., (K::ANON_DASH, _), (K::ATOM, attr_name)] if matches!(trigger, Some('-') | None) => { + if "module".starts_with(attr_name.text()) { + if let Some(module) = sema.module_name(file_position.file_id) { + acc.push(Completion { + kind: Kind::Attribute, + label: format!("-module({}).", module.to_quoted_string()), + contents: Contents::Snippet(format!( + "module({}).", + module.to_quoted_string() + )), + position: None, + sort_text: None, + deprecated: false, + }); + true + } else { + false + } + } else if "typing".starts_with(attr_name.text()) { + acc.push(Completion { + kind: Kind::Attribute, + label: "-typing([eqwalizer]).".to_string(), + contents: Contents::Snippet("typing([eqwalizer]).".to_string()), + position: None, + sort_text: None, + deprecated: false, + }); + true + } else { + false + } + } + // A common VSCode extension already has snippets for most attributes, so no need to include those here + _ => false, + } +} + +#[cfg(test)] +mod test { + use expect_test::expect; + use expect_test::Expect; + + use crate::tests::get_completions; + use crate::tests::render_completions; + + fn check(code: &str, trigger: Option, expect: Expect) { + let completions = get_completions(code, trigger); + let actual = &render_completions(completions); + expect.assert_eq(actual); + } + + #[test] + fn test_user_defined_behaviors() { + assert!(serde_json::to_string(&lsp_types::CompletionItemKind::INTERFACE).unwrap() == "8"); + + check( + r#" + //- /src/sample.erl + -module(sample1). + -behavior(gen~). + //- /src/gen_book.erl + -module(gen_book). + -callback bookit(term()) -> term(). + //- /src/gen_look.erl + -module(gen_look). + -callback lookit(term()) -> term(). + //- /src/other_behavior.erl + -module(other_behavior). + -callback other(term()) -> term(). + //- /src/gen_no_behavior.erl + % should not show up in completions + -module(gen_no_behavior). + "#, + None, + expect![[r#" + {label:gen_book, kind:Behavior, contents:SameAsLabel, position:None} + {label:gen_look, kind:Behavior, contents:SameAsLabel, position:None}"#]], + ); + } + + #[test] + fn test_otp_behaviors() { + check( + r#" +//- /src/sample1.erl +-module(sample1). +-behavior(gen~). +//- /opt/lib/stdlib-3.17/src/gen_server.erl otp_app:/opt/lib/stdlib-3.17 +-module(gen_server). +-callback init(term()) -> term(). +"#, + None, + expect!["{label:gen_server, kind:Behavior, contents:SameAsLabel, position:None}"], + ); + } + + #[test] + fn test_error_recovery() { + check( + r#" + //- /src/sample.erl + -module(sample1). + % U.S. English + -behavior(gen~ + //- /src/gen_book.erl + -module(gen_book). + -callback bookit(term()) -> term(). + "#, + None, + expect!["{label:gen_book, kind:Behavior, contents:SameAsLabel, position:None}"], + ); + + check( + r#" + //- /src/sample.erl + -module(sample1). + % U.K. English + -behaviour(gen~ + //- /src/gen_book.erl + -module(gen_book). + -callback bookit(term()) -> term(). + "#, + None, + expect!["{label:gen_book, kind:Behavior, contents:SameAsLabel, position:None}"], + ); + } + + #[test] + fn test_typing_attribute() { + check( + r#" + -module(sample). + -typ~ + "#, + None, + expect![[ + r#"{label:-typing([eqwalizer])., kind:Attribute, contents:Snippet("typing([eqwalizer])."), position:None}"# + ]], + ); + } + + #[test] + fn test_module_attribute() { + check( + r#" + -mod~ + "#, + None, + expect![[ + r#"{label:-module(main)., kind:Attribute, contents:Snippet("module(main)."), position:None}"# + ]], + ); + } + + #[test] + fn test_module_attribute_hyphen() { + check( + r#" + //- /src/my-module.erl + -mod~ + "#, + None, + expect![[ + r#"{label:-module('my-module')., kind:Attribute, contents:Snippet("module('my-module')."), position:None}"# + ]], + ); + } + + #[test] + fn test_module_attribute_at() { + check( + r#" + //- /src/my@module.erl + -mod~ + "#, + None, + expect![[ + r#"{label:-module(my@module)., kind:Attribute, contents:Snippet("module(my@module)."), position:None}"# + ]], + ); + } + + #[test] + fn test_module_attribute_underscore() { + check( + r#" + //- /src/my_module.erl + -mod~ + "#, + None, + expect![[ + r#"{label:-module(my_module)., kind:Attribute, contents:Snippet("module(my_module)."), position:None}"# + ]], + ); + } + + #[test] + fn test_module_attribute_uppercase() { + check( + r#" + //- /src/Module.erl + -mod~ + "#, + None, + expect![[ + r#"{label:-module('Module')., kind:Attribute, contents:Snippet("module('Module')."), position:None}"# + ]], + ); + } + + #[test] + fn test_module_attribute_uppercase_middle() { + check( + r#" + //- /src/moDule.erl + -mod~ + "#, + None, + expect![[ + r#"{label:-module(moDule)., kind:Attribute, contents:Snippet("module(moDule)."), position:None}"# + ]], + ); + } +} diff --git a/crates/ide_completion/src/ctx.rs b/crates/ide_completion/src/ctx.rs new file mode 100644 index 0000000000..c26bf25baf --- /dev/null +++ b/crates/ide_completion/src/ctx.rs @@ -0,0 +1,629 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_syntax; +use elp_syntax::algo; +use elp_syntax::ast; +use elp_syntax::match_ast; +use elp_syntax::ted::Element; +use elp_syntax::AstNode; +use elp_syntax::NodeOrToken; +use elp_syntax::SyntaxElement; +use elp_syntax::SyntaxKind; +use elp_syntax::SyntaxNode; +use elp_syntax::SyntaxToken; +use elp_syntax::TextSize; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Ctx { + Expr, + Type, + Export, + ExportType, + Other, +} + +impl Ctx { + pub fn new(node: &SyntaxNode, offset: TextSize) -> Self { + if Self::is_atom_colon(node, offset) && Self::is_expr(node, offset) { + Self::Expr + } else if Self::is_export(node, offset) { + Self::Export + } else if Self::is_export_type(node, offset) { + Self::ExportType + } else if Self::is_attribute(node, offset) { + Self::Other + } else if Self::is_type_level_param(node, offset) || Self::is_pattern(node, offset) { + Self::Other + } else if Self::is_type(node, offset) { + Self::Type + } else if Self::is_expr(node, offset) { + Self::Expr + } else if Self::is_pp_define(node, offset) { + Self::Expr + } else { + Self::Other + } + } + fn is_atom_colon(node: &SyntaxNode, offset: TextSize) -> bool { + // Temporary for T153426323 + let _pctx = stdx::panic_context::enter(format!("\nis_atom_colon")); + if let Some(parent) = algo::ancestors_at_offset(node, offset).next() { + match_ast! { + match parent { + ast::RemoteModule(_) => { + true + }, + _ => false + } + } + } else { + false + } + } + fn is_export(node: &SyntaxNode, offset: TextSize) -> bool { + algo::find_node_at_offset::(node, offset).is_some() + } + fn is_export_type(node: &SyntaxNode, offset: TextSize) -> bool { + algo::find_node_at_offset::(node, offset).is_some() + } + fn is_pp_define(node: &SyntaxNode, offset: TextSize) -> bool { + algo::find_node_at_offset::(node, offset).is_some() + } + fn is_attribute(_node: &SyntaxNode, _offset: TextSize) -> bool { + false + } + fn is_type_level_param(node: &SyntaxNode, offset: TextSize) -> bool { + let head_opt = algo::find_node_at_offset::(node, offset) + .and_then(|type_alias| type_alias.name()) + .or(algo::find_node_at_offset::(node, offset) + .and_then(|opaque| opaque.name())); + head_opt + .map(|head| offset <= head.syntax().text_range().end()) + .unwrap_or_default() + } + fn is_pattern(node: &SyntaxNode, offset: TextSize) -> bool { + // Temporary for T153426323 + let _pctx = stdx::panic_context::enter(format!("\nis_pattern")); + algo::ancestors_at_offset(node, offset).any(|n| { + let is_match = |node: &SyntaxNode| node.text_range() == n.text_range(); + if let Some(parent) = n.parent() { + match_ast! { + match parent { + ast::CatchClause(parent) => { + if let Some(it) = parent.pat() { + return is_match(it.syntax()) + } + }, + ast::FunClause(parent) => { + if let Some(it) = parent.args() { + return is_match(it.syntax()) + } + }, + ast::FunctionClause(parent) => { + if let Some(it) = parent.args() { + return is_match(it.syntax()) + } + }, + ast::MatchExpr(parent) => { + let prev_token = Self::previous_non_trivia_sibling_or_token(parent.syntax()); + if Self::is_in_error(node, offset) { + if let Some(NodeOrToken::Token(token)) = prev_token { + if token.kind() == SyntaxKind::ANON_CASE { + return false; + } + } + } + if let Some(it) = parent.lhs() { + return is_match(it.syntax()) + } + }, + ast::CrClause(parent) => { + if let Some(it) = parent.pat() { + return is_match(it.syntax()) + } + }, + _ => () + } + } + } + false + }) + } + + fn is_expr(node: &SyntaxNode, offset: TextSize) -> bool { + let mut in_expr = true; + // Temporary for T153426323 + let _pctx = stdx::panic_context::enter(format!("\nis_expr")); + let ancestor_offset = algo::ancestors_at_offset(node, offset) + .map(|n| { + if n.kind() == SyntaxKind::TYPE_SIG { + in_expr = false; + }; + n + }) + .take_while(|n| n.kind() != SyntaxKind::SOURCE_FILE) + .last() + .and_then(|n| n.first_token()) + .map(|tok: SyntaxToken| tok.text_range().start()) + .unwrap_or_default(); + if !in_expr { + return false; + } + // Temporary for T153426323 + let _pctx = stdx::panic_context::enter(format!("\nCtx::is_expr")); + if let Some(mut tok) = node.token_at_offset(offset).left_biased() { + if tok.text_range().start() < ancestor_offset { + return false; + } + while let Some(prev) = tok.prev_token() { + tok = prev; + match tok.kind() { + SyntaxKind::ANON_DASH_GT => return true, + _ => (), + } + } + false + } else { + false + } + } + + fn is_type(node: &SyntaxNode, offset: TextSize) -> bool { + // Temporary for T153426323 + let _pctx = stdx::panic_context::enter(format!("\nis_type")); + for n in algo::ancestors_at_offset(node, offset) { + match_ast! { + match n { + ast::Spec(_) => { + return true; + }, + ast::TypeName(_) => { + return false; + }, + ast::TypeAlias(_) => { + return true; + }, + ast::Opaque(_) => { + return true; + }, + ast::FieldType(_) => { + return true; + }, + _ => () + } + }; + } + false + } + fn is_in_error(node: &SyntaxNode, offset: TextSize) -> bool { + // Temporary for T153426323 + let _pctx = stdx::panic_context::enter(format!("\nis_in_error")); + algo::ancestors_at_offset(node, offset).any(|n| n.kind() == SyntaxKind::ERROR) + } + fn previous_non_trivia_sibling_or_token(node: &SyntaxNode) -> Option { + let mut sot = node.prev_sibling_or_token(); + while let Some(NodeOrToken::Token(inner)) = sot { + if !inner.kind().is_trivia() { + return Some(inner.syntax_element()); + } else { + sot = inner.prev_sibling_or_token(); + } + } + None + } +} + +/// Tests of internals, delete when autocomplete is full-featured T126163525 +#[cfg(test)] +mod ctx_tests { + use elp_ide_db::elp_base_db::fixture::WithFixture; + use elp_ide_db::elp_base_db::FilePosition; + use elp_ide_db::RootDatabase; + use elp_syntax::AstNode; + use hir::Semantic; + + use crate::Ctx; + + fn ctx(code: &str) -> Ctx { + let (db, FilePosition { file_id, offset }) = RootDatabase::with_position(code); + let sema = Semantic::new(&db); + let parsed = sema.parse(file_id); + let node = parsed.value.syntax(); + Ctx::new(node, offset) + } + + #[test] + fn expr_ctx() { + assert_eq!( + ctx(r#" + -module(sample). + test() -> + ~X. + "#), + Ctx::Expr + ); + + assert_eq!( + ctx(r#" + -module(sample). + test() -> + case 1 of. + 1 -> ~2 + end. + "#), + Ctx::Expr, + ); + + assert_eq!( + ctx(r#" + -module(sample). + test() -> + fun(_) -> ~X end. + "#), + Ctx::Expr, + ); + + assert_eq!( + ctx(r#" + -module(sample). + test() -> + try 1 + of + 1 -> X~ + catch + _:_ -> ok + catch + _:_ -> ok + end. + "#), + Ctx::Expr + ); + + assert_eq!( + ctx(r#" + -module(sample). + main(_) -> + #{(maps:from_list([~])) => 3}. + "#), + Ctx::Expr + ); + } + + #[test] + fn expr_ctx_2() { + assert_eq!( + ctx(r#" + -module(completion). + + start() -> + lists:~ + ok = preload_modules(), + ok. + "#), + Ctx::Expr // Ctx::Other + ); + } + + #[test] + fn ctx_pattern() { + assert_eq!( + ctx(r#" + -module(sample). + test(Y, X) -> + ~Y = X. + "#), + Ctx::Other, + ); + + assert_eq!( + ctx(r#" + -module(sample). + test(X) -> + case rand:uniform(1) of + {X~} -> true + end. + "#), + Ctx::Other, + ); + + assert_eq!( + ctx(r#" + -module(sample). + test(X) -> + fun(X~) -> 1 end. + "#), + Ctx::Other, + ); + + assert_eq!( + ctx(r#" + -module(sample). + test() -> + receive + [X~] -> true + end. + "#), + Ctx::Other, + ); + + assert_eq!( + ctx(r#" + -module(sample). + test() -> + try [1] + of + [X~] -> true + catch + _:_ -> ok + end. + "#), + Ctx::Other, + ); + + assert_eq!( + ctx(r#" + -module(sample). + test(X) -> + if + X~ -> ok + true -> error + end. + + "#), + Ctx::Expr, + ); + + assert_eq!( + ctx(r#" + -module(sample). + test(X~) -> + ok. + "#), + Ctx::Other, + ); + + assert_eq!( + ctx(r#" + -module(sample). + test(Y, X) -> + try ok of + X~ -> + + "#), + Ctx::Expr, + ); + + assert_eq!( + ctx(r#" + -module(sample). + test(Y, X) -> + try ok of + ok -> ok + catch + X~ -> ok + "#), + Ctx::Other, + ); + } + + #[test] + // Known cases where error recovery for detecting context is inaccurate. + // AST-based techniques may be more accurate, see D39766695 for details. + fn ctx_pattern_error_recovery_wip() { + assert_eq!( + ctx(r#" + -module(sample). + test(Y, X) -> + try ok of + X~ -> + + "#), + // should be Ctx::Other + Ctx::Expr, + ); + + assert_eq!( + ctx(r#" + -module(sample). + test(Y, X) -> + try ok of + ok -> ok + catch + X~ + "#), + // should be Ctx::Other + Ctx::Expr, + ); + } + + #[test] + fn test_type_param_ctx() { + assert_eq!( + ctx(r#" + -module(sample). + -type ty(s~) :: ok. + "#), + Ctx::Other + ); + } + + #[test] + fn test_export_ctx() { + assert_eq!( + ctx(r#" + -module(sample). + -export([ + f~ + ]) + "#), + Ctx::Export + ); + } + + #[test] + fn test_export_type_ctx() { + assert_eq!( + ctx(r#" + -module(sample). + -export_type([ + t~ + ]) + "#), + Ctx::ExportType + ); + } + + #[test] + fn test_type_ctx() { + assert_eq!( + ctx(r#" + -module(sample). + -spec test() -> ~ + test() -> ok. + "#), + Ctx::Type + ); + + assert_eq!( + ctx(r#" + -module(sample). + -spec test() -> o~k + test() -> ok. + "#), + Ctx::Type + ); + + assert_eq!( + ctx(r#" + -module(sample). + -spec test(o~) -> ok. + test() -> ok. + "#), + Ctx::Type + ); + + assert_eq!( + ctx(r#" + -module(sample). + -record(foo, {field1, field2 :: X~}). + "#), + Ctx::Type + ); + + assert_eq!( + ctx(r#" + -module(sample). + -opaque test() :: ~. + "#), + Ctx::Type + ); + + assert_eq!( + ctx(r#" + -module(sample). + -type test() :: m~ + "#), + Ctx::Type + ); + + assert_eq!( + ctx(r#" + -module(sample). + -spec test() -> ~ok. + "#), + Ctx::Type + ); + } + + #[test] + fn test_ctx_error_recovery() { + assert_eq!( + ctx(r#" + -module(sample). + test() -> + ~ + "#), + Ctx::Expr + ); + + assert_eq!( + ctx(r#" + -module(sample). + test() -> + X + ~ + "#), + Ctx::Expr, + ); + + assert_eq!( + ctx(r#" + -module(sample). + test() -> + X + ~. + "#), + Ctx::Expr, + ); + + assert_eq!( + ctx(r#" + -module(sample). + test() -> + case rand:uniform(1) of + 1 -> ~X + + "#), + Ctx::Expr, + ); + + assert_eq!( + ctx(r#" + -module(sample). + test() -> + (erlang:term_to_binary(~ + + "#), + Ctx::Expr, + ); + + assert_eq!( + ctx(r#" + -module(sample). + test() -> + (erlang:term_to_binary(~. + + "#), + Ctx::Expr, + ); + + assert_eq!( + ctx(r#" + -module(sample). + -type ty() :: ~ + "#), + Ctx::Other + ); + + assert_eq!( + ctx(r#" + -module(sample). + -type ty() :: l~. + "#), + Ctx::Type + ); + + assert_eq!( + ctx(r#" + -module(sample). + -record(rec, {field = lists:map(fun(X) -> X + 1 end, [1, ~])}). + "#), + Ctx::Expr, + ); + } +} diff --git a/crates/ide_completion/src/export_functions.rs b/crates/ide_completion/src/export_functions.rs new file mode 100644 index 0000000000..4f2b2bb4ff --- /dev/null +++ b/crates/ide_completion/src/export_functions.rs @@ -0,0 +1,97 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_syntax::algo; +use elp_syntax::ast; +use elp_syntax::AstNode; + +use crate::helpers; +use crate::Args; +use crate::Completion; +use crate::Kind; + +pub(crate) fn add_completions( + acc: &mut Vec, + Args { + file_position, + parsed, + sema, + .. + }: &Args, +) { + let node = parsed.value.syntax(); + let prefix = &match algo::find_node_at_offset::(node, file_position.offset) { + Some(fa) => { + let completion_needed = match fa.arity() { + Some(arity) => arity.value().is_none(), + None => true, + }; + + if !completion_needed { + return; + } + fa.fun().and_then(|name| name.text()).unwrap_or_default() + } + None => { + // T126163640 / T125984246 + // When we have better error recovery, delete this branch + node.token_at_offset(file_position.offset) + .peekable() + .peek() + .map(|token| token.text().to_string()) + .unwrap_or_default() + } + }; + + let def_map = sema.def_map(file_position.file_id); + let completions = def_map + .get_functions() + .keys() + .filter_map(|na| helpers::name_slash_arity_completion(na, prefix, Kind::Function)); + + acc.extend(completions); +} + +#[cfg(test)] +mod test { + use expect_test::expect; + use expect_test::Expect; + + use crate::tests::get_completions; + use crate::tests::render_completions; + + fn check(code: &str, trigger_character: Option, expect: Expect) { + let completions = get_completions(code, trigger_character); + let actual = &render_completions(completions); + expect.assert_eq(actual); + } + + #[test] + fn test_exports() { + assert!(serde_json::to_string(&lsp_types::CompletionItemKind::FUNCTION).unwrap() == "3"); + + check( + r#" + -module(sample). + -export([ + foo~ + ]). + foo() -> ok. + foo(X) -> X. + foon() -> ok. + bar() -> ok. + "#, + None, + expect![[r#" + {label:foo/0, kind:Function, contents:SameAsLabel, position:None} + {label:foo/1, kind:Function, contents:SameAsLabel, position:None} + {label:foon/0, kind:Function, contents:SameAsLabel, position:None}"#]], + ); + } +} diff --git a/crates/ide_completion/src/export_types.rs b/crates/ide_completion/src/export_types.rs new file mode 100644 index 0000000000..9258dec987 --- /dev/null +++ b/crates/ide_completion/src/export_types.rs @@ -0,0 +1,97 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_syntax::algo; +use elp_syntax::ast; +use elp_syntax::AstNode; + +use crate::helpers; +use crate::Args; +use crate::Completion; +use crate::Kind; + +pub(crate) fn add_completions( + acc: &mut Vec, + Args { + file_position, + parsed, + sema, + .. + }: &Args, +) { + let node = parsed.value.syntax(); + let prefix = &match algo::find_node_at_offset::(node, file_position.offset) { + Some(fa) => { + let completion_needed = match fa.arity() { + Some(arity) => arity.value().is_none(), + None => true, + }; + + if !completion_needed { + return; + } + fa.fun().and_then(|name| name.text()).unwrap_or_default() + } + None => { + // T126163640 / T125984246 + // When we have better error recovery, delete this branch + node.token_at_offset(file_position.offset) + .peekable() + .peek() + .map(|token| token.text().to_string()) + .unwrap_or_default() + } + }; + + let def_map = sema.def_map(file_position.file_id); + let completions = def_map + .get_types() + .into_iter() + .filter_map(|(na, _)| helpers::name_slash_arity_completion(na, prefix, Kind::Type)); + + acc.extend(completions); +} + +#[cfg(test)] +mod test { + use expect_test::expect; + use expect_test::Expect; + + use crate::tests::get_completions; + use crate::tests::render_completions; + + fn check(code: &str, trigger_character: Option, expect: Expect) { + let completions = get_completions(code, trigger_character); + let actual = &render_completions(completions); + expect.assert_eq(actual); + } + + #[test] + fn test_exports() { + assert!(serde_json::to_string(&lsp_types::CompletionItemKind::INTERFACE).unwrap() == "8"); + + check( + r#" + -module(sample). + -export_type([ + foo~ + ]). + -type foo() :: ok. + -opaque foo(X) :: X. + -type foon() :: ok. + -type bar() :: ok. + "#, + None, + expect![[r#" + {label:foo/0, kind:Type, contents:SameAsLabel, position:None} + {label:foo/1, kind:Type, contents:SameAsLabel, position:None} + {label:foon/0, kind:Type, contents:SameAsLabel, position:None}"#]], + ); + } +} diff --git a/crates/ide_completion/src/functions.rs b/crates/ide_completion/src/functions.rs new file mode 100644 index 0000000000..d37b1be55d --- /dev/null +++ b/crates/ide_completion/src/functions.rs @@ -0,0 +1,692 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_base_db::FileId; +use elp_base_db::FilePosition; +use elp_syntax::AstNode; +use hir::FunctionDef; +use hir::NameArity; +use hir::Semantic; + +use crate::helpers; +use crate::Args; +use crate::Completion; +use crate::Contents; +use crate::DoneFlag; +use crate::Kind; + +pub(crate) fn add_completions( + acc: &mut Vec, + Args { + sema, + trigger, + file_position, + previous_tokens, + .. + }: &Args, +) -> DoneFlag { + use elp_syntax::SyntaxKind as K; + let default = vec![]; + let previous_tokens: &[_] = previous_tokens.as_ref().unwrap_or(&default); + match previous_tokens { + [.., (K::ANON_FUN, _), (K::ATOM, function_prefix)] if trigger.is_none() => { + let def_map = sema.def_map(file_position.file_id); + + let completions = def_map.get_functions().keys().filter_map(|na| { + helpers::name_slash_arity_completion(na, function_prefix.text(), Kind::Function) + }); + acc.extend(completions); + true + } + // fun mod:function_name_prefix~ + [ + .., + (K::ANON_FUN, _), + (K::ATOM, module_name), + (K::ANON_COLON, _), + (K::ATOM, function_prefix), + ] if matches!(trigger, Some(':') | None) => { + if let Some(module) = + sema.resolve_module_name(file_position.file_id, module_name.text()) + { + let def_map = sema.def_map(module.file.file_id); + let completions = def_map + .get_exported_functions() + .into_iter() + .filter_map(|na| { + helpers::name_slash_arity_completion( + na, + function_prefix.text(), + Kind::Function, + ) + }); + acc.extend(completions); + true + } else { + false + } + } + // mod:function_name_prefix~ + [ + .., + (K::ATOM, module), + (K::ANON_COLON, _), + (K::ATOM, name_prefix), + ] if matches!(trigger, Some(':') | None) => { + complete_remote_function_call( + sema, + file_position.file_id, + module.text(), + name_prefix.text(), + acc, + ); + true + } + // mod: + [.., (K::ATOM, module), (K::ANON_COLON, _)] if matches!(trigger, Some(':') | None) => { + complete_remote_function_call(sema, file_position.file_id, module.text(), "", acc); + true + } + // foo + [.., (K::ATOM, function_prefix)] if trigger.is_none() => { + let def_map = sema.def_map(file_position.file_id); + let completions = def_map + .get_functions() + .keys() + .filter(|na| na.name().starts_with(function_prefix.text())) + .map(|na| { + let function_name = na.name(); + let def = def_map.get_function(na).unwrap(); + let args = def + .function + .param_names + .iter() + .enumerate() + .map(|(i, param_name)| { + let n = i + 1; + format!("${{{n}:{param_name}}}") + }) + .collect::>() + .join(", "); + let fun_decl_ast = def.source(sema.db.upcast()); + let deprecated = def_map.is_deprecated(na); + Completion { + label: na.to_string(), + kind: Kind::Function, + contents: Contents::Snippet(format!("{function_name}({args})")), + position: Some(FilePosition { + file_id: def.file.file_id, + offset: fun_decl_ast.syntax().text_range().start(), + }), + sort_text: None, + deprecated, + } + }); + + acc.extend(completions); + false + } + _ => false, + } +} + +fn complete_remote_function_call<'a>( + sema: &'a Semantic, + from_file: FileId, + module_name: &'a str, + fun_prefix: &'a str, + acc: &mut Vec, +) { + || -> Option<_> { + let module = sema.resolve_module_name(from_file, module_name)?; + let def_map = sema.def_map(module.file.file_id); + let completions = def_map + .get_exported_functions() + .into_iter() + .filter_map(|na| { + let def = def_map.get_function(na); + let position = def.map(|def| { + let fun_decl_ast = def.source(sema.db.upcast()); + FilePosition { + file_id: def.file.file_id, + offset: fun_decl_ast.syntax().text_range().start(), + } + }); + let deprecated = def_map.is_deprecated(na); + name_arity_to_call_completion(def, na, fun_prefix, position, deprecated) + }); + acc.extend(completions); + Some(()) + }(); +} + +fn name_arity_to_call_completion( + def: Option<&FunctionDef>, + na: &NameArity, + prefix: &str, + position: Option, + deprecated: bool, +) -> Option { + if na.name().starts_with(prefix) { + let contents = def.map_or(helpers::format_call(na.name(), na.arity()), |def| { + let arg_names = def + .function + .param_names + .iter() + .enumerate() + .map(|(i, param)| format!("${{{}:{}}}", i + 1, param)) + .collect::>() + .join(", "); + Contents::Snippet(format!("{}({})", na.name(), arg_names)) + }); + Some(Completion { + label: na.to_string(), + kind: Kind::Function, + contents, + position, + sort_text: None, + deprecated, + }) + } else { + None + } +} + +#[cfg(test)] +mod test { + use expect_test::expect; + use expect_test::Expect; + + use crate::tests::get_completions; + use crate::tests::render_completions; + use crate::Kind; + + // keywords are filtered out to avoid noise + fn check(code: &str, trigger_character: Option, expect: Expect) { + let completions = get_completions(code, trigger_character) + .into_iter() + .filter(|c| c.kind != Kind::Keyword) + .collect(); + let actual = &render_completions(completions); + expect.assert_eq(actual); + } + + #[test] + fn test_remote_calls_with_trigger() { + assert!(serde_json::to_string(&lsp_types::CompletionItemKind::FUNCTION).unwrap() == "3"); + + check( + r#" + //- /src/sample1.erl + -module(sample1). + local() -> + sample2:~. + //- /src/sample2.erl + -module(sample2). + -export([foo/0]). + -export([foon/2]). + -export([bar/2]). + foo() -> ok. + foon(A, B) -> ok. + bar(A, B, C) -> ok. + "#, + Some(':'), + expect![[r#" + {label:bar/2, kind:Function, contents:Snippet("bar(${1:Arg1}, ${2:Arg2})"), position:None} + {label:foo/0, kind:Function, contents:Snippet("foo()"), position:Some(FilePosition { file_id: FileId(1), offset: 73 })} + {label:foon/2, kind:Function, contents:Snippet("foon(${1:A}, ${2:B})"), position:Some(FilePosition { file_id: FileId(1), offset: 86 })}"#]], + ); + + check( + r#" + //- /src/sample1.erl + -module(sample1). + local() -> + sample2:f~. + //- /src/sample2.erl + -module(sample2). + -export([foo/0]). + -export([foon/2]). + -export([bar/2]). + foo() -> ok. + foon(A, B) -> ok. + bar(A, B, C) -> ok. + "#, + Some(':'), + expect![[r#" + {label:foo/0, kind:Function, contents:Snippet("foo()"), position:Some(FilePosition { file_id: FileId(1), offset: 73 })} + {label:foon/2, kind:Function, contents:Snippet("foon(${1:A}, ${2:B})"), position:Some(FilePosition { file_id: FileId(1), offset: 86 })}"#]], + ); + } + + #[test] + fn test_remote_calls_no_trigger() { + assert!(serde_json::to_string(&lsp_types::CompletionItemKind::MODULE).unwrap() == "9"); + + check( + r#" + //- /src/sample1.erl + -module(sample1). + local() -> + sample2:~. + //- /src/sample2.erl + -module(sample2). + -export([foo/0]). + -export([foon/2]). + -export([bar/2]). + foo() -> ok. + foon(A, B) -> ok. + bar(A, B, C) -> ok. + "#, + None, + expect![[r#" + {label:bar/2, kind:Function, contents:Snippet("bar(${1:Arg1}, ${2:Arg2})"), position:None} + {label:foo/0, kind:Function, contents:Snippet("foo()"), position:Some(FilePosition { file_id: FileId(1), offset: 73 })} + {label:foon/2, kind:Function, contents:Snippet("foon(${1:A}, ${2:B})"), position:Some(FilePosition { file_id: FileId(1), offset: 86 })}"#]], + ); + + check( + r#" + //- /src/sample1.erl + -module(sample1). + local() -> + sample2:f~. + //- /src/sample2.erl + -module(sample2). + -export([foo/0]). + -export([foon/2]). + -export([bar/2]). + foo() -> ok. + foon(A, B) -> ok. + bar(A, B, C) -> ok. + "#, + None, + expect![[r#" + {label:foo/0, kind:Function, contents:Snippet("foo()"), position:Some(FilePosition { file_id: FileId(1), offset: 73 })} + {label:foon/2, kind:Function, contents:Snippet("foon(${1:A}, ${2:B})"), position:Some(FilePosition { file_id: FileId(1), offset: 86 })}"#]], + ); + } + #[test] + fn test_remote_calls_deprecated() { + assert!(serde_json::to_string(&lsp_types::CompletionItemKind::MODULE).unwrap() == "9"); + + check( + r#" + //- /src/sample1.erl + -module(sample1). + local() -> + sample2:~. + //- /src/sample2.erl + -module(sample2). + -deprecated({foon, 2, "Don't use me!"}). + -export([foo/0]). + -export([foon/2]). + -export([bar/2]). + foo() -> ok. + foon(A, B) -> ok. + bar(A, B, C) -> ok. + "#, + None, + expect![[r#" + {label:bar/2, kind:Function, contents:Snippet("bar(${1:Arg1}, ${2:Arg2})"), position:None} + {label:foo/0, kind:Function, contents:Snippet("foo()"), position:Some(FilePosition { file_id: FileId(1), offset: 114 })} + {label:foon/2, kind:Function, contents:Snippet("foon(${1:A}, ${2:B})"), position:Some(FilePosition { file_id: FileId(1), offset: 127 }), deprecated:true}"#]], + ); + } + + #[test] + fn test_remote_calls_multiple_deprecated() { + assert!(serde_json::to_string(&lsp_types::CompletionItemKind::MODULE).unwrap() == "9"); + + check( + r#" + //- /src/sample1.erl + -module(sample1). + local() -> + sample2:~. + //- /src/sample2.erl + -module(sample2). + -deprecated([{foon, 2, "Don't use me!"}, {foo, 0}]). + -export([foo/0]). + -export([foon/2]). + -export([bar/2]). + foo() -> ok. + foon(A, B) -> ok. + bar(A, B, C) -> ok. + "#, + None, + expect![[r#" + {label:bar/2, kind:Function, contents:Snippet("bar(${1:Arg1}, ${2:Arg2})"), position:None} + {label:foo/0, kind:Function, contents:Snippet("foo()"), position:Some(FilePosition { file_id: FileId(1), offset: 126 }), deprecated:true} + {label:foon/2, kind:Function, contents:Snippet("foon(${1:A}, ${2:B})"), position:Some(FilePosition { file_id: FileId(1), offset: 139 }), deprecated:true}"#]], + ); + } + + #[test] + fn test_remote_module_deprecated() { + assert!(serde_json::to_string(&lsp_types::CompletionItemKind::MODULE).unwrap() == "9"); + + check( + r#" + //- /src/sample1.erl + -module(sample1). + local() -> + sample2:~. + //- /src/sample2.erl + -module(sample2). + -deprecated(module). + -export([foo/0]). + -export([bar/2]). + foo() -> ok. + bar(A, B, C) -> ok. + "#, + None, + expect![[r#" + {label:bar/2, kind:Function, contents:Snippet("bar(${1:Arg1}, ${2:Arg2})"), position:None, deprecated:true} + {label:foo/0, kind:Function, contents:Snippet("foo()"), position:Some(FilePosition { file_id: FileId(1), offset: 75 }), deprecated:true}"#]], + ); + } + + #[test] + fn test_remote_module_incorrect_deprecation() { + assert!(serde_json::to_string(&lsp_types::CompletionItemKind::MODULE).unwrap() == "9"); + + check( + r#" + //- /src/sample1.erl + -module(sample1). + local() -> + sample2:~. + //- /src/sample2.erl + -module(sample2). + -deprecated(incorrect). + -export([foo/0]). + foo() -> ok. + "#, + None, + expect![[r#" + {label:foo/0, kind:Function, contents:Snippet("foo()"), position:Some(FilePosition { file_id: FileId(1), offset: 60 })}"#]], + ); + } + + #[test] + fn test_no_function_completions() { + assert!(serde_json::to_string(&lsp_types::CompletionItemKind::MODULE).unwrap() == "9"); + check( + r#" + //- /src/sample1.erl + -module(sample1). + local() -> + sample2:bar(a, s~) + //- /src/sample2.erl + -module(sample2). + -export([bar/0]). + -export([bar/2]). + -export([baz/3]). + bar() -> ok. + bar(A, B) -> ok. + baz(A, B, C) -> ok. + "#, + None, + expect![[r#" + {label:sample1, kind:Module, contents:SameAsLabel, position:None} + {label:sample2, kind:Module, contents:SameAsLabel, position:None}"#]], + ); + } + + #[test] + fn test_local_calls_1() { + check( + r#" + -module(sample1). + foo() -> id(b~). + id(X) -> X. + bar() -> ok. + bar(X) -> X. + baz(X, _) -> X. + "#, + None, + expect![[r#" + {label:bar/0, kind:Function, contents:Snippet("bar()"), position:Some(FilePosition { file_id: FileId(0), offset: 46 })} + {label:bar/1, kind:Function, contents:Snippet("bar(${1:X})"), position:Some(FilePosition { file_id: FileId(0), offset: 59 })} + {label:baz/2, kind:Function, contents:Snippet("baz(${1:X}, ${2:Arg2})"), position:Some(FilePosition { file_id: FileId(0), offset: 72 })}"#]], + ); + } + + #[test] + fn test_local_calls_2() { + check( + r#" + -module(sample1). + foo() -> + b~. + bar() -> ok. + baz(X) -> X. + "#, + None, + expect![[r#" + {label:bar/0, kind:Function, contents:Snippet("bar()"), position:Some(FilePosition { file_id: FileId(0), offset: 34 })} + {label:baz/1, kind:Function, contents:Snippet("baz(${1:X})"), position:Some(FilePosition { file_id: FileId(0), offset: 47 })}"#]], + ); + } + + #[test] + fn test_local_calls_3() { + check( + r#" + -module(sample). + test() -> + try 1 + of + 1 -> ok + catch + b~ -> ok + end. + bar() -> ok. + baz(X) -> X. + "#, + None, + expect![""], + ); + } + + #[test] + fn test_local_call_arg_names() { + check( + r#" + //- /src/sample1.erl + -module(sample1). + foo(X, Y, Y, _, _, {W, 3}, [H], [], 99, Z, _) -> ok. + main(_) -> + fo~ + "#, + None, + expect![[ + r#"{label:foo/11, kind:Function, contents:Snippet("foo(${1:X}, ${2:Y}, ${3:Y}, ${4:Arg4}, ${5:Arg5}, ${6:Arg6}, ${7:Arg7}, ${8:Arg8}, ${9:Arg9}, ${10:Z}, ${11:Arg11})"), position:Some(FilePosition { file_id: FileId(0), offset: 18 })}"# + ]], + ); + } + + #[test] + fn test_remote_fun_exprs_with_trigger() { + check( + r#" + //- /src/sample1.erl + -module(sample1). + main(_) -> + lists:map(fun sample2:foo/~), []) + foo(_, _, _) -> ok. + //- /src/sample2.erl + -module(sample2). + -export([foo/0]). + -export([foo/2]). + -export([bar/2]). + foo() -> ok. + foo(A, B) -> ok. + bar(A, B) -> ok. + "#, + Some(':'), + expect![""], + ); + } + + #[test] + fn test_local_fun_exprs_with_trigger() { + check( + r#" + //- /src/sample1.erl + -module(sample1). + foo() -> ok. + main(_) -> + fun fo~ + "#, + Some(':'), + expect![""], + ); + } + + #[test] + fn test_local_fun_exprs_no_trigger() { + check( + r#" + //- /src/sample1.erl + -module(sample1). + foo() -> ok. + main(_) -> + fun fo~ + "#, + None, + expect!["{label:foo/0, kind:Function, contents:SameAsLabel, position:None}"], + ); + } + + #[test] + fn function_error_recovery() { + check( + r#" + -module(sample1). + foo() -> + b~ + % bar/0 should appear in autocompletes but doesn't yet + bar() -> ok. + baz(X) -> X. + "#, + None, + expect![[ + r#"{label:baz/1, kind:Function, contents:Snippet("baz(${1:X})"), position:Some(FilePosition { file_id: FileId(0), offset: 101 })}"# + ]], + ); + check( + r#" + //- /src/sample1.erl + -module(sample1). + local() -> + lists:map(fun sample2:f~, []) + //- /src/sample2.erl + -module(sample2). + -export([foo/0]). + foo() -> ok. + "#, + Some(':'), + expect!["{label:foo/0, kind:Function, contents:SameAsLabel, position:None}"], + ); + } + + #[test] + fn test_local_and_remote() { + check( + r#" + //- /src/sample1.erl + -module(sample1). + samba() -> ok. + main(_) -> + lists:map(fun sa~), []) + //- /src/sample2.erl + -module(sample2). + -export([foo/0]). + foo() -> ok. + + "#, + None, + expect!["{label:samba/0, kind:Function, contents:SameAsLabel, position:None}"], + ); + } + + #[test] + fn test_remote_call_broken_case() { + check( + r#" + //- /src/sample1.erl + -module(sample1). + test() -> + case sample2:m~ + A = 42, + //- /src/sample2.erl + -module(sample2). + -export([main/0]). + main() -> ok. + "#, + None, + expect![[ + r#"{label:main/0, kind:Function, contents:Snippet("main()"), position:Some(FilePosition { file_id: FileId(1), offset: 37 })}"# + ]], + ); + } + + #[test] + fn test_local_call_broken_case() { + check( + r#" + //- /src/sample1.erl + -module(sample1). + foo() -> ok. + test() -> + case fo~ + A = 42, + "#, + None, + expect![[ + r#"{label:foo/0, kind:Function, contents:Snippet("foo()"), position:Some(FilePosition { file_id: FileId(0), offset: 18 })}"# + ]], + ); + } + + #[test] + fn test_local_call_macro_rhs() { + check( + r#" + -module(main). + -define(MY_MACRO(), my_f~unction()). + my_function() -> ok. + "#, + None, + expect![[ + r#"{label:my_function/0, kind:Function, contents:Snippet("my_function()"), position:Some(FilePosition { file_id: FileId(0), offset: 51 })}"# + ]], + ); + } + + #[test] + fn test_remote_call_header_macro_rhs() { + check( + r#" + //- /include/main.hrl + -define(MY_MACRO(), main:my_f~unction()). + //- /src/main.erl + -module(main). + -export([my_function/0]). + my_function() -> ok. + "#, + None, + expect![[ + r#"{label:my_function/0, kind:Function, contents:Snippet("my_function()"), position:Some(FilePosition { file_id: FileId(1), offset: 41 })}"# + ]], + ); + } +} diff --git a/crates/ide_completion/src/helpers.rs b/crates/ide_completion/src/helpers.rs new file mode 100644 index 0000000000..0850704aa5 --- /dev/null +++ b/crates/ide_completion/src/helpers.rs @@ -0,0 +1,83 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_syntax::ast; +use elp_syntax::ast::ExprMax; +use elp_syntax::match_ast; +use elp_syntax::AstNode; +use elp_syntax::SmolStr; +use elp_syntax::SourceFile; +use elp_syntax::SyntaxKind; +use elp_syntax::TextSize; +use hir::InFile; +use hir::NameArity; + +use crate::Completion; +use crate::Contents; +use crate::Kind; + +pub(crate) fn atom_value(parsed: &InFile, offset: TextSize) -> Option { + let node = parsed.value.syntax(); + // Temporary for T153426323 + let _pctx = stdx::panic_context::enter(format!("\natom_value")); + let token = node.token_at_offset(offset); + let token = parsed.with_value(elp_ide_db::helpers::pick_best_token( + token, + |kind| match kind { + SyntaxKind::ATOM => 2, + _ => 1, + }, + )?); + + let parent = token.value.parent()?; + match_ast! { + match parent { + ast::Atom(a) => { + a.text() + }, + _ => None + } + } +} + +pub(crate) fn format_call(name: &str, arity: u32) -> Contents { + let args = (1..(arity + 1)) + .map(|n| format!("${{{}:Arg{}}}", n, n)) + .collect::>() + .join(", "); + Contents::Snippet(format!("{}({})", name, args)) +} + +pub(crate) fn name_slash_arity_completion( + na: &NameArity, + prefix: &str, + kind: Kind, +) -> Option { + if na.name().starts_with(prefix) { + Some(Completion { + label: na.to_string(), + kind, + contents: Contents::SameAsLabel, + position: None, + sort_text: None, + deprecated: false, + }) + } else { + None + } +} + +pub(crate) fn split_remote(remote: &ast::Remote) -> Option<(ast::Atom, SmolStr)> { + let module_atom = match remote.module()?.module()? { + ExprMax::Atom(atom) => atom, + _ => return None, + }; + let name: SmolStr = remote.fun().and_then(|f| f.name()).unwrap_or_default(); + Some((module_atom, name)) +} diff --git a/crates/ide_completion/src/keywords.rs b/crates/ide_completion/src/keywords.rs new file mode 100644 index 0000000000..21feed162a --- /dev/null +++ b/crates/ide_completion/src/keywords.rs @@ -0,0 +1,252 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use lazy_static::lazy_static; + +use crate::Args; +use crate::Completion; +use crate::Contents; +use crate::DoneFlag; + +lazy_static! { + // adapted from https://github.com/erlang-ls/erlang_ls d067267b906239c883fed6e0f9e69c4eb94dd580 + static ref KEYWORDS: Vec = [ + "case", + "after", + "and", + "andalso", + "band", + "begin", + "bnot", + "bor", + "bsl", + "bsr", + "bxor", + "case", + "catch", + "cond", + "div", + "end", + "fun", + "if", + "let", + "not", + "of", + "or", + "orelse", + "receive", + "rem", + "try", + "when", + "xor" + ].iter().map(|label| Completion{ label: label.to_string(), kind: crate::Kind::Keyword, contents: Contents::SameAsLabel, position: None, sort_text: None, deprecated: false}).collect(); +} + +pub(crate) fn add_completions(acc: &mut Vec, Args { trigger, .. }: &Args) -> DoneFlag { + if trigger.is_some() { + return false; + } + acc.append(&mut KEYWORDS.clone()); + false +} + +#[cfg(test)] +mod test { + use expect_test::expect; + use expect_test::Expect; + + use crate::tests::get_completions; + use crate::tests::render_completions; + + fn check(code: &str, trigger_character: Option, expect: Expect) { + let completions = get_completions(code, trigger_character); + let actual = &render_completions(completions); + expect.assert_eq(actual); + } + + #[test] + fn test_keywords() { + assert_eq!( + serde_json::to_string(&lsp_types::CompletionItemKind::KEYWORD).unwrap(), + "14" + ); + assert_eq!( + serde_json::to_string(&lsp_types::InsertTextFormat::PLAIN_TEXT).unwrap(), + "1" + ); + check( + r#" + -module(sample). + test(X) -> + a~ + "#, + None, + expect![[r#" + {label:after, kind:Keyword, contents:SameAsLabel, position:None} + {label:and, kind:Keyword, contents:SameAsLabel, position:None} + {label:andalso, kind:Keyword, contents:SameAsLabel, position:None} + {label:band, kind:Keyword, contents:SameAsLabel, position:None} + {label:begin, kind:Keyword, contents:SameAsLabel, position:None} + {label:bnot, kind:Keyword, contents:SameAsLabel, position:None} + {label:bor, kind:Keyword, contents:SameAsLabel, position:None} + {label:bsl, kind:Keyword, contents:SameAsLabel, position:None} + {label:bsr, kind:Keyword, contents:SameAsLabel, position:None} + {label:bxor, kind:Keyword, contents:SameAsLabel, position:None} + {label:case, kind:Keyword, contents:SameAsLabel, position:None} + {label:case, kind:Keyword, contents:SameAsLabel, position:None} + {label:catch, kind:Keyword, contents:SameAsLabel, position:None} + {label:cond, kind:Keyword, contents:SameAsLabel, position:None} + {label:div, kind:Keyword, contents:SameAsLabel, position:None} + {label:end, kind:Keyword, contents:SameAsLabel, position:None} + {label:fun, kind:Keyword, contents:SameAsLabel, position:None} + {label:if, kind:Keyword, contents:SameAsLabel, position:None} + {label:let, kind:Keyword, contents:SameAsLabel, position:None} + {label:not, kind:Keyword, contents:SameAsLabel, position:None} + {label:of, kind:Keyword, contents:SameAsLabel, position:None} + {label:or, kind:Keyword, contents:SameAsLabel, position:None} + {label:orelse, kind:Keyword, contents:SameAsLabel, position:None} + {label:receive, kind:Keyword, contents:SameAsLabel, position:None} + {label:rem, kind:Keyword, contents:SameAsLabel, position:None} + {label:try, kind:Keyword, contents:SameAsLabel, position:None} + {label:when, kind:Keyword, contents:SameAsLabel, position:None} + {label:xor, kind:Keyword, contents:SameAsLabel, position:None}"#]], + ); + } + + #[test] + fn test_no_keywords() { + check( + r#" + -module(sample). + test(X) -> + a~ + "#, + Some(':'), + expect![""], + ); + check( + r#" + -module(sample). + test(~X) -> + X. + "#, + None, + expect![""], + ); + + check( + r#" + -module(m~). + "#, + None, + expect![""], + ); + + check( + r#" + -module(m). + -~ + "#, + None, + expect![""], + ); + + check( + r#" + -module(m). + -type foo() :: ~. + "#, + None, + expect![[r#" + {label:foo/0, kind:Type, contents:Snippet("foo()"), position:None} + {label:main, kind:Module, contents:SameAsLabel, position:None}"#]], + ); + } + + #[test] + fn test_keywords_error_recovery() { + check( + r#" + -module(sample). + test(X) -> + X ~ + "#, + None, + expect![[r#" + {label:after, kind:Keyword, contents:SameAsLabel, position:None} + {label:and, kind:Keyword, contents:SameAsLabel, position:None} + {label:andalso, kind:Keyword, contents:SameAsLabel, position:None} + {label:band, kind:Keyword, contents:SameAsLabel, position:None} + {label:begin, kind:Keyword, contents:SameAsLabel, position:None} + {label:bnot, kind:Keyword, contents:SameAsLabel, position:None} + {label:bor, kind:Keyword, contents:SameAsLabel, position:None} + {label:bsl, kind:Keyword, contents:SameAsLabel, position:None} + {label:bsr, kind:Keyword, contents:SameAsLabel, position:None} + {label:bxor, kind:Keyword, contents:SameAsLabel, position:None} + {label:case, kind:Keyword, contents:SameAsLabel, position:None} + {label:case, kind:Keyword, contents:SameAsLabel, position:None} + {label:catch, kind:Keyword, contents:SameAsLabel, position:None} + {label:cond, kind:Keyword, contents:SameAsLabel, position:None} + {label:div, kind:Keyword, contents:SameAsLabel, position:None} + {label:end, kind:Keyword, contents:SameAsLabel, position:None} + {label:fun, kind:Keyword, contents:SameAsLabel, position:None} + {label:if, kind:Keyword, contents:SameAsLabel, position:None} + {label:let, kind:Keyword, contents:SameAsLabel, position:None} + {label:main, kind:Module, contents:SameAsLabel, position:None} + {label:not, kind:Keyword, contents:SameAsLabel, position:None} + {label:of, kind:Keyword, contents:SameAsLabel, position:None} + {label:or, kind:Keyword, contents:SameAsLabel, position:None} + {label:orelse, kind:Keyword, contents:SameAsLabel, position:None} + {label:receive, kind:Keyword, contents:SameAsLabel, position:None} + {label:rem, kind:Keyword, contents:SameAsLabel, position:None} + {label:try, kind:Keyword, contents:SameAsLabel, position:None} + {label:when, kind:Keyword, contents:SameAsLabel, position:None} + {label:xor, kind:Keyword, contents:SameAsLabel, position:None}"#]], + ); + + check( + r#" + -module(sample). + test(X) -> + ~ + "#, + None, + expect![[r#" + {label:after, kind:Keyword, contents:SameAsLabel, position:None} + {label:and, kind:Keyword, contents:SameAsLabel, position:None} + {label:andalso, kind:Keyword, contents:SameAsLabel, position:None} + {label:band, kind:Keyword, contents:SameAsLabel, position:None} + {label:begin, kind:Keyword, contents:SameAsLabel, position:None} + {label:bnot, kind:Keyword, contents:SameAsLabel, position:None} + {label:bor, kind:Keyword, contents:SameAsLabel, position:None} + {label:bsl, kind:Keyword, contents:SameAsLabel, position:None} + {label:bsr, kind:Keyword, contents:SameAsLabel, position:None} + {label:bxor, kind:Keyword, contents:SameAsLabel, position:None} + {label:case, kind:Keyword, contents:SameAsLabel, position:None} + {label:case, kind:Keyword, contents:SameAsLabel, position:None} + {label:catch, kind:Keyword, contents:SameAsLabel, position:None} + {label:cond, kind:Keyword, contents:SameAsLabel, position:None} + {label:div, kind:Keyword, contents:SameAsLabel, position:None} + {label:end, kind:Keyword, contents:SameAsLabel, position:None} + {label:fun, kind:Keyword, contents:SameAsLabel, position:None} + {label:if, kind:Keyword, contents:SameAsLabel, position:None} + {label:let, kind:Keyword, contents:SameAsLabel, position:None} + {label:main, kind:Module, contents:SameAsLabel, position:None} + {label:not, kind:Keyword, contents:SameAsLabel, position:None} + {label:of, kind:Keyword, contents:SameAsLabel, position:None} + {label:or, kind:Keyword, contents:SameAsLabel, position:None} + {label:orelse, kind:Keyword, contents:SameAsLabel, position:None} + {label:receive, kind:Keyword, contents:SameAsLabel, position:None} + {label:rem, kind:Keyword, contents:SameAsLabel, position:None} + {label:try, kind:Keyword, contents:SameAsLabel, position:None} + {label:when, kind:Keyword, contents:SameAsLabel, position:None} + {label:xor, kind:Keyword, contents:SameAsLabel, position:None}"#]], + ); + } +} diff --git a/crates/ide_completion/src/lib.rs b/crates/ide_completion/src/lib.rs new file mode 100644 index 0000000000..0bac978159 --- /dev/null +++ b/crates/ide_completion/src/lib.rs @@ -0,0 +1,206 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::fmt; + +use ctx::Ctx; +use elp_ide_db::elp_base_db::FilePosition; +use elp_ide_db::RootDatabase; +use elp_syntax::AstNode; +use elp_syntax::SourceFile; +use elp_syntax::SyntaxKind; +use elp_syntax::SyntaxNode; +use elp_syntax::SyntaxToken; +use hir::db::MinDefDatabase; +use hir::InFile; +use hir::Semantic; + +type DoneFlag = bool; + +#[cfg(test)] +mod tests; + +mod attributes; +mod ctx; +mod export_functions; +mod export_types; +mod functions; +mod helpers; +mod keywords; +mod macros; +// @fb-only: mod meta_only; +mod modules; +mod records; +mod types; +mod vars; + +/* +For token-based completions, this is the maximum number of previous tokens we consider. +*/ +static MAX_PREVIOUS_TOKENS_LEN: usize = 16; + +#[derive(Clone, Debug, Hash, Eq, PartialEq)] +pub struct Completion { + pub label: String, + pub kind: Kind, + pub contents: Contents, + // The position is used in the 'resolve' phase to look for documentation + pub position: Option, + pub sort_text: Option, + pub deprecated: bool, +} + +impl fmt::Display for Completion { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self.deprecated { + true => write!( + f, + "{{label:{}, kind:{:?}, contents:{:?}, position:{:?}, deprecated:{}}}", + self.label, self.kind, self.contents, self.position, self.deprecated + ), + false => write!( + f, + "{{label:{}, kind:{:?}, contents:{:?}, position:{:?}}}", + self.label, self.kind, self.contents, self.position + ), + } + } +} + +#[derive(Clone, Debug, Hash, Eq, PartialEq)] +pub enum Contents { + SameAsLabel, + String(String), + Snippet(String), +} + +/// More erlangy version of `lsp_types::completion::CompletionItemKind` +#[derive(Copy, Clone, Hash, PartialEq, Eq, Debug)] +pub enum Kind { + Function, + Keyword, + Module, + RecordField, + Type, + Behavior, + Macro, + #[allow(dead_code)] // TODO: T126083972 + Operator, + #[allow(dead_code)] // TODO: T126083980 + Record, + Variable, + Attribute, + AiAssist, +} + +struct Args<'a> { + db: &'a dyn MinDefDatabase, + sema: &'a Semantic<'a>, + parsed: InFile, + trigger: Option, + previous_tokens: Option>, + file_position: FilePosition, +} + +pub fn completions( + db: &RootDatabase, + file_position: FilePosition, + trigger: Option, +) -> Vec { + let sema = &Semantic::new(db); + let parsed = sema.parse(file_position.file_id); + let node = parsed.value.syntax(); + let node_range = node.text_range(); + // Check the failing assert condition for T153426323 + if !(node_range.start() <= file_position.offset && file_position.offset <= node_range.end()) { + let original_token = node.token_at_offset(file_position.offset).left_biased(); + // Confirming this as the origin for T153426323 + log::error!( + "completions:invalid position {:?} for range {:?}, original_token={:?}", + file_position.offset, + node.text_range(), + original_token + ); + return vec![]; + } + let ctx = Ctx::new(node, file_position.offset); + let mut acc = Vec::new(); + let previous_tokens = get_previous_tokens(node, file_position); + let args = &Args { + db, + sema, + parsed, + file_position, + previous_tokens, + trigger, + }; + + match ctx { + Ctx::Expr => { + let _ = macros::add_completions(&mut acc, args) + || records::add_completions(&mut acc, args) + || functions::add_completions(&mut acc, args) + || vars::add_completions(&mut acc, args) + || modules::add_completions(&mut acc, args) + || keywords::add_completions(&mut acc, args); + } + Ctx::Type => { + let _ = macros::add_completions(&mut acc, args) + || types::add_completions(&mut acc, args) + || modules::add_completions(&mut acc, args); + } + Ctx::Export => { + export_functions::add_completions(&mut acc, args); + } + Ctx::ExportType => { + export_types::add_completions(&mut acc, args); + } + Ctx::Other => { + let _ = attributes::add_completions(&mut acc, args) + // @fb-only: || meta_only::add_completions(&mut acc, args) + || vars::add_completions(&mut acc, args); + } + } + // Sort for maintainable snapshot tests: + // sorting isn't necessary for prod because LSP client sorts + acc.sort_by(|c1, c2| c1.label.cmp(&c2.label)); + acc +} + +// Note: in an ideal world, we would not need to use much token-level information +// to get reasonable error-recovery for completions. +// See T154356210 +fn get_previous_tokens( + node: &SyntaxNode, + file_position: FilePosition, +) -> Option> { + // Temporary for T153426323 + let _pctx = stdx::panic_context::enter(format!("\nget_previous_tokens")); + let mut token = node.token_at_offset(file_position.offset).left_biased()?; + let mut tokens = Vec::new(); + + while token.text_range().start() >= 0.into() && tokens.len() < MAX_PREVIOUS_TOKENS_LEN { + let next_opt = token.prev_token(); + if !token.kind().is_trivia() { + tokens.push(token.clone()); + } + if let Some(next) = next_opt { + token = next; + } else { + break; + } + } + Some( + tokens + .into_iter() + .rev() + .map(|tok| (tok.kind(), tok)) + .collect::>(), + ) +} diff --git a/crates/ide_completion/src/macros.rs b/crates/ide_completion/src/macros.rs new file mode 100644 index 0000000000..6798185ef5 --- /dev/null +++ b/crates/ide_completion/src/macros.rs @@ -0,0 +1,219 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_syntax::algo; +use elp_syntax::ast; +use elp_syntax::AstNode; +use hir::known; +use hir::MacroName; +use hir::Name; + +use crate::helpers; +use crate::Args; +use crate::Completion; +use crate::Contents; +use crate::DoneFlag; +use crate::Kind; + +pub(crate) fn add_completions( + acc: &mut Vec, + Args { + file_position, + parsed, + sema, + trigger, + .. + }: &Args, +) -> DoneFlag { + match trigger { + Some('?') | None => (), + _ => return false, + }; + + let node = parsed.value.syntax(); + match algo::find_node_at_offset::(node, file_position.offset) { + None => return false, + Some(call) => { + let prefix = &call.name().map(|n| n.to_string()).unwrap_or_default(); + let def_map = sema.def_map(file_position.file_id); + let user_defined = def_map + .get_macros() + .keys() + .filter(|macro_name| macro_name.name().starts_with(prefix)) + .map(macro_name_to_completion); + + acc.extend(user_defined); + + let built_in = BUILT_IN; + let predefined = built_in + .iter() + .filter(|name| name.starts_with(prefix)) + .map(built_in_macro_name_to_completion); + acc.extend(predefined); + + true + } + } +} + +fn macro_name_to_completion(macro_name: &MacroName) -> Completion { + match macro_name.arity() { + Some(arity) => { + let label = macro_name.to_string(); + let contents = helpers::format_call(macro_name.name(), arity); + Completion { + label, + kind: Kind::Macro, + contents, + position: None, + sort_text: None, + deprecated: false, + } + } + None => Completion { + label: macro_name.to_string(), + kind: Kind::Macro, + contents: Contents::SameAsLabel, + position: None, + sort_text: None, + deprecated: false, + }, + } +} + +fn built_in_macro_name_to_completion(name: &Name) -> Completion { + Completion { + label: name.to_string(), + kind: Kind::Macro, + contents: Contents::SameAsLabel, + position: None, + sort_text: None, + deprecated: false, + } +} + +const BUILT_IN: [Name; 8] = [ + known::FILE, + known::FUNCTION_NAME, + known::FUNCTION_ARITY, + known::LINE, + known::MODULE, + known::MODULE_STRING, + known::MACHINE, + known::OTP_RELEASE, +]; + +#[cfg(test)] +mod test { + use expect_test::expect; + use expect_test::Expect; + + use crate::tests::get_completions; + use crate::tests::render_completions; + + fn check(code: &str, trigger: Option, expect: Expect) { + let completions = get_completions(code, trigger); + let actual = &render_completions(completions); + expect.assert_eq(actual); + } + + #[test] + fn test_user_defined_macros() { + assert!(serde_json::to_string(&lsp_types::CompletionItemKind::CONSTANT).unwrap() == "21"); + + check( + r#" + -module(sample1). + -define(FOO, 1). + -define(FOO(), 1). + -define(FOO(X, Y, Z), {X, Y, Z}). + -define(FOB, 1). + -define(BAR, 1). + foo() -> ?FO~ + "#, + Some('?'), + expect![[r#" + {label:FOB, kind:Macro, contents:SameAsLabel, position:None} + {label:FOO, kind:Macro, contents:SameAsLabel, position:None} + {label:FOO/0, kind:Macro, contents:Snippet("FOO()"), position:None} + {label:FOO/3, kind:Macro, contents:Snippet("FOO(${1:Arg1}, ${2:Arg2}, ${3:Arg3})"), position:None}"#]], + ); + + check( + r#" + -module(sample1). + -define(FOO, 1). + -define(BAR, 1). + foo() -> ?FO~ + "#, + None, + expect!["{label:FOO, kind:Macro, contents:SameAsLabel, position:None}"], + ); + + check( + r#" + -module(sample1). + -define(FOO, ok). + -define(BAR, 1). + spec foo() -> ?FO~. + foo() -> ok. + "#, + None, + expect!["{label:FOO, kind:Macro, contents:SameAsLabel, position:None}"], + ); + } + + #[test] + fn test_predefined_macros() { + assert!(serde_json::to_string(&lsp_types::CompletionItemKind::CONSTANT).unwrap() == "21"); + check( + r#" + -module(sample1). + -define(F_USER_DEFINED, 1). + foo() -> ?F~ + "#, + Some('?'), + expect![[r#" + {label:FILE, kind:Macro, contents:SameAsLabel, position:None} + {label:FUNCTION_ARITY, kind:Macro, contents:SameAsLabel, position:None} + {label:FUNCTION_NAME, kind:Macro, contents:SameAsLabel, position:None} + {label:F_USER_DEFINED, kind:Macro, contents:SameAsLabel, position:None}"#]], + ); + + check( + r#" + -module(sample1). + foo() -> ?M~ + "#, + Some('?'), + expect![[r#" + {label:MACHINE, kind:Macro, contents:SameAsLabel, position:None} + {label:MODULE, kind:Macro, contents:SameAsLabel, position:None} + {label:MODULE_STRING, kind:Macro, contents:SameAsLabel, position:None}"#]], + ); + + check( + r#" + -module(sample1). + foo() -> ?L~ + "#, + Some('?'), + expect!["{label:LINE, kind:Macro, contents:SameAsLabel, position:None}"], + ); + + check( + r#" + -module(sample1). + foo() -> ?O~ + "#, + Some('?'), + expect!["{label:OTP_RELEASE, kind:Macro, contents:SameAsLabel, position:None}"], + ); + } +} diff --git a/crates/ide_completion/src/modules.rs b/crates/ide_completion/src/modules.rs new file mode 100644 index 0000000000..0e69a61b8b --- /dev/null +++ b/crates/ide_completion/src/modules.rs @@ -0,0 +1,165 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use crate::helpers; +use crate::Args; +use crate::Completion; +use crate::Contents; +use crate::DoneFlag; +use crate::Kind; + +pub(crate) fn add_completions( + acc: &mut Vec, + Args { + file_position, + parsed, + sema, + trigger, + .. + }: &Args, +) -> DoneFlag { + if trigger.is_some() { + return false; + } + let prefix = &helpers::atom_value(parsed, file_position.offset).unwrap_or_default(); + if let Some(modules) = sema.resolve_module_names(file_position.file_id) { + let completions = modules.into_iter().filter_map(|m| { + if m.starts_with(prefix) { + Some(Completion { + label: m.to_string(), + kind: Kind::Module, + contents: Contents::SameAsLabel, + position: None, + sort_text: None, + deprecated: false, + }) + } else { + None + } + }); + + acc.extend(completions) + } + false +} + +#[cfg(test)] +mod test { + use expect_test::expect; + use expect_test::Expect; + + use crate::tests::get_completions; + use crate::tests::render_completions; + use crate::Kind; + + // completions filtered to avoid noise + fn check(code: &str, expect: Expect) { + let completions = get_completions(code, None) + .into_iter() + .filter(|c| c.kind != Kind::Keyword) + .collect(); + let actual = &render_completions(completions); + expect.assert_eq(actual); + } + + #[test] + fn test_modules() { + assert!(serde_json::to_string(&lsp_types::CompletionItemKind::MODULE).unwrap() == "9"); + + check( + r#" + //- /src/sample1.erl + -module(sample1). + -spec foo() -> s~. + foo() -> + ok. + //- /src/sample2.erl + -module(sample2). + "#, + expect![[r#" + {label:sample1, kind:Module, contents:SameAsLabel, position:None} + {label:sample2, kind:Module, contents:SameAsLabel, position:None}"#]], + ); + + check( + r#" + //- /src/sample1.erl + -module(sample1). + foo() -> + s~. + //- /src/sample2.erl + -module(sample2). + "#, + expect![[r#" + {label:sample1, kind:Module, contents:SameAsLabel, position:None} + {label:sample2, kind:Module, contents:SameAsLabel, position:None}"#]], + ); + } + + #[test] + fn test_module_from_otp() { + assert!(serde_json::to_string(&lsp_types::CompletionItemKind::MODULE).unwrap() == "9"); + + check( + r#" +//- /src/sample1.erl +-module(sample1). +foo() -> + s~. +//- /src/sample2.erl +-module(sample2). +//- /opt/lib/stdlib-3.17/src/sets.erl otp_app:/opt/lib/stdlib-3.17 +-module(sets). + "#, + expect![[r#" + {label:sample1, kind:Module, contents:SameAsLabel, position:None} + {label:sample2, kind:Module, contents:SameAsLabel, position:None} + {label:sets, kind:Module, contents:SameAsLabel, position:None}"#]], + ); + } + + #[test] + fn test_no_modules() { + assert!(serde_json::to_string(&lsp_types::CompletionItemKind::MODULE).unwrap() == "9"); + + check( + r#" + //- /src/sample1.erl + -module(sample1). + -export([ + s~ + ]). + //- /src/sample2.erl + -module(sample2). + "#, + expect![""], + ); + } + + #[test] + fn test_modules_prefix_filtering() { + check( + r#" + //- /src/sample.erl + -module(sample). + test(X) -> + ma~ + //- /src/match1.erl + -module(match1). + //- /src/match2.erl + -module(match). + //- /src/no_prefix_match.erl + -module(no_prefix_match). + "#, + expect![[r#" + {label:match1, kind:Module, contents:SameAsLabel, position:None} + {label:match2, kind:Module, contents:SameAsLabel, position:None}"#]], + ); + } +} diff --git a/crates/ide_completion/src/records.rs b/crates/ide_completion/src/records.rs new file mode 100644 index 0000000000..b614398221 --- /dev/null +++ b/crates/ide_completion/src/records.rs @@ -0,0 +1,403 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_syntax::algo; +use elp_syntax::ast; +use elp_syntax::AstNode; +use hir::InFile; +use hir::Name; + +use crate::Args; +use crate::Completion; +use crate::Contents; +use crate::DoneFlag; +use crate::Kind; + +pub(crate) fn add_completions(acc: &mut Vec, args: &Args) -> DoneFlag { + add_in_create_or_update(acc, args) || add_token_based_completions(acc, args) +} + +/// #rec{field1~} or X#rec{field1~} +pub(crate) fn add_in_create_or_update( + acc: &mut Vec, + Args { + db, + file_position, + parsed, + sema, + trigger, + .. + }: &Args, +) -> DoneFlag { + let node = parsed.value.syntax(); + match trigger { + Some('#') | None => (), + _ => return false, + }; + + match algo::find_node_at_offset::(node, file_position.offset) + .and_then(|e| e.name()) + .or_else(|| { + algo::find_node_at_offset::(node, file_position.offset) + .and_then(|e| e.name()) + }) { + None => return false, + Some(record_name) => { + || -> Option<()> { + let record = sema.to_def(InFile::new(file_position.file_id, &record_name))?; + let field = + algo::find_node_at_offset::(node, file_position.offset)?; + let prefix = &field.name()?.text()?; + let completions = record + .field_names(*db) + .filter(|field_name| field_name.starts_with(prefix)) + .map(field_name_to_completion_with_equals); + + acc.extend(completions); + Some(()) + }(); + true + } + } +} + +fn add_token_based_completions( + acc: &mut Vec, + Args { + file_position, + previous_tokens, + sema, + db, + trigger, + .. + }: &Args, +) -> DoneFlag { + let add_record_name_completions = |name_prefix: &str, acc: &mut Vec| { + let def_map = sema.def_map(file_position.file_id); + let completions = def_map + .get_records() + .iter() + .filter(|(name, _)| name.starts_with(name_prefix)) + .map(|(name, _)| Completion { + label: name.to_string(), + kind: Kind::Record, + contents: Contents::SameAsLabel, + position: None, + sort_text: None, + deprecated: false, + }); + acc.extend(completions); + true + }; + let add_record_index_completions = + |rec_name: &str, field_prefix: &str, acc: &mut Vec| { + let def_map = sema.def_map(file_position.file_id); + let record_opt = def_map + .get_records() + .iter() + .find(|(name, _)| name.as_str() == rec_name) + .map(|(_, rec)| rec); + if let Some(record) = record_opt { + let completions = record + .field_names(*db) + .filter(|name| name.as_str().starts_with(field_prefix)) + .map(field_name_to_completion); + acc.extend(completions); + true + } else { + false + } + }; + + use elp_syntax::SyntaxKind as K; + let default = vec![]; + let previous_tokens: &[_] = previous_tokens.as_ref().unwrap_or(&default); + match previous_tokens { + // // #rec_name_prefix~ + [.., (K::ANON_POUND, _), (K::ATOM, rec_name_prefix)] + if matches!(trigger, Some('#') | None) => + { + add_record_name_completions(rec_name_prefix.text(), acc) + } + // // #~ + [.., (K::ANON_POUND, _)] if matches!(trigger, Some('#') | None) => { + add_record_name_completions("", acc) + } + // #rec_name.field_prefix + [ + .., + (K::ANON_POUND, _), + (K::ATOM, rec_name), + (K::ANON_DOT, _), + (K::ATOM, field_prefix), + ] if matches!(trigger, Some('.') | None) => { + add_record_index_completions(rec_name.text(), field_prefix.text(), acc) + } + // #rec_name. + [ + .., + (K::ANON_POUND, _), + (K::ATOM, rec_name), + (K::ANON_DOT, _), + ] if matches!(trigger, Some('.') | None) => { + add_record_index_completions(rec_name.text(), "", acc) + } + + _ => false, + } +} + +fn field_name_to_completion_with_equals(field_name: Name) -> Completion { + Completion { + label: field_name.to_string(), + kind: Kind::RecordField, + contents: Contents::String(format!("{} = ", &field_name)), + position: None, + sort_text: None, + deprecated: false, + } +} + +fn field_name_to_completion(field_name: Name) -> Completion { + Completion { + label: field_name.to_string(), + kind: Kind::RecordField, + contents: Contents::SameAsLabel, + position: None, + sort_text: None, + deprecated: false, + } +} + +#[cfg(test)] +mod test { + use expect_test::expect; + use expect_test::Expect; + + use crate::tests::get_completions; + use crate::tests::render_completions; + use crate::Kind; + + fn check(code: &str, trigger_character: Option, expect: Expect) { + let completions = get_completions(code, trigger_character) + .into_iter() + .filter(|c| c.kind != Kind::Keyword) + .collect(); + let actual = &render_completions(completions); + expect.assert_eq(actual); + } + + #[test] + fn test_record_index() { + assert!(serde_json::to_string(&lsp_types::CompletionItemKind::FIELD).unwrap() == "5"); + + check( + r#" + -module(sample). + -record(rec, {field1, field2, other}). + foo() -> _ = #rec.f~. + "#, + Some('.'), + expect![[r#" + {label:field1, kind:RecordField, contents:SameAsLabel, position:None} + {label:field2, kind:RecordField, contents:SameAsLabel, position:None}"#]], + ); + + check( + r#" + -module(sample). + -record(rec, {field1, field2, other}). + foo() -> _ = #rec.f~. + "#, + None, + expect![[r#" + {label:field1, kind:RecordField, contents:SameAsLabel, position:None} + {label:field2, kind:RecordField, contents:SameAsLabel, position:None}"#]], + ); + } + + #[test] + fn test_record_field() { + assert!(serde_json::to_string(&lsp_types::CompletionItemKind::FIELD).unwrap() == "5"); + + check( + r#" + -module(sample). + -record(rec, {field1, field2, other}). + foo(X) -> _ = X#rec.f~. + "#, + Some('.'), + expect![[r#" + {label:field1, kind:RecordField, contents:SameAsLabel, position:None} + {label:field2, kind:RecordField, contents:SameAsLabel, position:None}"#]], + ); + + check( + r#" + -module(sample). + -record(rec, {field1, field2, other}). + foo(X) -> _ = X#rec.f~. + "#, + Some('.'), + expect![[r#" + {label:field1, kind:RecordField, contents:SameAsLabel, position:None} + {label:field2, kind:RecordField, contents:SameAsLabel, position:None}"#]], + ); + } + + #[test] + fn test_field_in_create() { + check( + r#" + -module(sample). + -record(rec, {field1, field2, other}). + foo() -> #rec{fie~=3}. + "#, + None, + expect![[r#" + {label:field1, kind:RecordField, contents:String("field1 = "), position:None} + {label:field2, kind:RecordField, contents:String("field2 = "), position:None}"#]], + ); + + check( + r#" + -module(sample). + -record(rec, {field1, field2, other}). + foo() -> #rec{fie~=3}. + "#, + Some('#'), + expect![[r#" + {label:field1, kind:RecordField, contents:String("field1 = "), position:None} + {label:field2, kind:RecordField, contents:String("field2 = "), position:None}"#]], + ); + } + + #[test] + fn test_field_in_update() { + check( + r#" + -module(sample). + -record(rec, {field1, field2, other}). + foo(X) -> X#rec{fie~=3}. + "#, + None, + expect![[r#" + {label:field1, kind:RecordField, contents:String("field1 = "), position:None} + {label:field2, kind:RecordField, contents:String("field2 = "), position:None}"#]], + ); + + check( + r#" + -module(sample). + -record(rec, {field1, field2, other}). + foo(X) -> X#rec{fie~=3}. + "#, + Some('#'), + expect![[r#" + {label:field1, kind:RecordField, contents:String("field1 = "), position:None} + {label:field2, kind:RecordField, contents:String("field2 = "), position:None}"#]], + ); + } + + #[test] + fn test_record_name() { + check( + r#" + -module(sample). + -record(this_record, {field1=1, field2=2}). + -record(that_record, {}). + -record(another, {}). + foo(X) -> #th~ + "#, + None, + expect![[r#" + {label:that_record, kind:Record, contents:SameAsLabel, position:None} + {label:this_record, kind:Record, contents:SameAsLabel, position:None}"#]], + ); + + check( + r#" + -module(sample). + -record(this_record, {field1=1, field2=2}). + -record(that_record, {}). + -record(another, {}). + foo(X) -> #~ + "#, + None, + expect![[r#" + {label:another, kind:Record, contents:SameAsLabel, position:None} + {label:that_record, kind:Record, contents:SameAsLabel, position:None} + {label:this_record, kind:Record, contents:SameAsLabel, position:None}"#]], + ); + } + + #[test] + fn test_record_error_recovery() { + check( + r#" + -module(sample). + -record(rec, {field1=1, field2=2}). + foo(X) -> #rec{field1 = 1, field2~. + "#, + None, + expect![[ + r#"{label:field2, kind:RecordField, contents:String("field2 = "), position:None}"# + ]], + ); + + check( + r#" + -module(sample). + -record(rec, {field1=1, field2=2}). + foo(X) -> X#rec{field1 = 1, field2~. + "#, + None, + expect![[ + r#"{label:field2, kind:RecordField, contents:String("field2 = "), position:None}"# + ]], + ); + + check( + r#" + -module(sample). + -record(rec, {field1=1, field2=2}). + foo(X) -> case ok of + ok -> #r~ + "#, + None, + expect!["{label:rec, kind:Record, contents:SameAsLabel, position:None}"], + ); + + check( + r#" + -module(sample). + -record(rec, {field1=1, field2=2}). + foo(X) -> case ok of + ok -> #rec.~ + "#, + None, + expect![[r#" + {label:field1, kind:RecordField, contents:SameAsLabel, position:None} + {label:field2, kind:RecordField, contents:SameAsLabel, position:None}"#]], + ); + + check( + r#" + -module(sample). + -record(rec, {field1=1, field2=2}). + foo(X) -> case ok of + ok -> X#rec{field1 = 1, field2~}. + "#, + None, + expect![[ + r#"{label:field2, kind:RecordField, contents:String("field2 = "), position:None}"# + ]], + ); + } +} diff --git a/crates/ide_completion/src/tests.rs b/crates/ide_completion/src/tests.rs new file mode 100644 index 0000000000..b854f72440 --- /dev/null +++ b/crates/ide_completion/src/tests.rs @@ -0,0 +1,26 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_ide_db::elp_base_db::fixture::WithFixture; +use elp_ide_db::RootDatabase; + +use crate::Completion; + +pub(crate) fn render_completions(completions: Vec) -> String { + completions + .iter() + .map(|completion| format!("{}", completion)) + .collect::>() + .join("\n") +} + +pub(crate) fn get_completions(code: &str, trigger_character: Option) -> Vec { + let (db, position) = RootDatabase::with_position(code); + crate::completions(&db, position, trigger_character) +} diff --git a/crates/ide_completion/src/types.rs b/crates/ide_completion/src/types.rs new file mode 100644 index 0000000000..4807a79fee --- /dev/null +++ b/crates/ide_completion/src/types.rs @@ -0,0 +1,266 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use elp_syntax::algo; +use elp_syntax::ast; +use elp_syntax::ast::Atom; +use elp_syntax::AstNode; +use hir::InFile; +use hir::NameArity; + +use crate::helpers; +use crate::Args; +use crate::Completion; +use crate::DoneFlag; +use crate::Kind; + +pub(crate) fn add_completions(acc: &mut Vec, args: &Args) -> DoneFlag { + add_remote(acc, args) || add_local(acc, args) +} + +pub(crate) fn add_remote( + acc: &mut Vec, + args @ Args { + file_position, + parsed, + .. + }: &Args, +) -> DoneFlag { + let node = parsed.value.syntax(); + match algo::find_node_at_offset::(node, file_position.offset) { + None => { + return false; + } + Some(remote) => { + || -> Option<()> { + let (module_atom, name) = &helpers::split_remote(&remote)?; + complete_remote_name(acc, args, module_atom, name)?; + Some(()) + }(); + true + } + } +} + +fn complete_remote_name( + acc: &mut Vec, + Args { + file_position, + sema, + trigger, + .. + }: &Args, + module_atom: &Atom, + fun_prefix: &str, +) -> Option<()> { + match trigger { + Some(':') | None => (), + _ => return None, + }; + let module = sema.to_def(InFile::new(file_position.file_id, module_atom))?; + let def_map = sema.def_map(module.file.file_id); + + let completions = def_map + .get_exported_types() + .into_iter() + .filter(|na| na.name().starts_with(fun_prefix)) + .map(create_call_completion); + acc.extend(completions); + Some(()) +} + +pub(crate) fn add_local( + acc: &mut Vec, + Args { + file_position, + parsed, + sema, + trigger, + .. + }: &Args, +) -> DoneFlag { + if trigger.is_some() { + return false; + } + let prefix = &helpers::atom_value(parsed, file_position.offset).unwrap_or_default(); + let def_map = sema.def_map(file_position.file_id); + let completions = def_map + .get_types() + .into_iter() + .filter_map(|(name_arity, _)| { + if name_arity.name().starts_with(prefix) { + Some(create_call_completion(name_arity)) + } else { + None + } + }); + acc.extend(completions); + false +} + +fn create_call_completion(name_arity: &NameArity) -> Completion { + let contents = helpers::format_call(name_arity.name(), name_arity.arity()); + Completion { + label: name_arity.to_string(), + kind: Kind::Type, + contents, + position: None, + sort_text: None, + deprecated: false, + } +} + +#[cfg(test)] +mod test { + use expect_test::expect; + use expect_test::Expect; + + use crate::tests::get_completions; + use crate::tests::render_completions; + + fn check(code: &str, trigger: Option, expect: Expect) { + let completions = get_completions(code, trigger); + let actual = &render_completions(completions); + expect.assert_eq(actual); + } + + #[test] + fn user_defined_local() { + assert!(serde_json::to_string(&lsp_types::CompletionItemKind::INTERFACE).unwrap() == "8"); + check( + r#" + //- /src/sample.erl + -module(sample). + -type alias() :: ok. + -type alias(T) :: T. + -opaque alias_opaque() :: secret. + -opaque alias_opaque(T) :: T. + -spec foo() -> a~. + foo() -> ok. + //- /src/another_module.erl + -module(another_module). + -export_type([alias/0]). + -type alias() :: ok. + "#, + None, + expect![[r#" + {label:alias/0, kind:Type, contents:Snippet("alias()"), position:None} + {label:alias/1, kind:Type, contents:Snippet("alias(${1:Arg1})"), position:None} + {label:alias_opaque/0, kind:Type, contents:Snippet("alias_opaque()"), position:None} + {label:alias_opaque/1, kind:Type, contents:Snippet("alias_opaque(${1:Arg1})"), position:None} + {label:another_module, kind:Module, contents:SameAsLabel, position:None}"#]], + ); + } + + #[test] + fn user_defined_remote() { + assert!(serde_json::to_string(&lsp_types::CompletionItemKind::INTERFACE).unwrap() == "8"); + + check( + r#" + //- /src/sample.erl + -module(sample). + -type alias() :: ok. + -type alias(T) :: T. + -spec foo() -> sample2:al~. + foo() -> ok. + //- /src/sample2.erl + -module(sample2). + -export_type([alias2/0, alias_opaque2/0, alias_opaque2/1, bar2/0]). + -type alias2() :: ok. + -opaque alias_opaque2() :: secret. + -opaque alias_opaque2(T) :: T. + -type ba2() :: ok. + -type alias_not_exported2() :: ok. + "#, + Some(':'), + expect![[r#" + {label:alias2/0, kind:Type, contents:Snippet("alias2()"), position:None} + {label:alias_opaque2/0, kind:Type, contents:Snippet("alias_opaque2()"), position:None} + {label:alias_opaque2/1, kind:Type, contents:Snippet("alias_opaque2(${1:Arg1})"), position:None}"#]], + ); + + check( + r#" + //- /src/sample.erl + -module(sample). + -type alias() :: ok. + -type alias(T) :: T. + -spec foo() -> sample2:al~. + foo() -> ok. + //- /src/sample2.erl + -module(sample2). + -export_type([alias2/0, alias_opaque2/0, alias_opaque2/1, bar2/0]). + -type alias2() :: ok. + -opaque alias_opaque2() :: secret. + -opaque alias_opaque2(T) :: T. + -type ba2() :: ok. + -type alias_not_exported2() :: ok. + "#, + None, + expect![[r#" + {label:alias2/0, kind:Type, contents:Snippet("alias2()"), position:None} + {label:alias_opaque2/0, kind:Type, contents:Snippet("alias_opaque2()"), position:None} + {label:alias_opaque2/1, kind:Type, contents:Snippet("alias_opaque2(${1:Arg1})"), position:None}"#]], + ); + + check( + r#" + //- /src/sample.erl + -module(sample). + -spec foo() -> sample2:~. + foo() -> ok. + //- /src/sample2.erl + -module(sample2). + -export_type([alias2/0]). + -type alias2() :: ok. + "#, + None, + expect![[ + r#"{label:alias2/0, kind:Type, contents:Snippet("alias2()"), position:None}"# + ]], + ); + } + + #[test] + fn in_module_part() { + // really unlikely the user will try to + // complete in there, but ensure we do + // something reasonable (show nothing) + check( + r#" + //- /src/sample.erl + -module(sample). + -spec foo() -> sample~:. + foo() -> ok. + //- /src/sample2.erl + -module(sample2). + -export_type([alias2/0]). + -type alias2() :: ok. + "#, + Some(':'), + expect![""], + ); + + check( + r#" + //- /src/sample.erl + -module(sample). + -spec foo() -> sample~:. + foo() -> ok. + //- /src/sample2.erl + -module(sample2). + -export_type([alias2/0]). + -type alias2() :: ok. + "#, + None, + expect![""], + ); + } +} diff --git a/crates/ide_completion/src/vars.rs b/crates/ide_completion/src/vars.rs new file mode 100644 index 0000000000..a85e711d5a --- /dev/null +++ b/crates/ide_completion/src/vars.rs @@ -0,0 +1,171 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::iter; + +use elp_syntax::SyntaxToken; +use fxhash::FxHashSet; + +use crate::Args; +use crate::Completion; +use crate::Contents; +use crate::DoneFlag; +use crate::Kind; + +pub(crate) fn add_completions( + acc: &mut Vec, + Args { + sema: _, + trigger, + file_position, + previous_tokens, + .. + }: &Args, +) -> DoneFlag { + use elp_syntax::SyntaxKind as K; + let default = vec![]; + let previous_tokens: &[_] = previous_tokens.as_ref().unwrap_or(&default); + match previous_tokens { + // Local variables + [.., (K::VAR, var)] if trigger.is_none() => { + let mut completions = FxHashSet::default(); + // Scan backward until the end of the prior function + // (recognised by '.'), and forward to the end of this + // one. We could optimise to look for clause boundaries, + // but a ';' location is harder to disambiguate in broken + // code. + if var.text_range().end() == file_position.offset { + // We are on the end of the var, not in whitespace past it + iter::successors(var.prev_token(), |t| t.prev_token()) + .take_while(|tok| tok.text() != ".") + .for_each(|tok| { + complete_var(var, &tok, &mut completions); + }); + iter::successors(var.next_token(), |t| t.next_token()) + .take_while(|tok| tok.text() != ".") + .for_each(|tok| { + complete_var(var, &tok, &mut completions); + }); + acc.extend(completions); + true + } else { + false + } + } + _ => false, + } +} + +fn complete_var(var: &SyntaxToken, candidate: &SyntaxToken, acc: &mut FxHashSet) { + if candidate.text().starts_with(var.text()) { + acc.insert(Completion { + label: candidate.text().to_string(), + kind: Kind::Variable, + contents: Contents::SameAsLabel, + position: None, + sort_text: None, + deprecated: false, + }); + } +} + +#[cfg(test)] +mod test { + use expect_test::expect; + use expect_test::Expect; + + use crate::tests::get_completions; + use crate::tests::render_completions; + use crate::Kind; + + // keywords are filtered out to avoid noise + fn check(code: &str, trigger_character: Option, expect: Expect) { + let completions = get_completions(code, trigger_character) + .into_iter() + .filter(|c| c.kind != Kind::Keyword) + .collect(); + let actual = &render_completions(completions); + expect.assert_eq(actual); + } + + #[test] + fn test_local_variables_1() { + check( + r#" + //- /src/sample1.erl + -module(sample1). + test(AnArg1,Blah) -> + case An~ + Another = 42, + "#, + None, + expect![[r#" + {label:AnArg1, kind:Variable, contents:SameAsLabel, position:None} + {label:Another, kind:Variable, contents:SameAsLabel, position:None}"#]], + ); + } + + #[test] + fn test_local_variables_limit_to_current_function() { + check( + r#" + //- /src/sample1.erl + -module(sample1). + another(AnArgNotMatched) -> + AnotherNotMatched = 4. + test(AnArg1,Blah) -> + case An~ + Another = 42, + later_fun(AnArgLater) -> + AnotherLater = 4. + later_fun2(AnArgEvenLater) -> + AnotherEvenLater = 4. + "#, + None, + expect![[r#" + {label:AnArg1, kind:Variable, contents:SameAsLabel, position:None} + {label:AnArgLater, kind:Variable, contents:SameAsLabel, position:None} + {label:Another, kind:Variable, contents:SameAsLabel, position:None} + {label:AnotherLater, kind:Variable, contents:SameAsLabel, position:None}"#]], + ); + } + + #[test] + fn test_local_variables_none_if_space() { + check( + r#" + //- /src/sample1.erl + -module(sample1). + test(AnArg1,Blah) -> + case An ~ + Another = 42, + "#, + None, + expect!["{label:sample1, kind:Module, contents:SameAsLabel, position:None}"], + ); + } + + #[test] + fn test_local_variables_no_duplicates() { + check( + r#" + //- /src/sample1.erl + -module(sample1). + handle_update(Config, Contents) -> + gen_server:cast(?MODULE, {update_config, Config, Contents}), + Co~ + valid. + "#, + None, + expect![[r#" + {label:Config, kind:Variable, contents:SameAsLabel, position:None} + {label:Contents, kind:Variable, contents:SameAsLabel, position:None}"#]], + ); + } +} diff --git a/crates/ide_db/Cargo.toml b/crates/ide_db/Cargo.toml new file mode 100644 index 0000000000..7964e291b2 --- /dev/null +++ b/crates/ide_db/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "elp_ide_db" +edition.workspace = true +version.workspace = true + +[dependencies] +elp_base_db.workspace = true +elp_eqwalizer.workspace = true +elp_erlang_service.workspace = true +elp_project_model.workspace = true +elp_syntax.workspace = true +hir.workspace = true + +anyhow.workspace = true +eetf.workspace = true +either.workspace = true +fxhash.workspace = true +indexmap.workspace = true +log.workspace = true +memchr.workspace = true +once_cell.workspace = true +parking_lot.workspace = true +profile.workspace = true +rustc-hash.workspace = true +serde.workspace = true +stdx.workspace = true +text-edit.workspace = true + +[dev-dependencies] +expect-test.workspace = true diff --git a/crates/ide_db/src/apply_change.rs b/crates/ide_db/src/apply_change.rs new file mode 100644 index 0000000000..9057fdeaac --- /dev/null +++ b/crates/ide_db/src/apply_change.rs @@ -0,0 +1,23 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! Applies changes to the IDE state transactionally. + +use elp_base_db::Change; + +use crate::RootDatabase; + +impl RootDatabase { + pub fn apply_change(&mut self, change: Change) { + let _p = profile::span("RootDatabase::apply_change"); + self.request_cancellation(); + log::info!("apply_change {:?}", change); + change.apply(self); + } +} diff --git a/crates/ide_db/src/assists.rs b/crates/ide_db/src/assists.rs new file mode 100644 index 0000000000..1c6e4556e3 --- /dev/null +++ b/crates/ide_db/src/assists.rs @@ -0,0 +1,213 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! This module defines the `Assist` data structure. The actual assist live in +//! the `ide_assists` downstream crate. We want to define the data structures in +//! this low-level crate though, because `ide_diagnostics` also need them +//! (fixits for diagnostics and assists are the same thing under the hood). We +//! want to compile `ide_assists` and `ide_diagnostics` in parallel though, so +//! we pull the common definitions upstream, to this crate. + +use std::str::FromStr; + +use elp_syntax::TextRange; +use serde::Deserialize; +use serde::Serialize; + +use crate::label::Label; +use crate::source_change::SourceChange; + +#[derive(Debug, Clone)] +pub struct Assist { + pub id: AssistId, + /// Short description of the assist, as shown in the UI. + pub label: Label, + pub group: Option, + /// Target ranges are used to sort assists: the smaller the target range, + /// the more specific assist is, and so it should be sorted first. + pub target: TextRange, + /// Computing source change sometimes is much more costly then computing the + /// other fields. Additionally, the actual change is not required to show + /// the lightbulb UI, it only is needed when the user tries to apply an + /// assist. So, we compute it lazily: the API allow requesting assists with + /// or without source change. We could (and in fact, used to) distinguish + /// between resolved and unresolved assists at the type level, but this is + /// cumbersome, especially if you want to embed an assist into another data + /// structure, such as a diagnostic. + pub source_change: Option, + /// Some assists require additional input from the user, such as the name + /// of a newly-extracted function. + pub user_input: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AssistKind { + // FIXME: does the None variant make sense? Probably not. + None, + + QuickFix, + Generate, + Refactor, + RefactorExtract, + RefactorInline, + RefactorRewrite, +} + +impl AssistKind { + pub fn contains(self, other: AssistKind) -> bool { + if self == other { + return true; + } + + match self { + AssistKind::None | AssistKind::Generate => true, + AssistKind::Refactor => matches!( + other, + AssistKind::RefactorExtract + | AssistKind::RefactorInline + | AssistKind::RefactorRewrite + ), + _ => false, + } + } + + pub fn name(&self) -> &str { + match self { + AssistKind::None => "None", + AssistKind::QuickFix => "QuickFix", + AssistKind::Generate => "Generate", + AssistKind::Refactor => "Refactor", + AssistKind::RefactorExtract => "RefactorExtract", + AssistKind::RefactorInline => "RefactorInline", + AssistKind::RefactorRewrite => "RefactorRewrite", + } + } +} + +impl FromStr for AssistKind { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "None" => Ok(AssistKind::None), + "QuickFix" => Ok(AssistKind::QuickFix), + "Generate" => Ok(AssistKind::Generate), + "Refactor" => Ok(AssistKind::Refactor), + "RefactorExtract" => Ok(AssistKind::RefactorExtract), + "RefactorInline" => Ok(AssistKind::RefactorInline), + "RefactorRewrite" => Ok(AssistKind::RefactorRewrite), + unknown => Err(format!("Unknown AssistKind: '{}'", unknown)), + } + } +} + +/// Unique identifier of the assist, should not be shown to the user +/// directly. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AssistId(pub &'static str, pub AssistKind); + +/// A way to control how many assist to resolve during the assist resolution. +/// When an assist is resolved, its edits are calculated that might be costly to always do by default. +#[derive(Debug, Clone)] +pub enum AssistResolveStrategy { + /// No assists should be resolved. + None, + /// All assists should be resolved. + All, + /// Only a certain assist should be resolved. + Single(SingleResolve), +} + +/// Hold the [`AssistId`] data of a certain assist to resolve. +/// The original id object cannot be used due to a `'static` lifetime +/// and the requirement to construct this struct dynamically during the resolve handling. +#[derive(Debug, Clone)] +pub struct SingleResolve { + /// The id of the assist. + pub assist_id: String, + // The kind of the assist. + pub assist_kind: AssistKind, +} + +impl AssistResolveStrategy { + pub fn should_resolve(&self, id: &AssistId) -> bool { + match self { + AssistResolveStrategy::None => false, + AssistResolveStrategy::All => true, + AssistResolveStrategy::Single(single_resolve) => { + single_resolve.assist_id == id.0 && single_resolve.assist_kind == id.1 + } + } + } +} + +#[derive(Clone, Debug)] +pub enum AssistContextDiagnosticCode { + UndefinedFunction, + UnusedFunction, + UnusedVariable, +} + +impl FromStr for AssistContextDiagnosticCode { + type Err = String; + fn from_str(s: &str) -> Result { + match s { + "L1227" => Ok(AssistContextDiagnosticCode::UndefinedFunction), + "L1230" => Ok(AssistContextDiagnosticCode::UnusedFunction), + "L1268" => Ok(AssistContextDiagnosticCode::UnusedVariable), + unknown => Err(format!("Unknown AssistContextDiagnosticCode: '{unknown}'")), + } + } +} + +#[derive(Clone, Debug)] +pub struct GroupLabel(pub String); + +#[derive(Clone, Debug)] +pub struct AssistContextDiagnostic { + pub code: AssistContextDiagnosticCode, + pub message: String, + pub range: TextRange, +} + +impl AssistContextDiagnostic { + pub fn new( + code: AssistContextDiagnosticCode, + message: String, + range: TextRange, + ) -> AssistContextDiagnostic { + AssistContextDiagnostic { + code, + message, + range, + } + } +} + +// --------------------------------------------------------------------- + +/// Passed in the code action `data` field to be processed by +/// middleware in the client to request user input when the request is +/// being resolved. + +/// This is a 'fail-safe' process, if the client does not return the +/// requested new values the assist should use a default. +/// This is modelled on the erlang_ls Wrangler middleware introduced in +/// https://github.com/erlang-ls/vscode/pull/125 +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)] +pub struct AssistUserInput { + pub input_type: AssistUserInputType, + pub value: String, +} + +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)] +pub enum AssistUserInputType { + Variable, + Atom, +} diff --git a/crates/ide_db/src/defs.rs b/crates/ide_db/src/defs.rs new file mode 100644 index 0000000000..3e32502b22 --- /dev/null +++ b/crates/ide_db/src/defs.rs @@ -0,0 +1,423 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! This implements the "go to definiton" logic + +use std::iter; + +use either::Either; +use elp_base_db::FileId; +use elp_syntax::ast; +use elp_syntax::match_ast; +use elp_syntax::AstNode; +use elp_syntax::SmolStr; +use elp_syntax::SyntaxNode; +use elp_syntax::SyntaxToken; +use hir::db::MinDefDatabase; +use hir::CallDef; +use hir::CallbackDef; +use hir::DefineDef; +use hir::DefinitionOrReference; +use hir::FaDef; +use hir::File; +use hir::FunctionDef; +use hir::InFile; +use hir::Module; +use hir::RecordDef; +use hir::RecordFieldDef; +use hir::Semantic; +use hir::TypeAliasDef; +use hir::VarDef; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SymbolClass { + Definition(SymbolDefinition), + Reference { + refs: ReferenceClass, + typ: ReferenceType, + }, + // Operator(...) +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ReferenceType { + Direct, + Other, // spec, import, export +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ReferenceClass { + Definition(SymbolDefinition), + /// A variable defined in multiple places, e.g. after a case + /// for a variable defined in all branches + MultiVar(Vec), + /// An arity-less reference to a macro, can refer to multiple definitions + MultiMacro(Vec), +} + +impl ReferenceClass { + pub fn into_iter(self) -> impl Iterator { + match self { + ReferenceClass::Definition(def) => Either::Left(iter::once(def)), + ReferenceClass::MultiVar(vars) => { + Either::Right(Either::Left(vars.into_iter().map(SymbolDefinition::Var))) + } + ReferenceClass::MultiMacro(defs) => Either::Right(Either::Right( + defs.into_iter().map(SymbolDefinition::Define), + )), + } + } +} + +/// `SymbolDefinition` keeps information about the element we want to search references for. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SymbolDefinition { + Module(Module), + Function(FunctionDef), + Record(RecordDef), + RecordField(RecordFieldDef), + Type(TypeAliasDef), + Callback(CallbackDef), + Define(DefineDef), + Header(File), + Var(VarDef), +} + +impl SymbolClass { + /// Returns the SymbolClass for the token, if: + /// * it is reference place for the definition, e.g. a function call for a function + /// * it is a definition itself, e.g. a function definition + pub fn classify(sema: &Semantic, token: InFile) -> Option { + let wrapper = token.value.parent()?; + let parent = wrapper.parent()?; + + fn definition>(def: Option) -> Option { + def.map(|def| SymbolClass::Definition(def.into())) + } + + match_ast! { + match parent { + // All places that embed $._name + ast::ModuleAttribute(attr) => { + definition(sema.to_def(token.with_value(&attr))) + }, + ast::BehaviourAttribute(behaviour) => { + reference_direct(sema.to_def(token.with_value(&behaviour))) + }, + ast::ImportAttribute(import) => { + reference_other(sema.to_def(token.with_value(&import))) + }, + ast::Fa(fa) => { + reference_other(sema.to_def(token.with_value(&fa))) + }, + ast::TypeName(ty) => { + definition(sema.to_def(token.with_value(&ty))) + }, + ast::RecordDecl(rec) => { + definition(sema.to_def(token.with_value(&rec))) + }, + ast::Spec(spec) => { + reference_other(sema.to_def(token.with_value(&spec))) + }, + ast::Callback(cb) => { + definition(sema.to_def(token.with_value(&cb))) + }, + ast::Module(_) => { + if let Some(atom) = ast::Atom::cast(wrapper.clone()) { + reference_direct(sema.to_def(token.with_value(&atom))) + } else { + classify_var(sema, token.file_id, wrapper) + } + }, + ast::AttrName(_) => None, + ast::FunctionClause(clause) => { + definition(sema.to_def(token.with_value(&clause))) + }, + ast::BitTypeList(_) => None, + ast::RecordName(name) => { + reference_direct(sema.to_def(token.with_value(&name))) + }, + ast::RecordFieldName(field) => { + reference_direct(sema.to_def(token.with_value(&field))) + }, + ast::RecordField(field) => { + match sema.to_def(token.with_value(&field))? { + DefinitionOrReference::Definition(def) => definition(Some(def)), + DefinitionOrReference::Reference(def) => reference_direct(Some(def)), + } + }, + ast::InternalFun(fun) => { + if let Some(function) = sema.to_def(token.with_value(&fun)) { + reference_direct(Some(function)) + } else { + classify_var(sema, token.file_id, wrapper) + } + }, + ast::ExternalFun(fun) => { + if let Some(function) = sema.to_def(token.with_value(&fun)) { + reference_direct(Some(function)) + } else { + classify_var(sema, token.file_id, wrapper) + } + }, + ast::TryClass(_) => { + classify_var(sema, token.file_id, wrapper) + }, + // All places that embed $._macro_name + ast::MacroLhs(define) => { + definition(sema.to_def(token.with_value(&define))) + }, + ast::MacroCallExpr(macro_call) => { + reference_direct(sema.to_def(token.with_value(¯o_call))) + }, + ast::PpUndef(_) => { + classify_macro_name(sema, token.file_id, wrapper) + }, + ast::PpIfdef(_) => { + classify_macro_name(sema, token.file_id, wrapper) + }, + ast::PpIfndef(_) => { + classify_macro_name(sema, token.file_id, wrapper) + }, + // All places that embed $._expr with special meaning + ast::RemoteModule(_) => { + from_wrapper(sema, &token, wrapper) + }, + ast::Remote(remote) => { + if let Some(call) = sema.to_def(token.with_value(&remote)) { + reference_direct(Some(call)) + } else { + classify_var(sema, token.file_id, wrapper) + } + }, + ast::Call(call) => { + if let Some(call) = sema.to_def(token.with_value(&call)) { + reference_direct(Some(call)) + } else { + classify_var(sema, token.file_id, wrapper) + } + }, + ast::PpInclude(include) => { + reference_direct(sema.to_def(token.with_value(&include))) + }, + ast::PpIncludeLib(include) => { + reference_direct(sema.to_def(token.with_value(&include))) + }, + ast::ExprArgs(args) => { + from_apply(sema, &token, args.syntax()) + .or_else(|| from_wrapper(sema, &token, wrapper)) + }, + _ => { + from_wrapper(sema, &token, wrapper) + } + } + } + } + + pub fn into_iter(self) -> impl Iterator { + match self { + SymbolClass::Definition(def) => Either::Left(iter::once(def)), + SymbolClass::Reference { refs, typ: _ } => Either::Right(refs.into_iter()), + } + } +} + +impl SymbolDefinition { + pub fn file(&self) -> &File { + match self { + SymbolDefinition::Module(it) => &it.file, + SymbolDefinition::Function(it) => &it.file, + SymbolDefinition::Record(it) => &it.file, + SymbolDefinition::RecordField(it) => &it.record.file, + SymbolDefinition::Type(it) => &it.file, + SymbolDefinition::Callback(it) => &it.file, + SymbolDefinition::Define(it) => &it.file, + SymbolDefinition::Header(it) => it, + SymbolDefinition::Var(it) => &it.file, + } + } + + pub fn search_name(&self, db: &dyn MinDefDatabase) -> SmolStr { + match self { + SymbolDefinition::Module(it) => it.name(db).raw(), + SymbolDefinition::Function(it) => it.function.name.name().raw(), + SymbolDefinition::Record(it) => it.record.name.raw(), + SymbolDefinition::RecordField(it) => it.field.name.raw(), + SymbolDefinition::Type(it) => it.name().name().raw(), + SymbolDefinition::Callback(it) => it.callback.name.name().raw(), + SymbolDefinition::Define(it) => it.define.name.name().raw(), + SymbolDefinition::Header(it) => it.name(db.upcast()), + SymbolDefinition::Var(it) => it.name(db.upcast()).raw(), + } + } + + pub fn is_local(&self) -> bool { + match self { + SymbolDefinition::Function(fun) => !fun.exported, + SymbolDefinition::Record(_) => true, + SymbolDefinition::RecordField(_) => true, + SymbolDefinition::Type(ty) => !ty.exported, + SymbolDefinition::Callback(_) => true, + SymbolDefinition::Define(_) => true, + SymbolDefinition::Var(_) => true, + SymbolDefinition::Module(_) => false, + SymbolDefinition::Header(_) => false, + } + } +} + +impl From for SymbolDefinition { + fn from(it: Module) -> Self { + Self::Module(it) + } +} + +impl From for SymbolDefinition { + fn from(it: TypeAliasDef) -> Self { + Self::Type(it) + } +} + +impl From for SymbolDefinition { + fn from(it: RecordDef) -> Self { + Self::Record(it) + } +} + +impl From for SymbolDefinition { + fn from(it: RecordFieldDef) -> Self { + Self::RecordField(it) + } +} + +impl From for SymbolDefinition { + fn from(it: FunctionDef) -> Self { + Self::Function(it) + } +} + +impl From for SymbolDefinition { + fn from(it: CallbackDef) -> Self { + Self::Callback(it) + } +} + +impl From for SymbolDefinition { + fn from(it: DefineDef) -> Self { + Self::Define(it) + } +} + +impl From for SymbolDefinition { + fn from(it: File) -> Self { + Self::Header(it) + } +} + +impl From for SymbolDefinition { + fn from(it: FaDef) -> Self { + match it { + FaDef::Function(function) => function.into(), + FaDef::Type(alias) => alias.into(), + FaDef::Callback(cb) => cb.into(), + } + } +} + +impl From for SymbolDefinition { + fn from(it: CallDef) -> Self { + match it { + CallDef::Function(function) => function.into(), + CallDef::Type(alias) => alias.into(), + } + } +} + +fn classify_var(sema: &Semantic, file_id: FileId, wrapper: SyntaxNode) -> Option { + let var = ast::Var::cast(wrapper)?; + match sema.to_def(InFile::new(file_id, &var))? { + DefinitionOrReference::Definition(def) => { + Some(SymbolClass::Definition(SymbolDefinition::Var(def))) + } + DefinitionOrReference::Reference(mut vars) if vars.len() == 1 => { + Some(SymbolClass::Reference { + refs: ReferenceClass::Definition(SymbolDefinition::Var(vars.swap_remove(0))), + typ: ReferenceType::Direct, + }) + } + DefinitionOrReference::Reference(vars) => Some(SymbolClass::Reference { + refs: ReferenceClass::MultiVar(vars), + typ: ReferenceType::Direct, + }), + } +} + +fn classify_macro_name( + sema: &Semantic, + file_id: FileId, + wrapper: SyntaxNode, +) -> Option { + let name = ast::MacroName::cast(wrapper)?; + let mut defs = sema.to_def(InFile::new(file_id, &name))?; + if defs.len() == 1 { + Some(SymbolClass::Reference { + refs: ReferenceClass::Definition(SymbolDefinition::Define(defs.swap_remove(0))), + typ: ReferenceType::Direct, + }) + } else { + Some(SymbolClass::Reference { + refs: ReferenceClass::MultiMacro(defs), + typ: ReferenceType::Direct, + }) + } +} + +fn reference_direct>(def: Option) -> Option { + def.map(|def| SymbolClass::Reference { + refs: ReferenceClass::Definition(def.into()), + typ: ReferenceType::Direct, + }) +} + +fn reference_other>(def: Option) -> Option { + def.map(|def| SymbolClass::Reference { + refs: ReferenceClass::Definition(def.into()), + typ: ReferenceType::Other, + }) +} + +pub fn from_apply( + sema: &Semantic, + token: &InFile, + syntax: &SyntaxNode, +) -> Option { + let call = ast::Call::cast(syntax.parent()?)?; + let call_def = reference_direct(sema.to_def(token.with_value(&call.args()?)))?; + match call_def { + SymbolClass::Reference { + refs: ReferenceClass::Definition(def), + typ: _, + } => reference_other(Some(def)), + _ => None, + } +} + +/// Parent is nothing structured, it must be a raw atom or var literal +pub fn from_wrapper( + sema: &Semantic, + token: &InFile, + wrapper: SyntaxNode, +) -> Option { + // Parent is nothing structured, it must be a raw atom or var literal + if let Some(atom) = ast::Atom::cast(wrapper.clone()) { + return reference_direct(sema.to_def(token.with_value(&atom))); + } else { + classify_var(sema, token.file_id, wrapper) + } +} diff --git a/crates/ide_db/src/docs.rs b/crates/ide_db/src/docs.rs new file mode 100644 index 0000000000..ce875c7d76 --- /dev/null +++ b/crates/ide_db/src/docs.rs @@ -0,0 +1,434 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! This implements the "docs on hover" logic + +use std::fmt; +use std::sync::Arc; + +use elp_base_db::salsa; +use elp_base_db::FileId; +use elp_base_db::SourceDatabase; +use elp_base_db::SourceDatabaseExt; +use elp_base_db::Upcast; +use elp_erlang_service::DocDiagnostic; +use elp_erlang_service::DocOrigin; +use elp_erlang_service::DocRequest; +use elp_syntax::ast; +use elp_syntax::match_ast; +use elp_syntax::AstNode; +use elp_syntax::SyntaxToken; +use fxhash::FxHashMap; +use hir::db::MinDefDatabase; +use hir::CallDef; +use hir::InFile; +use hir::Name; +use hir::NameArity; +use hir::Semantic; + +pub trait DocLoader { + /// when origin = eep-48: + /// Reads docs from beam files. Supports custom doc setups, e.g. as used by + /// OTP, but at the cost of requiring pre-built BEAM files with embedded docs. + /// + /// when origin = edoc: + /// Reads edocs from comments in source files. Allows for dynamically + /// regenerating docs as the files are edited. + fn load_doc_descriptions(&self, file_id: FileId, origin: DocOrigin) -> FileDoc; +} + +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct Doc { + markdown_text: String, +} + +impl fmt::Debug for Doc { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut f = f.debug_struct("Doc"); + f.field("markdown_text", &self.markdown_text); + f.finish() + } +} + +impl Doc { + pub fn markdown_text(&self) -> &str { + &self.markdown_text + } + + pub fn new(markdown_text: String) -> Doc { + Doc { markdown_text } + } +} + +pub trait ToDoc: Clone { + fn to_doc(docs: &Documentation<'_>, ast: Self) -> Option; +} + +impl ToDoc for InFile<&ast::ModuleAttribute> { + fn to_doc(docs: &Documentation<'_>, ast: Self) -> Option { + docs.module_doc(ast.file_id) + } +} + +impl ToDoc for InFile<&ast::BehaviourAttribute> { + fn to_doc(docs: &Documentation<'_>, ast: Self) -> Option { + let module = docs.sema.to_def(ast)?; + docs.module_doc(module.file.file_id) + } +} + +impl ToDoc for InFile<&ast::Fa> { + fn to_doc(docs: &Documentation<'_>, ast: Self) -> Option { + let fa_def = docs.sema.to_def(ast)?; + let name = match fa_def { + hir::FaDef::Function(f) => Some(f.function.name), + hir::FaDef::Type(_) => None, + hir::FaDef::Callback(c) => Some(c.callback.name), + }?; + docs.function_doc(ast.file_id, name) + } +} + +impl ToDoc for InFile<&ast::Spec> { + fn to_doc(docs: &Documentation<'_>, ast: Self) -> Option { + let fun_def = docs.sema.to_def(ast)?; + docs.function_doc(ast.file_id, fun_def.function.name) + } +} + +impl ToDoc for InFile<&ast::Atom> { + fn to_doc(docs: &Documentation<'_>, ast: Self) -> Option { + docs.sema + .resolve_module_name(ast.file_id, &ast.value.raw_text()) + .and_then(|module| docs.module_doc(module.file.file_id)) + } +} + +impl ToDoc for InFile<&ast::ExternalFun> { + fn to_doc(docs: &Documentation<'_>, ast: Self) -> Option { + let fun_def = docs.sema.to_def(ast)?; + docs.function_doc(fun_def.file.file_id, fun_def.function.name) + } +} + +impl ToDoc for InFile<&ast::Remote> { + fn to_doc(docs: &Documentation<'_>, ast: Self) -> Option { + if let Some(call_def) = docs.sema.to_def(ast) { + match call_def { + CallDef::Function(fun_def) => { + let file_id = fun_def.file.file_id; + let name_arity = fun_def.function.name; + docs.function_doc(file_id, name_arity) + } + CallDef::Type(_) => None, + } + } else { + None + } + } +} + +impl ToDoc for InFile<&ast::Call> { + fn to_doc(docs: &Documentation<'_>, ast: Self) -> Option { + if let Some(call_def) = docs.sema.to_def(ast) { + match call_def { + CallDef::Function(fun_def) => { + docs.function_doc(fun_def.file.file_id, fun_def.function.name) + } + CallDef::Type(_) => None, + } + } else { + None + } + } +} + +impl ToDoc for InFile<&ast::FunctionClause> { + fn to_doc(docs: &Documentation<'_>, ast: Self) -> Option { + if let Some(function_id) = docs + .sema + .find_enclosing_function(ast.file_id, ast.value.syntax()) + { + let form_list = docs.sema.db.file_form_list(ast.file_id); + let function = &form_list[function_id]; + docs.function_doc(ast.file_id, function.name.clone()) + } else { + None + } + } +} + +// edocs can exist on either a module attribute or a function definition, +// see https://www.erlang.org/doc/apps/edoc/chapter.html#introduction +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct FileDoc { + module_doc: Option, + function_docs: FxHashMap, + pub diagnostics: Vec, +} + +// TODO Add an input so we know when to invalidate? +#[salsa::query_group(DocDatabaseStorage)] +pub trait DocDatabase: + MinDefDatabase + SourceDatabase + DocLoader + Upcast +{ + #[salsa::invoke(get_file_docs)] + fn file_doc(&self, file_id: FileId) -> Arc; +} + +/// Primary API to get documentation information from HIR +pub struct Documentation<'db> { + pub db: &'db dyn DocDatabase, + pub sema: &'db Semantic<'db>, +} + +impl<'db> Documentation<'db> { + pub fn new(db: &'db Db, sema: &'db Semantic) -> Self { + Self { db, sema } + } +} + +impl<'db> Documentation<'db> { + pub fn to_doc(&self, ast: T) -> Option { + ToDoc::to_doc(self, ast) + } + + fn file_doc(&self, file_id: FileId) -> Arc { + self.db.file_doc(file_id) + } + + fn function_doc(&self, file_id: FileId, function: NameArity) -> Option { + let file_docs = self.file_doc(file_id); + file_docs.function_docs.get(&function).map(|d| d.to_owned()) + } + + fn module_doc(&self, file_id: FileId) -> Option { + let file_docs = self.file_doc(file_id); + file_docs.module_doc.clone() + } +} + +// Some(true) -> file is in OTP +// Some(false) -> file is not in OTP +// None -> Unknown (e.g. because OTP is not being tracked) +fn is_file_in_otp(db: &dyn DocDatabase, file_id: FileId) -> Option { + let root_id = db.file_source_root(file_id); + if let Some(app_data) = db.app_data(root_id) { + let project_id = app_data.project_id; + Some(db.project_data(project_id).otp_project_id == Some(project_id)) + } else { + log::error!( + "Unknown application - could not load app_data to determine whether file is on OTP" + ); + None + } +} + +fn get_file_docs(db: &dyn DocDatabase, file_id: FileId) -> Arc { + let origin = if Some(true) == is_file_in_otp(db, file_id) { + DocOrigin::Eep48 + } else { + DocOrigin::Edoc + }; + + let descriptions = db.load_doc_descriptions(file_id, origin); + let specs = get_file_function_specs(db.upcast(), file_id); + Arc::new(FileDoc { + module_doc: descriptions.module_doc, + function_docs: merge_descriptions_and_specs(descriptions.function_docs, specs), + diagnostics: descriptions.diagnostics, + }) +} + +fn merge_descriptions_and_specs( + descriptions: FxHashMap, + specs: FxHashMap, +) -> FxHashMap { + let all_keys = descriptions + .keys() + .into_iter() + .chain(specs.keys().into_iter()); + + all_keys + .map(|na| match (descriptions.get(&na), specs.get(&na)) { + (Some(desc), Some(spec)) => ( + na.clone(), + Doc::new(format!( + "{}\n\n-----\n\n{}", + spec.markdown_text(), + desc.markdown_text() + )), + ), + (Some(desc), None) => (na.clone(), desc.clone()), + (None, Some(spec)) => (na.clone(), spec.clone()), + (None, None) => ( + na.clone(), + Doc::new("_No documentation available_".to_string()), + ), + }) + .collect::>() +} + +fn get_file_function_specs<'a>( + def_db: &dyn MinDefDatabase, + file_id: FileId, +) -> FxHashMap { + def_db + .file_form_list(file_id) + .specs() + .map(|(_, spec)| { + ( + spec.name.clone(), + Doc::new(format!( + "```erlang\n{}\n```", + spec.form_id + .get(&def_db.parse(file_id).tree()) + .syntax() + .text() + .to_string() + )), + ) + }) + .collect::>() +} + +impl DocLoader for crate::RootDatabase { + fn load_doc_descriptions(&self, file_id: FileId, doc_origin: DocOrigin) -> FileDoc { + _ = SourceDatabaseExt::file_text(self, file_id); // Take dependency on the contents of the file we're getting docs for + let root_id = self.file_source_root(file_id); + let root = self.source_root(root_id); + let src_db: &dyn SourceDatabase = self.upcast(); + let app_data = if let Some(app_data) = src_db.app_data(root_id) { + app_data + } else { + log::error!("No corresponding appdata found for file, so no docs can be loaded"); + return FileDoc { + module_doc: None, + function_docs: FxHashMap::default(), + diagnostics: vec![], + }; + }; + + let project_id = app_data.project_id; + if let Some(erlang_service) = self.erlang_services.read().get(&project_id).cloned() { + let path = root.path_for_file(&file_id).unwrap().as_path().unwrap(); + let raw_doc = erlang_service.request_doc(DocRequest { + src_path: path.to_path_buf().into(), + doc_origin, + }); + match raw_doc { + Ok(d) => FileDoc { + module_doc: Some(Doc { + markdown_text: d.module_doc, + }), + function_docs: d + .function_docs + .into_iter() + .map(|((name, arity), markdown_text)| { + ( + NameArity::new(Name::from_erlang_service(&name), arity), + Doc { markdown_text }, + ) + }) + .collect(), + diagnostics: d.diagnostics, + }, + Err(_) => FileDoc { + module_doc: None, + function_docs: FxHashMap::default(), + diagnostics: vec![], + }, + } + } else { + log::error!( + "No erlang_service found for project: {:?}, so no docs can be loaded", + project_id + ); + FileDoc { + module_doc: None, + function_docs: FxHashMap::default(), + diagnostics: vec![], + } + } + } +} + +impl Doc { + /// Returns the Doc for the token, assuming the token + /// is a reference to a module/function definition e.g.: + /// - gets the docs for the f in m:f(a) + /// - gets the docs for the m in m:f(a) + /// If both are available, we pick the more specific docs, + /// i.e. the docs for the function + pub fn from_reference(docdb: &Documentation, token: &InFile) -> Option { + let wrapper = token.value.parent()?; + let parent = wrapper.parent()?; + match_ast! { + // All places which refer to a function or module name + match parent { + ast::ModuleAttribute(module) => + docdb.to_doc(token.with_value(&module)), + ast::BehaviourAttribute(behaviour) => { + let b = token.with_value(&behaviour); + docdb.to_doc(b) + }, + ast::ImportAttribute(_) => None, + ast::Fa(fa) => + docdb.to_doc(token.with_value(&fa)), + ast::TypeName(_) => None, + ast::RecordDecl(_) => None, + ast::Spec(spec) => + docdb.to_doc(token.with_value(&spec)), + ast::Callback(_) => None, + ast::Module(_) => { + if let Some(atom) = ast::Atom::cast(wrapper.clone()) { + docdb.to_doc(token.with_value(&atom)) + } else { + None + } + }, + ast::AttrName(_) => None, + ast::FunctionClause(clause) => + docdb.to_doc(token.with_value(&clause)), + ast::BitTypeList(_) => None, + ast::RecordName(_) => None, + ast::RecordFieldName(_) => None, + ast::RecordField(_) => None, + ast::InternalFun(_) => None, + ast::ExternalFun(fun) => + docdb.to_doc(token.with_value(&fun)), + ast::TryClass(_) => None, + // All places that embed an expr with special meaning + ast::RemoteModule(_) => { + if let Some(atom) = ast::Atom::cast(wrapper.clone()) { + docdb.to_doc(token.with_value(&atom)) + } else { + None + } + }, + ast::Remote(remote) => + docdb.to_doc(token.with_value(&remote)), + ast::Call(call) => + docdb.to_doc(token.with_value(&call)), + _ => { + // Parent is nothing structured, it must be a raw atom or var literal + match_ast! { + match wrapper { + ast::Atom(atom) => + docdb.to_doc(token.with_value(&atom)), + _ => { + None + } + } + } + } + } + } + } +} diff --git a/crates/ide_db/src/eqwalizer.rs b/crates/ide_db/src/eqwalizer.rs new file mode 100644 index 0000000000..ba569be28f --- /dev/null +++ b/crates/ide_db/src/eqwalizer.rs @@ -0,0 +1,368 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::sync::Arc; + +use elp_base_db::salsa; +use elp_base_db::AbsPath; +use elp_base_db::FileId; +use elp_base_db::FileSource; +use elp_base_db::ModuleName; +use elp_base_db::ProjectId; +use elp_base_db::SourceDatabase; +use elp_base_db::SourceRootId; +use elp_eqwalizer::ast::db::EqwalizerASTDatabase; +use elp_eqwalizer::ast::db::EqwalizerErlASTStorage; +use elp_eqwalizer::ast::Error; +use elp_eqwalizer::ipc::IpcHandle; +use elp_eqwalizer::EqwalizerDiagnostics; +use elp_eqwalizer::EqwalizerDiagnosticsDatabase; +use elp_eqwalizer::EqwalizerStats; +use elp_syntax::ast; +use parking_lot::Mutex; + +use crate::ErlAstDatabase; + +pub trait EqwalizerLoader { + fn typecheck( + &self, + project_id: ProjectId, + build_info_path: &AbsPath, + modules: Vec, + ) -> EqwalizerDiagnostics; +} + +impl EqwalizerLoader for crate::RootDatabase { + fn typecheck( + &self, + project_id: ProjectId, + build_info_path: &AbsPath, + modules: Vec, + ) -> EqwalizerDiagnostics { + let module_index = self.module_index(project_id); + let module_names: Vec<&str> = modules + .iter() + .map(|&f| module_index.module_for_file(f).unwrap().as_str()) + .collect(); + self.eqwalizer + .typecheck(build_info_path.as_ref(), self, project_id, module_names) + } +} + +#[salsa::query_group(EqwalizerDatabaseStorage)] +pub trait EqwalizerDatabase: + EqwalizerDiagnosticsDatabase + + EqwalizerASTDatabase + + SourceDatabase + + EqwalizerLoader + + ErlAstDatabase +{ + fn eqwalizer_diagnostics( + &self, + project_id: ProjectId, + file_ids: Vec, + ) -> Arc; + fn eqwalizer_stats( + &self, + project_id: ProjectId, + file_id: FileId, + ) -> Option>; + fn has_eqwalizer_app_marker(&self, source_root_id: SourceRootId) -> bool; + fn has_eqwalizer_module_marker(&self, file_id: FileId) -> bool; + fn has_eqwalizer_ignore_marker(&self, file_id: FileId) -> bool; + fn is_eqwalizer_enabled(&self, file_id: FileId, include_generated: bool) -> bool; +} + +fn eqwalizer_diagnostics( + db: &dyn EqwalizerDatabase, + project_id: ProjectId, + file_ids: Vec, +) -> Arc { + let project = db.project_data(project_id); + if let Some(build_info_path) = &project.build_info_path { + Arc::new(db.typecheck(project_id, build_info_path, file_ids)) + } else { + // + log::error!("EqWAlizing in a fixture project"); + Arc::new(EqwalizerDiagnostics::Error( + "EqWAlizing in a fixture project".to_string(), + )) + } +} + +fn eqwalizer_stats( + db: &dyn EqwalizerDatabase, + project_id: ProjectId, + file_id: FileId, +) -> Option> { + let module_index = db.module_index(project_id); + let module_name: &str = module_index.module_for_file(file_id)?.as_str(); + db.compute_eqwalizer_stats(project_id, ModuleName::new(module_name)) +} + +fn is_eqwalizer_enabled( + db: &dyn EqwalizerDatabase, + file_id: FileId, + include_generated: bool, +) -> bool { + if !include_generated && db.is_generated(file_id) { + return false; + } + + let source_root = db.file_source_root(file_id); + let app_data = if let Some(app_data) = db.app_data(source_root) { + app_data + } else { + return false; + }; + let project_id = app_data.project_id; + let project = db.project_data(project_id); + let eqwalizer_config = &project.eqwalizer_config; + let module_index = db.module_index(project_id); + let is_src = module_index.file_source_for_file(file_id) == Some(FileSource::Src); + let app_or_global_opt_in = + eqwalizer_config.enable_all || db.has_eqwalizer_app_marker(source_root); + let opt_in = (app_or_global_opt_in && is_src) || db.has_eqwalizer_module_marker(file_id); + let ignored = db.has_eqwalizer_ignore_marker(file_id); + opt_in && !ignored +} + +fn has_eqwalizer_app_marker(db: &dyn EqwalizerDatabase, source_root_id: SourceRootId) -> bool { + if let Some(app_data) = db.app_data(source_root_id) { + let source_root = db.source_root(source_root_id); + return source_root.has_eqwalizer_marker(&app_data); + } + false +} + +fn has_eqwalizer_module_marker(db: &dyn EqwalizerDatabase, file_id: FileId) -> bool { + let parsed = db.parse(file_id); + parsed + .tree() + .forms() + .take_while(|form| !matches!(form, ast::Form::FunDecl(_))) + .filter_map(|form| match form { + ast::Form::WildAttribute(attr) => Some(attr), + _ => None, + }) + .filter(|attr| { + attr.name() + .and_then(|attr_name| match attr_name.name()? { + ast::Name::Atom(atom) => atom.self_token(), + ast::Name::MacroCallExpr(_) | ast::Name::Var(_) => None, + }) + .map(|token| token.text() == "typing") + .unwrap_or_default() + }) + .any(|attr| attr.value().map(has_eqwalizer_atom).unwrap_or_default()) +} + +fn has_eqwalizer_ignore_marker(db: &dyn EqwalizerDatabase, file_id: FileId) -> bool { + let parsed = db.parse(file_id); + parsed + .tree() + .forms() + .take_while(|form| !matches!(form, ast::Form::FunDecl(_))) + .filter_map(|form| match form { + ast::Form::WildAttribute(attr) => Some(attr), + _ => None, + }) + .filter(|attr| { + attr.name() + .and_then(|attr_name| match attr_name.name()? { + ast::Name::Atom(atom) => atom.self_token(), + ast::Name::MacroCallExpr(_) | ast::Name::Var(_) => None, + }) + .map(|token| token.text() == "eqwalizer") + .unwrap_or_default() + }) + .any(|attr| { + attr.value() + .map(is_ignore_or_fixme_atom) + .unwrap_or_default() + }) +} + +impl EqwalizerErlASTStorage for crate::RootDatabase { + fn get_erl_ast_bytes( + &self, + project_id: ProjectId, + module: ModuleName, + ) -> Result>, Error> { + if let Some(file_id) = self.module_index(project_id).file_for_module(&module) { + let result = self.module_ast(file_id, elp_erlang_service::Format::OffsetEtf); + if result.is_ok() { + Ok(result.ast.clone()) + } else { + Err(Error::ParseError) + } + } else { + Err(Error::ModuleNotFound(module.as_str().into())) + } + } + + fn get_erl_stub_bytes( + &self, + project_id: ProjectId, + module: ModuleName, + ) -> Result>, Error> { + if let Some(file_id) = self.module_index(project_id).file_for_module(&module) { + let result = self.module_ast(file_id, elp_erlang_service::Format::OffsetEtf); + if result.is_ok() { + Ok(result.stub.clone()) + } else { + Err(Error::ParseError) + } + } else { + Err(Error::ModuleNotFound(module.as_str().into())) + } + } +} + +impl elp_eqwalizer::DbApi for crate::RootDatabase { + fn eqwalizing_start(&self, module: String) -> () { + if let Some(reporter) = self.eqwalizer_progress_reporter.lock().as_mut() { + reporter.start_module(module) + } + } + + fn eqwalizing_done(&self, module: String) -> () { + if let Some(reporter) = self.eqwalizer_progress_reporter.lock().as_mut() { + reporter.done_module(&module); + } + } + + fn set_module_ipc_handle(&self, module: ModuleName, handle: Arc>) -> () { + self.ipc_handles + .write() + .insert(module.as_str().into(), handle.clone()); + } + + fn module_ipc_handle(&self, module: ModuleName) -> Option>> { + self.ipc_handles + .read() + .get(module.as_str().into()) + .map(|v| v.to_owned()) + } +} + +fn has_eqwalizer_atom(expr: ast::Expr) -> bool { + match expr { + ast::Expr::ExprMax(ast::ExprMax::ParenExpr(expr)) => match expr.expr() { + Some(ast::Expr::ExprMax(ast::ExprMax::List(list))) => { + list.exprs().any(|expr| match expr { + ast::Expr::ExprMax(ast::ExprMax::Atom(atom)) => atom + .self_token() + .map(|token| token.text() == "eqwalizer") + .unwrap_or_default(), + _ => false, + }) + } + _ => false, + }, + _ => false, + } +} + +fn is_ignore_or_fixme_atom(expr: ast::Expr) -> bool { + match expr { + ast::Expr::ExprMax(ast::ExprMax::ParenExpr(expr)) => match expr.expr() { + Some(ast::Expr::ExprMax(ast::ExprMax::Atom(atom))) => atom + .self_token() + .map(|token| token.text() == "ignore" || token.text() == "fixme") + .unwrap_or_default(), + _ => false, + }, + _ => false, + } +} + +#[cfg(test)] +mod tests { + use elp_base_db::fixture::WithFixture; + + use super::*; + use crate::RootDatabase; + + #[test] + fn test_has_eqwalizer_module_marker() { + let (db, file_id) = RootDatabase::with_single_file( + r#" +-module(test). +"#, + ); + + assert!(!db.has_eqwalizer_module_marker(file_id)); + + let (db, file_id) = RootDatabase::with_single_file( + r#" +-module(test). + +-typing([eqwalizer]). +"#, + ); + + assert!(db.has_eqwalizer_module_marker(file_id)); + } + + #[test] + fn test_has_eqwalizer_app_marker() { + let (db, file_ids) = RootDatabase::with_many_files( + r#" +//- /src/test.erl +-module(test). +"#, + ); + + let source_root = db.file_source_root(file_ids[0]); + assert!(!db.has_eqwalizer_app_marker(source_root)); + + let (db, file_ids) = RootDatabase::with_many_files( + r#" +//- /src/test.erl +-module(test). +//- /.eqwalizer +"#, + ); + + let source_root = db.file_source_root(file_ids[0]); + assert!(db.has_eqwalizer_app_marker(source_root)); + } + + #[test] + fn test_has_eqwalizer_ignore_marker() { + let (db, file_id) = RootDatabase::with_single_file( + r#" +-module(test). +"#, + ); + + assert!(!db.has_eqwalizer_ignore_marker(file_id)); + + let (db, file_id) = RootDatabase::with_single_file( + r#" +-module(test). + +-eqwalizer(ignore). +"#, + ); + + assert!(db.has_eqwalizer_ignore_marker(file_id)); + + let (db, file_id) = RootDatabase::with_single_file( + r#" +-module(test). + +-eqwalizer(fixme). +"#, + ); + + assert!(db.has_eqwalizer_ignore_marker(file_id)); + } +} diff --git a/crates/ide_db/src/erl_ast.rs b/crates/ide_db/src/erl_ast.rs new file mode 100644 index 0000000000..c555cd8152 --- /dev/null +++ b/crates/ide_db/src/erl_ast.rs @@ -0,0 +1,127 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::path::PathBuf; +use std::sync::Arc; + +use elp_base_db::salsa; +use elp_base_db::AbsPath; +use elp_base_db::AbsPathBuf; +use elp_base_db::FileId; +use elp_base_db::ProjectId; +use elp_base_db::SourceDatabase; +use elp_erlang_service::Format; +use elp_erlang_service::ParseError; +use elp_erlang_service::ParseResult; + +use crate::erlang_service::CompileOption; +use crate::erlang_service::ParseRequest; +use crate::fixmes; +use crate::LineIndexDatabase; + +pub trait AstLoader { + fn load_ast( + &self, + project_id: ProjectId, + path: &AbsPath, + include_path: &[AbsPathBuf], + macros: &[eetf::Term], + parse_transforms: &[eetf::Term], + elp_metadata: eetf::Term, + format: Format, + ) -> ParseResult; +} + +impl AstLoader for crate::RootDatabase { + fn load_ast( + &self, + project_id: ProjectId, + path: &AbsPath, + include_path: &[AbsPathBuf], + macros: &[eetf::Term], + parse_transforms: &[eetf::Term], + elp_metadata: eetf::Term, + format: Format, + ) -> ParseResult { + let includes = include_path + .iter() + .map(|path| path.clone().into()) + .collect(); + let options = vec![ + CompileOption::Includes(includes), + CompileOption::Macros(macros.to_vec()), + CompileOption::ParseTransforms(parse_transforms.to_vec()), + CompileOption::ElpMetadata(elp_metadata), + ]; + let path = path.to_path_buf().into(); + let req = ParseRequest { + options, + path, + format, + }; + + if let Some(erlang_service) = self.erlang_services.read().get(&project_id).cloned() { + erlang_service.request_parse(req) + } else { + log::error!("No parse server for project: {:?}", project_id); + ParseResult::error(ParseError { + path: PathBuf::new(), + location: None, + msg: "Unknown application".to_string(), + code: "L0004".to_string(), + }) + } + } +} + +#[salsa::query_group(ErlAstDatabaseStorage)] +pub trait ErlAstDatabase: SourceDatabase + AstLoader + LineIndexDatabase { + fn module_ast(&self, file_id: FileId, format: Format) -> Arc; +} + +fn module_ast(db: &dyn ErlAstDatabase, file_id: FileId, format: Format) -> Arc { + // Dummy read of file text and global revision ID to make DB track changes + let _track_changes_to_file = db.file_text(file_id); + let _track_global_changes = db.include_files_revision(); + + let root_id = db.file_source_root(file_id); + let root = db.source_root(root_id); + let path = root.path_for_file(&file_id).unwrap().as_path().unwrap(); + let app_data = if let Some(app_data) = db.app_data(root_id) { + app_data + } else { + return Arc::new(ParseResult::error(ParseError { + path: path.to_path_buf().into(), + location: None, + msg: "Unknown application".to_string(), + code: "L0003".to_string(), + })); + }; + let metadata = elp_metadata(db, file_id).into(); + Arc::new(db.load_ast( + app_data.project_id, + path, + &app_data.include_path, + &app_data.macros, + &app_data.parse_transforms, + metadata, + format, + )) +} + +fn elp_metadata(db: &dyn ErlAstDatabase, file_id: FileId) -> eetf::Term { + let line_index = db.file_line_index(file_id); + let file_text = db.file_text(file_id); + let fixmes = fixmes::fixmes_eetf(&line_index, &file_text); + // Erlang proplist: [{eqwalizer_fixmes, [Fixme1, Fixme2....]}] + eetf::List::from(vec![ + eetf::Tuple::from(vec![eetf::Atom::from("eqwalizer_fixmes").into(), fixmes]).into(), + ]) + .into() +} diff --git a/crates/ide_db/src/fixmes.rs b/crates/ide_db/src/fixmes.rs new file mode 100644 index 0000000000..2fed2c6f51 --- /dev/null +++ b/crates/ide_db/src/fixmes.rs @@ -0,0 +1,89 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::convert::TryInto; + +use elp_syntax::TextRange; +use elp_syntax::TextSize; + +use crate::LineIndex; + +#[derive(Debug)] +struct Fixme { + comment_range: TextRange, + suppression_range: TextRange, + is_ignore: bool, +} + +// serialize as: +// {FixmeCommentStart, FixmeCommentEnd, SuppressionRangeStart, SuppressionRangeEnd, IsIgnore} +impl Into for Fixme { + fn into(self) -> eetf::Term { + let to_term = |n: TextSize| -> eetf::Term { + let n: u32 = n.into(); + // eetf::FixInteger holds an i32, which means + // we can support files with about 2 million LOC + // otherwise we blow up (calculation based on 1000 chars() per line) + let n: i32 = n.try_into().unwrap(); + eetf::FixInteger::from(n).into() + }; + eetf::Tuple::from(vec![ + to_term(self.comment_range.start()), + to_term(self.comment_range.end()), + to_term(self.suppression_range.start()), + to_term(self.suppression_range.end()), + eetf::Atom { + name: self.is_ignore.to_string(), + } + .into(), + ]) + .into() + } +} + +pub fn fixmes_eetf(line_index: &LineIndex, file_text: &str) -> eetf::Term { + let fixmes = collect_fixmes(line_index, file_text); + let fixmes: Vec = fixmes.into_iter().map(|f| f.into()).collect(); + eetf::List::from(fixmes).into() +} + +fn collect_fixmes(line_index: &LineIndex, file_text: &str) -> Vec { + let mut fixmes = Vec::new(); + let pats = vec![("% eqwalizer:fixme", false), ("% eqwalizer:ignore", true)]; + for (pat, is_ignore) in pats { + let len = pat.len(); + for (i, _) in file_text.match_indices(pat) { + let start = TextSize::from(i as u32); + let end = TextSize::from((i + len) as u32); + let line_num = line_index.line_col(start).line; + if let Some(suppression_start) = line_index.line_at(line_num as usize + 1) { + let suppression_end = { + let next_next_line_start: u32 = line_index + .line_at(line_num as usize + 2) + .unwrap_or_else( + // end of last line + || TextSize::from(file_text.chars().count() as u32), + ) + .into(); + TextSize::from(next_next_line_start - 1) + }; + // Temporary for T148094436 + let _pctx = stdx::panic_context::enter(format!("\ncollect_fixmes")); + let comment_range = TextRange::new(start, end); + let suppression_range = TextRange::new(suppression_start, suppression_end); + fixmes.push(Fixme { + comment_range, + suppression_range, + is_ignore, + }); + } + } + } + fixmes +} diff --git a/crates/ide_db/src/helpers.rs b/crates/ide_db/src/helpers.rs new file mode 100644 index 0000000000..91b5fda730 --- /dev/null +++ b/crates/ide_db/src/helpers.rs @@ -0,0 +1,37 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! A module with ide helpers for high-level ide features. + +use elp_syntax::SyntaxKind; +use elp_syntax::SyntaxToken; +use elp_syntax::TokenAtOffset; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SnippetCap { + _private: (), +} + +impl SnippetCap { + pub const fn new(allow_snippets: bool) -> Option { + if allow_snippets { + Some(SnippetCap { _private: () }) + } else { + None + } + } +} + +/// Picks the token with the highest rank returned by the passed in function. +pub fn pick_best_token( + tokens: TokenAtOffset, + f: impl Fn(SyntaxKind) -> usize, +) -> Option { + tokens.max_by_key(move |t| f(t.kind())) +} diff --git a/crates/ide_db/src/label.rs b/crates/ide_db/src/label.rs new file mode 100644 index 0000000000..aed6369326 --- /dev/null +++ b/crates/ide_db/src/label.rs @@ -0,0 +1,58 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! See [`Label`] +use std::fmt; + +/// A type to specify UI label, like an entry in the list of assists. Enforces +/// proper casing: +/// +/// Frobnicate bar +/// +/// Note the upper-case first letter and the absence of `.` at the end. +#[derive(Clone)] +pub struct Label(String); + +impl PartialEq for Label { + fn eq(&self, other: &str) -> bool { + self.0 == other + } +} + +impl PartialEq<&'_ str> for Label { + fn eq(&self, other: &&str) -> bool { + self == *other + } +} + +impl From
{ + support::children(&self.syntax) + } +} +#[doc = r" Via NodeType::Node 2 struct"] +impl AstNode for SourceFile { + fn can_cast(kind: SyntaxKind) -> bool { + kind == SOURCE_FILE + } + #[doc = r" Via field_casts"] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +#[doc = r" Via NodeType::Node 2 display"] +impl std::fmt::Display for SourceFile { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.syntax(), f) + } +} +#[doc = r" Via NodeType::Node 2 struct inner"] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Spec { + pub(crate) syntax: SyntaxNode, +} +impl Spec { + pub fn fun(&self) -> Option { + support::child(&self.syntax, 0usize) + } + pub fn module(&self) -> Option { + support::child(&self.syntax, 0usize) + } + pub fn sigs(&self) -> AstChildren { + support::children(&self.syntax) + } +} +#[doc = r" Via NodeType::Node 2 struct"] +impl AstNode for Spec { + fn can_cast(kind: SyntaxKind) -> bool { + kind == SPEC + } + #[doc = r" Via field_casts"] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +#[doc = r" Via NodeType::Node 2 display"] +impl std::fmt::Display for Spec { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.syntax(), f) + } +} +#[doc = r" Via NodeType::Literal 2"] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct String { + pub(crate) syntax: SyntaxNode, +} +#[doc = r" Via NodeType::Literal 2"] +impl String { + pub fn self_token(&self) -> Option { + support::token(&self.syntax, STRING, 0) + } +} +#[doc = r" Via NodeType::Literal 2"] +impl AstNode for String { + fn can_cast(kind: SyntaxKind) -> bool { + kind == STRING + } + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +#[doc = r" Via NodeType::Enum 2"] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum StringLike { + MacroCallExpr(MacroCallExpr), + MacroString(MacroString), + String(String), +} +impl AstNode for StringLike { + #[allow(clippy::match_like_matches_macro)] + fn can_cast(kind: SyntaxKind) -> bool { + match kind { + MACRO_CALL_EXPR | MACRO_STRING | STRING => true, + _ => false, + } + } + #[allow(clippy::match_like_matches_macro)] + fn cast(syntax: SyntaxNode) -> Option { + match syntax.kind() { + MACRO_CALL_EXPR => Some(StringLike::MacroCallExpr(MacroCallExpr { syntax })), + MACRO_STRING => Some(StringLike::MacroString(MacroString { syntax })), + STRING => Some(StringLike::String(String { syntax })), + _ => None, + } + } + fn syntax(&self) -> &SyntaxNode { + match self { + StringLike::MacroCallExpr(it) => it.syntax(), + StringLike::MacroString(it) => it.syntax(), + StringLike::String(it) => it.syntax(), + } + } +} +#[doc = r" Via NodeType::Enum 2 forms"] +impl From for StringLike { + fn from(node: MacroCallExpr) -> StringLike { + StringLike::MacroCallExpr(node) + } +} +impl From for StringLike { + fn from(node: MacroString) -> StringLike { + StringLike::MacroString(node) + } +} +impl From for StringLike { + fn from(node: String) -> StringLike { + StringLike::String(node) + } +} +#[doc = r" Via NodeType::Enum 2 display"] +impl std::fmt::Display for StringLike { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.syntax(), f) + } +} +#[doc = r" Via NodeType::Node 2 struct inner"] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct TryAfter { + pub(crate) syntax: SyntaxNode, +} +impl TryAfter { + pub fn exprs(&self) -> AstChildren { + support::children(&self.syntax) + } +} +#[doc = r" Via NodeType::Node 2 struct"] +impl AstNode for TryAfter { + fn can_cast(kind: SyntaxKind) -> bool { + kind == TRY_AFTER + } + #[doc = r" Via field_casts"] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +#[doc = r" Via NodeType::Node 2 display"] +impl std::fmt::Display for TryAfter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.syntax(), f) + } +} +#[doc = r" Via NodeType::Node 2 struct inner"] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct TryClass { + pub(crate) syntax: SyntaxNode, +} +impl TryClass { + pub fn class(&self) -> Option { + support::child(&self.syntax, 0usize) + } +} +#[doc = r" Via NodeType::Node 2 struct"] +impl AstNode for TryClass { + fn can_cast(kind: SyntaxKind) -> bool { + kind == TRY_CLASS + } + #[doc = r" Via field_casts"] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +#[doc = r" Via NodeType::Node 2 display"] +impl std::fmt::Display for TryClass { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.syntax(), f) + } +} +#[doc = r" Via NodeType::Node 2 struct inner"] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct TryExpr { + pub(crate) syntax: SyntaxNode, +} +impl TryExpr { + pub fn clauses(&self) -> AstChildren { + support::children(&self.syntax) + } + pub fn exprs(&self) -> AstChildren { + support::children(&self.syntax) + } + pub fn catch(&self) -> AstChildren { + support::children(&self.syntax) + } + pub fn after(&self) -> Option { + support::child(&self.syntax, 0usize) + } +} +#[doc = r" Via NodeType::Node 2 struct"] +impl AstNode for TryExpr { + fn can_cast(kind: SyntaxKind) -> bool { + kind == TRY_EXPR + } + #[doc = r" Via field_casts"] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +#[doc = r" Via NodeType::Node 2 display"] +impl std::fmt::Display for TryExpr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.syntax(), f) + } +} +#[doc = r" Via NodeType::Node 2 struct inner"] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct TryStack { + pub(crate) syntax: SyntaxNode, +} +impl TryStack { + pub fn class(&self) -> Option { + support::child(&self.syntax, 0usize) + } +} +#[doc = r" Via NodeType::Node 2 struct"] +impl AstNode for TryStack { + fn can_cast(kind: SyntaxKind) -> bool { + kind == TRY_STACK + } + #[doc = r" Via field_casts"] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +#[doc = r" Via NodeType::Node 2 display"] +impl std::fmt::Display for TryStack { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.syntax(), f) + } +} +#[doc = r" Via NodeType::Node 2 struct inner"] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Tuple { + pub(crate) syntax: SyntaxNode, +} +impl Tuple { + pub fn expr(&self) -> AstChildren { + support::children(&self.syntax) + } +} +#[doc = r" Via NodeType::Node 2 struct"] +impl AstNode for Tuple { + fn can_cast(kind: SyntaxKind) -> bool { + kind == TUPLE + } + #[doc = r" Via field_casts"] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +#[doc = r" Via NodeType::Node 2 display"] +impl std::fmt::Display for Tuple { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.syntax(), f) + } +} +#[doc = r" Via NodeType::Node 2 struct inner"] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct TypeAlias { + pub(crate) syntax: SyntaxNode, +} +impl TypeAlias { + pub fn ty(&self) -> Option { + support::child(&self.syntax, 0usize) + } + pub fn name(&self) -> Option { + support::child(&self.syntax, 0usize) + } +} +#[doc = r" Via NodeType::Node 2 struct"] +impl AstNode for TypeAlias { + fn can_cast(kind: SyntaxKind) -> bool { + kind == TYPE_ALIAS + } + #[doc = r" Via field_casts"] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +#[doc = r" Via NodeType::Node 2 display"] +impl std::fmt::Display for TypeAlias { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.syntax(), f) + } +} +#[doc = r" Via NodeType::Node 2 struct inner"] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct TypeGuards { + pub(crate) syntax: SyntaxNode, +} +impl TypeGuards { + pub fn guards(&self) -> AstChildren { + support::children(&self.syntax) + } +} +#[doc = r" Via NodeType::Node 2 struct"] +impl AstNode for TypeGuards { + fn can_cast(kind: SyntaxKind) -> bool { + kind == TYPE_GUARDS + } + #[doc = r" Via field_casts"] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +#[doc = r" Via NodeType::Node 2 display"] +impl std::fmt::Display for TypeGuards { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.syntax(), f) + } +} +#[doc = r" Via NodeType::Node 2 struct inner"] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct TypeName { + pub(crate) syntax: SyntaxNode, +} +impl TypeName { + pub fn name(&self) -> Option { + support::child(&self.syntax, 0usize) + } + pub fn args(&self) -> Option { + support::child(&self.syntax, 0usize) + } +} +#[doc = r" Via NodeType::Node 2 struct"] +impl AstNode for TypeName { + fn can_cast(kind: SyntaxKind) -> bool { + kind == TYPE_NAME + } + #[doc = r" Via field_casts"] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +#[doc = r" Via NodeType::Node 2 display"] +impl std::fmt::Display for TypeName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.syntax(), f) + } +} +#[doc = r" Via NodeType::Node 2 struct inner"] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct TypeSig { + pub(crate) syntax: SyntaxNode, +} +impl TypeSig { + pub fn ty(&self) -> Option { + support::child(&self.syntax, 0usize) + } + pub fn args(&self) -> Option { + support::child(&self.syntax, 0usize) + } + pub fn guard(&self) -> Option { + support::child(&self.syntax, 0usize) + } +} +#[doc = r" Via NodeType::Node 2 struct"] +impl AstNode for TypeSig { + fn can_cast(kind: SyntaxKind) -> bool { + kind == TYPE_SIG + } + #[doc = r" Via field_casts"] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +#[doc = r" Via NodeType::Node 2 display"] +impl std::fmt::Display for TypeSig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.syntax(), f) + } +} +#[doc = r" Via NodeType::Node 2 struct inner"] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct UnaryOpExpr { + pub(crate) syntax: SyntaxNode, +} +impl UnaryOpExpr { + pub fn operand(&self) -> Option { + support::child(&self.syntax, 0usize) + } +} +#[doc = r" Via NodeType::Node 2 struct"] +impl AstNode for UnaryOpExpr { + fn can_cast(kind: SyntaxKind) -> bool { + kind == UNARY_OP_EXPR + } + #[doc = r" Via field_casts"] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +#[doc = r" Via NodeType::Node 2 display"] +impl std::fmt::Display for UnaryOpExpr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.syntax(), f) + } +} +#[doc = r" Via NodeType::Literal 2"] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Var { + pub(crate) syntax: SyntaxNode, +} +#[doc = r" Via NodeType::Literal 2"] +impl Var { + pub fn self_token(&self) -> Option { + support::token(&self.syntax, VAR, 0) + } +} +#[doc = r" Via NodeType::Literal 2"] +impl AstNode for Var { + fn can_cast(kind: SyntaxKind) -> bool { + kind == VAR + } + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +#[doc = r" Via NodeType::Node 2 struct inner"] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct VarArgs { + pub(crate) syntax: SyntaxNode, +} +impl VarArgs { + pub fn args(&self) -> AstChildren { + support::children(&self.syntax) + } +} +#[doc = r" Via NodeType::Node 2 struct"] +impl AstNode for VarArgs { + fn can_cast(kind: SyntaxKind) -> bool { + kind == VAR_ARGS + } + #[doc = r" Via field_casts"] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +#[doc = r" Via NodeType::Node 2 display"] +impl std::fmt::Display for VarArgs { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.syntax(), f) + } +} +#[doc = r" Via NodeType::Node 2 struct inner"] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct WildAttribute { + pub(crate) syntax: SyntaxNode, +} +impl WildAttribute { + pub fn value(&self) -> Option { + support::child(&self.syntax, 0usize) + } + pub fn name(&self) -> Option { + support::child(&self.syntax, 0usize) + } +} +#[doc = r" Via NodeType::Node 2 struct"] +impl AstNode for WildAttribute { + fn can_cast(kind: SyntaxKind) -> bool { + kind == WILD_ATTRIBUTE + } + #[doc = r" Via field_casts"] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +#[doc = r" Via NodeType::Node 2 display"] +impl std::fmt::Display for WildAttribute { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.syntax(), f) + } +} +#[doc = r" Tell emacs to automatically reload this file if it changes"] +#[doc = r" Local Variables:"] +#[doc = r" auto-revert-mode: 1"] +#[doc = r" End:"] +fn _dummy() -> bool { + false +} diff --git a/crates/syntax/src/ast/node_ext.rs b/crates/syntax/src/ast/node_ext.rs new file mode 100644 index 0000000000..6720328222 --- /dev/null +++ b/crates/syntax/src/ast/node_ext.rs @@ -0,0 +1,807 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! Various extension methods to ast Nodes, which are hard to code-generate. +//! Extensions for various expressions live in a sibling `expr_extensions` module. + +use std::borrow::Cow; + +use rowan::GreenNodeData; +use rowan::GreenTokenData; +use rowan::NodeOrToken; +use rowan::TextRange; +use smol_str::SmolStr; + +use super::generated::nodes; +use super::ArithOp; +use super::BinaryOp; +use super::CompOp; +use super::ListOp; +use super::LogicOp; +use super::MapOp; +use super::Name; +use super::Ordering; +use super::PpDefine; +use super::UnaryOp; +use crate::ast::AstNode; +use crate::ast::SyntaxNode; +use crate::token_text::TokenText; +use crate::unescape::unescape_string; +use crate::SyntaxKind; +use crate::SyntaxKind::*; +use crate::SyntaxToken; + +impl nodes::MacroName { + pub fn raw_text(&self) -> TokenText<'_> { + match self { + nodes::MacroName::Atom(a) => a.raw_text(), + nodes::MacroName::Var(v) => v.text(), + } + } + pub fn text(&self) -> Option { + unescape_string(&self.raw_text()).map(|cow| cow.to_string()) + } +} + +impl nodes::FunDecl { + pub fn name(&self) -> Option { + match self.clauses().into_iter().next() { + Some(c) => match c { + nodes::FunctionOrMacroClause::FunctionClause(c) => c.name(), + nodes::FunctionOrMacroClause::MacroCallExpr(_) => None, + }, + None => None, + } + } +} + +impl nodes::Atom { + pub fn raw_text(&self) -> TokenText { + text_of_token(self.syntax()) + } + + pub fn text(&self) -> Option { + unescape_string(&self.raw_text()).map(|cow| cow.to_string()) + } +} + +impl nodes::Var { + pub fn text(&self) -> TokenText { + text_of_token(self.syntax()) + } +} + +impl nodes::Char { + pub fn text(&self) -> TokenText { + text_of_token(self.syntax()) + } +} + +impl nodes::Float { + pub fn text(&self) -> TokenText { + text_of_token(self.syntax()) + } +} + +impl nodes::Integer { + pub fn text(&self) -> TokenText { + text_of_token(self.syntax()) + } +} + +impl nodes::String { + pub fn text(&self) -> TokenText { + text_of_token(self.syntax()) + } +} + +fn text_of_token(node: &SyntaxNode) -> TokenText { + fn first_token(green_ref: &GreenNodeData) -> &GreenTokenData { + green_ref + .children() + .next() + .and_then(NodeOrToken::into_token) + .unwrap() + } + + match node.green() { + Cow::Borrowed(green_ref) => TokenText::borrowed(first_token(green_ref).text()), + Cow::Owned(green) => TokenText::owned(first_token(&green).to_owned()), + } +} + +// --------------------------------------------------------------------- + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Comment { + pub(crate) syntax: SyntaxNode, +} + +impl AstNode for Comment { + fn can_cast(kind: SyntaxKind) -> bool { + kind == COMMENT + } + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} + +// --------------------------------------------------------------------- + +/// Used for macro arity definitions. It distinguishes between +/// `-define(FOO,x). => None +/// `-define(FOO(),x). => Some(0) +pub type Arity = Option; + +pub trait HasArity { + fn arity_value(&self) -> Arity; +} + +impl HasArity for super::PpDefine { + fn arity_value(&self) -> super::Arity { + self.arity() + } +} + +impl HasArity for super::Fa { + fn arity_value(&self) -> Arity { + self.arity()?.value()?.arity_value() + } +} + +impl HasArity for super::TypeSig { + fn arity_value(&self) -> Arity { + Some(self.args()?.args().count()) + } +} + +impl HasArity for super::Spec { + fn arity_value(&self) -> Arity { + // We assume all the signatures have the same arity + self.sigs().next()?.arity_value() + } +} +impl HasArity for super::FunDecl { + fn arity_value(&self) -> Arity { + self.clauses().next()?.arity_value() + } +} +impl HasArity for super::FunctionOrMacroClause { + fn arity_value(&self) -> Arity { + match self { + super::FunctionOrMacroClause::FunctionClause(it) => it.arity_value(), + super::FunctionOrMacroClause::MacroCallExpr(it) => it.arity_value(), + } + } +} + +impl HasArity for super::FunctionClause { + fn arity_value(&self) -> Arity { + Some(self.args()?.args().count()) + } +} + +impl HasArity for super::FunClause { + fn arity_value(&self) -> Arity { + Some(self.args()?.args().count()) + } +} + +impl HasArity for super::MacroCallExpr { + fn arity_value(&self) -> Arity { + self.arity() + } +} + +impl HasArity for super::Call { + fn arity_value(&self) -> Arity { + Some(self.args()?.args().count()) + } +} + +impl HasArity for super::Callback { + fn arity_value(&self) -> Arity { + Some(self.sigs().into_iter().next()?.args()?.args().count()) + } +} + +impl HasArity for super::ArityValue { + fn arity_value(&self) -> Arity { + match self { + super::ArityValue::Integer(i) => Some((*i).clone().into()), + super::ArityValue::MacroCallExpr(_) => None, + super::ArityValue::Var(_) => None, + } + } +} + +impl HasArity for super::InternalFun { + fn arity_value(&self) -> Arity { + self.arity()?.value()?.arity_value() + } +} + +impl HasArity for super::ExternalFun { + fn arity_value(&self) -> Arity { + self.arity()?.value()?.arity_value() + } +} + +impl HasArity for super::TypeAlias { + fn arity_value(&self) -> Arity { + Some(self.name()?.args()?.args().count()) + } +} + +impl HasArity for super::Opaque { + fn arity_value(&self) -> Arity { + Some(self.name()?.args()?.args().count()) + } +} + +impl HasArity for super::ExprArgs { + fn arity_value(&self) -> Arity { + Some(self.args().count()) + } +} + +// --------------------------------------------------------------------- + +impl PpDefine { + pub fn args(&self) -> impl Iterator { + self.lhs() + .into_iter() + .flat_map(|args| args.args()) + .flat_map(|args| args.args()) + } + + pub fn name(&self) -> Option { + self.lhs()?.name() + } + + pub fn arity(&self) -> Arity { + self.lhs()?.args().map(|_| self.args().count()) + } +} + +impl super::MacroCallExpr { + pub fn arity(&self) -> Option { + self.args().map(|args| args.args().count()) + } +} + +// --------------------------------------------------------------------- + +impl super::ExprMax { + pub fn name(&self) -> Option { + match self { + super::ExprMax::Atom(it) => Some(it.text()?.into()), + super::ExprMax::Var(it) => Some(it.text().into()), + super::ExprMax::ParenExpr(it) => match it.expr()? { + super::Expr::ExprMax(e) => e.name(), + _ => None, + }, + _ => None, + } + } +} + +// --------------------------------------------------------------------- + +impl Name { + pub fn text(&self) -> Option { + match self { + Name::Atom(atom) => atom.text(), + Name::MacroCallExpr(_) => None, + Name::Var(v) => Some(v.text().to_string()), + } + } +} + +// --------------------------------------------------------------------- + +impl nodes::PpInclude { + pub fn text_range(&self) -> TextRange { + self.syntax.text_range() + } +} +impl nodes::PpIncludeLib { + pub fn text_range(&self) -> TextRange { + self.syntax.text_range() + } +} + +// --------------------------------------------------------------------- +// rust standard types conversions +// --------------------------------------------------------------------- + +impl nodes::Integer { + /// Erlang Integer literals can have underscores. Return text with + /// them stripped out. + fn clean_text(self) -> String { + let text: std::string::String = From::from(self.syntax.text()); + text.replace('_', "") + } +} + +impl From for usize { + fn from(i: nodes::Integer) -> Self { + i.clean_text().trim().parse().unwrap() + } +} +impl From for u32 { + fn from(i: nodes::Integer) -> Self { + i.clean_text().trim().parse().unwrap() + } +} +impl From for i32 { + fn from(i: nodes::Integer) -> Self { + i.clean_text().trim().parse().unwrap() + } +} +impl From for std::string::String { + fn from(s: nodes::String) -> Self { + let source: std::string::String = From::from(s.syntax.text()); + trim_quotes(source) + } +} + +fn trim_quotes(s: String) -> String { + s.as_str().trim_matches(|c: char| c == '"').to_string() +} + +// Operators + +impl super::UnaryOpExpr { + pub fn op(&self) -> Option<(UnaryOp, SyntaxToken)> { + unary_op(self.syntax()) + } +} + +impl super::BinaryOpExpr { + pub fn op(&self) -> Option<(BinaryOp, SyntaxToken)> { + binary_op(self.syntax()) + } +} + +impl super::MapField { + pub fn op(&self) -> Option<(MapOp, SyntaxToken)> { + map_op(self.syntax()) + } +} + +fn unary_op(parent: &SyntaxNode) -> Option<(UnaryOp, SyntaxToken)> { + parent + .children_with_tokens() + .filter_map(|it| it.into_token()) + .find_map(|c| Some((match_unary_op(&c)?, c))) +} + +fn binary_op(parent: &SyntaxNode) -> Option<(BinaryOp, SyntaxToken)> { + parent + .children_with_tokens() + .filter_map(|it| it.into_token()) + .find_map(|c| Some((match_binary_op(&c)?, c))) +} + +fn map_op(parent: &SyntaxNode) -> Option<(MapOp, SyntaxToken)> { + parent + .children_with_tokens() + .filter_map(|it| it.into_token()) + .find_map(|c| Some((match_map_op(&c)?, c))) +} + +fn match_unary_op(c: &SyntaxToken) -> Option { + match c.kind() { + ANON_PLUS => Some(UnaryOp::Plus), + ANON_DASH => Some(UnaryOp::Minus), + ANON_BNOT => Some(UnaryOp::Bnot), + ANON_NOT => Some(UnaryOp::Not), + _ => None, + } +} + +fn match_binary_op(c: &SyntaxToken) -> Option { + match c.kind() { + ANON_AND => Some(BinaryOp::LogicOp(LogicOp::And { lazy: false })), + ANON_ANDALSO => Some(BinaryOp::LogicOp(LogicOp::And { lazy: true })), + ANON_OR => Some(BinaryOp::LogicOp(LogicOp::Or { lazy: false })), + ANON_ORELSE => Some(BinaryOp::LogicOp(LogicOp::Or { lazy: true })), + ANON_XOR => Some(BinaryOp::LogicOp(LogicOp::Xor)), + + ANON_DASH_DASH => Some(BinaryOp::ListOp(ListOp::Subtract)), + ANON_PLUS_PLUS => Some(BinaryOp::ListOp(ListOp::Append)), + + ANON_BANG => Some(BinaryOp::Send), + + ANON_BAND => Some(BinaryOp::ArithOp(ArithOp::Band)), + ANON_BOR => Some(BinaryOp::ArithOp(ArithOp::Bor)), + ANON_BSL => Some(BinaryOp::ArithOp(ArithOp::Bsl)), + ANON_BSR => Some(BinaryOp::ArithOp(ArithOp::Bsr)), + ANON_BXOR => Some(BinaryOp::ArithOp(ArithOp::Bxor)), + ANON_DASH => Some(BinaryOp::ArithOp(ArithOp::Sub)), + ANON_DIV => Some(BinaryOp::ArithOp(ArithOp::Div)), + ANON_PLUS => Some(BinaryOp::ArithOp(ArithOp::Add)), + ANON_REM => Some(BinaryOp::ArithOp(ArithOp::Rem)), + ANON_SLASH => Some(BinaryOp::ArithOp(ArithOp::FloatDiv)), + ANON_STAR => Some(BinaryOp::ArithOp(ArithOp::Mul)), + + ANON_EQ_COLON_EQ => Some(BinaryOp::CompOp(CompOp::Eq { + strict: true, + negated: false, + })), + ANON_EQ_EQ => Some(BinaryOp::CompOp(CompOp::Eq { + strict: false, + negated: false, + })), + ANON_EQ_SLASH_EQ => Some(BinaryOp::CompOp(CompOp::Eq { + strict: true, + negated: true, + })), + ANON_SLASH_EQ => Some(BinaryOp::CompOp(CompOp::Eq { + strict: false, + negated: true, + })), + ANON_EQ_LT => Some(BinaryOp::CompOp(CompOp::Ord { + ordering: Ordering::Less, + strict: false, + })), + ANON_GT => Some(BinaryOp::CompOp(CompOp::Ord { + ordering: Ordering::Greater, + strict: true, + })), + ANON_GT_EQ => Some(BinaryOp::CompOp(CompOp::Ord { + ordering: Ordering::Greater, + strict: false, + })), + ANON_LT => Some(BinaryOp::CompOp(CompOp::Ord { + ordering: Ordering::Less, + strict: true, + })), + + _ => None, + } +} + +fn match_map_op(c: &SyntaxToken) -> Option { + match c.kind() { + ANON_COLON_EQ => Some(MapOp::Exact), + ANON_EQ_GT => Some(MapOp::Assoc), + _ => None, + } +} + +// --------------------------------------------------------------------- +// conversions between different "expr" types +// --------------------------------------------------------------------- + +impl From for nodes::Expr { + fn from(expr: nodes::BitExpr) -> Self { + match expr { + nodes::BitExpr::ExprMax(expr) => nodes::Expr::ExprMax(expr), + nodes::BitExpr::BinaryOpExpr(expr) => nodes::Expr::BinaryOpExpr(expr), + nodes::BitExpr::UnaryOpExpr(expr) => nodes::Expr::UnaryOpExpr(expr), + } + } +} + +impl From for nodes::Expr { + fn from(expr: nodes::RecordExprBase) -> Self { + match expr { + nodes::RecordExprBase::ExprMax(expr) => nodes::Expr::ExprMax(expr), + nodes::RecordExprBase::RecordExpr(expr) => nodes::Expr::RecordExpr(expr), + nodes::RecordExprBase::RecordFieldExpr(expr) => nodes::Expr::RecordFieldExpr(expr), + nodes::RecordExprBase::RecordIndexExpr(expr) => nodes::Expr::RecordIndexExpr(expr), + nodes::RecordExprBase::RecordUpdateExpr(expr) => nodes::Expr::RecordUpdateExpr(expr), + } + } +} + +impl From for nodes::Expr { + fn from(expr: nodes::MapExprBase) -> Self { + match expr { + nodes::MapExprBase::ExprMax(expr) => nodes::Expr::ExprMax(expr), + nodes::MapExprBase::MapExpr(expr) => nodes::Expr::MapExpr(expr), + nodes::MapExprBase::MapExprUpdate(expr) => nodes::Expr::MapExprUpdate(expr), + } + } +} + +impl From for nodes::Expr { + fn from(expr: nodes::Name) -> Self { + match expr { + Name::Atom(atom) => nodes::Expr::ExprMax(nodes::ExprMax::Atom(atom)), + Name::MacroCallExpr(mac) => nodes::Expr::ExprMax(nodes::ExprMax::MacroCallExpr(mac)), + Name::Var(var) => nodes::Expr::ExprMax(nodes::ExprMax::Var(var)), + } + } +} + +impl From for nodes::Expr { + fn from(expr: nodes::ArityValue) -> Self { + match expr { + nodes::ArityValue::Integer(int) => nodes::Expr::ExprMax(nodes::ExprMax::Integer(int)), + nodes::ArityValue::MacroCallExpr(mac) => { + nodes::Expr::ExprMax(nodes::ExprMax::MacroCallExpr(mac)) + } + nodes::ArityValue::Var(var) => nodes::Expr::ExprMax(nodes::ExprMax::Var(var)), + } + } +} + +impl From for nodes::Expr { + fn from(expr: nodes::CatchPat) -> Self { + match expr { + nodes::CatchPat::ExprMax(expr_max) => nodes::Expr::ExprMax(expr_max), + nodes::CatchPat::BinaryOpExpr(binary_op) => nodes::Expr::BinaryOpExpr(binary_op), + nodes::CatchPat::MapExpr(map) => nodes::Expr::MapExpr(map), + nodes::CatchPat::RecordExpr(record) => nodes::Expr::RecordExpr(record), + nodes::CatchPat::RecordIndexExpr(index) => nodes::Expr::RecordIndexExpr(index), + nodes::CatchPat::UnaryOpExpr(unary_op) => nodes::Expr::UnaryOpExpr(unary_op), + } + } +} + +impl From for nodes::Expr { + fn from(var: nodes::Var) -> Self { + nodes::Expr::ExprMax(nodes::ExprMax::Var(var)) + } +} + +// --------------------------------------------------------------------- +// missing From implementations for nested forms +// --------------------------------------------------------------------- + +impl From for nodes::Form { + fn from(node: nodes::PpDefine) -> nodes::Form { + nodes::Form::PreprocessorDirective(node.into()) + } +} +impl From for nodes::Form { + fn from(node: nodes::PpElif) -> nodes::Form { + nodes::Form::PreprocessorDirective(node.into()) + } +} +impl From for nodes::Form { + fn from(node: nodes::PpElse) -> nodes::Form { + nodes::Form::PreprocessorDirective(node.into()) + } +} +impl From for nodes::Form { + fn from(node: nodes::PpEndif) -> nodes::Form { + nodes::Form::PreprocessorDirective(node.into()) + } +} +impl From for nodes::Form { + fn from(node: nodes::PpIf) -> nodes::Form { + nodes::Form::PreprocessorDirective(node.into()) + } +} +impl From for nodes::Form { + fn from(node: nodes::PpIfdef) -> nodes::Form { + nodes::Form::PreprocessorDirective(node.into()) + } +} +impl From for nodes::Form { + fn from(node: nodes::PpIfndef) -> nodes::Form { + nodes::Form::PreprocessorDirective(node.into()) + } +} +impl From for nodes::Form { + fn from(node: nodes::PpInclude) -> nodes::Form { + nodes::Form::PreprocessorDirective(node.into()) + } +} +impl From for nodes::Form { + fn from(node: nodes::PpIncludeLib) -> nodes::Form { + nodes::Form::PreprocessorDirective(node.into()) + } +} +impl From for nodes::Form { + fn from(node: nodes::PpUndef) -> nodes::Form { + nodes::Form::PreprocessorDirective(node.into()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast; + + fn parse_expr(arg: &str) -> ast::Expr { + let text = format!("f() -> {}.", arg); + let parse = ast::SourceFile::parse_text(&text); + + if !parse.errors().is_empty() { + panic!("expected no parse errors, got: {:?}", parse.errors()); + } + + let fun = match parse.tree().forms().next().expect("no form parsed") { + ast::Form::FunDecl(fun) => fun, + got => panic!("expected fun, got: {:?}", got), + }; + + let clause = match fun.clauses().next().expect("no clauses parsed") { + ast::FunctionOrMacroClause::FunctionClause(clause) => clause, + got => panic!("expected clause, got: {:?}", got), + }; + + clause + .body() + .iter() + .flat_map(|body| body.exprs()) + .next() + .expect("no expression parsed") + } + + #[test] + fn test_unary_op_expr() { + fn check(parse: &str, expected_op_text: &str, expected_op: UnaryOp) { + let (op, token) = match parse_expr(parse) { + ast::Expr::UnaryOpExpr(unary_op) => unary_op.op().expect("no operator found"), + got => panic!("expected unary op, got: {:?}", got), + }; + + assert_eq!(token.text(), expected_op_text); + assert_eq!(op.to_string(), expected_op_text); + assert_eq!(op, expected_op); + } + + check("+1", "+", UnaryOp::Plus); + check("-1", "-", UnaryOp::Minus); + check("not 1", "not", UnaryOp::Not); + check("bnot 1", "bnot", UnaryOp::Bnot); + } + + #[test] + fn test_binary_op_expr() { + fn check(parse: &str, expected_op_text: &str, expected_op: BinaryOp) { + let (op, token) = match parse_expr(parse) { + ast::Expr::BinaryOpExpr(binary_op) => binary_op.op().expect("no operator found"), + got => panic!("expected binary op, got: {:?}", got), + }; + + assert_eq!(token.text(), expected_op_text); + assert_eq!(op.to_string(), expected_op_text); + assert_eq!(op, expected_op); + } + + check( + "X and Y", + "and", + BinaryOp::LogicOp(LogicOp::And { lazy: false }), + ); + check( + "X andalso Y", + "andalso", + BinaryOp::LogicOp(LogicOp::And { lazy: true }), + ); + check( + "X or Y", + "or", + BinaryOp::LogicOp(LogicOp::Or { lazy: false }), + ); + check( + "X orelse Y", + "orelse", + BinaryOp::LogicOp(LogicOp::Or { lazy: true }), + ); + check("X xor Y", "xor", BinaryOp::LogicOp(LogicOp::Xor)); + + check("X ++ Y", "++", BinaryOp::ListOp(ListOp::Append)); + check("X -- Y", "--", BinaryOp::ListOp(ListOp::Subtract)); + + check("X ! Y", "!", BinaryOp::Send); + + check("X / Y", "/", BinaryOp::ArithOp(ArithOp::FloatDiv)); + check("X * Y", "*", BinaryOp::ArithOp(ArithOp::Mul)); + check("X - Y", "-", BinaryOp::ArithOp(ArithOp::Sub)); + check("X + Y", "+", BinaryOp::ArithOp(ArithOp::Add)); + check("X band Y", "band", BinaryOp::ArithOp(ArithOp::Band)); + check("X bor Y", "bor", BinaryOp::ArithOp(ArithOp::Bor)); + check("X bsl Y", "bsl", BinaryOp::ArithOp(ArithOp::Bsl)); + check("X bsr Y", "bsr", BinaryOp::ArithOp(ArithOp::Bsr)); + check("X div Y", "div", BinaryOp::ArithOp(ArithOp::Div)); + check("X rem Y", "rem", BinaryOp::ArithOp(ArithOp::Rem)); + + check( + "X =:= Y", + "=:=", + BinaryOp::CompOp(CompOp::Eq { + strict: true, + negated: false, + }), + ); + check( + "X == Y", + "==", + BinaryOp::CompOp(CompOp::Eq { + strict: false, + negated: false, + }), + ); + check( + "X =/= Y", + "=/=", + BinaryOp::CompOp(CompOp::Eq { + strict: true, + negated: true, + }), + ); + check( + "X /= Y", + "/=", + BinaryOp::CompOp(CompOp::Eq { + strict: false, + negated: true, + }), + ); + check( + "X =< Y", + "=<", + BinaryOp::CompOp(CompOp::Ord { + ordering: Ordering::Less, + strict: false, + }), + ); + check( + "X > Y", + ">", + BinaryOp::CompOp(CompOp::Ord { + ordering: Ordering::Greater, + strict: true, + }), + ); + check( + "X >= Y", + ">=", + BinaryOp::CompOp(CompOp::Ord { + ordering: Ordering::Greater, + strict: false, + }), + ); + check( + "X < Y", + "<", + BinaryOp::CompOp(CompOp::Ord { + ordering: Ordering::Less, + strict: true, + }), + ); + } + + #[test] + fn test_unary_map_expr() { + fn check(parse: &str, expected_op_text: &str, expected_op: MapOp) { + let parse = format!("#{{{}}}", parse); + let map_expr = match parse_expr(&parse) { + ast::Expr::MapExpr(map_expr) => map_expr, + got => panic!("expected map expr, got: {:?}", got), + }; + + let field = map_expr.fields().next().expect("no map fields parsed"); + + let (op, token) = field.op().unwrap(); + + assert_eq!(token.text(), expected_op_text); + assert_eq!(op.to_string(), expected_op_text); + assert_eq!(op, expected_op); + } + + check("X => Y", "=>", MapOp::Assoc); + check("X := Y", ":=", MapOp::Exact); + } +} diff --git a/crates/syntax/src/ast/operators.rs b/crates/syntax/src/ast/operators.rs new file mode 100644 index 0000000000..458fd7bea7 --- /dev/null +++ b/crates/syntax/src/ast/operators.rs @@ -0,0 +1,191 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::fmt; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub enum UnaryOp { + /// `+` + Plus, + /// `-` + Minus, + /// `bnot` + Bnot, + /// `not` + Not, +} + +impl fmt::Display for UnaryOp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let str = match self { + UnaryOp::Plus => "+", + UnaryOp::Minus => "-", + UnaryOp::Bnot => "bnot", + UnaryOp::Not => "not", + }; + f.write_str(str) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub enum BinaryOp { + LogicOp(LogicOp), + ArithOp(ArithOp), + ListOp(ListOp), + CompOp(CompOp), + /// `!` + Send, +} + +impl fmt::Display for BinaryOp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BinaryOp::LogicOp(logic_op) => logic_op.fmt(f), + BinaryOp::ArithOp(arith_op) => arith_op.fmt(f), + BinaryOp::ListOp(list_op) => list_op.fmt(f), + BinaryOp::CompOp(comp_op) => comp_op.fmt(f), + BinaryOp::Send => f.write_str("!"), + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub enum LogicOp { + /// `and` and `andalso` + And { lazy: bool }, + /// `or` and `orelse` + Or { lazy: bool }, + /// `xor` + Xor, +} + +impl fmt::Display for LogicOp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let str = match self { + LogicOp::And { lazy: true } => "andalso", + LogicOp::And { lazy: false } => "and", + LogicOp::Or { lazy: true } => "orelse", + LogicOp::Or { lazy: false } => "or", + LogicOp::Xor => "xor", + }; + f.write_str(str) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub enum ArithOp { + /// `+` + Add, + /// `*` + Mul, + /// `-` + Sub, + /// `/` + FloatDiv, + /// `div` + Div, + /// `rem` + Rem, + /// `band` + Band, + /// `bor` + Bor, + /// `bxor` + Bxor, + /// `bsr` + Bsr, + /// `bsl` + Bsl, +} + +impl fmt::Display for ArithOp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let str = match self { + ArithOp::Add => "+", + ArithOp::Mul => "*", + ArithOp::Sub => "-", + ArithOp::FloatDiv => "/", + ArithOp::Div => "div", + ArithOp::Rem => "rem", + ArithOp::Band => "band", + ArithOp::Bor => "bor", + ArithOp::Bxor => "bxor", + ArithOp::Bsr => "bsr", + ArithOp::Bsl => "bsl", + }; + f.write_str(str) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub enum ListOp { + /// `++` + Append, + /// `--` + Subtract, +} + +impl fmt::Display for ListOp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let str = match self { + ListOp::Append => "++", + ListOp::Subtract => "--", + }; + f.write_str(str) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub enum Ordering { + Less, + Greater, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub enum CompOp { + /// `==`, `/=` `=:=` and `=/=` + Eq { strict: bool, negated: bool }, + /// `=<`, `<`, `>=`, `>` + Ord { ordering: Ordering, strict: bool }, +} + +impl fmt::Display for CompOp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + #[rustfmt::skip] + let str = match self { + CompOp::Eq { strict: true, negated: false } => "=:=", + CompOp::Eq { strict: false, negated: false } => "==", + CompOp::Eq { strict: true, negated: true } => "=/=", + CompOp::Eq { strict: false, negated: true } => "/=", + CompOp::Ord { ordering: Ordering::Greater, strict: true } => ">", + CompOp::Ord { ordering: Ordering::Greater, strict: false } => ">=", + CompOp::Ord { ordering: Ordering::Less, strict: true } => "<", + CompOp::Ord { ordering: Ordering::Less, strict: false } => "=<", + }; + f.write_str(str) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub enum MapOp { + /// `=>` + Assoc, + /// `:=` + Exact, +} + +impl fmt::Display for MapOp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let str = match self { + MapOp::Assoc => "=>", + MapOp::Exact => ":=", + }; + f.write_str(str) + } +} diff --git a/crates/syntax/src/ast/traits.rs b/crates/syntax/src/ast/traits.rs new file mode 100644 index 0000000000..44bf1ede01 --- /dev/null +++ b/crates/syntax/src/ast/traits.rs @@ -0,0 +1,48 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! Various traits that are implemented by ast nodes. +//! +//! The implementations are usually trivial, and live in generated.rs + +use super::HasArity; +use crate::ast; +use crate::ast::support; +use crate::ast::AstNode; + +pub trait HasLabel: AstNode { + fn label(&self) -> Option { + let name: ast::Name = support::child(self.syntax(), 0)?; + name.text() + } +} + +impl HasLabel for ast::FunDecl { + fn label(&self) -> Option { + let name = self.name()?.text()?; + let arity = self.arity_value()?; + Some(format!("{}/{}", name, arity)) + } +} +impl HasLabel for ast::RecordDecl {} +impl HasLabel for ast::TypeAlias { + fn label(&self) -> Option { + self.name()?.name()?.text() + } +} +impl HasLabel for ast::PpDefine { + fn label(&self) -> Option { + let name = self.name()?.text()?; + if let Some(arity) = self.arity_value() { + Some(format!("{}/{}", name, arity)) + } else { + Some(name) + } + } +} diff --git a/crates/syntax/src/lib.rs b/crates/syntax/src/lib.rs new file mode 100644 index 0000000000..e11a8b7fd8 --- /dev/null +++ b/crates/syntax/src/lib.rs @@ -0,0 +1,854 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::convert::TryInto; +use std::marker::PhantomData; +use std::ops::Range; +use std::sync::Arc; + +use num_traits::FromPrimitive; +use rowan::GreenNodeBuilder; +use rowan::Language; +use tree_sitter::Node; +use tree_sitter::Tree; +use tree_sitter::TreeCursor; + +use crate::tree_sitter_elp::Parser; + +mod ptr; +mod syntax_error; +mod syntax_kind; +mod token_text; + +pub mod algo; +pub mod ast; +pub mod syntax_node; +pub mod ted; +pub mod tree_sitter_elp; +pub mod unescape; + +pub use rowan::Direction; +pub use rowan::GreenNode; +pub use rowan::TextRange; +pub use rowan::TextSize; +pub use rowan::TokenAtOffset; +pub use rowan::WalkEvent; +pub use smol_str::SmolStr; +pub use syntax_kind::SyntaxKind; +pub use syntax_node::*; + +pub use crate::algo::InsertPosition; +pub use crate::ast::AstNode; +pub use crate::ast::SourceFile; +pub use crate::ptr::AstPtr; +pub use crate::ptr::SyntaxNodePtr; +pub use crate::syntax_error::SyntaxError; +pub use crate::token_text::TokenText; + +/// `Parse` is the result of the parsing: a syntax tree and a collection of +/// errors. +/// +/// Note that we always produce a syntax tree, even for completely invalid +/// files. +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct Parse { + green: GreenNode, + errors: Arc>, + _ty: PhantomData T>, +} + +impl Clone for Parse { + fn clone(&self) -> Parse { + Parse { + green: self.green.clone(), + errors: self.errors.clone(), + _ty: PhantomData, + } + } +} + +impl Parse { + pub fn syntax_node(&self) -> SyntaxNode { + SyntaxNode::new_root(self.green.clone()) + } + + pub fn errors(&self) -> &[SyntaxError] { + &self.errors + } +} + +impl Parse { + pub fn tree(&self) -> T { + T::cast(self.syntax_node()).unwrap() + } + + pub fn ok(self) -> Result>> { + if self.errors.is_empty() { + Ok(self.tree()) + } else { + Err(self.errors) + } + } + + pub fn from_ast(ast: &T) -> Parse { + Parse { + green: ast.syntax().green().into(), + errors: Arc::new(vec![]), + _ty: PhantomData, + } + } +} + +// --------------------------------------------------------------------- + +struct Converter<'tree, 'text> { + cursor: TreeCursor<'tree>, + text: &'text str, + errors: Vec, + builder: GreenNodeBuilder<'static>, + last_position: usize, + // Debug counter to try and track down the panic on calling self.builder.finish() + open_count: isize, +} + +impl<'tree, 'text> Converter<'tree, 'text> { + pub fn new(tree: &'tree Tree, text: &'text str) -> Converter<'tree, 'text> { + Converter { + cursor: tree.walk(), + text, + errors: Vec::new(), + builder: GreenNodeBuilder::new(), + last_position: 0, + open_count: 0, + } + } + + pub fn convert(mut self) -> (GreenNode, Vec) { + self.convert_node(); + + if self.open_count > 0 { + // If the open_count is > 0 it means we have unfinished builder nodes. + // Log the text for later analysis, and close the nodes. T106727908 + log::error!( + "Converter::convert:open_count={}\n,text=[\n{}\n]", + self.open_count, + self.text + ); + while self.open_count > 0 { + self.builder.finish_node(); + self.open_count -= 1; + } + } + + (self.builder.finish(), self.errors) + } + + fn enter_node(&mut self, root: bool) -> bool { + let node = self.cursor.node(); + let kind = SyntaxKind::from_u16(node.kind_id()).unwrap(); + let range = node.byte_range(); + if node.is_error() { + let mut ret = false; + if root { + // Our parser has an invariant that the top level + // node is of kind SOURCE_FILE. We are aborting after + // adding this node, so make sure this is the case + self.start_node(SyntaxKind::SOURCE_FILE, range.start, root); + // This is an aberrant case, do not bother inserting + // the structure from the ERROR node + self.token(kind, &range, root); + self.finish_node(range.end, root); + } else if node.child_count() == 0 { + self.token(kind, &range, root); + } else { + self.start_node(kind, range.start, root); + ret = true; + } + + self.error("Error: ignoring", range); + ret + } else if node.is_missing() { + let text = node.kind(); + self.error(format!("Missing {}", text), range); + false + } else if node.child_count() == 0 { + if node.is_named() { + // We capture the node, and its enclosed token + self.start_node(kind, range.start, root); + self.token(kind, &range, false); + self.finish_node(range.end, root); + } else { + self.token(kind, &range, root); + } + false + } else { + self.start_node(kind, range.start, root); + true + } + } + + fn exit_node(&mut self, node: Node, root: bool) { + if !((root && node.is_error()) || node.is_missing() || node.child_count() == 0) { + let range = node.byte_range(); + self.finish_node(range.end, root); + } + } + + // Based on https://github.com/tree-sitter/tree-sitter/discussions/878 + fn convert_node(&mut self) { + if self.enter_node(true) { + let mut recurse = true; + + loop { + // enter child + if recurse && self.cursor.goto_first_child() { + recurse = self.enter_node(false); + } else { + let node = self.cursor.node(); + + // go to sibling + if self.cursor.goto_next_sibling() { + self.exit_node(node, false); + recurse = self.enter_node(false); + } else if self.cursor.goto_parent() { + self.exit_node(node, false); + recurse = false; + } else { + self.exit_node(node, true); + break; + } + } + } + } else { + let node = self.cursor.node(); + self.exit_node(node, true); + } + } + + fn error(&mut self, msg: impl Into, range: Range) { + let range = convert_range(range); + self.errors.push(SyntaxError::new(msg, range)); + } + + fn token(&mut self, kind: SyntaxKind, range: &Range, root: bool) { + if root { + self.start_node(kind, range.start, root); + } + + self.update_position(range.start, range.end); + let text = &self.text[range.clone()]; + self.builder.token(ELPLanguage::kind_to_raw(kind), text); + + if root { + self.finish_node(range.end, root); + } + } + + fn start_node(&mut self, kind: SyntaxKind, range_start: usize, root: bool) { + // Root node has whitespace inside of it (since there's nothing outside), + // all other nodes have it outside + if !root { + self.update_position(range_start, range_start); + } + self.builder.start_node(ELPLanguage::kind_to_raw(kind)); + self.open_count += 1; + if root { + self.update_position(range_start, range_start); + } + } + + fn finish_node(&mut self, range_end: usize, root: bool) { + // Root node has whitespace inside of it (since there's nothing outside), + // all other nodes have it outside + if root { + self.update_position(range_end, range_end); + } + self.builder.finish_node(); + self.open_count -= 1; + if !root { + self.update_position(range_end, range_end); + } + } + + fn update_position(&mut self, start: usize, end: usize) { + if self.last_position < start { + let kind = ELPLanguage::kind_to_raw(SyntaxKind::WHITESPACE); + let text = &self.text[self.last_position..start]; + self.builder.token(kind, text); + } + self.last_position = end; + } +} + +fn convert_range(range: Range) -> TextRange { + // Temporary for T148094436 + let _pctx = stdx::panic_context::enter(format!("\nsyntax::convert_range")); + TextRange::new( + range.start.try_into().unwrap(), + range.end.try_into().unwrap(), + ) +} + +// --------------------------------------------------------------------- + +impl SourceFile { + pub fn parse_text(text: &str) -> Parse { + let mut parser = Parser::new(); + let tree = parser.parse(text).expect("parsing should always succeed"); + let (green, errors) = Converter::new(&tree, text).convert(); + let root = SyntaxNode::new_root(green.clone()); + + assert_eq!(root.kind(), SyntaxKind::SOURCE_FILE); + Parse { + green, + errors: Arc::new(errors), + _ty: PhantomData, + } + } +} + +// --------------------------------------------------------------------- + +/// Matches a `SyntaxNode` against an `ast` type. +/// +/// # Example: +/// +/// ```ignore +/// match_ast! { +/// match node { +/// ast::CallExpr(it) => { ... }, +/// ast::MethodCallExpr(it) => { ... }, +/// ast::MacroCallExpr(it) => { ... }, +/// _ => None, +/// } +/// } +/// ``` +#[macro_export] +macro_rules! match_ast { + (match $node:ident { $($tt:tt)* }) => { match_ast!(match ($node) { $($tt)* }) }; + + (match ($node:expr) { + $( ast::$ast:ident($it:pat) => $res:expr, )* + _ => $catch_all:expr $(,)? + }) => {{ + $( if let Some($it) = ast::$ast::cast($node.clone()) { $res } else )* + { $catch_all } + }}; +} + +// --------------------------------------------------------------------- + +// To run the tests via cargo +// cargo test --package elp_syntax --lib +#[cfg(test)] +mod tests { + use expect_test::expect; + use expect_test::Expect; + use rowan::Direction; + use rowan::NodeOrToken; + use rowan::SyntaxText; + use rowan::WalkEvent; + use stdx::format_to; + + use super::*; + + #[test] + fn syntax_node() { + check_node( + "foo(1) -> 2 + 3.", + expect![[r#" + SOURCE_FILE@0..16 + FUN_DECL@0..16 + FUNCTION_CLAUSE@0..15 + ATOM@0..3 + ATOM@0..3 "foo" + EXPR_ARGS@3..6 + ANON_LPAREN@3..4 "(" + INTEGER@4..5 + INTEGER@4..5 "1" + ANON_RPAREN@5..6 ")" + WHITESPACE@6..7 " " + CLAUSE_BODY@7..15 + ANON_DASH_GT@7..9 "->" + WHITESPACE@9..10 " " + BINARY_OP_EXPR@10..15 + INTEGER@10..11 + INTEGER@10..11 "2" + WHITESPACE@11..12 " " + ANON_PLUS@12..13 "+" + WHITESPACE@13..14 " " + INTEGER@14..15 + INTEGER@14..15 "3" + ANON_DOT@15..16 ".""#]], + ) + } + + #[test] + fn whitespace() { + check_node( + "\nf() ->\n\t 1 + 2\t.\n\n", + expect![[r#" + SOURCE_FILE@0..19 + WHITESPACE@0..1 "\n" + FUN_DECL@1..17 + FUNCTION_CLAUSE@1..15 + ATOM@1..2 + ATOM@1..2 "f" + EXPR_ARGS@2..4 + ANON_LPAREN@2..3 "(" + ANON_RPAREN@3..4 ")" + WHITESPACE@4..5 " " + CLAUSE_BODY@5..15 + ANON_DASH_GT@5..7 "->" + WHITESPACE@7..10 "\n\t " + BINARY_OP_EXPR@10..15 + INTEGER@10..11 + INTEGER@10..11 "1" + WHITESPACE@11..12 " " + ANON_PLUS@12..13 "+" + WHITESPACE@13..14 " " + INTEGER@14..15 + INTEGER@14..15 "2" + WHITESPACE@15..16 "\t" + ANON_DOT@16..17 "." + WHITESPACE@17..19 "\n\n""#]], + ); + } + + #[test] + fn error_nodes1() { + check_node( + "f(1a) -> ok begin 1 end.", + expect![[r#" + SOURCE_FILE@0..24 + FUN_DECL@0..24 + FUNCTION_CLAUSE@0..11 + ATOM@0..1 + ATOM@0..1 "f" + EXPR_ARGS@1..5 + ANON_LPAREN@1..2 "(" + ERROR@2..3 + INTEGER@2..3 + INTEGER@2..3 "1" + ATOM@3..4 + ATOM@3..4 "a" + ANON_RPAREN@4..5 ")" + WHITESPACE@5..6 " " + CLAUSE_BODY@6..11 + ANON_DASH_GT@6..8 "->" + WHITESPACE@8..9 " " + ATOM@9..11 + ATOM@9..11 "ok" + WHITESPACE@11..12 " " + ERROR@12..23 + ATOM@12..17 + ATOM@12..17 "begin" + WHITESPACE@17..18 " " + INTEGER@18..19 + INTEGER@18..19 "1" + WHITESPACE@19..20 " " + ANON_END@20..23 "end" + ANON_DOT@23..24 ".""#]], + ); + } + + #[test] + fn error_nodes2() { + let input = "-define(,ok)."; + let parse = ast::SourceFile::parse_text(input); + + assert_eq!(parse.errors().len(), 1); + + check_node( + "-define(,ok).", + expect![[r#" + SOURCE_FILE@0..13 + PP_DEFINE@0..13 + ANON_DASH@0..1 "-" + ANON_DEFINE@1..7 "define" + ANON_LPAREN@7..8 "(" + MACRO_LHS@8..8 + ANON_COMMA@8..9 "," + ATOM@9..11 + ATOM@9..11 "ok" + ANON_RPAREN@11..12 ")" + ANON_DOT@12..13 ".""#]], + ) + } + + fn check_node(input: &str, expected: Expect) { + let parse = ast::SourceFile::parse_text(input); + + let actual_tree = format!("{:#?}", parse.syntax_node()); + // We cut off the last byte because formatting the SyntaxNode adds on a newline at the end. + expected.assert_eq(&actual_tree[0..actual_tree.len() - 1]); + + let expected_range = TextRange::new(TextSize::from(0), TextSize::of(input)); + assert_eq!(parse.syntax_node().text_range(), expected_range); + } + + /// This test does not assert anything and instead just shows off the crate's + /// API. + #[test] + fn api_walkthrough() { + // use ast::{ModuleItemOwner, NameOwner}; + + let source_code = "-module(foo).\nfoo(Bar) -> 1 + 1."; + + // `SourceFile` is the main entry point. + // + // The `parse` method returns a `Parse` -- a pair of syntax tree and a list + // of errors. That is, syntax tree is constructed even in presence of errors. + let parse = ast::SourceFile::parse_text(source_code); + assert!(parse.errors().is_empty()); + + // The `tree` method returns an owned syntax node of type `SourceFile`. + // Owned nodes are cheap: inside, they are `Rc` handles to the underling data. + let file: ast::SourceFile = parse.tree(); + + // `SourceFile` is the root of the syntax tree. We can iterate file's items. + // Let's fetch the three top level forms. + let mut module_attr = None; + let mut func = None; + for item in file.forms() { + match item { + ast::Form::ModuleAttribute(f) => module_attr = Some(f), + ast::Form::FunDecl(f) => func = Some(f), + x => panic!("{:?}", x), + // _ => unreachable!(), + } + } + let module: ast::ModuleAttribute = module_attr.unwrap(); + expect![[r#" + ModuleAttribute { + syntax: MODULE_ATTRIBUTE@0..13 + ANON_DASH@0..1 "-" + ANON_MODULE@1..7 "module" + ANON_LPAREN@7..8 "(" + ATOM@8..11 + ATOM@8..11 "foo" + ANON_RPAREN@11..12 ")" + ANON_DOT@12..13 "." + , + }"#]] + .assert_eq(format!("{:#?}", module).as_str()); + + match module.name().unwrap() { + ast::Name::Atom(name) => assert_eq!(name.raw_text(), "foo"), + ast::Name::MacroCallExpr(_) | ast::Name::Var(_) => { + panic!("unexpected name: {:?}", module.name()) + } + } + + let fun: ast::FunDecl = func.unwrap(); + expect![[r#" + FunDecl { + syntax: FUN_DECL@14..32 + FUNCTION_CLAUSE@14..31 + ATOM@14..17 + ATOM@14..17 "foo" + EXPR_ARGS@17..22 + ANON_LPAREN@17..18 "(" + VAR@18..21 + VAR@18..21 "Bar" + ANON_RPAREN@21..22 ")" + WHITESPACE@22..23 " " + CLAUSE_BODY@23..31 + ANON_DASH_GT@23..25 "->" + WHITESPACE@25..26 " " + BINARY_OP_EXPR@26..31 + INTEGER@26..27 + INTEGER@26..27 "1" + WHITESPACE@27..28 " " + ANON_PLUS@28..29 "+" + WHITESPACE@29..30 " " + INTEGER@30..31 + INTEGER@30..31 "1" + ANON_DOT@31..32 "." + , + }"#]] + .assert_eq(format!("{:#?}", fun).as_str()); + let fun_clauses = fun.clauses(); + assert_eq!(fun_clauses.clone().count(), 1); + + let clauses_cast: Vec = file + .syntax() + .descendants() + .filter_map(ast::FunctionClause::cast) + .map(|it| it) + .collect(); + expect![[r#" + FunctionClause { + syntax: FUNCTION_CLAUSE@14..31 + ATOM@14..17 + ATOM@14..17 "foo" + EXPR_ARGS@17..22 + ANON_LPAREN@17..18 "(" + VAR@18..21 + VAR@18..21 "Bar" + ANON_RPAREN@21..22 ")" + WHITESPACE@22..23 " " + CLAUSE_BODY@23..31 + ANON_DASH_GT@23..25 "->" + WHITESPACE@25..26 " " + BINARY_OP_EXPR@26..31 + INTEGER@26..27 + INTEGER@26..27 "1" + WHITESPACE@27..28 " " + ANON_PLUS@28..29 "+" + WHITESPACE@29..30 " " + INTEGER@30..31 + INTEGER@30..31 "1" + , + }"#]] + .assert_eq(format!("{:#?}", clauses_cast[0]).as_str()); + let function_clause = clauses_cast[0].clone(); + + let mut clause = None; + for item in fun_clauses { + match item { + ast::FunctionOrMacroClause::FunctionClause(f) => clause = Some(f), + x => panic!("{:?}", x), + // _ => unreachable!(), + } + } + expect![[r#" + FunctionClause { + syntax: FUNCTION_CLAUSE@14..31 + ATOM@14..17 + ATOM@14..17 "foo" + EXPR_ARGS@17..22 + ANON_LPAREN@17..18 "(" + VAR@18..21 + VAR@18..21 "Bar" + ANON_RPAREN@21..22 ")" + WHITESPACE@22..23 " " + CLAUSE_BODY@23..31 + ANON_DASH_GT@23..25 "->" + WHITESPACE@25..26 " " + BINARY_OP_EXPR@26..31 + INTEGER@26..27 + INTEGER@26..27 "1" + WHITESPACE@27..28 " " + ANON_PLUS@28..29 "+" + WHITESPACE@29..30 " " + INTEGER@30..31 + INTEGER@30..31 "1" + , + }"#]] + .assert_eq(format!("{:#?}", clause.unwrap()).as_str()); + + // Each AST node has a bunch of getters for children. All getters return + // `Option`s though, to account for incomplete code. Some getters are common + // for several kinds of node. In this case, a trait like `ast::NameOwner` + // usually exists. By convention, all ast types should be used with `ast::` + // qualifier. + let name = function_clause.name(); + let name = match name.unwrap() { + ast::Name::Atom(a) => a, + ast::Name::Var(_) | ast::Name::MacroCallExpr(_) => todo!(), + }; + assert_eq!(name.raw_text(), "foo"); + + // Let's get the `1 + 1` expression! + // let body: ast::BlockExpr = func.body().unwrap(); + // let expr: ast::Expr = body.tail_expr().unwrap(); + + let body = function_clause.body().unwrap(); + let mut oexpr = None; + for e in body.exprs() { + oexpr = Some(e); + } + let expr = oexpr.unwrap(); + + // Enums are used to group related ast nodes together, and can be used for + // matching. However, because there are no public fields, it's possible to + // match only the top level enum: that is the price we pay for increased API + // flexibility + let bin_expr: &ast::BinaryOpExpr = match &expr { + ast::Expr::BinaryOpExpr(e) => &e, + _ => unreachable!(), + }; + expect![[r#" + BinaryOpExpr { + syntax: BINARY_OP_EXPR@26..31 + INTEGER@26..27 + INTEGER@26..27 "1" + WHITESPACE@27..28 " " + ANON_PLUS@28..29 "+" + WHITESPACE@29..30 " " + INTEGER@30..31 + INTEGER@30..31 "1" + , + }"#]] + .assert_eq(format!("{:#?}", bin_expr).as_str()); + + // Besides the "typed" AST API, there's an untyped CST one as well. + // To switch from AST to CST, call `.syntax()` method: + let expr_syntax: &SyntaxNode = expr.syntax(); + + // Note how `expr` and `bin_expr` are in fact the same node underneath: + assert!(expr_syntax == bin_expr.syntax()); + + // To go from CST to AST, `AstNode::cast` function is used: + let _expr: ast::Expr = match ast::Expr::cast(expr_syntax.clone()) { + Some(e) => e, + None => unreachable!(), + }; + + // The two properties each syntax node has is a `SyntaxKind`: + assert_eq!(expr_syntax.kind(), SyntaxKind::BINARY_OP_EXPR); + + // And text range: + assert_eq!( + expr_syntax.text_range(), + TextRange::new(26.into(), 31.into()) + ); + + // You can get node's text as a `SyntaxText` object, which will traverse the + // tree collecting token's text: + let text: SyntaxText = expr_syntax.text(); + assert_eq!(text.to_string(), "1 + 1"); + + // There's a bunch of traversal methods on `SyntaxNode`: + assert_eq!(expr_syntax.parent().as_ref(), Some(body.syntax())); + assert_eq!( + body.syntax().first_child_or_token().map(|it| it.kind()), + Some(SyntaxKind::ANON_DASH_GT) + ); + // assert_eq!( + // expr_syntax.next_sibling_or_token().map(|it| it.kind()), + // Some(SyntaxKind::WHITESPACE) + // ); + + // As well as some iterator helpers: + let f = expr_syntax.ancestors().find_map(ast::FunDecl::cast); + assert_eq!(f.unwrap(), fun); + assert!( + expr_syntax + .siblings_with_tokens(Direction::Next) + .any(|it| it.kind() == SyntaxKind::BINARY_OP_EXPR) + ); + + assert_eq!( + expr_syntax.descendants_with_tokens().count(), + 8, // 5 tokens `1`, ` `, `+`, ` `, `1` + // 2 literal expressions: `1`, `1` + // 1 the node itself: `1 + 1` + ); + + // There's also a `preorder` method with a more fine-grained iteration control: + let mut buf = String::new(); + let mut indent = 0; + for event in expr_syntax.preorder_with_tokens() { + match event { + WalkEvent::Enter(node) => { + let text = match &node { + NodeOrToken::Node(it) => it.text().to_string(), + NodeOrToken::Token(it) => it.text().to_string(), + }; + format_to!( + buf, + "{:indent$}{:?} {:?}\n", + " ", + text, + node.kind(), + indent = indent + ); + indent += 2; + } + WalkEvent::Leave(_) => indent -= 2, + } + } + assert_eq!(indent, 0); + expect![[r#" + "1 + 1" BINARY_OP_EXPR + "1" INTEGER + "1" INTEGER + " " WHITESPACE + "+" ANON_PLUS + " " WHITESPACE + "1" INTEGER + "1" INTEGER"#]] + .assert_eq(buf.trim()); + + // To recursively process the tree, there are three approaches: + // 1. explicitly call getter methods on AST nodes. + // 2. use descendants and `AstNode::cast`. + // 3. use descendants and `match_ast!`. + // + // Here's how the first one looks like: + let exprs_cast: Vec = file + .syntax() + .descendants() + .filter_map(ast::Expr::cast) + .map(|expr| expr.syntax().text().to_string()) + .collect(); + + // An alternative is to use a macro. + let mut exprs_visit = Vec::new(); + for node in file.syntax().descendants() { + match_ast! { + match node { + ast::Expr(it) => { + let res = it.syntax().text().to_string(); + exprs_visit.push(res); + }, + _ => (), + } + } + } + assert_eq!(exprs_cast, exprs_visit); + } + + #[test] + fn char_escapes() { + let source_code = r#"foo(Bar) -> [$\$, $\r, $\s, $\n, $\t]."#; + + let parse = ast::SourceFile::parse_text(source_code); + assert_eq!(format!("{:?}", parse.errors()), "[]"); + assert!(parse.errors().is_empty()); + } + + #[test] + fn validate_missing_cleanup() { + // Test reporting of missing field value. + let source_code = r#"f() -> #name{field = }."#; + let parse = ast::SourceFile::parse_text(source_code); + + expect![[r#"[SyntaxError("Error: ignoring", 19..20)]"#]] + .assert_eq(format!("{:?}", parse.errors()).as_str()); + } + + #[test] + fn rowan() { + let source_code = r#" +-if (true). +-define(CASE_START_PEER_NODE, + case true of + Node -> + Node; +). +-else. +-define(CASE_START_PEER_NODE, + case true of + Node -> + Node;). +-endif. +"#; + + let parse = ast::SourceFile::parse_text(source_code); + assert_eq!( + format!("{:?}", parse.errors()), + "[SyntaxError(\"Error: ignoring\", 1..195)]" + ); + expect![[r#"SourceFile { syntax: SOURCE_FILE@0..195 }"#]] + .assert_eq(format!("{:?}", parse.tree()).as_str()); + } +} diff --git a/crates/syntax/src/ptr.rs b/crates/syntax/src/ptr.rs new file mode 100644 index 0000000000..cf02e109c1 --- /dev/null +++ b/crates/syntax/src/ptr.rs @@ -0,0 +1,167 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! In ELP/rust-analyzer, syntax trees are transient objects. +//! +//! That means that we create trees when we need them, and tear them down to +//! save memory. In this architecture, hanging on to a particular syntax node +//! for a long time is ill-advisable, as that keeps the whole tree resident. +//! +//! Instead, we provide a [`SyntaxNodePtr`] type, which stores information about +//! *location* of a particular syntax node in a tree. Its a small type which can +//! be cheaply stored, and which can be resolved to a real [`SyntaxNode`] when +//! necessary. + +use std::hash::Hash; +use std::hash::Hasher; +use std::iter::successors; +use std::marker::PhantomData; + +use crate::AstNode; +use crate::SyntaxKind; +use crate::SyntaxNode; +use crate::TextRange; + +/// A pointer to a syntax node inside a file. It can be used to remember a +/// specific node across reparses of the same file. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct SyntaxNodePtr { + // Don't expose this field further. At some point, we might want to replace + // range with node id. + pub(crate) range: TextRange, + kind: SyntaxKind, +} + +impl SyntaxNodePtr { + pub fn new(node: &SyntaxNode) -> SyntaxNodePtr { + SyntaxNodePtr { + range: node.text_range(), + kind: node.kind(), + } + } + + /// "Dereference" the pointer to get the node it points to. + /// + /// Panics if node is not found, so make sure that `root` syntax tree is + /// equivalent (is build from the same text) to the tree which was + /// originally used to get this [`SyntaxNodePtr`]. + /// + /// The complexity is linear in the depth of the tree and logarithmic in + /// tree width. As most trees are shallow, thinking about this as + /// `O(log(N))` in the size of the tree is not too wrong! + pub fn to_node(&self, root: &SyntaxNode) -> SyntaxNode { + assert!(root.parent().is_none()); + successors(Some(root.clone()), |node| { + node.child_or_token_at_range(self.range) + .and_then(|it| it.into_node()) + }) + .find(|it| it.text_range() == self.range && it.kind() == self.kind) + .unwrap_or_else(|| panic!("can't resolve local ptr to SyntaxNode: {:?}", self)) + } + + pub fn cast(self) -> Option> { + if !N::can_cast(self.kind) { + return None; + } + Some(AstPtr { + raw: self, + _ty: PhantomData, + }) + } + + pub fn range(&self) -> TextRange { + self.range + } +} + +/// Like `SyntaxNodePtr`, but remembers the type of node +/// Use `to_node(..)` to retrieve the contents. +#[derive(Debug)] +pub struct AstPtr { + raw: SyntaxNodePtr, + _ty: PhantomData N>, +} + +impl Clone for AstPtr { + fn clone(&self) -> AstPtr { + AstPtr { + raw: self.raw, + _ty: PhantomData, + } + } +} + +impl Copy for AstPtr {} + +impl Eq for AstPtr {} + +impl PartialEq for AstPtr { + fn eq(&self, other: &AstPtr) -> bool { + self.raw == other.raw + } +} + +impl Hash for AstPtr { + fn hash(&self, state: &mut H) { + self.raw.hash(state) + } +} + +impl AstPtr { + pub fn new(node: &N) -> AstPtr { + AstPtr { + raw: SyntaxNodePtr::new(node.syntax()), + _ty: PhantomData, + } + } + + pub fn to_node(&self, root: &SyntaxNode) -> N { + let syntax_node = self.raw.to_node(root); + N::cast(syntax_node).unwrap() + } + + pub fn syntax_node_ptr(&self) -> SyntaxNodePtr { + self.raw + } + + pub fn cast(self) -> Option> { + if !U::can_cast(self.raw.kind) { + return None; + } + Some(AstPtr { + raw: self.raw, + _ty: PhantomData, + }) + } +} + +impl From> for SyntaxNodePtr { + fn from(ptr: AstPtr) -> SyntaxNodePtr { + ptr.raw + } +} + +#[test] +fn test_local_syntax_ptr() { + use crate::ast; + use crate::AstNode; + use crate::SourceFile; + + let file = SourceFile::parse_text("foo() -> Expr#Name.Field.") + .ok() + .unwrap(); + let field = file + .syntax() + .descendants() + .find_map(ast::RecordFieldExpr::cast) + .unwrap(); + let ptr = SyntaxNodePtr::new(field.syntax()); + let field_syntax = ptr.to_node(file.syntax()); + assert_eq!(field.syntax(), &field_syntax); +} diff --git a/crates/syntax/src/syntax_error.rs b/crates/syntax/src/syntax_error.rs new file mode 100644 index 0000000000..be435de88a --- /dev/null +++ b/crates/syntax/src/syntax_error.rs @@ -0,0 +1,33 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::fmt; + +use crate::TextRange; + +/// Represents the result of unsuccessful tokenization, parsing +/// or tree validation. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct SyntaxError(String, TextRange); + +impl SyntaxError { + pub fn new(message: impl Into, range: TextRange) -> Self { + Self(message.into(), range) + } + + pub fn range(&self) -> TextRange { + self.1 + } +} + +impl fmt::Display for SyntaxError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} diff --git a/crates/syntax/src/syntax_kind.rs b/crates/syntax/src/syntax_kind.rs new file mode 100644 index 0000000000..2c7c45c299 --- /dev/null +++ b/crates/syntax/src/syntax_kind.rs @@ -0,0 +1,22 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! Defines [`SyntaxKind`] -- a fieldless enum of all possible syntactic +//! constructs of the Erlang language. + +mod generated; + +pub use self::generated::SyntaxKind; + +impl SyntaxKind { + #[inline] + pub fn is_trivia(self) -> bool { + matches!(self, SyntaxKind::WHITESPACE | SyntaxKind::COMMENT) + } +} diff --git a/crates/syntax/src/syntax_kind/generated.rs b/crates/syntax/src/syntax_kind/generated.rs new file mode 100644 index 0000000000..ea05c9d999 --- /dev/null +++ b/crates/syntax/src/syntax_kind/generated.rs @@ -0,0 +1,328 @@ +//! @generated file, do not edit by hand, see `xtask/src/codegen.rs` + +#![allow(bad_style, missing_docs, unreachable_pub)] +use num_derive::{FromPrimitive, ToPrimitive}; +#[doc = r" The kind of syntax node, e.g. `ATOM`, `IF_KW`, or `DOT`."] +#[derive( + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Debug, + FromPrimitive, + ToPrimitive +)] +#[repr(u16)] +pub enum SyntaxKind { + ANON_AFTER = 93u16, + ANON_AND = 103u16, + ANON_ANDALSO = 76u16, + ANN_TYPE = 174u16, + ANN_VAR = 175u16, + ANONYMOUS_FUN = 241u16, + ARITY = 244u16, + ATOM = 1u16, + ATTR_NAME = 170u16, + B_GENERATOR = 211u16, + ANON_BAND = 102u16, + ANON_BANG = 74u16, + ANON_BEGIN = 77u16, + ANON_BEHAVIOR = 31u16, + ANON_BEHAVIOUR = 29u16, + BEHAVIOUR_ATTRIBUTE = 141u16, + BIN_ELEMENT = 197u16, + BINARY = 196u16, + BINARY_COMPREHENSION = 206u16, + BINARY_OP_EXPR = 188u16, + BIT_SIZE_EXPR = 198u16, + BIT_TYPE_LIST = 199u16, + BIT_TYPE_UNIT = 204u16, + BLOCK_EXPR = 194u16, + ANON_BNOT = 98u16, + ANON_BOR = 104u16, + ANON_BSL = 106u16, + ANON_BSR = 107u16, + ANON_BXOR = 105u16, + CALL = 230u16, + CALLBACK = 165u16, + ANON_CALLBACK = 61u16, + ANON_CASE = 90u16, + CASE_EXPR = 233u16, + ANON_CATCH = 71u16, + CATCH_CLAUSE = 250u16, + CATCH_EXPR = 185u16, + CHAR = 124u16, + CLAUSE_BODY = 183u16, + ANON_COLON = 64u16, + ANON_COLON_COLON = 56u16, + ANON_COLON_EQ = 89u16, + ANON_COMMA = 26u16, + COMMENT = 125u16, + ANON_COMPILE = 43u16, + COMPILE_OPTIONS_ATTRIBUTE = 147u16, + CONCATABLES = 275u16, + COND_MATCH_EXPR = 187u16, + CR_CLAUSE = 236u16, + ANON_DASH = 2u16, + ANON_DASH_DASH = 111u16, + ANON_DASH_GT = 65u16, + ANON_DEFINE = 24u16, + ANON_DEPRECATED = 47u16, + DEPRECATED_ATTRIBUTE = 149u16, + DEPRECATED_FA = 153u16, + DEPRECATED_FAS = 152u16, + DEPRECATED_MODULE = 151u16, + DEPRECATED_WILDCARD = 51u16, + DEPRECATION_DESC = 154u16, + ANON_DIV = 100u16, + ANON_DOT = 7u16, + ANON_DOT_DOT = 69u16, + DOTDOTDOT = 70u16, + ANON_ELIF = 22u16, + ANON_ELSE = 16u16, + ANON_END = 78u16, + ANON_ENDIF = 18u16, + ANON_EQ = 72u16, + ANON_EQ_COLON_EQ = 118u16, + ANON_EQ_EQ = 112u16, + ANON_EQ_GT = 88u16, + ANON_EQ_LT = 114u16, + ANON_EQ_SLASH_EQ = 119u16, + ANON_EXPORT = 33u16, + EXPORT_ATTRIBUTE = 142u16, + ANON_EXPORT_TYPE = 41u16, + EXPORT_TYPE_ATTRIBUTE = 146u16, + EXPR_ARGS = 271u16, + EXTERNAL_FUN = 240u16, + FA = 145u16, + FIELD_EXPR = 228u16, + FIELD_TYPE = 229u16, + ANON_FILE = 45u16, + FILE_ATTRIBUTE = 148u16, + FLOAT = 122u16, + ANON_FUN = 68u16, + FUN_CLAUSE = 246u16, + FUN_DECL = 171u16, + FUN_TYPE = 177u16, + FUN_TYPE_SIG = 178u16, + FUNCTION_CLAUSE = 181u16, + GENERATOR = 210u16, + ANON_GT = 117u16, + ANON_GT_EQ = 116u16, + ANON_GT_GT = 80u16, + GUARD = 273u16, + GUARD_CLAUSE = 274u16, + ANON_IF = 20u16, + IF_CLAUSE = 232u16, + IF_EXPR = 231u16, + ANON_IFDEF = 12u16, + ANON_IFNDEF = 14u16, + ANON_IMPORT = 37u16, + IMPORT_ATTRIBUTE = 143u16, + ANON_INCLUDE = 3u16, + ANON_INCLUDE_LIB = 8u16, + INTEGER = 121u16, + INTERNAL_FUN = 239u16, + ANON_LBRACE = 49u16, + ANON_LBRACK = 35u16, + LC_EXPRS = 208u16, + LIST = 195u16, + LIST_COMPREHENSION = 205u16, + ANON_LPAREN = 5u16, + ANON_LT = 115u16, + ANON_LT_DASH = 86u16, + ANON_LT_EQ = 87u16, + ANON_LT_LT = 79u16, + MACRO_CALL_ARGS = 267u16, + MACRO_CALL_EXPR = 266u16, + MACRO_EXPR = 270u16, + MACRO_LHS = 264u16, + MACRO_STRING = 269u16, + MAP_COMPREHENSION = 207u16, + MAP_EXPR = 215u16, + MAP_EXPR_UPDATE = 214u16, + MAP_FIELD = 217u16, + MAP_GENERATOR = 212u16, + MATCH_EXPR = 186u16, + ANON_MAYBE = 95u16, + MAYBE_EXPR = 256u16, + MODULE = 168u16, + ANON_MODULE = 27u16, + MODULE_ATTRIBUTE = 140u16, + MULTI_STRING = 156u16, + ANON_NOT = 99u16, + ANON_OF = 91u16, + OPAQUE = 160u16, + ANON_OPAQUE = 54u16, + ANON_OPTIONAL_CALLBACKS = 39u16, + OPTIONAL_CALLBACKS_ATTRIBUTE = 144u16, + ANON_OR = 108u16, + ANON_ORELSE = 75u16, + PAREN_EXPR = 193u16, + PIPE = 176u16, + ANON_PIPE = 67u16, + ANON_PIPE_PIPE = 85u16, + ANON_PLUS = 97u16, + ANON_PLUS_PLUS = 110u16, + ANON_POUND = 84u16, + PP_DEFINE = 138u16, + PP_ELIF = 137u16, + PP_ELSE = 134u16, + PP_ENDIF = 135u16, + PP_IF = 136u16, + PP_IFDEF = 132u16, + PP_IFNDEF = 133u16, + PP_INCLUDE = 129u16, + PP_INCLUDE_LIB = 130u16, + PP_UNDEF = 131u16, + ANON_QMARK = 96u16, + ANON_QMARK_EQ = 73u16, + RANGE_TYPE = 179u16, + ANON_RBRACK = 36u16, + ANON_RECEIVE = 92u16, + RECEIVE_AFTER = 238u16, + RECEIVE_EXPR = 237u16, + ANON_RECORD = 57u16, + RECORD_DECL = 163u16, + RECORD_EXPR = 222u16, + RECORD_FIELD = 227u16, + RECORD_FIELD_EXPR = 220u16, + RECORD_FIELD_NAME = 224u16, + RECORD_INDEX_EXPR = 219u16, + RECORD_NAME = 223u16, + RECORD_UPDATE_EXPR = 221u16, + ANON_REM = 101u16, + REMOTE = 191u16, + REMOTE_MODULE = 192u16, + REPLACEMENT_CR_CLAUSES = 260u16, + REPLACEMENT_FUNCTION_CLAUSES = 259u16, + REPLACEMENT_GUARD_AND = 262u16, + REPLACEMENT_GUARD_OR = 261u16, + REPLACEMENT_PARENS = 263u16, + ANON_RPAREN = 6u16, + ANON_RRACE = 50u16, + ANON_SEMI = 63u16, + ANON_SLASH = 81u16, + ANON_SLASH_EQ = 113u16, + SOURCE_FILE = 126u16, + SPEC = 164u16, + ANON_SPEC = 59u16, + ANON_STAR = 82u16, + STRING = 123u16, + ANON_TRY = 94u16, + TRY_AFTER = 249u16, + TRY_CLASS = 251u16, + TRY_EXPR = 247u16, + TRY_STACK = 252u16, + TUPLE = 213u16, + ANON_TYPE = 52u16, + TYPE_ALIAS = 159u16, + TYPE_GUARDS = 173u16, + TYPE_NAME = 162u16, + TYPE_SIG = 172u16, + UNARY_OP_EXPR = 189u16, + ANON_UNDEF = 10u16, + ANON_UNIT = 83u16, + VAR = 120u16, + VAR_ARGS = 272u16, + ANON_WHEN = 66u16, + WILD_ATTRIBUTE = 169u16, + ANON_XOR = 109u16, + WHITESPACE = 277u16, + ERROR = u16::MAX, +} +use self::SyntaxKind::*; +impl SyntaxKind { + #[allow(clippy::match_like_matches_macro)] + pub fn is_keyword(&self) -> bool { + match self { + ANON_AFTER + | ANON_AND + | ANON_ANDALSO + | ANON_BAND + | ANON_BEGIN + | ANON_BEHAVIOR + | ANON_BEHAVIOUR + | ANON_BNOT + | ANON_BOR + | ANON_BSL + | ANON_BSR + | ANON_BXOR + | ANON_CALLBACK + | ANON_CASE + | ANON_CATCH + | ANON_COMPILE + | ANON_DEFINE + | ANON_DEPRECATED + | ANON_DIV + | ANON_ELIF + | ANON_ELSE + | ANON_END + | ANON_ENDIF + | ANON_EXPORT + | ANON_EXPORT_TYPE + | ANON_FILE + | ANON_FUN + | ANON_IF + | ANON_IFDEF + | ANON_IFNDEF + | ANON_IMPORT + | ANON_INCLUDE + | ANON_INCLUDE_LIB + | ANON_MAYBE + | ANON_MODULE + | ANON_NOT + | ANON_OF + | ANON_OPAQUE + | ANON_OPTIONAL_CALLBACKS + | ANON_OR + | ANON_ORELSE + | ANON_RECEIVE + | ANON_RECORD + | ANON_REM + | ANON_SPEC + | ANON_TRY + | ANON_TYPE + | ANON_UNDEF + | ANON_UNIT + | ANON_WHEN + | ANON_XOR => true, + _ => false, + } + } + #[allow(clippy::match_like_matches_macro)] + pub fn is_punct(&self) -> bool { + match self { + ANON_BANG | ANON_COLON | ANON_COLON_COLON | ANON_COLON_EQ | ANON_COMMA | ANON_DASH + | ANON_DASH_DASH | ANON_DASH_GT | ANON_DOT | ANON_DOT_DOT | ANON_EQ + | ANON_EQ_COLON_EQ | ANON_EQ_EQ | ANON_EQ_GT | ANON_EQ_LT | ANON_EQ_SLASH_EQ + | ANON_GT | ANON_GT_EQ | ANON_GT_GT | ANON_LBRACE | ANON_LBRACK | ANON_LPAREN + | ANON_LT | ANON_LT_DASH | ANON_LT_EQ | ANON_LT_LT | ANON_PIPE | ANON_PIPE_PIPE + | ANON_PLUS | ANON_PLUS_PLUS | ANON_POUND | ANON_QMARK | ANON_QMARK_EQ + | ANON_RBRACK | ANON_RPAREN | ANON_RRACE | ANON_SEMI | ANON_SLASH | ANON_SLASH_EQ + | ANON_STAR => true, + _ => false, + } + } + #[allow(clippy::match_like_matches_macro)] + pub fn is_literal(&self) -> bool { + match self { + ATOM | CHAR | COMMENT | DEPRECATED_WILDCARD | DOTDOTDOT | FLOAT | INTEGER | STRING + | VAR => true, + _ => false, + } + } + pub fn is_token(&self) -> bool { + self.is_keyword() || self.is_punct() || self.is_literal() + } +} +#[doc = r" Tell emacs to automatically reload this file if it changes"] +#[doc = r" Local Variables:"] +#[doc = r" auto-revert-mode: 1"] +#[doc = r" End:"] +fn _dummy() -> bool { + false +} diff --git a/crates/syntax/src/syntax_node.rs b/crates/syntax/src/syntax_node.rs new file mode 100644 index 0000000000..4ce603abcc --- /dev/null +++ b/crates/syntax/src/syntax_node.rs @@ -0,0 +1,36 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use num_traits::FromPrimitive; +use num_traits::ToPrimitive; +use rowan::Language; + +use crate::SyntaxKind; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ELPLanguage {} + +impl Language for ELPLanguage { + type Kind = SyntaxKind; + + fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind { + Self::Kind::from_u16(raw.0).unwrap() + } + + fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind { + rowan::SyntaxKind(kind.to_u16().unwrap()) + } +} + +pub type SyntaxNode = rowan::SyntaxNode; +pub type SyntaxToken = rowan::SyntaxToken; +pub type SyntaxElement = rowan::SyntaxElement; +pub type SyntaxNodeChildren = rowan::SyntaxNodeChildren; +pub type SyntaxElementChildren = rowan::SyntaxElementChildren; +pub type NodeOrToken = rowan::NodeOrToken; diff --git a/crates/syntax/src/ted.rs b/crates/syntax/src/ted.rs new file mode 100644 index 0000000000..6496ec2ff4 --- /dev/null +++ b/crates/syntax/src/ted.rs @@ -0,0 +1,213 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! Primitive tree editor, ed for trees. +//! +//! The `_raw`-suffixed functions insert elements as is, unsuffixed versions fix +//! up elements around the edges. +use std::mem; +use std::ops::RangeInclusive; + +use once_cell::sync::Lazy; + +use crate::Parse; +use crate::SourceFile; +use crate::SyntaxElement; +use crate::SyntaxKind; +use crate::SyntaxNode; +use crate::SyntaxToken; + +/// Utility trait to allow calling `ted` functions with references or owned +/// nodes. Do not use outside of this module. +pub trait Element { + fn syntax_element(self) -> SyntaxElement; +} + +impl Element for &'_ E { + fn syntax_element(self) -> SyntaxElement { + self.clone().syntax_element() + } +} +impl Element for SyntaxElement { + fn syntax_element(self) -> SyntaxElement { + self + } +} +impl Element for SyntaxNode { + fn syntax_element(self) -> SyntaxElement { + self.into() + } +} +impl Element for SyntaxToken { + fn syntax_element(self) -> SyntaxElement { + self.into() + } +} + +#[derive(Debug)] +pub struct Position { + repr: PositionRepr, +} + +#[derive(Debug)] +enum PositionRepr { + FirstChild(SyntaxNode), + After(SyntaxElement), +} + +impl Position { + pub fn after(elem: impl Element) -> Position { + let repr = PositionRepr::After(elem.syntax_element()); + Position { repr } + } + pub fn before(elem: impl Element) -> Position { + let elem = elem.syntax_element(); + let repr = match elem.prev_sibling_or_token() { + Some(it) => PositionRepr::After(it), + None => PositionRepr::FirstChild(elem.parent().unwrap()), + }; + Position { repr } + } + pub fn first_child_of(node: &(impl Into + Clone)) -> Position { + let repr = PositionRepr::FirstChild(node.clone().into()); + Position { repr } + } + pub fn last_child_of(node: &(impl Into + Clone)) -> Position { + let node = node.clone().into(); + let repr = match node.last_child_or_token() { + Some(it) => PositionRepr::After(it), + None => PositionRepr::FirstChild(node), + }; + Position { repr } + } +} + +pub fn insert(position: Position, elem: impl Element) { + insert_all(position, vec![elem.syntax_element()]); +} +pub fn insert_raw(position: Position, elem: impl Element) { + insert_all_raw(position, vec![elem.syntax_element()]); +} +pub fn insert_all(position: Position, mut elements: Vec) { + if let Some(first) = elements.first() { + if let Some(ws) = ws_before(&position, first) { + elements.insert(0, ws.into()); + } + } + if let Some(last) = elements.last() { + if let Some(ws) = ws_after(&position, last) { + elements.push(ws.into()); + } + } + insert_all_raw(position, elements); +} +pub fn insert_all_raw(position: Position, elements: Vec) { + let (parent, index) = match position.repr { + PositionRepr::FirstChild(parent) => (parent, 0), + PositionRepr::After(child) => (child.parent().unwrap(), child.index() + 1), + }; + parent.splice_children(index..index, elements); +} + +pub fn remove(elem: impl Element) { + elem.syntax_element().detach(); +} +pub fn remove_all(range: RangeInclusive) { + replace_all(range, Vec::new()); +} +pub fn remove_all_iter(range: impl IntoIterator) { + let mut it = range.into_iter(); + if let Some(mut first) = it.next() { + match it.last() { + Some(mut last) => { + if first.index() > last.index() { + mem::swap(&mut first, &mut last); + } + remove_all(first..=last); + } + None => remove(first), + } + } +} + +pub fn replace(old: impl Element, new: impl Element) { + replace_with_many(old, vec![new.syntax_element()]); +} +pub fn replace_with_many(old: impl Element, new: Vec) { + let old = old.syntax_element(); + replace_all(old.clone()..=old, new); +} +pub fn replace_all(range: RangeInclusive, new: Vec) { + let start = range.start().index(); + let end = range.end().index(); + let parent = range.start().parent().unwrap(); + parent.splice_children(start..end + 1, new); +} + +pub fn append_child(node: &(impl Into + Clone), child: impl Element) { + let position = Position::last_child_of(node); + insert(position, child); +} +pub fn append_child_raw(node: &(impl Into + Clone), child: impl Element) { + let position = Position::last_child_of(node); + insert_raw(position, child); +} + +fn ws_before(position: &Position, new: &SyntaxElement) -> Option { + let prev = match &position.repr { + PositionRepr::FirstChild(_) => return None, + PositionRepr::After(it) => it, + }; + + ws_between(prev, new) +} +fn ws_after(position: &Position, new: &SyntaxElement) -> Option { + let next = match &position.repr { + PositionRepr::FirstChild(parent) => parent.first_child_or_token()?, + PositionRepr::After(sibling) => sibling.next_sibling_or_token()?, + }; + ws_between(new, &next) +} +fn ws_between(left: &SyntaxElement, right: &SyntaxElement) -> Option { + if left.kind() == SyntaxKind::WHITESPACE || right.kind() == SyntaxKind::WHITESPACE { + return None; + } + if right.kind() == SyntaxKind::ANON_SEMI || right.kind() == SyntaxKind::ANON_COMMA { + return None; + } + Some(single_space()) +} + +// --------------------------------------------------------------------- +// From rust-analyzer make.rs + +static SOURCE_FILE: Lazy> = Lazy::new(|| SourceFile::parse_text("foo() -> ok.")); + +pub fn single_space() -> SyntaxToken { + SOURCE_FILE + // .tree() + .syntax_node() + // .syntax() + .clone_for_update() + .descendants_with_tokens() + .filter_map(|it| it.into_token()) + .find(|it| it.kind() == SyntaxKind::WHITESPACE && it.text() == " ") + .unwrap() +} + +pub fn make_whitespace(text: &str) -> SyntaxToken { + assert!(text.trim().is_empty()); + let sf = SourceFile::parse_text(text).ok().unwrap(); + sf.syntax + .clone_for_update() + .first_child_or_token() + .unwrap() + .into_token() + .unwrap() +} diff --git a/crates/syntax/src/token_text.rs b/crates/syntax/src/token_text.rs new file mode 100644 index 0000000000..0ef905e0b4 --- /dev/null +++ b/crates/syntax/src/token_text.rs @@ -0,0 +1,106 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +//! Yet another version of owned string, backed by a syntax tree token. + +use std::cmp::Ordering; +use std::fmt; +use std::ops; + +use rowan::GreenToken; + +pub struct TokenText<'a>(pub(crate) Repr<'a>); + +pub(crate) enum Repr<'a> { + Borrowed(&'a str), + Owned(GreenToken), +} + +impl<'a> TokenText<'a> { + pub(crate) fn borrowed(text: &'a str) -> Self { + TokenText(Repr::Borrowed(text)) + } + + pub(crate) fn owned(green: GreenToken) -> Self { + TokenText(Repr::Owned(green)) + } + + pub fn as_str(&self) -> &str { + match &self.0 { + Repr::Borrowed(it) => it, + Repr::Owned(green) => green.text(), + } + } +} + +impl ops::Deref for TokenText<'_> { + type Target = str; + + fn deref(&self) -> &str { + self.as_str() + } +} +impl AsRef for TokenText<'_> { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl From> for String { + fn from(token_text: TokenText) -> Self { + token_text.as_str().into() + } +} + +impl PartialEq<&'_ str> for TokenText<'_> { + fn eq(&self, other: &&str) -> bool { + self.as_str() == *other + } +} +impl PartialEq> for &'_ str { + fn eq(&self, other: &TokenText) -> bool { + other == self + } +} +impl PartialEq for TokenText<'_> { + fn eq(&self, other: &String) -> bool { + self.as_str() == other.as_str() + } +} +impl PartialEq> for String { + fn eq(&self, other: &TokenText) -> bool { + other == self + } +} +impl PartialEq for TokenText<'_> { + fn eq(&self, other: &TokenText) -> bool { + self.as_str() == other.as_str() + } +} +impl Eq for TokenText<'_> {} +impl Ord for TokenText<'_> { + fn cmp(&self, other: &Self) -> Ordering { + self.as_str().cmp(other.as_str()) + } +} +impl PartialOrd for TokenText<'_> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl fmt::Display for TokenText<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self.as_str(), f) + } +} +impl fmt::Debug for TokenText<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(self.as_str(), f) + } +} diff --git a/crates/syntax/src/tree_sitter_elp.rs b/crates/syntax/src/tree_sitter_elp.rs new file mode 100644 index 0000000000..b5196417cc --- /dev/null +++ b/crates/syntax/src/tree_sitter_elp.rs @@ -0,0 +1,27 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +pub use tree_sitter::*; +use tree_sitter_erlang::language; + +pub struct Parser(tree_sitter::Parser); + +impl Parser { + pub fn new() -> Self { + let mut parser = tree_sitter::Parser::new(); + parser + .set_language(language()) + .expect("incompatible tree-sitter"); + Parser(parser) + } + + pub fn parse(&mut self, text: &str) -> Option { + self.0.parse(text, None) + } +} diff --git a/crates/syntax/src/unescape.rs b/crates/syntax/src/unescape.rs new file mode 100644 index 0000000000..18869e3d50 --- /dev/null +++ b/crates/syntax/src/unescape.rs @@ -0,0 +1,251 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under both the MIT license found in the + * LICENSE-MIT file in the root directory of this source tree and the Apache + * License, Version 2.0 found in the LICENSE-APACHE file in the root directory + * of this source tree. + */ + +use std::borrow::Cow; +use std::char; +/// unescape Erlang strings +/// Based on https://docs.rs/unescape/latest/unescape/fn.unescape.html +use std::collections::VecDeque; +use std::ops::BitAnd; + +macro_rules! try_option { + ($o:expr) => { + match $o { + Some(s) => s, + None => return None, + } + }; +} + +/// Takes in a string with backslash escapes written out with literal +/// backslash characters and converts it to a string with the proper +/// escaped characters, according to Erlang syntax. +/// Only unescape if the string is surrounded by ' or " chars +pub fn unescape_string(s_in: &str) -> Option> { + if !s_in.contains(['\'', '"', '\\', '$']) { + return Some(Cow::Borrowed(s_in)); + } + + let mut queue: VecDeque<_> = String::from(s_in).chars().collect(); + let mut s = String::new(); + + if let Some(&c) = queue.get(0) { + if c != '\'' && c != '\"' && c != '$' { + return Some(Cow::Borrowed(s_in)); + } else { + // Remove leading delimiter + queue.pop_front(); + } + } + + while let Some(c) = queue.pop_front() { + if (c == '\'' || c == '\"') && queue.is_empty() { + return Some(Cow::Owned(s)); + } + if c != '\\' { + s.push(c); + continue; + } + + // Based on https://www.erlang.org/doc/reference_manual/data_types.html#escape-sequences + // Sequence Description + // \b Backspace + // \d Delete + // \e Escape + // \f Form feed + // \n Newline + // \r Carriage return + // \s Space + // \t Tab + // \v Vertical tab + // \XYZ, \YZ, \Z Character with octal representation XYZ, YZ or Z + // \xXY Character with hexadecimal representation XY + // \x{X...} Character with hexadecimal representation; X... is one or more hexadecimal characters + // \^a...\^z + // \^A...\^Z Control A to control Z + // \' Single quote + // \" Double quote + // \\ Backslash + + match queue.pop_front() { + Some('b') => s.push('\u{0008}'), + Some('d') => s.push('\u{007F}'), + Some('e') => s.push('\u{001B}'), + Some('f') => s.push('\u{000C}'), + Some('n') => s.push('\n'), + Some('r') => s.push('\r'), + Some('s') => s.push(' '), + Some('t') => s.push('\t'), + Some('v') => s.push('\u{000B}'), + Some(c) if c.is_digit(8) => s.push(try_option!(unescape_octal(c, &mut queue))), + Some('x') => s.push(try_option!(unescape_hex(&mut queue))), + Some('^') => s.push(try_option!(unescape_control(&mut queue))), + Some('\'') => s.push('\''), + Some('\"') => s.push('\"'), + Some('\\') => s.push('\\'), + Some(c) => s.push(c), + None => {} + }; + } + + Some(Cow::Owned(s)) +} + +fn unescape_octal(c: char, queue: &mut VecDeque) -> Option { + let mut s = String::new(); + s.push(c); + if next_digit(8, &mut s, queue) { + next_digit(8, &mut s, queue); + } + if let Ok(u) = u32::from_str_radix(&s, 8) { + char::from_u32(u) + } else { + None + } +} + +fn next_digit(radix: u32, s: &mut String, queue: &mut VecDeque) -> bool { + if let Some(d) = queue.pop_front() { + if d.is_digit(radix) { + s.push(d); + true + } else { + queue.push_front(d); + false + } + } else { + false + } +} + +fn unescape_hex(queue: &mut VecDeque) -> Option { + let mut s = String::new(); + + if let Some(c) = queue.pop_front() { + if c == '{' { + return unescape_hex_curly(queue); + } else { + s.push(c); + next_digit(16, &mut s, queue); + } + } + + let u = try_option!(u32::from_str_radix(&s, 16).ok()); + char::from_u32(u) +} + +fn unescape_hex_curly(queue: &mut VecDeque) -> Option { + let mut s = String::new(); + + let mut i = 0; + while i < 4 && next_digit(16, &mut s, queue) { + i += 1; + } + // Skip trailing curly brace + let _ = queue.pop_front(); + + let u = try_option!(u32::from_str_radix(&s, 16).ok()); + char::from_u32(u) +} + +// In erlang, \n is returned unchanged, all others anded with 31 +fn unescape_control(queue: &mut VecDeque) -> Option { + if let Some(c) = queue.pop_front() { + if c == '\n' { + Some(c) + } else { + char::from_u32((c as u32).bitand(31)) + } + } else { + None + } +} + +// --------------------------------------------------------------------- +#[cfg(test)] +mod tests { + use expect_test::expect; + + use super::unescape_string; + + #[test] + fn unescape_string_plain() { + expect![[r#"foo"#]].assert_eq(&unescape_string(r#"foo"#).unwrap()); + } + #[test] + fn unescape_string_quotes_only() { + expect![[r#"foo"#]].assert_eq(&unescape_string(r#"'foo'"#).unwrap()); + } + #[test] + fn unescape_string_unicode_chars() { + expect![[r#"!@#$%% ä"#]].assert_eq(&unescape_string(r#"'!@#$%% ä'"#).unwrap()); + } + + #[test] + fn unescape_string_char() { + expect![[r#"a"#]].assert_eq(&unescape_string(r#"$a"#).unwrap()); + } + + #[test] + fn unescape_string_string() { + expect![[r#"abc"#]].assert_eq(&unescape_string(r#""abc""#).unwrap()); + } + + #[test] + fn unescape_string_esc_chars() { + assert_eq!( + "a\u{8}\u{7f}\u{1b}\u{c}\n \t\u{b}b", + &unescape_string(r#"'a\b\d\e\f\n\s\t\vb"#).unwrap() + ); + } + #[test] + fn unescape_string_esc_cr() { + assert_eq!("a\rb", &unescape_string(r#"'a\rb"#).unwrap()); + } + + #[test] + fn unescape_string_esc_octal() { + assert_eq!( + "a b\u{7}c\u{0}d", + &unescape_string(r#"'a\040b\07c\0d"#).unwrap() + ); + } + + #[test] + fn unescape_string_esc_hex_simple() { + assert_eq!( + "a\x00b8<\x3f", + &unescape_string(r#"'a\x00b\x38\x3c\x3F"#).unwrap() + ); + } + + #[test] + fn unescape_string_esc_hex_braces() { + assert_eq!( + "a\x00b8<\x3f", + &unescape_string(r#"'a\x{00}b\x{38}\x3c\x{003F}"#).unwrap() + ); + } + + #[test] + fn unescape_string_esc_control_chars() { + assert_eq!( + "a\x07b\x07c\x1dd", + &unescape_string(r#"'a\^Gb\^gc\^]d"#).unwrap() + ); + } + + #[test] + fn unescape_string_esc_self_escape() { + assert_eq!( + "aGb%cd'\"\\", + &unescape_string(r#"'a\Gb\%cd\'\"\\"#).unwrap() + ); + } +} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000000..6b21428c20 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,1181 @@ +# Erlang Language Platform Architecture + +TL;DR: The API to use to programme against ELP is +[Semantics](#semantics) and the high level IR is stored in an +[ItemTree](#itemtree). + +This document describes the architecture of ELP at the point where the +diff stack at D32881714 lands. + +See [below](#crates) for an overview of the crates in the system and +their invariants. + +See also the [rust analyzer +guide](https://github.com/rust-lang/rust-analyzer/blob/master/docs/dev/guide.md) +for some background, most of which is applicable to ELP. + +## Data Flow + +ELP is built on [salsa](https://salsa-rs.github.io/salsa/) which is a +Rust framework for writing incremental, on-demand programs. + +The salsa engine is provided with inputs, which can be updated from +time to time, and then queries can be made on it to generate new +artifacts. + +Every time a query is called, the salsa internal state is checked to +see if any of its (transitive) dependencies has changed, and if not +the cached value is returned. + +Queries are normal rust code, implementing a specific trait tagged +with salsa attributes. + +### Inputs + +The diagram below maps salsa queries in ELP to the artifacts they +produce. Each transition is labeled with the relevant [salsa +database](#salsa-databases) and the query on it. + +![ELP Salsa Inputs](./input_data_graph.png) + +The key input is `file_text`, which maps a `FileId` to its source text. + +In the LSP case the ELP LSP server keeps the set of files and their text +up to date as the user changes files in their IDE, or the files change +on the file system. + +In the CLI case these are either preloaded or loaded as needed into +salsa. + +### Salsa Databases + +There is only one pool of salsa data, but in salsa terminology a +database is a grouping of related functionality exposed as a rust +trait, something like a Java interface. And a given database can +include others, in an inheritance relationship. + +The overall structure of this relationship is shown below + +![ELP Salsa Databases](./databases.png) + +The ones providing input are shaded grey, and each is marked with its +originating crate. + +The file source is set in `SourceDatabaseExt`, which is broken out of +the hierarchy to enable lightweight test fixture construction. + +The `SourceDatabase` includes setting up the project model. + +At the moment (2021-12), the `EqwalizerDatabase` works with the +project model and legacy parser. All the databases related to the +legacy parser have a red background. + +The `HirDatabase` provides the main API entry point for tools and +internal features to use the ELP frontend processing. HIR stands for +High level Internal Representation. The HIR databases have a light +blue background. + +The HIR database is morally divided into internal private sub_crates, +called `hir_xxx` for some `xxx`, which should only ever be accessed +via a fellow `hir_xxx`. + +### Data Pump + +Since Salsa provides a pull based architecture, something must do the +pulling to trigger any computation. + +For the CLI usage this is part of the command invoked, after the +project has been loaded, so it will not be discussed further here. + +In the LSP server, when a notification is received from the client +that a file has changed, this change is pushed into the [Virtual File +System (VFS)](#virtual-file-system-vfs) (which is outside of salsa) +and the fact that the file has changed is recorded in the VFS. + +The main loop then processes this list of changes, calling the salse +input function `set_file_text` per changed `FileId`, and also updating +the `LineIndexDatabase` for the given file. + +Once the salsa inputs are updated, a data pull is initiated by calling +for updated diagnostics, via an [Analysis](#analysis-and-analysishost) +snapshot. + +This represents a state of the salsa database at the time the +diagnostics are requested, and any operations on it will be cancelled +as soon as a new operation is invoked on an `Analysis` snapshot. + +The `ide::diagnostics::diagnostics()` function kicks off the process, +by parsing to the rowan tree AST. + +```rust +parse(&self, file_id: FileId) -> Parse; +``` + +Because the parse function is on `SourceDatabase`, the result will +automatically be stored in the salsa database against the `FileId` +key. Internally the parse function asks for the `file_text`, via +another salsa query, and salsa automatically tracks this dependency +for managing the validity of the cache. + +This process continues, requesting whatever level of analysis is +required from the code base until the required diagnostics are +generated. + +Any other part of ELP needing similar analysis, e.g. constructing +hover info, processing assists, will reuse the results cached from the +diagnostics calculation. + +## Analyis Layers + +The Erlang source text is converted through a series of +transformations into representations that are more abstract, but more +useful for analyis. + +All of these are computed as needed, and all aim to reference data in +the lower layers, rather than constructing new data. + +### Rowan Tree AST + +This is the lowest layer, and is generated from the +[tree-sitter](https://tree-sitter.github.io) parse output. + +It lives in the `elp_syntax` crate. + +The code for this layer is auto-generated from the artifacts produced +from the tree-sitter grammar for Erlang. + +It comprises a series of [rowan tree](https://crates.io/crates/rowan) +`SyntaxNode`s capturing the totality of the the input source, +including all whitespace and comments. + +These have a layer of `AstNode`s on top providing the as-parsed +structure in a `Parse`. + +While having high fidelity to the original source, this representation +is clumsy to work with and does not have any higher level analyis +attached to it. + +For example, no names are resolved, no macros expanded, etc. + +### Hir Def + +The HIR representation aims to offer a meaningful high-level +representation of source code, while maintaining full incrementality, +and to do this in an efficient, fast way, so it does not lead to lags +in the IDE. A tall order. + +The key data structure for this is an [`ItemTree`](#itemtree), which +captures a high-level view of each source file or macro expansion. + +#### ItemTree + +This is the primary IR used throughout `hir_def`. It is the input to +the name resolution algorithm, as well as for API usage. + +`ItemTree`s are indexed in the salsa database by +[`HirFileId`](#hirfileid). They are built from the syntax tree of the +parsed file or macro expansion. This means that they are +module-independent: they don't know which conditional compilation +flags are active or which module they belong to, since those concepts +don't exist at this level. + +One important purpose of this layer is to provide an "invalidation +barrier" for incremental computations: when typing inside an item +body, the `ItemTree` of the modified file is typically unaffected, +so we don't have to recompute name resolution results or item data + +The `ItemTree` for the currently open file can be displayed by using +the VS Code command "Erlang: Debug ItemTree". (coming soon: +T107829182) + +The representation of items in the `ItemTree` should generally mirror +the surface syntax: it is usually a bad idea to desugar a syntax-level +construct to something that is structurally different here. Name +resolution needs to be able to process conditional compilation and +expand macros, and having a 1-to-1 mapping between syntax and the +`ItemTree` avoids introducing subtle bugs. + +In general, any item in the `ItemTree` stores its [`AstId`](#astid), which +allows mapping it back to its surface syntax. + +##### ItemTree Construction + +An `ItemTree` is returned by the salsa `DefDatabase::file_item_tree`, which +invokes `ItemTree::file_item_tree_query`. + +This calls `DefDatabase::parse_or_expand` to get the top level +`SyntaxNode` of the file or macro expansion. + +It then uses the `match_ast!` rust macro to try candidate `SyntaxNode` +casts for the top node, and operate on the one that succeeds. The +candidates are the results of parsing a file (`ast::SourceFile`), or +the results of expanding a macro, which is one of the [`Fragmentkind`](#fragmentkind) +types. + +Each kind of `SyntaxNode` is lowered to the `ItemTree` using its own +specific process. + +At the moment this is only for the `ast::SourceFile` + +The lowering is done using a `hir_def::item_tree::lower::Ctx`, which +contains the `ItemTree` being built, and an `AstIdMap` which is +constructed for the [`HirFileId`](#hirfileid) being processed. + +For the `ast::SourceFile`, it iterates over the `ast::Form`s in the +file, and calls the appropriate lowering function on each, to produce +a `ModItem` for the `ItemTree`. + +At the moment (2021-12) only a few of these are implemented to provide +enough information for macro expansion. They will be fully fleshed out +as part of adding the new high-level IR AST to ELP. + +## Data flow in HIR + +### Processing "Erlang: Expand Macro" request + +This walkthough follows the happy path. + +The LSP client sends a request to ELP with the current cursor +location, which needs to be on an Erlang macro. + +The `elp::handlers::handle_expand_macro` handler is given an Analysis +snapshot of the current state of the salsa storage. + +For a snapshot, trying to change the inputs will block, and the +processing will be cancelled if any new change is processed by the +language server. + +It invokes `elp_ide::expand_macro::expand_macro` + +This constructs a new [Semantics](#semantics) object, which is the +primary API into the HIR layer, and asks it to parse the file +containing the cursor position. + +This invokes the `syntax` crate parse, but then caches the result +locally. + +The `Semantics` cache is a mapping of the syntax layer `SyntaxNode` to +a [HirFileId](#hirfileid). A `HirFileId` is a handle to either an +original source file, or to the contents of an expanded macro. + +The cache allows us to follow the trail back from a syntax node to the +macro expansion or file containing it. + +With the parse result, `expand_macro` identifies the specific token at +the cursor position representing the macro call. + +The token is a `SyntaxNode`, which allows requesting its +ancestors. These are traversed until the `ast::MacroCallExpr` is +found, identified by a successful cast from the generic syntax +representation. + +This expression is passed to `ide::expand_macro::expand_macro_recur`, +which expands the macro, together with any in the resulting expansion. + +Note: This is based on the current rust-analyzer process. For the +envisaged Erlang UI there would be an option to expand one step only, +and show the results. The LSP client user can then inspect it, and +potentially expand individual sub-macros if desired. + +The first step is common in both cases, expand the current macro by +calling the `hir::semantics::Semantics::expand` function with the +macro call. + +This call first performs a [Semantic Analysis](#semantic-analysis) to +return a `SourceAnalyzer` for the given macro call. This analyzer +includes a resolver which should be able to resolve the name of the +macro to its originating definition, even if it is in another file. + +It then calls [SourceAnalyzer::expand](#sourceanalyzer-expand) to expand the macro, returning a +`HirFileId` for this expansion. + +At this point the definition for the macro has been resolved, and the +macro has either been expanded eagerly or its `MacroCallLoc` has been +interned, and is accessible via the `HirFileId`. + +The expansion is forced by calling [ExpandDatabase::parse_or_expand](expanddatabase-parse_or_expand) +with the `HirFileId`. + +This result is also cached, mapping the root of the syntax tree to the +`HirFileId`, so the originating file for any `SyntaxNode` in the tree +can be later looked up when needed. + +At this point one step of macro expansion has been done. The results +of expansion may in turn include macros though, so for this +implementation they are also expanded, by iterating over all child +nodes in the expansion looking for macro calls, and recursively +invoking `expand_macro_recur` on them. + +For UI purposes we are likely to provide a single-step option too, +which does not recursivelyexpand the children. + +### Semantic Analyis + +Semantic analysis is done by calling the +`hir::semantics::SemanticsImpl::analyze` function on a `SyntaxNode` +which returns a [SourceAnalyzer](#sourceanalyzer). + +The first thing this does is to retrieve the originating `HirFileId` +which was cached when either the original source was parsed by +semantics, or the originating macro expansion was cached. This lookup +is done on the root node of the given `SyntaxNode`, and returns an +[InFile](#infile) containing the `HirFileId` and the syntax node. + +It then looks for the container, using `SourceToDefCtx::find_container`. + +This will either be a file or a macro call, captured in a +[ChildContainer](#childcontainer) structure. + +A [Resolver](#resolver) is computed for the container, according to +its type. + +This involves constructing a [DefMap](#defmap) for the container, and +returning the [Scope](#scopes)s from it. + +A [`SourceAnalyzer`](#sourceanalyzer) is returned with the resolver +from the container if found, or an empty one if not. + +### source_file_to_def() + +This function is in `hir::source_to_def::SourceToDefCtx`. + +It is passed an `InFile` with a [HirFileId](#hirfileid) and a +`ast::SourceFile`. + +It first retrieves the `FileId` of the original file, if it is a macro +expansion. + +It then invokes `file_to_def` which calls the salsa query +[DefDatabase::file_def_map](#defdatabase-file_def_map) on the `FileId`. + +### DefDatabase::file_def_map + +This is a transparent salsa query, so the result is not cached. + +The actual work happens in `DefMap::file_def_map_query` which +constructs and returns a [DefMap](#defmap). + +### Source to Def + +This is implemented in `hir::semantics::source_to_def`. + + Maps *syntax* of various definitions to their semantic ids. + +This is a very interesting module, and, in some sense, can be considered the +heart of the IDE parts of ELP. + +This module solves the following problem: + + Given a piece of syntax, find the corresponding semantic definition (def). + +This problem is a part of more-or-less every IDE feature implemented. Every +IDE functionality (like goto to definition), conceptually starts with a +specific cursor position in a file. Starting with this text offset, we first +figure out what syntactic construct are we at: is this a pattern, an +expression, a function clause. + +Knowing only the syntax gives us relatively little info. For +example, looking at the syntax of the clause we can realise that +it is a part of a `fundecl` block, but we won't be able to tell +what arity the current function has, and whether the clause does +that that correctly. For that, we need to go from +[`ast::FunctionClause`] to [`module::Function`], and that's +exactly what this module does. + +As syntax trees are values and don't know their place of origin/identity, +this module also requires [`InFile`] wrappers to understand which specific +real or macro-expanded file the tree comes from. + +The actual algorithm to resolve syntax to def is curious in two aspects: + +* It is recursive +* It uses the inverse algorithm (what is the syntax for this def?) + +Specifically, the algorithm goes like this: + +1. Find the syntactic container for the syntax. For example, field's + container is the struct, and structs container is a module. +2. Recursively get the def corresponding to container. +3. Ask the container def for all child defs. These child defs contain + the answer and answer's siblings. +4. For each child def, ask for it's source. +5. The child def whose source is the syntax node we've started with + is the answer. + +It's interesting that both Roslyn and Kotlin contain very similar code +shape. + +Let's take a look at Roslyn: + + + + +The `GetDeclaredType` takes `Syntax` as input, and returns `Symbol` as +output. First, it retrieves a `Symbol` for parent `Syntax`: + + + +Then, it iterates parent symbol's children, looking for one which has the +same text span as the original node: + + + +Now, let's look at Kotlin: + + + +This function starts with a syntax node (`KtExpression` is syntax, like all +`Kt` nodes), and returns a def. It uses +`getNonLocalContainingOrThisDeclaration` to get syntactic container for a +current node. Then, `findSourceNonLocalFirDeclaration` gets `Fir` for this +parent. Finally, `findElementIn` function traverses `Fir` children to find +one with the same source we originally started with. + +One question is left though -- where does the recursion stop? This happens +when we get to the file syntax node, which doesn't have a syntactic parent. +In that case, we loop through all the crates that might contain this file +and look for a module whose source is the given file. + +Note that the logic in this module is somewhat fundamentally +imprecise -- due to conditional compilation there's no injective +mapping from syntax nodes to defs. + +At the moment, we don't really handle this well and return the +first answer that works. Ideally, we should first let the caller +to pick a specific active conditional compilation configuration +for a given position, and then provide an API to resolve all +syntax nodes against this specific crate. + +### SourceAnalyzer::expand + +This function expands a macro, looking up its definition in the +`Resolver` stored in the `SourceAnalyzer`. + +The resolution uses +`hir_def::resolver::Resolver::resolve_path_as_macro`, which first +checks for a built in macro, and otherwise consults the +[DefMap](#defmap) providing module scope for the definition. + +The expansion process happens in `hir_def::AsMacroCall::as_call_id`, which +simply calls `hir_def::AsMacroCall::as_call_id_with_errors` with an +empty error destination. When we need diagnostics, this is invoked +with a real error destination. + +The `as_call_id_with_errors` creates an [AstId](#astid) for the +`MacroCallExpr`. This uses the `AstIdMap` for the given `HirFileId` +which is from the salsa database, so it will only be computed once. + +It then calls `hir_def::macro_call_as_call_id` with an `AstIdWithPath` +being a combination of the `FileId`, `AstId` and macro name. + +This invokes the resolver, looking up the macro name in the `Scope` +computed earlier. + +If it is an eagerly expanded macro this is done immediately by calling +[expand_eager_macro](#expand_eager_macro). + +Otherwise it is converted to a `MacroCallId` by making a salsa call to +[intern_macro](#intern_macro) with a `MacroCallLoc` capturing the +resolved definition. + +The `MacroCallId` is returned as a `MacroFile` variant of a `HirFileId`. + +### intern_macro + +Macro ids. That's probably the trickiest bit in ELP, and the reason +why we use salsa at all. + +We encode macro definitions into ids of macro calls, this what allows +us to be incremental. + +```rust +fn intern_macro(&self, macro_call: MacroCallLoc) -> MacroCallId; +``` + +This uses the special +[salsa::interned](https://salsa-rs.github.io/salsa/rfcs/RFC0002-Intern-Queries.html) +attribute on the salsa database, which means `db.intern_macro(..)` +will allocate a unique id to the thing interned, which can be looked +up via a call to `db.lookup_intern_macro(..)` with the given id. + +Calling `intern_macro` with the same info will return the same id. + +### expand_eager_macro + +We use this for built-in macros, and does a similar process to the +normal macro expansion. + +### ExpandDatabase::parse_or_expand + +This salsa call converts a `HirFileId` into a SyntaxNode. It is marked +as `salsa::transparent`, so is not actually stored in the database, it +is not possible to store `SyntaxNode`s, although the underlying rowan +tree nodes can be stored. + +For the `FileId` variant it does a normal parse, for the `MacroFile` +variant it calls [ExpandDatabase::parse_macro_expansion_ast](#expanddatabase-parse_macro_expansion_ast) with the +`MacroCallId`. + +### ExpandDatabase::parse_macro_expansion_ast + +This is also marked as transparent on the salsa db, so does not store +anything, but any non-transparent calls to salsa while processing it +will be cached in salsa. + +So the first call to `ExpandDatabase::macro_expand_ast` does cache its +result. + +`macro_expand_ast` looks up the interned macro id, and if it was eagerly expanded +returns the expansion directly. + +Otherwise it asks for the [AstExpander](#astexpander) for the provided +macro definition, and applies it, returning a set of tokens. + +Macros can occur in many places in the Erlang source code, +corresponding to different grammatical types. We need to parse the +expanded tokens as the same grammatical type, otherwise we will get +spurious syntax errors. We track the originating type in a [FragementKind](#fragmentkind). + +`token_tree_to_syntax_node` converts the tokens back to source text, +wraps it in the appropriate surroundings for the needed fragment kind, +and parses it, returning a `SyntaxNode` and a [TokenMap](#tokenmap) +mapping the locations in the `SyntaxNode` back to the original +locations in macro definition and arguments. + +These are returned. + +### AstExpander + +An AstExpander is an enumeration over built-in or user-defined macros. +Each of these as an `expand_ast` call defined. This takes the +`MacroCallId` and optional macro arguments, and returns a tree of +tokens. + +The `hir_expand::token_map::MacroDef::expand_ast` function for +user-defined macros operates as follows. + +It first retrieves the previously interned `MacroCallLoc` to access +the macro definition. This returns an `AstId`, which gets converted to +an actual definition via [AstId::to_node](#astid-to_node), re-parsing +the originating file if it has changed in the meantime. + +This results in the original low-level syntax +`ast::Macro::MacroDefine` instance. + +The macro_call is similarly retrieved. + +The macro definition arguments are matched up with the macro call +arguments into a substitutions map, from the definition variable name +to the call argument. + +The macro expansion then takes place in `MacroDef::substitute`, called +with the substitution map and the definition rhs. + +Because we cannot actually edit a rowan tree without affecting +everything else that refers to it, this substitution is constructed by +splicing the tokens from the definition rhs, in a traversal that looks +for macro argument usage, and uses the substitution tokens instead. + +So we end up with a tree representing the expansion with substitution. + +This is returned. + +## Key data structures + +### `elp_base_db::input::SourceRoot` + +Files are grouped into source roots. A source root is a directory on +the file systems which is watched for changes. Typically it +corresponds to an OTP application. Source roots *might* be nested: in +this case, a file belongs to the nearest enclosing source root. Paths +to files are always relative to a source root, and ELP does not know +the root path of the source root at all. So, a file from one source +root can't refer to a file in another source root by path. + +An `elp_base_db::input::SourceRootId` is used to refer to a +`SourceRoot` in a salsa query for it. + +### `elp_base_db::input::ProjectData` + +Source roots (apps) are grouped into projects that share some of the +configuration. They are indexed in salsa with +`elp_base_db::input::ProjectId` + +Prior to buck2, there is a correspondance between an instance of +`ProjectData` and a rebar config file. **True?** + +```rust +pub struct ProjectData { + pub root_dir: AbsPathBuf, + pub deps_ebins: Vec, + pub ast_dir: AbsPathBuf, + pub build_info_path: AbsPathBuf, +} +``` + +### `elp_base_db::input::AppData` + +`AppData` captures the information for an app from the rebar +`project_app_dirs` list. It includes the `ProjectId` of the +`ProjectData` containing this app. + +They are indexed in salsa with +`elp_base_db::input::SourceRootId` + +```rust +pub struct AppData { + pub project_id: ProjectId, + pub dir: AbsPathBuf, + pub include_path: Vec, + pub src_dirs: Vec, + pub extra_src_dirs: Vec, + pub macros: Vec, + pub parse_transforms: Vec, +} +``` + +### Analysis and AnalysisHost + +#### `elp_ide::Analysis` + +`Analysis` is a snapshot of a world state at a moment in time. It is the +main entry point for asking semantic information about the world. When +the world state is advanced using `AnalysisHost::apply_change` method, +all existing `Analysis` are canceled (most method return +`Err(Canceled)`). + +```rust +pub struct Analysis { + db: salsa::Snapshot, +} +``` + +#### `elp_ide::AnalysisHost` + +`AnalysisHost` stores the current state of the world. + +```rust +pub struct AnalysisHost { + db: RootDatabase, +} +``` + +### `elp_ide_db::RootDatabase` + +The `RootDatabase` is the physical structure providing storage for all +the non-test salsa databases. + +It also provides a means to access the external processes for +`Eqwalizer` and the legacy Erlang parser. + +```rust +pub struct RootDatabase { + storage: salsa::Storage, + erlang_services: Arc>>, + eqwalizer: Eqwalizer, +} +``` + +One of them is created when loading a project via the CLI, and the LSP +server has one too. + +### `elp_syntax::Parse` + +This is the output of parsing an Erlang source file. It is a +structure representing the rowan tree and errors found while parsing +the file. + +```rust +pub struct Parse { + green: GreenNode, + errors: Arc>, + _ty: PhantomData T>, +} +``` + +### Semantics + +`hir::semantics::Semantics` provides the primary API to get semantic +information, like types, from syntax trees. + +### HirFileId + +Input to ELP is a set of files, where each file is identified by +`FileId` and contains source code. However, another source of source +code in Erlang are macros: each macro can be thought of as producing a +"temporary file". To assign an id to such a file, we use the id of the +macro call that produced the file. So, a `HirFileId` is either a +`FileId` (source code written by user), or a `MacroCallId` (source +code produced by macro). + +What is a `MacroCallId`? Simplifying, it's a `HirFileId` of a file +containing the call plus the offset of the macro call in the +file. Note that this is a recursive definition! However, the size_of +of `HirFileId` is finite (because everything bottoms out at the real +`FileId`) and small (`MacroCallId` uses the location interning. You +can check details here: +). + +### SourceAnalyzer + +`SourceAnalyzer` is a convenience wrapper which exposes HIR API in +terms of original source files. It should not be used inside the HIR +itself. + +It contains a `HirFileId` and a [Resolver](#resolver) for it. + +### InFile + + `InFile` stores a value of `T` inside a particular file/syntax + tree. + + Typical usages are: + +* `InFile` -- syntax node in a file +* `InFile` -- ast node in a file +* `InFile` -- offset in a file + +```rust +pub struct InFile { + pub file_id: HirFileId, + pub value: T, +} +``` + +### ChildContainer + +Enum used as an index in a [SourceToDefCtx]. For ELP it currently +captures either a `ModuleId` or a `MacroCallExprId` + +### Resolver + +A `Resolver` stores [Scope](#scope)s for the relevant item, and +provides operations with these scopes. + +### Scope + +The `hir_def::resolver::Scope` is an enum. At the moment it only has +a variant to keep track of all the items and included names in a +module. + +When ELP is built out further, it will track scopes within clauses, +blocks etc. + +For a module this keeps the module id and a [DefMap](#defmap) for the +module, which in turn tracks detailed [ItemScope](#itemscope)s. + +### DefMap + +A `DefMap` is computed in `hir_def::nameres`. This process invokes +include file resolution and macro expansion, to provide the set of +items visible in a module, either directly declared or included. + +A `DefMap` contains `ModuleData` which contains +[ItemScope](#itemscope) information, detailing the scopes of all items +in the map. + +Computing `DefMap` can be partitioned into several logically +independent "phases". The phases are mutually recursive though, +there's no strict ordering. + +#### Collecting RawItems + +This happens in the `raw` module, which parses a single source +file into a set of top-level items. Macro calls are represented as +a triple of `(Path, Option, TokenTree)`. + +#### Collecting Modules + +This happens in the `collector` module. In this phase, we +recursively walk the module, collect raw items from submodules, +populate module scopes with defined items (so, we assign item ids +in this phase) and record the set of unresolved imports and +macros. + +While we walk tree of modules, we also record macro_rules definitions and +expand calls to macro_rules defined macros. + +#### Resolving Includes + +We maintain a list of currently unresolved includes. On every iteration, we +try to resolve some includes from this list. If the include is resolved, we +record it, by adding an item to current module scope. + +#### Resolving Macros + +Macros from include files are handled similarly to include +files. There's a list of unexpanded macros. On every iteration, we +try to resolve each macro call and, upon success, we run macro +expansion and "collect module" phase on the result + +#### Detail + +`DefMap::file_def_map_query` creates an empty `DefMap` for the file, +and and invokes `collector::collect_defs` on it. + +This creates a `DefCollector` structure to track the state, and seeds +it with the top level items in the module being collected, in an [ItemTree](#itemtree) + +These are returned by the salsa `DefDatabase::file_item_tree`, which +invokes `ItemTree::file_item_tree_query` to [construct the +ItemTree](#itemtree-construction) + +### ItemScope + +This structure carries the main payload for a [DefMap](#defmap). + +It keeps hashmaps of resolved names per entity type, as well as for +currently unresolved items. + +At the moment (2021-12) the only entity types populated are the +`unresolved` and `macros` ones, but provision is made for types and +values too. + +The key API to this is `ItemScope::get`, which is given a +[Name](#name) and returns a [PerNs](#perns) structure for it. This +allows looking up a name without knowing its type, then then +processing what is found. + +### PerNs + +The `hir_def::per_ns::PerNs` structure has optional fields for types, +values or macros, corresponding to the valid namespaces. + +When a name is resolved, one of these fields will be populated with +the actual id and visibility for it. + +This structure also supports name overloading, if a name can resolve +into more than one name space. + +### Name + +In ELP, `Name` can mean one of a number of things. + +At the syntax AST level, there is `ast::Name`, which can either be an +`ast::Atom` or `ast::Var`. + +At the HIR level, there is `hir_expand::name::Name`. This is a wrapper +around a string, and is used for both references and declarations. + +### MacroCallId + +A `hir_expand::MacroCallId` identifies a particular macro invocation. +It uses the [interning](#intern-macro) infrastructure to allow +retrieving the original `MacroCallLoc` from it. + +### AstId + +`AstId` points to an AST node in any file. + +It is stable across reparses, and can be used as salsa key/value. + +```rust +pub type AstId = InFile>; +``` + +It is used as an index into an `AstIdMap`, which is an `Arena` of [SyntaxNodePtr](#syntaxnodeptr). +An `Arena` is just a vector of its contents, accessed by the numeric index. + +The `AstIdMap` is constructed by traversing the `SyntaxNode` tree for +a file in top-down breadth-first order, and for each item of interest +adding an entry, where its location in the vector is its index. + +The breadth first order means even if a file changes its contents, the +index is more likely to still be valid for higher level items. + +### AstId::to_node + +This converts an `AstId` back to its original reference, even if it +may have moved. This is done by asking salsa to parse the file stored +in the `AstId`, and generate an `AstIdMap` from it. These operations +will be NOPs if the file has not changed in the meantime, or will use +the cached value. This may be different from the one used when the +`AstId` was stored though. The structure of the `AstId` allows higher +level syntax nodes to be looked up, even if the underlying file has +changed. + +### SyntaxNodePtr + +A pointer to a syntax node inside a file. It can be used to remember a +specific node across reparses of the same file. + +```rust +pub struct SyntaxNodePtr { + pub(crate) range: TextRange, + kind: SyntaxKind, +} +``` + +### TokenMap + +When expanding a macro, the expansion works on tokens extracted from +the original syntax AST of the macro definition and call +arguments. Each token is given a unique `TokenId` during this process. + +A `hir_expand::token_map::TokenMap` maps this `TokenId` back to a +*relative* text range. + +This map allows tracing a location in a macro expansion back to its +precise origin in the original definition or arguments. + +### FragmentKind + +A macro call can occur in various locations in the Erlang source code, +and as a consequence can have different types. Since macro expansion +proceeds by manipulating tokens when replacing arguments in a macro +replacement, these tokens need to be parsed back to a syntax AST. + +`hir_expand::syntax_bridge::FragmentKind` is an enumeration tracking +what the original syntax type was, so the appropriate parsing fragment +can be invoked. + +The ELP tree-sitter grammar has rules for a top-level pseudo Form that +can be one of these fragments, each wrapped in a text string that is +guaranteed not to be valid Erlang syntax. This allows us to parse a +fragment of a specific type. + +### Crates + +This section is based on the [rust analyzer architecture](https://github.com/rust-lang/rust-analyzer/blob/master/docs/dev/architecture.md) doc. + +#### syntax + +Erlang syntax tree structure and parser. + +- [rowan](https://github.com/rust-analyzer/rowan) library is used for constructing syntax trees. +- `ast` provides a type safe API on top of the raw `rowan` tree. +- the tree-sitter compile output generates a description of the + grammar, which is used to generate `syntax_kinds` and `ast` + modules, using `cargo xtask codegen` command. + +Note [`api_walkthrough`](https://www.internalfb.com/code/fbsource/[D32881714-V6]/fbcode/whatsapp/elp/crates/syntax/src/lib.rs?lines=1797) +in particular: it shows off various methods of working with syntax tree. + +**Architecture Invariant:** `syntax` crate is completely independent +from the rest of ELP. It knows nothing about salsa or LSP. +This is important because it is possible to make useful tooling using +only the syntax tree. Without semantic information, you don't need to +be able to _build_ code, which makes the tooling more robust. See +also https://web.stanford.edu/~mlfbrown/paper.pdf. You can view the +`syntax` crate as an entry point to rust-analyzer. `syntax` crate is +an **API Boundary**. + +**Architecture Invariant:** syntax tree is a value type. + +The tree is fully determined by the contents of its syntax nodes, it +doesn't need global context (like an interner) and doesn't store +semantic info. +Using the tree as a store for semantic info is convenient in +traditional compilers, but doesn't work nicely in the IDE. +Specifically, assists and refactors require transforming syntax trees, +and that becomes awkward if you need to do something with the semantic +info. + +**Architecture Invariant:** syntax tree is built for a single file. +This is to enable parallel parsing of all files. + +**Architecture Invariant:** Syntax trees are by design incomplete and +do not enforce well-formedness. If an AST method returns an `Option`, +it *can* be `None` at runtime, even if this is forbidden by the +grammar. + +### `crates/base_db` + +We use the [salsa](https://github.com/salsa-rs/salsa) crate for +incremental and on-demand computation. + +Roughly, you can think of salsa as a key-value store, but it can also +compute derived values using specified functions. + +The `base_db` crate provides basic infrastructure for interacting with +salsa. + +Crucially, it defines most of the "input" queries: facts supplied by +the client of the analyzer. + +Reading the docs of the `base_db::input` module should be useful: +everything else is strictly derived from those inputs. + +**Architecture Invariant:** particularities of the build system are *not* the part of the ground state. + +In particular, `base_db` knows nothing about rebar or buck. + +**Architecture Invariant:** `base_db` doesn't know about file system +and file paths. + +Files are represented with opaque `FileId`, there's no operation to +get an `std::path::Path` out of the `FileId`. + +### `crates/hir_expand`, `crates/hir_def` + +These crates are the *brain* of ELP. +This is the compiler part of the IDE. + +`hir_xxx` crates have a strong +[ECS](https://en.wikipedia.org/wiki/Entity_component_system) flavor, +in that they work with raw ids and directly query the database. +There's little abstraction here. These crates integrate deeply with +salsa and chalk. + +Name resolution, macro expansion and type inference all happen here. +These crates also define various intermediate representations of the core. + +[ItemTree](#itemtree) condenses a single `SyntaxTree` into a "summary" data +structure, which is stable over modifications to function bodies. + +[DefMap](#defmap) contains the module tree of a crate and stores module scopes. + +`Body` stores information about expressions. + +**Architecture Invariant:** these crates are not, and will never be, +an api boundary. + +**Architecture Invariant:** these crates explicitly care about being +incremental. The core invariant we maintain is "typing inside a +function's body never invalidates global derived data". i.e., if you +change the body of `foo`, all facts about `bar` should remain intact. + +**Architecture Invariant:** hir exists only in context of particular +file instance with specific configuration flags. + +### `crates/hir` + +The top-level `hir` crate is an **API Boundary**. + +If you think about "using ELP as a library", `hir` crate is +most likely the façade you'll be talking to. + +It wraps ECS-style internal API into a more OO-flavored API (with an +extra `db` argument for each call). + +**Architecture Invariant:** `hir` provides a static, fully resolved +view of the code. + +While internal `hir_*` crates _compute_ things, `hir`, from the +outside, looks like an inert data structure. + +`hir` also handles the delicate task of going from syntax to the +corresponding `hir`. +Remember that the mapping here is one-to-many. +See `Semantics` type and `source_to_def` module. + +Note in particular a curious recursive structure in `source_to_def`. +We first resolve the parent _syntax_ node to the parent _hir_ element. +Then we ask the _hir_ parent what _syntax_ children does it have. +Then we look for our node in the set of children. + +This is the heart of many IDE features, like goto definition, which +start with figuring out the hir node at the cursor. + +This is some kind of (yet unnamed) uber-IDE pattern, as it is present +in Roslyn and Kotlin as well. + +### `crates/ide` + +The `ide` crate builds on top of `hir` semantic model to provide +high-level IDE features like completion or goto definition. + +It is an **API Boundary**. + +If you want to use IDE parts of ELP via LSP, custom flatbuffers-based +protocol or just as a library in your text editor, this is the right +API. + +**Architecture Invariant:** `ide` crate's API is build out of POD +types with public fields. + +The API uses editor's terminology, it talks about offsets and string +labels rather than in terms of definitions or types. + +It is effectively the view in MVC and viewmodel in +[MVVM](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel). + +All arguments and return types are conceptually serializable. + +In particular, syntax trees and hir types are generally absent from +the API (but are used heavily in the implementation). + +Shout outs to LSP developers for popularizing the idea that "UI" is a +good place to draw a boundary at. + +`ide` is also the first crate which has the notion of change over time. +[AnalysisHost](#analysishost) is a state to which you can +transactionally `apply_change`. +[Analysis](#analysis) is an immutable snapshot of the state. + +Internally, `ide` is split across several crates. `ide_assists`, +`ide_completion` and `ide_ssr` implement large isolated features. + +`ide_db` implements common IDE functionality (notably, reference +search is implemented here). + +The `ide` contains a public API/façade, as well as implementation for +a plethora of smaller features. + +**Architecture Invariant:** `ide` crate strives to provide a _perfect_ API. + +There are currently two consumers of `ide`, the LSP server and the CLI client. + +### `crates/elp` + +This crate defines the `elp` binary, so it is the **entry point**. +It implements the language server, and CLI client. + +**Architecture Invariant:** `elp` is the only crate that knows about LSP and JSON serialization. +If you want to expose a data structure `X` from ide to LSP, don't make it serializable. +Instead, create a serializable counterpart in `elp` crate and manually convert between the two. + +`GlobalState` is the state of the server. +The `main_loop` defines the server event loop which accepts requests and sends responses. +Requests that modify the state or might block user's typing are handled on the main thread. +All other requests are processed in background. + +**Architecture Invariant:** the server is stateless, a-la HTTP. +Sometimes state needs to be preserved between requests. +For example, "what is the `edit` for the fifth completion item of the last completion edit?". +For this, the second request should include enough info to re-create the context from scratch. +This generally means including all the parameters of the original request. + +**Architecture Invariant:** `elp` should be partially available even when the build is broken. +Reloading process should not prevent IDE features from working. + +#### Crates Alphabetically + +base_db +eetf +elp +eqwalizer +erl_ast +hir +hir_def +hir_expand +ide +ide_assists +ide_db +erlang_service +project_model +syntax +tree-sitter-erlang + +#### Crates Graph + +![Crate graph](./crate_graph.png) + +## Virtual File System (VFS) + +[A virtual filesystem for Rust](https://docs.rs/vfs/0.5.1/vfs/) + +The virtual file system abstraction generalizes over file systems and +allows using different filesystem implementations (e.g. an in memory +implementation for unit tests) + +In ELP we use this to keep an in-memory image of the current files +open in the IDE, which will be different form the one on disk if the +LSP client has edited the file, but not yet saved it. + +This gives us the current source of truth for the source files, from +the IDE user perspective. + +The VFS file system can be partitioned into different areas, and we +use this to create a partition for every app occuring in every +project. See [Key data structures](#key-data-structures) diff --git a/docs/CODE_ACTIONS.md b/docs/CODE_ACTIONS.md new file mode 100644 index 0000000000..4b36367bbe --- /dev/null +++ b/docs/CODE_ACTIONS.md @@ -0,0 +1,295 @@ +# Code Actions (a.k.a. Assists) + +_Code actions_, also known as _assists_, are small local refactorings, often rendered in the text editor using a light bulb icon (💡). They are triggered by either clicking the light bulb icon in the editor or by using a shortcut. + +Code actions often provide the user with possible corrective actions right next to an error or warning (known as a _diagnostic_ message using LSP jargon). They can also occur independently of diagnostics. + +Here is an example of a _code action_ prompting the user to "Add Edoc comment" for a function which lacks Erlang EDoc documentation. + +![Code Action - Add Edoc](./images/code-action-add-edoc.png) + +## The _Code Action_ request + +Code actions are requested by the editor using the [textDocument/codeAction](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_codeAction) LSP request. Code action requests are handled by the `handlers::handle_code_action` function in the `elp` crate. + +## Adding a new code action + +### Creating the handler + +In this section we will go through the process of adding a new code action from scratch. The code action (aka _assist_) will suggest the user to delete a function, if it is deemed as unused by the Erlang compiler. + +Let's start by creating a file called `delete_function.rs` in the `crates/ide_assists/src/handlers` folder, containing a single function declaration: + +``` +use crate::assist_context::{Assists, AssistContext}; + +// Assist: delete_function +// +// Delete a function, if deemed as unused by the Erlang compiler. +// +// ``` +// -module(life). +// +// heavy_calculations(X) -> X. +// %% ^^^^^^^^^^^^^^^ 💡 L1230: Function heavy_calculations/1 is unused +// +// meaning() -> +// 42. +// ``` +// -> +// ``` +// -module(life). +// meaning() -> +// 42. +// ``` +pub(crate) fn delete_function(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { + todo!() +} +``` + +Notice how the function is accompanied by a comment which explains the expected transformation. In the first snippet, we notice that the `heavy_calculations` function is unused and we get a code action for it. In the second snippet we see the outcome of executing the given code action: the unused function is gone. We will go back to the funny syntax used in the snippet in a second. + +Before we can start implementing our code action, there's one more thing we need to do: add our new function to the list of _ELP assists_. Open the `crates/ide_assists/src/lib.rs` file and amend the list of handlers: + +``` +mod handlers { + [...] + mod delete_function + [...] + + pub(crate) fn all() -> &'static [Handler] { + &[ + [...] + delete_function:delete_function, + [...] + ] + } +} +``` + +### Adding a test case + +The easiest way to verify our new code action behaves in the expected way is to start with a test case. ELP allows us to write tests in a very intuitive and straightforward way. Do you remember the funny syntax which we introduced in the previous paragraph? We can reuse it to write a testcase! + +Add the following to the `delete_function.rs` file: + +``` +#[cfg(test)] +mod tests { + use expect_test::expect; + + use super::*; + use crate::tests::*; + + #[test] + fn test_delete_unused_function() { + check_assist( + delete_function, + r#" +-module(life). +heavy_calculations(X) -> X. +%% ^^^^^^^^^^^^^^^ 💡 L1230: Function heavy_calculations/1 is unused + +meaning() -> + 42. +"#, + expect![[r#" +-module(life). +meaning() -> + 42. + "#]], + ) + } +} +``` + +The `~` in the snippet represents the cursor position. In our testcase, we are asserting that, given a diagnostic message pointing to the unused function, if the user triggers the respective code action when the cursor is inside the function name, the unused function gets deleted. + +If we try running the test, it should fail with a _not yet implemented_ error: + +``` +$ cargo test --package elp_ide_assists --lib -- handlers::delete_function::tests::test_delete_unused_function --exact --nocapture + +[...] +---- handlers::delete_function::tests::test_delete_unused_function stdout ---- +thread 'handlers::delete_function::tests::test_delete_unused_function' panicked at 'not yet implemented', crates/ide_assists/src/handlers/delete_function.rs:21:5 +[...] +``` + +### Diagnostic Annotations and Error Codes + +Before starting with the actual implementation, let's for a second go back to the syntax we used to specify the _unused function_ diagnostic: + +``` +%% ^^^^^^^^^^^^^^^ 💡 L1230: Function heavy_calculations/1 is unused +``` + +This is a test _annotation_ which is used by the ELP testing framework to populate the "context" which is passed to our handler. This is a way to simulate diagnostics coming from external sources (such as the Erlang compiler or a linter), which would be received by the Language Server as part of a `textDocument/codeAction` request. + +The annotation has the following format: + +``` +[\s]%% [^]* 💡 CODE: MESSAGE +``` + +Essentially, a number of spaces, followed by the `%%` which resembles an Erlang comment, a light bulb, a _code_ identifying the diagnostic type and a string message. The _code_ is an _unofficial error_ code which is emitted by both ELP's _Erlang Service_ (see the `erlang_service:make_code/2` function in `erlang_service/src/erlang_service.erl`) and by the [Erlang LS](https://github.com/erlang-ls/erlang_ls/) language server. The idea is to eventually standardize Erlang error messages and to build what, in the end, should be similar to the [Rust](https://doc.rust-lang.org/error-index.html) or [Haskell](https://errors.haskell.org/) error indexes. In our case, `L1230` is the error corresponding to the `unused_function` diagnostic. The _message_ is a free text string that accompanies the diagnostic. + +### Matching on the diagnostic error code + +To be able to match the `L1230` error code, we need to add a new variant to the `AssistContextDiagnosticCode` enum. Open the `crates/ide_db/src/assists.rs` file and include the new error code. Don't forget to map it to the `L1230` string. + +``` +pub enum AssistContextDiagnosticCode { + UnusedFunction, // <--- Add this + UnusedVariable, +} + +impl FromStr for AssistContextDiagnosticCode { + type Err = String; + fn from_str(s: &str) -> Result { + match s { + "L1230" => Ok(AssistContextDiagnosticCode::UnusedFunction), // <--- Add this + "L1268" => Ok(AssistContextDiagnosticCode::UnusedVariable), + unknown => Err(format!("Unknown AssistContextDiagnosticCode: '{unknown}'")), + } + } +} +``` + +We are all set. Time to implement the `delete_function` function! + +### The implementation + +Let's look at our function again. + +``` +pub(crate) fn delete_function(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { + todo!() +} +``` + +We have two input arguments: a mutable _accumulator_ which contains the list of code actions (or _assists_) which we want to return and a _context_, from which we can extract diagnostics. + +The following code iterates through the list of diagnostics and, for each diagnostic matching the `UnusedFunction` kind, prints the diagnostic for debugging purposes. We also return `Some(())` to comply with the function signature. + +``` +use elp_ide_db::assists::AssistContextDiagnosticCode; + +[...] +pub(crate) fn delete_function(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { + for d in ctx.diagnostics { + if let AssistContextDiagnosticCode::UnusedFunction = d.code { + dbg!(d); + todo!() + } + } + Some(()) +} +[...] + +``` + +If we run the test, we can see what a diagnostic looks like: + +``` +$ cargo test --package elp_ide_assists --lib -- handlers::delete_function::tests::test_delete_unused_function --exact --nocapture + +[...] +running 1 test +[crates/ide_assists/src/handlers/delete_function.rs:25] d = AssistContextDiagnostic { + code: UnusedFunction, + message: "Function heavy_calculations/1 is unused", + range: 24..40, +} +[...] +``` + +The diagnostic contains the error code and message, together with its range. What we want to do is: + +* Find the function declaration which is pointed by the diagnostic range +* Create a code action to remove the function declaration and add it to the accumulator + +How do we find the element which the range covers? Context to the rescue! There's a handy `find_node_at_custom_offset` function we can use. The _offset_ here indicates the number of bytes from the beginning of the file. We can use the beginning of the diagnostic range for our purposes. + +``` +let function_declaration: ast::FunDecl = ctx.find_node_at_custom_offset::(d.range.start())?; +let function_range = function_declaration.syntax().text_range(); +``` + +Let's extract the function name/arity and produce a nice message for the user: + +``` +let function_name = function_declaration.name()?; +let function_arity = function_declaration.arity_value()?; +let message = format!("Remove the unused function `{function_name}/{function_arity}`"); +``` + +With the information we have, we can now create a new code action and add it to the accumulator: + +``` +let id = AssistId("delete_function", AssistKind::QuickFix); +let function_range = function_declaration.syntax().text_range(); +acc.add(id, + message, + function_range, + |builder| { + builder.edit_file(ctx.frange.file_id); + builder.delete(function_range) + }, +); +``` + +The `add` function takes four arguments: + +* An internal `AssistId` made of a unique string (the `"delete_function"` string in our case) and a `Kind`. We are specifying `QuickFix` in our case, but have a look to the [LSP specifications](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#codeActionKind) to get a list of the available kinds. +* A message which will be rendered to the user (`"Delete the unused function: [FUNCTION_NAME]"`) +* The range of the function. Notice how the range we got from the diagnostic was covering only the _name_ of the function, but we need to delete the whole function, so we need to pass the full range. +* A function which takes a `builder` as its input and uses it to manipulate the source file. Here we are saying that we want to edit the current file (we extract the `file_id` from the `ctx` context) and that we simply want to delete the range of the function declaration. + +Yes. It's as simple as that. For completeness, here is the full function implementation: + +``` +pub(crate) fn delete_function(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { + for d in ctx.diagnostics { + if let AssistContextDiagnosticCode::UnusedFunction = d.code { + let function_declaration: ast::FunDecl = + ctx.find_node_at_custom_offset::(d.range.start())?; + let function_name = function_declaration.name()?; + let function_arity = function_declaration.arity_value()?; + let function_range = function_declaration.syntax().text_range(); + + let id = AssistId("delete_function", AssistKind::QuickFix); + let message = format!("Remove the unused function `{function_name}/{function_arity}`"); + acc.add(id, message, function_range, |builder| { + builder.edit_file(ctx.frange.file_id); + builder.delete(function_range) + }); + } + } + Some(()) +} +``` + +You can look at existing assists for more complex manipulation examples. + +# Try it yourself + +What we wrote is a unit test, but there's nothing better than checking ourselves the behaviour in the IDE. + +Compile the `elp` executable: + +``` +cargo build +``` + +Then visit the Erlang extension settings page and edit the `elp.path` value to point to the newly built executable, which should reside in: + +``` +/Users/$USER/fbsource/buck-out/elp/debug/elp +``` + +Open a VS Code @ Meta (or reload the window if you have one open) and visit an Erlang file from the WASERVER repo. You should see something like: + +![Code Action - Remove Function](./images/code-action-remove-function.png) + +If that worked, congratulations! You managed to write your first ELP code action! diff --git a/docs/ELP-parser-dataflow.excalidraw b/docs/ELP-parser-dataflow.excalidraw new file mode 100644 index 0000000000..603941fe01 --- /dev/null +++ b/docs/ELP-parser-dataflow.excalidraw @@ -0,0 +1,1219 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.thefacebook.com", + "elements": [ + { + "id": "Rb-LFE5J7Yxxse19rsGQo", + "type": "rectangle", + "x": 359.75390625, + "y": 146.32421875, + "width": 101.0390625, + "height": 89.8203125, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 461995966, + "version": 88, + "versionNonce": 1532725026, + "isDeleted": false, + "boundElementIds": [] + }, + { + "id": "wfBex6-pDsOQHsT_8oAl-", + "type": "text", + "x": 378, + "y": 165, + "width": 33, + "height": 23, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 10220066, + "version": 9, + "versionNonce": 102344354, + "isDeleted": false, + "boundElementIds": null, + "text": "IDE", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 18 + }, + { + "id": "Avd7Hg3qOHiR6JOB1VVQx", + "type": "rectangle", + "x": 549.75, + "y": 146.6953125, + "width": 107.6484375, + "height": 90.65234375, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 2011286014, + "version": 67, + "versionNonce": 864422690, + "isDeleted": false, + "boundElementIds": [ + "KwANdqSR3xrZh1QuwSMuL" + ] + }, + { + "id": "djIi689_QtRsfW2NsuIp2", + "type": "text", + "x": 582.07421875, + "y": 169.021484375, + "width": 43, + "height": 46, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 2087736546, + "version": 16, + "versionNonce": 53510754, + "isDeleted": false, + "boundElementIds": null, + "text": "tree\nsitter", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "baseline": 41 + }, + { + "id": "SPnEYfaNF4n8lmvBE-9af", + "type": "rectangle", + "x": 767.8203125, + "y": 167.3515625, + "width": 114.625, + "height": 102.97265625, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 2063848162, + "version": 170, + "versionNonce": 1886595746, + "isDeleted": false, + "boundElementIds": [ + "CHQMH5aSHpe9JlQflH6pQ", + "KwANdqSR3xrZh1QuwSMuL" + ] + }, + { + "id": "hffLonB6LpYajyggAONDl", + "type": "text", + "x": 777.41015625, + "y": 191.677734375, + "width": 96, + "height": 46, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1619294526, + "version": 183, + "versionNonce": 1971305570, + "isDeleted": false, + "boundElementIds": [ + "UGEUEE0fA8Tdz2BP3Ztgc", + "uk2HBcfv1VGz2PUVYz5vi" + ], + "text": "elp_syntax\nAST", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "baseline": 41 + }, + { + "id": "0vz1zW85ykDvLcE2k28zd", + "type": "rectangle", + "x": 764.1328125, + "y": 314.0078125, + "width": 128.44921875, + "height": 100.109375, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1521936958, + "version": 122, + "versionNonce": 781310370, + "isDeleted": false, + "boundElementIds": [ + "UGEUEE0fA8Tdz2BP3Ztgc", + "CHQMH5aSHpe9JlQflH6pQ", + "17PMh8sbHjd7S-JWBnFE1", + "TvNdnmg6lhNbnMCqNX90f" + ] + }, + { + "id": "3Q3QCrQS64pVj-BoUQ5nW", + "type": "text", + "x": 777.71875, + "y": 330.95703125, + "width": 100, + "height": 69, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1952648638, + "version": 147, + "versionNonce": 1318941090, + "isDeleted": false, + "boundElementIds": [ + "nZLYBAq4RbPpSuph85TVa" + ], + "text": "erl_ast\nAbstract\nForms AST", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 64 + }, + { + "id": "YaRpYMvi_QGRQcMTCSxlg", + "type": "rectangle", + "x": 534.4609375, + "y": 113.41015625, + "width": 438.83203125, + "height": 356.88671875, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1425815422, + "version": 97, + "versionNonce": 1698641214, + "isDeleted": false, + "boundElementIds": null + }, + { + "id": "1nWOMRQhOzP2nY2pmIlSx", + "type": "text", + "x": 737.296875, + "y": 140.2890625, + "width": 38, + "height": 23, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1035517822, + "version": 15, + "versionNonce": 2036876706, + "isDeleted": false, + "boundElementIds": null, + "text": "ELP", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 18 + }, + { + "id": "LlFq5ExrJvXRvMND_JJyJ", + "type": "ellipse", + "x": 761.51171875, + "y": 551.8984375, + "width": 75.8515625, + "height": 71.9140625, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1678269950, + "version": 112, + "versionNonce": 1684135294, + "isDeleted": false, + "boundElementIds": [ + "17PMh8sbHjd7S-JWBnFE1" + ] + }, + { + "id": "6NUxWq6CV-jttksJFQtwX", + "type": "text", + "x": 771.4375, + "y": 576.35546875, + "width": 56, + "height": 23, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 2035050850, + "version": 12, + "versionNonce": 1901281634, + "isDeleted": false, + "boundElementIds": null, + "text": ".beam", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "baseline": 18 + }, + { + "id": "9DK0ccPYvHUk0TsXmjOrq", + "type": "ellipse", + "x": 882.0390625, + "y": 552.0078125, + "width": 69.28125, + "height": 66.73046875, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 720570046, + "version": 159, + "versionNonce": 1098714110, + "isDeleted": false, + "boundElementIds": [ + "bSnsBCshBNg3DGnlZwxbN" + ] + }, + { + "id": "x3CfGOD04GeP0BNAyM2M0", + "type": "text", + "x": 905.3203125, + "y": 575.818359375, + "width": 28, + "height": 23, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 934617890, + "version": 86, + "versionNonce": 1911080226, + "isDeleted": false, + "boundElementIds": null, + "text": ".etf", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "baseline": 18 + }, + { + "id": "xqyPsR-QIucrWe2OnVNYS", + "type": "line", + "x": 462.98046875, + "y": 183.7734375, + "width": 85.90234375, + "height": 1.93359375, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 448823358, + "version": 175, + "versionNonce": 298522110, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + 85.90234375, + -1.93359375 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null + }, + { + "id": "UGEUEE0fA8Tdz2BP3Ztgc", + "type": "arrow", + "x": 826.8950518156423, + "y": 247.69335937500006, + "width": 1.2654719762948616, + "height": 65.31445312499994, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 705961314, + "version": 247, + "versionNonce": 1720815458, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + -1.2654719762948616, + 65.31445312499994 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "hffLonB6LpYajyggAONDl", + "focus": -0.042953912560249385, + "gap": 10.015625 + }, + "endBinding": { + "elementId": "0vz1zW85ykDvLcE2k28zd", + "focus": -0.05701454124292956, + "gap": 1 + } + }, + { + "id": "CHQMH5aSHpe9JlQflH6pQ", + "type": "arrow", + "x": 776.2321267183561, + "y": 312.125, + "width": 0.40448510947703653, + "height": 39.55078124999994, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 834975614, + "version": 252, + "versionNonce": 663919970, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + 0.40448510947703653, + -39.55078124999994 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "0vz1zW85ykDvLcE2k28zd", + "focus": -0.8134007426603844, + "gap": 1.8828125 + }, + "endBinding": { + "elementId": "SPnEYfaNF4n8lmvBE-9af", + "focus": 0.8289665971882204, + "gap": 2.25 + } + }, + { + "id": "17PMh8sbHjd7S-JWBnFE1", + "type": "arrow", + "x": 794.59765625, + "y": 547.65625, + "width": 0.28125, + "height": 133.2890625, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 2053070882, + "version": 78, + "versionNonce": 664809086, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + -0.28125, + -133.2890625 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "LlFq5ExrJvXRvMND_JJyJ", + "focus": -0.125376740756825, + "gap": 4.506191267322009 + }, + "endBinding": { + "elementId": "0vz1zW85ykDvLcE2k28zd", + "focus": 0.5308105238971549, + "gap": 1 + } + }, + { + "id": "nZLYBAq4RbPpSuph85TVa", + "type": "arrow", + "x": 862.90234375, + "y": 403.73046875, + "width": 39.54296875, + "height": 153.47265625, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 2092270050, + "version": 91, + "versionNonce": 673532222, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + 39.54296875, + 153.47265625 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "3Q3QCrQS64pVj-BoUQ5nW", + "focus": -0.4299991364591939, + "gap": 3.7734375 + }, + "endBinding": null + }, + { + "id": "RaNH5kI_GUf-EORAca5d_", + "type": "rectangle", + "x": 1047.6875, + "y": 119.7890625, + "width": 368.62109375, + "height": 361.82421875, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1072179326, + "version": 136, + "versionNonce": 986820286, + "isDeleted": false, + "boundElementIds": null + }, + { + "id": "zwd2F1tAWgMiiw90BbYeX", + "type": "text", + "x": 1142, + "y": 134, + "width": 141, + "height": 23, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 816851390, + "version": 55, + "versionNonce": 2070332414, + "isDeleted": false, + "boundElementIds": null, + "text": "ELP consumers", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 18 + }, + { + "id": "WkGfTHkIjA9JYTwgdQjN3", + "type": "rectangle", + "x": 1078.0703125, + "y": 310.28515625, + "width": 131.24999999999991, + "height": 61.019531249999986, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 2005587362, + "version": 252, + "versionNonce": 773950434, + "isDeleted": false, + "boundElementIds": [ + "PbNk2KtaR-iQNbB4_HWyr", + "uk2HBcfv1VGz2PUVYz5vi" + ] + }, + { + "id": "gOMk1Dod4sNbkvsEdfpLr", + "type": "text", + "x": 1099, + "y": 320, + "width": 93, + "height": 23, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 106496062, + "version": 17, + "versionNonce": 257904610, + "isDeleted": false, + "boundElementIds": null, + "text": "EqWAlizer", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 18 + }, + { + "id": "6BLqDTJq5APRxb1_AsE8e", + "type": "line", + "x": 1099.9453125, + "y": 306.19140625, + "width": 121.35546875, + "height": 58.0703125, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 2026914594, + "version": 193, + "versionNonce": 1081017250, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + 9.18359375, + -14.00390625 + ], + [ + 121.35546875, + -12.890625 + ], + [ + 117.39453125, + 44.06640625 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null + }, + { + "id": "7gUjVnXmCqwj_zdh22V8p", + "type": "line", + "x": 1126.13671875, + "y": 285.6640625, + "width": 106.03515625, + "height": 69.34375, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 135598974, + "version": 284, + "versionNonce": 78511998, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + 0.578125, + -15.1640625 + ], + [ + 106.03515625, + -12.328125 + ], + [ + 104.88671875, + 54.1796875 + ], + [ + 92.59375, + 50.54296875 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null + }, + { + "id": "PbNk2KtaR-iQNbB4_HWyr", + "type": "arrow", + "x": 948.37109375, + "y": 585.58984375, + "width": 155.26953125, + "height": 209.3515625, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 1772632802, + "version": 245, + "versionNonce": 1071293118, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + 143.5703125, + -128.203125 + ], + [ + 155.26953125, + -209.3515625 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": { + "elementId": "WkGfTHkIjA9JYTwgdQjN3", + "focus": 0.49904306010987975, + "gap": 4.93359375 + } + }, + { + "id": "l9cLpo-xLMM3TgE3ycHnt", + "type": "rectangle", + "x": 342.03515625, + "y": 507.98046875, + "width": 138.1015625, + "height": 125.6484375, + "angle": 0, + "strokeColor": "#364fc7", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 2068024126, + "version": 61, + "versionNonce": 1279354914, + "isDeleted": false, + "boundElementIds": null + }, + { + "id": "0yjcEWK6fDb3NQ6xsL4aN", + "type": "text", + "x": 380, + "y": 538, + "width": 38, + "height": 46, + "angle": 0, + "strokeColor": "#364fc7", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 31724514, + "version": 14, + "versionNonce": 56930302, + "isDeleted": false, + "boundElementIds": null, + "text": "ELP\nCLI", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 41 + }, + { + "id": "SbhFMPyoD-FVbieXzhvCc", + "type": "line", + "x": 410.125, + "y": 506.14453125, + "width": 136.24609375, + "height": 295.6015625, + "angle": 0, + "strokeColor": "#364fc7", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 521569342, + "version": 96, + "versionNonce": 598804194, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + 9.4609375, + -216.75390625 + ], + [ + 136.24609375, + -295.6015625 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null + }, + { + "id": "FvN1rBcQ9ZzQfZkhRlVY1", + "type": "rectangle", + "x": 549.1953125, + "y": 600.97265625, + "width": 119.17578125, + "height": 107.31640625, + "angle": 0, + "strokeColor": "#364fc7", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1492554174, + "version": 71, + "versionNonce": 621431422, + "isDeleted": false, + "boundElementIds": null + }, + { + "id": "TxYhh2fh3nui0l30t7MSE", + "type": "text", + "x": 579.783203125, + "y": 643.130859375, + "width": 58, + "height": 23, + "angle": 0, + "strokeColor": "#364fc7", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 804603938, + "version": 10, + "versionNonce": 1226560510, + "isDeleted": false, + "boundElementIds": null, + "text": "Erlang", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "baseline": 18 + }, + { + "id": "Uc7e2Zg3AJCyxxNhhlKOv", + "type": "arrow", + "x": 668.3515625, + "y": 688.9296875, + "width": 231.16015625, + "height": 74.92578125, + "angle": 0, + "strokeColor": "#364fc7", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 1220322366, + "version": 129, + "versionNonce": 2093986878, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + 172.40625, + -0.21484375 + ], + [ + 231.16015625, + -74.92578125 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null + }, + { + "id": "SbRzLoI8wtLiPahARKfdN", + "type": "line", + "x": 460.70703125, + "y": 636.62109375, + "width": 90.34375, + "height": 39.88671875, + "angle": 0, + "strokeColor": "#364fc7", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 1401584766, + "version": 84, + "versionNonce": 1704373282, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + 16.85546875, + 34.796875 + ], + [ + 90.34375, + 39.88671875 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null + }, + { + "id": "uk2HBcfv1VGz2PUVYz5vi", + "type": "arrow", + "x": 886.6015625, + "y": 233.11328125, + "width": 179.10546875, + "height": 92.734375, + "angle": 0, + "strokeColor": "#0b7285", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 103633698, + "version": 152, + "versionNonce": 1402546942, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + 124.6640625, + 36.42578125 + ], + [ + 179.10546875, + 92.734375 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "hffLonB6LpYajyggAONDl", + "focus": 0.0150151060893416, + "gap": 13.19140625 + }, + "endBinding": { + "elementId": "WkGfTHkIjA9JYTwgdQjN3", + "focus": -0.6679417411720955, + "gap": 12.36328125 + } + }, + { + "id": "BGBNQKDQSF43qcIBXGnQD", + "type": "text", + "x": 920.75390625, + "y": 212.92578125, + "width": 107, + "height": 23, + "angle": 0, + "strokeColor": "#0b7285", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 758479714, + "version": 146, + "versionNonce": 2862270, + "isDeleted": false, + "boundElementIds": null, + "text": "future (H2+)", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 18 + }, + { + "id": "nslQpVkhlBIEynRdaM7i5", + "type": "text", + "x": 730.74609375, + "y": 665.97265625, + "width": 90, + "height": 23, + "angle": 0, + "strokeColor": "#364fc7", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1379494270, + "version": 40, + "versionNonce": 209039458, + "isDeleted": false, + "boundElementIds": null, + "text": "initial (H1)", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 18 + }, + { + "id": "KwANdqSR3xrZh1QuwSMuL", + "type": "arrow", + "x": 659.140625, + "y": 197.0546875, + "width": 95.34765625, + "height": 11.49609375, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 2052696510, + "version": 115, + "versionNonce": 153255998, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + 95.34765625, + 11.49609375 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "Avd7Hg3qOHiR6JOB1VVQx", + "focus": -0.032161135826232236, + "gap": 1.7421875 + }, + "endBinding": { + "elementId": "SPnEYfaNF4n8lmvBE-9af", + "focus": 0.03030109653627345, + "gap": 13.33203125 + } + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "gridSize": null + } +} \ No newline at end of file diff --git a/docs/ELP-parser-dataflow.png b/docs/ELP-parser-dataflow.png new file mode 100644 index 0000000000000000000000000000000000000000..e6246ad96c244cd876e291b16d27ce877bc3090e GIT binary patch literal 263768 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 z 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 Q1^o9+O!i5ki2lp}1N + + + + + + IDEtreesitterelp_syntaxASTerl_astAbstractForms ASTELP.beam.etfELP consumersEqWAlizerELPCLIErlangfuture (H2+)initial (H1) \ No newline at end of file diff --git a/docs/PROJECT_LOADING.md b/docs/PROJECT_LOADING.md new file mode 100644 index 0000000000..d1cb8f6318 --- /dev/null +++ b/docs/PROJECT_LOADING.md @@ -0,0 +1,137 @@ +# Investigating how the initial startup process happens in ELP LSP server + +## Notes on server initialisation + +### Initialize + +It starts in `elp::bin::main::run_server`. + +This calls `ServerSetup.to_server()`, which initiates the LSP +`initialize` process. + +`ServerSetup::initialize()` prepares a response for the client with +the server capabilities and other info. It calls +`Connection::initialize_finish()` to send this to the client and wait +for the returned `initialized` notification. This allows the client to +start sending request messages to ELP + +It then sets up a default configuration, and initiates project +discovery on the workspace roots provided in the initialization +message from the client. This calls +`ProjectManifest::discover_buck_all` on the workspace roots, and if +that fails falls back to rebar3 with `ProjectManifest::discover_all`. +These are intended to be quick queries to identify that a project of +the given type exists. + +The `Vec` is stored in +`config.discovered_projects` and returned from +`ServerSetup::initialize()`. + +When that ends it sets up the logger and calls +`server::setup::setup_server` with the config and logger. This creates +a vfs loader and a task pool, and returns the result of +`Server::new()`, which populates the `Server` data structure. + +Part of the `Server` state setup is to set `Server::status` to +`Status::Initialising`. + +It then enters `Server::main_loop()`. + +### Main Loop Startup + +The fetch projects operation request is signalled by a call to +`fetch_projects_request`, which signals to the +`Server::fetch_projects_queue` that there is work to be done. + +This is immediately followed by `self.fetch_projects_if_needed()`. + +If the `Server::fetch_projects_queue` flag is set (which it is, the +prior line call did it), spawn a task in the task pool to call +`project_model::Project::load` on all of them. This does the initial +project load process. For buck2, this does a series of queries to buck +to populate the `Project` structure, and similarly for legacy rebar3 +projects, using the rebar `project_info` plugin. + +When done, the task spawned by `fetch_projects_if_needed` sends +`Task::FetchProjects(projects)` to the main loop. + +Once the loader task is spawned, the server main loop message pump +starts running, for the life of the server. + +**So at this point the main loop is live, and the client is sending us +requests.** + +**BUT WE HAVE NOT YET CONFIGURED OUR PROJECTS, AS WE ARE WAITING FOR +THE PROJECTS TO BE FETCHED** + +### Main Loop Normal Operation + +On entry the server is still in `Status::Initialising`. + +The main loop message pump receives messages from the async tasks, vfs +loader, LSP client, and sub lsp, as well as a progress reporter. + +It dispatches according to the message type, and then processes any +changes generated. + +If an LSP request message comes in, + +- if the status is `Status::Initialising` or `Status::Loading` it is + forwarded to the sub lsp without being processed by ELP. +- if the status is `Status::ShuttingDown` an error response is + returned. +- otherwise it is processed by ELP. + +#### Project Fetching Completes + +Eventually the project fetching completes, and injects a message into the +main event loop, handled as + +```rust +Task::FetchProjects(projects) => self.fetch_projects_completed(projects)?, +``` + +`fetch_projects_completed` calls `switch_workspaces()`. + +This is the place where the all the ELP project information is put +into place, except source roots. This includes calling salsa +`set_app_data` and `set_project_data` for all discovered projects. + +The final step is to activate the vfs file loader, according to a +config generated from the project info. + +#### VFS File Loading + +The Vfs file loader is an asynchronous task, and it sends messages as +it works, one to indicate a file is loaded, and one to indicate +progress on loading the initial watch list. It seems that the +progress message is always sent after the loaded message for a given +file. + +The Vfs loaded message is dispatched to `Server.on_loader_loaded`. + +This calls `vfs.set_file_contents(path, contents);` for each loaded +file, which causes a change event to be stored in Vfs. + +Peridically Vfs sends a message handled by +`Server.on_loader_progress()`. This is used to initially set +`Status::Loading`, and when notification of the last file loaded +arrives, it transitions to `Status::Running`. At the same time, it +schedules compilation of the dependencies required by Eqwalizer via +the `erlang_service`, by calling `self.schedule_compile_deps()`. + +The normal main loop processing after handling the message picks up +the change, and at the end of `Server.process_changes` sets the source +roots for the given file. + +So the process of receiving notification that a file is loaded, +setting its vfs contents and source root is atomic in terms of main +loop message handling. + +### Places where server state changes + +`Status::Initialising`: set in server startup +`Status::Loading`: set when the initial `on_loader_progress` message is processed. +`Status::Running`: set when the vfs loader has finished loading. + THIS TRANSITION IS THE PROBLEMATIC ONE +`Status::ShuttingDown`: set if a shutdown request comes from the LSP client diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000000..3efd291216 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,20 @@ +# Documentation about ELP + +This currently just has a doodle about some possible internal +organisation of ELP and related components. + +## Updating the excalidraw diagrams + +Use https://excalidraw.thefacebook.com/ +Load the file, then export as svg or png. + +## Working on Markdown + +I find it useful to install the [Markdown Preview +Enhanced](https://marketplace.visualstudio.com/items?itemName=shd101wyy.markdown-preview-enhanced) +extension in VsCode @ FB, enable preview on the file (CMD-shift-v), +and leave it in the background while editing in emacs. It updates live +when you save. + +One thing it does not do is reflow text, but that is fine while +working on a draft. diff --git a/docs/crate_graph.dot b/docs/crate_graph.dot new file mode 100644 index 0000000000..fec6391150 --- /dev/null +++ b/docs/crate_graph.dot @@ -0,0 +1,63 @@ +digraph { + 0 [ label = "eetf" ] + 1 [ label = "elp" ] + 2 [ label = "elp_base_db" ] + 3 [ label = "elp_eqwalizer" ] + 4 [ label = "elp_hir" ] + 5 [ label = "elp_hir_def" ] + 6 [ label = "elp_hir_expand" ] + 7 [ label = "elp_ide" ] + 8 [ label = "elp_ide_assists" ] + 9 [ label = "elp_ide_db" ] + 10 [ label = "elp_log" ] + 11 [ label = "elp_erlang_service" ] + 12 [ label = "elp_project_model" ] + 13 [ label = "elp_syntax" ] + 14 [ label = "erl_ast" ] + 15 [ label = "text-size" ] + 16 [ label = "tree-sitter" ] + 17 [ label = "tree-sitter-erlang" ] + 1 -> 7 [ ] + 1 -> 10 [ ] + 1 -> 12 [ ] + 2 -> 0 [ ] + 2 -> 12 [ ] + 2 -> 13 [ ] + 3 -> 13 [ ] + 4 -> 2 [ ] + 4 -> 5 [ ] + 4 -> 6 [ ] + 4 -> 12 [ ] + 4 -> 13 [ ] + 5 -> 2 [ ] + 5 -> 6 [ ] + 5 -> 12 [ ] + 5 -> 13 [ ] + 6 -> 2 [ ] + 6 -> 12 [ ] + 6 -> 13 [ ] + 7 -> 0 [ ] + 7 -> 4 [ ] + 7 -> 8 [ ] + 7 -> 9 [ ] + 7 -> 12 [ ] + 7 -> 13 [ ] + 8 -> 9 [ ] + 8 -> 13 [ ] + 9 -> 0 [ ] + 9 -> 2 [ ] + 9 -> 3 [ ] + 9 -> 4 [ ] + 9 -> 11 [ ] + 9 -> 12 [ ] + 9 -> 13 [ ] + 11 -> 0 [ ] + 11 -> 15 [ ] + 12 -> 0 [ ] + 13 -> 0 [ ] + 13 -> 14 [ ] + 13 -> 16 [ ] + 13 -> 17 [ ] + 14 -> 0 [ ] + 17 -> 16 [ ] +} diff --git a/docs/crate_graph.png b/docs/crate_graph.png new file mode 100644 index 0000000000000000000000000000000000000000..7cea3b8d4178227082abf857b2c50930e1317a8a GIT binary patch literal 237633 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%0D1$&+>o$7!u|5B*>hGDVCN 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ⅈ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 KDo@%h+4_GngyGi! literal 0 HcmV?d00001 diff --git a/docs/databases.dot b/docs/databases.dot new file mode 100644 index 0000000000..6069e21d06 --- /dev/null +++ b/docs/databases.dot @@ -0,0 +1,80 @@ +// Convert to PNG by +// dot -Tpng databases.dot -o databases.png + +digraph { + +rankdir = "TB"; + +{ + rank=same; ExpandDatabase; InternDatabase +} +{ + rank=same; HirDatabase; EqwalizerDatabase +} +{ + rank=same; SourceDatabase; LineIndexDatabase +} +{ + rank=max; FileLoader; "salsa::Database" +} + + // Inputs + SourceDatabase [label="SourceDatabase\nbase_db"; style=filled;] + SourceDatabaseExt [label="SourceDatabaseExt\nbase_db"; style=filled;] + + // Using the erlang-based parser + EqwalizerDatabase [label="EqwalizerDatabase\nide_db"; style=filled; color="brown1"] + EqwalizerLoader [label="EqwalizerLoader\nide_db"; style=filled; color="brown1"] + ErlAstDatabase [label="ErlAstDatabase\nide_db"; style=filled; color="brown1"] + AstLoader [label="AstLoader\nerl_ast"; style=filled; color="brown1"] + + // Main external API exposure + HirDatabase [label="HirDatabase\nhir"; style=filled; color="lightblue"] + // Internal to HIR, but exposed via HirDatabase + ExpandDatabase [label="ExpandDatabase\nhir_expand"; style=filled; color="lightblue"] + DefDatabase [label="DefDatabase\nhir_def"; style=filled; color="lightblue"] + + InternDatabase [label="InternDatabase\nhir_def"] + + // Utility and low level + FileLoader [label="FileLoader\nbase_db"] + LineIndexDatabase [label="LineIndexDatabase\nide_db"] + "salsa::Database" + + //------------------------------------------------------------------ + + // pub trait SourceDatabase: FileLoader + salsa::Database { + SourceDatabase -> FileLoader + SourceDatabase -> "salsa::Database" + + // pub trait SourceDatabaseExt: SourceDatabase { + SourceDatabaseExt -> SourceDatabase + + // pub trait EqwalizerDatabase: SourceDatabase + EqwalizerLoader + ErlAstDatabase { + EqwalizerDatabase -> SourceDatabase + EqwalizerDatabase -> EqwalizerLoader + EqwalizerDatabase -> ErlAstDatabase + + // pub trait LineIndexDatabase: SourceDatabase { + LineIndexDatabase -> SourceDatabase + + // pub trait ErlAstDatabase: SourceDatabase + AstLoader { + ErlAstDatabase -> SourceDatabase + ErlAstDatabase -> AstLoader + + // pub trait HirDatabase: DefDatabase + Upcast {} + HirDatabase -> DefDatabase + // HirDatabase -> "Upcast" + + // pub trait ExpandDatabase: SourceDatabase { + ExpandDatabase -> SourceDatabase + + // pub trait InternDatabase: SourceDatabase { + InternDatabase -> SourceDatabase + + // pub trait DefDatabase: InternDatabase + ExpandDatabase + Upcast { + DefDatabase -> InternDatabase + DefDatabase -> ExpandDatabase + // DefDatabase -> "Upcast" + +} diff --git a/docs/databases.png b/docs/databases.png new file mode 100644 index 0000000000000000000000000000000000000000..95b4e7ff334dbc1943731a3629cbed852388e63e GIT binary patch literal 107227 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�lloVnQOizjkNoq8oLP320aG;*?avl&u-b2R* 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 literal 0 HcmV?d00001 diff --git a/docs/images/code-action-add-edoc.png b/docs/images/code-action-add-edoc.png new file mode 100644 index 0000000000000000000000000000000000000000..cbaf0a56dba8f80ad604408e1d923ffb6d9f9156 GIT binary patch literal 174990 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