Browse Source

Test and document Dialyzer

One bug was fixed: now Erlang.mk will properly pass relevant
ERLC_OPTS values to Dialyzer.

One bug still exists when using multi-application repositories:
dependencies are not detected automatically. We need list-deps
before this can work.
Loïc Hoguin 9 years ago
parent
commit
5327c56922
3 changed files with 326 additions and 6 deletions
  1. 69 2
      doc/src/guide/dialyzer.asciidoc
  2. 15 4
      plugins/dialyzer.mk
  3. 242 0
      test/plugin_dialyzer.mk

+ 69 - 2
doc/src/guide/dialyzer.asciidoc

@@ -1,5 +1,72 @@
 == Dialyzer
 
-// @todo Write it.
+Dialyzer is a tool that will detect discrepancies in your
+program. It does so using a technique known as success
+typing analysis which has the advantage of providing no
+false positives. Dialyzer is able to detect type errors,
+dead code and more.
 
-Placeholder chapter.
+Erlang.mk provides a wrapper around Dialyzer.
+
+=== How it works
+
+Dialyzer requires a PLT file to work. The PLT file contains
+the analysis information from all applications which are not
+expected to change, or rarely do. These would be all the
+dependencies of the application or applications you are
+currently working on, including standard applications in
+Erlang/OTP itself.
+
+Dialyzer can generate this PLT file. Erlang.mk includes rules
+to automatically generate the PLT file when it is missing.
+
+Once the PLT file is generated, Dialyzer can perform the
+analysis in record time.
+
+=== Configuration
+
+In a typical usage scenario, no variable needs to be set.
+The defaults should be enough. Do note however that the
+dependencies need to be set properly using the `DEPS` and
+`LOCAL_DEPS` variables.
+
+The `DIALYZER_PLT` file indicates where the PLT file will
+be written to (and read from). By default this is
+'$(PROJECT).plt' in the project's directory. Note that
+the `DIALYZER_PLT` variable is exported and is understood
+by Dialyzer directly.
+
+The `PLT_APPS` variable can be used to add additional
+applications to the PLT. You can either list application
+names or paths to these applications.
+
+Erlang.mk defines two variables for specifying options
+for the analysis: `DIALYZER_DIRS` and `DIALYZER_OPTS`.
+The former one defines which directories should be part
+of the analysis. The latter defines what extra warnings
+Dialyzer should report.
+
+Note that Erlang.mk enables the race condition warnings
+by default. As it can take considerably large resources
+to run, you may want to disable it on larger projects.
+
+=== Usage
+
+To perform an analysis, run the following command:
+
+[source,bash]
+$ make dialyze
+
+This will create the PLT file if it doesn't exist.
+
+The analysis will also be performed when you run the
+following command, alongside tests:
+
+[source,bash]
+$ make check
+
+You can use the `plt` target to create the PLT file if
+it doesn't exist. This is normally not necessary as
+Dialyzer creates it automatically.
+
+The PLT file will be removed when you run `make distclean`.

+ 15 - 4
plugins/dialyzer.mk

@@ -9,9 +9,8 @@ DIALYZER_PLT ?= $(CURDIR)/.$(PROJECT).plt
 export DIALYZER_PLT
 
 PLT_APPS ?=
-DIALYZER_DIRS ?= --src -r src
-DIALYZER_OPTS ?= -Werror_handling -Wrace_conditions \
-	-Wunmatched_returns # -Wunderspecs
+DIALYZER_DIRS ?= --src -r $(wildcard src) $(ALL_APPS_DIRS)
+DIALYZER_OPTS ?= -Werror_handling -Wrace_conditions -Wunmatched_returns # -Wunderspecs
 
 # Core targets.
 
@@ -27,6 +26,18 @@ help::
 
 # Plugin-specific targets.
 
+define filter_opts.erl
+	Opts = binary:split(<<"$1">>, <<"-">>, [global]),
+	Filtered = lists:reverse(lists:foldl(fun
+		(O = <<"pa ", _/bits>>, Acc) -> [O|Acc];
+		(O = <<"D ", _/bits>>, Acc) -> [O|Acc];
+		(O = <<"I ", _/bits>>, Acc) -> [O|Acc];
+		(_, Acc) -> Acc
+	end, [], Opts)),
+	io:format("~s~n", [[["-", O] || O <- Filtered]]),
+	halt().
+endef
+
 $(DIALYZER_PLT): deps app
 	$(verbose) dialyzer --build_plt --apps erts kernel stdlib $(PLT_APPS) $(OTP_DEPS) $(LOCAL_DEPS) $(DEPS)
 
@@ -40,4 +51,4 @@ dialyze:
 else
 dialyze: $(DIALYZER_PLT)
 endif
