coffeelint 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -7,7 +7,7 @@ module Coffeelint
7
7
  require 'coffeelint/railtie' if defined?(Rails)
8
8
 
9
9
  def self.path()
10
- @path ||= File.expand_path('../../coffeelint/src/coffeelint.coffee', __FILE__)
10
+ @path ||= File.expand_path('../../coffeelint/lib/coffeelint.js', __FILE__)
11
11
  end
12
12
 
13
13
  def self.colorize(str, color_code)
@@ -22,15 +22,24 @@ module Coffeelint
22
22
  pretty_output ? Coffeelint.colorize(str, 32) : str
23
23
  end
24
24
 
25
+ def self.context
26
+ coffeescriptSource = File.read(CoffeeScript::Source.path)
27
+ bootstrap = <<-EOF
28
+ window = {
29
+ CoffeeScript: CoffeeScript,
30
+ coffeelint: {}
31
+ };
32
+ EOF
33
+ coffeelintSource = File.read(Coffeelint.path)
34
+ ExecJS.compile(coffeescriptSource + bootstrap + coffeelintSource)
35
+ end
36
+
25
37
  def self.lint(script, config = {})
26
38
  if !config[:config_file].nil?
27
39
  fname = config.delete(:config_file)
28
40
  config.merge!(JSON.parse(File.read(fname)))
29
41
  end
30
- coffeescriptSource = File.read(CoffeeScript::Source.path)
31
- coffeelintSource = CoffeeScript.compile(File.read(Coffeelint.path))
32
- context = ExecJS.compile(coffeescriptSource + coffeelintSource)
33
- context.call('coffeelint.lint', script, config)
42
+ Coffeelint.context.call('window.coffeelint.lint', script, config)
34
43
  end
35
44
 
36
45
  def self.lint_file(filename, config = {})
@@ -1,3 +1,3 @@
1
1
  module Coffeelint
2
- VERSION = "0.1.3"
2
+ VERSION = "0.2.0"
3
3
  end
metadata CHANGED
@@ -1,61 +1,79 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: coffeelint
3
- version: !ruby/object:Gem::Version
4
- version: 0.1.3
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
5
  platform: ruby
6
- authors:
6
+ authors:
7
7
  - Zachary Bush
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
-
12
- date: 2013-11-14 00:00:00 Z
13
- dependencies:
14
- - !ruby/object:Gem::Dependency
11
+ date: 2014-01-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
15
14
  name: coffee-script
16
- prerelease: false
17
- requirement: &id001 !ruby/object:Gem::Requirement
18
- requirements:
19
- - &id002
20
- - ">="
21
- - !ruby/object:Gem::Version
22
- version: "0"
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
23
20
  type: :runtime
24
- version_requirements: *id001
25
- - !ruby/object:Gem::Dependency
26
- name: json
27
21
  prerelease: false
28
- requirement: &id003 !ruby/object:Gem::Requirement
29
- requirements:
30
- - *id002
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: json
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
31
34
  type: :runtime
32
- version_requirements: *id003
33
- - !ruby/object:Gem::Dependency
34
- name: rspec
35
35
  prerelease: false
36
- requirement: &id004 !ruby/object:Gem::Requirement
37
- requirements:
38
- - *id002
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
39
48
  type: :development
40
- version_requirements: *id004
41
- - !ruby/object:Gem::Dependency
42
- name: rake
43
49
  prerelease: false
44
- requirement: &id005 !ruby/object:Gem::Requirement
45
- requirements:
46
- - *id002
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
47
62
  type: :development
48
- version_requirements: *id005
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
49
69
  description: Ruby bindings for coffeelint
50
- email:
70
+ email:
51
71
  - zach@zmbush.com
52
- executables:
72
+ executables:
53
73
  - coffeelint.rb
54
74
  extensions: []
55
-
56
75
  extra_rdoc_files: []
57
-
58
- files:
76
+ files:
59
77
  - .gitignore
60
78
  - .gitmodules
61
79
  - Gemfile
@@ -70,30 +88,31 @@ files:
70
88
  - lib/tasks/coffeelint.rake
71
89
  - spec/coffeelint_spec.rb
72
90
  - spec/spec_helper.rb
73
- - coffeelint/src/coffeelint.coffee
91
+ - coffeelint/lib/coffeelint.js
74
92
  homepage: https://github.com/zipcodeman/coffeelint-ruby
75
- licenses:
93
+ licenses:
76
94
  - MIT
77
95
  metadata: {}
78
-
79
96
  post_install_message:
80
97
  rdoc_options: []
81
-
82
- require_paths:
98
+ require_paths:
83
99
  - lib
84
- required_ruby_version: !ruby/object:Gem::Requirement
85
- requirements:
86
- - *id002
87
- required_rubygems_version: !ruby/object:Gem::Requirement
88
- requirements:
89
- - *id002
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - '>='
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
90
110
  requirements: []
91
-
92
111
  rubyforge_project:
93
- rubygems_version: 2.1.9
112
+ rubygems_version: 2.0.14
94
113
  signing_key:
95
114
  specification_version: 4
96
115
  summary: Ruby bindings for coffeelint along with railtie to add rake task to rails
97
- test_files:
116
+ test_files:
98
117
  - spec/coffeelint_spec.rb
99
118
  - spec/spec_helper.rb
