coffeelint 0.1.3 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -7
- data/Rakefile +13 -0
- data/coffeelint.gemspec +1 -1
- data/coffeelint/lib/coffeelint.js +2015 -0
- data/lib/coffeelint.rb +14 -5
- data/lib/coffeelint/version.rb +1 -1
- metadata +72 -53
- data/coffeelint/src/coffeelint.coffee +0 -1125
data/lib/coffeelint.rb
CHANGED
@@ -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/
|
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
|
-
|
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 = {})
|
data/lib/coffeelint/version.rb
CHANGED
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.
|
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
|
-
|
13
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
29
|
-
requirements:
|
30
|
-
-
|
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
|
-
|
37
|
-
requirements:
|
38
|
-
-
|
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
|
-
|
45
|
-
requirements:
|
46
|
-
-
|
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
|
-
|
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/
|
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
|
-
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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.
|
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 = () ->
|
238
|
-
|
239
|
-
# We might favor this instead:
|
240
|
-
myFunction = ->
|
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
|