├── .gitignore ├── .nvmrc ├── CONTRIBUTING.MD ├── LICENSE ├── README.md ├── bin ├── cli.js ├── publish.js └── test-runner.js ├── circle.yml ├── index.js ├── package.json └── src ├── plugins ├── declaration-block-max-declarations │ ├── index.js │ └── test.js ├── else-placement │ ├── index.js │ └── test.js ├── import-path-filename-extension │ ├── index.js │ └── test.js ├── import-path-leading-underscore │ ├── index.js │ └── test.js ├── name-format │ ├── index.js │ └── test.js ├── no-at-debug │ ├── index.js │ └── test.js └── url-format │ ├── index.js │ └── test.js └── stylelint-config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 6 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.MD: -------------------------------------------------------------------------------- 1 | # Welcome! 2 | 3 | We're so glad you're thinking about contributing to an 18F open source project! If you're unsure about anything, just ask — or submit your issue or pull request anyway. The worst that can happen is we'll politely ask you to change something. We appreciate all friendly contributions. 4 | 5 | One of our goals is to ensure a welcoming environment for all contibutors to our projects. Our staff follows the [18F Code of Conduct](https://github.com/18F/code-of-conduct/blob/master/code-of-conduct.md), and all contributors should do the same. 6 | 7 | We encourage you to read this project's CONTRIBUTING policy (you are here), and its [LICENSE](../LICENSE.md) and [README](../README.md). 8 | 9 | If you have any questions or want to read more, check out the [18F Open Source Policy GitHub repository]( https://github.com/18f/open-source-policy), or just [shoot us an email](mailto:18f@gsa.gov). 10 | 11 | ## Guidelines 12 | 13 | 14 | ### Submitting a pull request 15 | 16 | Here are a few guidelines to follow when submitting a pull request: 17 | 18 | 1. Create a GitHub account or sign in to your existing account. 19 | 1. Fork this repo into your GitHub account (or just clone it if you're an 18F team member). Read more about forking a repo here on GitHub: 20 | [https://help.github.com/articles/fork-a-repo/](https://help.github.com/articles/fork-a-repo/) 21 | 1. Create a branch that lightly defines what you're working on (for example, add-styles). 22 | 2. Write tests for your plugin, and ensure they pass. 23 | 3. Submit your pull request against the `master` branch. 24 | 25 | Have questions or need help with setup? Open an issue here [https://github.com/18F/18F-stylelint](https://github.com/18F/18F-stylelint). 26 | 27 | 28 | ### The rest of this project is in the public domain 29 | 30 | The rest of this project is in the worldwide [public domain](LICENSE.md). 31 | 32 | This project is in the public domain within the United States, and 33 | copyright and related rights in the work worldwide are waived through 34 | the [CC0 1.0 Universal public domain dedication](https://creativecommons.org/publicdomain/zero/1.0/). 35 | 36 | All contributions to this project will be released under the CC0 37 | dedication. By submitting a pull request, you are agreeing to comply 38 | with this waiver of copyright interest. 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | CC0 1.0 Universal 2 | 3 | Statement of Purpose 4 | 5 | The laws of most jurisdictions throughout the world automatically confer 6 | exclusive Copyright and Related Rights (defined below) upon the creator and 7 | subsequent owner(s) (each and all, an "owner") of an original work of 8 | authorship and/or a database (each, a "Work"). 9 | 10 | Certain owners wish to permanently relinquish those rights to a Work for the 11 | purpose of contributing to a commons of creative, cultural and scientific 12 | works ("Commons") that the public can reliably and without fear of later 13 | claims of infringement build upon, modify, incorporate in other works, reuse 14 | and redistribute as freely as possible in any form whatsoever and for any 15 | purposes, including without limitation commercial purposes. These owners may 16 | contribute to the Commons to promote the ideal of a free culture and the 17 | further production of creative, cultural and scientific works, or to gain 18 | reputation or greater distribution for their Work in part through the use and 19 | efforts of others. 20 | 21 | For these and/or other purposes and motivations, and without any expectation 22 | of additional consideration or compensation, the person associating CC0 with a 23 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright 24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work 25 | and publicly distribute the Work under its terms, with knowledge of his or her 26 | Copyright and Related Rights in the Work and the meaning and intended legal 27 | effect of CC0 on those rights. 28 | 29 | 1. Copyright and Related Rights. A Work made available under CC0 may be 30 | protected by copyright and related or neighboring rights ("Copyright and 31 | Related Rights"). Copyright and Related Rights include, but are not limited 32 | to, the following: 33 | 34 | i. the right to reproduce, adapt, distribute, perform, display, communicate, 35 | and translate a Work; 36 | 37 | ii. moral rights retained by the original author(s) and/or performer(s); 38 | 39 | iii. publicity and privacy rights pertaining to a person's image or likeness 40 | depicted in a Work; 41 | 42 | iv. rights protecting against unfair competition in regards to a Work, 43 | subject to the limitations in paragraph 4(a), below; 44 | 45 | v. rights protecting the extraction, dissemination, use and reuse of data in 46 | a Work; 47 | 48 | vi. database rights (such as those arising under Directive 96/9/EC of the 49 | European Parliament and of the Council of 11 March 1996 on the legal 50 | protection of databases, and under any national implementation thereof, 51 | including any amended or successor version of such directive); and 52 | 53 | vii. other similar, equivalent or corresponding rights throughout the world 54 | based on applicable law or treaty, and any national implementations thereof. 55 | 56 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, 57 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and 58 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright 59 | and Related Rights and associated claims and causes of action, whether now 60 | known or unknown (including existing as well as future claims and causes of 61 | action), in the Work (i) in all territories worldwide, (ii) for the maximum 62 | duration provided by applicable law or treaty (including future time 63 | extensions), (iii) in any current or future medium and for any number of 64 | copies, and (iv) for any purpose whatsoever, including without limitation 65 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes 66 | the Waiver for the benefit of each member of the public at large and to the 67 | detriment of Affirmer's heirs and successors, fully intending that such Waiver 68 | shall not be subject to revocation, rescission, cancellation, termination, or 69 | any other legal or equitable action to disrupt the quiet enjoyment of the Work 70 | by the public as contemplated by Affirmer's express Statement of Purpose. 71 | 72 | 3. Public License Fallback. Should any part of the Waiver for any reason be 73 | judged legally invalid or ineffective under applicable law, then the Waiver 74 | shall be preserved to the maximum extent permitted taking into account 75 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver 76 | is so judged Affirmer hereby grants to each affected person a royalty-free, 77 | non transferable, non sublicensable, non exclusive, irrevocable and 78 | unconditional license to exercise Affirmer's Copyright and Related Rights in 79 | the Work (i) in all territories worldwide, (ii) for the maximum duration 80 | provided by applicable law or treaty (including future time extensions), (iii) 81 | in any current or future medium and for any number of copies, and (iv) for any 82 | purpose whatsoever, including without limitation commercial, advertising or 83 | promotional purposes (the "License"). The License shall be deemed effective as 84 | of the date CC0 was applied by Affirmer to the Work. Should any part of the 85 | License for any reason be judged legally invalid or ineffective under 86 | applicable law, such partial invalidity or ineffectiveness shall not 87 | invalidate the remainder of the License, and in such case Affirmer hereby 88 | affirms that he or she will not (i) exercise any of his or her remaining 89 | Copyright and Related Rights in the Work or (ii) assert any associated claims 90 | and causes of action with respect to the Work, in either case contrary to 91 | Affirmer's express Statement of Purpose. 92 | 93 | 4. Limitations and Disclaimers. 94 | 95 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 96 | surrendered, licensed or otherwise affected by this document. 97 | 98 | b. Affirmer offers the Work as-is and makes no representations or warranties 99 | of any kind concerning the Work, express, implied, statutory or otherwise, 100 | including without limitation warranties of title, merchantability, fitness 101 | for a particular purpose, non infringement, or the absence of latent or 102 | other defects, accuracy, or the present or absence of errors, whether or not 103 | discoverable, all to the greatest extent permissible under applicable law. 104 | 105 | c. Affirmer disclaims responsibility for clearing rights of other persons 106 | that may apply to the Work or any use thereof, including without limitation 107 | any person's Copyright and Related Rights in the Work. Further, Affirmer 108 | disclaims responsibility for obtaining any necessary consents, permissions 109 | or other rights required for any use of the Work. 110 | 111 | d. Affirmer understands and acknowledges that Creative Commons is not a 112 | party to this document and has no duty or obligation with respect to this 113 | CC0 or use of the Work. 114 | 115 | For more information, please see 116 | 117 | 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # stylelint-rules 3 | A style (CSS, Sass) linter for the 18F style guide 4 | 5 | The aim of this module is to present a sensible set of linting defaults for 6 | front-end projects, without requiring an additional dependency on ruby. It 7 | leverages stylelint and postCSS to perform many of the same linting functions as 8 | scss-lint. 9 | 10 | --- 11 | 12 | ## Usage Information 13 | 14 | To get started, run `npm install --save-dev @18f/stylelint-rules`. This adds the module 15 | to your project and saves the dependency to your package.json. 16 | 17 | stylelint-rules provides two ways to run its linter: via the command line, and 18 | via gulp. 19 | 20 | ### Run via gulp 21 | To run the linter using gulp, import the module into your gulpfile: 22 | `var stylelint = require('@18f/stylelint-rules');` 23 | 24 | The stylelint function accepts two arguments: 25 | * `files`: **required** A glob of files you want to lint. For example './src/scss/\*\*/*.scss' 26 | 27 | * `options`: **optional** An object of configuration options. 28 | ``` 29 | { 30 | syntax: Syntax the linter validates against. Valid options are `scss|css|less`. Defaults to scss 31 | ignore: A glob (or array of globs) of files the linter should ignore, 32 | config: A path to a stylelint config file. File should use the same 33 | conventions as the config file found in this repository, exporting a 34 | single javascript object. 35 | 36 | } 37 | ``` 38 | 39 | Example gulp task: 40 | 41 | ``` 42 | var lintFunction = stylelint('./src/css/**/*.scss', { 43 | ignore: 'some/lib/**/*.scss' 44 | }); 45 | 46 | gulp.task('my-lint-task', lintFunction); 47 | ``` 48 | 49 | ### Run via the command line 50 | The linter can also be run using the command line. The script is installed in 51 | the .bin folder of your node_modules directory. The only required argument to the script is a glob of directories (or path to a single file) to be linted. 52 | 53 | For example: `node_modules/.bin/18f-stylelint-rules "./path/to/sass/**/*.scss"` 54 | 55 | Additionally, the CLI exposes the following options: 56 | 57 | ``` 58 | -s, --syntax [scss|css|less], Linter syntax. Defaults to scss. 59 | -i, --ignore-files [string], Glob of directories or files to ignore 60 | -f, --formatter [verbose|json|string], Output formatter. Defaults to verbose. 61 | -c, --config [rules], Path to a js file that exports an object describing additional rules. 62 | ``` 63 | 64 | ## Contributing 65 | Please see [CONTRIBUTING.md](./CONTRIBUTING.md). 66 | -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var program = require('commander'); 3 | var path = require('path'); 4 | var stylelint = require('stylelint'); 5 | var formatters = require('stylelint/dist/formatters'); 6 | var lintConfig = require('../src/stylelint-config'); 7 | 8 | program 9 | .version('0.0.0') 10 | .usage('[options] ') 11 | .option('-s, --syntax ', 'Linter syntax. Defaults to scss.') 12 | .option('-i, --ignore-files ', 'Glob of directories or files to ignore') 13 | .option('-f, --formatter ', 'Output formatter. Defaults to verbose.') 14 | .option('-c, --config ', 'Path to a js file that exports an object describing additional rules.') 15 | .parse(process.argv); 16 | 17 | var files = program.args.pop(); 18 | 19 | if (!files) { 20 | console.log('You must supply the path of files to lint.'); 21 | process.exit(); 22 | } 23 | 24 | var formatter = program.formatter || 'verbose'; 25 | var ignoredFiles = program['ignore-files']; 26 | 27 | if (ignoredFiles) { 28 | lintConfig['ignoreFiles'] = ignoredFiles; 29 | } 30 | 31 | stylelint.lint({ 32 | files: files, 33 | config: program.config || lintConfig, 34 | configBasedir: path.join(__dirname, '../', './src'), 35 | syntax: program.syntax || 'scss', 36 | formatter: formatter 37 | }).then(function(output) { 38 | var outputFormatter = formatters[formatter]; 39 | console.log(outputFormatter && outputFormatter(output.results)); 40 | if (output.errored) { 41 | process.exit(1); 42 | } 43 | }).catch(function(err) { 44 | console.log(err); 45 | process.exit(1); 46 | }); 47 | -------------------------------------------------------------------------------- /bin/publish.js: -------------------------------------------------------------------------------- 1 | var execSync = require('child_process').execSync; 2 | 3 | try { 4 | execSync('npm publish --access=public'); 5 | } catch(e) { 6 | console.warn(e.message); 7 | process.exit(0); 8 | } 9 | -------------------------------------------------------------------------------- /bin/test-runner.js: -------------------------------------------------------------------------------- 1 | var glob = require('glob'); 2 | var path = require('path'); 3 | var exec = require('child_process').exec; 4 | 5 | function makeTest(file) { 6 | exec('node ' + file + ' | node_modules/.bin/tap-dot', function(err, stdout, stderr) { 7 | console.log(stdout); 8 | }); 9 | } 10 | 11 | glob(path.join(__dirname, '../', 'src/plugins/**/test.js'), function(err, matches) { 12 | if (err) { 13 | console.log('Error encountered: ', err, '\n\n\n\n'); 14 | } 15 | 16 | matches.forEach(makeTest); 17 | }); 18 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 6 4 | deployment: 5 | npm: 6 | branch: master 7 | commands: 8 | - echo -e "$NPM_USERNAME\n$NPM_PASSWORD\n$NPM_EMAIL" | npm login 9 | - npm run publish 10 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var gulp = require('gulp'); 3 | var gulpStylelint = require('gulp-stylelint'); 4 | var stylelint = require('stylelint'); 5 | var lintConfig = require('./src/stylelint-config'); 6 | 7 | module.exports = function(files, options) { 8 | options = (typeof options === 'object' && options) || {}; 9 | 10 | if (!files || typeof files !== 'string') { 11 | throw new Error('File to lint must be supplied'); 12 | } 13 | 14 | var lintConfig = require('./src/stylelint-config'); 15 | 16 | if (options.ignore) { 17 | lintConfig['ignoreFiles'] = options.ignore; 18 | } 19 | 20 | if (typeof options.config === 'string' && options.config) { 21 | lintConfig = require(path.resolve(__dirname, options.config)); 22 | } 23 | 24 | return function() { 25 | return gulp 26 | .src(files) 27 | .pipe(gulpStylelint({ 28 | config: lintConfig, 29 | configBasedir: path.join(__dirname, './src'), 30 | syntax: options.syntax || 'scss', 31 | reporters: [ 32 | {formatter: 'verbose', console: true} 33 | ] 34 | })); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@18f/stylelint-rules", 3 | "version": "2.0.0", 4 | "description": "A style (CSS, Sass) linter for the 18F style guide", 5 | "main": "index.js", 6 | "bin": { 7 | "18f-stylelint-rules": "bin/cli.js" 8 | }, 9 | "scripts": { 10 | "test": "node bin/test-runner.js", 11 | "publish": "node bin/publish.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/18F/stylelint-rules.git" 16 | }, 17 | "author": "Adam Biagianti (adam.biagianti@gsa.gov)", 18 | "license": "SEE LICENSE IN LICENSE.md", 19 | "bugs": { 20 | "url": "https://github.com/18F/stylelint-rules/issues" 21 | }, 22 | "dependencies": { 23 | "commander": "^2.9.0", 24 | "concat-map": "0.0.1", 25 | "glob": "^7.0.3", 26 | "gulp-stylelint": "^2.0.2", 27 | "stylelint": "^6.4.1" 28 | }, 29 | "devDependencies": { 30 | "gulp": "^3.9.1", 31 | "stylelint-test-rule-tape": "^0.2.0", 32 | "tap-dot": "^1.0.5" 33 | }, 34 | "homepage": "https://github.com/18F/stylelint-rules#readme" 35 | } 36 | -------------------------------------------------------------------------------- /src/plugins/declaration-block-max-declarations/index.js: -------------------------------------------------------------------------------- 1 | var stylelint = require('stylelint'); 2 | var utils = stylelint.utils; 3 | 4 | var ruleName = 'plugin/declaration-block-max-declarations'; 5 | var messages = { 6 | expected: function(actualPropCount, expectedPropCount) { 7 | return 'Expected no more than ' + expectedPropCount + ' declaration(s). Found ' + actualPropCount + '.'; 8 | } 9 | }; 10 | 11 | function isNumber(maybeNumber){ 12 | return typeof maybeNumber === 'number' && 13 | !isNaN(maybeNumber) && 14 | isFinite(maybeNumber); 15 | } 16 | 17 | var pluginDefinition = stylelint.createPlugin(ruleName, function(maxDeclarations, options) { 18 | var maxDeclInt = parseInt(maxDeclarations); 19 | 20 | return function(css, result) { 21 | var validOptions = utils.validateOptions(result, ruleName, { 22 | actual: maxDeclInt, 23 | possible: [isNumber] 24 | }); 25 | 26 | function checkMaxDeclarationsInBlock(statement) { 27 | // ignore any statements like imports or extends, we only want property 28 | // declarations. 29 | var statementDecls = statement.nodes.filter(function(node) { 30 | return node.type === 'decl'; 31 | }); 32 | var blockDeclarations = statementDecls.length; 33 | 34 | if (blockDeclarations > maxDeclInt) { 35 | utils.report({ 36 | node: statement, 37 | message: messages.expected(blockDeclarations, maxDeclInt), 38 | result: result, 39 | ruleName: ruleName 40 | }); 41 | } 42 | } 43 | 44 | if (!validOptions) { 45 | return; 46 | } 47 | 48 | css.walkRules(checkMaxDeclarationsInBlock); 49 | } 50 | }); 51 | 52 | module.exports = pluginDefinition; 53 | module.exports.ruleName = ruleName; 54 | module.exports.messages = messages; 55 | -------------------------------------------------------------------------------- /src/plugins/declaration-block-max-declarations/test.js: -------------------------------------------------------------------------------- 1 | var testRule = require('stylelint-test-rule-tape'); 2 | var propertyCount = require('./index'); 3 | 4 | testRule(propertyCount.rule, { 5 | ruleName: propertyCount.ruleName, 6 | config: [10], 7 | skipBasicChecks: true, 8 | accept: [{ 9 | code: '.foo { color: blue; border: 1px solid white; height: 10px; weight: 10px;}' 10 | }, { 11 | code: '.foo { @mixin my-mixin(10); background: black; color: white; display: inline-block; float: left; font-family: sans-serif; font-weight: 12px; height: 10px; line-height: 36px; vertical-align: middle; width: 10px;}' 12 | }, { 13 | code: '.foo { @mixin my-mixin(10); &.bar { color: pink; } color: white; display: inline-block; float: left; font-family: sans-serif; font-weight: 12px; height: 10px; line-height: 36px; vertical-align: middle; width: 10px;}' 14 | }], 15 | 16 | reject: [{ 17 | code: '.foo { background: black; color: white; display: inline-block; float: left; font-family: sans-serif; font-weight: 12px; height: 10px; line-height: 36px; vertical-align: middle; width: 10px; z-index: 1;}', 18 | message: propertyCount.messages.expected(11, 10) 19 | }] 20 | }); 21 | -------------------------------------------------------------------------------- /src/plugins/else-placement/index.js: -------------------------------------------------------------------------------- 1 | var stylelint = require('stylelint'); 2 | var utils = stylelint.utils; 3 | 4 | var ruleName = 'plugin/else-placement'; 5 | var messages = utils.ruleMessages(ruleName, { 6 | rejectedSameLine: '@else should be on the same line as the preceding \}', 7 | rejectedNextLine: '@else should be on a new line after the preceding \}' 8 | }); 9 | 10 | function hasNewLine(string) { 11 | return /[\r\n]/.test(string); 12 | } 13 | 14 | var pluginDefinition = stylelint.createPlugin(ruleName, function(enabled, options) { 15 | return function(css, result) { 16 | var validOptions = utils.validateOptions(result, ruleName, { 17 | actual: enabled, 18 | possible: ['same-line', 'next-line'] 19 | }); 20 | 21 | if (!validOptions) { 22 | return; 23 | } 24 | 25 | css.walkAtRules(function(statement) { 26 | var raw = statement.raws; 27 | 28 | // ignore everything but an else statement 29 | if (statement.name !== 'else') { 30 | return; 31 | } 32 | 33 | if (enabled === 'same-line' && hasNewLine(raw.before)) { 34 | utils.report({ 35 | node: statement, 36 | message: messages.rejectedSameLine, 37 | result: result, 38 | ruleName: ruleName 39 | }); 40 | } 41 | 42 | if (enabled === 'next-line' && !hasNewLine(raw.before)) { 43 | utils.report({ 44 | node: statement, 45 | message: messages.rejectedNextLine, 46 | result: result, 47 | ruleName: ruleName 48 | }); 49 | } 50 | }); 51 | } 52 | }); 53 | 54 | module.exports = pluginDefinition; 55 | module.exports.ruleName = ruleName; 56 | module.exports.messages = messages; 57 | -------------------------------------------------------------------------------- /src/plugins/else-placement/test.js: -------------------------------------------------------------------------------- 1 | var testRule = require('stylelint-test-rule-tape'); 2 | var elsePlacement = require('./index.js'); 3 | 4 | /** Test if @else at-rule is on the same line as the ending '}' of 5 | * an @if statement. Rejects based on the presence of a newline character, 6 | * which should be sufficient. 7 | * 8 | * Accepted example: 9 | * @if { 10 | * ... 11 | * } else { 12 | * ... 13 | * } 14 | */ 15 | testRule(elsePlacement.rule, { 16 | ruleName: elsePlacement.ruleName, 17 | config: ['same-line'], 18 | accept: [{ 19 | code: '@if {} @else {}' 20 | }, 21 | { 22 | code: '@if {} \t @else {}' 23 | }], 24 | 25 | reject: [{ 26 | code: '@if { } \n @else { }', 27 | message: elsePlacement.messages.rejectedSameLine 28 | }, { 29 | code: '@if { } \r @else { }', 30 | message: elsePlacement.messages.rejectedSameLine 31 | }], 32 | }); 33 | 34 | // Again check for the presence of a newline character, rejecting 35 | // if the newline isn't present, i.e.: 36 | // @if { 37 | // ... 38 | // } else { 39 | // ... 40 | // } 41 | testRule(elsePlacement.rule, { 42 | ruleName: elsePlacement.ruleName, 43 | skipBasicChecks: true, 44 | config: ['next-line'], 45 | accept: [{ 46 | code: '@if {} \n @else {}' 47 | }], 48 | 49 | reject: [{ 50 | code: '@if { } @else { }', 51 | message: elsePlacement.messages.rejectedNextLine 52 | }] 53 | }); 54 | -------------------------------------------------------------------------------- /src/plugins/import-path-filename-extension/index.js: -------------------------------------------------------------------------------- 1 | var stylelint = require('stylelint'); 2 | var utils = stylelint.utils; 3 | var path = require('path'); 4 | 5 | var ruleName = 'plugin/import-path-filename-extension'; 6 | var messages = utils.ruleMessages(ruleName, { 7 | expected: 'Expected extension in @import directive', 8 | rejected: 'Unexpected extension in @import directive' 9 | }); 10 | 11 | var pluginDefinition = stylelint.createPlugin(ruleName, function(expected, options) { 12 | return function (css, result) { 13 | var validOptions = utils.validateOptions(result, ruleName, { 14 | actual: expected, 15 | possible: [true, false] 16 | }); 17 | 18 | if (!validOptions) { 19 | return; 20 | } 21 | 22 | css.walkAtRules('import', function(atRule) { 23 | var extName = path.extname(atRule.params); 24 | 25 | // ignore css imports 26 | if (extName.indexOf('.css') !== -1) { 27 | return; 28 | } 29 | 30 | if (!expected) { 31 | // @import ends with a file extension, and shouldn't 32 | if (extName) { 33 | utils.report({ 34 | node: atRule, 35 | message: messages.rejected, 36 | result: result, 37 | ruleName: ruleName 38 | }); 39 | } 40 | } else { 41 | // @import does not end with a file extension and should 42 | if (!extName) { 43 | utils.report({ 44 | node: atRule, 45 | message: messages.expected, 46 | result: result, 47 | ruleName: ruleName 48 | }); 49 | } 50 | } 51 | }); 52 | }; 53 | }); 54 | 55 | module.exports = pluginDefinition; 56 | module.exports.ruleName = ruleName; 57 | module.exports.messages = messages; 58 | -------------------------------------------------------------------------------- /src/plugins/import-path-filename-extension/test.js: -------------------------------------------------------------------------------- 1 | var testRule = require('stylelint-test-rule-tape'); 2 | var importPath = require('./index'); 3 | 4 | testRule(importPath.rule, { 5 | ruleName: importPath.ruleName, 6 | skipBasicChecks: true, 7 | config: false, 8 | accept: [{ 9 | code: '@import "valid-file"' 10 | }], 11 | reject: [ 12 | { 13 | code: '@import "invalid-file.scss"', 14 | message: importPath.messages.rejected 15 | } 16 | ], 17 | description: '@import statement should not contain a file extension' 18 | }); 19 | 20 | testRule(importPath.rule, { 21 | ruleName: importPath.ruleName, 22 | skipBasicChecks: true, 23 | config: [true], 24 | accept: [{ 25 | code: '@import "valid-file.css"' 26 | }], 27 | reject: [ 28 | { 29 | code: '@import "invalid-file"', 30 | message: importPath.messages.expected 31 | } 32 | ], 33 | description: '@import statement should contain a file extension' 34 | }); 35 | -------------------------------------------------------------------------------- /src/plugins/import-path-leading-underscore/index.js: -------------------------------------------------------------------------------- 1 | var stylelint = require('stylelint'); 2 | var utils = stylelint.utils; 3 | var path = require('path'); 4 | 5 | 6 | var ruleName = 'plugin/import-path-leading-underscore'; 7 | var messages = utils.ruleMessages(ruleName, { 8 | expected: 'Expected leading underscore in @import directive', 9 | rejected: 'Unexpected leading underscore in @import directive' 10 | }); 11 | 12 | var startsWithUnderscore = new RegExp(/^["']?_/); 13 | 14 | function report(node, message, result) { 15 | utils.report({ 16 | node: node, 17 | message: message, 18 | result: result, 19 | ruleName: ruleName 20 | }); 21 | } 22 | 23 | var pluginDefinition = stylelint.createPlugin(ruleName, function(requireUnderscore, options) { 24 | return function (css, result) { 25 | var validOptions = utils.validateOptions(result, ruleName, { 26 | actual: requireUnderscore, 27 | possible: [true, false] 28 | }); 29 | 30 | if (!validOptions) { 31 | return; 32 | } 33 | 34 | function checkAtRules(atRule) { 35 | var pathInfo = path.parse(atRule.params); 36 | var fileToImport = pathInfo.base; 37 | var extName = pathInfo.ext; 38 | 39 | // ignore css imports 40 | if (extName.indexOf('.css') !== -1) { 41 | return; 42 | } 43 | 44 | if (!requireUnderscore) { 45 | // @import starts with an underscore, and shouldn't 46 | if (startsWithUnderscore.test(fileToImport)) { 47 | report(atRule, messages.rejected, result); 48 | } 49 | } else { 50 | // @import does not start with an underscore, and should 51 | if (!startsWithUnderscore.test(fileToImport)) { 52 | report(atRule, messages.expected, result); 53 | } 54 | } 55 | } 56 | 57 | css.walkAtRules('import', checkAtRules); 58 | }; 59 | }); 60 | 61 | module.exports = pluginDefinition; 62 | module.exports.ruleName = ruleName; 63 | module.exports.messages = messages; 64 | -------------------------------------------------------------------------------- /src/plugins/import-path-leading-underscore/test.js: -------------------------------------------------------------------------------- 1 | var testRule = require('stylelint-test-rule-tape'); 2 | var importPath = require('./index'); 3 | 4 | testRule(importPath.rule, { 5 | ruleName: importPath.ruleName, 6 | skipBasicChecks: true, 7 | config: [false], 8 | reject: [ 9 | { 10 | code: '@import "_some-file"', 11 | message: importPath.messages.rejected 12 | }, 13 | { 14 | code: '@import "patah/to/_some-file"', 15 | message: importPath.messages.rejected 16 | } 17 | ] 18 | }); 19 | 20 | testRule(importPath.rule, { 21 | ruleName: importPath.ruleName, 22 | skipBasicChecks: true, 23 | config: [true], 24 | reject: [ 25 | { 26 | code: '@import "some-file"', 27 | message: importPath.messages.expected 28 | }, 29 | { 30 | code: '@import "path/to/some-file"', 31 | message: importPath.messages.expected 32 | } 33 | ] 34 | }); 35 | -------------------------------------------------------------------------------- /src/plugins/name-format/index.js: -------------------------------------------------------------------------------- 1 | var stylelint = require('stylelint'); 2 | var utils = stylelint.utils; 3 | 4 | var ruleName = 'plugin/name-format'; 5 | var messages = { 6 | rejected: function(convention) { 7 | return 'Functions, mixins, variables, and placeholders should be declared using ' + convention; 8 | }, 9 | rejectedUnderscore: 'Function, mixins, and variable names should not be begin with a leading underscore' 10 | }; 11 | 12 | 13 | // If a string doesn't match this regex, it is valid and won't generate a 14 | // linting error 15 | function getFormatRegex(convention) { 16 | var regex; 17 | 18 | switch (convention) { 19 | case 'hyphenated-lowercase': 20 | regex = /[_A-Z][^(]/; 21 | break; 22 | default: 23 | // always opt out of the regex 24 | regex = {test: function() { return false;}} 25 | } 26 | 27 | return regex; 28 | } 29 | 30 | /** 31 | * Matches at-rule to list of at-rules this plugin cares about 32 | * @param {String} ruleName The at-rule to be tested 33 | * @return {Boolean} Returns true if the at-rule is in list of at-rules 34 | */ 35 | function atRuleWhitelistMatch(ruleName) { 36 | return ['function', 'mixin'].indexOf(ruleName) !== -1; 37 | } 38 | 39 | /** 40 | * Test if a given string is a scss variable 41 | * @param {String} declaration Value of the 'prop' property of a Declaration 42 | * @return {Boolean} 43 | */ 44 | function isVariable(declaration) { 45 | return /^\$/.test(declaration); 46 | } 47 | 48 | function hasLeadingUnderscore(declaration) { 49 | var declSansVariable = declaration.split('$').pop(); 50 | return /^_/.test(declSansVariable); 51 | } 52 | 53 | function report(statement, message, result, ruleName) { 54 | utils.report({ 55 | node: statement, 56 | message: message, 57 | result: result, 58 | ruleName: ruleName 59 | }); 60 | } 61 | 62 | var pluginDefinition = stylelint.createPlugin(ruleName, function(enabled, options) { 63 | return function(css, result) { 64 | var convention; 65 | var leadingUnderscore; 66 | var validOptions = utils.validateOptions(result, ruleName, { 67 | actual: enabled, 68 | possible: { 69 | 'allow-leading-underscore': [true, false], 70 | 'convention': ['hyphenated-lowercase'] 71 | } 72 | }); 73 | 74 | if (!validOptions) { 75 | return false; 76 | } 77 | 78 | // set flags into variables 79 | convention = enabled.convention; 80 | leadingUnderscore = enabled['allow-leading-underscore']; 81 | 82 | css.walk(function(statement) { 83 | var nodeType = statement.type; 84 | var formattingRegex = getFormatRegex(convention); 85 | 86 | //console.log(statement); 87 | // Check if the current node is prefixed with an '@' and is in our at-rule 88 | // whitelist 89 | if (nodeType === 'atrule' && atRuleWhitelistMatch(statement.name)) { 90 | if (convention && formattingRegex.test(statement.params)) { 91 | report(statement, messages.rejected(convention), result, ruleName); 92 | } 93 | 94 | if (!leadingUnderscore && hasLeadingUnderscore(statement.params)) { 95 | report(statement, messages.rejectedUnderscore, result, ruleName); 96 | } 97 | } 98 | 99 | // Check if the node is a variable 100 | if (nodeType === 'decl' && (statement.prop && isVariable(statement))) { 101 | if (convention && formattingRegex.test(statement.prop)) { 102 | report(statement, messages.rejected(convention), result, ruleName); 103 | } 104 | 105 | if (!leadingUnderscore && hasLeadingUnderscore(statement.prop)) { 106 | report(statement, messages.rejectedUnderscore, result, ruleName); 107 | } 108 | } 109 | }); 110 | } 111 | }); 112 | 113 | module.exports = pluginDefinition; 114 | module.exports.ruleName = ruleName; 115 | module.exports.messages = messages; 116 | -------------------------------------------------------------------------------- /src/plugins/name-format/test.js: -------------------------------------------------------------------------------- 1 | var testRule = require('stylelint-test-rule-tape'); 2 | var nameFormat = require('./index'); 3 | 4 | var conventionHL = 'hyphenated-lowercase'; 5 | 6 | testRule(nameFormat.rule, { 7 | ruleName: nameFormat.ruleName, 8 | config: [{ 9 | 'convention': conventionHL 10 | }], 11 | accept: [{ 12 | code: '@function test-fn($n1, $n2) { @return $n1 + $n2;};' 13 | }, { 14 | code: '$time-zone: #333;' 15 | }, { 16 | code: '@mixin my-mixin' 17 | }, { 18 | code: '.foo_bar {}' 19 | }, { 20 | code: '.item__list-selected {}' 21 | }, { 22 | code: '#_another-sanityCheck {}' 23 | }], 24 | reject: [{ 25 | code: '@function testFn($n1, $n2) { @return $n1 + $n2;};', 26 | message: nameFormat.messages.rejected(conventionHL) 27 | }, 28 | { 29 | code: '$myVariable: #333', 30 | message: nameFormat.messages.rejected(conventionHL) 31 | }] 32 | }); 33 | 34 | testRule(nameFormat.rule, { 35 | ruleName: nameFormat.ruleName, 36 | config: [{ 37 | 'allow-leading-underscore': true 38 | }], 39 | accept: [{ 40 | code: '@function _private-fn() {}' 41 | }, { 42 | code: '$_my-private-var: #333' 43 | }] 44 | }); 45 | 46 | testRule(nameFormat.rule, { 47 | ruleName: nameFormat.ruleName, 48 | config: [{ 49 | 'allow-leading-underscore': false 50 | }], 51 | reject: [{ 52 | code: '@function _private-fn() {}' 53 | }, { 54 | code: '$_my-private-var: #333' 55 | }] 56 | }); 57 | -------------------------------------------------------------------------------- /src/plugins/no-at-debug/index.js: -------------------------------------------------------------------------------- 1 | var stylelint = require('stylelint'); 2 | var utils = stylelint.utils; 3 | var ruleName = 'plugin/no-at-debug'; 4 | var messages = utils.ruleMessages(ruleName, { 5 | rejected: 'Remove debug statement' 6 | }); 7 | 8 | 9 | var pluginDefinition = stylelint.createPlugin(ruleName, function(enabled, options) { 10 | return function(css, result) { 11 | var validOptions = utils.validateOptions(result, ruleName, { 12 | actual: enabled, 13 | possible: [ true, false ], 14 | }, { 15 | actual: enabled, 16 | optional: true 17 | }); 18 | 19 | var warnDebug = function(atRule) { 20 | utils.report({ 21 | node: atRule, 22 | message: messages.rejected, 23 | result: result, 24 | ruleName: ruleName 25 | }); 26 | }; 27 | 28 | if (!validOptions) { 29 | console.log('no valid option') 30 | return; 31 | } 32 | 33 | css.walkAtRules('debug', warnDebug); 34 | }; 35 | }); 36 | 37 | module.exports = pluginDefinition; 38 | module.exports.ruleName = ruleName; 39 | module.exports.messages = messages; 40 | -------------------------------------------------------------------------------- /src/plugins/no-at-debug/test.js: -------------------------------------------------------------------------------- 1 | var testRule = require('stylelint-test-rule-tape'); 2 | var noAtDebug = require('./index'); 3 | 4 | testRule(noAtDebug.rule, { 5 | ruleName: noAtDebug.ruleName, 6 | skipBasicChecks: true, 7 | config: [true], 8 | accept: [ 9 | // i.e. any code withough a debug statement 10 | { code: '.foo { font-family: sans-serif; }' } 11 | ], 12 | reject: [ 13 | { 14 | code: '@debug $some-variable;', 15 | message: noAtDebug.ruleMessage 16 | }, 17 | { 18 | code: '.foo { @debug $some-variable; }', 19 | message: noAtDebug.ruleMessage 20 | }, 21 | { 22 | code: '@function sum($num1, $num2){@debug $num1; @return $num1 + $num2;}', 23 | message: noAtDebug.ruleMessage 24 | }, 25 | { 26 | code: '@mixin asLink($link){@debug $link; a {color: $link;}}', 27 | message: noAtDebug.ruleMessage 28 | } 29 | ] 30 | }); 31 | -------------------------------------------------------------------------------- /src/plugins/url-format/index.js: -------------------------------------------------------------------------------- 1 | var stylelint = require('stylelint'); 2 | var utils = stylelint.utils; 3 | var url = require('url'); 4 | 5 | var ruleName = 'plugin/url-format'; 6 | var messages = utils.ruleMessages(ruleName, { 7 | rejected: function(path) { 8 | return 'path should not contain a protocol or host'; 9 | } 10 | }); 11 | 12 | function startsWithUrl(value) { 13 | return /^url\(/.test(value); 14 | } 15 | 16 | var pluginDefinition = stylelint.createPlugin(ruleName, function(enabled, options) { 17 | return function(css, result) { 18 | var validOptions = utils.validateOptions(result, ruleName, { 19 | actual: enabled, 20 | possible: [true, false] 21 | }); 22 | 23 | if (!validOptions) { 24 | return false; 25 | } 26 | 27 | css.walkDecls(function(decl) { 28 | var value = decl.value; 29 | 30 | if (value && startsWithUrl(value)) { 31 | var maybeUrl = url.parse(value.replace(/url\(/, '')); 32 | 33 | if (maybeUrl.host || maybeUrl.protocol) { 34 | utils.report({ 35 | node: decl, 36 | message:messages.rejected(value), 37 | result: result, 38 | ruleName: ruleName 39 | }); 40 | } 41 | } 42 | }); 43 | }; 44 | }); 45 | 46 | module.exports = pluginDefinition; 47 | module.exports.ruleName = ruleName; 48 | module.exports.messages = messages; 49 | -------------------------------------------------------------------------------- /src/plugins/url-format/test.js: -------------------------------------------------------------------------------- 1 | var testRule = require('stylelint-test-rule-tape'); 2 | var urlFormat = require('./index'); 3 | 4 | var badUrlProtocol = 'http://cdn.com/path/to/my/image'; 5 | var badUrlDomain = 'cdn.com/path/to/my/image'; 6 | 7 | testRule(urlFormat.rule, { 8 | ruleName: urlFormat.ruleName, 9 | config: [true], 10 | accept: [{ 11 | code: '.foo { background: url("path/to/image")}' 12 | }, { 13 | code: '.foo { background: url("//path/to/image")}', 14 | }], 15 | reject: [{ 16 | code: '.foo { background: url(' + badUrlProtocol + '); }', 17 | message: urlFormat.messages.rejected('url(' + badUrlProtocol + ')') 18 | }] 19 | }); 20 | -------------------------------------------------------------------------------- /src/stylelint-config.js: -------------------------------------------------------------------------------- 1 | var configs = { 2 | "plugins": [ 3 | "./plugins/no-at-debug", 4 | "./plugins/import-path-leading-underscore", 5 | "./plugins/else-placement", 6 | "./plugins/name-format", 7 | "./plugins/url-format" 8 | ], 9 | "rules": { 10 | "at-rule-empty-line-before": [ "always", { 11 | except: [ "blockless-group", "first-nested" ], 12 | ignore: ["after-comment"], 13 | } ], 14 | "at-rule-name-case": "lower", 15 | "at-rule-name-space-after": "always-single-line", 16 | "at-rule-semicolon-newline-after": "always", 17 | "block-closing-brace-newline-after": "always", 18 | "block-closing-brace-newline-before": "always-multi-line", 19 | "block-closing-brace-space-before": "always-single-line", 20 | "block-no-empty": true, 21 | "block-opening-brace-newline-after": "always-multi-line", 22 | "block-opening-brace-space-after": "always-single-line", 23 | "block-opening-brace-space-before": "always", 24 | "color-hex-case": "lower", 25 | "color-no-invalid-hex": true, 26 | "comment-empty-line-before": [ "always", { 27 | except: ["first-nested"], 28 | ignore: ["stylelint-commands"], 29 | } ], 30 | "declaration-bang-space-after": "never", 31 | "declaration-bang-space-before": "always", 32 | "declaration-block-no-ignored-properties": true, 33 | "declaration-block-no-shorthand-property-overrides": true, 34 | "declaration-block-semicolon-newline-after": "always-multi-line", 35 | "declaration-block-semicolon-space-after": "always-single-line", 36 | "declaration-block-semicolon-space-before": "never", 37 | "declaration-block-single-line-max-declarations": 1, 38 | "declaration-block-trailing-semicolon": "always", 39 | "declaration-colon-space-after": "always-single-line", 40 | "declaration-colon-space-before": "never", 41 | "declaration-no-important": true, 42 | "function-calc-no-unspaced-operator": true, 43 | "function-comma-space-after": "always-single-line", 44 | "function-comma-space-before": "never", 45 | "function-linear-gradient-no-nonstandard-direction": true, 46 | "function-max-empty-lines": 0, 47 | "function-name-case": "lower", 48 | "function-parentheses-space-inside": "never-single-line", 49 | "function-url-quotes": "always", 50 | "function-whitespace-after": "always", 51 | "indentation": 2, 52 | "length-zero-no-unit": true, 53 | "max-empty-lines": 1, 54 | "max-nesting-depth": 4, 55 | "media-feature-colon-space-after": "always", 56 | "media-feature-colon-space-before": "never", 57 | "media-feature-no-missing-punctuation": true, 58 | "media-feature-range-operator-space-after": "always", 59 | "media-feature-range-operator-space-before": "always", 60 | "media-query-list-comma-space-after": "always-single-line", 61 | "media-query-list-comma-space-before": "never", 62 | "media-query-parentheses-space-inside": "never", 63 | "no-eol-whitespace": true, 64 | "no-extra-semicolons": true, 65 | "no-invalid-double-slash-comments": true, 66 | "number-no-trailing-zeros": true, 67 | "property-case": "lower", 68 | "declaration-property-value-whitelist": { 69 | "/color/": ["/(\$|\#)/"] 70 | }, 71 | "plugin/no-at-debug": true, 72 | "plugin/import-path-leading-underscore": false, 73 | "plugin/else-placement": 'same-line', 74 | "plugin/name-format": { 75 | 'allow-leading-underscore': false, 76 | 'convention': 'hyphenated-lowercase' 77 | }, 78 | "plugin/url-format": true, 79 | "rule-non-nested-empty-line-before": [ "always-multi-line", { 80 | ignore: ["after-comment"], 81 | } ], 82 | "selector-attribute-brackets-space-inside": "never", 83 | "selector-attribute-operator-space-after": "never", 84 | "selector-attribute-operator-space-before": "never", 85 | "selector-combinator-space-after": "always", 86 | "selector-combinator-space-before": "always", 87 | "selector-list-comma-space-before": "never", 88 | "selector-max-empty-lines": 0, 89 | "selector-no-id": true, 90 | "selector-no-qualifying-type": true, 91 | "selector-pseudo-class-case": "lower", 92 | "selector-pseudo-class-parentheses-space-inside": "never", 93 | "selector-pseudo-element-case": "lower", 94 | "selector-pseudo-element-no-unknown": true, 95 | "selector-type-case": "lower", 96 | "string-no-newline": true, 97 | "string-quotes": "single", 98 | "unit-case": "lower", 99 | "unit-no-unknown": true, 100 | "value-list-comma-newline-after": "always-multi-line", 101 | "value-list-comma-space-after": "always-single-line", 102 | "value-list-comma-space-before": "never" 103 | } 104 | }; 105 | 106 | module.exports = configs; 107 | --------------------------------------------------------------------------------