├── bin ├── .gitignore └── coffeelint ├── lib └── .gitignore ├── .npmignore ├── test ├── fixtures │ ├── syntax_error.coffee │ ├── subdir │ │ └── subdir │ │ │ └── subdir.coffee │ ├── findconfigtest │ │ ├── sevenspaces.coffee │ │ ├── package │ │ │ ├── sixspaces.coffee │ │ │ └── package.json │ │ └── coffeelint.json │ ├── fourspaces.coffee │ ├── clean.coffee │ ├── fourspaces.json │ ├── twospaces.warning.json │ ├── cyclo_fail.coffee │ ├── custom_rules │ │ ├── rule_module.json │ │ └── voldemort.coffee │ └── mock_node_modules │ │ └── he_who_must_not_be_named │ │ ├── index.js │ │ └── he_who_must_not_be_named.coffee ├── coffeelint.json ├── test_plusplus.coffee ├── test_backticks.coffee ├── test_throw.coffee ├── test_empty_param_list.coffee ├── test_debugger.coffee ├── test_literate.litcoffee ├── test_levels.coffee ├── test_no_stand_alone_at.coffee ├── test_no_interpolation_in_single_quotes.coffee ├── test_duplicate_key.coffee ├── test_coffeelint.coffee ├── test_comment_config.coffee ├── test_line_endings.coffee ├── test_1.5.0_plus.coffee ├── test_no_unnecessary_fat_arrows.coffee ├── test_tabs.coffee ├── test_identifier.coffee ├── test_braces.coffee ├── test_line_length.coffee ├── test_constructor_needs_parens.coffee ├── test_no_implicit_parens.coffee ├── test_newlines_after_classes.coffee ├── test_spacing.coffee ├── test_colon_assignment_spacing.coffee ├── test_semicolons.coffee ├── test_trailing.coffee ├── test_missing_fat_arrows.coffee ├── test_no_unnecessary_double_quotes.coffee ├── test_indent.coffee ├── test_cyclomatic_complexity.coffee ├── test_arrows.coffee └── test_commandline.coffee ├── .gitignore ├── vowsrunner.coffee ├── .travis.yml ├── .dir-locals.el ├── src ├── rules.coffee ├── rules │ ├── no_debugger.coffee │ ├── non_empty_constructor_needs_parens.coffee │ ├── no_plusplus.coffee │ ├── no_backticks.coffee │ ├── no_tabs.coffee │ ├── no_empty_param_list.coffee │ ├── newlines_after_classes.coffee │ ├── no_interpolation_in_single_quotes.coffee │ ├── line_endings.coffee │ ├── no_stand_alone_at.coffee │ ├── no_implicit_parens.coffee │ ├── no_unnecessary_fat_arrows.coffee │ ├── no_throwing_strings.coffee │ ├── empty_constructor_needs_parens.coffee │ ├── max_line_length.coffee │ ├── no_trailing_whitespace.coffee │ ├── camel_case_classes.coffee │ ├── cyclomatic_complexity.coffee │ ├── no_implicit_braces.coffee │ ├── duplicate_key.coffee │ ├── no_unnecessary_double_quotes.coffee │ ├── arrow_spacing.coffee │ ├── no_trailing_semicolons.coffee │ ├── colon_assignment_spacing.coffee │ ├── missing_fat_arrows.coffee │ ├── space_operators.coffee │ └── indentation.coffee ├── htmldoc.coffee ├── base_linter.coffee ├── configfinder.coffee ├── lexical_linter.coffee ├── ast_linter.coffee ├── line_linter.coffee ├── coffeelint.coffee └── coffeelint-schema.json ├── coffeelint.json ├── LICENSE ├── package.json ├── README.md ├── Cakefile ├── 3rd_party_rules.md └── generated_coffeelint.json /bin/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | .git* 3 | utils/ 4 | -------------------------------------------------------------------------------- /test/fixtures/syntax_error.coffee: -------------------------------------------------------------------------------- 1 | x = [1, 2 2 | -------------------------------------------------------------------------------- /test/fixtures/subdir/subdir/subdir.coffee: -------------------------------------------------------------------------------- 1 | here = 'is an error'; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/* 2 | node_modules 3 | *.log 4 | utils 5 | .package.json 6 | -------------------------------------------------------------------------------- /test/fixtures/findconfigtest/sevenspaces.coffee: -------------------------------------------------------------------------------- 1 | f = -> 2 | null 3 | -------------------------------------------------------------------------------- /test/fixtures/findconfigtest/package/sixspaces.coffee: -------------------------------------------------------------------------------- 1 | f = -> 2 | null 3 | -------------------------------------------------------------------------------- /test/fixtures/fourspaces.coffee: -------------------------------------------------------------------------------- 1 | x = () -> 2 | do -> 3 | return 1234 4 | -------------------------------------------------------------------------------- /vowsrunner.coffee: -------------------------------------------------------------------------------- 1 | require 'coffee-script/register' 2 | require './node_modules/vows/bin/vows' 3 | 4 | -------------------------------------------------------------------------------- /test/fixtures/clean.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | A lint free script. 3 | ### 4 | 5 | x = () -> 6 | return 1234 + 4567 7 | -------------------------------------------------------------------------------- /test/coffeelint.json: -------------------------------------------------------------------------------- 1 | { 2 | "": "Force all tests under test/ to use this default config unless explicitly requested" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/fourspaces.json: -------------------------------------------------------------------------------- 1 | { 2 | "indentation" : { 3 | "level" : "error", 4 | "value" : 4 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/twospaces.warning.json: -------------------------------------------------------------------------------- 1 | { 2 | "indentation" : { 3 | "level" : "warn", 4 | "value" : 2 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.8" 4 | - "0.10" 5 | 6 | branches: 7 | only: 8 | - master 9 | - next 10 | -------------------------------------------------------------------------------- /test/fixtures/cyclo_fail.coffee: -------------------------------------------------------------------------------- 1 | x = -> 2 | 1 and 2 and 3 and 3 | 4 and 5 and 6 and 4 | 7 and 8 and 9 and 5 | 10 and 11 and 12 6 | -------------------------------------------------------------------------------- /test/fixtures/findconfigtest/coffeelint.json: -------------------------------------------------------------------------------- 1 | { 2 | "indentation" : { 3 | "level" : "error", 4 | "value" : 7 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/custom_rules/rule_module.json: -------------------------------------------------------------------------------- 1 | { 2 | "he_who_must_not_be_named" : { 3 | "module": "he_who_must_not_be_named" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/mock_node_modules/he_who_must_not_be_named/index.js: -------------------------------------------------------------------------------- 1 | require('coffee-script'); 2 | module.exports = require('./he_who_must_not_be_named'); 3 | -------------------------------------------------------------------------------- /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ;;; Directory Local Variables 2 | ;;; See Info node `(emacs) Directory Variables' for more information. 3 | 4 | ((coffee-mode 5 | (tab-width . 4) 6 | (coffee-tab-width . 4))) 7 | -------------------------------------------------------------------------------- /test/fixtures/findconfigtest/package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "coffeelintConfig": { 4 | "indentation" : { 5 | "level" : "error", 6 | "value" : 6 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/rules.coffee: -------------------------------------------------------------------------------- 1 | 2 | # CoffeeLint error levels. 3 | ERROR = 'error' 4 | WARN = 'warn' 5 | IGNORE = 'ignore' 6 | 7 | # CoffeeLint's default rule configuration. 8 | module.exports = 9 | 10 | coffeescript_error : 11 | level : ERROR 12 | message : '' # The default coffeescript error is fine. 13 | -------------------------------------------------------------------------------- /test/fixtures/custom_rules/voldemort.coffee: -------------------------------------------------------------------------------- 1 | 2 | class Voldemort 3 | 4 | avadaKadavra: (enemy) -> 5 | enemy.die() 6 | 7 | generateHorcruxes: (scrifices = []) -> 8 | voldemort = [] 9 | for s in scrifices 10 | voldemort.push new Horcrux @avadaKadavra(s) 11 | 12 | return voldemort 13 | 14 | 15 | -------------------------------------------------------------------------------- /coffeelint.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrow_spacing": { 3 | "level": "error" 4 | }, 5 | "indentation" : { 6 | "level" : "error", 7 | "value" : 4 8 | }, 9 | 10 | "cyclomatic_complexity" : { 11 | "level" : "warn", 12 | "value" : 11 13 | }, 14 | 15 | "line_endings" : { 16 | "value" : "unix", 17 | "level" : "error" 18 | }, 19 | 20 | "space_operators" : { 21 | "level" : "error" 22 | } 23 | 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/rules/no_debugger.coffee: -------------------------------------------------------------------------------- 1 | 2 | module.exports = class NoDebugger 3 | 4 | rule: 5 | name: 'no_debugger' 6 | level : 'warn' 7 | message : 'Debugger statements will cause warnings' 8 | description: """ 9 | This rule detects the `debugger` statement. 10 | This rule is `warn` by default. 11 | """ 12 | 13 | tokens: [ "DEBUGGER" ] 14 | 15 | lintToken : (token, tokenApi) -> 16 | return {context : "found '#{token[0]}'"} 17 | -------------------------------------------------------------------------------- /test/fixtures/mock_node_modules/he_who_must_not_be_named/he_who_must_not_be_named.coffee: -------------------------------------------------------------------------------- 1 | 2 | matcher = /Voldemort/i 3 | 4 | module.exports = class HeWhoMustNotBeNamed 5 | rule: 6 | name: 'he_who_must_not_be_named' 7 | level : 'error' 8 | message : "Forbidden variable name. The snatchers have been alerted" 9 | description: """ 10 | """ 11 | 12 | tokens: [ 'IDENTIFIER' ] 13 | 14 | lintToken : (token, tokenApi) -> 15 | if matcher.test(token[1]) 16 | true 17 | 18 | -------------------------------------------------------------------------------- /src/rules/non_empty_constructor_needs_parens.coffee: -------------------------------------------------------------------------------- 1 | 2 | ParentClass = require './empty_constructor_needs_parens.coffee' 3 | 4 | module.exports = class NonEmptyConstructorNeedsParens extends ParentClass 5 | 6 | rule: 7 | name: 'non_empty_constructor_needs_parens' 8 | level: 'ignore' 9 | message: 'Invoking a constructor without parens and with arguments' 10 | description: 11 | "Requires constructors with parameters to include the parens" 12 | 13 | handleExpectedCallStart: (expectedCallStart) -> 14 | if expectedCallStart[0] is 'CALL_START' and expectedCallStart.generated 15 | return true 16 | -------------------------------------------------------------------------------- /src/htmldoc.coffee: -------------------------------------------------------------------------------- 1 | _ = require "underscore" 2 | rules = (require "./coffeelint").RULES 3 | 4 | render = () -> 5 | rulesHTML = "" 6 | 7 | for ruleName in _.sortBy (_.keys rules), ((s) -> s) 8 | rule = rules[ruleName] 9 | rule.name = ruleName 10 | rule.description = "[no description provided]" unless rule.description 11 | console.log ruleTemplate rule 12 | 13 | ruleTemplate = _.template """ 14 |
default level: <%= level %>
19 |
12 | # The empty parameter list in here is unnecessary:
13 | myFunction = () ->
14 |
15 | # We might favor this instead:
16 | myFunction = ->
17 |
18 |
19 | Empty parameter lists are permitted by default.
20 | """
21 |
22 | tokens: [ "PARAM_START" ]
23 |
24 | lintToken : (token, tokenApi) ->
25 | nextType = tokenApi.peek()[0]
26 | return nextType is 'PARAM_END'
27 |
28 |
--------------------------------------------------------------------------------
/test/test_plusplus.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 | vows = require 'vows'
3 | assert = require 'assert'
4 | coffeelint = require path.join('..', 'lib', 'coffeelint')
5 |
6 |
7 | vows.describe('plusplus').addBatch({
8 |
9 | 'The increment and decrement operators' :
10 |
11 | topic : '''
12 | y++
13 | ++y
14 | x--
15 | --x
16 | '''
17 |
18 | 'are permitted by default' : (source) ->
19 | errors = coffeelint.lint(source)
20 | assert.isArray(errors)
21 | assert.isEmpty(errors)
22 |
23 | 'can be forbidden' : (source) ->
24 | errors = coffeelint.lint(source, {no_plusplus: {'level':'error'}})
25 | assert.isArray(errors)
26 | assert.lengthOf(errors, 4)
27 | error = errors[0]
28 | assert.equal(error.lineNumber, 1)
29 | assert.equal(error.rule, 'no_plusplus')
30 |
31 | }).export(module)
32 |
--------------------------------------------------------------------------------
/src/rules/newlines_after_classes.coffee:
--------------------------------------------------------------------------------
1 |
2 | module.exports = class NewlinesAfterClasses
3 |
4 | rule:
5 | name: 'newlines_after_classes'
6 | value : 3
7 | level : 'ignore'
8 | message : 'Wrong count of newlines between a class and other code'
9 | description: "Checks the number of newlines between classes and other
10 | code"
11 |
12 | lintLine: (line, lineApi) ->
13 | ending = lineApi.config[@rule.name].value
14 |
15 | return null if not ending or lineApi.isLastLine()
16 |
17 | { lineNumber, context } = lineApi
18 | if not context.class.inClass and
19 | context.class.lastUnemptyLineInClass? and
20 | (lineNumber - context.class.lastUnemptyLineInClass) isnt
21 | ending
22 | got = lineNumber - context.class.lastUnemptyLineInClass
23 | return { context: "Expected #{ending} got #{got}" }
24 |
25 | null
26 |
27 |
--------------------------------------------------------------------------------
/test/test_backticks.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 | vows = require 'vows'
3 | assert = require 'assert'
4 | coffeelint = require path.join('..', 'lib', 'coffeelint')
5 |
6 | vows.describe('backticks').addBatch({
7 |
8 | 'Backticks' :
9 |
10 | topic : "`with(document) alert(height);`"
11 |
12 | 'are forbidden by default' : (source) ->
13 | errors = coffeelint.lint(source)
14 | assert.isArray(errors)
15 | assert.lengthOf(errors, 1)
16 | error = errors[0]
17 | assert.equal(error.rule, 'no_backticks')
18 | assert.equal(error.lineNumber, 1)
19 | assert.equal(error.message, "Backticks are forbidden")
20 |
21 | 'can be permitted' : (source) ->
22 | config = {no_backticks : {level:'ignore'}}
23 | errors = coffeelint.lint(source, config)
24 | assert.isArray(errors)
25 | assert.isEmpty(errors)
26 |
27 | }).export(module)
28 |
29 |
--------------------------------------------------------------------------------
/test/test_throw.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 | vows = require 'vows'
3 | assert = require 'assert'
4 | coffeelint = require path.join('..', 'lib', 'coffeelint')
5 |
6 | vows.describe('throw').addBatch({
7 |
8 | 'Throwing strings' :
9 |
10 | topic : '''
11 | throw 'my error'
12 | throw "#{1234}"
13 | throw """
14 | long string
15 | """
16 | '''
17 |
18 | 'is forbidden by default' : (source) ->
19 | errors = coffeelint.lint(source)
20 | assert.lengthOf(errors, 3)
21 | error = errors[0]
22 | assert.equal(error.message, 'Throwing strings is forbidden')
23 | assert.equal(error.rule, 'no_throwing_strings')
24 |
25 | 'can be permitted' : (source) ->
26 | config = {no_throwing_strings : {level : 'ignore'}}
27 | errors = coffeelint.lint(source, config)
28 | assert.isEmpty(errors)
29 |
30 | }).export(module)
31 |
--------------------------------------------------------------------------------
/src/rules/no_interpolation_in_single_quotes.coffee:
--------------------------------------------------------------------------------
1 |
2 |
3 | module.exports = class NoInterpolationInSingleQuotes
4 |
5 | rule:
6 | name: 'no_interpolation_in_single_quotes'
7 | level: 'ignore'
8 | message: 'Interpolation in single quoted strings is forbidden'
9 | description: '''
10 | This rule prohibits string interpolation in a single quoted string.
11 |
12 | # String interpolation in single quotes is not allowed:
13 | foo = '#{bar}'
14 |
15 | # Double quotes is OK of course
16 | foo = "#{bar}"
17 |
18 |
19 | String interpolation in single quoted strings is permitted by
20 | default.
21 | '''
22 |
23 | tokens: [ 'STRING' ]
24 |
25 | lintToken : (token, tokenApi) ->
26 | tokenValue = token[1]
27 |
28 | hasInterpolation = tokenValue.match(/#\{[^}]+\}/)
29 | return hasInterpolation
30 |
--------------------------------------------------------------------------------
/src/rules/line_endings.coffee:
--------------------------------------------------------------------------------
1 |
2 |
3 | module.exports = class LineEndings
4 |
5 | rule:
6 | name: 'line_endings'
7 | level : 'ignore'
8 | value : 'unix' # or 'windows'
9 | message : 'Line contains incorrect line endings'
10 | description: """
11 | This rule ensures your project uses only windows or
12 | unix line endings. This rule is disabled by default.
13 | """
14 |
15 | lintLine: (line, lineApi) ->
16 | ending = lineApi.config[@rule.name]?.value
17 |
18 | return null if not ending or lineApi.isLastLine() or not line
19 |
20 | lastChar = line[line.length - 1]
21 | valid = if ending == 'windows'
22 | lastChar == '\r'
23 | else if ending == 'unix'
24 | lastChar != '\r'
25 | else
26 | throw new Error("unknown line ending type: #{ending}")
27 | if not valid
28 | return {context:"Expected #{ending}"}
29 | else
30 | return null
31 |
--------------------------------------------------------------------------------
/test/test_empty_param_list.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 | vows = require 'vows'
3 | assert = require 'assert'
4 | coffeelint = require path.join('..', 'lib', 'coffeelint')
5 |
6 | vows.describe('params').addBatch({
7 |
8 | 'Empty param list' :
9 |
10 | topic : () ->
11 | '''
12 | blah = () ->
13 | '''
14 |
15 | 'are allowed by default' : (source) ->
16 | errors = coffeelint.lint(source)
17 | assert.isArray(errors)
18 | assert.isEmpty(errors)
19 |
20 | 'can be forbidden' : (source) ->
21 | config = {no_empty_param_list : {level:'error'}}
22 | errors = coffeelint.lint(source, config)
23 | assert.isArray(errors)
24 | assert.lengthOf(errors, 1)
25 | error = errors[0]
26 | assert.equal(error.lineNumber, 1)
27 | assert.equal(error.message, 'Empty parameter list is forbidden')
28 | assert.equal(error.rule, 'no_empty_param_list')
29 |
30 | }).export(module)
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | CoffeeLint
2 | Copyright (c) 2011 Matthew Perpick
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in
12 | all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 | THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/test/test_debugger.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 | vows = require 'vows'
3 | assert = require 'assert'
4 | coffeelint = require path.join('..', 'lib', 'coffeelint')
5 |
6 |
7 | vows.describe('no_debugger').addBatch({
8 |
9 | 'The debugger statement' :
10 |
11 | topic : ->
12 | '''
13 | debugger
14 | '''
15 |
16 | 'causes a warning when present' : (source) ->
17 | errors = coffeelint.lint(source)
18 | assert.isArray(errors)
19 | assert.lengthOf(errors, 1)
20 | error = errors[0]
21 | assert.equal(error.level, 'warn')
22 | assert.equal(error.lineNumber, 1)
23 | assert.equal(error.rule, 'no_debugger')
24 |
25 | 'can be set to error' : (source) ->
26 | errors = coffeelint.lint(source, {no_debugger: {'level':'error'}})
27 | assert.isArray(errors)
28 | assert.isArray(errors)
29 | assert.lengthOf(errors, 1)
30 | error = errors[0]
31 | assert.equal(error.lineNumber, 1)
32 | assert.equal(error.rule, 'no_debugger')
33 |
34 | }).export(module)
35 |
--------------------------------------------------------------------------------
/test/test_literate.litcoffee:
--------------------------------------------------------------------------------
1 | The post-test process involves linting all of the files under `test/`. By
2 | writing this file in Literate (style?) it verifies that literate files are
3 | automatically detected.
4 |
5 |
6 | path = require 'path'
7 | vows = require 'vows'
8 | assert = require 'assert'
9 | coffeelint = require path.join('..', 'lib', 'coffeelint')
10 |
11 | vows.describe('literate').addBatch({
12 |
13 | Markdown uses trailing spaces to force a line break.
14 |
15 | 'Trailing whitespace in markdown' :
16 |
17 | topic :
18 |
19 | The line of code is written weird because I had trouble getting the 4 space
20 | prefix in place.
21 |
22 | """This is some `Markdown`. \n\n
23 | \n x = 1234 \n y = 1
24 | """
25 |
26 | 'is ignored' : (source) ->
27 |
28 | The 3rd parameter here indicates that the incoming source is literate.
29 |
30 | errors = coffeelint.lint(source, {}, true)
31 |
32 | This intentionally includes trailing whitespace in code so it also verifies
33 | that the way `Markdown` spaces are stripped are not also stripping code.
34 |
35 | assert.equal(errors.length, 1)
36 |
37 | }).export(module)
38 |
--------------------------------------------------------------------------------
/src/rules/no_stand_alone_at.coffee:
--------------------------------------------------------------------------------
1 |
2 | module.exports = class NoStandAloneAt
3 |
4 | rule:
5 | name: 'no_stand_alone_at'
6 | level : 'ignore'
7 | message : '@ must not be used stand alone'
8 | description: """
9 | This rule checks that no stand alone @ are in use, they are
10 | discouraged. Further information in CoffeScript issue
12 | #1601
13 | """
14 |
15 |
16 | tokens: [ "@" ]
17 |
18 | lintToken : (token, tokenApi) ->
19 | nextToken = tokenApi.peek()
20 | spaced = token.spaced
21 | isIdentifier = nextToken[0] == 'IDENTIFIER'
22 | isIndexStart = nextToken[0] == 'INDEX_START'
23 | isDot = nextToken[0] == '.'
24 |
25 | # https://github.com/jashkenas/coffee-script/issues/1601
26 | # @::foo is valid, but @:: behaves inconsistently and is planned for
27 | # removal. Technically @:: is a stand alone ::, but I think it makes
28 | # sense to group it into no_stand_alone_at
29 | if nextToken[0] == '::'
30 | protoProperty = tokenApi.peek(2)
31 | isValidProtoProperty = protoProperty[0] == 'IDENTIFIER'
32 |
33 | if spaced or (not isIdentifier and not isIndexStart and
34 | not isDot and not isValidProtoProperty)
35 | return true
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/rules/no_implicit_parens.coffee:
--------------------------------------------------------------------------------
1 |
2 | module.exports = class NoImplicitParens
3 |
4 | rule:
5 | name: 'no_implicit_parens'
6 | strict : true
7 | level : 'ignore'
8 | message : 'Implicit parens are forbidden'
9 | description: """
10 | This rule prohibits implicit parens on function calls.
11 |
12 | # Some folks don't like this style of coding.
13 | myFunction a, b, c
14 |
15 | # And would rather it always be written like this:
16 | myFunction(a, b, c)
17 |
18 |
19 | Implicit parens are permitted by default, since their use is
20 | idiomatic CoffeeScript.
21 | """
22 |
23 |
24 | tokens: [ "CALL_END" ]
25 |
26 | lintToken : (token, tokenApi) ->
27 | if token.generated
28 | unless tokenApi.config[@rule.name].strict == false
29 | return true
30 | else
31 | # If strict mode is turned off it allows implicit parens when the
32 | # expression is spread over multiple lines.
33 | i = -1
34 | loop
35 | t = tokenApi.peek(i)
36 | if not t? or t[0] == 'CALL_START'
37 | return true
38 | if t.newLine
39 | return null
40 | i -= 1
41 |
--------------------------------------------------------------------------------
/src/rules/no_unnecessary_fat_arrows.coffee:
--------------------------------------------------------------------------------
1 | any = (arr, test) -> arr.reduce ((res, elt) -> res or test elt), false
2 |
3 | module.exports = class NoUnnecessaryFatArrows
4 |
5 | rule:
6 | name: 'no_unnecessary_fat_arrows'
7 | level: 'warn'
8 | message: 'Unnecessary fat arrow'
9 | description: """
10 | Disallows defining functions with fat arrows when `this`
11 | is not used within the function.
12 | """
13 |
14 | lintAST: (node, @astApi) ->
15 | @lintNode node
16 | undefined
17 |
18 | lintNode: (node) ->
19 | if (@isFatArrowCode node) and (not @needsFatArrow node)
20 | error = @astApi.createError
21 | lineNumber: node.locationData.first_line + 1
22 | @errors.push error
23 | node.eachChild (child) => @lintNode child
24 |
25 | isCode: (node) -> @astApi.getNodeName(node) is 'Code'
26 | isFatArrowCode: (node) -> @isCode(node) and node.bound
27 | isValue: (node) -> @astApi.getNodeName(node) is 'Value'
28 |
29 | isThis: (node) =>
30 | @isValue(node) and node.base.value is 'this'
31 |
32 |
33 | needsFatArrow: (node) =>
34 | @isCode(node) and (
35 | any(node.params, (param) => param.contains(@isThis)?) or
36 | node.body.contains(@isThis)? or
37 | node.body.contains((child) =>
38 | @isFatArrowCode(child) and @needsFatArrow(child))?
39 | )
40 |
--------------------------------------------------------------------------------
/test/test_levels.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 | vows = require 'vows'
3 | assert = require 'assert'
4 | coffeelint = require path.join('..', 'lib', 'coffeelint')
5 |
6 | vows.describe('levels').addBatch({
7 |
8 | 'CoffeeLint' :
9 |
10 | topic : "abc = 123;",
11 |
12 | 'can ignore errors' : (source) ->
13 | config =
14 | no_trailing_semicolons : {level: 'ignore'}
15 | errors = coffeelint.lint(source, config)
16 | assert.isEmpty(errors)
17 |
18 | 'can return warnings' : (source) ->
19 | config =
20 | no_trailing_semicolons : {level: 'warn'}
21 | errors = coffeelint.lint(source, config)
22 | assert.isArray(errors)
23 | assert.lengthOf(errors, 1)
24 | error = errors[0]
25 | assert.equal(error.level, 'warn')
26 |
27 | 'can return errors' : (source) ->
28 | config =
29 | no_trailing_semicolons : {level: 'error'}
30 | errors = coffeelint.lint(source, config)
31 | assert.isArray(errors)
32 | assert.lengthOf(errors, 1)
33 | error = errors[0]
34 | assert.equal(error.level, 'error')
35 |
36 | 'catches unknown levels' : (source) ->
37 |
38 | config =
39 | no_trailing_semicolons : {level: 'foobar'}
40 | assert.throws () ->
41 | coffeelint.lint(source, config)
42 |
43 |
44 | }).export(module)
45 |
--------------------------------------------------------------------------------
/src/rules/no_throwing_strings.coffee:
--------------------------------------------------------------------------------
1 |
2 | module.exports = class NoThrowingStrings
3 |
4 | rule:
5 | name: 'no_throwing_strings'
6 | level : 'error'
7 | message : 'Throwing strings is forbidden'
8 | description: """
9 | This rule forbids throwing string literals or interpolations. While
10 | JavaScript (and CoffeeScript by extension) allow any expression to
11 | be thrown, it is best to only throw Error objects,
14 | because they contain valuable debugging information like the stack
15 | trace. Because of JavaScript's dynamic nature, CoffeeLint cannot
16 | ensure you are always throwing instances of Error. It will
17 | only catch the simple but real case of throwing literal strings.
18 |
19 | # CoffeeLint will catch this:
20 | throw "i made a boo boo"
21 |
22 | # ... but not this:
23 | throw getSomeString()
24 |
25 |
26 | This rule is enabled by default.
27 | """
28 |
29 | tokens: [ "THROW" ]
30 |
31 | lintToken : (token, tokenApi) ->
32 | [n1, n2] = [tokenApi.peek(), tokenApi.peek(2)]
33 | # Catch literals and string interpolations, which are wrapped in
34 | # parens.
35 | nextIsString = n1[0] == 'STRING' or (n1[0] == '(' and n2[0] == 'STRING')
36 | return nextIsString
37 |
--------------------------------------------------------------------------------
/test/test_no_stand_alone_at.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 | vows = require 'vows'
3 | assert = require 'assert'
4 | coffeelint = require path.join('..', 'lib', 'coffeelint')
5 |
6 | vows.describe('stand alone @').addBatch({
7 |
8 | 'Stand alone @' :
9 |
10 | topic : () ->
11 | """
12 | @alright
13 | @ .error
14 | @ok()
15 | @ notok
16 | @[ok]
17 | @.ok
18 | not(@).ok
19 | @::ok
20 | @:: #notok
21 | """
22 |
23 | 'are allowed by default' : (source) ->
24 | errors = coffeelint.lint(source)
25 | assert.isArray(errors)
26 | assert.isEmpty(errors)
27 |
28 | 'can be forbidden' : (source) ->
29 | config = {no_stand_alone_at : {level:'error'}}
30 | errors = coffeelint.lint(source, config)
31 | assert.isArray(errors)
32 | assert.lengthOf(errors, 4)
33 | error = errors[0]
34 | assert.equal(error.lineNumber, 2)
35 | assert.equal(error.rule, 'no_stand_alone_at')
36 | error = errors[1]
37 | assert.equal(error.lineNumber, 4)
38 | assert.equal(error.rule, 'no_stand_alone_at')
39 | error = errors[2]
40 | assert.equal(error.lineNumber, 7)
41 | assert.equal(error.rule, 'no_stand_alone_at')
42 | error = errors[3]
43 | assert.equal(error.lineNumber, 9)
44 | assert.equal(error.rule, 'no_stand_alone_at')
45 |
46 | }).export(module)
47 |
--------------------------------------------------------------------------------
/src/rules/empty_constructor_needs_parens.coffee:
--------------------------------------------------------------------------------
1 |
2 | module.exports = class EmptyConstructorNeedsParens
3 |
4 | rule:
5 | name: 'empty_constructor_needs_parens'
6 | level: 'ignore'
7 | message: 'Invoking a constructor without parens and without arguments'
8 | description:
9 | "Requires constructors with no parameters to include the parens"
10 |
11 | tokens: [ 'UNARY' ]
12 |
13 | # Return an error if the given indentation token is not correct.
14 | lintToken: (token, tokenApi) ->
15 | if token[1] is 'new'
16 | # Find the last chained identifier, e.g. Bar in new foo.bar.Bar().
17 | identifierIndex = 1
18 | loop
19 | expectedIdentifier = tokenApi.peek(identifierIndex)
20 | expectedCallStart = tokenApi.peek(identifierIndex + 1)
21 | if expectedIdentifier?[0] is 'IDENTIFIER'
22 | if expectedCallStart?[0] is '.'
23 | identifierIndex += 2
24 | continue
25 | break
26 |
27 | # The callStart is generated if your parameters are all on the same
28 | # line with implicit parens, and if your parameters start on the
29 | # next line, but is missing if there are no params and no parens.
30 | if expectedIdentifier?[0] is 'IDENTIFIER' and expectedCallStart?
31 | return @handleExpectedCallStart expectedCallStart
32 |
33 | handleExpectedCallStart: (expectedCallStart) ->
34 | if expectedCallStart[0] isnt 'CALL_START'
35 | return true
36 |
--------------------------------------------------------------------------------
/src/rules/max_line_length.coffee:
--------------------------------------------------------------------------------
1 |
2 | regexes =
3 | literateComment: ///
4 | ^
5 | \#\s # This is prefixed on MarkDown lines.
6 | ///
7 | longUrlComment : ///
8 | ^\s*\# # indentation, up to comment
9 | \s*
10 | http[^\s]+$ # Link that takes up the rest of the line without spaces.
11 | ///
12 |
13 | module.exports = class MaxLineLength
14 |
15 | rule:
16 | name: 'max_line_length'
17 | value: 80
18 | level : 'error'
19 | limitComments: true
20 | message : 'Line exceeds maximum allowed length'
21 | description: """
22 | This rule imposes a maximum line length on your code. Python's style
24 | guide does a good job explaining why you might want to limit the
25 | length of your lines, though this is a matter of taste.
26 |
27 | Lines can be no longer than eighty characters by default.
28 | """
29 |
30 | lintLine: (line, lineApi) ->
31 | max = lineApi.config[@rule.name]?.value
32 | limitComments = lineApi.config[@rule.name]?.limitComments
33 |
34 | lineLength = line.length
35 | if lineApi.isLiterate() and regexes.literateComment.test(line)
36 | lineLength -= 2
37 |
38 | if max and max < lineLength and not regexes.longUrlComment.test(line)
39 |
40 | unless limitComments
41 | if lineApi.getLineTokens().length is 0
42 | return
43 |
44 | return {
45 | context: "Length is #{lineLength}, max is #{max}"
46 | }
47 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "coffeelint",
3 | "description": "Lint your CoffeeScript",
4 | "version": "1.2.0",
5 | "homepage": "http://www.coffeelint.org",
6 | "keywords": [
7 | "lint",
8 | "coffeescript",
9 | "coffee-script"
10 | ],
11 | "author": "Matthew Perpick
17 | # Good!
18 | class BoaConstrictor
19 |
20 | # Bad!
21 | class boaConstrictor
22 |
23 |
24 | This rule is enabled by default.
25 | """
26 |
27 | tokens: [ 'CLASS' ]
28 |
29 | lintToken: (token, tokenApi) ->
30 | # TODO: you can do some crazy shit in CoffeeScript, like
31 | # class func().ClassName. Don't allow that.
32 |
33 | # Don't try to lint the names of anonymous classes.
34 | if token.newLine? or tokenApi.peek()[0] in ['INDENT', 'EXTENDS']
35 | return null
36 |
37 | # It's common to assign a class to a global namespace, e.g.
38 | # exports.MyClassName, so loop through the next tokens until
39 | # we find the real identifier.
40 | className = null
41 | offset = 1
42 | until className
43 | if tokenApi.peek(offset + 1)?[0] == '.'
44 | offset += 2
45 | else if tokenApi.peek(offset)?[0] == '@'
46 | offset += 1
47 | else
48 | className = tokenApi.peek(offset)[1]
49 |
50 | # Now check for the error.
51 | if not regexes.camelCase.test(className)
52 | return {context: "class name: #{className}"}
53 |
--------------------------------------------------------------------------------
/src/rules/cyclomatic_complexity.coffee:
--------------------------------------------------------------------------------
1 | module.exports = class NoTabs
2 |
3 | rule:
4 | name: 'cyclomatic_complexity'
5 | value : 10
6 | level : 'ignore'
7 | message : 'The cyclomatic complexity is too damn high'
8 | description : 'Examine the complexity of your application.'
9 |
10 | # returns the "complexity" value of the current node.
11 | getComplexity : (node) ->
12 | name = @astApi.getNodeName node
13 | complexity = if name in ['If', 'While', 'For', 'Try']
14 | 1
15 | else if name == 'Op' and node.operator in ['&&', '||']
16 | 1
17 | else if name == 'Switch'
18 | node.cases.length
19 | else
20 | 0
21 | return complexity
22 |
23 | lintAST : (node, @astApi) ->
24 | @lintNode node
25 | undefined
26 |
27 | # Lint the AST node and return its cyclomatic complexity.
28 | lintNode : (node, line) ->
29 |
30 | # Get the complexity of the current node.
31 | name = @astApi?.getNodeName node
32 | complexity = @getComplexity(node)
33 |
34 | # Add the complexity of all child's nodes to this one.
35 | node.eachChild (childNode) =>
36 | nodeLine = childNode.locationData.first_line
37 | complexity += @lintNode(childNode, nodeLine) if childNode
38 |
39 | rule = @astApi.config[@rule.name]
40 |
41 | # If the current node is a function, and it's over our limit, add an
42 | # error to the list.
43 | if name == 'Code' and complexity >= rule.value
44 | error = @astApi.createError {
45 | context: complexity + 1
46 | lineNumber: line + 1
47 | lineNumberEnd: node.locationData.last_line + 1
48 | }
49 | @errors.push error if error
50 |
51 | # Return the complexity for the benefit of parent nodes.
52 | return complexity
53 |
--------------------------------------------------------------------------------
/src/rules/no_implicit_braces.coffee:
--------------------------------------------------------------------------------
1 |
2 | module.exports = class NoImplicitBraces
3 |
4 | rule:
5 | name: 'no_implicit_braces'
6 | level : 'ignore'
7 | message : 'Implicit braces are forbidden'
8 | strict: true
9 | description: """
10 | This rule prohibits implicit braces when declaring object literals.
11 | Implicit braces can make code more difficult to understand,
12 | especially when used in combination with optional parenthesis.
13 |
14 | # Do you find this code ambiguous? Is it a
15 | # function call with three arguments or four?
16 | myFunction a, b, 1:2, 3:4
17 |
18 | # While the same code written in a more
19 | # explicit manner has no ambiguity.
20 | myFunction(a, b, {1:2, 3:4})
21 |
22 |
23 | Implicit braces are permitted by default, since their use is
24 | idiomatic CoffeeScript.
25 | """
26 |
27 | tokens: [ "{" ]
28 |
29 | lintToken: (token, tokenApi) ->
30 | if token.generated
31 |
32 | # If strict mode is turned off it allows implicit braces when the
33 | # object is declared over multiple lines.
34 | unless tokenApi.config[@rule.name].strict
35 | [ previousToken ] = tokenApi.peek(-1)
36 | if previousToken is 'INDENT'
37 | return
38 |
39 | @isPartOfClass(tokenApi)
40 |
41 |
42 | isPartOfClass: (tokenApi) ->
43 | # Peek back to the last line break. If there is a class
44 | # definition, ignore the generated brace.
45 | i = -1
46 | loop
47 | t = tokenApi.peek(i)
48 | if not t? or t[0] == 'TERMINATOR'
49 | return true
50 | if t[0] == 'CLASS'
51 | return null
52 | i -= 1
53 |
--------------------------------------------------------------------------------
/src/base_linter.coffee:
--------------------------------------------------------------------------------
1 | # Patch the source properties onto the destination.
2 | extend = (destination, sources...) ->
3 | for source in sources
4 | (destination[k] = v for k, v of source)
5 | return destination
6 |
7 | # Patch any missing attributes from defaults to source.
8 | defaults = (source, defaults) ->
9 | extend({}, defaults, source)
10 |
11 | module.exports = class BaseLinter
12 |
13 | constructor: (@source, @config, rules) ->
14 | @setupRules rules
15 |
16 | isObject: (obj) ->
17 | obj is Object(obj)
18 |
19 | # Create an error object for the given rule with the given
20 | # attributes.
21 | createError: (ruleName, attrs = {}) ->
22 |
23 | # Level should default to what's in the config, but can be overridden.
24 | attrs.level ?= @config[ruleName].level
25 |
26 | level = attrs.level
27 | if level not in ['ignore', 'warn', 'error']
28 | throw new Error("unknown level #{level}")
29 |
30 | if level in ['error', 'warn']
31 | attrs.rule = ruleName
32 | return defaults(attrs, @config[ruleName])
33 | else
34 | null
35 |
36 | acceptRule: (rule) ->
37 | throw new Error "acceptRule needs to be overridden in the subclass"
38 |
39 | # Only rules that have a level of error or warn will even get constructed.
40 | setupRules: (rules) ->
41 | @rules = []
42 | for name, RuleConstructor of rules
43 | level = @config[name].level
44 | if level in ['error', 'warn']
45 | rule = new RuleConstructor this, @config
46 | if @acceptRule(rule)
47 | @rules.push rule
48 | else if level isnt 'ignore'
49 | throw new Error("unknown level #{level}")
50 |
51 | normalizeResult: (p, result) ->
52 | if result is true
53 | return @createError p.rule.name
54 | if @isObject result
55 | return @createError p.rule.name, result
56 |
--------------------------------------------------------------------------------
/test/test_comment_config.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 | vows = require 'vows'
3 | assert = require 'assert'
4 | coffeelint = require path.join('..', 'lib', 'coffeelint')
5 |
6 | vows.describe('comment config').addBatch({
7 |
8 | 'Disable statements' :
9 |
10 | topic : () ->
11 | """
12 | # coffeelint: disable=no_trailing_semicolons
13 | a 'you get a semi-colon';
14 | b 'you get a semi-colon';
15 | # coffeelint: enable=no_trailing_semicolons
16 | c 'everybody gets a semi-colon';
17 | """
18 |
19 | 'can disable rules in your config' : (source) ->
20 | config =
21 | no_trailing_semicolons : {level: 'error'}
22 | errors = coffeelint.lint(source, config)
23 | assert.equal(errors.length, 1)
24 |
25 | 'Enable statements' :
26 |
27 | topic : () ->
28 | """
29 | # coffeelint: enable=no_implicit_parens
30 | a 'implicit parens here'
31 | b 'implicit parens', 'also here'
32 | # coffeelint: disable=no_implicit_parens
33 | c 'implicit parens allowed here'
34 | """
35 |
36 | 'can enable rules not in your config' : (source) ->
37 | errors = coffeelint.lint(source)
38 | assert.equal(errors.length, 2)
39 |
40 | 'Enable all statements' :
41 | topic : () ->
42 | """
43 | # coffeelint: disable=no_trailing_semicolons,no_implicit_parens
44 | a 'you get a semi-colon';
45 | b 'you get a semi-colon';
46 | # coffeelint: enable
47 | c 'everybody gets a semi-colon';
48 | """
49 |
50 | 'will re-enable all rules in your config' : (source) ->
51 | config =
52 | no_implicit_parens : {level: 'error'}
53 | no_trailing_semicolons : {level: 'error'}
54 | errors = coffeelint.lint(source, config)
55 | assert.equal(errors.length, 2)
56 |
57 | }).export(module)
58 |
--------------------------------------------------------------------------------
/test/test_line_endings.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 | vows = require 'vows'
3 | assert = require 'assert'
4 | coffeelint = require path.join('..', 'lib', 'coffeelint')
5 |
6 | vows.describe('line endings').addBatch({
7 |
8 | 'Unix line endings' :
9 |
10 | topic : 'x = 1\ny=2'
11 |
12 | 'are allowed by default' : (source) ->
13 | errors = coffeelint.lint(source)
14 | assert.isEmpty(errors)
15 |
16 | 'can be forbidden' : (source) ->
17 | config = {line_endings : {level:'error', value:'windows'}}
18 | errors = coffeelint.lint(source, config)
19 | assert.isArray(errors)
20 | assert.lengthOf(errors, 1)
21 | error = errors[0]
22 | assert.equal(error.lineNumber, 1)
23 | assert.equal(error.message, 'Line contains incorrect line endings')
24 | assert.equal(error.context, 'Expected windows')
25 | assert.equal(error.rule, 'line_endings')
26 |
27 | 'Windows line endings' :
28 |
29 | topic : 'x = 1\r\ny=2'
30 |
31 | 'are allowed by default' : (source) ->
32 | errors = coffeelint.lint(source)
33 | assert.isEmpty(errors)
34 |
35 | 'can be forbidden' : (source) ->
36 | config = {line_endings : {level:'error', value:'unix'}}
37 | errors = coffeelint.lint(source, config)
38 | assert.isArray(errors)
39 | assert.lengthOf(errors, 1)
40 | error = errors[0]
41 | assert.equal(error.lineNumber, 1)
42 | assert.equal(error.message, 'Line contains incorrect line endings')
43 | assert.equal(error.context, 'Expected unix')
44 | assert.equal(error.rule, 'line_endings')
45 |
46 | 'Unknown line endings' :
47 |
48 | topic : 'x = 1\ny=2'
49 |
50 | 'throw errors' : (source) ->
51 | config =
52 | line_endings : {level: 'error', value: 'osx'}
53 | assert.throws () ->
54 | coffeelint.lint(source, config)
55 |
56 |
57 | }).export(module)
58 |
--------------------------------------------------------------------------------
/src/configfinder.coffee:
--------------------------------------------------------------------------------
1 | ###
2 | Helpers for finding CoffeeLint config in standard locations, similar to how
3 | JSHint does.
4 | ###
5 |
6 | fs = require 'fs'
7 | path = require 'path'
8 |
9 | # Cache for findFile
10 | findFileResults = {}
11 |
12 | # Searches for a file with a specified name starting with 'dir' and going all
13 | # the way up either until it finds the file or hits the root.
14 | findFile = (name, dir) ->
15 | dir = dir or process.cwd()
16 | filename = path.normalize(path.join(dir, name))
17 | return findFileResults[filename] if findFileResults[filename]
18 | parent = path.resolve(dir, "../")
19 | if fs.existsSync(filename)
20 | findFileResults[filename] = filename
21 | else if dir is parent
22 | findFileResults[filename] = null
23 | else
24 | findFile name, parent
25 |
26 | # Possibly find CoffeeLint configuration within a package.json file.
27 | loadNpmConfig = (dir) ->
28 | fp = findFile("package.json", dir)
29 | loadJSON(fp).coffeelintConfig if fp
30 |
31 | # Parse a JSON file gracefully.
32 | loadJSON = (filename) ->
33 | try
34 | JSON.parse(fs.readFileSync(filename).toString())
35 | catch e
36 | console.error "Could not load JSON file '%s': %s", filename, e
37 | null
38 |
39 | # Tries to find a configuration file in either project directory (if file is
40 | # given), as either the package.json's 'coffeelintConfig' property, or a project
41 | # specific 'coffeelint.json' or a global 'coffeelint.json' in the home
42 | # directory.
43 | exports.getConfig = (filename = null) ->
44 | if filename
45 | dir = path.dirname(path.resolve(filename))
46 | npmConfig = loadNpmConfig(dir)
47 | return npmConfig if npmConfig
48 | projConfig = findFile("coffeelint.json", dir)
49 | return loadJSON(projConfig) if projConfig
50 | envs = process.env.HOME or process.env.HOMEPATH or process.env.USERPROFILE
51 | home = path.normalize(path.join(envs, "coffeelint.json"))
52 | if fs.existsSync(home)
53 | console.log 'loaded', home
54 | return loadJSON(home)
55 |
--------------------------------------------------------------------------------
/src/rules/duplicate_key.coffee:
--------------------------------------------------------------------------------
1 |
2 | module.exports = class DuplicateKey
3 |
4 | rule:
5 | # I don't know of any legitimate reason to define duplicate keys in an
6 | # object. It seems to always be a mistake, it's also a syntax error in
7 | # strict mode.
8 | # See http://jslinterrors.com/duplicate-key-a/
9 | name: 'duplicate_key'
10 | level: 'error'
11 | message: 'Duplicate key defined in object or class'
12 | description:
13 | "Prevents defining duplicate keys in object literals and classes"
14 |
15 | tokens: [ 'IDENTIFIER', "{","}" ]
16 |
17 | constructor: ->
18 | @braceScopes = [] # A stack tracking keys defined in nexted scopes.
19 |
20 | lintToken : ([type], tokenApi) ->
21 |
22 | if type in [ "{","}" ]
23 | @lintBrace arguments...
24 | return undefined
25 |
26 | if type is "IDENTIFIER"
27 | @lintIdentifier arguments...
28 |
29 | lintIdentifier : (token, tokenApi) ->
30 | key = token[1]
31 |
32 | # Class names might not be in a scope
33 | return null if not @currentScope?
34 | nextToken = tokenApi.peek(1)
35 |
36 | # Exit if this identifier isn't being assigned. A and B
37 | # are identifiers, but only A should be examined:
38 | # A = B
39 | return null if nextToken[1] isnt ':'
40 | previousToken = tokenApi.peek(-1)
41 |
42 | # Assigning "@something" and "something" are not the same thing
43 | key = "@#{key}" if previousToken[0] == '@'
44 |
45 | # Added a prefix to not interfere with things like "constructor".
46 | key = "identifier-#{key}"
47 | if @currentScope[key]
48 | return true
49 | else
50 | @currentScope[key] = token
51 | null
52 |
53 | lintBrace : (token) ->
54 | if token[0] == '{'
55 | @braceScopes.push @currentScope if @currentScope?
56 | @currentScope = {}
57 | else
58 | @currentScope = @braceScopes.pop()
59 |
60 | return null
61 |
--------------------------------------------------------------------------------
/src/rules/no_unnecessary_double_quotes.coffee:
--------------------------------------------------------------------------------
1 |
2 |
3 | module.exports = class NoUnnecessaryDoubleQuotes
4 |
5 | rule:
6 | name: 'no_unnecessary_double_quotes'
7 | level : 'ignore'
8 | message : 'Unnecessary double quotes are forbidden'
9 | description: '''
10 | This rule prohibits double quotes unless string interpolation is
11 | used or the string contains single quotes.
12 |
13 | # Double quotes are discouraged:
14 | foo = "bar"
15 |
16 | # Unless string interpolation is used:
17 | foo = "#{bar}baz"
18 |
19 | # Or they prevent cumbersome escaping:
20 | foo = "I'm just following the 'rules'"
21 |
22 |
23 | Double quotes are permitted by default.
24 | '''
25 |
26 | tokens: [ 'STRING' ]
27 |
28 | lintToken : (token, tokenApi) ->
29 | tokenValue = token[1]
30 |
31 | stringValue = tokenValue.match(/^\"(.*)\"$/)
32 | return false unless stringValue # no double quotes, all OK
33 |
34 | hasLegalConstructs = @isInterpolated(tokenApi) or
35 | @containsSingleQuote(tokenValue)
36 |
37 | return not hasLegalConstructs
38 |
39 | isInterpolated : (tokenApi) ->
40 | currentIndex = tokenApi.i
41 | isInterpolated = false
42 | lineTokens = tokenApi.tokensByLine[tokenApi.lineNumber]
43 |
44 | # Traverse backwards to find signs that the current string token is
45 | # generated by the coffee rewriter for string interpolation
46 | for i in [1..currentIndex]
47 | token = tokenApi.peek(-i)
48 | tokenName = token[0]
49 |
50 | if tokenName is ')' and token.stringEnd
51 | break # No interpolation, we can quit
52 | else if tokenName is '(' and
53 | token.origin?[1] is "string interpolation"
54 | isInterpolated = true
55 | break
56 |
57 | return isInterpolated
58 |
59 | containsSingleQuote : (tokenValue) ->
60 | return tokenValue.indexOf("'") isnt -1
61 |
--------------------------------------------------------------------------------
/3rd_party_rules.md:
--------------------------------------------------------------------------------
1 | Creating a 3rd party rule
2 | =========================
3 |
4 | If your rule is overly specific or too restrictive it may not be accepted into
5 | the CoffeeLint repo. This doesn't mean you can't have your rule.
6 |
7 | Setup your repo
8 | ---------------
9 |
10 | My personal preference is to not commit compiled Javascript files to a
11 | CoffeeScript repo.
12 |
13 | 1. `npm init`
14 | 2. `npm install --save coffee-script` When it asks about `main` leave it as `index.js`
15 | 3. Create an `index.js` it only needs two lines
16 |
17 | require('coffee-script');
18 | module.exports = require('./your_rule_name.coffee');
19 |
20 | 4. `sudo npm link`: Once you run this CoffeeLint will be able to `require` your module
21 | 5. Build your rule, make sure the file name matches `index.js`'s `require`
22 |
23 | [link]: https://npmjs.org/doc/cli/npm-link.html
24 |
25 | Loading your rule
26 | -----------------
27 |
28 | It's probably best once you get this worked out, to commit a coffeelint.json to
29 | your repo with your rule enabled. Here is an example:
30 |
31 |
32 | {
33 | "your_rule_name": {
34 | "module": "your-rule-name"
35 | }
36 | }
37 |
38 |
39 | * `your_rule_name`: This MUST match the name inside your rule. A few of of the
40 | built in rules are no_plus_plus, no_tabs, and cyclomatic_complexity
41 | * `your-rule-name`: npm's convention is to use dashes. CoffeeLint is going to
42 | run `require('your-rule-name')` to find this rule.
43 |
44 | Publishing your rule
45 | --------------------
46 |
47 | Once you're ready you can publish it as a normal npm package. Remember to
48 | mention in your readme that they will need to `npm install -g your-rule-name`.
49 | If it's not global CoffeeLint won't see it.
50 |
51 | By convention rule authors add the keyword `coffeelintrule` to their npm
52 | `package.json` so custom rules can be found easily. Click
53 | [here](https://npmjs.org/search?q=coffeelintrule) to list all currently available
54 | custom rules on npm.
55 |
56 | Verify Installation
57 | -------------------
58 |
59 | This will verify your development install when you used `sudo npm link` or when
60 | users run `npm install -g your-rule-name`
61 |
62 | Verify your installation using:
63 |
64 | node -p "require('your-rule-name');"
65 |
66 | You should get output similar to
67 |
68 | [Function: YourRuleName]
69 |
--------------------------------------------------------------------------------
/src/rules/arrow_spacing.coffee:
--------------------------------------------------------------------------------
1 |
2 | module.exports = class ArrowSpacing
3 |
4 | rule:
5 | name: 'arrow_spacing'
6 | level : 'ignore'
7 | message : 'Function arrow (->) must be spaced properly'
8 | description: """
9 | This rule checks to see that there is spacing before and after 10 | the arrow operator that declares a function. This rule is disabled 11 | by default.
Note that if arrow_spacing is enabled, and you 12 | pass an empty function as a parameter, arrow_spacing will accept 13 | either a space or no space in-between the arrow operator and the 14 | parenthesis
15 |# Both of this will not trigger an error,
16 | # even with arrow_spacing enabled.
17 | x(-> 3)
18 | x( -> 3)
19 |
20 | # However, this will trigger an error
21 | x((a,b)-> 3)
22 |
23 |
24 | """
25 |
26 | tokens: [ '->' ]
27 |
28 | lintToken : (token, tokenApi) ->
29 | # Throw error unless the following happens.
30 | #
31 | # We will take a look at the previous token to see
32 | # 1. That the token is properly spaced
33 | # 2. Wasn't generated by the CoffeeScript compiler
34 | # 3. That it is just indentation
35 | # 4. If the function declaration has no parameters
36 | # e.g. x(-> 3)
37 | # x( -> 3)
38 | #
39 | # or a statement is wrapped in parentheses
40 | # e.g. (-> true)()
41 | #
42 | # we will accept either having a space or not having a space there.
43 |
44 | pp = tokenApi.peek(-1)
45 | unless (token.spaced? or token.newLine? or @atEof(tokenApi)) and
46 | # Throw error unless the previous token...
47 | ((pp.spaced? or pp[0] is 'TERMINATOR') or #1
48 | pp.generated? or #2
49 | pp[0] is "INDENT" or #3
50 | (pp[1] is "(" and not pp.generated?)) #4
51 | true
52 | else
53 | null
54 |
55 | # Are there any more meaningful tokens following the current one?
56 | atEof: (tokenApi) ->
57 | {tokens, i } = tokenApi
58 | for token in tokens.slice(i + 1)
59 | unless token.generated or token[0] in ['OUTDENT', 'TERMINATOR']
60 | return false
61 | true
62 |
--------------------------------------------------------------------------------
/test/test_1.5.0_plus.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 | vows = require 'vows'
3 | assert = require 'assert'
4 | CoffeeScript = require 'coffee-script'
5 | CoffeeScript.old_tokens = CoffeeScript.tokens
6 | CoffeeScript.tokens = (text) ->
7 | CoffeeScript.updated_tokens_called = true
8 | tokens = CoffeeScript.old_tokens(text)
9 | for token in tokens
10 | if typeof token[2] == "number"
11 | if token[0] == 'INDENT' or token[1] == 'OUTDENT'
12 | token[2] = {first_line: token[2] - 1, last_line: token[2]}
13 | else
14 | token[2] = {first_line: token[2], last_line: token[2]}
15 | token
16 | coffeelint = require path.join('..', 'lib', 'coffeelint')
17 |
18 |
19 | vows.describe("CoffeeScript 1.5.0+").addBatch({
20 |
21 | "lineNumber has become an object" :
22 |
23 | topic : () ->
24 | """
25 | x = 1234;
26 | y = 1234; z = 1234
27 | """
28 |
29 | 'work with 1.5.0+ tokens' : (source) ->
30 | assert.isUndefined(CoffeeScript.updated_tokens_called)
31 | errors = coffeelint.lint(source)
32 | assert.isArray(errors)
33 | assert.lengthOf(errors, 1)
34 | assert.isTrue(CoffeeScript.updated_tokens_called)
35 | error = errors[0]
36 | assert.equal(error.lineNumber, 1)
37 | assert.equal(error.message, "Line contains a trailing semicolon")
38 | assert.equal(error.rule, 'no_trailing_semicolons')
39 |
40 | "for indentation last_line is the correct value for lineNumber" :
41 |
42 | topic : () ->
43 | """
44 | x = () ->
45 | 'two spaces'
46 |
47 | a = () ->
48 | 'four spaces'
49 | """
50 |
51 | 'works with 1.5.0+' : (source) ->
52 | errors = coffeelint.lint(source)
53 | assert.equal(errors.length, 1)
54 | error = errors[0]
55 | msg = 'Line contains inconsistent indentation'
56 | assert.equal(error.message, msg)
57 | assert.equal(error.rule, 'indentation')
58 | assert.equal(error.lineNumber, 5)
59 | assert.equal(error.context, "Expected 2 got 4")
60 |
61 | }).addBatch({
62 |
63 | "Cleanup" : () ->
64 |
65 | CoffeeScript.tokens = CoffeeScript.old_tokens
66 | delete CoffeeScript.old_tokens
67 |
68 | }).export(module)
69 |
--------------------------------------------------------------------------------
/src/rules/no_trailing_semicolons.coffee:
--------------------------------------------------------------------------------
1 |
2 | regexes =
3 | trailingSemicolon : /;\r?$/
4 |
5 | module.exports = class NoTrailingSemicolons
6 |
7 | rule:
8 | name: 'no_trailing_semicolons'
9 | level : 'error'
10 | message : 'Line contains a trailing semicolon'
11 | description: """
12 | This rule prohibits trailing semicolons, since they are needless
13 | cruft in CoffeeScript.
14 |
15 | # This semicolon is meaningful.
16 | x = '1234'; console.log(x)
17 |
18 | # This semicolon is redundant.
19 | alert('end of line');
20 |
21 |
22 | Trailing semicolons are forbidden by default.
23 | """
24 |
25 |
26 | lintLine: (line, lineApi) ->
27 |
28 | # The TERMINATOR token is extended through to the next token. As a
29 | # result a line with a comment DOES have a token: the TERMINATOR from
30 | # the last line of code.
31 | lineTokens = lineApi.getLineTokens()
32 | if lineTokens.length is 1 and lineTokens[0][0] in ['TERMINATOR', 'HERECOMMENT']
33 | return
34 |
35 | newLine = line
36 | if lineTokens.length > 1 and lineTokens[lineTokens.length - 1][0] is 'TERMINATOR'
37 |
38 | # startPos contains the end position of the last non-TERMINATOR token
39 | # endPos contains the start position of the TERMINATOR token
40 | # if startPos and endPos arent equal, that probably means a comment
41 | # was sliced out of the tokenizer
42 |
43 | startPos = lineTokens[lineTokens.length - 2][2].last_column + 1
44 | endPos = lineTokens[lineTokens.length - 1][2].first_column
45 | if (startPos isnt endPos)
46 | startCounter = startPos
47 | while line[startCounter] isnt "#" and startCounter < line.length
48 | startCounter++
49 | newLine = line.substring(0, startCounter).replace(/\s*$/, '')
50 |
51 | hasSemicolon = regexes.trailingSemicolon.test(newLine)
52 | [first..., last] = lineTokens
53 | hasNewLine = last and last.newLine?
54 |
55 | # Don't throw errors when the contents of multiline strings,
56 | # regexes and the like end in ";"
57 | if hasSemicolon and not hasNewLine and lineApi.lineHasToken() and
58 | last[0] isnt 'STRING'
59 | return true
60 |
--------------------------------------------------------------------------------
/src/rules/colon_assignment_spacing.coffee:
--------------------------------------------------------------------------------
1 | module.exports = class ColonAssignmentSpacing
2 |
3 | rule :
4 | name : 'colon_assignment_spacing'
5 | level : 'ignore'
6 | message :'Colon assignment without proper spacing'
7 | spacing :
8 | left : 0
9 | right : 0
10 | description : """
11 | This rule checks to see that there is spacing before and 12 | after the colon in a colon assignment (i.e., classes, objects). 13 | The spacing amount is specified by 14 | spacing.left and spacing.right, respectively. 15 | A zero value means no spacing required. 16 |
17 |
18 | #
19 | # If spacing.left and spacing.right is 1
20 | #
21 |
22 | # Good
23 | object = {spacing : true}
24 | class Dog
25 | canBark : true
26 |
27 | # Bad
28 | object = {spacing: true}
29 | class Cat
30 | canBark: false
31 |
32 | """
33 |
34 | tokens : [':']
35 |
36 | lintToken : (token, tokenApi) ->
37 | spacingAllowances = tokenApi.config[@rule.name].spacing
38 | previousToken = tokenApi.peek -1
39 | nextToken = tokenApi.peek 1
40 |
41 | getSpaceFromToken = (direction) ->
42 | switch direction
43 | when 'left'
44 | token[2].first_column - previousToken[2].last_column - 1
45 | when 'right'
46 | nextToken[2].first_column - token[2].first_column - 1
47 |
48 | checkSpacing = (direction) ->
49 | spacing = getSpaceFromToken direction
50 | # when spacing is negative, the neighboring token is a newline
51 | isSpaced = if spacing < 0 then true else spacing is parseInt spacingAllowances[direction]
52 | [isSpaced, spacing]
53 |
54 | [isLeftSpaced, leftSpacing] = checkSpacing 'left'
55 | [isRightSpaced, rightSpacing] = checkSpacing 'right'
56 |
57 | if isLeftSpaced and isRightSpaced
58 | null
59 | else
60 | context :
61 | """
62 | Incorrect spacing around column #{token[2].first_column}.
63 | Expected left: #{spacingAllowances.left}, right: #{spacingAllowances.right}.
64 | Got left: #{leftSpacing}, right: #{rightSpacing}.
65 | """
--------------------------------------------------------------------------------
/test/test_no_unnecessary_fat_arrows.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 | vows = require 'vows'
3 | assert = require 'assert'
4 | coffeelint = require path.join('..', 'lib', 'coffeelint')
5 |
6 | runLint = (source) ->
7 | config = no_unnecessary_fat_arrows: level: 'error'
8 | coffeelint.lint source, config
9 |
10 | shouldError = (source, numErrors = 1) ->
11 | topic: source
12 | 'errors for unnecessary arrow': (source) ->
13 | errors = runLint source
14 | assert.lengthOf errors, numErrors, "Expected #{numErrors} errors"
15 | error = errors[0]
16 | assert.equal error.rule, 'no_unnecessary_fat_arrows'
17 |
18 | shouldPass = (source) ->
19 | topic: source
20 | 'does not error for necessary arrow': (source) ->
21 | errors = runLint source
22 | assert.isEmpty errors, "Expected no errors, got #{errors}"
23 |
24 | vows.describe('no unnecessary fat arrows').addBatch({
25 |
26 | 'empty function' : shouldError '=>'
27 | 'simple function' : shouldError '=> 1'
28 | 'function with this' : shouldPass '=> this'
29 | 'function with this.a' : shouldPass '=> this.a'
30 | 'function with @' : shouldPass '=> @'
31 | 'function with @a' : shouldPass '=> @a'
32 |
33 | 'nested simple functions':
34 | 'with inner fat arrow': shouldError '-> => 1'
35 | 'with outer fat arrow': shouldError '=> -> 1'
36 | 'with both fat arrows': shouldError '=> => 1', 2
37 |
38 | 'nested functions with this inside':
39 | 'with inner fat arrow': shouldPass '-> => this'
40 | 'with outer fat arrow': shouldError '=> -> this'
41 | 'with both fat arrows': shouldPass '=> => this'
42 |
43 | 'nested functions with this outside':
44 | 'with inner fat arrow': shouldError '-> (this; =>)'
45 | 'with outer fat arrow': shouldPass '=> (this; ->)'
46 | 'with both fat arrows': shouldError '=> (this; =>)'
47 |
48 | 'deeply nested simple function' : shouldError '-> -> -> -> => 1'
49 | 'deeply nested function with this' : shouldPass '-> -> -> -> => this'
50 |
51 | 'functions with multiple statements': shouldError """
52 | f = ->
53 | x = 2
54 | z ((a) => x; a)
55 | """
56 |
57 | 'functions with parameters' : shouldError '(a) =>'
58 | 'functions with parameter assignment' : shouldPass '(@a) =>'
59 | 'functions with destructuring parameter assignment': shouldPass '({@a}) =>'
60 |
61 | }).export(module)
62 |
--------------------------------------------------------------------------------
/test/test_tabs.coffee:
--------------------------------------------------------------------------------
1 | #
2 | # Tab tests.
3 | #
4 |
5 |
6 | path = require 'path'
7 | vows = require 'vows'
8 | assert = require 'assert'
9 | coffeelint = require path.join('..', 'lib', 'coffeelint')
10 |
11 |
12 | vows.describe('tabs').addBatch({
13 |
14 | 'Tabs' :
15 |
16 | topic : () ->
17 | """
18 | x = () ->
19 | \ty = () ->
20 | \t\treturn 1234
21 | """
22 |
23 | 'can be forbidden' : (source) ->
24 | config = {}
25 | errors = coffeelint.lint(source, config)
26 | assert.equal(errors.length, 4)
27 | error = errors[1]
28 | assert.equal(error.lineNumber, 2)
29 | assert.equal("Line contains tab indentation", error.message)
30 | assert.equal(error.rule, 'no_tabs')
31 |
32 | 'can be permitted' : (source) ->
33 | config =
34 | no_tabs : {level: 'ignore'}
35 | indentation : {level: 'error', value: 1}
36 | errors = coffeelint.lint(source, config)
37 | assert.equal(errors.length, 0)
38 |
39 | 'are forbidden by default' : (source) ->
40 | config =
41 | indentation : {level: 'error', value: 1}
42 | errors = coffeelint.lint(source, config)
43 | assert.isArray(errors)
44 | assert.equal(errors.length, 2)
45 |
46 | 'are allowed in strings' : () ->
47 | source = "x = () -> '\t'"
48 | errors = coffeelint.lint(source)
49 | assert.equal(errors.length, 0)
50 |
51 | 'Tabs in multi-line strings' :
52 |
53 | topic : '''
54 | x = 1234
55 | y = """
56 | \t\tasdf
57 | """
58 | '''
59 |
60 | 'are ignored' : (errors) ->
61 | errors = coffeelint.lint(errors)
62 | assert.isEmpty(errors)
63 |
64 | 'Tabs in Heredocs' :
65 |
66 | topic : '''
67 | ###
68 | \t\tMy Heredoc
69 | ###
70 | '''
71 |
72 | 'are ignored' : (errors) ->
73 | errors = coffeelint.lint(errors)
74 | assert.isEmpty(errors)
75 |
76 | 'Tabs in multi line regular expressions' :
77 |
78 | topic : '''
79 | ///
80 | \t\tMy Heredoc
81 | ///
82 | '''
83 |
84 | 'are ignored' : (errors) ->
85 | errors = coffeelint.lint(errors)
86 | assert.isEmpty(errors)
87 |
88 |
89 | }).export(module)
90 |
--------------------------------------------------------------------------------
/test/test_identifier.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 | vows = require 'vows'
3 | assert = require 'assert'
4 | coffeelint = require path.join('..', 'lib', 'coffeelint')
5 |
6 |
7 | vows.describe('identifiers').addBatch({
8 |
9 | 'Camel cased class names' :
10 |
11 | topic : """
12 | class Animal
13 |
14 | class Wolf extends Animal
15 |
16 | class BurmesePython extends Animal
17 |
18 | class Band
19 |
20 | class ELO extends Band
21 |
22 | class Eiffel65 extends Band
23 |
24 | class nested.Name
25 |
26 | class deeply.nested.Name
27 | """
28 |
29 | 'are valid by default' : (source) ->
30 | errors = coffeelint.lint(source)
31 | assert.isEmpty(errors)
32 |
33 | 'Non camel case class names' :
34 |
35 | topic : """
36 | class animal
37 |
38 | class wolf extends Animal
39 |
40 | class Burmese_Python extends Animal
41 |
42 | class canadaGoose extends Animal
43 | """
44 |
45 | 'are rejected by default' : (source) ->
46 | errors = coffeelint.lint(source)
47 | assert.lengthOf(errors, 4)
48 | error = errors[0]
49 | assert.equal(error.lineNumber, 1)
50 | assert.equal(error.message, 'Class names should be camel cased')
51 | assert.equal(error.context, 'class name: animal')
52 | assert.equal(error.rule, 'camel_case_classes')
53 |
54 | 'can be permitted' : (source) ->
55 | config =
56 | camel_case_classes : {level : 'ignore'}
57 | errors = coffeelint.lint(source, config)
58 | assert.isEmpty(errors)
59 |
60 | 'Anonymous class names' :
61 |
62 | topic : """
63 | x = class
64 | m : -> 123
65 |
66 | y = class extends x
67 | m : -> 456
68 |
69 | z = class
70 |
71 | r = class then 1:2
72 | """
73 |
74 | 'are permitted' : (source) ->
75 | errors = coffeelint.lint(source)
76 | assert.isEmpty(errors)
77 |
78 | 'Inner classes are permitted' :
79 |
80 | topic : '''
81 | class X
82 | class @Y
83 | f : 123
84 | class @constructor.Z
85 | f : 456
86 | '''
87 |
88 | 'are permitted' : (source) ->
89 | errors = coffeelint.lint(source)
90 | assert.lengthOf(errors, 0)
91 |
92 | }).export(module)
93 |
--------------------------------------------------------------------------------
/src/lexical_linter.coffee:
--------------------------------------------------------------------------------
1 |
2 | class TokenApi
3 | constructor: (CoffeeScript, source, @config, @tokensByLine) ->
4 | @tokens = CoffeeScript.tokens(source)
5 | @lines = source.split('\n')
6 | @tokensByLine = {} # A map of tokens by line.
7 |
8 | i: 0 # The index of the current token we're linting.
9 |
10 | # Return the token n places away from the current token.
11 | peek : (n = 1) ->
12 | @tokens[@i + n] || null
13 |
14 |
15 | BaseLinter = require './base_linter.coffee'
16 |
17 | #
18 | # A class that performs checks on the output of CoffeeScript's lexer.
19 | #
20 | module.exports = class LexicalLinter extends BaseLinter
21 |
22 | constructor : (source, config, rules, CoffeeScript) ->
23 | super source, config, rules
24 |
25 | @tokenApi = new TokenApi CoffeeScript, source, @config, @tokensByLine
26 | # This needs to be available on the LexicalLinter so it can be passed
27 | # to the LineLinter when this finishes running.
28 | @tokensByLine = @tokenApi.tokensByLine
29 |
30 | acceptRule: (rule) ->
31 | return typeof rule.lintToken is 'function'
32 |
33 | # Return a list of errors encountered in the given source.
34 | lint : () ->
35 | errors = []
36 |
37 | for token, i in @tokenApi.tokens
38 | @tokenApi.i = i
39 | errors.push(error) for error in @lintToken(token)
40 | errors
41 |
42 |
43 | # Return an error if the given token fails a lint check, false otherwise.
44 | lintToken : (token) ->
45 | [type, value, lineNumber] = token
46 |
47 | if typeof lineNumber == "object"
48 | if type == 'OUTDENT' or type == 'INDENT'
49 | lineNumber = lineNumber.last_line
50 | else
51 | lineNumber = lineNumber.first_line
52 | @tokensByLine[lineNumber] ?= []
53 | @tokensByLine[lineNumber].push(token)
54 | # CoffeeScript loses line numbers of interpolations and multi-line
55 | # regexes, so fake it by using the last line number we know.
56 | @lineNumber = lineNumber or @lineNumber or 0
57 |
58 | @tokenApi.lineNumber = @lineNumber
59 |
60 | # Multiple rules might run against the same token to build context.
61 | # Every every rule should run even if something has already produced an
62 | # error for the same token.
63 | errors = []
64 | for rule in @rules when token[0] in rule.tokens
65 | v = @normalizeResult rule, rule.lintToken(token, @tokenApi)
66 | errors.push v if v?
67 | errors
68 |
69 | createError : (ruleName, attrs = {}) ->
70 | attrs.lineNumber = @lineNumber + 1
71 | attrs.line = @tokenApi.lines[@lineNumber]
72 | super ruleName, attrs
73 |
74 |
--------------------------------------------------------------------------------
/src/rules/missing_fat_arrows.coffee:
--------------------------------------------------------------------------------
1 | any = (arr, test) -> arr.reduce ((res, elt) -> res or test elt), false
2 |
3 | module.exports = class MissingFatArrows
4 |
5 | rule:
6 | name: 'missing_fat_arrows'
7 | level: 'ignore'
8 | message: 'Used `this` in a function without a fat arrow'
9 | description: """
10 | Warns when you use `this` inside a function that wasn't defined
11 | with a fat arrow. This rule does not apply to methods defined in a
12 | class, since they have `this` bound to the class instance (or the
13 | class itself, for class methods).
14 |
15 | It is impossible to statically determine whether a function using
16 | `this` will be bound with the correct `this` value due to language
17 | features like `Function.prototype.call` and
18 | `Function.prototype.bind`, so this rule may produce false positives.
19 | """
20 |
21 | lintAST: (node, @astApi) ->
22 | @lintNode node
23 | undefined
24 |
25 | lintNode: (node, methods = []) ->
26 | if (not @isFatArrowCode node) and
27 | # Ignore any nodes we know to be methods
28 | (node not in methods) and
29 | (@needsFatArrow node)
30 | error = @astApi.createError
31 | lineNumber: node.locationData.first_line + 1
32 | @errors.push error
33 |
34 | node.eachChild (child) => @lintNode child,
35 | switch
36 | when @isClass node then @methodsOfClass node
37 | # Once we've hit a function, we know we can't be in the top
38 | # level of a method anymore, so we can safely reset the methods
39 | # to empty to save work.
40 | when @isCode node then []
41 | else methods
42 |
43 | isCode: (node) => @astApi.getNodeName(node) is 'Code'
44 | isClass: (node) => @astApi.getNodeName(node) is 'Class'
45 | isValue: (node) => @astApi.getNodeName(node) is 'Value'
46 | isObject: (node) => @astApi.getNodeName(node) is 'Obj'
47 | isThis: (node) => @isValue(node) and node.base.value is 'this'
48 | isFatArrowCode: (node) => @isCode(node) and node.bound
49 |
50 | needsFatArrow: (node) ->
51 | @isCode(node) and (
52 | any(node.params, (param) => param.contains(@isThis)?) or
53 | node.body.contains(@isThis)?
54 | )
55 |
56 | methodsOfClass: (classNode) ->
57 | bodyNodes = classNode.body.expressions
58 | returnNode = bodyNodes[bodyNodes.length - 1]
59 | if returnNode? and @isValue(returnNode) and @isObject(returnNode.base)
60 | returnNode.base.properties
61 | .map((assignNode) -> assignNode.value)
62 | .filter(@isCode)
63 | else []
64 |
65 |
--------------------------------------------------------------------------------
/test/test_braces.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 | vows = require 'vows'
3 | assert = require 'assert'
4 | coffeelint = require path.join('..', 'lib', 'coffeelint')
5 |
6 | vows.describe('braces').addBatch({
7 |
8 | 'Implicit braces' :
9 |
10 | topic : () ->
11 | '''
12 | a = 1:2
13 | y =
14 | 'a':'b'
15 | 3:4
16 | '''
17 |
18 | 'are allowed by default' : (source) ->
19 | errors = coffeelint.lint(source)
20 | assert.isArray(errors)
21 | assert.isEmpty(errors)
22 |
23 | 'can be forbidden' : (source) ->
24 | config = {no_implicit_braces : {level:'error'}}
25 | errors = coffeelint.lint(source, config)
26 | assert.isArray(errors)
27 | assert.lengthOf(errors, 2)
28 | error = errors[0]
29 | assert.equal(error.lineNumber, 1)
30 | assert.equal(error.message, 'Implicit braces are forbidden')
31 | assert.equal(error.rule, 'no_implicit_braces')
32 |
33 | 'Implicit braces strict' :
34 | topic: """
35 | foo =
36 | bar:
37 | baz: 1
38 | thing: 'a'
39 | baz: ['a', 'b', 'c']
40 | """
41 |
42 | "blocks all implicit braces by default": (source) ->
43 | config = {no_implicit_braces : {level:'error'}}
44 | errors = coffeelint.lint(source, config)
45 | assert.isArray(errors)
46 | assert.lengthOf(errors, 2)
47 | assert.equal(rule, 'no_implicit_braces') for {rule} in errors
48 |
49 | "allows braces at the end of lines when strict is false": (source) ->
50 | config =
51 | no_implicit_braces :
52 | level:'error'
53 | strict: false
54 | errors = coffeelint.lint(source, config)
55 | assert.isArray(errors)
56 | assert.isEmpty(errors)
57 |
58 | 'Implicit braces in class definitions' :
59 |
60 | topic : () ->
61 | '''
62 | class Animal
63 | walk: ->
64 |
65 | class Wolf extends Animal
66 | howl: ->
67 |
68 | class nested.Name
69 | constructor: (@options) ->
70 |
71 | class deeply.nested.Name
72 | constructor: (@options) ->
73 |
74 | x = class
75 | m : -> 123
76 |
77 | y = class extends x
78 | m : -> 456
79 |
80 | z = class
81 |
82 | r = class then 1:2
83 | '''
84 |
85 | 'are always ignored' : (source) ->
86 | config = {no_implicit_braces : {level:'error'}}
87 | errors = coffeelint.lint(source)
88 | assert.isArray(errors)
89 | assert.isEmpty(errors)
90 |
91 | }).export(module)
92 |
--------------------------------------------------------------------------------
/test/test_line_length.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 | vows = require 'vows'
3 | assert = require 'assert'
4 | coffeelint = require path.join('..', 'lib', 'coffeelint')
5 |
6 |
7 | vows.describe('linelength').addBatch({
8 |
9 | 'Maximum line length' :
10 |
11 | topic : () ->
12 | # Every line generated here is a comment.
13 | line = (length) ->
14 | return '# ' + new Array(length - 1).join('-')
15 | lengths = [50, 79, 80, 81, 100, 200]
16 | (line(l) for l in lengths).join("\n")
17 |
18 | 'defaults to 80' : (source) ->
19 | errors = coffeelint.lint(source)
20 | assert.equal(errors.length, 3)
21 | error = errors[0]
22 | assert.equal(error.lineNumber, 4)
23 | assert.equal(error.message, "Line exceeds maximum allowed length")
24 | assert.equal(error.rule, 'max_line_length')
25 |
26 | 'is configurable' : (source) ->
27 | config =
28 | max_line_length :
29 | value: 99
30 | level: 'error'
31 | errors = coffeelint.lint(source, config)
32 | assert.equal(errors.length, 2)
33 |
34 | 'is optional' : (source) ->
35 | for length in [null, 0, false]
36 | config =
37 | max_line_length :
38 | value: length
39 | level: 'ignore'
40 | errors = coffeelint.lint(source, config)
41 | assert.isEmpty(errors)
42 |
43 | 'can ignore comments': (source) ->
44 | config =
45 | max_line_length:
46 | limitComments: false
47 |
48 | errors = coffeelint.lint(source, config)
49 | assert.isEmpty(errors)
50 |
51 | 'Literate Line Length' :
52 | topic: ->
53 | # This creates a line with 80 Xs.
54 | source = new Array(81).join('X') + "\n"
55 |
56 | # Long URLs are ignored by default even in Literate code.
57 | source += "http://testing.example.com/really-really-long-url-" +
58 | "that-shouldnt-have-to-be-split-to-avoid-the-lint-error"
59 |
60 | '': (source) ->
61 | errors = coffeelint.lint(source, {}, true)
62 | assert.isEmpty(errors)
63 |
64 | 'Maximum length exceptions':
65 | topic: """
66 | # Since the line length check only reads lines in isolation it will
67 | # see the following line as a comment even though it's in a string.
68 | # I don't think that's a problem.
69 | #
70 | # http://testing.example.com/really-really-long-url-that-shouldnt-have-to-be-split-to-avoid-the-lint-error
71 | """
72 |
73 | 'excludes long urls': (source) ->
74 | errors = coffeelint.lint(source)
75 | assert.isEmpty(errors)
76 |
77 | }).export(module)
78 |
--------------------------------------------------------------------------------
/test/test_constructor_needs_parens.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 | vows = require 'vows'
3 | assert = require 'assert'
4 | coffeelint = require path.join('..', 'lib', 'coffeelint')
5 |
6 | vows.describe('newparens').addBatch({
7 |
8 | 'Missing Parentheses on "new Foo"' :
9 |
10 | topic: () ->
11 | """
12 | class Foo
13 |
14 | # Warn about missing parens here
15 | a = new Foo
16 | b = new bar.foo.Foo
17 | # The parens make it clear no parameters are intended
18 | c = new Foo()
19 | d = new bar.foo.Foo()
20 | e = new Foo 1, 2
21 | f = new bar.foo.Foo 1, 2
22 | # Since this does have a parameter it should not require parens
23 | g = new bar.foo.Foo
24 | config: 'parameter'
25 | """
26 |
27 | 'warns about missing parens': (source) ->
28 | config =
29 | empty_constructor_needs_parens:
30 | level: 'error'
31 | errors = coffeelint.lint(source, config)
32 | assert.equal(errors.length, 2)
33 | assert.equal(errors[0].lineNumber, 4)
34 | assert.equal(errors[0].rule, 'empty_constructor_needs_parens')
35 | assert.equal(errors[1].lineNumber, 5)
36 | assert.equal(errors[1].rule, 'empty_constructor_needs_parens')
37 |
38 | 'Missing Parentheses on "new Foo 1, 2"' :
39 |
40 | topic: () ->
41 | """
42 | class Foo
43 |
44 | a = new Foo
45 | b = new Foo()
46 | # Warn about missing parens here
47 | c = new Foo 1, 2
48 | d = new Foo
49 | config: 'parameter'
50 | e = new bar.foo.Foo 1, 2
51 | f = new bar.foo.Foo
52 | config: 'parameter'
53 | # But not here
54 | g = new Foo(1, 2)
55 | h = new Foo(
56 | config: 'parameter'
57 | )
58 | i = new bar.foo.Foo(1, 2)
59 | j = new bar.foo.Foo(
60 | config: 'parameter'
61 | )
62 | """
63 |
64 | 'warns about missing parens': (source) ->
65 | config =
66 | non_empty_constructor_needs_parens:
67 | level: 'error'
68 | errors = coffeelint.lint(source, config)
69 | assert.equal(errors.length, 4)
70 | assert.equal(errors[0].lineNumber, 6)
71 | assert.equal(errors[0].rule, 'non_empty_constructor_needs_parens')
72 | assert.equal(errors[1].lineNumber, 7)
73 | assert.equal(errors[1].rule, 'non_empty_constructor_needs_parens')
74 | assert.equal(errors[2].lineNumber, 9)
75 | assert.equal(errors[2].rule, 'non_empty_constructor_needs_parens')
76 | assert.equal(errors[3].lineNumber, 10)
77 | assert.equal(errors[3].rule, 'non_empty_constructor_needs_parens')
78 |
79 | }).export(module)
80 |
--------------------------------------------------------------------------------
/src/ast_linter.coffee:
--------------------------------------------------------------------------------
1 | BaseLinter = require './base_linter.coffee'
2 |
3 | node_children =
4 | Class: ['variable', 'parent', 'body']
5 | Code: ['params', 'body']
6 | For: ['body', 'source', 'guard', 'step']
7 | If: ['condition', 'body', 'elseBody']
8 | Obj: ['properties']
9 | Op: ['first', 'second']
10 | Switch: ['subject', 'cases', 'otherwise']
11 | Try: ['attempt', 'recovery', 'ensure']
12 | Value: ['base', 'properties']
13 | While: ['condition', 'guard', 'body']
14 |
15 | hasChildren = (node, children) ->
16 | node?.children?.length is children.length and
17 | node?.children.every (elem, i) -> elem is children[i]
18 |
19 | class ASTApi
20 | constructor: (@config) ->
21 | getNodeName: (node) ->
22 | name = node?.constructor?.name
23 | if node_children[name]
24 | return name
25 | else
26 | for own name, children of node_children
27 | if hasChildren(node, children)
28 | return name
29 |
30 |
31 | # A class that performs static analysis of the abstract
32 | # syntax tree.
33 | module.exports = class ASTLinter extends BaseLinter
34 |
35 | constructor : (source, config, rules, @CoffeeScript) ->
36 | super source, config, rules
37 | @astApi = new ASTApi @config
38 |
39 | # This uses lintAST instead of lintNode because I think it makes it a bit
40 | # more clear that the rule needs to walk the AST on its own.
41 | acceptRule: (rule) ->
42 | return typeof rule.lintAST is 'function'
43 |
44 | lint : () ->
45 | errors = []
46 | try
47 | @node = @CoffeeScript.nodes(@source)
48 | catch coffeeError
49 | # If for some reason you shut off the 'coffeescript_error' rule err
50 | # will be null and should NOT be added to errors
51 | err = @_parseCoffeeScriptError(coffeeError)
52 | errors.push err if err?
53 | return errors
54 |
55 | for rule in @rules
56 | @astApi.createError = (attrs = {}) =>
57 | @createError rule.rule.name, attrs
58 |
59 | # HACK: Push the local errors object into the plugin. This is a
60 | # temporary solution until I have a way for it to really return
61 | # multiple errors.
62 | rule.errors = errors
63 | v = @normalizeResult rule, rule.lintAST(@node, @astApi)
64 |
65 | return v if v?
66 | errors
67 |
68 | _parseCoffeeScriptError : (coffeeError) ->
69 | rule = @config['coffeescript_error']
70 |
71 | message = coffeeError.toString()
72 |
73 | # Parse the line number
74 | lineNumber = -1
75 | if coffeeError.location?
76 | lineNumber = coffeeError.location.first_line + 1
77 | else
78 | match = /line (\d+)/.exec message
79 | lineNumber = parseInt match[1], 10 if match?.length > 1
80 | attrs = {
81 | message: message
82 | level: rule.level
83 | lineNumber: lineNumber
84 | }
85 | return @createError 'coffeescript_error', attrs
86 |
87 |
88 |
--------------------------------------------------------------------------------
/test/test_no_implicit_parens.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 | vows = require 'vows'
3 | assert = require 'assert'
4 | coffeelint = require path.join('..', 'lib', 'coffeelint')
5 |
6 | vows.describe('parens').addBatch({
7 |
8 | 'Implicit parens' :
9 |
10 | topic : () ->
11 | '''
12 | console.log 'implict parens'
13 | blah = (a, b) ->
14 | blah 'a', 'b'
15 |
16 | class A
17 | @configure(1, 2, 3)
18 |
19 | constructor: ->
20 |
21 | class B
22 | _defaultC = 5
23 |
24 | constructor: (a) ->
25 | @c = a ? _defaultC
26 | '''
27 |
28 | 'are allowed by default' : (source) ->
29 | errors = coffeelint.lint(source)
30 | assert.isArray(errors)
31 | assert.isEmpty(errors)
32 |
33 | 'can be forbidden' : (source) ->
34 | config = {no_implicit_parens : {level:'error'}}
35 | errors = coffeelint.lint(source, config)
36 | assert.isArray(errors)
37 | assert.lengthOf(errors, 2)
38 | error = errors[0]
39 | assert.equal(error.lineNumber, 1)
40 | assert.equal(error.message, 'Implicit parens are forbidden')
41 | assert.equal(error.rule, 'no_implicit_parens')
42 |
43 | 'No implicit parens strict' :
44 | topic: """
45 | blah = (a, b) ->
46 | blah 'a'
47 | , 'b'
48 | """
49 |
50 | "blocks all implicit parens by default": (source) ->
51 | config = {no_implicit_parens : {level:'error'}}
52 | errors = coffeelint.lint(source, config)
53 | assert.isArray(errors)
54 | assert.lengthOf(errors, 1)
55 | assert.equal(rule, 'no_implicit_parens') for {rule} in errors
56 |
57 | "allows parens at the end of lines when strict is false": (source) ->
58 | config =
59 | no_implicit_parens:
60 | level:'error'
61 | strict: false
62 | errors = coffeelint.lint(source, config)
63 | assert.isArray(errors)
64 | assert.isEmpty(errors)
65 |
66 | 'Nested no implicit parens strict' :
67 | topic: """
68 | blah = (a, b) ->
69 |
70 | blah 'a'
71 | , blah('c', 'd')
72 |
73 | blah 'a'
74 | , (blah 'c'
75 | , 'd')
76 | """
77 |
78 | "blocks all implicit parens by default": (source) ->
79 | config = {no_implicit_parens : {level:'error'}}
80 | errors = coffeelint.lint(source, config)
81 | assert.isArray(errors)
82 | assert.lengthOf(errors, 3)
83 | assert.equal(rule, 'no_implicit_parens') for {rule} in errors
84 |
85 | "allows parens at the end of lines when strict is false": (source) ->
86 | config =
87 | no_implicit_parens:
88 | level:'error'
89 | strict: false
90 | errors = coffeelint.lint(source, config)
91 | assert.isArray(errors)
92 | assert.isEmpty(errors)
93 |
94 | }).export(module)
95 |
--------------------------------------------------------------------------------
/src/rules/space_operators.coffee:
--------------------------------------------------------------------------------
1 |
2 | module.exports = class SpaceOperators
3 |
4 | rule:
5 | name: 'space_operators'
6 | level : 'ignore'
7 | message : 'Operators must be spaced properly'
8 | description: "This rule enforces that operators have space around them."
9 |
10 | tokens: [ "+", "-", "=", "**", "MATH", "COMPARE", "LOGIC",
11 | "COMPOUND_ASSIGN", "(", ")", "CALL_START", "CALL_END" ]
12 |
13 | constructor: ->
14 | @callTokens = [] # A stack tracking the call token pairs.
15 | @parenTokens = [] # A stack tracking the parens token pairs.
16 |
17 | lintToken : ([type], tokenApi) ->
18 |
19 | # These just keep track of state
20 | if type in [ "CALL_START", "CALL_END" ]
21 | @lintCall arguments...
22 | return undefined
23 | if type in [ "(", ")" ]
24 | @lintParens arguments...
25 | return undefined
26 |
27 |
28 | # These may return errors
29 | if type in [ "+", "-" ]
30 | @lintPlus arguments...
31 | else
32 | @lintMath arguments...
33 |
34 | lintPlus: (token, tokenApi) ->
35 | # We can't check this inside of interpolations right now, because the
36 | # plusses used for the string type co-ercion are marked not spaced.
37 | if @isInInterpolation() or @isInExtendedRegex()
38 | return null
39 |
40 | p = tokenApi.peek(-1)
41 | unaries = ['TERMINATOR', '(', '=', '-', '+', ',', 'CALL_START',
42 | 'INDEX_START', '..', '...', 'COMPARE', 'IF',
43 | 'THROW', 'LOGIC', 'POST_IF', ':', '[', 'INDENT',
44 | 'COMPOUND_ASSIGN', 'RETURN', 'MATH', 'BY', 'LEADING_WHEN']
45 | isUnary = if not p then false else p[0] in unaries
46 | if (isUnary and token.spaced) or
47 | (not isUnary and not token.spaced and not token.newLine)
48 | return {context: token[1]}
49 | else
50 | null
51 |
52 | lintMath: (token, tokenApi) ->
53 | if not token.spaced and not token.newLine
54 | return {context: token[1]}
55 | else
56 | null
57 |
58 | isInExtendedRegex : () ->
59 | for t in @callTokens
60 | return true if t.isRegex
61 | return false
62 |
63 | lintCall : (token, tokenApi) ->
64 | if token[0] == 'CALL_START'
65 | p = tokenApi.peek(-1)
66 | # Track regex calls, to know (approximately) if we're in an
67 | # extended regex.
68 | token.isRegex = p and p[0] == 'IDENTIFIER' and p[1] == 'RegExp'
69 | @callTokens.push(token)
70 | else
71 | @callTokens.pop()
72 | return null
73 |
74 | isInInterpolation : () ->
75 | for t in @parenTokens
76 | return true if t.isInterpolation
77 | return false
78 |
79 | lintParens : (token, tokenApi) ->
80 | if token[0] == '('
81 | p1 = tokenApi.peek(-1)
82 | n1 = tokenApi.peek(1)
83 | n2 = tokenApi.peek(2)
84 | # String interpolations start with '' + so start the type co-ercion,
85 | # so track if we're inside of one. This is most definitely not
86 | # 100% true but what else can we do?
87 | i = n1 and n2 and n1[0] == 'STRING' and n2[0] == '+'
88 | token.isInterpolation = i
89 | @parenTokens.push(token)
90 | else
91 | @parenTokens.pop()
92 | # We're not linting, just tracking interpolations.
93 | null
94 |
--------------------------------------------------------------------------------
/test/test_newlines_after_classes.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 | vows = require 'vows'
3 | assert = require 'assert'
4 | coffeelint = require path.join('..', 'lib', 'coffeelint')
5 |
6 |
7 | vows.describe('newlines_after_classes').addBatch({
8 |
9 | 'Classfile ends with end of class' :
10 |
11 | topic : () ->
12 | """
13 | class Foo
14 |
15 | constructor: () ->
16 | bla()
17 |
18 | a: "b"
19 | c: "d"
20 | """
21 |
22 | "won't match" : (source) ->
23 | config =
24 | newlines_after_classes:
25 | level: 'error'
26 | value: 3
27 | indentation:
28 | level: 'ignore'
29 | value: 4
30 | errors = coffeelint.lint(source, config)
31 | assert.equal(errors.length, 0)
32 |
33 |
34 | 'Class with arbitrary Code following' :
35 |
36 | topic : () ->
37 | """
38 | class Foo
39 |
40 | constructor: ( ) ->
41 | bla()
42 |
43 | a: "b"
44 | c: "d"
45 |
46 |
47 |
48 | class Bar extends Foo
49 |
50 | constructor: ( ) ->
51 | bla()
52 | """
53 |
54 | "defaults to ignore newlines_after_classes" : (source) ->
55 | config =
56 | indentation:
57 | level: 'ignore'
58 | value: 4
59 | errors = coffeelint.lint(source, config)
60 | assert.equal(errors.length, 0)
61 |
62 | "has too few newlines after class" : (source) ->
63 | config =
64 | newlines_after_classes:
65 | level: 'error'
66 | value: 4
67 | indentation:
68 | level: 'ignore'
69 | value: 4
70 | errors = coffeelint.lint(source, config)
71 | assert.equal(errors.length, 1)
72 | error = errors[0]
73 | msg = 'Wrong count of newlines between a class and other code'
74 | assert.equal(error.message, msg)
75 | assert.equal(error.rule, 'newlines_after_classes')
76 | assert.equal(error.lineNumber, 10)
77 | assert.equal(error.context, "Expected 4 got 3")
78 |
79 | "has too many newlines after class" : (source) ->
80 | config =
81 | newlines_after_classes:
82 | level: 'error'
83 | value: 2
84 | indentation:
85 | level: 'ignore'
86 | value: 4
87 | errors = coffeelint.lint(source, config)
88 | assert.equal(errors.length, 1)
89 | error = errors[0]
90 | msg = 'Wrong count of newlines between a class and other code'
91 | assert.equal(error.message, msg)
92 | assert.equal(error.rule, 'newlines_after_classes')
93 | assert.equal(error.lineNumber, 10)
94 | assert.equal(error.context, "Expected 2 got 3")
95 |
96 | "works OK" : (source) ->
97 | config =
98 | newlines_after_classes:
99 | level: 'error'
100 | value: 3
101 | indentation:
102 | level: 'ignore'
103 | value: 4
104 | errors = coffeelint.lint(source, config)
105 | assert.equal(errors.length, 0)
106 |
107 | }).export(module)
108 |
--------------------------------------------------------------------------------
/generated_coffeelint.json:
--------------------------------------------------------------------------------
1 | {
2 | "coffeescript_error": {
3 | "level": "error"
4 | },
5 | "arrow_spacing": {
6 | "name": "arrow_spacing",
7 | "level": "ignore"
8 | },
9 | "no_tabs": {
10 | "name": "no_tabs",
11 | "level": "error"
12 | },
13 | "no_trailing_whitespace": {
14 | "name": "no_trailing_whitespace",
15 | "level": "error",
16 | "allowed_in_comments": false,
17 | "allowed_in_empty_lines": true
18 | },
19 | "max_line_length": {
20 | "name": "max_line_length",
21 | "value": 80,
22 | "level": "error",
23 | "limitComments": true
24 | },
25 | "line_endings": {
26 | "name": "line_endings",
27 | "level": "ignore",
28 | "value": "unix"
29 | },
30 | "no_trailing_semicolons": {
31 | "name": "no_trailing_semicolons",
32 | "level": "error"
33 | },
34 | "indentation": {
35 | "name": "indentation",
36 | "value": 2,
37 | "level": "error"
38 | },
39 | "camel_case_classes": {
40 | "name": "camel_case_classes",
41 | "level": "error"
42 | },
43 | "colon_assignment_spacing": {
44 | "name": "colon_assignment_spacing",
45 | "level": "ignore",
46 | "spacing": {
47 | "left": 0,
48 | "right": 0
49 | }
50 | },
51 | "no_implicit_braces": {
52 | "name": "no_implicit_braces",
53 | "level": "ignore",
54 | "strict": true
55 | },
56 | "no_plusplus": {
57 | "name": "no_plusplus",
58 | "level": "ignore"
59 | },
60 | "no_throwing_strings": {
61 | "name": "no_throwing_strings",
62 | "level": "error"
63 | },
64 | "no_backticks": {
65 | "name": "no_backticks",
66 | "level": "error"
67 | },
68 | "no_implicit_parens": {
69 | "name": "no_implicit_parens",
70 | "strict": true,
71 | "level": "ignore"
72 | },
73 | "no_empty_param_list": {
74 | "name": "no_empty_param_list",
75 | "level": "ignore"
76 | },
77 | "no_stand_alone_at": {
78 | "name": "no_stand_alone_at",
79 | "level": "ignore"
80 | },
81 | "space_operators": {
82 | "name": "space_operators",
83 | "level": "ignore"
84 | },
85 | "duplicate_key": {
86 | "name": "duplicate_key",
87 | "level": "error"
88 | },
89 | "empty_constructor_needs_parens": {
90 | "name": "empty_constructor_needs_parens",
91 | "level": "ignore"
92 | },
93 | "cyclomatic_complexity": {
94 | "name": "cyclomatic_complexity",
95 | "value": 10,
96 | "level": "ignore"
97 | },
98 | "newlines_after_classes": {
99 | "name": "newlines_after_classes",
100 | "value": 3,
101 | "level": "ignore"
102 | },
103 | "no_unnecessary_fat_arrows": {
104 | "name": "no_unnecessary_fat_arrows",
105 | "level": "warn"
106 | },
107 | "missing_fat_arrows": {
108 | "name": "missing_fat_arrows",
109 | "level": "ignore"
110 | },
111 | "non_empty_constructor_needs_parens": {
112 | "name": "non_empty_constructor_needs_parens",
113 | "level": "ignore"
114 | },
115 | "no_unnecessary_double_quotes": {
116 | "name": "no_unnecessary_double_quotes",
117 | "level": "ignore"
118 | },
119 | "no_debugger": {
120 | "name": "no_debugger",
121 | "level": "warn"
122 | },
123 | "no_interpolation_in_single_quotes": {
124 | "name": "no_interpolation_in_single_quotes",
125 | "level": "ignore"
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/test/test_spacing.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 | vows = require 'vows'
3 | assert = require 'assert'
4 | coffeelint = require path.join('..', 'lib', 'coffeelint')
5 |
6 | vows.describe('spacing').addBatch({
7 |
8 | 'No spaces around binary operators' :
9 |
10 | topic : ->
11 | '''
12 | x=1
13 | 1+1
14 | 1-1
15 | 1/1
16 | 1*1
17 | 1==1
18 | 1>=1
19 | 1>1
20 | 1<1
21 | 1<=1
22 | 1%1
23 | (a='b') -> a
24 | a|b
25 | a&b
26 | a*=-5
27 | a*=-b
28 | a*=5
29 | a*=a
30 | -a+=-2
31 | -a+=-a
32 | -a+=2
33 | -a+=a
34 | a*-b
35 | a**b
36 | a//b
37 | a%%b
38 | '''
39 |
40 | 'are permitted by default' : (source) ->
41 | errors = coffeelint.lint(source)
42 | assert.isEmpty(errors)
43 |
44 | 'can be forbidden' : (source) ->
45 | config = {space_operators : {level:'error'}}
46 | errors = coffeelint.lint(source, config)
47 | assert.lengthOf(errors, source.split("\n").length)
48 | error = errors[0]
49 | assert.equal(error.rule, 'space_operators')
50 | assert.equal(error.lineNumber, 1)
51 | assert.equal(error.message, "Operators must be spaced properly")
52 |
53 | 'Correctly spaced operators' :
54 |
55 | topic : ->
56 | '''
57 | x = 1
58 | 1 + 1
59 | 1 - 1
60 | 1 / 1
61 | 1 * 1
62 | 1 == 1
63 | 1 >= 1
64 | 1 > 1
65 | 1 < 1
66 | 1 <= 1
67 | (a = 'b') -> a
68 | +1
69 | -1
70 | y = -2
71 | x = -1
72 | y = x++
73 | x = y++
74 | 1 + (-1)
75 | -1 + 1
76 | x(-1)
77 | x(-1, 1, -1)
78 | x[..-1]
79 | x[-1..]
80 | x[-1...-1]
81 | 1 < -1
82 | a if -1
83 | a unless -1
84 | a if -1 and 1
85 | a if -1 or 1
86 | 1 and -1
87 | 1 or -1
88 | "#{a}#{b}"
89 | [+1, -1]
90 | [-1, +1]
91 | {a: -1}
92 | /// #{a} ///
93 | if -1 then -1 else -1
94 | a | b
95 | a & b
96 | a *= 5
97 | a *= -5
98 | a *= b
99 | a *= -b
100 | -a *= 5
101 | -a *= -5
102 | -a *= b
103 | -a *= -b
104 | a * -b
105 | a ** b
106 | a // b
107 | a %% b
108 | return -1
109 | for x in xs by -1 then x
110 |
111 | switch x
112 | when -1 then 42
113 | '''
114 |
115 | 'are permitted' : (source) ->
116 | config = {space_operators : {level:'error'}}
117 | errors = coffeelint.lint(source, config)
118 | assert.isEmpty(errors)
119 |
120 | 'Spaces around unary operators' :
121 |
122 | topic : ->
123 | '''
124 | + 1
125 | - - 1
126 | '''
127 |
128 | 'are permitted by default' : (source) ->
129 | errors = coffeelint.lint(source)
130 | assert.isEmpty(errors)
131 |
132 | 'can be forbidden' : (source) ->
133 | config = {space_operators : {level:'error'}}
134 | errors = coffeelint.lint(source, config)
135 | assert.lengthOf(errors, 2)
136 | error = errors[0]
137 |
138 | }).export(module)
139 |
140 |
--------------------------------------------------------------------------------
/test/test_colon_assignment_spacing.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 | vows = require 'vows'
3 | assert = require 'assert'
4 | coffeelint = require path.join('..', 'lib', 'coffeelint')
5 |
6 | vows.describe('colon_assignment_spacing').addBatch({
7 |
8 | 'Equal spacing around assignment' :
9 |
10 | topic : ->
11 | '''
12 | object = {spacing : true}
13 | class Dog
14 | barks : true
15 | stringyObject =
16 | 'stringkey' : 'ok'
17 | '''
18 |
19 | 'will not return an error' : (source) ->
20 | config =
21 | 'colon_assignment_spacing' :
22 | level : 'error'
23 | spacing :
24 | left : 1
25 | right : 1
26 | errors = coffeelint.lint(source, config)
27 | assert.isEmpty(errors)
28 |
29 | 'No space before assignment' :
30 |
31 | topic : ->
32 | '''
33 | object = {spacing: true}
34 | object =
35 | spacing: true
36 | class Dog
37 | barks: true
38 | stringyObject =
39 | 'stringkey': 'ok'
40 | '''
41 |
42 | 'will not return an error' : (source) ->
43 | config =
44 | 'colon_assignment_spacing' :
45 | level : 'error'
46 | spacing :
47 | left : 0
48 | right : 1
49 | errors = coffeelint.lint(source, config)
50 | assert.isEmpty(errors)
51 |
52 | 'Newline to the right of assignment' :
53 |
54 | topic : ->
55 | """
56 | query:
57 | method: 'GET'
58 | isArray: false
59 | """
60 |
61 | 'will not return an error' : (source) ->
62 | config =
63 | 'colon_assignment_spacing' :
64 | level : 'error'
65 | spacing :
66 | left : 0
67 | right : 1
68 | errors = coffeelint.lint(source, config)
69 | assert.isEmpty(errors)
70 |
71 | 'Improper spacing around assignment' :
72 |
73 | topic : ->
74 | '''
75 | object = {spacing: false}
76 | class Cat
77 | barks: false
78 | stringyObject =
79 | 'stringkey': 'notcool'
80 | '''
81 |
82 | 'will return an error' : (source) ->
83 | config =
84 | 'colon_assignment_spacing' :
85 | level : 'error'
86 | spacing :
87 | left : 1
88 | right : 1
89 | errors = coffeelint.lint(source, config)
90 | assert.equal(errors.length, 3)
91 |
92 | 'will ignore an error' : (source) ->
93 | config =
94 | 'colon_assignment_spacing' :
95 | level : 'ignore'
96 | spacing :
97 | left : 1
98 | right : 1
99 | errors = coffeelint.lint(source, config)
100 | assert.isEmpty(errors)
101 |
102 | 'Should not complain about strings' :
103 |
104 | topic : ->
105 | '''
106 | foo = (stuff) ->
107 | throw new Error("Error: stuff required") unless stuff?
108 | # do real work
109 | '''
110 |
111 | 'will return an error' : (source) ->
112 | config =
113 | 'colon_assignment_spacing' :
114 | level : 'error'
115 | spacing :
116 | left : 1
117 | right : 1
118 | errors = coffeelint.lint(source, config)
119 | assert.isEmpty(errors)
120 |
121 | }).export(module)
--------------------------------------------------------------------------------
/test/test_semicolons.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 | vows = require 'vows'
3 | assert = require 'assert'
4 | coffeelint = require path.join('..', 'lib', 'coffeelint')
5 |
6 | configError = {no_trailing_semicolons : {level : 'error'}}
7 | configIgnore = {no_trailing_semicolons : {level : 'ignore'}}
8 |
9 | vows.describe('semicolons').addBatch({
10 |
11 | 'Semicolons at end of lines' :
12 |
13 | topic : () ->
14 | """
15 | x = 1234;
16 | y = 1234; z = 1234
17 | """
18 |
19 | 'are forbidden' : (source) ->
20 | errors = coffeelint.lint(source)
21 | assert.lengthOf(errors, 1)
22 | error = errors[0]
23 | assert.equal(error.lineNumber, 1)
24 | assert.equal(error.message, "Line contains a trailing semicolon")
25 | assert.equal(error.rule, 'no_trailing_semicolons')
26 |
27 | 'can be ignored' : (source) ->
28 | errors = coffeelint.lint(source, configIgnore)
29 | assert.isEmpty(errors)
30 |
31 | 'Semicolons in multiline expressions' :
32 |
33 | topic : '''
34 | x = "asdf;
35 | asdf"
36 |
37 | y = """
38 | #{asdf1};
39 | _#{asdf2}_;
40 | asdf;
41 | """
42 |
43 | z = ///
44 | a*\;
45 | ///
46 | '''
47 |
48 | 'are ignored' : (source) ->
49 | errors = coffeelint.lint(source)
50 | assert.isEmpty(errors)
51 |
52 | 'Trailing semicolon in comments' :
53 | topic : "undefined\n# comment;\nundefined"
54 |
55 | 'are ignored' : (source) ->
56 | errors = coffeelint.lint(source, {})
57 | assert.isEmpty(errors)
58 |
59 | 'Trailing semicolon in comments with no semicolon in statement':
60 |
61 | topic : "x = 3 #set x to 3;"
62 |
63 | 'are ignored' : (source) ->
64 | errors = coffeelint.lint(source, configIgnore)
65 | assert.isEmpty(errors)
66 |
67 | 'will throw an error' : (source) ->
68 | errors = coffeelint.lint(source, configError)
69 | assert.isEmpty(errors)
70 |
71 | 'Trailing semicolon in comments with semicolon in statement':
72 |
73 | topic : "x = 3; #set x to 3;"
74 |
75 | 'are ignored' : (source) ->
76 | errors = coffeelint.lint(source, configIgnore)
77 | assert.isEmpty(errors)
78 |
79 | 'will throw an error' : (source) ->
80 | errors = coffeelint.lint(source, configError)
81 | assert.lengthOf(errors, 1)
82 | error = errors[0]
83 | assert.equal(error.lineNumber, 1)
84 | assert.equal(error.message, "Line contains a trailing semicolon")
85 | assert.equal(error.rule, 'no_trailing_semicolons')
86 |
87 | 'Trailing semicolon in block comments' :
88 |
89 | topic : "###\nThis is a block comment;\n###"
90 |
91 | 'are ignored' : (source) ->
92 | errors = coffeelint.lint(source, configIgnore)
93 | assert.isEmpty(errors)
94 |
95 | 'are ignored even if config level is error' : (source) ->
96 | errors = coffeelint.lint(source, configError)
97 | assert.isEmpty(errors)
98 |
99 | 'Semicolons with windows line endings' :
100 |
101 | topic : () ->
102 | "x = 1234;\r\n"
103 |
104 | 'works as expected' : (source) ->
105 | config = {
106 | line_endings : {value : 'windows'}
107 | }
108 | errors = coffeelint.lint(source, config)
109 | assert.lengthOf(errors, 1)
110 | error = errors[0]
111 | assert.equal(error.lineNumber, 1)
112 | assert.equal(error.message, "Line contains a trailing semicolon")
113 | assert.equal(error.rule, 'no_trailing_semicolons')
114 |
115 | }).export(module)
116 |
--------------------------------------------------------------------------------
/test/test_trailing.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 | vows = require 'vows'
3 | assert = require 'assert'
4 | coffeelint = require path.join('..', 'lib', 'coffeelint')
5 |
6 | vows.describe('trailing').addBatch({
7 |
8 | 'Trailing whitespace' :
9 |
10 | topic : () ->
11 | "x = 1234 \ny = 1"
12 |
13 | 'is forbidden by default' : (source) ->
14 | errors = coffeelint.lint(source)
15 | assert.equal(errors.length, 1)
16 | error = errors[0]
17 | assert.isObject(error)
18 | assert.equal(error.lineNumber, 1)
19 | assert.equal(error.message, "Line ends with trailing whitespace")
20 | assert.equal(error.rule, 'no_trailing_whitespace')
21 |
22 | 'can be permitted' : (source) ->
23 | config = {no_trailing_whitespace: {level: 'ignore'}}
24 | errors = coffeelint.lint(source, config)
25 | assert.equal(errors.length, 0)
26 |
27 | 'Trailing whitespace in comments' :
28 | topic : "x = 1234 # markdown comment \ny=1"
29 |
30 | 'is forbidden by default' : (source) ->
31 | errors = coffeelint.lint(source)
32 | assert.equal(errors.length, 1)
33 | error = errors[0]
34 | assert.isObject(error)
35 | assert.equal(error.lineNumber, 1)
36 | assert.equal(error.message, "Line ends with trailing whitespace")
37 | assert.equal(error.rule, 'no_trailing_whitespace')
38 |
39 | "can be permitted" : (source) ->
40 | config = {no_trailing_whitespace: {allowed_in_comments: true}}
41 | errors = coffeelint.lint(source, config)
42 | assert.equal(errors.length, 0)
43 |
44 | "a # in a string":
45 | topic: "x = 'some # string' "
46 | "does not confuse trailing_whitespace" : (source) ->
47 | config = {no_trailing_whitespace: {allowed_in_comments: true}}
48 | errors = coffeelint.lint(source, config)
49 | assert.isNotEmpty(errors)
50 |
51 | "Trailing whitespace in block comments" :
52 | topic : "###\nblock comment with trailing space: \n###"
53 |
54 | 'is forbidden by default' : (source) ->
55 | errors = coffeelint.lint(source)
56 | assert.equal(errors.length, 1)
57 | error = errors[0]
58 | assert.isObject(error)
59 | assert.equal(error.lineNumber, 2)
60 | assert.equal(error.message, "Line ends with trailing whitespace")
61 | assert.equal(error.rule, 'no_trailing_whitespace')
62 |
63 | "can be permitted" : (source) ->
64 | config = {no_trailing_whitespace: {allowed_in_comments: true}}
65 | errors = coffeelint.lint(source, config)
66 | assert.equal(errors.length, 0)
67 |
68 | "On empty lines": # https://github.com/clutchski/coffeelint/issues/39
69 | topic: "x = 1234\n \n"
70 |
71 | 'allowed by default': (source) ->
72 | errors = coffeelint.lint(source)
73 | assert.equal(errors.length, 0)
74 |
75 | 'can be forbidden': (source) ->
76 | config = {no_trailing_whitespace: {allowed_in_empty_lines: false}}
77 |
78 | errors = coffeelint.lint(source, config)
79 | assert.equal(errors.length, 1)
80 | error = errors[0]
81 | assert.isObject(error)
82 | assert.equal(error.lineNumber, 2)
83 | assert.equal(error.message, "Line ends with trailing whitespace")
84 | assert.equal(error.rule, 'no_trailing_whitespace')
85 |
86 | 'Trailing tabs' :
87 |
88 | topic : () ->
89 | "x = 1234\t"
90 |
91 | 'are forbidden as well' : (source) ->
92 | errors = coffeelint.lint(source, {})
93 | assert.equal(errors.length, 1)
94 |
95 | 'Windows line endings' :
96 |
97 | topic : 'x = 1234\r\ny = 5678'
98 |
99 | 'are permitted' : (source) ->
100 | assert.isEmpty(coffeelint.lint(source))
101 |
102 | }).export(module)
103 |
104 |
--------------------------------------------------------------------------------
/test/test_missing_fat_arrows.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 | vows = require 'vows'
3 | assert = require 'assert'
4 | {inspect} = require 'util'
5 | coffeelint = require path.join('..', 'lib', 'coffeelint')
6 |
7 | RULE = 'missing_fat_arrows'
8 |
9 | runLint = (source) ->
10 | config = {}
11 | config[name] = level: 'ignore' for name, rule of coffeelint.RULES
12 | config[RULE].level = 'error'
13 | coffeelint.lint source, config
14 |
15 | shouldError = (source, numErrors = 1) ->
16 | topic: source
17 | 'errors for missing arrow': (source) ->
18 | errors = runLint source
19 | assert.lengthOf errors, numErrors,
20 | "Expected #{numErrors} errors, got #{inspect errors}"
21 | error = errors[0]
22 | assert.equal error.rule, RULE
23 |
24 | shouldPass = (source) ->
25 | topic: source
26 | 'does not error for no missing arrows': (source) ->
27 | errors = runLint source
28 | assert.isEmpty errors, "Expected no errors, got #{inspect errors}"
29 |
30 | vows.describe(RULE).addBatch({
31 |
32 | 'empty function' : shouldPass '->'
33 | 'function without this' : shouldPass '-> 1'
34 | 'function with this' : shouldError '-> this'
35 | 'function with this.a' : shouldError '-> this.a'
36 | 'function with @' : shouldError '-> @'
37 | 'function with @a' : shouldError '-> @a'
38 |
39 | 'nested functions with this inside':
40 | 'with inner fat arrow': shouldPass '-> => this'
41 | 'with outer fat arrow': shouldError '=> -> this'
42 | 'with both fat arrows': shouldPass '=> => this'
43 |
44 | 'nested functions with this outside':
45 | 'with inner fat arrow': shouldError '-> (this; =>)'
46 | 'with outer fat arrow': shouldPass '=> (this; ->)'
47 | 'with both fat arrows': shouldPass '=> (this; =>)'
48 |
49 | 'deeply nested functions':
50 | 'with thin arrow' : shouldError '-> -> -> -> -> this'
51 | 'with fat arrow' : shouldPass '-> -> -> -> => this'
52 | 'with wrong fat arrow': shouldError '-> -> => -> -> this'
53 |
54 | 'functions with multiple statements' : shouldError """
55 | f = ->
56 | this.x = 2
57 | z ((a) -> a; this.x)
58 | """, 2
59 |
60 | 'functions with parameters' : shouldPass '(a) ->'
61 | 'functions with parameter assignment' : shouldError '(@a) ->'
62 | 'functions with destructuring parameter assignment': shouldError '({@a}) ->'
63 |
64 | 'class instance method':
65 | 'without this': shouldPass """
66 | class A
67 | @m: -> 1
68 | """
69 | 'with this': shouldPass """
70 | class A
71 | @m: -> this
72 | """
73 |
74 | 'class method':
75 | 'without this': shouldPass """
76 | class A
77 | m: -> 1
78 | """
79 | 'with this': shouldPass """
80 | class A
81 | m: -> this
82 | """
83 |
84 | 'function in class body':
85 | 'without this': shouldPass """
86 | class A
87 | f = -> 1
88 | x: 2
89 | """
90 | 'with this': shouldError """
91 | class A
92 | f = -> this
93 | x: 2
94 | """
95 |
96 | 'function inside class instance method':
97 | 'without this': shouldPass """
98 | class A
99 | m: -> -> 1
100 | """
101 | 'with this': shouldError """
102 | class A
103 | m: -> -> @a
104 | """
105 |
106 | 'mixture of class methods and function in class body':
107 | 'with this': shouldPass """
108 | class A
109 | f = => this
110 | m: -> this
111 | @n: -> this
112 | o: -> this
113 | @p: -> this
114 | """
115 |
116 | 'https://github.com/clutchski/coffeelint/issues/215':
117 | 'method with block comment': shouldPass """
118 | class SimpleClass
119 |
120 | ###
121 | A block comment
122 | ###
123 | doNothing: () ->
124 | """
125 |
126 | }).export(module)
127 |
--------------------------------------------------------------------------------
/test/test_no_unnecessary_double_quotes.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 | vows = require 'vows'
3 | assert = require 'assert'
4 | coffeelint = require path.join('..', 'lib', 'coffeelint')
5 |
6 | vows.describe('no_unnecessary_double_quotes').addBatch({
7 |
8 | 'Single quotes' :
9 |
10 | topic : () ->
11 | '''
12 | foo = 'single'
13 | '''
14 |
15 | 'single quotes should always be allowed' : (source) ->
16 | config = {no_unnecessary_double_quotes : {level:'error'}}
17 | errors = coffeelint.lint(source, config)
18 | assert.isArray(errors)
19 | assert.isEmpty(errors)
20 |
21 |
22 | 'Unnecessary double quotes' :
23 |
24 | topic : () ->
25 | '''
26 | foo = "double"
27 | '''
28 |
29 | 'double quotes are allowed by default' : (source) ->
30 | errors = coffeelint.lint(source)
31 | assert.isArray(errors)
32 | assert.isEmpty(errors)
33 |
34 | 'double quotes can be forbidden' : (source) ->
35 | config = {no_unnecessary_double_quotes : {level:'error'}}
36 | errors = coffeelint.lint(source, config)
37 | assert.isArray(errors)
38 | assert.lengthOf(errors, 1)
39 | error = errors[0]
40 | assert.equal(error.lineNumber, 1)
41 | assert.equal(error.message,
42 | 'Unnecessary double quotes are forbidden'
43 | )
44 | assert.equal(error.rule, 'no_unnecessary_double_quotes')
45 |
46 |
47 | 'Useful double quotes' :
48 |
49 | topic : () ->
50 | '''
51 | interpolation = "inter#{polation}"
52 | multipleInterpolation = "#{foo}bar#{baz}"
53 | singleQuote = "single'quote"
54 | '''
55 |
56 | 'string interpolation should always be allowed' : (source) ->
57 | config = {no_unnecessary_double_quotes : {level:'error'}}
58 | errors = coffeelint.lint(source, config)
59 | assert.isArray(errors)
60 | assert.isEmpty(errors)
61 |
62 |
63 | 'Block strings with double quotes' :
64 |
65 | topic : () ->
66 | '''
67 | foo = """
68 | doubleblock
69 | """
70 | '''
71 |
72 | 'block strings with double quotes are not allowed' : (source) ->
73 | config = {no_unnecessary_double_quotes : {level:'error'}}
74 | errors = coffeelint.lint(source, config)
75 | assert.lengthOf(errors, 1)
76 | error = errors[0]
77 | assert.equal(error.lineNumber, 1)
78 | assert.equal(error.message,
79 | 'Unnecessary double quotes are forbidden'
80 | )
81 | assert.equal(error.rule, 'no_unnecessary_double_quotes')
82 |
83 |
84 | 'Block strings with useful double quotes' :
85 |
86 | topic : () ->
87 | '''
88 | foo = """
89 | #{interpolation}foo 'some single quotes for good measure'
90 | """
91 | '''
92 |
93 | 'block strings with useful content should be allowed' : (source) ->
94 | config = {no_unnecessary_double_quotes : {level:'error'}}
95 | errors = coffeelint.lint(source, config)
96 | assert.isArray(errors)
97 | assert.isEmpty(errors)
98 |
99 |
100 | 'Block strings with single quotes' :
101 |
102 | topic : () ->
103 | """
104 | foo = '''
105 | singleblock
106 | '''
107 | """
108 |
109 | 'block strings with single quotes should be allowed' : (source) ->
110 | config = {no_unnecessary_double_quotes : {level:'error'}}
111 | errors = coffeelint.lint(source, config)
112 | assert.isArray(errors)
113 | assert.isEmpty(errors)
114 |
115 |
116 | 'Hand concatenated string with parenthesis' :
117 |
118 | topic : () ->
119 | '''
120 | foo = (("inter") + "polation")
121 | '''
122 |
123 | 'double quotes should not be allowed' : (source) ->
124 | config = {no_unnecessary_double_quotes : {level:'error'}}
125 | errors = coffeelint.lint(source, config)
126 | assert.lengthOf(errors, 2)
127 | error = errors[0]
128 | assert.equal(error.lineNumber, 1)
129 | assert.equal(error.message,
130 | 'Unnecessary double quotes are forbidden'
131 | )
132 | assert.equal(error.rule, 'no_unnecessary_double_quotes')
133 |
134 |
135 | }).export(module)
136 |
--------------------------------------------------------------------------------
/src/line_linter.coffee:
--------------------------------------------------------------------------------
1 |
2 | class LineApi
3 | constructor: (source, @config, @tokensByLine, @literate) ->
4 | @line = null
5 | @lines = source.split('\n')
6 | @lineCount = @lines.length
7 |
8 | # maintains some contextual information
9 | # inClass: bool; in class or not
10 | # lastUnemptyLineInClass: null or lineNumber, if the last not-empty
11 | # line was in a class it holds its number
12 | # classIndents: the number of indents within a class
13 | @context = {
14 | class: {
15 | inClass : false
16 | lastUnemptyLineInClass : null
17 | classIndents : null
18 | }
19 | }
20 |
21 | lineNumber: 0
22 |
23 | isLiterate: -> @literate
24 |
25 | # maintain the contextual information for class-related stuff
26 | maintainClassContext : (line) ->
27 | if @context.class.inClass
28 | if @lineHasToken 'INDENT'
29 | @context.class.classIndents++
30 | else if @lineHasToken 'OUTDENT'
31 | @context.class.classIndents--
32 | if @context.class.classIndents is 0
33 | @context.class.inClass = false
34 | @context.class.classIndents = null
35 |
36 | if @context.class.inClass and not line.match( /^\s*$/ )
37 | @context.class.lastUnemptyLineInClass = @lineNumber
38 | else
39 | unless line.match(/\\s*/)
40 | @context.class.lastUnemptyLineInClass = null
41 |
42 | if @lineHasToken 'CLASS'
43 | @context.class.inClass = true
44 | @context.class.lastUnemptyLineInClass = @lineNumber
45 | @context.class.classIndents = 0
46 |
47 | null
48 |
49 | isLastLine : () ->
50 | return @lineNumber == @lineCount - 1
51 |
52 | # Return true if the given line actually has tokens.
53 | # Optional parameter to check for a specific token type and line number.
54 | lineHasToken : (tokenType = null, lineNumber = null) ->
55 | lineNumber = lineNumber ? @lineNumber
56 | unless tokenType?
57 | return @tokensByLine[lineNumber]?
58 | else
59 | tokens = @tokensByLine[lineNumber]
60 | return null unless tokens?
61 | for token in tokens
62 | return true if token[0] == tokenType
63 | return false
64 |
65 | # Return tokens for the given line number.
66 | getLineTokens : () ->
67 | @tokensByLine[@lineNumber] || []
68 |
69 |
70 | BaseLinter = require './base_linter.coffee'
71 |
72 | # Some repeatedly used regular expressions.
73 | configStatement = /coffeelint:\s*(disable|enable)(?:=([\w\s,]*))?/
74 |
75 | #
76 | # A class that performs regex checks on each line of the source.
77 | #
78 | module.exports = class LineLinter extends BaseLinter
79 |
80 | # This is exposed here so coffeelint.coffee can reuse it
81 | @configStatement: configStatement
82 |
83 | constructor : (source, config, rules, tokensByLine, literate = false) ->
84 | super source, config, rules
85 |
86 | @lineApi = new LineApi source, config, tokensByLine, literate
87 |
88 | # Store suppressions in the form of { line #: type }
89 | @block_config =
90 | enable : {}
91 | disable : {}
92 |
93 | acceptRule: (rule) ->
94 | return typeof rule.lintLine is 'function'
95 |
96 | lint : () ->
97 | errors = []
98 | for line, lineNumber in @lineApi.lines
99 | @lineApi.lineNumber = @lineNumber = lineNumber
100 |
101 | @lineApi.maintainClassContext line
102 | @collectInlineConfig line
103 |
104 | errors.push(error) for error in @lintLine(line)
105 | errors
106 |
107 | # Return an error if the line contained failed a rule, null otherwise.
108 | lintLine : (line) ->
109 |
110 | # Multiple rules might run against the same line to build context.
111 | # Every every rule should run even if something has already produced an
112 | # error for the same token.
113 | errors = []
114 | for rule in @rules
115 | v = @normalizeResult rule, rule.lintLine(line, @lineApi)
116 | errors.push v if v?
117 | errors
118 |
119 | collectInlineConfig : (line) ->
120 | # Check for block config statements enable and disable
121 | result = configStatement.exec(line)
122 | if result?
123 | cmd = result[1]
124 | rules = []
125 | if result[2]?
126 | for r in result[2].split(',')
127 | rules.push r.replace(/^\s+|\s+$/g, "")
128 | @block_config[cmd][@lineNumber] = rules
129 | return null
130 |
131 |
132 | createError: (rule, attrs = {}) ->
133 | attrs.lineNumber = @lineNumber + 1 # Lines are indexed by zero.
134 | attrs.level = @config[rule]?.level
135 | super rule, attrs
136 |
137 |
--------------------------------------------------------------------------------
/src/rules/indentation.coffee:
--------------------------------------------------------------------------------
1 |
2 | module.exports = class Indentation
3 |
4 | rule:
5 | name: 'indentation'
6 | value : 2
7 | level : 'error'
8 | message : 'Line contains inconsistent indentation'
9 | description: """
10 | This rule imposes a standard number of spaces to be used for
11 | indentation. Since whitespace is significant in CoffeeScript, it's
12 | critical that a project chooses a standard indentation format and
13 | stays consistent. Other roads lead to darkness. #
14 | Enabling this option will prevent this ugly
15 | # but otherwise valid CoffeeScript.
16 | twoSpaces = () ->
17 | fourSpaces = () ->
18 | eightSpaces = () ->
19 | 'this is valid CoffeeScript'
20 |
21 |
22 |
23 | Two space indentation is enabled by default.
24 | """
25 |
26 |
27 | tokens: [ 'INDENT', "[", "]" ]
28 |
29 | constructor: ->
30 | @arrayTokens = [] # A stack tracking the array token pairs.
31 |
32 | # Return an error if the given indentation token is not correct.
33 | lintToken: (token, tokenApi) ->
34 | [type, numIndents, lineNumber] = token
35 |
36 | if type in [ "[", "]" ]
37 | @lintArray(token)
38 | return undefined
39 |
40 | return null if token.generated?
41 |
42 | # HACK: CoffeeScript's lexer insert indentation in string
43 | # interpolations that start with spaces e.g. "#{ 123 }"
44 | # so ignore such cases. Are there other times an indentation
45 | # could possibly follow a '+'?
46 | previous = tokenApi.peek(-2)
47 | isInterpIndent = previous and previous[0] == '+'
48 |
49 | # Ignore the indentation inside of an array, so that
50 | # we can allow things like:
51 | # x = ["foo",
52 | # "bar"]
53 | previous = tokenApi.peek(-1)
54 | isArrayIndent = @inArray() and previous?.newLine
55 |
56 | # Ignore indents used to for formatting on multi-line expressions, so
57 | # we can allow things like:
58 | # a = b =
59 | # c = d
60 | previousSymbol = tokenApi.peek(-1)?[0]
61 | isMultiline = previousSymbol in ['=', ',']
62 |
63 | # Summarize the indentation conditions we'd like to ignore
64 | ignoreIndent = isInterpIndent or isArrayIndent or isMultiline
65 |
66 | # Compensate for indentation in function invocations that span multiple
67 | # lines, which can be ignored.
68 | if @isChainedCall tokenApi
69 | { lines, lineNumber } = tokenApi
70 | currentLine = lines[lineNumber]
71 | prevNum = 1
72 |
73 | # keep going back until we are not at a comment or a blank line
74 | prevNum += 1 while (/^\s*(#|$)/.test(lines[lineNumber - prevNum]))
75 | previousLine = lines[lineNumber - prevNum]
76 |
77 | previousIndentation = previousLine.match(/^(\s*)/)[1].length
78 | # I don't know why, but when inside a function, you make a chained
79 | # call and define an inline callback as a parameter, the body of
80 | # that callback gets the indentation reported higher than it really
81 | # is. See issue #88
82 | # NOTE: Adding this line moved the cyclomatic complexity over the
83 | # limit, I'm not sure why
84 | numIndents = currentLine.match(/^(\s*)/)[1].length
85 | numIndents -= previousIndentation
86 |
87 |
88 | # Now check the indentation.
89 | expected = tokenApi.config[@rule.name].value
90 | if not ignoreIndent and numIndents != expected
91 | return {
92 | context: "Expected #{expected} got #{numIndents}"
93 | }
94 | # Return true if the current token is inside of an array.
95 | inArray : () ->
96 | return @arrayTokens.length > 0
97 |
98 | # Lint the given array token.
99 | lintArray : (token) ->
100 | # Track the array token pairs
101 | if token[0] == '['
102 | @arrayTokens.push(token)
103 | else if token[0] == ']'
104 | @arrayTokens.pop()
105 | # Return null, since we're not really linting
106 | # anything here.
107 | null
108 |
109 | # Return true if the current token is part of a property access
110 | # that is split across lines, for example:
111 | # $('body')
112 | # .addClass('foo')
113 | # .removeClass('bar')
114 | isChainedCall: (tokenApi) ->
115 | { tokens, i } = tokenApi
116 | # Get the index of the second most recent new line.
117 | lines = (i for token, i in tokens[..i] when token.newLine?)
118 |
119 | lastNewLineIndex = if lines then lines[lines.length - 2] else null
120 |
121 | # Bail out if there is no such token.
122 | return false if not lastNewLineIndex?
123 |
124 | # Otherwise, figure out if that token or the next is an attribute
125 | # look-up.
126 | tokens = [tokens[lastNewLineIndex], tokens[lastNewLineIndex + 1]]
127 |
128 | return !!(t for t in tokens when t and t[0] == '.').length
129 |
130 |
--------------------------------------------------------------------------------
/test/test_indent.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 | vows = require 'vows'
3 | assert = require 'assert'
4 | coffeelint = require path.join('..', 'lib', 'coffeelint')
5 |
6 |
7 | vows.describe('indent').addBatch({
8 |
9 | 'Indentation' :
10 |
11 | topic : () ->
12 | """
13 | x = () ->
14 | 'two spaces'
15 |
16 | a = () ->
17 | 'four spaces'
18 | """
19 |
20 | 'defaults to two spaces' : (source) ->
21 | errors = coffeelint.lint(source)
22 | assert.equal(errors.length, 1)
23 | error = errors[0]
24 | msg = 'Line contains inconsistent indentation'
25 | assert.equal(error.message, msg)
26 | assert.equal(error.rule, 'indentation')
27 | assert.equal(error.lineNumber, 5)
28 | assert.equal(error.context, "Expected 2 got 4")
29 |
30 | 'can be overridden' : (source) ->
31 | config =
32 | indentation:
33 | level: 'error'
34 | value: 4
35 | errors = coffeelint.lint(source, config)
36 | assert.equal(errors.length, 1)
37 | error = errors[0]
38 | assert.equal(error.lineNumber, 2)
39 |
40 | 'is optional' : (source) ->
41 | config =
42 | indentation:
43 | level: 'ignore'
44 | value: 4
45 | errors = coffeelint.lint(source, config)
46 | assert.equal(errors.length, 0)
47 |
48 | 'Nested indentation errors' :
49 |
50 | topic : () ->
51 | """
52 | x = () ->
53 | y = () ->
54 | 1234
55 | """
56 |
57 | 'are caught' : (source) ->
58 | errors = coffeelint.lint(source)
59 | assert.lengthOf(errors, 1)
60 | error = errors[0]
61 | assert.equal(error.lineNumber, 3)
62 |
63 | 'Compiler generated indentation' :
64 |
65 | topic : () ->
66 | """
67 | () ->
68 | if 1 then 2 else 3
69 | """
70 |
71 | 'is ignored when not using two spaces' : (source) ->
72 | config =
73 | indentation:
74 | value: 4
75 | errors = coffeelint.lint(source, config)
76 | assert.isEmpty(errors)
77 |
78 | 'Indentation inside interpolation' :
79 |
80 | topic : 'a = "#{ 1234 }"'
81 |
82 | 'is ignored' : (source) ->
83 | errors = coffeelint.lint(source)
84 | assert.isEmpty(errors)
85 |
86 | 'Indentation in multi-line expressions' :
87 |
88 | topic : """
89 | x = '1234' + '1234' + '1234' +
90 | '1234' + '1234'
91 | """
92 |
93 | 'is ignored' : (source) ->
94 | errors = coffeelint.lint(source)
95 | assert.isEmpty(errors)
96 |
97 | 'Indentation across line breaks' :
98 |
99 | topic : () ->
100 | """
101 | days = ["mon", "tues", "wed",
102 | "thurs", "fri"
103 | "sat", "sun"]
104 |
105 | x = myReallyLongFunctionName =
106 | 1234
107 |
108 | arr = [() ->
109 | 1234
110 | ]
111 | """
112 |
113 | 'is ignored' : (source) ->
114 | errors = coffeelint.lint(source)
115 | assert.isEmpty(errors)
116 |
117 | 'Indentation on seperate line invocation' :
118 |
119 | topic : """
120 | rockinRockin
121 | .around ->
122 | 3
123 |
124 | rockrockrock.
125 | around ->
126 | 1234
127 | """
128 |
129 | 'is ignored. Issue #4' : (source) ->
130 | errors = coffeelint.lint(source)
131 | assert.isEmpty(errors)
132 |
133 | 'Indented chained invocations' :
134 |
135 | topic : """
136 | $('body')
137 | .addClass('k')
138 | .removeClass 'k'
139 | .animate()
140 | .hide()
141 | """
142 |
143 | 'is permitted' : (source) ->
144 | assert.isEmpty(coffeelint.lint(source))
145 |
146 | 'Ignore comment in indented chained invocations' :
147 | topic : () ->
148 | """
149 | test()
150 | .r((s) ->
151 | # Ignore this comment
152 | # Ignore this one too
153 | # Ignore this one three
154 | ab()
155 | x()
156 | y()
157 | )
158 | .s()
159 | """
160 | 'no error when comment is in first line of a chain' : (source) ->
161 | config =
162 | indentation:
163 | value: 4
164 | errors = coffeelint.lint(source, config)
165 | assert.isEmpty(errors)
166 |
167 | 'Ignore blank line in indented chained invocations' :
168 | topic : () ->
169 | """
170 | test()
171 | .r((s) ->
172 |
173 |
174 | ab()
175 | x()
176 | y()
177 | )
178 | .s()
179 | """
180 | 'no error when blank line is in first line of a chain' : (source) ->
181 | config =
182 | indentation:
183 | value: 4
184 | errors = coffeelint.lint(source, config)
185 | assert.isEmpty(errors)
186 |
187 | 'Arbitrarily indented arguments' :
188 |
189 | topic : """
190 | myReallyLongFunction withLots,
191 | ofArguments,
192 | everywhere
193 | """
194 |
195 | 'are permitted' : (source) ->
196 | errors = coffeelint.lint(source)
197 | assert.isEmpty(errors)
198 |
199 | 'Indenting a callback in a chained call inside a function':
200 |
201 | topic: """
202 | someFunction = ->
203 | $.when(somePromise)
204 | .done (result) ->
205 | foo = result.bar
206 | """
207 | 'is permitted. See issue #88': (source) ->
208 | errors = coffeelint.lint(source)
209 | assert.isEmpty(errors)
210 |
211 | }).export(module)
212 |
--------------------------------------------------------------------------------
/test/test_cyclomatic_complexity.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 | vows = require 'vows'
3 | assert = require 'assert'
4 | coffeelint = require path.join('..', 'lib', 'coffeelint')
5 |
6 |
7 | # Return the cyclomatic complexity of a code snippet with one function.
8 | getComplexity = (source) ->
9 | config = {cyclomatic_complexity : {level: 'error', value: 0}}
10 | errors = coffeelint.lint(source, config)
11 | assert.isNotEmpty(errors)
12 | assert.lengthOf(errors, 1)
13 | error = errors[0]
14 | assert.equal(error.rule, 'cyclomatic_complexity')
15 | return error.context
16 |
17 |
18 | vows.describe('cyclomatic complexity').addBatch({
19 |
20 | 'Cyclomatic complexity' :
21 |
22 | topic : """
23 | x = ->
24 | 1 and 2 and 3 and
25 | 4 and 5 and 6 and
26 | 7 and 8 and 9 and
27 | 10 and 11
28 | """
29 |
30 | 'defaults to ignore' : (source) ->
31 | errors = coffeelint.lint(source)
32 | assert.isArray(errors)
33 | assert.isEmpty(errors)
34 |
35 | 'can be enabled' : (source) ->
36 | config = {cyclomatic_complexity : {level: 'error'}}
37 | errors = coffeelint.lint(source, config)
38 | assert.isArray(errors)
39 | assert.lengthOf(errors, 1)
40 | error = errors[0]
41 | assert.equal(error.rule, 'cyclomatic_complexity')
42 | assert.equal(error.context, 11)
43 | assert.equal(error.lineNumber, 1)
44 | assert.equal(error.lineNumberEnd, 5)
45 |
46 | 'can be enabled with configurable complexity' : (source) ->
47 | config = {cyclomatic_complexity : {level: 'error', value: 12}}
48 | errors = coffeelint.lint(source)
49 | assert.isArray(errors)
50 | assert.isEmpty(errors)
51 |
52 | 'An empty function' :
53 |
54 | topic : "x = () -> 1234"
55 |
56 | 'has a complexity of one' : (source) ->
57 | complexity = getComplexity(source)
58 | assert.equal(complexity, 1)
59 |
60 | 'If statement' :
61 |
62 | topic : "x = () -> 2 if $ == true"
63 |
64 | 'increments the complexity' : (source) ->
65 | complexity = getComplexity(source)
66 | assert.equal(complexity, 2)
67 |
68 |
69 | 'If Else statement' :
70 |
71 | topic : 'y = -> if $ then 1 else 3'
72 |
73 | 'increments the complexity' : (source) ->
74 | complexity = getComplexity(source)
75 | assert.equal(complexity, 2)
76 |
77 | 'If ElseIf statement' :
78 |
79 | topic : """
80 | x = ->
81 | if 1233
82 | 'abc'
83 | else if 456
84 | 'xyz'
85 | """
86 |
87 | 'has a complexity of three' : (source) ->
88 | complexity = getComplexity(source)
89 | assert.equal(complexity, 3)
90 |
91 | 'If If-Else Else statement' :
92 |
93 | topic : """
94 | z = () ->
95 | if x
96 | 1
97 | else if y
98 | 2
99 | else
100 | 3
101 | """
102 |
103 | 'has a complexity of three' : (source) ->
104 | complexity = getComplexity(source)
105 | assert.equal(complexity, 3)
106 |
107 | 'Nested if statements' :
108 |
109 | topic : """
110 | z = () ->
111 | if abc?
112 | if other?
113 | 123
114 | """
115 |
116 | 'has a complexity of three' : (source) ->
117 | complexity = getComplexity(source)
118 | assert.equal(complexity, 3)
119 |
120 |
121 | 'A while loop' :
122 |
123 | topic : """
124 | x = () ->
125 | while 1
126 | 'asdf'
127 | """
128 |
129 | 'increments complexity' : (source) ->
130 | complexity = getComplexity(source)
131 | assert.equal(complexity, 2)
132 |
133 | 'An until loop' :
134 |
135 | topic : "x = () -> log 'a' until $?"
136 |
137 | 'increments complexity' : (source) ->
138 | complexity = getComplexity(source)
139 | assert.equal(complexity, 2)
140 |
141 | 'A for loop' :
142 |
143 | topic : """
144 | x = () ->
145 | for i in window
146 | log i
147 | """
148 |
149 | 'increments complexity' : (source) ->
150 | complexity = getComplexity(source)
151 | assert.equal(complexity, 2)
152 |
153 | 'A list comprehension' :
154 |
155 | topic : "x = -> [a for a in window]"
156 |
157 | 'increments complexity' : (source) ->
158 | complexity = getComplexity(source)
159 | assert.equal(complexity, 2)
160 |
161 | 'Try / Catch blocks' :
162 |
163 | topic : """
164 | x = () ->
165 | try
166 | divide("byZero")
167 | catch error
168 | log("uh oh")
169 | """
170 |
171 | 'increments complexity' : (source) ->
172 | assert.equal(getComplexity(source), 2)
173 |
174 | 'Try / Catch / Finally blocks' :
175 |
176 | topic : """
177 | x = () ->
178 | try
179 | divide("byZero")
180 | catch error
181 | log("uh oh")
182 | finally
183 | clean()
184 | """
185 |
186 | 'increments complexity' : (source) ->
187 | assert.equal(getComplexity(source), 2)
188 |
189 | 'Switch statements without an else' :
190 |
191 | topic : '''
192 | x = () ->
193 | switch a
194 | when "b" then "b"
195 | when "c" then "c"
196 | when "d" then "d"
197 | '''
198 |
199 | 'increase complexity by the number of cases' : (source) ->
200 | complexity = getComplexity(source)
201 | assert.equal(complexity, 4)
202 |
203 | 'Switch statements with an else' :
204 |
205 | topic : '''
206 | x = () ->
207 | switch a
208 | when "b" then "b"
209 | when "c" then "c"
210 | when "d" then "d"
211 | else "e"
212 | '''
213 |
214 | 'increase complexity by the number of cases' : (source) ->
215 | complexity = getComplexity(source)
216 | assert.equal(complexity, 4)
217 |
218 | 'And operators' :
219 |
220 | topic : 'x = () -> $ and window'
221 |
222 | 'increments the complexity' : (source) ->
223 | complexity = getComplexity(source)
224 | assert.equal(complexity, 2)
225 |
226 | 'Or operators' :
227 |
228 | topic : 'x = () -> $ or window'
229 |
230 | 'increments the complexity' : (source) ->
231 | complexity = getComplexity(source)
232 | assert.equal(complexity, 2)
233 |
234 | 'A complicated example' :
235 |
236 | topic : """
237 | x = () ->
238 | if a and b and c or d and c or e
239 | if x or d or e of f
240 | 1
241 | else if window
242 | while 1 and 3
243 | 2
244 | while false
245 | y
246 | return false
247 | """
248 |
249 | 'works' : (source) ->
250 | config = {cyclomatic_complexity : {level: 'error'}}
251 | errors = coffeelint.lint(source, config)
252 | assert.isArray(errors)
253 | assert.lengthOf(errors, 1)
254 | error = errors[0]
255 | assert.equal(error.rule, 'cyclomatic_complexity')
256 | assert.equal(error.lineNumber, 1)
257 | assert.equal(error.lineNumberEnd, 10)
258 | assert.equal(error.context, 14)
259 |
260 | }).export(module)
261 |
--------------------------------------------------------------------------------
/test/test_arrows.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 | vows = require 'vows'
3 | assert = require 'assert'
4 | coffeelint = require path.join('..', 'lib', 'coffeelint')
5 |
6 | vows.describe('arrows').addBatch({
7 |
8 | 'No spacing around the arrow operator' :
9 |
10 | topic : ->
11 | '''
12 | test1 = (foo, bar)->console.log("foobar")
13 | test2 = (foo, bar) ->console.log("foo->bar")
14 | test3 = (foo, bar)-> console.log("foo->bar")
15 | test4 = (foo, bar)->
16 | console.log("foo->bar")
17 | test5 = (foo, bar) ->
18 | console.log("foo->bar")
19 | '''
20 |
21 | 'will return an error' : (source) ->
22 | config = {
23 | "indentation" : { "value": 2, "level": "error" }
24 | "arrow_spacing": { "level": "error"}
25 | }
26 | errors = coffeelint.lint(source, config)
27 | assert.equal(errors.length, 4)
28 | assert.equal(errors[0].lineNumber, 1)
29 | assert.equal(errors[1].lineNumber, 2)
30 | assert.equal(errors[2].lineNumber, 3)
31 | assert.equal(errors[3].lineNumber, 4)
32 |
33 | 'will be ignored (no error)' : (source) ->
34 | config = { "arrow_spacing": { "level": "ignore" } }
35 | errors = coffeelint.lint(source, config)
36 | assert.isEmpty(errors)
37 |
38 | 'Handles good spacing when parentheses are generated' :
39 |
40 | topic : ->
41 | '''
42 | testingThis
43 | .around ->
44 | -> 4
45 |
46 | testingThis
47 | .around ->
48 | 4
49 |
50 | testingThis
51 | .around -> 4
52 |
53 | testingThis =
54 | -> 5
55 |
56 | testingThis.around (a, b) ->
57 | -> "4"
58 |
59 | '''
60 |
61 | 'when spacing is not required around arrow operator' : (source) ->
62 | config = {
63 | "indentation": { "value": 2, "level": "error" },
64 | "arrow_spacing": { "level": "ignore" }
65 | }
66 | errors = coffeelint.lint(source, config)
67 | assert.isEmpty(errors)
68 |
69 | 'when spacing is required around arrow operator' : (source) ->
70 | config = {
71 | "indentation": { "value": 2, "level": "error" },
72 | "arrow_spacing": { "level": "error" }
73 | }
74 | errors = coffeelint.lint(source, config)
75 | assert.isEmpty(errors)
76 |
77 | 'Handles bad spacing when parentheses are generated' :
78 |
79 | topic : ->
80 | '''
81 | testingThis
82 | .around ->
83 | ->4
84 |
85 | testingThis
86 | .around ->4
87 |
88 | testingThis =
89 | ->5
90 |
91 | testingThis.around (a, b) ->
92 | ->"4"
93 |
94 | testingThis ->-> "X"
95 |
96 | testingThis ->->"X"
97 | '''
98 |
99 | 'when spacing is required around arrow operator' : (source) ->
100 | config = { "arrow_spacing": { "level": "error" } }
101 | errors = coffeelint.lint(source, config)
102 | assert.equal(errors[0].lineNumber, 3)
103 | assert.equal(errors[1].lineNumber, 6)
104 | assert.equal(errors[2].lineNumber, 9)
105 | assert.equal(errors[3].lineNumber, 12)
106 | assert.equal(errors[4].lineNumber, 14)
107 | assert.equal(errors[5].lineNumber, 16)
108 | assert.equal(errors[6].lineNumber, 16)
109 | assert.equal(errors.length, 7)
110 |
111 | 'when spacing is not required around arrow operator' : (source) ->
112 | config = { "arrow_spacing": { "level": "ignore" } }
113 | errors = coffeelint.lint(source, config)
114 | assert.isEmpty(errors)
115 |
116 | 'Ignore spacing for non-generated parentheses' :
117 | # if the function has no parameters (and thus no parentheses),
118 | # it will accept a lack of spacing preceding the arrow (first example)
119 | topic : ->
120 | '''
121 | x(-> 3)
122 | x( -> 3)
123 | x((a,b) -> c)
124 | (-> true)()
125 | '''
126 | 'when spacing is required around arrow operator' : (source) ->
127 | config = { "arrow_spacing": { "level": "error" } }
128 | errors = coffeelint.lint(source, config)
129 | assert.equal(errors.length, 0)
130 |
131 | 'when spacing is not required around arrow operator' : (source) ->
132 | config = { "arrow_spacing": { "level": "ignore" } }
133 | errors = coffeelint.lint(source, config)
134 | assert.isEmpty(errors, 0)
135 |
136 | 'Handle an arrow at beginning of statement' :
137 | topic : ->
138 | '''
139 | @waitForSelector ".application",
140 | -> @test.pass "homepage loaded ok"
141 | -> @test.fail "homepage didn't load"
142 | 2000
143 | '''
144 |
145 | 'when spacing is required around arrow operator' : (source) ->
146 | config = { "arrow_spacing": { "level": "error" } }
147 | errors = coffeelint.lint(source, config)
148 | assert.equal(errors.length, 0)
149 |
150 | 'when spacing is not required around arrow operator' : (source) ->
151 | config = { "arrow_spacing": { "level": "ignore" } }
152 | errors = coffeelint.lint(source, config)
153 | assert.isEmpty(errors, 0)
154 |
155 | 'Handle a nested arrow at end of file' :
156 | topic : ->
157 | 'class A\n f: ->'
158 |
159 | 'when spacing is required around arrow operator' : (source) ->
160 | config = { "arrow_spacing": { "level": "error" } }
161 | errors = coffeelint.lint(source, config)
162 | assert.equal(errors.length, 0)
163 |
164 | 'when spacing is not required around arrow operator' : (source) ->
165 | config = { "arrow_spacing": { "level": "ignore" } }
166 | errors = coffeelint.lint(source, config)
167 | assert.isEmpty(errors, 0)
168 |
169 | 'Handle a nested arrow at end of file' :
170 | topic : ->
171 | 'define ->\n class A\n f: ->'
172 |
173 | 'when spacing is required around arrow operator' : (source) ->
174 | config = { "arrow_spacing": { "level": "error" } }
175 | errors = coffeelint.lint(source, config)
176 | assert.equal(errors.length, 0)
177 |
178 | 'when spacing is not required around arrow operator' : (source) ->
179 | config = { "arrow_spacing": { "level": "ignore" } }
180 | errors = coffeelint.lint(source, config)
181 | assert.isEmpty(errors, 0)
182 |
183 | 'Handle an arrow at end of file' :
184 | topic : ->
185 | 'f: ->'
186 |
187 | 'when spacing is required around arrow operator' : (source) ->
188 | config = { "arrow_spacing": { "level": "error" } }
189 | errors = coffeelint.lint(source, config)
190 | assert.equal(errors.length, 0)
191 |
192 | 'when spacing is not required around arrow operator' : (source) ->
193 | config = { "arrow_spacing": { "level": "ignore" } }
194 | errors = coffeelint.lint(source, config)
195 | assert.isEmpty(errors, 0)
196 |
197 | 'Handle an arrow at end of file' :
198 | topic : ->
199 | '{f: ->}'
200 |
201 | 'when spacing is required around arrow operator' : (source) ->
202 | config = { "arrow_spacing": { "level": "error" } }
203 | errors = coffeelint.lint(source, config)
204 | assert.equal(errors.length, 1)
205 |
206 | 'when spacing is not required around arrow operator' : (source) ->
207 | config = { "arrow_spacing": { "level": "ignore" } }
208 | errors = coffeelint.lint(source, config)
209 | assert.isEmpty(errors, 0)
210 |
211 | }).export(module)
212 |
--------------------------------------------------------------------------------
/src/coffeelint.coffee:
--------------------------------------------------------------------------------
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 | # Browserify wrapps this file in a UMD that will set window.coffeelint to
11 | # exports
12 | coffeelint = exports
13 |
14 | if window?
15 | # If we're in the browser assume CoffeeScript is already loaded.
16 | CoffeeScript = window.CoffeeScript
17 | else
18 | # By storing this in a variable it prevents browserify from finding this
19 | # dependency. If it isn't hidden there is an error attempting to inline
20 | # CoffeeScript. if browserify uses `-i` to ignore the dependency it
21 | # creates an empty shim which breaks NodeJS
22 | # https://github.com/substack/node-browserify/issues/471
23 | cs = 'coffee-script'
24 | CoffeeScript = require cs
25 |
26 | # Browserify will inline the file at compile time.
27 | packageJSON = require('./../package.json')
28 |
29 | # The current version of Coffeelint.
30 | coffeelint.VERSION = packageJSON.version
31 |
32 |
33 | # CoffeeLint error levels.
34 | ERROR = 'error'
35 | WARN = 'warn'
36 | IGNORE = 'ignore'
37 |
38 | coffeelint.RULES = RULES = require './rules.coffee'
39 |
40 | # Patch the source properties onto the destination.
41 | extend = (destination, sources...) ->
42 | for source in sources
43 | (destination[k] = v for k, v of source)
44 | return destination
45 |
46 | # Patch any missing attributes from defaults to source.
47 | defaults = (source, defaults) ->
48 | extend({}, defaults, source)
49 |
50 | # Helper to remove rules from disabled list
51 | difference = (a, b) ->
52 | j = 0
53 | while j < a.length
54 | if a[j] in b
55 | a.splice(j, 1)
56 | else
57 | j++
58 |
59 | LineLinter = require './line_linter.coffee'
60 | LexicalLinter = require './lexical_linter.coffee'
61 | ASTLinter = require './ast_linter.coffee'
62 |
63 | # Merge default and user configuration.
64 | mergeDefaultConfig = (userConfig) ->
65 | config = {}
66 | for rule, ruleConfig of RULES
67 | config[rule] = defaults(userConfig[rule], ruleConfig)
68 |
69 |
70 | return config
71 |
72 | coffeelint.invertLiterate = (source) ->
73 | source = CoffeeScript.helpers.invertLiterate source
74 | # Strip the first 4 spaces from every line. After this the markdown is
75 | # commented and all of the other code should be at their natural location.
76 | newSource = ""
77 | for line in source.split "\n"
78 | if line.match(/^#/)
79 | # strip trailing space
80 | line = line.replace /\s*$/, ''
81 | # Strip the first 4 spaces of every line. This is how Markdown
82 | # indicates code, so in the end this pulls everything back to where it
83 | # would be indented if it hadn't been written in literate style.
84 | line = line.replace /^\s{4}/g, ''
85 | newSource += "#{line}\n"
86 |
87 | newSource
88 |
89 | _rules = {}
90 | coffeelint.registerRule = (RuleConstructor, ruleName = undefined) ->
91 | p = new RuleConstructor
92 |
93 | name = p?.rule?.name or "(unknown)"
94 | e = (msg) -> throw new Error "Invalid rule: #{name} #{msg}"
95 | unless p.rule?
96 | e "Rules must provide rule attribute with a default configuration."
97 |
98 | e "Rule defaults require a name" unless p.rule.name?
99 |
100 | if ruleName? and ruleName isnt p.rule.name
101 | e "Mismatched rule name: #{ruleName}"
102 |
103 | e "Rule defaults require a message" unless p.rule.message?
104 | e "Rule defaults require a description" unless p.rule.description?
105 | unless p.rule.level in [ 'ignore', 'warn', 'error' ]
106 | e "Default level must be 'ignore', 'warn', or 'error'"
107 |
108 | if typeof p.lintToken is 'function'
109 | e "'tokens' is required for 'lintToken'" unless p.tokens
110 | else if typeof p.lintLine isnt 'function' and
111 | typeof p.lintAST isnt 'function'
112 | e "Rules must implement lintToken, lintLine, or lintAST"
113 |
114 | # Capture the default options for the new rule.
115 | RULES[p.rule.name] = p.rule
116 | _rules[p.rule.name] = RuleConstructor
117 |
118 | # These all need to be explicitly listed so they get picked up by browserify.
119 | coffeelint.registerRule require './rules/arrow_spacing.coffee'
120 | coffeelint.registerRule require './rules/no_tabs.coffee'
121 | coffeelint.registerRule require './rules/no_trailing_whitespace.coffee'
122 | coffeelint.registerRule require './rules/max_line_length.coffee'
123 | coffeelint.registerRule require './rules/line_endings.coffee'
124 | coffeelint.registerRule require './rules/no_trailing_semicolons.coffee'
125 | coffeelint.registerRule require './rules/indentation.coffee'
126 | coffeelint.registerRule require './rules/camel_case_classes.coffee'
127 | coffeelint.registerRule require './rules/colon_assignment_spacing.coffee'
128 | coffeelint.registerRule require './rules/no_implicit_braces.coffee'
129 | coffeelint.registerRule require './rules/no_plusplus.coffee'
130 | coffeelint.registerRule require './rules/no_throwing_strings.coffee'
131 | coffeelint.registerRule require './rules/no_backticks.coffee'
132 | coffeelint.registerRule require './rules/no_implicit_parens.coffee'
133 | coffeelint.registerRule require './rules/no_empty_param_list.coffee'
134 | coffeelint.registerRule require './rules/no_stand_alone_at.coffee'
135 | coffeelint.registerRule require './rules/space_operators.coffee'
136 | coffeelint.registerRule require './rules/duplicate_key.coffee'
137 | coffeelint.registerRule require './rules/empty_constructor_needs_parens.coffee'
138 | coffeelint.registerRule require './rules/cyclomatic_complexity.coffee'
139 | coffeelint.registerRule require './rules/newlines_after_classes.coffee'
140 | coffeelint.registerRule require './rules/no_unnecessary_fat_arrows.coffee'
141 | coffeelint.registerRule require './rules/missing_fat_arrows.coffee'
142 | coffeelint.registerRule(
143 | require './rules/non_empty_constructor_needs_parens.coffee'
144 | )
145 | coffeelint.registerRule require './rules/no_unnecessary_double_quotes.coffee'
146 | coffeelint.registerRule require './rules/no_debugger.coffee'
147 | coffeelint.registerRule(
148 | require './rules/no_interpolation_in_single_quotes.coffee'
149 | )
150 |
151 | hasSyntaxError = (source) ->
152 | try
153 | # If there are syntax errors this will abort the lexical and line
154 | # linters.
155 | CoffeeScript.tokens(source)
156 | return false
157 | return true
158 |
159 | # Check the source against the given configuration and return an array
160 | # of any errors found. An error is an object with the following
161 | # properties:
162 | #
163 | # {
164 | # rule : 'Name of the violated rule',
165 | # lineNumber: 'Number of the line that caused the violation',
166 | # level: 'The error level of the violated rule',
167 | # message: 'Information about the violated rule',
168 | # context: 'Optional details about why the rule was violated'
169 | # }
170 | #
171 | coffeelint.lint = (source, userConfig = {}, literate = false) ->
172 | source = @invertLiterate source if literate
173 |
174 | config = mergeDefaultConfig(userConfig)
175 |
176 | # Check ahead for inline enabled rules
177 | disabled_initially = []
178 | for l in source.split('\n')
179 | s = LineLinter.configStatement.exec(l)
180 | if s?.length > 2 and 'enable' in s
181 | for r in s[1..]
182 | unless r in ['enable','disable']
183 | unless r of config and config[r].level in ['warn','error']
184 | disabled_initially.push r
185 | config[r] = { level: 'error' }
186 |
187 | # Do AST linting first so all compile errors are caught.
188 | astErrors = new ASTLinter(source, config, _rules, CoffeeScript).lint()
189 | errors = [].concat(astErrors)
190 |
191 | # only do further checks if the syntax is okay, otherwise they just fail
192 | # with syntax error exceptions
193 | unless hasSyntaxError(source)
194 | # Do lexical linting.
195 | lexicalLinter = new LexicalLinter(source, config, _rules, CoffeeScript)
196 | lexErrors = lexicalLinter.lint()
197 | errors = errors.concat(lexErrors)
198 |
199 | # Do line linting.
200 | tokensByLine = lexicalLinter.tokensByLine
201 | lineLinter = new LineLinter(source, config, _rules, tokensByLine,
202 | literate)
203 | lineErrors = lineLinter.lint()
204 | errors = errors.concat(lineErrors)
205 | block_config = lineLinter.block_config
206 | else
207 | # default this so it knows what to do
208 | block_config =
209 | enable : {}
210 | disable : {}
211 |
212 | # Sort by line number and return.
213 | errors.sort((a, b) -> a.lineNumber - b.lineNumber)
214 |
215 | # Disable/enable rules for inline blocks
216 | all_errors = errors
217 | errors = []
218 | disabled = disabled_initially
219 | next_line = 0
220 | for i in [0...source.split('\n').length]
221 | for cmd of block_config
222 | rules = block_config[cmd][i]
223 | {
224 | 'disable': ->
225 | disabled = disabled.concat(rules)
226 | 'enable': ->
227 | difference(disabled, rules)
228 | disabled = disabled_initially if rules.length is 0
229 | }[cmd]() if rules?
230 | # advance line and append relevant messages
231 | while next_line is i and all_errors.length > 0
232 | next_line = all_errors[0].lineNumber - 1
233 | e = all_errors[0]
234 | if e.lineNumber is i + 1 or not e.lineNumber?
235 | e = all_errors.shift()
236 | errors.push e unless e.rule in disabled
237 |
238 | errors
239 |
--------------------------------------------------------------------------------
/src/coffeelint-schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "JSON schema for coffeelint.json files",
3 | "$schema": "http://json-schema.org/draft-04/schema#",
4 | "definitions": {
5 | "level": {
6 | "description": "Determines the error level",
7 | "type": "string",
8 | "enum": [ "warn", "error", "ignore" ]
9 | },
10 | "coffeelint": {
11 | "type": "object",
12 | "additionalProperties": true,
13 | "properties": {
14 | "arrow_spacing": {
15 | "description": "This rule checks to see that there is spacing before and after the arrow operator that declares a function.",
16 | "type": "object",
17 | "properties": {
18 | "level": {
19 | "$ref": "#/definitions/level"
20 | }
21 | }
22 | },
23 | "camel_case_classes": {
24 | "description": "This rule mandates that all class names are CamelCased. Camel casing class names is a generally accepted way of distinguishing constructor functions - which require the 'new' prefix to behave properly - from plain old functions.",
25 | "type": "object",
26 | "properties": {
27 | "level": {
28 | "$ref": "#/definitions/level"
29 | }
30 | }
31 | },
32 | "coffeescript_error": {
33 | "type": "object",
34 | "properties": {
35 | "level": {
36 | "$ref": "#/definitions/level"
37 | }
38 | }
39 | },
40 | "colon_assignment_spacing": {
41 | "description": "This rule checks to see that there is spacing before and after the colon in a colon assignment (i.e., classes, objects).",
42 | "type": "object",
43 | "properties": {
44 | "level": {
45 | "$ref": "#/definitions/level"
46 | },
47 | "spacing": {
48 | "type": "object",
49 | "properties": {
50 | "left": {
51 | "type": "integer",
52 | "enum": [0, 1]
53 | },
54 | "right": {
55 | "type": "integer",
56 | "enum": [0, 1]
57 | }
58 | }
59 | }
60 | }
61 | },
62 | "cyclomatic_complexity": {
63 | "description": "Examine the complexity of your application.",
64 | "type": "object",
65 | "properties": {
66 | "level": {
67 | "$ref": "#/definitions/level"
68 | },
69 | "value": {
70 | "type": "integer"
71 | }
72 | }
73 | },
74 | "duplicate_key": {
75 | "description": "Prevents defining duplicate keys in object literals and classes.",
76 | "type": "object",
77 | "properties": {
78 | "level": {
79 | "$ref": "#/definitions/level"
80 | }
81 | }
82 | },
83 | "empty_constructor_needs_parens": {
84 | "description": "Requires constructors with no parameters to include the parens.",
85 | "type": "object",
86 | "properties": {
87 | "level": {
88 | "$ref": "#/definitions/level"
89 | }
90 | }
91 | },
92 | "indentation": {
93 | "description": " This rule imposes a standard number of spaces to be used for indentation. Since whitespace is significant in CoffeeScript, it's critical that a project chooses a standard indentation format and stays consistent. Other roads lead to darkness.",
94 | "type": "object",
95 | "properties": {
96 | "level": {
97 | "$ref": "#/definitions/level"
98 | },
99 | "value": {
100 | "type": "integer",
101 | "enum": [2, 4]
102 | }
103 | }
104 | },
105 | "line_endings": {
106 | "description": "This rule ensures your project uses only windows or unix line endings.",
107 | "type": "object",
108 | "properties": {
109 | "level": {
110 | "$ref": "#/definitions/level"
111 | },
112 | "value": {
113 | "type": "string",
114 | "enum": [ "unix", "windows" ]
115 | }
116 | }
117 | },
118 | "max_line_length": {
119 | "description": "This rule imposes a maximum line length on your code.",
120 | "type": "object",
121 | "properties": {
122 | "level": {
123 | "$ref": "#/definitions/level"
124 | },
125 | "value": {
126 | "type": "integer"
127 | },
128 | "limitComments": {
129 | "type": "boolean"
130 | }
131 | }
132 | },
133 | "missing_fat_arrows": {
134 | "description": "Warns when you use `this` inside a function that wasn't defined with a fat arrow. This rule does not apply to methods defined in a class, since they have `this` bound to the class instance (or the class itself, for class methods).",
135 | "type": "object",
136 | "properties": {
137 | "level": {
138 | "$ref": "#/definitions/level"
139 | }
140 | }
141 | },
142 | "newlines_after_classes": {
143 | "description": "Checks the number of newlines between classes and other code.",
144 | "type": "object",
145 | "properties": {
146 | "level": {
147 | "$ref": "#/definitions/level"
148 | },
149 | "value": {
150 | "type": "integer",
151 | "enum": [0, 1, 2]
152 | }
153 | }
154 | },
155 | "no_backticks": {
156 | "description": "Backticks allow snippets of JavaScript to be embedded in CoffeeScript. While some folks consider backticks useful in a few niche circumstances, they should be avoided because so none of JavaScript's \"bad parts\", like with and eval, sneak into CoffeeScript.",
157 | "type": "object",
158 | "properties": {
159 | "level": {
160 | "$ref": "#/definitions/level"
161 | }
162 | }
163 | },
164 | "no_debugger": {
165 | "description": "This rule detects the `debugger` statement.",
166 | "type": "object",
167 | "properties": {
168 | "level": {
169 | "$ref": "#/definitions/level"
170 | }
171 | }
172 | },
173 | "no_empty_param_list": {
174 | "description": "This rule prohibits empty parameter lists in function definitions.",
175 | "type": "object",
176 | "properties": {
177 | "level": {
178 | "$ref": "#/definitions/level"
179 | }
180 | }
181 | },
182 | "no_implicit_braces": {
183 | "description": "This rule prohibits implicit braces when declaring object literals. Implicit braces can make code more difficult to understand, especially when used in combination with optional parenthesis.",
184 | "type": "object",
185 | "properties": {
186 | "level": {
187 | "$ref": "#/definitions/level"
188 | },
189 | "strict": {
190 | "type": "boolean"
191 | }
192 | }
193 | },
194 | "no_implicit_parens": {
195 | "description": "This rule prohibits implicit parens on function calls.",
196 | "type": "object",
197 | "properties": {
198 | "level": {
199 | "$ref": "#/definitions/level"
200 | }
201 | }
202 | },
203 | "no_interpolation_in_single_quotes": {
204 | "description": "This rule prohibits string interpolation in a single quoted string.",
205 | "type": "object",
206 | "properties": {
207 | "level": {
208 | "$ref": "#/definitions/level"
209 | }
210 | }
211 | },
212 | "no_plusplus": {
213 | "description": "This rule forbids the increment and decrement arithmetic operators. Some people believe the ++ and -- to be cryptic and the cause of bugs due to misunderstandings of their precedence rules.",
214 | "type": "object",
215 | "properties": {
216 | "level": {
217 | "$ref": "#/definitions/level"
218 | }
219 | }
220 | },
221 | "no_stand_alone_at": {
222 | "description": "This rule checks that no stand alone @ are in use, they are discouraged.",
223 | "type": "object",
224 | "properties": {
225 | "level": {
226 | "$ref": "#/definitions/level"
227 | }
228 | }
229 | },
230 | "no_tabs": {
231 | "description": "This rule forbids tabs in indentation. Enough said.",
232 | "type": "object",
233 | "properties": {
234 | "level": {
235 | "$ref": "#/definitions/level"
236 | }
237 | }
238 | },
239 | "no_throwing_strings": {
240 | "description": " This rule forbids throwing string literals or interpolations. While JavaScript (and CoffeeScript by extension) allow any expression to be thrown, it is best to only throw Error objects, because they contain valuable debugging information like the stack trace.",
241 | "type": "object",
242 | "properties": {
243 | "level": {
244 | "$ref": "#/definitions/level"
245 | }
246 | }
247 | },
248 | "no_trailing_semicolons": {
249 | "description": "This rule prohibits trailing semicolons, since they are needless cruft in CoffeeScript.",
250 | "type": "object",
251 | "properties": {
252 | "level": {
253 | "$ref": "#/definitions/level"
254 | }
255 | }
256 | },
257 | "no_trailing_whitespace": {
258 | "description": "This rule forbids trailing whitespace in your code, since it is needless cruft.",
259 | "type": "object",
260 | "properties": {
261 | "level": {
262 | "$ref": "#/definitions/level"
263 | },
264 | "allowed_in_comments": {
265 | "type": "boolean"
266 | },
267 | "allowed_in_empty_lines": {
268 | "type": "boolean"
269 | }
270 | }
271 | },
272 | "no_unnecessary_double_quotes": {
273 | "description": "This rule prohibits double quotes unless string interpolation is used or the string contains single quotes.",
274 | "type": "object",
275 | "properties": {
276 | "level": {
277 | "$ref": "#/definitions/level"
278 | }
279 | }
280 | },
281 | "no_unnecessary_fat_arrows": {
282 | "description": "Disallows defining functions with fat arrows when `this` is not used within the function. ",
283 | "type": "object",
284 | "properties": {
285 | "level": {
286 | "$ref": "#/definitions/level"
287 | }
288 | }
289 | },
290 | "non_empty_constructor_needs_parens": {
291 | "description": "Requires constructors with parameters to include the parens.",
292 | "type": "object",
293 | "properties": {
294 | "level": {
295 | "$ref": "#/definitions/level"
296 | }
297 | }
298 | },
299 | "space_operators": {
300 | "description": "This rule enforces that operators have space around them. ",
301 | "type": "object",
302 | "properties": {
303 | "level": {
304 | "$ref": "#/definitions/level"
305 | }
306 | }
307 | }
308 | }
309 | }
310 | },
311 |
312 | "oneOf": [ { "$ref": "#/definitions/coffeelint" } ],
313 | "type": "object"
314 | }
--------------------------------------------------------------------------------
/test/test_commandline.coffee:
--------------------------------------------------------------------------------
1 | #
2 | # Tests for the command line tool.
3 | #
4 |
5 | path = require 'path'
6 | fs = require 'fs'
7 | vows = require 'vows'
8 | assert = require 'assert'
9 | {spawn, exec} = require 'child_process'
10 | coffeelint = require path.join('..', 'lib', 'coffeelint')
11 |
12 |
13 | # The path to the command line tool.
14 | coffeelintPath = path.join('bin', 'coffeelint')
15 |
16 | # Run the coffeelint command line with the given
17 | # args. Callback will be called with (error, stdout,
18 | # stderr)
19 | commandline = (args, callback) ->
20 | exec("#{coffeelintPath} #{args.join(" ")}", callback)
21 |
22 |
23 | process.env.HOME = ""
24 | process.env.HOMEPATH = ""
25 | process.env.USERPROFILE = ""
26 | process.env.COFFEELINT_CONFIG = ""
27 |
28 | # Custom rules are loaded by module name. Using a relative path in the test is
29 | # an unrealistic example when rules can be installed with `npm install -g
30 | # some-custom-rule`. This will setup a fake version of node_modules to a
31 | # relative path doesn't have to be used.
32 | process.env.NODE_PATH += ":" + path.resolve( __dirname,
33 | "fixtures/mock_node_modules/")
34 |
35 | vows.describe('commandline').addBatch({
36 |
37 | 'with no args' :
38 |
39 | topic : () ->
40 | commandline [], this.callback
41 | return undefined
42 |
43 | 'shows usage' : (error, stdout, stderr) ->
44 | assert.isNotNull(error)
45 | assert.notEqual(error.code, 0)
46 | assert.include(stderr, "Usage")
47 | assert.isEmpty(stdout)
48 |
49 | 'version' :
50 | topic : () ->
51 | commandline ["--version"], this.callback
52 | return undefined
53 |
54 | 'exists' : (error, stdout, stderr) ->
55 | assert.isNull(error)
56 | assert.isEmpty(stderr)
57 | assert.isString(stdout)
58 | assert.include(stdout, coffeelint.VERSION)
59 |
60 | 'with clean source' :
61 |
62 | topic : () ->
63 | args = [
64 | '--noconfig'
65 | 'test/fixtures/clean.coffee'
66 | ]
67 | commandline args, this.callback
68 | return undefined
69 |
70 | 'passes' : (error, stdout, stderr) ->
71 | assert.isNull(error)
72 | assert.include(stdout, '0 errors and 0 warnings')
73 | assert.isEmpty(stderr)
74 |
75 | 'with failing source' :
76 |
77 | topic : () ->
78 | args = [
79 | '--noconfig'
80 | 'test/fixtures/fourspaces.coffee'
81 | ]
82 | commandline args, this.callback
83 | return undefined
84 |
85 | 'works' : (error, stdout, stderr) ->
86 | assert.isNotNull(error)
87 | assert.include(stdout.toLowerCase(), 'line')
88 |
89 | 'with findconfig and local coffeelint.json' :
90 |
91 | topic : () ->
92 | args = [
93 | 'test/fixtures/findconfigtest/sevenspaces.coffee'
94 | ]
95 | commandline args, this.callback
96 | return undefined
97 |
98 | 'works' : (error, stdout, stderr) ->
99 | assert.isNull(error)
100 |
101 | 'with findconfig and local package.json' :
102 |
103 | topic : () ->
104 | args = [
105 | 'test/fixtures/findconfigtest/package/sixspaces.coffee'
106 | ]
107 | commandline args, this.callback
108 | return undefined
109 |
110 | 'works' : (error, stdout, stderr) ->
111 | assert.isNull(error)
112 |
113 | 'with custom configuration' :
114 |
115 | topic : () ->
116 | args = [
117 | '-f'
118 | 'test/fixtures/fourspaces.json'
119 | 'test/fixtures/fourspaces.coffee'
120 | ]
121 |
122 | commandline args, this.callback
123 | return undefined
124 |
125 | 'works' : (error, stdout, stderr) ->
126 | assert.isNull(error)
127 |
128 | 'with --rule parameter for a custom plugin':
129 | topic : () ->
130 | args = [
131 | '--rules'
132 | # It's up to NodeJS to resolve the actual path. The top of the
133 | # file modifies NODE_PATH so this can look like a 3rd party
134 | # module.
135 | "he_who_must_not_be_named"
136 | 'test/fixtures/custom_rules/voldemort.coffee'
137 | ]
138 |
139 | commandline args, this.callback
140 | return undefined
141 |
142 | 'works' : (error, stdout, stderr) ->
143 | assert.isNotNull(error)
144 | assert.include(stdout.toLowerCase(), 'forbidden variable name')
145 |
146 | 'with `module` specified for a specific rule':
147 | topic : () ->
148 | args = [
149 | '-f'
150 | 'test/fixtures/custom_rules/rule_module.json'
151 | 'test/fixtures/custom_rules/voldemort.coffee'
152 | ]
153 |
154 | commandline args, this.callback
155 | return undefined
156 |
157 | 'works' : (error, stdout, stderr) ->
158 | assert.isNotNull(error)
159 | assert.include(stdout.toLowerCase(), 'forbidden variable name')
160 |
161 | 'with multiple sources' :
162 |
163 | topic : () ->
164 | args = [
165 | '--noconfig'
166 | '-f'
167 | 'test/fixtures/fourspaces.json'
168 | 'test/fixtures/fourspaces.coffee'
169 | 'test/fixtures/clean.coffee'
170 | ]
171 |
172 | commandline args, this.callback
173 | return undefined
174 |
175 | 'works' : (error, stdout, stderr) ->
176 | assert.isNotNull(error)
177 |
178 | 'with configuration file' :
179 |
180 | topic : () ->
181 | configPath = 'generated_coffeelint.json'
182 | configFile = fs.openSync configPath, 'w'
183 | commandline ['--makeconfig'], (error, stdout, stderr) =>
184 | fs.writeSync configFile, stdout
185 | assert.isNull(error)
186 | args = [
187 | '-f'
188 | configPath
189 | 'test/fixtures/clean.coffee'
190 | ]
191 | commandline args, (args...) =>
192 | this.callback stdout, args...
193 |
194 | return undefined
195 |
196 | 'works' : (config, error, stdout, stderr) ->
197 | assert.isNotNull(config)
198 | # This will throw an exception if it doesn't parse.
199 | JSON.parse config
200 | assert.isNotNull(stdout)
201 | assert.isNull(error)
202 |
203 | 'does not fail on warnings' :
204 |
205 | topic : () ->
206 | args = [
207 | '-f'
208 | 'test/fixtures/twospaces.warning.json'
209 | 'test/fixtures/fourspaces.coffee'
210 | ]
211 |
212 | commandline args, this.callback
213 | return undefined
214 |
215 | 'works' : (error, stdout, stderr) ->
216 | assert.isNull(error)
217 |
218 | 'with broken source' :
219 |
220 | topic : () ->
221 | args = [
222 | '--noconfig'
223 | 'test/fixtures/syntax_error.coffee'
224 | ]
225 | commandline args, this.callback
226 | return undefined
227 |
228 | 'fails' : (error, stdout, stderr) ->
229 | assert.isNotNull(error)
230 |
231 | 'recurses subdirectories' :
232 |
233 | topic : () ->
234 | args = [
235 | '--noconfig',
236 | '-r',
237 | 'test/fixtures/clean.coffee',
238 | 'test/fixtures/subdir'
239 | ]
240 | commandline args, this.callback
241 | return undefined
242 |
243 | 'and reports errors' : (error, stdout, stderr) ->
244 | assert.isNotNull(error, "returned err")
245 | assert.include(stdout.toLowerCase(), 'line')
246 |
247 | 'allows JSLint XML reporting' :
248 |
249 | # FIXME: Not sure how to unit test escaping w/o major refactoring
250 | topic : () ->
251 | args = [
252 | '-f'
253 | 'coffeelint.json'
254 | 'test/fixtures/cyclo_fail.coffee'
255 | '--jslint'
256 | ]
257 | commandline args, this.callback
258 | return undefined
259 |
260 | 'Handles cyclomatic complexity check' : (error, stdout, stderr) ->
261 | assert.include(stdout.toLowerCase(), 'cyclomatic complexity')
262 |
263 | 'using stdin':
264 |
265 | 'with working string':
266 | topic: () ->
267 | exec("echo y = 1 | #{coffeelintPath} --noconfig --stdin",
268 | this.callback)
269 | return undefined
270 |
271 | 'passes': (error, stdout, stderr) ->
272 | assert.isNull(error)
273 | assert.isEmpty(stderr)
274 | assert.isString(stdout)
275 | assert.include(stdout, '0 errors and 0 warnings')
276 |
277 | 'with failing string due to whitespace':
278 | topic: () ->
279 | exec("echo 'x = 1 '| #{coffeelintPath} --noconfig --stdin",
280 | this.callback)
281 | return undefined
282 |
283 | 'fails': (error, stdout, stderr) ->
284 | assert.isNotNull(error)
285 | assert.include(stdout.toLowerCase(), 'trailing whitespace')
286 |
287 | 'literate coffeescript':
288 |
289 | 'with working string':
290 | topic: () ->
291 | exec("echo 'This is Markdown\n\n y = 1' | " +
292 | "#{coffeelintPath} --noconfig --stdin --literate",
293 | this.callback)
294 | return undefined
295 |
296 | 'passes': (error, stdout, stderr) ->
297 | assert.isNull(error)
298 | assert.isEmpty(stderr)
299 | assert.isString(stdout)
300 | assert.include(stdout, '0 errors and 0 warnings')
301 |
302 | 'with failing string due to whitespace':
303 | topic: () ->
304 | exec("echo 'This is Markdown\n\n x = 1 \n y=2'| " +
305 | "#{coffeelintPath} --noconfig --stdin --literate",
306 | this.callback)
307 | return undefined
308 |
309 | 'fails': (error, stdout, stderr) ->
310 | assert.isNotNull(error)
311 | assert.include(stdout.toLowerCase(), 'trailing whitespace')
312 |
313 | 'using environment config file':
314 |
315 | 'with non existing enviroment set config file':
316 | topic: () ->
317 | args = [
318 | 'test/fixtures/clean.coffee'
319 | ]
320 | process.env.COFFEELINT_CONFIG = "not_existing_293ujff"
321 | commandline args, this.callback
322 | return undefined
323 |
324 | 'passes': (error, stdout, stderr) ->
325 | assert.isNull(error)
326 |
327 | 'with existing enviroment set config file':
328 | topic: () ->
329 | args = [
330 | 'test/fixtures/fourspaces.coffee'
331 | ]
332 | conf = "test/fixtures/fourspaces.json"
333 | process.env.COFFEELINT_CONFIG = conf
334 | commandline args, this.callback
335 | return undefined
336 |
337 | 'passes': (error, stdout, stderr) ->
338 | assert.isNull(error)
339 |
340 | 'with existing enviroment set config file + --noconfig':
341 | topic: () ->
342 | args = [
343 | '--noconfig'
344 | 'test/fixtures/fourspaces.coffee'
345 | ]
346 | conf = "test/fixtures/fourspaces.json"
347 | process.env.COFFEELINT_CONFIG = conf
348 | commandline args, this.callback
349 | return undefined
350 |
351 | 'fails': (error, stdout, stderr) ->
352 | assert.isNotNull(error)
353 |
354 | 'reports using basic reporter':
355 | 'with option q set':
356 | 'and no errors occured':
357 | topic: () ->
358 | args = [ '-q', '--noconfig', 'test/fixtures/clean.coffee' ]
359 | commandline args, this.callback
360 | return undefined
361 |
362 | 'no output': (err, stdout, stderr) ->
363 | assert.isEmpty(stdout)
364 |
365 | 'and errors occured':
366 | topic: () ->
367 | args = [ '-q', 'test/fixtures/syntax_error.coffee' ]
368 | commandline args, this.callback
369 | return undefined
370 |
371 | 'output': (error, stdout, stderr) ->
372 | assert.isNotEmpty(stdout)
373 |
374 | 'with option q not set':
375 | 'and no errors occured':
376 | topic: () ->
377 | args = [ 'test/fixtures/clean.coffee' ]
378 | commandline args, this.callback
379 | return undefined
380 |
381 | 'output': (err, stdout, stderr) ->
382 | assert.isNotEmpty(stdout)
383 |
384 | 'and errors occured':
385 | topic: () ->
386 | args = [ 'test/fixtures/syntax_error.coffee' ]
387 | commandline args, this.callback
388 | return undefined
389 |
390 | 'output': (error, stdout, stderr) ->
391 | assert.isNotEmpty(stdout)
392 |
393 | }).export(module)
394 |
--------------------------------------------------------------------------------