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