-	$(verbose) dialyzer --no_native $(DIALYZER_DIRS) $(DIALYZER_OPTS)
+	$(verbose) dialyzer --no_native `$(call erlang,$(call filter_opts.erl,$(ERLC_OPTS)))` $(DIALYZER_DIRS) $(DIALYZER_OPTS)

+ 242 - 0
test/plugin_dialyzer.mk

@@ -0,0 +1,242 @@
+# Dialyzer plugin.
+
+DIALYZER_CASES = app apps-only apps-with-local-deps check custom-plt deps erlc-opts local-deps opts plt-apps
+DIALYZER_TARGETS = $(addprefix dialyzer-,$(DIALYZER_CASES))
+DIALYZER_CLEAN_TARGETS = $(addprefix clean-,$(DIALYZER_TARGETS))
+
+.PHONY: dialyzer $(C_SRC_TARGETS) clean-dialyzer $(DIALYZER_CLEAN_TARGETS)
+
+clean-dialyzer: $(DIALYZER_CLEAN_TARGETS)
+
+$(DIALYZER_CLEAN_TARGETS):
+	$t rm -rf $(APP_TO_CLEAN)/
+
+dialyzer: $(DIALYZER_TARGETS)
+
+dialyzer-app: build clean-dialyzer-app
+
+	$i "Bootstrap a new OTP application named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap $v
+
+	$i "Run Dialyzer"
+	$t $(MAKE) -C $(APP) dialyze $v
+
+	$i "Check that the PLT file was created"
+	$t test -f $(APP)/.$(APP).plt
+
+	$i "Create a module with a function that has no local return"
+	$t printf "%s\n" \
+		"-module(warn_me)." \
+		"doit() -> 1 = 2, ok." > $(APP)/src/warn_me.erl
+
+	$i "Confirm that Dialyzer errors out"
+	$t ! $(MAKE) -C $(APP) dialyze $v
+
+	$i "Distclean the application"
+	$t $(MAKE) -C $(APP) distclean $v
+
+	$i "Check that the PLT file was removed"
+	$t test ! -e $(APP)/.$(APP).plt
+
+dialyzer-apps-only: build clean-dialyzer-apps-only
+
+	$i "Create a multi application repository with no root application"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t echo "include erlang.mk" > $(APP)/Makefile
+
+	$i "Create a new application my_app"
+	$t $(MAKE) -C $(APP) new-app in=my_app $v
+
+	$i "Create a module my_server from gen_server template in my_app"
+	$t $(MAKE) -C $(APP) new t=gen_server n=my_server in=my_app $v
+
+	$i "Add Cowlib to the list of dependencies"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "DEPS = cowlib\n"}' $(APP)/apps/my_app/Makefile
+
+	$i "Run Dialyzer"
+	$t $(MAKE) -C $(APP) dialyze $v
+
+	$i "Check that the PLT file was created automatically"
+	$t test -f $(APP)/.$(APP).plt
+
+	$i "Confirm that Cowlib was included in the PLT"
+	$t dialyzer --plt_info --plt $(APP)/.$(APP).plt | grep -q cowlib
+
+	$i "Create a module with a function that has no local return"
+	$t printf "%s\n" \
+		"-module(warn_me)." \
+		"doit() -> 1 = 2, ok." > $(APP)/apps/my_app/src/warn_me.erl
+
+	$i "Confirm that Dialyzer errors out"
+	$t ! $(MAKE) -C $(APP) dialyze $v
+
+dialyzer-apps-with-local-deps: build clean-dialyzer-apps-with-local-deps
+
+	$i "Create a multi application repository with no root application"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t echo "include erlang.mk" > $(APP)/Makefile
+
+	$i "Create a new application my_app"
+	$t $(MAKE) -C $(APP) new-app in=my_app $v
+
+	$i "Create a new application my_core_app"
+	$t $(MAKE) -C $(APP) new-app in=my_core_app $v
+
+	$i "Add my_core_app to the list of local dependencies for my_app"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "LOCAL_DEPS = my_core_app\n"}' $(APP)/apps/my_app/Makefile
+
+	$i "Run Dialyzer"
+	$t $(MAKE) -C $(APP) dialyze $v
+
+	$i "Check that the PLT file was created automatically"
+	$t test -f $(APP)/.$(APP).plt
+
+	$i "Confirm that my_core_app was NOT included in the PLT"
+	$t ! dialyzer --plt_info --plt $(APP)/.$(APP).plt | grep -q my_core_app
+
+dialyzer-check: build clean-dialyzer-check
+
+	$i "Bootstrap a new OTP application named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap $v
+
+	$i "Run 'make check'"
+	$t $(MAKE) -C $(APP) check $v
+
+	$i "Check that the PLT file was created"
+	$t test -f $(APP)/.$(APP).plt
+
+	$i "Create a module with a function that has no local return"
+	$t printf "%s\n" \
+		"-module(warn_me)." \
+		"doit() -> 1 = 2, ok." > $(APP)/src/warn_me.erl
+
+	$i "Confirm that Dialyzer errors out on 'make check'"
+	$t ! $(MAKE) -C $(APP) check $v
+
+dialyzer-custom-plt: build clean-dialyzer-custom-plt
+
+	$i "Bootstrap a new OTP application named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap $v
+
+	$i "Set a custom DIALYZER_PLT location"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "DIALYZER_PLT = custom.plt\n"}' $(APP)/Makefile
+
+	$i "Run Dialyzer"
+	$t $(MAKE) -C $(APP) dialyze $v
+
+	$i "Check that the PLT file was created"
+	$t test -f $(APP)/custom.plt
+
+	$i "Distclean the application"
+	$t $(MAKE) -C $(APP) distclean $v
+
+	$i "Check that the PLT file was removed"
+	$t test ! -e $(APP)/custom.plt
+
+dialyzer-deps: build clean-dialyzer-deps
+
+	$i "Bootstrap a new OTP application named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap $v
+
+	$i "Add Cowlib to the list of dependencies"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "DEPS = cowlib\n"}' $(APP)/Makefile
+
+	$i "Run Dialyzer"
+	$t $(MAKE) -C $(APP) dialyze $v
+
+	$i "Check that the PLT file was created"
+	$t test -f $(APP)/.$(APP).plt
+
+	$i "Confirm that Cowlib was included in the PLT"
+	$t dialyzer --plt_info --plt $(APP)/.$(APP).plt | grep -q cowlib
+
+dialyzer-erlc-opts: build clean-dialyzer-erlc-opts
+
+	$i "Bootstrap a new OTP application named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap $v
+
+	$i "Create a header file in a non-standard directory"
+	$t mkdir $(APP)/exotic/
+	$t touch $(APP)/exotic/dialyze.hrl
+
+	$i "Create a module that includes this header"
+	$t printf "%s\n" \
+		"-module(no_warn)." \
+		"-export([doit/0])." \
+		"-include(\"dialyze.hrl\")." \
+		"doit() -> ok." > $(APP)/src/no_warn.erl
+
+	$i "Point ERLC_OPTS to the non-standard include directory"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "ERLC_OPTS += -I exotic\n"}' $(APP)/Makefile
+
+	$i "Run Dialyzer"
+	$t $(MAKE) -C $(APP) dialyze $v
+
+dialyzer-local-deps: build clean-dialyzer-local-deps
+
+	$i "Bootstrap a new OTP application named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap $v
+
+	$i "Add runtime_tools to the list of local dependencies"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "LOCAL_DEPS = runtime_tools\n"}' $(APP)/Makefile
+
+	$i "Build the PLT"
+	$t $(MAKE) -C $(APP) plt $v
+
+	$i "Confirm that runtime_tools was included in the PLT"
+	$t dialyzer --plt_info --plt $(APP)/.$(APP).plt | grep -q runtime_tools
+
+dialyzer-opts: build clean-dialyzer-opts
+
+	$i "Bootstrap a new OTP application named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap $v
+
+	$i "Make Dialyzer save output to a file"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "DIALYZER_OPTS = -o output.txt\n"}' $(APP)/Makefile
+
+	$i "Create a module with a function that has no local return"
+	$t printf "%s\n" \
+		"-module(warn_me)." \
+		"-export([doit/0])." \
+		"doit() -> gen_tcp:connect(a, b, c), ok." > $(APP)/src/warn_me.erl
+
+	$i "Run Dialyzer"
+	$t ! $(MAKE) -C $(APP) dialyze $v
+
+	$i "Check that the PLT file was created"
+	$t test -f $(APP)/.$(APP).plt
+
+	$i "Check that the output file was created"
+	$t test -f $(APP)/output.txt
+
+dialyzer-plt-apps: build clean-dialyzer-plt-apps
+
+	$i "Bootstrap a new OTP application named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap $v
+
+	$i "Add runtime_tools to PLT_APPS"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "PLT_APPS = runtime_tools\n"}' $(APP)/Makefile
+
+	$i "Build the PLT"
+	$t $(MAKE) -C $(APP) plt $v
+
+	$i "Confirm that runtime_tools was included in the PLT"
+	$t dialyzer --plt_info --plt $(APP)/.$(APP).plt | grep -q runtime_tools