@@ -1,1125 +0,0 @@
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.7"
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
- description: """
42
- This rule forbids tabs in indentation. Enough said. It is enabled by
43
- default.
44
- """
45
-
46
- no_trailing_whitespace :
47
- level : ERROR
48
- message : 'Line ends with trailing whitespace'
49
- allowed_in_comments : false
50
- description: """
51
- This rule forbids trailing whitespace in your code, since it is
52
- needless cruft. It is enabled by default.
53
- """
54
-
55
- max_line_length :
56
- value: 80
57
- level : ERROR
58
- message : 'Line exceeds maximum allowed length'
59
- description: """
60
- This rule imposes a maximum line length on your code. <a
61
- href="http://www.python.org/dev/peps/pep-0008/">Python's style
62
- guide</a> does a good job explaining why you might want to limit the
63
- length of your lines, though this is a matter of taste.
64
-
65
- Lines can be no longer than eighty characters by default.
66
- """
67
-
68
- camel_case_classes :
69
- level : ERROR
70
- message : 'Class names should be camel cased'
71
- description: """
72
- This rule mandates that all class names are camel cased. Camel
73
- casing class names is a generally accepted way of distinguishing
74
- constructor functions - which require the 'new' prefix to behave
75
- properly - from plain old functions.
76
- <pre>
77
- <code># Good!
78
- class BoaConstrictor
79
-
80
- # Bad!
81
- class boaConstrictor
82
- </code>
83
- </pre>
84
- This rule is enabled by default.
85
- """
86
- indentation :
87
- value : 2
88
- level : ERROR
89
- message : 'Line contains inconsistent indentation'
90
- description: """
91
- This rule imposes a standard number of spaces to be used for
92
- indentation. Since whitespace is significant in CoffeeScript, it's
93
- critical that a project chooses a standard indentation format and
94
- stays consistent. Other roads lead to darkness. <pre> <code>#
95
- Enabling this option will prevent this ugly
96
- # but otherwise valid CoffeeScript.
97
- twoSpaces = () ->
98
- fourSpaces = () ->
99
- eightSpaces = () ->
100
- 'this is valid CoffeeScript'
101
-
102
- </code>
103
- </pre>
104
- Two space indentation is enabled by default.
105
- """
106
-
107
- no_implicit_braces :
108
- level : IGNORE
109
- message : 'Implicit braces are forbidden'
110
- description: """
111
- This rule prohibits implicit braces when declaring object literals.
112
- Implicit braces can make code more difficult to understand,
113
- especially when used in combination with optional parenthesis.
114
- <pre>
115
- <code># Do you find this code ambiguous? Is it a
116
- # function call with three arguments or four?
117
- myFunction a, b, 1:2, 3:4
118
-
119
- # While the same code written in a more
120
- # explicit manner has no ambiguity.
121
- myFunction(a, b, {1:2, 3:4})
122
- </code>
123
- </pre>
124
- Implicit braces are permitted by default, since their use is
125
- idiomatic CoffeeScript.
126
- """
127
-
128
- no_trailing_semicolons:
129
- level : ERROR
130
- message : 'Line contains a trailing semicolon'
131
- description: """
132
- This rule prohibits trailing semicolons, since they are needless
133
- cruft in CoffeeScript.
134
- <pre>
135
- <code># This semicolon is meaningful.
136
- x = '1234'; console.log(x)
137
-
138
- # This semicolon is redundant.
139
- alert('end of line');
140
- </code>
141
- </pre>
142
- Trailing semicolons are forbidden by default.
143
- """
144
-
145
- no_plusplus:
146
- level : IGNORE
147
- message : 'The increment and decrement operators are forbidden'
148
- description: """
149
- This rule forbids the increment and decrement arithmetic operators.
150
- Some people believe the <tt>++</tt> and <tt>--</tt> to be cryptic
151
- and the cause of bugs due to misunderstandings of their precedence
152
- rules.
153
- This rule is disabled by default.
154
- """
155
-
156
- no_throwing_strings:
157
- level : ERROR
158
- message : 'Throwing strings is forbidden'
159
- description: """
160
- This rule forbids throwing string literals or interpolations. While
161
- JavaScript (and CoffeeScript by extension) allow any expression to
162
- be thrown, it is best to only throw <a
163
- href="https://developer.mozilla.org
164
- /en/JavaScript/Reference/Global_Objects/Error"> Error</a> objects,
165
- because they contain valuable debugging information like the stack
166
- trace. Because of JavaScript's dynamic nature, CoffeeLint cannot
167
- ensure you are always throwing instances of <tt>Error</tt>. It will
168
- only catch the simple but real case of throwing literal strings.
169
- <pre>
170
- <code># CoffeeLint will catch this:
171
- throw "i made a boo boo"
172
-
173
- # ... but not this:
174
- throw getSomeString()
175
- </code>
176
- </pre>
177
- This rule is enabled by default.
178
- """
179
-
180
- cyclomatic_complexity:
181
- value : 10
182
- level : IGNORE
183
- message : 'The cyclomatic complexity is too damn high'
184
-
185
- no_backticks:
186
- level : ERROR
187
- message : 'Backticks are forbidden'
188
- description: """
189
- Backticks allow snippets of JavaScript to be embedded in
190
- CoffeeScript. While some folks consider backticks useful in a few
191
- niche circumstances, they should be avoided because so none of
192
- JavaScript's "bad parts", like <tt>with</tt> and <tt>eval</tt>,
193
- sneak into CoffeeScript.
194
- This rule is enabled by default.
195
- """
196
-
197
- line_endings:
198
- level : IGNORE
199
- value : 'unix' # or 'windows'
200
- message : 'Line contains incorrect line endings'
201
- description: """
202
- This rule ensures your project uses only <tt>windows</tt> or
203
- <tt>unix</tt> line endings. This rule is disabled by default.
204
- """
205
- no_implicit_parens :
206
- level : IGNORE
207
- message : 'Implicit parens are forbidden'
208
- description: """
209
- This rule prohibits implicit parens on function calls.
210
- <pre>
211
- <code># Some folks don't like this style of coding.
212
- myFunction a, b, c
213
-
214
- # And would rather it always be written like this:
215
- myFunction(a, b, c)
216
- </code>
217
- </pre>
218
- Implicit parens are permitted by default, since their use is
219
- idiomatic CoffeeScript.
220
- """
221
-
222
- empty_constructor_needs_parens :
223
- level : IGNORE
224
- message : 'Invoking a constructor without parens and without arguments'
225
-
226
- non_empty_constructor_needs_parens :
227
- level : IGNORE
228
- message : 'Invoking a constructor without parens and with arguments'
229
-
230
- no_empty_param_list :
231
- level : IGNORE
232
- message : 'Empty parameter list is forbidden'
233
- description: """
234
- This rule prohibits empty parameter lists in function definitions.
235
- <pre>
236
- <code># The empty parameter list in here is unnecessary:
237
- myFunction = () -&gt;
238
-
239
- # We might favor this instead:
240
- myFunction = -&gt;
241
- </code>
242
- </pre>
243
- Empty parameter lists are permitted by default.
244
- """
245
-
246
-
247
- space_operators :
248
- level : IGNORE
249
- message : 'Operators must be spaced properly'
250
-
251
- # I don't know of any legitimate reason to define duplicate keys in an
252
- # object. It seems to always be a mistake, it's also a syntax error in
253
- # strict mode.
254
- # See http://jslinterrors.com/duplicate-key-a/
255
- duplicate_key :
256
- level : ERROR
257
- message : 'Duplicate key defined in object or class'
258
-
259
- newlines_after_classes :
260
- value : 3
261
- level : IGNORE
262
- message : 'Wrong count of newlines between a class and other code'
263
-
264
- no_stand_alone_at :
265
- level : IGNORE
266
- message : '@ must not be used stand alone'
267
- description: """
268
- This rule checks that no stand alone @ are in use, they are
269
- discouraged. Further information in CoffeScript issue <a
270
- href="https://github.com/jashkenas/coffee-script/issues/1601">
271
- #1601</a>
272
- """
273
-
274
- arrow_spacing :
275
- level : IGNORE
276
- message : 'Function arrow (->) must be spaced properly'
277
- description: """
278
- <p>This rule checks to see that there is spacing before and after
279
- the arrow operator that declares a function. This rule is disabled
280
- by default.</p> <p>Note that if arrow_spacing is enabled, and you
281
- pass an empty function as a parameter, arrow_spacing will accept
282
- either a space or no space in-between the arrow operator and the
283
- parenthesis</p>
284
- <pre><code># Both of this will not trigger an error,
285
- # even with arrow_spacing enabled.
286
- x(-> 3)
287
- x( -> 3)
288
-
289
- # However, this will trigger an error
290
- x((a,b)-> 3)
291
- </code>
292
- </pre>
293
- """
294
-
295
- coffeescript_error :
296
- level : ERROR
297
- message : '' # The default coffeescript error is fine.
298
-
299
-
300
- # Some repeatedly used regular expressions.
301
- regexes =
302
- trailingWhitespace : /[^\s]+[\t ]+\r?$/
303
- lineHasComment : /^\s*[^\#]*\#/
304
- indentation: /\S/
305
- longUrlComment: ///
306
- ^\s*\# # indentation, up to comment
307
- \s*
308
- http[^\s]+$ # Link that takes up the rest of the line without spaces.
309
- ///
310
- camelCase: /^[A-Z][a-zA-Z\d]*$/
311
- trailingSemicolon: /;\r?$/
312
- configStatement: /coffeelint:\s*(disable|enable)(?:=([\w\s,]*))?/
313
-
314
-
315
- # Patch the source properties onto the destination.
316
- extend = (destination, sources...) ->
317
- for source in sources
318
- (destination[k] = v for k, v of source)
319
- return destination
320
-
321
- # Patch any missing attributes from defaults to source.
322
- defaults = (source, defaults) ->
323
- extend({}, defaults, source)
324
-
325
-
326
- # Create an error object for the given rule with the given
327
- # attributes.
328
- createError = (rule, attrs = {}) ->
329
- level = attrs.level
330
- if level not in [IGNORE, WARN, ERROR]
331
- throw new Error("unknown level #{level}")
332
-
333
- if level in [ERROR, WARN]
334
- attrs.rule = rule
335
- return defaults(attrs, RULES[rule])
336
- else
337
- null
338
-
339
- # Store suppressions in the form of { line #: type }
340
- block_config =
341
- enable: {}
342
- disable: {}
343
-
344
- #
345
- # A class that performs regex checks on each line of the source.
346
- #
347
- class LineLinter
348
-
349
- constructor : (source, config, tokensByLine) ->
350
- @source = source
351
- @config = config
352
- @line = null
353
- @lineNumber = 0
354
- @tokensByLine = tokensByLine
355
- @lines = @source.split('\n')
356
- @lineCount = @lines.length
357
-
358
- # maintains some contextual information
359
- # inClass: bool; in class or not
360
- # lastUnemptyLineInClass: null or lineNumber, if the last not-empty
361
- # line was in a class it holds its number
362
- # classIndents: the number of indents within a class
363
- @context = {
364
- class: {
365
- inClass: false
366
- lastUnemptyLineInClass: null
367
- classIndents: null
368
- }
369
- }
370
-
371
- lint : () ->
372
- errors = []
373
- for line, lineNumber in @lines
374
- @lineNumber = lineNumber
375
- @line = line
376
- @maintainClassContext()
377
- error = @lintLine()
378
- errors.push(error) if error
379
- errors
380
-
381
- # Return an error if the line contained failed a rule, null otherwise.
382
- lintLine : () ->
383
- return @checkTabs() or
384
- @checkTrailingWhitespace() or
385
- @checkLineLength() or
386
- @checkTrailingSemicolon() or
387
- @checkLineEndings() or
388
- @checkComments() or
389
- @checkNewlinesAfterClasses()
390
-
391
- checkTabs : () ->
392
- # Only check lines that have compiled tokens. This helps
393
- # us ignore tabs in the middle of multi line strings, heredocs, etc.
394
- # since they are all reduced to a single token whose line number
395
- # is the start of the expression.
396
- indentation = @line.split(regexes.indentation)[0]
397
- if @lineHasToken() and '\t' in indentation
398
- @createLineError('no_tabs')
399
- else
400
- null
401
-
402
- checkTrailingWhitespace : () ->
403
- if regexes.trailingWhitespace.test(@line)
404
- # By default only the regex above is needed.
405
- if !@config['no_trailing_whitespace']?.allowed_in_comments
406
- return @createLineError('no_trailing_whitespace')
407
-
408
- line = @line
409
- tokens = @tokensByLine[@lineNumber]
410
-
411
- # If we're in a block comment there won't be any tokens on this
412
- # line. Some previous line holds the token spanning multiple lines.
413
- if !tokens
414
- return null
415
-
416
- # To avoid confusion when a string might contain a "#", every string
417
- # on this line will be removed. before checking for a comment
418
- for str in (token[1] for token in tokens when token[0] == 'STRING')
419
- line = line.replace(str, 'STRING')
420
-
421
- if !regexes.lineHasComment.test(line)
422
- return @createLineError('no_trailing_whitespace')
423
- else
424
- return null
425
- else
426
- return null
427
-
428
- checkLineLength : () ->
429
- rule = 'max_line_length'
430
- max = @config[rule]?.value
431
- if max and max < @line.length and not regexes.longUrlComment.test(@line)
432
- attrs =
433
- context: "Length is #{@line.length}, max is #{max}"
434
- @createLineError(rule, attrs)
435
- else
436
- null
437
-
438
- checkTrailingSemicolon : () ->
439
- hasSemicolon = regexes.trailingSemicolon.test(@line)
440
- [first..., last] = @getLineTokens()
441
- hasNewLine = last and last.newLine?
442
- # Don't throw errors when the contents of multiline strings,
443
- # regexes and the like end in ";"
444
- if hasSemicolon and not hasNewLine and @lineHasToken()
445
- @createLineError('no_trailing_semicolons')
446
- else
447
- return null
448
-
449
- checkLineEndings : () ->
450
- rule = 'line_endings'
451
- ending = @config[rule]?.value
452
-
453
- return null if not ending or @isLastLine() or not @line
454
-
455
- lastChar = @line[@line.length - 1]
456
- valid = if ending == 'windows'
457
- lastChar == '\r'
458
- else if ending == 'unix'
459
- lastChar != '\r'
460
- else
461
- throw new Error("unknown line ending type: #{ending}")
462
- if not valid
463
- return @createLineError(rule, {context:"Expected #{ending}"})
464
- else
465
- return null
466
-
467
- checkComments : () ->
468
- # Check for block config statements enable and disable
469
- result = regexes.configStatement.exec(@line)
470
- if result?
471
- cmd = result[1]
472
- rules = []
473
- if result[2]?
474
- for r in result[2].split(',')
475
- rules.push r.replace(/^\s+|\s+$/g, "")
476
- block_config[cmd][@lineNumber] = rules
477
- return null
478
-
479
- checkNewlinesAfterClasses : () ->
480
- rule = 'newlines_after_classes'
481
- ending = @config[rule].value
482
-
483
- return null if not ending or @isLastLine()
484
-
485
- if not @context.class.inClass and
486
- @context.class.lastUnemptyLineInClass? and
487
- ((@lineNumber - 1) - @context.class.lastUnemptyLineInClass) isnt
488
- ending
489
- got = (@lineNumber - 1) - @context.class.lastUnemptyLineInClass
490
- return @createLineError( rule, {
491
- context: "Expected #{ending} got #{got}"
492
- } )
493
-
494
- null
495
-
496
- createLineError : (rule, attrs = {}) ->
497
- attrs.lineNumber = @lineNumber + 1 # Lines are indexed by zero.
498
- attrs.level = @config[rule]?.level
499
- createError(rule, attrs)
500
-
501
- isLastLine : () ->
502
- return @lineNumber == @lineCount - 1
503
-
504
- # Return true if the given line actually has tokens.
505
- # Optional parameter to check for a specific token type and line number.
506
- lineHasToken : (tokenType = null, lineNumber = null) ->
507
- lineNumber = lineNumber ? @lineNumber
508
- unless tokenType?
509
- return @tokensByLine[lineNumber]?
510
- else
511
- tokens = @tokensByLine[lineNumber]
512
- return null unless tokens?
513
- for token in tokens
514
- return true if token[0] == tokenType
515
- return false
516
-
517
- # Return tokens for the given line number.
518
- getLineTokens : () ->
519
- @tokensByLine[@lineNumber] || []
520
-
521
- # maintain the contextual information for class-related stuff
522
- maintainClassContext: () ->
523
- if @context.class.inClass
524
- if @lineHasToken 'INDENT'
525
- @context.class.classIndents++
526
- else if @lineHasToken 'OUTDENT'
527
- @context.class.classIndents--
528
- if @context.class.classIndents is 0
529
- @context.class.inClass = false
530
- @context.class.classIndents = null
531
-
532
- if @context.class.inClass and not @line.match( /^\s*$/ )
533
- @context.class.lastUnemptyLineInClass = @lineNumber
534
- else
535
- unless @line.match(/\\s*/)
536
- @context.class.lastUnemptyLineInClass = null
537
-
538
- if @lineHasToken 'CLASS'
539
- @context.class.inClass = true
540
- @context.class.lastUnemptyLineInClass = @lineNumber
541
- @context.class.classIndents = 0
542
-
543
- null
544
-
545
- #
546
- # A class that performs checks on the output of CoffeeScript's lexer.
547
- #
548
- class LexicalLinter
549
-
550
- constructor : (source, config) ->
551
- @source = source
552
- @tokens = CoffeeScript.tokens(source)
553
- @config = config
554
- @i = 0 # The index of the current token we're linting.
555
- @tokensByLine = {} # A map of tokens by line.
556
- @arrayTokens = [] # A stack tracking the array token pairs.
557
- @parenTokens = [] # A stack tracking the parens token pairs.
558
- @callTokens = [] # A stack tracking the call token pairs.
559
- @lines = source.split('\n')
560
- @braceScopes = [] # A stack tracking keys defined in nexted scopes.
561
-
562
- # Return a list of errors encountered in the given source.
563
- lint : () ->
564
- errors = []
565
-
566
- for token, i in @tokens
567
- @i = i
568
- error = @lintToken(token)
569
- errors.push(error) if error
570
- errors
571
-
572
- # Return an error if the given token fails a lint check, false otherwise.
573
- lintToken : (token) ->
574
- [type, value, lineNumber] = token
575
-
576
- if typeof lineNumber == "object"
577
- if type == 'OUTDENT' or type == 'INDENT'
578
- lineNumber = lineNumber.last_line
579
- else
580
- lineNumber = lineNumber.first_line
581
- @tokensByLine[lineNumber] ?= []
582
- @tokensByLine[lineNumber].push(token)
583
- # CoffeeScript loses line numbers of interpolations and multi-line
584
- # regexes, so fake it by using the last line number we know.
585
- @lineNumber = lineNumber or @lineNumber or 0
586
- # Now lint it.
587
- switch type
588
- when "->" then @lintArrowSpacing(token)
589
- when "INDENT" then @lintIndentation(token)
590
- when "CLASS" then @lintClass(token)
591
- when "UNARY" then @lintUnary(token)
592
- when "{","}" then @lintBrace(token)
593
- when "IDENTIFIER" then @lintIdentifier(token)
594
- when "++", "--" then @lintIncrement(token)
595
- when "THROW" then @lintThrow(token)
596
- when "[", "]" then @lintArray(token)
597
- when "(", ")" then @lintParens(token)
598
- when "JS" then @lintJavascript(token)
599
- when "CALL_START", "CALL_END" then @lintCall(token)
600
- when "PARAM_START" then @lintParam(token)
601
- when "@" then @lintStandaloneAt(token)
602
- when "+", "-" then @lintPlus(token)
603
- when "=", "MATH", "COMPARE", "LOGIC", "COMPOUND_ASSIGN"
604
- @lintMath(token)
605
- else null
606
-
607
- lintUnary: (token) ->
608
- if token[1] is 'new'
609
- # Find the last chained identifier, e.g. Bar in new foo.bar.Bar().
610
- identifierIndex = 1
611
- loop
612
- expectedIdentifier = @peek(identifierIndex)
613
- expectedCallStart = @peek(identifierIndex + 1)
614
- if expectedIdentifier?[0] is 'IDENTIFIER'
615
- if expectedCallStart?[0] is '.'
616
- identifierIndex += 2
617
- continue
618
- break
619
-
620
- # The callStart is generated if your parameters are all on the same
621
- # line with implicit parens, and if your parameters start on the
622
- # next line, but is missing if there are no params and no parens.
623
- if expectedIdentifier?[0] is 'IDENTIFIER' and expectedCallStart?
624
- if expectedCallStart[0] is 'CALL_START'
625
- if expectedCallStart.generated
626
- @createLexError('non_empty_constructor_needs_parens')
627
- else
628
- @createLexError('empty_constructor_needs_parens')
629
-
630
- # Lint the given array token.
631
- lintArray : (token) ->
632
- # Track the array token pairs
633
- if token[0] == '['
634
- @arrayTokens.push(token)
635
- else if token[0] == ']'
636
- @arrayTokens.pop()
637
- # Return null, since we're not really linting
638
- # anything here.
639
- null
640
-
641
- lintParens : (token) ->
642
- if token[0] == '('
643
- p1 = @peek(-1)
644
- n1 = @peek(1)
645
- n2 = @peek(2)
646
- # String interpolations start with '' + so start the type co-ercion,
647
- # so track if we're inside of one. This is most definitely not
648
- # 100% true but what else can we do?
649
- i = n1 and n2 and n1[0] == 'STRING' and n2[0] == '+'
650
- token.isInterpolation = i
651
- @parenTokens.push(token)
652
- else
653
- @parenTokens.pop()
654
- # We're not linting, just tracking interpolations.
655
- null
656
-
657
- isInInterpolation : () ->
658
- for t in @parenTokens
659
- return true if t.isInterpolation
660
- return false
661
-
662
- isInExtendedRegex : () ->
663
- for t in @callTokens
664
- return true if t.isRegex
665
- return false
666
-
667
- lintPlus : (token) ->
668
- # We can't check this inside of interpolations right now, because the
669
- # plusses used for the string type co-ercion are marked not spaced.
670
- return null if @isInInterpolation() or @isInExtendedRegex()
671
-
672
- p = @peek(-1)
673
- unaries = ['TERMINATOR', '(', '=', '-', '+', ',', 'CALL_START',
674
- 'INDEX_START', '..', '...', 'COMPARE', 'IF',
675
- 'THROW', 'LOGIC', 'POST_IF', ':', '[', 'INDENT',
676
- 'COMPOUND_ASSIGN', 'RETURN', 'MATH']
677
- isUnary = if not p then false else p[0] in unaries
678
- if (isUnary and token.spaced) or
679
- (not isUnary and not token.spaced and not token.newLine)
680
- @createLexError('space_operators', {context: token[1]})
681
- else
682
- null
683
-
684
- lintMath: (token) ->
685
- if not token.spaced and not token.newLine
686
- @createLexError('space_operators', {context: token[1]})
687
- else
688
- null
689
-
690
- lintCall : (token) ->
691
- if token[0] == 'CALL_START'
692
- p = @peek(-1)
693
- # Track regex calls, to know (approximately) if we're in an
694
- # extended regex.
695
- token.isRegex = p and p[0] == 'IDENTIFIER' and p[1] == 'RegExp'
696
- @callTokens.push(token)
697
- if token.generated
698
- return @createLexError('no_implicit_parens')
699
- else
700
- return null
701
- else
702
- @callTokens.pop()
703
- return null
704
-
705
- lintParam : (token) ->
706
- nextType = @peek()[0]
707
- if nextType == 'PARAM_END'
708
- @createLexError('no_empty_param_list')
709
- else
710
- null
711
-
712
- lintIdentifier: (token) ->
713
- key = token[1]
714
-
715
- # Class names might not be in a scope
716
- return null if not @currentScope?
717
- nextToken = @peek(1)
718
-
719
- # Exit if this identifier isn't being assigned. A and B
720
- # are identifiers, but only A should be examined:
721
- # A = B
722
- return null if nextToken[1] isnt ':'
723
- previousToken = @peek(-1)
724
-
725
- # Assigning "@something" and "something" are not the same thing
726
- key = "@#{key}" if previousToken[0] == '@'
727
-
728
- # Added a prefix to not interfere with things like "constructor".
729
- key = "identifier-#{key}"
730
- if @currentScope[key]
731
- @createLexError('duplicate_key')
732
- else
733
- @currentScope[key] = token
734
- null
735
-
736
- lintBrace : (token) ->
737
- if token[0] == '{'
738
- @braceScopes.push @currentScope if @currentScope?
739
- @currentScope = {}
740
- else
741
- @currentScope = @braceScopes.pop()
742
-
743
- if token.generated and token[0] == '{'
744
- # Peek back to the last line break. If there is a class
745
- # definition, ignore the generated brace.
746
- i = -1
747
- loop
748
- t = @peek(i)
749
- if not t? or t[0] == 'TERMINATOR'
750
- return @createLexError('no_implicit_braces')
751
- if t[0] == 'CLASS'
752
- return null
753
- i -= 1
754
- else
755
- return null
756
-
757
- lintJavascript :(token) ->
758
- @createLexError('no_backticks')
759
-
760
- lintThrow : (token) ->
761
- [n1, n2] = [@peek(), @peek(2)]
762
- # Catch literals and string interpolations, which are wrapped in
763
- # parens.
764
- nextIsString = n1[0] == 'STRING' or (n1[0] == '(' and n2[0] == 'STRING')
765
- @createLexError('no_throwing_strings') if nextIsString
766
-
767
- lintIncrement : (token) ->
768
- attrs = {context : "found '#{token[0]}'"}
769
- @createLexError('no_plusplus', attrs)
770
-
771
- lintStandaloneAt: (token) ->
772
- nextToken = @peek()
773
- spaced = token.spaced
774
- isIdentifier = nextToken[0] == 'IDENTIFIER'
775
- isIndexStart = nextToken[0] == 'INDEX_START'
776
- isDot = nextToken[0] == '.'
777
-
778
- # https://github.com/jashkenas/coffee-script/issues/1601
779
- # @::foo is valid, but @:: behaves inconsistently and is planned for
780
- # removal. Technically @:: is a stand alone ::, but I think it makes
781
- # sense to group it into no_stand_alone_at
782
- if nextToken[0] == '::'
783
- protoProperty = @peek(2)
784
- isValidProtoProperty = protoProperty[0] == 'IDENTIFIER'
785
-
786
- if spaced or (not isIdentifier and not isIndexStart and
787
- not isDot and not isValidProtoProperty)
788
- @createLexError('no_stand_alone_at')
789
-
790
-
791
- # Return an error if the given indentation token is not correct.
792
- lintIndentation : (token) ->
793
- [type, numIndents, lineNumber] = token
794
-
795
- return null if token.generated?
796
-
797
- # HACK: CoffeeScript's lexer insert indentation in string
798
- # interpolations that start with spaces e.g. "#{ 123 }"
799
- # so ignore such cases. Are there other times an indentation
800
- # could possibly follow a '+'?
801
- previous = @peek(-2)
802
- isInterpIndent = previous and previous[0] == '+'
803
-
804
- # Ignore the indentation inside of an array, so that
805
- # we can allow things like:
806
- # x = ["foo",
807
- # "bar"]
808
- previous = @peek(-1)
809
- isArrayIndent = @inArray() and previous?.newLine
810
-
811
- # Ignore indents used to for formatting on multi-line expressions, so
812
- # we can allow things like:
813
- # a = b =
814
- # c = d
815
- previousSymbol = @peek(-1)?[0]
816
- isMultiline = previousSymbol in ['=', ',']
817
-
818
- # Summarize the indentation conditions we'd like to ignore
819
- ignoreIndent = isInterpIndent or isArrayIndent or isMultiline
820
-
821
- # Compensate for indentation in function invocations that span multiple
822
- # lines, which can be ignored.
823
- if @isChainedCall()
824
- currentLine = @lines[@lineNumber]
825
- prevNum = 1
826
-
827
- # keep going back until we are not at a comment or a blank line
828
- prevNum += 1 while (/^\s*(#|$)/.test(@lines[@lineNumber - prevNum]))
829
- previousLine = @lines[@lineNumber - prevNum]
830
-
831
- previousIndentation = previousLine.match(/^(\s*)/)[1].length
832
- # I don't know why, but when inside a function, you make a chained
833
- # call and define an inline callback as a parameter, the body of
834
- # that callback gets the indentation reported higher than it really
835
- # is. See issue #88
836
- # NOTE: Adding this line moved the cyclomatic complexity over the
837
- # limit, I'm not sure why
838
- numIndents = currentLine.match(/^(\s*)/)[1].length
839
- numIndents -= previousIndentation
840
-
841
-
842
- # Now check the indentation.
843
- expected = @config['indentation'].value
844
- if not ignoreIndent and numIndents != expected
845
- context = "Expected #{expected} " +
846
- "got #{numIndents}"
847
- @createLexError('indentation', {context})
848
- else
849
- null
850
-
851
- lintClass : (token) ->
852
- # TODO: you can do some crazy shit in CoffeeScript, like
853
- # class func().ClassName. Don't allow that.
854
-
855
- # Don't try to lint the names of anonymous classes.
856
- return null if token.newLine? or @peek()[0] in ['INDENT', 'EXTENDS']
857
-
858
- # It's common to assign a class to a global namespace, e.g.
859
- # exports.MyClassName, so loop through the next tokens until
860
- # we find the real identifier.
861
- className = null
862
- offset = 1
863
- until className
864
- if @peek(offset + 1)?[0] == '.'
865
- offset += 2
866
- else if @peek(offset)?[0] == '@'
867
- offset += 1
868
- else
869
- className = @peek(offset)[1]
870
-
871
- # Now check for the error.
872
- if not regexes.camelCase.test(className)
873
- attrs = {context: "class name: #{className}"}
874
- @createLexError('camel_case_classes', attrs)
875
- else
876
- null
877
-
878
- lintArrowSpacing : (token) ->
879
- # Throw error unless the following happens.
880
- #
881
- # We will take a look at the previous token to see
882
- # 1. That the token is properly spaced
883
- # 2. Wasn't generated by the CoffeeScript compiler
884
- # 3. That it is just indentation
885
- # 4. If the function declaration has no parameters
886
- # e.g. x(-> 3)
887
- # x( -> 3)
888
- #
889
- # or a statement is wrapped in parentheses
890
- # e.g. (-> true)()
891
- #
892
- # we will accept either having a space or not having a space there.
893
-
894
- pp = @peek(-1)
895
- unless (token.spaced? or token.newLine?) and
896
- # Throw error unless the previous token...
897
- ((pp.spaced? or pp[0] is 'TERMINATOR') or #1
898
- pp.generated? or #2
899
- pp[0] is "INDENT" or #3
900
- (pp[1] is "(" and not pp.generated?)) #4
901
- @createLexError('arrow_spacing')
902
- else
903
- null
904
-
905
- createLexError : (rule, attrs = {}) ->
906
- attrs.lineNumber = @lineNumber + 1
907
- attrs.level = @config[rule].level
908
- attrs.line = @lines[@lineNumber]
909
- createError(rule, attrs)
910
-
911
- # Return the token n places away from the current token.
912
- peek : (n = 1) ->
913
- @tokens[@i + n] || null
914
-
915
- # Return true if the current token is inside of an array.
916
- inArray : () ->
917
- return @arrayTokens.length > 0
918
-
919
- # Return true if the current token is part of a property access
920
- # that is split across lines, for example:
921
- # $('body')
922
- # .addClass('foo')
923
- # .removeClass('bar')
924
- isChainedCall : () ->
925
- # Get the index of the second most recent new line.
926
- lines = (i for token, i in @tokens[..@i] when token.newLine?)
927
-
928
- lastNewLineIndex = if lines then lines[lines.length - 2] else null
929
-
930
- # Bail out if there is no such token.
931
- return false if not lastNewLineIndex?
932
-
933
- # Otherwise, figure out if that token or the next is an attribute
934
- # look-up.
935
- tokens = [@tokens[lastNewLineIndex], @tokens[lastNewLineIndex + 1]]
936
-
937
- return !!(t for t in tokens when t and t[0] == '.').length
938
-
939
-
940
- # A class that performs static analysis of the abstract
941
- # syntax tree.
942
- class ASTLinter
943
-
944
- constructor : (source, config) ->
945
- @source = source
946
- @config = config
947
- @errors = []
948
-
949
- lint : () ->
950
- try
951
- @node = CoffeeScript.nodes(@source)
952
- catch coffeeError
953
- @errors.push @_parseCoffeeScriptError(coffeeError)
954
- return @errors
955
- @lintNode(@node)
956
- @errors
957
-
958
- # returns the "complexity" value of the current node.
959
- getComplexity : (node) ->
960
- name = node.constructor.name
961
- complexity = if name in ['If', 'While', 'For', 'Try']
962
- 1
963
- else if name == 'Op' and node.operator in ['&&', '||']
964
- 1
965
- else if name == 'Switch'
966
- node.cases.length
967
- else
968
- 0
969
- return complexity
970
-
971
- # Lint the AST node and return it's cyclomatic complexity.
972
- lintNode : (node, line) ->
973
-
974
- # Get the complexity of the current node.
975
- name = node.constructor.name
976
- complexity = @getComplexity(node)
977
-
978
- # Add the complexity of all child's nodes to this one.
979
- node.eachChild (childNode) =>
980
- nodeLine = childNode.locationData.first_line
981
- complexity += @lintNode(childNode, nodeLine) if childNode
982
-
983
- # If the current node is a function, and it's over our limit, add an
984
- # error to the list.
985
- rule = @config.cyclomatic_complexity
986
-
987
- if name == 'Code' and complexity >= rule.value
988
- attrs = {
989
- context: complexity + 1
990
- level: rule.level
991
- lineNumber: line + 1
992
- lineNumberEnd: node.locationData.last_line + 1
993
- }
994
- error = createError 'cyclomatic_complexity', attrs
995
- @errors.push error if error
996
-
997
- # Return the complexity for the benefit of parent nodes.
998
- return complexity
999
-
1000
- _parseCoffeeScriptError : (coffeeError) ->
1001
- rule = RULES['coffeescript_error']
1002
-
1003
- message = coffeeError.toString()
1004
-
1005
- # Parse the line number
1006
- lineNumber = -1
1007
- if coffeeError.location?
1008
- lineNumber = coffeeError.location.first_line + 1
1009
- else
1010
- match = /line (\d+)/.exec message
1011
- lineNumber = parseInt match[1], 10 if match?.length > 1
1012
- attrs = {
1013
- message: message
1014
- level: rule.level
1015
- lineNumber: lineNumber
1016
- }
1017
- return createError 'coffeescript_error', attrs
1018
-
1019
-
1020
-
1021
- # Merge default and user configuration.
1022
- mergeDefaultConfig = (userConfig) ->
1023
- config = {}
1024
- for rule, ruleConfig of RULES
1025
- config[rule] = defaults(userConfig[rule], ruleConfig)
1026
- return config
1027
-
1028
- coffeelint.invertLiterate = (source) ->
1029
- source = CoffeeScript.helpers.invertLiterate source
1030
- # Strip the first 4 spaces from every line. After this the markdown is
1031
- # commented and all of the other code should be at their natural location.
1032
- newSource = ""
1033
- for line in source.split "\n"
1034
- if line.match(/^#/)
1035
- # strip trailing space
1036
- line = line.replace /\s*$/, ''
1037
- # Strip the first 4 spaces of every line. This is how Markdown
1038
- # indicates code, so in the end this pulls everything back to where it
1039
- # would be indented if it hadn't been written in literate style.
1040
- line = line.replace /^\s{4}/g, ''
1041
- newSource += "#{line}\n"
1042
-
1043
- newSource
1044
-
1045
- # Check the source against the given configuration and return an array
1046
- # of any errors found. An error is an object with the following
1047
- # properties:
1048
- #
1049
- # {
1050
- # rule : 'Name of the violated rule',
1051
- # lineNumber: 'Number of the line that caused the violation',
1052
- # level: 'The error level of the violated rule',
1053
- # message: 'Information about the violated rule',
1054
- # context: 'Optional details about why the rule was violated'
1055
- # }
1056
- #
1057
- coffeelint.lint = (source, userConfig = {}, literate = false) ->
1058
- source = @invertLiterate source if literate
1059
-
1060
- config = mergeDefaultConfig(userConfig)
1061
-
1062
- # Check ahead for inline enabled rules
1063
- disabled_initially = []
1064
- for l in source.split('\n')
1065
- s = regexes.configStatement.exec(l)
1066
- if s?.length > 2 and 'enable' in s
1067
- for r in s[1..]
1068
- unless r in ['enable','disable']
1069
- unless r of config and config[r].level in ['warn','error']
1070
- disabled_initially.push r
1071
- config[r] = { level: 'error' }
1072
-
1073
- # Do AST linting first so all compile errors are caught.
1074
- astErrors = new ASTLinter(source, config).lint()
1075
-
1076
- # Do lexical linting.
1077
- lexicalLinter = new LexicalLinter(source, config)
1078
- lexErrors = lexicalLinter.lint()
1079
-
1080
- # Do line linting.
1081
- tokensByLine = lexicalLinter.tokensByLine
1082
- lineLinter = new LineLinter(source, config, tokensByLine)
1083
- lineErrors = lineLinter.lint()
1084
-
1085
- # Sort by line number and return.
1086
- errors = lexErrors.concat(lineErrors, astErrors)
1087
- errors.sort((a, b) -> a.lineNumber - b.lineNumber)
1088
-
1089
- # Helper to remove rules from disabled list
1090
- difference = (a, b) ->
1091
- j = 0
1092
- while j < a.length
1093
- if a[j] in b
1094
- a.splice(j, 1)
1095
- else
1096
- j++
1097
-
1098
- # Disable/enable rules for inline blocks
1099
- all_errors = errors
1100
- errors = []
1101
- disabled = disabled_initially
1102
- next_line = 0
1103
- for i in [0...source.split('\n').length]
1104
- for cmd of block_config
1105
- rules = block_config[cmd][i]
1106
- {
1107
- 'disable': ->
1108
- disabled = disabled.concat(rules)
1109
- 'enable': ->
1110
- difference(disabled, rules)
1111
- disabled = disabled_initially if rules.length is 0
1112
- }[cmd]() if rules?
1113
- # advance line and append relevant messages
1114
- while next_line is i and all_errors.length > 0
1115
- next_line = all_errors[0].lineNumber - 1
1116
- e = all_errors[0]
1117
- if e.lineNumber is i + 1 or not e.lineNumber?
1118
- e = all_errors.shift()
1119
- errors.push e unless e.rule in disabled
1120
-
1121
- block_config =
1122
- 'enable': {}
1123
- 'disable': {}
1124
-
1125
- errors