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