├── .appends └── .github │ └── labels.yml ├── .credo.exs ├── .dockerignore ├── .formatter.exs ├── .github ├── CODEOWNERS ├── dependabot.yml ├── labels.yml └── workflows │ ├── deploy.yml │ ├── elixir_test.yml │ ├── elixir_test_external.yml │ ├── ping-cross-track-maintainers-team.yml │ └── sync-labels.yml ├── .gitignore ├── .gitmodules ├── .tool-versions ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── bin ├── build.sh ├── check_files.sh ├── run-tests-in-docker.sh ├── run.sh └── smoke_test.sh ├── config ├── config.exs ├── dev.exs ├── prod.exs └── test.exs ├── docs ├── recipes.md ├── step-01 │ └── step-01.md ├── step-02 │ ├── 02-example-analysis.ex │ ├── 02-example-module.ex │ └── step-02.md ├── step-03 │ ├── 03-example-analysis.ex │ ├── 03-example-module.ex │ └── step-03.md ├── step-04 │ ├── 04-example-analysis-test.exs │ └── step-04.md └── writing-an-analyzer.md ├── lib ├── elixir_analyzer.ex └── elixir_analyzer │ ├── cli.ex │ ├── comment.ex │ ├── constants.ex │ ├── exercise_test.ex │ ├── exercise_test │ ├── assert_call.ex │ ├── assert_call │ │ ├── compiler.ex │ │ └── syntax_error.ex │ ├── check_source.ex │ ├── check_source │ │ └── compiler.ex │ ├── common_checks.ex │ ├── common_checks │ │ ├── boolean_functions.ex │ │ ├── comments.ex │ │ ├── compiler_warnings.ex │ │ ├── debug_functions.ex │ │ ├── deprecated_random_module.ex │ │ ├── exemplar_comparison.ex │ │ ├── function_annotation_order.ex │ │ ├── function_capture.ex │ │ ├── function_names.ex │ │ ├── indentation.ex │ │ ├── last_line_assignment.ex │ │ ├── list_prepend_head.ex │ │ ├── module_attribute_names.ex │ │ ├── module_pascal_case.ex │ │ ├── no_rescue.ex │ │ ├── private_helper_functions.ex │ │ ├── uncommon_errors.ex │ │ ├── unless_with_else.ex │ │ └── variable_names.ex │ ├── feature.ex │ └── feature │ │ ├── compiler.ex │ │ └── feature_error.ex │ ├── log_formatter.ex │ ├── quote_util.ex │ ├── source.ex │ ├── submission.ex │ ├── summary.ex │ └── test_suite │ ├── accumulate.ex │ ├── basketball_website.ex │ ├── bird_count.ex │ ├── boutique_inventory.ex │ ├── boutique_suggestions.ex │ ├── captains_log.ex │ ├── chessboard.ex │ ├── community_garden.ex │ ├── dancing_dots.ex │ ├── default.ex │ ├── dna_encoding.ex │ ├── file_sniffer.ex │ ├── freelancer_rates.ex │ ├── german_sysadmin.ex │ ├── guessing_game.ex │ ├── high_school_sweetheart.ex │ ├── high_score.ex │ ├── language_list.ex │ ├── lasagna.ex │ ├── leap.ex │ ├── library_fees.ex │ ├── list_ops.ex │ ├── log_level.ex │ ├── name_badge.ex │ ├── need_for_speed.ex │ ├── new_passport.ex │ ├── newsletter.ex │ ├── pacman_rules.ex │ ├── remote_control_car.ex │ ├── rpg_character_sheet.ex │ ├── rpn_calculator.ex │ ├── rpn_calculator_inspection.ex │ ├── rpn_calculator_output.ex │ ├── sieve.ex │ ├── square_root.ex │ ├── strain.ex │ ├── take_a_number.ex │ ├── take_a_number_deluxe.ex │ ├── top_secret.ex │ ├── two_fer.ex │ └── wine_cellar.ex ├── mix.exs ├── mix.lock ├── priv └── plts │ └── .keep ├── test ├── elixir_analyzer │ ├── cli_test.exs │ ├── comment_test.exs │ ├── constants_test.exs │ ├── exercise_test │ │ ├── assert_call │ │ │ ├── erlang_modules_test.exs │ │ │ ├── function_head_call_test.exs │ │ │ ├── function_parentheses_test.exs │ │ │ ├── indirect_call_test.exs │ │ │ ├── kernel_functions_test.exs │ │ │ ├── module_level_call_test.exs │ │ │ ├── module_tracking_test.exs │ │ │ └── multiple_clause_functions_test.exs │ │ ├── assert_call_test.exs │ │ ├── assert_no_call_test.exs │ │ ├── check_source_test.exs │ │ ├── comment_order_test.exs │ │ ├── common_checks │ │ │ ├── boolean_functions_test.exs │ │ │ ├── comments_test.exs │ │ │ ├── compiler_warnings_test.exs │ │ │ ├── function_annotation_order_test.exs │ │ │ ├── function_capture_test.exs │ │ │ ├── function_names_test.exs │ │ │ ├── indentation_test.exs │ │ │ ├── last_line_assignment_test.exs │ │ │ ├── list_prepend_head_test.exs │ │ │ ├── module_attribute_names_test.exs │ │ │ ├── module_pascal_case_test.exs │ │ │ ├── no_rescue_test.exs │ │ │ ├── private_helper_functions_test.exs │ │ │ ├── uncommon_errors_test.exs │ │ │ ├── unless_with_else.exs │ │ │ └── variable_names_test.exs │ │ ├── common_checks_test.exs │ │ ├── feature │ │ │ ├── block_ends_with_test.exs │ │ │ ├── block_includes_test.exs │ │ │ ├── duplicate_features_test.exs │ │ │ ├── pipes_test.exs │ │ │ └── string_interpolation_test.exs │ │ ├── feature_test.exs │ │ └── suppress_if_test.exs │ ├── exercise_test_test.exs │ ├── log_formatter_test.exs │ └── test_suite │ │ ├── accumulate_test.exs │ │ ├── basketball_website_test.exs │ │ ├── bird_count_test.exs │ │ ├── boutique_inventory_test.exs │ │ ├── boutique_suggestions_test.exs │ │ ├── captains_log_test.exs │ │ ├── chessboard_test.exs │ │ ├── community_garden_test.exs │ │ ├── dancing_dots_test.exs │ │ ├── dna_encoding_test.exs │ │ ├── file_sniffer_test.exs │ │ ├── freelancer_rates_test.exs │ │ ├── german_sysadmin_test.exs │ │ ├── guessing_game_test.exs │ │ ├── high_school_sweetheart_test.exs │ │ ├── high_score_test.exs │ │ ├── language_list_test.exs │ │ ├── lasagna_test.exs │ │ ├── leap_test.exs │ │ ├── library_fees_test.exs │ │ ├── list_ops_test.exs │ │ ├── log_level_test.exs │ │ ├── name_badge_test.exs │ │ ├── need_for_speed_test.exs │ │ ├── new_passport_test.exs │ │ ├── newsletter_test.exs │ │ ├── pacman_rules_test.exs │ │ ├── remote_control_car_test.exs │ │ ├── rpg_character_sheet_test.exs │ │ ├── rpn_calculator_inspection_test.exs │ │ ├── rpn_calculator_output_test.exs │ │ ├── sieve_test.exs │ │ ├── square_root_test.exs │ │ ├── strain_test.exs │ │ ├── take_a_number_deluxe_test.exs │ │ ├── take_a_number_test.exs │ │ ├── top_secret_test.exs │ │ ├── two_fer_test.exs │ │ └── wine_cellar_test.exs ├── elixir_analyzer_test.exs ├── support │ ├── analyzer_verification │ │ ├── assert_call.ex │ │ ├── assert_call │ │ │ ├── erlang_modules.ex │ │ │ ├── function_head_call.ex │ │ │ ├── function_parentheses.ex │ │ │ ├── indirect_call.ex │ │ │ ├── kernel_functions.ex │ │ │ ├── module_level_call.ex │ │ │ ├── module_tracking.ex │ │ │ └── multiple_clause_functions.ex │ │ ├── assert_no_call.ex │ │ ├── check_source.ex │ │ ├── comment_order.ex │ │ ├── feature.ex │ │ ├── feature │ │ │ ├── block_ends_with.ex │ │ │ ├── block_includes.ex │ │ │ ├── pipes.ex │ │ │ └── string_interpolation.ex │ │ └── suppress_if.ex │ ├── constants.ex │ └── exercise_test_case.ex └── test_helper.exs └── test_data ├── clock └── perfect_solution │ ├── .meta │ └── config.json │ ├── expected_analysis.json │ └── lib │ └── clock.ex ├── dancing-dots └── split_solution │ ├── .meta │ ├── config.json │ └── exemplar │ │ ├── animation.ex │ │ ├── flicker.ex │ │ └── zoom.ex │ ├── expected_analysis.json │ └── lib │ └── dancing_dots │ ├── animation.ex │ ├── dot.ex │ ├── dot_group.ex │ ├── flicker.ex │ └── zoom.ex ├── lasagna ├── deprecated_modules │ ├── .formatter.exs │ ├── .gitignore │ ├── .meta │ │ ├── config.json │ │ └── exemplar.ex │ ├── expected_analysis.json │ ├── lib │ │ └── lasagna.ex │ ├── mix.exs │ └── test │ │ ├── lasagna_test.exs │ │ └── test_helper.exs ├── failing_solution │ ├── .formatter.exs │ ├── .gitignore │ ├── .meta │ │ ├── config.json │ │ └── exemplar.ex │ ├── expected_analysis.json │ ├── lib │ │ └── lasagna.ex │ ├── mix.exs │ └── test │ │ ├── lasagna_test.exs │ │ └── test_helper.exs ├── missing_config │ └── expected_analysis.json ├── missing_exemplar │ ├── .formatter.exs │ ├── .gitignore │ ├── .meta │ │ └── config.json │ ├── expected_analysis.json │ ├── lib │ │ └── lasagna.ex │ ├── mix.exs │ └── test │ │ ├── lasagna_test.exs │ │ └── test_helper.exs ├── perfect_solution │ ├── .formatter.exs │ ├── .gitignore │ ├── .meta │ │ ├── config.json │ │ └── exemplar.ex │ ├── expected_analysis.json │ ├── lib │ │ └── lasagna.ex │ ├── mix.exs │ └── test │ │ ├── lasagna_test.exs │ │ └── test_helper.exs ├── wrong_config │ ├── .meta │ │ └── config.json │ └── expected_analysis.json ├── wrong_config2 │ ├── .meta │ │ └── config.json │ └── expected_analysis.json └── wrong_exemplar │ ├── .formatter.exs │ ├── .gitignore │ ├── .meta │ ├── config.json │ └── exemplar.ex │ ├── expected_analysis.json │ ├── lib │ └── lasagna.ex │ ├── mix.exs │ └── test │ ├── lasagna_test.exs │ └── test_helper.exs ├── square-root └── split_solution │ ├── .meta │ └── config.json │ ├── expected_analysis.json │ └── lib │ ├── square_root.ex │ └── square_root │ └── cheating.ex ├── take-a-number-deluxe └── perfect_solution │ ├── .meta │ └── config.json │ ├── expected_analysis.json │ └── lib │ ├── take_a_number_deluxe.ex │ └── take_a_number_deluxe │ ├── queue.ex │ └── state.ex └── two_fer ├── error_solution ├── .meta │ ├── config.json │ └── example.ex ├── README.md ├── expected_analysis.json ├── lib │ └── two_fer.ex └── test │ ├── test.exs │ └── two_fer_test.exs ├── imperfect_solution ├── .meta │ ├── config.json │ └── example.ex ├── README.md ├── expected_analysis.json ├── lib │ └── two_fer.ex └── test │ ├── test.exs │ └── two_fer_test.exs ├── informative_comments ├── .meta │ ├── config.json │ └── example.ex ├── expected_analysis.json └── lib │ └── two_fer.ex ├── missing_example_solution ├── .meta │ └── config.json ├── expected_analysis.json ├── lib │ └── two_fer.ex └── test │ └── two_fer_test.exs ├── missing_file_solution ├── .meta │ ├── config.json │ └── example.ex ├── README.md ├── expected_analysis.json └── test │ ├── test.exs │ └── two_fer_test.exs └── perfect_solution ├── .meta ├── config.json └── example.ex ├── README.md ├── expected_analysis.json ├── lib └── two_fer.ex └── test ├── test.exs └── two_fer_test.exs /.appends/.github/labels.yml: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------------------- # 2 | # These are the repository-specific labels that augment the Exercise-wide labels defined in # 3 | # https://github.com/exercism/org-wide-files/blob/main/global-files/.github/labels.yml. # 4 | # ----------------------------------------------------------------------------------------- # 5 | 6 | - name: "analyzer interface ⚙️" 7 | description: "" 8 | color: "cbe5f2" 9 | 10 | - name: "bug 🐛" 11 | description: "" 12 | color: "8e2c2c" 13 | 14 | - name: "dependencies" 15 | description: "Pull requests that update a dependency file" 16 | color: "0366d6" 17 | 18 | - name: "discussion 💬" 19 | description: "" 20 | color: "23296d" 21 | 22 | - name: "documentation 📖" 23 | description: "Improvements or additions to documentation" 24 | color: "23296d" 25 | 26 | - name: "duplicate" 27 | description: "This issue or pull request already exists" 28 | color: "cccccc" 29 | 30 | - name: "enhancement ⭐️" 31 | description: "A nice-to-have" 32 | color: "c3dcf7" 33 | 34 | - name: "good first issue 🐥" 35 | description: "" 36 | color: "C2E0C6" 37 | 38 | - name: "help wanted 🤝" 39 | description: "" 40 | color: "ccd4fc" 41 | 42 | - name: "in progress 🚧" 43 | description: "Already being worked on" 44 | color: "FEF6EE" 45 | 46 | - name: "invalid" 47 | description: "This doesn't seem right" 48 | color: "fce0e2" 49 | 50 | - name: "question ❔" 51 | description: "" 52 | color: "23296d" 53 | 54 | - name: "refactoring 💅" 55 | description: "" 56 | color: "efcbf2" 57 | 58 | - name: "waiting ⏳" 59 | description: "Waiting for something else to happen first" 60 | color: "FEF6EE" 61 | 62 | - name: "won't fix ⛔️" 63 | description: "" 64 | color: "fce0e2" 65 | 66 | - name: "hacktoberfest" 67 | description: "Advertizing the issue as suitable for hacktoberfest" 68 | color: "a6692d" 69 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | deps 3 | _build 4 | bin/elixir_analyzer 5 | elixir_analyzer 6 | .appends 7 | .github 8 | .gitignore 9 | .gitattributes 10 | .dockerignore 11 | Dockerfile 12 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | locals_without_parens: [ 5 | # Elixir Analyzer 6 | status: :*, 7 | find: :*, 8 | on_fail: :*, 9 | comment: :*, 10 | form: :*, 11 | suppress_if: :*, 12 | depth: :*, 13 | calling_fn: :*, 14 | called_fn: :*, 15 | should_be_present: :*, 16 | type: :* 17 | ] 18 | ] 19 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | .github/CODEOWNERS @exercism/guardians 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | # Keep dependencies for GitHub Actions up-to-date 5 | - package-ecosystem: 'github-actions' 6 | directory: '/' 7 | schedule: 8 | interval: 'monthly' 9 | labels: 10 | - 'x:size/tiny' 11 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: write 11 | 12 | jobs: 13 | build-and-push-image: 14 | if: github.repository_owner == 'exercism' # Stops this job from running on forks. 15 | uses: exercism/github-actions/.github/workflows/docker-build-push-image.yml@main 16 | secrets: 17 | AWS_ACCOUNT_ID: ${{secrets.AWS_ACCOUNT_ID}} 18 | AWS_REGION: ${{secrets.AWS_REGION}} 19 | AWS_ECR_ACCESS_KEY_ID: ${{secrets.AWS_ECR_ACCESS_KEY_ID}} 20 | AWS_ECR_SECRET_ACCESS_KEY: ${{secrets.AWS_ECR_SECRET_ACCESS_KEY}} 21 | DOCKERHUB_USERNAME: ${{secrets.DOCKERHUB_USERNAME}} 22 | DOCKERHUB_PASSWORD: ${{secrets.DOCKERHUB_PASSWORD}} 23 | -------------------------------------------------------------------------------- /.github/workflows/elixir_test.yml: -------------------------------------------------------------------------------- 1 | name: Elixir Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-22.04 8 | 9 | container: 10 | image: hexpm/elixir:1.18.1-erlang-27.2-debian-bookworm-20241223 11 | 12 | steps: 13 | - name: Install git 14 | run: | 15 | apt-get update 16 | apt-get install -y git 17 | 18 | - name: Checkout repository and submodules 19 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 20 | with: 21 | submodules: recursive 22 | 23 | - name: Update submodules 24 | run: | 25 | git config --global --add safe.directory /__w/elixir-analyzer/elixir-analyzer 26 | git submodule update --recursive --remote 27 | 28 | - name: Install Dependencies 29 | run: | 30 | mix local.rebar --force 31 | mix local.hex --force 32 | mix deps.get 33 | 34 | - name: Run Tests 35 | run: mix test --exclude external 36 | 37 | - name: Update coverage badge on push 38 | if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} 39 | run: mix coveralls.github --exclude external 40 | env: 41 | MIX_ENV: test 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | 44 | - name: Check formatting 45 | run: mix format --check-formatted 46 | 47 | - name: Run Credo 48 | run: mix credo 49 | 50 | - name: Retrieve PLT Cache 51 | 52 | uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 53 | id: plt-cache 54 | with: 55 | path: priv/plts 56 | key: elixir:1.18.1-erlang-27.2-debian-bookworm-20241223-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}-v3 57 | 58 | - name: Create PLTs 59 | if: steps.plt-cache.outputs.cache-hit != 'true' 60 | run: | 61 | mkdir -p priv/plts 62 | mix dialyzer --plt 63 | 64 | - name: Run dialyzer 65 | run: mix dialyzer 66 | 67 | smoke-test: 68 | runs-on: ubuntu-22.04 69 | 70 | steps: 71 | - name: Checkout repository 72 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 73 | 74 | - name: Run Smoke Test in Docker 75 | run: bin/run-tests-in-docker.sh 76 | -------------------------------------------------------------------------------- /.github/workflows/elixir_test_external.yml: -------------------------------------------------------------------------------- 1 | name: Elixir External Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-22.04 8 | 9 | container: 10 | image: hexpm/elixir:1.18.1-erlang-27.2-debian-bookworm-20241223 11 | 12 | steps: 13 | - name: Install git 14 | run: | 15 | apt-get update 16 | apt-get install -y git 17 | 18 | - name: Checkout repository and submodules 19 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 20 | with: 21 | submodules: recursive 22 | 23 | - name: Update submodules 24 | run: | 25 | git config --global --add safe.directory /__w/elixir-analyzer/elixir-analyzer 26 | git submodule update --recursive --remote 27 | 28 | - name: Install Dependencies 29 | run: | 30 | mix local.rebar --force 31 | mix local.hex --force 32 | mix deps.get 33 | 34 | - name: Run Tests 35 | run: mix test --only external 36 | -------------------------------------------------------------------------------- /.github/workflows/ping-cross-track-maintainers-team.yml: -------------------------------------------------------------------------------- 1 | name: Ping cross-track maintainers team 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | 8 | permissions: 9 | pull-requests: write 10 | 11 | jobs: 12 | ping: 13 | if: github.repository_owner == 'exercism' # Stops this job from running on forks 14 | uses: exercism/github-actions/.github/workflows/ping-cross-track-maintainers-team.yml@main 15 | secrets: 16 | github_membership_token: ${{ secrets.COMMUNITY_CONTRIBUTIONS_WORKFLOW_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/sync-labels.yml: -------------------------------------------------------------------------------- 1 | name: Tools 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - .github/labels.yml 9 | - .github/workflows/sync-labels.yml 10 | workflow_dispatch: 11 | schedule: 12 | - cron: 0 0 1 * * # First day of each month 13 | 14 | permissions: 15 | issues: write 16 | 17 | jobs: 18 | sync-labels: 19 | uses: exercism/github-actions/.github/workflows/labels.yml@main 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | _build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | ex_mentor-*.tar 24 | 25 | # the escript 26 | /elixir_analyzer 27 | /bin/elixir_analyzer 28 | 29 | #but not elixir_analyzer source 30 | !lib/elixir_analyzer/ 31 | 32 | test_results.json 33 | analysis.json 34 | 35 | .mix/ 36 | 37 | tmp/ 38 | 39 | /priv/plts/*.plt 40 | /priv/plts/*.plt.hash 41 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "elixir"] 2 | path = elixir 3 | branch = main 4 | url = https://github.com/exercism/elixir.git 5 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | elixir 1.18.1-otp-27 2 | erlang 27.2 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you so much for contributing! 🎉 4 | 5 | We welcome contributions of all sorts and sizes, from reporting issues to 6 | submitting patches, as well as joining the current [💬 discussions][issue-discussion]. 7 | 8 | Unprompted (without an issue) PRs with small bugfixes are welcome, but if you want to propose bigger changes, make sure to open an issue first so that we can discuss it. 9 | 10 | ## Code of Conduct 11 | 12 | Help us keep Exercism welcoming. Please read and abide by the [Code of Conduct][coc]. 13 | 14 | ## How does this project work? 15 | 16 | To learn how the Elixir Analyzer works: 17 | 18 | - Read [the guide to writing an Elixir Analyzer extension](https://github.com/exercism/elixir-analyzer/blob/main/docs/writing-an-analyzer.md). 19 | - Browse [recipes](https://github.com/exercism/elixir-analyzer/blob/main/docs/recipes.md). 20 | - Browse [existing exercise extensions](https://github.com/exercism/elixir-analyzer/tree/main/lib/elixir_analyzer/test_suite). 21 | 22 | ### Writing comments 23 | 24 | All the Analyzer comments for all tracks are stored together in [exercism/website-copy][analyzer-comments]. When adding a new Analyzer check with a new comment, you need to open a corresponding PR in [exercism/website-copy][analyzer-comments] with that comment's content. 25 | 26 | See [the Analyzer comment copy guidelines][analyzer-comments-guidelines] for guidance on how to write friendly and constructive Analyzer comments. 27 | 28 | [coc]: https://github.com/exercism/elixir/blob/main/CODE_OF_CONDUCT.md 29 | [analyzer-comments]: https://github.com/exercism/website-copy/tree/main/analyzer-comments 30 | [analyzer-comments-guidelines]: https://github.com/exercism/docs/blob/main/building/tooling/analyzers/comments.md#comment-copy-guidelines 31 | [issue-discussion]: https://github.com/exercism/elixir-analyzer/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22discussion+%F0%9F%92%AC%22 32 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM hexpm/elixir:1.18.1-erlang-27.2-debian-bookworm-20241223 as builder 2 | 3 | RUN apt-get update && \ 4 | apt-get install bash -y 5 | 6 | # Create appuser 7 | RUN useradd -ms /bin/bash appuser 8 | 9 | # Get the source code 10 | WORKDIR /elixir-analyzer 11 | COPY . . 12 | 13 | # Builds an escript bin/elixir_analyzer 14 | RUN ./bin/build.sh 15 | 16 | FROM hexpm/elixir:1.18.1-erlang-27.2-debian-bookworm-20241223 17 | COPY --from=builder /etc/passwd /etc/passwd 18 | 19 | COPY --from=builder /elixir-analyzer/bin /opt/analyzer/bin 20 | RUN apt-get update && \ 21 | apt-get install bash jq -y 22 | 23 | USER appuser 24 | WORKDIR /opt/analyzer 25 | ENTRYPOINT ["/opt/analyzer/bin/run.sh"] 26 | -------------------------------------------------------------------------------- /bin/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | mix local.hex --force 6 | mix deps.get 7 | mix deps.compile 8 | mix escript.build 9 | 10 | mv elixir_analyzer ./bin/elixir_analyzer 11 | chmod +x ./bin/elixir_analyzer 12 | -------------------------------------------------------------------------------- /bin/check_files.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | exercise=$1 6 | 7 | function installed { 8 | cmd=$(command -v "${1}") 9 | 10 | [[ -n "${cmd}" ]] && [[ -f "${cmd}" ]] 11 | return ${?} 12 | } 13 | 14 | function die { 15 | >&2 echo "Fatal: ${@}" 16 | exit 1 17 | } 18 | 19 | function main { 20 | expected_files=(/tmp/${exercise}/analysis.json ${exercise}/expected_analysis.json) 21 | 22 | for file in ${expected_files[@]}; do 23 | if [[ ! -f "${file}" ]]; then 24 | echo "🔥 ${exercise}: expected ${file} to exist on successful run 🔥" 25 | exit 1 26 | fi 27 | done 28 | 29 | if ! diff <(jq -S . ${exercise}/expected_analysis.json) <(jq -S . /tmp/${exercise}/analysis.json); then 30 | echo "🔥 ${exercise}: expected /tmp/${exercise}/analysis.json to equal ${exercise}/expected_analysis.json on successful run 🔥" 31 | exit 1 32 | fi 33 | 34 | echo "🏁 ${exercise}: expected files present after successful run 🏁" 35 | } 36 | 37 | # Check for all required dependencies 38 | deps=(diff jq) 39 | for dep in "${deps[@]}"; do 40 | installed "${dep}" || die "Missing '${dep}'" 41 | done 42 | 43 | main "$@"; exit 44 | -------------------------------------------------------------------------------- /bin/run-tests-in-docker.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Synopsis: 4 | # Test the analyzer Docker image by running it against a predefined set of 5 | # solutions with an expected output. 6 | # The analyzer Docker image is built automatically. 7 | 8 | # Output: 9 | # Outputs the diff of the expected test results against the actual test results 10 | # generated by the test runner Docker image. 11 | 12 | # Example: 13 | # ./bin/run-tests-in-docker.sh 14 | 15 | set -e # Make script exit when a command fail. 16 | set -u # Exit on usage of undeclared variable. 17 | # set -x # Trace what gets executed. 18 | set -o pipefail # Catch failures in pipes. 19 | 20 | # build docker image 21 | docker build --rm -t elixir-analyzer . 22 | 23 | # run image passing the arguments 24 | docker run \ 25 | --rm \ 26 | --network none \ 27 | --read-only \ 28 | --mount type=bind,src=$(realpath test_data),dst=/opt/analyzer/test_data \ 29 | --mount type=tmpfs,dst=/tmp \ 30 | --entrypoint /opt/analyzer/bin/smoke_test.sh \ 31 | elixir-analyzer 32 | -------------------------------------------------------------------------------- /bin/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | ./bin/elixir_analyzer "${1}" "${2}" "${3}" 6 | -------------------------------------------------------------------------------- /bin/smoke_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e # Make script exit when a command fail. 4 | set -u # Exit on usage of undeclared variable. 5 | # set -x # Trace what gets executed. 6 | set -o pipefail # Catch failures in pipes. 7 | 8 | for solution in test_data/*/* ; do 9 | slug=$(basename $(dirname $solution)) 10 | mkdir -p /tmp/$solution 11 | # run analysis 12 | bin/run.sh $slug $solution /tmp/$solution 13 | # check result 14 | bin/check_files.sh $solution 15 | done 16 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :logger, 4 | compile_time_purge_matching: [ 5 | [level_lower_than: :info] 6 | ] 7 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :logger, level: :warning 4 | -------------------------------------------------------------------------------- /docs/step-01/step-01.md: -------------------------------------------------------------------------------- 1 | # A Guide to Writing an Elixir Analyzer Extension 2 | 3 | ## Step 1: Setting up the analyzer locally 4 | 5 | Requirements: 6 | 7 | - See `mix.exs` for the required version of Elixir. 8 | - You may comment this out and run on an alternate version, but you may encounter problems. 9 | - Alternatively use [asdf][asdf]. 10 | 11 | 1. Clone the repo to your desired location. 12 | 2. Make sure that you can build the _escript_ with `mix escript.build`. 13 | 3. Continue to [step 2][step-2] 14 | 15 | [step-2]: ../step-02/step-02.md 16 | [asdf]: https://asdf-vm.com/ 17 | -------------------------------------------------------------------------------- /docs/step-02/02-example-analysis.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.Example do 2 | use ElixirAnalyzer.ExerciseTest 3 | end 4 | -------------------------------------------------------------------------------- /docs/step-02/02-example-module.ex: -------------------------------------------------------------------------------- 1 | defmodule Example do 2 | 3 | def hello(name) do 4 | "Hello, #{name}!" 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /docs/step-03/03-example-analysis.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.Example do 2 | use ElixirAnalyzer.ExerciseTest 3 | end 4 | -------------------------------------------------------------------------------- /docs/step-03/03-example-module.ex: -------------------------------------------------------------------------------- 1 | defmodule Example do 2 | 3 | def hello(name) do 4 | "Hello, #{name}!" 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /docs/step-04/04-example-analysis-test.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.ExampleTest do 2 | use ElixirAnalyzer.ExerciseTestCase, 3 | exercise_test_module: ElixirAnalyzer.TestSuite.Example 4 | 5 | test_exercise_analysis "perfect solution", 6 | comments: [] do 7 | defmodule Example do 8 | @moduledoc """ 9 | Greets the user 10 | """ 11 | def hello(name) do 12 | "Hello, #{name}!" 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /docs/step-04/step-04.md: -------------------------------------------------------------------------------- 1 | # A Guide to Writing an Elixir Analyzer Extension 2 | 3 | ## Step 4: Adding unit tests for the new extension 4 | 5 | 1. Create a new file in `/test/elixir_analyzer/test_suite/` called `analyzer_extension_module_name_test.exs`. 6 | 7 | 2. Do not use `ExUnit.Case` directly, but rather our custom `ElixirAnalyzer.ExerciseTestCase`. It takes the analyzer extension module as an option so that you don't need to repeat it with every test. 8 | 9 | ```elixir 10 | use ElixirAnalyzer.ExerciseTestCase, 11 | exercise_test_module: ElixirAnalyzer.ExerciseTest.Example 12 | ``` 13 | 14 | 3. Use the `test_exercise_analysis` macro to define test cases. It expects a test name, assertions about the analysis result comments, and a code snippet or list of code snippets in the `do` block. Refer to the macro's documentation for more details. 15 | 16 | ```elixir 17 | # 04-example-analysis-test.exs 18 | defmodule ElixirAnalyzer.TestSuite.ExampleTest do 19 | use ElixirAnalyzer.ExerciseTestCase, 20 | exercise_test_module: ElixirAnalyzer.TestSuite.Example 21 | 22 | test_exercise_analysis "perfect solution", 23 | comments: [] do 24 | defmodule Example do 25 | @moduledoc """ 26 | Greets the user 27 | """ 28 | def hello(name) do 29 | "Hello, #{name}!" 30 | end 31 | end 32 | end 33 | end 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/writing-an-analyzer.md: -------------------------------------------------------------------------------- 1 | # A Guide to Writing an Elixir Analyzer Extension 2 | 3 | 1. [Setting up the analyzer locally][step-1] 4 | 2. [Anatomy of an extension][step-2] 5 | 3. [Adding a feature test][step-3] 6 | 4. [Adding unit tests for the new extension][step-4] 7 | 8 | [step-1]: ./step-01/step-01.md 9 | [step-2]: ./step-02/step-02.md 10 | [step-3]: ./step-03/step-03.md 11 | [step-4]: ./step-04/step-04.md 12 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/cli.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.CLI do 2 | @moduledoc """ 3 | A CLI for running analysis on a single solution. 4 | """ 5 | 6 | @usage """ 7 | Usage: 8 | 9 | $ elixir_analyzer [options] 10 | 11 | You may also pass the following options: 12 | --help see this message 13 | --output-file output file name (default: analysis.json) 14 | --no-write-results doesn't write to JSON file 15 | --no-puts-summary doesn't print summary to stdio 16 | """ 17 | 18 | @options [ 19 | {{:output_file, :string}, "analysis.json"}, 20 | {{:write_results, :boolean}, true}, 21 | {{:puts_summary, :boolean}, true}, 22 | {{:help, :boolean}, false} 23 | ] 24 | 25 | @spec main(list(String.t())) :: no_return 26 | def main(args) do 27 | args |> parse_args() |> process() 28 | end 29 | 30 | defp parse_args(args) do 31 | default_ops = for({{key, _}, val} <- @options, do: {key, val}, into: %{}) 32 | 33 | cmd_opts = OptionParser.parse(args, strict: for({o, _} <- @options, do: o)) 34 | 35 | case cmd_opts do 36 | {[help: true], _, _} -> 37 | :help 38 | 39 | {opts, [exercise, input_path, output_path], _} -> 40 | {Enum.into(opts, default_ops), exercise, input_path, output_path} 41 | end 42 | rescue 43 | _ -> :help 44 | end 45 | 46 | defp process(:help), do: IO.puts(@usage) 47 | 48 | defp process({options, exercise, input_path, output_path}) do 49 | opts = Map.to_list(options) 50 | 51 | ElixirAnalyzer.analyze_exercise(exercise, input_path, output_path, opts) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/comment.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.Comment do 2 | @moduledoc """ 3 | Represents a single analysis comment 4 | (see https://github.com/exercism/docs/blob/main/building/tooling/analyzers/interface.md#comments) 5 | """ 6 | 7 | defstruct status: :test, name: nil, comment: nil, type: nil, suppress_if: [], params: nil 8 | 9 | @type t :: %__MODULE__{ 10 | name: String.t(), 11 | comment: String.t(), 12 | type: :essential | :actionable | :informative | :celebratory, 13 | suppress_if: [{String.t(), :pass | :fail}], 14 | params: map() | nil 15 | } 16 | 17 | @supported_types ~w(essential actionable informative celebratory)a 18 | 19 | @spec supported_type?(atom()) :: boolean() 20 | def supported_type?(type) do 21 | type in @supported_types 22 | end 23 | 24 | @spec supported_types() :: list(atom()) 25 | def supported_types do 26 | @supported_types 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/exercise_test/assert_call/syntax_error.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.ExerciseTest.AssertCall.SyntaxError do 2 | defexception message: "syntax error within assert_call test" 3 | end 4 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/exercise_test/check_source/compiler.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.ExerciseTest.CheckSource.Compiler do 2 | @moduledoc false 3 | 4 | alias ElixirAnalyzer.Comment 5 | alias ElixirAnalyzer.Source 6 | 7 | def compile({check_source_data, check_function}, code_source) do 8 | name = Keyword.fetch!(check_source_data, :description) 9 | comment = Keyword.fetch!(check_source_data, :comment) 10 | type = Keyword.get(check_source_data, :type, :informative) 11 | suppress_if = Keyword.get(check_source_data, :suppress_if, []) 12 | 13 | test_description = 14 | Macro.escape(%Comment{ 15 | name: name, 16 | comment: comment, 17 | type: type, 18 | suppress_if: suppress_if 19 | }) 20 | 21 | quote do 22 | (fn %Source{} = source -> 23 | case unquote(check_function).(source) do 24 | true -> 25 | {:pass, unquote(test_description)} 26 | 27 | false -> 28 | {:fail, unquote(test_description)} 29 | 30 | {true, params} when is_map(params) -> 31 | {:pass, %{unquote(test_description) | params: params}} 32 | 33 | {false, params} when is_map(params) -> 34 | {:fail, %{unquote(test_description) | params: params}} 35 | 36 | _ -> 37 | raise "check must be a boolean or a tuple with a boolean and a map of parameters for the comment" 38 | end 39 | end).(unquote(code_source)) 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/exercise_test/common_checks.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.ExerciseTest.CommonChecks do 2 | @moduledoc """ 3 | This module aggregates all common checks that should be run on every single solution. 4 | """ 5 | 6 | alias ElixirAnalyzer.ExerciseTest.CommonChecks.{ 7 | FunctionNames, 8 | VariableNames, 9 | ModuleAttributeNames, 10 | ModulePascalCase, 11 | CompilerWarnings, 12 | BooleanFunctions, 13 | ExemplarComparison, 14 | Indentation, 15 | FunctionAnnotationOrder, 16 | PrivateHelperFunctions, 17 | FunctionCapture, 18 | Comments 19 | } 20 | 21 | alias ElixirAnalyzer.Comment 22 | alias ElixirAnalyzer.Source 23 | 24 | # CommonChecks that use feature or assert_call should be called here 25 | defmacro __using__(_opts) do 26 | quote do 27 | use ElixirAnalyzer.ExerciseTest.CommonChecks.DebugFunctions 28 | use ElixirAnalyzer.ExerciseTest.CommonChecks.LastLineAssignment 29 | use ElixirAnalyzer.ExerciseTest.CommonChecks.ListPrependHead 30 | use ElixirAnalyzer.ExerciseTest.CommonChecks.UncommonErrors 31 | use ElixirAnalyzer.ExerciseTest.CommonChecks.UnlessWithElse 32 | use ElixirAnalyzer.ExerciseTest.CommonChecks.DeprecatedRandomModule 33 | use ElixirAnalyzer.ExerciseTest.CommonChecks.NoRescue 34 | end 35 | end 36 | 37 | @spec run(Source.t()) :: [{:pass | :fail, Comment.t()}] 38 | def run(%Source{ 39 | submitted_files: submitted_files, 40 | code_ast: code_ast, 41 | code_string: code_string, 42 | exercise_type: type, 43 | exemploid_ast: exemploid_ast 44 | }) do 45 | [ 46 | FunctionNames.run(code_ast), 47 | VariableNames.run(code_ast), 48 | ModuleAttributeNames.run(code_ast), 49 | ModulePascalCase.run(code_ast), 50 | CompilerWarnings.run(submitted_files), 51 | BooleanFunctions.run(code_ast), 52 | FunctionAnnotationOrder.run(code_ast), 53 | ExemplarComparison.run(code_ast, type, exemploid_ast), 54 | Indentation.run(code_ast, code_string), 55 | PrivateHelperFunctions.run(code_ast, exemploid_ast), 56 | FunctionCapture.run(code_ast), 57 | Comments.run(code_ast, code_string) 58 | ] 59 | |> List.flatten() 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/exercise_test/common_checks/comments.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.ExerciseTest.CommonChecks.Comments do 2 | @moduledoc """ 3 | Reports if concept exercise boilerplate comments or any TODO/FIXME comments are found. 4 | """ 5 | 6 | alias ElixirAnalyzer.Constants 7 | alias ElixirAnalyzer.Comment 8 | 9 | @spec run(Macro.t(), String.t()) :: [{:pass | :fail, Comment.t()}] 10 | def run(ast, string) do 11 | boilerplate_comment_check = 12 | if has_comment_matching_pattern?( 13 | ast, 14 | string, 15 | concept_exercise_boilerplate_comment_pattern() 16 | ) do 17 | {:fail, 18 | %Comment{ 19 | type: :actionable, 20 | name: Constants.solution_boilerplate_comment(), 21 | comment: Constants.solution_boilerplate_comment() 22 | }} 23 | end 24 | 25 | todo_comment_check = 26 | if has_comment_matching_pattern?( 27 | ast, 28 | string, 29 | generic_todo_comment_pattern() 30 | ) do 31 | {:fail, 32 | %Comment{ 33 | type: :informative, 34 | name: Constants.solution_todo_comment(), 35 | comment: Constants.solution_todo_comment() 36 | }} 37 | end 38 | 39 | Enum.filter([boilerplate_comment_check, todo_comment_check], & &1) 40 | end 41 | 42 | defp concept_exercise_boilerplate_comment_pattern() do 43 | # for example: 44 | # > # Please define the 'expected_minutes_in_oven/0' function 45 | # > # Please implement the ask_class/0 function 46 | # > # Please implement the struct with the specified fields 47 | # > # Please implement DivisionByZeroError here. 48 | 49 | # they're always used on their own line, hence the m (multiline) flag and beginning-of-line ^ anchor 50 | ~r/^\s*# Please (implement|define) /mi 51 | end 52 | 53 | defp generic_todo_comment_pattern() do 54 | ~r/#(\s)*(TODO|FIXME):?/i 55 | end 56 | 57 | defp has_comment_matching_pattern?(ast, string, pattern) do 58 | Regex.match?(pattern, string) and not Regex.match?(pattern, Macro.to_string(ast)) 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/exercise_test/common_checks/compiler_warnings.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.ExerciseTest.CommonChecks.CompilerWarnings do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module used for capturing compiler warnings 4 | """ 5 | alias ElixirAnalyzer.Constants 6 | alias ElixirAnalyzer.Comment 7 | 8 | def run(code_path) do 9 | Logger.configure(level: :critical) 10 | 11 | warnings = 12 | case Kernel.ParallelCompiler.compile(code_path) do 13 | {:ok, modules, warnings} -> 14 | Enum.each(modules, fn module -> 15 | :code.delete(module) 16 | :code.purge(module) 17 | end) 18 | 19 | warnings 20 | 21 | {:error, _errors, _warnings} -> 22 | # This should not happen, as real code is assumed to have compiled and 23 | # passed the tests 24 | [] 25 | end 26 | 27 | Logger.configure(level: :warning) 28 | 29 | Application.put_env(:elixir, :ansi_enabled, true) 30 | 31 | if Enum.empty?(warnings) do 32 | [] 33 | else 34 | [ 35 | {:fail, 36 | %Comment{ 37 | type: :actionable, 38 | name: Constants.solution_compiler_warnings(), 39 | comment: Constants.solution_compiler_warnings(), 40 | params: %{warnings: warnings |> Enum.sort() |> Enum.map_join(&format_warning/1)} 41 | }} 42 | ] 43 | end 44 | end 45 | 46 | defp format_warning({filepath, line, warning}) do 47 | [_ | after_lib] = String.split(filepath, "/lib/") 48 | filepath = "lib/" <> Enum.join(after_lib) 49 | 50 | line = 51 | case line do 52 | {line, col} when is_integer(line) and is_integer(col) -> "#{line}:#{col}" 53 | line when is_integer(line) -> "#{line}" 54 | end 55 | 56 | """ 57 | warning: #{warning} 58 | #{filepath}:#{line} 59 | 60 | """ 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/exercise_test/common_checks/debug_functions.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.ExerciseTest.CommonChecks.DebugFunctions do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module used for common tests looking for debugging functions 4 | """ 5 | 6 | alias ElixirAnalyzer.Constants 7 | 8 | defmacro __using__(_opts) do 9 | quote do 10 | assert_no_call Constants.solution_debug_functions() do 11 | type :informative 12 | comment Constants.solution_debug_functions() 13 | called_fn module: IO, name: :inspect 14 | end 15 | 16 | assert_no_call Constants.solution_debug_functions() do 17 | type :informative 18 | comment Constants.solution_debug_functions() 19 | called_fn module: Kernel, name: :dbg 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/exercise_test/common_checks/deprecated_random_module.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.ExerciseTest.CommonChecks.DeprecatedRandomModule do 2 | @moduledoc """ 3 | Asserts the deprecated Erlang module :random is not used 4 | """ 5 | alias ElixirAnalyzer.Constants 6 | 7 | defmacro __using__(_opts) do 8 | quote do 9 | assert_no_call Constants.solution_deprecated_random_module() do 10 | type :actionable 11 | comment Constants.solution_deprecated_random_module() 12 | called_fn module: :random, name: :_ 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/exercise_test/common_checks/exemplar_comparison.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.ExerciseTest.CommonChecks.ExemplarComparison do 2 | @moduledoc """ 3 | Compares the solution to the exemplar solution for concept exercises. 4 | Ignores practice exercises. 5 | """ 6 | 7 | alias ElixirAnalyzer.Constants 8 | alias ElixirAnalyzer.Comment 9 | 10 | @spec run(Macro.t(), atom, Macro.t()) :: [{:pass | :fail, Comment.t()}] 11 | 12 | def run(code_ast, :concept, exemplar_ast) do 13 | if format(code_ast) == format(exemplar_ast) do 14 | [ 15 | {:pass, 16 | %Comment{ 17 | type: :celebratory, 18 | name: Constants.solution_same_as_exemplar(), 19 | comment: Constants.solution_same_as_exemplar() 20 | }} 21 | ] 22 | else 23 | [] 24 | end 25 | end 26 | 27 | def run(_, _, _), do: [] 28 | 29 | defp format(ast) do 30 | ast 31 | |> Macro.to_string() 32 | |> Code.format_string!(line_length: 120, force_do_end_blocks: true) 33 | |> IO.iodata_to_binary() 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/exercise_test/common_checks/function_names.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.ExerciseTest.CommonChecks.FunctionNames do 2 | @moduledoc """ 3 | Reports the first function/macro/guard with a name that's not snake_case. 4 | 5 | Doesn't report more if there are more. 6 | A single comment should be enough for the student to know what to fix. 7 | 8 | Common check to be run on every single solution. 9 | """ 10 | 11 | alias ElixirAnalyzer.Constants 12 | alias ElixirAnalyzer.Comment 13 | 14 | @def_ops [:def, :defp, :defmacro, :defmacrop, :defguard, :defguardp] 15 | 16 | @spec run(Macro.t()) :: [{:pass | :fail, Comment.t()}] 17 | def run(ast) do 18 | {_, names} = Macro.prewalk(ast, [], &traverse/2) 19 | wrong_name = List.last(names) 20 | 21 | if wrong_name do 22 | wrong_name = to_string(wrong_name) 23 | correct_name = to_snake_case(wrong_name) 24 | 25 | [ 26 | {:fail, 27 | %Comment{ 28 | type: :actionable, 29 | name: Constants.solution_function_name_snake_case(), 30 | comment: Constants.solution_function_name_snake_case(), 31 | params: %{ 32 | expected: correct_name, 33 | actual: wrong_name 34 | } 35 | }} 36 | ] 37 | else 38 | [] 39 | end 40 | end 41 | 42 | defp traverse({op, _meta, [{:when, _, [{name, _, _} | _]} | _]} = ast, names) 43 | when op in @def_ops do 44 | if snake_case?(name) do 45 | {ast, names} 46 | else 47 | {ast, [name | names]} 48 | end 49 | end 50 | 51 | defp traverse({op, _meta, [{name, _meta2, _arguments} | _]} = ast, names) when op in @def_ops do 52 | if snake_case?(name) do 53 | {ast, names} 54 | else 55 | {ast, [name | names]} 56 | end 57 | end 58 | 59 | defp traverse(ast, names) do 60 | {ast, names} 61 | end 62 | 63 | defp snake_case?(name) do 64 | # the code had to compile and pass all the tests to get to the analyzer 65 | # so we can assume the name is otherwise valid 66 | to_snake_case(name) == to_string(name) 67 | end 68 | 69 | defp to_snake_case(name) do 70 | # Macro.underscore is good enough because a module attribute name must be a valid Elixir identifier anyway 71 | Macro.underscore(to_string(name)) 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/exercise_test/common_checks/indentation.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.ExerciseTest.CommonChecks.Indentation do 2 | @moduledoc """ 3 | Reports if tabs were used for indentation. 4 | """ 5 | 6 | alias ElixirAnalyzer.Constants 7 | alias ElixirAnalyzer.Comment 8 | 9 | @spec run(Macro.t(), String.t()) :: [{:pass | :fail, Comment.t()}] 10 | def run(ast, string) do 11 | if uses_tabs_for_indentation?(ast, string) do 12 | [ 13 | {:fail, 14 | %Comment{ 15 | type: :informative, 16 | name: Constants.solution_indentation(), 17 | comment: Constants.solution_indentation() 18 | }} 19 | ] 20 | else 21 | [] 22 | end 23 | end 24 | 25 | defp uses_tabs_for_indentation?(ast, string) do 26 | count_tabs(string) > count_tabs(Macro.to_string(ast)) 27 | end 28 | 29 | defp count_tabs(string) do 30 | string 31 | |> String.replace("\\t", "\t") 32 | |> String.graphemes() 33 | |> Enum.count(&(&1 == "\t")) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/exercise_test/common_checks/last_line_assignment.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.ExerciseTest.CommonChecks.LastLineAssignment do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module used for common tests looking for assignments 4 | on the last line of function definitions 5 | """ 6 | 7 | alias ElixirAnalyzer.Constants 8 | 9 | defmacro __using__(_opts) do 10 | quote do 11 | feature Constants.solution_last_line_assignment() do 12 | type :informative 13 | comment Constants.solution_last_line_assignment() 14 | find :none 15 | 16 | form do 17 | def _ignore do 18 | _block_ends_with do 19 | _ignore = _ignore 20 | end 21 | end 22 | end 23 | 24 | form do 25 | defp _ignore do 26 | _block_ends_with do 27 | _ignore = _ignore 28 | end 29 | end 30 | end 31 | 32 | form do 33 | defmacro _ignore do 34 | _block_ends_with do 35 | _ignore = _ignore 36 | end 37 | end 38 | end 39 | 40 | form do 41 | defmacrop _ignore do 42 | _block_ends_with do 43 | _ignore = _ignore 44 | end 45 | end 46 | end 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/exercise_test/common_checks/list_prepend_head.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.ExerciseTest.CommonChecks.ListPrependHead do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module used for common tests looking for 4 | usage of `[h] ++ t` instead of `[h | t]` to prepend an item to a list 5 | """ 6 | 7 | alias ElixirAnalyzer.Constants 8 | 9 | defmacro __using__(_opts) do 10 | quote do 11 | feature Constants.solution_list_prepend_head() do 12 | type :informative 13 | comment Constants.solution_list_prepend_head() 14 | find :none 15 | 16 | form do 17 | [_ignore] ++ _ignore 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/exercise_test/common_checks/module_attribute_names.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.ExerciseTest.CommonChecks.ModuleAttributeNames do 2 | @moduledoc """ 3 | Reports the first module attribute with a name that's not snake_case. 4 | 5 | Doesn't report more if there are more. 6 | A single comment should be enough for the student to know what to fix. 7 | 8 | Common check to be run on every single solution. 9 | """ 10 | 11 | alias ElixirAnalyzer.Constants 12 | alias ElixirAnalyzer.Comment 13 | 14 | @spec run(Macro.t()) :: [{:pass | :fail, Comment.t()}] 15 | def run(ast) do 16 | {_, names} = Macro.prewalk(ast, [], &traverse/2) 17 | wrong_name = List.last(names) 18 | 19 | if wrong_name do 20 | wrong_name = to_string(wrong_name) 21 | correct_name = to_snake_case(wrong_name) 22 | 23 | [ 24 | {:fail, 25 | %Comment{ 26 | type: :actionable, 27 | name: Constants.solution_module_attribute_name_snake_case(), 28 | comment: Constants.solution_module_attribute_name_snake_case(), 29 | params: %{ 30 | expected: correct_name, 31 | actual: wrong_name 32 | } 33 | }} 34 | ] 35 | else 36 | [] 37 | end 38 | end 39 | 40 | defp traverse({:@, _meta, [{name, _meta2, _arguments}]} = ast, names) do 41 | if snake_case?(name) do 42 | {ast, names} 43 | else 44 | {ast, [name | names]} 45 | end 46 | end 47 | 48 | defp traverse(ast, names) do 49 | {ast, names} 50 | end 51 | 52 | defp snake_case?(name) do 53 | # the code had to compile and pass all the tests to get to the analyzer 54 | # so we can assume the name is otherwise valid 55 | to_snake_case(name) == to_string(name) 56 | end 57 | 58 | defp to_snake_case(name) do 59 | # Macro.underscore is good enough because a module attribute name must be a valid Elixir identifier anyway 60 | Macro.underscore(to_string(name)) 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/exercise_test/common_checks/module_pascal_case.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.ExerciseTest.CommonChecks.ModulePascalCase do 2 | @moduledoc """ 3 | Report the first defined module names that is not in PascalCase 4 | 5 | Doesn't report more if there are more. 6 | A single comment should be enough for the student to know what to fix. 7 | 8 | Common check to be run on every single solution. 9 | """ 10 | 11 | alias ElixirAnalyzer.Constants 12 | alias ElixirAnalyzer.Comment 13 | 14 | @spec run(Macro.t()) :: [{:pass | :fail, Comment.t()}] 15 | def run(ast) do 16 | {_, names} = Macro.prewalk(ast, [], &traverse/2) 17 | wrong_name = List.last(names) 18 | 19 | if wrong_name do 20 | correct_name = Enum.map_join(wrong_name, ".", fn n -> n |> to_string |> to_pascal_case end) 21 | wrong_name = Enum.map_join(wrong_name, ".", &to_string/1) 22 | 23 | [ 24 | {:fail, 25 | %Comment{ 26 | type: :actionable, 27 | name: Constants.solution_module_pascal_case(), 28 | comment: Constants.solution_module_pascal_case(), 29 | params: %{ 30 | expected: correct_name, 31 | actual: wrong_name 32 | } 33 | }} 34 | ] 35 | else 36 | [] 37 | end 38 | end 39 | 40 | defp traverse({:defmodule, _meta, [{:__aliases__, _meta2, names}, _do]} = ast, wrong_names) do 41 | if Enum.all?(names, &pascal_case?/1) do 42 | {ast, wrong_names} 43 | else 44 | {ast, [names | wrong_names]} 45 | end 46 | end 47 | 48 | defp traverse(ast, names) do 49 | {ast, names} 50 | end 51 | 52 | defp pascal_case?(name) do 53 | # the code had to compile and pass all the tests to get to the analyzer 54 | # so we can assume the name is otherwise valid 55 | to_pascal_case(name) == to_string(name) 56 | end 57 | 58 | defp to_pascal_case(name) do 59 | # Macro.camelize is good enough because a module attribute name must be a valid Elixir identifier anyway 60 | Macro.camelize(to_string(name)) 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/exercise_test/common_checks/no_rescue.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.ExerciseTest.CommonChecks.NoRescue do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module used for common tests looking for the usage of `rescue` 4 | """ 5 | 6 | alias ElixirAnalyzer.Constants 7 | alias ElixirAnalyzer.Source 8 | 9 | defmacro __using__(_opts) do 10 | quote do 11 | check_source Constants.solution_no_rescue() do 12 | type :essential 13 | comment Constants.solution_no_rescue() 14 | 15 | check(%Source{code_ast: code_ast}) do 16 | {_, found} = 17 | Macro.prewalk(code_ast, false, fn node, found -> 18 | case node do 19 | {def_op, _, [_function, [{:do, _} | rest]]} when def_op in [:def, :defp] -> 20 | {node, found or List.keymember?(rest, :rescue, 0)} 21 | 22 | {:try, _, [[{:do, _} | rest]]} -> 23 | {node, found or List.keymember?(rest, :rescue, 0)} 24 | 25 | _ -> 26 | {node, found} 27 | end 28 | end) 29 | 30 | not found 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/exercise_test/common_checks/uncommon_errors.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.ExerciseTest.CommonChecks.UncommonErrors do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module used for common tests looking for uncommon errors raised 4 | """ 5 | alias ElixirAnalyzer.Constants 6 | 7 | defmacro __using__(_opts) do 8 | quote do 9 | feature Constants.solution_raise_fn_clause_error() do 10 | find :none 11 | type :actionable 12 | comment Constants.solution_raise_fn_clause_error() 13 | 14 | form do 15 | raise FunctionClauseError 16 | end 17 | 18 | form do 19 | raise %FunctionClauseError{} 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/exercise_test/common_checks/unless_with_else.ex: -------------------------------------------------------------------------------- 1 | # credo:disable-for-this-file Credo.Check.Refactor.UnlessWithElse 2 | defmodule ElixirAnalyzer.ExerciseTest.CommonChecks.UnlessWithElse do 3 | @moduledoc """ 4 | Reports the first occurrence of unless/2 macro used with else. 5 | """ 6 | alias ElixirAnalyzer.Constants 7 | 8 | defmacro __using__(_opts) do 9 | quote do 10 | feature Constants.solution_unless_with_else() do 11 | type :informative 12 | find :none 13 | comment Constants.solution_unless_with_else() 14 | 15 | form do 16 | unless _ignore do 17 | _ignore 18 | else 19 | _ignore 20 | end 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/exercise_test/feature/feature_error.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.ExerciseTest.Feature.FeatureError do 2 | defexception message: "A duplicate feature has been found" 3 | end 4 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/log_formatter.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.LogFormatter do 2 | @moduledoc """ 3 | Analyzer's custom formatter for the Logger. 4 | """ 5 | 6 | def format(level, message, timestamp, metadata) do 7 | "# #{fmt_timestamp(timestamp)} #{inspect(metadata)} [#{level}] #{message}\n" 8 | rescue 9 | _ -> "could not format message: #{inspect({level, message, timestamp, metadata})}\n" 10 | end 11 | 12 | defp fmt_timestamp({date, {hh, mm, ss, ms}}) do 13 | with {:ok, timestamp} <- NaiveDateTime.from_erl({date, {hh, mm, ss}}, {ms * 1000, 3}), 14 | result <- NaiveDateTime.to_iso8601(timestamp) do 15 | "#{result}Z" 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/source.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.Source do 2 | @moduledoc """ 3 | Represents all the data received: solution code, exemploid, slug and paths 4 | """ 5 | defstruct [ 6 | :slug, 7 | :path, 8 | :submitted_files, 9 | :code_string, 10 | :code_ast, 11 | :exercise_type, 12 | :exemploid_files, 13 | :exemploid_string, 14 | :exemploid_ast 15 | ] 16 | 17 | @type t() :: %__MODULE__{ 18 | slug: String.t(), 19 | path: String.t(), 20 | submitted_files: [String.t()], 21 | code_string: String.t(), 22 | code_ast: Macro.t(), 23 | exercise_type: :concept | :practice, 24 | exemploid_files: [String.t()], 25 | exemploid_string: String.t(), 26 | exemploid_ast: Macro.t() 27 | } 28 | end 29 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/summary.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.Summary do 2 | @moduledoc """ 3 | Utility functions to format the string to puts the summary in ElixirAnalyzer. 4 | """ 5 | 6 | alias ElixirAnalyzer.Submission 7 | 8 | @doc """ 9 | From the Submission, return a string representation of the Analysis summary 10 | """ 11 | @spec summary(Submission.t(), map()) :: String.t() 12 | def summary(%Submission{} = submission, params) do 13 | """ 14 | ElixirAnalyzer Report 15 | --------------------- 16 | 17 | Exercise: #{params.exercise} 18 | Status: #{result_to_string(submission)} 19 | Output written to ... #{Path.join(params.output_path, params.output_file)} 20 | """ 21 | end 22 | 23 | defp result_to_string(%Submission{} = submission) do 24 | case {submission.halted, submission.analyzed} do 25 | {true, _} -> "Halted" 26 | {_, false} -> "Analysis Incomplete" 27 | {_, true} -> "Analysis Complete" 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/accumulate.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.Accumulate do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module for the concept exercise Accumulate 4 | """ 5 | 6 | use ElixirAnalyzer.ExerciseTest 7 | alias ElixirAnalyzer.Constants 8 | 9 | assert_no_call "does not call any Enum functions" do 10 | type :essential 11 | called_fn module: Enum, name: :_ 12 | comment Constants.accumulate_use_recursion() 13 | end 14 | 15 | assert_no_call "does not call any Stream functions" do 16 | type :essential 17 | called_fn module: Stream, name: :_ 18 | comment Constants.accumulate_use_recursion() 19 | end 20 | 21 | assert_no_call "does not call any List functions" do 22 | type :essential 23 | called_fn module: List, name: :_ 24 | comment Constants.accumulate_use_recursion() 25 | end 26 | 27 | assert_no_call "doesn't use list comprehensions" do 28 | type :essential 29 | called_fn name: :for 30 | comment Constants.accumulate_use_recursion() 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/basketball_website.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.BasketballWebsite do 2 | alias ElixirAnalyzer.Constants 3 | use ElixirAnalyzer.ExerciseTest 4 | 5 | assert_no_call "does not use Map" do 6 | type :essential 7 | comment Constants.basketball_website_no_map() 8 | called_fn module: Map, name: :_ 9 | end 10 | 11 | assert_no_call "extract_from_path does not use get_in" do 12 | type :essential 13 | comment Constants.basketball_website_get_in() 14 | called_fn module: Kernel, name: :get_in 15 | calling_fn module: BasketballWebsite, name: :extract_from_path 16 | end 17 | 18 | assert_call "get_in_path must use get_in" do 19 | type :essential 20 | comment Constants.basketball_website_get_in() 21 | called_fn module: Kernel, name: :get_in 22 | calling_fn module: BasketballWebsite, name: :get_in_path 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/bird_count.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.BirdCount do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module for the concept exercise Bird Count 4 | """ 5 | 6 | use ElixirAnalyzer.ExerciseTest 7 | alias ElixirAnalyzer.Constants 8 | 9 | assert_no_call "does not call any Enum functions" do 10 | type :essential 11 | called_fn module: Enum, name: :_ 12 | comment Constants.bird_count_use_recursion() 13 | end 14 | 15 | assert_no_call "does not call any Stream functions" do 16 | type :essential 17 | called_fn module: Stream, name: :_ 18 | comment Constants.bird_count_use_recursion() 19 | end 20 | 21 | assert_no_call "does not call any List functions" do 22 | type :essential 23 | called_fn module: List, name: :_ 24 | comment Constants.bird_count_use_recursion() 25 | end 26 | 27 | assert_no_call "doesn't use list comprehensions" do 28 | type :essential 29 | called_fn name: :for 30 | comment Constants.bird_count_use_recursion() 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/boutique_suggestions.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.BoutiqueSuggestions do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module for the concept exercise Boutique Suggestions 4 | """ 5 | 6 | use ElixirAnalyzer.ExerciseTest 7 | alias ElixirAnalyzer.Constants 8 | 9 | assert_call "uses list comprehensions" do 10 | type :essential 11 | called_fn name: :for 12 | comment Constants.boutique_suggestions_use_list_comprehensions() 13 | end 14 | 15 | assert_no_call "does not call any Enum functions" do 16 | type :essential 17 | called_fn module: Enum, name: :_ 18 | comment Constants.boutique_suggestions_use_list_comprehensions() 19 | end 20 | 21 | assert_no_call "does not call any Stream functions" do 22 | type :essential 23 | called_fn module: Stream, name: :_ 24 | comment Constants.boutique_suggestions_use_list_comprehensions() 25 | end 26 | 27 | assert_no_call "does not call any List functions" do 28 | type :essential 29 | called_fn module: List, name: :_ 30 | comment Constants.boutique_suggestions_use_list_comprehensions() 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/captains_log.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.CaptainsLog do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module for the concept exercise Captains Log 4 | """ 5 | use ElixirAnalyzer.ExerciseTest 6 | alias ElixirAnalyzer.Constants 7 | 8 | assert_call "random_planet_class uses Enum.random" do 9 | type :essential 10 | calling_fn module: CaptainsLog, name: :random_planet_class 11 | called_fn module: Enum, name: :random 12 | comment Constants.captains_log_use_enum_random() 13 | end 14 | 15 | assert_call "random_ship_registry_number uses Enum.random" do 16 | type :essential 17 | calling_fn module: CaptainsLog, name: :random_ship_registry_number 18 | called_fn module: Enum, name: :random 19 | comment Constants.captains_log_use_enum_random() 20 | end 21 | 22 | assert_no_call "random_stardate does not use Enum.random" do 23 | type :essential 24 | calling_fn module: CaptainsLog, name: :random_stardate 25 | called_fn module: Enum, name: :random 26 | comment Constants.captains_log_do_not_use_enum_random() 27 | end 28 | 29 | assert_no_call "random_stardate does not use :rand.uniform_real" do 30 | type :essential 31 | calling_fn module: CaptainsLog, name: :random_stardate 32 | called_fn module: :rand, name: :uniform_real 33 | comment Constants.captains_log_do_not_use_rand_uniform_real() 34 | end 35 | 36 | assert_call "random_stardate uses :rand.uniform" do 37 | type :essential 38 | calling_fn module: CaptainsLog, name: :random_stardate 39 | called_fn module: :rand, name: :uniform 40 | comment Constants.captains_log_use_rand_uniform() 41 | suppress_if Constants.solution_deprecated_random_module(), :fail 42 | suppress_if "random_stardate does not use :rand.uniform_real", :fail 43 | suppress_if "random_stardate does not use Enum.random", :fail 44 | end 45 | 46 | assert_call "format_stardate uses :io_lib" do 47 | type :essential 48 | calling_fn module: CaptainsLog, name: :format_stardate 49 | called_fn module: :io_lib, name: :_ 50 | comment Constants.captains_log_use_io_lib() 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/chessboard.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.Chessboard do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module for the concept exercise Chessboard 4 | """ 5 | 6 | use ElixirAnalyzer.ExerciseTest 7 | alias ElixirAnalyzer.Constants 8 | 9 | assert_call "ranks calls rank_range" do 10 | type :actionable 11 | comment Constants.chessboard_function_reuse() 12 | called_fn name: :rank_range 13 | calling_fn module: Chessboard, name: :ranks 14 | end 15 | 16 | assert_call "files calls file_range" do 17 | type :actionable 18 | comment Constants.chessboard_function_reuse() 19 | called_fn name: :file_range 20 | calling_fn module: Chessboard, name: :files 21 | end 22 | 23 | feature "change codepoint to string directly" do 24 | type :actionable 25 | comment Constants.chessboard_change_codepoint_to_string_directly() 26 | 27 | form do 28 | <<_ignore>> 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/community_garden.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.CommunityGarden do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module for the concept exercise Community Garden 4 | """ 5 | use ElixirAnalyzer.ExerciseTest 6 | alias ElixirAnalyzer.Constants 7 | 8 | assert_call "register uses Agent.get_and_update" do 9 | type :essential 10 | calling_fn module: CommunityGarden, name: :register 11 | called_fn module: Agent, name: :get_and_update 12 | comment Constants.community_garden_use_get_and_update() 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/default.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.Default do 2 | @moduledoc """ 3 | This is the default exercise analyzer extension module. 4 | 5 | It will be run for any exercise submission that doesn't have its own extension module. 6 | It's intentionally empty, which means it will only run the common checks. 7 | """ 8 | 9 | use ElixirAnalyzer.ExerciseTest 10 | end 11 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/file_sniffer.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.FileSniffer do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module for the concept exercise File Sniffer 4 | """ 5 | 6 | alias ElixirAnalyzer.Constants 7 | use ElixirAnalyzer.ExerciseTest 8 | 9 | assert_call "use <<>> to pattern match a bitstring in function body" do 10 | type :essential 11 | comment Constants.file_sniffer_use_bitstring() 12 | called_fn name: :<<>> 13 | calling_fn module: FileSniffer, name: :type_from_binary 14 | end 15 | 16 | assert_call "use :: in bitstrings to specify segment type when pattern matching in function body" do 17 | type :essential 18 | comment Constants.file_sniffer_use_bitstring() 19 | called_fn name: :"::" 20 | calling_fn module: FileSniffer, name: :type_from_binary 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/freelancer_rates.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.FreelancerRates do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module for the concept exercise Freelancer Rates 4 | """ 5 | 6 | use ElixirAnalyzer.ExerciseTest 7 | alias ElixirAnalyzer.Constants 8 | 9 | assert_call "monthly_rate/2 reuses apply_discount/2" do 10 | type :actionable 11 | calling_fn module: FreelancerRates, name: :monthly_rate 12 | called_fn name: :apply_discount 13 | comment Constants.freelancer_rates_apply_discount_function_reuse() 14 | end 15 | 16 | assert_call "days_in_budget/2 reuses apply_discount/2" do 17 | type :actionable 18 | calling_fn module: FreelancerRates, name: :days_in_budget 19 | called_fn name: :apply_discount 20 | comment Constants.freelancer_rates_apply_discount_function_reuse() 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/guessing_game.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.GuessingGame do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module for the concept exercise Guessing Game 4 | """ 5 | 6 | use ElixirAnalyzer.ExerciseTest 7 | alias ElixirAnalyzer.Constants 8 | 9 | feature "uses guards" do 10 | find :any 11 | type :essential 12 | comment Constants.guessing_game_use_guards() 13 | 14 | form do 15 | def compare(_ignore, _ignore) when _ignore do 16 | _ignore 17 | end 18 | end 19 | end 20 | 21 | feature "uses default arguments" do 22 | find :any 23 | type :essential 24 | comment Constants.guessing_game_use_default_argument() 25 | 26 | form do 27 | def compare(_ignore, _ignore \\ _ignore) 28 | end 29 | 30 | form do 31 | def compare(_ignore, _ignore \\ _ignore) do 32 | _ignore 33 | end 34 | end 35 | 36 | form do 37 | def compare(_ignore, _ignore \\ _ignore) when _ignore do 38 | _ignore 39 | end 40 | end 41 | end 42 | 43 | assert_no_call "doesn't use if, only multiple clause functions" do 44 | type :essential 45 | called_fn name: :if 46 | comment Constants.guessing_game_use_multiple_clause_functions() 47 | end 48 | 49 | assert_no_call "doesn't use case, only multiple clause functions" do 50 | type :essential 51 | called_fn name: :case 52 | comment Constants.guessing_game_use_multiple_clause_functions() 53 | end 54 | 55 | assert_no_call "doesn't use cond, only multiple clause functions" do 56 | type :essential 57 | called_fn name: :cond 58 | comment Constants.guessing_game_use_multiple_clause_functions() 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/high_school_sweetheart.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.HighSchoolSweetheart do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module for the concept exercise HighSchoolSweetheart 4 | """ 5 | 6 | alias ElixirAnalyzer.Constants 7 | alias ElixirAnalyzer.Source 8 | 9 | use ElixirAnalyzer.ExerciseTest 10 | 11 | assert_call "initial/1 reuses first_letter/1" do 12 | type :actionable 13 | calling_fn module: HighSchoolSweetheart, name: :initial 14 | called_fn name: :first_letter 15 | comment Constants.high_school_sweetheart_function_reuse() 16 | end 17 | 18 | assert_call "initials/1 reuses initial/1" do 19 | type :actionable 20 | calling_fn module: HighSchoolSweetheart, name: :initials 21 | called_fn name: :initial 22 | comment Constants.high_school_sweetheart_function_reuse() 23 | end 24 | 25 | assert_call "pair/2 reuses initials/1" do 26 | type :actionable 27 | calling_fn module: HighSchoolSweetheart, name: :pair 28 | called_fn name: :initials 29 | comment Constants.high_school_sweetheart_function_reuse() 30 | end 31 | 32 | check_source "uses a multiline string in pair/2" do 33 | type :actionable 34 | comment Constants.high_school_sweetheart_multiline_string() 35 | 36 | check(%Source{code_string: code_string}) do 37 | String.contains?(code_string, ~s(""")) || 38 | String.contains?(code_string, ~s(''')) 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/high_score.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.HighScore do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module for the concept exercise High Score 4 | """ 5 | 6 | use ElixirAnalyzer.ExerciseTest 7 | alias ElixirAnalyzer.Constants 8 | 9 | feature "uses the module attribute in add_player function head" do 10 | find :any 11 | type :actionable 12 | comment Constants.high_score_use_default_argument_with_module_attribute() 13 | 14 | form do 15 | def add_player(_ignore, _ignore, _ignore \\ @_ignore) do 16 | _ignore 17 | end 18 | end 19 | 20 | form do 21 | def add_player(_ignore, _ignore, _ignore \\ @_ignore) 22 | end 23 | 24 | form do 25 | def add_player(_ignore, _ignore, _ignore \\ @_ignore) when _ignore do 26 | _ignore 27 | end 28 | end 29 | 30 | form do 31 | def add_player(_ignore, _ignore, _ignore \\ @_ignore) when _ignore 32 | end 33 | 34 | form do 35 | defdelegate add_player(_ignore, _ignore, _ignore \\ @_ignore), _ignore 36 | end 37 | end 38 | 39 | feature "uses a module attribute to define the initial score" do 40 | find :any 41 | type :essential 42 | comment Constants.high_score_use_module_attribute() 43 | 44 | form do 45 | @_shallow_ignore 0 46 | end 47 | end 48 | 49 | assert_call "uses the module attribute in reset_score" do 50 | type :essential 51 | comment Constants.high_score_use_module_attribute() 52 | calling_fn module: HighScore, name: :reset_score 53 | called_fn name: :@ 54 | suppress_if "uses add_player in reset_score", :pass 55 | end 56 | 57 | assert_call "uses add_player in reset_score" do 58 | type :essential 59 | comment Constants.high_score_use_module_attribute() 60 | calling_fn module: HighScore, name: :reset_score 61 | called_fn name: :add_player 62 | suppress_if "uses the module attribute in reset_score", :pass 63 | end 64 | 65 | assert_call "uses Map.update/4 in update_score" do 66 | type :actionable 67 | comment Constants.high_score_use_map_update() 68 | calling_fn module: HighScore, name: :update_score 69 | called_fn module: Map, name: :update 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/language_list.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.LanguageList do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module for the concept exercise Language List 4 | """ 5 | 6 | use ElixirAnalyzer.ExerciseTest 7 | alias ElixirAnalyzer.Constants 8 | 9 | assert_no_call "does not call any Enum functions" do 10 | type :essential 11 | called_fn module: Enum, name: :_ 12 | comment Constants.language_list_do_not_use_enum() 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/lasagna.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.Lasagna do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module for the concept exercise Lasagna 4 | """ 5 | 6 | use ElixirAnalyzer.ExerciseTest 7 | alias ElixirAnalyzer.Constants 8 | 9 | assert_call "remaining minutes in oven are calculated based on expected minutes in oven" do 10 | type :actionable 11 | calling_fn module: Lasagna, name: :remaining_minutes_in_oven 12 | called_fn name: :expected_minutes_in_oven 13 | comment Constants.lasagna_function_reuse() 14 | end 15 | 16 | assert_call "total time in minutes is calculated based on preparation time in minutes" do 17 | type :actionable 18 | calling_fn module: Lasagna, name: :total_time_in_minutes 19 | called_fn name: :preparation_time_in_minutes 20 | comment Constants.lasagna_function_reuse() 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/leap.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.Leap do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module for the concept exercise Leap 4 | """ 5 | 6 | use ElixirAnalyzer.ExerciseTest 7 | alias ElixirAnalyzer.Constants 8 | 9 | assert_no_call "does not call any functions from :calendar Erlang module" do 10 | type :essential 11 | called_fn module: :calendar, name: :_ 12 | comment Constants.leap_erlang_calendar() 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/library_fees.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.LibraryFees do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module for the concept exercise Library Fees 4 | """ 5 | 6 | use ElixirAnalyzer.ExerciseTest 7 | alias ElixirAnalyzer.Constants 8 | 9 | assert_call "calculate_late_fee/3 uses datetime_from_string/1" do 10 | type :essential 11 | calling_fn module: LibraryFees, name: :calculate_late_fee 12 | called_fn module: LibraryFees, name: :datetime_from_string 13 | comment Constants.library_fees_function_reuse() 14 | end 15 | 16 | assert_call "calculate_late_fee/3 uses return_date/1" do 17 | type :essential 18 | calling_fn module: LibraryFees, name: :calculate_late_fee 19 | called_fn module: LibraryFees, name: :return_date 20 | comment Constants.library_fees_function_reuse() 21 | end 22 | 23 | assert_call "calculate_late_fee/3 uses days_late/2" do 24 | type :essential 25 | calling_fn module: LibraryFees, name: :calculate_late_fee 26 | called_fn module: LibraryFees, name: :days_late 27 | comment Constants.library_fees_function_reuse() 28 | end 29 | 30 | assert_call "calculate_late_fee/3 uses monday?/1" do 31 | type :essential 32 | calling_fn module: LibraryFees, name: :calculate_late_fee 33 | called_fn module: LibraryFees, name: :monday? 34 | comment Constants.library_fees_function_reuse() 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/list_ops.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.ListOps do 2 | @moduledoc """ 3 | This is an exercise analyzer test suite for the practive exercise list-ops 4 | """ 5 | 6 | use ElixirAnalyzer.ExerciseTest 7 | alias ElixirAnalyzer.Constants 8 | 9 | assert_no_call "do not call List module" do 10 | type :essential 11 | called_fn module: List, name: :_ 12 | comment Constants.list_ops_do_not_use_list_functions() 13 | end 14 | 15 | assert_no_call "do not call Enum module" do 16 | type :essential 17 | called_fn module: Enum, name: :_ 18 | comment Constants.list_ops_do_not_use_list_functions() 19 | end 20 | 21 | assert_no_call "do not call Stream module" do 22 | type :essential 23 | called_fn module: Stream, name: :_ 24 | comment Constants.list_ops_do_not_use_list_functions() 25 | end 26 | 27 | assert_no_call "do not use ++" do 28 | type :essential 29 | called_fn name: :++ 30 | comment Constants.list_ops_do_not_use_list_functions() 31 | end 32 | 33 | assert_no_call "do not use --" do 34 | type :essential 35 | called_fn name: :-- 36 | comment Constants.list_ops_do_not_use_list_functions() 37 | end 38 | 39 | assert_no_call "do not use hd" do 40 | type :essential 41 | called_fn name: :hd 42 | comment Constants.list_ops_do_not_use_list_functions() 43 | end 44 | 45 | assert_no_call "do not use tl" do 46 | type :essential 47 | called_fn name: :tl 48 | comment Constants.list_ops_do_not_use_list_functions() 49 | end 50 | 51 | assert_no_call "do not use in" do 52 | type :essential 53 | called_fn name: :in 54 | comment Constants.list_ops_do_not_use_list_functions() 55 | end 56 | 57 | assert_no_call "do not use for" do 58 | type :essential 59 | called_fn name: :for 60 | comment Constants.list_ops_do_not_use_list_functions() 61 | end 62 | 63 | assert_no_call "do not use Kernel.length" do 64 | type :essential 65 | called_fn module: Kernel, name: :length 66 | comment Constants.list_ops_do_not_use_list_functions() 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/log_level.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.LogLevel do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module for the concept exercise Log Level 4 | """ 5 | 6 | use ElixirAnalyzer.ExerciseTest 7 | alias ElixirAnalyzer.Constants 8 | 9 | assert_call "to_label uses cond/1" do 10 | type :essential 11 | comment Constants.log_level_use_cond() 12 | calling_fn module: LogLevel, name: :to_label 13 | called_fn name: :cond 14 | end 15 | 16 | assert_call "alert_recipient uses cond/1" do 17 | type :essential 18 | comment Constants.log_level_use_cond() 19 | calling_fn module: LogLevel, name: :alert_recipient 20 | called_fn name: :cond 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/name_badge.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.NameBadge do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module for the concept exercise NameBadge 4 | """ 5 | 6 | use ElixirAnalyzer.ExerciseTest 7 | alias ElixirAnalyzer.Constants 8 | 9 | feature "Use an if macro" do 10 | find :any 11 | type :essential 12 | comment Constants.name_badge_use_if() 13 | 14 | form do 15 | if(_ignore, do: _ignore) 16 | end 17 | 18 | form do 19 | if(_ignore, do: _ignore, else: _ignore) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/need_for_speed.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.NeedForSpeed do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module for the concept exercise NeedForSpeed 4 | """ 5 | 6 | alias ElixirAnalyzer.Constants 7 | use ElixirAnalyzer.ExerciseTest 8 | 9 | feature "IO imports with :only option" do 10 | type :actionable 11 | comment Constants.need_for_speed_import_IO_with_only() 12 | 13 | form do 14 | import IO, only: _ignore 15 | end 16 | end 17 | 18 | feature "ANSI imports with :except option" do 19 | type :actionable 20 | comment Constants.need_for_speed_import_ANSI_with_except() 21 | 22 | form do 23 | import IO.ANSI, except: _ignore 24 | end 25 | end 26 | 27 | feature "given code wasn't modified" do 28 | type :informative 29 | comment Constants.need_for_speed_do_not_modify_code() 30 | 31 | form do 32 | def print_race(%Race{} = race) do 33 | puts(""" 34 | 🏁 #{race.title} 🏁 35 | Status: #{Race.display_status(race)} 36 | Distance: #{Race.display_distance(race)} 37 | 38 | Contestants: 39 | """) 40 | 41 | race.cars 42 | |> Enum.sort_by(&(-1 * &1.distance_driven_in_meters)) 43 | |> Enum.with_index() 44 | |> Enum.each(fn {car, index} -> print_car(car, index + 1) end) 45 | end 46 | 47 | defp print_car(%Car{} = car, index) do 48 | color = color(car) 49 | 50 | puts(""" 51 | #{index}. #{color}#{car.nickname}#{default_color()} 52 | Distance: #{Car.display_distance(car)} 53 | Battery: #{Car.display_battery(car)} 54 | """) 55 | end 56 | 57 | defp color(%Car{} = car) do 58 | case car.color do 59 | :red -> red() 60 | :blue -> cyan() 61 | :green -> green() 62 | end 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/pacman_rules.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.PacmanRules do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module for the concept exercise Pacman Rules 4 | """ 5 | 6 | alias ElixirAnalyzer.Constants 7 | 8 | use ElixirAnalyzer.ExerciseTest 9 | 10 | feature "suggests using strictly boolean operators" do 11 | find :none 12 | type :actionable 13 | comment Constants.pacman_rules_use_strictly_boolean_operators() 14 | 15 | form do 16 | _ignore && _ignore 17 | end 18 | 19 | form do 20 | Kernel.&&(_ignore, _ignore) 21 | end 22 | 23 | form do 24 | _ignore || _ignore 25 | end 26 | 27 | form do 28 | Kernel.||(_ignore, _ignore) 29 | end 30 | 31 | form do 32 | !_ignore 33 | end 34 | 35 | form do 36 | Kernel.!(_ignore) 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/remote_control_car.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.RemoteControlCar do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module for the exercise Remote Control Car 4 | """ 5 | 6 | alias ElixirAnalyzer.Constants 7 | 8 | use ElixirAnalyzer.ExerciseTest 9 | 10 | feature "new uses default parameter" do 11 | find :any 12 | type :actionable 13 | comment Constants.remote_control_car_use_default_argument() 14 | 15 | # function header 16 | form do 17 | def new(_ignore \\ _ignore) 18 | end 19 | 20 | # function without a guard and with a do block 21 | form do 22 | def new(_ignore \\ _ignore) do 23 | _ignore 24 | end 25 | end 26 | 27 | # function with do block 28 | form do 29 | def new(_ignore \\ _ignore) when _ignore do 30 | _ignore 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/rpg_character_sheet.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.RpgCharacterSheet do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module for the concept exercise rpg-character-sheet 4 | """ 5 | 6 | alias ElixirAnalyzer.Constants 7 | 8 | use ElixirAnalyzer.ExerciseTest, 9 | suppress_tests: [Constants.solution_debug_functions()] 10 | 11 | feature "welcome ends with IO.puts" do 12 | type :actionable 13 | find :none 14 | depth 1 15 | comment Constants.rpg_character_sheet_welcome_ends_with_IO_puts() 16 | 17 | form do 18 | def welcome() do 19 | _block_ends_with do 20 | :ok 21 | end 22 | end 23 | end 24 | end 25 | 26 | assert_call "run uses welcome" do 27 | type :actionable 28 | called_fn name: :welcome 29 | calling_fn module: RPG.CharacterSheet, name: :run 30 | comment Constants.rpg_character_sheet_run_uses_other_functions() 31 | end 32 | 33 | assert_call "run uses ask_name" do 34 | type :actionable 35 | called_fn name: :ask_name 36 | calling_fn module: RPG.CharacterSheet, name: :run 37 | comment Constants.rpg_character_sheet_run_uses_other_functions() 38 | end 39 | 40 | assert_call "run uses ask_class" do 41 | type :actionable 42 | called_fn name: :ask_class 43 | calling_fn module: RPG.CharacterSheet, name: :run 44 | comment Constants.rpg_character_sheet_run_uses_other_functions() 45 | end 46 | 47 | assert_call "run uses ask_level" do 48 | type :actionable 49 | called_fn name: :ask_level 50 | calling_fn module: RPG.CharacterSheet, name: :run 51 | comment Constants.rpg_character_sheet_run_uses_other_functions() 52 | end 53 | 54 | feature "run ends with IO.inspect" do 55 | type :essential 56 | find :one 57 | depth 1 58 | comment Constants.rpg_character_sheet_run_ends_with_IO_inspect() 59 | 60 | form do 61 | def run() do 62 | _block_ends_with do 63 | IO.inspect(_ignore) 64 | end 65 | end 66 | end 67 | 68 | form do 69 | def run() do 70 | _block_ends_with do 71 | IO.inspect(_ignore, _ignore) 72 | end 73 | end 74 | end 75 | end 76 | 77 | feature "IO.inspect uses the :label option" do 78 | type :essential 79 | find :all 80 | depth 1 81 | comment Constants.rpg_character_sheet_IO_inspect_uses_label() 82 | 83 | form do 84 | def run() do 85 | _block_includes do 86 | IO.inspect(_ignore, label: _ignore) 87 | end 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/rpn_calculator.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.RpnCalculator do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module for the concept exercise rpn-calculator 4 | """ 5 | 6 | alias ElixirAnalyzer.Constants 7 | use ElixirAnalyzer.ExerciseTest, suppress_tests: [Constants.solution_no_rescue()] 8 | end 9 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/rpn_calculator_inspection.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.RpnCalculatorInspection do 2 | @moduledoc """ 3 | This is an exercise analyzer test suite for the concept exercise rpn-calculator-inspection 4 | """ 5 | 6 | use ElixirAnalyzer.ExerciseTest 7 | alias ElixirAnalyzer.Constants 8 | 9 | assert_no_call "does not call Process.link" do 10 | type :essential 11 | called_fn module: Process, name: :link 12 | comment Constants.rpn_calculator_inspection_use_start_link() 13 | end 14 | 15 | assert_call "calls Task.start_link" do 16 | type :essential 17 | called_fn module: Task, name: :start_link 18 | comment Constants.rpn_calculator_inspection_use_start_link() 19 | suppress_if "calls spawn_link", :pass 20 | end 21 | 22 | assert_call "calls spawn_link" do 23 | type :essential 24 | called_fn name: :spawn_link 25 | comment Constants.rpn_calculator_inspection_use_start_link() 26 | suppress_if "calls Task.start_link", :pass 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/rpn_calculator_output.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.RpnCalculatorOutput do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module for the concept exercise rpn-calculator-output 4 | """ 5 | 6 | alias ElixirAnalyzer.Constants 7 | use ElixirAnalyzer.ExerciseTest, suppress_tests: [Constants.solution_no_rescue()] 8 | 9 | feature "uses all of try-rescue-else-after" do 10 | type :essential 11 | find :all 12 | depth 2 13 | comment Constants.rpn_calculator_output_try_rescue_else_after() 14 | 15 | form do 16 | try do 17 | _ignore 18 | rescue 19 | _ignore 20 | else 21 | _ignore 22 | after 23 | _ignore 24 | end 25 | end 26 | end 27 | 28 | feature "open must be before try" do 29 | type :essential 30 | find :all 31 | depth 2 32 | comment Constants.rpn_calculator_output_open_before_try() 33 | 34 | form do 35 | _block_includes do 36 | _ignore = _ignore.open(_ignore) 37 | try _ignore 38 | end 39 | end 40 | end 41 | 42 | feature "write must be in the try block" do 43 | type :essential 44 | find :all 45 | depth 2 46 | comment Constants.rpn_calculator_output_write_in_try() 47 | 48 | form do 49 | try do 50 | _block_includes do 51 | IO.write(_ignore, _ignore) 52 | end 53 | rescue 54 | _ignore 55 | else 56 | _ignore 57 | after 58 | _ignore 59 | end 60 | end 61 | end 62 | 63 | feature "final result must be in the else block" do 64 | type :essential 65 | find :all 66 | depth 2 67 | comment Constants.rpn_calculator_output_output_in_else() 68 | 69 | form do 70 | try do 71 | _ignore 72 | rescue 73 | _ignore 74 | else 75 | _block_includes do 76 | _ignore -> {:ok, _ignore} 77 | end 78 | after 79 | _ignore 80 | end 81 | end 82 | end 83 | 84 | feature "close must be in the after block" do 85 | type :essential 86 | find :all 87 | depth 2 88 | comment Constants.rpn_calculator_output_close_in_after() 89 | 90 | form do 91 | try do 92 | _ignore 93 | rescue 94 | _ignore 95 | else 96 | _ignore 97 | after 98 | _block_includes do 99 | _ignore.close(_ignore) 100 | end 101 | end 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/sieve.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.Sieve do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module for the concept exercise Sieve 4 | """ 5 | 6 | use ElixirAnalyzer.ExerciseTest 7 | alias ElixirAnalyzer.Constants 8 | alias ElixirAnalyzer.Source 9 | 10 | assert_no_call "does not call rem/2" do 11 | type :essential 12 | comment Constants.sieve_do_not_use_div_rem() 13 | called_fn module: Kernel, name: :rem 14 | end 15 | 16 | assert_no_call "does not call div/2" do 17 | type :essential 18 | comment Constants.sieve_do_not_use_div_rem() 19 | called_fn module: Kernel, name: :div 20 | end 21 | 22 | check_source "does not call Kernel.//2" do 23 | type :essential 24 | comment Constants.sieve_do_not_use_div_rem() 25 | 26 | check(%Source{code_ast: code_ast}) do 27 | {_, acc} = 28 | Macro.prewalk(code_ast, [], fn node, acc -> 29 | case node do 30 | # ignore `/` that are part of a function capture 31 | {:&, metadata, [{:/, metadata2, children2} | children]} -> 32 | node = 33 | {:&, metadata, 34 | [{:/, Keyword.put(metadata2, :ignore, true), children2} | children]} 35 | 36 | {node, acc} 37 | 38 | # usage: Kernel./(x, y) or &Kernel.//2 39 | {:., _, [{:__aliases__, _, [:Kernel]}, :/]} -> 40 | {node, [node | acc]} 41 | 42 | # usage: x / y 43 | {:/, metadata, _} -> 44 | if Keyword.get(metadata, :ignore) do 45 | {node, acc} 46 | else 47 | {node, [node | acc]} 48 | end 49 | 50 | _ -> 51 | {node, acc} 52 | end 53 | end) 54 | 55 | acc == [] 56 | end 57 | end 58 | 59 | assert_no_call "does not call Integer module" do 60 | type :essential 61 | comment Constants.sieve_do_not_use_div_rem() 62 | called_fn module: Integer, name: :_ 63 | end 64 | 65 | assert_no_call "does not call Float module" do 66 | type :essential 67 | comment Constants.sieve_do_not_use_div_rem() 68 | called_fn module: Float, name: :_ 69 | end 70 | 71 | assert_no_call "does not call :math module" do 72 | type :essential 73 | comment Constants.sieve_do_not_use_div_rem() 74 | called_fn module: :math, name: :_ 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/square_root.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.SquareRoot do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module for the practice exercise SquareRoot 4 | """ 5 | 6 | use ElixirAnalyzer.ExerciseTest 7 | alias ElixirAnalyzer.Constants 8 | 9 | assert_no_call "does not call :math.sqrt" do 10 | type :essential 11 | called_fn module: :math, name: :sqrt 12 | comment Constants.square_root_do_not_use_built_in_sqrt() 13 | end 14 | 15 | assert_no_call "does not call :math.pow" do 16 | type :essential 17 | called_fn module: :math, name: :pow 18 | comment Constants.square_root_do_not_use_built_in_sqrt() 19 | end 20 | 21 | assert_no_call "does not call Float.pow" do 22 | type :essential 23 | called_fn module: Float, name: :pow 24 | comment Constants.square_root_do_not_use_built_in_sqrt() 25 | end 26 | 27 | assert_no_call "do not use **" do 28 | type :essential 29 | called_fn module: Kernel, name: :** 30 | comment Constants.square_root_do_not_use_built_in_sqrt() 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/strain.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.Strain do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module for the concept exercise Strain 4 | """ 5 | 6 | use ElixirAnalyzer.ExerciseTest 7 | alias ElixirAnalyzer.Constants 8 | 9 | assert_no_call "does not call Enum.filter/2" do 10 | type :essential 11 | called_fn module: Enum, name: :filter 12 | comment Constants.strain_use_recursion() 13 | end 14 | 15 | assert_no_call "does not call Enum.reject/2" do 16 | type :essential 17 | called_fn module: Enum, name: :reject 18 | comment Constants.strain_use_recursion() 19 | end 20 | 21 | assert_no_call "does not call Stream.filter/2" do 22 | type :essential 23 | called_fn module: Stream, name: :filter 24 | comment Constants.strain_use_recursion() 25 | end 26 | 27 | assert_no_call "does not call Stream.reject/2" do 28 | type :essential 29 | called_fn module: Stream, name: :reject 30 | comment Constants.strain_use_recursion() 31 | end 32 | 33 | assert_no_call "doesn't use list comprehensions" do 34 | type :essential 35 | called_fn name: :for 36 | comment Constants.strain_use_recursion() 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/take_a_number.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.TakeANumber do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module for the concept exercise Take-A-Number 4 | """ 5 | 6 | alias ElixirAnalyzer.Constants 7 | 8 | use ElixirAnalyzer.ExerciseTest 9 | 10 | feature "forbids usage of abstractions like Agent or GenServer" do 11 | find :none 12 | type :essential 13 | comment Constants.take_a_number_do_not_use_abstractions() 14 | 15 | form do 16 | use Agent 17 | end 18 | 19 | form do 20 | use GenServer 21 | end 22 | end 23 | 24 | assert_no_call "doesn't call any Agent functions" do 25 | type :essential 26 | called_fn module: Agent, name: :_ 27 | comment Constants.take_a_number_do_not_use_abstractions() 28 | end 29 | 30 | assert_no_call "doesn't call any GenServer functions" do 31 | type :essential 32 | called_fn module: GenServer, name: :_ 33 | comment Constants.take_a_number_do_not_use_abstractions() 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/take_a_number_deluxe.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.TakeANumberDeluxe do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module for the concept exercise rpg-character-sheet 4 | """ 5 | 6 | alias ElixirAnalyzer.Constants 7 | alias ElixirAnalyzer.Source 8 | 9 | use ElixirAnalyzer.ExerciseTest 10 | 11 | feature "uses GenServer" do 12 | type :actionable 13 | find :any 14 | comment Constants.take_a_number_deluxe_use_genserver() 15 | 16 | form do 17 | use GenServer 18 | end 19 | end 20 | 21 | check_source "uses @impl GenServer" do 22 | type :actionable 23 | comment Constants.take_a_number_deluxe_annotate_impl_genserver() 24 | 25 | check(%Source{code_ast: code_ast}) do 26 | {_, %{defs_without_impls: defs_without_impls}} = 27 | Macro.prewalk( 28 | code_ast, 29 | %{defs_without_impls: [], impl?: false, defs_with_impls: []}, 30 | &find_defs_and_impls/2 31 | ) 32 | 33 | defs_without_impls == [] 34 | end 35 | end 36 | 37 | @def_ops [:def, :defp, :defmacro, :defmacrop, :defguard, :defguardp] 38 | @genserver_callbacks_in_this_exercise [:init, :handle_call, :handle_cast, :handle_info] 39 | defp find_defs_and_impls(node, acc) do 40 | acc = 41 | case node do 42 | {:@, _, [{:impl, _, [{:__aliases__, _, [:GenServer]}]}]} -> 43 | %{acc | impl?: true} 44 | 45 | {:@, _, [{:impl, _, [true]}]} -> 46 | %{acc | impl?: true} 47 | 48 | {op, _, [{function_name, _, _} | _]} when op in @def_ops -> 49 | acc = 50 | cond do 51 | function_name in @genserver_callbacks_in_this_exercise and 52 | function_name not in acc.defs_with_impls and !acc.impl? -> 53 | %{acc | defs_without_impls: [function_name | acc.defs_without_impls]} 54 | 55 | acc.impl? -> 56 | %{acc | defs_with_impls: [function_name | acc.defs_with_impls]} 57 | 58 | true -> 59 | acc 60 | end 61 | 62 | %{acc | impl?: false} 63 | 64 | _ -> 65 | acc 66 | end 67 | 68 | {node, acc} 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/top_secret.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.TopSecret do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module for the concept exercise Top Secret 4 | """ 5 | 6 | alias ElixirAnalyzer.Constants 7 | use ElixirAnalyzer.ExerciseTest 8 | 9 | assert_call "decode_secret_message/1 uses to_ast/1" do 10 | type :essential 11 | calling_fn module: TopSecret, name: :decode_secret_message 12 | called_fn module: TopSecret, name: :to_ast 13 | comment Constants.top_secret_function_reuse() 14 | end 15 | 16 | assert_call "decode_secret_message/1 uses decode_secret_message_part/2" do 17 | type :essential 18 | calling_fn module: TopSecret, name: :decode_secret_message 19 | called_fn module: TopSecret, name: :decode_secret_message_part 20 | comment Constants.top_secret_function_reuse() 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/elixir_analyzer/test_suite/wine_cellar.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.WineCellar do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module for the concept exercise Wine Cellar 4 | """ 5 | 6 | use ElixirAnalyzer.ExerciseTest 7 | alias ElixirAnalyzer.Constants 8 | 9 | assert_call "uses Keyword.get_values/2 to filter by color" do 10 | type :essential 11 | called_fn module: Keyword, name: :get_values 12 | comment Constants.wine_cellar_use_keyword_get_values() 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :elixir_analyzer, 7 | version: "0.1.0", 8 | elixir: "~> 1.18", 9 | elixirc_paths: elixirc_paths(Mix.env()), 10 | start_permanent: Mix.env() == :prod, 11 | # Turn off protocol consolidation to avoid warning in analyzed code 12 | consolidate_protocols: false, 13 | deps: deps(), 14 | escript: escript(), 15 | preferred_cli_env: [ 16 | # run dialyzer in test env so that files in test/support also get checked 17 | dialyzer: :test 18 | ], 19 | dialyzer: [ 20 | plt_core_path: "priv/plts", 21 | plt_file: {:no_warn, "priv/plts/eventstore.plt"} 22 | # important note: 23 | # The option ignore_warnings only works when running `mix dialyzer` (from `dialyxir`), 24 | # it DOES NOT work when ElixirLS runs dialyzer. 25 | # When using ElixirLS with VSCode reporting dialyzer warnings, obscuring code feedback. 26 | # So to ensure the best experience for devs using VSCode, try to disable dialyzer 27 | # warnings using the `@dialyzer` module attribute 28 | ], 29 | test_coverage: [tool: ExCoveralls], 30 | preferred_cli_env: [ 31 | coveralls: :test, 32 | "coveralls.detail": :test, 33 | "coveralls.post": :test, 34 | "coveralls.html": :test 35 | ] 36 | ] 37 | end 38 | 39 | # Run "mix help compile.app" to learn about applications. 40 | def application do 41 | [ 42 | extra_applications: [:logger, :ex_unit] 43 | ] 44 | end 45 | 46 | # Run "mix help deps" to learn about dependencies. 47 | defp deps do 48 | [ 49 | {:jason, "~> 1.2"}, 50 | {:credo, "~> 1.6", only: [:dev, :test], runtime: false}, 51 | {:dialyxir, "~> 1.0", only: [:dev, :test], runtime: false}, 52 | {:excoveralls, "~> 0.14", only: :test} 53 | ] 54 | end 55 | 56 | defp elixirc_paths(:test), do: ["lib", "test/support"] 57 | defp elixirc_paths(_), do: ["lib"] 58 | 59 | defp escript do 60 | [main_module: ElixirAnalyzer.CLI, strip_beams: [keep: ["ExCk"]]] 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /priv/plts/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exercism/elixir-analyzer/bb56b9540f00cecb71b6e940d54e5d9a5ac64b3b/priv/plts/.keep -------------------------------------------------------------------------------- /test/elixir_analyzer/comment_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.CommentsTest do 2 | use ExUnit.Case 3 | 4 | alias ElixirAnalyzer.Comment 5 | 6 | @types ~w(essential actionable informative celebratory)a 7 | 8 | describe "supported_type?/1" do 9 | for type <- @types do 10 | test "#{type} passes" do 11 | assert Comment.supported_type?(unquote(type)) 12 | end 13 | end 14 | 15 | test "other atoms are not supported" do 16 | refute Comment.supported_type?(:super_actionable) 17 | end 18 | end 19 | 20 | describe "supported_types/0" do 21 | test "lists all types" do 22 | assert Comment.supported_types() == @types 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/elixir_analyzer/constants_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.ConstantsTest do 2 | use ExUnit.Case 3 | doctest ElixirAnalyzer 4 | 5 | alias ElixirAnalyzer.Constants 6 | alias ElixirAnalyzer.Support 7 | 8 | setup_all do 9 | Application.ensure_all_started(:inets) 10 | Application.ensure_all_started(:ssl) 11 | 12 | :ok 13 | end 14 | 15 | test "check mock constant" do 16 | assert Support.Constants.mock_constant() == "mock.constant" 17 | end 18 | 19 | describe "if comment exists at exercism/website-copy" do 20 | @comments Constants.list_of_all_comments() 21 | @website_copy_url "https://github.com/exercism/website-copy/blob/master/analyzer-comments/" 22 | @file_ext ".md" 23 | 24 | for comment <- @comments do 25 | @comment comment 26 | @tag :external 27 | test "#{@comment} exists" do 28 | request_path = String.replace(@comment, ".", "/") 29 | request_url = "#{@website_copy_url}#{request_path}#{@file_ext}" |> to_charlist() 30 | 31 | {status, status_msg} = get_comment_with_retry_on_rate_limit_error(request_url) 32 | 33 | assert {status, status_msg, @comment} == {200, ~c"OK", @comment} 34 | end 35 | end 36 | end 37 | 38 | defp get_comment_with_retry_on_rate_limit_error(request_url) do 39 | {:ok, {{~c"HTTP/1.1", status, status_msg}, _headers, _body}} = 40 | :httpc.request(:head, {request_url, []}, [], []) 41 | 42 | if status == 429 do 43 | :timer.sleep(300) 44 | get_comment_with_retry_on_rate_limit_error(request_url) 45 | else 46 | {status, status_msg} 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/elixir_analyzer/exercise_test/assert_call/function_parentheses_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.ExerciseTest.AssertCall.FunctionParenthesesTest do 2 | use ElixirAnalyzer.ExerciseTestCase, 3 | exercise_test_module: 4 | ElixirAnalyzer.Support.AnalyzerVerification.AssertCall.FunctionParentheses 5 | 6 | test_exercise_analysis "defining run/0", 7 | comments_exclude: [ 8 | "did not match def run()", 9 | "did not match def run" 10 | ] do 11 | [ 12 | defmodule MyModule do 13 | def run do 14 | :ok 15 | end 16 | end, 17 | defmodule MyModule do 18 | def run() do 19 | :ok 20 | end 21 | end 22 | ] 23 | end 24 | 25 | test_exercise_analysis "assigning run/0", 26 | comments_exclude: [ 27 | "did not match run()", 28 | "did not match run" 29 | ] do 30 | [ 31 | defmodule MyModule do 32 | def sprint() do 33 | a = run() 34 | end 35 | end, 36 | defmodule MyModule do 37 | def sprint() do 38 | a = run 39 | end 40 | end 41 | ] 42 | end 43 | 44 | test_exercise_analysis "using run/0 in a pipe", 45 | comments_exclude: [ 46 | "did not match run() in a pipe", 47 | "did not match run in a pipe" 48 | ] do 49 | [ 50 | defmodule MyModule do 51 | def sprint() do 52 | a = :start |> run() 53 | end 54 | end, 55 | defmodule MyModule do 56 | def sprint() do 57 | a = :start |> run() |> jump() 58 | end 59 | end, 60 | defmodule MyModule do 61 | def sprint() do 62 | a = :start |> run 63 | end 64 | end, 65 | defmodule MyModule do 66 | def sprint() do 67 | a = :start |> run |> jump() 68 | end 69 | end 70 | ] 71 | end 72 | 73 | test_exercise_analysis "not using any run/0", 74 | comments_include: [ 75 | "did not match def run()", 76 | "did not match def run", 77 | "did not match run()", 78 | "did not match run", 79 | "did not match run() in a pipe", 80 | "did not match run in a pipe" 81 | ] do 82 | [ 83 | defmodule MyModule do 84 | def sprint() do 85 | a = :start |> sprint() |> jump() 86 | end 87 | end 88 | ] 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /test/elixir_analyzer/exercise_test/assert_call/module_level_call_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.ExerciseTest.AssertCall.ModuleLevelCallTest do 2 | use ElixirAnalyzer.ExerciseTestCase, 3 | exercise_test_module: ElixirAnalyzer.Support.AnalyzerVerification.AssertCall.ModuleLevelCall 4 | 5 | test_exercise_analysis "calls in function bodies count", comments: [] do 6 | [ 7 | defmodule AssertCallVerification do 8 | def main_function() do 9 | [0, 1] 10 | |> Enum.map(&(&1 + 1)) 11 | |> List.to_tuple() 12 | |> elem(0) 13 | end 14 | end, 15 | defmodule AssertCallVerification do 16 | defp main_function() do 17 | [0, 1] 18 | |> Enum.map(&(&1 + 1)) 19 | |> List.to_tuple() 20 | |> elem(0) 21 | end 22 | end 23 | ] 24 | end 25 | 26 | test_exercise_analysis "calls in macro bodies count if no calling_fn specified", 27 | comments: ["didn't find any call to List.to_tuple/1 from main_function/0"] do 28 | [ 29 | defmodule AssertCallVerification do 30 | defmacro main_function() do 31 | [0, 1] 32 | |> Enum.map(&(&1 + 1)) 33 | |> List.to_tuple() 34 | |> elem(0) 35 | end 36 | end, 37 | defmodule AssertCallVerification do 38 | defmacrop main_function() do 39 | [0, 1] 40 | |> Enum.map(&(&1 + 1)) 41 | |> List.to_tuple() 42 | |> elem(0) 43 | end 44 | end 45 | ] 46 | end 47 | 48 | test_exercise_analysis "module level calls do not count", 49 | comments: [ 50 | "didn't find any call to Enum.map/2 from anywhere", 51 | "didn't find any call to List.to_tuple/1 from main_function/0" 52 | ] do 53 | [ 54 | defmodule AssertCallVerification do 55 | def main_function() do 56 | 1 57 | end 58 | end, 59 | defmodule AssertCallVerification do 60 | {@one, @two} = List.to_tuple([1, 2]) 61 | 62 | def main_function() do 63 | @one 64 | end 65 | end, 66 | defmodule AssertCallVerification do 67 | {@one, @two} = 68 | [0, 1] 69 | |> Enum.map(&(&1 + 1)) 70 | |> List.to_tuple() 71 | 72 | def main_function() do 73 | @one 74 | end 75 | end 76 | ] 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /test/elixir_analyzer/exercise_test/comment_order_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.ExerciseTest.CommentOrderTest do 2 | use ElixirAnalyzer.ExerciseTestCase, 3 | exercise_test_module: ElixirAnalyzer.Support.AnalyzerVerification.CommentOrder, 4 | unsorted_comments: true 5 | 6 | test_exercise_analysis "triggering module", 7 | # Comments in order of importance 8 | comments: ["celebratory", "essential", "actionable", "informative"] do 9 | defmodule TriggeringtModule do 10 | def foo() do 11 | :celebratory 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/elixir_analyzer/exercise_test/common_checks/compiler_warnings_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.ExerciseTest.CommonChecks.CompilerWarningsTest do 2 | use ExUnit.Case 3 | alias ElixirAnalyzer.ExerciseTest.CommonChecks.CompilerWarnings 4 | 5 | test "Implementing a protocol doesn't trigger a compiler warning" do 6 | filepath = "test_data/clock/perfect_solution/lib/clock.ex" 7 | assert CompilerWarnings.run([filepath]) == [] 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/elixir_analyzer/exercise_test/common_checks/last_line_assignment_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.Support.AnalyzerVerification.LastLineAssignment do 2 | use ElixirAnalyzer.ExerciseTest 3 | end 4 | 5 | defmodule ElixirAnalyzer.ExerciseTest.CommonChecks.LastLineAssignmentTest do 6 | use ElixirAnalyzer.ExerciseTestCase, 7 | exercise_test_module: ElixirAnalyzer.Support.AnalyzerVerification.LastLineAssignment 8 | 9 | alias ElixirAnalyzer.Constants 10 | 11 | test_exercise_analysis "Solutions without a last line assignment", 12 | comments: [] do 13 | [ 14 | defmodule MyModule do 15 | def ok() do 16 | :ok 17 | end 18 | 19 | defp two() do 20 | two = 1 + 1 21 | two 22 | end 23 | 24 | defmacro nothing() do 25 | a = nil 26 | 27 | quote do 28 | unquote(a) 29 | end 30 | end 31 | 32 | defmacrop add_macro() do 33 | a = 1 + 1 34 | 35 | quote do 36 | unquote(a) 37 | end 38 | end 39 | end 40 | ] 41 | end 42 | 43 | test_exercise_analysis "Solutions with a last line assignment", 44 | comments: [Constants.solution_last_line_assignment()] do 45 | [ 46 | defmodule MyModule do 47 | def ok() do 48 | a = :ok 49 | end 50 | end, 51 | defmodule MyModule do 52 | def ok() do 53 | a = :ok |> Atom.to_string() 54 | end 55 | end, 56 | defmodule MyModule do 57 | defp two() do 58 | _a = 1 + 1 59 | end 60 | end, 61 | defmodule MyModule do 62 | defmacro nothing() do 63 | a = nil 64 | 65 | quote = 66 | quote do 67 | unquote(a) 68 | end 69 | end 70 | end, 71 | defmodule MyModule do 72 | defmacrop add_macro() do 73 | a = 1 + 1 74 | 75 | _quote = 76 | quote do 77 | unquote(a) 78 | end 79 | end 80 | end 81 | ] 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /test/elixir_analyzer/exercise_test/common_checks/list_prepend_head_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.Support.AnalyzerVerification.ListPrependHead do 2 | use ElixirAnalyzer.ExerciseTest 3 | end 4 | 5 | defmodule ElixirAnalyzer.ExerciseTest.CommonChecks.ListPrependHeadTest do 6 | use ElixirAnalyzer.ExerciseTestCase, 7 | exercise_test_module: ElixirAnalyzer.Support.AnalyzerVerification.ListPrependHead 8 | 9 | alias ElixirAnalyzer.Constants 10 | 11 | test_exercise_analysis "Solutions using [h | t] to prepend to list", 12 | comments: [] do 13 | [ 14 | defmodule MyModule do 15 | def prepend_ok(list) do 16 | [:ok | list] 17 | end 18 | end, 19 | defmodule MyModule do 20 | def concat_equal_lists() do 21 | [1, 2, 3] ++ [4, 5, 6] 22 | end 23 | end, 24 | defmodule MyModule do 25 | def append_element() do 26 | [1, 2, 3, 4, 5] ++ [6] 27 | end 28 | end 29 | ] 30 | end 31 | 32 | test_exercise_analysis "Solutions using [h] ++ t to prepend to list", 33 | comments: [Constants.solution_list_prepend_head()] do 34 | [ 35 | defmodule MyModule do 36 | def prepend_ok(list) do 37 | [:ok] ++ [:foo] 38 | end 39 | end, 40 | defmodule MyModule do 41 | def foo_list(), do: [:foo, :bar, :baz] 42 | 43 | def prepend_ok(list) do 44 | [:ok] ++ foo_list() 45 | end 46 | end, 47 | defmodule MyModule do 48 | def foo_list(), do: [:foo, :bar, :baz] 49 | 50 | def prepend_ok(list) do 51 | foo_list() |> (&([:ok] ++ &1)) 52 | end 53 | end, 54 | defmodule MyModule do 55 | def foo_list(), do: [:foo, :bar, :baz] 56 | 57 | def prepend_ok(list) do 58 | :ok |> (&([&1] ++ foo_list())) 59 | end 60 | end 61 | ] 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/elixir_analyzer/exercise_test/common_checks/uncommon_errors_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.Support.AnalyzerVerification.UncommonErrors do 2 | use ElixirAnalyzer.ExerciseTest 3 | end 4 | 5 | defmodule ElixirAnalyzer.ExerciseTest.CommonChecks.UncommonErrorsTest do 6 | use ElixirAnalyzer.ExerciseTestCase, 7 | exercise_test_module: ElixirAnalyzer.Support.AnalyzerVerification.UncommonErrors 8 | 9 | alias ElixirAnalyzer.Constants 10 | 11 | describe "FunctionClauseError" do 12 | test_exercise_analysis "solutions which do not raise FunctionClauseError", 13 | comments: [] do 14 | [ 15 | defmodule MyModule do 16 | def f(n), do: :ok 17 | end, 18 | defmodule MyModule do 19 | def f(1), do: :ok 20 | def g(), do: f(2) 21 | end 22 | ] 23 | end 24 | 25 | test_exercise_analysis "solutions which raise FunctionClauseError", 26 | comments: [Constants.solution_raise_fn_clause_error()] do 27 | [ 28 | defmodule MyModule do 29 | def f(1), do: :ok 30 | def f(_), do: raise(FunctionClauseError) 31 | end, 32 | defmodule MyModule do 33 | def f(1), do: :ok 34 | def f(_), do: raise(%FunctionClauseError{}) 35 | end 36 | ] 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/elixir_analyzer/exercise_test/common_checks/unless_with_else.exs: -------------------------------------------------------------------------------- 1 | # credo:disable-for-this-file Credo.Check.Refactor.UnlessWithElse 2 | defmodule ElixirAnalyzer.Support.AnalyzerVerification.UnlessWithElse do 3 | use ElixirAnalyzer.ExerciseTest 4 | end 5 | 6 | defmodule ElixirAnalyzer.ExerciseTest.CommonChecks.UnlessWithElseTest do 7 | use ElixirAnalyzer.ExerciseTestCase, 8 | exercise_test_module: ElixirAnalyzer.Support.AnalyzerVerification.UnlessWithElse 9 | 10 | alias ElixirAnalyzer.Constants 11 | 12 | test_exercise_analysis "solutions which do not use unless/else", comments: [] do 13 | [ 14 | defmodule MyModule do 15 | def unless_simple() do 16 | unless true do 17 | :error 18 | end 19 | end 20 | 21 | def unless_simple_keyword_list() do 22 | unless true, do: :error 23 | end 24 | 25 | def if_else() do 26 | if true do 27 | :ok 28 | else 29 | :error 30 | end 31 | end 32 | 33 | def if_else_keyword_list() do 34 | if true, do: :ok, else: :error 35 | end 36 | end 37 | ] 38 | end 39 | 40 | test_exercise_analysis "solutions which use unless/else", 41 | comments: [Constants.solution_unless_with_else()] do 42 | [ 43 | defmodule MyModule do 44 | def unless_else() do 45 | unless true do 46 | :error 47 | else 48 | :ok 49 | end 50 | end 51 | end, 52 | defmodule MyModule do 53 | def unless_else_keyword_list() do 54 | unless true, do: :error, else: :ok 55 | end 56 | end 57 | ] 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/elixir_analyzer/log_formatter_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.LogFormatterTest do 2 | use ExUnit.Case 3 | 4 | test "formatting a message" do 5 | assert ElixirAnalyzer.LogFormatter.format(:warn, "hi", {{2021, 12, 4}, {11, 59, 12, 0}}, []) == 6 | "# 2021-12-04T11:59:12.000Z [] [warn] hi\n" 7 | end 8 | 9 | test "formatting failure" do 10 | assert ElixirAnalyzer.LogFormatter.format(:warn, "hi", :not_a_timestamp, []) == 11 | "could not format message: {:warn, \"hi\", :not_a_timestamp, []}\n" 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/elixir_analyzer/test_suite/language_list_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.ExerciseTest.LanguageListTest do 2 | use ElixirAnalyzer.ExerciseTestCase, 3 | exercise_test_module: ElixirAnalyzer.TestSuite.LanguageList 4 | 5 | test_exercise_analysis "example solution", 6 | comments: [] do 7 | defmodule LanguageList do 8 | def new, do: [] 9 | 10 | def add(list, language), do: [language | list] 11 | 12 | def remove([_removed_language | list]), do: list 13 | 14 | def first([first_language | _list]), do: first_language 15 | 16 | def count(list), do: length(list) 17 | 18 | def functional_list?(list), do: "Elixir" in list 19 | end 20 | end 21 | 22 | test_exercise_analysis "forbids usage of the Enum", 23 | comments: [Constants.language_list_do_not_use_enum()] do 24 | [ 25 | defmodule LanguageList do 26 | def first(list), do: Enum.at(list, 0) 27 | end, 28 | defmodule LanguageList do 29 | import Enum 30 | def first(list), do: at(list, 0) 31 | end, 32 | defmodule LanguageList do 33 | import Enum, only: [at: 2] 34 | def first(list), do: at(list, 0) 35 | end, 36 | defmodule LanguageList do 37 | alias Enum, as: E 38 | def first(list), do: E.at(list, 0) 39 | end, 40 | defmodule LanguageList do 41 | def count(list), do: Enum.count(list) 42 | end, 43 | defmodule LanguageList do 44 | import Enum 45 | def count(list), do: count(list) 46 | end, 47 | defmodule LanguageList do 48 | import Enum, only: [count: 1] 49 | def count(list), do: count(list) 50 | end, 51 | defmodule LanguageList do 52 | alias Enum, as: E 53 | def count(list), do: E.count(list) 54 | end, 55 | defmodule LanguageList do 56 | def functional_list?(list), do: Enum.any?(list, &(&1 == "Elixir")) 57 | end, 58 | defmodule LanguageList do 59 | def functional_list?(list), do: Enum.filter(list, &(&1 == "Elixir")) != [] 60 | end, 61 | defmodule LanguageList do 62 | import Enum 63 | def functional_list?(list), do: any?(list, &(&1 == "Elixir")) 64 | end, 65 | defmodule LanguageList do 66 | import Enum, only: [any?: 2] 67 | def functional_list?(list), do: any?(list, &(&1 == "Elixir")) 68 | end, 69 | defmodule LanguageList do 70 | alias Enum, as: E 71 | def functional_list?(list), do: E.any?(list, &(&1 == "Elixir")) 72 | end 73 | ] 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /test/elixir_analyzer/test_suite/leap_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.TestSuite.LeapTest do 2 | use ElixirAnalyzer.ExerciseTestCase, 3 | exercise_test_module: ElixirAnalyzer.TestSuite.Leap 4 | 5 | test_exercise_analysis "example solution", 6 | comments: [] do 7 | defmodule Year do 8 | def leap_year?(year) do 9 | div4?(year) && div100?(year) == div400?(year) 10 | end 11 | 12 | defp divides?(dividend, divisor), do: rem(dividend, divisor) == 0 13 | defp div4?(dividend), do: divides?(dividend, 4) 14 | defp div100?(dividend), do: divides?(dividend, 100) 15 | defp div400?(dividend), do: divides?(dividend, 400) 16 | end 17 | end 18 | 19 | test_exercise_analysis "forbids usage of :calendar Erlang module", 20 | comments_include: [Constants.leap_erlang_calendar()] do 21 | [ 22 | defmodule Year do 23 | def leap_year?(year) do 24 | :calendar.is_leap_year(year) 25 | end 26 | end, 27 | defmodule Year do 28 | import :calendar 29 | 30 | def leap_year?(year) do 31 | is_leap_year(year) 32 | end 33 | end, 34 | defmodule Year do 35 | alias :calendar, as: C 36 | 37 | def leap_year?(year) do 38 | C.is_leap_year(year) 39 | end 40 | end 41 | ] 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/elixir_analyzer/test_suite/log_level_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.ExerciseTest.LogLevelTest do 2 | use ElixirAnalyzer.ExerciseTestCase, 3 | exercise_test_module: ElixirAnalyzer.TestSuite.LogLevel 4 | 5 | test_exercise_analysis "example solution", 6 | comments: [Constants.solution_same_as_exemplar()] do 7 | [ 8 | defmodule LogLevel do 9 | def to_label(level, legacy?) do 10 | cond do 11 | level === 0 && not legacy? -> :trace 12 | level === 1 -> :debug 13 | level === 2 -> :info 14 | level === 3 -> :warning 15 | level === 4 -> :error 16 | level === 5 && not legacy? -> :fatal 17 | true -> :unknown 18 | end 19 | end 20 | 21 | def alert_recipient(level, legacy?) do 22 | label = to_label(level, legacy?) 23 | 24 | cond do 25 | label == :fatal -> :ops 26 | label == :error -> :ops 27 | label == :unknown && legacy? -> :dev1 28 | label == :unknown -> :dev2 29 | true -> false 30 | end 31 | end 32 | end 33 | ] 34 | end 35 | 36 | test_exercise_analysis "requires usage of cond/1", 37 | comments: [Constants.log_level_use_cond()] do 38 | [ 39 | defmodule LogLevel do 40 | def to_label(0, false), do: :trace 41 | def to_label(1, _), do: :debug 42 | def to_label(2, _), do: :info 43 | def to_label(3, _), do: :warning 44 | def to_label(4, _), do: :error 45 | def to_label(5, false), do: :fatal 46 | def to_label(_, _), do: :unknown 47 | 48 | def alert_recipient(:fatal, _), do: :ops 49 | def alert_recipient(:error, _), do: :ops 50 | def alert_recipient(:unknown, true), do: :dev1 51 | def alert_recipient(:unknown, _), do: :dev2 52 | def alert_recipient(_, _), do: false 53 | end 54 | ] 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /test/elixir_analyzer/test_suite/strain_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.ExerciseTest.StrainTest do 2 | use ElixirAnalyzer.ExerciseTestCase, 3 | exercise_test_module: ElixirAnalyzer.TestSuite.Strain 4 | 5 | test_exercise_analysis "example solution", 6 | comments: [] do 7 | defmodule Strain do 8 | def keep(list, fun) do 9 | do_keep(list, fun, []) 10 | end 11 | 12 | defp do_keep([], _, results), do: Enum.reverse(results) 13 | 14 | defp do_keep([head | tail], fun, results) do 15 | case apply(fun, [head]) do 16 | true -> do_keep(tail, fun, [head | results]) 17 | _ -> do_keep(tail, fun, results) 18 | end 19 | end 20 | 21 | def discard(list, fun) do 22 | do_discard(list, fun, []) 23 | end 24 | 25 | defp do_discard([], _, results), do: Enum.reverse(results) 26 | 27 | defp do_discard([head | tail], fun, results) do 28 | case apply(fun, [head]) do 29 | true -> do_discard(tail, fun, results) 30 | _ -> do_discard(tail, fun, [head | results]) 31 | end 32 | end 33 | end 34 | end 35 | 36 | describe "forbids any method of iteration other than recursion" do 37 | test_exercise_analysis "detects Enum", 38 | comments: [Constants.strain_use_recursion()] do 39 | defmodule Strain do 40 | def keep(list, fun), do: Enum.filter(list, fun) 41 | def discard(list, fun), do: Enum.reject(list, fun) 42 | end 43 | end 44 | 45 | test_exercise_analysis "detects Stream", 46 | comments: [Constants.strain_use_recursion()] do 47 | defmodule Strain do 48 | def keep(list, fun), do: Stream.filter(list, fun) 49 | def discard(list, fun), do: Stream.reject(list, fun) 50 | end 51 | end 52 | 53 | test_exercise_analysis "detects list comprehensions", 54 | comments: [Constants.strain_use_recursion()] do 55 | defmodule Strain do 56 | def keep(list, fun) do 57 | for e <- list, fun.(e), do: e 58 | end 59 | 60 | def discard(list, fun) do 61 | for e <- list, !fun.(e), do: e 62 | end 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /test/elixir_analyzer/test_suite/wine_cellar_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.ExerciseTest.WineCellarTest do 2 | use ElixirAnalyzer.ExerciseTestCase, 3 | exercise_test_module: ElixirAnalyzer.TestSuite.WineCellar 4 | 5 | test_exercise_analysis "example solution", 6 | comments: [] do 7 | [ 8 | defmodule WineCellar do 9 | def filter(cellar, color, opts \\ []) do 10 | wines = Keyword.get_values(cellar, color) 11 | year = Keyword.get(opts, :year) 12 | country = Keyword.get(opts, :country) 13 | 14 | wines = if year, do: filter_by_year(wines, year), else: wines 15 | if country, do: filter_by_country(wines, country), else: wines 16 | end 17 | end, 18 | defmodule WineCellar do 19 | def filter(cellar, color, opts \\ []) 20 | 21 | def filter(cellar, color, []), do: Keyword.get_values(cellar, color) 22 | 23 | def filter(cellar, color, [{:year, year} | opts]) do 24 | cellar 25 | |> filter(color, opts) 26 | |> filter_by_year(year) 27 | end 28 | 29 | def filter(cellar, color, [{:country, country} | opts]) do 30 | cellar 31 | |> filter(color, opts) 32 | |> filter_by_country(country) 33 | end 34 | end 35 | ] 36 | end 37 | 38 | test_exercise_analysis "requires usage of Keyword.get_values/2", 39 | comments: [Constants.wine_cellar_use_keyword_get_values()] do 40 | [ 41 | defmodule WineCellar do 42 | def filter(cellar, color, opts \\ []) do 43 | cellar 44 | |> filter_by_color(color) 45 | |> optional_filter_by_year(Keyword.get(opts, :year)) 46 | |> optional_filter_by_country(Keyword.get(opts, :country)) 47 | end 48 | 49 | defp filter_by_color([], _), do: [] 50 | 51 | defp(filter_by_color([{color, wine} | tail], color)) do 52 | [wine | filter_by_color(tail, color)] 53 | end 54 | 55 | defp filter_by_color([{_, _} | tail], color) do 56 | filter_by_color(tail, color) 57 | end 58 | end, 59 | defmodule WineCellar do 60 | def filter(cellar, color, opts \\ []) do 61 | wines = 62 | for {^color, wine} <- cellar do 63 | wine 64 | end 65 | 66 | opts 67 | |> Enum.reduce(wines, fn 68 | {:year, year}, wines -> filter_by_year(wines, year) 69 | {:country, country}, wines -> filter_by_country(wines, country) 70 | end) 71 | end 72 | end 73 | ] 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /test/support/analyzer_verification/assert_call.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.Support.AnalyzerVerification.AssertCall do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module to test the assert_call macro 4 | """ 5 | 6 | use ElixirAnalyzer.ExerciseTest 7 | 8 | assert_call "finds call to local helper function" do 9 | type :informative 10 | called_fn name: :helper 11 | comment "didn't find a local call to helper/0" 12 | end 13 | 14 | assert_call "finds call to private local helper function" do 15 | type :informative 16 | called_fn name: :private_helper 17 | comment "didn't find a local call to private_helper/0" 18 | end 19 | 20 | assert_call "finds call to local helper function within function" do 21 | type :informative 22 | calling_fn module: AssertCallVerification, name: :function 23 | called_fn name: :helper 24 | comment "didn't find a local call to helper/0 within function/0" 25 | end 26 | 27 | assert_call "finds call to private local helper function within function" do 28 | type :informative 29 | calling_fn module: AssertCallVerification, name: :function 30 | called_fn name: :private_helper 31 | comment "didn't find a local call to private_helper/0 within function/0" 32 | end 33 | 34 | assert_call "finds call to IO.puts anywhere" do 35 | type :informative 36 | called_fn module: IO, name: :puts 37 | comment "didn't find a call to IO.puts/1 anywhere in solution" 38 | end 39 | 40 | assert_call "finds call to IO.puts in function/0" do 41 | type :informative 42 | called_fn module: IO, name: :puts 43 | calling_fn module: AssertCallVerification, name: :function 44 | comment "didn't find a call to IO.puts/1 in function/0" 45 | end 46 | 47 | assert_call "finds call to any List function anywhere" do 48 | type :informative 49 | called_fn module: List, name: :_ 50 | comment "didn't find a call to a List function" 51 | end 52 | 53 | assert_call "finds call to any List function anywhere" do 54 | type :informative 55 | called_fn module: List, name: :_ 56 | calling_fn module: AssertCallVerification, name: :function 57 | comment "didn't find a call to a List function in function/0" 58 | end 59 | 60 | assert_call "allows use of constants function" do 61 | type :informative 62 | called_fn module: List, name: :_ 63 | calling_fn module: AssertCallVerification, name: :function 64 | comment ElixirAnalyzer.Support.Constants.mock_constant() 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /test/support/analyzer_verification/assert_call/erlang_modules.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.Support.AnalyzerVerification.AssertCall.Erlang do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module to test the assert_call and assert_no_call 4 | macros specifically for using Erlang modules 5 | """ 6 | 7 | use ElixirAnalyzer.ExerciseTest 8 | 9 | assert_call "finds call to :rand.normal function anywhere" do 10 | type :informative 11 | called_fn module: :rand, name: :normal 12 | comment "didn't find a call to :rand.normal anywhere in module" 13 | end 14 | 15 | assert_call "finds call to :rand.normal in function/0" do 16 | type :informative 17 | called_fn module: :rand, name: :normal 18 | calling_fn module: AssertCallVerification, name: :function 19 | comment "didn't find a call to :rand.normal/0 in function/0" 20 | end 21 | 22 | assert_call "find a call to any :rand function from anywhere" do 23 | type :informative 24 | called_fn module: :rand, name: :_ 25 | comment "didn't find any call to a :rand function" 26 | end 27 | 28 | assert_call "finds call to any :rand function in function/0" do 29 | type :informative 30 | called_fn module: :rand, name: :_ 31 | calling_fn module: AssertCallVerification, name: :function 32 | comment "didn't find a call to a :rand function in function/0" 33 | end 34 | 35 | assert_no_call "does not call :rand.normal function" do 36 | type :informative 37 | called_fn module: :rand, name: :normal 38 | comment "found a call to :rand.normal in module" 39 | end 40 | 41 | assert_no_call "does not call :rand.normal in specific function" do 42 | type :informative 43 | calling_fn module: AssertCallVerification, name: :function 44 | called_fn module: :rand, name: :normal 45 | comment "found a call to :rand.normal in function/0" 46 | end 47 | 48 | assert_no_call "does not call any function from the :rand module" do 49 | type :informative 50 | called_fn module: :rand, name: :_ 51 | comment "found a call to :rand in module" 52 | end 53 | 54 | assert_no_call "does not call any function from the :rand module in specific function" do 55 | type :informative 56 | calling_fn module: AssertCallVerification, name: :function 57 | called_fn module: :rand, name: :_ 58 | comment "found a call to :rand in function/0" 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /test/support/analyzer_verification/assert_call/function_head_call.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.Support.AnalyzerVerification.AssertCall.FunctionHeadCall do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module to test assert_call calling a function from 4 | outside any function/macro bodies. 5 | """ 6 | 7 | use ElixirAnalyzer.ExerciseTest 8 | 9 | assert_call "find a call to Kernel.>/2" do 10 | type :informative 11 | called_fn module: Kernel, name: :> 12 | comment "didn't find any call to Kernel.>/2 from anywhere" 13 | end 14 | 15 | assert_call "find a call to Kernel.is_integer/1 from main_function" do 16 | type :informative 17 | called_fn module: Kernel, name: :is_integer 18 | calling_fn module: AssertCallVerification, name: :main_function 19 | comment "didn't find any call to Kernel.is_integer/1 from main_function/1" 20 | end 21 | 22 | assert_call "find a call to |/2 from main_function" do 23 | type :informative 24 | called_fn name: :| 25 | calling_fn module: AssertCallVerification, name: :main_function 26 | comment "didn't find any call to |/2 from main_function/1" 27 | end 28 | 29 | assert_call "find a call to Kernel.@/1 from main_function" do 30 | type :informative 31 | called_fn module: Kernel, name: :@ 32 | calling_fn module: AssertCallVerification, name: :main_function 33 | comment "didn't find any call to Kernel.@/1 from main_function/1" 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/support/analyzer_verification/assert_call/function_parentheses.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.Support.AnalyzerVerification.AssertCall.FunctionParentheses do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module to test that feature accepts function calls of 4 | arity 0 with or without parentheses 5 | """ 6 | 7 | use ElixirAnalyzer.ExerciseTest 8 | 9 | feature "run/0 defined with run()" do 10 | type :essential 11 | comment "did not match def run()" 12 | 13 | form do 14 | def run() do 15 | _ignore 16 | end 17 | end 18 | end 19 | 20 | feature "run/0 defined with run" do 21 | type :essential 22 | comment "did not match def run" 23 | 24 | form do 25 | def run do 26 | _ignore 27 | end 28 | end 29 | end 30 | 31 | feature "run/0 used with run()" do 32 | type :essential 33 | comment "did not match run()" 34 | 35 | form do 36 | _ignore = run() 37 | end 38 | end 39 | 40 | feature "run/0 used with run" do 41 | type :essential 42 | comment "did not match run" 43 | 44 | form do 45 | _ignore = run 46 | end 47 | end 48 | 49 | feature "run/0 used with run() in a pipe" do 50 | type :essential 51 | comment "did not match run() in a pipe" 52 | 53 | form do 54 | _ignore |> run() 55 | end 56 | end 57 | 58 | feature "run/0 used with run in a pipe" do 59 | type :essential 60 | comment "did not match run in a pipe" 61 | 62 | form do 63 | _ignore |> run 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /test/support/analyzer_verification/assert_call/indirect_call.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.Support.AnalyzerVerification.AssertCall.IndirectCall do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module to test assert_call calling a function from 4 | a calling function via helper functions 5 | """ 6 | 7 | use ElixirAnalyzer.ExerciseTest 8 | 9 | assert_call "find a call to Elixir.Mix.Utils.read_path/1 from main_function/0" do 10 | type :informative 11 | called_fn module: Elixir.Mix.Utils, name: :read_path 12 | calling_fn module: AssertCallVerification, name: :main_function 13 | comment "didn't find any call to Elixir.Mix.Utils.read_path/1 from main_function/0" 14 | end 15 | 16 | assert_call "find a call to :math.pi from main_function/0" do 17 | type :informative 18 | called_fn module: :math, name: :pi 19 | calling_fn module: AssertCallVerification, name: :main_function 20 | comment "didn't find any call to :math.pi from main_function/0" 21 | end 22 | 23 | assert_call "find a call to final_function/1 from main_function/0" do 24 | type :informative 25 | called_fn name: :final_function 26 | calling_fn module: AssertCallVerification, name: :main_function 27 | comment "didn't find any call to final_function/1 from main_function/0" 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/support/analyzer_verification/assert_call/kernel_functions.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.Support.AnalyzerVerification.AssertCall.Kernel do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module to test the assert_call and assert_no_call 4 | macros specifically for finding Kernel functions 5 | """ 6 | 7 | use ElixirAnalyzer.ExerciseTest 8 | 9 | assert_call "finds call to Kernel.dbg/0 with explicit module" do 10 | type :informative 11 | called_fn module: Kernel, name: :dbg 12 | comment "didn't find a call to Kernel.dbg/0" 13 | end 14 | 15 | assert_call "finds call to Kernel.dbg/0 without module" do 16 | type :informative 17 | called_fn name: :dbg 18 | comment "didn't find a call to dbg/0" 19 | end 20 | 21 | assert_call "finds call to Kernel.self/0 with explicit module" do 22 | type :informative 23 | called_fn module: Kernel, name: :self 24 | comment "didn't find a call to Kernel.self/0" 25 | end 26 | 27 | assert_call "finds call to Kernel.self/0 without module" do 28 | type :informative 29 | called_fn name: :self 30 | comment "didn't find a call to self/0" 31 | end 32 | 33 | assert_call "finds call to Kernel.SpecialForms.<<>>/1 without module" do 34 | type :informative 35 | called_fn name: :<<>> 36 | comment "didn't find a call to <<>>/1" 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/support/analyzer_verification/assert_call/module_level_call.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.Support.AnalyzerVerification.AssertCall.ModuleLevelCall do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module to test assert_call calling a function from 4 | outside any function/macro bodies. 5 | """ 6 | 7 | use ElixirAnalyzer.ExerciseTest 8 | 9 | assert_call "find a call to Enum.map" do 10 | type :informative 11 | called_fn module: Enum, name: :map 12 | comment "didn't find any call to Enum.map/2 from anywhere" 13 | end 14 | 15 | assert_call "find a call to List.to_tuple from main_function" do 16 | type :informative 17 | called_fn module: List, name: :to_tuple 18 | calling_fn module: AssertCallVerification, name: :main_function 19 | comment "didn't find any call to List.to_tuple/1 from main_function/0" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/support/analyzer_verification/assert_call/module_tracking.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.Support.AnalyzerVerification.AssertCall.ModuleTracking do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module to test the assert_call and assert_no_call 4 | macros specifically from imports and aliases 5 | """ 6 | 7 | use ElixirAnalyzer.ExerciseTest 8 | 9 | assert_call "find a call to Elixir.Mix.Utils.read_path/1 from anywhere" do 10 | type :informative 11 | called_fn module: Elixir.Mix.Utils, name: :read_path 12 | comment "didn't find any call to Elixir.Mix.Utils.read_path/1" 13 | end 14 | 15 | assert_no_call "didn't call to Elixir.Mix.Utils.read_path/1 from anywhere" do 16 | type :informative 17 | called_fn module: Elixir.Mix.Utils, name: :read_path 18 | comment "found a call to Elixir.Mix.Utils.read_path/1" 19 | end 20 | 21 | assert_call "find a call to a custom MyModule.Custom.my_function/0 from anywhere" do 22 | type :informative 23 | called_fn module: MyModule.Custom, name: :my_function 24 | comment "didn't find any call to MyModule.Custom.my_function/0" 25 | end 26 | 27 | assert_no_call "didn't call custom MyModule.Custom.my_function/0 from anywhere" do 28 | type :informative 29 | called_fn module: MyModule.Custom, name: :my_function 30 | comment "found a call to MyModule.Custom.my_function/0" 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/support/analyzer_verification/assert_call/multiple_clause_functions.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.Support.AnalyzerVerification.AssertCall.MultipleClauseFunctions do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module to test the assert_call and assert_no_call 4 | macros on functions with multiple clauses, with and without guards. 5 | """ 6 | 7 | use ElixirAnalyzer.ExerciseTest 8 | 9 | assert_call "finds call to Map.new/0 in function/1" do 10 | type :informative 11 | called_fn module: Map, name: :new 12 | calling_fn module: AssertCallVerification, name: :function 13 | comment "didn't find a call to Map.new/0 in function/1" 14 | end 15 | 16 | assert_no_call "does not find call to Map.new/0 in function/1" do 17 | type :informative 18 | called_fn module: Map, name: :new 19 | calling_fn module: AssertCallVerification, name: :function 20 | comment "found a call to Map.new/0 in function/1" 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/support/analyzer_verification/assert_no_call.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.Support.AnalyzerVerification.AssertNoCall do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module to test the assert_call macro 4 | """ 5 | 6 | use ElixirAnalyzer.ExerciseTest 7 | 8 | assert_no_call "does not call local function" do 9 | type :informative 10 | called_fn name: :helper 11 | comment "found a local call to helper/0" 12 | end 13 | 14 | assert_no_call "does not call local function from specific function" do 15 | type :informative 16 | calling_fn module: AssertNoCallVerification, name: :helper 17 | called_fn name: :private_helper 18 | comment "found a local call to private_helper/0 from helper/0" 19 | end 20 | 21 | assert_no_call "does not call other Enum.map function" do 22 | type :informative 23 | called_fn module: Enum, name: :map 24 | comment "found a call to Enum.map in solution" 25 | end 26 | 27 | assert_no_call "does not call other Atom.to_string in specific function" do 28 | type :informative 29 | calling_fn module: AssertNoCallVerification, name: :helper 30 | called_fn module: Atom, name: :to_string 31 | comment "found a call to Atom.to_string/1 in helper/0 function in solution" 32 | end 33 | 34 | assert_no_call "does not call any function from List module" do 35 | type :informative 36 | called_fn module: List, name: :_ 37 | comment "don't call List module functions" 38 | end 39 | 40 | assert_no_call "does not call any function from AlternativeList module" do 41 | type :informative 42 | called_fn module: AlternativeList, name: :_ 43 | comment "don't call AlternativeList module functions" 44 | suppress_if "does not call any function from List module", :fail 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/support/analyzer_verification/check_source.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.Support.AnalyzerVerification.CheckSource do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module to test the check_soucre macro 4 | """ 5 | use ElixirAnalyzer.ExerciseTest 6 | 7 | alias ElixirAnalyzer.Source 8 | 9 | check_source "always true" do 10 | type :actionable 11 | comment "always return true" 12 | 13 | check(_source) do 14 | true 15 | end 16 | end 17 | 18 | check_source "always false" do 19 | type :actionable 20 | comment "always return false" 21 | 22 | check(_source) do 23 | false 24 | end 25 | end 26 | 27 | check_source "finds integer literal" do 28 | type :actionable 29 | comment "used integer literal from ?a to ?z" 30 | 31 | check(%Source{code_string: code_string}) do 32 | integers = Enum.map(?a..?z, &to_string/1) 33 | 34 | not Enum.any?(integers, &String.contains?(code_string, &1)) 35 | end 36 | end 37 | 38 | check_source "finds use of multiline strings" do 39 | type :actionable 40 | comment "didn't use multiline" 41 | 42 | check(%Source{code_string: code_string}) do 43 | String.contains?(code_string, ["\"\"\"", "\'\'\'"]) 44 | end 45 | end 46 | 47 | check_source "module is suitably long" do 48 | type :actionable 49 | comment "module is too short" 50 | 51 | check(%Source{code_string: code_string}) do 52 | String.length(code_string) > 20 53 | end 54 | end 55 | 56 | check_source "module is well formatted" do 57 | type :actionable 58 | comment "module is not formatted" 59 | 60 | check(%Source{code_string: code_string}) do 61 | String.trim(code_string) == Code.format_string!(code_string) |> Enum.join() 62 | end 63 | end 64 | 65 | check_source "should have an even number of bananas in the code" do 66 | type :actionable 67 | comment "even banana count" 68 | 69 | check(%Source{code_string: code_string}) do 70 | chunks = String.split(code_string, "banana", trim: false) 71 | banana_count = Enum.count(chunks) - 1 72 | 73 | if rem(banana_count, 2) == 1 do 74 | {false, %{banana_count: banana_count}} 75 | else 76 | {true, %{banana_count: banana_count}} 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /test/support/analyzer_verification/comment_order.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.Support.AnalyzerVerification.CommentOrder do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module to test the order of comments 4 | """ 5 | 6 | use ElixirAnalyzer.ExerciseTest 7 | 8 | # features are defined in a non-alphabetical, non-importance order 9 | 10 | feature "find :celebratory" do 11 | type :celebratory 12 | comment "celebratory" 13 | 14 | form do 15 | :celebratory 16 | end 17 | end 18 | 19 | feature "find :essential" do 20 | type :essential 21 | comment "essential" 22 | 23 | form do 24 | :essential 25 | end 26 | end 27 | 28 | feature "find :informative" do 29 | type :informative 30 | comment "informative" 31 | 32 | form do 33 | :informative 34 | end 35 | end 36 | 37 | feature "find :actionable" do 38 | type :actionable 39 | comment "actionable" 40 | 41 | form do 42 | :actionable 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/support/analyzer_verification/feature.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.Support.AnalyzerVerification.Feature do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module to test the feature macro 4 | """ 5 | 6 | use ElixirAnalyzer.ExerciseTest 7 | 8 | feature "calc/1 must call some other function of any arity" do 9 | type :essential 10 | # sic! even though the form looks like it requires exactly 1 argument 11 | comment "calc/1 must call some other function of any arity" 12 | 13 | form do 14 | def calc(n) do 15 | arg1 = _ignore(n, 1) 16 | n * _ignore(arg1) 17 | end 18 | end 19 | end 20 | 21 | feature "strict_calc/1 must call some other function with one arguments named arg1" do 22 | type :essential 23 | comment "strict_calc/1 must call some other function with one arguments named arg1" 24 | 25 | form do 26 | def strict_calc(n) do 27 | arg1 = _shallow_ignore(n, 1) 28 | n * _shallow_ignore(arg1) 29 | end 30 | end 31 | end 32 | 33 | feature "there must be any module attribute with any value and any name" do 34 | type :essential 35 | find :all 36 | comment "there must be any module attribute with any value and any name" 37 | 38 | form do 39 | @_ignore 40 | end 41 | end 42 | 43 | feature "there must be any module attribute with the value 42, and any module attribute with the name 'answer'" do 44 | type :essential 45 | find :all 46 | 47 | comment "there must be any module attribute with the value 42, and any module attribute with the name 'answer'" 48 | 49 | form do 50 | @_shallow_ignore 42 51 | end 52 | 53 | form do 54 | @answer _ignore 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /test/support/analyzer_verification/feature/block_includes.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.Support.AnalyzerVerification.Feature.BlockIncludes do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module to test the _block_includes feature 4 | """ 5 | 6 | use ElixirAnalyzer.ExerciseTest 7 | 8 | feature "can detect :ok" do 9 | type :essential 10 | comment "cannot detect :ok" 11 | 12 | form do 13 | _block_includes do 14 | :ok 15 | end 16 | end 17 | end 18 | 19 | feature "can detect two lines" do 20 | type :essential 21 | comment "cannot detect two lines" 22 | 23 | form do 24 | _block_includes do 25 | name = "Bob" 26 | greeting = "hi #{name}" 27 | end 28 | end 29 | end 30 | 31 | feature "can detect pattern matches" do 32 | type :essential 33 | comment "cannot detect pattern matches" 34 | 35 | form do 36 | _block_includes do 37 | :ok -> "All good" 38 | _ -> "Whoops" 39 | end 40 | end 41 | end 42 | 43 | feature "can detect functions" do 44 | type :essential 45 | comment "cannot detect functions" 46 | 47 | form do 48 | _block_includes do 49 | def foo() do 50 | _ignore 51 | end 52 | 53 | def bar(_ignore) do 54 | _ignore 55 | end 56 | end 57 | end 58 | end 59 | 60 | feature "can detect nested blocks" do 61 | type :essential 62 | comment "cannot detect nested blocks" 63 | 64 | form do 65 | _block_includes do 66 | def foo() do 67 | _block_includes do 68 | name = "Bob" 69 | greeting = "hi #{name}" 70 | end 71 | end 72 | end 73 | end 74 | end 75 | 76 | feature "cannot match a line and a block on the same level" do 77 | type :essential 78 | comment "could match a line and a block in a row" 79 | 80 | form do 81 | :hello 82 | 83 | _block_includes do 84 | :goodbye 85 | end 86 | end 87 | end 88 | 89 | feature "cannot match two blocks in a row on the same level" do 90 | type :essential 91 | comment "could use two in a row" 92 | 93 | form do 94 | _block_includes do 95 | :hello 96 | end 97 | 98 | _block_includes do 99 | :goodbye 100 | end 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /test/support/analyzer_verification/feature/pipes.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.Support.AnalyzerVerification.Feature.Pipes do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module to test feature specifically on pipes 4 | """ 5 | 6 | use ElixirAnalyzer.ExerciseTest 7 | 8 | feature "function with one parameter" do 9 | type :essential 10 | comment "not one parameter" 11 | 12 | form do 13 | foo(_ignore) 14 | end 15 | end 16 | 17 | feature "function with three parameter" do 18 | type :essential 19 | comment "not three parameter" 20 | 21 | form do 22 | foo(_ignore, _ignore, _ignore) 23 | end 24 | end 25 | 26 | feature "function with one piped parameter" do 27 | type :essential 28 | comment "not one piped parameter" 29 | 30 | form do 31 | _ignore |> foo() 32 | end 33 | end 34 | 35 | feature "function with one piped parameter (no parens)" do 36 | type :essential 37 | comment "not one piped parameter (no parens)" 38 | 39 | form do 40 | _ignore |> foo 41 | end 42 | end 43 | 44 | feature "function with three parameters with one piped" do 45 | type :essential 46 | comment "not three parameters with one piped" 47 | 48 | form do 49 | _ignore |> foo(_ignore, _ignore) 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/support/analyzer_verification/feature/string_interpolation.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.Support.AnalyzerVerification.Feature.StringInterpolation do 2 | @moduledoc """ 3 | This is an exercise analyzer extension module to test feature specifically on string interpolation 4 | """ 5 | 6 | use ElixirAnalyzer.ExerciseTest 7 | 8 | feature "normal string" do 9 | type :essential 10 | comment "normal string" 11 | 12 | form do 13 | "hello you" 14 | end 15 | end 16 | 17 | feature "string with newline in the middle" do 18 | type :essential 19 | comment "string with newline in the middle" 20 | 21 | form do 22 | "hello\nyou" 23 | end 24 | end 25 | 26 | feature "string with interpolation" do 27 | type :essential 28 | comment "string with interpolation" 29 | 30 | form do 31 | "hello #{name}" 32 | end 33 | end 34 | 35 | feature "string with interpolation and newline at the end" do 36 | type :essential 37 | comment "string with interpolation and newline at the end" 38 | 39 | form do 40 | "hello #{name}\n" 41 | end 42 | end 43 | 44 | feature "string with interpolation and newline in the middle" do 45 | type :essential 46 | comment "string with interpolation and newline in the middle" 47 | 48 | form do 49 | "hello\n#{name}" 50 | end 51 | end 52 | 53 | feature "Multiline complex string inperpolation" do 54 | type :essential 55 | comment "Multiline complex string inperpolation" 56 | 57 | form do 58 | """ 59 | hello #{name.last} 60 | how are you? 61 | """ 62 | end 63 | end 64 | 65 | feature "multiline string interpolation must match exactly" do 66 | type :essential 67 | comment "multiline string interpolation doesn't match exactly" 68 | 69 | form do 70 | """ 71 | 🏁 #{race.title} 🏁 72 | Status: #{Race.display_status(race)} 73 | Distance: #{Race.display_distance(race)} 74 | Contestants: 75 | """ 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /test/support/constants.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirAnalyzer.Support.Constants do 2 | @moduledoc false 3 | 4 | def mock_constant(), do: "mock.constant" 5 | end 6 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | ExUnit.configure(exclude: [:pending, :external]) 3 | -------------------------------------------------------------------------------- /test_data/clock/perfect_solution/.meta/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "blurb": "Implement a clock that handles times without dates.", 3 | "authors": [ 4 | "tejasbubane" 5 | ], 6 | "contributors": [ 7 | "angelikatyborska", 8 | "Cohen-Carlisle", 9 | "devonestes", 10 | "nathanchere", 11 | "neenjaw", 12 | "parkerl", 13 | "sotojuan" 14 | ], 15 | "files": { 16 | "solution": [ 17 | "lib/clock.ex" 18 | ], 19 | "test": [ 20 | "test/clock_test.exs" 21 | ], 22 | "example": [ 23 | ".meta/example.ex" 24 | ] 25 | }, 26 | "source": "Pairing session with Erin Drummond", 27 | "source_url": "https://twitter.com/ebdrummond" 28 | } 29 | -------------------------------------------------------------------------------- /test_data/clock/perfect_solution/expected_analysis.json: -------------------------------------------------------------------------------- 1 | {"comments":[],"summary":"Submission analyzed. No automated suggestions found."} 2 | -------------------------------------------------------------------------------- /test_data/clock/perfect_solution/lib/clock.ex: -------------------------------------------------------------------------------- 1 | defmodule Clock do 2 | defstruct hour: 0, minute: 0 3 | @type t :: %Clock{hour: integer, minute: integer} 4 | @type t(hour, minute) :: %Clock{hour: hour, minute: minute} 5 | 6 | @doc """ 7 | Returns a string representation of a clock: 8 | 9 | iex> Clock.new(8, 9) |> to_string 10 | "08:09" 11 | """ 12 | @spec new(integer, integer) :: Clock.t() 13 | def new(hour, minute) do 14 | rollover(%Clock{hour: hour, minute: minute}) 15 | end 16 | 17 | @doc """ 18 | Adds two clock times: 19 | 20 | iex> Clock.new(10, 0) |> Clock.add(3) |> to_string 21 | "10:03" 22 | """ 23 | @spec add(Clock.t(), integer) :: Clock.t() 24 | def add(%Clock{hour: hour, minute: minute}, add_minute) do 25 | new(hour, minute + add_minute) 26 | end 27 | 28 | defp rollover(%Clock{hour: hour, minute: minute} = clock) do 29 | case {hour, minute} do 30 | {hour, minute} when minute >= 60 -> 31 | %Clock{hour: hour + 1, minute: minute - 60} 32 | |> rollover() 33 | 34 | {hour, minute} when minute < 0 -> 35 | %Clock{hour: hour - 1, minute: minute + 60} 36 | |> rollover() 37 | 38 | {hour, minute} when hour >= 24 -> 39 | %Clock{hour: hour - 24, minute: minute} 40 | |> rollover() 41 | 42 | {hour, minute} when hour < 0 -> 43 | %Clock{hour: hour + 24, minute: minute} 44 | |> rollover() 45 | 46 | _ -> 47 | clock 48 | end 49 | end 50 | 51 | defimpl String.Chars, for: Clock do 52 | def to_string(%Clock{hour: hour, minute: minute}) do 53 | "#{format(hour)}:#{format(minute)}" 54 | end 55 | 56 | defp format(number) do 57 | number |> Integer.to_string() |> String.pad_leading(2, "0") 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /test_data/dancing-dots/split_solution/.meta/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "authors": [ 3 | "angelikatyborska" 4 | ], 5 | "contributors": [ 6 | "jiegillet" 7 | ], 8 | "files": { 9 | "solution": [ 10 | "lib/dancing_dots/animation.ex", 11 | "lib/dancing_dots/flicker.ex", 12 | "lib/dancing_dots/zoom.ex" 13 | ], 14 | "test": [ 15 | "test/dancing_dots/animation_test.exs" 16 | ], 17 | "exemplar": [ 18 | ".meta/exemplar/animation.ex", 19 | ".meta/exemplar/flicker.ex", 20 | ".meta/exemplar/zoom.ex" 21 | ], 22 | "editor": [ 23 | "lib/dancing_dots/dot.ex", 24 | "lib/dancing_dots/dot_group.ex" 25 | ] 26 | }, 27 | "language_versions": ">=1.10", 28 | "blurb": "Learn about behaviours by writing animations for dot-based generative art." 29 | } 30 | -------------------------------------------------------------------------------- /test_data/dancing-dots/split_solution/.meta/exemplar/animation.ex: -------------------------------------------------------------------------------- 1 | defmodule DancingDots.Animation do 2 | @type dot :: DancingDots.Dot.t() 3 | @type opts :: keyword 4 | @type error :: any 5 | @type frame_number :: pos_integer 6 | 7 | @callback init(opts :: opts) :: {:ok, opts} | {:error, error} 8 | @callback handle_frame(dot :: dot, n :: frame_number, opts :: opts) :: dot 9 | 10 | defmacro __using__(_) do 11 | quote do 12 | @behaviour DancingDots.Animation 13 | def init(opts), do: {:ok, opts} 14 | defoverridable init: 1 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test_data/dancing-dots/split_solution/.meta/exemplar/flicker.ex: -------------------------------------------------------------------------------- 1 | defmodule DancingDots.Flicker do 2 | use DancingDots.Animation 3 | 4 | @impl DancingDots.Animation 5 | def handle_frame(dot, frame_number, _opts) do 6 | opacity = if rem(frame_number, 4) == 0, do: dot.opacity / 2, else: dot.opacity 7 | %{dot | opacity: opacity} 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test_data/dancing-dots/split_solution/.meta/exemplar/zoom.ex: -------------------------------------------------------------------------------- 1 | defmodule DancingDots.Zoom do 2 | use DancingDots.Animation 3 | 4 | @impl DancingDots.Animation 5 | def init(opts) do 6 | velocity = Keyword.get(opts, :velocity) 7 | 8 | if is_number(velocity) do 9 | {:ok, [velocity: velocity]} 10 | else 11 | {:error, 12 | "The :velocity option is required, and its value must be a number. Got: #{inspect(velocity)}"} 13 | end 14 | end 15 | 16 | @impl DancingDots.Animation 17 | def handle_frame(dot, frame_number, opts) do 18 | %{dot | radius: dot.radius + opts[:velocity] * (frame_number - 1)} 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test_data/dancing-dots/split_solution/expected_analysis.json: -------------------------------------------------------------------------------- 1 | {"comments":[],"summary":"Submission analyzed. No automated suggestions found."} -------------------------------------------------------------------------------- /test_data/dancing-dots/split_solution/lib/dancing_dots/animation.ex: -------------------------------------------------------------------------------- 1 | defmodule DancingDots.Animation do 2 | @type dot :: DancingDots.Dot.t() 3 | @type opts :: keyword 4 | @type error :: any 5 | @type frame_number :: pos_integer 6 | 7 | @callback init(opts :: opts) :: {:ok, opts} | {:error, error} 8 | @callback handle_frame(dot :: dot, n :: frame_number, opts :: opts) :: dot 9 | 10 | defmacro __using__(_) do 11 | quote do 12 | @behaviour DancingDots.Animation 13 | def init(opts), do: {:ok, opts} 14 | defoverridable init: 1 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test_data/dancing-dots/split_solution/lib/dancing_dots/dot.ex: -------------------------------------------------------------------------------- 1 | defmodule DancingDots.Dot do 2 | defstruct [:x, :y, :radius, :opacity] 3 | @type t :: %__MODULE__{} 4 | end 5 | -------------------------------------------------------------------------------- /test_data/dancing-dots/split_solution/lib/dancing_dots/dot_group.ex: -------------------------------------------------------------------------------- 1 | defmodule DancingDots.DotGroup do 2 | # This module is an example of how behaviours can be used in practice. 3 | # You don't need to read it to solve this exercise. 4 | # It's here for the curious :) 5 | 6 | @moduledoc """ 7 | Allows applying a list of one or more animations to a group of one or more dots. 8 | """ 9 | 10 | defstruct [:dots, :animations_with_opts] 11 | @type t :: %__MODULE__{} 12 | 13 | @doc """ 14 | Creates a new dot group with given dots and an empty list of animations. 15 | """ 16 | @spec new([DancingDots.Dot.t()]) :: t() 17 | def new(dots) do 18 | %__MODULE__{ 19 | dots: dots, 20 | animations_with_opts: [] 21 | } 22 | end 23 | 24 | @doc """ 25 | Validates the given animation module with its given options and adds it to the group. 26 | """ 27 | @spec add_animation(t(), module, DancingDots.Animation.opts()) :: 28 | {:ok, t()} | {:error, DancingDots.Animation.error()} 29 | def add_animation(dot_group, animation_module, opts) do 30 | # using Animation's init/1 callback 31 | init_result = animation_module.init(opts) 32 | 33 | case init_result do 34 | {:ok, opts} -> 35 | animations_with_opts = [{animation_module, opts} | dot_group.animations_with_opts] 36 | dot_group = %{dot_group | animations_with_opts: animations_with_opts} 37 | {:ok, dot_group} 38 | 39 | {:error, error} -> 40 | {:error, error} 41 | end 42 | end 43 | 44 | @doc """ 45 | Applies the list of animations to all the dots. 46 | """ 47 | @spec render_dots(t(), DancingDots.Animation.frame_number()) :: [DancingDots.Dot.t()] 48 | def render_dots(dot_group, frame_number) do 49 | %{ 50 | dots: dots, 51 | animations_with_opts: animations_with_opts 52 | } = dot_group 53 | 54 | Enum.map(dots, fn dot -> 55 | Enum.reduce(animations_with_opts, dot, fn {animation_module, opts}, acc -> 56 | # using Animation's handle_frame/3 callback 57 | animation_module.handle_frame(acc, frame_number, opts) 58 | end) 59 | end) 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /test_data/dancing-dots/split_solution/lib/dancing_dots/flicker.ex: -------------------------------------------------------------------------------- 1 | defmodule DancingDots.Flicker do 2 | use DancingDots.Animation 3 | 4 | @impl DancingDots.Animation 5 | def handle_frame(dot, frame_number, _opts) do 6 | opacity = if rem(frame_number, 4) == 0, do: dot.opacity / 2, else: dot.opacity 7 | %{dot | opacity: opacity} 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test_data/dancing-dots/split_solution/lib/dancing_dots/zoom.ex: -------------------------------------------------------------------------------- 1 | defmodule DancingDots.Zoom do 2 | use DancingDots.Animation 3 | 4 | @impl DancingDots.Animation 5 | def init(opts) do 6 | velocity = Keyword.get(opts, :velocity) 7 | 8 | if is_number(velocity) do 9 | {:ok, [velocity: velocity]} 10 | else 11 | {:error, 12 | "The :velocity option is required, and its value must be a number. Got: #{inspect(velocity)}"} 13 | end 14 | end 15 | 16 | @impl DancingDots.Animation 17 | def handle_frame(dot, frame_number, opts) do 18 | %{dot | radius: dot.radius + opts[:velocity] * (frame_number - 1)} 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test_data/lasagna/deprecated_modules/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /test_data/lasagna/deprecated_modules/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | basics-*.tar 24 | 25 | -------------------------------------------------------------------------------- /test_data/lasagna/deprecated_modules/.meta/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "blurb": "Learn about the basics of Elixir by following a lasagna recipe.", 3 | "authors": [ 4 | "neenjaw" 5 | ], 6 | "contributors": [ 7 | "angelikatyborska" 8 | ], 9 | "files": { 10 | "solution": [ 11 | "lib/lasagna.ex" 12 | ], 13 | "test": [ 14 | "test/lasagna_test.exs" 15 | ], 16 | "exemplar": [ 17 | ".meta/exemplar.ex" 18 | ] 19 | }, 20 | "forked_from": [ 21 | "csharp/lucians-luscious-lasagna" 22 | ], 23 | "language_versions": ">=1.10" 24 | } 25 | -------------------------------------------------------------------------------- /test_data/lasagna/deprecated_modules/.meta/exemplar.ex: -------------------------------------------------------------------------------- 1 | defmodule Lasagna do 2 | def expected_minutes_in_oven() do 3 | 40 4 | end 5 | 6 | def remaining_minutes_in_oven(actual_minutes_in_oven) do 7 | expected_minutes_in_oven() - actual_minutes_in_oven 8 | end 9 | 10 | def preparation_time_in_minutes(number_of_layers) do 11 | number_of_layers * 2 12 | end 13 | 14 | def total_time_in_minutes(number_of_layers, actual_minutes_in_oven) do 15 | preparation_time_in_minutes(number_of_layers) + actual_minutes_in_oven 16 | end 17 | 18 | def alarm() do 19 | "Ding!" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test_data/lasagna/deprecated_modules/expected_analysis.json: -------------------------------------------------------------------------------- 1 | {"comments":[{"comment":"elixir.solution.compiler_warnings","params":{"warnings":"warning: Behaviour.defcallback/1 is deprecated. Use the @callback module attribute instead\n lib/lasagna.ex:4:13\n\nwarning: HashDict.new/0 is deprecated. Use maps and the Map module instead\n lib/lasagna.ex:7:14\n\nwarning: HashSet.member?/2 is deprecated. Use the MapSet module instead\n lib/lasagna.ex:12:13\n\nwarning: HashSet.new/0 is deprecated. Use the MapSet module instead\n lib/lasagna.ex:12:29\n\n"},"type":"actionable"},{"comment":"elixir.general.feedback_request","type":"informative","params": {"mentoring_request_url":"https://exercism.org/tracks/elixir/exercises/lasagna/mentor_discussions"}}],"summary":"Check the comments for some suggestions. 📣"} 2 | -------------------------------------------------------------------------------- /test_data/lasagna/deprecated_modules/lib/lasagna.ex: -------------------------------------------------------------------------------- 1 | defmodule Lasagna do 2 | require Behaviour 3 | 4 | Behaviour.defcallback(init) 5 | 6 | def expected_minutes_in_oven() do 7 | HashDict.new() 8 | 40 9 | end 10 | 11 | def remaining_minutes_in_oven(actual_minutes_in_oven) do 12 | HashSet.member?(HashSet.new(), :foo) 13 | expected_minutes_in_oven() - actual_minutes_in_oven 14 | end 15 | 16 | def preparation_time_in_minutes(number_of_layers) do 17 | number_of_layers * 2 18 | end 19 | 20 | def total_time_in_minutes(number_of_layers, actual_minutes_in_oven) do 21 | preparation_time_in_minutes(number_of_layers) + actual_minutes_in_oven 22 | end 23 | 24 | def alarm() do 25 | "Ding!" 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test_data/lasagna/deprecated_modules/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Lasagna.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :lasagna, 7 | version: "0.1.0", 8 | # elixir: "~> 1.10", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [ 17 | extra_applications: [:logger] 18 | ] 19 | end 20 | 21 | # Run "mix help deps" to learn about dependencies. 22 | defp deps do 23 | [ 24 | # {:dep_from_hexpm, "~> 0.3.0"}, 25 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 26 | ] 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test_data/lasagna/deprecated_modules/test/lasagna_test.exs: -------------------------------------------------------------------------------- 1 | defmodule LasagnaTest do 2 | use ExUnit.Case 3 | doctest Lasagna 4 | 5 | @tag task_id: 1 6 | test "expected minutes in oven" do 7 | assert Lasagna.expected_minutes_in_oven() === 40 8 | end 9 | 10 | @tag task_id: 2 11 | test "remaining minutes in oven" do 12 | assert Lasagna.remaining_minutes_in_oven(25) === 15 13 | end 14 | 15 | @tag task_id: 3 16 | test "preparation time in minutes for one layer" do 17 | assert Lasagna.preparation_time_in_minutes(1) === 2 18 | end 19 | 20 | @tag task_id: 3 21 | test "preparation time in minutes for multiple layers" do 22 | assert Lasagna.preparation_time_in_minutes(4) === 8 23 | end 24 | 25 | @tag task_id: 4 26 | test "total time in minutes for one layer" do 27 | assert Lasagna.total_time_in_minutes(1, 30) === 32 28 | end 29 | 30 | @tag task_id: 4 31 | test "total time in minutes for multiple layers" do 32 | assert Lasagna.total_time_in_minutes(4, 8) === 16 33 | end 34 | 35 | @tag task_id: 5 36 | test "notification message" do 37 | assert Lasagna.alarm() === "Ding!" 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test_data/lasagna/deprecated_modules/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | ExUnit.configure(exclude: :pending, trace: true, seed: 0) 3 | -------------------------------------------------------------------------------- /test_data/lasagna/failing_solution/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /test_data/lasagna/failing_solution/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | basics-*.tar 24 | 25 | -------------------------------------------------------------------------------- /test_data/lasagna/failing_solution/.meta/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "blurb": "Learn about the basics of Elixir by following a lasagna recipe.", 3 | "authors": [ 4 | "neenjaw" 5 | ], 6 | "contributors": [ 7 | "angelikatyborska" 8 | ], 9 | "files": { 10 | "solution": [ 11 | "lib/lasagna.ex" 12 | ], 13 | "test": [ 14 | "test/lasagna_test.exs" 15 | ], 16 | "exemplar": [ 17 | ".meta/exemplar.ex" 18 | ] 19 | }, 20 | "forked_from": [ 21 | "csharp/lucians-luscious-lasagna" 22 | ], 23 | "language_versions": ">=1.10" 24 | } 25 | -------------------------------------------------------------------------------- /test_data/lasagna/failing_solution/.meta/exemplar.ex: -------------------------------------------------------------------------------- 1 | defmodule Lasagna do 2 | def expected_minutes_in_oven() do 3 | 40 4 | end 5 | 6 | def remaining_minutes_in_oven(actual_minutes_in_oven) do 7 | expected_minutes_in_oven() - actual_minutes_in_oven 8 | end 9 | 10 | def preparation_time_in_minutes(number_of_layers) do 11 | number_of_layers * 2 12 | end 13 | 14 | def total_time_in_minutes(number_of_layers, actual_minutes_in_oven) do 15 | preparation_time_in_minutes(number_of_layers) + actual_minutes_in_oven 16 | end 17 | 18 | def alarm() do 19 | "Ding!" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test_data/lasagna/failing_solution/expected_analysis.json: -------------------------------------------------------------------------------- 1 | {"comments":[{"comment":"elixir.lasagna.function_reuse","type":"actionable"},{"comment":"elixir.solution.private_helper_functions","params":{"actual":"def public_helper(_)","expected":"defp public_helper(_)"},"type":"informative"},{"comment":"elixir.solution.todo_comment","type":"informative"},{"comment":"elixir.general.feedback_request","type":"informative","params": {"mentoring_request_url":"https://exercism.org/tracks/elixir/exercises/lasagna/mentor_discussions"}}],"summary":"Check the comments for some suggestions. 📣"} 2 | -------------------------------------------------------------------------------- /test_data/lasagna/failing_solution/lib/lasagna.ex: -------------------------------------------------------------------------------- 1 | defmodule Lasagna do 2 | # TODO: define the 'expected_minutes_in_oven/0' function 3 | 4 | # TODO: define the 'remaining_minutes_in_oven/1' function 5 | 6 | # TODO: define the 'preparation_time_in_minutes/1' function 7 | 8 | # TODO: define the 'total_time_in_minutes/2' function 9 | 10 | # TODO: define the 'alarm/0' function 11 | 12 | def public_helper(_pasta), do: :delicious 13 | end 14 | -------------------------------------------------------------------------------- /test_data/lasagna/failing_solution/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Lasagna.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :lasagna, 7 | version: "0.1.0", 8 | # elixir: "~> 1.10", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [ 17 | extra_applications: [:logger] 18 | ] 19 | end 20 | 21 | # Run "mix help deps" to learn about dependencies. 22 | defp deps do 23 | [ 24 | # {:dep_from_hexpm, "~> 0.3.0"}, 25 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 26 | ] 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test_data/lasagna/failing_solution/test/lasagna_test.exs: -------------------------------------------------------------------------------- 1 | defmodule LasagnaTest do 2 | use ExUnit.Case 3 | doctest Lasagna 4 | 5 | @tag task_id: 1 6 | test "expected minutes in oven" do 7 | assert Lasagna.expected_minutes_in_oven() === 40 8 | end 9 | 10 | @tag task_id: 2 11 | test "remaining minutes in oven" do 12 | assert Lasagna.remaining_minutes_in_oven(25) === 15 13 | end 14 | 15 | @tag task_id: 3 16 | test "preparation time in minutes for one layer" do 17 | assert Lasagna.preparation_time_in_minutes(1) === 2 18 | end 19 | 20 | @tag task_id: 3 21 | test "preparation time in minutes for multiple layers" do 22 | assert Lasagna.preparation_time_in_minutes(4) === 8 23 | end 24 | 25 | @tag task_id: 4 26 | test "total time in minutes for one layer" do 27 | assert Lasagna.total_time_in_minutes(1, 30) === 32 28 | end 29 | 30 | @tag task_id: 4 31 | test "total time in minutes for multiple layers" do 32 | assert Lasagna.total_time_in_minutes(4, 8) === 16 33 | end 34 | 35 | @tag task_id: 5 36 | test "notification message" do 37 | assert Lasagna.alarm() === "Ding!" 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test_data/lasagna/failing_solution/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | ExUnit.configure(exclude: :pending, trace: true, seed: 0) 3 | -------------------------------------------------------------------------------- /test_data/lasagna/missing_config/expected_analysis.json: -------------------------------------------------------------------------------- 1 | {"comments":[],"summary":"Analysis was halted. Analysis skipped, not able to read solution config."} -------------------------------------------------------------------------------- /test_data/lasagna/missing_exemplar/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /test_data/lasagna/missing_exemplar/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | basics-*.tar 24 | 25 | -------------------------------------------------------------------------------- /test_data/lasagna/missing_exemplar/.meta/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "blurb": "Learn about the basics of Elixir by following a lasagna recipe.", 3 | "authors": [ 4 | "neenjaw" 5 | ], 6 | "contributors": [ 7 | "angelikatyborska" 8 | ], 9 | "files": { 10 | "solution": [ 11 | "lib/lasagna.ex" 12 | ], 13 | "test": [ 14 | "test/lasagna_test.exs" 15 | ], 16 | "exemplar": [ 17 | ".meta/exemplar.ex" 18 | ] 19 | }, 20 | "forked_from": [ 21 | "csharp/lucians-luscious-lasagna" 22 | ], 23 | "language_versions": ">=1.10" 24 | } 25 | -------------------------------------------------------------------------------- /test_data/lasagna/missing_exemplar/expected_analysis.json: -------------------------------------------------------------------------------- 1 | {"comments":[],"summary":"Submission analyzed. No automated suggestions found."} -------------------------------------------------------------------------------- /test_data/lasagna/missing_exemplar/lib/lasagna.ex: -------------------------------------------------------------------------------- 1 | defmodule Lasagna do 2 | def expected_minutes_in_oven() do 3 | 40 4 | end 5 | 6 | def remaining_minutes_in_oven(actual_minutes_in_oven) do 7 | expected_minutes_in_oven() - actual_minutes_in_oven 8 | end 9 | 10 | def preparation_time_in_minutes(number_of_layers) do 11 | number_of_layers * 2 12 | end 13 | 14 | def total_time_in_minutes(number_of_layers, actual_minutes_in_oven) do 15 | preparation_time_in_minutes(number_of_layers) + actual_minutes_in_oven 16 | end 17 | 18 | def alarm() do 19 | "Ding!" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test_data/lasagna/missing_exemplar/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Lasagna.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :lasagna, 7 | version: "0.1.0", 8 | # elixir: "~> 1.10", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [ 17 | extra_applications: [:logger] 18 | ] 19 | end 20 | 21 | # Run "mix help deps" to learn about dependencies. 22 | defp deps do 23 | [ 24 | # {:dep_from_hexpm, "~> 0.3.0"}, 25 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 26 | ] 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test_data/lasagna/missing_exemplar/test/lasagna_test.exs: -------------------------------------------------------------------------------- 1 | defmodule LasagnaTest do 2 | use ExUnit.Case 3 | doctest Lasagna 4 | 5 | @tag task_id: 1 6 | test "expected minutes in oven" do 7 | assert Lasagna.expected_minutes_in_oven() === 40 8 | end 9 | 10 | @tag task_id: 2 11 | test "remaining minutes in oven" do 12 | assert Lasagna.remaining_minutes_in_oven(25) === 15 13 | end 14 | 15 | @tag task_id: 3 16 | test "preparation time in minutes for one layer" do 17 | assert Lasagna.preparation_time_in_minutes(1) === 2 18 | end 19 | 20 | @tag task_id: 3 21 | test "preparation time in minutes for multiple layers" do 22 | assert Lasagna.preparation_time_in_minutes(4) === 8 23 | end 24 | 25 | @tag task_id: 4 26 | test "total time in minutes for one layer" do 27 | assert Lasagna.total_time_in_minutes(1, 30) === 32 28 | end 29 | 30 | @tag task_id: 4 31 | test "total time in minutes for multiple layers" do 32 | assert Lasagna.total_time_in_minutes(4, 8) === 16 33 | end 34 | 35 | @tag task_id: 5 36 | test "notification message" do 37 | assert Lasagna.alarm() === "Ding!" 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test_data/lasagna/missing_exemplar/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | ExUnit.configure(exclude: :pending, trace: true, seed: 0) 3 | -------------------------------------------------------------------------------- /test_data/lasagna/perfect_solution/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /test_data/lasagna/perfect_solution/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | basics-*.tar 24 | 25 | -------------------------------------------------------------------------------- /test_data/lasagna/perfect_solution/.meta/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "blurb": "Learn about the basics of Elixir by following a lasagna recipe.", 3 | "authors": [ 4 | "neenjaw" 5 | ], 6 | "contributors": [ 7 | "angelikatyborska" 8 | ], 9 | "files": { 10 | "solution": [ 11 | "lib/lasagna.ex" 12 | ], 13 | "test": [ 14 | "test/lasagna_test.exs" 15 | ], 16 | "exemplar": [ 17 | ".meta/exemplar.ex" 18 | ] 19 | }, 20 | "forked_from": [ 21 | "csharp/lucians-luscious-lasagna" 22 | ], 23 | "language_versions": ">=1.10" 24 | } 25 | -------------------------------------------------------------------------------- /test_data/lasagna/perfect_solution/.meta/exemplar.ex: -------------------------------------------------------------------------------- 1 | defmodule Lasagna do 2 | def expected_minutes_in_oven() do 3 | 40 4 | end 5 | 6 | def remaining_minutes_in_oven(actual_minutes_in_oven) do 7 | expected_minutes_in_oven() - actual_minutes_in_oven 8 | end 9 | 10 | def preparation_time_in_minutes(number_of_layers) do 11 | number_of_layers * 2 12 | end 13 | 14 | def total_time_in_minutes(number_of_layers, actual_minutes_in_oven) do 15 | preparation_time_in_minutes(number_of_layers) + actual_minutes_in_oven 16 | end 17 | 18 | def alarm() do 19 | "Ding!" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test_data/lasagna/perfect_solution/expected_analysis.json: -------------------------------------------------------------------------------- 1 | {"comments":[{"comment":"elixir.solution.same_as_exemplar","type":"celebratory"}],"summary":"You're doing something right. 🎉"} -------------------------------------------------------------------------------- /test_data/lasagna/perfect_solution/lib/lasagna.ex: -------------------------------------------------------------------------------- 1 | defmodule Lasagna do 2 | def expected_minutes_in_oven() do 3 | 40 4 | end 5 | 6 | def remaining_minutes_in_oven(actual_minutes_in_oven) do 7 | expected_minutes_in_oven() - actual_minutes_in_oven 8 | end 9 | 10 | def preparation_time_in_minutes(number_of_layers) do 11 | number_of_layers * 2 12 | end 13 | 14 | def total_time_in_minutes(number_of_layers, actual_minutes_in_oven) do 15 | preparation_time_in_minutes(number_of_layers) + actual_minutes_in_oven 16 | end 17 | 18 | def alarm() do 19 | "Ding!" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test_data/lasagna/perfect_solution/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Lasagna.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :lasagna, 7 | version: "0.1.0", 8 | # elixir: "~> 1.10", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [ 17 | extra_applications: [:logger] 18 | ] 19 | end 20 | 21 | # Run "mix help deps" to learn about dependencies. 22 | defp deps do 23 | [ 24 | # {:dep_from_hexpm, "~> 0.3.0"}, 25 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 26 | ] 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test_data/lasagna/perfect_solution/test/lasagna_test.exs: -------------------------------------------------------------------------------- 1 | defmodule LasagnaTest do 2 | use ExUnit.Case 3 | doctest Lasagna 4 | 5 | @tag task_id: 1 6 | test "expected minutes in oven" do 7 | assert Lasagna.expected_minutes_in_oven() === 40 8 | end 9 | 10 | @tag task_id: 2 11 | test "remaining minutes in oven" do 12 | assert Lasagna.remaining_minutes_in_oven(25) === 15 13 | end 14 | 15 | @tag task_id: 3 16 | test "preparation time in minutes for one layer" do 17 | assert Lasagna.preparation_time_in_minutes(1) === 2 18 | end 19 | 20 | @tag task_id: 3 21 | test "preparation time in minutes for multiple layers" do 22 | assert Lasagna.preparation_time_in_minutes(4) === 8 23 | end 24 | 25 | @tag task_id: 4 26 | test "total time in minutes for one layer" do 27 | assert Lasagna.total_time_in_minutes(1, 30) === 32 28 | end 29 | 30 | @tag task_id: 4 31 | test "total time in minutes for multiple layers" do 32 | assert Lasagna.total_time_in_minutes(4, 8) === 16 33 | end 34 | 35 | @tag task_id: 5 36 | test "notification message" do 37 | assert Lasagna.alarm() === "Ding!" 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test_data/lasagna/perfect_solution/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | ExUnit.configure(exclude: :pending, trace: true, seed: 0) 3 | -------------------------------------------------------------------------------- /test_data/lasagna/wrong_config/.meta/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "blurb": "Learn about the basics of Elixir by following a lasagna recipe.", 3 | "authors": [ 4 | "neenjaw" 5 | ], 6 | "contributors": [ 7 | "angelikatyborska" 8 | ], 9 | "files": { 10 | "solution": [ 11 | "lib/lasagna.ex" 12 | -------------------------------------------------------------------------------- /test_data/lasagna/wrong_config/expected_analysis.json: -------------------------------------------------------------------------------- 1 | {"comments":[],"summary":"Analysis was halted. Analysis skipped, not able to decode solution config."} -------------------------------------------------------------------------------- /test_data/lasagna/wrong_config2/.meta/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "blurb": "Learn about the basics of Elixir by following a lasagna recipe.", 3 | "authors": [ 4 | "neenjaw" 5 | ], 6 | "contributors": [ 7 | "angelikatyborska" 8 | ], 9 | "files": { 10 | "solution": [], 11 | "test": [ 12 | "test/lasagna_test.exs" 13 | ], 14 | "exemplar": [ 15 | ".meta/exemplar.ex" 16 | ] 17 | }, 18 | "forked_from": [ 19 | "csharp/lucians-luscious-lasagna" 20 | ], 21 | "language_versions": ">=1.10" 22 | } 23 | -------------------------------------------------------------------------------- /test_data/lasagna/wrong_config2/expected_analysis.json: -------------------------------------------------------------------------------- 1 | {"comments":[],"summary":"Analysis was halted. Analysis skipped, unexpected error Elixir.RuntimeError"} 2 | -------------------------------------------------------------------------------- /test_data/lasagna/wrong_exemplar/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /test_data/lasagna/wrong_exemplar/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | basics-*.tar 24 | 25 | -------------------------------------------------------------------------------- /test_data/lasagna/wrong_exemplar/.meta/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "blurb": "Learn about the basics of Elixir by following a lasagna recipe.", 3 | "authors": [ 4 | "neenjaw" 5 | ], 6 | "contributors": [ 7 | "angelikatyborska" 8 | ], 9 | "files": { 10 | "solution": [ 11 | "lib/lasagna.ex" 12 | ], 13 | "test": [ 14 | "test/lasagna_test.exs" 15 | ], 16 | "exemplar": [ 17 | ".meta/exemplar.ex" 18 | ] 19 | }, 20 | "forked_from": [ 21 | "csharp/lucians-luscious-lasagna" 22 | ], 23 | "language_versions": ">=1.10" 24 | } 25 | -------------------------------------------------------------------------------- /test_data/lasagna/wrong_exemplar/.meta/exemplar.ex: -------------------------------------------------------------------------------- 1 | defmodule Lasagna do 2 | 3 | def expected_minutes_in_oven() do 4 | -------------------------------------------------------------------------------- /test_data/lasagna/wrong_exemplar/expected_analysis.json: -------------------------------------------------------------------------------- 1 | {"comments":[],"summary":"Submission analyzed. No automated suggestions found."} -------------------------------------------------------------------------------- /test_data/lasagna/wrong_exemplar/lib/lasagna.ex: -------------------------------------------------------------------------------- 1 | defmodule Lasagna do 2 | def expected_minutes_in_oven() do 3 | 40 4 | end 5 | 6 | def remaining_minutes_in_oven(actual_minutes_in_oven) do 7 | expected_minutes_in_oven() - actual_minutes_in_oven 8 | end 9 | 10 | def preparation_time_in_minutes(number_of_layers) do 11 | number_of_layers * 2 12 | end 13 | 14 | def total_time_in_minutes(number_of_layers, actual_minutes_in_oven) do 15 | preparation_time_in_minutes(number_of_layers) + actual_minutes_in_oven 16 | end 17 | 18 | def alarm() do 19 | "Ding!" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test_data/lasagna/wrong_exemplar/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Lasagna.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :lasagna, 7 | version: "0.1.0", 8 | # elixir: "~> 1.10", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [ 17 | extra_applications: [:logger] 18 | ] 19 | end 20 | 21 | # Run "mix help deps" to learn about dependencies. 22 | defp deps do 23 | [ 24 | # {:dep_from_hexpm, "~> 0.3.0"}, 25 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 26 | ] 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test_data/lasagna/wrong_exemplar/test/lasagna_test.exs: -------------------------------------------------------------------------------- 1 | defmodule LasagnaTest do 2 | use ExUnit.Case 3 | doctest Lasagna 4 | 5 | @tag task_id: 1 6 | test "expected minutes in oven" do 7 | assert Lasagna.expected_minutes_in_oven() === 40 8 | end 9 | 10 | @tag task_id: 2 11 | test "remaining minutes in oven" do 12 | assert Lasagna.remaining_minutes_in_oven(25) === 15 13 | end 14 | 15 | @tag task_id: 3 16 | test "preparation time in minutes for one layer" do 17 | assert Lasagna.preparation_time_in_minutes(1) === 2 18 | end 19 | 20 | @tag task_id: 3 21 | test "preparation time in minutes for multiple layers" do 22 | assert Lasagna.preparation_time_in_minutes(4) === 8 23 | end 24 | 25 | @tag task_id: 4 26 | test "total time in minutes for one layer" do 27 | assert Lasagna.total_time_in_minutes(1, 30) === 32 28 | end 29 | 30 | @tag task_id: 4 31 | test "total time in minutes for multiple layers" do 32 | assert Lasagna.total_time_in_minutes(4, 8) === 16 33 | end 34 | 35 | @tag task_id: 5 36 | test "notification message" do 37 | assert Lasagna.alarm() === "Ding!" 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test_data/lasagna/wrong_exemplar/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | ExUnit.configure(exclude: :pending, trace: true, seed: 0) 3 | -------------------------------------------------------------------------------- /test_data/square-root/split_solution/.meta/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "authors": [ 3 | "jiegillet" 4 | ], 5 | "files": { 6 | "solution": [ 7 | "lib/square_root.ex" 8 | ], 9 | "test": [ 10 | "test/square_root_test.exs" 11 | ], 12 | "example": [ 13 | ".meta/example.ex" 14 | ] 15 | }, 16 | "blurb": "Given a natural radicand, return its square root.", 17 | "source": "wolf99", 18 | "source_url": "https://github.com/exercism/problem-specifications/pull/1582" 19 | } 20 | -------------------------------------------------------------------------------- /test_data/square-root/split_solution/expected_analysis.json: -------------------------------------------------------------------------------- 1 | {"comments":[{"comment":"elixir.square-root.do_not_use_built_in_sqrt","type":"essential"},{"comment":"elixir.general.feedback_request","type":"informative","params": {"mentoring_request_url":"https://exercism.org/tracks/elixir/exercises/square-root/mentor_discussions"}}],"summary":"Check the comments for things to fix. 🛠"} 2 | 3 | -------------------------------------------------------------------------------- /test_data/square-root/split_solution/lib/square_root.ex: -------------------------------------------------------------------------------- 1 | defmodule SquareRoot do 2 | @doc """ 3 | Calculate the integer square root of a positive integer 4 | """ 5 | @spec calculate(radicand :: pos_integer) :: pos_integer 6 | def calculate(radicand) do 7 | SquareRoot.Cheating.calculate(radicand) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test_data/square-root/split_solution/lib/square_root/cheating.ex: -------------------------------------------------------------------------------- 1 | defmodule SquareRoot.Cheating do 2 | def calculate(n) do 3 | Float.pow(n / 1, 0.5) |> floor() 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test_data/take-a-number-deluxe/perfect_solution/.meta/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "authors": [ 3 | "angelikatyborska" 4 | ], 5 | "contributors": [ 6 | "jiegillet" 7 | ], 8 | "files": { 9 | "solution": [ 10 | "lib/take_a_number_deluxe.ex" 11 | ], 12 | "test": [ 13 | "test/take_a_number_deluxe_test.exs" 14 | ], 15 | "exemplar": [ 16 | ".meta/exemplar.ex" 17 | ], 18 | "editor": [ 19 | "lib/take_a_number_deluxe/state.ex", 20 | "lib/take_a_number_deluxe/queue.ex" 21 | ] 22 | }, 23 | "language_versions": ">=1.10", 24 | "icon": "take-a-number", 25 | "blurb": "Learn about GenServers by writing an embedded system for a Take-A-Number machine." 26 | } 27 | -------------------------------------------------------------------------------- /test_data/take-a-number-deluxe/perfect_solution/expected_analysis.json: -------------------------------------------------------------------------------- 1 | {"comments":[],"summary":"Submission analyzed. No automated suggestions found."} 2 | -------------------------------------------------------------------------------- /test_data/take-a-number-deluxe/perfect_solution/lib/take_a_number_deluxe/queue.ex: -------------------------------------------------------------------------------- 1 | defmodule TakeANumberDeluxe.Queue do 2 | # You don't need to read this module to solve this exercise. 3 | 4 | # We would have used Erlang's queue module instead 5 | # (https://www.erlang.org/doc/man/queue.html), 6 | # but it lacks a `delete` function before OTP 24, 7 | # and we want this exercise to work on older versions too. 8 | 9 | defstruct in: [], out: [] 10 | @type t :: %__MODULE__{} 11 | 12 | @spec new() :: t() 13 | def new(), do: %__MODULE__{} 14 | 15 | @spec push(t(), any()) :: t() 16 | def push(%__MODULE__{in: in_q} = q, a), do: %__MODULE__{q | in: [a | in_q]} 17 | 18 | @spec out(t()) :: {{:value, any()}, t()} | {:empty, t()} 19 | def out(%__MODULE__{in: [], out: []} = q), do: {:empty, q} 20 | def out(%__MODULE__{out: [head | tail]} = q), do: {{:value, head}, %__MODULE__{q | out: tail}} 21 | def out(%__MODULE__{in: in_q}), do: out(%__MODULE__{out: Enum.reverse(in_q)}) 22 | 23 | @spec empty?(t()) :: boolean() 24 | def empty?(%__MODULE__{in: [], out: []}), do: true 25 | def empty?(%__MODULE__{}), do: false 26 | 27 | @spec member?(t(), any()) :: boolean() 28 | def member?(%__MODULE__{in: in_q, out: out}, a), do: a in in_q or a in out 29 | 30 | @spec delete(t(), any()) :: t() 31 | def delete(%__MODULE__{in: in_q, out: out}, a) do 32 | out = out ++ Enum.reverse(in_q) 33 | out = List.delete(out, a) 34 | %__MODULE__{out: out} 35 | end 36 | 37 | @spec from_list([any()]) :: t() 38 | def from_list(list), do: %__MODULE__{out: list} 39 | 40 | @spec to_list(t()) :: [any()] 41 | def to_list(%__MODULE__{in: in_q, out: out}), do: out ++ Enum.reverse(in_q) 42 | end 43 | -------------------------------------------------------------------------------- /test_data/two_fer/error_solution/.meta/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "blurb": "Create a sentence of the form \"One for X, one for me.\"", 3 | "authors": [], 4 | "files": { 5 | "solution": ["lib/two_fer.ex"], 6 | "test": ["test/two_fer_test.exs"], 7 | "example": [".meta/example.ex"] 8 | }, 9 | "source_url": "https://github.com/exercism/problem-specifications/issues/757" 10 | } 11 | -------------------------------------------------------------------------------- /test_data/two_fer/error_solution/.meta/example.ex: -------------------------------------------------------------------------------- 1 | defmodule TwoFer do 2 | @doc """ 3 | Two-fer or 2-fer is short for two for one. One for you and one for me. 4 | """ 5 | # Corrupted file that won't compile 6 | -------------------------------------------------------------------------------- /test_data/two_fer/error_solution/README.md: -------------------------------------------------------------------------------- 1 | # Two Fer 2 | 3 | `Two-fer` or `2-fer` is short for two for one. One for you and one for me. 4 | 5 | ```text 6 | "One for X, one for me." 7 | ``` 8 | 9 | When X is a name or "you". 10 | 11 | If the given name is "Alice", the result should be "One for Alice, one for me." 12 | If no name is given, the result should be "One for you, one for me." 13 | 14 | 15 | ## Running tests 16 | 17 | Execute the tests with: 18 | 19 | ```bash 20 | $ elixir two_fer_test.exs 21 | ``` 22 | 23 | ### Pending tests 24 | 25 | In the test suites, all but the first test have been skipped. 26 | 27 | Once you get a test passing, you can unskip the next one by 28 | commenting out the relevant `@tag :pending` with a `#` symbol. 29 | 30 | For example: 31 | 32 | ```elixir 33 | # @tag :pending 34 | test "shouting" do 35 | assert Bob.hey("WATCH OUT!") == "Whoa, chill out!" 36 | end 37 | ``` 38 | 39 | Or, you can enable all the tests by commenting out the 40 | `ExUnit.configure` line in the test suite. 41 | 42 | ```elixir 43 | # ExUnit.configure exclude: :pending, trace: true 44 | ``` 45 | 46 | If you're stuck on something, it may help to look at some of 47 | the [available resources](https://exercism.io/tracks/elixir/resources) 48 | out there where answers might be found. 49 | 50 | ## Source 51 | 52 | [https://en.wikipedia.org/wiki/Two-fer](https://en.wikipedia.org/wiki/Two-fer) 53 | 54 | ## Submitting Incomplete Solutions 55 | It's possible to submit an incomplete solution so you can see how others have completed the exercise. 56 | -------------------------------------------------------------------------------- /test_data/two_fer/error_solution/expected_analysis.json: -------------------------------------------------------------------------------- 1 | {"comments":[{"comment":"elixir.general.parsing_error","params":{"error":"missing terminator: end","line":1},"type":"essential"}],"summary":"Check the comments for things to fix. 🛠"} 2 | -------------------------------------------------------------------------------- /test_data/two_fer/error_solution/lib/two_fer.ex: -------------------------------------------------------------------------------- 1 | defmodule TwoFer do 2 | @doc """ 3 | Two-fer or 2-fer is short for two for one. One for you and one for me. 4 | """ 5 | @spec two_fer(String.t()) :: String.t() 6 | def two_fer(name \\ "you") 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /test_data/two_fer/error_solution/test/test.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start(autorun: false) 2 | ExUnit.configure(exclude: :pending, trace: true) 3 | 4 | Code.require_file("two_fer.exs", __DIR__) 5 | Code.require_file("two_fer_test.exs", __DIR__) 6 | 7 | ExUnit.Server.modules_loaded() 8 | 9 | ExUnit.run() 10 | 11 | -------------------------------------------------------------------------------- /test_data/two_fer/error_solution/test/two_fer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TwoFerTest do 2 | use ExUnit.Case 3 | 4 | test "no name given" do 5 | assert TwoFer.two_fer() == "One for you, one for me." 6 | end 7 | 8 | @tag :pending 9 | test "a name given" do 10 | assert TwoFer.two_fer("Gilberto Barros") == "One for Gilberto Barros, one for me." 11 | end 12 | 13 | @tag :pending 14 | test "when the parameter is a number" do 15 | assert_raise FunctionClauseError, fn -> 16 | TwoFer.two_fer(10) 17 | end 18 | end 19 | 20 | @tag :pending 21 | test "when the parameter is an atom" do 22 | assert_raise FunctionClauseError, fn -> 23 | TwoFer.two_fer(:bob) 24 | end 25 | end 26 | 27 | @tag :pending 28 | test "when the parameter is a charlist" do 29 | assert_raise FunctionClauseError, fn -> 30 | refute TwoFer.two_fer('Jon Snow') 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test_data/two_fer/imperfect_solution/.meta/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "blurb": "Create a sentence of the form \"One for X, one for me.\"", 3 | "authors": [], 4 | "files": { 5 | "solution": ["lib/two_fer.ex"], 6 | "test": ["test/two_fer_test.exs"], 7 | "example": [".meta/example.ex"] 8 | }, 9 | "source_url": "https://github.com/exercism/problem-specifications/issues/757" 10 | } 11 | -------------------------------------------------------------------------------- /test_data/two_fer/imperfect_solution/.meta/example.ex: -------------------------------------------------------------------------------- 1 | defmodule TwoFer do 2 | @doc """ 3 | Two-fer or 2-fer is short for two for one. One for you and one for me. 4 | """ 5 | @spec two_fer(String.t()) :: String.t() 6 | def two_fer(name \\ "you") when is_binary(name), do: "One for #{name}, one for me." 7 | end 8 | -------------------------------------------------------------------------------- /test_data/two_fer/imperfect_solution/README.md: -------------------------------------------------------------------------------- 1 | # Two Fer 2 | 3 | `Two-fer` or `2-fer` is short for two for one. One for you and one for me. 4 | 5 | ```text 6 | "One for X, one for me." 7 | ``` 8 | 9 | When X is a name or "you". 10 | 11 | If the given name is "Alice", the result should be "One for Alice, one for me." 12 | If no name is given, the result should be "One for you, one for me." 13 | 14 | 15 | ## Running tests 16 | 17 | Execute the tests with: 18 | 19 | ```bash 20 | $ elixir two_fer_test.exs 21 | ``` 22 | 23 | ### Pending tests 24 | 25 | In the test suites, all but the first test have been skipped. 26 | 27 | Once you get a test passing, you can unskip the next one by 28 | commenting out the relevant `@tag :pending` with a `#` symbol. 29 | 30 | For example: 31 | 32 | ```elixir 33 | # @tag :pending 34 | test "shouting" do 35 | assert Bob.hey("WATCH OUT!") == "Whoa, chill out!" 36 | end 37 | ``` 38 | 39 | Or, you can enable all the tests by commenting out the 40 | `ExUnit.configure` line in the test suite. 41 | 42 | ```elixir 43 | # ExUnit.configure exclude: :pending, trace: true 44 | ``` 45 | 46 | If you're stuck on something, it may help to look at some of 47 | the [available resources](https://exercism.io/tracks/elixir/resources) 48 | out there where answers might be found. 49 | 50 | ## Source 51 | 52 | [https://en.wikipedia.org/wiki/Two-fer](https://en.wikipedia.org/wiki/Two-fer) 53 | 54 | ## Submitting Incomplete Solutions 55 | It's possible to submit an incomplete solution so you can see how others have completed the exercise. 56 | -------------------------------------------------------------------------------- /test_data/two_fer/imperfect_solution/expected_analysis.json: -------------------------------------------------------------------------------- 1 | {"comments":[{"comment":"elixir.solution.raise_fn_clause_error","type":"actionable"},{"comment":"elixir.solution.variable_name_snake_case","params":{"actual":"_nameInPascalCase","expected":"_name_in_pascal_case"},"type":"actionable"},{"comment":"elixir.solution.module_attribute_name_snake_case","params":{"actual":"someUnusedModuleAttribute","expected":"some_unused_module_attribute"},"type":"actionable"},{"comment":"elixir.solution.module_pascal_case","params":{"actual":"My_empty_module","expected":"MyEmptyModule"},"type":"actionable"},{"comment":"elixir.solution.compiler_warnings","params":{"warnings":"warning: module attribute @someUnusedModuleAttribute was set but never used\n lib/two_fer.ex:2\n\n"},"type":"actionable"},{"comment":"elixir.solution.indentation","type":"informative"},{"comment":"elixir.solution.private_helper_functions","params":{"actual":"def public_helper(_)","expected":"defp public_helper(_)"},"type":"informative"},{"comment":"elixir.general.feedback_request","type":"informative","params": {"mentoring_request_url":"https://exercism.org/tracks/elixir/exercises/two_fer/mentor_discussions"}}],"summary":"Check the comments for some suggestions. 📣"} 2 | -------------------------------------------------------------------------------- /test_data/two_fer/imperfect_solution/lib/two_fer.ex: -------------------------------------------------------------------------------- 1 | defmodule TwoFer do 2 | @someUnusedModuleAttribute 1 3 | 4 | defmodule My_empty_module do 5 | end 6 | 7 | @doc """ 8 | Two-fer or 2-fer is short for two for one. One for you and one for me. 9 | """ 10 | def two_fer(name \\ "you") 11 | 12 | def two_fer(name) when is_binary(name) do 13 | "One for #{name}, one for me." 14 | end 15 | 16 | def two_fer(_nameInPascalCase), do: raise(FunctionClauseError) 17 | 18 | def public_helper(_pasta), do: :delicious 19 | end 20 | -------------------------------------------------------------------------------- /test_data/two_fer/imperfect_solution/test/test.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start(autorun: false) 2 | ExUnit.configure(exclude: :pending, trace: true) 3 | 4 | Code.require_file("two_fer.exs", __DIR__) 5 | Code.require_file("two_fer_test.exs", __DIR__) 6 | 7 | ExUnit.Server.modules_loaded() 8 | 9 | ExUnit.run() 10 | 11 | -------------------------------------------------------------------------------- /test_data/two_fer/imperfect_solution/test/two_fer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TwoFerTest do 2 | use ExUnit.Case 3 | 4 | test "no name given" do 5 | assert TwoFer.two_fer() == "One for you, one for me." 6 | end 7 | 8 | @tag :pending 9 | test "a name given" do 10 | assert TwoFer.two_fer("Gilberto Barrosi") == "One for Gilberto Barros, one for me." 11 | end 12 | 13 | @tag :pending 14 | test "when the parameter is a number" do 15 | assert_raise FunctionClauseError, fn -> 16 | TwoFer.two_fer(10) 17 | end 18 | end 19 | 20 | @tag :pending 21 | test "when the parameter is an atom" do 22 | assert_raise FunctionClauseError, fn -> 23 | TwoFer.two_fer(:bob) 24 | end 25 | end 26 | 27 | @tag :pending 28 | test "when the parameter is a charlist" do 29 | assert_raise FunctionClauseError, fn -> 30 | refute TwoFer.two_fer('Jon Snow') 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test_data/two_fer/informative_comments/.meta/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "blurb": "Create a sentence of the form \"One for X, one for me.\"", 3 | "authors": [], 4 | "files": { 5 | "solution": ["lib/two_fer.ex"], 6 | "test": ["test/two_fer_test.exs"], 7 | "example": [".meta/example.ex"] 8 | }, 9 | "source_url": "https://github.com/exercism/problem-specifications/issues/757" 10 | } 11 | -------------------------------------------------------------------------------- /test_data/two_fer/informative_comments/.meta/example.ex: -------------------------------------------------------------------------------- 1 | defmodule TwoFer do 2 | @doc """ 3 | Two-fer or 2-fer is short for two for one. One for you and one for me. 4 | """ 5 | @spec two_fer(String.t()) :: String.t() 6 | def two_fer(name \\ "you") when is_binary(name), do: "One for #{name}, one for me." 7 | end 8 | -------------------------------------------------------------------------------- /test_data/two_fer/informative_comments/expected_analysis.json: -------------------------------------------------------------------------------- 1 | {"comments":[],"summary":"Submission analyzed. No automated suggestions found."} -------------------------------------------------------------------------------- /test_data/two_fer/informative_comments/lib/two_fer.ex: -------------------------------------------------------------------------------- 1 | defmodule TwoFer do 2 | # missing moduledoc 3 | # @moduledoc false 4 | 5 | @doc """ 6 | Two-fer or 2-fer is short for two for one. One for you and one for me. 7 | 8 | Using a tab like this or like this \t in a @doc is allowed. 9 | """ 10 | @spec two_fer(String.t()) :: String.t() 11 | def two_fer(name \\ "you") when is_binary(name) do 12 | "One for #{name}, one for me." 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test_data/two_fer/missing_example_solution/.meta/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "blurb": "Create a sentence of the form \"One for X, one for me.\"", 3 | "authors": [], 4 | "files": { 5 | "solution": ["lib/two_fer.ex"], 6 | "test": ["test/two_fer_test.exs"], 7 | "example": [".meta/example.ex"] 8 | }, 9 | "source_url": "https://github.com/exercism/problem-specifications/issues/757" 10 | } 11 | -------------------------------------------------------------------------------- /test_data/two_fer/missing_example_solution/expected_analysis.json: -------------------------------------------------------------------------------- 1 | {"comments":[],"summary":"Submission analyzed. No automated suggestions found."} -------------------------------------------------------------------------------- /test_data/two_fer/missing_example_solution/lib/two_fer.ex: -------------------------------------------------------------------------------- 1 | defmodule TwoFer do 2 | @moduledoc false 3 | 4 | @doc """ 5 | Two-fer or 2-fer is short for two for one. One for you and one for me. 6 | 7 | Using a tab like this or like this \t in a @doc is allowed. 8 | """ 9 | @spec two_fer(String.t()) :: String.t() 10 | def two_fer(name \\ "you") when is_binary(name) do 11 | "One for #{name}, one for me." 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test_data/two_fer/missing_example_solution/test/two_fer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TwoFerTest do 2 | use ExUnit.Case 3 | 4 | test "no name given" do 5 | assert TwoFer.two_fer() == "One for you, one for me." 6 | end 7 | 8 | @tag :pending 9 | test "a name given" do 10 | assert TwoFer.two_fer("Gilberto Barrosi") == "One for Gilberto Barros, one for me." 11 | end 12 | 13 | @tag :pending 14 | test "when the parameter is a number" do 15 | assert_raise FunctionClauseError, fn -> 16 | TwoFer.two_fer(10) 17 | end 18 | end 19 | 20 | @tag :pending 21 | test "when the parameter is an atom" do 22 | assert_raise FunctionClauseError, fn -> 23 | TwoFer.two_fer(:bob) 24 | end 25 | end 26 | 27 | @tag :pending 28 | test "when the parameter is a charlist" do 29 | assert_raise FunctionClauseError, fn -> 30 | refute TwoFer.two_fer('Jon Snow') 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test_data/two_fer/missing_file_solution/.meta/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "blurb": "Create a sentence of the form \"One for X, one for me.\"", 3 | "authors": [], 4 | "files": { 5 | "solution": ["lib/two_fer.ex"], 6 | "test": ["test/two_fer_test.exs"], 7 | "example": [".meta/example.ex"] 8 | }, 9 | "source_url": "https://github.com/exercism/problem-specifications/issues/757" 10 | } 11 | -------------------------------------------------------------------------------- /test_data/two_fer/missing_file_solution/.meta/example.ex: -------------------------------------------------------------------------------- 1 | defmodule TwoFer do 2 | @doc """ 3 | Two-fer or 2-fer is short for two for one. One for you and one for me. 4 | """ 5 | @spec two_fer(String.t()) :: String.t() 6 | def two_fer(name \\ "you") when is_binary(name), do: "One for #{name}, one for me." 7 | end 8 | -------------------------------------------------------------------------------- /test_data/two_fer/missing_file_solution/README.md: -------------------------------------------------------------------------------- 1 | # Two Fer 2 | 3 | `Two-fer` or `2-fer` is short for two for one. One for you and one for me. 4 | 5 | ```text 6 | "One for X, one for me." 7 | ``` 8 | 9 | When X is a name or "you". 10 | 11 | If the given name is "Alice", the result should be "One for Alice, one for me." 12 | If no name is given, the result should be "One for you, one for me." 13 | 14 | 15 | ## Running tests 16 | 17 | Execute the tests with: 18 | 19 | ```bash 20 | $ elixir two_fer_test.exs 21 | ``` 22 | 23 | ### Pending tests 24 | 25 | In the test suites, all but the first test have been skipped. 26 | 27 | Once you get a test passing, you can unskip the next one by 28 | commenting out the relevant `@tag :pending` with a `#` symbol. 29 | 30 | For example: 31 | 32 | ```elixir 33 | # @tag :pending 34 | test "shouting" do 35 | assert Bob.hey("WATCH OUT!") == "Whoa, chill out!" 36 | end 37 | ``` 38 | 39 | Or, you can enable all the tests by commenting out the 40 | `ExUnit.configure` line in the test suite. 41 | 42 | ```elixir 43 | # ExUnit.configure exclude: :pending, trace: true 44 | ``` 45 | 46 | If you're stuck on something, it may help to look at some of 47 | the [available resources](https://exercism.io/tracks/elixir/resources) 48 | out there where answers might be found. 49 | 50 | ## Source 51 | 52 | [https://en.wikipedia.org/wiki/Two-fer](https://en.wikipedia.org/wiki/Two-fer) 53 | 54 | ## Submitting Incomplete Solutions 55 | It's possible to submit an incomplete solution so you can see how others have completed the exercise. 56 | -------------------------------------------------------------------------------- /test_data/two_fer/missing_file_solution/expected_analysis.json: -------------------------------------------------------------------------------- 1 | {"comments":[{"comment":"elixir.general.file_not_found","params":{"file_name":"two_fer.ex","path":"test_data/two_fer/missing_file_solution"},"type":"essential"}],"summary":"Check the comments for things to fix. 🛠"} -------------------------------------------------------------------------------- /test_data/two_fer/missing_file_solution/test/test.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start(autorun: false) 2 | ExUnit.configure(exclude: :pending, trace: true) 3 | 4 | Code.require_file("two_fer.exs", __DIR__) 5 | Code.require_file("two_fer_test.exs", __DIR__) 6 | 7 | ExUnit.Server.modules_loaded() 8 | 9 | ExUnit.run() 10 | 11 | -------------------------------------------------------------------------------- /test_data/two_fer/missing_file_solution/test/two_fer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TwoFerTest do 2 | use ExUnit.Case 3 | 4 | test "no name given" do 5 | assert TwoFer.two_fer() == "One for you, one for me." 6 | end 7 | 8 | @tag :pending 9 | test "a name given" do 10 | assert TwoFer.two_fer("Gilberto Barrosi") == "One for Gilberto Barros, one for me." 11 | end 12 | 13 | @tag :pending 14 | test "when the parameter is a number" do 15 | assert_raise FunctionClauseError, fn -> 16 | TwoFer.two_fer(10) 17 | end 18 | end 19 | 20 | @tag :pending 21 | test "when the parameter is an atom" do 22 | assert_raise FunctionClauseError, fn -> 23 | TwoFer.two_fer(:bob) 24 | end 25 | end 26 | 27 | @tag :pending 28 | test "when the parameter is a charlist" do 29 | assert_raise FunctionClauseError, fn -> 30 | refute TwoFer.two_fer('Jon Snow') 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test_data/two_fer/perfect_solution/.meta/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "blurb": "Create a sentence of the form \"One for X, one for me.\"", 3 | "authors": [], 4 | "files": { 5 | "solution": ["lib/two_fer.ex"], 6 | "test": ["test/two_fer_test.exs"], 7 | "example": [".meta/example.ex"] 8 | }, 9 | "source_url": "https://github.com/exercism/problem-specifications/issues/757" 10 | } 11 | -------------------------------------------------------------------------------- /test_data/two_fer/perfect_solution/.meta/example.ex: -------------------------------------------------------------------------------- 1 | defmodule TwoFer do 2 | @doc """ 3 | Two-fer or 2-fer is short for two for one. One for you and one for me. 4 | """ 5 | @spec two_fer(String.t()) :: String.t() 6 | def two_fer(name \\ "you") when is_binary(name), do: "One for #{name}, one for me." 7 | end 8 | -------------------------------------------------------------------------------- /test_data/two_fer/perfect_solution/README.md: -------------------------------------------------------------------------------- 1 | # Two Fer 2 | 3 | `Two-fer` or `2-fer` is short for two for one. One for you and one for me. 4 | 5 | ```text 6 | "One for X, one for me." 7 | ``` 8 | 9 | When X is a name or "you". 10 | 11 | If the given name is "Alice", the result should be "One for Alice, one for me." 12 | If no name is given, the result should be "One for you, one for me." 13 | 14 | 15 | ## Running tests 16 | 17 | Execute the tests with: 18 | 19 | ```bash 20 | $ elixir two_fer_test.exs 21 | ``` 22 | 23 | ### Pending tests 24 | 25 | In the test suites, all but the first test have been skipped. 26 | 27 | Once you get a test passing, you can unskip the next one by 28 | commenting out the relevant `@tag :pending` with a `#` symbol. 29 | 30 | For example: 31 | 32 | ```elixir 33 | # @tag :pending 34 | test "shouting" do 35 | assert Bob.hey("WATCH OUT!") == "Whoa, chill out!" 36 | end 37 | ``` 38 | 39 | Or, you can enable all the tests by commenting out the 40 | `ExUnit.configure` line in the test suite. 41 | 42 | ```elixir 43 | # ExUnit.configure exclude: :pending, trace: true 44 | ``` 45 | 46 | If you're stuck on something, it may help to look at some of 47 | the [available resources](https://exercism.io/tracks/elixir/resources) 48 | out there where answers might be found. 49 | 50 | ## Source 51 | 52 | [https://en.wikipedia.org/wiki/Two-fer](https://en.wikipedia.org/wiki/Two-fer) 53 | 54 | ## Submitting Incomplete Solutions 55 | It's possible to submit an incomplete solution so you can see how others have completed the exercise. 56 | -------------------------------------------------------------------------------- /test_data/two_fer/perfect_solution/expected_analysis.json: -------------------------------------------------------------------------------- 1 | {"comments":[],"summary":"Submission analyzed. No automated suggestions found."} -------------------------------------------------------------------------------- /test_data/two_fer/perfect_solution/lib/two_fer.ex: -------------------------------------------------------------------------------- 1 | defmodule TwoFer do 2 | @moduledoc false 3 | 4 | @doc """ 5 | Two-fer or 2-fer is short for two for one. One for you and one for me. 6 | 7 | Using a tab like this or like this \t in a @doc is allowed. 8 | """ 9 | @spec two_fer(String.t()) :: String.t() 10 | def two_fer(name \\ "you") when is_binary(name) do 11 | "One for #{name}, one for me." 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test_data/two_fer/perfect_solution/test/test.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start(autorun: false) 2 | ExUnit.configure(exclude: :pending, trace: true) 3 | 4 | Code.require_file("two_fer.exs", __DIR__) 5 | Code.require_file("two_fer_test.exs", __DIR__) 6 | 7 | ExUnit.Server.modules_loaded() 8 | 9 | ExUnit.run() 10 | 11 | -------------------------------------------------------------------------------- /test_data/two_fer/perfect_solution/test/two_fer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TwoFerTest do 2 | use ExUnit.Case 3 | 4 | test "no name given" do 5 | assert TwoFer.two_fer() == "One for you, one for me." 6 | end 7 | 8 | @tag :pending 9 | test "a name given" do 10 | assert TwoFer.two_fer("Gilberto Barros") == "One for Gilberto Barros, one for me." 11 | end 12 | 13 | @tag :pending 14 | test "when the parameter is a number" do 15 | assert_raise FunctionClauseError, fn -> 16 | TwoFer.two_fer(10) 17 | end 18 | end 19 | 20 | @tag :pending 21 | test "when the parameter is an atom" do 22 | assert_raise FunctionClauseError, fn -> 23 | TwoFer.two_fer(:bob) 24 | end 25 | end 26 | 27 | @tag :pending 28 | test "when the parameter is a charlist" do 29 | assert_raise FunctionClauseError, fn -> 30 | refute TwoFer.two_fer('Jon Snow') 31 | end 32 | end 33 | end 34 | --------------------------------------------------------------------------------