├── .stlintrc
├── test2.styl
├── .gitignore
├── .travis.yml
├── src
├── core
│ ├── ast
│ │ ├── each.ts
│ │ ├── obj.ts
│ │ ├── rgb.ts
│ │ ├── params.ts
│ │ ├── return.ts
│ │ ├── atrule.ts
│ │ ├── block.ts
│ │ ├── bool.ts
│ │ ├── group.ts
│ │ ├── keyframes.ts
│ │ ├── querylist.ts
│ │ ├── supports.ts
│ │ ├── unit.ts
│ │ ├── import.ts
│ │ ├── comment.ts
│ │ ├── condition.ts
│ │ ├── query.ts
│ │ ├── unaryop.ts
│ │ ├── media.ts
│ │ ├── ident.ts
│ │ ├── literal.ts
│ │ ├── func.ts
│ │ ├── call.ts
│ │ ├── property.ts
│ │ ├── ternary.ts
│ │ ├── feature.ts
│ │ ├── selector.ts
│ │ ├── tree.ts
│ │ ├── member.ts
│ │ ├── binop.ts
│ │ ├── value.ts
│ │ ├── index.ts
│ │ └── node.ts
│ ├── types
│ │ ├── IStats.ts
│ │ ├── reader.ts
│ │ ├── response.ts
│ │ ├── context.ts
│ │ ├── line.ts
│ │ ├── autocomplete.ts
│ │ ├── state.ts
│ │ ├── message.ts
│ │ ├── content.ts
│ │ ├── ast
│ │ │ ├── snode.ts
│ │ │ └── node.ts
│ │ ├── reporter.ts
│ │ ├── config.ts
│ │ └── rule.ts
│ ├── helpers
│ │ ├── lcfirst.ts
│ │ ├── splitAndStrip.ts
│ │ ├── index.ts
│ │ ├── isPlainObject.ts
│ │ ├── mergeArray.ts
│ │ ├── splitLines.ts
│ │ ├── checkPrefix.ts
│ │ ├── shortcutColor.ts
│ │ ├── calcPosition.ts
│ │ ├── unwrapObject.ts
│ │ └── objToHash.ts
│ ├── reporters
│ │ ├── silentReporter.ts
│ │ ├── jsonReporter.ts
│ │ └── rawReporter.ts
│ ├── runner.ts
│ ├── visitor.ts
│ ├── preprocessor.ts
│ ├── documentator
│ │ ├── readmePatcher.ts
│ │ └── documentator.ts
│ ├── autocomplete.ts
│ ├── parser.ts
│ ├── line.ts
│ ├── content.ts
│ ├── reporter.ts
│ ├── reader.ts
│ ├── baseConfig.ts
│ ├── rule.ts
│ └── checker.ts
├── doc.ts
├── autocomplete
│ └── index.ts
├── rules
│ ├── emptyLines.ts
│ ├── index.ts
│ ├── mixedSpaces.ts
│ ├── commaInObject.ts
│ ├── leadingZero.ts
│ ├── prefixVarsWithDollar.ts
│ ├── semicolons.ts
│ ├── quotePref.ts
│ ├── colons.ts
│ ├── useMixinInsteadUnit.ts
│ ├── brackets.ts
│ ├── depthControl.ts
│ ├── color.ts
│ └── sortOrder.ts
├── typings.d.ts
├── preprocessors
│ └── safeComments.ts
├── commander.ts
├── config.ts
├── linter.ts
└── defaultRules.json
├── tests
├── staff
│ ├── disable-sort-order-rule.json
│ ├── subfolder
│ │ ├── extends.json
│ │ └── preprocessors
│ │ │ └── preprocessor.js
│ ├── preprocessor.js
│ ├── extra
│ │ └── testRule.js
│ ├── extends.js
│ ├── test.styl
│ ├── config.json
│ ├── extends.json
│ └── bootstrap.ts
├── rules
│ ├── emptyLinesTest.ts
│ ├── bracketsTest.ts
│ ├── semicolonTest.ts
│ ├── leadingZeroTest.ts
│ ├── mixedSpacesTest.ts
│ ├── quotePrefTest.ts
│ ├── commaInObjectTest.ts
│ ├── prefixVarsWithDollarTest.ts
│ ├── useMixinInsteadUnitTest.ts
│ ├── colonsTest.ts
│ ├── colorTest.ts
│ ├── sortOrderTest.ts
│ └── depthControlTest.ts
├── grepTest.ts
├── helpers
│ ├── shortcutColorTest.ts
│ └── calcPositionTest.ts
├── beforeCheckNodeTest.ts
├── extraRulesTest.ts
├── smokeTest.ts
├── extendsConfigTest.ts
├── ignoreDirectivesTest.ts
└── configTest.ts
├── .prettierrc.json
├── test.styl
├── .editorconfig
├── tsconfig.json
├── webpack.config.js
├── LICENSE
├── index.ts
├── bin
└── stlint
├── package.json
├── tslint.json
└── readme.md
/.stlintrc:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {}
3 | }
4 |
--------------------------------------------------------------------------------
/test2.styl:
--------------------------------------------------------------------------------
1 | .tab
2 | color #DDD;
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | node_modules/
3 | .DS_Store
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: node_js
3 | node_js:
4 | - 8
5 | - stable
6 |
--------------------------------------------------------------------------------
/src/core/ast/each.ts:
--------------------------------------------------------------------------------
1 | import { Node } from './node';
2 |
3 | export class Each extends Node {}
4 |
--------------------------------------------------------------------------------
/src/core/ast/obj.ts:
--------------------------------------------------------------------------------
1 | import { Node } from './node';
2 |
3 | export class Obj extends Node {}
4 |
--------------------------------------------------------------------------------
/src/core/ast/rgb.ts:
--------------------------------------------------------------------------------
1 | import { Unit } from './unit';
2 |
3 | export class RGB extends Unit {}
4 |
--------------------------------------------------------------------------------
/src/core/ast/params.ts:
--------------------------------------------------------------------------------
1 | import { Node } from './node';
2 |
3 | export class Params extends Node {}
4 |
--------------------------------------------------------------------------------
/src/core/ast/return.ts:
--------------------------------------------------------------------------------
1 | import { Node } from './node';
2 |
3 | export class Return extends Node {}
4 |
--------------------------------------------------------------------------------
/src/core/ast/atrule.ts:
--------------------------------------------------------------------------------
1 | import { Node } from './node';
2 |
3 | export class Atrule extends Node {
4 | }
5 |
--------------------------------------------------------------------------------
/src/core/ast/block.ts:
--------------------------------------------------------------------------------
1 | import { Node } from './node';
2 |
3 | export class Block extends Node {
4 | }
5 |
--------------------------------------------------------------------------------
/src/core/ast/bool.ts:
--------------------------------------------------------------------------------
1 | import { Node } from './node';
2 |
3 | export class Bool extends Node {
4 |
5 | }
6 |
--------------------------------------------------------------------------------
/src/core/ast/group.ts:
--------------------------------------------------------------------------------
1 | import { Node } from './node';
2 |
3 | export class Group extends Node {
4 | }
5 |
--------------------------------------------------------------------------------
/src/core/ast/keyframes.ts:
--------------------------------------------------------------------------------
1 | import { Node } from './node';
2 |
3 | export class Keyframes extends Node {}
4 |
--------------------------------------------------------------------------------
/src/core/ast/querylist.ts:
--------------------------------------------------------------------------------
1 | import { Node } from './node';
2 |
3 | export class Querylist extends Node {}
4 |
--------------------------------------------------------------------------------
/tests/staff/disable-sort-order-rule.json:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "sortOrder": false
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/tests/staff/subfolder/extends.json:
--------------------------------------------------------------------------------
1 | {
2 | "preprocessors": ["./preprocessors/preprocessor.js"]
3 | }
4 |
--------------------------------------------------------------------------------
/src/core/ast/supports.ts:
--------------------------------------------------------------------------------
1 | import { Node } from './node';
2 |
3 | export class Supports extends Node {
4 | }
5 |
--------------------------------------------------------------------------------
/src/core/types/IStats.ts:
--------------------------------------------------------------------------------
1 | export interface IStats {
2 | isFile(): boolean;
3 | isDirectory(): boolean;
4 | }
5 |
--------------------------------------------------------------------------------
/src/core/types/reader.ts:
--------------------------------------------------------------------------------
1 | export interface IFile {
2 | path: string;
3 | }
4 |
5 | export type Files = File[];
6 |
--------------------------------------------------------------------------------
/src/core/ast/unit.ts:
--------------------------------------------------------------------------------
1 | import { Node } from './node';
2 |
3 | export class Unit extends Node {
4 | value: string = '';
5 | }
6 |
--------------------------------------------------------------------------------
/tests/staff/subfolder/preprocessors/preprocessor.js:
--------------------------------------------------------------------------------
1 | module.exports = function (str) {
2 | return str + 'i work';
3 | };
4 |
--------------------------------------------------------------------------------
/src/core/ast/import.ts:
--------------------------------------------------------------------------------
1 | import { Node } from './node';
2 |
3 | export class Import extends Node {
4 | value: string = '';
5 | }
6 |
--------------------------------------------------------------------------------
/src/core/ast/comment.ts:
--------------------------------------------------------------------------------
1 | import { Node } from './node';
2 |
3 | export class Comment extends Node {
4 | value: string = '';
5 | }
6 |
--------------------------------------------------------------------------------
/src/core/ast/condition.ts:
--------------------------------------------------------------------------------
1 | import { Node } from './node';
2 |
3 | export class Condition extends Node {
4 | cond: Node | null = null;
5 | }
6 |
--------------------------------------------------------------------------------
/tests/staff/preprocessor.js:
--------------------------------------------------------------------------------
1 | module.exports = function (str) {
2 | return str + '\n' +
3 | '.color\n' +
4 | '\tcolor: #fFfFfF;\n'
5 | };
6 |
--------------------------------------------------------------------------------
/src/core/ast/query.ts:
--------------------------------------------------------------------------------
1 | import { Node } from './node';
2 |
3 | export class Query extends Node {
4 | predicate: string = '';
5 | type: Node | null = null;
6 | }
7 |
--------------------------------------------------------------------------------
/src/core/types/response.ts:
--------------------------------------------------------------------------------
1 | import { IMessagePack } from './message';
2 |
3 | export interface IResponse {
4 | passed: boolean;
5 | errors?: IMessagePack[];
6 | }
7 |
--------------------------------------------------------------------------------
/src/core/ast/unaryop.ts:
--------------------------------------------------------------------------------
1 | import { Node } from './node';
2 |
3 | export class UnaryOp extends Node {
4 | left: Node | null = null;
5 | right: Node | null = null;
6 | }
7 |
--------------------------------------------------------------------------------
/src/core/ast/media.ts:
--------------------------------------------------------------------------------
1 | import { Node } from './node';
2 | import { INode } from '../types/ast/node';
3 |
4 | export class Media extends Node {
5 | query: INode | null = null;
6 | }
7 |
--------------------------------------------------------------------------------
/src/core/ast/ident.ts:
--------------------------------------------------------------------------------
1 | import { Node } from './node';
2 | import { Value } from './value';
3 |
4 | export class Ident extends Node {
5 | key: string = '';
6 | value: Value | null = null;
7 | }
8 |
--------------------------------------------------------------------------------
/src/core/ast/literal.ts:
--------------------------------------------------------------------------------
1 | import { Node } from './node';
2 |
3 | export class Literal extends Node {
4 | val: string = '';
5 |
6 | toString(): string {
7 | return this.val;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/core/helpers/lcfirst.ts:
--------------------------------------------------------------------------------
1 | export const lcfirst = (str: string) =>
2 | str[0].toLowerCase() + str.substr(1);
3 |
4 | export const ucfirst = (str: string) =>
5 | str[0].toUpperCase() + str.substr(1);
6 |
--------------------------------------------------------------------------------
/src/core/types/context.ts:
--------------------------------------------------------------------------------
1 | export interface IContext {
2 | hashDeep: number
3 | inHash: boolean
4 | inComment: boolean
5 | openBracket: boolean
6 | vars: Dictionary
7 | valueToVar: Dictionary
8 | }
9 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "none",
3 | "tabWidth": 4,
4 | "useTabs": true,
5 | "semi": true,
6 | "singleQuote": true,
7 | "endOfLine": "lf",
8 | "arrow-parens": "always"
9 | }
10 |
--------------------------------------------------------------------------------
/src/doc.ts:
--------------------------------------------------------------------------------
1 | export const doc = (options = {}) => {
2 | const {Documentator} = require('./core/documentator/documentator');
3 | const documentator = new Documentator(options);
4 | documentator.generate();
5 | };
6 |
--------------------------------------------------------------------------------
/src/core/ast/func.ts:
--------------------------------------------------------------------------------
1 | import { Node } from './node';
2 | import { INode } from '../types/ast/node';
3 |
4 | export class Func extends Node {
5 | key: string = '';
6 | value: string = '';
7 | params: INode[] = [];
8 | }
9 |
--------------------------------------------------------------------------------
/test.styl:
--------------------------------------------------------------------------------
1 | .test
2 | max-height $headerHeight
3 | // @stlint-ignore
4 | border 10px
5 | color red
6 | margin-bottom 10px
7 |
8 | getBgFor($name)
9 | return getField("exterior." + $name + ".backgroundColor", $p)
10 |
--------------------------------------------------------------------------------
/src/core/helpers/splitAndStrip.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Split str by reg exp
3 | * @param re
4 | * @param line
5 | */
6 | export const splitAndStrip = (re: RegExp, line: string): string[] =>
7 | line.split(re).filter((str) => str.length > 0);
8 |
--------------------------------------------------------------------------------
/src/core/reporters/silentReporter.ts:
--------------------------------------------------------------------------------
1 | import { Reporter } from '../reporter';
2 |
3 | export class SilentReporter extends Reporter {
4 | log(): void {
5 | // ignore
6 | }
7 | reset(): void {
8 | // ignore
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/core/ast/call.ts:
--------------------------------------------------------------------------------
1 | import { Node } from './node';
2 |
3 | export class Call extends Node {
4 | key: string = '';
5 | toString(): string {
6 | return `${this.key}(${this.nodes.map((arg) => arg.toString(), this).join(', ')})`;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/core/ast/property.ts:
--------------------------------------------------------------------------------
1 | import { Node } from './node';
2 | import { Value } from './value';
3 | import { Ident } from './ident';
4 |
5 | export class Property extends Node {
6 | key: Ident | string = '';
7 | value: Value | null = null;
8 | }
9 |
--------------------------------------------------------------------------------
/src/core/types/line.ts:
--------------------------------------------------------------------------------
1 | export interface ILine {
2 | line: string;
3 | lineno: number;
4 | lines: ILine[];
5 | next(): null | ILine;
6 | prev(): null | ILine;
7 | isEmpty(): boolean;
8 | isLast(): boolean;
9 | isIgnored: boolean;
10 | }
11 |
--------------------------------------------------------------------------------
/src/core/ast/ternary.ts:
--------------------------------------------------------------------------------
1 | import { Node } from './node';
2 | import { Ident } from './ident';
3 | import { Value } from './value';
4 |
5 | export class Ternary extends Node {
6 | cond!: Ident;
7 | trueExpr!: Value;
8 | falseExpr!: Value;
9 | }
10 |
--------------------------------------------------------------------------------
/src/core/types/autocomplete.ts:
--------------------------------------------------------------------------------
1 | export interface ISuggestionItem {
2 | title: string;
3 | }
4 |
5 | export type Suggestions = ISuggestionItem[];
6 |
7 | export type AutocompleteFunction = (str: string, offset: number, lineOffset: number) => Suggestions;
8 |
--------------------------------------------------------------------------------
/tests/staff/extra/testRule.js:
--------------------------------------------------------------------------------
1 | function TestRule() {
2 | this.checkLine = (line) => {
3 | if (line.lineno === 1) {
4 | this.msg('Test error on test line', 1, 1, line.length);
5 | }
6 | };
7 | }
8 |
9 | module.exports.TestRule = TestRule;
10 |
--------------------------------------------------------------------------------
/src/core/ast/feature.ts:
--------------------------------------------------------------------------------
1 | import { Node } from './node';
2 | import { INode } from '../types/ast/node';
3 |
4 | export class Feature extends Node {
5 | segments: INode[] = [];
6 | toString(): string {
7 | return this.segments.join('');
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/core/ast/selector.ts:
--------------------------------------------------------------------------------
1 | import { Node } from './node';
2 | import { INode } from '../types/ast/node';
3 |
4 | export class Selector extends Node {
5 | segments: INode[] = [];
6 | toString(): string {
7 | return this.segments.join('');
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/tests/staff/extends.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "rules": {
3 | "color": {
4 | "conf": "test-extends",
5 | "enabled": false,
6 | "allowOnlyInVar": 5,
7 | "allowShortcut": 7
8 | }
9 | },
10 | "grep": "sortOrder",
11 | "reporter": "silent"
12 | };
13 |
--------------------------------------------------------------------------------
/tests/staff/test.styl:
--------------------------------------------------------------------------------
1 | $p = {
2 | color: #CCC
3 | padding: 10px
4 | background: url('./test.png')
5 | }
6 |
7 | .red
8 | margin-left 10px
9 | color #ccc
10 |
11 | @supports display flex
12 | div
13 | display flex
14 |
15 | dark
16 | color red
17 |
18 | test extends dark
19 |
--------------------------------------------------------------------------------
/src/core/ast/tree.ts:
--------------------------------------------------------------------------------
1 | import { Node } from './node';
2 | import { ISNode } from '../types/ast/snode';
3 | import { INode } from '../types/ast/node';
4 |
5 | export class Tree extends Node {
6 | readonly parent: INode | null = null;
7 | constructor(block: ISNode) {
8 | super(block, null);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/tests/staff/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tests/staff/extends.json",
3 | "rules": {
4 | "color": {
5 | "conf": "test-config",
6 | "enabled": false,
7 | "allowOnlyInVar": 3
8 | },
9 | "someTestRule": false
10 | },
11 | "grep": "color",
12 | "reporter": "raw"
13 | }
14 |
--------------------------------------------------------------------------------
/src/core/types/state.ts:
--------------------------------------------------------------------------------
1 | export type modes = 'always' | 'never' | 'lowercase' | 'uppercase' | 'double' | 'single' | 'grouped' | 'alphabetical';
2 |
3 | export interface IState {
4 | conf: modes;
5 | enabled?: boolean;
6 | [key: string]: any;
7 | }
8 |
9 | export type State = IState | [ modes, boolean] | [ modes ] | boolean;
10 |
--------------------------------------------------------------------------------
/src/autocomplete/index.ts:
--------------------------------------------------------------------------------
1 | import { Suggestions } from '../core/types/autocomplete';
2 |
3 | // tslint:disable-next-line:completed-docs
4 | export function defaultAutocomplete(): Suggestions {
5 | return [
6 | {
7 | title: 'StLint is fantastic!'
8 | },
9 | {
10 | title: 'Stylus is perfect!'
11 | }
12 | ];
13 | }
14 |
--------------------------------------------------------------------------------
/src/core/types/message.ts:
--------------------------------------------------------------------------------
1 | export interface IFix {
2 | replace: string;
3 | }
4 |
5 | export interface IMessage {
6 | rule: string;
7 | descr: string;
8 | path: string;
9 | line: number;
10 | endline: number;
11 | start: number;
12 | end: number;
13 | fix: IFix | null;
14 | }
15 |
16 | export interface IMessagePack {
17 | message: IMessage[]
18 | }
19 |
--------------------------------------------------------------------------------
/tests/staff/extends.json:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "color": {
4 | "conf": "test-extends",
5 | "enabled": false,
6 | "allowOnlyInVar": 5,
7 | "allowShortcut": 7
8 | },
9 | "someTestRule": {
10 | "conf": "always",
11 | "enabled": false
12 | }
13 | },
14 | "grep": "sortOrder",
15 | "reporter": "silent"
16 | }
17 |
--------------------------------------------------------------------------------
/src/core/ast/member.ts:
--------------------------------------------------------------------------------
1 | import { Node } from './node';
2 | import { INode } from '../types/ast/node';
3 |
4 | export class Member extends Node {
5 | left: INode | null = null;
6 | right: INode | null = null;
7 |
8 | toString(): string | string {
9 | return (this.left && this.right) ? `${this.left.toString()}.${this.right.toString()}` : super.toString();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/core/helpers/index.ts:
--------------------------------------------------------------------------------
1 | export * from './calcPosition';
2 | export * from './checkPrefix';
3 | export * from './isPlainObject';
4 | export * from './lcfirst';
5 |
6 | export * from './mergeArray';
7 | export * from './objToHash';
8 | export * from './shortcutColor';
9 | export * from './unwrapObject';
10 | export * from './splitLines';
11 | export * from './splitAndStrip';
12 |
--------------------------------------------------------------------------------
/src/core/helpers/isPlainObject.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Check if element is simple plaint object
3 | *
4 | * @param obj
5 | */
6 | export const isPlainObject = (obj: unknown): obj is object => {
7 | if (typeof obj !== 'object') {
8 | return false;
9 | }
10 |
11 | return !(
12 | obj &&
13 | obj.constructor &&
14 | !{}.hasOwnProperty.call(obj.constructor.prototype, 'isPrototypeOf')
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/src/core/helpers/mergeArray.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Merge two array
3 | * @param a
4 | * @param b
5 | */
6 | export function mergeArray(a: Array, b: Array): Array {
7 | let result = a.map((value, index) => b[index] !== undefined ? b[index] : a[index]);
8 |
9 | if (b.length > a.length) {
10 | result = result.concat(b.slice(a.length));
11 | }
12 |
13 | return result;
14 | }
15 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = tab
5 | end_of_line = lf
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
10 | [{LICENSE,.gitattributes,.gitignore,.npmignore,.eslintignore}]
11 | indent_style = space
12 |
13 | [*.{json,yml,md,yaspellerrc,bowerrc,babelrc,snakeskinrc,eslintrc,tsconfig,pzlrrc}]
14 | indent_style = space
15 | indent_size = 2
16 |
--------------------------------------------------------------------------------
/src/core/helpers/splitLines.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Split line on lines
3 | * @param content
4 | */
5 | import { Line } from '../line';
6 |
7 | const SPLIT_REG = /\n/;
8 |
9 | export const splitLines = (content: string): Line[] => {
10 | const
11 | lines: Line[] = [];
12 |
13 | content.split(SPLIT_REG)
14 | .forEach((ln, index) => {
15 | lines[index + 1] = new Line(ln, index + 1, lines);
16 | });
17 |
18 | return lines;
19 | };
20 |
--------------------------------------------------------------------------------
/src/rules/emptyLines.ts:
--------------------------------------------------------------------------------
1 | import { Rule } from '../core/rule';
2 | import { Line } from '../core/line';
3 |
4 | /**
5 | * Check if document has several empty lines
6 | */
7 | export class EmptyLines extends Rule {
8 | checkLine(line: Line): void {
9 | if (line.isEmpty()) {
10 | const prev = line.prev();
11 |
12 | if (prev && prev.isEmpty()) {
13 | this.msg('Deny several empty lines', line.lineno);
14 | }
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/core/ast/binop.ts:
--------------------------------------------------------------------------------
1 | import { Node } from './node';
2 |
3 | export class BinOp extends Node {
4 | left: Node | null = null;
5 | right: Node | null = null;
6 |
7 | toString(): string | string {
8 | let right = this.right ? this.right.toString() : '';
9 |
10 | if (right) {
11 | right = /^[0-9]$/.test(right) ? `[${right}]` : `.${right}`;
12 | }
13 |
14 | return (this.left && right) ? this.left.toString() + right : super.toString();
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/rules/index.ts:
--------------------------------------------------------------------------------
1 | export * from './color';
2 | export * from './colons';
3 | export * from './leadingZero';
4 | export * from './useMixinInsteadUnit';
5 | export * from './semicolons';
6 | export * from './quotePref';
7 | export * from './sortOrder';
8 | export * from './prefixVarsWithDollar';
9 | export * from './mixedSpaces';
10 | export * from './commaInObject';
11 | export * from './depthControl';
12 | export * from './emptyLines';
13 | export * from './brackets';
14 |
--------------------------------------------------------------------------------
/src/core/ast/value.ts:
--------------------------------------------------------------------------------
1 | import { Node } from './node';
2 | import { INode } from '../types/ast/node';
3 |
4 | export class Value extends Node implements INode {
5 | get key(): string {
6 | if (this.nodes.length && (this.nodes[0]).key) {
7 | return String((this.nodes[0]).key);
8 | }
9 |
10 | return '';
11 | }
12 |
13 | set key(value: string) {
14 | // do nothing
15 | }
16 |
17 | toString(): string {
18 | return this.nodes.join(' ');
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 |
5 | "target": "es2016",
6 | "moduleResolution": "classic",
7 | "module": "none",
8 | "types": [
9 | "@types/node",
10 | "@types/chai",
11 | "@types/mocha",
12 | "@types/node",
13 | "@types/async",
14 | "@types/glob",
15 | "./src/typings"
16 | ]
17 |
18 | },
19 | "exclude": [
20 | "./node_modules"
21 | ],
22 | "sourceMap": true
23 | }
24 |
--------------------------------------------------------------------------------
/src/core/reporters/jsonReporter.ts:
--------------------------------------------------------------------------------
1 | import { Reporter } from '../reporter';
2 |
3 | export class JsonReporter extends Reporter {
4 | /**
5 | * @override
6 | */
7 | log(): void {
8 | if (this.response.errors) {
9 | this.response.errors.forEach((error) => error.message.forEach((message) => {
10 | message.descr = `${message.rule}: ${message.descr}`;
11 | }));
12 | }
13 |
14 | console.clear();
15 | console.log(JSON.stringify(this.response, null, 2));
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/core/helpers/checkPrefix.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Used in conjunction with the valid check (for valid css)
3 | *
4 | * @param [prop] the property to prepend prefixes to
5 | * @param [css] the css key we're checking against (from valid.json)
6 | * @param [valid] the valid.json object
7 | * @returns {boolean} true if at least one match found, false if not
8 | */
9 | export const checkPrefix = (prop: string, css: string, valid: ValidCSS): boolean =>
10 | valid.prefixes.some((prefix) => prop === prefix + css);
11 |
--------------------------------------------------------------------------------
/src/core/helpers/shortcutColor.ts:
--------------------------------------------------------------------------------
1 | const
2 | regOneElementColor = /([a-f0-9])\1{5}/i,
3 | regThreeElementColor = /([a-f0-9])\1([a-f0-9])\2([a-f0-9])\3/i;
4 |
5 | /**
6 | * Return shortcut for color if it possible
7 | * @param color
8 | */
9 | export function shortcutColor(color: string): string {
10 | if (regOneElementColor.test(color)) {
11 | return color.replace(regOneElementColor, '$1$1$1');
12 | }
13 |
14 | if (regThreeElementColor.test(color)) {
15 | return color.replace(regThreeElementColor, '$1$2$3');
16 | }
17 |
18 | return color;
19 | }
20 |
--------------------------------------------------------------------------------
/src/core/types/content.ts:
--------------------------------------------------------------------------------
1 | import { ILine } from './line';
2 | import { IMessage } from './message';
3 |
4 | export type eachLineCallback = (line: ILine, lineno: number) => void | false;
5 |
6 | export interface IContent {
7 | toString(): string;
8 |
9 | firstLine(): ILine;
10 |
11 | /**
12 | * Apply callback on every lines. if callback returns false - break cycle
13 | * @param callback
14 | */
15 | forEach(callback: eachLineCallback): void;
16 | getLine(lineno: number): ILine | null;
17 |
18 | applyFixes(messages: IMessage[]): IContent;
19 | }
20 |
--------------------------------------------------------------------------------
/src/rules/mixedSpaces.ts:
--------------------------------------------------------------------------------
1 | import { Rule } from '../core/rule';
2 | import { ILine } from '../core/types/line';
3 |
4 | /**
5 | * check for mixed spaces and tabs
6 | */
7 | export class MixedSpaces extends Rule {
8 | checkLine(line: ILine): boolean {
9 | const
10 | mixed = /( \t|\t )[\t\s]*/.exec(line.line),
11 | isMixed = mixed !== null;
12 |
13 | if (isMixed && mixed && !this.context.inComment) {
14 | this.msg('mixed spaces and tabs', line.lineno, mixed.index, mixed.index + mixed[0].length);
15 | }
16 |
17 | return isMixed;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/typings.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'stylus/lib/parser';
2 | declare module 'strip-json-comments';
3 | declare module 'columnify';
4 | declare module 'chalk';
5 | declare module 'node-watch';
6 | declare module 'native-require';
7 | declare module 'escaper';
8 |
9 | declare module '*.json' {
10 | const value: any;
11 | export default value;
12 | }
13 |
14 | // @ts-ignore
15 | interface Dictionary {[key: string]: T}
16 |
17 | interface ValidCSS {
18 | css: string[];
19 | html: string[];
20 | prefixes: string[];
21 | pseudo: string[];
22 | scope: string[];
23 | }
24 |
--------------------------------------------------------------------------------
/src/core/runner.ts:
--------------------------------------------------------------------------------
1 | import { Visitor } from './visitor';
2 | import { Node } from './ast/index';
3 | import { INode } from './types/ast/node';
4 |
5 | export class Runner extends Visitor {
6 | constructor(ast: INode, readonly fn: (node: INode) => void) {
7 | super(ast);
8 | }
9 |
10 | visitNode(node: INode, parent: INode): INode {
11 | this.fn(node);
12 |
13 | node.nodes.forEach((elm) => this.visit(elm, parent));
14 |
15 | if (node.value && node.value instanceof Node) {
16 | this.visit(node.value, parent);
17 | }
18 |
19 | return node;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tests/rules/emptyLinesTest.ts:
--------------------------------------------------------------------------------
1 | import { EmptyLines } from '../../src/rules/index';
2 | import { parseAndRun } from '../staff/bootstrap';
3 | import { expect } from 'chai';
4 |
5 | describe('Empty lines Test', () => {
6 | it('Should show error on several empty lines', () => {
7 | const rule = new EmptyLines({
8 | conf: 'always'
9 | });
10 |
11 | parseAndRun(
12 | '.test\n' +
13 | '\tborder 1px solid #ccc\n' +
14 | '\n' +
15 | '\n' +
16 | '\tcolor red\n' +
17 | ''
18 | ,
19 | rule
20 | );
21 |
22 | expect(rule.errors.length).to.be.equal(1)
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/tests/rules/bracketsTest.ts:
--------------------------------------------------------------------------------
1 | import { parseAndRun } from '../staff/bootstrap';
2 | import { expect } from 'chai';
3 | import { Brackets } from '../../src/rules/index';
4 |
5 | describe('Brackets Test', () => {
6 | it('Should show error on using brackets', () => {
7 | const rule = new Brackets({
8 | conf: 'never'
9 | });
10 |
11 | parseAndRun(
12 | '$p = {\n' +
13 | '\tcolor: red\n' +
14 | '}\n' +
15 | '.test {\n' +
16 | '\tborder 1px solid #ccc\n' +
17 | '\tcolor red\n' +
18 | '}'
19 | ,
20 | rule
21 | );
22 |
23 | expect(rule.errors.length).to.equal(2);
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/core/types/ast/snode.ts:
--------------------------------------------------------------------------------
1 | export interface ISNode {
2 | lineno: number;
3 | column: number;
4 |
5 | block?: ISNode | null;
6 | nodeName: string;
7 | path?: string;
8 | name?: string;
9 | string?: string;
10 | expr?: ISNode;
11 | val?: ISNode | string;
12 | left?: ISNode;
13 | right?: ISNode;
14 | type?: ISNode;
15 | cond?: ISNode;
16 | trueExpr?: ISNode;
17 | falseExpr?: ISNode;
18 | predicate?: string;
19 |
20 | nodes: ISNode[];
21 | params?: ISNode;
22 | args?: ISNode;
23 | vals?: Dictionary;
24 | keys?: Dictionary;
25 |
26 | [key: string]: unknown;
27 | toString(): string;
28 | }
29 |
--------------------------------------------------------------------------------
/src/core/helpers/calcPosition.ts:
--------------------------------------------------------------------------------
1 | import { splitLines } from './splitLines';
2 |
3 | /**
4 | * Calc position in text by line and column
5 | *
6 | * @param line
7 | * @param column
8 | * @param content
9 | */
10 |
11 | export const calcPosition = (line: number, column: number, content: string): number => {
12 | if (line === 1) {
13 | return column - 1;
14 | }
15 |
16 | let
17 | position = 0;
18 |
19 | splitLines(content)
20 | .forEach((ln, lineno) => {
21 | if (lineno >= line) {
22 | return false;
23 | }
24 |
25 | position += ln.line.length + 1;
26 | });
27 |
28 | return position + column - 1;
29 | };
30 |
--------------------------------------------------------------------------------
/src/core/types/reporter.ts:
--------------------------------------------------------------------------------
1 | import { IResponse } from './response';
2 | import { IMessagePack } from './message';
3 |
4 | export type ReporterType = 'json' | 'silent' | 'raw';
5 |
6 | export interface IReporter {
7 | errors: IMessagePack[];
8 |
9 | response: IResponse;
10 | reset(): void;
11 |
12 | add(
13 | rule: string,
14 | message: string,
15 | line: number,
16 | start: number,
17 | end?: number,
18 | fix?: string | null,
19 | endLine?: number
20 | ): void;
21 |
22 | display(exit: boolean): void;
23 | log(): void;
24 | setPath(path: string): void;
25 | fillResponse(): void;
26 |
27 | filterErrors(grep: string): void;
28 | }
29 |
--------------------------------------------------------------------------------
/tests/grepTest.ts:
--------------------------------------------------------------------------------
1 | import { Linter } from '../src/linter';
2 | import { expect } from 'chai';
3 |
4 | const
5 | wrongContent = '.tab\n\tcolor: #ccc;';
6 |
7 | describe('Test grep option', () => {
8 | describe('Set grep option', () => {
9 | it('should show only errors contains grep option', () => {
10 | const linter = new Linter({
11 | grep: 'color',
12 | reporter: 'silent'
13 | });
14 |
15 | linter.lint('./test.styl', wrongContent);
16 | linter.display(false);
17 |
18 | const response = linter.reporter.response;
19 |
20 | expect(response.passed).to.be.false;
21 | expect(response.errors && response.errors.length).to.be.equal(2);
22 | });
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/core/helpers/unwrapObject.ts:
--------------------------------------------------------------------------------
1 | import { isPlainObject } from './isPlainObject';
2 |
3 | export const unwrapObject = (obj: Dictionary, prefix: string[] = []) => {
4 | let result: Dictionary = {};
5 |
6 | Object.keys(obj).forEach((_key) => {
7 | const
8 | key = prefix.concat([_key]).join('.'),
9 | item = obj[_key];
10 |
11 | if (Array.isArray(item)) {
12 | item.forEach((value: string, index: number) => {
13 | result[value] = `${key}[${index}]`;
14 | });
15 |
16 | } else if (isPlainObject(item)) {
17 | result = {...result, ...unwrapObject(item, prefix.concat([_key]))};
18 |
19 | } else {
20 | result[item] = key;
21 | }
22 | });
23 |
24 | return result;
25 | };
26 |
--------------------------------------------------------------------------------
/tests/helpers/shortcutColorTest.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import { shortcutColor } from '../../src/core/helpers/index';
3 |
4 | describe('colorCanBeShortcut helper test', () => {
5 | it('Should check color can have shortcut form', () => {
6 | const colors: Array<[string, string]> = [
7 | ['#ffffff', '#fff'],
8 | ['#FFFFFF', '#FFF'],
9 | ['#fffffd', '#fffffd'],
10 | ['#ffccff', '#fcf'],
11 | ['#113322', '#132'],
12 | ['#113321', '#113321'],
13 | ['#abccba', '#abccba'],
14 | ['#cccddd', '#cccddd'],
15 | ['#000000', '#000'],
16 | ['#ddd', '#ddd']
17 | ];
18 |
19 | colors.forEach((pars) => {
20 | expect(shortcutColor(pars[0])).to.be.equal(pars[1]);
21 | });
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/core/helpers/objToHash.ts:
--------------------------------------------------------------------------------
1 | import { Ident, Obj, Property } from '../ast/index';
2 | import { INode } from '../types/ast/node';
3 |
4 | export const objTohash = (node: INode): Dictionary => {
5 | const result: Dictionary = {};
6 |
7 | node.nodes.forEach((prop) => {
8 | if (prop instanceof Property && prop.key instanceof Ident && prop.value) {
9 | const subkey = prop.key.key;
10 |
11 | if (prop.value.nodes && prop.value.nodes[0] && prop.value.nodes[0] instanceof Obj) {
12 | result[subkey] = objTohash(prop.value.nodes[0]);
13 |
14 | } else {
15 | if (prop.value.nodes.length > 1) {
16 | result[subkey] = prop.value.nodes.map((node) => node.toString());
17 | } else {
18 | result[subkey] = prop.value.toString();
19 | }
20 | }
21 | }
22 | });
23 |
24 | return result;
25 | };
26 |
--------------------------------------------------------------------------------
/src/core/visitor.ts:
--------------------------------------------------------------------------------
1 | import { ISNode } from './types/ast/snode';
2 | import { INode } from './types/ast/node';
3 |
4 | export abstract class Visitor {
5 | root: In;
6 |
7 | protected constructor(root: In) {
8 | this.root = root;
9 | }
10 |
11 | abstract visitNode(node: In, parent: Out | null): Out;
12 |
13 | methodNotExists(method: string, node: In): void {
14 | // ignore
15 | }
16 |
17 | visit(node: In, parent: Out | null): Out {
18 | const method = 'visit' + (node).constructor.name;
19 |
20 | const fn: undefined | ((node: In, parent: Out | null) => Out) = (this)[method];
21 |
22 | if (fn && typeof fn === 'function') {
23 | return fn.call(this, node, parent);
24 | }
25 |
26 | this.methodNotExists(method, node);
27 |
28 | return this.visitNode(node, parent);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const nodeExternals = require('webpack-node-externals');
3 |
4 | module.exports = {
5 | target: 'node',
6 | entry: './index.ts',
7 | context: path.resolve(__dirname),
8 | externals: [nodeExternals()],
9 | devtool: false,
10 | resolve: {
11 | extensions: [ '.ts', '.js' ]
12 | },
13 | module: {
14 | rules: [
15 | {
16 | test: /\.(ts)$/,
17 | use: {
18 | loader: 'awesome-typescript-loader',
19 | },
20 | exclude: path.resolve(__dirname, "node_modules/stylus")
21 | }
22 | ]
23 | },
24 |
25 | node: {
26 | __dirname: false,
27 | fs: 'mock'
28 | },
29 |
30 | output: {
31 | filename: 'index.js',
32 | libraryTarget: "umd",
33 | path: path.resolve(__dirname, './')
34 | },
35 |
36 | mode: 'development',
37 | optimization: {
38 | minimize: false
39 | }
40 | };
41 |
--------------------------------------------------------------------------------
/src/preprocessors/safeComments.ts:
--------------------------------------------------------------------------------
1 | import Escaper = require('escaper');
2 |
3 | /**
4 | * Replace all comments to safe (without error) value
5 | * @param content
6 | */
7 | export function safeComments(content: string): string {
8 | const
9 | replacedText: string[] = [],
10 | str = Escaper.replace(content, {
11 | strings: [
12 | 'comments'
13 | ]
14 | }, replacedText);
15 |
16 | if (replacedText.length && str !== content && typeof str === 'string') {
17 | return Escaper.paste(str, replacedText.map((comment) => {
18 | if (comment.indexOf('/*') === 0) {
19 | return comment.replace(/(\/\*)(.*)(\*\/)/s, (res, ...match) =>
20 | match[0] +
21 | match[1].split(/\n/).fill('empty').join('\n') +
22 | match[2]
23 | );
24 | }
25 |
26 | return /@stlint/.test(comment) ? comment : '// empty';
27 | }));
28 | }
29 |
30 | return content;
31 | }
32 |
--------------------------------------------------------------------------------
/tests/rules/semicolonTest.ts:
--------------------------------------------------------------------------------
1 | import { Semicolons } from '../../src/rules/index';
2 | import { expect } from 'chai';
3 | import { splitAndRun } from '../staff/bootstrap';
4 |
5 | describe('Semicolons test', () => {
6 | it('Should check the line has semicolons and they are needed', () => {
7 | const rule = new Semicolons({
8 | conf: 'always'
9 | });
10 |
11 | splitAndRun(
12 | '.test\n' +
13 | '\tmax-height red;\n' +
14 | '\tborder black',
15 | rule
16 | );
17 |
18 | expect(rule.errors.length).to.be.equal(1);
19 | });
20 | it('Should check the line has semicolons and they are not needed', () => {
21 | const rule = new Semicolons({
22 | conf: 'never'
23 | });
24 |
25 | splitAndRun(
26 | '.test\n' +
27 | '\tmax-height red\n' +
28 | '\tborder black',
29 | rule
30 | );
31 |
32 | expect(rule.errors.length).to.be.equal(0);
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/src/core/types/config.ts:
--------------------------------------------------------------------------------
1 | import { State } from './state';
2 | import { ReporterType } from './reporter';
3 |
4 | export interface IConfig {
5 | [key: string]: any;
6 | configName: string
7 | configFile: string
8 |
9 | debug: boolean
10 | reporter: ReporterType
11 |
12 | basepath: string
13 | path: string
14 |
15 | preprocessors: string[];
16 | autocompletes: string[];
17 |
18 | grep: string
19 | doc: string
20 | fix: boolean
21 |
22 | rules: Dictionary;
23 | defaultRules: Dictionary;
24 |
25 | excludes: string[]
26 |
27 | watch: boolean
28 |
29 | stylusParserOptions: Dictionary
30 | reportOptions: Dictionary
31 |
32 | extends: string | string[];
33 | extraRules: string | string[];
34 |
35 | customProperties: string[];
36 |
37 | extendsOption(from: Dictionary, to: Dictionary): void;
38 | applyConfig(path: string, config: Dictionary): void;
39 | }
40 |
--------------------------------------------------------------------------------
/src/core/types/rule.ts:
--------------------------------------------------------------------------------
1 | import { INode } from './ast/node';
2 | import { ILine } from './line';
3 | import { IState } from './state';
4 | import { IContent } from './content';
5 | import { IConfig } from './config';
6 |
7 | export type ErrorArray = [string, string, number, number, number, null | string, number];
8 |
9 | export interface IRule {
10 | state: T;
11 | config: IConfig;
12 | setConfig(config: IConfig): void;
13 |
14 | cache: Dictionary;
15 |
16 | nodesFilter: string[] | null;
17 |
18 | errors: ErrorArray[];
19 |
20 | context: Dictionary;
21 | clearContext(): void;
22 |
23 | checkNode?(node: INode, content: IContent): void;
24 | checkLine?(line: ILine, index: number, content: IContent): void;
25 |
26 | msg(message: string, line: number, start: number, end: number, fix: null | string, endLine: number): void;
27 |
28 | isMatchType(type: string): boolean;
29 | clearErrors(): void;
30 | }
31 |
--------------------------------------------------------------------------------
/src/core/ast/index.ts:
--------------------------------------------------------------------------------
1 | export * from './node';
2 | export * from './selector';
3 | export * from './tree';
4 | export * from './group';
5 | export * from './block';
6 | export * from './property';
7 | export * from './literal';
8 | export * from './value';
9 | export * from './rgb';
10 | export * from './ident';
11 | export * from './import';
12 | export * from './obj';
13 | export * from './unit';
14 | export * from './call';
15 | export * from './member';
16 | export * from './binop';
17 | export * from './func';
18 | export * from './comment';
19 | export * from './params';
20 | export * from './bool';
21 | export * from './each';
22 | export * from './condition';
23 | export * from './unaryop';
24 | export * from './media';
25 | export * from './querylist';
26 | export * from './query';
27 | export * from './feature';
28 | export * from './keyframes';
29 | export * from './atrule';
30 | export * from './ternary';
31 | export * from './supports';
32 | export * from './return';
33 |
--------------------------------------------------------------------------------
/src/core/types/ast/node.ts:
--------------------------------------------------------------------------------
1 | import { ISNode } from './snode';
2 | import { ILine } from '../line';
3 | import { IContent } from '../content';
4 |
5 | export interface INode {
6 | lineno: number;
7 | column: number;
8 |
9 | content: IContent | null;
10 |
11 | /**
12 | * Get line object
13 | */
14 | line: ILine | null;
15 |
16 | parent: INode | null;
17 | block?: INode | null;
18 |
19 | nodeName: string;
20 | key: string | INode;
21 |
22 | nodes: INode[];
23 | segments: INode[];
24 |
25 | source: ISNode | null;
26 |
27 | value: INode | string | null;
28 | append(node: INode, listField?: keyof T): void;
29 | toString(): string;
30 |
31 | getSibling(next?: boolean): null | INode;
32 | getChild(findClass?: string, last?: boolean): null | INode;
33 | previousSibling(): null | INode;
34 | nextSibling(): null | INode;
35 |
36 | closest(parentClass: string): null | INode;
37 | lastChild(findClass?: string): null | INode;
38 | firstChild(findClass?: string): null | INode;
39 | }
40 |
--------------------------------------------------------------------------------
/tests/rules/leadingZeroTest.ts:
--------------------------------------------------------------------------------
1 | import { LeadingZero } from '../../src/rules/index';
2 | import { expect } from 'chai';
3 | import { checkLine } from '../staff/bootstrap';
4 |
5 | describe('Leading Zero test', () => {
6 | it('Should check the line has wrong unit notation', () => {
7 | const rule = new LeadingZero({
8 | conf: 'always'
9 | });
10 |
11 | expect(checkLine('font-size: .1em', rule)).to.be.false;
12 |
13 | expect(checkLine('font-size:.1em', rule)).to.be.false;
14 |
15 | expect(checkLine('font-size : .1111px', rule)).to.be.false;
16 |
17 | expect(rule.errors.length).to.be.equal(3);
18 | });
19 | it('Should check the line has right unit notation', () => {
20 | const rule = new LeadingZero({
21 | conf: 'always'
22 | });
23 |
24 | expect(checkLine('font-size: 0.1em', rule)).to.be.true;
25 |
26 | expect(checkLine('font-size:0.1em', rule)).to.be.true;
27 |
28 | expect(checkLine('font-size : 0.1111px', rule)).to.be.true;
29 |
30 | expect(rule.errors.length).to.be.equal(0);
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/src/core/preprocessor.ts:
--------------------------------------------------------------------------------
1 | import { Content } from './content';
2 | import _require = require('native-require');
3 | import { safeComments } from '../preprocessors/safeComments';
4 |
5 | export class Preprocessor {
6 | private list: Array<(str: string) => string> = [];
7 |
8 | constructor(files: string[]) {
9 | if (files.length) {
10 | this.list = files.map((file) => {
11 | const func = _require(file);
12 |
13 | if (typeof func === 'function') {
14 | return func;
15 | }
16 |
17 | return null;
18 | })
19 | .filter((f) => f);
20 | }
21 |
22 | this.list.push(safeComments);
23 | }
24 |
25 | /**
26 | * Apply some preprocessors function to content
27 | * @param content
28 | */
29 | apply(content: Content): Content {
30 | if (!this.list.length) {
31 | return content;
32 | }
33 |
34 | const str = this.list.reduce((str, func) => func(str), content.content);
35 |
36 | if (typeof str === 'string' && str !== content.content) {
37 | return new Content(str);
38 | }
39 |
40 | return content;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/rules/commaInObject.ts:
--------------------------------------------------------------------------------
1 | import { Rule } from '../core/rule';
2 | import { ILine } from '../core/types/line';
3 |
4 | const
5 | reg = /(,)(\s)*$/,
6 | keyValue = /:/,
7 | hashEnd = /}/;
8 |
9 | /**
10 | * Allow or deny commas in object hash
11 | */
12 | export class CommaInObject extends Rule {
13 | checkLine(line: ILine): void | boolean {
14 | if (!this.context.inHash) {
15 | return;
16 | }
17 |
18 | let hasComma = false;
19 | const match = reg.exec(line.line);
20 |
21 | if (match) {
22 | hasComma = true;
23 | }
24 |
25 | if (hasComma && this.state.conf === 'never') {
26 | this.msg('Remove comma from object hash', line.lineno, match ? match.index + 1 : 0, match ? match.index + 1 : 0, '');
27 |
28 | } else if (!hasComma && this.state.conf === 'always') {
29 | const next = line.next();
30 |
31 | if (keyValue.test(line.line) && !hashEnd.test(line.line) && next && !hashEnd.test(next.line)) {
32 | this.msg('Add comma after object key: value', line.lineno, line.line.length - 2);
33 | }
34 | }
35 |
36 | return hasComma;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/core/documentator/readmePatcher.ts:
--------------------------------------------------------------------------------
1 | import { readFile, writeFileSync } from 'fs';
2 | import { RuleDocs } from './documentator';
3 |
4 | /**
5 | * Patch readme file
6 | * @param result
7 | */
8 | export function readmePatcher(result: RuleDocs[]): void {
9 | const readmeFile = process.cwd() + '/readme.md';
10 |
11 | readFile(readmeFile, 'utf-8', (err, readme: string) => {
12 | if (err) {
13 | throw err;
14 | }
15 |
16 | const
17 | text = result.map((item: RuleDocs) =>
18 | [
19 | '\n',
20 | `### ${item.name}`,
21 | `${item.description}\n`,
22 | '**Default value**',
23 | '```json',
24 | `${JSON.stringify(item.default, null, 2)}`,
25 | '```',
26 | '----'
27 | ].join('\n')
28 | ).join('');
29 |
30 | const readmeNew = readme.replace(
31 | /(.*)/msg,
32 | `${text}`
33 | );
34 |
35 | if (readmeNew !== readme) {
36 | writeFileSync(readmeFile, readmeNew);
37 | console.log('Readme file patched');
38 | } else {
39 | console.log('Readme file not patched');
40 | }
41 | });
42 | }
43 |
--------------------------------------------------------------------------------
/src/commander.ts:
--------------------------------------------------------------------------------
1 | import { ucfirst } from './core/helpers/lcfirst';
2 | import yargs = require('yargs');
3 | import { Autocomplete } from './core/autocomplete';
4 | import { Linter } from './linter';
5 |
6 | export class Commander {
7 | private linter: Linter;
8 |
9 | constructor(options: Dictionary = {}) {
10 | this.linter = new Linter(options);
11 | }
12 |
13 | exec(command: string): void {
14 | const ucommand = ucfirst(command);
15 |
16 | if ((this as any)[`command${ucommand}`]) {
17 | (this as any)[`command${ucommand}`]();
18 | }
19 |
20 | process.exit();
21 | }
22 |
23 | protected commandAutocomplete(): void {
24 | const { content, offset, offsetline } = yargs.options({
25 | content: { type: 'string' },
26 | offset: { type: 'number' },
27 | offsetline: { type: 'number' }
28 | }).argv;
29 |
30 | const config = this.linter.config;
31 |
32 | const autocomplete = new Autocomplete(config);
33 |
34 | console.clear();
35 | console.log(
36 | JSON.stringify({
37 | suggests: autocomplete.getItems(
38 | content || '',
39 | offset || 0,
40 | offsetline || 0
41 | )
42 | })
43 | );
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | (The MIT License)
2 |
3 | Copyright (c) Automattic
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | 'Software'), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/tests/rules/mixedSpacesTest.ts:
--------------------------------------------------------------------------------
1 | import { MixedSpaces } from '../../src/rules/index';
2 | import { expect } from 'chai';
3 | import { checkLine, splitAndRun } from '../staff/bootstrap';
4 |
5 | describe('Mixed spaces check test', () => {
6 | it('Should check the line has mixed tabs and spaces', () => {
7 | const rule = new MixedSpaces({
8 | conf: 'always'
9 | });
10 |
11 | expect(checkLine('\tcolor red', rule)).to.be.false;
12 |
13 | expect(checkLine('\t color red', rule)).to.be.true;
14 |
15 | expect(checkLine(' \tcolor red', rule)).to.be.true;
16 |
17 | expect(checkLine('\t\tfont-size .1111px', rule)).to.be.false;
18 |
19 | expect(checkLine(' font-size .1111px', rule)).to.be.false;
20 |
21 | expect(rule.errors.length).to.be.equal(2);
22 | });
23 | describe('In cssdoc', () => {
24 | it('Should not find the error', () => {
25 | const rule = new MixedSpaces({
26 | conf: 'always'
27 | });
28 |
29 | splitAndRun('/**\n' +
30 | '\t * Base rule\n' +
31 | '\t */\n' +
32 | '\t&__offer-info\n' +
33 | '\t\tmargin-top basis(2.375)',
34 | rule);
35 |
36 | expect(rule.errors.length).to.be.equal(0);
37 | });
38 | });
39 | });
40 |
--------------------------------------------------------------------------------
/src/core/autocomplete.ts:
--------------------------------------------------------------------------------
1 | import _require = require('native-require');
2 | import { defaultAutocomplete } from '../autocomplete/index';
3 | import { AutocompleteFunction, Suggestions } from './types/autocomplete';
4 | import { IConfig } from './types/config';
5 |
6 | export class Autocomplete {
7 | private list: AutocompleteFunction[] = [];
8 |
9 | constructor(readonly config: IConfig) {
10 | if (config.autocompletes.length) {
11 | this.list = config.autocompletes.map((file) => {
12 | const func = _require(file);
13 |
14 | if (typeof func === 'function') {
15 | return func;
16 | }
17 |
18 | return null;
19 | })
20 | .filter((f) => f);
21 | }
22 |
23 | this.list.push(defaultAutocomplete);
24 | }
25 |
26 | /**
27 | * Apply some preprocessors function to content
28 | *
29 | * @param search
30 | * @param offset
31 | * @param lineOffset
32 | */
33 | getItems(search: string, offset: number, lineOffset: number): Suggestions {
34 | if (!this.list.length) {
35 | return [];
36 | }
37 |
38 | return this.list.reduce(
39 | (res, func) =>
40 | res.concat(func.call(this, search, offset, lineOffset)), [] as Suggestions
41 | );
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/core/parser.ts:
--------------------------------------------------------------------------------
1 | import Parser = require('stylus/lib/parser');
2 | import { Tree } from './ast/index';
3 | import { Translator } from './translator';
4 | import { ISNode } from './types/ast/snode';
5 | import { Content } from './content';
6 |
7 | export class StylusParser {
8 | /**
9 | * @param options Stylus parser options
10 | */
11 | constructor(readonly options: Dictionary = {}) {
12 | }
13 |
14 | /**
15 | * Parse use native stylus parser into StylusAST and convert it in our AST
16 | *
17 | * @param {string} content
18 | * @returns {Tree}
19 | */
20 | parse(content: Content): Tree {
21 | const
22 | parser = new Parser(content.toString(), this.options);
23 |
24 | try {
25 | const
26 | stylusAST: ISNode = parser.parse({
27 | resolver: (path: string) => {
28 | console.log(path);
29 | }
30 | });
31 |
32 | const
33 | translator = new Translator(stylusAST, content);
34 |
35 | return translator.transpile();
36 | } catch (err) {
37 |
38 | err.lineno = parser.lexer.lineno || err.lineno || 1;
39 | err.column = parser.lexer.column || err.column || 1;
40 | err.message = `Syntax error: ${err.message} (${err.lineno},${err.column})`;
41 |
42 | throw err;
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/rules/leadingZero.ts:
--------------------------------------------------------------------------------
1 | import { Rule } from '../core/rule';
2 | import { ILine } from '../core/types/line';
3 |
4 | const decimalRe = /[^\d+](0+\.\d+)|[\s,(:](\.\d+)/i;
5 | const leadZeroRe = /([^\d+])(0+\.\d+)/;
6 | const nonZeroRe = /([\s,(:])(\.\d+)/;
7 |
8 | /**
9 | * Check for leading 0 on numbers ( 0.5 )
10 | */
11 | export class LeadingZero extends Rule {
12 | checkLine(line: ILine): void | boolean {
13 | if (!decimalRe.test(line.line)) {
14 | return;
15 | }
16 |
17 | const leadZeroFound = leadZeroRe.exec(line.line);
18 | const leadZeroMissing = nonZeroRe.exec(line.line);
19 |
20 | if (this.state.conf === 'always' && leadZeroMissing) {
21 | this.msg(
22 | 'leading zeros for decimal points are required',
23 | line.lineno,
24 | leadZeroMissing.index + leadZeroMissing[1].length + 1,
25 | leadZeroMissing.index + leadZeroMissing[1].length + 1,
26 | '0.'
27 | );
28 | } else if (this.state.conf === 'never' && leadZeroFound) {
29 | this.msg(
30 | 'leading zeros for decimal points are unnecessary',
31 | line.lineno,
32 | leadZeroFound.index + leadZeroFound[1].length + 1,
33 | leadZeroFound.index + leadZeroFound[1].length + 1,
34 | ''
35 | );
36 | }
37 |
38 | return Boolean(leadZeroFound);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/tests/helpers/calcPositionTest.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import { calcPosition } from '../../src/core/helpers/index';
3 |
4 | const multyLineText = '.item2-title\n' + // 1 13
5 | ' display flex\n' + // 2 27
6 | ' align-items center\n' + // 3 47
7 | ' box-sizing border-box\n' + // 4 70
8 | ' height $p.item2Height\n' + // 5 93
9 | ' padding-left $p.paddingLeft - $p.border[0]\n' + // 6 137
10 | '\n' + // 7 138
11 | ' font-size basis(1.875)\n' + // 8 162
12 | '\n' + // 9 163
13 | ' border $p.border\n' + // 10 181
14 | ' border-width 0 $p.border[0]\n' + // 11 210
15 | ' color $p.secondSlideTitleColor`;\n'; // 12 244
16 |
17 | describe('CalcPosition helper test', () => {
18 | it('Should calc position right', () => {
19 | const content = multyLineText;
20 | expect(calcPosition(1, 1, content)).to.equal(0);
21 | expect(calcPosition(1, 5, content)).to.equal(4);
22 | expect(calcPosition(2, 5, content)).to.equal(17);
23 | expect(calcPosition(10, 5, content)).to.equal(167);
24 | expect(calcPosition(-1, 5, content)).to.equal(4);
25 | expect(calcPosition(100, 5, content)).to.equal(249);
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/src/core/line.ts:
--------------------------------------------------------------------------------
1 | import { ILine } from './types/line';
2 |
3 | export class Line implements ILine {
4 | readonly line: string;
5 | readonly lineno: number = 1;
6 | isIgnored: boolean = false;
7 |
8 | readonly lines: Line[] = [];
9 |
10 | constructor(line: string, lineno: number = 1, lines: Line[] = []) {
11 | this.line = line;
12 | this.lineno = lineno;
13 | this.lines = lines;
14 | }
15 |
16 | /**
17 | * Get next line
18 | */
19 | next(): ILine | null {
20 | const index = this.lines.indexOf(this);
21 |
22 | if (index !== -1) {
23 | return this.lines[index + 1] || null;
24 | }
25 |
26 | return null;
27 | }
28 |
29 | /**
30 | * Get previous line
31 | */
32 | prev(): ILine | null {
33 | const index = this.lines.indexOf(this);
34 |
35 | if (index !== -1) {
36 | return this.lines[index - 1] || null;
37 | }
38 |
39 | return null;
40 | }
41 |
42 | /**
43 | * Check the line is empty
44 | */
45 | isEmpty(): boolean {
46 | return this.line.trim().length === 0;
47 | }
48 |
49 | /**
50 | * This is last line
51 | */
52 | isLast(): boolean {
53 | const
54 | index = this.lines.indexOf(this);
55 |
56 | let lastIndex = this.lines.length - 1;
57 |
58 | for (let i = lastIndex; i > 0; i -= 1) {
59 | if (this.lines[i].isEmpty()) {
60 | lastIndex = i - 1;
61 | }
62 | }
63 |
64 | return index === lastIndex;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/rules/prefixVarsWithDollar.ts:
--------------------------------------------------------------------------------
1 | import { Rule } from '../core/rule';
2 | import { Func, Ident, Tree } from '../core/ast/index';
3 | import { IState } from '../core/types/state';
4 |
5 | interface IPrefixVarState extends IState {
6 | prefix: string;
7 | allowConst?: boolean;
8 | }
9 |
10 | /**
11 | * Check that $ is used when declaring vars
12 | */
13 | export class PrefixVarsWithDollar extends Rule {
14 | nodesFilter: string[] = ['ident'];
15 |
16 | checkNode(node: Ident): boolean | void {
17 | if (!(node.parent instanceof Tree) || (node.value instanceof Func)) {
18 | return;
19 | }
20 |
21 | const
22 | hasDollar = node.key.indexOf(this.state.prefix) === 0;
23 |
24 | if (this.state.conf === 'always' && hasDollar === false) {
25 | if (this.state.allowConst && /^[A-Z0-9_]+$/.test(node.key)) {
26 | return;
27 | }
28 |
29 | this.msg(
30 | `Variables and parameters must be prefixed with the ${this.state.prefix} sign (${node.key})`,
31 | node.lineno,
32 | node.column,
33 | node.column + node.key.length - 1
34 | );
35 | } else if (this.state.conf === 'never' && hasDollar === true) {
36 | this.msg(
37 | `${this.state.prefix} sign is disallowed for variables and parameters (${node.key})`,
38 | node.lineno, node.column,
39 | node.column + node.key.length - 1
40 | );
41 | }
42 |
43 | return hasDollar;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/rules/semicolons.ts:
--------------------------------------------------------------------------------
1 | import { Rule } from '../core/rule';
2 | import { ILine } from '../core/types/line';
3 |
4 | // we only want to check semicolons on properties/values
5 | const ignoreRe = /(^[*#.])|[&>/]|{|}|if|for(?!\w)|else|@block|@media|([}{=,])$/igm;
6 |
7 | /**
8 | * Check that selector properties are sorted accordingly
9 | */
10 | export class Semicolons extends Rule {
11 | checkLine(line: ILine): void | boolean {
12 | if (ignoreRe.test(line.line.trim())) {
13 | return;
14 | }
15 |
16 | let
17 | semicolon;
18 | const
19 | index = line.line.indexOf(';');
20 |
21 | if (this.state.conf === 'never' && index !== -1) {
22 | semicolon = true;
23 | }
24 |
25 | // for reasons that perplex me, even when the first use
26 | // of this at the top returns true, sometimes the method
27 | // still runs, so we add this second ignoreCheck here to catch it
28 | if (this.state.conf === 'always' && !ignoreRe.test(line.line.trim())) {
29 | if (index === -1 &&
30 | line.line.indexOf('}') === -1 &&
31 | line.line.indexOf('{') === -1) {
32 | semicolon = false;
33 | }
34 | }
35 |
36 | if (this.state.conf === 'never' && semicolon === true) {
37 | this.msg('unnecessary semicolon found', line.lineno, index + 1, index + 1, '');
38 | } else if (this.state.conf === 'always' && semicolon === false) {
39 | this.msg('missing semicolon', line.lineno, line.line.length + 1, line.line.length + 1, ';');
40 | }
41 |
42 | return semicolon;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/tests/beforeCheckNodeTest.ts:
--------------------------------------------------------------------------------
1 | import { Linter } from '../src/linter';
2 | import { Rule } from '../src/core/rule';
3 | import { expect } from 'chai';
4 |
5 | describe('Before check node checker doing some work', () => {
6 | it('Should parse hash all variables', () => {
7 | const linter = new Linter();
8 |
9 | linter.lint('./test.styl', `$p = {
10 | colors: #ccc #ddd #eee rgba(0, 0, 0, 0.1)
11 | width: basis(10, 20px, #ccc) 1px solid,
12 | color: {
13 | fff: black red
14 | },
15 | background: $colors.white[0],
16 | offersShopsText: $colors.grey[2],
17 | discountBgColor: $colors.base.third
18 | }
19 |
20 | color = #ccc`
21 | );
22 |
23 | expect(Rule.getContext().vars).to.be.deep.equal({
24 | $p: {
25 | colors: ['#ccc', '#ddd', '#eee', 'rgba(0, 0, 0, 0.1)'],
26 | width: ['basis(10, 20px, #ccc)', '1px', 'solid'],
27 | color: {fff: ['black', 'red']},
28 | background: '$colors.white[0]',
29 | offersShopsText: '$colors.grey[2]',
30 | discountBgColor: '$colors.base.third'
31 | },
32 | color: '#ccc'
33 | });
34 |
35 | expect(Rule.getContext().valueToVar).to.be.deep.equal({
36 | '#ccc': 'color',
37 | '#ddd': '$p.colors[1]',
38 | '#eee': '$p.colors[2]',
39 | '$colors.base.third': '$p.discountBgColor',
40 | '$colors.grey[2]': '$p.offersShopsText',
41 | '$colors.white[0]': '$p.background',
42 | '1px': '$p.width[1]',
43 | 'basis(10, 20px, #ccc)': '$p.width[0]',
44 | 'black': '$p.color.fff[0]',
45 | 'red': '$p.color.fff[1]',
46 | 'rgba(0, 0, 0, 0.1)': '$p.colors[3]',
47 | 'solid': '$p.width[2]'
48 | });
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/tests/extraRulesTest.ts:
--------------------------------------------------------------------------------
1 | import { Linter } from '../src/linter';
2 | import { expect } from 'chai';
3 |
4 | describe('Test `extraRules` options', () => {
5 | describe('like folder', () => {
6 | it('Should load rules from path and check these', () => {
7 | const linter = new Linter({
8 | extraRules: './tests/staff/extra'
9 | });
10 |
11 | linter.lint('./test.styl', '.test\n\tcolor red');
12 |
13 | expect(linter.reporter.response.passed).to.be.false;
14 | const errors = linter.reporter.response.errors;
15 | expect(errors && errors[0].message[0].rule).to.be.equal('testRule');
16 | });
17 | });
18 |
19 | describe('like files', () => {
20 | it('Should load rules from path and check these', () => {
21 | const linter = new Linter({
22 | extraRules: './tests/staff/extra/testRule.js'
23 | });
24 |
25 | linter.lint('./test.styl', '.test\n\tcolor red');
26 |
27 | expect(linter.reporter.response.passed).to.be.false;
28 | const errors = linter.reporter.response.errors;
29 | expect(errors && errors[0].message[0].rule).to.be.equal('testRule');
30 | });
31 | });
32 |
33 | describe('like list of files', () => {
34 | it('Should load rules from all paths and check these', () => {
35 | const linter = new Linter({
36 | extraRules: [
37 | './tests/staff/extra/testRule.js'
38 | ]
39 | });
40 |
41 | linter.lint('./test.styl', '.test\n\tcolor red');
42 |
43 | expect(linter.reporter.response.passed).to.be.false;
44 | const errors = linter.reporter.response.errors;
45 | expect(errors && errors[0].message[0].rule).to.be.equal('testRule');
46 | });
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/tests/rules/quotePrefTest.ts:
--------------------------------------------------------------------------------
1 | import { QuotePref } from '../../src/rules/index';
2 | import { expect } from 'chai';
3 | import { splitAndRun } from '../staff/bootstrap';
4 |
5 | describe('Test quote pref rule', () => {
6 | describe('You use single quotes', () => {
7 | it('Should throw error if line has double quote', () => {
8 | const rule = new QuotePref({
9 | conf: 'single'
10 | });
11 |
12 | splitAndRun(
13 | '.test\n' +
14 | '\tbackground-image: url("./logo.png")\n',
15 | rule
16 | );
17 |
18 | expect(rule.errors.length).to.be.equal(1);
19 | });
20 | it('Should not throw error if line has single quote', () => {
21 | const rule = new QuotePref({
22 | conf: 'single'
23 | });
24 |
25 | splitAndRun(
26 | '.test\n' +
27 | '\tbackground-image: url(\'./logo.png\')\n',
28 | rule
29 | );
30 |
31 | expect(rule.errors.length).to.be.equal(0);
32 | });
33 | });
34 | describe('You use double quotes', () => {
35 | it('Should throw error if in line single quote', () => {
36 | const rule = new QuotePref({
37 | conf: 'double'
38 | });
39 |
40 | splitAndRun(
41 | '.test\n' +
42 | '\tbackground-image: url(\'./logo.png\')\n',
43 | rule
44 | );
45 |
46 | expect(rule.errors.length).to.be.equal(1);
47 | });
48 | it('Should not throw error if line has double quote', () => {
49 | const rule = new QuotePref({
50 | conf: 'double'
51 | });
52 |
53 | splitAndRun(
54 | '.test\n' +
55 | '\tbackground-image: url("./logo.png")\n',
56 | rule
57 | );
58 |
59 | expect(rule.errors.length).to.be.equal(0);
60 | });
61 | });
62 | });
63 |
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
1 | import { Linter } from './src/linter';
2 | import { Reader } from './src/core/reader';
3 | import * as astList from './src/core/ast/index';
4 |
5 | export * from './src/core/rule';
6 | export const ast = astList;
7 | export * from './src/doc';
8 | export * from './src/linter';
9 | export * from './src/commander';
10 | export * from './src/core/content';
11 | export * from './src/core/parser';
12 | export * from './src/core/runner';
13 | export * from './src/core/visitor';
14 | export * from './src/core/translator';
15 | export * from './src/core/line';
16 | export * from './src/core/reader';
17 | export * from './src/core/checker';
18 | export * from './src/core/baseConfig';
19 |
20 | /**
21 | * Main stylus checker
22 | *
23 | * @param path
24 | * @param content
25 | * @param options
26 | * @constructor
27 | */
28 | export async function StylusLinter(path: string | string[], content?: string, options: Dictionary = {}): Promise {
29 | const
30 | linter = new Linter(options),
31 | first = () => Array.isArray(path) ? path[0] : path;
32 |
33 | if (content) {
34 | linter.lint(first(), content);
35 | return linter.display();
36 | }
37 |
38 | if (!path) {
39 | path = linter.config.path || process.cwd();
40 | }
41 |
42 | const
43 | reader = new Reader(linter.config),
44 | readAndDisplay = async () => {
45 | await reader.read(path, linter.lint.bind(linter));
46 |
47 | linter.display(!linter.config.watch);
48 | };
49 |
50 | if (linter.config.watch) {
51 | linter.watch(Array.isArray(path) ? path[0] : path, () => {
52 | console.log('Recheck files...');
53 | readAndDisplay();
54 | });
55 | }
56 |
57 | await readAndDisplay();
58 | }
59 |
--------------------------------------------------------------------------------
/tests/staff/bootstrap.ts:
--------------------------------------------------------------------------------
1 | import { IRule } from '../../src/core/types/rule';
2 | import { Runner } from '../../src/core/runner';
3 | import { StylusParser } from '../../src/core/parser';
4 | import { Rule } from '../../src/core/rule';
5 | import { Content } from '../../src/core/content';
6 |
7 | /**
8 | * Parse tree AST and apply rule
9 | *
10 | * @param text
11 | * @param rule
12 | */
13 | export const parseAndRun = (text: string, rule: IRule) => {
14 | const
15 | content = new Content(text),
16 | parser = new StylusParser({}),
17 | ast = parser.parse(content);
18 |
19 | if (rule.checkNode) {
20 | const
21 | runner = new Runner(ast, (node) => {
22 | if (rule.checkNode && rule.isMatchType(node.nodeName)) {
23 | rule.checkNode(node, content);
24 | }
25 | });
26 |
27 | runner.visit(ast, null);
28 | }
29 |
30 | if (rule.checkLine) {
31 | splitAndRun(text, rule, content);
32 | }
33 | };
34 |
35 | /**
36 | * Split content on lines and apply rule on every lines
37 | *
38 | * @param text
39 | * @param rule
40 | * @param content
41 | */
42 | export const splitAndRun = (text: string, rule: IRule, content: Content = new Content(text)) => {
43 | if (rule.checkLine) {
44 | Rule.clearContext();
45 |
46 | content.forEach((line, index) => {
47 | if (index) {
48 | Rule.beforeCheckLine(line);
49 | rule.checkLine && rule.checkLine(line, index, content);
50 | }
51 | });
52 | }
53 | };
54 |
55 | /**
56 | * Check rule only on one line
57 | *
58 | * @param line
59 | * @param rule
60 | */
61 | export const checkLine = (line: string, rule: IRule): void | boolean => {
62 | const content = new Content(line);
63 |
64 | return rule.checkLine && rule.checkLine(content.firstLine(), 0, content);
65 | };
66 |
--------------------------------------------------------------------------------
/src/rules/quotePref.ts:
--------------------------------------------------------------------------------
1 | import { Rule } from '../core/rule';
2 | import { ILine } from '../core/types/line';
3 |
4 | const stringRe = /(?=["'])(?:"[^"\\]*(?:\\[\s\S][^"\\]*)*"|'[^'\\]*(?:\\[\s\S][^'\\]*)*')/g;
5 |
6 | /**
7 | * Check that quote style is consistent with config
8 | */
9 | export class QuotePref extends Rule {
10 | checkLine(line: ILine): void | boolean {
11 | if (line.line.indexOf('"') === -1 && line.line.indexOf("'") === -1) {
12 | return;
13 | }
14 |
15 | stringRe.lastIndex = 0;
16 |
17 | let badQuotes = false;
18 | let hasInnerQuote = true;
19 | let match = stringRe.exec(line.line);
20 |
21 | while (match !== null) {
22 | const content = match[0].slice(1, -1);
23 |
24 | if (this.state.conf === 'single' && match[0].indexOf('"') === 0) {
25 | hasInnerQuote = content.indexOf("'") !== -1;
26 |
27 | if (!hasInnerQuote) {
28 | badQuotes = true;
29 | this.msg(
30 | `Preferred quote style is ${this.state.conf} quotes`,
31 | line.lineno,
32 | match.index + 1,
33 | match[0].length + match.index,
34 | match[0].replace(/^"/g, '\'').replace(/'$/g, '\'')
35 | );
36 | }
37 |
38 | } else if (this.state.conf === 'double' && match[0].indexOf("'") === 0) {
39 | hasInnerQuote = content.indexOf('"') !== -1;
40 |
41 | if (!hasInnerQuote) {
42 | badQuotes = true;
43 |
44 | this.msg(
45 | `Preferred quote style is ${this.state.conf} quotes`,
46 | line.lineno,
47 | match.index + 1,
48 | match[0].length + match.index,
49 | match[0].replace(/^'/g, '"').replace(/'$/g, '"')
50 | ); }
51 | }
52 |
53 | match = stringRe.exec(line.line);
54 | }
55 |
56 | return badQuotes;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/rules/colons.ts:
--------------------------------------------------------------------------------
1 | import { Rule } from '../core/rule';
2 | import { ILine } from '../core/types/line';
3 |
4 | const validJSON = require('../data/valid.json');
5 |
6 | /**
7 | * Use/Do not use colons after property
8 | */
9 | export class Colons extends Rule {
10 | checkLine(line: ILine): void | boolean {
11 | if (this.context.inHash) {
12 | return;
13 | }
14 |
15 | let colon = this.state.conf === 'always';
16 | let hasPseudo = false;
17 | let hasScope = false;
18 | const arr = line.line.split(/ /);
19 |
20 | if (this.state.conf === 'always' &&
21 | arr.length > 1 &&
22 | arr[0].indexOf(':') === -1 &&
23 | arr[0].indexOf(',') === -1) {
24 | colon = false;
25 | } else if (this.state.conf === 'never' && line.line.indexOf(':') !== -1) {
26 | // check for pseudo selector
27 | hasPseudo = validJSON.pseudo.some((val: string) => line.line.indexOf(val) !== -1);
28 |
29 | // check for scope selector
30 | hasScope = validJSON.scope.some((val: string) => line.line.indexOf(val) !== -1);
31 |
32 | const
33 | index = line.line.indexOf(':'),
34 | url = /url\(.*?\)/i.exec(line.line);
35 |
36 | if (url && url.index < index && url[0].length + url.index > index) {
37 | colon = false;
38 | } else {
39 | if (!hasPseudo && !hasScope) {
40 | colon = true;
41 | }
42 | }
43 | }
44 |
45 | if (this.state.conf === 'always' && colon === false) {
46 | this.msg(
47 | 'missing colon between property and value',
48 | line.lineno,
49 | arr[0].length + 1,
50 | arr[0].length + 1,
51 | ': '
52 | );
53 | } else if (this.state.conf === 'never' && colon === true) {
54 | const index = line.line.indexOf(':');
55 | this.msg('unnecessary colon found', line.lineno, index + 1, index + 2, ' ');
56 | }
57 |
58 | return colon;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/core/reporters/rawReporter.ts:
--------------------------------------------------------------------------------
1 | import { IMessagePack } from '../types/message';
2 | import columnify = require('columnify');
3 | import chalk from 'chalk';
4 | import { Reporter } from '../reporter';
5 |
6 | interface RawMessage {
7 | file?: string
8 | line: number
9 | description: string
10 | rule: string
11 | }
12 |
13 | export class RawReporter extends Reporter {
14 | /**
15 | * @override
16 | */
17 | log(): void {
18 | const
19 | cwd = process.cwd(),
20 | warningsOrErrors = [...this.errors], // TODO add warning mode
21 | messagesToFile: Dictionary = {},
22 | msg = [],
23 | columns = process.stdout.columns || this.options.maxWidth || 400,
24 | calcWidth = (percent: number): number => Math.ceil((columns / 100) * percent) - 4,
25 | pl = (str: string, percent: number): string => str.padEnd(calcWidth(percent), ' ');
26 |
27 | warningsOrErrors.forEach((pack: IMessagePack) => {
28 | pack.message.forEach((message) => {
29 | const path = message.path.replace(cwd, '');
30 |
31 | if (!messagesToFile[path]) {
32 | messagesToFile[path] = [];
33 | }
34 |
35 | const row = {
36 | //file: chalk.magenta(pl(path, 30)),
37 | line: chalk.yellow(pl(message.line.toString(), 3)),
38 | description: chalk.red(pl(message.descr, 75)),
39 | rule: chalk.cyan(message.rule)
40 | };
41 |
42 | messagesToFile[path].push(row);
43 | });
44 | });
45 |
46 | const msgGrouped = Object.keys(messagesToFile).map((file) =>
47 | [
48 | chalk.blue(file),
49 | columnify(messagesToFile[file], this.options),
50 | ''
51 | ].join('\n')
52 | );
53 |
54 | msg.push(msgGrouped.join('\n'));
55 |
56 | const cnt = this.errors.length;
57 |
58 | msg.push(`\nStlint: ${(cnt ? chalk.red(cnt) : chalk.green(0))} Errors.\n`);
59 |
60 | console.log(msg.join(''));
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
1 | import { State } from './core/types/state';
2 | import data = require('./defaultRules.json');
3 | import { BaseConfig } from './core/baseConfig';
4 | import { ReporterType } from './core/types/reporter';
5 | import { IConfig } from './core/types/config';
6 | import chalk from 'chalk';
7 | import { resolve } from 'path';
8 |
9 | export class Config extends BaseConfig implements IConfig {
10 | debug: boolean = false;
11 | reporter: ReporterType = 'raw';
12 |
13 | rules: Dictionary = {...data};
14 | defaultRules: Dictionary = Object.freeze({...data});
15 |
16 | excludes: string[] = ['node_modules/'];
17 |
18 | watch: boolean = false;
19 |
20 | basepath: string = '';
21 | path: string = '';
22 |
23 | grep: string = '';
24 | doc: string = '';
25 | fix: boolean = false;
26 |
27 | stylusParserOptions: Dictionary = {};
28 |
29 | extends: string | string[] = '';
30 | customProperties: string[] = [];
31 |
32 | reportOptions: Dictionary = {
33 | columnSplitter: ' | ',
34 | headingTransform: (heading: string) =>
35 | chalk.yellow(heading.toUpperCase()),
36 | truncate: false
37 | };
38 |
39 | constructor(options: Dictionary) {
40 | super();
41 |
42 | this.extendsOption(options, this);
43 |
44 | if (!this.basepath) {
45 | this.basepath = process.cwd();
46 | }
47 |
48 | if (!this.configFile) {
49 | this.configFile = resolve(this.basepath, this.configName);
50 | }
51 |
52 | const
53 | customConfig = this.readFile(this.configFile);
54 |
55 | this.extendsOption(options, customConfig);
56 |
57 | if (customConfig.extends) {
58 | if (Array.isArray(customConfig.extends)) {
59 | customConfig.extends.forEach(this.extendsByPath.bind(this));
60 | } else {
61 | this.extendsByPath(customConfig.extends);
62 | }
63 | }
64 |
65 | this.applyConfig(this.configFile, customConfig);
66 |
67 | delete options.extraRules;
68 |
69 | this.extendsOption(options, this); // options are main
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/rules/useMixinInsteadUnit.ts:
--------------------------------------------------------------------------------
1 | import { Rule } from '../core/rule';
2 | import { INode } from '../core/types/ast/node';
3 | import { IState } from '../core/types/state';
4 |
5 | interface IUseMixinInsteadunitState extends IState {
6 | mixin: string;
7 | unitType: string;
8 | allowOneUnit?: boolean;
9 | }
10 |
11 | /**
12 | * Allo or deny some mixin instead of unit statement
13 | */
14 | export class useMixinInsteadUnit extends Rule {
15 | nodesFilter: string[] = ['unit', 'call'];
16 |
17 | checkNode(node: INode): void | boolean {
18 |
19 | if (this.state.conf === 'always') {
20 | if (node.value && typeof node.value === 'string') {
21 | const unit = RegExp('([\\d]+)' + this.state.unitType).exec(node.value);
22 |
23 | if (unit) {
24 | const
25 | unitSize: number = Number(unit[1]);
26 |
27 | if (this.state.allowOneUnit && unitSize === 1) {
28 | return false;
29 | }
30 |
31 | let
32 | fix = '';
33 |
34 | if (this.state.mixin === 'basis') {
35 | const
36 | basis = (unitSize / 8);
37 |
38 | fix = `basis(${basis})`;
39 | }
40 |
41 | this.msg(
42 | `Use "${this.state.mixin}" mixin instead "${this.state.unitType}"`,
43 | node.lineno,
44 | node.column,
45 | node.column + node.value.trimRight().length - 1,
46 | fix || null
47 | );
48 |
49 | return true;
50 | }
51 | }
52 | } else {
53 | if (node.nodeName === 'call' && typeof node.key === 'string' && node.key === this.state.mixin) {
54 | let fix = null;
55 |
56 | if (this.state.mixin === 'basis' && node.nodes[0].toString()) {
57 | const
58 | unitSize: number = Number(node.nodes[0].toString()),
59 | basis = (unitSize * 8);
60 |
61 | fix = `${basis}px`;
62 | }
63 |
64 | this.msg(`Do not use "${this.state.mixin}" mixin`,
65 | node.lineno,
66 | node.column,
67 | node.column + node.toString().length - 1,
68 | fix || null);
69 | }
70 | }
71 |
72 | return false;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/core/content.ts:
--------------------------------------------------------------------------------
1 | import { eachLineCallback, IContent } from './types/content';
2 | import { Line } from './line';
3 | import { IMessage } from './types/message';
4 | import { calcPosition, splitLines } from './helpers/index';
5 |
6 | export class Content implements IContent {
7 | protected lines: Line[];
8 |
9 | constructor(readonly content: string) {
10 | this.lines = splitLines(content);
11 | }
12 |
13 | toString(): string {
14 | return this.content;
15 | }
16 |
17 | /**
18 | * Get first line
19 | */
20 | firstLine(): Line {
21 | return this.lines[1];
22 | }
23 |
24 | /**
25 | * Apply callback on every line
26 | *
27 | * @param callback
28 | */
29 | forEach(callback: eachLineCallback): void {
30 | for (let lineno = 1; lineno < this.lines.length; lineno += 1) {
31 | if (callback(this.lines[lineno], lineno) === false) {
32 | break;
33 | }
34 | }
35 | }
36 |
37 | /**
38 | * Return line
39 | * @param lineno
40 | */
41 | getLine(lineno: number): Line {
42 | if (!this.lines[lineno]) {
43 | throw new Error('Line not exists');
44 | }
45 |
46 | return this.lines[lineno];
47 | }
48 |
49 | /**
50 | * Apply some fix to text
51 | *
52 | * @param messages
53 | */
54 | applyFixes(messages: IMessage[]): Content {
55 | let content = this.content;
56 |
57 | messages.forEach((message, index) => {
58 | if (message.fix) {
59 | const
60 | start = calcPosition(message.line, message.start, content),
61 | end = calcPosition(message.endline, message.end, content),
62 | oldPart = content.substring(start, end + 1),
63 | fix = message.fix.replace.toString(),
64 | diffLines = splitLines(fix).length - splitLines(oldPart).length;
65 |
66 | content = content.substr(0, start) + fix + content.substr(end + 1);
67 |
68 | if (diffLines) {
69 | for (let i = index + 1; i < messages.length; i += 1) {
70 | messages[i].line += diffLines;
71 | messages[i].endline += diffLines;
72 | }
73 | }
74 | }
75 | });
76 |
77 | return new Content(content);
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/tests/rules/commaInObjectTest.ts:
--------------------------------------------------------------------------------
1 | import { CommaInObject } from '../../src/rules/index';
2 | import { expect } from 'chai';
3 | import { splitAndRun } from '../staff/bootstrap';
4 |
5 | describe('Comma In Object test', () => {
6 | describe('Need use', () => {
7 | describe('Right content', () => {
8 | it('Should check object fields have trailing comma', () => {
9 | const rule = new CommaInObject({
10 | conf: 'always'
11 | });
12 |
13 | splitAndRun(
14 | '$p = {\n' +
15 | '\ta: #ccc,\n' +
16 | '\tb: #ddd\n' +
17 | '}.test\n' +
18 | '\tmax-height red;\n' +
19 | '\tborder black',
20 | rule
21 | );
22 |
23 | expect(rule.errors.length).to.be.equal(0);
24 | });
25 | });
26 | describe('Wrong content', () => {
27 | it('Should check object fields have trailing comma', () => {
28 | const rule = new CommaInObject({
29 | conf: 'always'
30 | });
31 |
32 | splitAndRun(
33 | '$p = {\n' +
34 | '\ta: #ccc\n' +
35 | '\tb: #ddd\n' +
36 | '\tc: #ddd\n' +
37 | '}.test\n' +
38 | '\tmax-height red;\n' +
39 | '\tborder black',
40 | rule
41 | );
42 |
43 | expect(rule.errors.length).to.be.equal(2);
44 | });
45 | });
46 | });
47 |
48 | describe('Do not need use', () => {
49 | describe('Right content', () => {
50 | it('Should check object fields have not trailing comma', () => {
51 | const rule = new CommaInObject({
52 | conf: 'never'
53 | });
54 |
55 | splitAndRun(
56 | '$p = {\n' +
57 | '\ta: #ccc\n' +
58 | '\tb: #ddd\n' +
59 | '}.test\n' +
60 | '\tmax-height red;\n' +
61 | '\tborder black',
62 | rule
63 | );
64 |
65 | expect(rule.errors.length).to.be.equal(0);
66 | });
67 | });
68 | describe('Wrong content', () => {
69 | it('Should check object fields have not trailing comma', () => {
70 | const rule = new CommaInObject({
71 | conf: 'never'
72 | });
73 |
74 | splitAndRun(
75 | '$p = {\n' +
76 | '\ta: #ccc,\n' +
77 | '\tc: #ccc,\n' +
78 | '\tb: #ddd\n' +
79 | '}.test\n' +
80 | '\tmax-height red;\n' +
81 | '\tborder black',
82 | rule
83 | );
84 |
85 | expect(rule.errors.length).to.be.equal(2);
86 | });
87 | });
88 | });
89 | });
90 |
--------------------------------------------------------------------------------
/bin/stlint:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | 'use strict';
4 |
5 | const yargs = require('yargs');
6 | const stlint = require('../').StylusLinter;
7 | const Linter = require('../').Linter;
8 | const Commander = require('../').Commander;
9 |
10 | const options = yargs
11 | .usage('Usage: $0 file')
12 | .option('info', {
13 | alias: 'i',
14 | describe: 'Show info',
15 | type: 'boolean'
16 | })
17 | .option('config', {
18 | alias: 'c',
19 | describe: 'Location of custom config file',
20 | type: 'string'
21 | })
22 | .option('command', {
23 | alias: 'cmd',
24 | describe: 'Some command for run',
25 | type: 'string'
26 | })
27 | .option('reporter', {
28 | alias: 'r',
29 | describe: 'Reporter name',
30 | type: 'string'
31 | })
32 | .option('grep', {
33 | alias: 'g',
34 | describe: 'Only run rules matching this string or regexp',
35 | type: 'string'
36 | })
37 | .option('newline', {
38 | alias: 'nl',
39 | describe: 'Character for replacing newline',
40 | default: '',
41 | type: 'string'
42 | })
43 | .option('fix', {
44 | alias: 'f',
45 | describe: 'Try fix some rules',
46 | type: 'boolean'
47 | })
48 | .option('doc', {
49 | describe: 'Generate doc',
50 | type: 'string'
51 | })
52 | .option('watch', {
53 | alias: 'w',
54 | describe: 'Watch changes',
55 | type: 'boolean'
56 | })
57 | .version(require('../package').version)
58 | .alias('version', 'v')
59 | .help('help')
60 | .alias('help', 'h')
61 | .alias('help', '?')
62 | .example('$0 file.styl', 'Run Stylus Linter on .styl-file')
63 | .example('$0 ./directory', 'Run Stylus Linter on directory with .styl-files')
64 | .example('$0 ./directory --fix', 'Run Stylus Linter on directory with .styl-files and try fix some issues')
65 | .example('$0 ./directory --watch', 'Check all .styl files in directory after some of these was changed')
66 | .epilogue('MIT')
67 | .argv;
68 |
69 | if (options.newline) {
70 | options.content = options.content.replace(new RegExp(`${options.newline}`, 'g'), '\n');
71 | }
72 |
73 | if (options.command) {
74 | const commander = new Commander();
75 | commander.exec(options.command);
76 |
77 | } else if (options.info) {
78 | const linter = new Linter(options);
79 | linter.info();
80 |
81 | } else if (options.doc) {
82 | require('../').doc(options);
83 |
84 | } else {
85 | stlint(options._[0], options.content, options);
86 | }
87 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "stlint",
3 | "version": "1.0.65",
4 | "description": "Stylus Linter",
5 | "main": "index.js",
6 | "bin": {
7 | "stlint": "./bin/stlint"
8 | },
9 | "files": [
10 | "bin/",
11 | "index.js",
12 | "src/"
13 | ],
14 | "repository": {
15 | "type": "git",
16 | "url": "https://github.com/stylus/stlint"
17 | },
18 | "bugs": {
19 | "url": "https://github.com/stylus/stlint/issues"
20 | },
21 | "scripts": {
22 | "newversion": "npm test && npm version patch --no-git-tag-version && npm run build && npm run doc && npm run newversiongit && npm publish ./",
23 | "newversiongit": "git add --all && git commit -m \"New version $npm_package_version. Read more https://github.com/stylus/stlint/releases/tag/$npm_package_version \" && git tag $npm_package_version && git push --tags origin HEAD:master",
24 | "start": "webpack --watch",
25 | "build": "webpack",
26 | "doc": "./bin/stlint --doc rules --fix",
27 | "test2": "./bin/stlint ./test.styl",
28 | "test": "mocha tests/**/**.ts tests/**.ts",
29 | "fix": "tslint -c tslint.json ./src/**/*.ts ./src/**/**/*.ts ./src/*.ts --fix"
30 | },
31 | "keywords": [
32 | "lint",
33 | "linter",
34 | "stylus",
35 | "stylus-linter",
36 | "stlint"
37 | ],
38 | "author": "Chupurnov Valeriy",
39 | "license": "MIT",
40 | "dependencies": {
41 | "@types/yargs": "^15.0.3",
42 | "async": "^2.6.3",
43 | "chalk": "^2.4.2",
44 | "columnify": "^1.5.4",
45 | "escaper": "^3.0.3",
46 | "glob": "^7.1.6",
47 | "husky": "^4.2.3",
48 | "native-require": "^1.1.4",
49 | "node-watch": "^0.6.3",
50 | "prettier": "^1.19.1",
51 | "strip-json-comments": "^2.0.1",
52 | "stylus": "^0.54.7",
53 | "yargs": "^13.3.0"
54 | },
55 | "devDependencies": {
56 | "@types/async": "^2.4.2",
57 | "@types/chai": "^4.2.9",
58 | "@types/glob": "^7.1.1",
59 | "@types/mocha": "^5.2.7",
60 | "@types/node": "^11.15.7",
61 | "awesome-typescript-loader": "^5.2.1",
62 | "chai": "^4.2.0",
63 | "mocha": "^6.2.2",
64 | "ts-node": "^8.6.2",
65 | "tslint": "^5.20.1",
66 | "tslint-config-prettier": "^1.18.0",
67 | "tslint-plugin-prettier": "^2.1.0",
68 | "typescript": "^3.7.5",
69 | "typings": "^2.1.1",
70 | "webpack": "^4.41.6",
71 | "webpack-cli": "^3.3.11",
72 | "webpack-node-externals": "^1.7.2"
73 | },
74 | "mocha": {
75 | "require": [
76 | "ts-node/register",
77 | "tests/staff/bootstrap.ts"
78 | ]
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/tests/smokeTest.ts:
--------------------------------------------------------------------------------
1 | import { Linter } from '../src/linter';
2 | import { expect } from 'chai';
3 |
4 | const
5 | wrongContent = '.tab\n\tcolor: #ccc'
6 |
7 | describe('Smoke test', () => {
8 | it('should work fine', () => {
9 | const linter = new Linter();
10 | linter.lint('./test.styl', wrongContent);
11 |
12 | const response = linter.reporter.response;
13 |
14 | expect(response.passed).to.be.false;
15 | expect(response.errors && response.errors.length).to.be.equal(3);
16 | });
17 |
18 | describe('Empty file test', () => {
19 | it('should work fine', () => {
20 | const linter = new Linter();
21 | linter.lint('./test.styl', '');
22 |
23 | const response = linter.reporter.response;
24 |
25 | expect(response.passed).to.be.true;
26 | expect(response.errors).to.be.equal(void(0));
27 | });
28 | });
29 |
30 | describe('Broken content file test', () => {
31 | it('should work fine', () => {
32 | const linter = new Linter();
33 | linter.lint('./test.styl', '.');
34 |
35 | const response = linter.reporter.response;
36 |
37 | expect(response.passed).to.be.false;
38 | expect(response.errors && response.errors.length).to.be.equal(1);
39 | });
40 |
41 | describe('Broken content 2', () => {
42 | it('should work fine', () => {
43 | const linter = new Linter();
44 | linter.lint('./test.styl', '.t');
45 |
46 | const response = linter.reporter.response;
47 |
48 | expect(response.passed).to.be.false;
49 | expect(response.errors && response.errors.length).to.be.equal(1);
50 | });
51 | });
52 | });
53 | describe('Try Syntax error', () => {
54 | describe('Get hash field', () => {
55 | it('should not return error', () => {
56 | const
57 | linter = new Linter();
58 |
59 | linter.lint('./test.styl', '$p = {\n' +
60 | '\toptionColor: #CCC\n' +
61 | '}\n' +
62 | '.test\n' +
63 | '\tmargin-top $p.optionColor'
64 | );
65 |
66 | const response = linter.reporter.response;
67 |
68 | expect(response.passed).to.be.true;
69 | });
70 | });
71 | describe('Hash field with index', () => {
72 | it('should not return error', () => {
73 | const
74 | linter = new Linter();
75 |
76 | linter.lint('./test.styl', '$colors = {\n' +
77 | '\twhite: #CCC #FFF #F00\n' +
78 | '}\n' +
79 | '$p = {\n' +
80 | '\toptionColor: $colors.white[0]\n' +
81 | '}\n' +
82 | '.b-checkbox-list\n' +
83 | '\tcolor $colors.white[1]'
84 | );
85 |
86 | const response = linter.reporter.response;
87 |
88 | expect(response.passed).to.be.true;
89 | });
90 | });
91 | describe('sss', () => {
92 | it('sss', () => {
93 | const
94 | linter = new Linter();
95 | linter.lint('./test.styl');
96 | });
97 | });
98 | });
99 | });
100 |
--------------------------------------------------------------------------------
/src/rules/brackets.ts:
--------------------------------------------------------------------------------
1 | import { Rule } from '../core/rule';
2 | import { ILine } from '../core/types/line';
3 | import { splitAndStrip, checkPrefix } from '../core/helpers/index';
4 |
5 | const
6 | ignoreRe = /\(.*\)|@extend|\(|if|for(?!\w)|else|return|@block|@media|@import|@require|,$/,
7 | stripRe = /(?=\S)\[\S+]|([.#])\w+/,
8 | equalsRe = /( =|\?=|\+=|-=)+/,
9 | validJSON: ValidCSS = require('../data/valid.json');
10 |
11 | /**
12 | * Check for brackets
13 | */
14 | export class Brackets extends Rule {
15 | checkLine(line: ILine): void | boolean {
16 | if (this.context.inHash || line.isEmpty() || equalsRe.test(line.line) || ignoreRe.test(line.line)) {
17 | return;
18 | }
19 |
20 | let
21 | arr = ['hint'],
22 | isCSS = false,
23 | isMixin = false,
24 | bracket: false | number = false;
25 |
26 | if (this.state.conf === 'never') {
27 | if (line.line.indexOf('{') !== -1 && line.line.indexOf('=') === -1 && line.line.indexOf('}') === -1) {
28 | bracket = line.line.indexOf('{');
29 | } else if (line.line.indexOf('{') === -1 && line.line.indexOf('}') !== -1) {
30 | bracket = line.line.indexOf('}');
31 | }
32 |
33 | } else if (this.state.conf === 'always') {
34 |
35 | arr = splitAndStrip(new RegExp(/[\s\t,:]/), line.line);
36 |
37 | if (typeof arr[0] !== 'undefined') {
38 | arr[0] = arr[0].replace(stripRe, '').trim();
39 |
40 | isCSS = validJSON.css.some((css) => arr[0] === css || checkPrefix(arr[0], css, validJSON));
41 |
42 | isMixin = this.config.customProperties.some((mixin) => arr[0] === mixin);
43 | }
44 |
45 | // basically, we don't care about properties like margin or padding
46 | if (line.line.trim().indexOf('}') !== -1 || isCSS || isMixin) {
47 | return;
48 | }
49 |
50 | if (line.line.indexOf('{') !== -1) {
51 | bracket = line.line.indexOf('{');
52 | this.context.openBracket = true;
53 | } else if (line.line.indexOf('}') !== -1 && this.context.openBracket) {
54 | bracket = line.line.indexOf('}');
55 | this.context.openBracket = false;
56 | }
57 | }
58 |
59 | if (this.state.conf === 'never' && bracket !== false) {
60 | this.msg('unnecessary bracket', line.lineno, bracket + 1, line.lineno, '');
61 |
62 | } else if (this.state.conf === 'always' && bracket === false) {
63 | this.msg('always use brackets when defining selectors',
64 | line.lineno,
65 | line.line.length,
66 | line.lineno,
67 | line.line[line.line.length - 1] + (this.context.openBracket ? '}' : ' {')
68 | );
69 |
70 | if (!this.context.openBracket) {
71 | this.context.openBracket = true;
72 | }
73 | }
74 |
75 | if (this.state.conf === 'always' && line.isLast() && this.context.openBracket) {
76 | this.msg('need close bracket',
77 | line.lineno,
78 | line.line.length,
79 | line.lineno,
80 | line.line[line.line.length - 1] + '\n}'
81 | );
82 | }
83 |
84 | return bracket !== false;
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/rules/depthControl.ts:
--------------------------------------------------------------------------------
1 | import { Rule } from '../core/rule';
2 | import { IState } from '../core/types/state';
3 | import { Block, Selector, Property, Obj, Ident, Node, Media, Condition } from '../core/ast/index';
4 | import { INode } from '../core/types/ast/node';
5 |
6 | interface IDepthControlState extends IState {
7 | indentPref?: 'tab' | number
8 | }
9 |
10 | /**
11 | * Control depth spaces or tab
12 | */
13 | export class DepthControl extends Rule {
14 |
15 | nodesFilter: string[] = ['block', 'selector', 'obj'];
16 |
17 | checkNode(node: Block | Selector | Obj): void {
18 | const
19 | indentPref: number = typeof this.state.indentPref === 'number' ? this.state.indentPref : 1;
20 |
21 | if (node instanceof Block || node instanceof Selector) {
22 | let
23 | parentNode: INode | null = node.closest('selector|media|condition|keyframes|func'),
24 | needCheckPreviousSelector = false,
25 | prev: INode | null = parentNode;
26 |
27 | if (parentNode && parentNode instanceof Selector) {
28 | while (prev && parentNode) {
29 | prev = prev.previousSibling();
30 |
31 | if (prev && prev instanceof Selector && prev.lineno === parentNode.lineno) {
32 | parentNode = prev;
33 | } else {
34 | break;
35 | }
36 | }
37 | }
38 |
39 | if (parentNode) {
40 | if (node instanceof Block) {
41 | node.nodes.forEach((child) => {
42 | if (child.line && child.line.isIgnored) {
43 | return;
44 | }
45 |
46 | if (
47 | parentNode &&
48 | (
49 | child instanceof Property ||
50 | child instanceof Media ||
51 | child instanceof Condition
52 | ) &&
53 | child.column - indentPref !== parentNode.column
54 | ) {
55 | this.msg('incorrect indent', child.lineno, 1, child.column);
56 | }
57 | });
58 | } else if (node.column - indentPref !== parentNode.column) {
59 | needCheckPreviousSelector = true;
60 | }
61 | } else if (node instanceof Selector && node.column !== 1) {
62 | needCheckPreviousSelector = true;
63 | }
64 |
65 | if (needCheckPreviousSelector) {
66 | prev = node.previousSibling();
67 |
68 | if (!prev || prev.lineno !== node.lineno) {
69 | this.msg('incorrect indent', node.lineno, 1, node.column);
70 | }
71 | }
72 |
73 | return;
74 | }
75 |
76 | if (node instanceof Obj) {
77 | const
78 | key: Ident | Property | null = node.closest('ident|property');
79 |
80 | if (key) {
81 | const parentColumn = (key instanceof Property && key.key instanceof Ident) ? key.key.column : key.column;
82 |
83 | node.nodes.forEach((child) => {
84 | if (child.line && child.line.isIgnored) {
85 | return;
86 | }
87 |
88 | if (child instanceof Property && child.key instanceof Ident && child.key.column - indentPref !== parentColumn) {
89 | this.msg('incorrect indent', child.key.lineno, 1, child.key.column);
90 | }
91 | });
92 | }
93 |
94 | return;
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/core/reporter.ts:
--------------------------------------------------------------------------------
1 | import { IReporter } from './types/reporter';
2 | import { IMessagePack } from './types/message';
3 | import { IResponse } from './types/response';
4 | import { inspect } from 'util';
5 |
6 | export abstract class Reporter implements IReporter {
7 |
8 | static getInstance(type: string, config: Dictionary): IReporter {
9 | switch (type) {
10 | case 'json':
11 | return new (require('./reporters/jsonReporter').JsonReporter)(config);
12 |
13 | case 'silent':
14 | return new (require('./reporters/silentReporter').SilentReporter)(config);
15 |
16 | default:
17 | return new (require('./reporters/rawReporter').RawReporter)(config);
18 | }
19 | }
20 |
21 | errors: IMessagePack[] = [];
22 |
23 | response: IResponse = {
24 | passed: true
25 | };
26 |
27 | private path: string = '';
28 |
29 | protected constructor(readonly options: Dictionary) {
30 | }
31 |
32 | /**
33 | * Set current working file
34 | * @param path
35 | */
36 | setPath(path: string): void {
37 | this.path = path;
38 | }
39 |
40 | /**
41 | * Add new error in message pull
42 | * @param rule
43 | * @param message
44 | * @param line
45 | * @param start
46 | * @param end
47 | * @param fix
48 | * @param endLine
49 | */
50 | add(
51 | rule: string,
52 | message: string,
53 | line: number = 1,
54 | start: number = 1,
55 | end: number = 1,
56 | fix: string | null = null,
57 | endLine: number = line
58 | ): void {
59 | this.errors.push({
60 | message: [{
61 | rule,
62 | descr: message,
63 | path: this.path,
64 | line,
65 | endline: endLine >= line ? endLine : line,
66 | start,
67 | end: end > start ? end : start,
68 | fix: (fix !== undefined && fix !== null) ? {
69 | replace: fix.toString()
70 | } : null
71 | }]
72 | });
73 | }
74 |
75 | /**
76 | * Output data some methods
77 | */
78 | abstract log(): void;
79 |
80 | /**
81 | * Fill response object
82 | */
83 | fillResponse(): void {
84 | this.response.passed = !this.errors.length;
85 | this.response.errors = this.errors.length ? this.errors : undefined;
86 | }
87 |
88 | /**
89 | * Prepare data and output result
90 | * @param exit
91 | */
92 | display(exit: boolean): void {
93 | this.fillResponse();
94 | this.log();
95 | this.reset();
96 |
97 | if (exit) {
98 | process.exit(this.response.passed ? 0 : 1);
99 | }
100 | }
101 |
102 | /**
103 | * Reset all error stores
104 | */
105 | reset(): void {
106 | this.errors.length = 0;
107 | this.response = {
108 | passed: true
109 | };
110 | }
111 |
112 | /**
113 | * Filter messages
114 | * @param grep
115 | */
116 | filterErrors(grep: string): void {
117 | this.errors = this.errors.filter(
118 | (error) => {
119 | error.message = error.message.filter((msg) => !!msg.descr.match(grep) || !!msg.rule.match(grep));
120 |
121 | return error.message.length;
122 | }
123 | );
124 | }
125 | }
126 |
127 | export const log = (val: any) => console.log(inspect(val, {
128 | depth: 10
129 | }));
130 |
--------------------------------------------------------------------------------
/src/core/documentator/documentator.ts:
--------------------------------------------------------------------------------
1 | import { Config } from '../../config';
2 | import { Glob } from 'glob';
3 | import { readFileSync } from 'fs';
4 | import { readmePatcher } from './readmePatcher';
5 | // @ts-ignore
6 | import * as ts from 'typescript';
7 | import { lcfirst } from '../helpers/lcfirst';
8 | import { State } from '../types/state';
9 |
10 | /**
11 | * Visit all ts nodes
12 | *
13 | * @param callback
14 | * @param node
15 | */
16 | function visit(callback: (node: ts.Node) => void, node: ts.Node): void {
17 | callback(node);
18 |
19 | ts.forEachChild(node, visit.bind(null, callback));
20 | }
21 |
22 | export interface RuleDocs {
23 | name: string;
24 | description: string;
25 | default: State;
26 | }
27 |
28 | export class Documentator {
29 | private config: Config;
30 |
31 | constructor(options: Dictionary) {
32 | this.config = new Config(options);
33 | }
34 |
35 | /**
36 | * Generate documentation
37 | */
38 | generate(): void {
39 | switch (this.config.doc) {
40 | default:
41 | this.generateRules();
42 | }
43 | }
44 |
45 | /**
46 | * Generate rules docs
47 | */
48 | private generateRules(): void {
49 | const
50 | result: RuleDocs[] = [];
51 |
52 | new Glob('./src/rules/*.ts', {}, async (err: Error | null, files: string[]) => {
53 | if (err) {
54 | throw err;
55 | }
56 |
57 | files.forEach(async (file) => {
58 | const match = /\/(\w+)\.ts/.exec(file);
59 |
60 | if (match) {
61 | const rule = match[1];
62 | if (rule !== 'index') {
63 |
64 | const sourceFile = ts.createSourceFile(
65 | file,
66 | readFileSync(file).toString(),
67 | ts.ScriptTarget.ES2018,
68 | /*setParentNodes */ true
69 | );
70 |
71 | visit((node) => {
72 | switch (node.kind) {
73 | case ts.SyntaxKind.ClassDeclaration: {
74 | const
75 | name = lcfirst(node.name.escapedText);
76 |
77 | let
78 | description = (node.jsDoc && node.jsDoc[0]) ? node.jsDoc[0].comment : '';
79 |
80 | description = description
81 | .replace(/\t/g, ' ')
82 | .replace(/(```stylus)(.*)(```)/s, (...match: string[]) => {
83 | match[2] = match[2]
84 | .split('\n')
85 | .map((line) =>
86 | line
87 | .replace(/^[ \t]+\*/g, '')
88 | .replace(/^ /g, '')
89 | )
90 | .join('\n');
91 | return `${match[1]}${match[2]}${match[3]}`;
92 | });
93 |
94 | result.push({
95 | name,
96 | description,
97 | default: this.config.defaultRules[name]
98 | });
99 | }
100 | }
101 | }, sourceFile);
102 | }
103 | }
104 | });
105 |
106 | if (!this.config.fix) {
107 | return this.log(result);
108 | }
109 |
110 | readmePatcher(result);
111 | });
112 | }
113 |
114 | /**
115 | *
116 | * @param data
117 | */
118 | private log(data: object): void {
119 | console.log(JSON.stringify(data));
120 | process.exit();
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/core/reader.ts:
--------------------------------------------------------------------------------
1 | import { Glob } from 'glob';
2 | import { relative } from 'path';
3 | import { map } from 'async';
4 | import { readFile, stat } from 'fs';
5 | import { IConfig } from './types/config';
6 |
7 | type ReaderCallback = (file: string, content: string) => void;
8 |
9 | export class Reader {
10 | constructor(readonly config: IConfig) {
11 | }
12 |
13 | /**
14 | * Check `dir` parameter for folder or file call `readFolder` or `readFiles`
15 | *
16 | * @param dir
17 | * @param callback
18 | * @return Promise
19 | */
20 | read(dir: string | string[], callback: ReaderCallback): Promise {
21 | return new Promise(async (resolve) => {
22 | if (typeof dir !== 'string' && !(dir instanceof Array)) {
23 | throw new TypeError('getFiles err. Expected string or array, but received: ' + typeof dir);
24 | }
25 |
26 | if (typeof dir === 'string') {
27 | if (dir === process.cwd()) {
28 | dir = dir + '/**/*.styl';
29 | await this.readFolder(dir, callback);
30 | resolve();
31 | return;
32 | }
33 |
34 | return stat(dir, async (err, stats) => {
35 | if (!stats || err) {
36 | throw Error('Stlint Error: No such file or dir exists!');
37 | }
38 |
39 | if (stats.isFile()) {
40 | await this.readFiles([dir.toString()], callback);
41 | } else if (stats.isDirectory()) {
42 | await this.readFolder(dir.toString() + '/**/*.styl', callback);
43 | }
44 |
45 | resolve();
46 | });
47 | }
48 |
49 | return Promise.all(dir.map((path) => this.read(path, callback)));
50 | });
51 | }
52 |
53 | /**
54 | * Find all 'styl' files in the directory and call `readFiles`
55 | *
56 | * @param dir
57 | * @param callback
58 | * @return Promise
59 | */
60 | readFolder(dir: string, callback: ReaderCallback): Promise {
61 | return new Promise((resolve) =>
62 | new Glob(dir, {}, async (err: Error | null, files: string[]) => {
63 | if (err) {
64 | throw err;
65 | }
66 |
67 | if (this.config.excludes && this.config.excludes.length) {
68 | files = files.filter((file) => {
69 | const
70 | relPath = relative(dir.replace('/**/*.styl', ''), file);
71 |
72 | return !this.config.excludes.some((exclude) => {
73 | const reg = new RegExp(exclude);
74 |
75 | return reg.test(relPath);
76 | });
77 | });
78 | }
79 |
80 | await this.readFiles(files, callback);
81 |
82 | resolve();
83 | }));
84 | }
85 |
86 | /**
87 | * Read all files from array and call ReaderCallback
88 | *
89 | * @param files
90 | * @param callback
91 | * @return Promise
92 | */
93 | readFiles(files: string[], callback: ReaderCallback): Promise {
94 | return new Promise((resolve) => {
95 | map(files, readFile, (error: Error | null | void, buffer: (Buffer | void)[] | void) => {
96 | if (error) {
97 | throw error;
98 | }
99 |
100 | if (buffer) {
101 | buffer.forEach(async (bf, index) => {
102 | bf && await callback(files[index], bf.toString());
103 |
104 | if (index === files.length - 1) {
105 | resolve();
106 | }
107 | });
108 | }
109 | });
110 | });
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/src/core/baseConfig.ts:
--------------------------------------------------------------------------------
1 | import { isPlainObject, mergeArray } from './helpers/index';
2 | import { existsSync, readFileSync, statSync } from 'fs';
3 | import stripJsonComments = require('strip-json-comments');
4 | import { resolve, dirname, extname } from 'path';
5 | import { IStats } from './types/IStats';
6 | import _require = require('native-require');
7 |
8 | export class BaseConfig {
9 | configName: string = '.stlintrc';
10 | configFile: string = '';
11 |
12 | extraRules: string | string[] = '';
13 |
14 | preprocessors: string[] = [];
15 | autocompletes: string[] = [];
16 |
17 | /**
18 | * Wrapper for path.statSync
19 | * @param path
20 | */
21 | statSync(path: string): IStats {
22 | return statSync(path);
23 | }
24 |
25 | /**
26 | * Read some file format
27 | * @param configFile
28 | */
29 | readFile(configFile: string): Dictionary {
30 | const ext = extname(configFile) || '';
31 |
32 | try {
33 | switch (ext.toLowerCase()) {
34 | case '.js':
35 | return _require(configFile);
36 | default:
37 | return this.readJSONFile(configFile);
38 | }
39 | } catch {
40 | }
41 |
42 | return {};
43 | }
44 |
45 | /**
46 | * Read JSON File
47 | */
48 | readJSONFile(configFile: string): Dictionary {
49 | if (existsSync(configFile)) {
50 | try {
51 | return JSON.parse(stripJsonComments(readFileSync(configFile, 'utf8')));
52 | } catch {
53 | }
54 | }
55 |
56 | return {};
57 | }
58 |
59 | /**
60 | * Try read config file .stlintrc
61 | */
62 | applyConfig(configFile: string, customConfig: Dictionary): void {
63 | const
64 | dir = dirname(configFile),
65 | normalizePath = (extra: string): string => resolve(dir, extra);
66 |
67 | if (customConfig.extraRules) {
68 | customConfig.extraRules = Array.isArray(customConfig.extraRules) ?
69 | customConfig.extraRules.map(normalizePath) : normalizePath(customConfig.extraRules);
70 | }
71 |
72 | if (customConfig.preprocessors) {
73 | customConfig.preprocessors = customConfig.preprocessors.map(normalizePath);
74 | }
75 |
76 | if (customConfig.autocompletes) {
77 | customConfig.autocompletes = customConfig.autocompletes.map(normalizePath);
78 | }
79 |
80 | this.extendsOption(customConfig, this);
81 | }
82 |
83 | /**
84 | * Extends second object from first
85 | *
86 | * @param from
87 | * @param to
88 | */
89 | extendsOption(from: Dictionary, to: Dictionary): Dictionary {
90 | const result: Dictionary = to;
91 |
92 | Object.keys(from).forEach((key) => {
93 | if (isPlainObject(from[key]) && isPlainObject(to[key])) {
94 | result[key] = {...this.extendsOption(from[key], {...to[key]})};
95 |
96 | } else if (Array.isArray(from[key]) && Array.isArray(to[key])) {
97 | result[key] = mergeArray(to[key], from[key]);
98 |
99 | } else {
100 | result[key] = from[key];
101 | }
102 | });
103 |
104 | return result;
105 | }
106 |
107 | /**
108 | * Load extra config files
109 | */
110 | extendsByPath(pathOrPackage: string): void {
111 | const
112 | path: string = /^\./.test(pathOrPackage) ?
113 | resolve(process.cwd(), pathOrPackage) : resolve(process.cwd(), 'node_modules', pathOrPackage),
114 | stat = this.statSync(path);
115 |
116 | const
117 | file = stat.isFile() ? path : resolve(path, this.configName);
118 |
119 | this.applyConfig(file, this.readFile(file));
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/tests/rules/prefixVarsWithDollarTest.ts:
--------------------------------------------------------------------------------
1 | import { PrefixVarsWithDollar } from '../../src/rules/index';
2 | import { parseAndRun } from '../staff/bootstrap';
3 | import { expect } from 'chai';
4 |
5 | const withDollar = '$test = #ccc\n' +
6 | '$p = {}\n' +
7 | '.tab\n' +
8 | '\tposition absolte\n' +
9 | '\tmargin 10px\n' +
10 | '\tbackground-color $p.color\n' +
11 | '';
12 |
13 | const withoutDollar = 'test = #ccc\n' +
14 | 'p = {}\n' +
15 | '.tab\n' +
16 | '\tposition absolte\n' +
17 | '\tmargin 10px\n' +
18 | '\tbackground-color $p.color\n' +
19 | '';
20 |
21 | describe('Test prefixVarsWithDollar rule', () => {
22 | describe('For mixins', () => {
23 | it('should not doing check', () => {
24 | const rule = new PrefixVarsWithDollar({
25 | conf: 'always',
26 | prefix: '$'
27 | });
28 |
29 | parseAndRun('$c = #ccc\n' +
30 | 'app()\n' +
31 | '\tcolor #ccc\n' +
32 | '\n' +
33 | '.test\n' +
34 | '\tfont-size 10px\n' +
35 | '\tapp()\n',
36 | rule);
37 |
38 | expect(rule.errors.length).to.be.equal(0);
39 | });
40 | });
41 | describe('Always', () => {
42 | it('should check variable name and set error if name is not starting with dollar', () => {
43 | const rule = new PrefixVarsWithDollar({
44 | conf: 'always',
45 | prefix: '$'
46 | });
47 |
48 | parseAndRun(withoutDollar, rule);
49 |
50 | expect(rule.errors.length).to.be.equal(2);
51 | });
52 | it('should check variable name and not set error if name is starting with dollar', () => {
53 | const rule = new PrefixVarsWithDollar({
54 | conf: 'always',
55 | prefix: '$'
56 | });
57 |
58 | parseAndRun(withDollar, rule);
59 |
60 | expect(rule.errors.length).to.be.equal(0);
61 | });
62 | });
63 | describe('Never', () => {
64 | it('should check variable name and set error if name is starting with dollar', () => {
65 | const rule = new PrefixVarsWithDollar({
66 | conf: 'never',
67 | prefix: '$'
68 | });
69 |
70 | parseAndRun(withDollar, rule);
71 |
72 | expect(rule.errors.length).to.be.equal(2);
73 | });
74 | it('should check variable name and not set error if name is not starting with dollar', () => {
75 | const rule = new PrefixVarsWithDollar({
76 | conf: 'never',
77 | prefix: '$'
78 | });
79 |
80 | parseAndRun(withoutDollar, rule);
81 |
82 | expect(rule.errors.length).to.be.equal(0);
83 | });
84 | });
85 | describe('Check another prefix', () => {
86 | it('should check variable name and set error if name is not starting with this prefix', () => {
87 | const rule = new PrefixVarsWithDollar({
88 | conf: 'always',
89 | prefix: '_'
90 | });
91 |
92 | parseAndRun('$p = #ccc', rule);
93 |
94 | expect(rule.errors.length).to.be.equal(1);
95 | });
96 | describe('Without error', () => {
97 | it('should check variable name and set error if name is not starting with this prefix', () => {
98 | const rule = new PrefixVarsWithDollar({
99 | conf: 'always',
100 | prefix: '_'
101 | });
102 |
103 | parseAndRun('_p = #ccc', rule);
104 |
105 | expect(rule.errors.length).to.be.equal(0);
106 | });
107 | });
108 | describe('Allow constant', () => {
109 | it('should allow use variable in uppercase', () => {
110 | const rule = new PrefixVarsWithDollar({
111 | conf: 'always',
112 | prefix: '$',
113 | allowConst: true
114 | });
115 |
116 | parseAndRun('COLOR_GRAY = #ccc', rule);
117 |
118 | expect(rule.errors.length).to.be.equal(0);
119 | });
120 | });
121 | });
122 | });
123 |
--------------------------------------------------------------------------------
/tests/rules/useMixinInsteadUnitTest.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import { useMixinInsteadUnit } from '../../src/rules/index';
3 | import { parseAndRun } from '../staff/bootstrap';
4 |
5 | describe('Use basis mixin test', () => {
6 | describe('Need replace px unit to basis(*)', () => {
7 | it('Should check the AST has Unit node with wrong unit notation', () => {
8 |
9 | const rule = new useMixinInsteadUnit({
10 | conf: 'always',
11 | mixin: 'basis',
12 | unitType: 'px'
13 | });
14 |
15 | parseAndRun('.tab\n\tfontsize 12px', rule);
16 |
17 | expect(rule.errors.length).to.be.equal(1);
18 | expect(rule.errors[0][1]).to.be.equal('Use "basis" mixin instead "px"');
19 | expect(rule.errors[0][5]).to.be.equal('basis(1.5)');
20 | });
21 | describe('Deny use some mixin', () => {
22 | it('Should check the AST has Unit node with wrong unit notation but no need replace', () => {
23 |
24 | const rule = new useMixinInsteadUnit({
25 | conf: 'never',
26 | mixin: 'basis',
27 | unitType: 'px'
28 | });
29 |
30 | parseAndRun('.tab\n\tfontsize 12px', rule);
31 |
32 | expect(rule.errors.length).to.be.equal(0);
33 | });
34 | it('Should check the AST has ? node with wrong "basis" notation', () => {
35 |
36 | const rule = new useMixinInsteadUnit({
37 | conf: 'never',
38 | mixin: 'basis',
39 | unitType: 'px'
40 | });
41 |
42 | parseAndRun('.tab\n\tfontsize basis(1.5)', rule);
43 |
44 | expect(rule.errors.length).to.be.equal(1);
45 | expect(rule.errors[0][1]).to.be.equal('Do not use "basis" mixin');
46 | });
47 | describe('Another mixin name', () => {
48 | it('Should check the AST has ? node with wrong "MixinName" notation', () => {
49 |
50 | const rule = new useMixinInsteadUnit({
51 | conf: 'never',
52 | mixin: 'MixinName',
53 | unitType: 'px'
54 | });
55 |
56 | parseAndRun('.tab\n\tfontsize MixinName(1.5)', rule);
57 |
58 | expect(rule.errors.length).to.be.equal(1);
59 | expect(rule.errors[0][1]).to.be.equal('Do not use "MixinName" mixin');
60 | });
61 | });
62 | });
63 | describe('One unit', () => {
64 | describe('Enable', () => {
65 | it('Should check the AST has Unit node with wrong unit notation but ignore it', () => {
66 |
67 | const rule = new useMixinInsteadUnit({
68 | conf: 'always',
69 | mixin: 'basis',
70 | unitType: 'px',
71 | allowOneUnit: true
72 | });
73 |
74 | parseAndRun('.tab\n\tfontsize 1px', rule);
75 |
76 | expect(rule.errors.length).to.be.equal(0);
77 | });
78 | });
79 | describe('Disable', () => {
80 | it('Should check the AST has Unit node with wrong unit notation', () => {
81 |
82 | const rule = new useMixinInsteadUnit({
83 | conf: 'always',
84 | mixin: 'basis',
85 | unitType: 'px',
86 | allowOneUnit: false
87 | });
88 |
89 | parseAndRun('.tab\n\tfontsize 1px', rule);
90 |
91 | expect(rule.errors.length).to.be.equal(1);
92 | expect(rule.errors[0][1]).to.be.equal('Use "basis" mixin instead "px"');
93 | expect(rule.errors[0][5]).to.be.equal('basis(0.125)');
94 | });
95 | });
96 | });
97 | });
98 | describe('Use another unit type', () => {
99 | it('Should check the AST has Unit node with wrong unit notation', () => {
100 |
101 | const rule = new useMixinInsteadUnit({
102 | conf: 'always',
103 | mixin: 'percent',
104 | unitType: 'em'
105 | });
106 |
107 | parseAndRun('.tab\n\twidth 1em', rule);
108 |
109 | expect(rule.errors.length).to.be.equal(1);
110 | expect(rule.errors[0][1]).to.be.equal('Use "percent" mixin instead "em"');
111 | });
112 | });
113 | });
114 |
--------------------------------------------------------------------------------
/src/core/ast/node.ts:
--------------------------------------------------------------------------------
1 | import { INode } from '../types/ast/node';
2 | import { ISNode } from '../types/ast/snode';
3 | import { Line } from '../line';
4 | import { IContent } from '../types/content';
5 |
6 | export class Node implements INode {
7 |
8 | get nodeName(): string {
9 | return this.constructor.name.toLowerCase();
10 | }
11 |
12 | parent: INode | null = null;
13 | lineno: number = 0;
14 | column: number = 0;
15 | nodes: INode[] = [];
16 | segments: INode[] = [];
17 | source: ISNode | null = null;
18 |
19 | key: string | INode = '';
20 |
21 | /**
22 | * Content
23 | */
24 | content: IContent | null = null;
25 |
26 | /**
27 | * Get line object
28 | */
29 | get line(): Line | null {
30 | return (this.content && this.lineno && this.content.getLine(this.lineno)) || null;
31 | }
32 |
33 | value: string | INode | null = '';
34 |
35 | constructor(block: ISNode, parent: INode | null) {
36 | this.lineno = block.lineno;
37 | this.column = block.column;
38 | this.source = block;
39 | this.parent = parent;
40 | }
41 |
42 | append(node: INode, listField: keyof T = 'nodes'): void {
43 | const list = (this)[listField];
44 |
45 | if (list && Array.isArray(list) && node instanceof Node) {
46 | list.push(node);
47 | }
48 |
49 | node.parent = this;
50 | }
51 |
52 | /**
53 | * Use stylus source
54 | */
55 | toString(): string {
56 | if (this.source) {
57 | return this.source.toString();
58 | }
59 |
60 | return this.value ? this.value.toString() : ' ';
61 | }
62 |
63 | getSibling(next: boolean = false): null | INode {
64 | if (this.parent && this.parent.nodes.length) {
65 | const index = this.parent.nodes.indexOf(this);
66 |
67 | if (index !== -1 && ((!next && index > 0) || (next && index < this.parent.nodes.length - 2))) {
68 | return (this.parent.nodes[index + (next ? 1 : -1)]) || null;
69 | }
70 | }
71 |
72 | return null;
73 | }
74 |
75 | /**
76 | * Get previous node in parent.nodes
77 | */
78 | previousSibling(): null | INode {
79 | return this.getSibling();
80 | }
81 |
82 | /**
83 | * Get next node in parent.nodes
84 | */
85 | nextSibling(): null | INode {
86 | return this.getSibling(true);
87 | }
88 |
89 | /**
90 | * Get matched parent
91 | * @param parentClass
92 | */
93 | closest(parentClass: string): null | T {
94 | const
95 | reg = RegExp(`^(${parentClass})$`, 'i');
96 |
97 | let
98 | node = this.parent;
99 |
100 | while (node) {
101 | if (reg.test(node.nodeName)) {
102 | return node;
103 | }
104 |
105 | node = node.parent;
106 | }
107 |
108 | return null;
109 | }
110 |
111 | getChild(findClass?: string, last: boolean | undefined = false): null | INode {
112 | let
113 | node: INode | null | void = this.nodes[last ? this.nodes.length - 1 : 0];
114 |
115 | if (findClass === undefined) {
116 | return node || null;
117 | }
118 |
119 | if (node) {
120 | const
121 | reg = RegExp(`^(${findClass})$`, 'i');
122 |
123 | while (node) {
124 | if (reg.test(node.nodeName)) {
125 | return node;
126 | }
127 |
128 | node = (last ? node.previousSibling() : node.nextSibling());
129 | }
130 | }
131 |
132 | return null;
133 | }
134 |
135 | /**
136 | * Get first matched child
137 | * @param findClass
138 | */
139 | lastChild(findClass?: string): null | T {
140 | return this.getChild(findClass, true);
141 | }
142 |
143 | /**
144 | * Get last matched child
145 | * @param findClass
146 | */
147 | firstChild(findClass?: string): null | T {
148 | return this.getChild(findClass, false);
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/tests/extendsConfigTest.ts:
--------------------------------------------------------------------------------
1 | import { Config } from '../src/config';
2 | import { expect } from 'chai';
3 | import { IState } from '../src/core/types/state';
4 |
5 | describe('Test extends option', () => {
6 | const tmp: Dictionary = {};
7 |
8 | beforeEach(() => {
9 | tmp.readJSONFile = Config.prototype.readJSONFile;
10 | tmp.statSync = Config.prototype.statSync;
11 | });
12 | afterEach(() => {
13 | Config.prototype.readJSONFile = tmp.readJSONFile;
14 | Config.prototype.statSync = tmp.statSync;
15 | });
16 |
17 | describe('Define `extends` options', () => {
18 | describe('like filepath', () => {
19 | it('Should load config from `extends` and redefine some values from it config', () => {
20 | Config.prototype.readJSONFile = (path) =>
21 | path.match('testrc') ? {
22 | rules: {
23 | color: false,
24 | colons: false
25 | }
26 | } : {};
27 |
28 | Config.prototype.statSync = () => ({
29 | isDirectory(): boolean {
30 | return false;
31 | },
32 | isFile(): boolean {
33 | return true;
34 | }
35 | });
36 |
37 | const config = new Config({
38 | rules: {
39 | color: true
40 | },
41 | extends: './tests/.testrc'
42 | });
43 |
44 | expect((config.rules.color)).to.be.true; // redefine
45 | expect((config.rules.colons)).to.be.false; // from extends config
46 | expect((config.rules.prefixVarsWithDollar)).to.be.deep.equal({
47 | allowConst: true,
48 | conf: 'always',
49 | prefix: '$'
50 | }); // default option
51 |
52 | });
53 | });
54 | describe('like array of filepath', () => {
55 | it('Should load all configs from `extends`', () => {
56 | Config.prototype.readJSONFile = (path) => {
57 | switch (true) {
58 | case !!path.match('testrA'):
59 | return {
60 | rules: {
61 | color: {
62 | conf: 'lowercase'
63 | }
64 | }
65 | };
66 | case !!path.match('testrB'):
67 | return {
68 | rules: {
69 | color: {
70 | conf: 'test'
71 | }
72 | }
73 | };
74 | }
75 |
76 | return {};
77 | };
78 |
79 | Config.prototype.statSync = () => ({
80 | isDirectory(): boolean {
81 | return false;
82 | },
83 | isFile(): boolean {
84 | return true;
85 | }
86 | });
87 |
88 | const config = new Config({
89 | extends: [
90 | './tests/.testrA',
91 | './tests/.testrB'
92 | ]
93 | });
94 |
95 | expect((config.rules.color).conf).to.be.equal('test'); // redefine
96 | expect((config.rules.prefixVarsWithDollar)).to.be.deep.equal({
97 | allowConst: true,
98 | conf: 'always',
99 | prefix: '$'
100 | }); // default option
101 |
102 | });
103 | });
104 | describe('like array of packages', () => {
105 | it('Should load all configs from packages root from node_modules folder', () => {
106 | Config.prototype.readJSONFile = (path) => {
107 | path = path.replace(process.cwd(), '');
108 |
109 | switch (path) {
110 | case '/node_modules/stlint-a':
111 | return {
112 | rules: {
113 | color: {
114 | conf: 'lowercase'
115 | }
116 | }
117 | };
118 | case '/node_modules/stlint-b':
119 | return {
120 | rules: {
121 | color: {
122 | conf: 'test'
123 | }
124 | }
125 | };
126 | }
127 |
128 | return {};
129 | };
130 |
131 | Config.prototype.statSync = () => ({
132 | isDirectory(): boolean {
133 | return false;
134 | },
135 | isFile(): boolean {
136 | return true;
137 | }
138 | });
139 |
140 | const config = new Config({
141 | extends: [
142 | 'stlint-a',
143 | 'stlint-b'
144 | ]
145 | });
146 |
147 | expect((config.rules.color).conf).to.be.equal('test'); // redefine
148 | expect((config.rules.prefixVarsWithDollar)).to.be.deep.equal({
149 | conf: 'always',
150 | allowConst: true,
151 | prefix: '$'
152 | }); // default option
153 |
154 | });
155 | });
156 | });
157 | });
158 |
--------------------------------------------------------------------------------
/src/core/rule.ts:
--------------------------------------------------------------------------------
1 | import { ErrorArray, IRule } from './types/rule';
2 | import { IState } from './types/state';
3 | import { lcfirst, objTohash, unwrapObject } from './helpers/index';
4 | import { ILine } from './types/line';
5 | import { IContext } from './types/context';
6 | import { INode } from './types/ast/node';
7 | import { Ident, Obj, Value } from './ast/index';
8 | import { IConfig } from './types/config';
9 |
10 | const initContext: () => IContext = () => ({
11 | hashDeep: 0,
12 | inHash: false,
13 | inComment: false,
14 | openBracket: false,
15 | vars: {},
16 | valueToVar: {}
17 | });
18 |
19 | const
20 | hashStartRe = /\$?[\w]+\s*[=:]\s*{/,
21 | hashEndRe = /}/,
22 | startMultiComment = /\/\*/,
23 | endMultiComment = /\*\//;
24 |
25 | export class Rule implements IRule {
26 | config!: IConfig;
27 | setConfig(config: IConfig): void {
28 | this.config = config;
29 | }
30 |
31 | get context(): IContext {
32 | return Rule.context;
33 | }
34 |
35 | /**
36 | * Rule name
37 | */
38 | get name(): string {
39 | return lcfirst(this.constructor.name);
40 | }
41 |
42 | static clearContext(): void {
43 | Rule.context = initContext();
44 | }
45 |
46 | static getContext(): IContext {
47 | return Rule.context;
48 | }
49 |
50 | /**
51 | *
52 | * @param node
53 | */
54 | static beforeCheckNode(node: INode): void {
55 | if (node instanceof Ident && node.value instanceof Value) {
56 | const isHash = node.value.nodes && node.value.nodes.length && node.value.nodes[0] instanceof Obj;
57 |
58 | this.context.vars[node.key] = isHash ? objTohash(node.value.nodes[0]) : node.value.nodes[0].toString();
59 |
60 | this.context.valueToVar = {...this.context.valueToVar, ...unwrapObject(this.context.vars)};
61 | }
62 | }
63 |
64 | /**
65 | * Check hash object etc
66 | * @param line
67 | */
68 | static beforeCheckLine(line: ILine): void {
69 | if (hashStartRe.test(line.line)) {
70 | Rule.context.hashDeep += 1;
71 | }
72 |
73 | Rule.context.inHash = Rule.context.hashDeep > 0;
74 |
75 | if (Rule.context.hashDeep && hashEndRe.test(line.line)) {
76 | Rule.context.hashDeep -= 1;
77 | }
78 |
79 | if (startMultiComment.test(line.line)) {
80 | Rule.context.inComment = true;
81 | }
82 |
83 | if (Rule.context.inComment) {
84 | const prev = line.prev();
85 |
86 | if (prev && endMultiComment.test(prev.line)) {
87 | Rule.context.inComment = false;
88 | }
89 | }
90 | }
91 |
92 | private static context: IContext = initContext();
93 |
94 | nodesFilter: string[] | null = null;
95 |
96 | state: T = {
97 | conf: 'always',
98 | enabled: true
99 | };
100 |
101 | cache: Dictionary = {};
102 |
103 | hashErrors: Dictionary = {};
104 | errors: ErrorArray[] = [];
105 |
106 | constructor(readonly conf?: T | false) {
107 | if (conf === undefined) {
108 | return;
109 | }
110 |
111 | if (typeof conf !== 'boolean') {
112 | if (Array.isArray(conf)) {
113 | this.state.conf = conf[0];
114 | this.state.enabled = conf[1] === undefined || Boolean(conf[1]);
115 | } else {
116 | this.state = {...this.state, ...conf};
117 |
118 | if (conf.enabled === undefined) {
119 | this.state.enabled = true;
120 | }
121 | }
122 | } else {
123 | this.state.enabled = conf;
124 | }
125 | }
126 |
127 | clearErrors(): void {
128 | this.errors.length = 0;
129 | this.hashErrors = {};
130 | }
131 |
132 | clearContext(): void {
133 | Rule.clearContext();
134 | }
135 |
136 | /**
137 | * Add error message in list
138 | *
139 | * @param message
140 | * @param line
141 | * @param start
142 | * @param end
143 | * @param fix
144 | * @param endLine
145 | */
146 | msg(
147 | message: string,
148 | line: number = 1,
149 | start: number = 1,
150 | end: number = 1,
151 | fix: null | string = null,
152 | endLine: number = line
153 | ): void {
154 | const
155 | error: ErrorArray = [this.name, message, line, start, end, fix, endLine],
156 | hash = error.join('&');
157 |
158 | if (!this.hashErrors[hash]) {
159 | this.hashErrors[hash] = true;
160 | this.errors.push(error);
161 | }
162 | }
163 |
164 | /**
165 | * Check type included in filter
166 | * @param type
167 | */
168 | isMatchType(type: string): boolean {
169 | return !this.nodesFilter || this.nodesFilter.includes(type);
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/tests/ignoreDirectivesTest.ts:
--------------------------------------------------------------------------------
1 | import { Linter } from '../src/linter';
2 | import { expect } from 'chai';
3 | import { IMessage, IMessagePack } from '../src/core/types/message';
4 |
5 | describe('Test ignore directives', () => {
6 | describe('Multiline', () => {
7 | it('should ignore error in disable block', () => {
8 | const linter = new Linter();
9 |
10 | linter.lint('./test.styl',
11 | '$p = {\n' +
12 | '\ta: #CCC\n' +
13 | '\tb: #ccc\n' +
14 | '\tc: 10px\n' +
15 | '}\n' +
16 | '.test\n' +
17 | '\tmargin-top 20px' +
18 | '\tcolor #ccc\n'
19 | );
20 |
21 | let response = linter.reporter.response;
22 |
23 | expect(response.passed).to.be.false;
24 | expect(response.errors && response.errors.length).to.be.equal(5);
25 |
26 | linter.reporter.reset();
27 |
28 | linter.lint('./test.styl',
29 | '$p = {\n' +
30 | '\ta: #CCC\n' +
31 | '\t// @stlint-disable\n' +
32 | '\tb: #ccc\n' +
33 | '\tc: 10px\n' +
34 | '\t// @stlint-enable\n' +
35 | '}\n' +
36 | '.test\n' +
37 | '\tmargin-top 20px' +
38 | '\t// @stlint-disable\n' +
39 | '\tcolor #ccc\n' +
40 | '\t// @stlint-enable\n'
41 | );
42 |
43 | response = linter.reporter.response;
44 |
45 | expect(response.passed).to.be.false;
46 |
47 | expect(response.errors && response.errors.length).to.be.equal(1);
48 |
49 | });
50 | });
51 | describe('One line', () => {
52 | it('should ignore error in line after @stlint-ignore directive', () => {
53 | const
54 | linter = new Linter();
55 |
56 | linter.lint('./test.styl',
57 | '$p = {\n' +
58 | '\ta: #CCC\n' +
59 | '\tb: #ccc\n' +
60 | '\tc: 10px\n' +
61 | '}\n' +
62 | '.test\n' +
63 | '\tmargin-top 20px\n' +
64 | '\tcolor #ccc\n' +
65 | '\tbackground-color #ddd\n'
66 | );
67 |
68 | let response = linter.reporter.response;
69 |
70 | expect(response.passed).to.be.false;
71 | expect(response.errors && response.errors.length).to.be.equal(7);
72 |
73 | linter.reporter.reset();
74 |
75 | linter.lint('./test.styl',
76 | '$p = {\n' +
77 | '\ta: #CCC\n' +
78 | '\t// @stlint-ignore\n' +
79 | '\tb: #ccc\n' + // 2 errors ignored
80 | '\tc: 10px\n' + // error
81 | '}\n' +
82 | '.test\n' +
83 | '\tmargin-top 20px\n' + // error
84 | '\t// @stlint-ignore\n' +
85 | '\tcolor #ccc\n' + // ignored
86 | '\tbackground-color #ddd\n' // 2 errors
87 | );
88 |
89 | response = linter.reporter.response;
90 |
91 | expect(response.passed).to.be.false;
92 | expect(response.errors && response.errors.length).to.be.equal(4);
93 | });
94 | describe('Ignore line in order rule', () => {
95 | it('should ignore error in line after @stlint-ignore directive', () => {
96 | const
97 | linter = new Linter(),
98 | getOrderError = (errors?: IMessagePack[]): IMessage => {
99 | if (!errors) {
100 | throw new Error('We do not have any errors');
101 | }
102 |
103 | const [error] = errors.filter((value) => value.message[0].rule === 'sortOrder');
104 |
105 | if (!error || !error.message || !error.message[0]) {
106 | throw new Error('We do not have sortOrder error');
107 | }
108 |
109 | return error && error.message && error.message[0];
110 | };
111 |
112 | linter.lint('./test.styl',
113 | '.test\n' +
114 | '\tborder 10px\n' +
115 | '\tmax-height $headerHeight\n' +
116 | '\tcolor red\n' +
117 | '\tmargin-bottom 10px\n'
118 | );
119 |
120 | let response = linter.reporter.response;
121 |
122 | expect(response.passed).to.be.false;
123 | expect(response.errors && response.errors.length).to.be.equal(3);
124 |
125 | let orderError = getOrderError(response.errors);
126 |
127 | expect(orderError.line).to.be.equal(2);
128 |
129 | linter.reporter.reset();
130 |
131 | linter.lint('./test.styl',
132 | '.test\n' +
133 | '\t// @stlint-ignore\n' +
134 | '\tborder 10px\n' +
135 | '\tmax-height $headerHeight\n' +
136 | '\tcolor red\n' +
137 | '\tmargin-bottom 10px\n'
138 | );
139 |
140 | response = linter.reporter.response;
141 |
142 | expect(response.passed).to.be.false;
143 |
144 | expect(response.errors && response.errors.length).to.be.equal(2);
145 |
146 | orderError = getOrderError(response.errors);
147 |
148 | expect(orderError.line).to.be.equal(5);
149 | });
150 | });
151 | });
152 | });
153 |
--------------------------------------------------------------------------------
/tests/configTest.ts:
--------------------------------------------------------------------------------
1 | import { Linter } from '../src/linter';
2 | import { expect } from 'chai';
3 | import * as path from 'path';
4 | import { IState } from '../src/core/types/state';
5 |
6 | describe('Test extends options', () => {
7 | describe('Replace order array in sortOrder rule', () => {
8 | it('Should replace order array', () => {
9 | const linter = new Linter({
10 | rules: {
11 | sortOrder: {
12 | conf: 'grouped',
13 | order: [
14 | 'padding',
15 | 'background',
16 | 'absolute',
17 | 'left',
18 | 'color',
19 | 'outline'
20 | ]
21 | }
22 | },
23 | grep: 'sortOrder',
24 | reporter: 'silent',
25 | fix: true
26 | });
27 |
28 | expect((linter.config.rules.sortOrder).order).to.be.deep.equal([
29 | 'padding',
30 | 'background',
31 | 'absolute',
32 | 'left',
33 | 'color',
34 | 'outline'
35 | ]);
36 |
37 | expect(linter.config.grep).to.be.equal('sortOrder');
38 | expect(linter.config.reporter).to.be.equal('silent');
39 | expect(linter.config.fix).to.be.true;
40 | });
41 | });
42 |
43 | describe('Use JS file as config', () => {
44 | it('Should replace default rule', () => {
45 | const
46 | linter1 = new Linter(),
47 | linter2 = new Linter({
48 | extends: [path.join(__dirname, './staff/extends.js')]
49 | }),
50 | state1 = linter1.config.rules.color,
51 | state2 = linter2.config.rules.color;
52 |
53 | expect(state1.conf).to.be.equal('uppercase');
54 | expect(state2.conf).to.be.equal('test-extends');
55 | });
56 | });
57 |
58 | describe('Extends rules', () => {
59 | it('Should replace default rule', () => {
60 | const
61 | linter1 = new Linter(),
62 | linter2 = new Linter({
63 | extends: [path.join(__dirname, './staff/extends.json')]
64 | }),
65 | state1 = linter1.config.rules.color,
66 | state2 = linter2.config.rules.color;
67 |
68 | expect(state1.conf).to.be.equal('uppercase');
69 | expect(state2.conf).to.be.equal('test-extends');
70 | });
71 |
72 | describe('With custom rules', () => {
73 | it('Should not replace custom rule', () => {
74 | const
75 | linter = new Linter({
76 | extends: [path.join(__dirname, './staff/extends.json')],
77 | rules: {
78 | color: {
79 | conf: 'test-sign',
80 | enabled: 2
81 | }
82 | }
83 | }),
84 | state = linter.config.rules.color;
85 |
86 | expect(state.conf).to.be.equal('test-sign');
87 | expect(state.enabled).to.be.equal(2);
88 | expect(state.allowOnlyInVar).to.be.equal(5);
89 | expect(state.denyRGB).to.be.equal(true);
90 | });
91 | describe('Use file config', () => {
92 | it('Should not replace custom rule', () => {
93 | const
94 | linter = new Linter({
95 | config: path.join(__dirname, './staff/config.json')
96 | }),
97 | state = linter.config.rules.color,
98 | someTestRuleState = linter.config.rules.someTestRule;
99 |
100 | expect(state.conf).to.be.equal('test-config'); // from config.json
101 | expect(state.enabled).to.be.equal(false); // from config.json
102 | expect(state.allowOnlyInVar).to.be.equal(3); // from config.json
103 | expect(state.denyRGB).to.be.equal(true); // default
104 | expect(state.allowShortcut).to.be.equal(7); // from extends.json
105 | expect(someTestRuleState).to.be.equal(false); // from config.json
106 | });
107 | });
108 | });
109 | });
110 |
111 | describe('Preprocess content', () => {
112 | it('Should load preprocessor function and apply this to content before lint', () => {
113 | const
114 | linter = new Linter({
115 | preprocessors: [
116 | path.join(__dirname, './staff/preprocessor.js')
117 | ]
118 | });
119 |
120 | const rightContent = '.c\n\tabsolute left right 0';
121 |
122 | linter.lint('./test.styl', rightContent);
123 |
124 | const response = linter.reporter.response;
125 |
126 | expect(response.passed).to.be.false;
127 |
128 | expect(response.errors && response.errors.length).to.be.equal(4);
129 | });
130 |
131 | describe('Path to preprocessor', () => {
132 | it('Should calculate by config file', () => {
133 | const
134 | linter = new Linter({
135 | extends: [
136 | path.join(__dirname, './staff/subfolder/extends.json')
137 | ]
138 | });
139 |
140 | const rightContent = '.c\n\tabsolute left right 0';
141 |
142 | const content = linter.lint('./test.styl', rightContent);
143 |
144 | expect(content.content).to.be.equal(rightContent + 'i work');
145 | });
146 | });
147 | });
148 | });
149 |
--------------------------------------------------------------------------------
/src/rules/color.ts:
--------------------------------------------------------------------------------
1 | import { Rule } from '../core/rule';
2 | import { IState } from '../core/types/state';
3 | import { Call, RGB } from '../core/ast/index';
4 | import { shortcutColor } from '../core/helpers/index';
5 |
6 | interface IColorState extends IState {
7 | allowOnlyInVar?: boolean
8 | allowShortcut?: boolean
9 | denyRBG?: boolean
10 | }
11 |
12 | /**
13 | * Process all color values. Allow or deny use it not in variable and use uppercase or lowercase statements
14 | * For example this code has error - because we use only color in `uppercase`
15 | * ```stylus
16 | * .test
17 | * color #ccc
18 | * ```
19 | * If `allowOnlyInVar` === true code above also has error - no use color without variable
20 | *
21 | * Fixed code
22 | * ```stylus
23 | * $color = #CCC
24 | * .test
25 | * color $color
26 | * ```
27 | */
28 | export class Color extends Rule {
29 | nodesFilter: string[] = ['rgb', 'call'];
30 |
31 | checkNode(node: RGB | Call): void | boolean {
32 | if (node instanceof Call) {
33 | return this.checkRGB(node);
34 | }
35 |
36 | const checkReg = this.state.conf !== 'lowercase' ? /[a-z]/ : /[A-Z]/;
37 |
38 | let fixed = false;
39 |
40 | if (this.state.allowOnlyInVar && node.closest('block')) {
41 | const fix = this.context.valueToVar[node.value] ||
42 | this.context.valueToVar[node.value.toLowerCase()] ||
43 | this.context.valueToVar[node.value.toUpperCase()];
44 |
45 | this.msg(
46 | 'Set color only in variable' + (fix ? `(${fix})` : ''),
47 | node.lineno, node.column,
48 | node.column + node.value.length - 1,
49 | fix || null
50 | );
51 |
52 | fixed = !!fix;
53 | }
54 |
55 | if (node.value && typeof node.value === 'string' && checkReg.test(node.value)) {
56 | const fix = node.value.toString();
57 |
58 | this.msg(
59 | `Only ${ this.state.conf } HEX format`,
60 | node.lineno,
61 | node.column,
62 | node.column + node.value.length - 1,
63 | fixed ? null : this.state.conf === 'uppercase' ? fix.toUpperCase() : fix.toLowerCase()
64 | );
65 |
66 | return true;
67 | }
68 |
69 | if (this.checkShortcutErrors(node) === true) {
70 | return true;
71 | }
72 |
73 | return false;
74 | }
75 |
76 | private RGBToHex(...args: [number, number, number]): string {
77 | return '#' + args.map((c) => {
78 | let
79 | hex = c.toString(16);
80 |
81 | if (hex.length === 1) {
82 | hex = '0' + hex;
83 | }
84 |
85 | return hex;
86 | }).join('');
87 | }
88 |
89 | /**
90 | * Check using rgba and rgb notation
91 | * @param node
92 | */
93 | private checkRGB(node: Call): void {
94 | if (this.state.denyRGB) {
95 | if (node.key && /^rgb(a)?$/i.test(node.key) && node.nodes.length > 2) {
96 | let
97 | fix = '';
98 |
99 | const
100 | firstValue = node.nodes[0] ? node.nodes[0].toString() : '0';
101 |
102 | let hex = this.RGBToHex(
103 | parseInt(firstValue, 10),
104 | parseInt(node.nodes[1] ? node.nodes[1].toString() : '0', 10),
105 | parseInt(node.nodes[2] ? node.nodes[2].toString() : '0', 10)
106 | );
107 |
108 | hex = this.state.conf === 'uppercase' ? hex.toUpperCase() : hex.toLowerCase();
109 |
110 | if (node.key === 'rgb') {
111 | fix = hex;
112 | } else {
113 | if (/^#[0-9a-f]+/i.test(firstValue)) {
114 | return;
115 | }
116 |
117 | fix = `rgba(${hex}, ${node.nodes[3] ? node.nodes[3].toString() : '1'})`;
118 | }
119 |
120 | const
121 | line = node.line,
122 | endIndex = line ? line.line.indexOf(')', node.column - 1) + 1 : node.column + node.key.length - 1;
123 |
124 | this.msg(
125 | 'Deny rgb/rgba format',
126 | node.lineno,
127 | node.column,
128 | endIndex,
129 | fix
130 | );
131 | }
132 | }
133 | }
134 |
135 | private checkShortcutErrors(node: RGB): void | true {
136 | if (node.value && typeof node.value === 'string') {
137 | if (this.state.allowShortcut) {
138 | const shortcut = shortcutColor(node.value);
139 |
140 | if (shortcut !== node.value) {
141 | const fix = this.state.conf === 'uppercase' ? shortcut.toUpperCase() : shortcut.toLowerCase();
142 |
143 | this.msg(
144 | `Color ${ node.value } can have shortcut`,
145 | node.lineno,
146 | node.column,
147 | node.column + node.value.length - 1,
148 | fix
149 | );
150 |
151 | return true;
152 | }
153 | } else {
154 | if (node.value.length < 5) {
155 | const
156 | color = node.value.replace(/([a-f0-9])([a-f0-9])([a-f0-9])/i, '$1$1$2$2$3$3'),
157 | fix = this.state.conf === 'uppercase' ? color.toUpperCase() : color.toLowerCase();
158 |
159 | this.msg(
160 | 'Color must not have shortcut',
161 | node.lineno,
162 | node.column,
163 | node.column + node.value.length - 1,
164 | fix
165 | );
166 |
167 | return true;
168 | }
169 | }
170 | }
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/tests/rules/colonsTest.ts:
--------------------------------------------------------------------------------
1 | import { Colons } from '../../src/rules/index';
2 | import { expect } from 'chai';
3 | import { checkLine, splitAndRun } from '../staff/bootstrap';
4 | import { Linter } from '../../src/linter';
5 |
6 | describe('Colons test', () => {
7 | it('Should check the line has colons and they are needed', () => {
8 | const rule = new Colons({
9 | conf: 'always'
10 | });
11 |
12 | expect(checkLine('color:#ccc', rule)).to.be.true;
13 |
14 | expect(rule.errors.length).to.be.equal(0);
15 | });
16 |
17 | describe('In hash object', () => {
18 | it('Should not find errors', () => {
19 | const rule = new Colons({
20 | conf: 'never'
21 | });
22 |
23 | splitAndRun('$p = {\n\tcolor: #ccc;\n}', rule);
24 |
25 | expect(rule.errors.length).to.be.equal(0);
26 | });
27 | });
28 |
29 | describe('For variable', () => {
30 | it('Should work some way like for usual value', () => {
31 | const rule = new Colons({
32 | conf: 'never'
33 | });
34 |
35 | splitAndRun(
36 | '$p = {\n' +
37 | '\tb: 1px solid #ccc\n' +
38 | '}\n' +
39 | '.test\n' +
40 | '\tmax-height: $headerHeight\n' +
41 | '\tborder: $p.border',
42 | rule
43 | );
44 |
45 | expect(rule.errors.length).to.be.equal(2);
46 | });
47 | describe('For nested variable', () => {
48 | it('Should not count errors', () => {
49 | const rule = new Colons({
50 | conf: 'never'
51 | });
52 |
53 | splitAndRun(
54 | '$p = {\n' +
55 | '\tc: {test: 15px}\n' +
56 | '\tb: 1px solid #ccc\n' +
57 | '}\n' +
58 | '.test\n' +
59 | '\tmax-height: $headerHeight\n' +
60 | '\tborder: $p.border',
61 | rule
62 | );
63 |
64 | expect(rule.errors.length).to.be.equal(2);
65 | });
66 | });
67 | });
68 |
69 | it('Should check the line does not have colons but they are needed', () => {
70 | const rule = new Colons({
71 | conf: 'always'
72 | });
73 |
74 | expect(checkLine('color #ccc', rule)).to.be.false;
75 |
76 | expect(rule.errors.length).to.be.equal(1);
77 | });
78 |
79 | it('Should check the line has colons but they are not needed', () => {
80 | const rule = new Colons({
81 | conf: 'never'
82 | });
83 |
84 | expect(checkLine('color:#ccc', rule)).to.be.true;
85 |
86 | expect(rule.errors.length).to.be.equal(1);
87 | });
88 |
89 | it('Should check the line does not have colons and they are not needed', () => {
90 | const rule = new Colons({
91 | conf: 'never'
92 | });
93 |
94 | expect(checkLine('color #ccc', rule)).to.be.false;
95 |
96 | expect(rule.errors.length).to.be.equal(0);
97 | });
98 |
99 | describe('Detect pseudo elements', () => {
100 | it('Should detect different between pseudo element and property: value expression', () => {
101 | const rule = new Colons({
102 | conf: 'never'
103 | });
104 |
105 | expect(checkLine('.tab:first-child', rule)).to.be.not.true;
106 | });
107 | });
108 |
109 | describe('Colons in url', () => {
110 | it('Should not find error url', () => {
111 | const rule = new Colons({
112 | conf: 'never'
113 | });
114 |
115 | splitAndRun(
116 | '.test\n' +
117 | '\tmax-height $headerHeight\n' +
118 | '\tborder basis(10)\n' +
119 | '\tbackground url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmEU9fChc4AMP6lbBP.woff2)',
120 | rule
121 | );
122 |
123 | expect(rule.errors.length).to.be.equal(0);
124 | });
125 | });
126 |
127 | describe('Colons in comment', () => {
128 | it('Should not find error', () => {
129 | const linter = new Linter({
130 | grep: 'colons',
131 | reporter: 'silent',
132 | fix: true
133 | });
134 |
135 | linter.lint('./test.styl',
136 | '/*!\n' +
137 | ' * V4Fire Client Core\n' +
138 | ' * https://github.com/V4Fire/Client\n' +
139 | ' *\n' +
140 | ' * Released under the MIT license\n' +
141 | ' * https://github.com/V4Fire/Client/blob/master/LICENSE\n' +
142 | ' */\n' +
143 | '.test\n' +
144 | '\tmax-height $headerHeight\n' +
145 | '\tborder basis(10)\n' +
146 | ''
147 | );
148 |
149 | expect(linter.reporter.response.passed).to.be.true;
150 | });
151 |
152 | describe('All lines', () => {
153 | it('Should not be changed', () => {
154 | const linter = new Linter({
155 | reporter: 'silent',
156 | fix: true
157 | });
158 |
159 | console.log(linter.lint('./test.styl',
160 | '/*!\n' +
161 | ' * V4Fire Client Core\n' +
162 | ' * https://github.com/V4Fire/Client\n' +
163 | ' *\n' +
164 | ' * Released under the MIT license\n' +
165 | ' * https://github.com/V4Fire/Client/blob/master/LICENSE\n' +
166 | ' */\n' +
167 | '.test\n' +
168 | '\tmax-height $headerHeight\n' +
169 | '\tborder 10px\n' +
170 | ''
171 | ).content);
172 |
173 | expect(linter.reporter.response.passed).to.be.false;
174 | const errors = linter.reporter.response.errors;
175 | expect(errors && errors.length).to.be.eq(1);
176 | expect(errors && errors[0].message[0].line).to.be.eq(10);
177 | });
178 | });
179 | });
180 | });
181 |
--------------------------------------------------------------------------------
/src/core/checker.ts:
--------------------------------------------------------------------------------
1 | import { IRule } from './types/rule';
2 | import * as rules from '../rules/index';
3 | import { INode } from './types/ast/node';
4 | import { Tree } from './ast/index';
5 | import { Runner } from './runner';
6 | import { Linter } from '../linter';
7 | import { Line } from './line';
8 | import { Rule } from './rule';
9 | import { IReporter } from './types/reporter';
10 | import { lcfirst } from './helpers/lcfirst';
11 | import { statSync, readdirSync } from 'fs';
12 | import { resolve } from 'path';
13 | import _require = require('native-require');
14 | import { IContent } from './types/content';
15 | import { IState } from './types/state';
16 |
17 | export class Checker {
18 | rulesListForNodes: IRule[] = [];
19 | rulesListForLines: IRule[] = [];
20 | rulesList: IRule[] = [];
21 |
22 | constructor(readonly linter: Linter) {
23 | }
24 |
25 | /**
26 | * Load and init rules (and external rules too)
27 | */
28 | loadAndInitRules(): void {
29 |
30 | this.rulesList = this.initRules(rules);
31 |
32 | if (this.linter.config.extraRules) {
33 | const extraRules = this.loadRules(this.linter.config.extraRules);
34 | this.rulesList = this.rulesList.concat(this.initRules(extraRules));
35 | }
36 |
37 | this.rulesListForLines = this.rulesList.filter((rule) => rule.checkLine);
38 | this.rulesListForNodes = this.rulesList.filter((rule) => rule.checkNode);
39 | }
40 |
41 | /**
42 | * Create instance od all rules all rules
43 | * @param rulesConstructors
44 | */
45 | private initRules(rulesConstructors: Dictionary): IRule[] {
46 | const
47 | rulesNames: string[] = Object.keys(rulesConstructors),
48 | config = this.linter.config;
49 |
50 | return rulesNames
51 | .filter((key) => typeof rulesConstructors[key] === 'function')
52 | .map((key: string): IRule => {
53 | let options = config.rules[lcfirst(key)];
54 |
55 | if (options === true && config.defaultRules[lcfirst(key)]) {
56 | options = config.defaultRules[lcfirst(key)];
57 | }
58 |
59 | if (!(rulesConstructors[key].prototype instanceof Rule)) {
60 | rulesConstructors[key].prototype = new Rule(options);
61 | rulesConstructors[key].prototype.constructor = rulesConstructors[key];
62 | }
63 |
64 | const rule: IRule = new (rulesConstructors)[key](options);
65 |
66 | rule.setConfig(config);
67 |
68 | return rule;
69 | })
70 | .filter((rule) => rule.state.enabled);
71 | }
72 |
73 | /**
74 | * Load rules from folder
75 | */
76 | private loadRules(path: string | string[]): Dictionary {
77 | let results: Dictionary = {};
78 |
79 | if (Array.isArray(path)) {
80 | path.map(this.loadRules.bind(this)).forEach((rules) => {
81 | results = {...results, ...rules};
82 | });
83 |
84 | return results;
85 | }
86 |
87 | const stat = statSync(path);
88 |
89 | if (stat.isFile()) {
90 | results = {...results, ...this.requireRule(path)};
91 | } else if (stat.isDirectory()) {
92 | readdirSync(path).forEach((file) => {
93 | // @ts-ignore
94 | results = {...results, ...this.requireRule(resolve(path, file))};
95 | });
96 | }
97 |
98 | return results;
99 | }
100 |
101 | /**
102 | * Load one rule or several rules
103 | * @param path
104 | */
105 | private requireRule(path: string): Dictionary {
106 |
107 | if (/\.js$/.test(path)) {
108 | try {
109 | const rule = _require(`${path}`);
110 |
111 | if (typeof rule === 'function') {
112 | return {
113 | [rule.name]: rule
114 | };
115 | } else {
116 | return {
117 | ...rule
118 | };
119 | }
120 | } catch (e) {
121 | this.linter.reporter.add('JS', e.message, 1, 1);
122 | }
123 | }
124 |
125 | return {};
126 | }
127 |
128 | /**
129 | * Check whole AST
130 | *
131 | * @param ast
132 | * @param content
133 | */
134 | checkASTRules(ast: Tree, content: IContent): void {
135 | try {
136 | const runner = new Runner(ast, this.check.bind(this, content));
137 | runner.visit(ast, null);
138 |
139 | } catch (e) {
140 | if (this.linter.config.debug) {
141 | throw e;
142 | }
143 |
144 | this.linter.reporter.add('astRulesError', e.message, e.lineno || 1, 0);
145 |
146 | } finally {
147 | this.afterCheck();
148 | }
149 | }
150 |
151 | /**
152 | * Check line by line
153 | * @param content
154 | */
155 | checkLineRules(content: IContent): void {
156 | try {
157 | content.forEach((line, index) => {
158 | if (index && !line.isIgnored) {
159 | Rule.beforeCheckLine(line);
160 | this.rulesListForLines.forEach((rule) => rule.checkLine && rule.checkLine(line, index, content));
161 | }
162 | });
163 |
164 | } catch (e) {
165 | this.linter.reporter.add('Line', e.message, e.lineno || 1, 0);
166 |
167 | } finally {
168 | this.afterCheck();
169 | }
170 | }
171 |
172 | private check(content: IContent, node: INode): void {
173 | const type = node.nodeName;
174 |
175 | Rule.beforeCheckNode(node);
176 |
177 | this.rulesListForNodes.forEach((rule: IRule) => {
178 | const line = node.line;
179 |
180 | if (line && !line.isIgnored && rule.checkNode && rule.isMatchType(type)) {
181 | rule.checkNode(node, content);
182 | }
183 | });
184 | }
185 |
186 | /**
187 | * After checking put errors in reporter
188 | */
189 | private afterCheck(): void {
190 | const reporter: IReporter = this.linter.reporter;
191 |
192 | this.rulesList.forEach((rule) => {
193 | rule.errors.forEach((msg) => reporter.add.apply(reporter, msg));
194 | rule.clearErrors();
195 | });
196 |
197 | reporter.fillResponse();
198 | }
199 | }
200 |
--------------------------------------------------------------------------------
/src/linter.ts:
--------------------------------------------------------------------------------
1 | import { Reporter } from './core/reporter';
2 | import { StylusParser } from './core/parser';
3 | import { Checker } from './core/checker';
4 | import { Preprocessor } from './core/preprocessor';
5 | import { existsSync, readFileSync, writeFileSync } from 'fs';
6 | import { resolve } from 'path';
7 | import { IReporter } from './core/types/reporter';
8 | import { Rule } from './core/rule';
9 | import { IConfig } from './core/types/config';
10 | import { Config } from './config';
11 | import watch = require('node-watch');
12 | import { Content } from './core/content';
13 | import { IContent } from './core/types/content';
14 | import { IMessage } from './core/types/message';
15 |
16 | const pkg = require('../package.json');
17 |
18 | export class Linter {
19 | options: Dictionary = {};
20 |
21 | config: IConfig;
22 |
23 | reporter: IReporter;
24 | parser: StylusParser;
25 | checker: Checker;
26 | preprocessor!: Preprocessor;
27 |
28 | /**
29 | * @param options
30 | */
31 | constructor(options: Dictionary = {}) {
32 | this.options = options;
33 |
34 | if (options.config && typeof options.config === 'string') {
35 | options.configFile = options.config;
36 | }
37 |
38 | this.config = new Config(this.options);
39 |
40 | this.reporter = Reporter.getInstance(this.config.reporter, this.config.reportOptions);
41 |
42 | this.parser = new StylusParser(this.config.stylusParserOptions);
43 | this.checker = new Checker(this);
44 |
45 | try {
46 | this.preprocessor = new Preprocessor(this.config.preprocessors);
47 | } catch (e) {
48 | this.reporter.add('preprocessorError', e.message, e.lineno || 1, e.startOffset || 1);
49 | }
50 | }
51 |
52 | /**
53 | * Parse styl file and check rules
54 | */
55 | lint(path: string, str: string | null = null): Content {
56 | path = resolve(path);
57 |
58 | if (!existsSync(path)) {
59 | throw new Error('File not exists');
60 | }
61 |
62 | if (typeof str !== 'string') {
63 | str = readFileSync(path, 'utf8');
64 | }
65 |
66 | let content = new Content(str);
67 |
68 | if (this.preprocessor) {
69 | content = this.preprocessor.apply(content);
70 | }
71 |
72 | try {
73 |
74 | this.checker.loadAndInitRules();
75 |
76 | this.reporter.setPath(path);
77 |
78 | Rule.clearContext();
79 |
80 | this.fillIgnoredLines(content);
81 |
82 | try {
83 | const ast = this.parser.parse(content);
84 | this.checker.checkASTRules(ast, content);
85 |
86 | } catch (e) {
87 | if (this.config.debug) {
88 | throw e;
89 | }
90 |
91 | this.reporter.add('syntaxError', e.message, e.lineno, e.startOffset);
92 | }
93 |
94 | this.checker.checkLineRules(content);
95 | } catch (e) {
96 | if (this.config.debug) {
97 | throw e;
98 | }
99 | } finally {
100 | if (this.config.grep) {
101 | this.reporter.filterErrors(this.config.grep);
102 | }
103 |
104 | if (this.config.fix && str !== null && this.reporter.errors && this.reporter.errors.length) {
105 | this.fix(path, new Content(str));
106 | }
107 | }
108 |
109 | return content;
110 | }
111 |
112 | protected fillIgnoredLines(content: Content): void {
113 | let
114 | ignoreBlock = false;
115 |
116 | content.forEach((line) => {
117 | if (ignoreBlock) {
118 | line.isIgnored = true;
119 | if (/@stlint-enable/.test(line.line)) {
120 | ignoreBlock = false;
121 | }
122 |
123 | } else if (/@stlint-ignore/.test(line.line)) {
124 | line.isIgnored = true;
125 | const next = line.next();
126 |
127 | if (next) {
128 | next.isIgnored = true;
129 | }
130 |
131 | } else if (/@stlint-disable/.test(line.line)) {
132 | ignoreBlock = true;
133 | }
134 | });
135 | }
136 |
137 | /**
138 | * Watch to some directory or file
139 | *
140 | * @param path
141 | * @param callback
142 | */
143 | watch(path: string, callback: () => void): void {
144 | watch(path, {
145 | encoding: 'utf-8',
146 | recursive: true,
147 | filter: /\.styl$/
148 | }, callback);
149 | }
150 |
151 | /**
152 | * Print all errors or warnings
153 | */
154 | display(exit: boolean = true): void {
155 | this.reporter.display(exit);
156 | }
157 |
158 | /**
159 | * Try fix some errors
160 | */
161 | fix(path: string, content: IContent): string {
162 | let
163 | diffContent = content;
164 |
165 | const
166 | fixes: IMessage[] = this.reporter.errors.reduce((fxs, error) => {
167 | error.message.forEach((message: IMessage) => {
168 | if (message.fix !== null) {
169 | fxs.push({...message});
170 | }
171 | });
172 |
173 | return fxs;
174 | }, []);
175 |
176 | fixes.sort((a, b) => a.line - b.line);
177 |
178 | diffContent = diffContent.applyFixes(fixes);
179 |
180 | if (diffContent.toString() !== content.toString()) {
181 | this.saveFix(path, diffContent.toString());
182 | }
183 |
184 | return diffContent.toString();
185 | }
186 |
187 | saveFix(path: string, content: string): void {
188 | writeFileSync(path, content);
189 | }
190 |
191 | info(): void {
192 | const
193 | rules = Object.keys(this.config.rules)
194 | .filter((ruleKey) => ruleKey.match(this.config.grep))
195 | .reduce((rls, ruleKey) => {
196 | rls[ruleKey] = this.config.rules[ruleKey];
197 | return rls;
198 | }, {});
199 |
200 | console.log(
201 | `Version: ${pkg.version}\n` +
202 | `Config: ${this.config.configFile}\n` +
203 | (this.config.extraRules ? `Extra Rules: ${JSON.stringify(this.config.extraRules)}\n` : '') +
204 | (this.config.extends ? `Extends: ${JSON.stringify(this.config.extends)}\n` : '') +
205 | `Rules: ${JSON.stringify(rules, null, 2)}\n` +
206 | ''
207 | );
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/src/defaultRules.json:
--------------------------------------------------------------------------------
1 | {
2 | "mixedSpaces": {
3 | "indentPref": "tab"
4 | },
5 |
6 | "prefixVarsWithDollar": {
7 | "conf": "always",
8 | "prefix": "$",
9 | "allowConst": true
10 | },
11 |
12 | "emptyLines": true,
13 |
14 | "brackets": [
15 | "never"
16 | ],
17 |
18 | "commaInObject": [
19 | "never"
20 | ],
21 |
22 | "depthControl": {
23 | "indentPref": "tab"
24 | },
25 |
26 | "quotePref": [
27 | "double"
28 | ],
29 |
30 | "semicolons": [
31 | "never"
32 | ],
33 |
34 | "colons": [
35 | "never"
36 | ],
37 |
38 | "color": {
39 | "conf": "uppercase",
40 | "enabled": true,
41 | "allowOnlyInVar": true,
42 | "allowShortcut": true,
43 | "denyRGB": true
44 | },
45 |
46 | "leadingZero": [
47 | "always"
48 | ],
49 |
50 | "useMixinInsteadUnit": {
51 | "conf": "always",
52 | "mixin": "basis",
53 | "unitType": "px",
54 | "allowOneUnit": false
55 | },
56 |
57 | "sortOrder": {
58 | "conf": "grouped",
59 | "startGroupChecking": 6,
60 | "order": [
61 | [
62 | "absolute",
63 | "position",
64 | "z-index",
65 | "top",
66 | "right",
67 | "bottom",
68 | "left"
69 | ],
70 |
71 | [
72 | "content",
73 | "display",
74 |
75 | "flexbox",
76 | "flex",
77 | "flex-grow",
78 | "flex-shrink",
79 | "flex-basis",
80 | "flex-direction",
81 | "order",
82 | "flex-order",
83 | "flex-wrap",
84 | "flex-flow",
85 | "justify-content",
86 | "align-self",
87 | "align-items",
88 | "align-content",
89 | "flex-pack",
90 | "flex-align",
91 |
92 | "box-sizing",
93 | "vertical-align",
94 |
95 | "size",
96 | "width",
97 | "height",
98 | "max-width",
99 | "min-width",
100 | "max-height",
101 | "min-height",
102 |
103 | "overflow",
104 | "overflow-x",
105 | "overflow-y",
106 |
107 | "float",
108 | "clear",
109 |
110 | "visibility",
111 | "opacity",
112 |
113 | "margin",
114 | "margin-top",
115 | "margin-right",
116 | "margin-bottom",
117 | "margin-left",
118 | "padding",
119 | "padding-top",
120 | "padding-right",
121 | "padding-bottom",
122 | "padding-left"
123 | ],
124 |
125 | [
126 | "font",
127 | "font-family",
128 | "font-size",
129 | "font-weight",
130 | "font-style",
131 | "font-variant",
132 | "font-size-adjust",
133 | "font-stretch",
134 |
135 | "line-height",
136 | "letter-spacing",
137 |
138 | "text-align",
139 | "text-align-last",
140 | "text-decoration",
141 | "text-emphasis",
142 | "text-emphasis-position",
143 | "text-emphasis-style",
144 | "text-emphasis-color",
145 | "text-indent",
146 | "text-justify",
147 | "text-outline",
148 | "text-transform",
149 | "text-wrap",
150 | "text-overflow",
151 | "text-overflow-ellipsis",
152 | "text-overflow-mode",
153 |
154 | "word-spacing",
155 | "word-wrap",
156 | "word-break",
157 | "tab-size",
158 | "hyphens"
159 | ],
160 |
161 | [
162 | "pointer-events",
163 |
164 | "border",
165 | "border-spacing",
166 | "border-collapse",
167 | "border-width",
168 | "border-style",
169 | "border-color",
170 | "border-top",
171 | "border-top-width",
172 | "border-top-style",
173 | "border-top-color",
174 | "border-right",
175 | "border-right-width",
176 | "border-right-style",
177 | "border-right-color",
178 | "border-bottom",
179 | "border-bottom-width",
180 | "border-bottom-style",
181 | "border-bottom-color",
182 | "border-left",
183 | "border-left-width",
184 | "border-left-style",
185 | "border-left-color",
186 | "border-radius",
187 | "border-top-left-radius",
188 | "border-top-right-radius",
189 | "border-bottom-right-radius",
190 | "border-bottom-left-radius",
191 | "border-image",
192 | "border-image-source",
193 | "border-image-slice",
194 | "border-image-width",
195 | "border-image-outset",
196 | "border-image-repeat",
197 | "border-top-image",
198 | "border-right-image",
199 | "border-bottom-image",
200 | "border-left-image",
201 | "border-corner-image",
202 | "border-top-left-image",
203 | "border-top-right-image",
204 | "border-bottom-right-image",
205 | "border-bottom-left-image",
206 |
207 | "color",
208 |
209 | "background",
210 | "filter",
211 | "background-color",
212 | "background-image",
213 | "background-attachment",
214 | "background-position",
215 | "background-position-x",
216 | "background-position-y",
217 | "background-clip",
218 | "background-origin",
219 | "background-size",
220 | "background-repeat",
221 |
222 | "clip",
223 | "list-style",
224 |
225 | "outline",
226 | "outline-width",
227 | "outline-style",
228 | "outline-color",
229 | "outline-offset",
230 |
231 | "cursor",
232 | "box-shadow",
233 | "text-shadow",
234 | "table-layout",
235 | "backface-visibility",
236 | "will-change",
237 | "transition",
238 | "transform",
239 | "animation"
240 | ]
241 | ]
242 | }
243 | }
244 |
--------------------------------------------------------------------------------
/tests/rules/colorTest.ts:
--------------------------------------------------------------------------------
1 | import { Color } from '../../src/rules/index';
2 | import { expect } from 'chai';
3 | import { parseAndRun } from '../staff/bootstrap';
4 |
5 | describe('Color test', () => {
6 | describe('Boolean enable state', () => {
7 | it('Should work like default options', () => {
8 | const rule = new Color({
9 | conf: 'always'
10 | });
11 |
12 | parseAndRun('.tab\n\tcolor: #ccc\n\tbackground-color #fff', rule);
13 |
14 | expect(rule.errors.length).to.be.equal(2);
15 | });
16 | });
17 | describe('Need uppercase notation', () => {
18 | it('Should check the AST has RGB node with wrong color notation', () => {
19 | const rule = new Color({
20 | conf: 'uppercase'
21 | });
22 |
23 | parseAndRun('.tab\n\tcolor: #ccc\n\tbackground-color #fff', rule);
24 |
25 | expect(rule.errors.length).to.be.equal(2);
26 | });
27 | it('Should check the AST has RGB node with right color notation', () => {
28 | const rule = new Color({
29 | conf: 'uppercase',
30 | allowShortcut: true
31 | });
32 |
33 | parseAndRun('.tab\n\tcolor: #CCC', rule);
34 |
35 | expect(rule.errors.length).to.be.equal(0);
36 | });
37 | });
38 | describe('Need lowercase notation', () => {
39 | it('Should check the AST has RGB node with right color notation', () => {
40 | const rule = new Color({
41 | conf: 'lowercase',
42 | allowShortcut: true
43 | });
44 |
45 | parseAndRun('.tab\n\tcolor: #ccc\n\tbackground-color #fff', rule);
46 |
47 | expect(rule.errors.length).to.be.equal(0);
48 | });
49 |
50 | it('Should check the AST has RGB node with wrong color notation', () => {
51 | const rule = new Color({
52 | conf: 'lowercase'
53 | });
54 |
55 | parseAndRun('.tab\n\tcolor: #CCC', rule);
56 |
57 | expect(rule.errors.length).to.be.equal(1);
58 | });
59 | });
60 | describe('RGB notation', () => {
61 | describe('Deny rgb notation', () => {
62 | it('Should find error in RGB node with rgb or rgba color notation', () => {
63 | const rule = new Color({
64 | conf: 'uppercase',
65 | denyRGB: true,
66 | allowShortcut: true
67 | });
68 |
69 | parseAndRun(
70 | '.tab\n\tcolor: rgba(127, 127, 127, 0.6)\n\tbackground-color rgb(0, 0, 0)\n\tborder-color rgba(#CCC, 1)',
71 | rule
72 | );
73 |
74 | expect(rule.errors.length).to.be.equal(2);
75 | });
76 | });
77 | describe('Allow rgb notation', () => {
78 | it('Should not find error in RGB node with rgb or rgba color notation', () => {
79 | const rule = new Color({
80 | conf: 'uppercase',
81 | denyRGB: false
82 | });
83 |
84 | parseAndRun('.tab\n\tcolor: rgba(127, 127, 127, 0.6)\n\tbackground-color rgb(0, 0, 0)', rule);
85 |
86 | expect(rule.errors.length).to.be.equal(0);
87 | });
88 | });
89 | });
90 | describe('Only in variable', () => {
91 | it('Should check RGB node only in variable', () => {
92 | const rule = new Color({
93 | conf: 'uppercase',
94 | allowOnlyInVar: true,
95 | allowShortcut: true
96 | });
97 |
98 | parseAndRun('$stop = #FFF\n' +
99 | '$p = {\n' +
100 | '\tcolor: #FFF\n' +
101 | '}\n' +
102 | '.tab\n' +
103 | '\tcolor: #CCC\n' +
104 | '\tcolor: $p.color\n', rule);
105 |
106 | expect(rule.errors.length).to.be.equal(1);
107 | });
108 | describe('Without error', () => {
109 | it('Should check RGB node only in variable', () => {
110 | const rule = new Color({
111 | conf: 'uppercase',
112 | allowOnlyInVar: true,
113 | allowShortcut: true
114 | });
115 |
116 | parseAndRun('$stop = #FFF\n' +
117 | '$p = {\n' +
118 | '\tcolor: #FFF\n' +
119 | '}\n' +
120 | '.tab\n' +
121 | '\tcolor $p.color\n' +
122 | '\tbackground-color $stop', rule);
123 |
124 | expect(rule.errors.length).to.be.equal(0);
125 | });
126 | });
127 | describe('Disable option rule', () => {
128 | it('Should not check RGB node only in variable', () => {
129 | const rule = new Color({
130 | conf: 'uppercase',
131 | allowOnlyInVar: false,
132 | allowShortcut: true
133 | });
134 |
135 | parseAndRun('$stop = #FFF\n' +
136 | '$p = {\n' +
137 | '\tcolor: #FFF\n' +
138 | '}\n' +
139 | '.tab\n' +
140 | '\tcolor: #CCC\n' +
141 | '\tcolor: $p.color\n', rule);
142 |
143 | expect(rule.errors.length).to.be.equal(0);
144 | });
145 | });
146 | });
147 | describe('Shortcut', () => {
148 | describe('Allow shortcut', () => {
149 | describe('Wrong value', () => {
150 | it('Should throw the error if content can have shortcut color notation', () => {
151 | const rule = new Color({
152 | conf: 'lowercase',
153 | allowShortcut: true
154 | });
155 |
156 | parseAndRun('.tab\n\tcolor: #cccccc\n\tbackground-color #ffffff', rule);
157 |
158 | expect(rule.errors.length).to.be.equal(2);
159 | });
160 | });
161 | describe('Right value', () => {
162 | it('Should not throw the error if content has shortcut color notation', () => {
163 | const rule = new Color({
164 | conf: 'lowercase',
165 | allowShortcut: true
166 | });
167 |
168 | parseAndRun('.tab\n\tcolor: #ccc\n\tbackground-color #fffffd', rule);
169 |
170 | expect(rule.errors.length).to.be.equal(0);
171 | });
172 | });
173 | });
174 | describe('Deny shortcut', () => {
175 | describe('Wrong value', () => {
176 | it('Should throw the error if content have shortcut color notation', () => {
177 | const rule = new Color({
178 | conf: 'lowercase',
179 | allowShortcut: false
180 | });
181 |
182 | parseAndRun('.tab\n\tcolor: #ccc\n\tbackground-color #fff', rule);
183 |
184 | expect(rule.errors.length).to.be.equal(2);
185 | });
186 | });
187 | describe('Right value', () => {
188 | it('Should not throw the error if content have not shortcut color notation', () => {
189 | const rule = new Color({
190 | conf: 'lowercase',
191 | allowShortcut: false
192 | });
193 |
194 | parseAndRun('.tab\n\tcolor: #cccccc\n\tbackground-color #fffffd', rule);
195 |
196 | expect(rule.errors.length).to.be.equal(0);
197 | });
198 | });
199 | });
200 | });
201 | });
202 |
--------------------------------------------------------------------------------
/tests/rules/sortOrderTest.ts:
--------------------------------------------------------------------------------
1 | import { SortOrder } from '../../src/rules/index';
2 | import { parseAndRun } from '../staff/bootstrap';
3 | import { expect } from 'chai';
4 | import data = require('../../src/defaultRules.json');
5 | import { Linter } from '../../src/linter';
6 | import * as path from 'path';
7 |
8 | const content = '.tab\n' +
9 | '\tcolor #CCC\n' +
10 | '\tmargin 10px\n' +
11 | '\tbackground-color $p.color\n' +
12 | '';
13 |
14 | describe('Test order rule', () => {
15 | describe('Disable order rule', () => {
16 | it('should not check properties order', () => {
17 | const linter = new Linter({
18 | rules: {
19 | sortOrder: false
20 | },
21 | grep: 'sortOrder',
22 | reporter: 'silent'
23 | });
24 |
25 | linter.lint('./test.styl', content);
26 | linter.display(false);
27 |
28 | const response = linter.reporter.response;
29 |
30 | expect(response.passed).to.be.true;
31 | });
32 |
33 | describe('Use config', () => {
34 | it('should not check properties order', () => {
35 | const linter = new Linter({
36 | config: path.join(__dirname, '../staff/disable-sort-order-rule.json'),
37 | grep: 'sortOrder',
38 | reporter: 'silent'
39 | });
40 |
41 | linter.lint('./test.styl', content);
42 | linter.display(false);
43 |
44 | const response = linter.reporter.response;
45 |
46 | expect(response.passed).to.be.true;
47 | });
48 | });
49 | });
50 |
51 | describe('Alphabetical order', () => {
52 | it('should check properties in alphabetical order', () => {
53 | const rule = new SortOrder({
54 | conf: 'alphabetical'
55 | });
56 |
57 | parseAndRun(content, rule);
58 |
59 | expect(rule.errors.length).to.be.equal(1);
60 | });
61 | });
62 |
63 | describe('Custom order', () => {
64 | describe('Matched order', () => {
65 | it('should check properties sorted in custom order', () => {
66 | const rule = new SortOrder({
67 | conf: 'grouped',
68 | order: [
69 | 'color',
70 | 'margin',
71 | 'background-color'
72 | ]
73 | });
74 |
75 | parseAndRun(content, rule);
76 |
77 | expect(rule.errors.length).to.be.equal(0);
78 | });
79 | });
80 |
81 | describe('Not matched order', () => {
82 | it('should check properties sorted in custom order', () => {
83 | const rule = new SortOrder({
84 | conf: 'grouped',
85 | order: [
86 | 'margin',
87 | 'background-color',
88 | 'color'
89 | ]
90 | });
91 |
92 | parseAndRun(content, rule);
93 |
94 | expect(rule.errors.length).to.be.equal(1);
95 | });
96 | });
97 | });
98 |
99 | describe('Grouped order', () => {
100 | it('should check properties order by list positions', () => {
101 | const rule = new SortOrder({
102 | conf: 'grouped',
103 | startGroupChecking: 1,
104 | order: [
105 | [
106 | 'position',
107 | 'right',
108 | 'left',
109 | 'flexbox'
110 | ],
111 | ['font-size'],
112 | [
113 | 'color',
114 | 'background-color'
115 | ]
116 | ]
117 | });
118 |
119 | parseAndRun('.tab\n' +
120 | '\tposition absolute\n' +
121 | '\tright 10px\n' +
122 | '\tleft 10px\n' +
123 | '\tflexbox()\n' +
124 | '\tfont-size 10px\n' +
125 | '\tcolor #CCC\n' +
126 | '\tbackground-color $p.color\n' +
127 | '', rule);
128 |
129 | expect(rule.errors.length).to.be.equal(2);
130 | });
131 |
132 | describe('Set big startGroupChecking', () => {
133 | it('should not check properties in alphabetical order', () => {
134 | const rule = new SortOrder({
135 | conf: 'grouped',
136 | startGroupChecking: 7,
137 | order: [
138 | [
139 | 'position',
140 | 'right',
141 | 'left'
142 | ],
143 | ['font-size'],
144 | [
145 | 'color',
146 | 'background-color'
147 | ]
148 | ]
149 | });
150 |
151 | parseAndRun('.tab\n' +
152 | '\tposition absolute\n' +
153 | '\tright 10px\n' +
154 | '\tleft 10px\n' +
155 | '\tfont-size 10px\n' +
156 | '\tcolor #CCC\n' +
157 | '\tbackground-color $p.color\n' +
158 | '', rule);
159 |
160 | expect(rule.errors.length).to.be.equal(0);
161 | });
162 | });
163 |
164 | describe('Different register', () => {
165 | it('should check the properties has some order', () => {
166 | const rule = new SortOrder({
167 | conf: 'grouped',
168 | startGroupChecking: 17,
169 | order: [
170 | [
171 | 'position',
172 | 'right',
173 | 'useGPU',
174 | 'usegRu',
175 | 'left'
176 | ],
177 | ['font-size'],
178 | [
179 | 'color',
180 | 'background-color'
181 | ]
182 | ]
183 | });
184 |
185 | parseAndRun('.tab\n' +
186 | '\tposition absolute\n' +
187 | '\tright 10px\n' +
188 | '\tuseGPU();\n' +
189 | '\tuseGRU();\n' +
190 | '\tleft 10px\n' +
191 | '\tfont-size 10px\n' +
192 | '\tcolor #CCC\n' +
193 | '\tbackground-color $p.color\n' +
194 | '', rule);
195 |
196 | expect(rule.errors.length).to.be.equal(0);
197 | });
198 | });
199 |
200 | describe('Check default options rule', () => {
201 | it('Should check by default options', () => {
202 | const rule = new SortOrder({
203 | conf: 'grouped',
204 | startGroupChecking: 6,
205 | order: (data).sortOrder.order
206 | });
207 |
208 | parseAndRun('&__item3-title\n' +
209 | '\tabsolute top basis(3) right basis(3)\n' +
210 | '\tz-index 100\n' +
211 | '\n' +
212 | '\tdisplay flex\n' +
213 | '\tflex-direction column\n' +
214 | '\tjustify-content center\n' +
215 | '\talign-items center\n' +
216 | '\tsize 12px 0\n' +
217 | '\twidth basis(40)\n' +
218 | '\tpadding basis(4) 0\n' +
219 | '\n' +
220 | '\tfont Roboto\n' +
221 | '\tfont-aboto Bold\n' +
222 | '\tfont-roboto Bold\n' +
223 | '\tfont-toboto Bold\n' +
224 | '\tfont-size 12px\n' +
225 | '\n' +
226 | '\tborder-radius 7px\n' +
227 | '\tbackground-color $p.dialogBackground' +
228 | '\n' +
229 | '\tdiv&\n' +
230 | '\t\tcursor default\n' +
231 | '\n' +
232 | '\ta&\n' +
233 | '\t\tuser-select none\n' +
234 | '\t\t&::before\n' +
235 | '\t\t\tabsolute left top\n' +
236 | '\n' +
237 | '\t\t\tcontent ""\n' +
238 | '\t\t\tdisplay block\n' +
239 | '\t\t\twidth 100%\n' +
240 | '\t\t\theight basis()\n' +
241 | '\n' +
242 | '\t\t\tbackground-image $p.shadowBgColor\n' +
243 | '\t&_selected_true\n' +
244 | '\t\tpadding-left $p.paddingLeft' +
245 | '', rule);
246 |
247 | expect(rule.errors.length).to.be.equal(0);
248 | });
249 | });
250 | });
251 | });
252 |
--------------------------------------------------------------------------------
/tests/rules/depthControlTest.ts:
--------------------------------------------------------------------------------
1 | import { DepthControl } from '../../src/rules/index';
2 | import { expect } from 'chai';
3 | import { parseAndRun } from '../staff/bootstrap';
4 |
5 | describe('Depth control test', () => {
6 | describe('Right content', () => {
7 | it('Should check all line has normal indent by previous', () => {
8 | const rule = new DepthControl({
9 | conf: 'always',
10 | indentPref: 'tab'
11 | });
12 |
13 | parseAndRun(
14 | '$p = {\n' +
15 | '\ta: #ccc,\n' +
16 | '\tb: #ddd\n' +
17 | '}\n' +
18 | '.test\n' +
19 | '\tmax-height red;\n' +
20 | '\n' +
21 | '\tborder black',
22 | rule
23 | );
24 |
25 | expect(rule.errors.length).to.be.equal(0);
26 | });
27 | describe('Selector after nested selector', () => {
28 | it('Should not throw error', () => {
29 | const rule = new DepthControl({
30 | conf: 'always',
31 | indentPref: 'tab'
32 | });
33 |
34 | parseAndRun(
35 | '.test\n' +
36 | '\t&__offer\n' +
37 | '\t\tdisplay block\n' +
38 | '\n' +
39 | '\t\t&:last-child\n' +
40 | '\t\t\tbackground-position top center\n' +
41 | '\n' +
42 | '\t&__rtb-offers\n' +
43 | '\t\tdisplay block\n',
44 | rule
45 | );
46 |
47 | expect(rule.errors.length).to.be.equal(0);
48 | });
49 | });
50 | describe('Property after @media', () => {
51 | describe('Property has indent equal @media\'s indent + 1', () => {
52 | it('Should not show error', () => {
53 | const rule = new DepthControl({
54 | conf: 'always'
55 | });
56 |
57 | parseAndRun(
58 | '.test\n' +
59 | '\tmax-height red;\n' +
60 | '\t@media screen and (max-width 600px) \n' +
61 | '\t\tmax-height red;\n' +
62 | '\t\tborder black',
63 | rule
64 | );
65 |
66 | expect(rule.errors.length).to.be.equal(0);
67 | });
68 | });
69 | describe('Property has indent not equal @media\'s indent + 1', () => {
70 | it('Should show error', () => {
71 | const rule = new DepthControl({
72 | conf: 'always'
73 | });
74 |
75 | parseAndRun(
76 | '.test\n' +
77 | '\tmax-height red;\n' +
78 | '\t@media screen and (max-width 600px) \n' +
79 | '\t\t\tborder black',
80 | rule
81 | );
82 |
83 | expect(rule.errors.length).to.be.equal(1);
84 | });
85 | });
86 | });
87 | });
88 | describe('Wrong content', () => {
89 | it('Should check all line has normal indent by previous', () => {
90 | const rule = new DepthControl({
91 | conf: 'always',
92 | indentPref: 'tab'
93 | });
94 |
95 | parseAndRun(
96 | '$p = {\n' +
97 | '\ta: #ccc,\n' +
98 | '\t\tb: #ddd\n' +
99 | '}\n' +
100 | '.test\n' +
101 | '\tmax-height red\n' +
102 | '\n' +
103 | '\t\t\tmax-height red;\n' +
104 | '\tborder black',
105 | rule
106 | );
107 |
108 | expect(rule.errors.length).to.be.equal(2);
109 | });
110 |
111 | describe('Selector after selector', () => {
112 | describe('In one line', () => {
113 | it('Should not show error', () => {
114 | const rule = new DepthControl({
115 | conf: 'always'
116 | });
117 |
118 | parseAndRun(
119 | '.test, .test2\n' +
120 | '\tmax-height red;\n'
121 | , rule
122 | );
123 |
124 | expect(rule.errors.length).to.be.equal(0);
125 | });
126 | });
127 | describe('On different lines', () => {
128 | it('Should show error', () => {
129 | const rule = new DepthControl({
130 | conf: 'always'
131 | });
132 |
133 | parseAndRun(
134 | '.test,\n' +
135 | '\t\t.test2\n' +
136 | '\tmax-height red;\n'
137 | , rule
138 | );
139 |
140 | // Property indent wrong too because it's parents .test2 and .test
141 | expect(rule.errors.length).to.be.equal(2);
142 | });
143 | });
144 | });
145 |
146 | describe('Use spaces', () => {
147 | it('Should check all line has normal indent by previous', () => {
148 | const rule = new DepthControl({
149 | conf: 'always',
150 | indentPref: 4 // TODO does not work fo 2 - because of lexer.js remove some spaces
151 | });
152 |
153 | parseAndRun(
154 | '$p = {\n' +
155 | ' a: #ccc\n' +
156 | ' b: #ddd\n' +
157 | '}\n' +
158 | '.test\n' +
159 | ' max-height red;\n' +
160 | '\n' +
161 | ' max-height red;\n' +
162 | ' border black',
163 | rule
164 | );
165 |
166 | expect(rule.errors.length).to.be.equal(0);
167 | });
168 | });
169 | });
170 | describe('Depth control in the hash', () => {
171 | it('Should check depth in hash', () => {
172 | const rule = new DepthControl({
173 | conf: 'always'
174 | });
175 |
176 | parseAndRun(
177 | '$p = {\n' +
178 | '\ta: #ccc\n' +
179 | '\t\tb: #ddd\n' +
180 | '}\n' +
181 | '.test\n' +
182 | '\tcolor red\n'
183 | ,
184 | rule
185 | );
186 |
187 | expect(rule.errors.length).to.be.equal(1);
188 | });
189 |
190 | describe('hash in hash', () => {
191 | it('Should check depth', () => {
192 | const rule = new DepthControl({
193 | conf: 'always'
194 | });
195 |
196 | parseAndRun(
197 | '$p = {\n' +
198 | '\ta: #ccc\n' +
199 | '\tb: {\n' +
200 | '\t\tc: #ddd\n' +
201 | '\t\td: #fff\n' +
202 | '\t}\n' +
203 | '}\n' +
204 | '.test\n' +
205 | '\tcolor red\n'
206 | ,
207 | rule
208 | );
209 |
210 | expect(rule.errors.length).to.be.equal(0);
211 | });
212 | describe('Wrong depth', () => {
213 | it('Should check depth', () => {
214 | const rule = new DepthControl({
215 | conf: 'always'
216 | });
217 |
218 | parseAndRun(
219 | '$p = {\n' +
220 | '\ta: #ccc\n' +
221 | '\tb: {\n' +
222 | '\t\tc: #ddd\n' +
223 | '\td: #fff\n' +
224 | '\t}\n' +
225 | '}\n' +
226 | '.test\n' +
227 | '\tcolor red\n'
228 | ,
229 | rule
230 | );
231 |
232 | expect(rule.errors.length).to.be.equal(1);
233 | });
234 | });
235 | });
236 | });
237 | describe('IF ELSE statement', () => {
238 | describe('Right depth', () => {
239 | it('Should not show error', () => {
240 | const rule = new DepthControl({
241 | conf: 'always'
242 | });
243 |
244 | parseAndRun(
245 | '$p = {\n' +
246 | '\tshow: true\n' +
247 | '}\n' +
248 | '.test\n' +
249 | '\tdisplay none\n' +
250 | '\tif $p.show\n' +
251 | '\t\tdisplay block\n' +
252 | ''
253 | ,
254 | rule
255 | );
256 |
257 | expect(rule.errors.length).to.be.equal(0);
258 | });
259 | });
260 | describe('Wrong depth', () => {
261 | it('Should show error', () => {
262 | const rule = new DepthControl({
263 | conf: 'always'
264 | });
265 |
266 | parseAndRun(
267 | '$p = {\n' +
268 | '\tshow: true\n' +
269 | '}\n' +
270 | '.test\n' +
271 | '\tdisplay none\n' +
272 | '\tif $p.show\n' +
273 | '\t\t\tdisplay block\n' +
274 | ''
275 | ,
276 | rule
277 | );
278 |
279 | expect(rule.errors.length).to.be.equal(1);
280 | });
281 | });
282 | });
283 |
284 | describe('Keyframes', () => {
285 | describe('Right depth', () => {
286 | it('Should not show error', () => {
287 | const rule = new DepthControl({
288 | conf: 'always'
289 | });
290 |
291 | parseAndRun(
292 | '@keyframes spinner\n' +
293 | '\t0%\n' +
294 | '\t\ttransform rotate(0)\n' +
295 | '\t100%\n' +
296 | '\t\ttransform rotate(360deg)\n' +
297 | ''
298 | ,
299 | rule
300 | );
301 |
302 | expect(rule.errors.length).to.be.equal(0);
303 | });
304 | });
305 | describe('Wrong depth', () => {
306 | it('Should show error', () => {
307 | const rule = new DepthControl({
308 | conf: 'always'
309 | });
310 |
311 | parseAndRun(
312 | '@keyframes spinner\n' +
313 | '\t\t0%\n' +
314 | '\t\t\ttransform rotate(0)\n' +
315 | '\t100%\n' +
316 | '\t\ttransform rotate(360deg)\n' +
317 | ''
318 | ,
319 | rule
320 | );
321 |
322 | expect(rule.errors.length).to.be.equal(2);
323 | });
324 | });
325 | });
326 |
327 | describe('Inside mixin', () => {
328 | describe('Right depth', () => {
329 | it('Should not show error', () => {
330 | const rule = new DepthControl({
331 | conf: 'always'
332 | });
333 |
334 | parseAndRun(
335 | '.test\n' +
336 | '\tchildren-shadow()\n' +
337 | '\t\tposition relative\n' +
338 | ''
339 | ,
340 | rule
341 | );
342 |
343 | expect(rule.errors.length).to.be.equal(0);
344 | });
345 | });
346 | describe('Wrong depth', () => {
347 | it('Should show error', () => {
348 | const rule = new DepthControl({
349 | conf: 'always'
350 | });
351 |
352 | parseAndRun(
353 | '.test\n' +
354 | '\tchildren-shadow()\n' +
355 | '\t\t\tposition relative\n' +
356 | ''
357 | ,
358 | rule
359 | );
360 |
361 | expect(rule.errors.length).to.be.equal(1);
362 | });
363 | });
364 | });
365 | });
366 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "tslint:recommended",
4 | "tslint-config-prettier",
5 | "tslint-plugin-prettier"
6 | ],
7 | "exclude": [
8 | "node_modules/**/*"
9 | ],
10 | "rules": {
11 | "member-ordering": false,
12 | "adjacent-overload-signatures": true,
13 | "ban-comma-operator": false,
14 | "member-access": false,
15 | "no-any": false,
16 | "no-empty-interface": false,
17 | "no-import-side-effect": false,
18 | "no-inferrable-types": [
19 | true,
20 | "ignore-params",
21 | "ignore-properties"
22 | ],
23 | "no-internal-module": true,
24 | "no-magic-numbers": false,
25 | "no-namespace": false,
26 | "no-non-null-assertion": false,
27 | "no-parameter-reassignment": false,
28 | "no-reference": true,
29 | "no-unnecessary-type-assertion": false,
30 | "no-var-requires": false,
31 | "no-var-keyword": true,
32 | "only-arrow-functions": [
33 | true,
34 | "allow-declarations",
35 | "allow-named-functions"
36 | ],
37 | "prefer-for-of": false,
38 | "prefer-template": false,
39 | "promise-function-async": false,
40 | "typedef": [
41 | true,
42 | "call-signature",
43 | "parameter",
44 | "property-declaration",
45 | "member-variable-declaration"
46 | ],
47 | "typedef-whitespace": [
48 | true,
49 | {
50 | "call-signature": "nospace",
51 | "index-signature": "nospace",
52 | "parameter": "nospace",
53 | "property-declaration": "nospace",
54 | "variable-declaration": "nospace"
55 | },
56 | {
57 | "call-signature": "onespace",
58 | "index-signature": "onespace",
59 | "parameter": "onespace",
60 | "property-declaration": "onespace",
61 | "variable-declaration": "onespace"
62 | }
63 | ],
64 | "unified-signatures": true,
65 | "await-promise": false,
66 | "curly": true,
67 | "forin": false,
68 | "label-position": true,
69 | "no-arg": true,
70 | "no-bitwise": true,
71 | "no-conditional-assignment": true,
72 | "no-console": false,
73 | "no-construct": true,
74 | "no-debugger": false,
75 | "no-duplicate-super": true,
76 | "no-duplicate-switch-case": true,
77 | "no-duplicate-variable": true,
78 | "no-empty": [
79 | true,
80 | "allow-empty-catch"
81 | ],
82 | "no-eval": true,
83 | "no-floating-promises": false,
84 | "no-for-in-array": true,
85 | "no-implicit-dependencies": false,
86 | "no-inferred-empty-object-type": false,
87 | "no-invalid-template-strings": true,
88 | "no-invalid-this": false,
89 | "no-misused-new": true,
90 | "no-null-keyword": false,
91 | "no-object-literal-type-assertion": false,
92 | "no-return-await": true,
93 | "no-shadowed-variable": false,
94 | "no-sparse-arrays": true,
95 | "no-string-literal": true,
96 | "no-string-throw": true,
97 | "no-submodule-imports": false,
98 | "no-switch-case-fall-through": true,
99 | "no-this-assignment": [
100 | true,
101 | {
102 | "allowed-names": [
103 | "^that"
104 | ],
105 | "allow-destructuring": true
106 | }
107 | ],
108 | "no-unbound-method": [
109 | true,
110 | "ignore-static"
111 | ],
112 | "no-unsafe-any": false,
113 | "no-unsafe-finally": true,
114 | "no-unused-expression": [
115 | false,
116 | "allow-fast-null-checks",
117 | "allow-new"
118 | ],
119 | "no-unused-variable": [
120 | true,
121 | {
122 | "ignore-pattern": "^_",
123 | "check-parameters": true
124 | }
125 | ],
126 | "no-use-before-declare": true,
127 | "no-void-expression": false,
128 | "prefer-conditional-expression": true,
129 | "prefer-object-spread": true,
130 | "radix": true,
131 | "restrict-plus-operands": true,
132 | "strict-boolean-expressions": false,
133 | "strict-type-predicates": true,
134 | "switch-default": false,
135 | "triple-equals": [
136 | true,
137 | "allow-null-check"
138 | ],
139 | "typeof-compare": true,
140 | "use-default-type-parameter": false,
141 | "use-isnan": true,
142 | "cyclomatic-complexity": [
143 | true,
144 | 50
145 | ],
146 | "deprecation": true,
147 | "eofline": true,
148 | "indent": [
149 | true,
150 | "tabs"
151 | ],
152 | "linebreak-style": [
153 | true,
154 | "LF"
155 | ],
156 | "max-classes-per-file": [
157 | true,
158 | 5,
159 | "exclude-class-expressions"
160 | ],
161 | "max-file-line-count": [
162 | true,
163 | 1700
164 | ],
165 | "max-line-length": [
166 | true,
167 | 120
168 | ],
169 | "no-default-export": false,
170 | "no-duplicate-imports": true,
171 | "no-mergeable-namespace": true,
172 | "no-require-imports": false,
173 | "object-literal-sort-keys": false,
174 | "prefer-const": true,
175 | "trailing-comma": [
176 | true,
177 | {
178 | "multiline": "never",
179 | "singleline": "never"
180 | }
181 | ],
182 | "align": [
183 | true,
184 | "elements",
185 | "members",
186 | "parameters",
187 | "statements"
188 | ],
189 | "array-type": false,
190 | "arrow-parens": true,
191 | "arrow-return-shorthand": [
192 | true,
193 | "multiline"
194 | ],
195 | "binary-expression-operand-order": true,
196 | "callable-types": false,
197 | "class-name": false,
198 | "comment-format": [
199 | false,
200 | "check-space",
201 | "check-uppercase"
202 | ],
203 | "completed-docs": [
204 | true,
205 | {
206 | "functions": {
207 | "visibilities": [
208 | "exported"
209 | ]
210 | },
211 | "properties": {
212 | "locations": "all",
213 | "privacies": [
214 | "public",
215 | "protected"
216 | ]
217 | },
218 | "methods": {
219 | "locations": "all",
220 | "privacies": [
221 | "public",
222 | "protected"
223 | ]
224 | },
225 | "tags": {
226 | "content": {
227 | "see": [
228 | "#.*"
229 | ]
230 | },
231 | "exists": [
232 | "inheritdoc"
233 | ]
234 | }
235 | }
236 | ],
237 | "encoding": true,
238 | "file-header": false,
239 | "import-spacing": true,
240 | "interface-name": false,
241 | "interface-over-type-literal": true,
242 | "jsdoc-format": true,
243 | "match-default-export-name": false,
244 | "newline-before-return": false,
245 | "new-parens": true,
246 | "no-angle-bracket-type-assertion": false,
247 | "no-boolean-literal-compare": false,
248 | "no-consecutive-blank-lines": [
249 | true,
250 | 1
251 | ],
252 | "no-irregular-whitespace": true,
253 | "no-parameter-properties": false,
254 | "no-redundant-jsdoc": false,
255 | "no-reference-import": true,
256 | "no-trailing-whitespace": true,
257 | "no-unnecessary-callback-wrapper": true,
258 | "no-unnecessary-initializer": true,
259 | "no-unnecessary-qualifier": true,
260 | "number-literal-format": true,
261 | "object-literal-key-quotes": [
262 | true,
263 | "consistent-as-needed"
264 | ],
265 | "object-literal-shorthand": true,
266 | "one-line": [
267 | true,
268 | "check-catch",
269 | "check-finally",
270 | "check-else",
271 | "check-open-brace",
272 | "check-whitespace"
273 | ],
274 | "one-variable-per-declaration": false,
275 | "ordered-imports": false,
276 | "prefer-function-over-method": false,
277 | "prefer-method-signature": true,
278 | "prefer-switch": [
279 | true,
280 | {
281 | "min-cases": 3
282 | }
283 | ],
284 | "quotemark": [
285 | true,
286 | "single",
287 | "avoid-escape",
288 | "avoid-template"
289 | ],
290 | "return-undefined": true,
291 | "semicolon": [
292 | true,
293 | "always",
294 | "ignore-interfaces",
295 | "strict-bound-class-methods"
296 | ],
297 | "space-before-function-paren": [
298 | true,
299 | {
300 | "anonymous": "always",
301 | "asyncArrow": "always"
302 | }
303 | ],
304 | "space-within-parens": [
305 | true,
306 | 0
307 | ],
308 | "switch-final-break": true,
309 | "type-literal-delimiter": true,
310 | "variable-name": [
311 | true,
312 | "check-format",
313 | "ban-keywords",
314 | "allow-leading-underscore",
315 | "allow-pascal-case"
316 | ],
317 | "whitespace": [
318 | true,
319 | "check-branch",
320 | "check-decl",
321 | "check-operator",
322 | "check-module",
323 | "check-separator",
324 | "check-rest-spread",
325 | "check-type",
326 | "check-type-operator",
327 | "check-preblock"
328 | ]
329 | }
330 | }
331 |
--------------------------------------------------------------------------------
/src/rules/sortOrder.ts:
--------------------------------------------------------------------------------
1 | import { Rule } from '../core/rule';
2 | import { Block, Property, Value, Node } from '../core/ast/index';
3 | import { IState } from '../core/types/state';
4 | import { Line } from '../core/line';
5 | import { Content } from '../core/content';
6 | import { INode } from '../core/types/ast/node';
7 |
8 | interface IOrderState extends IState {
9 | order?: Array,
10 | startGroupChecking?: number
11 | }
12 |
13 | interface PropertyNameAndBound {
14 | name: string;
15 | startLine: number;
16 | endLine: number;
17 | }
18 |
19 | type Properties = PropertyNameAndBound[];
20 | type PropertiesGroups = Array;
21 |
22 | interface FixObject {
23 | first?: Property | Value | void;
24 | last?: Property | Value | void;
25 | toString(): string;
26 | fix: Array>;
27 | }
28 |
29 | /**
30 | * Rule for checking properties order. Can use alphabetical order or order from grouped array
31 | */
32 | export class SortOrder extends Rule {
33 | nodesFilter: string[] = ['block'];
34 |
35 | checkNode(node: Block, content: Content): void {
36 | const
37 | propertiesGroups: PropertiesGroups = [],
38 | propertyToLine: Dictionary = {};
39 |
40 | this.fillPropertiesNameAndLine(node, propertiesGroups, propertyToLine, content);
41 |
42 | // sort only 2 and more properties
43 | if (propertiesGroups.reduce((cnt, array) => {
44 | cnt += array.length;
45 | return cnt;
46 | }, 0) < 2) {
47 | return;
48 | }
49 |
50 | this.sort(propertiesGroups);
51 |
52 | propertiesGroups.forEach((properties, group) => {
53 | const
54 | hasOrderError = this.hasSortError(node, properties, group),
55 | fixObject: FixObject = this.getFixObject(node, properties, content, group);
56 |
57 | if (hasOrderError && fixObject.last && fixObject.first) {
58 | const
59 | lastLine = this.getLastLine(fixObject.last);
60 |
61 | this.msg(
62 | `Properties have wrong order - ${properties.map((item) => item.name).join(', ')}`,
63 | fixObject.first.lineno,
64 | 1,
65 | content.getLine(lastLine).line.length,
66 | fixObject, // We can change 'fix' array in checkSeparatorLines
67 | lastLine
68 | );
69 | }
70 |
71 | this.checkSeparatorLines(hasOrderError, properties, propertyToLine, fixObject);
72 | });
73 | }
74 |
75 | private forEachProperty(
76 | node: Node,
77 | groupId: number,
78 | callback: (child: Property | Value, indexInGroup: number) => void | boolean
79 | ): void | boolean {
80 | let child: INode, group = 0, indexInGroup = 0, result: void | boolean;
81 |
82 | for (let i = 0; i < node.nodes.length; i += 1) {
83 | child = node.nodes[i];
84 |
85 | if (child.line && child.line.isIgnored) {
86 | continue;
87 | }
88 |
89 | if (child instanceof Property || child instanceof Value) {
90 | if (group === groupId) {
91 | result = callback(child, indexInGroup);
92 |
93 | if (result !== undefined) {
94 | return result;
95 | }
96 |
97 | indexInGroup += 1;
98 | }
99 | } else {
100 | group += 1;
101 | indexInGroup = 0;
102 | }
103 | }
104 | }
105 |
106 | private getLastLine(child: Node): number {
107 | return (child.value && child.value instanceof Node) ? child.value.lineno : child.lineno;
108 | }
109 |
110 | private fillPropertiesNameAndLine(
111 | node: Block,
112 | properties: PropertiesGroups,
113 | propertyToLine: Dictionary,
114 | content: Content
115 | ): void {
116 | let group: PropertyNameAndBound[] = [];
117 |
118 | node.nodes.forEach((child) => {
119 | if (child.line && child.line.isIgnored) {
120 | return;
121 | }
122 |
123 | if (child instanceof Property || child instanceof Value) {
124 | const
125 | name = child.key.toString().toLowerCase();
126 |
127 | group.push(
128 | {
129 | name,
130 | startLine: child.lineno,
131 | endLine: this.getLastLine(child)
132 | }
133 | );
134 |
135 | propertyToLine[name] = content.getLine(child.lineno);
136 | } else {
137 | if (group.length) {
138 | properties.push(group);
139 | }
140 |
141 | group = [];
142 | }
143 | });
144 |
145 | if (group.length) {
146 | properties.push(group);
147 | }
148 | }
149 |
150 | private sort(properties: PropertiesGroups): void {
151 | if (this.state.conf === 'alphabetical') {
152 | this.sortAlphabetical(properties);
153 | } else {
154 | this.fillCacheOrder(this.state.order || []);
155 | this.sortByGroupedOrder(properties);
156 | }
157 | }
158 |
159 | private sortAlphabetical(properties: PropertiesGroups): void {
160 | properties
161 | .forEach((group) => group.sort((a, b) => {
162 | if (a.name === b.name) {
163 | return 0;
164 | }
165 |
166 | return a.name > b.name ? 1 : -1;
167 | }));
168 | }
169 |
170 | private fillCacheOrder(order: Array): void {
171 | if (!this.cache.order) {
172 | this.cache.keyToGroup = {};
173 |
174 | let groupIndex = 0;
175 |
176 | this.cache.order = order.reduce((sort, key) => {
177 | if (typeof key === 'string') {
178 | sort.push(key.toLowerCase());
179 | } else {
180 | sort.push.apply(sort, key.map((subkey) => subkey.toLowerCase()));
181 | key.forEach((subkey) => this.cache.keyToGroup[subkey.toLowerCase()] = groupIndex);
182 | groupIndex += 1;
183 | }
184 | return sort;
185 | }, []);
186 | }
187 | }
188 |
189 | private sortByGroupedOrder(properties: PropertiesGroups): void {
190 | properties
191 | .forEach((group) => group.sort((keyA, keyB) => {
192 | const
193 | values = >{
194 | keyA: keyA.name,
195 | keyB: keyB.name
196 | },
197 | index = >{
198 | keyA: this.cache.order.indexOf(keyA.name),
199 | keyB: this.cache.order.indexOf(keyB.name)
200 | },
201 | keys = Object.keys(index);
202 |
203 | for (const key of keys) {
204 | if (index[key] === -1) {
205 | const parts = values[key].split('-');
206 |
207 | if (parts.length > 1) {
208 | let l = parts.length - 1;
209 |
210 | while (l > 0 && index[key] === -1) {
211 | index[key] = this.cache.order.indexOf(parts.slice(0, l).join('-'));
212 |
213 | if (index[key] !== -1) {
214 | index[key] += 1;
215 | }
216 | l -= 1;
217 | }
218 | }
219 | }
220 |
221 | if (index[key] === -1) {
222 | return values.keyA > values.keyB ? 1 : -1;
223 | }
224 | }
225 |
226 | if (index.keyA === index.keyB) {
227 | return values.keyA > values.keyB ? 1 : -1;
228 | }
229 |
230 | return index.keyA - index.keyB;
231 | })
232 | );
233 | }
234 |
235 | private hasSortError(node: Node, properties: Properties, groupId: number): boolean {
236 | return this.forEachProperty(node, groupId, (child, index) => {
237 | const name = child.key.toString().toLowerCase();
238 |
239 | if (properties[index].name !== name) {
240 | return true;
241 | }
242 | }) || false;
243 | }
244 |
245 | /**
246 | * Returns fix object for fix some part of stylus file
247 | *
248 | * @param node
249 | * @param properties
250 | * @param content
251 | * @param groupId
252 | */
253 | private getFixObject(node: Node, properties: Properties, content: Content, groupId: number): FixObject {
254 | let
255 | index = 0,
256 | indexNoOrdered = 0,
257 | last: Property | Value | void = void (0),
258 | first: Property | Value | void = void (0);
259 | const
260 | fix: Array> = [];
261 |
262 | const
263 | partLines: Array> = [];
264 |
265 | this.forEachProperty(node, groupId, (child) => {
266 | const name = child.key.toString().toLowerCase();
267 |
268 | if (properties[index].name !== name) {
269 | if (!first) {
270 | first = child;
271 | }
272 |
273 | last = child;
274 |
275 | let
276 | start = properties[index].startLine;
277 |
278 | if (start !== properties[index].endLine) {
279 | const st = [];
280 |
281 | for (; start <= properties[index].endLine; start += 1) {
282 | st.push(content.getLine(start));
283 | }
284 |
285 | fix[indexNoOrdered] = st;
286 | } else {
287 | fix[indexNoOrdered] = content.getLine(start);
288 | }
289 | }
290 |
291 | if (first) {
292 | const end = this.getLastLine(child);
293 |
294 | let
295 | start = child.lineno;
296 |
297 | if (start !== end) {
298 | const st = [];
299 |
300 | for (; start <= end; start += 1) {
301 | st.push(content.getLine(start));
302 | }
303 |
304 | partLines[indexNoOrdered] = st;
305 | } else {
306 | partLines[indexNoOrdered] = content.getLine(start);
307 | }
308 |
309 | indexNoOrdered += 1;
310 | }
311 |
312 | index += 1;
313 | });
314 |
315 | for (let i = 0; i < fix.length; i += 1) {
316 | if (fix[i] === undefined) {
317 | fix[i] = partLines[i];
318 | }
319 | }
320 |
321 | const result = {
322 | first,
323 | last,
324 | fix,
325 | toString(): string {
326 | return result.fix
327 | .reduce>((array, line) => {
328 | if (Array.isArray(line)) {
329 | array.push(...line);
330 | } else {
331 | array.push(line);
332 | }
333 |
334 | return array;
335 | }, [])
336 | .map((line) => typeof line === 'string' ? line : line.line)
337 | .join('\n');
338 |
339 | }
340 | };
341 |
342 | return result;
343 | }
344 |
345 | private checkSeparatorLines(
346 | hasOrderError: boolean,
347 | properties: Properties,
348 | propertyToLine: Dictionary,
349 | fixObject: FixObject
350 | ): void {
351 | const
352 | startGroupChecking = this.state.startGroupChecking || 6;
353 |
354 | if (
355 | properties.length >= startGroupChecking &&
356 | this.state.conf === 'grouped'
357 | ) {
358 | let
359 | lastGroup: null | number | void = null;
360 |
361 | properties.forEach((property) => {
362 | const group = this.getGroupByName(property.name);
363 |
364 | if (group !== undefined && group !== lastGroup) {
365 | if (lastGroup !== null) {
366 | const line = propertyToLine[property.name];
367 |
368 | if (line) {
369 | if (!hasOrderError) {
370 | const prev = line.prev();
371 |
372 | if (prev && prev.line.trim().length !== 0) {
373 | this.msg('Need new line after group', prev.lineno, 1, prev.line.length, prev.line + '\n');
374 | }
375 | } else {
376 | this.addSeparateLineAfterLine(fixObject, line);
377 | }
378 | }
379 | }
380 |
381 | lastGroup = group;
382 | }
383 | });
384 | }
385 | }
386 |
387 | private getGroupByName(name: string): number | void {
388 | let
389 | group = this.cache.keyToGroup[name];
390 |
391 | if (group === undefined) {
392 | const parts = name.split('-');
393 |
394 | if (parts.length > 1) {
395 | let l = parts.length - 1;
396 |
397 | while (l > 0 && group === undefined) {
398 | group = this.cache.keyToGroup[parts.slice(0, l).join('-')];
399 |
400 | l -= 1;
401 | }
402 | }
403 | }
404 |
405 | return group;
406 | }
407 |
408 | private addSeparateLineAfterLine(fixObject: FixObject, line: Line): void {
409 | const
410 | index = fixObject.fix.indexOf(line) - 1,
411 | prev = fixObject.fix[index];
412 |
413 | if (index > 0 && prev && prev instanceof Line && prev.line && prev.line.trim().length) {
414 | fixObject.fix = [...fixObject.fix.slice(0, index + 1), '', ...fixObject.fix.slice(index + 1)];
415 | }
416 | }
417 | }
418 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Stylus Linter
2 | [](https://travis-ci.org/stylus/stlint)
3 | [](https://www.npmjs.org/package/stlint)
4 | [](https://npmcharts.com/compare/stlint?minimal=true)
5 |
6 | [](https://nodei.co/npm/stlint/)
7 |
8 | * [Issues](https://github.com/stylus/stlint/issues)
9 | * [Installation](#installation)
10 | * [Example cli Usage](#example-cli-usage)
11 | * [CLI](#cli)
12 | * [Non CLI Usage](#non-cli-usage)
13 | * [As Part of Your Workflow](#as-part-of-your-workflow)
14 | * [Ignore errors](#ignore-errors)
15 | * [Rules](#rules)
16 | * [License](#license)
17 |
18 | ## Installation
19 |
20 | As part of your project
21 | ```bash
22 | npm i stlint -D
23 | ```
24 |
25 | As a cli tool:
26 | ```bash
27 | npm install stlint -g
28 | ```
29 |
30 | ## Example cli Usage:
31 | `npx stlint` Run stlint on cwd
32 |
33 | `stlint` Run stlint on cwd as global
34 |
35 | `stlint path/to/filename.styl` Run stlint on a file
36 |
37 | `stlint path/to/dir --watch` Watch dir, run stlint on file change
38 |
39 | `stlint --help` Get list of commands
40 |
41 | `stlint --version` Get version number
42 |
43 | `stlint --config path/to/config/.configrc` Run stlint with custom config settings
44 |
45 | `stlint styl/ --watch -c path/to/config/.configrc` Watch dir, use custom config
46 |
47 | `stlint --command autocomplete --content #e --offset 0 --lineoffset 0` Get autocomplete suggestions for `#` string
48 |
49 | ## CLI
50 | `-h` or `--help` Display list of commands
51 |
52 | `-w` or `--watch` Watch file or directory and run lint on change
53 |
54 | `-c` or `--config` Pass in location of custom config file
55 |
56 | `-v` or `--version` Display current version
57 |
58 | `-g` or `--grep` Only run rules matching this string or regexp
59 |
60 | `-f` or `--fix` Try fix some issues
61 |
62 | `-i` or `--info` Show info about version and config (can be used with --grep for filter rules)
63 |
64 | `-r` or `--reporter` Reporter "raw", "json" or "silent"
65 |
66 | All another options from [config](#Config file)
67 |
68 | ## Non CLI Usage
69 | ```javascript
70 | const StylusLinter = require('stlint').StylusLinter;
71 | StylusLinter('path/to/stylus/', {
72 | watch: true
73 | });
74 | ```
75 | or check only one file or text
76 | ```javascript
77 | const Linter = require('stlint').Linter;
78 | const linter = new Linter({
79 | ...options
80 | });
81 |
82 | // if you want check content
83 | linter.lint('./test.styl', 'content');
84 |
85 | // if you want check file
86 | linter.lint('./test.styl');
87 |
88 | // and display errors.
89 | linter.display();
90 | ```
91 |
92 | ## Config file
93 | Create `.stlintrc` file in project root
94 | ```json
95 | {
96 | "reporter": "raw",
97 | "watch": false,
98 | "extends": ["stlint-v4fire", "./test/.myfileconfig.json"],
99 | "extraRules": "./my-rules/",
100 | "rules": {
101 | "color": false,
102 | "colons": ["always"],
103 | "depthControl": {
104 | "indentPref": 4
105 | }
106 | },
107 | "path": "./src",
108 | "excludes": ["node_modules/"],
109 | "stylusParserOptions": {},
110 | "reportOptions": {
111 | "columnSplitter": " | ",
112 | "maxWidth": 70,
113 | "minWidth": 70,
114 | "truncate": false
115 | }
116 | }
117 | ```
118 | ## As Part of Your Workflow
119 | Stlint integrations with IDEs are available.
120 |
121 | * [WebStorm / PhpStorm / IntelliJ IDEA](https://github.com/stylus/stlint-idea-plugin)
122 | * [VSCode](https://marketplace.visualstudio.com/items?itemName=xdan.stlint-vscode-plugin)
123 |
124 | ## Ignore errors
125 | sometimes you want to ignore the error for this there are two directives:
126 | * `@stlint-ignore` - ignores only one line after it
127 | * `@stlint-disable` `@stlint-enable` - ignore block (@stlint-enable is optional)
128 |
129 | For example, in the following code, some errors will be ignored.
130 | ```stylus
131 | $p = {
132 | a: #CCC
133 | // @stlint-ignore
134 | b: #ccc // need use uppercase notation will be ignored
135 | c: 10px
136 | }
137 | .test
138 | margin-top 20px
139 | // @stlint-disable
140 | padding-top 20px // need use mixin will be ignored
141 | color #ccc // need use uppercase notation and use variable will be ignored
142 | // @stlint-enable
143 | background-color #ddd
144 | ```
145 |
146 | Respectively, in order not to display errors of the entire file, it is enough to add an
147 | `@stlint-disable` directive to its beginning
148 |
149 | ```stylus
150 | // @stlint-disable - all errors below will be ignored
151 | $p = {
152 | a: #CCC
153 | b: #ccc
154 | c: 10px
155 | }
156 | .test
157 | margin-top 20px
158 | padding-top 20px
159 | color #ccc
160 | background-color #ddd
161 | ```
162 |
163 | ## Rules
164 |
165 |
166 | ### brackets
167 | Check for brackets
168 |
169 | **Default value**
170 | ```json
171 | [
172 | "never"
173 | ]
174 | ```
175 | ----
176 |
177 | ### colons
178 | Use/Do not use colons after property
179 |
180 | **Default value**
181 | ```json
182 | [
183 | "never"
184 | ]
185 | ```
186 | ----
187 |
188 | ### color
189 | Process all color values. Allow or deny use it not in variable and use uppercase or lowercase statements
190 | For example this code has error - because we use only color in `uppercase`
191 | ```stylus
192 | .test
193 | color #ccc
194 | ```
195 | If `allowOnlyInVar` === true code above also has error - no use color without variable
196 |
197 | Fixed code
198 | ```stylus
199 | $color = #CCC
200 | .test
201 | color $color
202 | ```
203 |
204 | **Default value**
205 | ```json
206 | {
207 | "conf": "uppercase",
208 | "enabled": true,
209 | "allowOnlyInVar": true,
210 | "allowShortcut": true,
211 | "denyRGB": true
212 | }
213 | ```
214 | ----
215 |
216 | ### commaInObject
217 | Allow or deny commas in object hash
218 |
219 | **Default value**
220 | ```json
221 | [
222 | "never"
223 | ]
224 | ```
225 | ----
226 |
227 | ### depthControl
228 | Control depth spaces or tab
229 |
230 | **Default value**
231 | ```json
232 | {
233 | "indentPref": "tab"
234 | }
235 | ```
236 | ----
237 |
238 | ### emptyLines
239 | Check if document has several empty lines
240 |
241 | **Default value**
242 | ```json
243 | true
244 | ```
245 | ----
246 |
247 | ### leadingZero
248 | Check for leading 0 on numbers ( 0.5 )
249 |
250 | **Default value**
251 | ```json
252 | [
253 | "always"
254 | ]
255 | ```
256 | ----
257 |
258 | ### mixedSpaces
259 | check for mixed spaces and tabs
260 |
261 | **Default value**
262 | ```json
263 | {
264 | "indentPref": "tab"
265 | }
266 | ```
267 | ----
268 |
269 | ### prefixVarsWithDollar
270 | Check that $ is used when declaring vars
271 |
272 | **Default value**
273 | ```json
274 | {
275 | "conf": "always",
276 | "prefix": "$",
277 | "allowConst": true
278 | }
279 | ```
280 | ----
281 |
282 | ### quotePref
283 | Check that quote style is consistent with config
284 |
285 | **Default value**
286 | ```json
287 | [
288 | "double"
289 | ]
290 | ```
291 | ----
292 |
293 | ### semicolons
294 | Check that selector properties are sorted accordingly
295 |
296 | **Default value**
297 | ```json
298 | [
299 | "never"
300 | ]
301 | ```
302 | ----
303 |
304 | ### sortOrder
305 | Rule for checking properties order. Can use alphabetical order or order from grouped array
306 |
307 | **Default value**
308 | ```json
309 | {
310 | "conf": "grouped",
311 | "startGroupChecking": 6,
312 | "order": [
313 | [
314 | "absolute",
315 | "position",
316 | "z-index",
317 | "top",
318 | "right",
319 | "bottom",
320 | "left"
321 | ],
322 | [
323 | "content",
324 | "display",
325 | "flexbox",
326 | "flex",
327 | "flex-grow",
328 | "flex-shrink",
329 | "flex-basis",
330 | "flex-direction",
331 | "order",
332 | "flex-order",
333 | "flex-wrap",
334 | "flex-flow",
335 | "justify-content",
336 | "align-self",
337 | "align-items",
338 | "align-content",
339 | "flex-pack",
340 | "flex-align",
341 | "box-sizing",
342 | "vertical-align",
343 | "size",
344 | "width",
345 | "height",
346 | "max-width",
347 | "min-width",
348 | "max-height",
349 | "min-height",
350 | "overflow",
351 | "overflow-x",
352 | "overflow-y",
353 | "float",
354 | "clear",
355 | "visibility",
356 | "opacity",
357 | "margin",
358 | "margin-top",
359 | "margin-right",
360 | "margin-bottom",
361 | "margin-left",
362 | "padding",
363 | "padding-top",
364 | "padding-right",
365 | "padding-bottom",
366 | "padding-left"
367 | ],
368 | [
369 | "font",
370 | "font-family",
371 | "font-size",
372 | "font-weight",
373 | "font-style",
374 | "font-variant",
375 | "font-size-adjust",
376 | "font-stretch",
377 | "line-height",
378 | "letter-spacing",
379 | "text-align",
380 | "text-align-last",
381 | "text-decoration",
382 | "text-emphasis",
383 | "text-emphasis-position",
384 | "text-emphasis-style",
385 | "text-emphasis-color",
386 | "text-indent",
387 | "text-justify",
388 | "text-outline",
389 | "text-transform",
390 | "text-wrap",
391 | "text-overflow",
392 | "text-overflow-ellipsis",
393 | "text-overflow-mode",
394 | "word-spacing",
395 | "word-wrap",
396 | "word-break",
397 | "tab-size",
398 | "hyphens"
399 | ],
400 | [
401 | "pointer-events",
402 | "border",
403 | "border-spacing",
404 | "border-collapse",
405 | "border-width",
406 | "border-style",
407 | "border-color",
408 | "border-top",
409 | "border-top-width",
410 | "border-top-style",
411 | "border-top-color",
412 | "border-right",
413 | "border-right-width",
414 | "border-right-style",
415 | "border-right-color",
416 | "border-bottom",
417 | "border-bottom-width",
418 | "border-bottom-style",
419 | "border-bottom-color",
420 | "border-left",
421 | "border-left-width",
422 | "border-left-style",
423 | "border-left-color",
424 | "border-radius",
425 | "border-top-left-radius",
426 | "border-top-right-radius",
427 | "border-bottom-right-radius",
428 | "border-bottom-left-radius",
429 | "border-image",
430 | "border-image-source",
431 | "border-image-slice",
432 | "border-image-width",
433 | "border-image-outset",
434 | "border-image-repeat",
435 | "border-top-image",
436 | "border-right-image",
437 | "border-bottom-image",
438 | "border-left-image",
439 | "border-corner-image",
440 | "border-top-left-image",
441 | "border-top-right-image",
442 | "border-bottom-right-image",
443 | "border-bottom-left-image",
444 | "color",
445 | "background",
446 | "filter",
447 | "background-color",
448 | "background-image",
449 | "background-attachment",
450 | "background-position",
451 | "background-position-x",
452 | "background-position-y",
453 | "background-clip",
454 | "background-origin",
455 | "background-size",
456 | "background-repeat",
457 | "clip",
458 | "list-style",
459 | "outline",
460 | "outline-width",
461 | "outline-style",
462 | "outline-color",
463 | "outline-offset",
464 | "cursor",
465 | "box-shadow",
466 | "text-shadow",
467 | "table-layout",
468 | "backface-visibility",
469 | "will-change",
470 | "transition",
471 | "transform",
472 | "animation"
473 | ]
474 | ]
475 | }
476 | ```
477 | ----
478 |
479 | ### useMixinInsteadUnit
480 | Allo or deny some mixin instead of unit statement
481 |
482 | **Default value**
483 | ```json
484 | {
485 | "conf": "always",
486 | "mixin": "basis",
487 | "unitType": "px",
488 | "allowOneUnit": false
489 | }
490 | ```
491 | ----
492 |
493 | ## Self rules
494 | You can create folder and use it for extra rules
495 | ```json
496 | {
497 | "extraRules": "/Users/v-chupurnov/WebstormProjects/test/rules/"
498 | }
499 | ```
500 | In this folder you can create native JavaScript files
501 | ```javascript
502 | const Rgb = require('stlint').ast.RGB;
503 |
504 | function TestRule() {
505 | nodesFilter = ['rgb']; // can be one of https://github.com/stylus/stlint/tree/master/src/core/ast
506 |
507 | /**
508 | * Check the AST nodes
509 | * @param node
510 | */
511 | this.checkNode = (node) => {
512 | if (node instanceof Rgb) {
513 | console.log(this.state.conf); // test111
514 | console.log(this.state.someExtraVariable); // 112
515 | // this.msg('Test error on test node', node.lineno, node.column, node.line.length);
516 | }
517 | };
518 |
519 | /**
520 | * Check every line
521 | * @param line
522 | */
523 | this.checkLine = (line) => {
524 | if (line.lineno === 1) {
525 | // this.msg('Test error on test line', line.lineno, 1, line.line.length);
526 | }
527 | };
528 | }
529 |
530 | module.exports.TestRule = TestRule;
531 | ```
532 | And you need add this rule in your config
533 | ```json
534 | {
535 | "extraRules": "/Users/v-chupurnov/WebstormProjects/test/rules/",
536 | "rules": {
537 | "testRule": {
538 | "conf": "test111",
539 | "someExtraVariable": 112,
540 | "enabled": true
541 | }
542 | }
543 | }
544 | ```
545 |
546 | ## License
547 |
548 | [The MIT License](https://raw.githubusercontent.com/stylus/stlint/master/LICENSE).
549 |
--------------------------------------------------------------------------------