Home

Awesome

Moonpick

Build Status

What is it?

Moonpick is an alternative linter for Moonscript. While moonscript ships with a built-in linter, it is currently limited in what it can detect. The built-in linter will for instance detect unused variables, but only for a subset of all possible unused declarations. It will not detect unused import variables, decomposition variables, unused functions, etc.. Moonpick was born in an attempt to detect the above and more.

Installation and usage

Moonpick can be installed via Luarocks:

$ luarocks install moonpick

It can then be run from command line:

$ moonpick <path-to-file>

The output closely mimics the output of Moonscript's built-in linter.

It's also easily bundled into a standalone application as it's sole dependency is moonscript. See the API section for more information on how to run it programmatically.

What does it detect?

Unused variables

Moonpick detects unused variables in all their forms, whether they're explicitly used as variables via assigments or implicitly created as part of a import statement, table decomposition statement, etc.

Unused function parameters

Moonpick can also detect and complain about declared but unused function parameters. This is not enabled by default, as it's very common to have unused parameters. E.g. a function might follow an external API and still wants to indicate the available parameters even though not all are used. To enable this, set the report_params configuration option to true.

Moonpick ships with a default configuration that whitelists any parameter starting with a '_', providing a way of keeping the documentational aspects for a function and still pleasing the linter.

Unused loop variables

Unused loop variables are detected. It's possible to disable this completely in the configuration, or to provide an explicit whitelist only for loop variables. Moonpick ships with a default configuration that whitelists the arguments 'i' and 'j', or any variable starting with a '_'.

Undefined global accesses

Similar to the built-in linter Moonpick detects undefined references.

Declaration shadowing

Declaration shadowing occurs whenever a declaration shadows an earlier declaration with the same name. Consider the following code:

my_mod = require 'my_mod'

-- [.. more code in between.. ]

for my_mod in get_modules('foo')
  my_mod.bar!

While it in the example above is rather clear that the my_mod declared in the loop is different from the top level my_mod, this can quickly become less clear should more code be inserted between the for declaration and later usage. At that point the code becomes ambiguous. Declaration shadowing helps with this by ensuring that each variable is defined at most once, in an unambiguous manner.

The detection can be turned off completely by setting the report_shadowing configuration variable to false, and the whitelisting can be configured by specifying a whitelist_shadowing configuration list.

Note that for versions of Moonscript earlier than 0.5 these kind of shadowings would actually just re-use the prior declaration, leading to easily overlooked and confounding bugs.

Reassignment of function variables

Reassigning of a previously defined variable holding a function value is rarely wanted, and is often the result of forgetting an earlier declaration.

-- with the following declaration and usage
done = (x) -> x.foo and x.bar
done({})

-- one might mistakenly reuse the name further down
i = 1
-- [..]
done = i == 10

This can can cause hard to debug issues, particularly if the reassignment is only done in a code path that is not always exercised.

The detection can be turned off completely by setting the report_fndef_reassignments configuration variable to false, and the whitelisting can be configured by specifying a whitelist_fndef_reassignments configuration list.

Reassignment of top level variables from a function or method

Reassignment of a top level variable from within a function or method can sometimes be the cause of non-obvious and elusive bugs, e.g.:

module = require 'lib.module'

-- [..] much further down
get_foo = (y) ->
  module = y\match('%w+')\lower! -- mistakenly reusing the `module` var
  return "#{module}_bar"

Should get_foo above only be called conditionally this could cause serious bugs to go unnoticed.

In contrast to the other detections, this detection is not enabled by default. The detection can be turned on by setting the report_top_level_reassignments configuration variable to true, and the whitelisting can be configured by specifying a whitelist_top_level_reassignments configuration list. It's highly recommended to enable this however.

The reason this is not enabled by default is that it's not uncommon to have legitimate code that manipulates top level variables from within sub functions or methods. In order to avoid complaints from the linter one would then either have to configure the whitelist, or one would need to adopt a different style of coding where top level variables are not reassigned (for instance by using a table to hold module state instead).

