├── .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 | [![Build Status](https://secure.travis-ci.org/clutchski/coffeelint.svg)](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 20 | 21 | 3. Write changelog 22 | ------------------ 23 | 24 | git checkout gh-pages 25 | 26 | The changelog is in `index-bottom.html`. Update it based on the PRs found in 27 | step 1. I don't always mention every PR. Many internal changes like updates to 28 | Travis don't matter to users of CoffeeLint, so I leave them out. 29 | 30 | This next step is going to end up checkout out master, so I'll usually commit 31 | the changelog updates and then I'll run `git commit --amend` after the next step. 32 | 33 | 4. Update `gh-page`'s coffeelint 34 | -------------------------------- 35 | 36 | rake update 37 | 38 | I've never rewritten how this gets generated. Because it needs to pull a 39 | compiled version of CoffeeLint from master, `rake update` gives you a set of 40 | commands to copy and paste. 41 | 42 | # It doesn't matter if you ammend or add a new commit, this is just what I do. 43 | git commit --amend 44 | 45 | 5. Release all the things! 46 | -------------------------- 47 | 48 | git checkout master 49 | git push origin master 50 | git push origin gh-pages 51 | git push origin 52 | 53 | I think it's important that people be able to install CoffeeLint directly from 54 | git. People also got upset when the NPM version required installing browserify 55 | and coffeeify when they were never actually used. For this reason I have a 56 | `prepublish` script that will yank those and the `install` script out of 57 | `package.json`. I had this fail to run for me once, so now I run it manually 58 | just to make sure it's fine before I publish. 59 | 60 | npm run prepublish 61 | git diff 62 | npm publish 63 | git checkout package.json 64 | 65 | [changelog]: http://www.coffeelint.org/#changelog 66 | [review]: https://github.com/clutchski/coffeelint/compare/v1.8.1...master 67 | -------------------------------------------------------------------------------- /doc/user.md: -------------------------------------------------------------------------------- 1 | How do I configure CoffeeLint? 2 | ============================== 3 | 4 | There are two main options. In the root of your project create a 5 | `coffeelint.json`, or add a `coffeelintConfig` section to your `package.json`. 6 | Either way, the configuration is exactly the same. If CoffeeLint doesn't find 7 | any configuration for the current project, it will check for a 8 | `$HOME/coffeelint.json` to use. 9 | 10 | `package.json` 11 | -------------- 12 | ```json 13 | { 14 | "name": "your-project", 15 | "version": "0.0.0", 16 | "coffeelintConfig": { 17 | "indentation" : { 18 | "level" : "error", 19 | "value" : 4 20 | }, 21 | "line_endings" : { 22 | "value" : "unix", 23 | "level" : "error" 24 | } 25 | } 26 | } 27 | ``` 28 | 29 | `coffeelint.json` 30 | ----------------- 31 | ```json 32 | { 33 | "indentation" : { 34 | "level" : "error", 35 | "value" : 4 36 | }, 37 | "line_endings" : { 38 | "value" : "unix", 39 | "level" : "error" 40 | } 41 | } 42 | ``` 43 | 44 | What are the rules? 45 | =================== 46 | 47 | See [coffeelint.org][options] for all of the built in rules. Every rule has a 48 | `level` of `ignore`, `error`, or `warn`. Most rules have a single behavior and 49 | `level` is the only thing to configure. `indentation` is one of the exceptions, 50 | it has a `value` that defaults to 2. 51 | 52 | How do I temporarily disable a rule? 53 | ==================================== 54 | 55 | ```CoffeeScript 56 | # coffeelint: disable=max_line_length 57 | object: 58 | attr: "some/huge/line/string/with/embed/#{values}.that/surpasses/the/max/column/width" 59 | # coffeelint: enable=max_line_length 60 | ``` 61 | 62 | What about 3rd party rules? 63 | =========================== 64 | 65 | CoffeeLint 0.6 to 1.3 required 3rd party rules to be installed globally (`[sudo] 66 | npm install -g `). 67 | 68 | Starting with CoffeeLint 1.4 rules can (and should) be installed per project. 69 | Consult the `README.md` or npmjs.org page for exact configuration instructions. 70 | It's generally the same as built in rules but with the addition of a `module` 71 | attribute to specify the correct module name. It may not exactly match the rule 72 | name. 73 | 74 | All rules should have a `coffeelintrule` tag on [npmjs.org][rules]. 75 | 76 | How do I use JSX (ReactJS) 77 | ========================== 78 | 79 | CoffeeLint 1.8 allows you to add transformers that will run over the code 80 | before CoffeeLint processes it. 81 | 82 | *WARNING*: CoffeeLint cannot control what these transformers do. They may 83 | violate all kinds of rules you have setup. It's up to you to wrap your code in 84 | `# coffeelint: disable=max_line_length` or whatever you need. 85 | 86 | *WARNING*: These transformers might not maintain line numbers. If this happens 87 | and it's a problem, it's up to you to contact the developers to see if they can 88 | keep everything on the same lines. 89 | 90 | In your coffeelint.json: 91 | 92 | ```json 93 | { 94 | "coffeelint": { 95 | "transforms": [ "coffee-react-transform" ] 96 | } 97 | } 98 | ``` 99 | 100 | What about different flavors of CoffeeScript, like IcedCoffeeScript? 101 | ==================================================================== 102 | 103 | While this functionality was added in 1.8, it's basically unsupported. If your 104 | chosen flavor breaks things it's up to you to contact the maintainer and see if 105 | they are willing to bring their implementation in line with the official 106 | CoffeeScript. 107 | 108 | Using IcedCoffeeScript [does break][IcedCoffeeScript] the `cyclomatic_complexity` rule 109 | 110 | ```json 111 | { 112 | "coffeelint": { 113 | "coffeescript": [ "iced-coffee-script" ] 114 | } 115 | } 116 | ``` 117 | 118 | [options]: http://www.coffeelint.org/#options 119 | [rules]: https://www.npmjs.org/search?q=coffeelintrule 120 | [IcedCoffeeScript]: https://github.com/clutchski/coffeelint/issues/349#issuecomment-67737784 121 | -------------------------------------------------------------------------------- /generated_coffeelint.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrow_spacing": { 3 | "level": "ignore" 4 | }, 5 | "braces_spacing": { 6 | "level": "ignore", 7 | "spaces": 0, 8 | "empty_object_spaces": 0 9 | }, 10 | "camel_case_classes": { 11 | "level": "error" 12 | }, 13 | "coffeescript_error": { 14 | "level": "error" 15 | }, 16 | "colon_assignment_spacing": { 17 | "level": "ignore", 18 | "spacing": { 19 | "left": 0, 20 | "right": 0 21 | } 22 | }, 23 | "cyclomatic_complexity": { 24 | "value": 10, 25 | "level": "ignore" 26 | }, 27 | "duplicate_key": { 28 | "level": "error" 29 | }, 30 | "empty_constructor_needs_parens": { 31 | "level": "ignore" 32 | }, 33 | "ensure_comprehensions": { 34 | "level": "warn" 35 | }, 36 | "eol_last": { 37 | "level": "ignore" 38 | }, 39 | "indentation": { 40 | "value": 2, 41 | "level": "error" 42 | }, 43 | "line_endings": { 44 | "level": "ignore", 45 | "value": "unix" 46 | }, 47 | "max_line_length": { 48 | "value": 80, 49 | "level": "error", 50 | "limitComments": true 51 | }, 52 | "missing_fat_arrows": { 53 | "level": "ignore", 54 | "is_strict": false 55 | }, 56 | "newlines_after_classes": { 57 | "value": 3, 58 | "level": "ignore" 59 | }, 60 | "no_backticks": { 61 | "level": "error" 62 | }, 63 | "no_debugger": { 64 | "level": "warn", 65 | "console": false 66 | }, 67 | "no_empty_functions": { 68 | "level": "ignore" 69 | }, 70 | "no_empty_param_list": { 71 | "level": "ignore" 72 | }, 73 | "no_implicit_braces": { 74 | "level": "ignore", 75 | "strict": true 76 | }, 77 | "no_implicit_parens": { 78 | "strict": true, 79 | "level": "ignore" 80 | }, 81 | "no_interpolation_in_single_quotes": { 82 | "level": "ignore" 83 | }, 84 | "no_nested_string_interpolation": { 85 | "level": "warn" 86 | }, 87 | "no_plusplus": { 88 | "level": "ignore" 89 | }, 90 | "no_private_function_fat_arrows": { 91 | "level": "warn" 92 | }, 93 | "no_stand_alone_at": { 94 | "level": "ignore" 95 | }, 96 | "no_tabs": { 97 | "level": "error" 98 | }, 99 | "no_this": { 100 | "level": "ignore" 101 | }, 102 | "no_throwing_strings": { 103 | "level": "error" 104 | }, 105 | "no_trailing_semicolons": { 106 | "level": "error" 107 | }, 108 | "no_trailing_whitespace": { 109 | "level": "error", 110 | "allowed_in_comments": false, 111 | "allowed_in_empty_lines": true 112 | }, 113 | "no_unnecessary_double_quotes": { 114 | "level": "ignore" 115 | }, 116 | "no_unnecessary_fat_arrows": { 117 | "level": "warn" 118 | }, 119 | "non_empty_constructor_needs_parens": { 120 | "level": "ignore" 121 | }, 122 | "prefer_english_operator": { 123 | "level": "ignore", 124 | "doubleNotLevel": "ignore" 125 | }, 126 | "space_operators": { 127 | "level": "ignore" 128 | }, 129 | "spacing_after_comma": { 130 | "level": "ignore" 131 | }, 132 | "transform_messes_up_line_numbers": { 133 | "level": "warn" 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /lib/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clutchski/coffeelint/86631c2c8ce82b4403fb84a377364ccc07180a9a/lib/.gitignore -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coffeelint", 3 | "description": "Lint your CoffeeScript", 4 | "version": "2.1.0", 5 | "homepage": "http://www.coffeelint.org", 6 | "keywords": [ 7 | "lint", 8 | "coffeescript", 9 | "coffee-script" 10 | ], 11 | "author": "Matthew Perpick ", 12 | "main": "./lib/coffeelint.js", 13 | "engines": { 14 | "npm": ">=1.3.7", 15 | "node": ">=6.9.1" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git://github.com/clutchski/coffeelint.git" 20 | }, 21 | "bin": { 22 | "coffeelint": "./bin/coffeelint" 23 | }, 24 | "dependencies": { 25 | "browserify": "^13.1.0", 26 | "coffeeify": "~1.0.0", 27 | "coffeescript": "^2.1.0", 28 | "glob": "^7.0.6", 29 | "ignore": "^3.0.9", 30 | "optimist": "^0.6.1", 31 | "resolve": "^0.6.3", 32 | "strip-json-comments": "^1.0.2" 33 | }, 34 | "devDependencies": { 35 | "vows": ">=0.8.1", 36 | "underscore": ">=1.4.4" 37 | }, 38 | "license": "MIT", 39 | "scripts": { 40 | "pretest": "cake compile", 41 | "test": "./vowsrunner.js --spec test/*.coffee test/*.litcoffee", 42 | "testrule": "npm run compile && ./vowsrunner.js --spec", 43 | "posttest": "npm run lint", 44 | "prepublish": "cake prepublish", 45 | "postpublish": "cake postpublish", 46 | "publish": "cake publish", 47 | "install": "cake install", 48 | "lint": "cake compile && ./bin/coffeelint .", 49 | "lint-csv": "cake compile && ./bin/coffeelint --csv .", 50 | "lint-jslint": "cake compile && ./bin/coffeelint --jslint .", 51 | "compile": "cake compile" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/ast_linter.coffee: -------------------------------------------------------------------------------- 1 | BaseLinter = require './base_linter.coffee' 2 | 3 | node_children = 4 | Class: ['variable', 'parent', 'body'] 5 | Code: ['params', 'body'] 6 | For: ['body', 'source', 'guard', 'step'] 7 | If: ['condition', 'body', 'elseBody'] 8 | Obj: ['properties'] 9 | Op: ['first', 'second'] 10 | Switch: ['subject', 'cases', 'otherwise'] 11 | Try: ['attempt', 'recovery', 'ensure'] 12 | Value: ['base', 'properties'] 13 | While: ['condition', 'guard', 'body'] 14 | 15 | hasChildren = (node, children) -> 16 | node?.children?.length is children.length and 17 | node?.children.every (elem, i) -> elem is children[i] 18 | 19 | class ASTApi 20 | constructor: (@config) -> 21 | getNodeName: (node) -> 22 | name = node?.constructor?.name 23 | if node_children[name] 24 | return name 25 | else 26 | for own name, children of node_children 27 | if hasChildren(node, children) 28 | return name 29 | 30 | 31 | # A class that performs static analysis of the abstract 32 | # syntax tree. 33 | module.exports = class ASTLinter extends BaseLinter 34 | 35 | constructor: (source, config, rules, @CoffeeScript) -> 36 | super source, config, rules 37 | @astApi = new ASTApi @config 38 | 39 | # This uses lintAST instead of lintNode because I think it makes it a bit 40 | # more clear that the rule needs to walk the AST on its own. 41 | acceptRule: (rule) -> 42 | return typeof rule.lintAST is 'function' 43 | 44 | lint: () -> 45 | errors = [] 46 | try 47 | @node = @CoffeeScript.nodes(@source) 48 | catch coffeeError 49 | # If for some reason you shut off the 'coffeescript_error' rule err 50 | # will be null and should NOT be added to errors 51 | err = @_parseCoffeeScriptError(coffeeError) 52 | errors.push err if err? 53 | return errors 54 | 55 | for rule in @rules 56 | @astApi.createError = (attrs = {}) => 57 | @createError rule.rule.name, attrs 58 | 59 | # HACK: Push the local errors object into the plugin. This is a 60 | # temporary solution until I have a way for it to really return 61 | # multiple errors. 62 | rule.errors = errors 63 | v = @normalizeResult rule, rule.lintAST(@node, @astApi) 64 | 65 | return v if v? 66 | errors 67 | 68 | _parseCoffeeScriptError: (coffeeError) -> 69 | rule = @config['coffeescript_error'] 70 | 71 | message = coffeeError.toString() 72 | 73 | # Parse the line number 74 | lineNumber = -1 75 | if coffeeError.location? 76 | lineNumber = coffeeError.location.first_line + 1 77 | else 78 | match = /line (\d+)/.exec message 79 | lineNumber = parseInt match[1], 10 if match?.length > 1 80 | attrs = { 81 | message: message 82 | level: rule.level 83 | lineNumber: lineNumber 84 | } 85 | return @createError 'coffeescript_error', attrs 86 | -------------------------------------------------------------------------------- /src/base_linter.coffee: -------------------------------------------------------------------------------- 1 | # Patch the source properties onto the destination. 2 | extend = (destination, sources...) -> 3 | for source in sources 4 | (destination[k] = v for k, v of source) 5 | return destination 6 | 7 | # Patch any missing attributes from defaults to source. 8 | defaults = (source, defaults) -> 9 | extend({}, defaults, source) 10 | 11 | module.exports = class BaseLinter 12 | 13 | constructor: (@source, @config, rules) -> 14 | @setupRules rules 15 | 16 | isObject: (obj) -> 17 | obj is Object(obj) 18 | 19 | # Create an error object for the given rule with the given 20 | # attributes. 21 | createError: (ruleName, attrs = {}) -> 22 | # Level should default to what's in the config, but can be overridden. 23 | attrs.level ?= @config[ruleName].level 24 | 25 | level = attrs.level 26 | if level not in ['ignore', 'warn', 'error'] 27 | throw new Error("unknown level #{level} for rule: #{ruleName}") 28 | 29 | if level in ['error', 'warn'] 30 | attrs.rule = ruleName 31 | return defaults(attrs, @config[ruleName]) 32 | else 33 | null 34 | 35 | acceptRule: (rule) -> 36 | throw new Error 'acceptRule needs to be overridden in the subclass' 37 | 38 | # Only rules that have a level of error or warn will even get constructed. 39 | setupRules: (rules) -> 40 | @rules = [] 41 | for name, RuleConstructor of rules 42 | level = @config[name].level 43 | if level in ['error', 'warn'] 44 | rule = new RuleConstructor this, @config 45 | if @acceptRule(rule) 46 | @rules.push rule 47 | else if level isnt 'ignore' 48 | throw new Error("unknown level #{level} for rule: #{rule}") 49 | 50 | normalizeResult: (p, result) -> 51 | if result is true 52 | return @createError p.rule.name 53 | if @isObject result 54 | return @createError p.rule.name, result 55 | -------------------------------------------------------------------------------- /src/cache.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | crypto = require 'crypto' 4 | 5 | ltVer = require('./../package.json').version 6 | csVer = (window?.CoffeeScript or require 'coffeescript').VERSION 7 | 8 | 9 | module.exports = class Cache 10 | 11 | constructor: (@basepath) -> 12 | unless fs.existsSync @basepath 13 | fs.mkdirSync @basepath, 0o755 14 | 15 | 16 | path: (source) -> 17 | path.join @basepath, "#{csVer}-#{ltVer}-#{@prefix}-#{@hash(source)}" 18 | 19 | 20 | get: (source) -> JSON.parse fs.readFileSync @path(source), 'utf8' 21 | 22 | 23 | set: (source, result) -> 24 | fs.writeFileSync @path(source), JSON.stringify result 25 | 26 | 27 | has: (source) -> fs.existsSync @path source 28 | 29 | 30 | hash: (data) -> 31 | crypto.createHash('md5').update('' + data).digest('hex').substring(0, 8) 32 | 33 | 34 | # Use user config as a "namespace" so that 35 | # when he/she changes it the cache becomes invalid 36 | setConfig: (config) -> @prefix = @hash JSON.stringify config 37 | -------------------------------------------------------------------------------- /src/configfinder.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Helpers for finding CoffeeLint config in standard locations, similar to how 3 | JSHint does. 4 | ### 5 | 6 | fs = require 'fs' 7 | path = require 'path' 8 | stripComments = require 'strip-json-comments' 9 | resolve = require('resolve').sync 10 | 11 | # Cache for findFile 12 | findFileResults = {} 13 | 14 | # Searches for a file with a specified name starting with 'dir' and going all 15 | # the way up either until it finds the file or hits the root. 16 | findFile = (name, dir) -> 17 | dir = dir or process.cwd() 18 | filename = path.normalize(path.join(dir, name)) 19 | return findFileResults[filename] if findFileResults[filename] 20 | parent = path.resolve(dir, '../') 21 | if fs.existsSync(filename) 22 | findFileResults[filename] = filename 23 | else if dir is parent 24 | findFileResults[filename] = null 25 | else 26 | findFile name, parent 27 | 28 | # Possibly find CoffeeLint configuration within a package.json file. 29 | loadNpmConfig = (dir) -> 30 | fp = findFile('package.json', dir) 31 | loadJSON(fp)?.coffeelintConfig if fp 32 | 33 | # Parse a JSON file gracefully. 34 | loadJSON = (filename) -> 35 | try 36 | JSON.parse(stripComments(fs.readFileSync(filename).toString())) 37 | catch e 38 | process.stderr.write "Could not load JSON file '#{filename}': #{e}" 39 | null 40 | 41 | # Tries to find a configuration file in either project directory (if file is 42 | # given), as either the package.json's 'coffeelintConfig' property, or a project 43 | # specific 'coffeelint.json' or a global 'coffeelint.json' in the home 44 | # directory. 45 | getConfig = (dir) -> 46 | if (process.env.COFFEELINT_CONFIG and 47 | fs.existsSync(process.env.COFFEELINT_CONFIG)) 48 | return loadJSON(process.env.COFFEELINT_CONFIG) 49 | 50 | npmConfig = loadNpmConfig(dir) 51 | return npmConfig if npmConfig 52 | projConfig = findFile('coffeelint.json', dir) 53 | return loadJSON(projConfig) if projConfig 54 | 55 | envs = process.env.USERPROFILE or process.env.HOME or process.env.HOMEPATH 56 | home = path.normalize(path.join(envs, 'coffeelint.json')) 57 | if fs.existsSync(home) 58 | return loadJSON(home) 59 | 60 | # configfinder is the only part of coffeelint that actually has the full 61 | # filename and can accurately resolve module names. This will find all of the 62 | # modules and expand them into full paths so that they can be found when the 63 | # source and config are passed to `coffeelint.lint` 64 | expandModuleNames = (dir, config) -> 65 | for ruleName, data of config when data?.module? 66 | config[ruleName]._module = config[ruleName].module 67 | config[ruleName].module = resolve data.module, { 68 | basedir: dir, 69 | extensions: ['.js', '.coffee', '.litcoffee', '.coffee.md'] 70 | } 71 | 72 | coffeelint = config.coffeelint 73 | if coffeelint?.transforms? 74 | coffeelint._transforms = coffeelint.transforms 75 | coffeelint.transforms = coffeelint.transforms.map (moduleName) -> 76 | return resolve moduleName, { 77 | basedir: dir, 78 | extensions: ['.js', '.coffee', '.litcoffee', '.coffee.md'] 79 | } 80 | if coffeelint?.coffeescript? 81 | coffeelint._coffeescript = coffeelint.coffeescript 82 | coffeelint.coffeescript = resolve coffeelint.coffeescript, { 83 | basedir: dir, 84 | extensions: ['.js', '.coffee', '.litcoffee', '.coffee.md'] 85 | } 86 | 87 | config 88 | 89 | extendConfig = (config) -> 90 | unless config.extends 91 | return config 92 | 93 | parentConfig = require config.extends 94 | extendedConfig = {} 95 | 96 | for ruleName, rule of config 97 | extendedConfig[ruleName] = rule 98 | for ruleName, rule of parentConfig 99 | extendedConfig[ruleName] = config[ruleName] or rule 100 | 101 | return extendedConfig 102 | 103 | 104 | exports.getConfig = (filename = null) -> 105 | if filename 106 | dir = path.dirname(path.resolve(filename)) 107 | else 108 | dir = process.cwd() 109 | 110 | config = getConfig(dir) 111 | 112 | if config 113 | config = extendConfig(config) 114 | config = expandModuleNames(dir, config) 115 | 116 | config 117 | -------------------------------------------------------------------------------- /src/error_report.coffee: -------------------------------------------------------------------------------- 1 | # A summary of errors in a CoffeeLint run. 2 | module.exports = class ErrorReport 3 | 4 | constructor: (@coffeelint) -> 5 | @paths = {} 6 | 7 | lint: (filename, source, config = {}, literate = false) -> 8 | @paths[filename] = @coffeelint.lint(source, config, literate) 9 | 10 | getExitCode: () -> 11 | for path of @paths 12 | return 1 if @pathHasError(path) 13 | return 0 14 | 15 | getSummary: () -> 16 | pathCount = errorCount = warningCount = 0 17 | for path, errors of @paths 18 | pathCount++ 19 | for error in errors 20 | errorCount++ if error.level is 'error' 21 | warningCount++ if error.level is 'warn' 22 | return { errorCount, warningCount, pathCount } 23 | 24 | getErrors: (path) -> 25 | return @paths[path] 26 | 27 | pathHasWarning: (path) -> 28 | return @_hasLevel(path, 'warn') 29 | 30 | pathHasError: (path) -> 31 | return @_hasLevel(path, 'error') 32 | 33 | hasError: () -> 34 | for path of @paths 35 | return true if @pathHasError(path) 36 | return false 37 | 38 | _hasLevel: (path, level) -> 39 | for error in @paths[path] 40 | return true if error.level is level 41 | return false 42 | -------------------------------------------------------------------------------- /src/htmldoc.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | { RULES: rules } = require './coffeelint' 3 | 4 | render = () -> 5 | rulesHTML = '' 6 | ruleNames = Object.keys(rules).sort() 7 | for ruleName in ruleNames 8 | rule = rules[ruleName] 9 | rule.name = ruleName 10 | rule.description = '[no description provided]' unless rule.description 11 | # coffeelint: disable=no_debugger 12 | console.log ruleTemplate rule 13 | # coffeelint: enable=no_debugger 14 | 15 | ruleTemplate = _.template ''' 16 | 17 | <%= name %> 18 | 19 | <%= description %> 20 |

default level: <%= level %>

21 | 22 | 23 | ''' 24 | 25 | render() 26 | -------------------------------------------------------------------------------- /src/lexical_linter.coffee: -------------------------------------------------------------------------------- 1 | class TokenApi 2 | 3 | constructor: (CoffeeScript, source, @config, @tokensByLine) -> 4 | @tokens = CoffeeScript.tokens(source) 5 | @lines = source.split('\n') 6 | @tokensByLine = {} # A map of tokens by line. 7 | 8 | i: 0 # The index of the current token we're linting. 9 | 10 | # Return the token n places away from the current token. 11 | peek: (n = 1) -> 12 | @tokens[@i + n] || null 13 | 14 | BaseLinter = require './base_linter.coffee' 15 | 16 | # 17 | # A class that performs checks on the output of CoffeeScript's lexer. 18 | # 19 | module.exports = class LexicalLinter extends BaseLinter 20 | 21 | constructor: (source, config, rules, CoffeeScript) -> 22 | super source, config, rules 23 | 24 | @tokenApi = new TokenApi CoffeeScript, source, @config, @tokensByLine 25 | # This needs to be available on the LexicalLinter so it can be passed 26 | # to the LineLinter when this finishes running. 27 | @tokensByLine = @tokenApi.tokensByLine 28 | 29 | acceptRule: (rule) -> 30 | return typeof rule.lintToken is 'function' 31 | 32 | # Return a list of errors encountered in the given source. 33 | lint: () -> 34 | errors = [] 35 | 36 | for token, i in @tokenApi.tokens 37 | @tokenApi.i = i 38 | errors.push(error) for error in @lintToken(token) 39 | errors 40 | 41 | 42 | # Return an error if the given token fails a lint check, false otherwise. 43 | lintToken: (token) -> 44 | [type, value, { first_line: lineNumber }] = token 45 | 46 | @tokensByLine[lineNumber] ?= [] 47 | @tokensByLine[lineNumber].push(token) 48 | # CoffeeScript loses line numbers of interpolations and multi-line 49 | # regexes, so fake it by using the last line number we know. 50 | @lineNumber = lineNumber or @lineNumber or 0 51 | 52 | @tokenApi.lineNumber = @lineNumber 53 | 54 | # Multiple rules might run against the same token to build context. 55 | # Every rule should run even if something has already produced an 56 | # error for the same token. 57 | errors = [] 58 | for rule in @rules when token[0] in rule.tokens 59 | v = @normalizeResult rule, rule.lintToken(token, @tokenApi) 60 | errors.push v if v? 61 | errors 62 | 63 | createError: (ruleName, attrs = {}) -> 64 | attrs.lineNumber ?= @lineNumber 65 | attrs.lineNumber += 1 66 | attrs.line = @tokenApi.lines[attrs.lineNumber - 1] 67 | super ruleName, attrs 68 | -------------------------------------------------------------------------------- /src/line_linter.coffee: -------------------------------------------------------------------------------- 1 | 2 | class LineApi 3 | constructor: (source, @config, @tokensByLine, @literate) -> 4 | @line = null 5 | @lines = source.split('\n') 6 | @lineCount = @lines.length 7 | 8 | # maintains some contextual information 9 | # inClass: bool; in class or not 10 | # lastUnemptyLineInClass: null or lineNumber, if the last not-empty 11 | # line was in a class it holds its number 12 | # classIndents: the number of indents within a class 13 | @context = { 14 | class: { 15 | inClass: false 16 | lastUnemptyLineInClass: null 17 | classIndents: null 18 | } 19 | } 20 | 21 | lineNumber: 0 22 | 23 | isLiterate: -> @literate 24 | 25 | # maintain the contextual information for class-related stuff 26 | maintainClassContext: (line) -> 27 | if @context.class.inClass 28 | if @lineHasToken 'INDENT' 29 | @context.class.classIndents++ 30 | else if @lineHasToken 'OUTDENT' 31 | @context.class.classIndents-- 32 | if @context.class.classIndents is 0 33 | @context.class.inClass = false 34 | @context.class.classIndents = null 35 | if not line.match(/^\s*$/) 36 | @context.class.lastUnemptyLineInClass = @lineNumber 37 | else 38 | unless line.match(/\\s*/) 39 | @context.class.lastUnemptyLineInClass = null 40 | 41 | if @lineHasToken 'CLASS' 42 | @context.class.inClass = true 43 | @context.class.lastUnemptyLineInClass = @lineNumber 44 | @context.class.classIndents = 0 45 | 46 | null 47 | 48 | isLastLine: () -> 49 | return @lineNumber is @lineCount - 1 50 | 51 | # Return true if the given line actually has tokens. 52 | # Optional parameter to check for a specific token type and line number. 53 | lineHasToken: (tokenType = null, lineNumber = null) -> 54 | lineNumber = lineNumber ? @lineNumber 55 | unless tokenType? 56 | return @tokensByLine[lineNumber]? 57 | else 58 | tokens = @tokensByLine[lineNumber] 59 | return null unless tokens? 60 | for token in tokens 61 | return true if token[0] is tokenType 62 | return false 63 | 64 | # Return tokens for the given line number. 65 | getLineTokens: () -> 66 | @tokensByLine[@lineNumber] || [] 67 | 68 | 69 | BaseLinter = require './base_linter.coffee' 70 | 71 | # Some repeatedly used regular expressions. 72 | configStatement = /coffeelint:\s*((disable|enable)(-line)?)(?:=([\w\s,]*))?/ 73 | configShortcuts = [ 74 | # TODO: make this user (and / or api) configurable 75 | [/\#.*noqa/, 'coffeelint: disable-line'] 76 | ] 77 | 78 | # 79 | # A class that performs regex checks on each line of the source. 80 | # 81 | module.exports = class LineLinter extends BaseLinter 82 | 83 | @getDirective: (line) -> 84 | for [shortcut, replacement] in configShortcuts 85 | if line.match(shortcut) 86 | return configStatement.exec(replacement) 87 | return configStatement.exec(line) 88 | 89 | constructor: (source, config, rules, tokensByLine, literate = false) -> 90 | super source, config, rules 91 | 92 | @lineApi = new LineApi source, config, tokensByLine, literate 93 | 94 | # Store suppressions in the form of { line #: type } 95 | @inlineConfig = 96 | enable: {} 97 | disable: {} 98 | 'enable-line': {} 99 | 'disable-line': {} 100 | 101 | acceptRule: (rule) -> 102 | return typeof rule.lintLine is 'function' 103 | 104 | lint: () -> 105 | errors = [] 106 | for line, lineNumber in @lineApi.lines 107 | @lineApi.lineNumber = @lineNumber = lineNumber 108 | @lineApi.line = @lineApi.lines[lineNumber] 109 | @lineApi.maintainClassContext line 110 | @collectInlineConfig line 111 | 112 | errors.push(error) for error in @lintLine(line) 113 | errors 114 | 115 | # Return an error if the line contained failed a rule, null otherwise. 116 | lintLine: (line) -> 117 | 118 | # Multiple rules might run against the same line to build context. 119 | # Every every rule should run even if something has already produced an 120 | # error for the same token. 121 | errors = [] 122 | for rule in @rules 123 | v = @normalizeResult rule, rule.lintLine(line, @lineApi) 124 | errors.push v if v? 125 | errors 126 | 127 | collectInlineConfig: (line) -> 128 | # Check for block config statements enable and disable 129 | result = @constructor.getDirective(line) 130 | if result? 131 | cmd = result[1] 132 | rules = [] 133 | if result[4]? 134 | for r in result[4].split(',') 135 | rules.push r.replace(/^\s+|\s+$/g, '') 136 | @inlineConfig[cmd][@lineNumber] = rules 137 | return null 138 | 139 | 140 | createError: (rule, attrs = {}) -> 141 | attrs.lineNumber = @lineNumber + 1 # Lines are indexed by zero. 142 | attrs.level = @config[rule]?.level 143 | super rule, attrs 144 | -------------------------------------------------------------------------------- /src/reporters/checkstyle.coffee: -------------------------------------------------------------------------------- 1 | JsLintReporter = require './jslint' 2 | 3 | module.exports = class CheckstyleReporter 4 | 5 | constructor: (@errorReport, options = {}) -> 6 | { @quiet } = options 7 | 8 | print: (message) -> 9 | # coffeelint: disable=no_debugger 10 | console.log message 11 | # coffeelint: enable=no_debugger 12 | 13 | escape: JsLintReporter::escape 14 | 15 | publish: () -> 16 | @print '' 17 | @print '' 18 | 19 | for path, errors of @errorReport.paths 20 | if errors.length 21 | @print "" 22 | 23 | for e in errors when not @quiet or e.level is 'error' 24 | level = e.level 25 | level = 'warning' if level is 'warn' 26 | 27 | # context is optional, this avoids generating the string 28 | # "context: undefined" 29 | context = e.context ? '' 30 | @print """ 31 | 35 | """ 36 | @print '' 37 | 38 | @print '' 39 | -------------------------------------------------------------------------------- /src/reporters/csv.coffee: -------------------------------------------------------------------------------- 1 | module.exports = class CSVReporter 2 | 3 | constructor: (@errorReport, options = {}) -> 4 | { @quiet } = options 5 | 6 | print: (message) -> 7 | # coffeelint: disable=no_debugger 8 | console.log message 9 | # coffeelint: enable=no_debugger 10 | 11 | publish: () -> 12 | header = ['path', 'lineNumber', 'lineNumberEnd', 'level', 'message'] 13 | @print header.join(',') 14 | for path, errors of @errorReport.paths 15 | for e in errors when not @quiet or e.level is 'error' 16 | # Having the context is useful for the cyclomatic_complexity 17 | # rule and critical for the undefined_variables rule. 18 | e.message += " #{e.context}." if e.context 19 | f = [ 20 | path 21 | e.lineNumber 22 | e.lineNumberEnd ? e.lineNumberEnd 23 | e.level 24 | e.message 25 | ] 26 | @print f.join(',') 27 | -------------------------------------------------------------------------------- /src/reporters/default.coffee: -------------------------------------------------------------------------------- 1 | # Reports errors to the command line. 2 | module.exports = class Reporter 3 | 4 | constructor: (@errorReport, options = {}) -> 5 | { 6 | @colorize 7 | @quiet 8 | } = options 9 | @ok = '✓' 10 | @warn = '⚡' 11 | @err = '✗' 12 | 13 | stylize: (message, styles...) -> 14 | return message if not @colorize 15 | map = { 16 | bold: [1, 22], 17 | yellow: [33, 39], 18 | green: [32, 39], 19 | red: [31, 39] 20 | } 21 | return styles.reduce (m, s) -> 22 | return '\u001b[' + map[s][0] + 'm' + m + '\u001b[' + map[s][1] + 'm' 23 | , message 24 | 25 | publish: () -> 26 | paths = @errorReport.paths 27 | 28 | report = '' 29 | report += @reportPath(path, errors) for path, errors of paths 30 | report += @reportSummary(@errorReport.getSummary()) 31 | report += '' 32 | 33 | @print report if not @quiet or @errorReport.hasError() 34 | return this 35 | 36 | reportSummary: (s) -> 37 | start = if s.errorCount > 0 38 | "#{@err} #{@stylize('Lint!', 'red', 'bold')}" 39 | else if s.warningCount > 0 40 | "#{@warn} #{@stylize('Warning!', 'yellow', 'bold')}" 41 | else 42 | "#{@ok} #{@stylize('Ok!', 'green', 'bold')}" 43 | e = s.errorCount 44 | w = s.warningCount 45 | p = s.pathCount 46 | err = @plural('error', e) 47 | warn = @plural('warning', w) 48 | file = @plural('file', p) 49 | msg = "#{start} » #{e} #{err} and #{w} #{warn} in #{p} #{file}" 50 | return '\n' + @stylize(msg) + '\n' 51 | 52 | reportPath: (path, errors) -> 53 | [overall, color] = if hasError = @errorReport.pathHasError(path) 54 | [@err, 'red'] 55 | else if hasWarning = @errorReport.pathHasWarning(path) 56 | [@warn, 'yellow'] 57 | else 58 | [@ok, 'green'] 59 | 60 | pathReport = '' 61 | if not @quiet or hasError 62 | pathReport += " #{overall} #{@stylize(path, color, 'bold')}\n" 63 | 64 | for e in errors when not @quiet or e.level is 'error' 65 | o = if e.level is 'error' then @err else @warn 66 | lineEnd = '' 67 | lineEnd = "-#{e.lineNumberEnd}" if e.lineNumberEnd? 68 | output = '#' + e.lineNumber + lineEnd 69 | 70 | pathReport += ' ' + 71 | "#{o} #{@stylize(output, color)}: #{e.message}." 72 | pathReport += " #{e.context}." if e.context 73 | pathReport += '\n' 74 | 75 | pathReport 76 | 77 | print: (message) -> 78 | # coffeelint: disable=no_debugger 79 | console.log message 80 | # coffeelint: enable=no_debugger 81 | 82 | plural: (str, count) -> 83 | if count is 1 then str else "#{str}s" 84 | -------------------------------------------------------------------------------- /src/reporters/jslint.coffee: -------------------------------------------------------------------------------- 1 | module.exports = class JSLintReporter 2 | 3 | constructor: (@errorReport, options = {}) -> 4 | { @quiet } = options 5 | 6 | print: (message) -> 7 | # coffeelint: disable=no_debugger 8 | console.log message 9 | # coffeelint: enable=no_debugger 10 | 11 | publish: () -> 12 | @print '' 13 | 14 | for path, errors of @errorReport.paths 15 | if errors.length 16 | @print "" 17 | 18 | for e in errors when not @quiet or e.level is 'error' 19 | # continue if @quiet and e.level isnt 'error' 20 | 21 | @print """ 22 | 26 | """ 27 | @print '' 28 | 29 | @print '' 30 | 31 | escape: (msg) -> 32 | # Force msg to be a String 33 | msg = '' + msg 34 | unless msg 35 | return 36 | # Perhaps some other HTML Special Chars should be added here 37 | # But this are the XML Special Chars listed in Wikipedia 38 | replacements = [ 39 | [/&/g, '&'] 40 | [/"/g, '"'] 41 | [//g, '>'] 43 | [/'/g, '''] 44 | ] 45 | 46 | for r in replacements 47 | msg = msg.replace r[0], r[1] 48 | 49 | msg 50 | -------------------------------------------------------------------------------- /src/reporters/passthrough.coffee: -------------------------------------------------------------------------------- 1 | # this is used for testing... best not to actually use 2 | 3 | RawReporter = require './raw' 4 | 5 | module.exports = class PassThroughReporter extends RawReporter 6 | print: (input) -> 7 | return JSON.parse(input) 8 | -------------------------------------------------------------------------------- /src/reporters/raw.coffee: -------------------------------------------------------------------------------- 1 | module.exports = class RawReporter 2 | 3 | constructor: (@errorReport, options = {}) -> 4 | { @quiet } = options 5 | 6 | print: (message) -> 7 | # coffeelint: disable=no_debugger 8 | console.log message 9 | # coffeelint: enable=no_debugger 10 | 11 | publish: () -> 12 | er = {} 13 | for path, errors of @errorReport.paths 14 | er[path] = (e for e in errors when not @quiet or e.level is 'error') 15 | 16 | @print JSON.stringify(er, undefined, 2) 17 | -------------------------------------------------------------------------------- /src/ruleLoader.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | resolve = require('resolve').sync 3 | 4 | # moduleName is a NodeJS module, or a path to a module NodeJS can load. 5 | module.exports = 6 | require: (moduleName) -> 7 | try 8 | # Try to find the project-level rule first. 9 | rulePath = resolve moduleName, { 10 | basedir: process.cwd(), 11 | extensions: ['.js', '.coffee', '.litcoffee', '.coffee.md'] 12 | } 13 | return require rulePath 14 | try 15 | # Globally installed rule 16 | return require moduleName 17 | try 18 | # Maybe the user used a relative path from the command line. This 19 | # doesn't make much sense from a config file, but seems natural 20 | # with the --rules option. 21 | # 22 | # No try around this one, an exception here should abort the rest of 23 | # this function. 24 | return require path.resolve(process.cwd(), moduleName) 25 | 26 | # This was already tried once. It will definitely fail, but it will 27 | # fail with a more sensible error message than the last require() 28 | # above. 29 | require moduleName 30 | 31 | loadFromConfig: (coffeelint, config) -> 32 | for ruleName, data of config when data?.module? 33 | @loadRule(coffeelint, data.module, ruleName) 34 | 35 | # moduleName is a NodeJS module, or a path to a module NodeJS can load. 36 | loadRule: (coffeelint, moduleName, ruleName = undefined) -> 37 | try 38 | ruleModule = @require moduleName 39 | 40 | # Most rules can export as a single constructor function 41 | if typeof ruleModule is 'function' 42 | coffeelint.registerRule ruleModule, ruleName 43 | else 44 | # Or it can export an array of rules to load. 45 | for rule in ruleModule 46 | coffeelint.registerRule rule 47 | catch e 48 | # coffeelint: disable=no_debugger 49 | console.error "Error loading #{moduleName}" 50 | throw e 51 | # coffeelint: enable=no_debugger 52 | -------------------------------------------------------------------------------- /src/rules.coffee: -------------------------------------------------------------------------------- 1 | 2 | # CoffeeLint error levels. 3 | ERROR = 'error' 4 | WARN = 'warn' 5 | IGNORE = 'ignore' 6 | 7 | # CoffeeLint's default rule configuration. 8 | module.exports = 9 | 10 | coffeescript_error: 11 | level: ERROR 12 | message: '' # The default coffeescript error is fine. 13 | -------------------------------------------------------------------------------- /src/rules/arrow_spacing.coffee: -------------------------------------------------------------------------------- 1 | module.exports = class ArrowSpacing 2 | 3 | rule: 4 | name: 'arrow_spacing' 5 | level: 'ignore' 6 | message: 'Function arrows (-> and =>) must be spaced properly' 7 | description: ''' 8 |

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 |
{### comment ###}
35 | ''' 36 | 'are ignored when no_backtick rule is enabled': (source) -> 37 | config = no_backticks: { level: 'error' } 38 | errors = coffeelint.lint(source) 39 | assert.isEmpty(errors) 40 | 41 | }).export(module) 42 | -------------------------------------------------------------------------------- /test/test_no_debugger.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | vows = require 'vows' 3 | assert = require 'assert' 4 | coffeelint = require path.join('..', 'lib', 'coffeelint') 5 | 6 | RULE = 'no_debugger' 7 | 8 | vows.describe(RULE).addBatch({ 9 | 10 | 'console calls': 11 | topic: 12 | ''' 13 | console.log("hello world") 14 | ''' 15 | 16 | 'causes a warning when present': (source) -> 17 | config = no_debugger: { level: 'error', console: true } 18 | errors = coffeelint.lint(source, config) 19 | assert.isArray(errors) 20 | assert.lengthOf(errors, 1) 21 | 22 | 'The debugger statement': 23 | topic: 24 | ''' 25 | debugger 26 | ''' 27 | 28 | 'causes a warning when present': (source) -> 29 | errors = coffeelint.lint(source) 30 | assert.isArray(errors) 31 | assert.lengthOf(errors, 1) 32 | error = errors[0] 33 | assert.equal(error.level, 'warn') 34 | assert.equal(error.lineNumber, 1) 35 | assert.equal(error.rule, RULE) 36 | 37 | 'can be set to error': (source) -> 38 | config = no_debugger: level: 'error' 39 | errors = coffeelint.lint(source, config) 40 | assert.isArray(errors) 41 | assert.isArray(errors) 42 | assert.lengthOf(errors, 1) 43 | error = errors[0] 44 | assert.equal(error.lineNumber, 1) 45 | assert.equal(error.rule, RULE) 46 | 47 | }).export(module) 48 | -------------------------------------------------------------------------------- /test/test_no_empty_functions.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | vows = require 'vows' 3 | assert = require 'assert' 4 | coffeelint = require path.join('..', 'lib', 'coffeelint') 5 | 6 | RULE = 'no_empty_functions' 7 | 8 | runLint = (source) -> 9 | config = no_empty_functions: level: 'error' 10 | coffeelint.lint source, config 11 | 12 | shouldError = (source, numErrors = 1, errorNames = ['no_empty_functions']) -> 13 | topic: source 14 | 'errors for empty function': (source) -> 15 | errors = runLint source 16 | assert.lengthOf errors, numErrors, "Expected #{numErrors} errors, got 17 | [#{errors.map( (error) -> error.name).join ', '}] instead" 18 | for errorName in errorNames 19 | assert.notEqual errors.indexOf errorName, -1 20 | 21 | shouldPass = (source) -> 22 | topic: source 23 | 'does not error for empty function': (source) -> 24 | errors = runLint source 25 | assert.isEmpty errors, "Expected no errors, got 26 | [#{errors.map( (error) -> error.name).join ', '}] instead" 27 | 28 | vows.describe(RULE).addBatch({ 29 | 'empty fat-arrow function': shouldError( 30 | '=>', 2) 31 | 'empty function': shouldError( 32 | '->') 33 | 'function with undefined statement': shouldPass( 34 | '-> undefined') 35 | 'function within function with undefined statement': shouldPass( 36 | '-> -> undefined') 37 | 'empty fat arrow function within a function ': shouldError( 38 | '-> =>', 2) 39 | 'empty function within a function ': shouldError( 40 | '-> ->') 41 | "empty function as param's default value": shouldError( 42 | 'foo = (empty=(->)) -> undefined') 43 | "non-empty function as param's default value": shouldPass( 44 | 'foo = (empty=(-> undefined)) -> undefined') 45 | 'empty function with implicit instance member assignment as param': 46 | shouldError('foo = (@_fooMember) ->') 47 | 48 | }).export(module) 49 | -------------------------------------------------------------------------------- /test/test_no_empty_param_list.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | vows = require 'vows' 3 | assert = require 'assert' 4 | coffeelint = require path.join('..', 'lib', 'coffeelint') 5 | 6 | RULE = 'no_empty_param_list' 7 | 8 | vows.describe(RULE).addBatch({ 9 | 10 | 'Empty param list': 11 | topic: 12 | ''' 13 | blah = () -> 14 | ''' 15 | 16 | 'are allowed by default': (source) -> 17 | errors = coffeelint.lint(source) 18 | assert.isArray(errors) 19 | assert.isEmpty(errors) 20 | 21 | 'can be forbidden': (source) -> 22 | config = no_empty_param_list: { level: 'error' } 23 | errors = coffeelint.lint(source, config) 24 | assert.isArray(errors) 25 | assert.lengthOf(errors, 1) 26 | error = errors[0] 27 | assert.equal(error.lineNumber, 1) 28 | assert.equal(error.message, 'Empty parameter list is forbidden') 29 | assert.equal(error.rule, RULE) 30 | 31 | }).export(module) 32 | -------------------------------------------------------------------------------- /test/test_no_implicit_parens.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | vows = require 'vows' 3 | assert = require 'assert' 4 | coffeelint = require path.join('..', 'lib', 'coffeelint') 5 | 6 | RULE = 'no_implicit_parens' 7 | 8 | vows.describe(RULE).addBatch({ 9 | 10 | 'Implicit parens': 11 | topic: 12 | ''' 13 | console.log 'implict parens' 14 | blah = (a, b) -> 15 | blah 'a', 'b' 16 | 17 | class A 18 | @configure(1, 2, 3) 19 | 20 | constructor: -> 21 | 22 | class B 23 | _defaultC = 5 24 | 25 | constructor: (a) -> 26 | @c = a ? _defaultC 27 | ''' 28 | 29 | 'are allowed by default': (source) -> 30 | errors = coffeelint.lint(source) 31 | assert.isArray(errors) 32 | assert.isEmpty(errors) 33 | 34 | 'can be forbidden': (source) -> 35 | config = no_implicit_parens: { level: 'error' } 36 | errors = coffeelint.lint(source, config) 37 | assert.isArray(errors) 38 | assert.lengthOf(errors, 2) 39 | error = errors[0] 40 | assert.equal(error.lineNumber, 1) 41 | assert.equal(error.message, 'Implicit parens are forbidden') 42 | assert.equal(error.rule, RULE) 43 | 44 | 'No implicit parens strict': 45 | topic: 46 | ''' 47 | blah = (a) -> 48 | blah 49 | foo: 'bar' 50 | 51 | blah = (a, b) -> 52 | blah 'a' 53 | , 'b' 54 | ''' 55 | 56 | 'blocks all implicit parens by default': (source) -> 57 | config = no_implicit_parens: { level: 'error' } 58 | errors = coffeelint.lint(source, config) 59 | assert.isArray(errors) 60 | assert.lengthOf(errors, 2) 61 | assert.equal(rule, RULE) for { rule } in errors 62 | 63 | 'allows parens at the end of lines when strict is false': (source) -> 64 | config = 65 | no_implicit_parens: 66 | level: 'error' 67 | strict: false 68 | errors = coffeelint.lint(source, config) 69 | assert.isArray(errors) 70 | assert.isEmpty(errors) 71 | 72 | 'Nested no implicit parens strict': 73 | topic: 74 | ''' 75 | blah = (a) -> 76 | blah 77 | foo: blah('a') 78 | 79 | blah = (a, b) -> 80 | 81 | blah 'a' 82 | , blah('c', 'd') 83 | 84 | blah 'a' 85 | , (blah 'c' 86 | , 'd') 87 | ''' 88 | 89 | 'blocks all implicit parens by default': (source) -> 90 | config = no_implicit_parens: { level: 'error' } 91 | errors = coffeelint.lint(source, config) 92 | assert.isArray(errors) 93 | assert.lengthOf(errors, 4) 94 | assert.equal(rule, RULE) for { rule } in errors 95 | 96 | 'allows parens at the end of lines when strict is false': (source) -> 97 | config = 98 | no_implicit_parens: 99 | level: 'error' 100 | strict: false 101 | errors = coffeelint.lint(source, config) 102 | assert.isArray(errors) 103 | assert.isEmpty(errors) 104 | 105 | 'Test for when implicit parens are on the last line': 106 | topic: 107 | ''' 108 | class Something 109 | constructor: -> 110 | return $ '#something' 111 | 112 | yo: -> 113 | 114 | class AnotherSomething 115 | constructor: -> 116 | return $ '#something' 117 | 118 | blah 'a' 119 | , blah('c', 'd') 120 | ''' 121 | 122 | 'throws three errors when strict is true': (source) -> 123 | config = 124 | no_implicit_parens: 125 | level: 'error' 126 | strict: true 127 | 128 | errors = coffeelint.lint(source, config) 129 | assert.isArray(errors) 130 | assert.lengthOf(errors, 3) 131 | assert.equal(errors[0].rule, RULE) 132 | assert.equal(errors[1].rule, RULE) 133 | assert.equal(errors[2].rule, RULE) 134 | 135 | # When implicit parens are separated out on multiple lines 136 | # and strict is set to false, do not return an error. 137 | 'throws two errors when strict is false': (source) -> 138 | config = 139 | no_implicit_parens: 140 | level: 'error' 141 | strict: false 142 | 143 | errors = coffeelint.lint(source, config) 144 | assert.isArray(errors) 145 | assert.lengthOf(errors, 2) 146 | assert.equal(errors[0].rule, RULE) 147 | assert.equal(errors[1].rule, RULE) 148 | 149 | }).export(module) 150 | -------------------------------------------------------------------------------- /test/test_no_interpolation_in_single_quotes.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | vows = require 'vows' 3 | assert = require 'assert' 4 | coffeelint = require path.join('..', 'lib', 'coffeelint') 5 | 6 | RULE = 'no_interpolation_in_single_quotes' 7 | 8 | vows.describe(RULE).addBatch({ 9 | 10 | 'Interpolation in single quotes': 11 | topic: 12 | ''' 13 | foo = '#{inter}foo#{polation}' 14 | ''' 15 | 16 | 'interpolation in single quotes is allowed by default': (source) -> 17 | errors = coffeelint.lint(source) 18 | assert.isArray(errors) 19 | assert.isEmpty(errors) 20 | 21 | 'interpolation in single quotes can be forbidden': (source) -> 22 | config = no_interpolation_in_single_quotes: { level: 'error' } 23 | errors = coffeelint.lint(source, config) 24 | assert.lengthOf(errors, 1) 25 | error = errors[0] 26 | assert.equal(error.lineNumber, 1) 27 | assert.equal(error.rule, RULE) 28 | 29 | 'Interpolation in double quotes': 30 | topic: 31 | ''' 32 | foo = "#{inter}foo#{polation}" 33 | bar = "ive\#{escaped}" 34 | ''' 35 | 36 | 'interpolation in double quotes is always allowed': (source) -> 37 | config = no_interpolation_in_single_quotes: { level: 'error' } 38 | errors = coffeelint.lint(source, config) 39 | assert.isArray(errors) 40 | assert.isEmpty(errors) 41 | 42 | }).export(module) 43 | -------------------------------------------------------------------------------- /test/test_no_nested_string_interpolation.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | vows = require 'vows' 3 | assert = require 'assert' 4 | coffeelint = require path.join('..', 'lib', 'coffeelint') 5 | 6 | RULE = 'no_nested_string_interpolation' 7 | 8 | vows.describe(RULE).addBatch({ 9 | 10 | 'Non-nested string interpolation': 11 | topic: 12 | ''' 13 | "Book by #{firstName.toUpperCase()} #{lastName.toUpperCase()}" 14 | ''' 15 | 16 | 'is allowed': (source) -> 17 | errors = coffeelint.lint(source) 18 | assert.isArray(errors) 19 | assert.isEmpty(errors) 20 | 21 | 'Nested string interpolation': 22 | topic: 23 | ''' 24 | str = "Book by #{"#{firstName} #{lastName}".toUpperCase()}" 25 | ''' 26 | 27 | 'should generate a warning': (source) -> 28 | errors = coffeelint.lint(source) 29 | assert.isArray(errors) 30 | assert.lengthOf(errors, 1) 31 | error = errors[0] 32 | assert.equal(error.rule, RULE) 33 | assert.equal(error.lineNumber, 1) 34 | assert.equal(error.level, 'warn') 35 | assert.equal(error.message, 36 | 'Nested string interpolation is forbidden') 37 | 38 | 'can be permitted': (source) -> 39 | config = no_nested_string_interpolation: { level: 'ignore' } 40 | errors = coffeelint.lint(source, config) 41 | assert.isArray(errors) 42 | assert.isEmpty(errors) 43 | 44 | 'Deeply nested string interpolation': 45 | topic: 46 | ''' 47 | str1 = "string #{"interpolation #{"inception"}"}" 48 | str2 = "going #{"in #{"even #{"deeper"}"}"}" 49 | str3 = "#{"multiple #{"warnings"}"} for #{"diff #{"nestings"}"}" 50 | ''' 51 | 52 | 'generates only one warning per string': (source) -> 53 | errors = coffeelint.lint(source) 54 | assert.isArray(errors) 55 | assert.lengthOf(errors, 4) 56 | assert.equal(rule, RULE) for { rule } in errors 57 | assert.equal(errors[0].lineNumber, 1) 58 | assert.equal(errors[1].lineNumber, 2) 59 | assert.equal(errors[2].lineNumber, 3) 60 | assert.equal(errors[3].lineNumber, 3) 61 | 62 | }).export(module) 63 | -------------------------------------------------------------------------------- /test/test_no_plusplus.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | vows = require 'vows' 3 | assert = require 'assert' 4 | coffeelint = require path.join('..', 'lib', 'coffeelint') 5 | 6 | RULE = 'no_plusplus' 7 | 8 | vows.describe(RULE).addBatch({ 9 | 10 | 'The increment and decrement operators': 11 | topic: 12 | ''' 13 | y++ 14 | ++y 15 | x-- 16 | --x 17 | ''' 18 | 19 | 'are permitted by default': (source) -> 20 | errors = coffeelint.lint(source) 21 | assert.isArray(errors) 22 | assert.isEmpty(errors) 23 | 24 | 'can be forbidden': (source) -> 25 | errors = coffeelint.lint(source, { no_plusplus: 'level': 'error' }) 26 | assert.isArray(errors) 27 | assert.lengthOf(errors, 4) 28 | error = errors[0] 29 | assert.equal(error.lineNumber, 1) 30 | assert.equal(error.rule, RULE) 31 | 32 | }).export(module) 33 | -------------------------------------------------------------------------------- /test/test_no_private_function_fat_arrows.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | vows = require 'vows' 3 | assert = require 'assert' 4 | coffeelint = require path.join('..', 'lib', 'coffeelint') 5 | 6 | config = 7 | no_unnecessary_fat_arrows: { level: 'ignore' } 8 | no_private_function_fat_arrows: { level: 'error' } 9 | 10 | RULE = 'no_private_function_fat_arrows' 11 | 12 | vows.describe(RULE).addBatch({ 13 | 'eol': 14 | 'should warn with fat arrow': -> 15 | result = coffeelint.lint(''' 16 | class Foo 17 | foo = => 18 | ''', config) 19 | assert.equal(result.length, 1) 20 | assert.equal(result[0].rule, RULE) 21 | assert.equal(result[0].level, 'error') 22 | 23 | 'should work with nested classes': -> 24 | result = coffeelint.lint(''' 25 | class Bar 26 | foo = -> 27 | class 28 | bar2 = => 29 | ''', config) 30 | assert.equal(result.length, 1) 31 | assert.equal(result[0].rule, RULE) 32 | assert.equal(result[0].level, 'error') 33 | 34 | # Same method name as external function. 35 | result = coffeelint.lint(''' 36 | class Bar 37 | foo = -> 38 | class 39 | foo = => 40 | ''', config) 41 | assert.equal(result.length, 1) 42 | assert.equal(result[0].rule, RULE) 43 | assert.equal(result[0].level, 'error') 44 | 45 | 'should not warn without fat arrow': -> 46 | assert.isEmpty(coffeelint.lint(''' 47 | class Foo 48 | foo = -> 49 | ''', config)) 50 | 51 | }).export(module) 52 | -------------------------------------------------------------------------------- /test/test_no_stand_alone_at.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | vows = require 'vows' 3 | assert = require 'assert' 4 | coffeelint = require path.join('..', 'lib', 'coffeelint') 5 | 6 | RULE = 'no_stand_alone_at' 7 | 8 | vows.describe(RULE).addBatch({ 9 | 10 | 'Stand alone @': 11 | topic: 12 | ''' 13 | @alright 14 | @ .error 15 | @ok() 16 | @ notok 17 | @[ok] 18 | @.ok 19 | not(@).ok 20 | @::ok 21 | @:: #notok 22 | @(fn) 23 | ''' 24 | 25 | 'are allowed by default': (source) -> 26 | errors = coffeelint.lint(source) 27 | assert.isArray(errors) 28 | assert.isEmpty(errors) 29 | 30 | 'can be forbidden': (source) -> 31 | config = no_stand_alone_at: { level: 'error' } 32 | errors = coffeelint.lint(source, config) 33 | assert.isArray(errors) 34 | assert.lengthOf(errors, 3) 35 | error = errors[0] 36 | assert.equal(error.lineNumber, 4) 37 | assert.equal(error.rule, RULE) 38 | error = errors[1] 39 | assert.equal(error.lineNumber, 7) 40 | assert.equal(error.rule, RULE) 41 | error = errors[2] 42 | assert.equal(error.lineNumber, 9) 43 | assert.equal(error.rule, RULE) 44 | 45 | }).export(module) 46 | -------------------------------------------------------------------------------- /test/test_no_tabs.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | vows = require 'vows' 3 | assert = require 'assert' 4 | coffeelint = require path.join('..', 'lib', 'coffeelint') 5 | 6 | RULE = 'no_tabs' 7 | 8 | vows.describe(RULE).addBatch({ 9 | 10 | 'Tabs': 11 | topic: 12 | ''' 13 | x = () -> 14 | \ty = () -> 15 | \t\treturn 1234 16 | ''' 17 | 18 | 'can be forbidden': (source) -> 19 | errors = coffeelint.lint(source) 20 | assert.equal(errors.length, 4) 21 | error = errors[1] 22 | assert.equal(error.lineNumber, 2) 23 | assert.equal(error.message, 'Line contains tab indentation') 24 | assert.equal(error.rule, RULE) 25 | 26 | 'can be permitted': (source) -> 27 | config = 28 | no_tabs: { level: 'ignore' } 29 | indentation: { level: 'error', value: 1 } 30 | 31 | errors = coffeelint.lint(source, config) 32 | assert.equal(errors.length, 0) 33 | 34 | 'are forbidden by default': (source) -> 35 | config = indentation: { level: 'error', value: 1 } 36 | errors = coffeelint.lint(source, config) 37 | assert.isArray(errors) 38 | assert.equal(errors.length, 2) 39 | assert.equal(rule, RULE) for { rule } in errors 40 | 41 | 'are allowed in strings': () -> 42 | source = "x = () -> '\t'" 43 | errors = coffeelint.lint(source) 44 | assert.equal(errors.length, 0) 45 | 46 | 'Tabs in multi-line strings': 47 | topic: 48 | ''' 49 | x = 1234 50 | y = """ 51 | \t\tasdf 52 | """ 53 | ''' 54 | 55 | 'are ignored': (errors) -> 56 | errors = coffeelint.lint(errors) 57 | assert.isEmpty(errors) 58 | 59 | 'Tabs in Heredocs': 60 | topic: 61 | ''' 62 | ### 63 | \t\tMy Heredoc 64 | ### 65 | ''' 66 | 67 | 'are ignored': (errors) -> 68 | errors = coffeelint.lint(errors) 69 | assert.isEmpty(errors) 70 | 71 | 'Tabs in multi line regular expressions': 72 | topic: 73 | ''' 74 | /// 75 | \t\tMy Heredoc 76 | /// 77 | ''' 78 | 79 | 'are ignored': (errors) -> 80 | errors = coffeelint.lint(errors) 81 | assert.isEmpty(errors) 82 | 83 | }).export(module) 84 | -------------------------------------------------------------------------------- /test/test_no_this.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | vows = require 'vows' 3 | assert = require 'assert' 4 | coffeelint = require path.join('..', 'lib', 'coffeelint') 5 | 6 | configError = { no_this: { level: 'error' } } 7 | 8 | RULE = 'no_this' 9 | 10 | vows.describe(RULE).addBatch({ 11 | 12 | 'this': 13 | 'should warn when \'this\' is used': -> 14 | result = coffeelint.lint('this.foo()', configError)[0] 15 | assert.equal(result.lineNumber, 1) 16 | assert.equal(result.rule, RULE) 17 | 18 | '@': 19 | 'should not warn when \'@\' is used': -> 20 | assert.isEmpty(coffeelint.lint('@foo()', configError)) 21 | 22 | 'Comments': 23 | topic: 24 | ''' 25 | # this.foo() 26 | ### 27 | this.foo() 28 | ### 29 | ''' 30 | 31 | 'should not warn when \'this\' is used in a comment': (source) -> 32 | assert.isEmpty(coffeelint.lint(source, configError)) 33 | 34 | 'Strings': 35 | 'should not warn when \'this\' is used in a single-quote string': -> 36 | assert.isEmpty(coffeelint.lint('\'this.foo()\'', configError)) 37 | 38 | 'should not warn when \'this\' is used in a double-quote string': -> 39 | assert.isEmpty(coffeelint.lint('"this.foo()"', configError)) 40 | 41 | 'should not warn when \'this\' is used in a multiline string': -> 42 | source = ''' 43 | """ 44 | this.foo() 45 | """ 46 | ''' 47 | assert.isEmpty(coffeelint.lint(source, configError)) 48 | 49 | 'Compatibility with no_stand_alone_at': 50 | topic: 51 | ''' 52 | class X 53 | constructor: -> 54 | this 55 | 56 | class Y extends X 57 | constructor: -> 58 | this.hello 59 | 60 | ''' 61 | 62 | 'returns an error if no_stand_alone_at is on ignore': (source) -> 63 | config = 64 | no_stand_alone_at: 65 | level: 'ignore' 66 | no_this: 67 | level: 'error' 68 | 69 | errors = coffeelint.lint(source, config) 70 | assert.equal(errors.length, 2) 71 | error = errors[0] 72 | assert.equal(error.lineNumber, 3) 73 | assert.equal(error.rule, RULE) 74 | 75 | error = errors[1] 76 | assert.equal(error.lineNumber, 7) 77 | assert.equal(error.rule, RULE) 78 | 79 | 'returns no errors if no_stand_alone_at is on warn/error': (source) -> 80 | config = 81 | no_stand_alone_at: 82 | level: 'warn' 83 | no_this: 84 | level: 'error' 85 | 86 | errors = coffeelint.lint(source, config) 87 | assert.equal(errors.length, 1) 88 | 89 | error = errors[0] 90 | assert.equal(error.lineNumber, 7) 91 | assert.equal(error.rule, RULE) 92 | 93 | config = 94 | no_stand_alone_at: 95 | level: 'error' 96 | no_this: 97 | level: 'error' 98 | 99 | errors = coffeelint.lint(source, config) 100 | assert.equal(errors.length, 1) 101 | 102 | error = errors[0] 103 | assert.equal(error.lineNumber, 7) 104 | assert.equal(error.rule, RULE) 105 | 106 | }).export(module) 107 | -------------------------------------------------------------------------------- /test/test_no_throwing_strings.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | vows = require 'vows' 3 | assert = require 'assert' 4 | coffeelint = require path.join('..', 'lib', 'coffeelint') 5 | 6 | RULE = 'no_throwing_strings' 7 | 8 | vows.describe(RULE).addBatch({ 9 | 10 | 'Throwing strings': 11 | topic: 12 | ''' 13 | throw 'my error' 14 | throw "#{1234}" 15 | throw """ 16 | long string 17 | """ 18 | ''' 19 | 20 | 'is forbidden by default': (source) -> 21 | errors = coffeelint.lint(source) 22 | assert.lengthOf(errors, 3) 23 | error = errors[0] 24 | assert.equal(error.message, 'Throwing strings is forbidden') 25 | assert.equal(error.rule, RULE) 26 | 27 | 'can be permitted': (source) -> 28 | config = no_throwing_strings: { level: 'ignore' } 29 | errors = coffeelint.lint(source, config) 30 | assert.isEmpty(errors) 31 | 32 | }).export(module) 33 | -------------------------------------------------------------------------------- /test/test_no_trailing_semicolons.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | vows = require 'vows' 3 | assert = require 'assert' 4 | coffeelint = require path.join('..', 'lib', 'coffeelint') 5 | 6 | configError = no_trailing_semicolons: { level: 'error' } 7 | configIgnore = no_trailing_semicolons: { level: 'ignore' } 8 | 9 | RULE = 'no_trailing_semicolons' 10 | 11 | vows.describe(RULE).addBatch({ 12 | 13 | 'Semicolons at end of lines': 14 | topic: 15 | ''' 16 | x = 1234; 17 | y = 1234; z = 1234 18 | ''' 19 | 20 | 'are forbidden': (source) -> 21 | errors = coffeelint.lint(source) 22 | assert.lengthOf(errors, 1) 23 | error = errors[0] 24 | assert.equal(error.lineNumber, 1) 25 | assert.equal(error.message, 'Line contains a trailing semicolon') 26 | assert.equal(error.rule, RULE) 27 | 28 | 'can be ignored': (source) -> 29 | errors = coffeelint.lint(source, configIgnore) 30 | assert.isEmpty(errors) 31 | 32 | 'Semicolons in multiline expressions': 33 | topic: 34 | ''' 35 | x = "asdf; 36 | asdf" 37 | 38 | y1 = """ 39 | #{asdf1}; 40 | _#{asdf2}_; 41 | asdf; 42 | """ 43 | 44 | y2 = """ 45 | #{asdf1}; 46 | _#{asdf2}_; 47 | asdf; 48 | """ 49 | 50 | z = /// 51 | a*\; 52 | /// 53 | ''' 54 | 55 | 'are ignored': (source) -> 56 | errors = coffeelint.lint(source) 57 | assert.isEmpty(errors) 58 | 59 | 'Trailing semicolon in comments': 60 | topic: 61 | ''' 62 | undefined\n# comment;\nundefined 63 | ''' 64 | 65 | 'are ignored': (source) -> 66 | errors = coffeelint.lint(source, {}) 67 | assert.isEmpty(errors) 68 | 69 | 'Trailing semicolon in comments with no semicolon in statement': 70 | topic: 71 | ''' 72 | x = 3 #set x to 3; 73 | ''' 74 | 75 | 'are ignored': (source) -> 76 | errors = coffeelint.lint(source, configIgnore) 77 | assert.isEmpty(errors) 78 | 79 | 'will throw an error': (source) -> 80 | errors = coffeelint.lint(source, configError) 81 | assert.isEmpty(errors) 82 | 83 | 'Trailing semicolon in comments with semicolon in statement': 84 | topic: 85 | ''' 86 | x = 3; #set x to 3; 87 | ''' 88 | 89 | 'are ignored': (source) -> 90 | errors = coffeelint.lint(source, configIgnore) 91 | assert.isEmpty(errors) 92 | 93 | 'will throw an error': (source) -> 94 | errors = coffeelint.lint(source, configError) 95 | assert.lengthOf(errors, 1) 96 | error = errors[0] 97 | assert.equal(error.lineNumber, 1) 98 | assert.equal(error.message, 'Line contains a trailing semicolon') 99 | assert.equal(error.rule, RULE) 100 | 101 | 'Trailing semicolon in block comments': 102 | topic: 103 | ''' 104 | ###\nThis is a block comment;\n### 105 | ''' 106 | 107 | 'are ignored': (source) -> 108 | errors = coffeelint.lint(source, configIgnore) 109 | assert.isEmpty(errors) 110 | 111 | 'are ignored even if config level is error': (source) -> 112 | errors = coffeelint.lint(source, configError) 113 | assert.isEmpty(errors) 114 | 115 | 'Semicolons with windows line endings': 116 | topic: 117 | ''' 118 | x = 1234;\r\n 119 | ''' 120 | 121 | 'works as expected': (source) -> 122 | config = line_endings: { value: 'windows' } 123 | errors = coffeelint.lint(source, config) 124 | assert.lengthOf(errors, 1) 125 | error = errors[0] 126 | assert.equal(error.lineNumber, 1) 127 | assert.equal(error.message, 'Line contains a trailing semicolon') 128 | assert.equal(error.rule, RULE) 129 | 130 | 'Semicolons inside of blockquote string': 131 | topic: 132 | ''' 133 | foo = bar(); 134 | 135 | errs = """ 136 | this does not err; 137 | this does; 138 | #{isEmpty a}; 139 | #{isEmpty(b)}; 140 | """ 141 | 142 | nothing = """ 143 | this does not err; 144 | this neither; 145 | #{isEmpty(x)}; 146 | #{isEmpty y}; 147 | """ 148 | ''' 149 | 150 | 'are ignored': (source) -> 151 | errors = coffeelint.lint(source, configError) 152 | assert.lengthOf(errors, 1) 153 | error = errors[0] 154 | assert.equal(error.lineNumber, 1) 155 | assert.equal(error.message, 'Line contains a trailing semicolon') 156 | assert.equal(error.rule, RULE) 157 | 158 | }).export(module) 159 | -------------------------------------------------------------------------------- /test/test_no_trailing_whitespace.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | vows = require 'vows' 3 | assert = require 'assert' 4 | coffeelint = require path.join('..', 'lib', 'coffeelint') 5 | 6 | RULE = 'no_trailing_whitespace' 7 | 8 | vows.describe(RULE).addBatch({ 9 | 10 | 'Trailing whitespace': 11 | topic: 12 | ''' 13 | x = 1234 \ny = 1 14 | ''' 15 | 16 | 'is forbidden by default': (source) -> 17 | errors = coffeelint.lint(source) 18 | assert.equal(errors.length, 1) 19 | error = errors[0] 20 | assert.isObject(error) 21 | assert.equal(error.lineNumber, 1) 22 | assert.equal(error.message, 'Line ends with trailing whitespace') 23 | assert.equal(error.rule, RULE) 24 | 25 | 'can be permitted': (source) -> 26 | config = no_trailing_whitespace: { level: 'ignore' } 27 | errors = coffeelint.lint(source, config) 28 | assert.equal(errors.length, 0) 29 | 30 | 'Trailing whitespace in comments': 31 | topic: 32 | ''' 33 | x = 1234 # markdown comment \ny=1 34 | ''' 35 | 36 | 'is forbidden by default': (source) -> 37 | errors = coffeelint.lint(source) 38 | assert.equal(errors.length, 1) 39 | error = errors[0] 40 | assert.isObject(error) 41 | assert.equal(error.lineNumber, 1) 42 | assert.equal(error.message, 'Line ends with trailing whitespace') 43 | assert.equal(error.rule, RULE) 44 | 45 | 'can be permitted': (source) -> 46 | config = no_trailing_whitespace: { allowed_in_comments: true } 47 | errors = coffeelint.lint(source, config) 48 | assert.equal(errors.length, 0) 49 | 50 | 'a # in a string': 51 | # writen this way to preserve spacing 52 | topic: '''x = "some # string" ''' 53 | 54 | 'does not confuse trailing_whitespace': (source) -> 55 | config = no_trailing_whitespace: { allowed_in_comments: true } 56 | errors = coffeelint.lint(source, config) 57 | assert.isNotEmpty(errors) 58 | 59 | 'Trailing whitespace in block comments': 60 | topic: 61 | ''' 62 | ###\nblock comment with trailing space: \n### 63 | ''' 64 | 65 | 'is forbidden by default': (source) -> 66 | errors = coffeelint.lint(source) 67 | assert.equal(errors.length, 1) 68 | error = errors[0] 69 | assert.isObject(error) 70 | assert.equal(error.lineNumber, 2) 71 | assert.equal(error.message, 'Line ends with trailing whitespace') 72 | assert.equal(error.rule, RULE) 73 | 74 | 'can be permitted': (source) -> 75 | config = no_trailing_whitespace: { allowed_in_comments: true } 76 | errors = coffeelint.lint(source, config) 77 | assert.equal(errors.length, 0) 78 | 79 | 'On empty lines': # https://github.com/clutchski/coffeelint/issues/39 80 | topic: 81 | ''' 82 | x = 1234\n \n 83 | ''' 84 | 85 | 'allowed by default': (source) -> 86 | errors = coffeelint.lint(source) 87 | assert.equal(errors.length, 0) 88 | 89 | 'can be forbidden': (source) -> 90 | config = 91 | no_trailing_whitespace: 92 | allowed_in_empty_lines: false 93 | 94 | errors = coffeelint.lint(source, config) 95 | assert.equal(errors.length, 1) 96 | error = errors[0] 97 | assert.isObject(error) 98 | assert.equal(error.lineNumber, 2) 99 | assert.equal(error.message, 'Line ends with trailing whitespace') 100 | assert.equal(error.rule, RULE) 101 | 102 | 'Trailing tabs': 103 | topic: 104 | ''' 105 | x = 1234\t 106 | ''' 107 | 108 | 'are forbidden as well': (source) -> 109 | errors = coffeelint.lint(source) 110 | assert.equal(errors.length, 1) 111 | 112 | 'Windows line endings': 113 | topic: 114 | ''' 115 | x = 1234\r\ny = 5678 116 | ''' 117 | 118 | 'are permitted': (source) -> 119 | assert.isEmpty(coffeelint.lint(source)) 120 | 121 | }).export(module) 122 | -------------------------------------------------------------------------------- /test/test_no_unnecessary_double_quotes.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | vows = require 'vows' 3 | assert = require 'assert' 4 | coffeelint = require path.join('..', 'lib', 'coffeelint') 5 | 6 | RULE = 'no_unnecessary_double_quotes' 7 | 8 | vows.describe(RULE).addBatch({ 9 | 10 | 'Single quotes': 11 | topic: 12 | ''' 13 | foo = 'single' 14 | ''' 15 | 16 | 'single quotes should always be allowed': (source) -> 17 | config = { no_unnecessary_double_quotes: { level: 'error' } } 18 | errors = coffeelint.lint(source, config) 19 | assert.isArray(errors) 20 | assert.isEmpty(errors) 21 | 22 | 23 | 'Unnecessary double quotes': 24 | topic: 25 | ''' 26 | foo = "double" 27 | ''' 28 | 29 | 'double quotes are allowed by default': (source) -> 30 | errors = coffeelint.lint(source) 31 | assert.isArray(errors) 32 | assert.isEmpty(errors) 33 | 34 | 'double quotes can be forbidden': (source) -> 35 | config = { no_unnecessary_double_quotes: { level: 'error' } } 36 | errors = coffeelint.lint(source, config) 37 | assert.isArray(errors) 38 | assert.lengthOf(errors, 1) 39 | error = errors[0] 40 | assert.equal(error.lineNumber, 1) 41 | assert.equal(error.message, 42 | 'Unnecessary double quotes are forbidden' 43 | ) 44 | assert.equal(error.rule, RULE) 45 | 46 | 47 | 'Useful double quotes': 48 | topic: 49 | ''' 50 | interpolation = "inter#{polation}" 51 | multipleInterpolation = "#{foo}bar#{baz}" 52 | singleQuote = "single'quote" 53 | ''' 54 | 55 | 'string interpolation should always be allowed': (source) -> 56 | config = { no_unnecessary_double_quotes: { level: 'error' } } 57 | errors = coffeelint.lint(source, config) 58 | assert.isArray(errors) 59 | assert.isEmpty(errors) 60 | 61 | 62 | 'Block strings with double quotes': 63 | topic: 64 | ''' 65 | foo = """ 66 | doubleblock 67 | """ 68 | ''' 69 | 70 | 'block strings with double quotes are not allowed': (source) -> 71 | config = { no_unnecessary_double_quotes: { level: 'error' } } 72 | errors = coffeelint.lint(source, config) 73 | assert.lengthOf(errors, 1) 74 | error = errors[0] 75 | assert.equal(error.lineNumber, 1) 76 | assert.equal(error.message, 77 | 'Unnecessary double quotes are forbidden' 78 | ) 79 | assert.equal(error.rule, RULE) 80 | 81 | 82 | 'Block strings with useful double quotes': 83 | topic: 84 | ''' 85 | foo = """ 86 | #{interpolation}foo 'some single quotes for good measure' 87 | """ 88 | ''' 89 | 90 | 'block strings with useful content should be allowed': (source) -> 91 | config = { no_unnecessary_double_quotes: { level: 'error' } } 92 | errors = coffeelint.lint(source, config) 93 | assert.isArray(errors) 94 | assert.isEmpty(errors) 95 | 96 | 97 | 'Block strings with single quotes': 98 | topic: 99 | """ 100 | foo = ''' 101 | singleblock 102 | ''' 103 | """ 104 | 105 | 'block strings with single quotes should be allowed': (source) -> 106 | config = { no_unnecessary_double_quotes: { level: 'error' } } 107 | errors = coffeelint.lint(source, config) 108 | assert.isArray(errors) 109 | assert.isEmpty(errors) 110 | 111 | 112 | 'Hand concatenated string with parenthesis': 113 | topic: 114 | ''' 115 | foo = (("inter") + "polation") 116 | ''' 117 | 118 | 'double quotes should not be allowed': (source) -> 119 | config = { no_unnecessary_double_quotes: { level: 'error' } } 120 | errors = coffeelint.lint(source, config) 121 | assert.lengthOf(errors, 2) 122 | error = errors[0] 123 | assert.equal(error.lineNumber, 1) 124 | assert.equal(error.message, 125 | 'Unnecessary double quotes are forbidden' 126 | ) 127 | assert.equal(error.rule, RULE) 128 | 129 | 'use strict': 130 | topic: 131 | ''' 132 | "use strict" 133 | foo = 'foo' 134 | ''' 135 | 136 | 'should not error at the start of the file #306': (source) -> 137 | config = { no_unnecessary_double_quotes: { level: 'error' } } 138 | # Without the fix for 306 this throws an Error. 139 | errors = coffeelint.lint(source, config) 140 | assert.lengthOf(errors, 1) 141 | error = errors[0] 142 | assert.equal(error.lineNumber, 1) 143 | assert.equal(error.rule, RULE) 144 | 145 | 'Test RegExp flags #405': 146 | topic: 147 | ''' 148 | d = ///#{foo}///i 149 | ''' 150 | 151 | 'should not generate an error': (source) -> 152 | config = { no_unnecessary_double_quotes: { level: 'error' } } 153 | errors = coffeelint.lint(source, config) 154 | assert.lengthOf(errors, 0) 155 | 156 | 'Test multiline regexp #286': 157 | topic: 158 | ''' 159 | a = 'hello' 160 | b = /// 161 | .* 162 | #{a} 163 | [0-9] 164 | /// 165 | c = RegExp(".*#{a}0-9") 166 | ''' 167 | 168 | 'should not generate an error': (source) -> 169 | config = { no_unnecessary_double_quotes: { level: 'error' } } 170 | errors = coffeelint.lint(source, config) 171 | assert.lengthOf(errors, 0) 172 | 173 | }).export(module) 174 | -------------------------------------------------------------------------------- /test/test_no_unnecessary_fat_arrows.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | vows = require 'vows' 3 | assert = require 'assert' 4 | coffeelint = require path.join('..', 'lib', 'coffeelint') 5 | 6 | runLint = (source) -> 7 | config = { no_unnecessary_fat_arrows: { level: 'error' } } 8 | coffeelint.lint source, config 9 | 10 | shouldError = (source, numErrors = 1) -> 11 | topic: source 12 | 13 | 'errors for unnecessary arrow': (source) -> 14 | errors = runLint source 15 | assert.lengthOf errors, numErrors, "Expected #{numErrors} errors" 16 | error = errors[0] 17 | assert.equal error.rule, RULE 18 | 19 | shouldPass = (source) -> 20 | topic: source 21 | 'does not error for necessary arrow': (source) -> 22 | errors = runLint source 23 | assert.isEmpty errors, "Expected no errors, got #{errors}" 24 | 25 | RULE = 'no_unnecessary_fat_arrows' 26 | 27 | vows.describe(RULE).addBatch({ 28 | 29 | 'empty function': shouldError '=>' 30 | 'simple function': shouldError '=> 1' 31 | 'function with this': shouldPass '=> this' 32 | 'function with this.a': shouldPass '=> this.a' 33 | 'function with @': shouldPass '=> @' 34 | 'function with @a': shouldPass '=> @a' 35 | 36 | 'nested simple functions': 37 | 'with inner fat arrow': shouldError '-> => 1' 38 | 'with outer fat arrow': shouldError '=> -> 1' 39 | 'with both fat arrows': shouldError '=> => 1', 2 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': shouldError '=> (this; =>)' 50 | 51 | 'deeply nested simple function': shouldError '-> -> -> -> => 1' 52 | 'deeply nested function with this': shouldPass '-> -> -> -> => this' 53 | 54 | 'functions with multiple statements': shouldError ''' 55 | f = -> 56 | x = 2 57 | z ((a) => x; a) 58 | ''' 59 | 60 | 'functions with parameters': shouldError '(a) =>' 61 | 'functions with parameter assignment': shouldPass '(@a) =>' 62 | 'functions with destructuring parameter assignment': shouldPass '({@a}) =>' 63 | 64 | 'RequireJS modules containing classes with static methods': shouldPass ''' 65 | define [], -> 66 | class MyClass 67 | @omg: -> 68 | 69 | ''' 70 | 71 | }).export(module) 72 | -------------------------------------------------------------------------------- /test/test_non_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 = 'non_empty_constructor_needs_parens' 7 | 8 | vows.describe(RULE).addBatch({ 9 | 10 | 'Missing Parentheses on "new Foo 1, 2"': 11 | topic: 12 | ''' 13 | class Foo 14 | 15 | a = new Foo 16 | b = new Foo() 17 | # Warn about missing parens here 18 | c = new Foo 1, 2 19 | d = new Foo 20 | config: 'parameter' 21 | e = new bar.foo.Foo 1, 2 22 | f = new bar.foo.Foo 23 | config: 'parameter' 24 | # But not here 25 | g = new Foo(1, 2) 26 | h = new Foo( 27 | config: 'parameter' 28 | ) 29 | i = new bar.foo.Foo(1, 2) 30 | j = new bar.foo.Foo( 31 | config: 'parameter' 32 | ) 33 | ''' 34 | 35 | 'warns about missing parens': (source) -> 36 | config = 37 | non_empty_constructor_needs_parens: 38 | level: 'error' 39 | 40 | errors = coffeelint.lint(source, config) 41 | assert.equal(errors.length, 4) 42 | assert.equal(errors[0].lineNumber, 6) 43 | assert.equal(errors[1].lineNumber, 7) 44 | assert.equal(errors[2].lineNumber, 9) 45 | assert.equal(errors[3].lineNumber, 10) 46 | assert.equal(rule, RULE) for { rule } in errors 47 | 48 | }).export(module) 49 | -------------------------------------------------------------------------------- /test/test_prefer_english_operator.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | vows = require 'vows' 3 | assert = require 'assert' 4 | coffeelint = require path.join('..', 'lib', 'coffeelint') 5 | 6 | configError = { prefer_english_operator: { level: 'error' } } 7 | 8 | RULE = 'prefer_english_operator' 9 | 10 | vows.describe(RULE).addBatch({ 11 | 12 | 'non-English operators': 13 | 'should warn when == is used': -> 14 | result = coffeelint.lint('1 == 1', configError)[0] 15 | assert.equal result.context, 'Replace "==" with "is"' 16 | 17 | 'should warn when != is used': -> 18 | result = coffeelint.lint('1 != 1', configError)[0] 19 | assert.equal result.context, 'Replace "!=" with "isnt"' 20 | 21 | 'should warn when && is used': -> 22 | result = coffeelint.lint('1 && 1', configError)[0] 23 | assert.equal result.context, 'Replace "&&" with "and"' 24 | 25 | 'should warn when || is used': -> 26 | result = coffeelint.lint('1 || 1', configError)[0] 27 | assert.equal result.context, 'Replace "||" with "or"' 28 | 29 | 'should warn when ! is used': -> 30 | result = coffeelint.lint('x = !y', configError)[0] 31 | assert.equal result.context, 'Replace "!" with "not"' 32 | 33 | 'double not (!!)': 34 | 'is ignored by default': -> 35 | result = coffeelint.lint('x = !!y', configError) 36 | assert.equal(result.length, 0) 37 | 38 | 'can be configred at an independent level': -> 39 | configError = 40 | prefer_english_operator: 41 | level: 'error' 42 | doubleNotLevel: 'warn' 43 | 44 | result = coffeelint.lint('x = !!y', configError) 45 | assert.equal(result.length, 1) 46 | assert.equal(result[0].level, 'warn') 47 | assert.equal(result[0].rule, RULE) 48 | 49 | 'English operators': 50 | 'should not warn when \'is\' is used': -> 51 | assert.isEmpty(coffeelint.lint('1 is 1', configError)) 52 | 53 | 'should not warn when \'isnt\' is used': -> 54 | assert.isEmpty(coffeelint.lint('1 isnt 1', configError)) 55 | 56 | 'should not warn when \'and\' is used': -> 57 | assert.isEmpty(coffeelint.lint('1 and 1', configError)) 58 | 59 | 'should not warn when \'or\' is used': -> 60 | assert.isEmpty(coffeelint.lint('1 or 1', configError)) 61 | 62 | 'Comments': -> 63 | topic: 64 | ''' 65 | # 1 == 1 66 | # 1 != 1 67 | # 1 && 1 68 | # 1 || 1 69 | ### 70 | 1 == 1 71 | 1 != 1 72 | 1 && 1 73 | 1 || 1 74 | ### 75 | ''' 76 | 77 | 'should not warn when == is used in a comment': (source) -> 78 | assert.isEmpty(coffeelint.lint(source, configError)) 79 | 80 | 'Strings': 81 | 'should not warn when == is used in a single-quote string': -> 82 | assert.isEmpty(coffeelint.lint('\'1 == 1\'', configError)) 83 | 84 | 'should not warn when == is used in a double-quote string': -> 85 | assert.isEmpty(coffeelint.lint('"1 == 1"', configError)) 86 | 87 | 'should not warn when == is used in a multiline string': -> 88 | source = ''' 89 | """ 90 | 1 == 1 91 | """ 92 | ''' 93 | assert.isEmpty(coffeelint.lint(source, configError)) 94 | 95 | }).export(module) 96 | -------------------------------------------------------------------------------- /test/test_reporters.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | vows = require 'vows' 3 | assert = require 'assert' 4 | 5 | ### 6 | # # This will work from 3rd party code 7 | # coffeelint = require 'coffeelint' 8 | # RawReporter = require 'coffeelint/lib/reporters/raw' 9 | ### 10 | 11 | coffeelint = require path.join('..', 'lib', 'coffeelint') 12 | PassThroughReporter = require path.join('..', 'lib', 'reporters', 'passthrough') 13 | 14 | vows.describe('reporters').addBatch({ 15 | 16 | 'Can be used by 3rd party projects': 17 | topic: 18 | ''' 19 | if true 20 | undefined 21 | ''' 22 | 23 | '(example)': (code) -> 24 | 25 | # Grab your own ErrorReport 26 | errorReport = coffeelint.getErrorReport() 27 | # Lint your files, no need to save the results. 28 | # They're captured inside the ErrorReport. 29 | errorReport.lint 'stdin', code 30 | 31 | # Construct a new reporter and publish the results. 32 | # You can use the built in reporters, or make your own. 33 | reporter = new PassThroughReporter errorReport 34 | result = reporter.publish() 35 | 36 | assert.equal(result.stdin.length, 1) 37 | error = result.stdin[0] 38 | assert.equal(error.name, 'indentation') 39 | 40 | }).export(module) 41 | -------------------------------------------------------------------------------- /test/test_space_operators.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | vows = require 'vows' 3 | assert = require 'assert' 4 | coffeelint = require path.join('..', 'lib', 'coffeelint') 5 | 6 | RULE = 'space_operators' 7 | 8 | vows.describe(RULE).addBatch({ 9 | 10 | 'No spaces around binary operators': 11 | topic: 12 | ''' 13 | x= 1 14 | 1+ 1 15 | 1- 1 16 | 1/ 1 17 | 1* 1 18 | 1== 1 19 | 1>= 1 20 | 1> 1 21 | 1< 1 22 | 1<= 1 23 | 1% 1 24 | (a= 'b') -> a 25 | a| b 26 | a& b 27 | a*= -5 28 | a*= -b 29 | a*= 5 30 | a*= a 31 | -a+= -2 32 | -a+= -a 33 | -a+= 2 34 | -a+= a 35 | a* -b 36 | a** b 37 | a// b 38 | a%% b 39 | x =1 40 | 1 +1 41 | 1 -1 42 | 1 /1 43 | 1 *1 44 | 1 ==1 45 | 1 >=1 46 | 1 >1 47 | 1 <1 48 | 1 <=1 49 | 1 %1 50 | (a ='b') -> a 51 | a |b 52 | a &b 53 | a *=-5 54 | a *=-b 55 | a *=5 56 | a *=a 57 | -a +=-2 58 | -a +=-a 59 | -a +=2 60 | -a +=a 61 | a *-b 62 | a **b 63 | a //b 64 | a %%b 65 | x=1 66 | 1+1 67 | 1-1 68 | 1/1 69 | 1*1 70 | 1==1 71 | 1>=1 72 | 1>1 73 | 1<1 74 | 1<=1 75 | 1%1 76 | (a='b') -> a 77 | a|b 78 | a&b 79 | a*=-5 80 | a*=-b 81 | a*=5 82 | a*=a 83 | -a+=-2 84 | -a+=-a 85 | -a+=2 86 | -a+=a 87 | a*-b 88 | a**b 89 | a//b 90 | a%%b 91 | ''' 92 | 93 | 'are permitted by default': (source) -> 94 | config = { no_nested_string_interpolation: { level: 'ignore' } } 95 | errors = coffeelint.lint(source, config) 96 | assert.isEmpty(errors) 97 | 98 | 'can be forbidden': (source) -> 99 | config = 100 | space_operators: { level: 'error' }, 101 | no_nested_string_interpolation: { level: 'ignore' } 102 | 103 | errors = coffeelint.lint(source, config) 104 | sources = source.split('\n') 105 | 106 | assert.equal(err.line, sources[i]) for err, i in errors 107 | assert.equal(errors.length, sources.length) 108 | 109 | error = errors[0] 110 | assert.equal(error.rule, RULE) 111 | assert.equal(error.lineNumber, 1) 112 | assert.equal(error.message, 'Operators must be spaced properly') 113 | 114 | 'Correctly spaced operators': 115 | topic: 116 | ''' 117 | x = 1 118 | 1 + 1 119 | 1 - 1 120 | 1 / 1 121 | 1 * 1 122 | 1 == 1 123 | 1 >= 1 124 | 1 > 1 125 | 1 < 1 126 | 1 <= 1 127 | (a = 'b') -> a 128 | +1 129 | -1 130 | y = -2 131 | x = -1 132 | y = x++ 133 | x = y++ 134 | 1 + (-1) 135 | -1 + 1 136 | x(-1) 137 | x(-1, 1, -1) 138 | x[..-1] 139 | x[-1..] 140 | x[-1...-1] 141 | 1 < -1 142 | a if -1 143 | a unless -1 144 | a if -1 and 1 145 | a if -1 or 1 146 | 1 and -1 147 | 1 or -1 148 | "#{a}#{b}" 149 | "#{"#{a}"}#{b}" 150 | [+1, -1] 151 | [-1, +1] 152 | {a: -1} 153 | /// #{a} /// 154 | if -1 then -1 else -1 155 | a | b 156 | a & b 157 | a *= 5 158 | a *= -5 159 | a *= b 160 | a *= -b 161 | -a *= 5 162 | -a *= -5 163 | -a *= b 164 | -a *= -b 165 | a * -b 166 | a ** b 167 | a // b 168 | a %% b 169 | return -1 170 | for x in xs by -1 then x 171 | 172 | switch x 173 | when -1 then 42 174 | ''' 175 | 176 | 'are permitted': (source) -> 177 | config = 178 | space_operators: { level: 'error' }, 179 | no_nested_string_interpolation: { level: 'ignore' } 180 | 181 | errors = coffeelint.lint(source, config) 182 | assert.isEmpty(errors) 183 | 184 | 'Spaces around unary operators': 185 | topic: 186 | ''' 187 | + 1 188 | - - 1 189 | ''' 190 | 191 | 'are permitted by default': (source) -> 192 | errors = coffeelint.lint(source) 193 | assert.isEmpty(errors) 194 | 195 | 'can be forbidden': (source) -> 196 | config = 197 | space_operators: { level: 'error' }, 198 | no_nested_string_interpolation: { level: 'ignore' } 199 | 200 | errors = coffeelint.lint(source, config) 201 | assert.lengthOf(errors, 2) 202 | assert.equal(rule, RULE) for { rule } in errors 203 | 204 | }).export(module) 205 | -------------------------------------------------------------------------------- /test/test_spacing_after_comma.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | vows = require 'vows' 3 | assert = require 'assert' 4 | coffeelint = require path.join('..', 'lib', 'coffeelint') 5 | 6 | RULE = 'spacing_after_comma' 7 | 8 | vows.describe(RULE).addBatch({ 9 | 10 | 'regex': 11 | topic: 12 | ''' 13 | ///^#{ inputValue }///i.test field.name 14 | ''' 15 | 16 | 'should not error': (source) -> 17 | config = { spacing_after_comma: { level: 'warn' } } 18 | errors = coffeelint.lint(source, config) 19 | assert.equal(errors.length, 0) 20 | 21 | 'Whitespace after commas': 22 | topic: 23 | ''' 24 | doSomething(foo = ',',bar)\nfooBar() 25 | ''' 26 | 27 | 'permitted by default': (source) -> 28 | errors = coffeelint.lint(source) 29 | assert.equal(errors.length, 0) 30 | 31 | 'can be forbidden': (source) -> 32 | config = { spacing_after_comma: { level: 'warn' } } 33 | errors = coffeelint.lint(source, config) 34 | assert.equal(errors.length, 1) 35 | error = errors[0] 36 | assert.equal(error.lineNumber, 1) 37 | assert.equal(error.message, 'a space is required after commas') 38 | assert.equal(error.rule, RULE) 39 | 40 | 'newline after commas': 41 | topic: 42 | ''' 43 | multiLineFuncCall( 44 | arg1, 45 | arg2, 46 | arg3 47 | ) 48 | ''' 49 | 50 | 'should not issue warns': (source) -> 51 | config = { spacing_after_comma: { level: 'warn' } } 52 | errors = coffeelint.lint(source, config) 53 | assert.equal(errors.length, 0) 54 | 55 | }).export(module) 56 | -------------------------------------------------------------------------------- /test/test_transform_messes_up_line_numbers.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | vows = require 'vows' 3 | assert = require 'assert' 4 | coffeelint = require path.join('..', 'lib', 'coffeelint') 5 | 6 | fs = require('fs') 7 | thisdir = path.dirname(fs.realpathSync(__filename)) 8 | 9 | prefix = path.join(thisdir, 'fixtures', 'prefix_transform.coffee') 10 | cloud = path.join(thisdir, 'fixtures', 'cloud_transform.coffee') 11 | 12 | vows.describe('transform_messes_up_line_numbers').addBatch({ 13 | 14 | 'transform_messes_up_line_numbers': 15 | topic: 16 | ''' 17 | console.log('Hello cloud') 18 | ''' 19 | 20 | 'will warn if the number of lines changes': (source) -> 21 | config = 22 | coffeelint: 23 | transforms: [prefix, cloud] 24 | 25 | errors = coffeelint.lint(source, config) 26 | assert.equal(errors.length, 1) 27 | assert.equal(errors[0].name, 'transform_messes_up_line_numbers') 28 | 29 | "will not warn if the number of lines doesn't change": (source) -> 30 | config = 31 | coffeelint: 32 | transforms: [cloud] 33 | 34 | errors = coffeelint.lint(source, config) 35 | assert.equal(errors.length, 0) 36 | 37 | }).export(module) 38 | -------------------------------------------------------------------------------- /vowsrunner.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('coffeescript/register') 3 | require('./node_modules/vows/bin/vows') 4 | 5 | --------------------------------------------------------------------------------