coffeelint 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +17 -0
- data/.gitmodules +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +5 -0
- data/coffeelint.gemspec +22 -0
- data/coffeelint/src/coffeelint.coffee +868 -0
- data/lib/coffeelint.rb +31 -0
- data/lib/coffeelint/railtie.rb +11 -0
- data/lib/coffeelint/version.rb +3 -0
- data/lib/tasks/coffeelint.rake +22 -0
- metadata +73 -0
data/.gitignore
ADDED
data/.gitmodules
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2013 Zachary Bush
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Coffeelint
|
|
2
|
+
|
|
3
|
+
TODO: Write a gem description
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
gem 'coffeelint'
|
|
10
|
+
|
|
11
|
+
And then execute:
|
|
12
|
+
|
|
13
|
+
$ bundle
|
|
14
|
+
|
|
15
|
+
Or install it yourself as:
|
|
16
|
+
|
|
17
|
+
$ gem install coffeelint
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
TODO: Write usage instructions here
|
|
22
|
+
|
|
23
|
+
## Contributing
|
|
24
|
+
|
|
25
|
+
1. Fork it
|
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
27
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/coffeelint.gemspec
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
require 'coffeelint/version'
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |gem|
|
|
7
|
+
gem.name = "coffeelint"
|
|
8
|
+
gem.version = Coffeelint::VERSION
|
|
9
|
+
gem.authors = ["Zachary Bush"]
|
|
10
|
+
gem.email = ["zach@zmbush.com"]
|
|
11
|
+
gem.description = %q{Ruby bindings for coffeelint}
|
|
12
|
+
gem.summary = %q{Ruby bindings for coffeelint along with railtie to add rake task to rails}
|
|
13
|
+
gem.homepage = "https://github.com/zipcodeman/coffeelint-ruby"
|
|
14
|
+
|
|
15
|
+
gem.files = `git ls-files`.split($/)
|
|
16
|
+
gem.files << 'coffeelint/src/coffeelint.coffee'
|
|
17
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
|
18
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
|
19
|
+
gem.require_paths = ["lib"]
|
|
20
|
+
|
|
21
|
+
gem.add_dependency "coffee-script"
|
|
22
|
+
end
|
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
###
|
|
2
|
+
CoffeeLint
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2011 Matthew Perpick.
|
|
5
|
+
CoffeeLint is freely distributable under the MIT license.
|
|
6
|
+
###
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Coffeelint's namespace.
|
|
10
|
+
coffeelint = {}
|
|
11
|
+
|
|
12
|
+
if exports?
|
|
13
|
+
# If we're running in node, export our module and
|
|
14
|
+
# load dependencies.
|
|
15
|
+
coffeelint = exports
|
|
16
|
+
CoffeeScript = require 'coffee-script'
|
|
17
|
+
else
|
|
18
|
+
# If we're in the browser, export out module to
|
|
19
|
+
# global scope. Assume CoffeeScript is already
|
|
20
|
+
# loaded.
|
|
21
|
+
this.coffeelint = coffeelint
|
|
22
|
+
CoffeeScript = this.CoffeeScript
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# The current version of Coffeelint.
|
|
26
|
+
coffeelint.VERSION = "0.5.4"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# CoffeeLint error levels.
|
|
30
|
+
ERROR = 'error'
|
|
31
|
+
WARN = 'warn'
|
|
32
|
+
IGNORE = 'ignore'
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# CoffeeLint's default rule configuration.
|
|
36
|
+
coffeelint.RULES = RULES =
|
|
37
|
+
|
|
38
|
+
no_tabs :
|
|
39
|
+
level : ERROR
|
|
40
|
+
message : 'Line contains tab indentation'
|
|
41
|
+
|
|
42
|
+
no_trailing_whitespace :
|
|
43
|
+
level : ERROR
|
|
44
|
+
message : 'Line ends with trailing whitespace'
|
|
45
|
+
allowed_in_comments : false
|
|
46
|
+
|
|
47
|
+
max_line_length :
|
|
48
|
+
value: 80
|
|
49
|
+
level : ERROR
|
|
50
|
+
message : 'Line exceeds maximum allowed length'
|
|
51
|
+
|
|
52
|
+
camel_case_classes :
|
|
53
|
+
level : ERROR
|
|
54
|
+
message : 'Class names should be camel cased'
|
|
55
|
+
|
|
56
|
+
indentation :
|
|
57
|
+
value : 2
|
|
58
|
+
level : ERROR
|
|
59
|
+
message : 'Line contains inconsistent indentation'
|
|
60
|
+
|
|
61
|
+
no_implicit_braces :
|
|
62
|
+
level : IGNORE
|
|
63
|
+
message : 'Implicit braces are forbidden'
|
|
64
|
+
|
|
65
|
+
no_trailing_semicolons:
|
|
66
|
+
level : ERROR
|
|
67
|
+
message : 'Line contains a trailing semicolon'
|
|
68
|
+
|
|
69
|
+
no_plusplus:
|
|
70
|
+
level : IGNORE
|
|
71
|
+
message : 'The increment and decrement operators are forbidden'
|
|
72
|
+
|
|
73
|
+
no_throwing_strings:
|
|
74
|
+
level : ERROR
|
|
75
|
+
message : 'Throwing strings is forbidden'
|
|
76
|
+
|
|
77
|
+
cyclomatic_complexity:
|
|
78
|
+
value : 10
|
|
79
|
+
level : IGNORE
|
|
80
|
+
message : 'The cyclomatic complexity is too damn high'
|
|
81
|
+
|
|
82
|
+
no_backticks:
|
|
83
|
+
level : ERROR
|
|
84
|
+
message : 'Backticks are forbidden'
|
|
85
|
+
|
|
86
|
+
line_endings:
|
|
87
|
+
level : IGNORE
|
|
88
|
+
value : 'unix' # or 'windows'
|
|
89
|
+
message : 'Line contains incorrect line endings'
|
|
90
|
+
|
|
91
|
+
no_implicit_parens :
|
|
92
|
+
level : IGNORE
|
|
93
|
+
message : 'Implicit parens are forbidden'
|
|
94
|
+
|
|
95
|
+
empty_constructor_needs_parens :
|
|
96
|
+
level : IGNORE
|
|
97
|
+
message : 'Invoking a constructor without parens and without arguments'
|
|
98
|
+
|
|
99
|
+
no_empty_param_list :
|
|
100
|
+
level : IGNORE
|
|
101
|
+
message : 'Empty parameter list is forbidden'
|
|
102
|
+
|
|
103
|
+
space_operators :
|
|
104
|
+
level : IGNORE
|
|
105
|
+
message : 'Operators must be spaced properly'
|
|
106
|
+
|
|
107
|
+
# I don't know of any legitimate reason to define duplicate keys in an
|
|
108
|
+
# object. It seems to always be a mistake, it's also a syntax error in
|
|
109
|
+
# strict mode.
|
|
110
|
+
# See http://jslinterrors.com/duplicate-key-a/
|
|
111
|
+
duplicate_key :
|
|
112
|
+
level : ERROR
|
|
113
|
+
message : 'Duplicate key defined in object or class'
|
|
114
|
+
|
|
115
|
+
newlines_after_classes :
|
|
116
|
+
value : 3
|
|
117
|
+
level : IGNORE
|
|
118
|
+
message : 'Wrong count of newlines between a class and other code'
|
|
119
|
+
|
|
120
|
+
no_stand_alone_at :
|
|
121
|
+
level : IGNORE
|
|
122
|
+
message : '@ must not be used stand alone'
|
|
123
|
+
|
|
124
|
+
coffeescript_error :
|
|
125
|
+
level : ERROR
|
|
126
|
+
message : '' # The default coffeescript error is fine.
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# Some repeatedly used regular expressions.
|
|
130
|
+
regexes =
|
|
131
|
+
trailingWhitespace : /[^\s]+[\t ]+\r?$/
|
|
132
|
+
lineHasComment : /^\s*[^\#]*\#/
|
|
133
|
+
indentation: /\S/
|
|
134
|
+
longUrlComment: ///
|
|
135
|
+
^\s*\# # indentation, up to comment
|
|
136
|
+
\s*
|
|
137
|
+
http[^\s]+$ # Link that takes up the rest of the line without spaces.
|
|
138
|
+
///
|
|
139
|
+
camelCase: /^[A-Z][a-zA-Z\d]*$/
|
|
140
|
+
trailingSemicolon: /;\r?$/
|
|
141
|
+
configStatement: /coffeelint:\s*(disable|enable)(?:=([\w\s,]*))?/
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# Patch the source properties onto the destination.
|
|
145
|
+
extend = (destination, sources...) ->
|
|
146
|
+
for source in sources
|
|
147
|
+
(destination[k] = v for k, v of source)
|
|
148
|
+
return destination
|
|
149
|
+
|
|
150
|
+
# Patch any missing attributes from defaults to source.
|
|
151
|
+
defaults = (source, defaults) ->
|
|
152
|
+
extend({}, defaults, source)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# Create an error object for the given rule with the given
|
|
156
|
+
# attributes.
|
|
157
|
+
createError = (rule, attrs = {}) ->
|
|
158
|
+
level = attrs.level
|
|
159
|
+
if level not in [IGNORE, WARN, ERROR]
|
|
160
|
+
throw new Error("unknown level #{level}")
|
|
161
|
+
|
|
162
|
+
if level in [ERROR, WARN]
|
|
163
|
+
attrs.rule = rule
|
|
164
|
+
return defaults(attrs, RULES[rule])
|
|
165
|
+
else
|
|
166
|
+
null
|
|
167
|
+
|
|
168
|
+
# Store suppressions in the form of { line #: type }
|
|
169
|
+
block_config =
|
|
170
|
+
enable: {}
|
|
171
|
+
disable: {}
|
|
172
|
+
|
|
173
|
+
#
|
|
174
|
+
# A class that performs regex checks on each line of the source.
|
|
175
|
+
#
|
|
176
|
+
class LineLinter
|
|
177
|
+
|
|
178
|
+
constructor : (source, config, tokensByLine) ->
|
|
179
|
+
@source = source
|
|
180
|
+
@config = config
|
|
181
|
+
@line = null
|
|
182
|
+
@lineNumber = 0
|
|
183
|
+
@tokensByLine = tokensByLine
|
|
184
|
+
@lines = @source.split('\n')
|
|
185
|
+
@lineCount = @lines.length
|
|
186
|
+
|
|
187
|
+
# maintains some contextual information
|
|
188
|
+
# inClass: bool; in class or not
|
|
189
|
+
# lastUnemptyLineInClass: null or lineNumber, if the last not-empty
|
|
190
|
+
# line was in a class it holds its number
|
|
191
|
+
# classIndents: the number of indents within a class
|
|
192
|
+
@context = {
|
|
193
|
+
class: {
|
|
194
|
+
inClass: false
|
|
195
|
+
lastUnemptyLineInClass: null
|
|
196
|
+
classIndents: null
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
lint : () ->
|
|
201
|
+
errors = []
|
|
202
|
+
for line, lineNumber in @lines
|
|
203
|
+
@lineNumber = lineNumber
|
|
204
|
+
@line = line
|
|
205
|
+
@maintainClassContext()
|
|
206
|
+
error = @lintLine()
|
|
207
|
+
errors.push(error) if error
|
|
208
|
+
errors
|
|
209
|
+
|
|
210
|
+
# Return an error if the line contained failed a rule, null otherwise.
|
|
211
|
+
lintLine : () ->
|
|
212
|
+
return @checkTabs() or
|
|
213
|
+
@checkTrailingWhitespace() or
|
|
214
|
+
@checkLineLength() or
|
|
215
|
+
@checkTrailingSemicolon() or
|
|
216
|
+
@checkLineEndings() or
|
|
217
|
+
@checkComments() or
|
|
218
|
+
@checkNewlinesAfterClasses()
|
|
219
|
+
|
|
220
|
+
checkTabs : () ->
|
|
221
|
+
# Only check lines that have compiled tokens. This helps
|
|
222
|
+
# us ignore tabs in the middle of multi line strings, heredocs, etc.
|
|
223
|
+
# since they are all reduced to a single token whose line number
|
|
224
|
+
# is the start of the expression.
|
|
225
|
+
indentation = @line.split(regexes.indentation)[0]
|
|
226
|
+
if @lineHasToken() and '\t' in indentation
|
|
227
|
+
@createLineError('no_tabs')
|
|
228
|
+
else
|
|
229
|
+
null
|
|
230
|
+
|
|
231
|
+
checkTrailingWhitespace : () ->
|
|
232
|
+
if regexes.trailingWhitespace.test(@line)
|
|
233
|
+
# By default only the regex above is needed.
|
|
234
|
+
if !@config['no_trailing_whitespace']?.allowed_in_comments
|
|
235
|
+
return @createLineError('no_trailing_whitespace')
|
|
236
|
+
|
|
237
|
+
line = @line
|
|
238
|
+
tokens = @tokensByLine[@lineNumber]
|
|
239
|
+
|
|
240
|
+
# If we're in a block comment there won't be any tokens on this
|
|
241
|
+
# line. Some previous line holds the token spanning multiple lines.
|
|
242
|
+
if !tokens
|
|
243
|
+
return null
|
|
244
|
+
|
|
245
|
+
# To avoid confusion when a string might contain a "#", every string
|
|
246
|
+
# on this line will be removed. before checking for a comment
|
|
247
|
+
for str in (token[1] for token in tokens when token[0] == 'STRING')
|
|
248
|
+
line = line.replace(str, 'STRING')
|
|
249
|
+
|
|
250
|
+
if !regexes.lineHasComment.test(line)
|
|
251
|
+
return @createLineError('no_trailing_whitespace')
|
|
252
|
+
else
|
|
253
|
+
return null
|
|
254
|
+
else
|
|
255
|
+
return null
|
|
256
|
+
|
|
257
|
+
checkLineLength : () ->
|
|
258
|
+
rule = 'max_line_length'
|
|
259
|
+
max = @config[rule]?.value
|
|
260
|
+
if max and max < @line.length
|
|
261
|
+
@createLineError(rule) unless regexes.longUrlComment.test(@line)
|
|
262
|
+
else
|
|
263
|
+
null
|
|
264
|
+
|
|
265
|
+
checkTrailingSemicolon : () ->
|
|
266
|
+
hasSemicolon = regexes.trailingSemicolon.test(@line)
|
|
267
|
+
[first..., last] = @getLineTokens()
|
|
268
|
+
hasNewLine = last and last.newLine?
|
|
269
|
+
# Don't throw errors when the contents of multiline strings,
|
|
270
|
+
# regexes and the like end in ";"
|
|
271
|
+
if hasSemicolon and not hasNewLine and @lineHasToken()
|
|
272
|
+
@createLineError('no_trailing_semicolons')
|
|
273
|
+
else
|
|
274
|
+
return null
|
|
275
|
+
|
|
276
|
+
checkLineEndings : () ->
|
|
277
|
+
rule = 'line_endings'
|
|
278
|
+
ending = @config[rule]?.value
|
|
279
|
+
|
|
280
|
+
return null if not ending or @isLastLine() or not @line
|
|
281
|
+
|
|
282
|
+
lastChar = @line[@line.length - 1]
|
|
283
|
+
valid = if ending == 'windows'
|
|
284
|
+
lastChar == '\r'
|
|
285
|
+
else if ending == 'unix'
|
|
286
|
+
lastChar != '\r'
|
|
287
|
+
else
|
|
288
|
+
throw new Error("unknown line ending type: #{ending}")
|
|
289
|
+
if not valid
|
|
290
|
+
return @createLineError(rule, {context:"Expected #{ending}"})
|
|
291
|
+
else
|
|
292
|
+
return null
|
|
293
|
+
|
|
294
|
+
checkComments : () ->
|
|
295
|
+
# Check for block config statements enable and disable
|
|
296
|
+
result = regexes.configStatement.exec(@line)
|
|
297
|
+
if result?
|
|
298
|
+
cmd = result[1]
|
|
299
|
+
rules = []
|
|
300
|
+
if result[2]?
|
|
301
|
+
for r in result[2].split(',')
|
|
302
|
+
rules.push r.replace(/^\s+|\s+$/g, "")
|
|
303
|
+
block_config[cmd][@lineNumber] = rules
|
|
304
|
+
return null
|
|
305
|
+
|
|
306
|
+
checkNewlinesAfterClasses : () ->
|
|
307
|
+
rule = 'newlines_after_classes'
|
|
308
|
+
ending = @config[rule].value
|
|
309
|
+
|
|
310
|
+
return null if not ending or @isLastLine()
|
|
311
|
+
|
|
312
|
+
if not @context.class.inClass and
|
|
313
|
+
@context.class.lastUnemptyLineInClass? and
|
|
314
|
+
((@lineNumber - 1) - @context.class.lastUnemptyLineInClass) isnt
|
|
315
|
+
ending
|
|
316
|
+
got = (@lineNumber - 1) - @context.class.lastUnemptyLineInClass
|
|
317
|
+
return @createLineError( rule, {
|
|
318
|
+
context: "Expected #{ending} got #{got}"
|
|
319
|
+
} )
|
|
320
|
+
|
|
321
|
+
null
|
|
322
|
+
|
|
323
|
+
createLineError : (rule, attrs = {}) ->
|
|
324
|
+
attrs.lineNumber = @lineNumber + 1 # Lines are indexed by zero.
|
|
325
|
+
attrs.level = @config[rule]?.level
|
|
326
|
+
createError(rule, attrs)
|
|
327
|
+
|
|
328
|
+
isLastLine : () ->
|
|
329
|
+
return @lineNumber == @lineCount - 1
|
|
330
|
+
|
|
331
|
+
# Return true if the given line actually has tokens.
|
|
332
|
+
# Optional parameter to check for a specific token type and line number.
|
|
333
|
+
lineHasToken : (tokenType = null, lineNumber = null) ->
|
|
334
|
+
lineNumber = lineNumber ? @lineNumber
|
|
335
|
+
unless tokenType?
|
|
336
|
+
return @tokensByLine[lineNumber]?
|
|
337
|
+
else
|
|
338
|
+
tokens = @tokensByLine[lineNumber]
|
|
339
|
+
return null unless tokens?
|
|
340
|
+
for token in tokens
|
|
341
|
+
return true if token[0] == tokenType
|
|
342
|
+
return false
|
|
343
|
+
|
|
344
|
+
# Return tokens for the given line number.
|
|
345
|
+
getLineTokens : () ->
|
|
346
|
+
@tokensByLine[@lineNumber] || []
|
|
347
|
+
|
|
348
|
+
# maintain the contextual information for class-related stuff
|
|
349
|
+
maintainClassContext: () ->
|
|
350
|
+
if @context.class.inClass
|
|
351
|
+
if @lineHasToken 'INDENT'
|
|
352
|
+
@context.class.classIndents++
|
|
353
|
+
else if @lineHasToken 'OUTDENT'
|
|
354
|
+
@context.class.classIndents--
|
|
355
|
+
if @context.class.classIndents is 0
|
|
356
|
+
@context.class.inClass = false
|
|
357
|
+
@context.class.classIndents = null
|
|
358
|
+
|
|
359
|
+
if @context.class.inClass and not @line.match( /^\s*$/ )
|
|
360
|
+
@context.class.lastUnemptyLineInClass = @lineNumber
|
|
361
|
+
else
|
|
362
|
+
unless @line.match(/\\s*/)
|
|
363
|
+
@context.class.lastUnemptyLineInClass = null
|
|
364
|
+
|
|
365
|
+
if @lineHasToken 'CLASS'
|
|
366
|
+
@context.class.inClass = true
|
|
367
|
+
@context.class.lastUnemptyLineInClass = @lineNumber
|
|
368
|
+
@context.class.classIndents = 0
|
|
369
|
+
|
|
370
|
+
null
|
|
371
|
+
|
|
372
|
+
#
|
|
373
|
+
# A class that performs checks on the output of CoffeeScript's
|
|
374
|
+
# lexer.
|
|
375
|
+
#
|
|
376
|
+
class LexicalLinter
|
|
377
|
+
|
|
378
|
+
constructor : (source, config) ->
|
|
379
|
+
@source = source
|
|
380
|
+
@tokens = CoffeeScript.tokens(source)
|
|
381
|
+
@config = config
|
|
382
|
+
@i = 0 # The index of the current token we're linting.
|
|
383
|
+
@tokensByLine = {} # A map of tokens by line.
|
|
384
|
+
@arrayTokens = [] # A stack tracking the array token pairs.
|
|
385
|
+
@parenTokens = [] # A stack tracking the parens token pairs.
|
|
386
|
+
@callTokens = [] # A stack tracking the call token pairs.
|
|
387
|
+
@lines = source.split('\n')
|
|
388
|
+
@braceScopes = [] # A stack tracking keys defined in nexted scopes.
|
|
389
|
+
|
|
390
|
+
# Return a list of errors encountered in the given source.
|
|
391
|
+
lint : () ->
|
|
392
|
+
errors = []
|
|
393
|
+
|
|
394
|
+
for token, i in @tokens
|
|
395
|
+
@i = i
|
|
396
|
+
error = @lintToken(token)
|
|
397
|
+
errors.push(error) if error
|
|
398
|
+
errors
|
|
399
|
+
|
|
400
|
+
# Return an error if the given token fails a lint check, false
|
|
401
|
+
# otherwise.
|
|
402
|
+
lintToken : (token) ->
|
|
403
|
+
[type, value, lineNumber] = token
|
|
404
|
+
|
|
405
|
+
lineNumber = lineNumber["first_line"] if lineNumber["first_line"]?
|
|
406
|
+
@tokensByLine[lineNumber] ?= []
|
|
407
|
+
@tokensByLine[lineNumber].push(token)
|
|
408
|
+
# CoffeeScript loses line numbers of interpolations and multi-line
|
|
409
|
+
# regexes, so fake it by using the last line number we know.
|
|
410
|
+
@lineNumber = lineNumber or @lineNumber or 0
|
|
411
|
+
|
|
412
|
+
# Now lint it.
|
|
413
|
+
switch type
|
|
414
|
+
when "INDENT" then @lintIndentation(token)
|
|
415
|
+
when "CLASS" then @lintClass(token)
|
|
416
|
+
when "UNARY" then @lintUnary(token)
|
|
417
|
+
when "{","}" then @lintBrace(token)
|
|
418
|
+
when "IDENTIFIER" then @lintIdentifier(token)
|
|
419
|
+
when "++", "--" then @lintIncrement(token)
|
|
420
|
+
when "THROW" then @lintThrow(token)
|
|
421
|
+
when "[", "]" then @lintArray(token)
|
|
422
|
+
when "(", ")" then @lintParens(token)
|
|
423
|
+
when "JS" then @lintJavascript(token)
|
|
424
|
+
when "CALL_START", "CALL_END" then @lintCall(token)
|
|
425
|
+
when "PARAM_START" then @lintParam(token)
|
|
426
|
+
when "@" then @lintStandaloneAt(token)
|
|
427
|
+
when "+", "-" then @lintPlus(token)
|
|
428
|
+
when "=", "MATH", "COMPARE", "LOGIC"
|
|
429
|
+
@lintMath(token)
|
|
430
|
+
else null
|
|
431
|
+
|
|
432
|
+
lintUnary: (token) ->
|
|
433
|
+
if token[1] is 'new'
|
|
434
|
+
expectedIdentifier = @peek(1)
|
|
435
|
+
# The callStart is generated if your parameters are on the next line
|
|
436
|
+
# but is missing if there are no params and no parens.
|
|
437
|
+
expectedCallStart = @peek(2)
|
|
438
|
+
if expectedIdentifier?[0] is 'IDENTIFIER' and
|
|
439
|
+
expectedCallStart?[0] isnt 'CALL_START'
|
|
440
|
+
@createLexError('empty_constructor_needs_parens')
|
|
441
|
+
|
|
442
|
+
# Lint the given array token.
|
|
443
|
+
lintArray : (token) ->
|
|
444
|
+
# Track the array token pairs
|
|
445
|
+
if token[0] == '['
|
|
446
|
+
@arrayTokens.push(token)
|
|
447
|
+
else if token[0] == ']'
|
|
448
|
+
@arrayTokens.pop()
|
|
449
|
+
# Return null, since we're not really linting
|
|
450
|
+
# anything here.
|
|
451
|
+
null
|
|
452
|
+
|
|
453
|
+
lintParens : (token) ->
|
|
454
|
+
if token[0] == '('
|
|
455
|
+
p1 = @peek(-1)
|
|
456
|
+
n1 = @peek(1)
|
|
457
|
+
n2 = @peek(2)
|
|
458
|
+
# String interpolations start with '' + so start the type co-ercion,
|
|
459
|
+
# so track if we're inside of one. This is most definitely not
|
|
460
|
+
# 100% true but what else can we do?
|
|
461
|
+
i = n1 and n2 and n1[0] == 'STRING' and n2[0] == '+'
|
|
462
|
+
token.isInterpolation = i
|
|
463
|
+
@parenTokens.push(token)
|
|
464
|
+
else
|
|
465
|
+
@parenTokens.pop()
|
|
466
|
+
# We're not linting, just tracking interpolations.
|
|
467
|
+
null
|
|
468
|
+
|
|
469
|
+
isInInterpolation : () ->
|
|
470
|
+
for t in @parenTokens
|
|
471
|
+
return true if t.isInterpolation
|
|
472
|
+
return false
|
|
473
|
+
|
|
474
|
+
isInExtendedRegex : () ->
|
|
475
|
+
for t in @callTokens
|
|
476
|
+
return true if t.isRegex
|
|
477
|
+
return false
|
|
478
|
+
|
|
479
|
+
lintPlus : (token) ->
|
|
480
|
+
# We can't check this inside of interpolations right now, because the
|
|
481
|
+
# plusses used for the string type co-ercion are marked not spaced.
|
|
482
|
+
return null if @isInInterpolation() or @isInExtendedRegex()
|
|
483
|
+
|
|
484
|
+
p = @peek(-1)
|
|
485
|
+
unaries = ['TERMINATOR', '(', '=', '-', '+', ',', 'CALL_START',
|
|
486
|
+
'INDEX_START', '..', '...', 'COMPARE', 'IF',
|
|
487
|
+
'THROW', 'LOGIC', 'POST_IF', ':', '[', 'INDENT']
|
|
488
|
+
isUnary = if not p then false else p[0] in unaries
|
|
489
|
+
if (isUnary and token.spaced) or
|
|
490
|
+
(not isUnary and not token.spaced and not token.newLine)
|
|
491
|
+
@createLexError('space_operators', {context: token[1]})
|
|
492
|
+
else
|
|
493
|
+
null
|
|
494
|
+
|
|
495
|
+
lintMath: (token) ->
|
|
496
|
+
if not token.spaced and not token.newLine
|
|
497
|
+
@createLexError('space_operators', {context: token[1]})
|
|
498
|
+
else
|
|
499
|
+
null
|
|
500
|
+
|
|
501
|
+
lintCall : (token) ->
|
|
502
|
+
if token[0] == 'CALL_START'
|
|
503
|
+
p = @peek(-1)
|
|
504
|
+
# Track regex calls, to know (approximately) if we're in an
|
|
505
|
+
# extended regex.
|
|
506
|
+
token.isRegex = p and p[0] == 'IDENTIFIER' and p[1] == 'RegExp'
|
|
507
|
+
@callTokens.push(token)
|
|
508
|
+
if token.generated
|
|
509
|
+
return @createLexError('no_implicit_parens')
|
|
510
|
+
else
|
|
511
|
+
return null
|
|
512
|
+
else
|
|
513
|
+
@callTokens.pop()
|
|
514
|
+
return null
|
|
515
|
+
|
|
516
|
+
lintParam : (token) ->
|
|
517
|
+
nextType = @peek()[0]
|
|
518
|
+
if nextType == 'PARAM_END'
|
|
519
|
+
@createLexError('no_empty_param_list')
|
|
520
|
+
else
|
|
521
|
+
null
|
|
522
|
+
|
|
523
|
+
lintIdentifier: (token) ->
|
|
524
|
+
key = token[1]
|
|
525
|
+
|
|
526
|
+
# Class names might not be in a scope
|
|
527
|
+
return null if not @currentScope?
|
|
528
|
+
nextToken = @peek(1)
|
|
529
|
+
|
|
530
|
+
# Exit if this identifier isn't being assigned. A and B
|
|
531
|
+
# are identifiers, but only A should be examined:
|
|
532
|
+
# A = B
|
|
533
|
+
return null if nextToken[1] isnt ':'
|
|
534
|
+
previousToken = @peek(-1)
|
|
535
|
+
|
|
536
|
+
# Assigning "@something" and "something" are not the same thing
|
|
537
|
+
key = "@#{key}" if previousToken[0] == '@'
|
|
538
|
+
|
|
539
|
+
# Added a prefix to not interfere with things like "constructor".
|
|
540
|
+
key = "identifier-#{key}"
|
|
541
|
+
if @currentScope[key]
|
|
542
|
+
@createLexError('duplicate_key')
|
|
543
|
+
else
|
|
544
|
+
@currentScope[key] = token
|
|
545
|
+
null
|
|
546
|
+
|
|
547
|
+
lintBrace : (token) ->
|
|
548
|
+
if token[0] == '{'
|
|
549
|
+
@braceScopes.push @currentScope if @currentScope?
|
|
550
|
+
@currentScope = {}
|
|
551
|
+
else
|
|
552
|
+
@currentScope = @braceScopes.pop()
|
|
553
|
+
|
|
554
|
+
if token.generated and token[0] == '{'
|
|
555
|
+
# Peek back to the last line break. If there is a class
|
|
556
|
+
# definition, ignore the generated brace.
|
|
557
|
+
i = -1
|
|
558
|
+
loop
|
|
559
|
+
t = @peek(i)
|
|
560
|
+
if not t? or t[0] == 'TERMINATOR'
|
|
561
|
+
return @createLexError('no_implicit_braces')
|
|
562
|
+
if t[0] == 'CLASS'
|
|
563
|
+
return null
|
|
564
|
+
i -= 1
|
|
565
|
+
else
|
|
566
|
+
return null
|
|
567
|
+
|
|
568
|
+
lintJavascript :(token) ->
|
|
569
|
+
@createLexError('no_backticks')
|
|
570
|
+
|
|
571
|
+
lintThrow : (token) ->
|
|
572
|
+
[n1, n2] = [@peek(), @peek(2)]
|
|
573
|
+
# Catch literals and string interpolations, which are wrapped in
|
|
574
|
+
# parens.
|
|
575
|
+
nextIsString = n1[0] == 'STRING' or (n1[0] == '(' and n2[0] == 'STRING')
|
|
576
|
+
@createLexError('no_throwing_strings') if nextIsString
|
|
577
|
+
|
|
578
|
+
lintIncrement : (token) ->
|
|
579
|
+
attrs = {context : "found '#{token[0]}'"}
|
|
580
|
+
@createLexError('no_plusplus', attrs)
|
|
581
|
+
|
|
582
|
+
lintStandaloneAt: (token) ->
|
|
583
|
+
nextToken = @peek()
|
|
584
|
+
spaced = token.spaced
|
|
585
|
+
isIdentifier = nextToken[0] == 'IDENTIFIER'
|
|
586
|
+
isIndexStart = nextToken[0] == 'INDEX_START'
|
|
587
|
+
isDot = nextToken[0] == '.'
|
|
588
|
+
|
|
589
|
+
if spaced or (not isIdentifier and not isIndexStart and not isDot)
|
|
590
|
+
@createLexError('no_stand_alone_at')
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
# Return an error if the given indentation token is not correct.
|
|
594
|
+
lintIndentation : (token) ->
|
|
595
|
+
[type, numIndents, lineNumber] = token
|
|
596
|
+
|
|
597
|
+
return null if token.generated?
|
|
598
|
+
|
|
599
|
+
# HACK: CoffeeScript's lexer insert indentation in string
|
|
600
|
+
# interpolations that start with spaces e.g. "#{ 123 }"
|
|
601
|
+
# so ignore such cases. Are there other times an indentation
|
|
602
|
+
# could possibly follow a '+'?
|
|
603
|
+
previous = @peek(-2)
|
|
604
|
+
isInterpIndent = previous and previous[0] == '+'
|
|
605
|
+
|
|
606
|
+
# Ignore the indentation inside of an array, so that
|
|
607
|
+
# we can allow things like:
|
|
608
|
+
# x = ["foo",
|
|
609
|
+
# "bar"]
|
|
610
|
+
previous = @peek(-1)
|
|
611
|
+
isArrayIndent = @inArray() and previous?.newLine
|
|
612
|
+
|
|
613
|
+
# Ignore indents used to for formatting on multi-line expressions, so
|
|
614
|
+
# we can allow things like:
|
|
615
|
+
# a = b =
|
|
616
|
+
# c = d
|
|
617
|
+
previousSymbol = @peek(-1)?[0]
|
|
618
|
+
isMultiline = previousSymbol in ['=', ',']
|
|
619
|
+
|
|
620
|
+
# Summarize the indentation conditions we'd like to ignore
|
|
621
|
+
ignoreIndent = isInterpIndent or isArrayIndent or isMultiline
|
|
622
|
+
|
|
623
|
+
# Compensate for indentation in function invocations that span multiple
|
|
624
|
+
# lines, which can be ignored.
|
|
625
|
+
if @isChainedCall()
|
|
626
|
+
currentLine = @lines[@lineNumber]
|
|
627
|
+
previousLine = @lines[@lineNumber - 1]
|
|
628
|
+
previousIndentation = previousLine.match(/^(\s*)/)[1].length
|
|
629
|
+
|
|
630
|
+
# I don't know why, but when inside a function, you make a chained
|
|
631
|
+
# call and define an inline callback as a parameter, the body of
|
|
632
|
+
# that callback gets the indentation reported higher than it really
|
|
633
|
+
# is. See issue #88
|
|
634
|
+
# NOTE: Adding this line moved the cyclomatic complexity over the
|
|
635
|
+
# limit, I'm not sure why
|
|
636
|
+
numIndents = currentLine.match(/^(\s*)/)[1].length
|
|
637
|
+
numIndents -= previousIndentation
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
# Now check the indentation.
|
|
641
|
+
expected = @config['indentation'].value
|
|
642
|
+
if not ignoreIndent and numIndents != expected
|
|
643
|
+
context = "Expected #{expected} " +
|
|
644
|
+
"got #{numIndents}"
|
|
645
|
+
@createLexError('indentation', {context})
|
|
646
|
+
else
|
|
647
|
+
null
|
|
648
|
+
|
|
649
|
+
lintClass : (token) ->
|
|
650
|
+
# TODO: you can do some crazy shit in CoffeeScript, like
|
|
651
|
+
# class func().ClassName. Don't allow that.
|
|
652
|
+
|
|
653
|
+
# Don't try to lint the names of anonymous classes.
|
|
654
|
+
return null if token.newLine? or @peek()[0] in ['INDENT', 'EXTENDS']
|
|
655
|
+
|
|
656
|
+
# It's common to assign a class to a global namespace, e.g.
|
|
657
|
+
# exports.MyClassName, so loop through the next tokens until
|
|
658
|
+
# we find the real identifier.
|
|
659
|
+
className = null
|
|
660
|
+
offset = 1
|
|
661
|
+
until className
|
|
662
|
+
if @peek(offset + 1)?[0] == '.'
|
|
663
|
+
offset += 2
|
|
664
|
+
else if @peek(offset)?[0] == '@'
|
|
665
|
+
offset += 1
|
|
666
|
+
else
|
|
667
|
+
className = @peek(offset)[1]
|
|
668
|
+
|
|
669
|
+
# Now check for the error.
|
|
670
|
+
if not regexes.camelCase.test(className)
|
|
671
|
+
attrs = {context: "class name: #{className}"}
|
|
672
|
+
@createLexError('camel_case_classes', attrs)
|
|
673
|
+
else
|
|
674
|
+
null
|
|
675
|
+
|
|
676
|
+
createLexError : (rule, attrs = {}) ->
|
|
677
|
+
attrs.lineNumber = @lineNumber + 1
|
|
678
|
+
attrs.level = @config[rule].level
|
|
679
|
+
attrs.line = @lines[@lineNumber]
|
|
680
|
+
createError(rule, attrs)
|
|
681
|
+
|
|
682
|
+
# Return the token n places away from the current token.
|
|
683
|
+
peek : (n = 1) ->
|
|
684
|
+
@tokens[@i + n] || null
|
|
685
|
+
|
|
686
|
+
# Return true if the current token is inside of an array.
|
|
687
|
+
inArray : () ->
|
|
688
|
+
return @arrayTokens.length > 0
|
|
689
|
+
|
|
690
|
+
# Return true if the current token is part of a property access
|
|
691
|
+
# that is split across lines, for example:
|
|
692
|
+
# $('body')
|
|
693
|
+
# .addClass('foo')
|
|
694
|
+
# .removeClass('bar')
|
|
695
|
+
isChainedCall : () ->
|
|
696
|
+
# Get the index of the second most recent new line.
|
|
697
|
+
lines = (i for token, i in @tokens[..@i] when token.newLine?)
|
|
698
|
+
|
|
699
|
+
lastNewLineIndex = if lines then lines[lines.length - 2] else null
|
|
700
|
+
|
|
701
|
+
# Bail out if there is no such token.
|
|
702
|
+
return false if not lastNewLineIndex?
|
|
703
|
+
|
|
704
|
+
# Otherwise, figure out if that token or the next is an attribute
|
|
705
|
+
# look-up.
|
|
706
|
+
tokens = [@tokens[lastNewLineIndex], @tokens[lastNewLineIndex + 1]]
|
|
707
|
+
|
|
708
|
+
return !!(t for t in tokens when t and t[0] == '.').length
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
# A class that performs static analysis of the abstract
|
|
712
|
+
# syntax tree.
|
|
713
|
+
class ASTLinter
|
|
714
|
+
|
|
715
|
+
constructor : (source, config) ->
|
|
716
|
+
@source = source
|
|
717
|
+
@config = config
|
|
718
|
+
@errors = []
|
|
719
|
+
|
|
720
|
+
lint : () ->
|
|
721
|
+
try
|
|
722
|
+
@node = CoffeeScript.nodes(@source)
|
|
723
|
+
catch coffeeError
|
|
724
|
+
@errors.push @_parseCoffeeScriptError(coffeeError)
|
|
725
|
+
return @errors
|
|
726
|
+
@lintNode(@node)
|
|
727
|
+
@errors
|
|
728
|
+
|
|
729
|
+
# Lint the AST node and return it's cyclomatic complexity.
|
|
730
|
+
lintNode : (node) ->
|
|
731
|
+
|
|
732
|
+
# Get the complexity of the current node.
|
|
733
|
+
name = node.constructor.name
|
|
734
|
+
complexity = if name in ['If', 'While', 'For', 'Try']
|
|
735
|
+
1
|
|
736
|
+
else if name == 'Op' and node.operator in ['&&', '||']
|
|
737
|
+
1
|
|
738
|
+
else if name == 'Switch'
|
|
739
|
+
node.cases.length
|
|
740
|
+
else
|
|
741
|
+
0
|
|
742
|
+
|
|
743
|
+
# Add the complexity of all child's nodes to this one.
|
|
744
|
+
node.eachChild (childNode) =>
|
|
745
|
+
return false unless childNode
|
|
746
|
+
complexity += @lintNode(childNode)
|
|
747
|
+
return true
|
|
748
|
+
|
|
749
|
+
# If the current node is a function, and it's over our limit, add an
|
|
750
|
+
# error to the list.
|
|
751
|
+
rule = @config.cyclomatic_complexity
|
|
752
|
+
if name == 'Code' and complexity >= rule.value
|
|
753
|
+
attrs = {
|
|
754
|
+
context: complexity + 1
|
|
755
|
+
level: rule.level
|
|
756
|
+
line: 0
|
|
757
|
+
}
|
|
758
|
+
error = createError 'cyclomatic_complexity', attrs
|
|
759
|
+
@errors.push error if error
|
|
760
|
+
|
|
761
|
+
# Return the complexity for the benefit of parent nodes.
|
|
762
|
+
return complexity
|
|
763
|
+
|
|
764
|
+
_parseCoffeeScriptError : (coffeeError) ->
|
|
765
|
+
rule = RULES['coffeescript_error']
|
|
766
|
+
|
|
767
|
+
message = coffeeError.toString()
|
|
768
|
+
|
|
769
|
+
# Parse the line number
|
|
770
|
+
lineNumber = -1
|
|
771
|
+
match = /line (\d+)/.exec message
|
|
772
|
+
lineNumber = parseInt match[1], 10 if match?.length > 1
|
|
773
|
+
attrs = {
|
|
774
|
+
message: message
|
|
775
|
+
level: rule.level
|
|
776
|
+
lineNumber: lineNumber
|
|
777
|
+
}
|
|
778
|
+
return createError 'coffeescript_error', attrs
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
# Merge default and user configuration.
|
|
783
|
+
mergeDefaultConfig = (userConfig) ->
|
|
784
|
+
config = {}
|
|
785
|
+
for rule, ruleConfig of RULES
|
|
786
|
+
config[rule] = defaults(userConfig[rule], ruleConfig)
|
|
787
|
+
return config
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
# Check the source against the given configuration and return an array
|
|
791
|
+
# of any errors found. An error is an object with the following
|
|
792
|
+
# properties:
|
|
793
|
+
#
|
|
794
|
+
# {
|
|
795
|
+
# rule : 'Name of the violated rule',
|
|
796
|
+
# lineNumber: 'Number of the line that caused the violation',
|
|
797
|
+
# level: 'The error level of the violated rule',
|
|
798
|
+
# message: 'Information about the violated rule',
|
|
799
|
+
# context: 'Optional details about why the rule was violated'
|
|
800
|
+
# }
|
|
801
|
+
#
|
|
802
|
+
coffeelint.lint = (source, userConfig = {}) ->
|
|
803
|
+
config = mergeDefaultConfig(userConfig)
|
|
804
|
+
|
|
805
|
+
# Check ahead for inline enabled rules
|
|
806
|
+
disabled_initially = []
|
|
807
|
+
for l in source.split('\n')
|
|
808
|
+
s = regexes.configStatement.exec(l)
|
|
809
|
+
if s? and s.length > 2 and 'enable' in s
|
|
810
|
+
for r in s[1..]
|
|
811
|
+
unless r in ['enable','disable']
|
|
812
|
+
unless r of config and config[r].level in ['warn','error']
|
|
813
|
+
disabled_initially.push r
|
|
814
|
+
config[r] = { level: 'error' }
|
|
815
|
+
|
|
816
|
+
# Do AST linting first so all compile errors are caught.
|
|
817
|
+
astErrors = new ASTLinter(source, config).lint()
|
|
818
|
+
|
|
819
|
+
# Do lexical linting.
|
|
820
|
+
lexicalLinter = new LexicalLinter(source, config)
|
|
821
|
+
lexErrors = lexicalLinter.lint()
|
|
822
|
+
|
|
823
|
+
# Do line linting.
|
|
824
|
+
tokensByLine = lexicalLinter.tokensByLine
|
|
825
|
+
lineLinter = new LineLinter(source, config, tokensByLine)
|
|
826
|
+
lineErrors = lineLinter.lint()
|
|
827
|
+
|
|
828
|
+
# Sort by line number and return.
|
|
829
|
+
errors = lexErrors.concat(lineErrors, astErrors)
|
|
830
|
+
errors.sort((a, b) -> a.lineNumber - b.lineNumber)
|
|
831
|
+
|
|
832
|
+
# Helper to remove rules from disabled list
|
|
833
|
+
difference = (a, b) ->
|
|
834
|
+
j = 0
|
|
835
|
+
while j < a.length
|
|
836
|
+
if a[j] in b
|
|
837
|
+
a.splice(j, 1)
|
|
838
|
+
else
|
|
839
|
+
j++
|
|
840
|
+
|
|
841
|
+
# Disable/enable rules for inline blocks
|
|
842
|
+
all_errors = errors
|
|
843
|
+
errors = []
|
|
844
|
+
disabled = disabled_initially
|
|
845
|
+
next_line = 0
|
|
846
|
+
for i in [0...source.split('\n').length]
|
|
847
|
+
for cmd of block_config
|
|
848
|
+
rules = block_config[cmd][i]
|
|
849
|
+
{
|
|
850
|
+
'disable': ->
|
|
851
|
+
disabled = disabled.concat(rules)
|
|
852
|
+
'enable': ->
|
|
853
|
+
difference(disabled, rules)
|
|
854
|
+
disabled = disabled_initially if rules.length is 0
|
|
855
|
+
}[cmd]() if rules?
|
|
856
|
+
# advance line and append relevant messages
|
|
857
|
+
while next_line is i and all_errors.length > 0
|
|
858
|
+
next_line = all_errors[0].lineNumber - 1
|
|
859
|
+
e = all_errors[0]
|
|
860
|
+
if e.lineNumber is i + 1 or not e.lineNumber?
|
|
861
|
+
e = all_errors.shift()
|
|
862
|
+
errors.push e unless e.rule in disabled
|
|
863
|
+
|
|
864
|
+
block_config =
|
|
865
|
+
'enable': {}
|
|
866
|
+
'disable': {}
|
|
867
|
+
|
|
868
|
+
errors
|
data/lib/coffeelint.rb
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require "coffeelint/version"
|
|
2
|
+
require 'execjs'
|
|
3
|
+
require 'coffee-script'
|
|
4
|
+
|
|
5
|
+
module Coffeelint
|
|
6
|
+
require 'coffeelint/railtie' if defined?(Rails)
|
|
7
|
+
|
|
8
|
+
def self.path()
|
|
9
|
+
@path ||= File.expand_path('../../coffeelint/src/coffeelint.coffee', __FILE__)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.lint(script)
|
|
13
|
+
coffeescriptSource = File.read(CoffeeScript::Source.path)
|
|
14
|
+
coffeelintSource = CoffeeScript.compile(File.read(Coffeelint.path))
|
|
15
|
+
context = ExecJS.compile(coffeescriptSource + coffeelintSource)
|
|
16
|
+
context.call('coffeelint.lint', script)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.lint_file(filename)
|
|
20
|
+
Coffeelint.lint(File.read(filename))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.lint_dir(directory)
|
|
24
|
+
retval = {}
|
|
25
|
+
Dir.glob("#{directory}/**/*.coffee") do |name|
|
|
26
|
+
retval[name] = Coffeelint.lint_file(name)
|
|
27
|
+
yield name, retval[name]
|
|
28
|
+
end
|
|
29
|
+
retval
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
desc "lint application javascript"
|
|
2
|
+
task :coffeelint do
|
|
3
|
+
success = true
|
|
4
|
+
Coffeelint.lint_dir('.') do |name, errors|
|
|
5
|
+
name = name[2..-1]
|
|
6
|
+
|
|
7
|
+
good = "\u2713"
|
|
8
|
+
bad = "\u2717"
|
|
9
|
+
|
|
10
|
+
if errors.length == 0
|
|
11
|
+
puts " #{good} \e[1m\e[32m#{name}\e[0m"
|
|
12
|
+
else
|
|
13
|
+
success = false
|
|
14
|
+
puts " #{bad} \e[1m\e[31m#{name}\e[0m"
|
|
15
|
+
errors.each do |error|
|
|
16
|
+
puts " #{bad} \e[31m##{error["lineNumber"]}\e[0m: #{error["message"]}, #{error["context"]}."
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
fail "Lint!" unless success
|
|
22
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: coffeelint
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
prerelease:
|
|
6
|
+
platform: ruby
|
|
7
|
+
authors:
|
|
8
|
+
- Zachary Bush
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2013-05-06 00:00:00.000000000 Z
|
|
13
|
+
dependencies:
|
|
14
|
+
- !ruby/object:Gem::Dependency
|
|
15
|
+
name: coffee-script
|
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
|
17
|
+
none: false
|
|
18
|
+
requirements:
|
|
19
|
+
- - ! '>='
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '0'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
none: false
|
|
26
|
+
requirements:
|
|
27
|
+
- - ! '>='
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '0'
|
|
30
|
+
description: Ruby bindings for coffeelint
|
|
31
|
+
email:
|
|
32
|
+
- zach@zmbush.com
|
|
33
|
+
executables: []
|
|
34
|
+
extensions: []
|
|
35
|
+
extra_rdoc_files: []
|
|
36
|
+
files:
|
|
37
|
+
- .gitignore
|
|
38
|
+
- .gitmodules
|
|
39
|
+
- Gemfile
|
|
40
|
+
- LICENSE.txt
|
|
41
|
+
- README.md
|
|
42
|
+
- Rakefile
|
|
43
|
+
- coffeelint.gemspec
|
|
44
|
+
- lib/coffeelint.rb
|
|
45
|
+
- lib/coffeelint/railtie.rb
|
|
46
|
+
- lib/coffeelint/version.rb
|
|
47
|
+
- lib/tasks/coffeelint.rake
|
|
48
|
+
- coffeelint/src/coffeelint.coffee
|
|
49
|
+
homepage: https://github.com/zipcodeman/coffeelint-ruby
|
|
50
|
+
licenses: []
|
|
51
|
+
post_install_message:
|
|
52
|
+
rdoc_options: []
|
|
53
|
+
require_paths:
|
|
54
|
+
- lib
|
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
56
|
+
none: false
|
|
57
|
+
requirements:
|
|
58
|
+
- - ! '>='
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
62
|
+
none: false
|
|
63
|
+
requirements:
|
|
64
|
+
- - ! '>='
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '0'
|
|
67
|
+
requirements: []
|
|
68
|
+
rubyforge_project:
|
|
69
|
+
rubygems_version: 1.8.24
|
|
70
|
+
signing_key:
|
|
71
|
+
specification_version: 3
|
|
72
|
+
summary: Ruby bindings for coffeelint along with railtie to add rake task to rails
|
|
73
|
+
test_files: []
|