├── .coffeelintignore ├── .gitignore ├── .npmignore ├── .travis.yml ├── 3rd_party_rules.md ├── Cakefile ├── LICENSE ├── OLD_README.md ├── README.md ├── bin ├── .gitignore └── coffeelint ├── coffeelint.json ├── doc ├── integration.md ├── release.md └── user.md ├── generated_coffeelint.json ├── lib └── .gitignore ├── package.json ├── src ├── ast_linter.coffee ├── base_linter.coffee ├── cache.coffee ├── coffeelint.coffee ├── commandline.coffee ├── configfinder.coffee ├── error_report.coffee ├── htmldoc.coffee ├── lexical_linter.coffee ├── line_linter.coffee ├── reporters │ ├── checkstyle.coffee │ ├── csv.coffee │ ├── default.coffee │ ├── jslint.coffee │ ├── passthrough.coffee │ └── raw.coffee ├── ruleLoader.coffee ├── rules.coffee └── rules │ ├── arrow_spacing.coffee │ ├── braces_spacing.coffee │ ├── camel_case_classes.coffee │ ├── colon_assignment_spacing.coffee │ ├── cyclomatic_complexity.coffee │ ├── duplicate_key.coffee │ ├── empty_constructor_needs_parens.coffee │ ├── ensure_comprehensions.coffee │ ├── eol_last.coffee │ ├── indentation.coffee │ ├── line_endings.coffee │ ├── max_line_length.coffee │ ├── missing_fat_arrows.coffee │ ├── newlines_after_classes.coffee │ ├── no_backticks.coffee │ ├── no_debugger.coffee │ ├── no_empty_functions.coffee │ ├── no_empty_param_list.coffee │ ├── no_implicit_braces.coffee │ ├── no_implicit_parens.coffee │ ├── no_interpolation_in_single_quotes.coffee │ ├── no_nested_string_interpolation.coffee │ ├── no_plusplus.coffee │ ├── no_private_function_fat_arrows.coffee │ ├── no_stand_alone_at.coffee │ ├── no_tabs.coffee │ ├── no_this.coffee │ ├── no_throwing_strings.coffee │ ├── no_trailing_semicolons.coffee │ ├── no_trailing_whitespace.coffee │ ├── no_unnecessary_double_quotes.coffee │ ├── no_unnecessary_fat_arrows.coffee │ ├── non_empty_constructor_needs_parens.coffee │ ├── prefer_english_operator.coffee │ ├── space_operators.coffee │ ├── spacing_after_comma.coffee │ └── transform_messes_up_line_numbers.coffee ├── test ├── fixtures │ ├── clean.coffee │ ├── cloud_transform.coffee │ ├── coffeelint.json │ ├── custom_extention │ │ └── clean.csbx │ ├── custom_rules │ │ ├── rule_module.json │ │ └── voldemort.coffee │ ├── cyclo_fail.coffee │ ├── find_extended_test │ │ ├── coffeelint.json │ │ └── invalid.coffee │ ├── findconfigtest │ │ ├── coffeelint.json │ │ ├── package │ │ │ ├── package.json │ │ │ └── sixspaces.coffee │ │ └── sevenspaces.coffee │ ├── fourspaces.coffee │ ├── fourspaces.json │ ├── mock_node_modules │ │ ├── coffeelint-extends-test │ │ │ ├── index.json │ │ │ └── package.json │ │ └── he_who_must_not_be_named │ │ │ ├── he_who_must_not_be_named.coffee │ │ │ └── index.js │ ├── prefix_transform.coffee │ ├── subdir │ │ └── subdir │ │ │ └── subdir.coffee │ ├── syntax_error.coffee │ └── twospaces.warning.json ├── test_arrow_spacing.coffee ├── test_braces_spacing.coffee ├── test_camel_case_classes.coffee ├── test_coffeelint.coffee ├── test_colon_assignment_spacing.coffee ├── test_commandline.coffee ├── test_comment_config.coffee ├── test_cyclomatic_complexity.coffee ├── test_duplicate_key.coffee ├── test_empty_constructor_needs_parens.coffee ├── test_ensure_comprehensions.coffee ├── test_eol_last.coffee ├── test_filenames.coffee ├── test_indentation.coffee ├── test_levels.coffee ├── test_line_endings.coffee ├── test_literate.litcoffee ├── test_max_line_length.coffee ├── test_missing_fat_arrows.coffee ├── test_newlines_after_classes.coffee ├── test_no_backticks.coffee ├── test_no_debugger.coffee ├── test_no_empty_functions.coffee ├── test_no_empty_param_list.coffee ├── test_no_implicit_braces.coffee ├── test_no_implicit_parens.coffee ├── test_no_interpolation_in_single_quotes.coffee ├── test_no_nested_string_interpolation.coffee ├── test_no_plusplus.coffee ├── test_no_private_function_fat_arrows.coffee ├── test_no_stand_alone_at.coffee ├── test_no_tabs.coffee ├── test_no_this.coffee ├── test_no_throwing_strings.coffee ├── test_no_trailing_semicolons.coffee ├── test_no_trailing_whitespace.coffee ├── test_no_unnecessary_double_quotes.coffee ├── test_no_unnecessary_fat_arrows.coffee ├── test_non_empty_constructor_needs_parens.coffee ├── test_prefer_english_operator.coffee ├── test_reporters.coffee ├── test_space_operators.coffee ├── test_spacing_after_comma.coffee └── test_transform_messes_up_line_numbers.coffee └── vowsrunner.js /.coffeelintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test/fixtures 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/* 2 | node_modules 3 | *.log 4 | utils 5 | .package.json 6 | .node-version 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | test/ 3 | coffeelint.json 4 | .travis.yml 5 | .package.json 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "6.9.1" 5 | - "7" 6 | - "8" 7 | - "node" 8 | branches: 9 | only: 10 | - master 11 | - next 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Cakefile: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | glob = require 'glob' 3 | path = require 'path' 4 | browserify = require 'browserify' 5 | CoffeeScript = require 'coffeescript' 6 | { exec } = require 'child_process' 7 | 8 | copySync = (src, dest) -> 9 | fs.writeFileSync dest, fs.readFileSync(src) 10 | 11 | coffeeSync = (input, output) -> 12 | coffee = fs.readFileSync(input).toString() 13 | fs.writeFileSync output, CoffeeScript.compile(coffee) 14 | 15 | task 'compile', 'Compile Coffeelint', -> 16 | console.log 'Compiling Coffeelint...' 17 | fs.mkdirSync 'lib' unless fs.existsSync 'lib' 18 | invoke 'compile:browserify' 19 | invoke 'compile:commandline' 20 | 21 | task 'compile:commandline', 'Compiles commandline.js', -> 22 | coffeeSync 'src/commandline.coffee', 'lib/commandline.js' 23 | coffeeSync 'src/configfinder.coffee', 'lib/configfinder.js' 24 | coffeeSync 'src/cache.coffee', 'lib/cache.js' 25 | coffeeSync 'src/ruleLoader.coffee', 'lib/ruleLoader.js' 26 | fs.mkdirSync 'lib/reporters' unless fs.existsSync 'lib/reporters' 27 | for src in glob.sync('reporters/*.coffee', { cwd: 'src' }) 28 | # Slice the "coffee" extension of the end and replace with js 29 | dest = src[...-6] + 'js' 30 | coffeeSync "src/#{src}", "lib/#{dest}" 31 | 32 | task 'compile:browserify', 'Uses browserify to compile coffeelint', -> 33 | opts = 34 | standalone: 'coffeelint' 35 | b = browserify(opts) 36 | b.add [ './src/coffeelint.coffee' ] 37 | b.transform require('coffeeify') 38 | b.bundle().pipe fs.createWriteStream('lib/coffeelint.js') 39 | 40 | task 'prepublish', 'Prepublish', -> 41 | { npm_config_argv } = process.env 42 | if npm_config_argv? and JSON.parse(npm_config_argv).original[0] is 'install' 43 | return 44 | 45 | copySync 'package.json', '.package.json' 46 | packageJson = require './package.json' 47 | 48 | delete packageJson.dependencies.browserify 49 | delete packageJson.dependencies.coffeeify 50 | delete packageJson.scripts.install 51 | 52 | fs.writeFileSync 'package.json', JSON.stringify(packageJson, undefined, 2) 53 | 54 | invoke 'compile' 55 | 56 | task 'postpublish', 'Postpublish', -> 57 | # Revert the package.json back to it's original state 58 | exec 'git checkout ./package.json', (err) -> 59 | if err 60 | console.error('Error reverting package.json: ' + err) 61 | 62 | task 'publish', 'publish', -> 63 | copySync '.package.json', 'package.json' 64 | 65 | task 'install', 'Install', -> 66 | unless require("fs").existsSync("lib/commandline.js") 67 | invoke 'compile' 68 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /OLD_README.md: -------------------------------------------------------------------------------- 1 | CoffeeLint 2 | ========== 3 | 4 | CoffeeLint is a style checker that helps keep CoffeeScript code 5 | clean and consistent. 6 | 7 | For guides on installing, using and configuring CoffeeLint, head over 8 | [here](http://www.coffeelint.org). 9 | 10 | To suggest a feature, report a bug, or general discussion, head over 11 | [here](http://github.com/clutchski/coffeelint/issues/). 12 | 13 | ## Team 14 | 15 | Current: 16 | 17 | - [Shuan Wang](https://github.com/swang) 18 | 19 | Past: 20 | 21 | - [Asa Ayers](https://github.com/AsaAyers) - [You Don't Need CoffeeScript](https://gist.github.com/AsaAyers/d09e4de118b8d6b5e2d8fa3e38e496e0) 22 | - [Matt Perpick](https://github.com/clutchski) 23 | 24 | ## Contributing 25 | 26 | * New rules should be set to a `warn` level. Developers will expect new changes to NOT break their existing workflow, so unless your change is extremely usefull, default to `warn`. Expect discussion if you choose to use `error`. 27 | 28 | * Look at existing rules and test structures when deciding how to name your rule. `no_foo.coffee` is used for many tests designed to catch specific errors, whereas `foo.coffee` is used for tests that are designed to enforce formatting and syntax. 29 | 30 | ### Steps 31 | 32 | 1. Fork the repo locally. 33 | 2. Run `npm install` to get dependencies. 34 | 3. Create your rule in a single file as `src/rules/your_rule_here.coffee`, using the existing 35 | rules as a guide. 36 | You may examine the AST and tokens using 37 | [http://asaayers.github.io/clfiddle/](http://asaayers.github.io/clfiddle/). 38 | 4. Add your test file `my_test.coffee` to the `test` directory. 39 | 5. Register your rule in `src/coffeelint.coffee`. 40 | 6. Run your test using `npm run testrule test/your_test_here.coffee`. 41 | 7. Run the whole tests suite using `npm test`. 42 | 8. Check that your rule's documentation is generated properly (see _Updating documentation when 43 | adding a new rule_ below). 44 | 9. Squash all commits into a single commit when done. 45 | 10. Submit a pull request. 46 | 47 | [](http://travis-ci.org/clutchski/coffeelint) 48 | 49 | ### Updating documentation when adding a new rule 50 | 51 | When adding a new rule, its documentation is specified by setting a 52 | `description` property within its `rule` property: 53 | ```coffeescript 54 | module.exports = class NoComment 55 | 56 | rule: 57 | name: 'no_comment' 58 | level: 'ignore' 59 | message: 'No comment' 60 | description: ''' 61 | Disallows any comment in the code 62 | ''' 63 | 64 | tokens: ['#', '###'] 65 | 66 | lintToken : (token, tokenApi) -> 67 | return {context: "Found '#{token[0]}'"} 68 | ``` 69 | 70 | The description property is a string that can embed HTML code: 71 | ```html 72 | description: ''' 73 | Disallows any comment in the code. This code would not pass: 74 |
75 | ### Some code with comments
76 | foo = ->
77 | # some other comments
78 | bar()
79 |
80 |
81 | '''
82 | ```
83 | [Coffeelint's website](http://www.coffeelint.org/) generates each
84 | rule's documentation based on this `description` property.
85 |
86 | When adding a new rule, it is suggested that you check that the documentation
87 | for your new rule is generated correctly. In order to do that, you should
88 | follow these steps:
89 | * Checkout the branch that contains the changes adding the new rule.
90 | * Run `npm run compile`.
91 | * Checkout the `gh-pages` branch: `git checkout origin/gh-pages`.
92 | * Run `cp lib/coffeelint.js js/coffeelint.js`.
93 | * Regenerate the HTML documentation: `rake updatehtml`. Note that you will
94 | probably need to install rake.
95 | * Open the `index.html` file with your favorite browser and make sure that your
96 | rule's documentation is generated properly.
97 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | CoffeeLint
2 | ==========
3 |
4 | CoffeeLint ~~is~~ was a style checker that ~~helps~~ helped keep CoffeeScript code
5 | clean and consistent.
6 |
7 | This project is no longer maintained but the code remains freely available for your
8 | own use.
9 |
--------------------------------------------------------------------------------
/bin/.gitignore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clutchski/coffeelint/86631c2c8ce82b4403fb84a377364ccc07180a9a/bin/.gitignore
--------------------------------------------------------------------------------
/bin/coffeelint:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var path = require("path");
4 | var fs = require("fs");
5 | var resolve = require('resolve').sync;
6 |
7 | // path.existsSync was moved to fs.existsSync node 0.6 -> 0.8
8 | var existsFn = fs.existsSync || path.existsSync;
9 |
10 | var thisdir = path.dirname(fs.realpathSync(__filename));
11 |
12 |
13 |
14 |
15 | // This setup allows for VERY fast development. I clear the lib directory so
16 | // that every time coffeelint runs, it uses CoffeeScript to re-compile at
17 | // runtime.
18 | //
19 | // I use this so vim runs the newest code while I work on CoffeeLint. -Asa
20 | commandline = path.join(thisdir, '..', "lib", "commandline.js");
21 | if (!existsFn(commandline)) {
22 | require('coffee-script/register');
23 | require('../src/commandline');
24 | } else {
25 | // This is the code path that everyone else is really going to use.
26 | try {
27 | // Try to find a project-specific install first. This works the same
28 | // way grunt-cli does.
29 | filepath = resolve('coffeelint', { basedir: process.cwd() });
30 | commandline = path.dirname(filepath) + path.sep + 'commandline.js';
31 | } catch (ex) {
32 | }
33 |
34 | require(commandline);
35 | }
36 |
--------------------------------------------------------------------------------
/coffeelint.json:
--------------------------------------------------------------------------------
1 | {
2 | "arrow_spacing": {
3 | "level": "error"
4 | },
5 | "line_endings": {
6 | "level": "error"
7 | },
8 | "indentation": {
9 | "value": 4
10 | },
11 | "colon_assignment_spacing": {
12 | "level": "error",
13 | "spacing": {
14 | "left": 0,
15 | "right": 1
16 | }
17 | },
18 | "space_operators": {
19 | "level": "error"
20 | },
21 | "cyclomatic_complexity": {
22 | "value": 11,
23 | "level": "warn"
24 | },
25 | "no_debugger": {
26 | "level": "error",
27 | "console": true
28 | },
29 | "spacing_after_comma": {
30 | "level": "error"
31 | },
32 | "no_unnecessary_double_quotes": {
33 | "level": "error"
34 | },
35 | "braces_spacing": {
36 | "level": "error",
37 | "spaces": 1
38 | },
39 | "eol_last": {
40 | "level": "error"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/doc/integration.md:
--------------------------------------------------------------------------------
1 | This file is for anyone working on integrating CoffeeLint with another project.
2 |
3 | What kinds of things can I build?
4 | =================================
5 |
6 | CoffeeLint is [exposed as a library][API] that you could use in build tools, or
7 | in the browser. Anywhere you can run Javascript. You can also build [your own
8 | rules][CustomRules], or custom reporter.
9 |
10 | CoffeeLint is also a command line tool, so most non-javascript integrations run
11 | that and parse the output. There are built in reporters for `csv`, `jslint`,
12 | `checkstyle`, and `raw`.
13 |
14 | Which version of CoffeeLint should I depend on?
15 | ===============================================
16 |
17 | CoffeeLint follows [Semantic Versioning][semver], so any breaking change to the
18 | API will be a major version change. I recommend depending on `^1.x` where x is
19 | the current version.
20 |
21 | How do I list my editor/build plugin on coffeelint.org?
22 | =======================================================
23 |
24 | Coffeelint.org uses `gh-pages`, just send a pull request with your addition and
25 | it'll get reviewed. The list of plugins is in `index-bottom.html`. `rake
26 | updatehtml` will regenerate `index.html` with your changes.
27 |
28 | How can people find my rule?
29 | ============================
30 |
31 | All rules need to include the `coffeelintrule`. Coffeelint.org directs users to
32 | [https://www.npmjs.org/search?q=coffeelintrule][rules] to locate available
33 | rules.
34 |
35 | Does my rule need to be built into CoffeeLint?
36 | ==============================================
37 |
38 | Built in rules use the same APIs as 3rd party rules. The only benefit of built
39 | in rules is that they get included with CoffeeLint and are exposed to a wider
40 | audience. Most new rules are set to `ignore` by default. My general guide line
41 | is that if you can demonstrate the rule prevents a type of error it can be
42 | `warn` or `error` by default. I think `no_debugger` is a good example of such a
43 | rule.
44 |
45 | [semver]: semver.org
46 | [rules]: https://www.npmjs.org/search?q=coffeelintrule
47 | [API]: http://www.coffeelint.org/#api
48 | [CustomRules]: http://www.coffeelint.org/#api
49 |
--------------------------------------------------------------------------------
/doc/release.md:
--------------------------------------------------------------------------------
1 | Release Steps
2 | =============
3 |
4 | 1. Review changelog
5 | -------------------
6 |
7 | I always use the top changelog link on [coffeelint.org][changelog] and change it
8 | to point to `compare/vx.x.x...master`. Look through the pull request to figure
9 | out whether this is a minor or patch release.
10 |
11 | 2. Tag
12 | ------
13 |
14 | CoffeeLint follows [semver](http://semver.org/). When a new rule is added even
15 | if it's off by default, it's at least a minor release. There are some things
16 | marked deprecated. If we ever have a need for a 2.0 I'll remove those at that
17 | time.
18 |
19 | npm version default level: <%= level %>
21 |This rule checks to see that there is spacing before and after 9 | the arrow operator that declares a function. This rule is disabled 10 | by default.
Note that if arrow_spacing is enabled, and you 11 | pass an empty function as a parameter, arrow_spacing will accept 12 | either a space or no space in-between the arrow operator and the 13 | parenthesis
14 |# Both of this will not trigger an error,
15 | # even with arrow_spacing enabled.
16 | x(-> 3)
17 | x( -> 3)
18 |
19 | # However, this will trigger an error
20 | x((a,b)-> 3)
21 |
22 |
23 | '''
24 |
25 | tokens: ['->', '=>']
26 |
27 | lintToken: (token, tokenApi) ->
28 | # Throw error unless the following happens.
29 | #
30 | # We will take a look at the previous token to see
31 | # 1. That the token is properly spaced
32 | # 2. Wasn't generated by the CoffeeScript compiler
33 | # 3. That it is just indentation
34 | # 4. If the function declaration has no parameters
35 | # e.g. x(-> 3)
36 | # x( -> 3)
37 | #
38 | # or a statement is wrapped in parentheses
39 | # e.g. (-> true)()
40 | #
41 | # we will accept either having a space or not having a space there.
42 | #
43 | # Also if the -> is the beginning of the file, then simply just return
44 |
45 | pp = tokenApi.peek(-1)
46 |
47 | return unless pp
48 |
49 | # Ignore empty functions
50 | if not token.spaced and
51 | tokenApi.peek(1)[0] is 'INDENT' and
52 | tokenApi.peek(2)[0] is 'OUTDENT'
53 | null
54 | else unless (token.spaced? or token.newLine?) and
55 | # Throw error unless the previous token...
56 | ((pp.spaced? or pp[0] is 'TERMINATOR') or #1
57 | pp.generated? or #2
58 | pp[0] is 'INDENT' or #3
59 | (pp[1] is '(' and not pp.generated?)) #4
60 | true
61 | else
62 | null
63 |
--------------------------------------------------------------------------------
/src/rules/braces_spacing.coffee:
--------------------------------------------------------------------------------
1 | module.exports = class BracesSpacing
2 |
3 | rule:
4 | name: 'braces_spacing'
5 | level: 'ignore'
6 | spaces: 0
7 | empty_object_spaces: 0
8 | message: 'Curly braces must have the proper spacing'
9 | description: '''
10 | This rule checks to see that there is the proper spacing inside
11 | curly braces. The spacing amount is specified by "spaces".
12 | The spacing amount for empty objects is specified by
13 | "empty_object_spaces".
14 |
15 |
16 | # Spaces is 0
17 | {a: b} # Good
18 | {a: b } # Bad
19 | { a: b} # Bad
20 | { a: b } # Bad
21 |
22 | # Spaces is 1
23 | {a: b} # Bad
24 | {a: b } # Bad
25 | { a: b} # Bad
26 | { a: b } # Good
27 | { a: b } # Bad
28 | { a: b } # Bad
29 | { a: b } # Bad
30 |
31 | # Empty Object Spaces is 0
32 | {} # Good
33 | { } # Bad
34 |
35 | # Empty Object Spaces is 1
36 | {} # Bad
37 | { } # Good
38 |
39 |
40 | This rule is disabled by default.
41 | '''
42 |
43 | tokens: ['{', '}']
44 |
45 | distanceBetweenTokens: (firstToken, secondToken) ->
46 | secondToken[2].first_column - firstToken[2].last_column - 1
47 |
48 | findNearestToken: (token, tokenApi, difference) ->
49 | totalDifference = 0
50 | loop
51 | totalDifference += difference
52 | nearestToken = tokenApi.peek(totalDifference)
53 | continue if nearestToken[0] is 'OUTDENT' or nearestToken.generated?
54 | return nearestToken
55 |
56 | tokensOnSameLine: (firstToken, secondToken) ->
57 | firstToken[2].first_line is secondToken[2].first_line
58 |
59 | getExpectedSpaces: (tokenApi, firstToken, secondToken) ->
60 | config = tokenApi.config[@rule.name]
61 | if firstToken[0] is '{' and secondToken[0] is '}'
62 | config.empty_object_spaces ? config.spaces
63 | else
64 | config.spaces
65 |
66 | lintToken: (token, tokenApi) ->
67 | return null if token.generated
68 |
69 | [firstToken, secondToken] = if token[0] is '{'
70 | [token, @findNearestToken(token, tokenApi, 1)]
71 | else
72 | [@findNearestToken(token, tokenApi, -1), token]
73 |
74 | return null unless @tokensOnSameLine firstToken, secondToken
75 |
76 | expected = @getExpectedSpaces tokenApi, firstToken, secondToken
77 | actual = @distanceBetweenTokens firstToken, secondToken
78 |
79 | if actual is expected
80 | null
81 | else
82 | msg = "There should be #{expected} space"
83 | msg += 's' unless expected is 1
84 | msg += " inside \"#{token[0]}\""
85 | context: msg
86 |
--------------------------------------------------------------------------------
/src/rules/camel_case_classes.coffee:
--------------------------------------------------------------------------------
1 | regexes =
2 | camelCase: /^[A-Z_][a-zA-Z\d]*$/
3 |
4 | module.exports = class CamelCaseClasses
5 |
6 | rule:
7 | name: 'camel_case_classes'
8 | level: 'error'
9 | message: 'Class name should be UpperCamelCased'
10 | description: '''
11 | This rule mandates that all class names are UpperCamelCased.
12 | Camel casing class names is a generally accepted way of
13 | distinguishing constructor functions - which require the 'new'
14 | prefix to behave properly - from plain old functions.
15 |
16 | # Good!
17 | class BoaConstrictor
18 |
19 | # Bad!
20 | class boaConstrictor
21 |
22 |
23 | This rule is enabled by default.
24 | '''
25 |
26 | tokens: ['CLASS']
27 |
28 | lintToken: (token, tokenApi) ->
29 | # TODO: you can do some crazy shit in CoffeeScript, like
30 | # class func().ClassName. Don't allow that.
31 |
32 | # Don't try to lint the names of anonymous classes.
33 | if token.newLine? or tokenApi.peek()[0] in ['INDENT', 'EXTENDS']
34 | return null
35 |
36 | # It's common to assign a class to a global namespace, e.g.
37 | # exports.MyClassName, so loop through the next tokens until
38 | # we find the real identifier.
39 | className = null
40 | offset = 1
41 | until className
42 | if tokenApi.peek(offset + 1)?[0] is '.'
43 | offset += 2
44 | else if tokenApi.peek(offset)?[0] is '@'
45 | offset += 1
46 | else
47 | className = tokenApi.peek(offset)[1]
48 |
49 | # Now check for the error.
50 | if not regexes.camelCase.test(className)
51 | return { context: "class name: #{className}" }
52 |
--------------------------------------------------------------------------------
/src/rules/colon_assignment_spacing.coffee:
--------------------------------------------------------------------------------
1 | module.exports = class ColonAssignmentSpacing
2 | rule:
3 | name: 'colon_assignment_spacing'
4 | level: 'ignore'
5 | message: 'Colon assignment without proper spacing'
6 | spacing:
7 | left: 0
8 | right: 0
9 | description: '''
10 | This rule checks to see that there is spacing before and 11 | after the colon in a colon assignment (i.e., classes, objects). 12 | The spacing amount is specified by 13 | spacing.left and spacing.right, respectively. 14 | A zero value means no spacing required. 15 |
16 |
17 | #
18 | # If spacing.left and spacing.right is 1
19 | #
20 |
21 | # Doesn't throw an error
22 | object = {spacing : true}
23 | class Dog
24 | canBark : true
25 |
26 | # Throws an error
27 | object = {spacing: true}
28 | class Cat
29 | canBark: false
30 |
31 | '''
32 |
33 | tokens: [':']
34 |
35 | lintToken: (token, tokenApi) ->
36 | spaceRules = tokenApi.config[@rule.name].spacing
37 | previousToken = tokenApi.peek -1
38 | nextToken = tokenApi.peek 1
39 |
40 | getSpaceFromToken = (direction) ->
41 | switch direction
42 | when 'left'
43 | token[2].first_column - previousToken[2].last_column - 1
44 | when 'right'
45 | nextToken[2].first_column - token[2].first_column - 1
46 |
47 | checkSpacing = (direction) ->
48 | spacing = getSpaceFromToken direction
49 | # when spacing is negative, the neighboring token is a newline
50 | isSpaced = if spacing < 0
51 | true
52 | else
53 | spacing is parseInt spaceRules[direction]
54 |
55 | [isSpaced, spacing]
56 |
57 | [isLeftSpaced, leftSpacing] = checkSpacing 'left'
58 | [isRightSpaced, rightSpacing] = checkSpacing 'right'
59 |
60 | if isLeftSpaced and isRightSpaced
61 | null
62 | else
63 | context: "Incorrect spacing around column #{token[2].first_column}"
64 |
--------------------------------------------------------------------------------
/src/rules/cyclomatic_complexity.coffee:
--------------------------------------------------------------------------------
1 | module.exports = class CyclomaticComplexity
2 |
3 | rule:
4 | name: 'cyclomatic_complexity'
5 | level: 'ignore'
6 | message: 'The cyclomatic complexity is too damn high'
7 | value: 10
8 | description: '''
9 | Examine the complexity of your function.
10 | '''
11 |
12 | # returns the "complexity" value of the current node.
13 | getComplexity: (node) ->
14 | name = @astApi.getNodeName node
15 | complexity = if name in ['If', 'While', 'For', 'Try']
16 | 1
17 | else if name is 'Op' and node.operator in ['&&', '||']
18 | 1
19 | else if name is 'Switch'
20 | node.cases.length
21 | else
22 | 0
23 | return complexity
24 |
25 | lintAST: (node, @astApi) ->
26 | @lintNode node
27 | undefined
28 |
29 | # Lint the AST node and return its cyclomatic complexity.
30 | lintNode: (node) ->
31 | # Get the complexity of the current node.
32 | name = @astApi?.getNodeName node
33 | complexity = @getComplexity(node)
34 |
35 | # Add the complexity of all child's nodes to this one.
36 | node.eachChild (childNode) =>
37 | childComplexity = @lintNode(childNode)
38 | if @astApi?.getNodeName(childNode) isnt 'Code'
39 | complexity += childComplexity
40 |
41 | rule = @astApi.config[@rule.name]
42 |
43 | # If the current node is a function, and it's over our limit, add an
44 | # error to the list.
45 | if name is 'Code' and complexity >= rule.value
46 | error = @astApi.createError {
47 | context: complexity + 1
48 | lineNumber: node.locationData.first_line + 1
49 | lineNumberEnd: node.locationData.last_line + 1
50 | }
51 | @errors.push error if error
52 |
53 | # Return the complexity for the benefit of parent nodes.
54 | return complexity
55 |
--------------------------------------------------------------------------------
/src/rules/duplicate_key.coffee:
--------------------------------------------------------------------------------
1 | module.exports = class DuplicateKey
2 |
3 | rule:
4 | # I don't know of any legitimate reason to define duplicate keys in an
5 | # object. It seems to always be a mistake, it's also a syntax error in
6 | # strict mode.
7 | # See http://jslinterrors.com/duplicate-key-a/
8 | name: 'duplicate_key'
9 | level: 'error'
10 | message: 'Duplicate key defined in object or class'
11 | description: '''
12 | Prevents defining duplicate keys in object literals and classes
13 | '''
14 |
15 | # TODO: after <1.10.0 is not supported, remove 'IDENTIFIER' here
16 | tokens: ['IDENTIFIER', 'PROPERTY', '{', '}']
17 |
18 | constructor: ->
19 | @braceScopes = [] # A stack tracking keys defined in nexted scopes.
20 |
21 | lintToken: ([type], tokenApi) ->
22 |
23 | if type in ['{', '}']
24 | @lintBrace arguments...
25 | return undefined
26 |
27 | # TODO: after <1.10.0 is not supported, remove 'IDENTIFIER' here
28 | if type in ['IDENTIFIER', 'PROPERTY']
29 | @lintIdentifier arguments...
30 |
31 | lintIdentifier: (token, tokenApi) ->
32 | key = token[1]
33 |
34 | # Class names might not be in a scope
35 | return null if not @currentScope?
36 | nextToken = tokenApi.peek(1)
37 |
38 | # Exit if this identifier isn't being assigned. A and B
39 | # are identifiers, but only A should be examined:
40 | # A = B
41 | return null if nextToken[1] isnt ':'
42 | previousToken = tokenApi.peek(-1)
43 |
44 | # Assigning "@something" and "something" are not the same thing
45 | key = "@#{key}" if previousToken[0] is '@'
46 |
47 | # Added a prefix to not interfere with things like "constructor".
48 | key = "identifier-#{key}"
49 | if @currentScope[key]
50 | return true
51 | else
52 | @currentScope[key] = token
53 | null
54 |
55 | lintBrace: (token) ->
56 | if token[0] is '{'
57 | @braceScopes.push @currentScope if @currentScope?
58 | @currentScope = {}
59 | else
60 | @currentScope = @braceScopes.pop()
61 |
62 | return null
63 |
--------------------------------------------------------------------------------
/src/rules/empty_constructor_needs_parens.coffee:
--------------------------------------------------------------------------------
1 | module.exports = class EmptyConstructorNeedsParens
2 |
3 | rule:
4 | name: 'empty_constructor_needs_parens'
5 | level: 'ignore'
6 | message: 'Invoking a constructor without parens and without arguments'
7 | description: '''
8 | Requires constructors with no parameters to include the parens
9 | '''
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 | peek = tokenApi.peek.bind(tokenApi)
17 | # Find the last chained identifier, e.g. Bar in new foo.bar.Bar().
18 | identIndex = 1
19 | loop
20 | isIdent = peek(identIndex)?[0] in ['IDENTIFIER', 'PROPERTY']
21 | nextToken = peek(identIndex + 1)
22 | if isIdent
23 | if nextToken?[0] is '.'
24 | # skip the dot and start with the next token
25 | identIndex += 2
26 | continue
27 | if nextToken?[0] is 'INDEX_START'
28 | while peek(identIndex)?[0] isnt 'INDEX_END'
29 | identIndex++
30 | continue
31 |
32 | break
33 |
34 | # The callStart is generated if your parameters are all on the same
35 | # line with implicit parens, and if your parameters start on the
36 | # next line, but is missing if there are no params and no parens.
37 | if isIdent and nextToken?
38 | return @handleExpectedCallStart(nextToken)
39 |
40 | handleExpectedCallStart: (isCallStart) ->
41 | if isCallStart[0] isnt 'CALL_START'
42 | return true
43 |
--------------------------------------------------------------------------------
/src/rules/ensure_comprehensions.coffee:
--------------------------------------------------------------------------------
1 | module.exports = class EnsureComprehensions
2 |
3 | rule:
4 | name: 'ensure_comprehensions'
5 | level: 'warn'
6 | message: 'Comprehensions must have parentheses around them'
7 | description: '''
8 | This rule makes sure that parentheses are around comprehensions.
9 | '''
10 |
11 | tokens: ['FOR']
12 |
13 | forBlock: false
14 |
15 | lintToken: (token, tokenApi) ->
16 | # Rules
17 | # Ignore if normal for-loop with a block
18 | # If LHS of operation contains either the key or value variable of
19 | # the loop, assume that it is not a comprehension.
20 |
21 | # Find all identifiers (including lhs values and parts of for loop)
22 | idents = @findIdents(tokenApi)
23 |
24 | # if it looks like a for block, don't bother checking
25 | if @forBlock
26 | @forBlock = false
27 | return
28 |
29 | peeker = -1
30 | atEqual = false
31 | numCallEnds = 0
32 | numCallStarts = 0
33 | numParenStarts = 0
34 | numParenEnds = 0
35 | prevIdents = []
36 |
37 | while (prevToken = tokenApi.peek(peeker))
38 |
39 | numCallEnds++ if prevToken[0] is 'CALL_END'
40 | numCallStarts++ if prevToken[0] is 'CALL_START'
41 |
42 | numParenStarts++ if prevToken[0] is '('
43 | numParenEnds++ if prevToken[0] is ')'
44 |
45 | if prevToken[0] is 'IDENTIFIER'
46 | if not atEqual
47 | prevIdents.push prevToken[1]
48 | else if prevToken[1] in idents
49 | return
50 |
51 | if prevToken[0] in ['(', '->', 'TERMINATOR'] or prevToken.newLine?
52 | break
53 |
54 | if prevToken[0] is '=' and numParenEnds is numParenStarts
55 | atEqual = true
56 |
57 | peeker--
58 |
59 | # If we hit a terminal node (TERMINATOR token or w/ property newLine)
60 | # or if we hit the top of the file and we've seen an '=' sign without
61 | # any identifiers that are part of the for-loop, and there is an equal
62 | # amount of CALL_START/CALL_END tokens. An unequal number means the list
63 | # comprehension is inside of a function call
64 | if atEqual and numCallStarts is numCallEnds
65 | return { context: '' }
66 |
67 | findIdents: (tokenApi) ->
68 | peeker = 1
69 | idents = []
70 |
71 | while (nextToken = tokenApi.peek(peeker))
72 | if nextToken[0] is 'IDENTIFIER'
73 | idents.push(nextToken[1])
74 | if nextToken[0] in ['FORIN', 'FOROF']
75 | break
76 | peeker++
77 |
78 | # now search ahead to see if this becomes a FOR block
79 | while (nextToken = tokenApi.peek(peeker))
80 | if nextToken[0] is 'TERMINATOR'
81 | break
82 | if nextToken[0] is 'INDENT'
83 | @forBlock = true
84 | break
85 | peeker++
86 |
87 | return idents
88 |
--------------------------------------------------------------------------------
/src/rules/eol_last.coffee:
--------------------------------------------------------------------------------
1 | module.exports = class EOLLast
2 |
3 | rule:
4 | name: 'eol_last'
5 | level: 'ignore'
6 | message: 'File does not end with a single newline'
7 | description: '''
8 | Checks that the file ends with a single newline
9 | '''
10 |
11 | lintLine: (line, lineApi) ->
12 | return null unless lineApi.isLastLine()
13 |
14 | isNewline = line.length is 0
15 |
16 | previousIsNewline = if lineApi.lineCount > 1
17 | lineApi.lines[lineApi.lineNumber - 1].length is 0
18 | else
19 | false
20 |
21 | return true unless isNewline and not previousIsNewline
22 |
--------------------------------------------------------------------------------
/src/rules/line_endings.coffee:
--------------------------------------------------------------------------------
1 | module.exports = class LineEndings
2 |
3 | rule:
4 | name: 'line_endings'
5 | level: 'ignore'
6 | value: 'unix' # or 'windows'
7 | message: 'Line contains incorrect line endings'
8 | description: '''
9 | This rule ensures your project uses only windows or
10 | unix line endings. This rule is disabled by default.
11 | '''
12 |
13 | lintLine: (line, lineApi) ->
14 | ending = lineApi.config[@rule.name]?.value
15 |
16 | return null if not ending or lineApi.isLastLine() or not line
17 |
18 | lastChar = line[line.length - 1]
19 | valid = if ending is 'windows'
20 | lastChar is '\r'
21 | else if ending is 'unix'
22 | lastChar isnt '\r'
23 | else
24 | throw new Error("unknown line ending type: #{ending}")
25 | if not valid
26 | return { context: "Expected #{ending}" }
27 | else
28 | return null
29 |
--------------------------------------------------------------------------------
/src/rules/max_line_length.coffee:
--------------------------------------------------------------------------------
1 | regexes =
2 | literateComment: ///
3 | ^
4 | \#\s # This is prefixed on MarkDown lines.
5 | ///
6 | longUrlComment: ///
7 | ^\s*\# # indentation, up to comment
8 | \s*
9 | http[^\s]+$ # Link that takes up the rest of the line without spaces.
10 | ///
11 |
12 | module.exports = class MaxLineLength
13 |
14 | rule:
15 | name: 'max_line_length'
16 | value: 80
17 | level: 'error'
18 | limitComments: true
19 | message: 'Line exceeds maximum allowed length'
20 | description: '''
21 | This rule imposes a maximum line length on your code. Python's style
23 | guide does a good job explaining why you might want to limit the
24 | length of your lines, though this is a matter of taste.
25 |
26 | Lines can be no longer than eighty characters by default.
27 | '''
28 |
29 | lintLine: (line, lineApi) ->
30 | max = lineApi.config[@rule.name]?.value
31 | limitComments = lineApi.config[@rule.name]?.limitComments
32 |
33 | lineLength = line.replace(/\s+$/, '').length
34 | if lineApi.isLiterate() and regexes.literateComment.test(line)
35 | lineLength -= 2
36 |
37 | if max and max < lineLength and not regexes.longUrlComment.test(line)
38 |
39 | unless limitComments
40 | if lineApi.getLineTokens().length is 0
41 | return
42 |
43 | return {
44 | context: "Length is #{lineLength}, max is #{max}"
45 | }
46 |
--------------------------------------------------------------------------------
/src/rules/missing_fat_arrows.coffee:
--------------------------------------------------------------------------------
1 | any = (arr, test) -> arr.reduce ((res, elt) -> res or test elt), false
2 |
3 | containsButIsnt = (node, nIsThis, nIsClass) ->
4 | target = undefined
5 | node.traverseChildren false, (n) ->
6 | if nIsClass n
7 | return false
8 | if nIsThis n
9 | target = n
10 | return false
11 | target
12 |
13 | module.exports = class MissingFatArrows
14 |
15 | rule:
16 | name: 'missing_fat_arrows'
17 | level: 'ignore'
18 | is_strict: false
19 | message: 'Used `this` in a function without a fat arrow'
20 | description: '''
21 | Warns when you use `this` inside a function that wasn't defined
22 | with a fat arrow. This rule does not apply to methods defined in a
23 | class, since they have `this` bound to the class instance (or the
24 | class itself, for class methods). The option `is_strict` is
25 | available for checking bindings of class methods.
26 |
27 | It is impossible to statically determine whether a function using
28 | `this` will be bound with the correct `this` value due to language
29 | features like `Function.prototype.call` and
30 | `Function.prototype.bind`, so this rule may produce false positives.
31 | '''
32 |
33 | lintAST: (node, @astApi) ->
34 | @lintNode node
35 | undefined
36 |
37 | lintNode: (node, methods = []) ->
38 | isStrict = @astApi.config[@rule.name]?.is_strict
39 |
40 | if @isPrototype(node)
41 | return
42 |
43 | if @isConstructor node
44 | return
45 |
46 | if (not @isFatArrowCode node) and
47 | # Ignore any nodes we know to be methods
48 | (if isStrict then true else node not in methods) and
49 | (@needsFatArrow node)
50 | error = @astApi.createError
51 | lineNumber: node.locationData.first_line + 1
52 | @errors.push error
53 |
54 | node.eachChild (child) => @lintNode child,
55 | switch
56 | when @isClass node then @methodsOfClass node
57 | # Once we've hit a function, we know we can't be in the top
58 | # level of a method anymore, so we can safely reset the methods
59 | # to empty to save work.
60 | when @isCode node then []
61 | else methods
62 |
63 | isCode: (node) => @astApi.getNodeName(node) is 'Code'
64 | isClass: (node) => @astApi.getNodeName(node) is 'Class'
65 | isValue: (node) => @astApi.getNodeName(node) is 'Value'
66 | isObject: (node) => @astApi.getNodeName(node) is 'Obj'
67 | isPrototype: (node) ->
68 | props = node?.variable?.properties or []
69 | for ident in props when ident.name?.value is 'prototype'
70 | return true
71 | false
72 | isThis: (node) => @isValue(node) and node.base.value is 'this'
73 | isFatArrowCode: (node) => @isCode(node) and node.bound
74 | isConstructor: (node) -> node.variable?.base?.value is 'constructor'
75 | needsFatArrow: (node) ->
76 | @isCode(node) and (
77 | any(node.params, (param) => param.contains(@isThis)?) or
78 | containsButIsnt(node.body, @isThis, @isClass)
79 | )
80 |
81 | methodsOfClass: (classNode) ->
82 | bodyNodes = classNode.body.expressions
83 | returnNode = bodyNodes[bodyNodes.length - 1]
84 | if returnNode? and @isValue(returnNode) and @isObject(returnNode.base)
85 | returnNode.base.properties
86 | .map((assignNode) -> assignNode.value)
87 | .filter(@isCode)
88 | else []
89 |
--------------------------------------------------------------------------------
/src/rules/newlines_after_classes.coffee:
--------------------------------------------------------------------------------
1 | module.exports = class NewlinesAfterClasses
2 |
3 | rule:
4 | name: 'newlines_after_classes'
5 | value: 3
6 | level: 'ignore'
7 | message: 'Wrong count of newlines between a class and other code'
8 | description: '''
9 | Checks the number of newlines between classes and other code.
10 | 11 | Options: 12 | -value
- The number of required newlines
13 | after class definitions. Defaults to 3.
14 | '''
15 |
16 | tokens: ['CLASS', '}', '{']
17 |
18 | classBracesCount: 0
19 | classCount: 0
20 |
21 | lintToken: (token, tokenApi) ->
22 | [type, numIndents, { first_line: lineNumber }] = token
23 | { lines } = tokenApi
24 |
25 | ending = tokenApi.config[@rule.name].value
26 | if type is 'CLASS'
27 | @classCount++
28 |
29 | if @classCount > 0 and token.generated?
30 | if type is '{' and token.origin?[0] is ':'
31 | @classBracesCount++
32 |
33 | if type is '}' and token.origin?[0] is 'OUTDENT'
34 | @classBracesCount--
35 | @classCount--
36 | if @classCount is 0 and @classBracesCount is 0
37 | befores = 1
38 | afters = 1
39 | comment = 0
40 | outdent = token.origin[2].first_line
41 | start = Math.min(lineNumber, outdent)
42 | trueLine = Infinity
43 |
44 | while (/^\s*(#|$)/.test(lines[start + afters]))
45 | if /^\s*#/.test(lines[start + afters])
46 | comment += 1
47 | else
48 | trueLine = Math.min(trueLine, start + afters)
49 | afters += 1
50 |
51 | while (/^\s*(#|$)/.test(lines[start - befores]))
52 | if /^\s*#/.test(lines[start - befores])
53 | comment += 1
54 | else
55 | trueLine = Math.min(trueLine, start - befores)
56 | befores += 1
57 |
58 | # add up blank lines, subtract comments, subtract 2 because
59 | # before/after counters started at 1.
60 | got = afters + befores - comment - 2
61 |
62 | # if `got` and `ending` don't match throw an error _unless_
63 | # we are at the end of the file.
64 | if got isnt ending and trueLine + ending <= lines.length
65 | return {
66 | context: "Expected #{ending} got #{got}"
67 | lineNumber: trueLine
68 | }
69 |
--------------------------------------------------------------------------------
/src/rules/no_backticks.coffee:
--------------------------------------------------------------------------------
1 | module.exports = class NoBackticks
2 |
3 | rule:
4 | name: 'no_backticks'
5 | level: 'error'
6 | message: 'Backticks are forbidden'
7 | description: '''
8 | Backticks allow snippets of JavaScript to be embedded in
9 | CoffeeScript. While some folks consider backticks useful in a few
10 | niche circumstances, they should be avoided because so none of
11 | JavaScript's "bad parts", like with and eval,
12 | sneak into CoffeeScript.
13 | This rule is enabled by default.
14 | '''
15 |
16 | tokens: ['JS']
17 |
18 | lintToken: (token, tokenApi) ->
19 | return not token.comments?
20 |
--------------------------------------------------------------------------------
/src/rules/no_debugger.coffee:
--------------------------------------------------------------------------------
1 | module.exports = class NoDebugger
2 |
3 | rule:
4 | name: 'no_debugger'
5 | level: 'warn'
6 | message: 'Found debugging code'
7 | console: false
8 | description: '''
9 | This rule detects `debugger` and optionally `console` calls
10 | This rule is `warn` by default.
11 | '''
12 |
13 | # TODO: after <1.10.0 is not supported, remove 'DEBUGGER' here
14 | tokens: ['STATEMENT', 'DEBUGGER', 'IDENTIFIER']
15 |
16 | lintToken: (token, tokenApi) ->
17 | if token[0] in ['DEBUGGER', 'STATEMENT'] and token[1] is 'debugger'
18 | return { context: "found '#{token[0]}'" }
19 |
20 | if tokenApi.config[@rule.name]?.console
21 | if token[1] is 'console' and tokenApi.peek(1)?[0] is '.'
22 | method = tokenApi.peek(2)
23 | return { context: "found 'console.#{method[1]}'" }
24 |
--------------------------------------------------------------------------------
/src/rules/no_empty_functions.coffee:
--------------------------------------------------------------------------------
1 | isEmptyCode = (node, astApi) ->
2 | nodeName = astApi.getNodeName node
3 | nodeName is 'Code' and node.body.isEmpty()
4 |
5 | module.exports = class NoEmptyFunctions
6 |
7 | rule:
8 | name: 'no_empty_functions'
9 | level: 'ignore'
10 | message: 'Empty function'
11 | description: '''
12 | Disallows declaring empty functions. The goal of this rule is that
13 | unintentional empty callbacks can be detected:
14 |
15 | someFunctionWithCallback ->
16 | doSomethingSignificant()
17 |
18 |
19 | The problem is that the call to
20 | doSomethingSignificant will be made regardless
21 | of someFunctionWithCallback's execution. It can
22 | be because you did not indent the call to
23 | doSomethingSignificant properly.
24 |
25 | If you really meant that someFunctionWithCallback
26 | should call a callback that does nothing, you can write your code
27 | this way:
28 |
29 | someFunctionWithCallback ->
30 | undefined
31 | doSomethingSignificant()
32 |
33 |
34 | '''
35 |
36 | lintAST: (node, astApi) ->
37 | @lintNode node, astApi
38 | undefined
39 |
40 | lintNode: (node, astApi) ->
41 | if isEmptyCode node, astApi
42 | error = astApi.createError
43 | lineNumber: node.locationData.first_line + 1
44 | @errors.push error
45 | node.eachChild (child) => @lintNode child, astApi
46 |
--------------------------------------------------------------------------------
/src/rules/no_empty_param_list.coffee:
--------------------------------------------------------------------------------
1 | module.exports = class NoEmptyParamList
2 |
3 | rule:
4 | name: 'no_empty_param_list'
5 | level: 'ignore'
6 | message: 'Empty parameter list is forbidden'
7 | description: '''
8 | This rule prohibits empty parameter lists in function definitions.
9 |
10 | # The empty parameter list in here is unnecessary:
11 | myFunction = () ->
12 |
13 | # We might favor this instead:
14 | myFunction = ->
15 |
16 |
17 | Empty parameter lists are permitted by default.
18 | '''
19 |
20 | tokens: ['PARAM_START']
21 |
22 | lintToken: (token, tokenApi) ->
23 | nextType = tokenApi.peek()[0]
24 | return nextType is 'PARAM_END'
25 |
--------------------------------------------------------------------------------
/src/rules/no_implicit_braces.coffee:
--------------------------------------------------------------------------------
1 | module.exports = class NoImplicitBraces
2 |
3 | rule:
4 | name: 'no_implicit_braces'
5 | level: 'ignore'
6 | message: 'Implicit braces are forbidden'
7 | strict: true
8 | description: '''
9 | This rule prohibits implicit braces when declaring object literals.
10 | Implicit braces can make code more difficult to understand,
11 | especially when used in combination with optional parenthesis.
12 |
13 | # Do you find this code ambiguous? Is it a
14 | # function call with three arguments or four?
15 | myFunction a, b, 1:2, 3:4
16 |
17 | # While the same code written in a more
18 | # explicit manner has no ambiguity.
19 | myFunction(a, b, {1:2, 3:4})
20 |
21 |
22 | Implicit braces are permitted by default, since their use is
23 | idiomatic CoffeeScript.
24 | '''
25 |
26 | tokens: [
27 | '{', 'OUTDENT', 'INDENT', 'CLASS',
28 | 'IDENTIFIER', 'PROPERTY', 'EXTENDS'
29 | ]
30 | dent: 0
31 |
32 | constructor: ->
33 | @isClass = false
34 | @className = ''
35 |
36 | lintToken: (token, tokenApi) ->
37 | [type, val, lineNum] = token
38 | if type in ['OUTDENT', 'INDENT', 'CLASS']
39 | return @trackClass arguments...
40 |
41 | # reset "className" if class uses EXTENDS keyword
42 | if type is 'EXTENDS'
43 | @className = ''
44 | return
45 |
46 | # If we're looking at an IDENTIFIER, and we're in a class, and we've not
47 | # set a className (or the previous non-identifier was 'EXTENDS', set the
48 | # current identifier as the class name)
49 | if type in ['IDENTIFIER', 'PROPERTY'] and @isClass and @className is ''
50 | # Backtrack to get the full classname
51 | c = 0
52 | while tokenApi.peek(c)[0] in ['IDENTIFIER', 'PROPERTY', '.']
53 | @className += tokenApi.peek(c)[1]
54 | c++
55 |
56 | if token.generated and type is '{'
57 | # If strict mode is set to false it allows implicit braces when the
58 | # object is declared over multiple lines.
59 | unless tokenApi.config[@rule.name].strict
60 | [prevToken] = tokenApi.peek(-1)
61 | if prevToken in ['INDENT', 'TERMINATOR']
62 | return
63 |
64 | if @isClass
65 | # The way CoffeeScript generates tokens for classes
66 | # is a bit weird. It generates '{' tokens around instance
67 | # methods (also known as the prototypes of an Object).
68 |
69 | [prevToken] = tokenApi.peek(-1)
70 | # If there is a TERMINATOR token right before the '{' token
71 | if prevToken is 'TERMINATOR'
72 | return
73 |
74 | peekIdent = ''
75 | c = -2
76 | # Go back until you grab all the tokens with IDENTIFIER,
77 | # PROPERTY or '.'
78 | while ([_type, _val] = tokenApi.peek(c))
79 | break if _type not in ['IDENTIFIER', 'PROPERTY', '.']
80 | peekIdent = _val + peekIdent
81 | c--
82 |
83 | if peekIdent is @className
84 | return
85 |
86 | return true
87 |
88 | trackClass: (token, tokenApi) ->
89 |
90 | [[n0, ..., ln], [n1, ...]] = [token, tokenApi.peek()]
91 |
92 | @dent++ if n0 is 'INDENT'
93 | @dent-- if n0 is 'OUTDENT'
94 |
95 | if @dent is 0 and n0 is 'OUTDENT' and n1 is 'TERMINATOR'
96 | @isClass = false
97 | if n0 is 'CLASS'
98 | @isClass = true
99 | @className = ''
100 | return null
101 |
--------------------------------------------------------------------------------
/src/rules/no_implicit_parens.coffee:
--------------------------------------------------------------------------------
1 | module.exports = class NoImplicitParens
2 |
3 | rule:
4 | name: 'no_implicit_parens'
5 | level: 'ignore'
6 | message: 'Implicit parens are forbidden'
7 | strict: true
8 | description: '''
9 | This rule prohibits implicit parens on function calls.
10 |
11 | # Some folks don't like this style of coding.
12 | myFunction a, b, c
13 |
14 | # And would rather it always be written like this:
15 | myFunction(a, b, c)
16 |
17 |
18 | Implicit parens are permitted by default, since their use is
19 | idiomatic CoffeeScript.
20 | '''
21 |
22 |
23 | tokens: ['CALL_END']
24 |
25 | lintToken: (token, tokenApi) ->
26 | if token.generated
27 | unless tokenApi.config[@rule.name].strict is false
28 | return true
29 | else
30 | # If strict mode is turned off it allows implicit parens when
31 | # the expression is spread over multiple lines.
32 | i = -1
33 | loop
34 | t = tokenApi.peek(i)
35 | sameLine = t[2].first_line is token[2].first_line
36 | genCallStart = t[0] is 'CALL_START' and t.generated
37 |
38 | if not t? or genCallStart and sameLine
39 | return true
40 |
41 | # If we have not found a CALL_START token that is generated,
42 | # and we've moved into a new line, this is fine and should
43 | # just return.
44 | if not sameLine
45 | return null
46 |
47 | i -= 1
48 |
--------------------------------------------------------------------------------
/src/rules/no_interpolation_in_single_quotes.coffee:
--------------------------------------------------------------------------------
1 | module.exports = class NoInterpolationInSingleQuotes
2 |
3 | rule:
4 | name: 'no_interpolation_in_single_quotes'
5 | level: 'ignore'
6 | message: 'Interpolation in single quoted strings is forbidden'
7 | description: '''
8 | This rule prohibits string interpolation in a single quoted string.
9 |
10 | # String interpolation in single quotes is not allowed:
11 | foo = '#{bar}'
12 |
13 | # Double quotes is OK of course
14 | foo = "#{bar}"
15 |
16 |
17 | String interpolation in single quoted strings is permitted by
18 | default.
19 | '''
20 |
21 | tokens: ['STRING']
22 |
23 | lintToken: (token, tokenApi) ->
24 | tokenValue = token[1]
25 | hasInterpolation = tokenValue.match(/^\'.*#\{[^}]+\}.*\'$/)
26 | return hasInterpolation
27 |
--------------------------------------------------------------------------------
/src/rules/no_nested_string_interpolation.coffee:
--------------------------------------------------------------------------------
1 | module.exports = class NoNestedStringInterpolation
2 |
3 | rule:
4 | name: 'no_nested_string_interpolation'
5 | level: 'warn'
6 | message: 'Nested string interpolation is forbidden'
7 | description: '''
8 | This rule warns about nested string interpolation,
9 | as it tends to make code harder to read and understand.
10 |
11 | # Good!
12 | str = "Book by #{firstName.toUpperCase()} #{lastName.toUpperCase()}"
13 |
14 | # Bad!
15 | str = "Book by #{"#{firstName} #{lastName}".toUpperCase()}"
16 |
17 |
18 | '''
19 |
20 | tokens: ['STRING_START', 'STRING_END']
21 |
22 | constructor: ->
23 | @startedStrings = 0
24 | @generatedError = false
25 |
26 | lintToken: ([type], tokenApi) ->
27 | if type is 'STRING_START'
28 | @trackStringStart()
29 | else
30 | @trackStringEnd()
31 |
32 | trackStringStart: ->
33 | @startedStrings += 1
34 |
35 | # Don't generate multiple errors for deeply nested string interpolation
36 | return if @startedStrings <= 1 or @generatedError
37 |
38 | @generatedError = true
39 | return true
40 |
41 | trackStringEnd: ->
42 | @startedStrings -= 1
43 | @generatedError = false if @startedStrings is 1
44 |
--------------------------------------------------------------------------------
/src/rules/no_plusplus.coffee:
--------------------------------------------------------------------------------
1 | module.exports = class NoPlusPlus
2 |
3 | rule:
4 | name: 'no_plusplus'
5 | level: 'ignore'
6 | message: 'The increment and decrement operators are forbidden'
7 | description: '''
8 | This rule forbids the increment and decrement arithmetic operators.
9 | Some people believe the ++ and -- to be cryptic
10 | and the cause of bugs due to misunderstandings of their precedence
11 | rules.
12 | This rule is disabled by default.
13 | '''
14 |
15 | tokens: ['++', '--']
16 |
17 | lintToken: (token, tokenApi) ->
18 | return { context: "found '#{token[0]}'" }
19 |
--------------------------------------------------------------------------------
/src/rules/no_private_function_fat_arrows.coffee:
--------------------------------------------------------------------------------
1 | module.exports = class NoPrivateFunctionFatArrows
2 |
3 | rule:
4 | name: 'no_private_function_fat_arrows'
5 | level: 'warn'
6 | message: 'Used the fat arrow for a private function'
7 | description: '''
8 | Warns when you use the fat arrow for a private function
9 | inside a class definition scope. It is not necessary and
10 | it does not do anything.
11 | '''
12 |
13 | lintAST: (node, @astApi) ->
14 | @lintNode node
15 | undefined
16 |
17 | lintNode: (node, functions = []) ->
18 | if @isFatArrowCode(node) and node in functions
19 | error = @astApi.createError
20 | lineNumber: node.locationData.first_line + 1
21 | @errors.push error
22 |
23 | node.eachChild (child) => @lintNode child,
24 | switch
25 | when @isClass node then @functionsOfClass node
26 | # Once we've hit a function, we know we can't be in the top
27 | # level of a function anymore, so we can safely reset the
28 | # functions to empty to save work.
29 | when @isCode node then []
30 | else functions
31 |
32 | isCode: (node) => @astApi.getNodeName(node) is 'Code'
33 | isClass: (node) => @astApi.getNodeName(node) is 'Class'
34 | isValue: (node) => @astApi.getNodeName(node) is 'Value'
35 | isObject: (node) => @astApi.getNodeName(node) is 'Obj'
36 | isFatArrowCode: (node) => @isCode(node) and node.bound
37 |
38 | functionsOfClass: (classNode) ->
39 | bodyValues = for bodyNode in classNode.body.expressions
40 | continue if @isValue(bodyNode) and @isObject(bodyNode.base)
41 |
42 | bodyNode.value
43 | bodyValues.filter(@isCode)
44 |
--------------------------------------------------------------------------------
/src/rules/no_stand_alone_at.coffee:
--------------------------------------------------------------------------------
1 | module.exports = class NoStandAloneAt
2 |
3 | rule:
4 | name: 'no_stand_alone_at'
5 | level: 'ignore'
6 | message: '@ must not be used stand alone'
7 | description: '''
8 | This rule checks that no stand alone @ are in use, they are
9 | discouraged. Further information in CoffeeScript issue
11 | #1601
12 | '''
13 |
14 | tokens: ['@']
15 |
16 | lintToken: (token, tokenApi) ->
17 | [nextToken] = tokenApi.peek()
18 | noSpace = not token.spaced
19 | # TODO: after <1.10.0 is not supported, remove 'IDENTIFIER' here
20 | isProp = nextToken in ['IDENTIFIER', 'PROPERTY']
21 | isAStart = nextToken in ['INDEX_START', 'CALL_START'] # @[] or @()
22 | isDot = nextToken is '.'
23 |
24 | # https://github.com/jashkenas/coffee-script/issues/1601
25 | # @::foo is valid, but @:: behaves inconsistently and is planned for
26 | # removal. Technically @:: is a stand alone ::, but I think it makes
27 | # sense to group it into no_stand_alone_at
28 | #
29 | # TODO: after v1.10.0 is not supported, remove 'IDENTIFIER' here
30 | isProtoProp = nextToken is '::' and
31 | tokenApi.peek(2)?[0] in ['IDENTIFIER', 'PROPERTY']
32 |
33 | # Return an error after an '@' token unless:
34 | # 1: there is a '.' afterwards (isDot)
35 | # 2: there isn't a space after the '@' and the token following the '@'
36 | # is an property, the start of an index '[' or is an property after
37 | # the '::'
38 | unless (isDot or (noSpace and (isProp or isAStart or isProtoProp)))
39 | return true
40 |
--------------------------------------------------------------------------------
/src/rules/no_tabs.coffee:
--------------------------------------------------------------------------------
1 | indentationRegex = /\S/
2 |
3 | module.exports = class NoTabs
4 |
5 | rule:
6 | name: 'no_tabs'
7 | level: 'error'
8 | message: 'Line contains tab indentation'
9 | description: '''
10 | This rule forbids tabs in indentation. Enough said. It is enabled by
11 | default.
12 | '''
13 |
14 | lintLine: (line, lineApi) ->
15 | # Only check lines that have compiled tokens. This helps
16 | # us ignore tabs in the middle of multi line strings, heredocs, etc.
17 | # since they are all reduced to a single token whose line number
18 | # is the start of the expression.
19 | indentation = line.split(indentationRegex)[0]
20 | if lineApi.lineHasToken() and '\t' in indentation
21 | true
22 | else
23 | null
24 |
--------------------------------------------------------------------------------
/src/rules/no_this.coffee:
--------------------------------------------------------------------------------
1 | module.exports = class NoThis
2 |
3 | rule:
4 | name: 'no_this'
5 | level: 'ignore'
6 | message: "Don't use 'this', use '@' instead"
7 | description: '''
8 | This rule prohibits 'this'.
9 | Use '@' instead.
10 | '''
11 |
12 | tokens: ['THIS']
13 |
14 | lintToken: (token, tokenApi) ->
15 | { config: { no_stand_alone_at: { level } } } = tokenApi
16 | nextToken = tokenApi.peek(1)?[0]
17 |
18 | true unless level isnt 'ignore' and nextToken isnt '.'
19 |
--------------------------------------------------------------------------------
/src/rules/no_throwing_strings.coffee:
--------------------------------------------------------------------------------
1 | module.exports = class NoThrowingStrings
2 |
3 | rule:
4 | name: 'no_throwing_strings'
5 | level: 'error'
6 | message: 'Throwing strings is forbidden'
7 | description: '''
8 | This rule forbids throwing string literals or interpolations. While
9 | JavaScript (and CoffeeScript by extension) allow any expression to
10 | be thrown, it is best to only throw Error objects,
13 | because they contain valuable debugging information like the stack
14 | trace. Because of JavaScript's dynamic nature, CoffeeLint cannot
15 | ensure you are always throwing instances of Error. It will
16 | only catch the simple but real case of throwing literal strings.
17 |
18 | # CoffeeLint will catch this:
19 | throw "i made a boo boo"
20 |
21 | # ... but not this:
22 | throw getSomeString()
23 |
24 |
25 | This rule is enabled by default.
26 | '''
27 |
28 | tokens: ['THROW']
29 |
30 | lintToken: (token, tokenApi) ->
31 | [n1, ...] = tokenApi.peek()
32 | # Catch literals and string interpolations, which are wrapped in parens.
33 |
34 | nextIsString = n1 is 'STRING' or n1 is 'STRING_START'
35 |
36 | return nextIsString
37 |
--------------------------------------------------------------------------------
/src/rules/no_trailing_semicolons.coffee:
--------------------------------------------------------------------------------
1 | regexes =
2 | trailingSemicolon: /;\r?$/
3 |
4 | module.exports = class NoTrailingSemicolons
5 |
6 | rule:
7 | name: 'no_trailing_semicolons'
8 | level: 'error'
9 | message: 'Line contains a trailing semicolon'
10 | description: '''
11 | This rule prohibits trailing semicolons, since they are needless
12 | cruft in CoffeeScript.
13 |
14 | # This semicolon is meaningful.
15 | x = '1234'; console.log(x)
16 |
17 | # This semicolon is redundant.
18 | alert('end of line');
19 |
20 |
21 | Trailing semicolons are forbidden by default.
22 | '''
23 |
24 | lintLine: (line, lineApi) ->
25 |
26 | # The TERMINATOR token is extended through to the next token. As a
27 | # result a line with a comment DOES have a token: the TERMINATOR from
28 | # the last line of code.
29 | lineTokens = lineApi.getLineTokens()
30 | tokenLen = lineTokens.length
31 | stopTokens = ['TERMINATOR', 'HERECOMMENT']
32 |
33 | if tokenLen is 1 and lineTokens[0][0] in stopTokens
34 | return
35 |
36 | newLine = line
37 | if tokenLen > 1 and lineTokens[tokenLen - 1][0] is 'TERMINATOR'
38 |
39 | # `startPos` contains the end pos of the last non-TERMINATOR token
40 | # `endPos` contains the start position of the TERMINATOR token
41 |
42 | # if startPos and endPos arent equal, that probably means a comment
43 | # was sliced out of the tokenizer
44 |
45 | startPos = lineTokens[tokenLen - 2][2].last_column + 1
46 | endPos = lineTokens[tokenLen - 1][2].first_column
47 | if (startPos isnt endPos)
48 | startCounter = startPos
49 | while line[startCounter] isnt '#' and startCounter < line.length
50 | startCounter++
51 | newLine = line.substring(0, startCounter).replace(/\s*$/, '')
52 |
53 | hasSemicolon = regexes.trailingSemicolon.test(newLine)
54 | [first..., last] = lineTokens
55 | hasNewLine = last and last.newLine?
56 | # Don't throw errors when the contents of multiline strings,
57 | # regexes and the like end in ";"
58 | if hasSemicolon and not hasNewLine and lineApi.lineHasToken() and
59 | not (last[0] in ['STRING', 'IDENTIFIER', 'STRING_END'])
60 | return true
61 |
--------------------------------------------------------------------------------
/src/rules/no_trailing_whitespace.coffee:
--------------------------------------------------------------------------------
1 | regexes =
2 | trailingWhitespace: /[^\s]+[\t ]+\r?$/
3 | onlySpaces: /^[\t ]+\r?$/
4 | lineHasComment: /^\s*[^\#]*\#/
5 |
6 | module.exports = class NoTrailingWhitespace
7 |
8 | rule:
9 | name: 'no_trailing_whitespace'
10 | level: 'error'
11 | message: 'Line ends with trailing whitespace'
12 | allowed_in_comments: false
13 | allowed_in_empty_lines: true
14 | description: '''
15 | This rule forbids trailing whitespace in your code, since it is
16 | needless cruft. It is enabled by default.
17 | '''
18 |
19 | lintLine: (line, lineApi) ->
20 | unless lineApi.config['no_trailing_whitespace']?.allowed_in_empty_lines
21 | if regexes.onlySpaces.test(line)
22 | return true
23 |
24 | if regexes.trailingWhitespace.test(line)
25 | # By default only the regex above is needed.
26 | unless lineApi.config['no_trailing_whitespace']?.allowed_in_comments
27 | return true
28 |
29 | line = line
30 | tokens = lineApi.tokensByLine[lineApi.lineNumber]
31 |
32 | # If we're in a block comment there won't be any tokens on this
33 | # line. Some previous line holds the token spanning multiple lines.
34 | if !tokens
35 | return null
36 |
37 | # To avoid confusion when a string might contain a "#", every string
38 | # on this line will be removed. before checking for a comment
39 | for str in (token[1] for token in tokens when token[0] is 'STRING')
40 | line = line.replace(str, 'STRING')
41 |
42 | if !regexes.lineHasComment.test(line)
43 | return true
44 |
--------------------------------------------------------------------------------
/src/rules/no_unnecessary_double_quotes.coffee:
--------------------------------------------------------------------------------
1 | module.exports = class NoUnnecessaryDoubleQuotes
2 |
3 | rule:
4 | name: 'no_unnecessary_double_quotes'
5 | level: 'ignore'
6 | message: 'Unnecessary double quotes are forbidden'
7 | description: '''
8 | This rule prohibits double quotes unless string interpolation is
9 | used or the string contains single quotes.
10 |
11 | # Double quotes are discouraged:
12 | foo = "bar"
13 |
14 | # Unless string interpolation is used:
15 | foo = "#{bar}baz"
16 |
17 | # Or they prevent cumbersome escaping:
18 | foo = "I'm just following the 'rules'"
19 |
20 |
21 | Double quotes are permitted by default.
22 | '''
23 |
24 | constructor: ->
25 | @regexps = []
26 | @interpolationLevel = 0
27 |
28 | tokens: ['STRING', 'STRING_START', 'STRING_END']
29 |
30 | lintToken: (token, tokenApi) ->
31 | [type, tokenValue] = token
32 |
33 | if type in ['STRING_START', 'STRING_END']
34 | return @trackParens arguments...
35 |
36 | stringValue = tokenValue.match(/^\"(.*)\"$/)
37 |
38 | return false unless stringValue # no double quotes, all OK
39 |
40 | # When CoffeeScript generates calls to RegExp it double quotes the 2nd
41 | # parameter. Using peek(2) becuase the peek(1) would be a CALL_END
42 | if tokenApi.peek(2)?[0] is 'REGEX_END'
43 | return false
44 |
45 | hasLegalConstructs = @isInInterpolation() or @hasSingleQuote(tokenValue)
46 | return not hasLegalConstructs
47 |
48 | isInInterpolation: () ->
49 | @interpolationLevel > 0
50 |
51 | trackParens: (token, tokenApi) ->
52 | if token[0] is 'STRING_START'
53 | @interpolationLevel += 1
54 | else if token[0] is 'STRING_END'
55 | @interpolationLevel -= 1
56 | # We're not linting, just tracking interpolations.
57 | null
58 |
59 | hasSingleQuote: (tokenValue) ->
60 | return tokenValue.indexOf("'") isnt -1
61 |
--------------------------------------------------------------------------------
/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 | node.constructor?.name is 'ThisLiteral' or
31 | @isValue(node) and node.base.value is 'this'
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 | unless @astApi.getNodeName(child)
39 | child.constructor?.name is 'SuperCall' or
40 | (child.isSuper? and child.isSuper)
41 | # TODO: after <1.10.0 is not supported, remove child.isSuper
42 | else
43 | @isFatArrowCode(child) and @needsFatArrow(child))?
44 | )
45 |
--------------------------------------------------------------------------------
/src/rules/non_empty_constructor_needs_parens.coffee:
--------------------------------------------------------------------------------
1 | ParentClass = require './empty_constructor_needs_parens.coffee'
2 |
3 | module.exports = class NonEmptyConstructorNeedsParens extends ParentClass
4 |
5 | rule:
6 | name: 'non_empty_constructor_needs_parens'
7 | level: 'ignore'
8 | message: 'Invoking a constructor without parens and with arguments'
9 | description: '''
10 | Requires constructors with parameters to include the parens
11 | '''
12 |
13 | handleExpectedCallStart: (isCallStart) ->
14 | if isCallStart[0] is 'CALL_START' and isCallStart.generated
15 | return true
16 |
--------------------------------------------------------------------------------
/src/rules/prefer_english_operator.coffee:
--------------------------------------------------------------------------------
1 | module.exports = class PreferEnglishOperator
2 |
3 | rule:
4 | name: 'prefer_english_operator'
5 | level: 'ignore'
6 | message: 'Don\'t use &&, ||, ==, !=, or !'
7 | doubleNotLevel: 'ignore'
8 | description: '''
9 | This rule prohibits &&, ||, ==, != and !.
10 | Use and, or, is, isnt, and not instead.
11 | !! for converting to a boolean is ignored.
12 | '''
13 |
14 | tokens: ['COMPARE', 'UNARY_MATH', '&&', '||']
15 |
16 | lintToken: (token, tokenApi) ->
17 | config = tokenApi.config[@rule.name]
18 | level = config.level
19 | # Compare the actual token with the lexed token.
20 | { first_column, last_column } = token[2]
21 | line = tokenApi.lines[tokenApi.lineNumber]
22 | actual_token = line[first_column..last_column]
23 | context =
24 | switch actual_token
25 | when '==' then 'Replace "==" with "is"'
26 | when '!=' then 'Replace "!=" with "isnt"'
27 | when '||' then 'Replace "||" with "or"'
28 | when '&&' then 'Replace "&&" with "and"'
29 | when '!'
30 | # `not not expression` seems awkward, so `!!expression`
31 | # gets special handling.
32 | if tokenApi.peek(1)?[0] is 'UNARY_MATH'
33 | level = config.doubleNotLevel
34 | '"?" is usually better than "!!"'
35 | else if tokenApi.peek(-1)?[0] is 'UNARY_MATH'
36 | # Ignore the 2nd half of the double not
37 | undefined
38 | else
39 | 'Replace "!" with "not"'
40 | else undefined
41 |
42 | if context?
43 | { level, context }
44 |
--------------------------------------------------------------------------------
/src/rules/space_operators.coffee:
--------------------------------------------------------------------------------
1 | module.exports = class SpaceOperators
2 |
3 | rule:
4 | name: 'space_operators'
5 | level: 'ignore'
6 | message: 'Operators must be spaced properly'
7 | description: '''
8 | This rule enforces that operators have spaces around them.
9 | '''
10 |
11 | tokens: ['+', '-', '=', '**', 'MATH', 'COMPARE',
12 | '&', '^', '|', '&&', '||', 'COMPOUND_ASSIGN',
13 | 'STRING_START', 'STRING_END', 'CALL_START', 'CALL_END'
14 | ]
15 |
16 | constructor: ->
17 | @callTokens = [] # A stack tracking the call token pairs.
18 | @parenTokens = [] # A stack tracking the parens token pairs.
19 | @interpolationLevel = 0
20 |
21 | lintToken: (token, tokenApi) ->
22 | [type, rest...] = token
23 | # These just keep track of state
24 | if type in ['CALL_START', 'CALL_END']
25 | @trackCall token, tokenApi
26 | return
27 |
28 | if type in ['STRING_START', 'STRING_END']
29 | return @trackParens token, tokenApi
30 |
31 | # These may return errors
32 | if type in ['+', '-']
33 | @lintPlus token, tokenApi
34 | else
35 | @lintMath token, tokenApi
36 |
37 | lintPlus: (token, tokenApi) ->
38 | # We can't check this inside of interpolations right now, because the
39 | # plusses used for the string type co-ercion are marked not spaced.
40 | if @isInInterpolation() or @isInExtendedRegex()
41 | return null
42 |
43 | p = tokenApi.peek(-1)
44 |
45 | unaries = ['TERMINATOR', '(', '=', '-', '+', ',', 'CALL_START',
46 | 'INDEX_START', '..', '...', 'COMPARE', 'IF', 'THROW',
47 | '&', '^', '|', '&&', '||', 'POST_IF', ':', '[', 'INDENT',
48 | 'COMPOUND_ASSIGN', 'RETURN', 'MATH', 'BY', 'LEADING_WHEN']
49 |
50 | isUnary = if not p then false else p[0] in unaries
51 | notFirstToken = (p or token.spaced? or token.newLine)
52 | if notFirstToken and ((isUnary and token.spaced?) or
53 | (not isUnary and not token.newLine and
54 | (not token.spaced or (p and not p.spaced))))
55 | return { context: token[1] }
56 | else
57 | null
58 |
59 | lintMath: (token, tokenApi) ->
60 | p = tokenApi.peek(-1)
61 | if not token.newLine and (not token.spaced or (p and not p.spaced))
62 | return { context: token[1] }
63 | else
64 | null
65 |
66 | isInExtendedRegex: () ->
67 | for t in @callTokens
68 | return true if t.isRegex
69 | return false
70 |
71 | isInInterpolation: () ->
72 | @interpolationLevel > 0
73 |
74 | trackCall: (token, tokenApi) ->
75 | if token[0] is 'CALL_START'
76 | p = tokenApi.peek(-1)
77 | # Track regex calls, to know (approximately) if we're in an
78 | # extended regex.
79 | token.isRegex = p and p[0] is 'IDENTIFIER' and p[1] is 'RegExp'
80 | @callTokens.push(token)
81 | else
82 | @callTokens.pop()
83 | return null
84 |
85 | trackParens: (token, tokenApi) ->
86 | if token[0] is 'STRING_START'
87 | @interpolationLevel += 1
88 | else if token[0] is 'STRING_END'
89 | @interpolationLevel -= 1
90 | # We're not linting, just tracking interpolations.
91 | null
92 |
--------------------------------------------------------------------------------
/src/rules/spacing_after_comma.coffee:
--------------------------------------------------------------------------------
1 | module.exports = class SpacingAfterComma
2 | rule:
3 | name: 'spacing_after_comma'
4 | level: 'ignore'
5 | message: 'a space is required after commas'
6 | description: '''
7 | This rule checks to make sure you have a space after commas.
8 | '''
9 |
10 | tokens: [',', 'REGEX_START', 'REGEX_END']
11 |
12 | constructor: ->
13 | @inRegex = false
14 |
15 | lintToken: (token, tokenApi) ->
16 | [type] = token
17 |
18 | if type is 'REGEX_START'
19 | @inRegex = true
20 | return
21 | if type is 'REGEX_END'
22 | @inRegex = false
23 | return
24 |
25 | unless token.spaced or token.newLine or token.generated or
26 | @isRegexFlag(token, tokenApi)
27 | return true
28 |
29 | # When generating a regex (///${whatever}///i) CoffeeScript generates tokens
30 | # for RegEx(whatever, "i") but doesn't bother to mark that comma as
31 | # generated or spaced. Looking 3 tokens ahead skips the STRING and CALL_END
32 | isRegexFlag: (token, tokenApi) ->
33 | return false unless @inRegex
34 |
35 | maybeEnd = tokenApi.peek(3)
36 | return maybeEnd?[0] is 'REGEX_END'
37 |
--------------------------------------------------------------------------------
/src/rules/transform_messes_up_line_numbers.coffee:
--------------------------------------------------------------------------------
1 | module.exports = class TransformMessesUpLineNumbers
2 |
3 | rule:
4 | name: 'transform_messes_up_line_numbers'
5 | level: 'warn'
6 | message: 'Transforming source messes up line numbers'
7 | description: '''
8 | This rule detects when changes are made by transform function,
9 | and warns that line numbers are probably incorrect.
10 | '''
11 |
12 | tokens: []
13 |
14 | lintToken: (token, tokenApi) ->
15 | # implemented before the tokens are created, using the entire source.
16 |
--------------------------------------------------------------------------------
/test/fixtures/clean.coffee:
--------------------------------------------------------------------------------
1 | ###
2 | A lint free script.
3 | ###
4 |
5 | x = () ->
6 | return 1234 + 4567
7 |
--------------------------------------------------------------------------------
/test/fixtures/cloud_transform.coffee:
--------------------------------------------------------------------------------
1 | module.exports = (source) ->
2 | return source.replace('cloud', 'butt')
3 |
--------------------------------------------------------------------------------
/test/fixtures/coffeelint.json:
--------------------------------------------------------------------------------
1 | {
2 | "_comment": "Force all tests under test/ to use this default config unless explicitly requested"
3 | }
4 |
--------------------------------------------------------------------------------
/test/fixtures/custom_extention/clean.csbx:
--------------------------------------------------------------------------------
1 | ###
2 | A lint free script.
3 | ###
4 |
5 | x = () ->
6 | return 1234 + 4567
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/custom_rules/voldemort.coffee:
--------------------------------------------------------------------------------
1 | class Voldemort
2 |
3 | avadaKadavra: (enemy) ->
4 | enemy.die()
5 |
6 | generateHorcruxes: (scrifices = []) ->
7 | voldemort = []
8 | for s in scrifices
9 | voldemort.push new Horcrux @avadaKadavra(s)
10 |
11 | return voldemort
12 |
13 |
--------------------------------------------------------------------------------
/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/find_extended_test/coffeelint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends" : "coffeelint-extends-test"
3 | }
4 |
--------------------------------------------------------------------------------
/test/fixtures/find_extended_test/invalid.coffee:
--------------------------------------------------------------------------------
1 | a = ( ->
2 |
--------------------------------------------------------------------------------
/test/fixtures/findconfigtest/coffeelint.json:
--------------------------------------------------------------------------------
1 | {
2 | "indentation" : {
3 | "level" : "error",
4 | "value" : 7
5 | }
6 | }
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 |
--------------------------------------------------------------------------------
/test/fixtures/findconfigtest/package/sixspaces.coffee:
--------------------------------------------------------------------------------
1 | f = ->
2 | null
3 |
--------------------------------------------------------------------------------
/test/fixtures/findconfigtest/sevenspaces.coffee:
--------------------------------------------------------------------------------
1 | f = ->
2 | null
3 |
--------------------------------------------------------------------------------
/test/fixtures/fourspaces.coffee:
--------------------------------------------------------------------------------
1 | x = () ->
2 | do ->
3 | return 1234
4 |
--------------------------------------------------------------------------------
/test/fixtures/fourspaces.json:
--------------------------------------------------------------------------------
1 | {
2 | "indentation" : {
3 | "level" : "error",
4 | "value" : 4
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/test/fixtures/mock_node_modules/coffeelint-extends-test/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "coffeescript_error": {
3 | "level": "ignore"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/test/fixtures/mock_node_modules/coffeelint-extends-test/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "coffeelint-extends-test",
3 | "version": "0.1.0",
4 | "main": "index.json"
5 | }
6 |
--------------------------------------------------------------------------------
/test/fixtures/mock_node_modules/he_who_must_not_be_named/he_who_must_not_be_named.coffee:
--------------------------------------------------------------------------------
1 | matcher = /Voldemort/i
2 |
3 | module.exports = class HeWhoMustNotBeNamed
4 | rule:
5 | name: 'he_who_must_not_be_named'
6 | level: 'error'
7 | message: 'Forbidden variable name. The snatchers have been alerted'
8 | description: ''
9 |
10 | tokens: ['IDENTIFIER']
11 |
12 | lintToken : (token, tokenApi) ->
13 | if matcher.test(token[1])
14 | true
15 |
16 |
--------------------------------------------------------------------------------
/test/fixtures/mock_node_modules/he_who_must_not_be_named/index.js:
--------------------------------------------------------------------------------
1 | require('coffeescript');
2 | module.exports = require('./he_who_must_not_be_named');
3 |
--------------------------------------------------------------------------------
/test/fixtures/prefix_transform.coffee:
--------------------------------------------------------------------------------
1 | module.exports = (source) ->
2 | return "\n\n\n\n" + source
3 |
--------------------------------------------------------------------------------
/test/fixtures/subdir/subdir/subdir.coffee:
--------------------------------------------------------------------------------
1 | here = 'is an error';
2 |
--------------------------------------------------------------------------------
/test/fixtures/syntax_error.coffee:
--------------------------------------------------------------------------------
1 | x = [1, 2
2 |
--------------------------------------------------------------------------------
/test/fixtures/twospaces.warning.json:
--------------------------------------------------------------------------------
1 | {
2 | "indentation" : {
3 | "level" : "warn",
4 | "value" : 2
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/test/test_camel_case_classes.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 | vows = require 'vows'
3 | assert = require 'assert'
4 | coffeelint = require path.join('..', 'lib', 'coffeelint')
5 |
6 | RULE = 'camel_case_classes'
7 |
8 | vows.describe(RULE).addBatch({
9 |
10 | 'Camel cased class names':
11 | topic:
12 | '''
13 | class Animal
14 |
15 | class Wolf extends Animal
16 |
17 | class BurmesePython extends Animal
18 |
19 | class Band
20 |
21 | class ELO extends Band
22 |
23 | class Eiffel65 extends Band
24 |
25 | class nested.Name
26 |
27 | class deeply.nested.Name
28 | '''
29 |
30 | 'are valid by default': (source) ->
31 | errors = coffeelint.lint(source)
32 | assert.isEmpty(errors)
33 |
34 | 'Non camel case class names':
35 | topic:
36 | '''
37 | class animal
38 |
39 | class wolf extends Animal
40 |
41 | class Burmese_Python extends Animal
42 |
43 | class canadaGoose extends Animal
44 |
45 | class _PrivatePrefix
46 | '''
47 |
48 | 'are rejected by default': (source) ->
49 | errors = coffeelint.lint(source)
50 | assert.lengthOf(errors, 4)
51 | error = errors[0]
52 | assert.equal(error.lineNumber, 1)
53 | assert.equal(error.message, 'Class name should be UpperCamelCased')
54 | assert.equal(error.context, 'class name: animal')
55 | assert.equal(error.rule, RULE)
56 |
57 | 'can be permitted': (source) ->
58 | config = camel_case_classes: { level: 'ignore' }
59 | errors = coffeelint.lint(source, config)
60 | assert.isEmpty(errors)
61 |
62 | 'Anonymous class names':
63 | topic:
64 | '''
65 | x = class
66 | m : -> 123
67 |
68 | y = class extends x
69 | m : -> 456
70 |
71 | z = class
72 |
73 | r = class then 1:2
74 | '''
75 |
76 | 'are permitted': (source) ->
77 | errors = coffeelint.lint(source)
78 | assert.isEmpty(errors)
79 |
80 | 'Inner classes are permitted':
81 | topic:
82 | '''
83 | class X
84 | class @Y
85 | f : 123
86 | class @constructor.Z
87 | f : 456
88 | '''
89 |
90 | 'are permitted': (source) ->
91 | errors = coffeelint.lint(source)
92 | assert.lengthOf(errors, 0)
93 |
94 | }).export(module)
95 |
--------------------------------------------------------------------------------
/test/test_coffeelint.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 | vows = require 'vows'
3 | assert = require 'assert'
4 | coffeelint = require path.join('..', 'lib', 'coffeelint')
5 |
6 | vows.describe('coffeelint').addBatch({
7 |
8 | "CoffeeLint's version number":
9 | topic: coffeelint.VERSION
10 |
11 | 'exists': (version) ->
12 | assert.isString(version)
13 |
14 | "CoffeeLint's errors":
15 | topic: () -> coffeelint.lint '''
16 | a = () ->\t
17 | 1234
18 | '''
19 |
20 | 'are sorted by line number': (errors) ->
21 | assert.isArray(errors)
22 | assert.lengthOf(errors, 2)
23 | assert.equal(errors[1].lineNumber, 2)
24 | assert.equal(errors[0].lineNumber, 1)
25 |
26 | 'Errors in the source':
27 | topic:
28 | '''
29 | fruits = [orange, apple, banana]
30 | switch 'a'
31 | when in fruits
32 | something
33 | '''
34 |
35 | 'are reported': (source) ->
36 | errors = coffeelint.lint(source)
37 | assert.isArray(errors)
38 | assert.lengthOf(errors, 1)
39 | error = errors[0]
40 | assert.equal(error.rule, 'coffeescript_error')
41 | assert.equal(error.lineNumber, 3)
42 |
43 | if error.message.indexOf('on line') isnt -1
44 | m = "Error: Parse error on line 3: Unexpected 'RELATION'"
45 | else if error.message.indexOf('SyntaxError:') isnt -1
46 | m = 'SyntaxError: unexpected RELATION'
47 | else
48 | # CoffeeLint changed the format to be more complex. I don't
49 | # think an exact match really needs to be verified.
50 | return
51 |
52 | assert.equal(error.message, m)
53 |
54 | }).export(module)
55 |
--------------------------------------------------------------------------------
/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 | RULE = 'colon_assignment_spacing'
7 |
8 | vows.describe(RULE).addBatch({
9 |
10 | 'Equal spacing around assignment':
11 | topic:
12 | '''
13 | object = {spacing : true}
14 | class Dog
15 | barks : true
16 | stringyObject =
17 | 'stringkey' : 'ok'
18 | '''
19 |
20 | 'will not return an error': (source) ->
21 | config =
22 | colon_assignment_spacing:
23 | level: 'error'
24 | spacing:
25 | left: 1
26 | right: 1
27 | errors = coffeelint.lint(source, config)
28 | assert.isEmpty(errors)
29 |
30 | 'No space before assignment':
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 | topic:
54 | '''
55 | query:
56 | method: 'GET'
57 | isArray: false
58 | '''
59 |
60 | 'will not return an error': (source) ->
61 | config =
62 | colon_assignment_spacing:
63 | level: 'error'
64 | spacing:
65 | left: 0
66 | right: 1
67 | errors = coffeelint.lint(source, config)
68 | assert.isEmpty(errors)
69 |
70 | 'Improper spacing around assignment':
71 | topic:
72 | '''
73 | object = {spacing: false}
74 | class Cat
75 | barks: false
76 | stringyObject =
77 | 'stringkey': 'notcool'
78 | '''
79 |
80 | 'will return an error': (source) ->
81 | config =
82 | colon_assignment_spacing:
83 | level: 'error'
84 | spacing:
85 | left: 1
86 | right: 1
87 | errors = coffeelint.lint(source, config)
88 | assert.equal(rule, RULE) for { rule } in errors
89 | assert.lengthOf(errors, 3)
90 |
91 | 'will ignore an error': (source) ->
92 | config =
93 | colon_assignment_spacing:
94 | level: 'ignore'
95 | spacing:
96 | left: 1
97 | right: 1
98 | errors = coffeelint.lint(source, config)
99 | assert.isEmpty(errors)
100 |
101 | 'Should not complain about strings':
102 | topic:
103 | '''
104 | foo = (stuff) ->
105 | throw new Error("Error: stuff required") unless stuff?
106 | # do real work
107 | '''
108 |
109 | 'will return an error': (source) ->
110 | config =
111 | colon_assignment_spacing:
112 | level: 'error'
113 | spacing:
114 | left: 1
115 | right: 1
116 | errors = coffeelint.lint(source, config)
117 | assert.isEmpty(errors)
118 |
119 | }).export(module)
120 |
--------------------------------------------------------------------------------
/test/test_duplicate_key.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 | vows = require 'vows'
3 | assert = require 'assert'
4 | coffeelint = require path.join('..', 'lib', 'coffeelint')
5 |
6 | RULE = 'duplicate_key'
7 |
8 | vows.describe(RULE).addBatch({
9 |
10 | 'Duplicate Keys':
11 | topic:
12 | '''
13 | class SomeThing
14 | getConfig: ->
15 | one = 1
16 | one = 5
17 | @config =
18 | keyA: one
19 | keyB: one
20 | keyA: 2
21 | getConfig: ->
22 | @config =
23 | foo: 1
24 |
25 | @getConfig: ->
26 | config =
27 | foo: 1
28 | '''
29 |
30 | 'should error by default': (source) ->
31 | # Moved to a variable to avoid lines being too long.
32 | message = 'Duplicate key defined in object or class'
33 | errors = coffeelint.lint(source)
34 | # Verify the two actual duplicate keys are found and it is not
35 | # mistaking @getConfig as a duplicate key
36 | assert.equal(errors.length, 2)
37 | error = errors[0]
38 | assert.equal(error.lineNumber, 8) # 2nd getA
39 | assert.equal(error.message, message)
40 | assert.equal(error.rule, RULE)
41 | error = errors[1]
42 | assert.equal(error.lineNumber, 9) # 2nd getConfig
43 | assert.equal(error.message, message)
44 | assert.equal(error.rule, RULE)
45 |
46 | 'is optional': (source) ->
47 | for length in [null, 0, false]
48 | config =
49 | duplicate_key:
50 | level: 'ignore'
51 | errors = coffeelint.lint(source, config)
52 | assert.isEmpty(errors)
53 |
54 | }).export(module)
55 |
--------------------------------------------------------------------------------
/test/test_empty_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 | RULE = 'empty_constructor_needs_parens'
7 |
8 | vows.describe(RULE).addBatch({
9 |
10 | 'Make sure no errors if constructors are indexed (#421)':
11 | topic:
12 | '''
13 | new OPERATIONS[operationSpec.type] operationSpec.field
14 |
15 | new Foo[bar].baz[qux] param1
16 | '''
17 |
18 | 'should pass': (source) ->
19 | config =
20 | empty_constructor_needs_parens:
21 | level: 'error'
22 | errors = coffeelint.lint(source, config)
23 | assert.equal(errors.length, 0)
24 |
25 | 'Missing Parentheses on "new Foo"':
26 | topic:
27 | '''
28 | class Foo
29 |
30 | # Warn about missing parens here
31 | a = new Foo
32 | b = new bar.foo.Foo
33 | # The parens make it clear no parameters are intended
34 | c = new Foo()
35 | d = new bar.foo.Foo()
36 | e = new Foo 1, 2
37 | f = new bar.foo.Foo 1, 2
38 | # Since this does have a parameter it should not require parens
39 | g = new bar.foo.Foo
40 | config: 'parameter'
41 | '''
42 |
43 | 'warns about missing parens': (source) ->
44 | config =
45 | empty_constructor_needs_parens:
46 | level: 'error'
47 | errors = coffeelint.lint(source, config)
48 | assert.equal(errors.length, 2)
49 | assert.equal(errors[0].lineNumber, 4)
50 | assert.equal(errors[0].rule, RULE)
51 | assert.equal(errors[1].lineNumber, 5)
52 | assert.equal(errors[1].rule, RULE)
53 |
54 | }).export(module)
55 |
--------------------------------------------------------------------------------
/test/test_eol_last.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 | vows = require 'vows'
3 | assert = require 'assert'
4 | coffeelint = require path.join('..', 'lib', 'coffeelint')
5 |
6 | configError = eol_last: { level: 'error' }
7 |
8 | RULE = 'eol_last'
9 |
10 | vows.describe(RULE).addBatch({
11 |
12 | 'eol':
13 | 'should not warn by default': ->
14 | assert.isEmpty(coffeelint.lint('foobar'))
15 |
16 | 'should warn when enabled': ->
17 | result = coffeelint.lint('foobar', configError)
18 | assert.equal(result.length, 1)
19 | assert.equal(result[0].level, 'error')
20 | assert.equal(result[0].rule, RULE)
21 |
22 | 'should warn when enabled with multiple newlines': ->
23 | result = coffeelint.lint('foobar\n\n', configError)
24 | assert.equal(result.length, 1)
25 | assert.equal(result[0].level, 'error')
26 | assert.equal(result[0].rule, RULE)
27 |
28 | 'should not warn with newline': ->
29 | assert.isEmpty(coffeelint.lint('foobar\n', configError))
30 |
31 | }).export(module)
32 |
--------------------------------------------------------------------------------
/test/test_filenames.coffee:
--------------------------------------------------------------------------------
1 | vows = require 'vows'
2 | assert = require 'assert'
3 |
4 | fs = require('fs')
5 | path = require('path')
6 | glob = require('glob')
7 |
8 | batch = {}
9 |
10 | thisdir = path.dirname(fs.realpathSync(__filename))
11 | rulesDir = path.join(thisdir, '..', 'src', 'rules')
12 |
13 | rules = glob.sync(path.join(rulesDir, '*.coffee'))
14 |
15 | hasTests = {
16 | 'has tests': ->
17 | ruleFilename = this.context.name
18 | testFilename = path.join(thisdir, 'test_' + ruleFilename)
19 | assert(fs.existsSync(testFilename), "expected #{testFilename} to exist")
20 |
21 | 'has correct filename': ->
22 | ruleFilename = this.context.name
23 | Rule = require(path.join(rulesDir, ruleFilename))
24 |
25 | tmp = new Rule
26 | expectedFilename = tmp.rule.name + '.coffee'
27 |
28 | assert.equal(ruleFilename, expectedFilename)
29 | }
30 |
31 | rules.forEach((rule) ->
32 | filename = path.basename(rule)
33 | batch[filename] = hasTests
34 | )
35 |
36 | vows.describe('filenames').addBatch(batch).export(module)
37 |
--------------------------------------------------------------------------------
/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 | topic:
10 | '''
11 | abc = 123;
12 | '''
13 |
14 | 'can ignore errors': (source) ->
15 | config = no_trailing_semicolons: { level: 'ignore' }
16 | errors = coffeelint.lint(source, config)
17 | assert.isEmpty(errors)
18 |
19 | 'can return warnings': (source) ->
20 | config = 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 = no_trailing_semicolons: { level: 'error' }
29 | errors = coffeelint.lint(source, config)
30 | assert.isArray(errors)
31 | assert.lengthOf(errors, 1)
32 | error = errors[0]
33 | assert.equal(error.level, 'error')
34 |
35 | 'catches unknown levels': (source) ->
36 | config = no_trailing_semicolons: { level: 'foobar' }
37 | assert.throws () ->
38 | coffeelint.lint(source, config)
39 |
40 |
41 | }).export(module)
42 |
--------------------------------------------------------------------------------
/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 | RULE = 'line_endings'
7 |
8 | vows.describe(RULE).addBatch({
9 |
10 | 'Unix line endings':
11 | topic:
12 | '''
13 | x = 1\ny=2
14 | '''
15 |
16 | 'are allowed by default': (source) ->
17 | errors = coffeelint.lint(source)
18 | assert.isEmpty(errors)
19 |
20 | 'can be forbidden': (source) ->
21 | config = line_endings: { level: 'error', value: 'windows' }
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, 'Line contains incorrect line endings')
28 | assert.equal(error.context, 'Expected windows')
29 | assert.equal(error.rule, RULE)
30 |
31 | 'Windows line endings':
32 | topic:
33 | '''
34 | x = 1\r\ny=2
35 | '''
36 |
37 | 'are allowed by default': (source) ->
38 | errors = coffeelint.lint(source)
39 | assert.isEmpty(errors)
40 |
41 | 'can be forbidden': (source) ->
42 | config = line_endings: { level: 'error', value: 'unix' }
43 | errors = coffeelint.lint(source, config)
44 | assert.isArray(errors)
45 | assert.lengthOf(errors, 1)
46 | error = errors[0]
47 | assert.equal(error.lineNumber, 1)
48 | assert.equal(error.message, 'Line contains incorrect line endings')
49 | assert.equal(error.context, 'Expected unix')
50 | assert.equal(error.rule, RULE)
51 |
52 | 'Unknown line endings':
53 | topic:
54 | '''
55 | x = 1\ny=2
56 | '''
57 |
58 | 'throw errors': (source) ->
59 | config = line_endings: { level: 'error', value: 'osx' }
60 | assert.throws () ->
61 | coffeelint.lint(source, config)
62 |
63 | }).export(module)
64 |
--------------------------------------------------------------------------------
/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 | 'Tab indented markdown':
38 |
39 | topic:
40 |
41 | Second line in this topic is used to test support for a tab indented lines.
42 | Third line verifies that only a first tab is removed.
43 |
44 | """This is some `Markdown`.\n\n
45 | \n x = 1\n y = 1
46 | """
47 |
48 | 'is ignored': (source) ->
49 |
50 | errors = coffeelint.lint(source, {}, true)
51 | assert.equal(errors.length, 3)
52 |
53 | }).export(module)
54 |
--------------------------------------------------------------------------------
/test/test_max_line_length.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 | vows = require 'vows'
3 | assert = require 'assert'
4 | coffeelint = require path.join('..', 'lib', 'coffeelint')
5 |
6 | RULE = 'max_line_length'
7 | vows.describe(RULE).addBatch({
8 |
9 | 'Maximum line length':
10 | topic: () ->
11 | # Every line generated here is a comment.
12 | line = (length) ->
13 | return '# ' + new Array(length - 1).join('-')
14 | lengths = [50, 79, 80, 81, 100, 200]
15 | (line(l) for l in lengths).join('\n')
16 |
17 | 'defaults to 80': (source) ->
18 | errors = coffeelint.lint(source)
19 | assert.equal(errors.length, 3)
20 | error = errors[0]
21 | assert.equal(error.lineNumber, 4)
22 | assert.equal(error.message, 'Line exceeds maximum allowed length')
23 | assert.equal(error.rule, RULE)
24 |
25 | 'is configurable': (source) ->
26 | config =
27 | max_line_length:
28 | value: 99
29 | level: 'error'
30 | errors = coffeelint.lint(source, config)
31 | assert.equal(errors.length, 2)
32 |
33 | 'is optional': (source) ->
34 | for length in [null, 0, false]
35 | config =
36 | max_line_length:
37 | value: length
38 | level: 'ignore'
39 | errors = coffeelint.lint(source, config)
40 | assert.isEmpty(errors)
41 |
42 | 'can ignore comments': (source) ->
43 | config =
44 | max_line_length:
45 | limitComments: false
46 |
47 | errors = coffeelint.lint(source, config)
48 | assert.isEmpty(errors)
49 |
50 | 'respects Windows line breaks': ->
51 | source = new Array(81).join('X') + '\r\n'
52 |
53 | errors = coffeelint.lint(source, {})
54 | assert.isEmpty(errors)
55 |
56 | 'Literate Line Length':
57 | topic: ->
58 | # This creates a line with 80 Xs.
59 | source = new Array(81).join('X') + '\n'
60 |
61 | # Long URLs are ignored by default even in Literate code.
62 | source += 'http://testing.example.com/really-really-long-url-' +
63 | 'that-shouldnt-have-to-be-split-to-avoid-the-lint-error'
64 |
65 | 'long urls are ignored': (source) ->
66 | errors = coffeelint.lint(source, {}, true)
67 | assert.isEmpty(errors)
68 |
69 | 'Maximum length exceptions':
70 | topic:
71 | '''
72 | # Since the line length check only reads lines in isolation it will
73 | # see the following line as a comment even though it's in a string.
74 | # I don't think that's a problem.
75 | #
76 | # http://testing.example.com/really-really-long-url-that-shouldnt-have-to-be-split-to-avoid-the-lint-error
77 | '''
78 |
79 | 'excludes long urls': (source) ->
80 | errors = coffeelint.lint(source)
81 | assert.isEmpty(errors)
82 |
83 | }).export(module)
84 |
--------------------------------------------------------------------------------
/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, is_strict) ->
10 | config = {}
11 | config[name] = level: 'ignore' for name, rule of coffeelint.RULES
12 | config[RULE].level = 'error'
13 | config[RULE].is_strict = is_strict
14 | coffeelint.lint source, config
15 |
16 | shouldError = (source, numErrors, is_strict = false) ->
17 | topic: source
18 | 'errors for missing arrow': (source) ->
19 | numErrors ?= 1
20 | errors = runLint source, is_strict
21 | assert.lengthOf errors, numErrors,
22 | "Expected #{numErrors} errors, got #{inspect errors}"
23 | error = errors[0]
24 | assert.equal error.rule, RULE
25 |
26 | shouldPass = (source, is_strict = false) ->
27 | topic: source
28 | 'does not error for no missing arrows': (source) ->
29 | errors = runLint source, is_strict
30 | assert.isEmpty errors, "Expected no errors, got #{inspect errors}"
31 |
32 | vows.describe(RULE).addBatch({
33 |
34 | 'empty function': shouldPass '->'
35 | 'function without this': shouldPass '-> 1'
36 | 'function with this': shouldError '-> this'
37 | 'function with this.a': shouldError '-> this.a'
38 | 'function with @': shouldError '-> @'
39 | 'function with @a': shouldError '-> @a'
40 |
41 | 'nested functions with this inside':
42 | 'with inner fat arrow': shouldPass '-> => this'
43 | 'with outer fat arrow': shouldError '=> -> this'
44 | 'with both fat arrows': shouldPass '=> => this'
45 |
46 | 'nested functions with this outside':
47 | 'with inner fat arrow': shouldError '-> (this; =>)'
48 | 'with outer fat arrow': shouldPass '=> (this; ->)'
49 | 'with both fat arrows': shouldPass '=> (this; =>)'
50 |
51 | 'deeply nested functions':
52 | 'with thin arrow': shouldError '-> -> -> -> -> this'
53 | 'with fat arrow': shouldPass '-> -> -> -> => this'
54 | 'with wrong fat arrow': shouldError '-> -> => -> -> this'
55 |
56 | 'functions with multiple statements': shouldError '''
57 | f = ->
58 | this.x = 2
59 | z ((a) -> a; this.x)
60 | ''', 2
61 |
62 | 'functions with parameters': shouldPass '(a) ->'
63 | 'functions with parameter assignment': shouldError '(@a) ->'
64 | 'functions with destructuring parameter assignment': shouldError '({@a}) ->'
65 |
66 | 'class instance method':
67 | 'without this': shouldPass '''
68 | class A
69 | @m: -> 1
70 | '''
71 | 'with this': shouldPass '''
72 | class A
73 | @m: -> this
74 | '''
75 |
76 | 'class instance method in strict mode':
77 | 'without this': shouldPass '''
78 | class A
79 | @m: -> 1
80 | ''', true
81 | 'with this': shouldError '''
82 | class A
83 | @m: -> this
84 | ''', null, true
85 |
86 | # https://github.com/clutchski/coffeelint/issues/412
87 | 'do methods should not error': shouldPass '''
88 | do -> 1
89 | '''
90 |
91 | 'class method':
92 | 'without this': shouldPass '''
93 | class A
94 | m: -> 1
95 | '''
96 | 'with this': shouldPass '''
97 | class A
98 | m: -> this
99 | '''
100 |
101 | 'class method in strict mode':
102 | 'without this': shouldPass '''
103 | class A
104 | m: -> 1
105 | ''', true
106 | 'with this': shouldError '''
107 | class A
108 | m: -> this
109 | ''', null, true
110 |
111 | 'class constructor in strict mode':
112 | 'without this': shouldPass '''
113 | class A
114 | constructor: -> 1
115 | ''', true
116 | 'with this': shouldPass '''
117 | class A
118 | constructor: -> this
119 | dd: 'constructor'
120 | xx: -> 'constructor'
121 | ''', true
122 |
123 | 'function in class body':
124 | 'without this': shouldPass '''
125 | class A
126 | f = -> 1
127 | x: 2
128 | '''
129 | 'with this': shouldError '''
130 | class A
131 | f = -> this
132 | x: 2
133 | '''
134 |
135 | 'function inside class instance method':
136 | 'without this': shouldPass '''
137 | class A
138 | m: -> -> 1
139 | '''
140 | 'with this': shouldError '''
141 | class A
142 | m: -> -> @a
143 | '''
144 |
145 | 'mixture of class methods and function in class body':
146 | 'with this': shouldPass '''
147 | class A
148 | f = => this
149 | m: -> this
150 | @n: -> this
151 | o: -> this
152 | @p: -> this
153 | '''
154 |
155 | 'mixture of class methods and function in class body in strict mode':
156 | 'with this': shouldPass '''
157 | class A
158 | f = => this
159 | m: => this
160 | @n: => this
161 | o: => this
162 | @p: => this
163 | ''', true
164 |
165 | 'https://github.com/clutchski/coffeelint/issues/215':
166 | 'method with block comment': shouldPass '''
167 | class SimpleClass
168 |
169 | ###
170 | A block comment
171 | ###
172 | doNothing: () ->
173 | '''
174 | 'function outside class instance method':
175 | 'without this': shouldPass '''
176 | ->
177 | class A
178 | m: ->
179 | '''
180 | 'with this': shouldPass '''
181 | ->
182 | class A
183 | @m: ->
184 | '''
185 | 'do not require fat arrows in prototype (::) methods':
186 | 'method declared by :: (Fixes #296)': shouldPass '''
187 | X::getName = ->
188 | @name
189 | '''
190 |
191 | }).export(module)
192 |
--------------------------------------------------------------------------------
/test/test_no_backticks.coffee:
--------------------------------------------------------------------------------
1 | path = require 'path'
2 | vows = require 'vows'
3 | assert = require 'assert'
4 | coffeelint = require path.join('..', 'lib', 'coffeelint')
5 |
6 | RULE = 'no_backticks'
7 |
8 | vows.describe(RULE).addBatch({
9 |
10 | 'Backticks':
11 | topic:
12 | '''
13 | `with(document) alert(height);`
14 | '''
15 |
16 | 'are forbidden by default': (source) ->
17 | errors = coffeelint.lint(source)
18 | assert.isArray(errors)
19 | assert.lengthOf(errors, 1)
20 | error = errors[0]
21 | assert.equal(error.rule, RULE)
22 | assert.equal(error.lineNumber, 1)
23 | assert.equal(error.message, 'Backticks are forbidden')
24 |
25 | 'can be permitted': (source) ->
26 | config = no_backticks: { level: 'ignore' }
27 | errors = coffeelint.lint(source, config)
28 | assert.isArray(errors)
29 | assert.isEmpty(errors)
30 |
31 | 'Ignore string interpolation from comments':
32 | topic:
33 | '''
34 |