├── .formatter.exs ├── .github └── FUNDING.yml ├── .gitignore ├── .todo.exs ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── config └── config.exs ├── lib ├── cli │ └── cli.ex ├── codetag_entry.ex ├── config.ex ├── ex_todo.ex ├── file_summary.ex ├── file_utils.ex ├── mix │ ├── todo.ex │ └── todo.gen.config.ex └── output_utils.ex ├── mix.exs ├── mix.lock ├── sample_output.jpg └── test ├── file_utils_test.exs ├── sample_files ├── c_sample.c ├── ex_sample.ex └── js │ └── js_sample.js └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [akoutmos] 2 | -------------------------------------------------------------------------------- /.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_todo-*.tar 24 | 25 | -------------------------------------------------------------------------------- /.todo.exs: -------------------------------------------------------------------------------- 1 | %ExTodo.Config{ 2 | error_codetags: ["FIXME", "BUG"], 3 | skip_patterns: [ 4 | ~r/\.git/, 5 | ~r/_build/, 6 | ~r/deps/, 7 | ~r/cover/, 8 | ~r/docs/, 9 | ~r/\.todo\.exs/, 10 | ~r/README\.md/, 11 | ~r/^lib\/.*/, 12 | ~r/file_utils_test\.exs$/ 13 | ], 14 | supported_codetags: ["NOTE", "TODO", "FIXME", "HACK", "BUG"] 15 | } 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | elixir: 3 | - 1.8.1 4 | otp_release: 5 | - 21.1 6 | matrix: 7 | include: 8 | - otp_release: 22.1 9 | elixir: 1.8 10 | - otp_release: 22.1 11 | elixir: 1.9 12 | - otp_release: 22.1 13 | elixir: 1.10 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.1.0] - 2019-6-18 11 | 12 | ### Added 13 | 14 | - Initial release of ExTodo 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Alexander Koutmos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ExTodo 2 | 3 | [![Hex.pm](https://img.shields.io/hexpm/v/ex_todo.svg)](http://hex.pm/packages/ex_todo) [![Build Status](https://travis-ci.org/akoutmos/ex_todo.svg?branch=master)](https://travis-ci.org/akoutmos/ex_todo) 4 | 5 | A simple utility to keep track of codetags in your project. The list of codetags that are captured is configurable via a `.todo.exs` file, and you can even have `mix todo` return a non-zero exit status if it finds certain codetags in your codebase. For example if you would like ex_todo to fail in CI/CD if FIXME or BUG codetags are found, you can do that. 6 | 7 | ExTodo works on all file formats and can be given a list of regular expressions for files/paths that should be skipped. 8 | 9 | Inspiration for output taken from [https://www.npmjs.com/package/leasot](https://www.npmjs.com/package/leasot) 10 | 11 | ## Installation 12 | 13 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 14 | by adding `ex_todo` to your list of dependencies in `mix.exs`: 15 | 16 | ```elixir 17 | def deps do 18 | [ 19 | {:ex_todo, "~> 0.1.0"} 20 | ] 21 | end 22 | ``` 23 | 24 | Documentation can be found at [https://hexdocs.pm/ex_todo](https://hexdocs.pm/ex_todo). 25 | 26 | ## Usage 27 | 28 | ExTodo comes with 2 mix tasks. One to run the documentation coverage report, and another to generate a `.todo.exs` config file. 29 | 30 | To run the ex_todo mix task and generate a report run: `mix todo` 31 | To generate a `.todo.exs` config file with defaults, run: `mix todo.gen.config` 32 | 33 | ## Sample report 34 | 35 | By running `mix todo` in this repo we get: 36 | 37 | ExTodo 38 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # third-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :ex_todo, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:ex_todo, :key) 18 | # 19 | # You can also configure a third-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env()}.exs" 31 | -------------------------------------------------------------------------------- /lib/cli/cli.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTodo.CLI do 2 | alias ExTodo.{Config, FileSummary, FileUtils, OutputUtils, CodetagEntry} 3 | alias Mix.Shell.IO 4 | 5 | @doc "Given a config, run the codetags report" 6 | def run_report(%Config{} = config) do 7 | config 8 | |> FileUtils.get_all_files() 9 | |> FileUtils.read_file_list_contents() 10 | |> FileUtils.get_file_list_codetags(config) 11 | |> Enum.map(fn {path, file_codetags} -> 12 | FileSummary.build(path, file_codetags) 13 | end) 14 | |> Enum.sort(fn file_summary_1, file_summary_2 -> 15 | file_summary_1.file_path <= file_summary_2.file_path 16 | end) 17 | |> output_report() 18 | |> output_summary(config) 19 | |> report_result(config) 20 | end 21 | 22 | defp output_report([]) do 23 | IO.info("No codetags of interest have been found in the codebase.") 24 | end 25 | 26 | defp output_report(entries) do 27 | Enum.each(entries, fn entry -> 28 | entry.file_path 29 | |> OutputUtils.blue_text() 30 | |> OutputUtils.underline_text() 31 | |> IO.info() 32 | 33 | Enum.each(entry.todo_entries, fn todo_entry -> 34 | line_label = 35 | " line #{Integer.to_string(todo_entry.line)}" 36 | |> OutputUtils.gen_fixed_width_string(12, 1) 37 | |> OutputUtils.green_text() 38 | 39 | type_label = 40 | todo_entry.type 41 | |> OutputUtils.gen_fixed_width_string(8, 1) 42 | |> OutputUtils.white_text() 43 | 44 | comment = OutputUtils.light_cyan_text(todo_entry.comment) 45 | 46 | IO.info("#{line_label}#{type_label}#{comment}") 47 | end) 48 | 49 | IO.info("") 50 | end) 51 | 52 | entries 53 | end 54 | 55 | defp output_summary(entries, %Config{} = config) do 56 | "ExTodo Scan Summary" 57 | |> OutputUtils.blue_text() 58 | |> OutputUtils.underline_text() 59 | |> IO.info() 60 | 61 | entries 62 | |> Enum.map(fn files -> 63 | files.todo_entries 64 | end) 65 | |> List.flatten() 66 | |> Enum.reduce(%{}, fn entry, acc -> 67 | acc 68 | |> Map.update(entry.type, 1, &(&1 + 1)) 69 | end) 70 | |> Map.to_list() 71 | |> Enum.sort(fn {keyword_1, _}, {keyword_2, _} -> 72 | keyword_1 <= keyword_2 73 | end) 74 | |> Enum.each(fn {keyword, count} -> 75 | if keyword in config.error_codetags do 76 | type = 77 | " #{keyword}" 78 | |> OutputUtils.gen_fixed_width_string(10, 1) 79 | |> OutputUtils.red_text() 80 | 81 | count = 82 | count 83 | |> OutputUtils.gen_fixed_width_string(10, 1) 84 | |> OutputUtils.red_text() 85 | 86 | IO.info("#{type}#{count}") 87 | else 88 | type = 89 | " #{keyword}" 90 | |> OutputUtils.gen_fixed_width_string(10, 1) 91 | |> OutputUtils.green_text() 92 | 93 | count = 94 | count 95 | |> OutputUtils.gen_fixed_width_string(10, 1) 96 | |> OutputUtils.green_text() 97 | 98 | IO.info("#{type}#{count}") 99 | end 100 | end) 101 | 102 | entries 103 | end 104 | 105 | defp report_result(entries, %Config{} = config) do 106 | found_errors = 107 | entries 108 | |> Enum.map(fn files -> 109 | files.todo_entries 110 | end) 111 | |> List.flatten() 112 | |> Enum.find_value(false, fn %CodetagEntry{type: type} -> 113 | type in config.error_codetags 114 | end) 115 | 116 | not found_errors 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /lib/codetag_entry.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTodo.CodetagEntry do 2 | @moduledoc """ 3 | This module defines a struct which contains all the information for a single 4 | codetag entry. 5 | """ 6 | 7 | alias __MODULE__ 8 | 9 | defstruct ~w(type line comment)a 10 | 11 | @doc "Build a single codetag entry" 12 | def build(type, line, comment) do 13 | %CodetagEntry{ 14 | type: type, 15 | line: line, 16 | comment: comment 17 | } 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/config.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTodo.Config do 2 | @moduledoc """ 3 | This module defines a struct which houses all the 4 | configuration data for ex_todo. 5 | """ 6 | 7 | @config_file ".todo.exs" 8 | 9 | alias __MODULE__ 10 | 11 | defstruct supported_codetags: ~w(NOTE TODO FIXME HACK BUG), 12 | error_codetags: ~w(FIXME BUG), 13 | skip_patterns: [~r/\.git/, ~r/_build/, ~r/deps/, ~r/cover/, ~r/docs/, ~r/\.todo\.exs/] 14 | 15 | @doc """ 16 | Get the configuration defaults as a Config struct 17 | """ 18 | def config_defaults_as_map, do: %Config{} 19 | 20 | @doc """ 21 | Get the configuration defaults as a string 22 | """ 23 | def config_defaults_as_string do 24 | config = quote do: unquote(%Config{}) 25 | 26 | config 27 | |> Macro.to_string() 28 | |> Code.format_string!() 29 | end 30 | 31 | @doc """ 32 | Get the configuration file name 33 | """ 34 | def config_file, do: @config_file 35 | end 36 | -------------------------------------------------------------------------------- /lib/ex_todo.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTodo do 2 | @moduledoc """ 3 | Documentation for ExTodo. 4 | """ 5 | end 6 | -------------------------------------------------------------------------------- /lib/file_summary.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTodo.FileSummary do 2 | @moduledoc """ 3 | This module defines a struct which is used to encapsulate all the information 4 | for a file that contains code tags 5 | """ 6 | 7 | alias __MODULE__ 8 | 9 | defstruct file_path: nil, todo_entries: [] 10 | 11 | @doc "Build a file summary sruct" 12 | def build(file_path, todo_entries) do 13 | %FileSummary{ 14 | file_path: file_path, 15 | todo_entries: todo_entries 16 | } 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/file_utils.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTodo.FileUtils do 2 | @moduledoc """ 3 | Utililities for deals with files and searching for code tags. 4 | """ 5 | 6 | alias ExTodo.{Config, CodetagEntry} 7 | 8 | @glob_pattern "./**" 9 | 10 | @doc "Get all of the files according to the fileglob" 11 | def get_all_files(%Config{} = config, file_glob \\ @glob_pattern) do 12 | file_glob 13 | |> Path.wildcard(match_dot: true) 14 | |> Enum.reject(fn entry -> 15 | not_file?(entry) or path_in_ignore_list?(entry, config.skip_patterns) 16 | end) 17 | end 18 | 19 | @doc "Read the contents of all the files in the provided list" 20 | def read_file_list_contents(file_list) do 21 | file_list 22 | |> Enum.reduce([], fn file_path, acc -> 23 | file_path 24 | |> File.read() 25 | |> case do 26 | {:ok, file_contents} -> 27 | [{file_path, file_contents} | acc] 28 | 29 | {:error, _reason} -> 30 | acc 31 | end 32 | end) 33 | end 34 | 35 | @doc "Get all of the codetags within the list of files given the config settings" 36 | def get_file_list_codetags(file_contents_list, %Config{} = config) do 37 | file_contents_list 38 | |> Enum.reduce([], fn {file_path, file_contents}, acc -> 39 | file_contents 40 | |> String.split("\n") 41 | |> get_lines_with_codetags(config) 42 | |> case do 43 | [] -> 44 | acc 45 | 46 | codetag_entries -> 47 | [{file_path, codetag_entries} | acc] 48 | end 49 | end) 50 | end 51 | 52 | defp path_in_ignore_list?(path, skip_patterns) do 53 | Enum.find_value(skip_patterns, false, fn skip_pattern -> 54 | Regex.match?(skip_pattern, path) 55 | end) 56 | end 57 | 58 | defp not_file?(path), do: not File.regular?(path) 59 | 60 | defp get_lines_with_codetags(file_contents, config) do 61 | get_lines_with_codetags(file_contents, config, 1, []) 62 | end 63 | 64 | defp get_lines_with_codetags([], _config, _line_num, acc) do 65 | Enum.reverse(acc) 66 | end 67 | 68 | defp get_lines_with_codetags([current_line | tail], config, line_num, acc) do 69 | fuzzy_match_list = 70 | config.supported_codetags 71 | |> Enum.map(fn keyword -> 72 | [ 73 | {keyword, "#{keyword}:"}, 74 | {keyword, "#{keyword} :"}, 75 | {keyword, "#{keyword}-"}, 76 | {keyword, "#{keyword} -"}, 77 | {keyword, keyword} 78 | ] 79 | end) 80 | |> List.flatten() 81 | 82 | {original, keyword_in_line} = 83 | Enum.find(fuzzy_match_list, {:not_found, :not_found}, fn {_original, keyword} -> 84 | String.contains?(current_line, keyword) 85 | end) 86 | 87 | acc = 88 | if keyword_in_line != :not_found do 89 | comment = 90 | current_line 91 | |> String.split(keyword_in_line, parts: 2) 92 | |> Enum.at(1) 93 | |> String.trim() 94 | 95 | [%CodetagEntry{type: original, line: line_num, comment: comment} | acc] 96 | else 97 | acc 98 | end 99 | 100 | get_lines_with_codetags(tail, config, line_num + 1, acc) 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /lib/mix/todo.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Todo do 2 | @moduledoc false 3 | 4 | use Mix.Task 5 | 6 | alias ExTodo.Config 7 | alias Mix.Shell.IO 8 | 9 | @shortdoc "Find TODO, FIXME, NOTE, etc throughout your codebase" 10 | 11 | @doc """ 12 | This Mix task generates a ExTodo report of the project. 13 | """ 14 | def run(_args) do 15 | result = 16 | Config.config_file() 17 | |> load_config_file() 18 | |> merge_defaults() 19 | |> ExTodo.CLI.run_report() 20 | 21 | unless result do 22 | System.at_exit(fn _ -> 23 | exit({:shutdown, 1}) 24 | end) 25 | end 26 | 27 | :ok 28 | end 29 | 30 | defp load_config_file(file) do 31 | if File.exists?(file) do 32 | IO.info("ExTodo file found. Loading configuration.") 33 | 34 | {config, _bindings} = Code.eval_file(file) 35 | 36 | config 37 | else 38 | IO.info("ExTodo file not found. Using defaults.") 39 | 40 | %{} 41 | end 42 | end 43 | 44 | defp merge_defaults(config) do 45 | Map.merge(Config.config_defaults_as_map(), config) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/mix/todo.gen.config.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Todo.Gen.Config do 2 | @moduledoc false 3 | 4 | use Mix.Task 5 | 6 | alias Mix.Shell.IO 7 | alias ExTodo.Config 8 | 9 | @shortdoc "Creates a .todo.exs config file with defaults" 10 | 11 | @doc """ 12 | This Mix task generates a .todo.exs configuration file 13 | """ 14 | def run(_args) do 15 | create_file = 16 | if File.exists?(Config.config_file()) do 17 | IO.yes?("An existing ex_todo config file already exists. Overwrite?") 18 | else 19 | true 20 | end 21 | 22 | if create_file do 23 | create_config_file() 24 | 25 | IO.info("Successfully created .todo.exs file.") 26 | else 27 | IO.info("Did not create .todo.exs file.") 28 | end 29 | end 30 | 31 | defp create_config_file do 32 | File.cwd!() 33 | |> Path.join(Config.config_file()) 34 | |> File.write(Config.config_defaults_as_string()) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/output_utils.ex: -------------------------------------------------------------------------------- 1 | defmodule ExTodo.OutputUtils do 2 | @moduledoc """ 3 | This module is used to format strings for STDOUT so that reports are easy 4 | to read. 5 | """ 6 | 7 | alias Elixir.IO.ANSI 8 | 9 | @doc "Underline the provided text" 10 | def underline_text(text) do 11 | ANSI.underline() <> text <> ANSI.reset() 12 | end 13 | 14 | @doc "Make the provided text green" 15 | def green_text(text) do 16 | ANSI.green() <> text <> ANSI.reset() 17 | end 18 | 19 | @doc "Make the provided text blue" 20 | def blue_text(text) do 21 | ANSI.blue() <> text <> ANSI.reset() 22 | end 23 | 24 | @doc "Make the provided text white" 25 | def white_text(text) do 26 | ANSI.white() <> text <> ANSI.reset() 27 | end 28 | 29 | @doc "Make the provided text red" 30 | def red_text(text) do 31 | ANSI.red() <> text <> ANSI.reset() 32 | end 33 | 34 | @doc "Make the provided text ligth cyan" 35 | def light_cyan_text(text) do 36 | ANSI.light_cyan() <> text <> ANSI.reset() 37 | end 38 | 39 | @doc "Format a string to be of a certain width and have a certain padding" 40 | def gen_fixed_width_string(value, width, padding \\ 2) 41 | 42 | def gen_fixed_width_string(value, width, padding) when is_integer(value) do 43 | value 44 | |> Integer.to_string() 45 | |> gen_fixed_width_string(width, padding) 46 | end 47 | 48 | def gen_fixed_width_string(value, width, padding) do 49 | sub_string_length = width - (padding + 1) 50 | 51 | value 52 | |> String.slice(0..sub_string_length) 53 | |> String.pad_trailing(width) 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ExTodo.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :ex_todo, 7 | version: "0.1.0", 8 | elixir: "~> 1.7", 9 | name: "ExTodo", 10 | source_url: "https://github.com/akoutmos/ex_todo", 11 | homepage_url: "https://hex.pm/packages/ex_todo", 12 | description: "A simple utility to find codetags within a project", 13 | elixirc_paths: elixirc_paths(Mix.env()), 14 | start_permanent: Mix.env() == :prod, 15 | docs: [ 16 | main: "readme", 17 | extras: ["README.md"] 18 | ], 19 | package: package(), 20 | deps: deps() 21 | ] 22 | end 23 | 24 | def application do 25 | [ 26 | extra_applications: [:logger] 27 | ] 28 | end 29 | 30 | defp package() do 31 | [ 32 | name: "ex_todo", 33 | files: ~w(lib mix.exs README.md LICENSE CHANGELOG.md), 34 | licenses: ["MIT"], 35 | links: %{"GitHub" => "https://github.com/akoutmos/ex_todo"} 36 | ] 37 | end 38 | 39 | defp elixirc_paths(:test), do: ["lib", "test/sample_files"] 40 | defp elixirc_paths(_), do: ["lib"] 41 | 42 | defp deps do 43 | [ 44 | {:ex_doc, ">= 0.0.0"} 45 | ] 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark": {:hex, :earmark, "1.3.2", "b840562ea3d67795ffbb5bd88940b1bed0ed9fa32834915125ea7d02e35888a5", [:mix], [], "hexpm"}, 3 | "ex_doc": {:hex, :ex_doc, "0.20.2", "1bd0dfb0304bade58beb77f20f21ee3558cc3c753743ae0ddbb0fd7ba2912331", [:mix], [{:earmark, "~> 1.3", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, 4 | "makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, 5 | "makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, 6 | "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"}, 7 | } 8 | -------------------------------------------------------------------------------- /sample_output.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akoutmos/ex_todo/f1aaf2ccb4ecdefb3fb48a36ef2cc76a0d9045fa/sample_output.jpg -------------------------------------------------------------------------------- /test/file_utils_test.exs: -------------------------------------------------------------------------------- 1 | defmodule FilUtilsTest do 2 | use ExUnit.Case 3 | 4 | alias ExTodo.{Config, FileUtils} 5 | 6 | describe "get_all_files/2" do 7 | test "should return a list of all the files that match the catch all glob" do 8 | files = get_all_sample_files() 9 | 10 | assert files == [ 11 | "test/sample_files/c_sample.c", 12 | "test/sample_files/ex_sample.ex", 13 | "test/sample_files/js/js_sample.js" 14 | ] 15 | end 16 | 17 | test "should return a list of all the files that match a certain file glob" do 18 | files = 19 | %Config{} 20 | |> FileUtils.get_all_files("./**/*.c") 21 | |> Enum.sort() 22 | 23 | assert files == ["test/sample_files/c_sample.c"] 24 | end 25 | 26 | test "should return a list of all files except the skipped files" do 27 | files = get_all_sample_files(%Config{skip_patterns: [~r/c_sample\.c/]}) 28 | 29 | assert files == [ 30 | "test/sample_files/ex_sample.ex", 31 | "test/sample_files/js/js_sample.js" 32 | ] 33 | end 34 | 35 | test "should return a list of all files except the skipped directories" do 36 | files = get_all_sample_files(%Config{skip_patterns: [~r/test\/sample_files\/js/]}) 37 | 38 | assert files == [ 39 | "test/sample_files/c_sample.c", 40 | "test/sample_files/ex_sample.ex" 41 | ] 42 | end 43 | end 44 | 45 | describe "read_file_list_contents/1" do 46 | test "should read the contents of all the files provided" do 47 | file_paths = get_all_sample_files() 48 | 49 | files_contents = 50 | file_paths 51 | |> FileUtils.read_file_list_contents() 52 | 53 | Enum.each(files_contents, fn {file_path, contents} -> 54 | assert file_path in file_paths 55 | assert String.length(contents) > 0 56 | end) 57 | end 58 | 59 | test "should return an empty list if file list is empty" do 60 | files = 61 | %Config{} 62 | |> FileUtils.get_all_files("./**/*.no_file") 63 | |> FileUtils.read_file_list_contents() 64 | 65 | assert files == [] 66 | end 67 | end 68 | 69 | describe "get_file_list_codetags/2" do 70 | test "should return all the configured codetags found within some files" do 71 | config = %Config{skip_patterns: [~r/c_sample\.c/, ~r/js_sample\.js/]} 72 | files = get_all_sample_files(config) 73 | 74 | [{"test/sample_files/ex_sample.ex", codetag_results}] = 75 | files 76 | |> FileUtils.read_file_list_contents() 77 | |> FileUtils.get_file_list_codetags(config) 78 | 79 | assert length(codetag_results) == 2 80 | end 81 | 82 | test "should return an empty list if no codetags are found within some files" do 83 | config = %Config{ 84 | skip_patterns: [~r/c_sample\.c/, ~r/js_sample\.js/], 85 | supported_codetags: [] 86 | } 87 | 88 | files = get_all_sample_files(config) 89 | 90 | results = 91 | files 92 | |> FileUtils.read_file_list_contents() 93 | |> FileUtils.get_file_list_codetags(config) 94 | 95 | assert results == [] 96 | end 97 | 98 | test "should return a list of files that only fullfil the configured codetags" do 99 | config = %Config{ 100 | skip_patterns: [~r/c_sample\.c/, ~r/js_sample\.js/], 101 | supported_codetags: ["FIXME"] 102 | } 103 | 104 | files = get_all_sample_files(config) 105 | 106 | [{"test/sample_files/ex_sample.ex", codetag_results}] = 107 | files 108 | |> FileUtils.read_file_list_contents() 109 | |> FileUtils.get_file_list_codetags(config) 110 | 111 | assert length(codetag_results) == 1 112 | end 113 | end 114 | 115 | defp get_all_sample_files(config \\ %Config{}) do 116 | config 117 | |> FileUtils.get_all_files("./test/sample_files/**") 118 | |> Enum.sort() 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /test/sample_files/c_sample.c: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * TODO: This app is pretty complex 4 | * 5 | * FIXME: I should refactor it 6 | * 7 | * NOTE: You needs to start somewhere 8 | * 9 | */ 10 | 11 | #include 12 | 13 | int main(void) { 14 | printf("Hello World!\n"); 15 | return 0; 16 | } 17 | -------------------------------------------------------------------------------- /test/sample_files/ex_sample.ex: -------------------------------------------------------------------------------- 1 | defmodule ExSample do 2 | @moduledoc """ 3 | This module is super complex. 4 | 5 | TODO: This should really really really be refactored 6 | 7 | FIXME: I mean it! 8 | """ 9 | 10 | def do_th_things(num) do 11 | num + 1 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/sample_files/js/js_sample.js: -------------------------------------------------------------------------------- 1 | // FIXME: Don't do anything CPU bound and block the event loop 2 | // TODO: Watch https://www.youtube.com/watch?v=JvBT4XBdoUE to see why the BEAM is awesome 3 | 4 | const http = require('http') 5 | 6 | http 7 | .createServer(function(req, res) { 8 | res.end('hello world!') 9 | }) 10 | .listen(9000) 11 | 12 | console.log('Server listening on port 9000') 13 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------