coffeelint 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|