├── Commands ├── Ensure Trailing Newline.tmCommand ├── LintStripEnsure on Save.tmCommand ├── Linter.tmCommand └── Strip Trailing Whitespace.tmCommand ├── LICENSE.txt ├── README.md ├── Support └── lib │ ├── hedge_words.rb │ ├── languages │ ├── bash.rb │ ├── json.rb │ ├── markdown.rb │ └── ruby.rb │ └── linter.rb └── info.plist /Commands/Ensure Trailing Newline.tmCommand: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | beforeRunningCommand 6 | saveActiveFile 7 | command 8 | #!/usr/bin/env ruby 9 | require "#{ENV["TM_BUNDLE_SUPPORT"]}/lib/linter" 10 | Linter.ensure_trailing_newline! 11 | 12 | input 13 | document 14 | inputFormat 15 | text 16 | name 17 | Ensure Trailing Newline 18 | outputCaret 19 | interpolateByLine 20 | outputFormat 21 | text 22 | outputLocation 23 | discard 24 | uuid 25 | B6CC0FDC-0705-4435-AAFA-BC45A8E9A0E7 26 | version 27 | 2 28 | 29 | 30 | -------------------------------------------------------------------------------- /Commands/LintStripEnsure on Save.tmCommand: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | beforeRunningCommand 6 | saveActiveFile 7 | command 8 | #!/usr/bin/env ruby 9 | require "#{ENV["TM_BUNDLE_SUPPORT"]}/lib/linter" 10 | Linter.lint_strip_ensure_on_save 11 | input 12 | document 13 | inputFormat 14 | text 15 | keyEquivalent 16 | @s 17 | name 18 | Lint/Strip/Ensure on Save 19 | outputCaret 20 | interpolateByLine 21 | outputFormat 22 | html 23 | outputLocation 24 | newWindow 25 | scope 26 | bundle.linter.lint-on-save, bundle.linter.strip-whitespace-on-save, bundle.linter.ensure-newline-on-save 27 | uuid 28 | 483D338D-EC9F-4777-92C0-DFCBE02BD3A0 29 | version 30 | 2 31 | 32 | 33 | -------------------------------------------------------------------------------- /Commands/Linter.tmCommand: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | beforeRunningCommand 6 | saveActiveFile 7 | command 8 | #!/usr/bin/env ruby 9 | require "#{ENV["TM_BUNDLE_SUPPORT"]}/lib/linter" 10 | Linter.lint 11 | input 12 | document 13 | inputFormat 14 | text 15 | name 16 | Lint Document 17 | outputCaret 18 | interpolateByLine 19 | outputFormat 20 | html 21 | outputLocation 22 | newWindow 23 | scope 24 | source.shell, source.json, text.html.markdown, source.ruby 25 | uuid 26 | 26A8FF38-100D-4E72-9ADC-1EF964095C44 27 | version 28 | 2 29 | 30 | 31 | -------------------------------------------------------------------------------- /Commands/Strip Trailing Whitespace.tmCommand: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | beforeRunningCommand 6 | saveActiveFile 7 | command 8 | #!/usr/bin/env ruby 9 | require "#{ENV["TM_BUNDLE_SUPPORT"]}/lib/linter" 10 | Linter.strip_trailing_whitespace! 11 | 12 | input 13 | document 14 | inputFormat 15 | text 16 | name 17 | Strip Trailing Whitespace 18 | outputCaret 19 | interpolateByLine 20 | outputFormat 21 | text 22 | outputLocation 23 | discard 24 | uuid 25 | 5983C2DD-EB3D-4282-AF06-DC0CC94DD9D0 26 | version 27 | 2 28 | 29 | 30 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2015 by Mike McQuaid 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Linter.tmbundle 2 | Provides Linting functionality to TextMate to make it easier to write better quality code. 3 | 4 | ## Features 5 | - Linting on request of Bash, JSON, Markdown and Ruby files 6 | - Strips trailing whitespace on request from the end of lines (except after Ruby `__END__` blocks) 7 | - Adds trailing newlines on request to the end of files missing them 8 | - If set in `.tm_properties`'s [`scopeProperties`](https://gist.github.com/dvessel/1478685#other) (space separated, set either globally or for a given language like `[ source.ruby ]`): 9 | - `bundle.linter.lint-on-save` automatically runs Linter on save for supported file types 10 | - `bundle.linter.strip-whitespace-on-save` automatically strips trailing whitespace on save 11 | - `bundle.linter.ensure-newline-on-save` automatically adds a training newline on save 12 | - `bundle.linter.fix-on-save` automatically fixes lints if possible (e.g. with RuboCop) 13 | 14 | ## Linters 15 | ### Bash 16 | - `bash -n` (found in `LINTER_BASH`, `BASH` or `PATH` environment variables) 17 | - [`shellcheck`](https://www.shellcheck.net) (`brew install shellcheck`, found in `LINTER_SHELLCHECK`, `SHELLCHECK` or `PATH` environment variables) 18 | 19 | ### JSON 20 | - `JSON.parse` in Ruby (found in `LINTER_RUBY`, `RUBY` or `PATH` environment variables) 21 | - [`jsonlint`](https://jsonlint.com) (`brew install jsonlint`, found in `LINTER_JSONLINT`, `JSONLINT` or `PATH` environment variables) 22 | 23 | ### Markdown 24 | - [list of hedge words](https://github.com/MikeMcQuaid/Linter.tmbundle/blob/master/Support/lib/hedge_words.rb) 25 | - [`alex`](https://github.com/wooorm/alex) (`brew install alex`, found in `LINTER_ALEX`, `ALEX` or `PATH` environment variables) 26 | - [`write-good`](https://github.com/wooorm/alex) (`brew install write-good`, found in `LINTER_ALEX`, `ALEX` or `PATH` environment variables) 27 | 28 | ### Ruby 29 | - `ruby -wc` (found in `LINTER_RUBY`, `RUBY` or `PATH` environment variables) 30 | - [`rubocop`](http://rubocop.readthedocs.io/en/latest/) (`gem install --user rubocop`, found in `LINTER_RUBOCOP`, `RUBOCOP` or `PATH` environment variables) 31 | 32 | ## Installation 33 | 34 | ```bash 35 | mkdir -p ~/Library/Application\ Support/TextMate/Bundles 36 | cd ~/Library/Application\ Support/TextMate/Bundles 37 | git clone https://github.com/MikeMcQuaid/Linter.tmbundle 38 | ``` 39 | 40 | ## Status 41 | The above features work for my day-to-day use. 42 | 43 | Tested using TextMate 2. May work in TextMate 1 or Sublime Text; I've no idea. 44 | 45 | [Patches welcome](https://github.com/MikeMcQuaid/Linter.tmbundle/pulls). 46 | 47 | ## Maintainers 48 | [@MikeMcQuaid](https://github.com/MikeMcQuaid) 49 | 50 | ## License 51 | Linter.tmbundle is under the [MIT License](http://en.wikipedia.org/wiki/MIT_License). The full license text is 52 | available in 53 | [LICENSE.txt](https://github.com/MikeMcQuaid/Linter.tmbundle/blob/master/LICENSE.txt). 54 | -------------------------------------------------------------------------------- /Support/lib/hedge_words.rb: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2014 lexicalunit@lexicalunit.com 3 | # https://github.com/lexicalunit/linter-just-say-no/blob/master/resources/hedges.cson 4 | HEDGE_WORDS = [ 5 | "a bit", 6 | "a little", 7 | "a tad", 8 | "a touch", 9 | "almost", 10 | "apologize", 11 | "apparently", 12 | "appears", 13 | "around", 14 | "basically", 15 | "bother", 16 | "could", 17 | "does that make sense", 18 | "effectively", 19 | "evidently", 20 | "fairly", 21 | "generally", 22 | "hardly", 23 | "hopefully", 24 | "I am about", 25 | "I think", 26 | "I will try", 27 | "I'll try", 28 | "I'm about", 29 | "I'm just trying", 30 | "I'm no expert", 31 | "I'm not an expert", 32 | "I'm trying", 33 | "in general", 34 | "just about", 35 | "just", 36 | "kind of", 37 | "largely", 38 | "likely", 39 | "mainly", 40 | "may", 41 | "maybe", 42 | "might", 43 | "more or less", 44 | "mostly", 45 | "nearly", 46 | "overall", 47 | "partially", 48 | "partly", 49 | "perhaps", 50 | "possibly", 51 | "presumably", 52 | "pretty", 53 | "probably", 54 | "quite clearly", 55 | "quite", 56 | "rather", 57 | "really quite", 58 | "really", 59 | "seem", 60 | "seemed", 61 | "seems", 62 | "some", 63 | "sometimes", 64 | "somewhat", 65 | "sorry", 66 | "sort of", 67 | "suppose", 68 | "supposedly", 69 | "think", 70 | ].freeze 71 | -------------------------------------------------------------------------------- /Support/lib/languages/bash.rb: -------------------------------------------------------------------------------- 1 | module Linter 2 | module_function 3 | 4 | def bash 5 | settings = [bash_n] 6 | settings << shellcheck if which("shellcheck") 7 | settings 8 | end 9 | 10 | def shellcheck 11 | { 12 | name: "ShellCheck", 13 | version: `'#{which("shellcheck")}' --version`.lines[1].to_s.gsub("version: ", ""), 14 | output_command: "'#{which("shellcheck")}' --shell=bash --format=gcc", 15 | line_column_match: /#{filepath}:(\d+):(\d+): /, 16 | extra_gsubs: { 17 | /(\[(SC\d+)\])/ => 18 | '\1', 19 | }, 20 | } 21 | end 22 | 23 | def bash_n 24 | /(?\d+\.\d+\.\d+)/ =~ `#{which("bash")} --version`.lines.first 25 | { 26 | name: "Bash", 27 | version: version, 28 | output_command: "'#{which("bash")}' -n", 29 | line_match: /bash: line (\d+): /, 30 | } 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /Support/lib/languages/json.rb: -------------------------------------------------------------------------------- 1 | module Linter 2 | module_function 3 | 4 | def json 5 | if which("jsonlint") 6 | jsonlint 7 | else 8 | ruby_json 9 | end 10 | end 11 | 12 | def ruby_json 13 | /(?\d+\.\d+\.\d+(p\d+)?)/ =~ `#{which("ruby")} --version` 14 | { 15 | name: "Ruby (JSON module)", 16 | version: version, 17 | output_command: "'#{which("ruby")}' -rjson -e'begin JSON.parse(IO.read(ARGV.first)); rescue => e; puts e; end' test.json", 18 | } 19 | end 20 | 21 | def jsonlint 22 | { 23 | name: "JSONLint", 24 | version: `'#{which("jsonlint")}' --version`, 25 | output_command: "'#{which("jsonlint")}' --compact --quiet", 26 | line_column_match: /#{filepath}: line (\d+), col (\d+), /, 27 | } 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /Support/lib/languages/markdown.rb: -------------------------------------------------------------------------------- 1 | require "hedge_words" 2 | 3 | module Linter 4 | module_function 5 | 6 | def markdown 7 | settings = [hedge_words] 8 | settings << alex if which("alex") 9 | settings << write_good if which("write-good") 10 | settings 11 | end 12 | 13 | def alex 14 | { 15 | name: "Alex", 16 | version: `'#{which("alex")}' --version`, 17 | output_command: "'#{which("alex")}' '#{filepath}' --why", 18 | line_column_match: /(\d+):(\d+)-(\d+):(\d+) /, 19 | extra_gsubs: { 20 | /^\s+/ => "", 21 | /⚠ \d+ warnings?/ => "", 22 | /warning\s+/ => "", 23 | %r{^(\.\./?)*#{filepath}} => "", 24 | /^#{filename}$/ => "", 25 | ": no issues found" => "", 26 | }, 27 | } 28 | end 29 | 30 | def write_good 31 | { 32 | name: "Write Good", 33 | version: `'#{which("write-good")}' --version`.gsub("write-good version ", " "), 34 | output_command: "'#{which("write-good")}'", 35 | line_column_match: /on line (\d+) at column (\d+)/, 36 | extra_gsubs: { 37 | "In #{filepath}" => "", 38 | /^=+$/ => " ", 39 | /^-+$/ => "\n ", 40 | }, 41 | } 42 | end 43 | 44 | def self.hedge_words 45 | matches = [] 46 | File.read(filepath).to_s.lines.each_with_index do |line, line_number| 47 | HEDGE_WORDS.each do |word| 48 | offset = 0 49 | while (column = line.index(/(^|[^a-z])#{word}([^a-z]|$)/i, offset)) 50 | matches << "L#{line_number + 1}:C#{column + 1} warning: used hedge word '#{word}'" 51 | offset = column + 1 52 | end 53 | end 54 | end 55 | { 56 | name: "hedge words list", 57 | output: matches.join("\n"), 58 | line_column_match: /L(\d+):C(\d+) /, 59 | } 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /Support/lib/languages/ruby.rb: -------------------------------------------------------------------------------- 1 | module Linter 2 | module_function 3 | 4 | def ruby 5 | settings = [ruby_wc] 6 | settings << rubocop if which("rubocop") 7 | settings 8 | end 9 | 10 | def ruby_wc 11 | /(?\d+\.\d+\.\d+(p\d+)?)/ =~ `#{which("ruby")} --version` 12 | { 13 | name: "Ruby", 14 | version: version, 15 | output_command: "'#{which("ruby")}' -wc", 16 | line_match: /#{filepath}:(\d+):/, 17 | extra_gsubs: { 18 | "Syntax OK" => "", 19 | }, 20 | } 21 | end 22 | 23 | def rubocop 24 | if setting?(:fix_on_save) && !filepath.include?("/vendor/ruby/gems/") 25 | fix = " --auto-correct" 26 | end 27 | rubocop_type_regex = %r{([WC]:)( \[Corrected\])? (\w+)/(\w+)} 28 | rubocop_docs_lambda = lambda do |match| 29 | rubocop_type_regex =~ match 30 | _, type, corrected, category, check = Regexp.last_match.to_a 31 | category_down = category.downcase 32 | check_down = check.downcase 33 | url = "https://rubocop.readthedocs.io/en/latest/cops_#{category_down}/##{category_down}#{check_down}" 34 | href = "javascript:TextMate.system('open #{url}', null);" 35 | "#{type}#{corrected} #{category}/#{check}" 36 | end 37 | { 38 | name: "RuboCop", 39 | version: `'#{which("rubocop")}' --version`, 40 | output_command: "'#{which("rubocop")}' --format=emacs --display-cop-names#{fix}", 41 | line_column_match: /#{filepath}:(\d+):(\d+): /, 42 | extra_gsubs: { 43 | rubocop_type_regex => rubocop_docs_lambda, 44 | "C: " => "convention: ", 45 | "W: " => "warning: ", 46 | }, 47 | } 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /Support/lib/linter.rb: -------------------------------------------------------------------------------- 1 | require "#{ENV["TM_SUPPORT_PATH"]}/lib/textmate" 2 | ENV["PATH"] += ":#{ENV["TM_BUNDLE_SUPPORT"]}/bin" 3 | $LOAD_PATH << "#{ENV["TM_BUNDLE_SUPPORT"]}/lib" 4 | 5 | module Linter 6 | module_function 7 | 8 | def tm_scope 9 | ENV["TM_SCOPE"].to_s 10 | end 11 | 12 | def tm_scopes 13 | tm_scope.split(" ") 14 | end 15 | 16 | def setting?(key) 17 | tm_scopes.include?("bundle.linter.#{key.to_s.tr("_", "-")}") 18 | end 19 | 20 | def strip_trailing_whitespace!(write: true) 21 | return if filepath.empty? 22 | 23 | end_string = "__END__\n".freeze 24 | seen_end = false 25 | whitespace_regex = /[\t ]+$/ 26 | empty_string = "".freeze 27 | @stdin_lines ||= STDIN.read.lines 28 | @stdin_lines.map! do |line| 29 | if seen_end || line == end_string 30 | seen_end ||= true 31 | line 32 | else 33 | line.gsub(whitespace_regex, empty_string) 34 | end 35 | end 36 | return unless write 37 | IO.write(filepath, @stdin_lines.join) 38 | end 39 | 40 | def ensure_trailing_newline! 41 | return if filepath.empty? 42 | 43 | @stdin_lines ||= STDIN.read.lines 44 | last_char = @stdin_lines.last.to_s.chars.last 45 | newline = "\n".freeze 46 | @stdin_lines << newline if last_char != newline 47 | IO.write(filepath, @stdin_lines.join) 48 | end 49 | 50 | def lint_strip_ensure_on_save 51 | if setting?(:strip_whitespace_on_save) 52 | if setting?(:ensure_newline_on_save) 53 | strip_trailing_whitespace!(write: false) 54 | ensure_trailing_newline! 55 | else 56 | strip_trailing_whitespace! 57 | end 58 | elsif setting?(:ensure_newline_on_save) 59 | ensure_trailing_newline! 60 | end 61 | 62 | lint(manually_requested: false) if setting?(:lint_on_save) 63 | end 64 | 65 | def lint(manually_requested: true) 66 | return if filepath.empty? 67 | 68 | language_found = false 69 | language_selectors = { 70 | bash: { select: /source\.shell/ }, 71 | json: { select: /source\.json/ }, 72 | markdown: { select: /text\.html\.markdown/ }, 73 | ruby: { 74 | select: /source\.ruby/, 75 | reject: /(source\.ruby\.embedded(\.haml)?|text\.html\.(erb|ruby))/, 76 | }, 77 | } 78 | language_selectors.each do |language, match| 79 | next if (reject = match[:reject]) && tm_scope =~ reject 80 | next unless tm_scope =~ match[:select] 81 | language_found ||= true 82 | 83 | require "languages/#{language}" 84 | output(send(language)) 85 | end 86 | 87 | return if language_found 88 | return unless manually_requested 89 | 90 | first_scope = tm_scopes.first 91 | puts "Error: no Linter found for #{first_scope}! Please consider submitting a pull request to https://github.com/MikeMcQuaid/Linter.tmbundle to add one." 92 | end 93 | 94 | def filepath 95 | ENV["TM_FILEPATH"].to_s 96 | end 97 | 98 | def filename 99 | return "" if filepath.empty? 100 | File.basename(filepath) 101 | end 102 | 103 | def which(name) 104 | @which_cache ||= {} 105 | @which_cache.fetch(name) do 106 | env = name.upcase.tr("-", "_") 107 | name = ENV["LINTER_#{env}"] || ENV[env] || name 108 | which = `which '#{name}'`.chomp 109 | next if which.empty? 110 | which 111 | end 112 | end 113 | 114 | def output(all_settings) 115 | all_settings = [all_settings].flatten 116 | 117 | file_link = "#{filename}" 118 | 119 | names_versions = [] 120 | all_settings.each do |settings| 121 | name = settings[:name] 122 | version = settings[:version].to_s.chomp 123 | name_version = 124 | if version.empty? 125 | settings[:name] 126 | else 127 | "#{name} #{version}" 128 | end 129 | names_versions << name_version 130 | end 131 | names_versions = names_versions.join(", ") 132 | puts "Linting #{file_link} with #{names_versions}
"
133 | 
134 |     output_something = false
135 |     all_settings.each do |settings|
136 |       output = settings[:output]
137 |       output ||= begin
138 |         output_command = settings[:output_command]
139 |         unless output_command.include?(filepath)
140 |           output_command += " '#{filepath}'"
141 |         end
142 |         env = settings[:output_command_env]
143 |         if env
144 |           old_env = {}
145 |           env.each do |key, value|
146 |             key = key.to_s
147 |             old_env[key] = ENV.delete(key)
148 |             ENV[key] = value
149 |           end
150 |         end
151 |         `#{output_command} 2>&1`
152 |       ensure
153 |         ENV.update(old_env) if env
154 |       end
155 | 
156 |       if (line_column_match = settings[:line_column_match])
157 |         output.gsub! \
158 |           line_column_match,
159 |           "L\\1 "
160 |       elsif (line_match = settings[:line_match])
161 |         output.gsub! \
162 |           line_match,
163 |           "L\\1"
164 |       end
165 | 
166 |       if (extra_gsubs = settings[:extra_gsubs])
167 |         extra_gsubs.each do |pattern, replacement|
168 |           if replacement.is_a?(Proc)
169 |             output.gsub! pattern, &replacement
170 |           else
171 |             output.gsub! pattern, replacement
172 |           end
173 |         end
174 |       end
175 | 
176 |       output = output.squeeze("\n").strip.chomp
177 |       next if output.empty?
178 |       output_something ||= true
179 |       puts output
180 |     end
181 | 
182 |     puts "(no output)" unless output_something
183 |   end
184 | end
185 | 


--------------------------------------------------------------------------------
/info.plist:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | 
 4 | 
 5 | 	contactEmailRot13
 6 | 	zvxr@zvxrzpdhnvq.pbz
 7 | 	contactName
 8 | 	Mike McQuaid
 9 | 	description
10 | 	Linting functionality to make it easier to write better quality code.
11 | 	mainMenu
12 | 	
13 | 		items
14 | 		
15 | 			26A8FF38-100D-4E72-9ADC-1EF964095C44
16 | 			B6CC0FDC-0705-4435-AAFA-BC45A8E9A0E7
17 | 			5983C2DD-EB3D-4282-AF06-DC0CC94DD9D0
18 | 			------------------------------------
19 | 			483D338D-EC9F-4777-92C0-DFCBE02BD3A0
20 | 		
21 | 	
22 | 	name
23 | 	Linter
24 | 	uuid
25 | 	4A4ECE1D-B6CC-49BA-B8CD-B367A0F1C3AA
26 | 
27 | 
28 | 


--------------------------------------------------------------------------------