coffeelint 0.1.3 → 0.2.0

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.
@@ -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