diff --git a/README.md b/README.md index 8dd57d42..c512b59f 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ branch](https://github.com/leafo/moonscript/tree/binaries) ## License (MIT) -Copyright (C) 2015 by Leaf Corcoran +Copyright (C) 2015 by Leaf Corcoran, Nils Nordman Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -64,4 +64,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. \ No newline at end of file +THE SOFTWARE. diff --git a/bin/moon.moon b/bin/moon.moon index eb36eab5..1d18e337 100644 --- a/bin/moon.moon +++ b/bin/moon.moon @@ -7,7 +7,7 @@ errors = require "moonscript.errors" unpack = util.unpack -opts, ind = alt_getopt.get_opts arg, "cvhd", { +opts, ind = alt_getopt.get_opts _G.arg, "cvhd", { version: "v" help: "h" } @@ -26,7 +26,7 @@ print_err = (...) -> io.stderr\write msg .. "\n" print_help = (err) -> - help = help\format arg[0] + help = help\format _G.arg[0] if err print_err err @@ -44,15 +44,15 @@ run = -> require("moonscript.version").print_version! os.exit! - script_fname = arg[ind] + script_fname = _G.arg[ind] unless script_fname print_help "repl not yet supported" new_arg = { - [-1]: arg[0], - [0]: arg[ind], - select ind + 1, unpack arg + [-1]: _G.arg[0], + [0]: _G.arg[ind], + select ind + 1, unpack _G.arg } local moonscript_chunk, lua_parse_error diff --git a/bin/moonc b/bin/moonc index dfd56969..5944525b 100755 --- a/bin/moonc +++ b/bin/moonc @@ -174,8 +174,14 @@ if opts.w then -- build function to check for lint or compile in watch local handle_file if opts.l then - local lint = require "moonscript.cmd.lint" - handle_file = lint.lint_file + local lint = require "moonscript.lint" + handle_file = function(file) + local res, err = lint.lint_file(file) + if res and #res > 0 then + return file .. "\n\n" .. lint.format_inspections(res) + end + return nil, err + end else handle_file = compile_and_write end @@ -220,16 +226,17 @@ if opts.w then io.stderr:write("\nQuitting...\n") elseif opts.l then local has_linted_with_error; - local lint = require "moonscript.cmd.lint" + local lint = require "moonscript.lint" for _, tuple in pairs(files) do local fname = tuple[1] local res, err = lint.lint_file(fname) - if res then + if res and #res > 0 then has_linted_with_error = true - io.stderr:write(res .. "\n\n") + local inspections = lint.format_inspections(res) + io.stderr:write(fname .. "\n\n" .. inspections .. "\n\n") elseif err then has_linted_with_error = true - io.stderr:write(fname .. "\n" .. err.. "\n\n") + io.stderr:write(fname .. "\n\n" .. err.. "\n\n") end end if has_linted_with_error then diff --git a/bin/splat.moon b/bin/splat.moon index df523c14..d8186896 100755 --- a/bin/splat.moon +++ b/bin/splat.moon @@ -5,18 +5,18 @@ lfs = require "lfs" alt_getopt = require "alt_getopt" -import insert, concat from table -import dump, split from require "moonscript.util" +import insert from table +import split from require "moonscript.util" -opts, ind = alt_getopt.get_opts arg, "l:", { +opts, ind = alt_getopt.get_opts _G.arg, "l:", { load: "l" } -if not arg[ind] +if not _G.arg[ind] print "usage: splat [-l module_names] directory [directories...]" os.exit! -dirs = [a for a in *arg[ind,]] +dirs = [a for a in *_G.arg[ind,]] normalize = (path) -> path\match("(.-)/*$").."/" diff --git a/docs/command_line.md b/docs/command_line.md index 31fe48ee..de22842d 100644 --- a/docs/command_line.md +++ b/docs/command_line.md @@ -133,9 +133,16 @@ A full list of flags can be seen by passing the `-h` or `--help` flag. ### Linter `moonc` contains a [lint][1] tool for statically detecting potential problems -with code. The linter has two tests: detects accessed global variables, -detect unused declared variables. If the linter detects any issues with a file, -the program will exit with a status of `1`. +with code. The linter can detect the following classes of potential errors: + +- global variable accesses +- declared but unused variables +- unused parameter declarations +- unused loop variables +- declaration shadowing + +If the linter detects any issues with a file, the program will exit with a +status of `1`. You can execute the linter with the `-l` flag. When the linting flag is provided only linting takes place and no compiled code is generated. @@ -149,7 +156,46 @@ moonc -l file1.moon file2.moon Like when compiling, you can also pass a directory as a command line argument to recursively process all the `.moon` files. -#### Global Variable Checking +#### Linter configuration file + +When linting a file, the linter will look for a file named either +`lint_config.moon` or `lint_config.lua` in the same directory as the file, or in +one of the parent directories. Written in Moonscript or Lua respectively, this +file can provide additional configuration for the linter. The specific +configuration options available are discussed in the sections below, but +generally speaking, the linter configuration consists of either boolean flags or +whitelisting lists. In the case of the lists, they are defined in a way that +lets you easily define different lists for different files or directories. As an +example, consider the following whitelist for globals: + +```moononly +-- lint_config.moon +{ + whitelist_globals: { + ['.']: { 'foo' } + + ['sub_dir/']: { 'bar' } + + ['sub_dir/example.moon']: { + 'zed', + '[A-Z]%w+' + } + } +} +``` + +The structure of a list is that it's a table with keys that are Lua patterns +which are matched against the path of the file being linted, with _all_ of the +matching entries being considered for the file. The entries themselves are +typically plain strings, but can also be Lua patterns. The above list would thus +result in `foo` being whitelisted for all files in the project, while files +below the `sub_dir` directory would have both `foo` and `bar` whitelisted. +Finally, for the specific file "sub_dir/example.moon" all four entries would be +used for whitelisting - `foo`, `bar`, `zed` and `[A-Z]%w+`. The latter, being a +pattern would whitelist all occurrences matching the pattern, such as "Foo", +"Bar2" and "FooBar2". + +#### Global Variable Accesses It's considered good practice to avoid using global variables and create local variables for all the values referenced. A good case for not using global @@ -192,7 +238,7 @@ Outputs: ================================== > my_nmuber + 10 -#### Global Variable Whitelist +##### Global Variable Whitelist In most circumstances it's impossible to avoid using some global variables. For example, to access any of the built in modules or functions you typically @@ -208,11 +254,8 @@ global functions (like `describe`, `before_each`, `setup`) to make writing tests easy. It would be nice if we could allow all of those global functions to be called -for `.moon` files located in the `spec/` directory. We can do that by creating -a `lint_config` file. - -`lint_config` is a regular MoonScript or Lua file that provides configuration -for the linter. One of those settings is `whitelist_globals`. +for `.moon` files located in the `spec/` directory. We can do that by providing +a `whitelist_globals` list in the `lint_config` file. To create a configuration for Busted we might do something like this: @@ -242,13 +285,6 @@ $ moonc -l . The whitelisted global references in `spec/` will no longer raise notices. -The `whitelist_globals` property of the `lint_config` is a table where the keys -are Lua patterns that match file names, and the values are an array of globals -that are allowed. - -Multiple patterns in `whitelist_globals` can match a single file, the union of -the allowed globals will be used when linting that file. - #### Unused Variable Assigns Sometimes when debugging, refactoring, or just developing, you might leave @@ -256,9 +292,9 @@ behind stray assignments that aren't actually necessary for the execution of your code. It's good practice to clean them up to avoid any potential confusion they might cause. -The unused assignment detector keeps track of any variables that are assigned, -and if they aren't accessed in within their available scope, they are reported -as an error. +The unused assignment detector keeps track of any variables that are assigned or +otherwise declared, and if they aren't accessed in within their available scope, +they are reported as an error. Given the following code: @@ -288,5 +324,90 @@ _, name, _, count = unpack item print name, count ``` +There are very few cases where one would need additional whitelisting for unused +variables, but it's possible that there are some, e.g. in tests. For this +purpose additional whitelisting can be specified using the `whitelist_unused` +configuration list: + +```moononly +-- lint_config.moon +{ + whitelist_unused: { + ['spec/']: { + 'my_unused' + } + } +} +``` + +#### Unused parameter declarations + +The linter 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 detection, set the `report_params` configuration option to +`true`: + +```moononly +-- lint_config.moon +{ + report_params: true +} +``` + +The linter 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. Other whitelisting can be specified by +adding a `whitelist_params' list to the linter configuration (please note that +the default whitelisting is not used when the configuration specifies a list). + +#### Unused loop variables + +Unused loop variables are detected. It's possible to disable this completely in +the configuration by setting the `report_loop_variables` variable to `false`, or +to provide an explicit whitelist only for loop variables. The linter ships with +a default configuration that whitelists the arguments 'i' and 'j', or any +variable starting with a '_'. + +Other whitelisting can be specified by adding a `whitelist_loop_variables' list +to the linter configuration (please note that the default whitelisting is not +used when the configuration specifies a list). + +#### Declaration shadowing + +Declaration shadowing occurs whenever a declaration shadows an earlier +declaration with the same name. Consider the following code: + +```moononly +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: + +```bash +$ moonc -l lint_example.moon +``` + + line 5: shadowing outer variable - `my_mod` + =========================================== + > for my_mod in get_modules('foo') + + +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. + [1]: http://en.wikipedia.org/wiki/Lint_(software) diff --git a/lint_config.moon b/lint_config.moon new file mode 100644 index 00000000..6eaf9ae4 --- /dev/null +++ b/lint_config.moon @@ -0,0 +1,28 @@ +{ + whitelist_globals: { + '.': { + }, + + 'parse.moon': { + '[A-Z][a-z]+' + }, + + spec: { + 'after_each', + 'async', + 'before_each', + 'describe', + 'it', + 'settimeout', + 'setup', + 'spy', + 'teardown', + + 'hello' + } + } + + whitelist_unused: { + 'spec/import_spec': { 'hello', 'foo' } + } +} diff --git a/moonscript-dev-1.rockspec b/moonscript-dev-1.rockspec index c4388d0a..cf7b5a2d 100644 --- a/moonscript-dev-1.rockspec +++ b/moonscript-dev-1.rockspec @@ -28,7 +28,6 @@ build = { ["moonscript"] = "moonscript/init.lua", ["moonscript.base"] = "moonscript/base.lua", ["moonscript.cmd.coverage"] = "moonscript/cmd/coverage.lua", - ["moonscript.cmd.lint"] = "moonscript/cmd/lint.lua", ["moonscript.cmd.moonc"] = "moonscript/cmd/moonc.lua", ["moonscript.cmd.watchers"] = "moonscript/cmd/watchers.lua", ["moonscript.compile"] = "moonscript/compile.lua", @@ -38,6 +37,8 @@ build = { ["moonscript.dump"] = "moonscript/dump.lua", ["moonscript.errors"] = "moonscript/errors.lua", ["moonscript.line_tables"] = "moonscript/line_tables.lua", + ["moonscript.lint"] = "moonscript/lint/init.lua", + ["moonscript.lint.config"] = "moonscript/lint/config.lua", ["moonscript.parse"] = "moonscript/parse.lua", ["moonscript.parse.env"] = "moonscript/parse/env.lua", ["moonscript.parse.literals"] = "moonscript/parse/literals.lua", diff --git a/moonscript/base.moon b/moonscript/base.moon index 3a4502d4..c50cad4a 100644 --- a/moonscript/base.moon +++ b/moonscript/base.moon @@ -2,7 +2,7 @@ compile = require "moonscript.compile" parse = require "moonscript.parse" import concat, insert, remove from table -import split, dump, get_options, unpack from require "moonscript.util" +import split, get_options, unpack from require "moonscript.util" lua = :loadstring, :load diff --git a/moonscript/cmd/args.moon b/moonscript/cmd/args.moon index ac65c0e3..9f575d2f 100644 --- a/moonscript/cmd/args.moon +++ b/moonscript/cmd/args.moon @@ -1,11 +1,11 @@ import unpack from require "moonscript.util" parse_spec = (spec) -> - flags, words = if type(spec) == "table" - unpack(spec), spec + flags = if type(spec) == "table" + unpack(spec) else - spec, {} + spec - assert "no flags for arguments" + assert flags, "no flags for arguments" out = {} for part in flags\gmatch "%w:?" @@ -25,7 +25,6 @@ parse_arguments = (spec, args) -> last_flag = nil for arg in *args - group = {} if last_flag out[last_flag] = arg continue diff --git a/moonscript/cmd/lint.lua b/moonscript/cmd/lint.lua deleted file mode 100644 index 8f036ff1..00000000 --- a/moonscript/cmd/lint.lua +++ /dev/null @@ -1,323 +0,0 @@ -local insert -insert = table.insert -local Set -Set = require("moonscript.data").Set -local Block -Block = require("moonscript.compile").Block -local mtype -mtype = require("moonscript.util").moon.type -local default_whitelist = Set({ - '_G', - '_VERSION', - 'assert', - 'bit32', - 'collectgarbage', - 'coroutine', - 'debug', - 'dofile', - 'error', - 'getfenv', - 'getmetatable', - 'io', - 'ipairs', - 'load', - 'loadfile', - 'loadstring', - 'math', - 'module', - 'next', - 'os', - 'package', - 'pairs', - 'pcall', - 'print', - 'rawequal', - 'rawget', - 'rawlen', - 'rawset', - 'require', - 'select', - 'setfenv', - 'setmetatable', - 'string', - 'table', - 'tonumber', - 'tostring', - 'type', - 'unpack', - 'xpcall', - "nil", - "true", - "false" -}) -local LinterBlock -do - local _class_0 - local _parent_0 = Block - local _base_0 = { - lint_mark_used = function(self, name) - if self.lint_unused_names and self.lint_unused_names[name] then - self.lint_unused_names[name] = false - return - end - if self.parent then - return self.parent:lint_mark_used(name) - end - end, - lint_check_unused = function(self) - if not (self.lint_unused_names and next(self.lint_unused_names)) then - return - end - local names_by_position = { } - for name, pos in pairs(self.lint_unused_names) do - local _continue_0 = false - repeat - if not (pos) then - _continue_0 = true - break - end - names_by_position[pos] = names_by_position[pos] or { } - insert(names_by_position[pos], name) - _continue_0 = true - until true - if not _continue_0 then - break - end - end - local tuples - do - local _accum_0 = { } - local _len_0 = 1 - for pos, names in pairs(names_by_position) do - _accum_0[_len_0] = { - pos, - names - } - _len_0 = _len_0 + 1 - end - tuples = _accum_0 - end - table.sort(tuples, function(a, b) - return a[1] < b[1] - end) - for _index_0 = 1, #tuples do - local _des_0 = tuples[_index_0] - local pos, names - pos, names = _des_0[1], _des_0[2] - insert(self:get_root_block().lint_errors, { - "assigned but unused " .. tostring(table.concat((function() - local _accum_0 = { } - local _len_0 = 1 - for _index_1 = 1, #names do - local n = names[_index_1] - _accum_0[_len_0] = "`" .. tostring(n) .. "`" - _len_0 = _len_0 + 1 - end - return _accum_0 - end)(), ", ")), - pos - }) - end - end, - render = function(self, ...) - self:lint_check_unused() - return _class_0.__parent.__base.render(self, ...) - end, - block = function(self, ...) - do - local _with_0 = _class_0.__parent.__base.block(self, ...) - _with_0.block = self.block - _with_0.render = self.render - _with_0.get_root_block = self.get_root_block - _with_0.lint_check_unused = self.lint_check_unused - _with_0.lint_mark_used = self.lint_mark_used - _with_0.value_compilers = self.value_compilers - _with_0.statement_compilers = self.statement_compilers - return _with_0 - end - end - } - _base_0.__index = _base_0 - setmetatable(_base_0, _parent_0.__base) - _class_0 = setmetatable({ - __init = function(self, whitelist_globals, ...) - if whitelist_globals == nil then - whitelist_globals = default_whitelist - end - _class_0.__parent.__init(self, ...) - self.get_root_block = function() - return self - end - self.lint_errors = { } - local vc = self.value_compilers - self.value_compilers = setmetatable({ - ref = function(block, val) - local name = val[2] - if not (block:has_name(name) or whitelist_globals[name] or name:match("%.")) then - insert(self.lint_errors, { - "accessing global `" .. tostring(name) .. "`", - val[-1] - }) - end - block:lint_mark_used(name) - return vc.ref(block, val) - end - }, { - __index = vc - }) - local sc = self.statement_compilers - self.statement_compilers = setmetatable({ - assign = function(block, node) - local names = node[2] - for _index_0 = 1, #names do - local _continue_0 = false - repeat - local name = names[_index_0] - if type(name) == "table" and name[1] == "temp_name" then - _continue_0 = true - break - end - local real_name, is_local = block:extract_assign_name(name) - if not (is_local or real_name and not block:has_name(real_name, true)) then - _continue_0 = true - break - end - if real_name == "_" then - _continue_0 = true - break - end - block.lint_unused_names = block.lint_unused_names or { } - block.lint_unused_names[real_name] = node[-1] or 0 - _continue_0 = true - until true - if not _continue_0 then - break - end - end - return sc.assign(block, node) - end - }, { - __index = sc - }) - end, - __base = _base_0, - __name = "LinterBlock", - __parent = _parent_0 - }, { - __index = function(cls, name) - local val = rawget(_base_0, name) - if val == nil then - local parent = rawget(cls, "__parent") - if parent then - return parent[name] - end - else - return val - end - end, - __call = function(cls, ...) - local _self_0 = setmetatable({}, _base_0) - cls.__init(_self_0, ...) - return _self_0 - end - }) - _base_0.__class = _class_0 - if _parent_0.__inherited then - _parent_0.__inherited(_parent_0, _class_0) - end - LinterBlock = _class_0 -end -local format_lint -format_lint = function(errors, code, header) - if not (next(errors)) then - return - end - local pos_to_line, get_line - do - local _obj_0 = require("moonscript.util") - pos_to_line, get_line = _obj_0.pos_to_line, _obj_0.get_line - end - local formatted - do - local _accum_0 = { } - local _len_0 = 1 - for _index_0 = 1, #errors do - local _des_0 = errors[_index_0] - local msg, pos - msg, pos = _des_0[1], _des_0[2] - if pos then - local line = pos_to_line(code, pos) - msg = "line " .. tostring(line) .. ": " .. tostring(msg) - local line_text = "> " .. get_line(code, line) - local sep_len = math.max(#msg, #line_text) - _accum_0[_len_0] = table.concat({ - msg, - ("="):rep(sep_len), - line_text - }, "\n") - else - _accum_0[_len_0] = msg - end - _len_0 = _len_0 + 1 - end - formatted = _accum_0 - end - if header then - table.insert(formatted, 1, header) - end - return table.concat(formatted, "\n\n") -end -local whitelist_for_file -do - local lint_config - whitelist_for_file = function(fname) - if not (lint_config) then - lint_config = { } - pcall(function() - lint_config = require("lint_config") - end) - end - if not (lint_config.whitelist_globals) then - return default_whitelist - end - local final_list = { } - for pattern, list in pairs(lint_config.whitelist_globals) do - if fname:match(pattern) then - for _index_0 = 1, #list do - local item = list[_index_0] - insert(final_list, item) - end - end - end - return setmetatable(Set(final_list), { - __index = default_whitelist - }) - end -end -local lint_code -lint_code = function(code, name, whitelist_globals) - if name == nil then - name = "string input" - end - local parse = require("moonscript.parse") - local tree, err = parse.string(code) - if not (tree) then - return nil, err - end - local scope = LinterBlock(whitelist_globals) - scope:stms(tree) - scope:lint_check_unused() - return format_lint(scope.lint_errors, code, name) -end -local lint_file -lint_file = function(fname) - local f, err = io.open(fname) - if not (f) then - return nil, err - end - return lint_code(f:read("*a"), fname, whitelist_for_file(fname)) -end -return { - lint_code = lint_code, - lint_file = lint_file -} diff --git a/moonscript/cmd/lint.moon b/moonscript/cmd/lint.moon deleted file mode 100644 index 103c25ed..00000000 --- a/moonscript/cmd/lint.moon +++ /dev/null @@ -1,204 +0,0 @@ - -import insert from table -import Set from require "moonscript.data" -import Block from require "moonscript.compile" - -{type: mtype} = require("moonscript.util").moon - --- globals allowed to be referenced -default_whitelist = Set { - '_G' - '_VERSION' - 'assert' - 'bit32' - 'collectgarbage' - 'coroutine' - 'debug' - 'dofile' - 'error' - 'getfenv' - 'getmetatable' - 'io' - 'ipairs' - 'load' - 'loadfile' - 'loadstring' - 'math' - 'module' - 'next' - 'os' - 'package' - 'pairs' - 'pcall' - 'print' - 'rawequal' - 'rawget' - 'rawlen' - 'rawset' - 'require' - 'select' - 'setfenv' - 'setmetatable' - 'string' - 'table' - 'tonumber' - 'tostring' - 'type' - 'unpack' - 'xpcall' - - "nil" - "true" - "false" -} - -class LinterBlock extends Block - new: (whitelist_globals=default_whitelist, ...) => - super ... - @get_root_block = -> @ - - @lint_errors = {} - - vc = @value_compilers - @value_compilers = setmetatable { - ref: (block, val) -> - name = val[2] - unless block\has_name(name) or whitelist_globals[name] or name\match "%." - insert @lint_errors, { - "accessing global `#{name}`" - val[-1] - } - - block\lint_mark_used name - vc.ref block, val - }, __index: vc - - sc = @statement_compilers - @statement_compilers = setmetatable { - assign: (block, node) -> - names = node[2] - -- extract the names to be declared - for name in *names - -- don't include autogenerated names - if type(name) == "table" and name[1] == "temp_name" - continue - - real_name, is_local = block\extract_assign_name name - -- already defined in some other scope - unless is_local or real_name and not block\has_name real_name, true - continue - - continue if real_name == "_" - - block.lint_unused_names or= {} - block.lint_unused_names[real_name] = node[-1] or 0 - - sc.assign block, node - }, __index: sc - - lint_mark_used: (name) => - if @lint_unused_names and @lint_unused_names[name] - @lint_unused_names[name] = false - return - - if @parent - @parent\lint_mark_used name - - lint_check_unused: => - return unless @lint_unused_names and next @lint_unused_names - - names_by_position = {} - for name, pos in pairs @lint_unused_names - continue unless pos - names_by_position[pos] or= {} - insert names_by_position[pos], name - - tuples = [{pos, names} for pos,names in pairs names_by_position] - table.sort tuples, (a,b) -> a[1] < b[1] - - for {pos, names} in *tuples - insert @get_root_block!.lint_errors, { - "assigned but unused #{table.concat ["`#{n}`" for n in *names], ", "}" - pos - } - - render: (...) => - @lint_check_unused! - super ... - - block: (...) => - - with super ... - .block = @block - .render = @render - .get_root_block = @get_root_block - .lint_check_unused = @lint_check_unused - .lint_mark_used = @lint_mark_used - .value_compilers = @value_compilers - .statement_compilers = @statement_compilers - -format_lint = (errors, code, header) -> - return unless next errors - - import pos_to_line, get_line from require "moonscript.util" - formatted = for {msg, pos} in *errors - if pos - line = pos_to_line code, pos - msg = "line #{line}: #{msg}" - line_text = "> " .. get_line code, line - - sep_len = math.max #msg, #line_text - table.concat { - msg - "="\rep sep_len - line_text - }, "\n" - - else - msg - - table.insert formatted, 1, header if header - table.concat formatted, "\n\n" - - --- { --- whitelist_globals: { --- ["some_file_pattern"]: { --- "some_var", "another_var" --- } --- } --- } -whitelist_for_file = do - local lint_config - (fname) -> - unless lint_config - lint_config = {} - pcall -> lint_config = require "lint_config" - - return default_whitelist unless lint_config.whitelist_globals - final_list = {} - for pattern, list in pairs lint_config.whitelist_globals - if fname\match(pattern) - for item in *list - insert final_list, item - - setmetatable Set(final_list), __index: default_whitelist - -lint_code = (code, name="string input", whitelist_globals) -> - parse = require "moonscript.parse" - tree, err = parse.string code - return nil, err unless tree - - scope = LinterBlock whitelist_globals - scope\stms tree - scope\lint_check_unused! - - format_lint scope.lint_errors, code, name - -lint_file = (fname) -> - f, err = io.open fname - return nil, err unless f - lint_code f\read("*a"), fname, whitelist_for_file fname - - -{ :lint_code, :lint_file } diff --git a/moonscript/compile.moon b/moonscript/compile.moon index a2b17fee..e1d196b0 100644 --- a/moonscript/compile.moon +++ b/moonscript/compile.moon @@ -443,20 +443,20 @@ format_error = (msg, pos, file_str) -> line_message }, "\n" -value = (value) -> +value = (v) -> out = nil with RootBlock! - \add \value value + \add \value v out = \render! out -tree = (tree, options={}) -> - assert tree, "missing tree" +tree = (t, options={}) -> + assert t, "missing tree" scope = (options.scope or RootBlock) options runner = coroutine.create -> - scope\root_stms tree + scope\root_stms t success, err = coroutine.resume runner @@ -467,7 +467,7 @@ tree = (tree, options={}) -> unpack err, 2 else -- unknown error, bubble it - error "Unknown error thrown", util.dump error_msg + error "Unknown error thrown", util.dump err else concat {err, debug.traceback runner}, "\n" diff --git a/moonscript/compile/statement.moon b/moonscript/compile/statement.moon index 25f53fd0..8ee075bd 100644 --- a/moonscript/compile/statement.moon +++ b/moonscript/compile/statement.moon @@ -1,6 +1,6 @@ import ntype from require "moonscript.types" -import concat, insert from table +import concat from table import unpack from require "moonscript.util" @@ -74,7 +74,7 @@ import unpack from require "moonscript.util" current.next = next current = next - add_clause cond for cond in *node[4,] + add_clause clause for clause in *node[4,] root repeat: (node) => diff --git a/moonscript/compile/value.moon b/moonscript/compile/value.moon index 7cfdda4e..c4a06ad6 100644 --- a/moonscript/compile/value.moon +++ b/moonscript/compile/value.moon @@ -63,8 +63,8 @@ string_chars = { if sup = @get "super" return @value sup self, node - chain_item = (node) -> - t, arg = unpack node + chain_item = (item) -> + t, arg = unpack item if t == "call" -- print arg, util.dump arg "(", @values(arg), ")" diff --git a/moonscript/dump.lua b/moonscript/dump.lua index 1496ed43..ec9136e2 100644 --- a/moonscript/dump.lua +++ b/moonscript/dump.lua @@ -31,8 +31,8 @@ local tree tree = function(block) local _list_0 = block for _index_0 = 1, #_list_0 do - local value = _list_0[_index_0] - print(flat_value(value)) + local v = _list_0[_index_0] + print(flat_value(v)) end end return { diff --git a/moonscript/dump.moon b/moonscript/dump.moon index a6373daa..c817aebc 100644 --- a/moonscript/dump.moon +++ b/moonscript/dump.moon @@ -12,7 +12,7 @@ value = (op) -> flat_value op tree = (block) -> - print flat_value value for value in *block + print flat_value v for v in *block { :value, :tree } diff --git a/moonscript/errors.lua b/moonscript/errors.lua index d85c3162..52810920 100644 --- a/moonscript/errors.lua +++ b/moonscript/errors.lua @@ -1,10 +1,7 @@ local util = require("moonscript.util") local lpeg = require("lpeg") -local concat, insert -do - local _obj_0 = table - concat, insert = _obj_0.concat, _obj_0.insert -end +local concat +concat = table.concat local split, pos_to_line split, pos_to_line = util.split, util.pos_to_line local user_error @@ -69,7 +66,7 @@ rewrite_traceback = function(text, err) V, S, Ct, C = lpeg.V, lpeg.S, lpeg.Ct, lpeg.C local header_text = "stack traceback:" local Header, Line = V("Header"), V("Line") - local Break = lpeg.S("\n") + local Break = S("\n") local g = lpeg.P({ Header, Header = header_text * Break * Ct(Line ^ 1), diff --git a/moonscript/errors.moon b/moonscript/errors.moon index b04c5671..c290da01 100644 --- a/moonscript/errors.moon +++ b/moonscript/errors.moon @@ -3,7 +3,7 @@ util = require "moonscript.util" lpeg = require "lpeg" -import concat, insert from table +import concat from table import split, pos_to_line from util user_error = (...) -> @@ -47,7 +47,7 @@ rewrite_traceback = (text, err) -> header_text = "stack traceback:" Header, Line = V("Header"), V("Line") - Break = lpeg.S "\n" + Break = S "\n" g = lpeg.P { Header Header: header_text * Break * Ct(Line^1) diff --git a/moonscript/lint/config.lua b/moonscript/lint/config.lua new file mode 100644 index 00000000..b18bdd94 --- /dev/null +++ b/moonscript/lint/config.lua @@ -0,0 +1,224 @@ +local append = table.insert +local builtin_whitelist_globals = { + '_G', + '_VERSION', + 'assert', + 'collectgarbage', + 'dofile', + 'error', + 'getfenv', + 'getmetatable', + 'ipairs', + 'load', + 'loadfile', + 'loadstring', + 'module', + 'next', + 'pairs', + 'pcall', + 'print', + 'rawequal', + 'rawget', + 'rawset', + 'require', + 'select', + 'setfenv', + 'setmetatable', + 'tonumber', + 'tostring', + 'type', + 'unpack', + 'xpcall', + 'coroutine', + 'debug', + 'io', + 'math', + 'os', + 'package', + 'string', + 'table', + 'true', + 'false', + 'nil' +} +local config_for +config_for = function(path) + local has_moonscript = pcall(require, 'moonscript') + local look_for = { + 'lint_config.lua' + } + if has_moonscript then + table.insert(look_for, 1, 'lint_config.moon') + end + local exists + exists = function(f) + local fh = io.open(f, 'r') + if fh then + fh:close() + return true + end + return false + end + local dir = path:match('(.+)[/\\].+$') or path + while dir do + for _index_0 = 1, #look_for do + local name = look_for[_index_0] + local config = tostring(dir) .. "/" .. tostring(name) + if exists(config) then + return config + end + end + dir = dir:match('(.+)[/\\].+$') + end + if not (path:match('^/')) then + for _index_0 = 1, #look_for do + local name = look_for[_index_0] + if exists(name) then + return name + end + end + end + return nil +end +local load_config_from +load_config_from = function(config, file) + if type(config) == 'string' then + local loader = loadfile + if config:match('.moon$') then + loader = require("moonscript.base").loadfile + end + local chunk = assert(loader(config)) + config = chunk() or { } + end + local opts = { + report_loop_variables = config.report_loop_variables, + report_params = config.report_params + } + local _list_0 = { + 'whitelist_globals', + 'whitelist_loop_variables', + 'whitelist_params', + 'whitelist_unused', + 'whitelist_shadowing' + } + for _index_0 = 1, #_list_0 do + local list = _list_0[_index_0] + if config[list] then + local wl = { } + for k, v in pairs(config[list]) do + if file:find(k) then + for _index_1 = 1, #v do + local token = v[_index_1] + append(wl, token) + end + end + end + opts[list] = wl + end + end + return opts +end +local whitelist +whitelist = function(...) + local lists = { + ... + } + if not (#lists > 0) then + return function() + return false + end + end + local wl = { } + local patterns = { } + for _index_0 = 1, #lists do + local list = lists[_index_0] + for _index_1 = 1, #list do + local p = list[_index_1] + if p:match('^%w+$') then + append(wl, p) + else + append(patterns, p) + end + end + end + do + local _tbl_0 = { } + for _index_0 = 1, #wl do + local k = wl[_index_0] + _tbl_0[k] = true + end + wl = _tbl_0 + end + return function(sym) + if wl[sym] then + return true + end + for _index_0 = 1, #patterns do + local p = patterns[_index_0] + if sym:match(p) then + return true + end + end + return false + end +end +local evaluator +evaluator = function(opts) + if opts == nil then + opts = { } + end + local report_params = opts.report_params + if report_params == nil then + report_params = false + end + local whitelist_params = whitelist(opts.whitelist_params or { + '^_', + '%.%.%.' + }) + local report_loop_variables = opts.report_loop_variables + if report_loop_variables == nil then + report_loop_variables = true + end + local whitelist_loop_variables = whitelist(opts.whitelist_loop_variables or { + '^_', + 'i', + 'j' + }) + local report_shadowing = opts.report_shadowing + if report_shadowing == nil then + report_shadowing = true + end + local builtin_whitelist_shadowing = whitelist({ + '%.%.%.', + '_ENV' + }) + local whitelist_shadowing = whitelist(opts.whitelist_shadowing) or builtin_whitelist_shadowing + local whitelist_global_access = whitelist(builtin_whitelist_globals, opts.whitelist_globals) + local whitelist_unused = whitelist({ + '^_$', + 'tostring', + '_ENV' + }, opts.whitelist_unused) + return { + allow_global_access = function(p) + return whitelist_global_access(p) + end, + allow_unused_param = function(p) + return not report_params or whitelist_params(p) + end, + allow_unused_loop_variable = function(p) + return not report_loop_variables or whitelist_loop_variables(p) + end, + allow_unused = function(p) + return whitelist_unused(p) + end, + allow_shadowing = function(p) + return not report_shadowing or (whitelist_shadowing(p) or builtin_whitelist_shadowing(p)) + end + } +end +return { + config_for = config_for, + load_config_from = load_config_from, + evaluator = evaluator +} diff --git a/moonscript/lint/config.moon b/moonscript/lint/config.moon new file mode 100644 index 00000000..016d78d8 --- /dev/null +++ b/moonscript/lint/config.moon @@ -0,0 +1,181 @@ +append = table.insert + +builtin_whitelist_globals = { + '_G' + '_VERSION' + 'assert' + 'collectgarbage' + 'dofile' + 'error' + 'getfenv' + 'getmetatable' + 'ipairs' + 'load' + 'loadfile' + 'loadstring' + 'module' + 'next' + 'pairs' + 'pcall' + 'print' + 'rawequal' + 'rawget' + 'rawset' + 'require' + 'select' + 'setfenv' + 'setmetatable' + 'tonumber' + 'tostring' + 'type' + 'unpack' + 'xpcall' + 'coroutine' + 'debug' + 'io' + 'math' + 'os' + 'package' + 'string' + 'table' + + 'true', + 'false', + 'nil' +} + +config_for = (path) -> + has_moonscript = pcall require, 'moonscript' + look_for = { 'lint_config.lua' } + if has_moonscript + table.insert look_for, 1, 'lint_config.moon' + + exists = (f) -> + fh = io.open f, 'r' + if fh + fh\close! + return true + + false + + dir = path\match('(.+)[/\\].+$') or path + while dir + for name in *look_for + config = "#{dir}/#{name}" + return config if exists(config) + + dir = dir\match('(.+)[/\\].+$') + + unless path\match('^/') + for name in *look_for + return name if exists(name) + + nil + +load_config_from = (config, file) -> + if type(config) == 'string' -- assume path to config + loader = loadfile + if config\match('.moon$') + loader = require("moonscript.base").loadfile + + chunk = assert loader(config) + config = chunk! or {} + + opts = { + report_loop_variables: config.report_loop_variables, + report_params: config.report_params + } + for list in *{ + 'whitelist_globals', + 'whitelist_loop_variables', + 'whitelist_params', + 'whitelist_unused', + 'whitelist_shadowing' + } + if config[list] + wl = {} + for k, v in pairs config[list] + if file\find(k) + for token in *v + append wl, token + + opts[list] = wl + + opts + +whitelist = (...) -> + lists = {...} + unless #lists > 0 + return -> false + + wl = {} + patterns = {} + + for list in *lists + for p in *list + if p\match '^%w+$' + append wl, p + else + append patterns, p + + wl = {k, true for k in *wl} + + (sym) -> + if wl[sym] + return true + + for p in *patterns + if sym\match(p) + return true + + false + +evaluator = (opts = {}) -> + report_params = opts.report_params + report_params = false if report_params == nil + whitelist_params = whitelist opts.whitelist_params or { + '^_', + '%.%.%.' + } + + report_loop_variables = opts.report_loop_variables + report_loop_variables = true if report_loop_variables == nil + whitelist_loop_variables = whitelist opts.whitelist_loop_variables or { + '^_', + 'i', + 'j' + } + + report_shadowing = opts.report_shadowing + report_shadowing = true if report_shadowing == nil + builtin_whitelist_shadowing = whitelist { '%.%.%.', '_ENV' } + whitelist_shadowing = whitelist(opts.whitelist_shadowing) or builtin_whitelist_shadowing + + whitelist_global_access = whitelist builtin_whitelist_globals, opts.whitelist_globals + whitelist_unused = whitelist { + '^_$', + 'tostring', + '_ENV' + }, opts.whitelist_unused + + { + allow_global_access: (p) -> + whitelist_global_access(p) + + allow_unused_param: (p) -> + not report_params or whitelist_params(p) + + allow_unused_loop_variable: (p) -> + not report_loop_variables or whitelist_loop_variables(p) + + allow_unused: (p) -> + whitelist_unused(p) + + allow_shadowing: (p) -> + not report_shadowing or ( + whitelist_shadowing(p) or + builtin_whitelist_shadowing(p) + ) + } + +:config_for, :load_config_from, :evaluator diff --git a/moonscript/lint/init.lua b/moonscript/lint/init.lua new file mode 100644 index 00000000..7b3e8726 --- /dev/null +++ b/moonscript/lint/init.lua @@ -0,0 +1,702 @@ +local parse = require("moonscript.parse") +local pos_to_line, get_line +do + local _obj_0 = require("moonscript.util") + pos_to_line, get_line = _obj_0.pos_to_line, _obj_0.get_line +end +local config = require("moonscript.lint.config") +local append = table.insert +local add +add = function(map, key, val) + local list = map[key] + if not (list) then + list = { } + map[key] = list + end + return append(list, val) +end +local Scope +Scope = function(node, parent) + assert(node, "Missing node") + local declared = { } + local used = { } + local scopes = { } + local shadowing_decls = { } + local pos = node[-1] + if not pos and parent then + pos = parent.pos + end + return { + parent = parent, + declared = declared, + used = used, + shadowing_decls = shadowing_decls, + scopes = scopes, + node = node, + pos = pos, + type = 'default', + has_declared = function(self, name) + if declared[name] then + return true + end + return parent and parent:has_declared(name) + end, + has_parent = function(self, type) + if not (parent) then + return false + end + if parent.type == type then + return true + end + return parent:has_parent(type) + end, + add_declaration = function(self, name, opts) + if parent and parent:has_declared(name) then + add(shadowing_decls, name, opts) + end + return add(declared, name, opts) + end, + add_assignment = function(self, name, ass) + if self:has_declared(name) then + return + end + if not parent or not parent:has_declared(name) then + return add(declared, name, ass) + end + end, + add_ref = function(self, name, ref) + if declared[name] then + return add(used, name, ref) + else + if parent and parent:has_declared(name) then + return parent:add_ref(name, ref) + else + return add(used, name, ref) + end + end + end, + open_scope = function(self, sub_node, type) + local scope = Scope(sub_node, self) + scope.type = type + append(scopes, scope) + return scope + end + } +end +local has_subnode +has_subnode = function(node, types) + if not (type(node) == 'table') then + return false + end + for _index_0 = 1, #types do + local t = types[_index_0] + if node[1] == t then + return true + end + end + for _index_0 = 1, #node do + local n = node[_index_0] + if has_subnode(n, types) then + return true + end + end + return false +end +local is_loop_assignment +is_loop_assignment = function(list) + local node = list[1] + if not (type(node) == 'table') then + return false + end + if not (node[1] == 'chain') then + return false + end + local last = node[#node] + if not (last[1] == 'call') then + return false + end + local c_target = last[2] + if not (type(c_target) == 'table' and #c_target == 1) then + return false + end + local op = c_target[1][1] + return op == 'for' or op == 'foreach' +end +local is_fndef_assignment +is_fndef_assignment = function(list) + local node = list[1] + if not (type(node) == 'table') then + return false + end + return node[1] == 'fndef' +end +local destructuring_decls +destructuring_decls = function(list) + local found = { } + for _index_0 = 1, #list do + local exp_list = list[_index_0] + for _index_1 = 1, #exp_list do + local t_var = exp_list[_index_1] + if type(t_var) == 'table' then + local _exp_0 = t_var[1] + if 'ref' == _exp_0 then + append(found, { + t_var[2], + t_var[-1] + }) + elseif 'table' == _exp_0 then + for name, pos in destructuring_decls(t_var[2]) do + append(found, { + name, + pos + }) + end + end + end + end + end + local i = 1 + return function() + local decl = found[i] + if not (decl) then + return nil + end + i = i + 1 + return decl[1], decl[2] + end +end +local handlers = { + update = function(node, scope, walk, ref_pos) + local target, val = node[2], node[4] + if not (scope.is_wrapper) then + if is_loop_assignment({ + val + }) then + scope = scope:open_scope(node, 'loop-update') + scope.is_wrapper = true + end + end + if target[1] == 'ref' then + scope:add_assignment(target[2], { + pos = target[-1] or ref_pos + }) + else + walk(target, scope, ref_pos) + end + return walk({ + val + }, scope, ref_pos) + end, + assign = function(node, scope, walk, ref_pos) + local targets = node[2] + local values = node[3] + local pos = node[-1] or ref_pos + if not (scope.is_wrapper) then + if is_loop_assignment(values) then + scope = scope:open_scope(node, 'loop-assignment') + scope.is_wrapper = true + end + end + local is_fndef = is_fndef_assignment(values) + if not (is_fndef) then + walk(values, scope, ref_pos) + end + for _index_0 = 1, #targets do + local t = targets[_index_0] + local _exp_0 = t[1] + if 'ref' == _exp_0 then + scope:add_assignment(t[2], { + pos = t[-1] or pos + }) + elseif 'chain' == _exp_0 then + walk(t, scope, ref_pos) + elseif 'table' == _exp_0 then + for name, d_pos in destructuring_decls(t[2]) do + scope:add_assignment(name, { + pos = d_pos or pos + }) + end + end + end + if is_fndef then + return walk(values, scope, ref_pos) + end + end, + chain = function(node, scope, walk, ref_pos) + if not scope.is_wrapper and is_loop_assignment({ + node + }) then + scope = scope:open_scope(node, 'chain') + scope.is_wrapper = true + end + return walk(node, scope, ref_pos) + end, + ref = function(node, scope, walk, ref_pos) + return scope:add_ref(node[2], { + pos = node[-1] or ref_pos + }) + end, + fndef = function(node, scope, walk, ref_pos) + local params, f_type, body = node[2], node[4], node[5] + local t = f_type == 'fat' and 'method' or 'function' + scope = scope:open_scope(node, t) + local pos = node[-1] or ref_pos + for _index_0 = 1, #params do + local p = params[_index_0] + local def = p[1] + if type(def) == 'string' then + scope:add_declaration(def, { + pos = pos, + type = 'param' + }) + if p[2] then + walk({ + p[2] + }, scope, ref_pos) + end + elseif type(def) == 'table' and def[1] == 'self' then + scope:add_declaration(def[2], { + pos = pos, + type = 'param' + }) + scope:add_ref(def[2], { + pos = pos + }) + if p[2] then + walk({ + p[2] + }, scope, ref_pos) + end + else + walk({ + p + }, scope, ref_pos) + end + end + return walk(body, scope, ref_pos) + end, + ["for"] = function(node, scope, walk, ref_pos) + local var, args, body = node[2], node[3], node[4] + if not (scope.is_wrapper) then + scope = scope:open_scope(node, 'for') + end + scope:add_declaration(var, { + pos = node[-1] or ref_pos, + type = 'loop-var' + }) + walk(args, scope, ref_pos) + if body then + return walk(body, scope, ref_pos) + end + end, + foreach = function(node, scope, walk, ref_pos) + local vars, args, body = node[2], node[3], node[4] + if not body then + body = args + args = nil + end + if not (scope.is_wrapper) then + scope = scope:open_scope(node, 'for-each') + end + if args then + walk(args, scope, ref_pos) + end + for _index_0 = 1, #vars do + local var = vars[_index_0] + local _exp_0 = type(var) + if 'string' == _exp_0 then + scope:add_declaration(var, { + pos = node[-1] or ref_pos, + type = 'loop-var' + }) + elseif 'table' == _exp_0 then + if var[1] == 'table' then + for name, pos in destructuring_decls(var[2]) do + scope:add_declaration(name, { + pos = pos or ref_pos, + type = 'loop-var' + }) + end + end + end + end + return walk(body, scope, ref_pos) + end, + declare_with_shadows = function(node, scope, walk, ref_pos) + local names = node[2] + for _index_0 = 1, #names do + local name = names[_index_0] + scope:add_declaration(name, { + pos = node[-1] or ref_pos + }) + end + end, + export = function(node, scope, walk, ref_pos) + local names, vals = node[2], node[3] + if type(names) == 'string' then + scope.exported_from = node[-1] + else + for _index_0 = 1, #names do + local name = names[_index_0] + scope:add_declaration(name, { + pos = node[-1] or ref_pos, + is_exported = true, + type = 'export' + }) + end + end + if vals then + return walk({ + vals + }, scope, ref_pos) + end + end, + import = function(node, scope, walk, ref_pos) + local names, values = node[2], node[3] + for _index_0 = 1, #names do + local name = names[_index_0] + if type(name) == 'table' and name[1] == 'colon' then + name = name[2] + end + scope:add_declaration(name, { + pos = node[-1] or ref_pos, + type = 'import' + }) + end + return walk({ + values + }, scope, ref_pos) + end, + decorated = function(node, scope, walk, ref_pos) + local stm, vals = node[2], node[3] + if not (vals[1] == 'if' or vals[1] == 'unless') then + scope = scope:open_scope(node, 'decorated') + scope.is_wrapper = true + end + walk({ + vals + }, scope, ref_pos) + return walk({ + stm + }, scope, ref_pos) + end, + comprehension = function(node, scope, walk, ref_pos) + local exps, loop = node[2], node[3] + if not (scope.is_wrapper) then + scope = scope:open_scope(node, 'comprehension') + scope.is_wrapper = true + end + if not (loop) then + loop = exps + exps = nil + end + walk({ + loop + }, scope, ref_pos) + if exps then + return walk({ + exps + }, scope, ref_pos) + end + end, + tblcomprehension = function(node, scope, walk, ref_pos) + local exps, loop = node[2], node[3] + if not (scope.is_wrapper) then + scope = scope:open_scope(node, 'tblcomprehension') + scope.is_wrapper = true + end + if not (loop) then + loop = exps + exps = nil + end + walk({ + loop + }, scope, ref_pos) + if exps then + return walk({ + exps + }, scope, ref_pos) + end + end, + class = function(node, scope, walk, ref_pos) + local name, parent, body = node[2], node[3], node[4] + local handle_name = name and type(name) == 'string' + if handle_name then + scope:add_declaration(name, { + pos = node[-1] or ref_pos, + type = 'class' + }) + end + if handle_name and scope.node[#scope.node] == node then + scope:add_ref(name, { + pos = node[-1] or ref_pos + }) + end + walk({ + parent + }, scope, ref_pos) + scope = scope:open_scope(node, 'class') + return walk(body, scope, ref_pos) + end, + ["while"] = function(node, scope, walk, ref_pos) + local conds, body = node[2], node[3] + walk({ + conds + }, scope, ref_pos) + local cond_scope = scope:open_scope(node, 'while') + if body then + return walk(body, cond_scope, ref_pos) + end + end, + with = function(node, scope, walk, ref_pos) + local assigns, body = node[2], node[3] + local with_scope = scope:open_scope(node, 'with') + walk({ + assigns + }, with_scope, ref_pos) + for name in pairs(with_scope.declared) do + with_scope:add_ref(name, { + pos = ref_pos + }) + end + return walk({ + body + }, with_scope, ref_pos) + end, + cond_block = function(node, scope, walk, ref_pos) + local op, conds, body = node[1], node[2], node[3] + walk({ + conds + }, scope, ref_pos) + local cond_scope = scope:open_scope(node, op) + if body then + walk(body, cond_scope, ref_pos) + end + local rest + do + local _accum_0 = { } + local _len_0 = 1 + for i, n in ipairs(node) do + if i > 3 then + _accum_0[_len_0] = n + _len_0 = _len_0 + 1 + end + end + rest = _accum_0 + end + if #rest > 0 then + return walk(rest, scope, ref_pos) + end + end, + ["else"] = function(node, scope, walk, ref_pos) + local body = node[2] + scope = scope:open_scope(node, 'else') + return walk(body, scope, ref_pos) + end +} +handlers['if'] = handlers.cond_block +handlers['elseif'] = handlers.cond_block +handlers['unless'] = handlers.cond_block +local resolve_pos +resolve_pos = function(node, base_pos) + if node[-1] then + return node[-1] + end + if type(node) == 'table' then + for _index_0 = 1, #node do + local sub_node = node[_index_0] + if type(sub_node) == 'table' then + if sub_node[-1] then + return sub_node[-1] + end + end + end + end + return base_pos +end +local walk +walk = function(tree, scope, base_pos) + if not (tree) then + error("nil passed for node: " .. tostring(debug.traceback())) + end + if not (base_pos) then + error("nil passed for base_pos: " .. tostring(debug.traceback())) + end + for _index_0 = 1, #tree do + local node = tree[_index_0] + local ref_pos = resolve_pos(node, base_pos) + local handler = handlers[node[1]] + if handler then + handler(node, scope, walk, ref_pos) + else + for _index_1 = 1, #node do + local sub_node = node[_index_1] + if type(sub_node) == 'table' then + walk({ + sub_node + }, scope, ref_pos) + end + end + end + end +end +local report_on_scope +report_on_scope = function(scope, evaluator, inspections) + if inspections == nil then + inspections = { } + end + for name, decls in pairs(scope.declared) do + local _continue_0 = false + repeat + if scope.used[name] then + _continue_0 = true + break + end + for _index_0 = 1, #decls do + local _continue_1 = false + repeat + local decl = decls[_index_0] + if decl.is_exported or scope.exported_from and scope.exported_from < decl.pos then + _continue_1 = true + break + end + if decl.type == 'param' then + if evaluator.allow_unused_param(name) then + _continue_1 = true + break + end + elseif decl.type == 'loop-var' then + if evaluator.allow_unused_loop_variable(name) then + _continue_1 = true + break + end + else + if evaluator.allow_unused(name) then + _continue_1 = true + break + end + end + append(inspections, { + msg = "declared but unused - `" .. tostring(name) .. "`", + pos = decl.pos or scope.pos + }) + _continue_1 = true + until true + if not _continue_1 then + break + end + end + _continue_0 = true + until true + if not _continue_0 then + break + end + end + for name, nodes in pairs(scope.used) do + local _continue_0 = false + repeat + if not (scope.declared[name] or evaluator.allow_global_access(name)) then + if name == 'self' or name == 'super' then + if scope.type == 'method' or scope:has_parent('method') then + _continue_0 = true + break + end + end + for _index_0 = 1, #nodes do + local node = nodes[_index_0] + append(inspections, { + msg = "accessing global - `" .. tostring(name) .. "`", + pos = node.pos or scope.pos + }) + end + end + _continue_0 = true + until true + if not _continue_0 then + break + end + end + for name, nodes in pairs(scope.shadowing_decls) do + if not (evaluator.allow_shadowing(name)) then + for _index_0 = 1, #nodes do + local node = nodes[_index_0] + append(inspections, { + msg = "shadowing outer variable - `" .. tostring(name) .. "`", + pos = node.pos or scope.pos + }) + end + end + end + local _list_0 = scope.scopes + for _index_0 = 1, #_list_0 do + local sub_scope = _list_0[_index_0] + report_on_scope(sub_scope, evaluator, inspections) + end + return inspections +end +local format_inspections +format_inspections = function(inspections) + local chunks = { } + for _index_0 = 1, #inspections do + local inspection = inspections[_index_0] + local chunk = "line " .. tostring(inspection.line) .. ": " .. tostring(inspection.msg) .. "\n" + chunk = chunk .. (string.rep('=', #chunk - 1) .. '\n') + chunk = chunk .. "> " .. tostring(inspection.code) .. "\n" + chunks[#chunks + 1] = chunk + end + return table.concat(chunks, '\n') +end +local report +report = function(scope, code, opts) + if opts == nil then + opts = { } + end + local inspections = { } + local evaluator = config.evaluator(opts) + report_on_scope(scope, evaluator, inspections) + for _index_0 = 1, #inspections do + local inspection = inspections[_index_0] + local line = pos_to_line(code, inspection.pos) + inspection.line = line + inspection.code = get_line(code, line) + end + table.sort(inspections, function(a, b) + return a.line < b.line + end) + return inspections +end +local lint +lint = function(code, opts) + if opts == nil then + opts = { } + end + local tree, err = parse.string(code) + if not (tree) then + return nil, err + end + if opts.print_tree then + require('moon').p(tree) + end + local scope = Scope(tree) + walk(tree, scope, 1) + return report(scope, code, opts) +end +local lint_file +lint_file = function(file, opts) + if opts == nil then + opts = { } + end + local fh = assert(io.open(file, 'r')) + local code = fh:read('*a') + fh:close() + local config_file = opts.lint_config or config.config_for(file) + opts = config_file and config.load_config_from(config_file, file) or { } + opts.file = file + return lint(code, opts) +end +return { + lint = lint, + lint_file = lint_file, + format_inspections = format_inspections, + config = config +} diff --git a/moonscript/lint/init.moon b/moonscript/lint/init.moon new file mode 100644 index 00000000..0a6f62eb --- /dev/null +++ b/moonscript/lint/init.moon @@ -0,0 +1,467 @@ +parse = require "moonscript.parse" +{:pos_to_line, :get_line} = require "moonscript.util" +config = require "moonscript.lint.config" + +append = table.insert + +add = (map, key, val) -> + list = map[key] + unless list + list = {} + map[key] = list + + append list, val + +Scope = (node, parent) -> + assert node, "Missing node" + declared = {} + used = {} + scopes = {} + shadowing_decls = {} + pos = node[-1] + if not pos and parent + pos = parent.pos + + { + :parent, + :declared, + :used, + :shadowing_decls, + :scopes, + :node, + :pos, + type: 'default' + + has_declared: (name) => + return true if declared[name] + parent and parent\has_declared(name) + + has_parent: (type) => + return false unless parent + return true if parent.type == type + return parent\has_parent type + + add_declaration: (name, opts) => + if parent and parent\has_declared(name) + add shadowing_decls, name, opts + + add declared, name, opts + + add_assignment: (name, ass) => + return if @has_declared name + if not parent or not parent\has_declared(name) + add declared, name, ass + + add_ref: (name, ref) => + if declared[name] + add used, name, ref + else if parent and parent\has_declared(name) + parent\add_ref name, ref + else + add used, name, ref + + open_scope: (sub_node, type) => + scope = Scope sub_node, @ + scope.type = type + append scopes, scope + scope + } + +has_subnode = (node, types) -> + return false unless type(node) == 'table' + for t in *types + return true if node[1] == t + + for n in *node + return true if has_subnode n, types + + false + +is_loop_assignment = (list) -> + node = list[1] + return false unless type(node) == 'table' + return false unless node[1] == 'chain' + last = node[#node] + return false unless last[1] == 'call' + c_target = last[2] + return false unless type(c_target) == 'table' and #c_target == 1 + op = c_target[1][1] + op == 'for' or op == 'foreach' + +is_fndef_assignment = (list) -> + node = list[1] + return false unless type(node) == 'table' + node[1] == 'fndef' + +destructuring_decls = (list) -> + found = {} + for exp_list in *list + for t_var in *exp_list + if type(t_var) == 'table' + switch t_var[1] + when 'ref' + append found, { t_var[2], t_var[-1] } + when 'table' + for name, pos in destructuring_decls(t_var[2]) + append found, { name, pos } + + i = 1 + -> + decl = found[i] + return nil unless decl + i += 1 + decl[1], decl[2] + +handlers = { + update: (node, scope, walk, ref_pos) -> + target, val = node[2], node[4] + + unless scope.is_wrapper + if is_loop_assignment({val}) + scope = scope\open_scope node, 'loop-update' + scope.is_wrapper = true + + if target[1] == 'ref' + scope\add_assignment target[2], pos: target[-1] or ref_pos + else + walk target, scope, ref_pos + + walk {val}, scope, ref_pos + + -- x, y = foo!, ... + assign: (node, scope, walk, ref_pos) -> + targets = node[2] + values = node[3] + pos = node[-1] or ref_pos + + unless scope.is_wrapper + if is_loop_assignment(values) + scope = scope\open_scope node, 'loop-assignment' + scope.is_wrapper = true + + is_fndef = is_fndef_assignment values + + -- values are walked before the lvalue, except for fndefs where + -- the lvalue is implicitly local + walk values, scope, ref_pos unless is_fndef + + for t in *targets + switch t[1] -- type of target + when 'ref' -- plain assignment, e.g. 'x = 1' + scope\add_assignment t[2], pos: t[-1] or pos + when 'chain' + -- chained assignment, e.g. 'x.foo = 1' - walk all references + walk t, scope, ref_pos + when 'table' -- handle decomposition syntax, e.g. '{:foo} = table' + for name, d_pos in destructuring_decls(t[2]) + scope\add_assignment name, pos: d_pos or pos + + walk values, scope, ref_pos if is_fndef + + chain: (node, scope, walk, ref_pos) -> + if not scope.is_wrapper and is_loop_assignment({node}) + scope = scope\open_scope node, 'chain' + scope.is_wrapper = true + + walk node, scope, ref_pos + + ref: (node, scope, walk, ref_pos) -> + scope\add_ref node[2], pos: node[-1] or ref_pos + + fndef: (node, scope, walk, ref_pos) -> + params, f_type, body = node[2], node[4], node[5] + t = f_type == 'fat' and 'method' or 'function' + scope = scope\open_scope node, t + pos = node[-1] or ref_pos + for p in *params + def = p[1] + if type(def) == 'string' + scope\add_declaration def, :pos, type: 'param' + if p[2] -- default parameter assignment + walk {p[2]}, scope, ref_pos + elseif type(def) == 'table' and def[1] == 'self' + scope\add_declaration def[2], :pos, type: 'param' + scope\add_ref def[2], :pos + if p[2] -- default parameter assignment + walk {p[2]}, scope, ref_pos + else + walk {p}, scope, ref_pos + + walk body, scope, ref_pos + + for: (node, scope, walk, ref_pos) -> + var, args, body = node[2], node[3], node[4] + + unless scope.is_wrapper + scope = scope\open_scope node, 'for' + + scope\add_declaration var, pos: node[-1] or ref_pos, type: 'loop-var' + + walk args, scope, ref_pos + walk body, scope, ref_pos if body + + foreach: (node, scope, walk, ref_pos) -> + vars, args, body = node[2], node[3], node[4] + + if not body + body = args + args = nil + + unless scope.is_wrapper + scope = scope\open_scope node, 'for-each' + + walk args, scope, ref_pos if args + + for var in *vars + switch type(var) + when 'string' + scope\add_declaration var, pos: node[-1] or ref_pos, type: 'loop-var' + when 'table' + if var[1] == 'table' + for name, pos in destructuring_decls(var[2]) + scope\add_declaration name, pos: pos or ref_pos, type: 'loop-var' + + walk body, scope, ref_pos + + declare_with_shadows: (node, scope, walk, ref_pos) -> + names = node[2] + for name in *names + scope\add_declaration name, pos: node[-1] or ref_pos + + export: (node, scope, walk, ref_pos) -> + names, vals = node[2], node[3] + if type(names) == 'string' -- `export *` + scope.exported_from = node[-1] + else + for name in *names + scope\add_declaration name, pos: node[-1] or ref_pos, is_exported: true, type: 'export' + + if vals + walk {vals}, scope, ref_pos + + import: (node, scope, walk, ref_pos) -> + names, values = node[2], node[3] + + for name in *names + if type(name) == 'table' and name[1] == 'colon' + name = name[2] -- import \foo from bar + + scope\add_declaration name, pos: node[-1] or ref_pos, type: 'import' + + walk {values}, scope, ref_pos + + decorated: (node, scope, walk, ref_pos) -> + stm, vals = node[2], node[3] + + -- statement modifiers with `if` and `unless` does not open a new scope + unless vals[1] == 'if' or vals[1] == 'unless' + scope = scope\open_scope node, 'decorated' + scope.is_wrapper = true + + walk {vals}, scope, ref_pos + walk {stm}, scope, ref_pos + + comprehension: (node, scope, walk, ref_pos) -> + exps, loop = node[2], node[3] + + unless scope.is_wrapper + scope = scope\open_scope node, 'comprehension' + scope.is_wrapper = true + + unless loop + loop = exps + exps = nil + + -- we walk the loop first, as it's there that the declarations are + walk {loop}, scope, ref_pos + walk {exps}, scope, ref_pos if exps + + tblcomprehension: (node, scope, walk, ref_pos) -> + exps, loop = node[2], node[3] + + unless scope.is_wrapper + scope = scope\open_scope node, 'tblcomprehension' + scope.is_wrapper = true + + -- we walk the loop first, as it's there that the declarations are + unless loop + loop = exps + exps = nil + + walk {loop}, scope, ref_pos + walk {exps}, scope, ref_pos if exps + + class: (node, scope, walk, ref_pos) -> + name, parent, body = node[2], node[3], node[4] + handle_name = name and type(name) == 'string' + if handle_name + scope\add_declaration name, pos: node[-1] or ref_pos, type: 'class' + + -- handle implicit return of class, if last node of current scope + if handle_name and scope.node[#scope.node] == node + scope\add_ref name, pos: node[-1] or ref_pos + + walk {parent}, scope, ref_pos + scope = scope\open_scope node, 'class' + walk body, scope, ref_pos + + while: (node, scope, walk, ref_pos) -> + conds, body = node[2], node[3] + walk {conds}, scope, ref_pos + + cond_scope = scope\open_scope node, 'while' + walk body, cond_scope, ref_pos if body + + with: (node, scope, walk, ref_pos) -> + assigns, body = node[2], node[3] + + with_scope = scope\open_scope node, 'with' + walk {assigns}, with_scope, ref_pos + + -- mark any declaration as used immediately + for name in pairs with_scope.declared + with_scope\add_ref name, pos: ref_pos + + walk {body}, with_scope, ref_pos + + -- if, elseif, unless + cond_block: (node, scope, walk, ref_pos) -> + op, conds, body = node[1], node[2], node[3] + walk {conds}, scope, ref_pos + + cond_scope = scope\open_scope node, op + walk body, cond_scope, ref_pos if body + + -- walk any following elseifs/elses as necessary + rest = [n for i, n in ipairs(node) when i > 3] + if #rest > 0 + walk rest, scope, ref_pos + + else: (node, scope, walk, ref_pos) -> + body = node[2] + scope = scope\open_scope node, 'else' + walk body, scope, ref_pos + +} + +handlers['if'] = handlers.cond_block +handlers['elseif'] = handlers.cond_block +handlers['unless'] = handlers.cond_block + +resolve_pos = (node, base_pos) -> + return node[-1] if node[-1] + if type(node) == 'table' + for sub_node in *node + if type(sub_node) == 'table' + if sub_node[-1] + return sub_node[-1] + + base_pos + +walk = (tree, scope, base_pos) -> + unless tree + error "nil passed for node: #{debug.traceback!}" + + unless base_pos + error "nil passed for base_pos: #{debug.traceback!}" + + for node in *tree + ref_pos = resolve_pos(node, base_pos) + handler = handlers[node[1]] + if handler + handler node, scope, walk, ref_pos + else + for sub_node in *node + if type(sub_node) == 'table' + walk { sub_node }, scope, ref_pos + +report_on_scope = (scope, evaluator, inspections = {}) -> + + -- Declared but unused variables + for name, decls in pairs scope.declared + continue if scope.used[name] + + for decl in *decls + if decl.is_exported or scope.exported_from and scope.exported_from < decl.pos + continue + + if decl.type == 'param' + continue if evaluator.allow_unused_param(name) + elseif decl.type == 'loop-var' + continue if evaluator.allow_unused_loop_variable(name) + else + continue if evaluator.allow_unused(name) + + append inspections, { + msg: "declared but unused - `#{name}`" + pos: decl.pos or scope.pos, + } + + -- Used but undefined references + for name, nodes in pairs scope.used + unless scope.declared[name] or evaluator.allow_global_access(name) + if name == 'self' or name == 'super' + if scope.type == 'method' or scope\has_parent('method') + continue + + for node in *nodes + append inspections, { + msg: "accessing global - `#{name}`" + pos: node.pos or scope.pos, + } + + -- Shadowing declarations + for name, nodes in pairs scope.shadowing_decls + unless evaluator.allow_shadowing(name) + for node in *nodes + append inspections, { + msg: "shadowing outer variable - `#{name}`" + pos: node.pos or scope.pos, + } + + for sub_scope in *scope.scopes + report_on_scope sub_scope, evaluator, inspections + + inspections + +format_inspections = (inspections) -> + chunks = {} + for inspection in *inspections + chunk = "line #{inspection.line}: #{inspection.msg}\n" + chunk ..= string.rep('=', #chunk - 1) .. '\n' + chunk ..= "> #{inspection.code}\n" + chunks[#chunks + 1] = chunk + + table.concat chunks, '\n' + +report = (scope, code, opts = {}) -> + inspections = {} + evaluator = config.evaluator opts + report_on_scope scope, evaluator, inspections + + for inspection in *inspections + line = pos_to_line(code, inspection.pos) + inspection.line = line + inspection.code = get_line code, line + + table.sort inspections, (a, b) -> a.line < b.line + inspections + +lint = (code, opts = {}) -> + tree, err = parse.string code + return nil, err unless tree + require('moon').p(tree) if opts.print_tree + scope = Scope tree + walk tree, scope, 1 + report scope, code, opts + +lint_file = (file, opts = {}) -> + fh = assert io.open file, 'r' + code = fh\read '*a' + fh\close! + config_file = opts.lint_config or config.config_for(file) + opts = config_file and config.load_config_from(config_file, file) or {} + opts.file = file + lint code, opts + +:lint, :lint_file, :format_inspections, :config diff --git a/moonscript/parse.lua b/moonscript/parse.lua index da6731e7..5ccb38ef 100644 --- a/moonscript/parse.lua +++ b/moonscript/parse.lua @@ -13,12 +13,12 @@ local unpack unpack = require("moonscript.util").unpack local wrap_env wrap_env = require("moonscript.parse.env").wrap_env -local R, S, V, P, C, Ct, Cmt, Cg, Cb, Cc -R, S, V, P, C, Ct, Cmt, Cg, Cb, Cc = lpeg.R, lpeg.S, lpeg.V, lpeg.P, lpeg.C, lpeg.Ct, lpeg.Cmt, lpeg.Cg, lpeg.Cb, lpeg.Cc -local White, Break, Stop, Comment, Space, SomeSpace, SpaceBreak, EmptyLine, AlphaNum, Num, Shebang, L, _Name +local S, P, C, Ct, Cmt, Cg, Cb, Cc +S, P, C, Ct, Cmt, Cg, Cb, Cc = lpeg.S, lpeg.P, lpeg.C, lpeg.Ct, lpeg.Cmt, lpeg.Cg, lpeg.Cb, lpeg.Cc +local White, Break, Stop, Space, SomeSpace, SpaceBreak, EmptyLine, AlphaNum, Num, Shebang, L, _Name do local _obj_0 = require("moonscript.parse.literals") - White, Break, Stop, Comment, Space, SomeSpace, SpaceBreak, EmptyLine, AlphaNum, Num, Shebang, L, _Name = _obj_0.White, _obj_0.Break, _obj_0.Stop, _obj_0.Comment, _obj_0.Space, _obj_0.SomeSpace, _obj_0.SpaceBreak, _obj_0.EmptyLine, _obj_0.AlphaNum, _obj_0.Num, _obj_0.Shebang, _obj_0.L, _obj_0.Name + White, Break, Stop, Space, SomeSpace, SpaceBreak, EmptyLine, AlphaNum, Num, Shebang, L, _Name = _obj_0.White, _obj_0.Break, _obj_0.Stop, _obj_0.Space, _obj_0.SomeSpace, _obj_0.SpaceBreak, _obj_0.EmptyLine, _obj_0.AlphaNum, _obj_0.Num, _obj_0.Shebang, _obj_0.L, _obj_0.Name end local SpaceName = Space * _Name Num = Space * (Num / function(v) @@ -27,10 +27,10 @@ Num = Space * (Num / function(v) v } end) -local Indent, Cut, ensure, extract_line, mark, pos, flatten_or_mark, is_assignable, check_assignable, format_assign, format_single_assign, sym, symx, simple_string, wrap_func_arg, join_chain, wrap_decorator, check_lua_string, self_assign, got +local Indent, Cut, ensure, extract_line, mark, pos, flatten_or_mark, check_assignable, format_assign, format_single_assign, sym, symx, simple_string, wrap_func_arg, join_chain, wrap_decorator, check_lua_string, self_assign do local _obj_0 = require("moonscript.parse.util") - Indent, Cut, ensure, extract_line, mark, pos, flatten_or_mark, is_assignable, check_assignable, format_assign, format_single_assign, sym, symx, simple_string, wrap_func_arg, join_chain, wrap_decorator, check_lua_string, self_assign, got = _obj_0.Indent, _obj_0.Cut, _obj_0.ensure, _obj_0.extract_line, _obj_0.mark, _obj_0.pos, _obj_0.flatten_or_mark, _obj_0.is_assignable, _obj_0.check_assignable, _obj_0.format_assign, _obj_0.format_single_assign, _obj_0.sym, _obj_0.symx, _obj_0.simple_string, _obj_0.wrap_func_arg, _obj_0.join_chain, _obj_0.wrap_decorator, _obj_0.check_lua_string, _obj_0.self_assign, _obj_0.got + Indent, Cut, ensure, extract_line, mark, pos, flatten_or_mark, check_assignable, format_assign, format_single_assign, sym, symx, simple_string, wrap_func_arg, join_chain, wrap_decorator, check_lua_string, self_assign = _obj_0.Indent, _obj_0.Cut, _obj_0.ensure, _obj_0.extract_line, _obj_0.mark, _obj_0.pos, _obj_0.flatten_or_mark, _obj_0.check_assignable, _obj_0.format_assign, _obj_0.format_single_assign, _obj_0.sym, _obj_0.symx, _obj_0.simple_string, _obj_0.wrap_func_arg, _obj_0.join_chain, _obj_0.wrap_decorator, _obj_0.check_lua_string, _obj_0.self_assign end local build_grammar = wrap_env(debug_grammar, function(root) local _indent = Stack(0) @@ -39,12 +39,12 @@ local build_grammar = wrap_env(debug_grammar, function(root) last_pos = 0 } local check_indent - check_indent = function(str, pos, indent) - state.last_pos = pos + check_indent = function(str, at_pos, indent) + state.last_pos = at_pos return _indent:top() == indent end local advance_indent - advance_indent = function(str, pos, indent) + advance_indent = function(str, at_pos, indent) local top = _indent:top() if top ~= -1 and indent > top then _indent:push(indent) @@ -52,7 +52,7 @@ local build_grammar = wrap_env(debug_grammar, function(root) end end local push_indent - push_indent = function(str, pos, indent) + push_indent = function(str, at_pos, indent) _indent:push(indent) return true end @@ -62,7 +62,7 @@ local build_grammar = wrap_env(debug_grammar, function(root) return true end local check_do - check_do = function(str, pos, do_node) + check_do = function(str, at_pos, do_node) local top = _do_stack:top() if top == nil or top then return true, do_node @@ -96,7 +96,7 @@ local build_grammar = wrap_env(debug_grammar, function(root) end return patt end - local Name = Cmt(SpaceName, function(str, pos, name) + local Name = Cmt(SpaceName, function(str, at_pos, name) if keywords[name] then return false end diff --git a/moonscript/parse.moon b/moonscript/parse.moon index 7fd5de6f..27538226 100644 --- a/moonscript/parse.moon +++ b/moonscript/parse.moon @@ -11,11 +11,11 @@ import unpack from require "moonscript.util" import wrap_env from require "moonscript.parse.env" { - :R, :S, :V, :P, :C, :Ct, :Cmt, :Cg, :Cb, :Cc + :S, :P, :C, :Ct, :Cmt, :Cg, :Cb, :Cc } = lpeg { - :White, :Break, :Stop, :Comment, :Space, :SomeSpace, :SpaceBreak, :EmptyLine, + :White, :Break, :Stop, :Space, :SomeSpace, :SpaceBreak, :EmptyLine, :AlphaNum, :Num, :Shebang, :L Name: _Name } = require "moonscript.parse.literals" @@ -25,9 +25,9 @@ Num = Space * (Num / (v) -> {"number", v}) { :Indent, :Cut, :ensure, :extract_line, :mark, :pos, :flatten_or_mark, - :is_assignable, :check_assignable, :format_assign, :format_single_assign, + :check_assignable, :format_assign, :format_single_assign, :sym, :symx, :simple_string, :wrap_func_arg, :join_chain, - :wrap_decorator, :check_lua_string, :self_assign, :got + :wrap_decorator, :check_lua_string, :self_assign } = require "moonscript.parse.util" @@ -41,17 +41,17 @@ build_grammar = wrap_env debug_grammar, (root) -> last_pos: 0 } - check_indent = (str, pos, indent) -> - state.last_pos = pos + check_indent = (str, at_pos, indent) -> + state.last_pos = at_pos _indent\top! == indent - advance_indent = (str, pos, indent) -> + advance_indent = (str, at_pos, indent) -> top = _indent\top! if top != -1 and indent > top _indent\push indent true - push_indent = (str, pos, indent) -> + push_indent = (str, at_pos, indent) -> _indent\push indent true @@ -59,7 +59,7 @@ build_grammar = wrap_env debug_grammar, (root) -> assert _indent\pop!, "unexpected outdent" true - check_do = (str, pos, do_node) -> + check_do = (str, at_pos, do_node) -> top = _do_stack\top! if top == nil or top return true, do_node @@ -90,7 +90,7 @@ build_grammar = wrap_env debug_grammar, (root) -> patt - Name = Cmt(SpaceName, (str, pos, name) -> + Name = Cmt(SpaceName, (str, at_pos, name) -> return false if keywords[name] true ) / trim diff --git a/moonscript/parse/util.lua b/moonscript/parse/util.lua index 2687384b..08e2bef2 100644 --- a/moonscript/parse/util.lua +++ b/moonscript/parse/util.lua @@ -104,9 +104,9 @@ mark = function(name) end local pos pos = function(patt) - return (Cp() * patt) / function(pos, value) + return (Cp() * patt) / function(at_pos, value) if type(value) == "table" then - value[-1] = pos + value[-1] = at_pos end return value end @@ -116,8 +116,8 @@ got = function(what, context) if context == nil then context = true end - return Cmt("", function(str, pos) - print("++ got " .. tostring(what), "[" .. tostring(show_line_position(str, pos, context)) .. "]") + return Cmt("", function(str, at_pos) + print("++ got " .. tostring(what), "[" .. tostring(show_line_position(str, at_pos, context)) .. "]") return true end) end @@ -153,7 +153,7 @@ do end end local check_assignable -check_assignable = function(str, pos, value) +check_assignable = function(str, at_pos, value) if is_assignable(value) then return true, value else @@ -264,11 +264,11 @@ wrap_decorator = function(stm, dec) } end local check_lua_string -check_lua_string = function(str, pos, right, left) +check_lua_string = function(str, at_pos, right, left) return #left == #right end local self_assign -self_assign = function(name, pos) +self_assign = function(name, at_pos) return { { "key_literal", @@ -277,7 +277,7 @@ self_assign = function(name, pos) { "ref", name, - [-1] = pos + [-1] = at_pos } } end diff --git a/moonscript/parse/util.moon b/moonscript/parse/util.moon index 17adf77a..79eb9c92 100644 --- a/moonscript/parse/util.moon +++ b/moonscript/parse/util.moon @@ -79,16 +79,16 @@ mark = (name) -> -- pos is the character offset from the buffer where the node was parsed from. -- Used to generate error messages pos = (patt) -> - (Cp! * patt) / (pos, value) -> + (Cp! * patt) / (at_pos, value) -> if type(value) == "table" - value[-1] = pos + value[-1] = at_pos value -- generates a debug pattern that always succeeds and prints out where we are -- in the buffer with a label got = (what, context=true) -> - Cmt "", (str, pos) -> - print "++ got #{what}", "[#{show_line_position str, pos, context}]" + Cmt "", (str, at_pos) -> + print "++ got #{what}", "[#{show_line_position str, at_pos, context}]" true -- converts 1 element array to its value, otherwise marks it @@ -112,7 +112,7 @@ is_assignable = do else false -check_assignable = (str, pos, value) -> +check_assignable = (str, at_pos, value) -> if is_assignable value true, value else @@ -184,12 +184,12 @@ wrap_decorator = (stm, dec) -> return stm unless dec {"decorated", stm, dec} -check_lua_string = (str, pos, right, left) -> +check_lua_string = (str, at_pos, right, left) -> #left == #right -- constructor for :name self assignments in table literals -self_assign = (name, pos) -> - {{"key_literal", name}, {"ref", name, [-1]: pos}} +self_assign = (name, at_pos) -> + {{"key_literal", name}, {"ref", name, [-1]: at_pos}} { :Indent, :Cut, :ensure, :extract_line, :mark, :pos, :flatten_or_mark, :is_assignable, :check_assignable, :format_assign, :format_single_assign, diff --git a/moonscript/transform/destructure.lua b/moonscript/transform/destructure.lua index 2ec46bef..06319696 100644 --- a/moonscript/transform/destructure.lua +++ b/moonscript/transform/destructure.lua @@ -1,7 +1,7 @@ -local ntype, mtype, build +local ntype, build do local _obj_0 = require("moonscript.types") - ntype, mtype, build = _obj_0.ntype, _obj_0.mtype, _obj_0.build + ntype, build = _obj_0.ntype, _obj_0.build end local NameProxy NameProxy = require("moonscript.transform.names").NameProxy @@ -163,8 +163,8 @@ split_assign = function(scope, assign) (function() local _accum_0 = { } local _len_0 = 1 - for i = start, stop do - _accum_0[_len_0] = names[i] + for j = start, stop do + _accum_0[_len_0] = names[j] _len_0 = _len_0 + 1 end return _accum_0 @@ -172,8 +172,8 @@ split_assign = function(scope, assign) (function() local _accum_0 = { } local _len_0 = 1 - for i = start, stop do - _accum_0[_len_0] = values[i] + for j = start, stop do + _accum_0[_len_0] = values[j] _len_0 = _len_0 + 1 end return _accum_0 diff --git a/moonscript/transform/destructure.moon b/moonscript/transform/destructure.moon index 5b22247a..39a7f868 100644 --- a/moonscript/transform/destructure.moon +++ b/moonscript/transform/destructure.moon @@ -1,5 +1,5 @@ -import ntype, mtype, build from require "moonscript.types" +import ntype, build from require "moonscript.types" import NameProxy from require "moonscript.transform.names" import insert from table import unpack from require "moonscript.util" @@ -100,10 +100,10 @@ split_assign = (scope, assign) -> stop = i - 1 insert g, { "assign" - for i=start,stop - names[i] - for i=start,stop - values[i] + for j=start,stop + names[j] + for j=start,stop + values[j] } insert g, build_assign scope, n, values[i] diff --git a/moonscript/transform/names.lua b/moonscript/transform/names.lua index f6af4c46..83ab8d6f 100644 --- a/moonscript/transform/names.lua +++ b/moonscript/transform/names.lua @@ -1,7 +1,5 @@ local build build = require("moonscript.types").build -local unpack -unpack = require("moonscript.util").unpack local LocalName do local _class_0 diff --git a/moonscript/transform/names.moon b/moonscript/transform/names.moon index 4af6aa0d..cdd6975c 100644 --- a/moonscript/transform/names.moon +++ b/moonscript/transform/names.moon @@ -1,6 +1,5 @@ import build from require "moonscript.types" -import unpack from require "moonscript.util" -- always declares as local class LocalName diff --git a/moonscript/transform/statement.lua b/moonscript/transform/statement.lua index 7303666a..9470edc3 100644 --- a/moonscript/transform/statement.lua +++ b/moonscript/transform/statement.lua @@ -409,9 +409,9 @@ return Transformer({ end, comprehension = function(self, node, action) local exp, clauses = unpack(node, 2) - action = action or function(exp) + action = action or function(arg) return { - exp + arg } end return construct_comprehension(action(exp), clauses) diff --git a/moonscript/transform/statement.moon b/moonscript/transform/statement.moon index f5cf3a54..60857710 100644 --- a/moonscript/transform/statement.moon +++ b/moonscript/transform/statement.moon @@ -249,7 +249,7 @@ Transformer { comprehension: (node, action) => exp, clauses = unpack node, 2 - action = action or (exp) -> {exp} + action = action or (arg) -> {arg} construct_comprehension action(exp), clauses do: (node, ret) => @@ -490,5 +490,5 @@ Transformer { } class: require "moonscript.transform.class" - + } diff --git a/moonscript/util.lua b/moonscript/util.lua index bde0d576..1c1a191f 100644 --- a/moonscript/util.lua +++ b/moonscript/util.lua @@ -75,7 +75,7 @@ split = function(str, delim) return _accum_0 end local dump -dump = function(what) +dump = function(object) local seen = { } local _dump _dump = function(what, depth) @@ -107,7 +107,7 @@ dump = function(what) return tostring(what) .. "\n" end end - return _dump(what) + return _dump(object) end local debug_posmap debug_posmap = function(posmap, moon_code, lua_code) diff --git a/moonscript/util.moon b/moonscript/util.moon index ea7c8a5b..5f6223fb 100644 --- a/moonscript/util.moon +++ b/moonscript/util.moon @@ -53,7 +53,7 @@ split = (str, delim) -> str ..= delim [m for m in str\gmatch("(.-)"..delim)] -dump = (what) -> +dump = (object) -> seen = {} _dump = (what, depth=0) -> t = type what @@ -74,7 +74,7 @@ dump = (what) -> else tostring(what).."\n" - _dump what + _dump object debug_posmap = (posmap, moon_code, lua_code) -> diff --git a/spec/cmd_spec.moon b/spec/cmd_spec.moon index 039973d5..2d19ed4d 100644 --- a/spec/cmd_spec.moon +++ b/spec/cmd_spec.moon @@ -89,7 +89,7 @@ describe "moonc", -> it "parses arguments", -> import parse_arguments from require "moonscript.cmd.args" - out, res = parse_arguments { + out = parse_arguments { "ga:p" print: "p" }, {"hello", "word", "-gap"} diff --git a/spec/comprehension_spec.moon b/spec/comprehension_spec.moon index d8823728..7a0be499 100644 --- a/spec/comprehension_spec.moon +++ b/spec/comprehension_spec.moon @@ -5,7 +5,6 @@ describe "comprehension", -> it "should double every number", -> input = {1,2,3,4,5,6} output_1 = [i * 2 for _, i in pairs input ] - output_2 = [i * 2 for i in *input ] assert.same output_1, {2,4,6,8,10,12} diff --git a/spec/coverage_output_handler.moon b/spec/coverage_output_handler.moon index 0043e462..e566eb7f 100644 --- a/spec/coverage_output_handler.moon +++ b/spec/coverage_output_handler.moon @@ -21,8 +21,6 @@ load_line_table = (chunk_name) -> busted = require "busted" handler = require("busted.outputHandlers.utfTerminal") options - local spec_name - coverage = require "moonscript.cmd.coverage" cov = coverage.CodeCoverage! diff --git a/spec/error_rewriting_spec.moon b/spec/error_rewriting_spec.moon index c3616116..de3eb434 100644 --- a/spec/error_rewriting_spec.moon +++ b/spec/error_rewriting_spec.moon @@ -1,13 +1,12 @@ -import unindent, with_dev from require "spec.helpers" +import unindent from require "spec.helpers" describe "moonscript.errors", -> - local moonscript, errors, util, to_lua + local moonscript, errors, to_lua -- with_dev -> moonscript = require "moonscript.base" errors = require "moonscript.errors" - util = require "moonscript.util" {:to_lua} = moonscript @@ -42,7 +41,7 @@ describe "moonscript.errors", -> print "cats" ]] - lua_code, posmap = assert to_lua moon_code + _, posmap = assert to_lua moon_code -- print util.debug_posmap(posmap, moon_code, lua_code) assert.same { 1, 23, 36, 21 }, posmap @@ -59,13 +58,13 @@ describe "moonscript.errors", -> print "two" ]] - lua_code, posmap = assert to_lua moon_code + _, posmap = assert to_lua moon_code -- print util.debug_posmap(posmap, moon_code, lua_code) assert.same {[1]: 1, [2]: 13, [7]: 13, [8]: 57}, posmap describe "error reporting", -> it "should compile bad code twice", -> - code, err = to_lua "{b=5}" + _, err = to_lua "{b=5}" assert.truthy err - code, err2 = to_lua "{b=5}" + _, err2 = to_lua "{b=5}" assert.same err, err2 diff --git a/spec/helpers.moon b/spec/helpers.moon index 45545b91..cb0e7d1d 100644 --- a/spec/helpers.moon +++ b/spec/helpers.moon @@ -13,7 +13,7 @@ in_dev = false with_dev = (fn) -> error "already in dev mode" if in_dev - -- a package loader that only looks in currect directory + -- a package loader that only looks in the current directory import make_loader from require "loadkit" loader = make_loader "lua", nil, "./?.lua" diff --git a/spec/import_spec.moon b/spec/import_spec.moon index bb1472cd..145b07e2 100644 --- a/spec/import_spec.moon +++ b/spec/import_spec.moon @@ -21,6 +21,6 @@ describe "import", -> { hello: "world", foo: "bar" } import hello, foo from source! - + assert.same count, 1 diff --git a/spec/lang_spec.moon b/spec/lang_spec.moon index b0fa1bab..83d1e544 100644 --- a/spec/lang_spec.moon +++ b/spec/lang_spec.moon @@ -2,8 +2,6 @@ lfs = require "lfs" import with_dev from require "spec.helpers" -pattern = ... - unpack = table.unpack or unpack options = { @@ -27,7 +25,7 @@ timings = {} gettime = nil pcall -> - require "socket" + socket = require "socket" gettime = socket.gettime gettime or= os.clock @@ -91,7 +89,7 @@ describe "input tests", -> with_dev -> parse = require "moonscript.parse" compile = require "moonscript.compile" - + for name in *inputs input = input_fname name it input .. " #input", -> diff --git a/spec/lint_spec.moon b/spec/lint_spec.moon new file mode 100644 index 00000000..72fac0f3 --- /dev/null +++ b/spec/lint_spec.moon @@ -0,0 +1,786 @@ +import with_dev from require "spec.helpers" + +describe 'lint', -> + local lint + + with_dev -> + lint = require "moonscript.lint.init" + + clean = (code) -> + initial_indent = code\match '^([ \t]*)%S' + code = code\gsub '\n\n', "\n#{initial_indent}\n" + lines = [l\gsub("^#{initial_indent}", '') for l in code\gmatch('[^\n]+')] + code = table.concat lines, '\n' + code = code\match '^%s*(.-)%s*$' + code .. '\n' + + do_lint = (code, opts) -> + inspections = assert lint.lint code, opts + res = {} + + for i in *inspections + {:line, :msg} = i + res[#res + 1] = :line, :msg + + res + + describe 'unused variables', -> + it 'detects unused variables', -> + code = 'used = 2\nfoo = 2\nused' + res = do_lint code, {} + assert.same { + {line: 2, msg: 'declared but unused - `foo`'} + }, res + + it 'handles multiple assignments', -> + code = 'a, b = 1, 2\na' + res = do_lint code, {} + assert.same { + {line: 1, msg: 'declared but unused - `b`'} + }, res + + it 'does not report variable used in a different scope', -> + code = clean [[ + a = 1 + -> + a + 1 + ]] + res = do_lint code, {} + assert.same {}, res + + it 'detects function scoped, unused variables', -> + code = clean [[ + x = -> a = 1 + x = -> a = 1 + x + ]] + res = do_lint code, {} + assert.same { + {line: 1, msg: 'declared but unused - `a`'} + {line: 2, msg: 'declared but unused - `a`'} + }, res + + it 'detects control flow scoped, unused variables', -> + code = clean [[ + if _G.foo + x = 2 + elseif _G.zed + x = 1 + else + x = 1 + unless _G.bar + x = 3 + x + ]] + res = do_lint code, {} + assert.same { + {line: 2, msg: 'declared but unused - `x`'} + {line: 4, msg: 'declared but unused - `x`'} + {line: 6, msg: 'declared but unused - `x`'} + {line: 8, msg: 'declared but unused - `x`'} + {line: 9, msg: 'accessing global - `x`'} + }, res + + it 'detects unused variables in with statements', -> + code = clean [[ + with _G.foo + x = 1 + with _G.bar + x = 2 + x = 3 + x + ]] + res = do_lint code, {} + assert.same { + {line: 2, msg: 'declared but unused - `x`'} + {line: 4, msg: 'declared but unused - `x`'} + }, res + + it 'detects while scoped unused variables', -> + code = clean [[ + while true + x = 1 + break + ]] + res = do_lint code, {} + assert.same { + {line: 2, msg: 'declared but unused - `x`'} + }, res + + it 'accounts for implicit returns', -> + code = clean [[ + x = 1 + -> + y = 1 + y + x + ]] + res = do_lint code, {} + assert.same {}, res + + it 'handles inline with variable assignment statements', -> + code = clean [[ + with foo = 2 + foo += 3 + ]] + res = do_lint code, {} + assert.same {}, res + + it 'detects unused function parameters if requested', -> + code = '(foo) -> 2' + res = do_lint code, report_params: true + assert.same { + {line: 1, msg: 'declared but unused - `foo`'} + }, res + + it 'detects usages in parameter lists', -> + code = clean [[ + x = 1 + (v = x)-> + v + ]] + res = do_lint code + assert.same {}, res + + it 'does not complain about varargs', -> + code = clean [[ + (...) -> + ... + ]] + res = do_lint code, {} + assert.same {}, res + + it 'respects a given whitelist_params', -> + code = clean '(x) -> 1' + res = do_lint code, { whitelist_params: {'x'} } + assert.same {}, res + + it 'respects a given whitelist_loop_variables', -> + code = clean 'for x in *{1,2}\n _G.other = 1' + res = do_lint code, { whitelist_loop_variables: {'x'} } + assert.same {}, res + + it 'does not complain about @variables in methods', -> + code = clean [[ + class Foo + new: (@bar) => + other: (@zed) => + + Foo + ]] + res = do_lint code + assert.same {}, res + + it 'detects unused class definitions', -> + code = clean [[ + class Foo extends _G.Bar + new: => + + {} + ]] + res = do_lint code, {} + assert.same { + {line: 1, msg: 'declared but unused - `Foo`'} + }, res + + it 'detects implicit returns of class definitions', -> + code = clean [[ + class Foo + new: => + ]] + res = do_lint code, {} + assert.same { + }, res + + it 'detects dotted assignment references', -> + code = clean [[ + (arg) -> + arg.foo = .zed + ]] + res = do_lint code + assert.same {}, res + + it 'handles local declarations', -> + code = clean [[ + local x, y + -> + x = 2 + y = 1 + y + x + ]] + res = do_lint code + assert.same {}, res + + it 'handles export declarations', -> + code = clean [[ + export foo + -> + foo = 2 + y = 1 + export zed = -> + y + 2 + ]] + res = do_lint code + assert.same {}, res + + it 'handles wildcard export declarations', -> + code = clean [[ + x = 1 + export * + y = 2 + ]] + res = do_lint code + assert.same { + {line: 1, msg: 'declared but unused - `x`'} + }, res + + it 'detects indexing references', -> + code = clean [[ + (foo) -> + _G[foo] = 2 + ]] + res = do_lint code + assert.same {}, res + + it 'detects unused imports', -> + code = clean [[ + import foo from _G.bar + import \func from _G.thing + ]] + res = do_lint code, {} + assert.same { + {line: 1, msg: 'declared but unused - `foo`'} + {line: 2, msg: 'declared but unused - `func`'} + }, res + + it 'detects usages in import source lists', -> + code = clean [[ + ffi = require 'ffi' + import C from ffi + C + ]] + res = do_lint code + assert.same {}, res + + it 'detects unused destructured variables', -> + code = clean [[ + {:foo} = _G.bar + {bar: other} = _G.zed + {frob} = {1,2} + {numbers: {first}} = _G.frob + {props: {color: my_col}} = _G.what + ]] + res = do_lint code, {} + assert.same { + {line: 1, msg: 'declared but unused - `foo`'} + {line: 2, msg: 'declared but unused - `other`'} + {line: 3, msg: 'declared but unused - `frob`'} + {line: 4, msg: 'declared but unused - `first`'} + {line: 5, msg: 'declared but unused - `my_col`'} + }, res + + code = '{:foo, :bar} = _G.bar' + res = do_lint code, {} + assert.equal 2, #res + + it 'detects unused variables in ordinary loops', -> + code = clean [[ + for foo = 1,10 + _G.other! + + for foo = 1,10 + _G.other foo + ]] + res = do_lint code, {} + assert.same { + {line: 1, msg: 'declared but unused - `foo`'} + }, res + + it 'detects unused variables in for each loops', -> + code = clean [[ + for foo in *{2, 3} + _G.other! + + ]] + res = do_lint code, {} + assert.same { + {line: 1, msg: 'declared but unused - `foo`'} + }, res + + it 'detects unused destructured variables in for each loops', -> + code = clean [[ + for {foo} in *{2, 3} + _G.other! + + for {:bar} in *{2, 3} + _G.other! + + for {bar: zed} in *{2, 3} + _G.other! + ]] + res = do_lint code + assert.same { + {line: 1, msg: 'declared but unused - `foo`'} + {line: 4, msg: 'declared but unused - `bar`'} + {line: 7, msg: 'declared but unused - `zed`'} + }, res + + it 'does not warn for used vars in decorated statements', -> + code = clean [[ + _G[a] = nil for a in *_G.list + ]] + res = do_lint code, {} + assert.same {}, res + + it 'detects variable usages correctly in comprehensions', -> + code = clean [[ + [x * 2 for x in *_G.foo] + ]] + res = do_lint code, {} + assert.same {}, res + + it 'detects variable usages correctly in for comprehensions', -> + code = clean [[ + [tostring(l) for l = 1, 100] + ]] + res = do_lint code + assert.same {}, res + + it 'detects variable usages correctly in comprehensions 2', -> + code = clean [[ + [name for name in pairs _G.foo] + ]] + res = do_lint code + assert.same {}, res + + it 'detects variable usages correctly in hash comprehensions', -> + code = clean [[ + {k, _G.foo[k] for k in *{1,2}} + ]] + res = do_lint code + assert.same {}, res + + describe 'undeclared access', -> + it 'detected undeclared accesses', -> + code = 'foo!' + res = do_lint code, {} + assert.same { + {line: 1, msg: 'accessing global - `foo`'} + }, res + + it 'detected undeclared accesses for chained expressions', -> + code = 'foo.x' + res = do_lint code, {} + assert.same { + {line: 1, msg: 'accessing global - `foo`'} + }, res + + it 'reports each undeclared usage separately', -> + code = clean [[ + x 1 + x 2 + ]] + res = do_lint code, {} + assert.same { + {line: 1, msg: 'accessing global - `x`'} + {line: 2, msg: 'accessing global - `x`'} + }, res + + it 'includes built-ins in the global whitelist', -> + code = clean [[ + x = tostring(_G.foo) + y = table.concat {}, '\n' + x + y + ]] + res = do_lint code + assert.same {}, res + + it 'allows access to self and super in methods', -> + code = clean [[ + class Foo + meth: => + self.bar! + super! + ]] + res = do_lint code + assert.same {}, res + + it 'allows access to self in methods and sub scopes thereof', -> + code = clean [[ + class Foo + meth: => + if true + self.bar! + ]] + res = do_lint code + assert.same {}, res + + it 'disallows access to self in functions', -> + code = clean [[ + -> + self.bar! + ]] + res = do_lint code + assert.same { + {line: 2, msg: 'accessing global - `self`'} + }, res + + it 'handles variabled assigned with statement modifiers correctly', -> + code = clean [[ + x = _G.foo if true + x + ]] + res = do_lint code + assert.same {}, res + + code = clean [[ + x = _G.foo unless false + x + ]] + res = do_lint code + assert.same {}, res + + it 'handles variabled assigned with statement modifiers correctly', -> + code = clean [[ + x or= _G.foo + y or= _G.bar\zed! + x + y + ]] + res = do_lint code + assert.same {}, res + + it 'handles variables assigned with destructuring correctly', -> + code = clean [[ + {foo, bar} = _G.zed + foo + bar + ]] + res = do_lint code + assert.same {}, res + + it 'detects class parent references', -> + code = clean [[ + import Base from _G + class I extends Base + ]] + res = do_lint code + assert.same {}, res + + it 'handles non-prefixed member access', -> + code = clean [[ + class Foo + bar: (@x = 'zed') => + x + ]] + res = do_lint code + assert.same {}, res + + it 'handles loop modified statements', -> + code = clean [[ + _G.foo[t] = true for t in pairs {} + t! for t in *{} + _G.foo += i for i = 1, 10 + ]] + res = do_lint code + assert.same {}, res + + it 'handles "with" statement assignments', -> + code = clean [[ + with x = 2 + .y + 2 + ]] + res = do_lint code + assert.same {}, res + + it 'handles while scoped unused variables', -> + code = clean [[ + while true + x = 1 + if x + break + x + ]] + res = do_lint code, {} + assert.same { + {line: 5, msg: 'accessing global - `x`'} + }, res + + describe 'format_inspections(inspections)', -> + it 'returns a string representation of inspections', -> + code = clean [[ + {:foo} = _G.bar + {bar: other} = _G.zed + ]] + inspections = assert lint.lint code, {} + assert.same clean([[ + line 1: declared but unused - `foo` + =================================== + > {:foo} = _G.bar + + line 2: declared but unused - `other` + ===================================== + > {bar: other} = _G.zed + ]]), lint.format_inspections(inspections) + + describe 'shadowing warnings', -> + it 'detects shadowing outer variables in for each', -> + code = clean [[ + x = 2 + for x in *{1,2} + _G.other x + x + ]] + res = do_lint code, {} + assert.same { + {line: 2, msg: 'shadowing outer variable - `x`'} + }, res + + it 'detects shadowing using local statements', -> + code = clean [[ + x = 2 + -> + local x + x = 2 + x * 2 + x + ]] + res = do_lint code, {} + assert.same { + {line: 3, msg: 'shadowing outer variable - `x`'} + }, res + + it 'understand lexical scoping', -> + code = clean [[ + for x in *{1,2} + _G.other x + x = 2 -- defined after previous declaration + x + ]] + res = do_lint code, {} + assert.same {}, res + + it 'rvalue declaration values generally does not shadow lvalues', -> + code = clean [[ + x = { -- assignment lvalue target + f: (x) -> -- this is part of the rvalue + } + x + ]] + res = do_lint code, {} + assert.same {}, res + + it 'implicitly local lvalue declarations are recognized (i.e. fndefs)', -> + code = clean [[ + f = (x) -> x + f(x + 1) + f + ]] + res = do_lint code, {} + assert.same {}, res + + it 'does not complain about foreach comprehension vars shadowing target', -> + code = clean [[ + for x in *[x for x in *_G.foo when x != 'bar' ] + x! + ]] + res = do_lint code, {} + assert.same {}, res + + it 'handles scope shadowing and unused variables correctly', -> + code = clean [[ + (a) -> + [ { a, b } for a, b in pairs {} ] + ]] + res = do_lint code, report_params: true + assert.same { + {line: 1, msg: 'declared but unused - `a`'}, + {line: 2, msg: 'shadowing outer variable - `a`'} + }, res + + it 'handles shadowing with decorated statements correctly', -> + code = clean [[ + x = 1 + (arg) -> x! for x in *arg + ]] + res = do_lint code, {} + assert.same { + {line: 1, msg: 'declared but unused - `x`'}, + {line: 2, msg: 'shadowing outer variable - `x`'} + }, res + + describe 'lint.config', -> + lfs = require 'lfs' + dir = require 'pl.dir' + local config + + with_dev -> + config = require "moonscript.lint.config" + + write_file = (path, contents) -> + f = assert io.open(path, 'wb') + f\write contents + assert f\close! + + describe 'config_for(file)', -> + local base_dir + + before_each -> + base_dir = os.tmpname! + assert(os.remove(base_dir)) if lfs.attributes(base_dir) + assert(lfs.mkdir(base_dir)) + + after_each -> dir.rmtree(base_dir) + + it 'returns the first available lint_config by moving up the path', -> + assert(lfs.mkdir("#{base_dir}/sub")) + ex_file = "#{base_dir}/sub/file.moon" + + in_dir_cfg = "#{base_dir}/sub/lint_config.lua" + write_file in_dir_cfg, '{}' + assert.equal in_dir_cfg, config.config_for(ex_file) + os.remove(in_dir_cfg) + + parent_dir_cfg = "#{base_dir}/lint_config.lua" + write_file parent_dir_cfg, '{}' + assert.equal parent_dir_cfg, config.config_for(ex_file) + + it 'supports and prefers moonscript config files if available', -> + assert(lfs.mkdir("#{base_dir}/sub")) + ex_file = "#{base_dir}/sub/file.moon" + + lua_cfg = "#{base_dir}/lint_config.lua" + moon_cfg = "#{base_dir}/lint_config.moon" + write_file lua_cfg, '{}' + write_file moon_cfg, '{}' + + assert.equal moon_cfg, config.config_for(ex_file) + os.remove(moon_cfg) + assert.equal lua_cfg, config.config_for(ex_file) + + describe 'load_config_from(config, file)', -> + sorted = (t) -> + table.sort t + t + + it 'loads the relevant settings for from ', -> + cfg = { + report_loop_variables: true + report_params: false + + whitelist_globals: { + ['.']: { 'foo' }, + test: { 'bar' } + other: { 'zed' } + } + whitelist_loop_variables: { + test: {'k'} + } + whitelist_params: { + ['.']: {'pipe'} + } + whitelist_unused: { + ['.']: {'general'} + } + whitelist_shadowing: { + ['.']: {'table'} + } + } + loaded = config.load_config_from(cfg, '/test/foo.moon') + assert.same { + 'bar', + 'foo' + }, sorted loaded.whitelist_globals + + assert.same { 'k' }, loaded.whitelist_loop_variables + assert.same { 'pipe' }, loaded.whitelist_params + assert.same { 'general' }, loaded.whitelist_unused + assert.same { 'table' }, loaded.whitelist_shadowing + assert.is_true loaded.report_loop_variables + assert.is_false loaded.report_params + + it 'loads as a file when passed as a string', -> + path = os.tmpname! + write_file path, [[ + return { + whitelist_globals = { + test = {'bar'} + } + } + ]] + assert.same { + whitelist_globals: { 'bar' } + }, config.load_config_from(path, '/test/foo.moon') + + describe 'evaluator(config)', -> + evaluator = config.evaluator + + describe 'allow_unused_param(p)', -> + it 'generally returns true', -> + assert.is_true evaluator({}).allow_unused_param('foo') + + it 'returns false if config.report_params is true', -> + assert.is_false evaluator(report_params: true).allow_unused_param('foo') + + it 'returns true if config.whitelist_params contains

', -> + whitelist_params = {'foo'} + assert.is_true evaluator(:whitelist_params).allow_unused_param('foo') + + it 'supports patterns', -> + whitelist_params = {'^a+'} + assert.is_true evaluator(:whitelist_params).allow_unused_param('aardwark') + + it 'defaults to white listings params starting with an underscore', -> + for sym in *{'_', '_foo', '_bar2'} + assert.is_true evaluator().allow_unused_param(sym) + + describe 'allow_unused_loop_variable(p)', -> + it 'generally returns false', -> + assert.is_false evaluator({}).allow_unused_loop_variable('foo') + + it 'returns true if config.report_loop_variables is false', -> + assert.is_true evaluator(report_loop_variables: false).allow_unused_loop_variable('foo') + + it 'returns true if config.whitelist_loop_variables contains

', -> + whitelist_loop_variables = {'foo'} + assert.is_true evaluator(:whitelist_loop_variables).allow_unused_loop_variable('foo') + + it 'supports patterns', -> + whitelist_loop_variables = {'^a+'} + assert.is_true evaluator(:whitelist_loop_variables).allow_unused_loop_variable('aardwark') + + it 'defaults to white listing params starting with an underscore', -> + for sym in *{'_', '_foo', '_bar2'} + assert.is_true evaluator().allow_unused_loop_variable(sym) + + it 'defaults to white listing the common names "i" and "j"', -> + for sym in *{'i', 'j'} + assert.is_true evaluator().allow_unused_loop_variable(sym) + + describe 'allow_global_access(p)', -> + it 'generally returns false', -> + assert.is_false evaluator({}).allow_global_access('foo') + + it 'returns true if config.whitelist_globals contains

', -> + whitelist_globals = {'foo'} + assert.is_true evaluator(:whitelist_globals).allow_global_access('foo') + + it 'supports patterns', -> + whitelist_globals = {'^a+'} + assert.is_true evaluator(:whitelist_globals).allow_global_access('aardwark') + + it 'always includes whitelisting of builtins', -> + for sym in *{'require', '_G', 'tostring'} + assert.is_true evaluator().allow_global_access(sym) + + whitelist_globals = {'foo'} + assert.is_true evaluator(:whitelist_globals).allow_global_access('table') + + describe 'allow_unused(p)', -> + it 'generally returns false', -> + assert.is_false evaluator({}).allow_unused('foo') + + it 'returns true if config.whitelist_unused contains

', -> + whitelist_unused = {'foo'} + assert.is_true evaluator(:whitelist_unused).allow_unused('foo') + + it 'supports patterns', -> + whitelist_unused = {'^a+'} + assert.is_true evaluator(:whitelist_unused).allow_unused('aardwark')