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 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: []