Configuration

Moonpick supports a super set of the same configuration file and format as the built-in linter.

It provides additional configuration options by adding support for configuring linting of function parameters and loop variables, and also allows Lua patterns in all whitelists. Linter configuration files can be written in either Lua or Moonscript (lint_config.lua and lint_config.moon respectively).

See the below example (lint_config.moon, using Moonscript syntax):

{
  whitelist_globals: {
    -- whitelist for all files
    ["."]: { 'always_ignore' },

    -- whitelist for files matching 'spec'
    spec: { 'test_helper' },
  }

  whitelist_params: {
    -- whitelist params for all files
    ["."]: { 'my_param' },

    -- ignore unused param for files in api
    api: { 'extra_info' },
  }

  whitelist_loop_variables: {
    -- always allow loop variables 'i', 'j', 'k', as well as any
    -- variable starting with '_' (using a Lua pattern)
    ["."]: { 'i', 'j', 'k', '^_' },
  }

  -- general whitelist for unused variables if desired for
  -- some reason
  whitelist_unused: {
    ["."]: {},
  }

  -- below you'll see the boolean switches controlling the
  -- linting, shown with the default value

  -- report_loop_variables: true
  -- report_params: true
  -- report_shadowing: true
  -- report_fndef_reassignments: true
  -- report_top_level_reassignments: false
}

A whitelist item is treated as a pattern if it consist of anything other than alphanumeric characters.

API

moonpick

local moonpick = require('moonpick')

lint(code, config = {})

Lints the given code in code, returning a table of linting inspections. config is the linting configuration to use for the file, and can contain flat versions of the elements typically found in a configuration file (whitelist_globals, whitelist_params, whitelist_loop_variables, whitelist_unused, report_params, report_loop_variables).

Example of a configuration table (Lua syntax):

local moonpick = require('moonpick')
local code = 'a = 2'
moonpick.lint(code, {
  whitelist_globals = { 'foo', 'bar', }
  whitelist_params = { '^_+', 'other_+'}
})

The returned inspections table would look like this for the above example:

{
  {
    line = 1,
    pos = 1,
    msg = 'declared but unused - `a`',
    code = 'a = 2'
  }
}

lint_file(file, opts = {})

Lints the given file, returning a table of linting inspections. opts can currently contain one value, lint_config, which specifies the configuration file to load configuration from.

moonpick.config

local moonpick_config = require('moonpick.config')

config_for(path)

Returns the path of relevant configuration file for path, or nil if none was found.

load_config_from(config_path, file)

Loads linting configuration for the file file from the configuration file given by config_path. The returned configuration will be a table flattened configuration options for file.

evaluator(config)

Returns an evaluator instance for the given linting options (e.g. as returned by load_config_from). The evaluator instance provides the following functions (note that these are functions, to be invoked using the ordinary dot operator .):

allow_global_access, allow_unused_param, allow_unused_loop_variable, allow_unused, allow_fndef_reassignment, allow_top_level_reassignment.

All of these take as their first argument a symbol (as string) and returns true or false depending on whether the symbol passes linting or not.

Current status

Note that Moonpick is rather young at this stage, and while it has been run with success on larger code bases it may very well produce false positives and incorrect reports. Should you encounter this then please open an issue with a code sample that illustrates the incorrect behaviour.

License

Copyright 2016-2017 Nils Nordman <nino at nordman.org>

Moonpick is released under the MIT license (see the LICENSE file for the full details).

Running the specs

Tests require busted to run, as well as the pl module (Penlight - luarock install penlight). Just run busted in the project's root directory.

Running it locally for development purposes

Execute with a specified LUA_PATH pointing to the local src directory. Presuming a checkout location of ~/code/moonpick:

LUA_PATH="$HOME/code/moonpick/src/?.lua;$HOME/code/moonpick/src/?/init.lua;$(lua -e 'print(package.path)')" ~/code/moonpick/bin/moonpick *.moon