├── .gitignore ├── .sublimelinterrc ├── .travis.yml ├── LICENSE ├── README.md ├── linter.py ├── messages.json └── messages ├── 1.3.0.txt ├── 1.4.0.txt └── install.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /.sublimelinterrc: -------------------------------------------------------------------------------- 1 | { 2 | "@python": 3, 3 | "linters": { 4 | "flake8": { 5 | "max-line-length": 120 6 | }, 7 | "pep257": { 8 | "ignore": ["D202"] 9 | }, 10 | "pep8": { 11 | "max-line-length": 120 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.3" 4 | # command to install dependencies 5 | install: 6 | - pip install flake8 7 | - pip install pep257 8 | # command to run tests 9 | script: 10 | - flake8 *.py --max-line-length=120 --ignore=W391,W291 11 | - pep257 . --ignore=D202,D203 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy 2 | of this software and associated documentation files (the "Software"), to deal 3 | in the Software without restriction, including without limitation the rights 4 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 5 | copies of the Software, and to permit persons to whom the Software is 6 | furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in 9 | all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 17 | THE SOFTWARE. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NOTE: This repository is no longer maintained, as I've switched to a far-superior way of linting by using [JakeBecker's elixir-ls](https://github.com/JakeBecker/elixir-ls) and [tom564's LSP](https://github.com/tomv564/LSP). Be sure to check it out yourself. 2 | 3 | SublimeLinter-contrib-elixirc 4 | ========================== 5 | 6 | [![Build Status](https://travis-ci.org/smanolloff/SublimeLinter-contrib-elixirc.svg?branch=master)](https://travis-ci.org/smanolloff/SublimeLinter-contrib-elixirc) 7 | 8 | This linter plugin for [SublimeLinter][docs] provides an interface to check [elixir](http://elixir-lang.org) syntax using **elixirc**. It will be used with files that have the “elixir” syntax. 9 | 10 | ## Installation 11 | SublimeLinter 3 must be installed in order to use this plugin. If SublimeLinter 3 is not installed, please follow the instructions [here][installation]. 12 | 13 | ### Linter installation 14 | Before installing this plugin, you must ensure that `elixir` (>= 1.0) is installed on your system. For instructions on how to install elixir, please refer to the [elixir-lang docs](http://elixir-lang.org/install.html). 15 | 16 | ### Linter configuration 17 | In order for `elixir` to be executed by SublimeLinter, you must ensure that its path is available to SublimeLinter. Before going any further, please read and follow the steps in [“Finding a linter executable”](http://sublimelinter.readthedocs.org/en/latest/troubleshooting.html#finding-a-linter-executable) through “Validating your PATH” in the documentation. 18 | 19 | Once `elixir` is installed and configured, you can proceed to install the SublimeLinter-contrib-elixirc plugin if it is not yet installed. 20 | 21 | ### Plugin installation 22 | Please use [Package Control][pc] to install the linter plugin. This will ensure that the plugin will be updated when new versions are available. If you want to install from source so you can modify the source code, you probably know what you are doing so we won’t cover that here. 23 | 24 | To install via Package Control, do the following: 25 | 26 | 1. Within Sublime Text, bring up the [Command Palette][cmd] and type `install`. Among the commands you should see `Package Control: Install Package`. If that command is not highlighted, use the keyboard or mouse to select it. There will be a pause of a few seconds while Package Control fetches the list of available plugins. 27 | 28 | 1. When the plugin list appears, type `elixirc`. Among the entries you should see `SublimeLinter-contrib-elixirc`. If that entry is not highlighted, use the keyboard or mouse to select it. 29 | 30 | ## Settings 31 | For general information on how SublimeLinter works with settings, please see [Settings][settings]. For information on generic linter settings, please see [Linter Settings][linter-settings]. 32 | 33 | In addition to the standard SublimeLinter settings, SublimeLinter-contrib-elixirc provides its own settings. 34 | 35 | | Setting | Description | 36 | |:------------|:-------------------------------------------------| 37 | | pa | _(list)_ dirs for `-pa` option | 38 | | require | _(list)_ dirs/files to require | 39 | | mix_project | _(bool)_ use mix for linting | 40 | | chdir | _(string)_ run linter from the specified dir | 41 | | prepend | _(list)_ will be prepended to the linter command | 42 | | append | _(list)_ will be appended to the linter command | 43 | 44 | ### In a mix project: 45 | * set `chdir` to your mix project's root directory. 46 | * set `mix_project` to `true` 47 | 48 | #### Example: 49 | In your _.sublime-project_ file: 50 | ```JSON 51 | "SublimeLinter": { 52 | "linters": { 53 | "elixirc": { 54 | "mix_project": true, 55 | "chdir": "PROJECT_ROOT" 56 | } 57 | } 58 | } 59 | ``` 60 | 61 | Where: 62 | * `PROJECT_ROOT` is the path to the root your project (use `${project}` if your sublime project is saved there) 63 | 64 | Note: Currently, `exs` files within a mix project (e.g. ExUnit tests) are linted for syntax errors only. This is a known issue and will be resolved in a future version. 65 | 66 | If you also use an elixir version manager, set `prepend` as [per the example below](#example-2) 67 | 68 | 69 | ### Outside a mix project: 70 | * if a file uses macros, the beam output paths must be added to code path through `pa` 71 | * files (or directories) to require prior to linting must be added through `require`. They are required in the given order. Directories, if given, are traversed recursively and alphabetically. 72 | 73 | #### Example 74 | In your _.sublime-project_ file: 75 | ```JSON 76 | "SublimeLinter": { 77 | "linters": { 78 | "elixirc": { 79 | "pa": ["PROJECT_ROOT/_build/dev/lib/PROJECT_WITH_MACROS/ebin"], 80 | "require": ["PROJECT_ROOT/deps/DEP1"] 81 | } 82 | } 83 | } 84 | ``` 85 | 86 | Where: 87 | * `PROJECT_ROOT` is the path to the root of your project (use `${project}` if your sublime project is saved there) 88 | * `PROJECT_WITH_MACROS` is the project name which contains the macros. List all projects in `pa` 89 | * `DEP1` is a directory with files to require. If needed, list specific files first. 90 | 91 | ### Command customization 92 | The `prepend` and `append` options allow you to modify the executed command. 93 | 94 | A typical use-case for this would be if you use elixir version managers (e.g. [kiex](https://github.com/taylor/kiex)), which alter certain environment variables when switching between different versions of elixir. 95 | 96 | Since sublime is *very* limited in terms of user-configurable environment variables, the problem could be solved with the `env` command on any typical UNIX-based OS. 97 | 98 | #### Example 99 | 100 | ```JSON 101 | "SublimeLinter": { 102 | "linters": { 103 | "elixirc": { 104 | "chdir": "/Users/foo/projects/myapp", 105 | "mix_project": true, 106 | "prepend": ["/usr/bin/env", "MIX_ARCHIVES=/Users/foo/.kiex/mix/archives/elixir-1.3.3"] 107 | } 108 | } 109 | } 110 | ``` 111 | 112 | ## Contributing 113 | If you would like to contribute enhancements or fixes, please do the following: 114 | 115 | 1. Fork the plugin repository. 116 | 1. Hack on a separate topic branch created from the latest `master`. 117 | 1. Commit and push the topic branch. 118 | 1. Make a pull request. 119 | 1. Be patient. ;-) 120 | 121 | Please note that modifications should follow these coding guidelines: 122 | 123 | - Indent is 4 spaces. 124 | - Code should pass flake8 and pep257 linters. 125 | - Vertical whitespace helps readability, don’t be afraid to use it. 126 | - Please use descriptive variable names, no abbreviations unless they are very well known. 127 | 128 | Thank you for helping out! 129 | 130 | [docs]: http://sublimelinter.readthedocs.org 131 | [installation]: http://sublimelinter.readthedocs.org/en/latest/installation.html 132 | [locating-executables]: http://sublimelinter.readthedocs.org/en/latest/usage.html#how-linter-executables-are-located 133 | [pc]: https://sublime.wbond.net/installation 134 | [cmd]: http://docs.sublimetext.info/en/sublime-text-3/extensibility/command_palette.html 135 | [settings]: http://sublimelinter.readthedocs.org/en/latest/settings.html 136 | [linter-settings]: http://sublimelinter.readthedocs.org/en/latest/linter_settings.html 137 | [inline-settings]: http://sublimelinter.readthedocs.org/en/latest/settings.html#inline-settings 138 | -------------------------------------------------------------------------------- /linter.py: -------------------------------------------------------------------------------- 1 | """This module exports the elixirc plugin class.""" 2 | 3 | import tempfile 4 | import os 5 | import re 6 | from SublimeLinter.lint import Linter, persist 7 | 8 | 9 | class Elixirc(Linter): 10 | """ 11 | Provides an interface to elixirc. 12 | 13 | Error formats: 14 | 15 | 1) Error type 1: 16 | |== Compilation error on file {filename} == 17 | |** ({error_name}) {filename}:{line}: {message} 18 | | ... 19 | 20 | 2.1) Error type 2 -- source file first in trace: 21 | |== Compilation error on file {filename} == 22 | |** ({error_name}) {message} 23 | | {filename}:{line} 24 | | ... 25 | 26 | 2.2) Error type 2.2 -- lib files first in trace: 27 | |== Compilation error on file {filename} == 28 | |** ({error_name}) {message} 29 | | (libname) {other_filename}:{line} 30 | | ... 31 | | {filename}:{line} 32 | | ... 33 | 34 | 2.3) Error type 2.3 -- function names first in trace: 35 | |== Compilation error on file {filename} == 36 | |** ({error_name}) {message} 37 | | {function_name}() 38 | | ... 39 | | {filename}:{line} 40 | | ... 41 | 42 | 2.4) Error type 2.4 -- irrelevant files first in trace: 43 | |== Compilation error on file {filename} == 44 | |** ({error_name}) {message} 45 | | {filename_2}:{line_2} 46 | | {filename_3}:{line_3} 47 | | ... 48 | | {filename}:{line} 49 | | ... 50 | 51 | 3) Error type 3: 52 | |** ({error_name}) {filename}:{line}: {message} 53 | |...... 54 | 55 | Warning formats: 56 | 57 | 1) Warning type 1: 58 | |{filename}:{line}: warning: {message} 59 | 60 | 2) Warning type 2: 61 | |warning: {message} 62 | | {filename}:{line} 63 | 64 | In order to cover all cases we need a complex regex. 65 | Since a single regex does *not* allow to have several 66 | groups with the same name, we introduce custom group 67 | names. 68 | The group names are then transformed back to the ones 69 | expected by the Linter. This is done by overriding 70 | the split_match method. 71 | 72 | 73 | Examples (mix project of a Phoenix app): 74 | 1) 75 | |== Compilation error on file web/router.ex == 76 | |** (CompileError) web/router.ex:19: undefined function get/2 77 | | ... 78 | 79 | 2.1) #todo 80 | 81 | 2.2) Insert a "resources :users, UserController" line in router.ex 82 | |== Compilation error on file web/router.ex == 83 | |** (FunctionClauseError) no function clause matching in Phoenix.Router.Resource.build/3 84 | | (phoenix) lib/phoenix/router/resource.ex:30: Phoenix.Router.Resource.build(:users, UserController, []) 85 | | web/router.ex:20: (module) 86 | | ... 87 | 88 | 2.3) Modify line 2 to "use MyApp.Web, :dasdsadasda" in router.ex 89 | |== Compilation error on file web/controllers/page_controller.ex == 90 | |** (UndefinedFunctionError) undefined function: MyApp.Web.controllers/0 91 | | MyApp.Web.controllers() 92 | | expanding macro: MyApp.Web.__using__/1 93 | | web/controllers/page_controller.ex:2: MyApp.PageController (module) 94 | | ... 95 | 96 | 2.4) Define an virtual attribute with the same name as an existing association 97 | |== Compilation error on file web/models/user.ex == 98 | |** (ArgumentError) field/association :roles is already set on schema 99 | | lib/ecto/schema.ex:1196: Ecto.Schema.put_struct_field/3 100 | | lib/ecto/schema.ex:1176: Ecto.Schema.association/5 101 | | web/models/user.ex:20: (module) 102 | | (stdlib) erl_eval.erl:669: :erl_eval.do_apply/6 103 | | (elixir) lib/kernel/parallel_compiler.ex:97: anonymous fn/4 in Kernel.ParallelCompiler.spawn_compilers/8 104 | 105 | 3) #todo 106 | 107 | """ 108 | 109 | syntax = ("elixir") 110 | tempfile_suffix = "-" 111 | 112 | regex_parts = ( 113 | # Error type 1 114 | r"== Compilation error on file (?P.+) ==\n" 115 | r"\*\* \(.+?\) (?P=e_file1):(?P\d+): (?P.+)", 116 | 117 | # Error type 2 118 | r"== Compilation error on file (?P.+) ==\n" 119 | r"\*\* \(.+?\) (?P.+)\n" 120 | r"(.+\n)*?" 121 | r" (?P=e_file2):(?P\d+)", 122 | 123 | # Error type 3 124 | r"\*\* \(.+?\) (?P.+):(?P\d+): (?P.+)", 125 | 126 | # Warning type 1 127 | r"(?P.+):(?P\d+): warning: (?P.+)", 128 | 129 | # Warning type 2 130 | r"warning: (?P.+)\n" 131 | r" (?P.+):(?P\d+)" 132 | ) 133 | 134 | regex = "|".join([r"^(?:%s)" % x for x in regex_parts]) 135 | 136 | dummy_regex = re.compile( 137 | r"(?P.+):" 138 | r"(?P\d+):" 139 | r"(?:(?Perror)|(?Pwarning)):" 140 | r"(?P.+)", 141 | re.UNICODE 142 | ) 143 | 144 | defaults = { 145 | "include_dirs": [], 146 | "pa": [], 147 | "mix": False 148 | } 149 | 150 | # 151 | # Make elixir 'lint' itself by at least checking the syntax 152 | # (see https://groups.google.com/forum/#!msg/elixir-lang-talk/B29noPHvQ-8/9JvSGPop7n0J) 153 | # 154 | exs_script = ''' 155 | case Code.string_to_quoted(System.argv |> Enum.fetch!(0) |> File.read!) do 156 | {:ok, _} -> :ok 157 | {:error, {l, msg1, msg2}} -> IO.puts("** (...) %s:#{l}: #{msg1}#{msg2}") 158 | end 159 | ''' 160 | 161 | multiline = True 162 | executable = "elixir" 163 | 164 | def cmd(self): 165 | """Convert the linter options to command arguments.""" 166 | settings = self.get_view_settings() 167 | require = settings.get('require', []) 168 | paths = settings.get('pa', []) 169 | mix = settings.get('mix_project', None) 170 | prepend = settings.get('prepend', []) 171 | append = settings.get('append', []) 172 | 173 | command = prepend + [self.executable_path] + append 174 | 175 | if self.filename.endswith('.exs'): 176 | command.extend(self.exs_args()) 177 | elif mix: 178 | command.extend(self.mix_args()) 179 | else: 180 | command.extend(self.regular_args(paths, require)) 181 | 182 | return command 183 | 184 | def exs_args(self): 185 | """Build the argument list when linting an EXS file.""" 186 | return [ 187 | '-e', 188 | self.exs_script % (self.filename), 189 | self.filename 190 | ] 191 | 192 | def regular_args(self, paths, require): 193 | """ 194 | Build the argument list when mix is not configured. 195 | 196 | * set the compiler outpur into a tmpdir 197 | * from the `require` and `pa` config values, build 198 | the list of `-pr` and `-pa` command arguments 199 | 200 | """ 201 | args = [ 202 | '+elixirc', 203 | '-o', os.path.join(tempfile.gettempdir(), 'SublimeLinter3'), 204 | '--warnings-as-errors', 205 | '--ignore-module-conflict' 206 | ] 207 | 208 | for p in paths: 209 | args.extend(["-pa", p]) 210 | 211 | for i in require: 212 | if os.path.isdir(i): 213 | args.extend(["-pr", "%s/**/*.ex" % i]) 214 | else: 215 | args.extend(["-pr", i]) 216 | 217 | args.extend([self.filename]) 218 | 219 | return args 220 | 221 | def mix_args(self): 222 | """With mix, we just need to invoke its compile task.""" 223 | # TODO: this does not work for .exs files 224 | # (e.g. ExUnit test files) -- i haven't found 225 | # out how to lint them with mix 226 | args = [ 227 | '-S', 'mix', 'compile', 228 | '--warnings-as-errors', 229 | '--ignore-module-conflict' 230 | ] 231 | 232 | return args 233 | 234 | def split_match(self, match): 235 | """ 236 | Pre-process the matchObject before passing it upstream. 237 | 238 | Several reasons for this: 239 | * unrelated library files can throw errors, and 240 | we only want errors from the linted file. 241 | * our regex contains more than just the basic 242 | capture groups (filename, line, message, etc.) 243 | but we still need to pass a match object that 244 | contains the above groups upstream. 245 | * Line is not reported for some macro errors 246 | * etc.. 247 | 248 | """ 249 | dummy_match = None 250 | 251 | if match: 252 | captures = match.groupdict() 253 | dummy_string = self.build_dummy_string(captures) 254 | dummy_match = re.match(self.dummy_regex, dummy_string) 255 | 256 | if dummy_match: 257 | filename = os.path.join(self.chdir, dummy_match.group('filename')) 258 | 259 | persist.debug("Linted file: %s" % self.filename) 260 | persist.debug("Error source: %s" % filename) 261 | 262 | if self.filename != filename: 263 | persist.debug( 264 | "Ignore error from %s (linted file: %s)" % 265 | (filename, self.filename) 266 | ) 267 | 268 | dummy_match = None 269 | 270 | return super().split_match(dummy_match) 271 | 272 | def build_dummy_string(self, captures): 273 | """ 274 | Build a string to be matched against self.dummy_regex. 275 | 276 | It is used to ensure that a matchObject with the 277 | appropriate group names is passed upstream. 278 | 279 | Returns a string with the following format: 280 | {filename}:{line}:{error_type}:{message} 281 | 282 | """ 283 | if captures['e_file1'] is not None: 284 | persist.debug('Error type 1') 285 | dummy_str = '%s:%s:%s:%s' % ( 286 | captures['e_file1'], 287 | captures['e_line1'], 288 | 'error', 289 | captures['e_msg1'] 290 | ) 291 | elif captures['e_file2'] is not None: 292 | persist.debug('Error type 2') 293 | dummy_str = "%s:%s:%s:%s" % ( 294 | captures['e_file2'], 295 | captures['e_line2'], 296 | 'error', 297 | captures['e_msg2'] 298 | ) 299 | elif captures['e_file3'] is not None: 300 | persist.debug('Error type 3') 301 | dummy_str = "%s:%s:%s:%s" % ( 302 | captures['e_file3'], 303 | captures['e_line3'], 304 | 'error', 305 | captures['e_msg3'] 306 | ) 307 | elif captures['w_file1'] is not None: 308 | persist.debug('Warning type 1') 309 | dummy_str = "%s:%s:%s:%s" % ( 310 | captures['w_file1'], 311 | captures['w_line1'], 312 | 'warning', 313 | captures['w_msg1'] 314 | ) 315 | elif captures['w_file2'] is not None: 316 | persist.debug('Warning type 2') 317 | dummy_str = "%s:%s:%s:%s" % ( 318 | captures['w_file2'], 319 | captures['w_line2'], 320 | 'warning', 321 | captures['w_msg2'] 322 | ) 323 | else: 324 | persist.debug('No match') 325 | dummy_str = "" 326 | 327 | persist.debug("Dummy string: %s" % dummy_str) 328 | return dummy_str 329 | -------------------------------------------------------------------------------- /messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "install": "messages/install.txt", 3 | "1.3.0": "messages/1.3.0.txt", 4 | "1.4.0": "messages/1.4.0.txt" 5 | } 6 | -------------------------------------------------------------------------------- /messages/1.3.0.txt: -------------------------------------------------------------------------------- 1 | Command customization 2 | --------------------- 3 | 4 | Now you can modify the executed command with the `prepend` and `append` options. 5 | Both options expect a list of strings. 6 | 7 | A typical use-case for this would be if you use elixir version managers (e.g. kiex), 8 | which alter certain environment variables when switching between different versions of elixir. 9 | 10 | Since sublime is *very* limited in terms of user-configurable environment variables, 11 | the problem could be solved with the `env` command on any typical UNIX-based OS. 12 | 13 | #### Example 14 | 15 | "SublimeLinter": { 16 | "linters": { 17 | "elixirc": { 18 | "chdir": "/Users/foo/projects/myapp", 19 | "mix_project": true, 20 | "prepend": ["/usr/bin/env", "MIX_ARCHIVES=/Users/foo/.kiex/mix/archives/elixir-1.3.3"] 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /messages/1.4.0.txt: -------------------------------------------------------------------------------- 1 | EXS file support 2 | ---------------- 3 | 4 | As of this version, .exs files within a mix project will also be linted, but in a very limited manner 5 | (see https://groups.google.com/forum/#!msg/elixir-lang-talk/B29noPHvQ-8/9JvSGPop7n0J) 6 | 7 | Basically, it will at least detect syntactic, but not semantic errors. 8 | -------------------------------------------------------------------------------- /messages/install.txt: -------------------------------------------------------------------------------- 1 | SublimeLinter-contrib-elixirc 2 | ------------------------------- 3 | This linter plugin for SublimeLinter provides an interface to elixirc. 4 | 5 | ** IMPORTANT! ** 6 | 7 | Before this plugin will activate, you *must* 8 | follow the installation instructions here: 9 | 10 | https://github.com/smanolloff/SublimeLinter-contrib-elixirc 11 | --------------------------------------------------------------------------------