├── .formatter.exs ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── config ├── config.exs ├── dev.exs ├── prod.exs └── test.exs ├── lib ├── meta.ex ├── todo.ex └── todo_task.ex ├── mix.exs ├── mix.lock └── test ├── test_helper.exs └── todo_test.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | locals_without_parens = [ 2 | todo: 1 3 | ] 4 | 5 | [ 6 | inputs: ["mix.exs", "{lib,test}/**/*.{ex,exs}"], 7 | locals_without_parens: locals_without_parens, 8 | export: [locals_without_parens: locals_without_parens] 9 | ] 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | /doc 6 | .fetch 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org/) and [Keep a Changelog](http://keepachangelog.com/). 5 | 6 | 7 | 8 | ## Unreleased 9 | --- 10 | 11 | ### New 12 | 13 | ### Changes 14 | 15 | ### Fixes 16 | 17 | ### Breaks 18 | 19 | 20 | ## 1.5.1 - (2020-09-29) 21 | --- 22 | 23 | ### New 24 | * Todos texts are wrapped to 70 characters and indented in lists 25 | 26 | 27 | ## 1.5.0 - (2020-09-27) 28 | --- 29 | 30 | ### New 31 | * Display a warning at compile time if attributes are persisted but env is neither :dev nor :test 32 | * Removed configuration for compile time print 33 | * Removed the compile time prints 34 | * Added some colors to the CLI output 35 | 36 | 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 niahoo osef 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A simple Todo utiliy for Elixir 2 | 3 | 4 | 5 | Todo is a small macro that helps you procrastinate more when writing Elixir 6 | code. 7 | 8 | Just put todo messages in your code and you will be able to see them all at once 9 | in the command line. 10 | 11 | A [mix command](#mix-command) is available to scan all modules for todo items 12 | and print all at once. 13 | 14 | ```elixir 15 | defmodule MyApp.MyMod do 16 | use TODO 17 | 18 | @todo "0.0.1": "Finish that feature later" 19 | @todo "add @moduledoc" 20 | 21 | def function(data) when is_list(data) do 22 | todo "0.0.1": "support binary data" 23 | send_data(data) 24 | end 25 | end 26 | ``` 27 | 28 | ### Installation 29 | 30 | Add the dependency in your `mix.exs` file. 31 | 32 | ```elixir 33 | defp deps do 34 | [{:todo, "~> 1.5"}] 35 | end 36 | ``` 37 | 38 | ### How to use 39 | 40 | Add `@todo` attributes in a module body, or use the `todo` macro inside 41 | functions. Both take the same arguments: 42 | 43 | - A simple message. It will be printed as an info and will have no target 44 | version associated. 45 | - A keyword list with version numbers as keys and messages as values. Messages 46 | with a version number lower than your current project version (according to 47 | your mix project file) will be printed as warnings as those features should be 48 | finished already. 49 | 50 | You can set multiple todos at once : 51 | 52 | ```elixir 53 | defmodule MyApp.MyMod do 54 | use TODO 55 | 56 | @todo "0.0.1": "Finish that feature later", 57 | "0.0.2": "Add this other feature" 58 | 59 | end 60 | ``` 61 | 62 | ### Configuration 63 | 64 | Configuration options can be set at module level or at project level. In-module 65 | configuration takes precedence over the global configuration. 66 | 67 | ```elixir 68 | # config/dev.exs 69 | config :todo, persist: true 70 | 71 | # config/prod.exs 72 | config :todo, persist: false 73 | ``` 74 | 75 | ```elixir 76 | # mymod.ex 77 | defmodule MyApp.MyMod do 78 | use TODO, persist: true 79 | end 80 | ``` 81 | 82 | ### Mix command 83 | 84 | This requires `@todo` attributes to be persistent. See the configuration to 85 | enable persistence. 86 | 87 | Todos messages printed at compilation time are interleaved with compilation 88 | messages. Hence the color of ouverdue features. The command allows for a simpler 89 | way to read all the messages at once. 90 | 91 | Enter `mix todo --all` or `mix todo --overdue` in your console to print all the 92 | todos of the current project at once. 93 | 94 | ### Configuration 95 | 96 | The following configuration options is available : 97 | 98 | #### `:print` 99 | 100 | This option controls the default output mode of the mix command. 101 | 102 | - `:overdue` (default value) : only unversionned and features whose version is 103 | outdated are shown. 104 | - `:all` show all todos. 105 | 106 | #### `:persist` 107 | 108 | This option sets the `@todo` module attributes to be persistent. The mix command 109 | shows only persistent attributes. It accepts a boolean value : 110 | 111 | - `true` : todos items are persitent, shown with the mix command and accessible 112 | through the Elixir module API. 113 | - `false` : todos will only be available at compile time. 114 | 115 | The default value is `true` so the command works out of the box with all 116 | modules. It should be set to `false` in production environment. 117 | 118 | ```elixir 119 | config :todo, persist: false 120 | ``` 121 | 122 | 123 | ### Notes 124 | 125 | You may want to have a look at [fixme](https://github.com/henrik/fixme-elixir) 126 | too, which inspired this project. 127 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | import_config "#{Mix.env}.exs" 4 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :todo, persist: true 4 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | # config :todo, persist: true 3 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | -------------------------------------------------------------------------------- /lib/meta.ex: -------------------------------------------------------------------------------- 1 | defmodule TODO.Meta do 2 | @moduledoc false 3 | use TODO 4 | 5 | @todo "1.7.0": "prefer macro usage to pass binary VSNs instead of atoms" 6 | @todo "1.7.0": "provide a --lines option to show each todo's file:line link" 7 | @todo "1.7.0": "provide --next-major --next-minor --next-patch to match the next version" 8 | @todo "1.7.0": "ensure the command fails if there are outdated todos" 9 | end 10 | -------------------------------------------------------------------------------- /lib/todo.ex: -------------------------------------------------------------------------------- 1 | defmodule TODO do 2 | @readme File.cwd!() |> Path.join("README.md") 3 | @external_resource @readme 4 | @moduledoc @readme 5 | |> File.read!() 6 | |> String.split("") 7 | |> Enum.at(1) 8 | 9 | def config_default(:prod, :persist), do: false 10 | def config_default(_, :persist), do: true 11 | def config_default(_, :print), do: :overdue 12 | 13 | def config(key) do 14 | Application.get_env(:todo, key, config_default(Mix.env(), key)) 15 | end 16 | 17 | def validate_print_conf(nil), do: :ok 18 | def validate_print_conf(:all), do: :ok 19 | def validate_print_conf(:overdue), do: :ok 20 | 21 | def validate_print_conf(value) do 22 | raise "Bad print configuration value #{inspect(value)}." 23 | end 24 | 25 | defmacro __using__(opts) do 26 | custom_persist = warn_literal(Keyword.get(opts, :persist)) 27 | persist_conf = if custom_persist == nil, do: config(:persist), else: custom_persist 28 | persist = false !== persist_conf 29 | 30 | Module.register_attribute(__CALLER__.module, :todo, 31 | accumulate: true, 32 | persist: persist 33 | ) 34 | 35 | quote do 36 | mix_env = Mix.env() 37 | 38 | if unquote(persist) and mix_env not in ~w(dev test)a do 39 | TODO.warn_persist_in_prod(__MODULE__, mix_env) 40 | end 41 | 42 | import TODO, only: [todo: 1] 43 | end 44 | end 45 | 46 | defp warn_literal(persist) when is_boolean(persist) when nil == persist, do: persist 47 | 48 | defp warn_literal(other) do 49 | """ 50 | The :persist configuration given to `use TODO` must be a literal boolean, got: 51 | 52 | #{Macro.to_string(other)} 53 | 54 | This is required to support calls to the `todo` macro in the module body scope. 55 | """ 56 | |> IO.warn() 57 | 58 | nil 59 | end 60 | 61 | @doc false 62 | def warn_persist_in_prod(module, mix_env) do 63 | case :persistent_term.get(:todo_prod_warning, false) do 64 | true -> 65 | :ok 66 | 67 | _ -> 68 | :persistent_term.put(:todo_prod_warning, true) 69 | 70 | IO.warn(""" 71 | TODO attributes are persisted in module #{inspect(module)} whereas environment is neither :dev nor :test. (#{inspect(mix_env)}) 72 | 73 | You can disable persistence in your configuration, for instance in 74 | config/prod.exs : 75 | 76 | config :todo, persist: false 77 | """) 78 | end 79 | end 80 | 81 | defmacro todo(items) do 82 | Module.put_attribute(__CALLER__.module, :todo, items) 83 | [] 84 | end 85 | 86 | def get_todos(module) when is_atom(module) do 87 | for {:todo, sublist} <- module.module_info(:attributes) do 88 | sublist 89 | end 90 | |> :lists.flatten() 91 | |> Enum.map(&put_meta(&1, module)) 92 | end 93 | 94 | def get_todos(modules) when is_list(modules) do 95 | modules 96 | |> Enum.map(&get_todos/1) 97 | |> :lists.flatten() 98 | end 99 | 100 | defp put_meta({vsn, msg}, module) do 101 | vsn = if is_atom(vsn), do: Atom.to_string(vsn), else: vsn 102 | 103 | case Version.parse(vsn) do 104 | {:ok, vsn} -> {vsn, module, msg} 105 | :error -> {:no_version, module, "(Invalid vsn) #{msg}"} 106 | end 107 | end 108 | 109 | defp put_meta(msg, module), do: {:no_version, module, msg} 110 | 111 | def output_todos(todos, print_spec, %Version{} = max_vsn) 112 | when length(todos) > 0 and print_spec in [:all, :overdue] do 113 | groups = 114 | case print_spec do 115 | :all -> todos 116 | :overdue -> filter_overdue(todos, max_vsn) 117 | end 118 | # Group by version 119 | |> Enum.group_by(&elem(&1, 0)) 120 | 121 | {unversionned, versionned} = 122 | case Map.pop(groups, :no_version) do 123 | {nil, versionned} -> {[], versionned} 124 | tuple -> tuple 125 | end 126 | 127 | versionned = Enum.sort_by(versionned, &elem(&1, 0), Version) |> :lists.reverse() 128 | 129 | {:ok, width} = :io.columns() 130 | width = min(width, 70) 131 | 132 | # Main title 133 | [?\n, IO.ANSI.cyan(), String.pad_trailing("-- TODO ", width, "-"), IO.ANSI.reset(), ?\n, ?\n] 134 | |> IO.puts() 135 | 136 | if length(unversionned) > 0 do 137 | output_vsn_group(unversionned, version_title("Unversionned"), :normal, width) 138 | |> IO.puts() 139 | end 140 | 141 | versionned 142 | |> Enum.map(fn {vsn, todos} -> 143 | mode = colorspec(vsn, max_vsn) 144 | 145 | title = version_title(vsn, mode) 146 | 147 | output_vsn_group(todos, title, mode, width) 148 | |> IO.puts() 149 | end) 150 | end 151 | 152 | def output_todos(todos, print_spec, %Version{}) 153 | when length(todos) == 0 and print_spec in [:all, :overdue] do 154 | case print_spec do 155 | :all -> 156 | IO.puts("No todos found.") 157 | 158 | :overdue -> 159 | IO.puts("No overdue todos. You're fine.") 160 | end 161 | end 162 | 163 | defp colorspec(vsn, max_vsn) do 164 | if overdue?(vsn, max_vsn), 165 | do: :warn, 166 | else: :normal 167 | end 168 | 169 | defp output_vsn_group(todos, title, mode, width) do 170 | todos_by_mods = 171 | todos 172 | |> group_by_module() 173 | |> Enum.map(fn {module, todos} -> 174 | todolist = Enum.map(todos, &["– ", todo_to_string(&1, width, 2), "\n"]) 175 | 176 | {todolist, module_color} = 177 | case mode do 178 | :normal -> {todolist, :cyan} 179 | :warn -> {color(todolist, :yellow), :light_red} 180 | end 181 | 182 | source_link = format_module_link(module) 183 | 184 | [ 185 | color([inspect(module)], module_color), 186 | source_link, 187 | "\n", 188 | todolist 189 | ] 190 | end) 191 | |> Enum.intersperse("\n") 192 | 193 | [title, "\n\n", todos_by_mods, "\n"] 194 | end 195 | 196 | defp wrap_block(str, width, indent) do 197 | line_prefix = String.duplicate(" ", indent) 198 | 199 | str 200 | # Splitting block on two consecutive line breaks to preserve paragraphs but 201 | # not simple breaks 202 | |> String.split("\n\n") 203 | |> Enum.map(&wrap_line(&1, width - indent, line_prefix)) 204 | |> Enum.intersperse("\n\n") 205 | end 206 | 207 | defp wrap_line(str, width, line_prefix) do 208 | words = 209 | str 210 | |> String.replace("\n", " ") 211 | |> String.split(" ", trim: true) 212 | 213 | Enum.reduce(words, {[], 0}, fn word, {line, len} -> 214 | wlen = String.length(word) 215 | 216 | if wlen + len > width do 217 | {[word, ["\n", line_prefix] | line], wlen} 218 | else 219 | case len do 220 | 0 -> {[word | line], len + wlen + 1} 221 | _ -> {[word, " " | line], len + wlen + 1} 222 | end 223 | end 224 | end) 225 | |> elem(0) 226 | |> :lists.reverse() 227 | end 228 | 229 | defp group_by_module(todos) do 230 | todos 231 | |> Enum.group_by(&elem(&1, 1)) 232 | |> Enum.sort_by(&elem(&1, 0)) 233 | end 234 | 235 | # Return only todos whose version is lower or equal to max_vsn 236 | defp filter_overdue(todos, max_vsn) do 237 | Enum.filter(todos, fn 238 | {:no_version, _, _} -> true 239 | {vsn, _, _} -> overdue?(vsn, max_vsn) 240 | end) 241 | end 242 | 243 | defp color(msg, col) do 244 | [apply(IO.ANSI, col, []), msg, IO.ANSI.default_color()] 245 | end 246 | 247 | defp todo_to_string({_, _, msg}, width, indent) do 248 | if is_binary(msg) do 249 | msg 250 | else 251 | inspect(msg) 252 | end 253 | |> String.trim() 254 | |> wrap_block(width, indent) 255 | end 256 | 257 | defp overdue?(vsn, max_vsn) do 258 | Version.compare(vsn, max_vsn) != :gt 259 | end 260 | 261 | defp version_title(%Version{} = vsn, :normal), 262 | do: version_title("Version #{vsn}") 263 | 264 | defp version_title(%Version{} = vsn, :warn), 265 | do: [version_title("Version #{vsn} – "), color("OVERDUE", :light_red)] 266 | 267 | defp version_title(title), 268 | do: "# #{title}" 269 | 270 | defp format_module_link(module) do 271 | # for {f, 0} <- IO.ANSI.module_info(:exports), f not in [:reset, :clear] do 272 | # IO.puts([ 273 | # String.pad_trailing(to_string(f), 50), 274 | # ": ", 275 | # apply(IO.ANSI, f, []), 276 | # "sample text", 277 | # IO.ANSI.reset() 278 | # ]) 279 | # end 280 | 281 | case get_module_source(module) do 282 | nil -> [] 283 | path -> [color([" ", path], :light_black)] 284 | end 285 | end 286 | 287 | defp get_module_source(module) do 288 | compile_info = module.module_info(:compile) 289 | 290 | case Keyword.fetch(compile_info, :source) do 291 | :error -> 292 | nil 293 | 294 | {:ok, source} -> 295 | source 296 | |> to_string() 297 | |> Path.relative_to(File.cwd!()) 298 | end 299 | end 300 | end 301 | -------------------------------------------------------------------------------- /lib/todo_task.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Todo do 2 | use Mix.Task 3 | alias Mix.Shell.IO, as: Shell 4 | 5 | @shortdoc "List all todos items for the current project" 6 | 7 | def run(argv) do 8 | Mix.Task.run("compile") 9 | config = Mix.Project.config() 10 | app = config[:app] 11 | max_vsn = Version.parse!(config[:version]) 12 | 13 | print_mode = 14 | cond do 15 | Enum.member?(argv, "--all") -> :all 16 | Enum.member?(argv, "--overdue") -> :overdue 17 | true -> TODO.config_default(Mix.env(), :print) 18 | end 19 | 20 | app 21 | |> get_all_modules() 22 | |> TODO.get_todos() 23 | |> TODO.output_todos(print_mode, max_vsn) 24 | end 25 | 26 | defp get_all_modules(app) do 27 | appfile = Application.app_dir(app) <> "/ebin/#{app}.app" 28 | 29 | case :file.consult(appfile) do 30 | {:error, :enoent} -> 31 | Shell.error("File missing. App not compiled ?") 32 | exit(:normal) 33 | 34 | {:ok, [data]} -> 35 | {:application, _app, infos} = data 36 | infos[:modules] || [] 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Todo.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :todo, 7 | version: "1.6.0", 8 | elixir: "~> 1.4", 9 | description: "A small TODO comments utility.", 10 | package: [ 11 | contributors: ["Ludovic Demblans"], 12 | licenses: ["MIT"], 13 | links: %{ 14 | "GitHub" => "https://github.com/lud/elixir-todo" 15 | } 16 | ], 17 | deps: deps() 18 | ] 19 | end 20 | 21 | def application do 22 | [applications: []] 23 | end 24 | 25 | defp deps do 26 | [ 27 | {:ex_doc, "~> 0.26", only: :dev} 28 | ] 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark": {:hex, :earmark, "1.4.4", "4821b8d05cda507189d51f2caeef370cf1e18ca5d7dfb7d31e9cafe6688106a4", [:mix], [], "hexpm", "1f93aba7340574847c0f609da787f0d79efcab51b044bb6e242cae5aca9d264d"}, 3 | "earmark_parser": {:hex, :earmark_parser, "1.4.17", "6f3c7e94170377ba45241d394389e800fb15adc5de51d0a3cd52ae766aafd63f", [:mix], [], "hexpm", "f93ac89c9feca61c165b264b5837bf82344d13bebc634cd575cb711e2e342023"}, 4 | "ex_doc": {:hex, :ex_doc, "0.26.0", "1922164bac0b18b02f84d6f69cab1b93bc3e870e2ad18d5dacb50a9e06b542a3", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2775d66e494a9a48355db7867478ffd997864c61c65a47d31c4949459281c78d"}, 5 | "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, 6 | "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"}, 7 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 8 | "nimble_parsec": {:hex, :nimble_parsec, "1.2.0", "b44d75e2a6542dcb6acf5d71c32c74ca88960421b6874777f79153bbbbd7dccc", [:mix], [], "hexpm", "52b2871a7515a5ac49b00f214e4165a40724cf99798d8e4a65e4fd64ebd002c1"}, 9 | } 10 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /test/todo_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TodoTest do 2 | use ExUnit.Case 3 | use TODO 4 | 5 | @todo "XX This todo has no version" 6 | @todo "0.0.0": "This todo should be always a warning", 7 | "999.999.999": "This message should not be shown without --all", 8 | "bad.version": "This should be tagged as invalid" 9 | 10 | def f do 11 | todo "XX This todo has no version too" 12 | 13 | todo "0.0.0": "This message is always outdate", 14 | "999.999.999": "Cannot be seen without --all" 15 | end 16 | 17 | # test "get module todos" do 18 | # __MODULE__ 19 | # |> TODO.get_todos() 20 | # |> IO.inspect(label: "module todos") 21 | # end 22 | 23 | defmodule SubModA do 24 | use TODO 25 | @todo "XX Unversionned todo in submod A" 26 | @todo "0.0.0": "Versionned todo in submod A" 27 | end 28 | 29 | defmodule SubModB do 30 | use TODO 31 | @todo "XX Unversionned todo in submod B" 32 | @todo "0.0.0": "Versionned todo in submod B" 33 | end 34 | 35 | test "get multiple modules todos" do 36 | todos = 37 | [__MODULE__, SubModA, SubModB] 38 | |> TODO.get_todos() 39 | |> IO.inspect(label: "multi module todos") 40 | 41 | IO.puts("Print all") 42 | TODO.output_todos(todos, :all, Version.parse!("1.0.0")) 43 | # IO.puts("Print overdue for 1.0.0") 44 | # TODO.output_todos(todos, :overdue, Version.parse!("1.0.0")) 45 | end 46 | end 47 | --------------------------------------------------------------------------------