summaryrefslogtreecommitdiff
path: root/doc/plugins.md
blob: 572b41df16c5aeb1d4b2e309768aa9ba5a415ee9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
#### TODO ####

# Plugins #

Rebar3's system is based on the concept of *[providers](https://github.com/tsloughter/providers)*. A provider has three callbacks:

- `init(State) -> {ok, NewState}`, which helps set up the state required, state dependencies, etc.
- `do(State) -> {ok, NewState} | {error, Error}`, which does the actual work.
- `format_error(Error) -> String`, which allows to print errors when they happen, and to filter out sensitive elements from the state.

A provider should also be an OTP Library application, which can be fetched as any other Erlang dependency, except for Rebar3 rather than your own system or application.

This document contains the following elements:

- [Using a Plugin](#using-a-plugin)
- [Reference](#reference)
  - [Provider Interface](#provider-interface)
  - [List of Possible Dependencies](#list-of-possible-dependencies)
  - [Rebar State Manipulation](#rebar-state-manipulation)
- [Tutorial](#tutorial)

## Using a Plugin ##

To use the a plugin, add it to the rebar.config:

```erlang
{plugins, [
  {plugin_name, ".*", {git, "git@host:user/name-of-plugin.git", {tag, "v1.0.0"}}}
]}.
```

Then you can just call it directly:

```
→ rebar3 plugin_name
===> Fetching plugin_name
Cloning into '.tmp_dir539136867963'...
===> Compiling plugin_name
<PLUGIN OUTPUT>
```

## Reference ##

TODO

### Provider Interface ###

TODO

### List of Possible Dependencies ###

TODO

### Rebar State Manipulation ###

TODO


## Tutorial ##

### First version ###

In this tutorial, we'll show how to start from scratch, and get a basic plugin written. The plugin will be quite simple: it will look for instances of 'TODO:' lines in comments and report them as warnings. The final code for the plugin can be found on [bitbucket](https://bitbucket.org/ferd/rebar3-todo-plugin).

The first step is to create a new OTP Application that will contain the plugin:

    → git init
    Initialized empty Git repository in /Users/ferd/code/self/rebar3-todo-plugin/.git/
    → rebar3 new plugin provider_todo desc="example rebar3 plugin"
    ...

Open up the `src/provider_todo.erl` file and make sure you have the following skeleton in place:

```erlang
-module(provider_todo).
-behaviour(provider).

-export([init/1, do/1, format_error/1]).

-include_lib("rebar3/include/rebar.hrl").

-define(PROVIDER, todo).
-define(DEPS, [app_discovery]).

%% ===================================================================
%% Public API
%% ===================================================================
-spec init(rebar_state:t()) -> {ok, rebar_state:t()}.
init(State) ->
    Provider = providers:create([
            {name, ?PROVIDER},          % The 'user friendly' name of the task
            {module, ?MODULE},          % The module implementation of the task
            {bare, true},               % The task can be run by the user, always true
            {deps, ?DEPS},              % The list of dependencies
            {example, "rebar provider_todo"}, % How to use the plugin
            {opts, []}                  % list of options understood by the plugin
            {short_desc, "example rebar3 plugin"},
            {desc, ""}
    ]),
    {ok, rebar_state:add_provider(State, Provider)}.


-spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}.
do(State) ->
    {ok, State}.

-spec format_error(any()) -> iolist().
format_error(Reason) ->
    io_lib:format("~p", [Reason]).
```

This shows all the basic content needed. Note that we leave the `DEPS` macro to the value `app_discovery`, used to mean that the plugin should at least find the project's source code (excluding dependencies).

In this case, we need to change very little in `init/1`. Here's the new provider description:

```erlang
        Provider = providers:create([
            {name, ?PROVIDER},       % The 'user friendly' name of the task
            {module, ?MODULE},       % The module implementation of the task
            {bare, true},            % The task can be run by the user, always true
            {deps, ?DEPS},           % The list of dependencies
            {example, "rebar todo"}, % How to use the plugin
            {opts, []},              % list of options understood by the plugin
            {short_desc, "Reports TODOs in source code"},
            {desc, "Scans top-level application source and find "
                   "instances of TODO: in commented out content "
                   "to report it to the user."}
    ]),
```

Instead, most of the work will need to be done directly in `do/1`. We'll use the `rebar_state` module to fetch all the applications we need. This can be done by calling the `project_apps/1` function, which returns the list of the project's top-level applications.

```erlang
do(State) ->
    lists:foreach(fun check_todo_app/1, rebar_state:project_apps(State)),
    {ok, State}.
```

This, on a high level, means that we'll check each top-level app one at a time (there may often be more than one top-level application when working with releases)

The rest is filler code specific to the plugin, in charge of reading each app path, go read code in there, and find instances of 'TODO:' in comments in the code:

```erlang
check_todo_app(App) ->
    Path = filename:join(rebar_app_info:dir(App),"src"),
    Mods = find_source_files(Path),
    case lists:foldl(fun check_todo_mod/2, [], Mods) of
        [] -> ok;
        Instances -> display_todos(rebar_app_info:name(App), Instances)
    end.

find_source_files(Path) ->
    [filename:join(Path, Mod) || Mod <- filelib:wildcard("*.erl", Path)].

check_todo_mod(ModPath, Matches) ->
    {ok, Bin} = file:read_file(ModPath),
    case find_todo_lines(Bin) of
        [] -> Matches;
        Lines -> [{ModPath, Lines} | Matches]
    end.

find_todo_lines(File) ->
    case re:run(File, "%+.*(TODO:.*)", [{capture, all_but_first, binary}, global, caseless]) of
        {match, DeepBins} -> lists:flatten(DeepBins);
        nomatch -> []
    end.

display_todos(_, []) -> ok;
display_todos(App, FileMatches) ->
    io:format("Application ~s~n",[App]),
    [begin
      io:format("\t~s~n",[Mod]),
      [io:format("\t  ~s~n",[TODO]) || TODO <- TODOs]
     end || {Mod, TODOs} <- FileMatches],
    ok.
```

Just using `io:format/2` to output is going to be fine.

To test the plugin, push it to a source repository somewhere. Pick one of your projects, and add something  to the rebar.config:

```erlang
{plugins, [
  {provider_todo, ".*", {git, "git@bitbucket.org:ferd/rebar3-todo-plugin.git", {branch, "master"}}}
]}.
```

Then you can just call it directly:

```
→ rebar3 todo
===> Fetching provider_todo
Cloning into '.tmp_dir539136867963'...
===> Compiling provider_todo
Application merklet
    /Users/ferd/code/self/merklet/src/merklet.erl
      todo: consider endianness for absolute portability
```

Rebar3 will download and install the plugin, and figure out when to run it. Once compiled, it can be run at any time again.

### Optionally Search Deps ###

Let's extend things a bit. Maybe from time to time (when cutting a release), we'd like to make sure none of our dependencies contain 'TODO:'s either.

To do this, we'll need to go parse command line arguments a bit, and change our execution model. The `?DEPS` macro will now need to specify that the `todo` provider can only run *after* dependencies have been installed:

```erlang
-define(DEPS, [install_deps]).
```

We can add the option to the list we use to configure the provider in `init/1`:

```erlang
{opts, [                 % list of options understood by the plugin
    {deps, $d, "deps", undefined, "also run against dependencies"}
]},
```

Meaning that deps can be flagged in by using the option `-d` (or `--deps`), and if it's not defined, well, we get the default value `undefined`. The last element of the 4-tuple is documentation for the option.

And then we can implement the switch to figure out what to search:

```erlang
do(State) ->
    Apps = case discovery_type(State) of
        project -> rebar_state:project_apps(State);
        deps -> rebar_state:project_apps(State) ++ rebar_state:src_deps(State)
    end,
    lists:foreach(fun check_todo_app/1, Apps),
    {ok, State}.

[...]

discovery_type(State) ->
    {Args, _} = rebar_state:command_parsed_args(State),
    case proplists:get_value(deps, Args) of
        undefined -> project;
        _ -> deps
    end.
```

The `deps` option is found using `rebar_state:command_parsed_args(State)`, which will return a proplist of terms on the command-line after 'todo', and will take care of validating whether the flags are accepted or not. The rest can remain the same.

Push the new code for the plugin, and try it again on a project with dependencies:

```
→ rebar3 todo --deps
===> Fetching provider_todo
===> Compiling provider_todo
===> Fetching bootstrap
===> Fetching file_monitor
===> Fetching recon
[...]
Application dirmon
    /Users/ferd/code/self/figsync/apps/dirmon/src/dirmon_tracker.erl
      TODO: Peeranha should expose the UUID from a node.
Application meck
    /Users/ferd/code/self/figsync/_deps/meck/src/meck_proc.erl
      TODO: What to do here?
      TODO: What to do here?
```

Rebar3 will now go pick dependencies before running the plugin on there.

you can also see that the help will be completed for you:

```
→ rebar3 help todo
Scans top-level application source and find instances of TODO: in commented out content to report it to the user.

Usage: rebar todo [-d]

  -d, --deps  also run against dependencies
```

That's it, the todo plugin is now complete! It's ready to ship and be included in other repositories.