├── .babelrc ├── .eslintrc ├── .fecsrc ├── .gitignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── bin └── lesslint-cli ├── package.json ├── src ├── checker.js ├── cli.js ├── config.js ├── config.yml ├── rule │ ├── block-indent.js │ ├── hex-color.js │ ├── import.js │ ├── leading-zero.js │ ├── operate-unit.js │ ├── require-after-space.js │ ├── require-around-space.js │ ├── require-before-space.js │ ├── require-newline.js │ ├── shorthand.js │ ├── single-comment.js │ ├── variable-name.js │ └── zero-unit.js └── util.js └── test ├── fixture ├── .lesslintignore ├── .lesslintrc ├── block-indent.less ├── config.json ├── config.yml ├── esui.less ├── hex-color.less ├── import.less ├── leading-zero.less ├── mixins.less ├── operate-unit.less ├── require-after-space.less ├── require-around-space.less ├── require-before-space.less ├── require-newline.less ├── shorthand.less ├── single-comment.less ├── test.less ├── vari.less ├── variable-name.less └── zero-unit.less └── spec ├── checker.spec.js ├── rule.spec.js └── util.spec.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2"] 3 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "rules": { 6 | "no-console": 0 7 | }, 8 | "parserOptions": { 9 | "ecmaVersion": 7, 10 | "sourceType": "module", 11 | "ecmaFeatures": { 12 | "jsx": true 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /.fecsrc: -------------------------------------------------------------------------------- 1 | files: 2 | - src 3 | eslint: 4 | env: 5 | es6: true 6 | rules: 7 | no-console: 0 8 | fecs-camelcase: 9 | - 2 10 | - 11 | ignore: 12 | - "/-_/" 13 | fecs-min-vars-per-destructure: false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | npm-debug.log 4 | Thumbs.db 5 | .DS_Store 6 | *.swp 7 | *bak* 8 | lib 9 | coverage 10 | output -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext": true, 3 | "camelcase": true, 4 | "devel": true, 5 | "expr": true, 6 | "curly": true, 7 | "eqeqeq": true, 8 | "forin": true, 9 | "immed": true, 10 | "latedef": "nofunc", 11 | "newcap": true, 12 | "noarg": true, 13 | "nonew": true, 14 | "quotmark": "single", 15 | "sub": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": false, 19 | "globalstrict": true, 20 | "trailing": true, 21 | "browser": true, 22 | "maxparams": 5, 23 | "maxdepth": 5, 24 | "maxstatements": 25, 25 | "maxcomplexity": 10, 26 | "laxbreak": true, 27 | "node": true, 28 | "predef": [ 29 | "System" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | npm-debug.log 4 | Thumbs.db 5 | .DS_Store 6 | *.swp 7 | *.bak 8 | src 9 | .babelignore 10 | .gitignore 11 | .npmignore 12 | coverage 13 | output -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | - "4" 5 | - "6" 6 | after_script: 7 | - npm run coveralls 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | === 3 | 4 | #### 2016.10.27 5 | 6 | 1. 修复了一些 bug。 7 | 2. 修改了读取配置的逻辑,现在 `.lesslintrc` 配置文件中可以是 JSON 也可以是 YAML 了。 8 | 9 | #### 2016.10.21 10 | 11 | 1. 修改了 block-indent 规则了实现方式。 12 | 2. 修复了一些 bug。 13 | 14 | #### 2016.09.07 15 | 16 | 1. 重构,切换底层解析器。 17 | 18 | #### 2015.12.10 19 | 20 | 1. 读取文件流,提高性能。 21 | 2. block-indent: 分析 mixin 时,由于 AST 上没有 index 属性,因此无法获取行号,默认会获取到最后一行,因此行内容部准确,会造成错误。修改的方法是把 mixin 在全局匹配获取 index,根据这个 index 去获取行号和行内容。 22 | 3. 获取属性名称时,如果 name 是数组,那么 name.reduce 循环时 item 可能没有 toCSS 方法,本次加上了一个后备方案。 23 | 4. variable-name: 修复了 `@{aa}@{bb}@{cc}: value;` 的问题 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Lesslint 2 | === 3 | 4 | [![lesslint](https://travis-ci.org/ecomfe/node-lesslint.svg?branch=master)](https://travis-ci.org/ecomfe/node-lesslint) 5 | [![npm version](https://badge.fury.io/js/lesslint.svg)](http://badge.fury.io/js/lesslint) 6 | [![Coverage Status](https://img.shields.io/coveralls/ecomfe/node-lesslint.svg?style=flat)](https://coveralls.io/r/ecomfe/node-lesslint) 7 | [![Dependency Status](https://david-dm.org/ecomfe/node-lesslint.png)](https://david-dm.org/ecomfe/node-lesslint) 8 | [![devDependency Status](https://david-dm.org/ecomfe/node-lesslint/dev-status.png)](https://david-dm.org/ecomfe/node-lesslint#info=devDependencies) 9 | 10 | Lesslint 是一个基于 NodeJS 以及 EDP 的一个 lint 工具,使用它可以 `lint` 你的 less code,目前的 lint 规则是基于 ecomfe 的[Less编码规范 [1.0]](https://github.com/ecomfe/spec/blob/master/less-code-style.md)。 11 | 12 | 经过了一段时间的重构,终于来到这个全新的版本。在这个版本中,`less` 解析器切换成 [postcss](https://github.com/postcss/postcss) 以及一个 [less 的解析插件](https://github.com/webschik/postcss-less)。这个版本里,改变了实现方式,没有依赖 Less 本身的 parser 以及 visitor 来进行解析,因此性能较以前的版本有比较大的提升。(这是个重构版本,因此并未对功能上做扩充,下个版本会对功能上做一些扩充,尽请期待~) 13 | 14 | 具体的配置参见 [config](https://github.com/ecomfe/node-lesslint/blob/master/lib/config.js) 15 | 16 | 已经实现的 lint 规则: 17 | 18 | + [@import 检验](https://github.com/ecomfe/spec/blob/master/less-code-style.md#import-%E8%AF%AD%E5%8F%A5):@import 语句引用的文件必须(MUST)写在一对引号内,.less 后缀不得(MUST NOT)省略(与引入 CSS 文件时的路径格式一致)。引号使用 ' 和 " 均可,但在同一项目内必须(MUST)统一。`import` 19 | 20 | + [颜色检验](https://github.com/ecomfe/spec/blob/master/less-code-style.md#%E9%A2%9C%E8%89%B2):颜色定义必须(MUST)使用 #RRGGBB 格式定义,并在可能时尽量(SHOULD)缩写为 #RGB 形式,且避免直接使用颜色名称与 rgb() 表达式。`hex-color`, `shorthand` 21 | 22 | + [注释检验](https://github.com/ecomfe/spec/blob/master/less-code-style.md#%E6%B3%A8%E9%87%8A):单行注释尽量使用 // 方式。`single-comment` 23 | 24 | + [数值检验](https://github.com/ecomfe/spec/blob/master/less-code-style.md#%E6%95%B0%E5%80%BC):对于处于 (0, 1) 范围内的数值,小数点前的 0 可以(MAY)省略,同一项目中必须(MUST)保持一致。`leading-zero` 25 | 26 | + [选择器检验](https://github.com/ecomfe/spec/blob/master/less-code-style.md#%E9%80%89%E6%8B%A9%E5%99%A8):当多个选择器共享一个声明块时,每个选择器声明必须(MUST)独占一行。`require-newline` 27 | 28 | + [变量检验](https://github.com/ecomfe/spec/blob/master/less-code-style.md#%E5%8F%98%E9%87%8F):变量命名必须(MUST)采用 @foo-bar 形式,不得(MUST NOT)使用 @fooBar 形式。`variable-name` 29 | 30 | 31 | 32 | 33 | 34 | + [0 值检验](https://github.com/ecomfe/spec/blob/master/less-code-style.md#0-%E5%80%BC):属性值为 0 时,必须省略可省的单位(长度单位如 px、em,不包括时间、角度等如 s、deg)。`zero-unit` 35 | 36 | + [运算](https://github.com/ecomfe/spec/blob/master/less-code-style.md#%E8%BF%90%E7%AE%97):+ / - / * / / 四个运算符两侧必须(MUST)保留一个空格。+ / - 两侧的操作数必须(MUST)有相同的单位,如果其中一个是变量,另一个数值必须(MUST)书写单位。`require-around-space`, `operate-unit` 37 | 38 | + [属性、变量](https://github.com/ecomfe/spec/blob/master/less-code-style.md#%E5%B1%9E%E6%80%A7%E5%8F%98%E9%87%8F):选择器和 { 之间必须(MUST)保留一个空格。`require-before-space` 39 | 40 | + [缩进](https://github.com/ecomfe/spec/blob/master/less-code-style.md#%E5%B5%8C%E5%A5%97%E5%92%8C%E7%BC%A9%E8%BF%9B):必须(MUST)采用 4 个空格为一次缩进, 不得(MUST NOT)采用 TAB 作为缩进。`block-indent` 41 | 42 | + [属性、变量](https://github.com/ecomfe/spec/blob/master/less-code-style.md#%E5%B1%9E%E6%80%A7%E5%8F%98%E9%87%8F):属性名后的冒号(:)与属性值之间必须(MUST)保留一个空格,冒号前不得(MUST NOT)保留空格;定义变量时冒号(:)与变量值之间必须(MUST)保留一个空格,冒号前不得(MUST NOT)保留空格。`require-after-space` 43 | 44 | 安装与更新 45 | ------- 46 | 47 | lesslint 已发布到 npm 上,可通过如下命令安装。`-g`是必选项。 48 | 49 | $ [sudo] npm install lesslint -g 50 | 51 | 升级 lesslint 请用如下命令。 52 | 53 | $ [sudo] npm update lesslint -g 54 | 55 | 56 | 使用 57 | ------ 58 | 59 | lesslint 目前就一条命令,后面带 `-v` 参数,会显示版本信息;后面带目录或者文件名就会对目录或文件执行 lesslint。 60 | 61 | $ lesslint -v // 显示版本信息 62 | $ lesslint [filePath|dirPath] // 对 file 或 dir 执行 lesslint 63 | 64 | 65 | TODO 66 | ------ 67 | 68 | 1. 覆盖更多的规则,现在还未实现的规则如下: 69 | 70 | `disallow-mixin-name-space`, `vendor-prefixes-sort`, `extend-must-firstline` 71 | 72 | 73 | 74 | ### [CHANGELOG](https://github.com/ecomfe/node-lesslint/blob/master/CHANGELOG.md) -------------------------------------------------------------------------------- /bin/lesslint-cli: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | var cli = require('../lib/cli'); 4 | cli.parse(process.argv); 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lesslint", 3 | "description": "lint your less code", 4 | "version": "1.0.4", 5 | "maintainers": [ 6 | { 7 | "name": "ielgnaw", 8 | "email": "wuji0223@gmail.com" 9 | } 10 | ], 11 | "dependencies": { 12 | "chalk": "^1.1.3", 13 | "edp-core": "^1.0.32", 14 | "js-yaml": "^3.6.1", 15 | "less": "^2.7.1", 16 | "manis": "^0.3.0", 17 | "postcss": "^5.2.0", 18 | "postcss-less": "^0.14.0", 19 | "postcss-values-parser": "^0.1.7", 20 | "strip-json-comments": "^2.0.1" 21 | }, 22 | "devDependencies": { 23 | "babel-cli": "^6.11.4", 24 | "babel-core": "^6.13.2", 25 | "babel-istanbul": "^0.8.0", 26 | "babel-node-debug": "^2.0.0", 27 | "babel-preset-es2015": "^6.13.2", 28 | "babel-preset-stage-2": "^6.13.0", 29 | "chai": "^3.5.0", 30 | "coveralls": "^2.11.12", 31 | "debug": "^2.2.0", 32 | "fecs": "stable", 33 | "json-stringify-safe": "^5.0.1", 34 | "mocha": "^2.5.3" 35 | }, 36 | "scripts": { 37 | "lint": "fecs src test/**/*.spec.js --type=js", 38 | "compile": "rm -rf lib && ./node_modules/.bin/babel src -d lib --source-maps inline --copy-files", 39 | "debug": "npm run compile && ./node_modules/.bin/babel-node-debug lib/index.js", 40 | "test": "npm run compile && ./node_modules/.bin/_mocha --compilers js:babel-core/register --recursive", 41 | "test-single": "npm run compile && ./node_modules/.bin/babel-node ./node_modules/.bin/_mocha 'test/spec/util.spec.@(js|es|es6)'", 42 | "coverage": "npm run compile && ./node_modules/.bin/babel-node ./node_modules/.bin/babel-istanbul cover _mocha 'test/**/*.spec.@(js|es|es6)'", 43 | "coverage-single": "npm run compile && ./node_modules/.bin/babel-node ./node_modules/.bin/babel-istanbul cover _mocha 'test/spec/util.spec.@(js|es|es6)'", 44 | "coverage1": "npm run compile && ./node_modules/.bin/babel-node ./node_modules/.bin/babel-istanbul cover _mocha -- --recursive", 45 | "coveralls": "cat ./coverage/lcov.info | coveralls", 46 | "sourcemap": "./node_modules/.bin/babel src -d lib -s", 47 | "watch": "./node_modules/.bin/babel -w src -d lib", 48 | "prepublish": "npm run compile" 49 | }, 50 | "main": "./lib/checker.js", 51 | "bin": { 52 | "lesslint": "./bin/lesslint-cli" 53 | }, 54 | "repository": { 55 | "type": "git", 56 | "url": "git@github.com:ecomfe/node-lesslint" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/checker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file checker 针对 less 文件的校验器 3 | * @author ielgnaw(wuji0223@gmail.com) 4 | */ 5 | 6 | import {join} from 'path'; 7 | import {existsSync} from 'fs'; 8 | import chalk from 'chalk'; 9 | import postcssLess from 'postcss-less'; 10 | import postcss from 'postcss'; 11 | 12 | import {isIgnored} from './util'; 13 | import {loadConfig} from './config'; 14 | 15 | 'use strict'; 16 | 17 | /** 18 | * rule 逻辑实现的文件夹路径 19 | */ 20 | const ruleDir = join(__dirname, './rule'); 21 | 22 | /** 23 | * 检测 css 文件内容 24 | * 25 | * @param {string} fileContent 文件内容 26 | * @param {string} filePath 文件路径,根据这个参数来设置 less 编译时的 paths 27 | * @param {Object=} realConfig 检测规则的配置,可选 28 | * 29 | * @return {Promise} Promise 对象 30 | */ 31 | export function checkString(fileContent, filePath, realConfig) { 32 | // 这里把文件内容的 \r\n 统一替换成 \n,便于之后获取行号 33 | fileContent = fileContent.replace(/\r\n?/g, '\n'); 34 | 35 | // postcss 插件集合即规则检测的文件集合 36 | const plugins = []; 37 | 38 | Object.getOwnPropertyNames( 39 | realConfig 40 | ).forEach( 41 | function (prop) { 42 | const ruleFilePath = join(ruleDir, prop) + '.js'; 43 | if (existsSync(ruleFilePath)) { 44 | plugins.push( 45 | require(join(ruleDir, prop)).check({ 46 | ruleVal: realConfig[prop], 47 | // 实际上在 postcss 的 plugin 里面通过 node.source.input.css 也可以拿到文件内容 48 | // 但是通过这种方式拿到的内容是去掉 BOM 的,因此在检测 no-bom 规则时候会有问题 49 | // 所以这里把文件的原内容传入进去 50 | fileContent: fileContent, 51 | filePath: filePath 52 | }) 53 | ); 54 | } 55 | } 56 | ); 57 | 58 | // 不合法的信息集合 59 | const invalidList = []; 60 | 61 | const invalid = { 62 | path: '', 63 | messages: [] 64 | }; 65 | 66 | const checkPromise = new Promise((resolve, reject) => { 67 | postcss(plugins).process(fileContent, { 68 | syntax: postcssLess 69 | }).then(result => { 70 | result.warnings().forEach(data => { 71 | invalid.messages.push({ 72 | ruleName: data.ruleName, 73 | line: data.line, 74 | col: data.col, 75 | errorChar: data.errorChar || '', 76 | message: data.message, 77 | colorMessage: data.colorMessage 78 | }); 79 | if (invalid.path !== filePath) { 80 | invalid.path = filePath; 81 | invalidList.push(invalid); 82 | } 83 | }); 84 | resolve(invalidList); 85 | 86 | // const parserRet = safeStringify(result.root.toResult().root, null, 4); 87 | // const outputFile = join(__dirname, '../ast.json'); 88 | // writeFileSync(outputFile, parserRet); 89 | }).catch(e => { 90 | // 这里 catch 的是代码中的错误 91 | const str = e.toString(); 92 | invalid.messages.push({ 93 | ruleName: 'CssSyntaxError', 94 | line: e.line, 95 | col: e.column, 96 | message: str, 97 | colorMessage: chalk.red(str) 98 | }); 99 | 100 | if (invalid.path !== filePath) { 101 | invalid.path = filePath; 102 | invalidList.push(invalid); 103 | } 104 | reject(invalidList); 105 | }); 106 | }); 107 | 108 | return checkPromise; 109 | } 110 | 111 | /** 112 | * 校验文件 113 | * 114 | * @param {Object} file 包含 path 和 content 键的对象 115 | * @param {Array} errors 本分类的错误信息数组 116 | * @param {Function} done 校验完成的通知回调 117 | * 118 | * @return {Function} checkString 方法 119 | */ 120 | export function check(file, errors, done) { 121 | if (isIgnored(file.path, '.lesslintignore')) { 122 | done(); 123 | return; 124 | } 125 | 126 | /** 127 | * checkString 的 promise 的 reject 和 resolve 的返回值的结构以及处理方式都是一样的 128 | * reject 指的是 parse 本身的错误以及 ast.toCSS({}) 的错误,这些代表程序的错误。 129 | * resolve 代表的是 lesslint 检测出来的问题 130 | * 131 | * @param {Array.} invalidList 错误信息集合 132 | */ 133 | const callback = invalidList => { 134 | if (invalidList.length) { 135 | invalidList.forEach(invalid => { 136 | errors.push({ 137 | path: invalid.path, 138 | messages: invalid.messages 139 | }); 140 | }); 141 | } 142 | done(); 143 | }; 144 | 145 | return checkString(file.content, file.path, loadConfig(file.path, true)).then(callback).catch(callback); 146 | } 147 | -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 命令行功能模块 3 | * @author ielgnaw(wuji0223@gmail.com) 4 | */ 5 | 6 | import {createReadStream} from 'fs'; 7 | import chalk from 'chalk'; 8 | import {log} from 'edp-core'; 9 | import sys from '../package'; 10 | import {formatMsg, getCandidates} from './util'; 11 | import {check} from './checker'; 12 | 13 | 'use strict'; 14 | 15 | /** 16 | * 显示默认的信息 17 | */ 18 | const showDefaultInfo = () => { 19 | console.warn(sys); 20 | console.log(''); 21 | console.log((sys.name + ' v' + sys.version)); 22 | console.log(chalk.bold.green(formatMsg(sys.description))); 23 | }; 24 | 25 | 26 | /** 27 | * 校验结果报告 28 | * 29 | * @inner 30 | * @param {Object} errors 按文件类型为 key,值为对应的校验错误信息列表的对象 31 | */ 32 | const report = errors => { 33 | let t12 = true; 34 | 35 | if (errors.length) { 36 | errors.forEach(error => { 37 | log.info(error.path); 38 | error.messages.sort((left, right) => { 39 | return left.line - right.line; 40 | }); 41 | error.messages.forEach(message => { 42 | const ruleName = message.ruleName || ''; 43 | let msg = '→ ' + (ruleName ? chalk.bold(ruleName) + ': ' : ''); 44 | // 全局性的错误可能没有位置信息 45 | if (typeof message.line === 'number') { 46 | msg += ('line ' + message.line); 47 | if (typeof message.col === 'number') { 48 | msg += (', col ' + message.col); 49 | } 50 | msg += ': '; 51 | } 52 | 53 | msg += message.colorMessage || message.message; 54 | log.warn(msg); 55 | }); 56 | }); 57 | t12 = false; 58 | } 59 | 60 | if (t12) { 61 | log.info('Congratulations! Everything gone well, you are T12!'); 62 | } 63 | else { 64 | process.exit(1); 65 | } 66 | }; 67 | 68 | 69 | /** 70 | * 解析参数。作为命令行执行的入口 71 | * 72 | * @param {Array} args 参数列表 73 | */ 74 | export function parse(args) { 75 | args = args.slice(2); 76 | 77 | // 不带参数时,默认检测当前目录下所有的 less 文件 78 | if (args.length === 0) { 79 | args.push('.'); 80 | } 81 | 82 | if (args[0] === '--version' || args[0] === '-v') { 83 | showDefaultInfo(); 84 | return; 85 | } 86 | 87 | const patterns = [ 88 | '**/*.less', 89 | '!**/{output,test,node_modules,asset,dist,release,doc,dep,report}/**' 90 | ]; 91 | 92 | const candidates = getCandidates(args, patterns); 93 | 94 | let count = candidates.length; 95 | 96 | if (!count) { 97 | return; 98 | } 99 | 100 | // 错误信息的集合 101 | const errors = []; 102 | 103 | /** 104 | * 每个文件的校验结果回调,主要用于统计校验完成情况 105 | * 106 | * @inner 107 | */ 108 | const callback = () => { 109 | count--; 110 | if (!count) { 111 | report(errors); 112 | } 113 | }; 114 | 115 | // 遍历每个需要检测的 less 文件 116 | candidates.forEach(candidate => { 117 | const readable = createReadStream(candidate, { 118 | encoding: 'utf8' 119 | }); 120 | readable.on('data', chunk => { 121 | const file = { 122 | content: chunk, 123 | path: candidate 124 | }; 125 | check(file, errors, callback); 126 | }); 127 | readable.on('error', err => { 128 | throw err; 129 | }); 130 | }); 131 | } 132 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 对配置文件的读取合并等等 3 | * @author ielgnaw(wuji0223@gmail.com) 4 | */ 5 | 6 | import Manis from 'manis'; 7 | import {join} from 'path'; 8 | import yaml from 'js-yaml'; 9 | 10 | 'use strict'; 11 | 12 | let STORAGE = null; 13 | 14 | /** 15 | * 获取 merge 后的配置文件 16 | * 用户自定义的配置文件优先级 .lesslintrc > config.yml > config.json 17 | * 18 | * @param {string} filePath 待检查的文件路径,根据这个路径去寻找用户自定义的配置文件,然后和默认的配置文件 merge 19 | * @param {boolean} refresh 是否强制刷新内存中已经存在的配置 20 | * 21 | * @return {Object} merge 后的配置对象 22 | */ 23 | export function loadConfig(filePath, refresh) { 24 | if (refresh && STORAGE) { 25 | return STORAGE; 26 | } 27 | 28 | let manis = new Manis({ 29 | files: [ 30 | '.lesslintrc' 31 | ], 32 | loader(content) { 33 | if (!content) { 34 | return ''; 35 | } 36 | let ret; 37 | try { 38 | ret = yaml.load(content); 39 | } 40 | catch (e) {} 41 | return ret; 42 | } 43 | }); 44 | 45 | manis.setDefault(join(__dirname, './config.yml')); 46 | 47 | STORAGE = manis.from(filePath); 48 | 49 | return STORAGE; 50 | } 51 | 52 | /** 53 | * 清空 STORAGE 54 | */ 55 | export function clearStorage() { 56 | STORAGE = null; 57 | } 58 | -------------------------------------------------------------------------------- /src/config.yml: -------------------------------------------------------------------------------- 1 | # @import 语句引用的文件必须(MUST)写在一对引号内,.less 后缀不得(MUST NOT)省略(与引入 CSS 文件时的路径格式一致)。 2 | # 引号使用 '' 和 " 均可,但在同一项目内必须(MUST)统一。 3 | import: true 4 | 5 | # `{` : 选择器和 { 之间必须(MUST)保留一个空格。 6 | require-before-space: 7 | - "{" 8 | 9 | # `:` : 1. 属性名后的冒号(:)与属性值之间必须(MUST)保留一个空格,冒号前不得(MUST NOT)保留空格。 10 | # 2. 定义变量时冒号(:)与变量值之间必须(MUST)保留一个空格,冒号前不得(MUST NOT)保留空格。 11 | # `,` : 1. 在用逗号(,)分隔的列表(Less 函数参数列表、以 , 分隔的属性值等)中,逗号后必须(MUST)保留一个空格, 12 | # 逗号前不得(MUST NOT)保留空格。 13 | # 2. 在给 mixin 传递参数时,在参数分隔符(, / ;)后必须(MUST)保留一个空格 14 | require-after-space: 15 | - ":" 16 | - "," 17 | 18 | # + / - / * / / 四个运算符两侧必须(MUST)保留一个空格。 19 | require-around-space: 20 | - "+" 21 | - "-" 22 | - "*" 23 | - "/" 24 | 25 | # + / - 两侧的操作数必须(MUST)有相同的单位,如果其中一个是变量,另一个数值必须(MUST)书写单位。 26 | operate-unit: 27 | - "+" 28 | - "-" 29 | 30 | # Mixin 和后面的括号之间不得(MUST NOT)包含空格。 31 | disallow-mixin-name-space: true 32 | 33 | # `selector` : 当多个选择器共享一个声明块时,每个选择器声明必须(MUST)独占一行。 34 | require-newline: 35 | - "selector" 36 | 37 | # 对于处于 (0, 1) 范围内的数值,小数点前的 0 可以(MAY)省略,同一项目中必须(MUST)保持一致。 38 | leading-zero: true 39 | 40 | # 当属性值为 0 时,必须(MUST)省略可省的单位(长度单位如 px、em,不包括时间、角度等如 s、deg)。 41 | zero-unit: true 42 | 43 | # 颜色定义必须(MUST)使用 #rrggbb 格式定义,并在可能时尽量(SHOULD)缩写为 #rgb 形式,且避免直接使用颜色名称与 rgb() 表达式。 44 | hex-color: true 45 | 46 | # `color` 颜色值可以缩写时,必须使用缩写形式。 47 | shorthand: 48 | - "color" 49 | 50 | # 同一属性有不同私有前缀的,尽量(SHOULD)按前缀长度降序书写,标准形式必须(MUST)写在最后。 51 | # 且这一组属性以第一条的位置为准,尽量(SHOULD)按冒号的位置对齐。 52 | vendor-prefixes-sort: true 53 | 54 | # 必须(MUST)采用 4 个空格为一次缩进, 不得(MUST NOT)采用 TAB 作为缩进。 55 | block-indent: true 56 | 57 | # 变量命名必须采用 @foo-bar 形式,不得使用 @fooBar 形式 58 | variable-name: true 59 | 60 | # 使用继承时,如果在声明块内书写 :extend 语句,必须(MUST)写在开头: 61 | extend-must-firstline: true 62 | 63 | # 单行注释尽量使用 // 方式 64 | single-comment: true 65 | 66 | -------------------------------------------------------------------------------- /src/rule/block-indent.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 缩进校验 3 | * 必须(MUST)采用 4 个空格为一次缩进, 不得(MUST NOT)采用 TAB 作为缩进。 4 | * https://github.com/ecomfe/spec/blob/master/less-code-style.md#%E5%B5%8C%E5%A5%97%E5%92%8C%E7%BC%A9%E8%BF%9B 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import chalk from 'chalk'; 9 | import postcss from 'postcss'; 10 | import {getLineContent} from '../util'; 11 | 12 | 'use strict'; 13 | 14 | /** 15 | * 规则名称 16 | * 17 | * @const 18 | * @type {string} 19 | */ 20 | const RULENAME = 'block-indent'; 21 | 22 | /** 23 | * 行号的缓存,防止同一行多次报错 24 | * 25 | * @type {number} 26 | */ 27 | let lineCache = 0; 28 | 29 | /** 30 | * 获取错误信息 31 | * 32 | * @param {string} curIndent 当前的缩进(错误的) 33 | * @param {string} neededIndent 期望的的缩进(正确的) 34 | * 35 | * @return {string} 错误信息 36 | */ 37 | const getMsg = (curIndent, neededIndent) => '' 38 | + 'Bad indentation, Expected `' 39 | + (neededIndent) 40 | + '` but saw `' 41 | + (curIndent) 42 | + '`'; 43 | 44 | /** 45 | * 添加报错信息 46 | * 47 | * @param {Object} node node 对象,可能是 decl 也可能是 rule 48 | * @param {Object} result postcss 转换的结果对象 49 | * @param {string} msg 错误信息 50 | * @param {string} hackPrefixChar 属性 hack 的前缀,`_` 或者 `*` 51 | */ 52 | const addWarn = (node, result, msg, hackPrefixChar = '') => { 53 | const source = node.source; 54 | const line = source.start.line; 55 | if (lineCache !== line) { 56 | lineCache = line; 57 | const col = source.start.column; 58 | 59 | let lineContent = getLineContent(line, source.input.css) || ''; 60 | let colorStr = ''; 61 | 62 | if (node.selector) { 63 | colorStr = node.selector; 64 | } 65 | else if (node.type === 'atrule') { 66 | colorStr = lineContent; 67 | } 68 | else { 69 | colorStr = hackPrefixChar + node.prop + node.raws.between + node.value; 70 | colorStr = colorStr.replace(/\n/g, ''); 71 | } 72 | 73 | result.warn(RULENAME, { 74 | node: node, 75 | ruleName: RULENAME, 76 | line: line, 77 | col: col, 78 | message: msg, 79 | colorMessage: '`' 80 | + lineContent.replace( 81 | colorStr, 82 | chalk.magenta(colorStr) 83 | ) 84 | + '` ' 85 | + chalk.grey(msg) 86 | }); 87 | } 88 | }; 89 | 90 | /** 91 | * 遍历 ruleList,为了分析 decl 92 | * 93 | * @param {Array} ruleList rule 集合 94 | * @param {Object} result postcss 转换的结果对象 95 | */ 96 | const ruleListIterator = (ruleList, result) => { 97 | ruleList.forEach(r => { 98 | const rule = r.node; 99 | let indentStr = r.indentStr; 100 | if (rule.nodes && rule.nodes.length) { 101 | // 属性要比它所属的选择器多一层缩进 102 | indentStr += ' '; 103 | rule.nodes.forEach(childNode => { 104 | if (childNode.type !== 'decl') { 105 | return; 106 | } 107 | 108 | const curIndent = childNode.raws.before.replace(/\n*/, '').length; 109 | const neededIndent = indentStr.length; 110 | if (curIndent !== neededIndent) { 111 | addWarn(childNode, result, getMsg(curIndent + 1, neededIndent + 1)); 112 | } 113 | }); 114 | } 115 | }); 116 | }; 117 | 118 | /** 119 | * 具体的检测逻辑 120 | * 121 | * @param {Object} opts 参数 122 | * @param {*} opts.ruleVal 当前规则具体配置的值 123 | * @param {string} opts.fileContent 文件内容 124 | * @param {string} opts.filePath 文件路径 125 | */ 126 | export const check = postcss.plugin(RULENAME, opts => 127 | (css, result) => { 128 | if (!opts.ruleVal) { 129 | return; 130 | } 131 | 132 | lineCache = 0; 133 | 134 | // 收集顶层变量定义 135 | css.walkDecls(decl => { 136 | if (decl.parent.type === 'root') { 137 | let curIndent = decl.raws.before.replace(/\n*/, '').length; 138 | const neededIndent = 0; 139 | if (curIndent !== neededIndent) { 140 | addWarn(decl, result, getMsg(curIndent + 1, neededIndent + 1)); 141 | } 142 | } 143 | }); 144 | 145 | const ruleList = []; 146 | 147 | const analyzeIndent = (rule, indentStr) => { 148 | if (rule.type !== 'rule') { 149 | return; 150 | } 151 | 152 | let curIndent = rule.raws.before.replace(/\n*/, '').length; 153 | const neededIndent = indentStr.length; 154 | if (curIndent !== neededIndent) { 155 | addWarn(rule, result, getMsg(curIndent + 1, neededIndent + 1)); 156 | } 157 | 158 | ruleList.push({ 159 | node: rule, 160 | indentStr: indentStr 161 | }); 162 | 163 | if (rule.nodes && rule.nodes.length) { 164 | rule.nodes.forEach(r => { 165 | analyzeIndent(r, indentStr + ' '); 166 | }); 167 | } 168 | }; 169 | 170 | // 收集顶层选择器 171 | css.walkRules(rule => { 172 | if (rule.parent.type === 'root') { 173 | analyzeIndent(rule, ''); 174 | } 175 | }); 176 | 177 | ruleListIterator(ruleList, result); 178 | 179 | } 180 | ); 181 | -------------------------------------------------------------------------------- /src/rule/hex-color.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 颜色检验 3 | * 颜色定义必须(MUST)使用 #RRGGBB 格式定义,并在可能时尽量(SHOULD)缩写为 #RGB 形式,且避免直接使用颜色名称与 rgb() 表达式。 4 | * https://github.com/ecomfe/spec/blob/master/less-code-style.md#%E9%A2%9C%E8%89%B2 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import chalk from 'chalk'; 9 | import postcss from 'postcss'; 10 | import less from 'less'; 11 | import {getLineContent, changeColorByStartAndEndIndex} from '../util'; 12 | 13 | 'use strict'; 14 | 15 | /** 16 | * 规则名称 17 | * 18 | * @const 19 | * @type {string} 20 | */ 21 | const RULENAME = 'hex-color'; 22 | 23 | /** 24 | * Less 中的所有颜色值 25 | * 26 | * @const 27 | * @type {Object} 28 | */ 29 | const LESS_COLORS = less.data.colors; 30 | 31 | /** 32 | * 对 Less 中的所有颜色值做处理,便于之后正则 33 | * 34 | * @const 35 | * @return {string} 字符串 36 | */ 37 | // const namedColors = (function () { 38 | // let ret = ''; 39 | // for (let key of Object.keys(LESS_COLORS)) { 40 | // ret += key + '|'; 41 | // } 42 | // return ret.slice(0, -1); // 去掉最后一个 | 43 | // })(); 44 | 45 | /** 46 | * 匹配颜色名的正则 47 | * 48 | * @const 49 | * @type {RegExp} 50 | */ 51 | // const PATTERN_NAMED_COLOR_EXP = new RegExp('\\b\\s?:\\s*(' + namedColors + ')', 'g'); 52 | // console.log(PATTERN_NAMED_COLOR_EXP); 53 | // console.log(); 54 | 55 | /** 56 | * 匹配 rgb, hsl 颜色表达式的正则 57 | * 58 | * @const 59 | * @type {RegExp} 60 | */ 61 | const PATTERN_COLOR_EXP = /(\brgb\b|\bhsl\b)/gi; 62 | 63 | /** 64 | * 错误信息 65 | * 66 | * @const 67 | * @type {string} 68 | */ 69 | const MSG = '' 70 | + 'Color value must use the hexadecimal mark forms such as `#RRGGBB`.' 71 | + ' Don\'t use RGB、HSL expression'; 72 | 73 | /** 74 | * 添加报错信息 75 | * 76 | * @param {Object} node decl 对象 77 | * @param {Object} result postcss 转换的结果对象 78 | */ 79 | const addWarn = (decl, result) => { 80 | const {source, prop, raws} = decl; 81 | const line = source.start.line; 82 | const lineContent = getLineContent(line, source.input.css, true); 83 | const col = source.start.column + prop.length + raws.between.length; 84 | result.warn(RULENAME, { 85 | node: decl, 86 | ruleName: RULENAME, 87 | line: line, 88 | col: col, 89 | message: MSG, 90 | colorMessage: '`' 91 | + changeColorByStartAndEndIndex( 92 | lineContent, col, source.end.column 93 | ) 94 | + '` ' 95 | + chalk.grey(MSG) 96 | }); 97 | }; 98 | 99 | /** 100 | * 具体的检测逻辑 101 | * 102 | * @param {Object} opts 参数 103 | * @param {*} opts.ruleVal 当前规则具体配置的值 104 | * @param {string} opts.fileContent 文件内容 105 | * @param {string} opts.filePath 文件路径 106 | */ 107 | export const check = postcss.plugin(RULENAME, opts => 108 | (css, result) => { 109 | if (!opts.ruleVal) { 110 | return; 111 | } 112 | 113 | css.walkDecls(decl => { 114 | const value = decl.value; 115 | if (LESS_COLORS[value]) { 116 | addWarn(decl, result); 117 | } 118 | else { 119 | let match = null; 120 | /* eslint-disable no-extra-boolean-cast */ 121 | while (!!(match = PATTERN_COLOR_EXP.exec(value))) { 122 | addWarn(decl, result); 123 | } 124 | /* eslint-enable no-extra-boolean-cast */ 125 | } 126 | }); 127 | } 128 | ); 129 | -------------------------------------------------------------------------------- /src/rule/import.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file @import 检验 3 | * @import 语句引用的文件必须(MUST)写在一对引号内,.less 后缀不得(MUST NOT)省略(与引入 CSS 文件时的路径格式一致)。 4 | * 引号使用 ' 和 " 均可,但在同一项目内必须(MUST)统一。 5 | * https://github.com/ecomfe/spec/blob/master/less-code-style.md#import-%E8%AF%AD%E5%8F%A5 6 | * @author ielgnaw(wuji0223@gmail.com) 7 | */ 8 | 9 | import chalk from 'chalk'; 10 | import postcss from 'postcss'; 11 | import {getLineContent} from '../util'; 12 | 13 | 'use strict'; 14 | 15 | /** 16 | * 规则名称 17 | * 18 | * @const 19 | * @type {string} 20 | */ 21 | const RULENAME = 'import'; 22 | 23 | /** 24 | * less 文件后缀正则 25 | * 26 | * @const 27 | * @type {RegExp} 28 | */ 29 | const LESS_SUFFIX_REG = /\.less$/; 30 | 31 | /** 32 | * 记录当前检测的 less 文件中 @import 的引号是单引号还是双引号 33 | * 按第一个读取到的引号为准,同一文件内要统一 {quoteVal, filePath} 34 | * 35 | * @type {Object} 36 | */ 37 | const importQuote = { 38 | quoteVal: null, 39 | filePath: '' 40 | }; 41 | 42 | /** 43 | * 具体的检测逻辑 44 | * 45 | * @param {Object} opts 参数 46 | * @param {*} opts.ruleVal 当前规则具体配置的值 47 | * @param {string} opts.fileContent 文件内容 48 | * @param {string} opts.filePath 文件路径 49 | */ 50 | export const check = postcss.plugin(RULENAME, opts => 51 | (css, result) => { 52 | 53 | if (!opts.ruleVal) { 54 | return; 55 | } 56 | 57 | if (importQuote.filePath !== opts.filePath) { 58 | importQuote.filePath = opts.filePath; 59 | importQuote.quoteVal = null; 60 | } 61 | 62 | css.walkAtRules(rule => { 63 | if (rule.name !== 'import') { 64 | return; 65 | } 66 | 67 | const params = rule.params.replace(/^(['"])/, '').replace(/(['"])$/, ''); 68 | 69 | const quote = RegExp.$1; 70 | const lineNum = rule.source.start.line; 71 | const lineContent = getLineContent(lineNum, opts.fileContent); 72 | 73 | // @import 语句引用的文件必须(MUST)写在一对引号内 74 | if (!quote) { 75 | result.warn(RULENAME, { 76 | node: rule, 77 | ruleName: RULENAME, 78 | line: lineNum, 79 | col: rule.source.end.column - rule.params.length, 80 | message: `\`${lineContent}\` @import statement must wrote a pair of quotation marks`, 81 | colorMessage: '`' 82 | + lineContent.replace( 83 | params, 84 | chalk.magenta(params) 85 | ) 86 | + '` ' 87 | + chalk.grey('@import statement must wrote a pair of quotation marks') 88 | }); 89 | } 90 | else { 91 | if (!importQuote.quoteVal) { 92 | importQuote.quoteVal = quote; 93 | } 94 | 95 | // 同一个文件内,引号和当前文件的第一个引号不相同 96 | if (quote !== importQuote.quoteVal && opts.filePath === importQuote.filePath) { 97 | result.warn(RULENAME, { 98 | node: rule, 99 | ruleName: RULENAME, 100 | line: lineNum, 101 | col: rule.source.end.column - rule.params.length, 102 | message: '' 103 | + `\`${lineContent}\` Quotes must be the same in the same file,` 104 | + `Current file the first quote is \`${importQuote.quoteVal}\``, 105 | colorMessage: '`' 106 | + lineContent.replace( 107 | new RegExp(quote, 'g'), 108 | chalk.magenta(quote) 109 | ) 110 | + ' ' 111 | + chalk.grey('Quotes must be the same in the same file, Current file ' 112 | + 'the first quote is `' 113 | + chalk.magenta(importQuote.quoteVal) 114 | + '`') 115 | }); 116 | } 117 | } 118 | 119 | // .less 后缀不得(MUST NOT)省略 120 | if (!LESS_SUFFIX_REG.test(params)) { 121 | result.warn(RULENAME, { 122 | node: rule, 123 | ruleName: RULENAME, 124 | line: lineNum, 125 | col: rule.source.end.column - rule.params.length, 126 | message: `\`${lineContent}\` .less suffix must not be omitted`, 127 | colorMessage: '`' 128 | + lineContent.replace( 129 | params, 130 | chalk.magenta(params) 131 | ) 132 | + '` ' 133 | + chalk.grey('.less suffix must not be omitted') 134 | }); 135 | } 136 | }); 137 | } 138 | ); 139 | -------------------------------------------------------------------------------- /src/rule/leading-zero.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 数值检验 3 | * 对于处于 (0, 1) 范围内的数值,小数点前的 0 可以(MAY)省略,同一项目中必须(MUST)保持一致。 4 | * https://github.com/ecomfe/spec/blob/master/less-code-style.md#%E6%95%B0%E5%80%BC 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import chalk from 'chalk'; 9 | import postcss from 'postcss'; 10 | import {getLineContent, changeColorByIndex} from '../util'; 11 | 12 | 'use strict'; 13 | 14 | /** 15 | * 规则名称 16 | * 17 | * @const 18 | * @type {string} 19 | */ 20 | const RULENAME = 'leading-zero'; 21 | 22 | /** 23 | * 错误信息 24 | * 25 | * @const 26 | * @type {string} 27 | */ 28 | const MSG = 'When value is between 0 - 1 decimal, omitting the integer part of the `0`'; 29 | 30 | /** 31 | * 具体的检测逻辑 32 | * 33 | * @param {Object} opts 参数 34 | * @param {*} opts.ruleVal 当前规则具体配置的值 35 | * @param {string} opts.fileContent 文件内容 36 | * @param {string} opts.filePath 文件路径 37 | */ 38 | export const check = postcss.plugin(RULENAME, opts => 39 | (css, result) => { 40 | if (!opts.ruleVal) { 41 | return; 42 | } 43 | 44 | css.walkDecls(decl => { 45 | const parts = postcss.list.space(decl.value); 46 | const source = decl.source; 47 | const lineNum = source.start.line; 48 | 49 | function check(part, startCol) { 50 | const numericVal = parseFloat(part); 51 | if (numericVal < 1 && numericVal > 0 || numericVal < 0 && numericVal > -1) { 52 | if (part.slice(0, 2) === '0.' || part.slice(0, 3) === '-0.') { 53 | const lineContent = getLineContent(lineNum, source.input.css, true); 54 | const col = lineContent.indexOf(part, startCol); 55 | result.warn(RULENAME, { 56 | node: decl, 57 | ruleName: RULENAME, 58 | line: lineNum, 59 | col: col + 1, 60 | message: MSG, 61 | colorMessage: '`' 62 | + changeColorByIndex(lineContent, col, part) 63 | + '` ' 64 | + chalk.grey(MSG) 65 | }); 66 | } 67 | } 68 | } 69 | 70 | const pattern = /\(([^\)]+)\)/; 71 | for (var i = 0, len = parts.length; i < len; i++) { 72 | var part = parts[i]; 73 | const match = part.match(pattern); 74 | if (match) { 75 | var start = match.index; 76 | match[1].split(/,\s*/).forEach(function (property) { 77 | start = part.indexOf(property, start); 78 | check(property, start); 79 | }); 80 | } 81 | else { 82 | check(part, 0); 83 | } 84 | } 85 | }); 86 | } 87 | ); 88 | -------------------------------------------------------------------------------- /src/rule/operate-unit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 运算符检验 3 | * + / - 两侧的操作数必须(MUST)有相同的单位,如果其中一个是变量,另一个数值必须(MUST)书写单位。 4 | * https://github.com/ecomfe/spec/blob/master/less-code-style.md#%E8%BF%90%E7%AE%97 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import chalk from 'chalk'; 9 | import postcss from 'postcss'; 10 | import parser from 'postcss-values-parser'; 11 | 12 | import {getLineContent, changeColorByStartAndEndIndex} from '../util'; 13 | 14 | 'use strict'; 15 | 16 | /** 17 | * 规则名称 18 | * 19 | * @const 20 | * @type {string} 21 | */ 22 | const RULENAME = 'operate-unit'; 23 | 24 | /** 25 | * 错误信息 26 | * 27 | * @const 28 | * @type {string} 29 | */ 30 | const MSG = '' 31 | + '`+`、`-` on both sides of the operand must have the same unit, ' 32 | + 'if one side has unit, the other side must has unit'; 33 | 34 | /** 35 | * 具体的检测逻辑 36 | * 37 | * @param {Object} opts 参数 38 | * @param {*} opts.ruleVal 当前规则具体配置的值 39 | * @param {string} opts.fileContent 文件内容 40 | * @param {string} opts.filePath 文件路径 41 | */ 42 | export const check = postcss.plugin(RULENAME, opts => 43 | (css, result) => { 44 | if (!opts.ruleVal) { 45 | return; 46 | } 47 | 48 | // TODO: 49 | // `@a: 1 + 2;` is walkDecls 50 | // `@a : 1 + 2;` is walkAtRules 这种情况还未处理 51 | 52 | /* jshint maxcomplexity:false */ 53 | css.walkDecls(decl => { 54 | const valueAst = parser(decl.value).parse(); 55 | 56 | valueAst.walk(child => { 57 | if (child.type !== 'operator' || (child.value !== '+' && child.value !== '-')) { 58 | return; 59 | } 60 | 61 | const {parent} = child; 62 | 63 | // 当前 child 的索引 64 | const index = parent.index(child); 65 | 66 | // child 的后一个元素 67 | const nextElem = parent.nodes[index + 1]; 68 | 69 | // child 的前一个元素 70 | const prevElem = parent.nodes[index - 1] || {}; 71 | 72 | let problemElem = null; 73 | 74 | // 后一个是变量 75 | if (nextElem.type === 'atword' && prevElem.type !== 'atword') { 76 | if (!prevElem.unit) { 77 | problemElem = prevElem; 78 | } 79 | } 80 | 81 | // 前一个是变量 82 | if (prevElem.type === 'atword' && nextElem.type !== 'atword') { 83 | if (!nextElem.unit) { 84 | problemElem = nextElem; 85 | } 86 | } 87 | 88 | if (problemElem) { 89 | const {source, prop, raws} = decl; 90 | const line = source.start.line; 91 | const lineContent = getLineContent(line, source.input.css, true); 92 | const col 93 | = source.start.column + prop.length + raws.between.length + problemElem.source.start.column - 1; 94 | 95 | result.warn(RULENAME, { 96 | node: decl, 97 | ruleName: RULENAME, 98 | line: line, 99 | col: col, 100 | message: '`' + lineContent + '` ' + MSG, 101 | colorMessage: '`' 102 | + changeColorByStartAndEndIndex(lineContent, col, col + problemElem.value.length) 103 | + '` ' 104 | + chalk.grey(MSG) 105 | }); 106 | } 107 | }); 108 | }); 109 | } 110 | ); 111 | -------------------------------------------------------------------------------- /src/rule/require-after-space.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * `:` : 1. 属性名后的冒号(:)与属性值之间必须(MUST)保留一个空格,冒号前不得(MUST NOT)保留空格。 4 | * 2. 定义变量时冒号(:)与变量值之间必须(MUST)保留一个空格,冒号前不得(MUST NOT)保留空格。 5 | * `,` : 1. 在用逗号(,)分隔的列表(Less 函数参数列表、以 , 分隔的属性值等)中,逗号后必须(MUST)保留一个空格, 6 | * 逗号前不得(MUST NOT)保留空格。 7 | * 2. 在给 mixin 传递参数时,在参数分隔符(, / ;)后必须(MUST)保留一个空格 8 | * 9 | * 逗号暂时不太好实现 10 | * 11 | * https://github.com/ecomfe/spec/blob/master/less-code-style.md#%E5%B1%9E%E6%80%A7%E5%8F%98%E9%87%8F 12 | * @author ielgnaw(wuji0223@gmail.com) 13 | */ 14 | 15 | import chalk from 'chalk'; 16 | import postcss from 'postcss'; 17 | import {getLineContent} from '../util'; 18 | 19 | 'use strict'; 20 | 21 | /** 22 | * 规则名称 23 | * 24 | * @const 25 | * @type {string} 26 | */ 27 | const RULENAME = 'require-after-space'; 28 | 29 | /** 30 | * 冒号 31 | * 32 | * @const 33 | * @type {string} 34 | */ 35 | const COLON = ':'; 36 | 37 | /** 38 | * 逗号 39 | * 40 | * @const 41 | * @type {string} 42 | */ 43 | const COMMA = ','; 44 | 45 | /** 46 | * 匹配 css 属性值的 url(...); 47 | * 48 | * @const 49 | * @type {RegExp} 50 | */ 51 | const PATTERN_URI = /url\(["']?([^\)"']+)["']?\)/i; 52 | 53 | /** 54 | * 冒号的错误信息 55 | * 56 | * @const 57 | * @type {string} 58 | */ 59 | const COLON_MSG = '' 60 | + 'Disallow contain spaces between the `attr-name` and `:`, ' 61 | + 'Must contain spaces between `:` and `attr-value`'; 62 | 63 | /** 64 | * 逗号的错误信息 65 | * 66 | * @const 67 | * @type {string} 68 | */ 69 | const COMMA_MSG = 'Must contain spaces after `,` in `attr-value`'; 70 | 71 | /** 72 | * 具体的检测逻辑 73 | * 74 | * @param {Object} opts 参数 75 | * @param {*} opts.ruleVal 当前规则具体配置的值 76 | * @param {string} opts.fileContent 文件内容 77 | * @param {string} opts.filePath 文件路径 78 | */ 79 | export const check = postcss.plugin(RULENAME, opts => 80 | (css, result) => { 81 | const ruleVal = opts.ruleVal; 82 | const realRuleVal = []; 83 | Array.prototype.push[Array.isArray(ruleVal) ? 'apply' : 'call'](realRuleVal, ruleVal); 84 | 85 | if (realRuleVal.length) { 86 | 87 | css.walkDecls(decl => { 88 | const source = decl.source; 89 | const line = source.start.line; 90 | const lineContent = getLineContent(line, source.input.css) || ''; 91 | 92 | if (realRuleVal.indexOf(COLON) !== -1) { 93 | const between = decl.raws.between; 94 | 95 | if (between.slice(0, 1) !== ':' // `属性名` 与之后的 `:` 之间包含空格了 96 | || between.slice(-1) === ':' // `:` 与 `属性值` 之间不包含空格 97 | ) { 98 | const colorStr = decl.prop + decl.raws.between + decl.value; 99 | result.warn(RULENAME, { 100 | node: decl, 101 | ruleName: RULENAME, 102 | line: line, 103 | message: COLON_MSG, 104 | colorMessage: '`' 105 | + lineContent.replace( 106 | colorStr, 107 | chalk.magenta(colorStr) 108 | ) 109 | + '` ' 110 | + chalk.grey(COLON_MSG) 111 | }); 112 | } 113 | } 114 | 115 | if (realRuleVal.indexOf(COMMA) !== -1) { 116 | const value = decl.value; 117 | 118 | // 排除掉 uri 的情况,例如 119 | // background-image: url(data:image/gif;base64,R0lGODlhCAAHAIABAGZmZv...); 120 | // background-image: 2px 2px url(data:image/gif;base64,R0lGODlhCAAHAIABAGZmZv...); 121 | // background-image: url(data:image/gif;base64,R0lGODlhCAAHAIABAGZmZv...) 2px 2px; 122 | if (!PATTERN_URI.test(value)) { 123 | const items = lineContent.split(';'); 124 | for (let j = 0, jLen = items.length; j < jLen; j++) { 125 | const s = items[j]; 126 | if (s.indexOf(',') > -1 127 | && /.*,(?!\s)/.test(s) 128 | && s.length !== lineContent.length // s.length === lineContent.length 的情况表示当前行结束了 129 | ) { 130 | result.warn(RULENAME, { 131 | node: decl, 132 | ruleName: RULENAME, 133 | errorChar: COMMA, 134 | line: line, 135 | message: COMMA_MSG, 136 | colorMessage: '`' 137 | + lineContent.replace( 138 | value, 139 | chalk.magenta(value) 140 | ) 141 | + '` ' 142 | + chalk.grey(COMMA_MSG) 143 | }); 144 | } 145 | } 146 | } 147 | } 148 | 149 | }); 150 | } 151 | } 152 | ); 153 | -------------------------------------------------------------------------------- /src/rule/require-around-space.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 运算符检验 3 | * + / - / * / / 四个运算符两侧必须(MUST)保留一个空格。 4 | * https://github.com/ecomfe/spec/blob/master/less-code-style.md#%E8%BF%90%E7%AE%97 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import chalk from 'chalk'; 9 | import postcss from 'postcss'; 10 | import parser from 'postcss-values-parser'; 11 | 12 | import {getLineContent, changeColorByStartAndEndIndex} from '../util'; 13 | 14 | 'use strict'; 15 | 16 | /** 17 | * 规则名称 18 | * 19 | * @const 20 | * @type {string} 21 | */ 22 | const RULENAME = 'require-around-space'; 23 | 24 | /** 25 | * 错误信息 26 | * 27 | * @const 28 | * @type {string} 29 | */ 30 | const MSG = '`+`、`-`、`*`、`/` four operators on both sides must keep a space'; 31 | 32 | /** 33 | * 具体的检测逻辑 34 | * 35 | * @param {Object} opts 参数 36 | * @param {*} opts.ruleVal 当前规则具体配置的值 37 | * @param {string} opts.fileContent 文件内容 38 | * @param {string} opts.filePath 文件路径 39 | */ 40 | export const check = postcss.plugin(RULENAME, opts => 41 | (css, result) => { 42 | if (!opts.ruleVal) { 43 | return; 44 | } 45 | 46 | /* jshint maxcomplexity:false */ 47 | css.walkDecls(decl => { 48 | const valueAst = parser(decl.value).parse(); 49 | 50 | valueAst.walk(child => { 51 | if (child.type !== 'operator') { 52 | return; 53 | } 54 | 55 | const {parent} = child; 56 | 57 | // 当前 child 的索引 58 | const index = parent.index(child); 59 | 60 | // child 的后一个元素 61 | const nextElem = parent.nodes[index + 1]; 62 | 63 | // child 的前一个元素 64 | const prevElem = parent.nodes[index - 1]; 65 | 66 | // 忽略负数 -1 67 | if (child.value === '-' 68 | && (child.raws.before || decl.raws.between) 69 | && nextElem.type === 'number' 70 | && !nextElem.raws.before 71 | ) { 72 | return; 73 | } 74 | 75 | // 忽略变量 -@foo 76 | if (child.value === '-' 77 | && (child.raws.before || decl.raws.between) 78 | && nextElem.type === 'atword' 79 | && !nextElem.raws.before 80 | ) { 81 | return; 82 | } 83 | 84 | // 忽略 font-size/line-height 简写定义 85 | if (decl.prop === 'font' 86 | && child.value === '/' 87 | && prevElem.type === 'number' 88 | && nextElem.type === 'number' 89 | ) { 90 | return; 91 | } 92 | 93 | // 判断 operator 前面是否有空格 94 | const isBeforeValid = child.raws.before === ' ' || /^\s/.test(child.raws.before); 95 | 96 | // 判断 operator 后面是否有空格 97 | const isAfterValid = nextElem.raws.before === ' ' || /\s$/.test(nextElem.raws.before); 98 | 99 | if (!isBeforeValid || !isAfterValid) { 100 | const problemElem = !/\s$/.test(child.raws.before) ? child : nextElem; 101 | const {source, prop, raws} = decl; 102 | const line = source.start.line; 103 | const lineContent = getLineContent(line, source.input.css, true); 104 | const col = 0 105 | + source.start.column + prop.length + raws.between.length + problemElem.source.start.column 106 | - 1 107 | - (isBeforeValid ? child.value.length : 0); 108 | 109 | result.warn(RULENAME, { 110 | node: decl, 111 | ruleName: RULENAME, 112 | line: line, 113 | col: col, 114 | message: '`' + lineContent + '` ' + MSG, 115 | colorMessage: '`' 116 | + changeColorByStartAndEndIndex(lineContent, col, col + child.value.length) 117 | + '` ' 118 | + chalk.grey(MSG) 119 | }); 120 | } 121 | }); 122 | }); 123 | } 124 | ); 125 | -------------------------------------------------------------------------------- /src/rule/require-before-space.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 选择器和 { 之间必须(MUST)保留一个空格。 3 | * https://github.com/ecomfe/spec/blob/master/less-code-style.md#%E5%B1%9E%E6%80%A7%E5%8F%98%E9%87%8F 4 | * @author ielgnaw(wuji0223@gmail.com) 5 | */ 6 | 7 | import chalk from 'chalk'; 8 | import postcss from 'postcss'; 9 | import {getLineContent} from '../util'; 10 | 11 | 'use strict'; 12 | 13 | /** 14 | * 规则名称 15 | * 16 | * @const 17 | * @type {string} 18 | */ 19 | const RULENAME = 'require-before-space'; 20 | 21 | /** 22 | * 错误信息 23 | * 24 | * @const 25 | * @type {string} 26 | */ 27 | const MSG = 'Must contain spaces before the `{`'; 28 | 29 | /** 30 | * 具体的检测逻辑 31 | * 32 | * @param {Object} opts 参数 33 | * @param {*} opts.ruleVal 当前规则具体配置的值 34 | * @param {string} opts.fileContent 文件内容 35 | * @param {string} opts.filePath 文件路径 36 | */ 37 | export const check = postcss.plugin(RULENAME, opts => 38 | (css, result) => { 39 | const ruleVal = opts.ruleVal; 40 | const realRuleVal = []; 41 | Array.prototype.push[Array.isArray(ruleVal) ? 'apply' : 'call'](realRuleVal, ruleVal); 42 | 43 | if (realRuleVal.length) { 44 | css.walkRules(rule => { 45 | // 只有 { 时,才能用 between 处理,其他符号的 require-before-space 规则还未实现 46 | if ( 47 | !rule.ruleWithoutBody // 排除 mixin 调用 48 | && rule.raws.between === '' && realRuleVal.indexOf('{') !== -1 49 | ) { 50 | const source = rule.source; 51 | const line = source.start.line; 52 | const col = source.start.column + rule.selector.length; 53 | const lineContent = getLineContent(line, source.input.css) || ''; 54 | result.warn(RULENAME, { 55 | node: rule, 56 | ruleName: RULENAME, 57 | line: line, 58 | col: col, 59 | message: MSG, 60 | colorMessage: '`' 61 | + lineContent.replace( 62 | '{', 63 | chalk.magenta('{') 64 | ) 65 | + '` ' 66 | + chalk.grey(MSG) 67 | }); 68 | } 69 | }); 70 | } 71 | } 72 | ); 73 | -------------------------------------------------------------------------------- /src/rule/require-newline.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 选择器检验 3 | * 当多个选择器共享一个声明块时,每个选择器声明必须(MUST)独占一行。 4 | * https://github.com/ecomfe/spec/blob/master/less-code-style.md#%E9%80%89%E6%8B%A9%E5%99%A8 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import chalk from 'chalk'; 9 | import postcss from 'postcss'; 10 | import {getLineContent} from '../util'; 11 | 12 | 'use strict'; 13 | 14 | /** 15 | * 规则名称 16 | * 17 | * @const 18 | * @type {string} 19 | */ 20 | const RULENAME = 'require-newline'; 21 | 22 | /** 23 | * 判断逗号后面没有跟着换行符的正则 24 | * 如果未匹配,则说明逗号后面有换行符 25 | * 26 | * @const 27 | * @type {RegExp} 28 | */ 29 | const PATTERN_NOTLF = /(,(?!\s*\n))/; 30 | 31 | /** 32 | * 错误信息 33 | * 34 | * @const 35 | * @type {string} 36 | */ 37 | const MSG = 'When multiple selectors share a statement block, each selector statement must be per line'; 38 | 39 | /** 40 | * 具体的检测逻辑 41 | * 42 | * @param {Object} opts 参数 43 | * @param {*} opts.ruleVal 当前规则具体配置的值 44 | * @param {string} opts.fileContent 文件内容 45 | * @param {string} opts.filePath 文件路径 46 | */ 47 | export const check = postcss.plugin(RULENAME, opts => 48 | (css, result) => { 49 | const ruleVal = opts.ruleVal; 50 | const realRuleVal = []; 51 | 52 | Array.prototype.push[Array.isArray(ruleVal) ? 'apply' : 'call'](realRuleVal, ruleVal); 53 | 54 | if (!realRuleVal.length) { 55 | return; 56 | } 57 | 58 | if (realRuleVal.indexOf('selector') > -1) { 59 | css.walkRules(rule => { 60 | const selector = rule.selector; 61 | if ( 62 | !rule.ruleWithoutBody // 排除 mixin 调用 63 | && !rule.params // 排除 mixin 定义 64 | && PATTERN_NOTLF.test(selector) 65 | ) { 66 | const source = rule.source; 67 | const line = source.start.line; 68 | const lineContent = getLineContent(line, source.input.css); 69 | const col = source.start.column; 70 | // 如果是 `p, i, \n.cc` 这样的选择器,那么高亮就应该把后面的 `\n.cc` 去掉 71 | // 直接用 lineContent 来匹配 `p, i, \n.cc` 无法高亮 72 | const colorStr = selector.replace(/\n.*/, ''); 73 | result.warn(RULENAME, { 74 | node: rule, 75 | ruleName: RULENAME, 76 | line: line, 77 | col: col, 78 | message: MSG, 79 | colorMessage: '`' 80 | + lineContent.replace(colorStr, chalk.magenta(colorStr)) 81 | + '` ' 82 | + chalk.grey(MSG) 83 | }); 84 | } 85 | }); 86 | } 87 | } 88 | ); 89 | -------------------------------------------------------------------------------- /src/rule/shorthand.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 颜色检验 3 | * 颜色定义必须(MUST)使用 #RRGGBB 格式定义,并在可能时尽量(SHOULD)缩写为 #RGB 形式,且避免直接使用颜色名称与 rgb() 表达式。 4 | * https://github.com/ecomfe/spec/blob/master/less-code-style.md#%E9%A2%9C%E8%89%B2 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import chalk from 'chalk'; 9 | import postcss from 'postcss'; 10 | import {getLineContent, changeColorByStartAndEndIndex} from '../util'; 11 | 12 | 'use strict'; 13 | 14 | /** 15 | * 规则名称 16 | * 17 | * @const 18 | * @type {string} 19 | */ 20 | const RULENAME = 'hex-color'; 21 | 22 | /** 23 | * 判断颜色值是否可以缩写 24 | * 25 | * @const 26 | * @type {RegExp} 27 | */ 28 | const PATTERN_COLOR = /#([\da-f])\1([\da-f])\2([\da-f])\3/i; 29 | 30 | /** 31 | * 错误信息 32 | * 33 | * @const 34 | * @type {string} 35 | */ 36 | const MSG = 'Color value can be abbreviated, must use the abbreviation form'; 37 | 38 | /** 39 | * 行数的缓存,避免相同的行报多次,因为这里是直接按照 value 值整体来报的 40 | */ 41 | let lineCache = 0; 42 | 43 | /** 44 | * 具体的检测逻辑 45 | * 46 | * @param {Object} opts 参数 47 | * @param {*} opts.ruleVal 当前规则具体配置的值 48 | * @param {string} opts.fileContent 文件内容 49 | * @param {string} opts.filePath 文件路径 50 | */ 51 | export const check = postcss.plugin(RULENAME, opts => 52 | (css, result) => { 53 | const ruleVal = opts.ruleVal; 54 | const realRuleVal = []; 55 | 56 | Array.prototype.push[Array.isArray(ruleVal) ? 'apply' : 'call'](realRuleVal, ruleVal); 57 | 58 | if (!realRuleVal.length) { 59 | return; 60 | } 61 | 62 | lineCache = 0; 63 | 64 | if (realRuleVal.indexOf('color') > -1) { 65 | 66 | css.walkDecls(decl => { 67 | const {value, source} = decl; 68 | const parts = postcss.list.space(value); 69 | for (let i = 0, len = parts.length; i < len; i++) { 70 | const part = parts[i]; 71 | if (PATTERN_COLOR.test(part)) { 72 | if (lineCache === source.start.line) { 73 | continue; 74 | } 75 | 76 | const line = source.start.line; 77 | lineCache = line; 78 | const lineContent = getLineContent(line, source.input.css, true); 79 | const col = source.start.column + decl.prop.length + decl.raws.between.length; 80 | result.warn(RULENAME, { 81 | node: decl, 82 | ruleName: RULENAME, 83 | line: line, 84 | col: col, 85 | message: MSG, 86 | colorMessage: '`' 87 | + changeColorByStartAndEndIndex( 88 | lineContent, col, source.end.column 89 | ) 90 | + '` ' 91 | + chalk.grey(MSG) 92 | }); 93 | } 94 | } 95 | }); 96 | } 97 | } 98 | ); 99 | -------------------------------------------------------------------------------- /src/rule/single-comment.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 注释检验 3 | * 单行注释尽量使用 // 方式 4 | * https://github.com/ecomfe/spec/blob/master/less-code-style.md#%E6%B3%A8%E9%87%8A 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import chalk from 'chalk'; 9 | import postcss from 'postcss'; 10 | import {getLineContent} from '../util'; 11 | 12 | 'use strict'; 13 | 14 | /** 15 | * 规则名称 16 | * 17 | * @const 18 | * @type {string} 19 | */ 20 | const RULENAME = 'single-comment'; 21 | 22 | /** 23 | * 具体的检测逻辑 24 | * 25 | * @param {Object} opts 参数 26 | * @param {*} opts.ruleVal 当前规则具体配置的值 27 | * @param {string} opts.fileContent 文件内容 28 | * @param {string} opts.filePath 文件路径 29 | */ 30 | export const check = postcss.plugin(RULENAME, opts => 31 | (css, result) => { 32 | if (!opts.ruleVal) { 33 | return; 34 | } 35 | 36 | css.walkComments(comment => { 37 | // 排除单行注释 38 | if (comment.inline) { 39 | return; 40 | } 41 | 42 | const {source} = comment; 43 | 44 | const startLine = source.start.line; 45 | const endLine = source.end.line; 46 | if (startLine === endLine) { 47 | const lineContent = getLineContent(startLine, opts.fileContent); 48 | 49 | result.warn(RULENAME, { 50 | node: comment, 51 | ruleName: RULENAME, 52 | line: startLine, 53 | col: source.start.column, 54 | message: `\`${lineContent}\` Single Comment should be use \`//\``, 55 | colorMessage: '`' 56 | + lineContent.replace( 57 | lineContent, 58 | $1 => chalk.magenta($1) 59 | ) 60 | + '` ' 61 | + chalk.grey('Single Comment should be use `//`') 62 | }); 63 | } 64 | }); 65 | } 66 | ); 67 | -------------------------------------------------------------------------------- /src/rule/variable-name.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 变量检验 3 | * 变量命名必须(MUST)采用 @foo-bar 形式,不得(MUST NOT)使用 @fooBar 形式。 4 | * https://github.com/ecomfe/spec/blob/master/less-code-style.md#%E5%8F%98%E9%87%8F 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import chalk from 'chalk'; 9 | import postcss from 'postcss'; 10 | import {getLineContent} from '../util'; 11 | 12 | 'use strict'; 13 | 14 | /** 15 | * 规则名称 16 | * 17 | * @const 18 | * @type {string} 19 | */ 20 | const RULENAME = 'variable-name'; 21 | 22 | /** 23 | * 匹配变量名字的正则 24 | * 25 | * @const 26 | * @type {RegExp} 27 | */ 28 | const VARIABLE_NAME_REG = /^@([a-z0-9\-]+)$/; 29 | 30 | /** 31 | * 错误信息 32 | * 33 | * @const 34 | * @type {string} 35 | */ 36 | const MSG = 'Variable name must be like this `@foo-bar or @foobar`'; 37 | 38 | /** 39 | * 具体的检测逻辑 40 | * 41 | * @param {Object} opts 参数 42 | * @param {*} opts.ruleVal 当前规则具体配置的值 43 | * @param {string} opts.fileContent 文件内容 44 | * @param {string} opts.filePath 文件路径 45 | */ 46 | export const check = postcss.plugin(RULENAME, opts => 47 | (css, result) => { 48 | if (!opts.ruleVal) { 49 | return; 50 | } 51 | 52 | css.walkDecls(decl => { 53 | const {source, prop} = decl; 54 | const lineNum = source.start.line; 55 | const lineContent = getLineContent(lineNum, opts.fileContent); 56 | if (/^@/.test(prop) 57 | && !VARIABLE_NAME_REG.test(prop) 58 | ) { 59 | result.warn(RULENAME, { 60 | node: decl, 61 | ruleName: RULENAME, 62 | line: lineNum, 63 | col: source.start.column, 64 | message: '`' + lineContent + '` ' + MSG, 65 | colorMessage: '`' + lineContent.replace( 66 | prop, 67 | $1 => chalk.magenta($1) 68 | ) 69 | + '` ' 70 | + chalk.grey(MSG) 71 | }); 72 | } 73 | }); 74 | } 75 | ); 76 | -------------------------------------------------------------------------------- /src/rule/zero-unit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 0 值检验 3 | * 属性值为 0 时,必须省略可省的单位(长度单位如 px、em,不包括时间、角度等如 s、deg) 4 | * https://github.com/ecomfe/spec/blob/master/less-code-style.md#0-%E5%80%BC 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import chalk from 'chalk'; 9 | import postcss from 'postcss'; 10 | import {getLineContent} from '../util'; 11 | 12 | 'use strict'; 13 | 14 | /** 15 | * 规则名称 16 | * 17 | * @const 18 | * @type {string} 19 | */ 20 | const RULENAME = 'zero-unit'; 21 | 22 | /** 23 | * css 长度单位集合 24 | * https://developer.mozilla.org/en-US/docs/Web/CSS/length 25 | * 26 | * @const 27 | * @type {Array} 28 | */ 29 | const LENGTH_UNITS = [ 30 | // Relative length units 31 | 'em', 'ex', 'ch', 'rem', // Font-relative lengths 32 | 'vh', 'vw', 'vmin', 'vmax', // Viewport-percentage lengths 33 | 34 | // Absolute length units 35 | 'px', 'mm', 'cm', 'in', 'pt', 'pc' 36 | ]; 37 | 38 | /** 39 | * 数字正则 40 | * 41 | * @const 42 | * @type {RegExp} 43 | */ 44 | const PATTERN_NUMERIC = /\d+[\.\d]*/; 45 | 46 | /** 47 | * 错误信息 48 | * 49 | * @const 50 | * @type {string} 51 | */ 52 | const MSG = 'Values of 0 shouldn\'t have units specified'; 53 | 54 | /** 55 | * 行号的缓存,防止同一行多次报错 56 | * 57 | * @type {number} 58 | */ 59 | let lineCache = 0; 60 | 61 | /** 62 | * 具体的检测逻辑 63 | * 64 | * @param {Object} opts 参数 65 | * @param {*} opts.ruleVal 当前规则具体配置的值 66 | * @param {string} opts.fileContent 文件内容 67 | * @param {string} opts.filePath 文件路径 68 | */ 69 | export const check = postcss.plugin(RULENAME, opts => 70 | (css, result) => { 71 | if (!opts.ruleVal) { 72 | return; 73 | } 74 | 75 | lineCache = 0; 76 | 77 | css.walkDecls(decl => { 78 | const parts = postcss.list.space(decl.value); 79 | for (let i = 0, len = parts.length; i < len; i++) { 80 | const part = parts[i]; 81 | const numericVal = parseFloat(part); 82 | 83 | // TODO: background-color: darken(#fff, 0px); 84 | if (numericVal === 0) { 85 | const unit = part.replace(PATTERN_NUMERIC, ''); 86 | const source = decl.source; 87 | const line = source.start.line; 88 | if (LENGTH_UNITS.indexOf(unit) > -1 && lineCache !== line) { 89 | lineCache = line; 90 | const lineContent = getLineContent(line, source.input.css); 91 | result.warn(RULENAME, { 92 | node: decl, 93 | ruleName: RULENAME, 94 | line: line, 95 | col: source.start.column + decl.prop.length + decl.raws.between.length, 96 | message: MSG, 97 | colorMessage: '`' 98 | + lineContent.replace( 99 | decl.value, 100 | chalk.magenta(decl.value) 101 | ) 102 | + '` ' 103 | + chalk.grey(MSG) 104 | }); 105 | } 106 | } 107 | } 108 | }); 109 | } 110 | ); 111 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 通用方法 3 | * @author ielgnaw(wuji0223@gmail.com) 4 | */ 5 | 6 | import chalk from 'chalk'; 7 | import {statSync, existsSync, readFileSync} from 'fs'; 8 | import {glob, log, util as edpUtil, path as edpPath} from 'edp-core'; 9 | 10 | 'use strict'; 11 | 12 | /** 13 | * 调用给定的迭代函数 n 次,每一次传递 index 参数,调用迭代函数。 14 | * from underscore 15 | * 16 | * @param {number} n 迭代次数 17 | * @param {Function} iterator 处理函数 18 | * @param {Object} context 上下文 19 | * 20 | * @return {Array} 结果 21 | */ 22 | function times(n, iterator, context) { 23 | const accum = new Array(Math.max(0, n)); 24 | for (let i = 0; i < n; i++) { 25 | accum[i] = iterator.call(context, i); 26 | } 27 | return accum; 28 | } 29 | 30 | 31 | /** 32 | * 格式化信息 33 | * 34 | * @param {string} msg 输出的信息 35 | * @param {number} spaceCount 信息前面空格的个数即缩进的长度 36 | * 37 | * @return {string} 格式化后的信息 38 | */ 39 | export function formatMsg(msg, spaceCount = 0) { 40 | let space = ''; 41 | times(spaceCount, () => { 42 | space += ' '; 43 | }); 44 | return space + msg; 45 | } 46 | 47 | /** 48 | * 去掉 error.messages 里面重复的信息 49 | * 50 | * @param {Array} msg error.messages 51 | * 52 | * @return {Array} 结果数组,是一个新数组 53 | */ 54 | export function uniqueMsg(msg) { 55 | let ret = []; 56 | let tmp = []; 57 | for (let i = 0, j = 1, len = msg.length; i < len; i++, j++) { 58 | let cur = msg[i]; 59 | if (!cur.uniqueFlag) { 60 | ret.push(cur); 61 | } 62 | else { 63 | if (tmp.indexOf(cur.uniqueFlag) === -1) { 64 | tmp.push(cur.uniqueFlag); 65 | ret.push(cur); 66 | } 67 | } 68 | } 69 | return ret; 70 | } 71 | 72 | 73 | /** 74 | * 根据参数以及模式匹配相应的文件 75 | * 76 | * @param {Array} args 文件 77 | * @param {Array} patterns minimatch 模式 78 | * 79 | * @return {Array.} 匹配的文件集合 80 | */ 81 | export function getCandidates(args, patterns) { 82 | let candidates = []; 83 | 84 | args = args.filter(item => item !== '.'); 85 | 86 | if (!args.length) { 87 | candidates = glob.sync(patterns); 88 | } 89 | else { 90 | let i = -1; 91 | let len = args.length; 92 | while (++i < len) { 93 | let target = args[i]; 94 | if (!existsSync(target)) { 95 | log.warn('No such file or directory %s', target); 96 | continue; 97 | } 98 | 99 | let stat = statSync(target); 100 | if (stat.isDirectory()) { 101 | target = target.replace(/[\/|\\]+$/, ''); 102 | candidates.push.apply( 103 | candidates, 104 | glob.sync(target + '/' + patterns[0]) 105 | ); 106 | } 107 | /* istanbul ignore else */ 108 | else if (stat.isFile()) { 109 | candidates.push(target); 110 | } 111 | } 112 | } 113 | 114 | return candidates; 115 | } 116 | 117 | /** 118 | * 获取忽略的 pattern 119 | * 120 | * @param {string} file 文件路径 121 | * 122 | * @return {Array.} 结果 123 | */ 124 | export function getIgnorePatterns(file) { 125 | if (!existsSync(file)) { 126 | return []; 127 | } 128 | 129 | let patterns = readFileSync(file, 'utf-8').split(/\r?\n/g); 130 | return patterns.filter(item => item.trim().length > 0 && item[0] !== '#'); 131 | } 132 | 133 | const _IGNORE_CACHE = {}; 134 | 135 | /** 136 | * 判断一下是否应该忽略这个文件. 137 | * 138 | * @param {string} file 需要检查的文件路径. 139 | * @param {string=} name ignore文件的名称. 140 | * @return {boolean} 141 | */ 142 | export function isIgnored(file, name = '.jshintignore') { 143 | let ignorePatterns = null; 144 | 145 | file = edpPath.resolve(file); 146 | 147 | let key = name + '@' + edpPath.dirname(file); 148 | if (_IGNORE_CACHE[key]) { 149 | ignorePatterns = _IGNORE_CACHE[key]; 150 | } 151 | else { 152 | let options = { 153 | name: name, 154 | factory(item) { 155 | let config = {}; 156 | getIgnorePatterns(item).forEach(line => { 157 | config[line] = true; 158 | }); 159 | return config; 160 | } 161 | }; 162 | ignorePatterns = edpUtil.getConfig( 163 | edpPath.dirname(file), 164 | options 165 | ); 166 | 167 | _IGNORE_CACHE[key] = ignorePatterns; 168 | } 169 | 170 | let bizOrPkgRoot = process.cwd(); 171 | 172 | try { 173 | bizOrPkgRoot = edpPath.getRootDirectory(); 174 | } 175 | catch (ex) { 176 | } 177 | 178 | const dirname = edpPath.relative(bizOrPkgRoot, file); 179 | const isMatch = glob.match(dirname, Object.keys(ignorePatterns)); 180 | 181 | return isMatch; 182 | } 183 | 184 | /** 185 | * 根据行号获取当前行的内容 186 | * 187 | * @param {number} line 行号 188 | * @param {string} fileData 文件内容 189 | * @param {boolean} notRemoveSpace 不去掉前面的空格,为 true,则不去掉,为 false 则去掉 190 | * 这是后加的参数,为了兼容之前的代码 191 | * 192 | * @return {string} 当前行内容 193 | */ 194 | export function getLineContent(line, fileData, notRemoveSpace) { 195 | if (notRemoveSpace) { 196 | return fileData.split('\n')[line - 1]; 197 | } 198 | // 去掉前面的缩进 199 | return fileData.split('\n')[line - 1].replace(/^\s*/, ''); 200 | } 201 | 202 | /** 203 | * 根据索引把一行内容中的某个子串变色 204 | * 直接用正则匹配的话,可能会把这一行所有的 colorStr 给变色,所以要通过索引来判断 205 | * 206 | * @param {string} source 源字符串 207 | * @param {number} startIndex 开始的索引,通常是 col 208 | * @param {string} colorStr 要变色的字符串 209 | * 210 | * @return {string} 改变颜色后的字符串 211 | */ 212 | export function changeColorByIndex(source, startIndex, colorStr) { 213 | let ret = ''; 214 | if (source) { 215 | const colorStrLen = colorStr.length; 216 | const endIndex = startIndex + colorStrLen; 217 | ret = '' 218 | + source.slice(0, startIndex) // colorStr 前面的部分 219 | + chalk.magenta(source.slice(startIndex, endIndex)) // colorStr 的部分 220 | + source.slice(endIndex, source.length); // colorStr 后面的部分 221 | } 222 | return ret; 223 | } 224 | 225 | /** 226 | * 根据开始和结束的索引来高亮字符串的子串 227 | * 228 | * @param {string} source 源字符串 229 | * @param {number} startIndex 开始的索引 230 | * @param {number} endIndex 结束的索引 231 | * 232 | * @return {string} 改变颜色后的字符串 233 | */ 234 | export function changeColorByStartAndEndIndex(source, startIndex = 0, endIndex = 0) { 235 | if (!source) { 236 | return ''; 237 | } 238 | 239 | startIndex -= 1; 240 | endIndex -= 1; 241 | 242 | return '' 243 | + source.slice(0, startIndex) // colorStr 前面的部分 244 | + chalk.magenta(source.slice(startIndex, endIndex)) // colorStr 的部分 245 | + source.slice(endIndex, source.length); // colorStr 后面的部分 246 | } 247 | 248 | /** 249 | * 把错误信息放入 errors 数组中 250 | * 251 | * @param {string} ruleName 规则名称 252 | * @param {number} line 行号 253 | * @param {number} col 列号 254 | * @param {string} message 错误信息 255 | * @param {string} colorMessage 彩色错误信息 256 | */ 257 | // function addInvalidList(ruleName, line, col, message, colorMessage) { 258 | // this.push({ 259 | // ruleName: ruleName, 260 | // line: line, 261 | // col: col, 262 | // message: message, 263 | // colorMessage: colorMessage 264 | // }); 265 | // } 266 | -------------------------------------------------------------------------------- /test/fixture/.lesslintignore: -------------------------------------------------------------------------------- 1 | hex-colordd.less 2 | -------------------------------------------------------------------------------- /test/fixture/.lesslintrc: -------------------------------------------------------------------------------- 1 | import: false 2 | -------------------------------------------------------------------------------- /test/fixture/block-indent.less: -------------------------------------------------------------------------------- 1 | body { 2 | div ~ .c, 3 | .qqq { 4 | padding: 1; 5 | span { 6 | margin: 2; 7 | } 8 | } 9 | } 10 | 11 | // a, 12 | // b { 13 | // color: #fff; 14 | // } 15 | 16 | body { 17 | color: #fff; 18 | } 19 | 20 | div, 21 | a { 22 | width: 0; 23 | } 24 | 25 | 26 | @zero: 0px; 27 | 28 | body { 29 | margin: @zero; 30 | padding: @zero; 31 | 32 | a { 33 | color: red; 34 | } 35 | } 36 | 37 | .foo { 38 | margin: -10px; 39 | color: red; 40 | } 41 | 42 | .foo { 43 | color: red; 44 | width: 10px; 45 | } 46 | 47 | .foo { 48 | color: red; 49 | .bar { 50 | margin: -10px; 51 | .cc { 52 | width: 100px; 53 | .dd { 54 | height: 200px; 55 | } 56 | } 57 | } 58 | } 59 | 60 | .cc { 61 | 62 | } 63 | 64 | body { 65 | div ~ .c, 66 | .qqq { 67 | padding: 1; 68 | span { 69 | margin: 2; 70 | } 71 | } 72 | } 73 | 74 | div, 75 | a { 76 | width: 0; 77 | } 78 | -------------------------------------------------------------------------------- /test/fixture/config.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecomfe/node-lesslint/8adf5296bcd0e7cb51b01f15fcb8de28c27845b2/test/fixture/config.json -------------------------------------------------------------------------------- /test/fixture/config.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecomfe/node-lesslint/8adf5296bcd0e7cb51b01f15fcb8de28c27845b2/test/fixture/config.yml -------------------------------------------------------------------------------- /test/fixture/esui.less: -------------------------------------------------------------------------------- 1 | // 这里是对esui原有样式的修改 2 | 3 | .ui-commandmenu-layer { 4 | height: auto; 5 | } 6 | .skin-command-menu { 7 | .inline-block(); 8 | } 9 | 10 | .state-error { 11 | color: #f00; 12 | } 13 | 14 | .state-disabled { 15 | color: #999; 16 | } 17 | 18 | *+html .ui-select-layer { 19 | height: auto; 20 | 21 | .ui-select-item { 22 | /*width: 100%;*/ 23 | display: block; 24 | } 25 | } 26 | 27 | /** 28 | * ESUI (Enterprise Simple UI) 29 | * Copyright 2013 Baidu Inc. All rights reserved. 30 | * 31 | * @file 对话框样式 32 | * @author dbear 33 | */ 34 | .ui-actiondialog { 35 | background: none repeat scroll 0 0 #FFFFFF; 36 | border: 2px solid #394459; 37 | -moz-border-radius: 3px 3px 3px 3px; 38 | -webkit-border-radius: 3px 3px 3px 3px; 39 | border-radius: 3px 3px 3px 3px; 40 | position: absolute; 41 | width: 660px; 42 | z-index: 1203; 43 | } 44 | 45 | .ui-actiondialog .ui-actiondialog-head { 46 | background-image: url("./img/esui-actiondialog-head-bg.png"); 47 | background-repeat: repeat; 48 | background-position: 0 0; 49 | background-color: transparent; 50 | color: #FFFFFF; 51 | font-weight: bold; 52 | height: 26px; 53 | position: relative; 54 | width: 100%; 55 | margin: 0; 56 | padding: 0; 57 | -moz-user-select: none; 58 | -webkit-user-select: none; 59 | -ms-user-select: none; 60 | -khtml-user-select: none; 61 | user-select: none; 62 | } 63 | .ui-actiondialog .ui-actiondialog-head .ui-actiondialog-close-icon { 64 | background-image: url("../img/esui-actiondialog-head-close.png"); 65 | background-repeat: no-repeat; 66 | background-position: 0 0; 67 | background-color: transparent; 68 | height: 13px; 69 | width: 13px; 70 | position: absolute; 71 | right: 10px; 72 | top: 5px; 73 | cursor: pointer; 74 | } 75 | .ui-actiondialog .ui-actiondialog-head .ui-actiondialog-title { 76 | padding-left: 12px; 77 | line-height: 28px; 78 | font-size: 15px; 79 | line-height: 26px; 80 | } 81 | .ui-actiondialog .ui-actiondialog-body { 82 | padding: 10px 8px 8px; 83 | overflow: hidden; 84 | zoom: 1; 85 | } 86 | .ui-actiondialog .ui-actiondialog-foot-panel { 87 | -moz-user-select: none; 88 | -webkit-user-select: none; 89 | -ms-user-select: none; 90 | -khtml-user-select: none; 91 | user-select: none; 92 | margin: 0px 8px 3px; 93 | padding: 15px 0 20px 25px; 94 | border-top: 1px solid #F0F0F0; 95 | overflow: hidden; 96 | zoom: 1; 97 | } 98 | .ui-actiondialog .ui-actiondialog-foot-panel .ui-actiondialog-foot { 99 | float: right; 100 | } 101 | .ui-actiondialog .ui-actiondialog-foot-panel .ui-actiondialog-foot .ui-button { 102 | margin-right: 6px; 103 | } 104 | .ui-actiondialog-draggable .ui-actiondialog-head { 105 | cursor: move; 106 | } 107 | .ui-actiondialog-dragging { 108 | -moz-user-select: none; 109 | -webkit-user-select: none; 110 | -ms-user-select: none; 111 | -khtml-user-select: none; 112 | user-select: none; 113 | } 114 | 115 | /** 警告框,确认框 */ 116 | .skin-alert-dialog .ui-dialog-body div.ui-dialog-text, 117 | .skin-confirm-dialog .ui-dialog-body div.ui-dialog-text { 118 | float: left; 119 | margin-top: 20px; 120 | line-height: 18px; 121 | margin-right: 0; 122 | } 123 | /*.skin-alert-dialog .ui-dialog-body div.ui-dialog-text { 124 | width: 200px; 125 | }*/ 126 | 127 | .ui-actiondialog-mask { 128 | position: fixed; 129 | top: 0; 130 | left: 0; 131 | right: 0; 132 | bottom: 0; 133 | // 兼容IE6 134 | _position: absolute; 135 | _width: 100%; 136 | _height: 100%; 137 | background: #333; 138 | opacity: .2; 139 | filter: alpha(opacity=20); 140 | z-index: 1003; 141 | } 142 | 143 | .ui-sidebar-body { 144 | left: 0; 145 | right: 0; 146 | } 147 | 148 | // 弹出层的样式设定 149 | 150 | .skin-alert-dialog, 151 | .skin-confirm-dialog { 152 | width: 300px; 153 | 154 | .ui-dialog-body { 155 | .ui-dialog-icon-fail, 156 | .ui-dialog-icon-warning, 157 | .ui-dialog-icon-success, 158 | .ui-dialog-icon-confirm { 159 | display: block; 160 | float: left; 161 | height: 24px; 162 | width: 24px; 163 | margin: 15px 10px 8px 40px; 164 | _margin: 12px 10px 8px 25px; 165 | } 166 | 167 | .ui-dialog-icon-warning { 168 | background-image: url('./img/esui-actiondialog-notice.png'); 169 | } 170 | 171 | .ui-dialog-icon-success { 172 | background-image: url('./img/esui-actiondialog-done.png'); 173 | } 174 | 175 | .ui-dialog-icon-fail { 176 | background-image: url('./img/esui-actiondialog-fail.png'); 177 | } 178 | 179 | .ui-dialog-icon-confirm { 180 | background-image: url('./img/esui-actiondialog-question.png'); 181 | } 182 | } 183 | } 184 | 185 | .skin-exconfirm-dialog { 186 | 187 | line-height: 18px; 188 | 189 | .ui-dialog-body { 190 | padding: 15px; 191 | } 192 | 193 | } 194 | 195 | .ui-crumb { 196 | font-size: 12px; 197 | 198 | .ui-crumb-node-last { 199 | color: #000; 200 | } 201 | } 202 | 203 | // Button pseudo states 204 | // ------------------------- 205 | // Easily pump out default styles, as well as :hover, :focus, :active, 206 | // and disabled options for all buttons 207 | .btn-pseudo-states(@color, @background, @border) { 208 | color: @color; 209 | background-color: @background; 210 | border-color: @border; 211 | 212 | &:hover, 213 | &:focus, 214 | &:active, 215 | &.active { 216 | background: none; 217 | background-color: darken(@background, 5%); 218 | border-color: darken(@border, 10%); 219 | box-shadow: 0 0 2px lighten(@border, 50%); 220 | } 221 | 222 | &.disabled, 223 | &[disabled], 224 | fieldset[disabled] & { 225 | &, 226 | &:hover, 227 | &:focus, 228 | &:active, 229 | &.active { 230 | background-color: @background; 231 | border-color: @border 232 | } 233 | } 234 | 235 | &:before { 236 | content: ''; 237 | } 238 | } 239 | 240 | /** 冬天系列 */ 241 | @btn-primary-color: #fff; 242 | @btn-primary-bg: #4A74CE; 243 | @btn-primary-border: #428bca; 244 | 245 | .skin-winter-button { 246 | background: none; 247 | border: 1px solid @btn-primary-border; 248 | 249 | .btn-pseudo-states(@btn-primary-color, @btn-primary-bg, @btn-primary-border); 250 | } 251 | 252 | .skin-major-button:hover { 253 | .box-shadow(0 0 2px lighten(@btn-primary-border, 10%)); 254 | text-shadow: 1px 1px 1px #666; 255 | } 256 | 257 | .ui-tree .ui-tree-node .ui-tree-content-wrapper .skin-folder-tree-node-indicator-level-2 { 258 | display: none; 259 | } 260 | 261 | // 长得像link一样的button 262 | .skin-link, 263 | .skin-link-button { 264 | display: inline; 265 | padding: 0; 266 | margin: 0; 267 | margin-left: 5px; 268 | border: 0; 269 | color: @link-color; 270 | background: none; 271 | line-height: 1; 272 | height: auto; 273 | *height: auto; 274 | vertical-align: baseline; 275 | 276 | &:hover, &:active { 277 | border: none; 278 | text-decoration: underline; 279 | line-height: 1; 280 | height: auto; 281 | } 282 | } 283 | 284 | .ui-pager-item, .ui-pager-item-extend { 285 | &:hover { 286 | text-shadow: 1px 1px 1px #ccc; 287 | box-shadow: 0 0 1px #ccc; 288 | } 289 | } 290 | 291 | .ui-pager-item-current, .ui-pager-item-omit { 292 | &:hover { 293 | text-shadow: 0; 294 | box-shadow: 0; 295 | } 296 | } 297 | 298 | .ui-textarea-validity-label-invalid { 299 | color: #c00; 300 | padding-left: 10px; 301 | } 302 | 303 | a.ui-button { 304 | text-decoration: none; 305 | } 306 | 307 | .ui-boxgroup-radio input { 308 | margin-top: 0; 309 | } 310 | 311 | .ui-textline .ui-textbox { 312 | right: 0; 313 | bottom: 0; 314 | } 315 | -------------------------------------------------------------------------------- /test/fixture/hex-color.less: -------------------------------------------------------------------------------- 1 | @color1: white; 2 | @color2: rgb(255, 255, 255); 3 | @color3: rgba(255, 255, 255, 1); 4 | @color4: #ffffFf; 5 | @color5: hsl(120, 65%, 75%); 6 | @color6: hsla(120, 65%, 75%, 1); 7 | @color7: #fff; 8 | @color8: black; 9 | @color9: #ffffff; 10 | 11 | @no: 1px; 12 | @white-color: #f3f3f3; 13 | 14 | div { 15 | border: 1px solid hsl(120, 65%, 75%); 16 | color: hsl(10, 5%, 5%); 17 | span { 18 | color: rgb(255, 255, 255); 19 | } 20 | } 21 | 22 | @import './esui'; -------------------------------------------------------------------------------- /test/fixture/import.less: -------------------------------------------------------------------------------- 1 | @aa: 1px; 2 | 3 | @import " Tip"; 4 | 5 | 6 | @import 'color'; 7 | 8 | @import "color.less"; 9 | 10 | @import aaa; 11 | @color1: white; 12 | 13 | @font-face { 14 | font-family: 'iconfont'; 15 | src: url('../font/iconfont.eot?t=1459183273'); 16 | src: url('../font/iconfont.eot?t=1459183273#iefix') format('embedded-opentype'), 17 | url('../font/iconfont.woff?t=1459183273') format('woff'), 18 | url('../font/iconfont.ttf?t=1459183273') format('truetype'), 19 | url('../font/iconfont.svg?t=1459183273#iconfont') format('svg'); 20 | } -------------------------------------------------------------------------------- /test/fixture/leading-zero.less: -------------------------------------------------------------------------------- 1 | 2 | @number1: 0.2px; 3 | @number2: .6px; 4 | @number2: 10.3px; 5 | @number3: 1.3px; 6 | @margin: 0.33em 0.1em 0.5rem; 7 | @color: #fff; 8 | 9 | div { 10 | height: .2px; 11 | width: 0.3px; 12 | transition-duration: 0.5s, 0.7s; 13 | margin: -0.5px; 14 | span { 15 | width: 0.99px; 16 | } 17 | }; 18 | 19 | 20 | .for-test { 21 | color: rgba(0, 0, 0, 0.25); 22 | opacity: 0.6; 23 | transform: scale(0.5); 24 | } 25 | 26 | -------------------------------------------------------------------------------- /test/fixture/mixins.less: -------------------------------------------------------------------------------- 1 | @import 'ccc'; 2 | 3 | @import "qqq"; 4 | 5 | 6 | 7 | .a() { 8 | color: red; 9 | } 10 | 11 | .b() { 12 | width: 100px; 13 | } 14 | 15 | body { 16 | .a(); 17 | } -------------------------------------------------------------------------------- /test/fixture/operate-unit.less: -------------------------------------------------------------------------------- 1 | @d: 0; 2 | @b: 1px + @d; 3 | @c: @b + 13; 4 | @a: 12 + @c; 5 | -------------------------------------------------------------------------------- /test/fixture/require-after-space.less: -------------------------------------------------------------------------------- 1 | span { 2 | *border-color:rgb(200,200,200);color: rgb(100,100,100); 3 | } 4 | 5 | div { 6 | background-image: 2px 2px url(data:image/gif;base64,R0lGODlhCAAHAIABAGZmZv///yH5BAEAAAEALAAAAAAIAAcAAAINjAOnkIr8mGxGOnhPAQA7); 7 | } 8 | -------------------------------------------------------------------------------- /test/fixture/require-around-space.less: -------------------------------------------------------------------------------- 1 | @a: 12px+ 1; 2 | @b: 12 *5; 3 | @c: 12-1; 4 | @d: 12/4; 5 | 6 | body { 7 | font: italic bold 12px/20px arial,sans-serif; 8 | height: 33 +33; 9 | } 10 | 11 | div { 12 | font: bold 13px/30px ''; 13 | font: bold 13px/30px '宋体'; 14 | } 15 | -------------------------------------------------------------------------------- /test/fixture/require-before-space.less: -------------------------------------------------------------------------------- 1 | @zero: 0; 2 | 3 | body{ 4 | margin: @zero; 5 | padding: @zero; 6 | } 7 | 8 | 9 | .aa{ 10 | .bb{ 11 | width: 0; 12 | .qq{ 13 | height: 0; 14 | } 15 | } 16 | color: #ff0; 17 | } 18 | 19 | .foo { 20 | .transition(width 1s); 21 | .size(30px, 20px); 22 | .clearfix(); 23 | } 24 | -------------------------------------------------------------------------------- /test/fixture/require-newline.less: -------------------------------------------------------------------------------- 1 | 2 | @media handheld and (min-width:360px), 3 | screen and (min-width:480px),screen and (max-width:980px) { 4 | body {font-size:large;} 5 | } 6 | 7 | @media screen and (min-width:1024px) and (max-width:1280px) { 8 | body {font-size:medium;} 9 | } 10 | 11 | @media screen and (min-width:800px),print and (min-width:7in) { 12 | body {font-size:small;} 13 | } 14 | 15 | span, label { 16 | color: #f00; 17 | } 18 | 19 | 20 | div, 21 | a:hover { 22 | color: #ccc; 23 | } 24 | 25 | 26 | span, 27 | label { 28 | color: #f00; 29 | } 30 | 31 | p { 32 | width: 10px;height: 30px; 33 | } 34 | 35 | 36 | p, 37 | i,.cc { 38 | width: 100px;height: 100px; 39 | } 40 | 41 | .foo { 42 | .transition(width 1s); 43 | .size(30px, 20px); 44 | .clearfix(); 45 | } 46 | 47 | .textOverflowMulti(@line: 3, @bg: #fff) { 48 | position: relative; 49 | overflow: hidden; 50 | max-height: @line * 1.5em; 51 | margin-right: -1em; 52 | padding-right: 1em; 53 | line-height: 1.5em; 54 | text-align: justify; 55 | &:before { 56 | content: "..."; 57 | position: absolute; 58 | right: 14px; 59 | bottom: 0; 60 | padding: 0 1px; 61 | background: @bg; 62 | } 63 | &:after { 64 | content: ""; 65 | position: absolute; 66 | right: 14px; 67 | width: 1em; 68 | height: 1em; 69 | margin-top: .2em; 70 | background: #fff; 71 | } 72 | } 73 | 74 | -------------------------------------------------------------------------------- /test/fixture/shorthand.less: -------------------------------------------------------------------------------- 1 | body { 2 | color: #ffffff; 3 | border-color: #ffaabb; 4 | } 5 | 6 | .c { 7 | color: white; 8 | border: 1px solid #ffffff #ccddcc; 9 | } 10 | 11 | // .right { 12 | // color: #fff; 13 | // border-color: 2px solid #abc; 14 | // } -------------------------------------------------------------------------------- /test/fixture/single-comment.less: -------------------------------------------------------------------------------- 1 | @import './leading-zero'; 2 | 3 | @hhh: 0px; 4 | 5 | /** 6 | * 合法多行注释 7 | * sadsadsadsada 8 | */ 9 | a { 10 | color: red; 11 | } 12 | /* *12312312**/ 13 | 14 | // aaaaa 15 | // bbbbb 16 | 17 | div{ 18 | // 合法单行注释 19 | border: 0px solid #fff; 20 | height: 0px; 21 | 22 | width: 0px; 23 | 24 | /** invalid single comment **/ 25 | color: #f00; 26 | } -------------------------------------------------------------------------------- /test/fixture/test.less: -------------------------------------------------------------------------------- 1 | @foo: 1px; 2 | @bar: 2px; 3 | @u-border: 1px solid #fff; 4 | 5 | .test { 6 | @c: 0px; 7 | border: 0px solid #fff; 8 | width: 1px; 9 | } 10 | 11 | 12 | // root { 13 | // selectors: null, 14 | // rules: [ 15 | // [length]: 4, 16 | // { 17 | // name: '', 18 | // value: { 19 | // value: [ 20 | // { 21 | // value: [ 22 | // { 23 | // value:'', 24 | // unit: {...} 25 | // }, 26 | // [length]: 1 27 | // ] 28 | // } 29 | // ] 30 | // } 31 | // } 32 | // ] 33 | // } 34 | -------------------------------------------------------------------------------- /test/fixture/vari.less: -------------------------------------------------------------------------------- 1 | @color1: green; 2 | @color2: rgb(255, 255, 255); 3 | 4 | .absolute(...) { 5 | position: absolute; 6 | } 7 | -------------------------------------------------------------------------------- /test/fixture/variable-name.less: -------------------------------------------------------------------------------- 1 | @img-url: './test.png'; 2 | @url: ''; 3 | @fooBar: 10px; 4 | @fooBar1: 10px; 5 | @foo-bar: 20px; 6 | @barFoo: 12px; 7 | @urlA: ~"@{img-url}"; 8 | @is-url-exp: ~`/^url\([^()]+\)$/i.test("@{url}") ? 'true' : 'false'`; 9 | 10 | @import 'sds'; 11 | 12 | div{ 13 | @cc-aa: 1px; 14 | @ccAA: 2px; 15 | @CCCC: 1px; 16 | color: @fooBar; 17 | background-color: @fooBar1; 18 | } -------------------------------------------------------------------------------- /test/fixture/zero-unit.less: -------------------------------------------------------------------------------- 1 | // @import './comment.less'; 2 | 3 | @right-unit: 1px; 4 | @right-unit1: 12px; 5 | @right-unit2: 0deg; 6 | @right-unit3: 0s; 7 | @right-unit4: 0ms; 8 | @wrong-unit: 0px; 9 | @wrong-unit1: 0em; 10 | @color: green; 11 | 12 | @import './variable'; 13 | 14 | 15 | div{ 16 | background-color: darken(#fff, 0%); 17 | margin: 0px @right-unit; 18 | h1 { 19 | height: @wrong-unit; 20 | h2 { 21 | height: @wrong-unit; 22 | box-shadow: 0 0px 2px lighten(@color, 0px); 23 | } 24 | } 25 | }; 26 | 27 | -------------------------------------------------------------------------------- /test/spec/checker.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file lib/checker.js 的测试用例 3 | * @author ielgnaw(wuji0223@gmail.com) 4 | */ 5 | 6 | 'use strict'; 7 | 8 | import chai from 'chai'; 9 | import path from 'path'; 10 | 11 | let checker = require(path.join(__dirname, '../../src', 'checker')); 12 | let config = require(path.join(__dirname, '../../src', 'config')); 13 | 14 | const expect = chai.expect; 15 | 16 | /* globals describe, it */ 17 | 18 | /* eslint-disable max-nested-callbacks */ 19 | describe('checker test suite\n', () => { 20 | describe('checkString: \n', () => { 21 | it('should be return right result', () => { 22 | const filePath = 'path/to/file.css'; 23 | const fileContent = '\np {\n height: 0px;}\n\n'; 24 | return checker.checkString(fileContent, filePath, config.loadConfig(filePath, false)).then(invalidList => { 25 | expect(invalidList[0].messages.length).to.equal(1); 26 | }); 27 | }); 28 | 29 | it('should catch error with line', () => { 30 | const filePath = 'path/to/file.css'; 31 | const fileContent = '\np {\nheight: 0px\n\n'; 32 | return checker.checkString(fileContent, filePath, config.loadConfig(filePath, false)).then(() => { 33 | }).catch(invalidList => { 34 | const messages = invalidList[0].messages; 35 | expect(messages[0].line).to.equal(2); 36 | expect(messages[0].ruleName).to.equal('CssSyntaxError'); 37 | }); 38 | }); 39 | }); 40 | 41 | describe('check: \n', () => { 42 | it('should be return right result', () => { 43 | const errors = []; 44 | const file = { 45 | path: 'path/to/file.css', 46 | content: '\np {\n height: 0px;}\n\n' 47 | }; 48 | return checker.check(file, errors, () => {}).then(() => { 49 | expect(errors[0].messages.length).to.equal(2); 50 | }); 51 | }); 52 | 53 | it('should catch error with line', () => { 54 | const errors = []; 55 | const file = { 56 | path: 'path/to/file.css', 57 | content: '\np {\n height: 0px\ncolor: ccc;\n\n' 58 | }; 59 | return checker.check(file, errors, () => {}).then(() => { 60 | config.clearStorage(); 61 | const messages = errors[0].messages; 62 | expect(messages[0].line).to.equal(3); 63 | expect(messages[0].ruleName).to.equal('CssSyntaxError'); 64 | }); 65 | }); 66 | }); 67 | }); 68 | /* eslint-enable max-nested-callbacks */ 69 | -------------------------------------------------------------------------------- /test/spec/rule.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file lib/rule 的测试用例 3 | * @author ielgnaw(wuji0223@gmail.com) 4 | */ 5 | 6 | 'use strict'; 7 | 8 | import chai from 'chai'; 9 | import fs from 'fs'; 10 | import path from 'path'; 11 | 12 | let checker = require(path.join(__dirname, '../../src', 'checker')); 13 | let config = require(path.join(__dirname, '../../src', 'config')); 14 | 15 | const expect = chai.expect; 16 | 17 | /* globals describe, it */ 18 | 19 | /* eslint-disable max-nested-callbacks */ 20 | describe('rule test suite\n', () => { 21 | describe('block-indent: ', () => { 22 | it('should return right message length', () => { 23 | const filePath = path.join(__dirname, '../fixture/block-indent.less'); 24 | const fileContent = fs.readFileSync( 25 | path.join(__dirname, '../fixture/block-indent.less'), 26 | 'utf8' 27 | ).replace(/\r\n?/g, '\n'); 28 | 29 | const file = { 30 | path: filePath, 31 | content: fileContent 32 | }; 33 | 34 | const errors = []; 35 | return checker.check(file, errors, () => {}).then(() => { 36 | expect(errors[0].messages.length).to.equal(8); 37 | }); 38 | }); 39 | }); 40 | 41 | describe('hex-color: ', () => { 42 | it('should return right message length', () => { 43 | const filePath = path.join(__dirname, '../fixture/hex-color.less'); 44 | const fileContent = fs.readFileSync( 45 | path.join(__dirname, '../fixture/hex-color.less'), 46 | 'utf8' 47 | ).replace(/\r\n?/g, '\n'); 48 | 49 | const file = { 50 | path: filePath, 51 | content: fileContent 52 | }; 53 | 54 | const errors = []; 55 | return checker.check(file, errors, () => {}).then(() => { 56 | expect(errors[0].messages.length).to.equal(9); 57 | }); 58 | }); 59 | }); 60 | 61 | describe('import: ', () => { 62 | it('should return right message length', () => { 63 | // 清空 config storage,设置成默认的 config 64 | // 因为 fixture 目录下的 .lesslintrc 中配置了 import: false 65 | config.clearStorage(); 66 | config.loadConfig('.', true); 67 | 68 | const filePath = path.join(__dirname, '../fixture/import.less'); 69 | const fileContent = fs.readFileSync( 70 | path.join(__dirname, '../fixture/import.less'), 71 | 'utf8' 72 | ).replace(/\r\n?/g, '\n'); 73 | 74 | const file = { 75 | path: filePath, 76 | content: fileContent 77 | }; 78 | 79 | const errors = []; 80 | return checker.check(file, errors, () => {}).then(() => { 81 | expect(errors[0].messages.length).to.equal(6); 82 | }); 83 | }); 84 | }); 85 | 86 | describe('leading-zero: ', () => { 87 | it('should return right message length', () => { 88 | const filePath = path.join(__dirname, '../fixture/leading-zero.less'); 89 | const fileContent = fs.readFileSync( 90 | path.join(__dirname, '../fixture/leading-zero.less'), 91 | 'utf8' 92 | ).replace(/\r\n?/g, '\n'); 93 | 94 | const file = { 95 | path: filePath, 96 | content: fileContent 97 | }; 98 | 99 | const errors = []; 100 | return checker.check(file, errors, () => {}).then(() => { 101 | expect(errors[0].messages.length).to.equal(14); 102 | }); 103 | }); 104 | }); 105 | 106 | describe('operate-unit: ', () => { 107 | it('should return right message length', () => { 108 | const filePath = path.join(__dirname, '../fixture/operate-unit.less'); 109 | const fileContent = fs.readFileSync( 110 | path.join(__dirname, '../fixture/operate-unit.less'), 111 | 'utf8' 112 | ).replace(/\r\n?/g, '\n'); 113 | 114 | const file = { 115 | path: filePath, 116 | content: fileContent 117 | }; 118 | 119 | const errors = []; 120 | return checker.check(file, errors, () => {}).then(() => { 121 | expect(errors[0].messages.length).to.equal(2); 122 | }); 123 | }); 124 | }); 125 | 126 | describe('require-after-space: ', () => { 127 | it('should return right message length', () => { 128 | const filePath = path.join(__dirname, '../fixture/require-after-space.less'); 129 | const fileContent = fs.readFileSync( 130 | path.join(__dirname, '../fixture/require-after-space.less'), 131 | 'utf8' 132 | ).replace(/\r\n?/g, '\n'); 133 | 134 | const file = { 135 | path: filePath, 136 | content: fileContent 137 | }; 138 | 139 | const errors = []; 140 | return checker.check(file, errors, () => {}).then(() => { 141 | expect(errors[0].messages.length).to.equal(8); 142 | }); 143 | }); 144 | }); 145 | 146 | describe('require-around-space: ', () => { 147 | it('should return right message length', () => { 148 | const filePath = path.join(__dirname, '../fixture/require-around-space.less'); 149 | const fileContent = fs.readFileSync( 150 | path.join(__dirname, '../fixture/require-around-space.less'), 151 | 'utf8' 152 | ).replace(/\r\n?/g, '\n'); 153 | 154 | const file = { 155 | path: filePath, 156 | content: fileContent 157 | }; 158 | 159 | const errors = []; 160 | return checker.check(file, errors, () => {}).then(() => { 161 | expect(errors[0].messages.length).to.equal(5); 162 | }); 163 | }); 164 | }); 165 | 166 | describe('require-before-space: ', () => { 167 | it('should return right message length', () => { 168 | const filePath = path.join(__dirname, '../fixture/require-before-space.less'); 169 | const fileContent = fs.readFileSync( 170 | path.join(__dirname, '../fixture/require-before-space.less'), 171 | 'utf8' 172 | ).replace(/\r\n?/g, '\n'); 173 | 174 | const file = { 175 | path: filePath, 176 | content: fileContent 177 | }; 178 | 179 | const errors = []; 180 | return checker.check(file, errors, () => {}).then(() => { 181 | expect(errors[0].messages.length).to.equal(9); 182 | }); 183 | }); 184 | }); 185 | 186 | describe('require-newline: ', () => { 187 | it('should return right message length', () => { 188 | const filePath = path.join(__dirname, '../fixture/require-newline.less'); 189 | const fileContent = fs.readFileSync( 190 | path.join(__dirname, '../fixture/require-newline.less'), 191 | 'utf8' 192 | ).replace(/\r\n?/g, '\n'); 193 | 194 | const file = { 195 | path: filePath, 196 | content: fileContent 197 | }; 198 | 199 | const errors = []; 200 | return checker.check(file, errors, () => {}).then(() => { 201 | expect(errors[0].messages.length).to.equal(7); 202 | }); 203 | }); 204 | }); 205 | 206 | describe('shorthand: ', () => { 207 | it('should return right message length', () => { 208 | const filePath = path.join(__dirname, '../fixture/shorthand.less'); 209 | const fileContent = fs.readFileSync( 210 | path.join(__dirname, '../fixture/shorthand.less'), 211 | 'utf8' 212 | ).replace(/\r\n?/g, '\n'); 213 | 214 | const file = { 215 | path: filePath, 216 | content: fileContent 217 | }; 218 | 219 | const errors = []; 220 | return checker.check(file, errors, () => {}).then(() => { 221 | expect(errors[0].messages.length).to.equal(4); 222 | }); 223 | }); 224 | }); 225 | 226 | describe('single-comment: ', () => { 227 | it('should return right message length', () => { 228 | const filePath = path.join(__dirname, '../fixture/single-comment.less'); 229 | const fileContent = fs.readFileSync( 230 | path.join(__dirname, '../fixture/single-comment.less'), 231 | 'utf8' 232 | ).replace(/\r\n?/g, '\n'); 233 | 234 | const file = { 235 | path: filePath, 236 | content: fileContent 237 | }; 238 | 239 | const errors = []; 240 | return checker.check(file, errors, () => {}).then(() => { 241 | expect(errors[0].messages.length).to.equal(9); 242 | }); 243 | }); 244 | }); 245 | 246 | describe('variable-name: ', () => { 247 | it('should return right message length', () => { 248 | const filePath = path.join(__dirname, '../fixture/variable-name.less'); 249 | const fileContent = fs.readFileSync( 250 | path.join(__dirname, '../fixture/variable-name.less'), 251 | 'utf8' 252 | ).replace(/\r\n?/g, '\n'); 253 | 254 | const file = { 255 | path: filePath, 256 | content: fileContent 257 | }; 258 | 259 | const errors = []; 260 | return checker.check(file, errors, () => {}).then(() => { 261 | expect(errors[0].messages.length).to.equal(10); 262 | }); 263 | }); 264 | }); 265 | 266 | describe('zero-unit: ', () => { 267 | it('should return right message length', () => { 268 | const filePath = path.join(__dirname, '../fixture/zero-unit.less'); 269 | const fileContent = fs.readFileSync( 270 | path.join(__dirname, '../fixture/zero-unit.less'), 271 | 'utf8' 272 | ).replace(/\r\n?/g, '\n'); 273 | 274 | const file = { 275 | path: filePath, 276 | content: fileContent 277 | }; 278 | 279 | const errors = []; 280 | return checker.check(file, errors, () => {}).then(() => { 281 | expect(errors[0].messages.length).to.equal(7); 282 | }); 283 | }); 284 | }); 285 | }); 286 | /* eslint-enable max-nested-callbacks */ 287 | -------------------------------------------------------------------------------- /test/spec/util.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file lib/util.js 的测试用例 3 | * @author ielgnaw(wuji0223@gmail.com) 4 | */ 5 | 6 | 'use strict'; 7 | 8 | import chai from 'chai'; 9 | import fs from 'fs'; 10 | import path from 'path'; 11 | 12 | let util = require(path.join(__dirname, '../../src', 'util')); 13 | 14 | const expect = chai.expect; 15 | 16 | /* globals describe, it */ 17 | 18 | /* eslint-disable max-nested-callbacks */ 19 | describe('util test suite\n', () => { 20 | describe('formatMsg: ', () => { 21 | it('should return right message', () => { 22 | const ret = util.formatMsg('abcdefg', 5); 23 | expect(ret).to.equal(' abcdefg'); 24 | }); 25 | }); 26 | 27 | describe('lineContent: ', () => { 28 | const candidateLineNumber = 1; 29 | 30 | const fileContent = fs.readFileSync( 31 | path.join(__dirname, '../fixture/test.less'), 32 | 'utf8' 33 | ).replace(/\r\n?/g, '\n'); 34 | 35 | it('should return right linecontent: ', () => { 36 | const lineContent = util.getLineContent(candidateLineNumber, fileContent); 37 | expect(lineContent).to.equal('@foo: 1px;'); 38 | }); 39 | }); 40 | 41 | describe('uniqueMsg: ', () => { 42 | it('should return right result', () => { 43 | const msg = [ 44 | { 45 | uniqueFlag: '111', 46 | ruleName: 'hex-color', 47 | line: 1, 48 | message: '' 49 | + '`@color1: green;` Color value must use the hexadecimal mark forms ' 50 | + 'such as `#RRGGBB`. Don\'t use RGB、HSL expression', 51 | colorMessage: '' 52 | + '`@color1\u001b[35m: green\u001b[39m;` \u001b[90mColor value must use ' 53 | + 'the hexadecimal mark forms such as `#RRGGBB`. Don\'t use RGB、HSL ' 54 | + 'expression\u001b[39m' 55 | }, 56 | { 57 | uniqueFlag: '111', 58 | ruleName: 'hex-color', 59 | line: 2, 60 | message: '' 61 | + '`@color2: rgb(255, 255, 255);` Color value must use the hexadecimal mark forms ' 62 | + 'such as `#RRGGBB`. Don\'t use RGB、HSL expression', 63 | colorMessage: '' 64 | + '`@color2: \u001b[35mrgb\u001b[39m(255, 255, 255);` \u001b[90mColor value ' 65 | + 'must use the hexadecimal mark forms such as `#RRGGBB`. Don\'t use RGB、HSL ' 66 | + 'expression\u001b[39m' 67 | }, 68 | { 69 | ruleName: 'hex-color', 70 | line: 2, 71 | message: '' 72 | + '`@color2: rgb(255, 255, 255);` Color value must use the hexadecimal mark forms ' 73 | + 'such as `#RRGGBB`. Don\'t use RGB、HSL expression', 74 | colorMessage: '' 75 | + '`@color2: \u001b[35mrgb\u001b[39m(255, 255, 255);` \u001b[90mColor value ' 76 | + 'must use the hexadecimal mark forms such as `#RRGGBB`. Don\'t use RGB、HSL ' 77 | + 'expression\u001b[39m' 78 | } 79 | ]; 80 | expect(util.uniqueMsg(msg).length).to.equal(2); 81 | }); 82 | }); 83 | 84 | describe('getCandidates: ', function () { 85 | this.timeout(50000); 86 | it('should return right result', () => { 87 | const patterns = [ 88 | '**/block-indent.less', 89 | '!**/{output,node_modules,asset,dist,release,doc,dep,report,*.bak}/**' 90 | ]; 91 | 92 | const candidates = util.getCandidates([], patterns); 93 | expect(candidates.length).to.equal(1); 94 | expect(candidates[0]).to.equal('test/fixture/block-indent.less'); 95 | 96 | const patterns1 = [ 97 | '**/*.less', 98 | '!**/{output,node_modules,asset,dist,release,doc,dep,report,*.bak}/**' 99 | ]; 100 | const candidates1 = util.getCandidates([], patterns1); 101 | expect(candidates1.length).to.equal(17); 102 | 103 | process.chdir(__dirname); 104 | const patterns2 = [ 105 | '**/*.js', 106 | '!**/{output,node_modules,asset,dist,release,doc,dep,report,*.bak}/**' 107 | ]; 108 | const candidates2 = util.getCandidates(['rule.spec.js', '.'], patterns2); 109 | expect(candidates2[0]).to.equal('rule.spec.js'); 110 | 111 | const patterns3 = [ 112 | '**/*.js', 113 | '!**/{output,node_modules,asset,dist,release,doc,dep,report,*.bak}/**' 114 | ]; 115 | const candidates3 = util.getCandidates(['not-exist.js', '.'], patterns3); 116 | expect(candidates3.length).to.equal(0); 117 | 118 | const patterns4 = [ 119 | '**/*.js', 120 | '!**/{output,node_modules,asset,dist,release,doc,dep,report,*.bak}/**' 121 | ]; 122 | const candidates4 = util.getCandidates(['aaa', '.'], patterns4); 123 | expect(candidates4.length).to.equal(0); 124 | }); 125 | }); 126 | 127 | 128 | describe('getIgnorePatterns: ', () => { 129 | it('should return right result', () => { 130 | expect( 131 | util.getIgnorePatterns(path.join(__dirname, '../', 'fixture/block-indent.less')).length 132 | ).to.equal(63); 133 | }); 134 | it('should return right result', () => { 135 | expect( 136 | util.getIgnorePatterns(path.join(__dirname, '../', 'fixture/no-exist.less')).length 137 | ).to.equal(0); 138 | }); 139 | }); 140 | }); 141 | /* eslint-enable max-nested-callbacks */ 142 | --------------------------------------------------------------------------------