coffeelint 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.gitmodules ADDED
@@ -0,0 +1,3 @@
1
+ [submodule "coffeelint"]
2
+ path = coffeelint
3
+ url = git://github.com/zipcodeman/coffeelint.git
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in coffeelint.gemspec
4
+ gemspec
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
@@ -0,0 +1,5 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ task :console do
4
+ sh "irb -rubygems -I lib -r coffeelint.rb"
5
+ end
@@ -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,11 @@
1
+ require 'coffeelint'
2
+ require 'rails'
3
+ module Coffeelint
4
+ class Railtie < Rails::Railtie
5
+ railtie_name :coffeelint
6
+
7
+ rake_tasks do
8
+ load 'tasks/coffeelint.rake'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ module Coffeelint
2
+ VERSION = "0.0.1"
3
+ 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: []