├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── contributing.md ├── issue_template.md ├── pull_request_template.md └── workflows │ └── ci.yml ├── .gitignore ├── .husky └── pre-commit ├── .node-version ├── .npmrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example.png ├── gitattributes ├── index.js ├── jest.setup.js ├── lib └── css-selector-classes.js ├── package-lock.json ├── package.json └── test ├── default.test.js ├── edge-cases.test.js ├── invalid-config.test.js ├── namespace.test.js ├── namespaces.test.js ├── prefixes.test.js └── pseudo-classes.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig. Frontend. Default. Namics. 2 | # @see: http://EditorConfig.org 3 | # install a plugin to your editor: http://editorconfig.org/#download 4 | # mark: not all plugins supports the same EditorConfig properties 5 | 6 | # This is the top-most .editorconfig file (do not search in parent directories) 7 | root = true 8 | 9 | ### All files 10 | [*] 11 | # Force charset utf-8 12 | charset = utf-8 13 | # Force Unix-style newlines with a newline ending every file & trim trailing whitespace 14 | end_of_line = lf 15 | # Indentation 16 | indent_style = tab 17 | indent_size = 4 18 | 19 | insert_final_newline = true 20 | trim_trailing_whitespace = true 21 | 22 | ### Frontend files 23 | [*.{css,scss,less,js,json,ts,sass,php,html,hbs,mustache,phtml,html.twig}] 24 | 25 | ### Markdown 26 | [*.md] 27 | indent_style = space 28 | indent_size = 4 29 | trim_trailing_whitespace = false 30 | 31 | ### YAML 32 | [*.yml] 33 | indent_style = space 34 | indent_size = 2 35 | 36 | ### Specific files 37 | [{package,bower}.json] 38 | indent_style = space 39 | indent_size = 2 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | '@merkle-open/eslint-config/configurations/es8-node.js', 4 | ], 5 | globals: { 6 | testRule: 'readonly', 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /.github/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We are more than happy to accept external contributions to the project 4 | in the form of feedback, bug reports and even better - pull requests :) 5 | 6 | ## How to contribute 7 | 8 | ### Give feedback on issues 9 | 10 | We're always looking for more opinions on discussions in the [issue tracker](https://github.com/merkle-open/stylelint-bem/issues). 11 | It's a good opportunity to influence the future direction of the plugin. 12 | 13 | ### Creating Issues 14 | 15 | In order for us to help you, please check that you've completed the following steps: 16 | 17 | * Make sure you're on the latest version 18 | * Use the search feature to ensure that the issue hasn't been reported before 19 | * Include as much information about the issue as possible, including 20 | any output you've received, what OS and version you're on, etc. 21 | 22 | [Submit your issue](https://github.com/merkle-open/stylelint-bem/issues/new) 23 | 24 | ### Opening pull requests 25 | 26 | * Please check to make sure that there aren't existing pull requests attempting to address the issue mentioned. We also recommend checking for issues related to the issue on the tracker, as a team member may be working on the issue in a branch or fork. 27 | * Non-trivial changes should be discussed in an issue first 28 | * Please check project guidelines from `.editorconfig` & `.eslintrc` 29 | * Develop in a topic branch 30 | * Make sure test-suite passes: `npm test` (This includes linting). 31 | * Push to your fork and submit a pull request to the development branch 32 | 33 | Some things that will increase the chance that your pull request is accepted: 34 | 35 | * Write tests 36 | * Write a meaningful commit message 37 | * Write a convincing description of your PR and why we should land it 38 | 39 | #### Quick Start 40 | 41 | * You need [node](../.node-version) of course 42 | * Fork, then clone the repo and then run `npm install` in them 43 | * Start hacking ;-) 44 | 45 | You can keep your repo up to date by running `git pull --rebase upstream master`. 46 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ## Type of issue 10 | 11 | 12 | 13 | * Regression (a behavior that used to work and stopped working in a new release) 14 | * Bug report 15 | * Feature request 16 | * Documentation issue or request 17 | * Other... Please describe 18 | 19 | 20 | 21 | ---- 22 | 23 | ## Environment 24 | 25 | 34 | 35 | ## Expected behavior 36 | 37 | 38 | 39 | ## Current behavior 40 | 41 | 42 | 43 | ## Steps to reproduce the behavior 44 | 45 | 46 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | ## Purpose of this pull request? 6 | 7 | 8 | 9 | * Documentation update 10 | * Bug fix 11 | * Enhancement 12 | * Other... Please describe 13 | 14 | ## What changes did you make? 15 | 16 | 17 | 18 | ## Does this pull request introduce a breaking change? 19 | 20 | 21 | 22 | ## Is there anything you'd like reviewers to focus on? 23 | 24 | 25 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies and run tests across different versions of node on different environments 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: ci 5 | on: 6 | push: 7 | branches: 8 | - master 9 | - develop 10 | pull_request: 11 | branches: 12 | - master 13 | - develop 14 | jobs: 15 | test: 16 | name: Test - ${{ matrix.platform }} - Node v${{ matrix.node-version }} 17 | strategy: 18 | matrix: 19 | node-version: [20.x, 22.x, 24.x] 20 | platform: [ ubuntu-latest, windows-latest ] 21 | runs-on: ${{ matrix.platform }} 22 | steps: 23 | - uses: actions/checkout@v3 24 | 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | 30 | - name: Install dependencies 31 | run: npm install 32 | 33 | - name: Run tests 34 | run: npm test 35 | env: 36 | CI: true 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS 2 | .DS_Store 3 | Thumbs.db 4 | 5 | # IDE 6 | _.* 7 | .project 8 | .idea 9 | /*.iml 10 | .vscode 11 | .cache 12 | !*.cache 13 | .settings 14 | !*.settings 15 | .config 16 | !*.config 17 | .buildpath 18 | .metadata 19 | .tmp* 20 | *.prefs 21 | .deployables 22 | atlassian-ide-plugin.xml 23 | 24 | # Node 25 | node_modules 26 | npm-debug.log 27 | yarn-error.log 28 | lerna-debug.log 29 | 30 | # allow 31 | !.gitkeep 32 | 33 | # build 34 | dist 35 | build 36 | 37 | # Project 38 | *.log 39 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 24.8.0 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 10.1.0 2 | 3 | - [fix] config validation is now performed correctly #32 4 | 5 | ## 10.0.1 6 | 7 | - [maintenance] update dependency postcss-resolve-nested-selector 8 | 9 | ## 10.0.0 10 | 11 | - [BREAKING] drop support for node < 18.12.0 12 | - [BREAKING] drop support for stylelint < 14 13 | - [feat] add support for stylelint 16 14 | 15 | ## 9.0.0 16 | 17 | - [BREAKING] drop support for node < 16 18 | - [fix] avoid conflicts with some pseudo-classes and improvements for attribute and nested selectors (#26) 19 | - [maintenance] update dependencies 20 | 21 | ## 8.1.0 22 | 23 | - [feature] add support for stylelint 15.x 24 | 25 | ## 8.0.0 26 | 27 | - [BREAKING] drop support for node < 14 28 | - [maintenance] update dependencies 29 | 30 | ## 7.0.0 31 | 32 | - [BREAKING] drop support for node < 12 33 | - [feature] add support for stylelint 14.x 34 | 35 | ## 6.3.4 36 | 37 | - [docs] GitHub organization name change 38 | 39 | ## 6.3.3 40 | 41 | - [chore] repo url change 42 | 43 | ## 6.3.1 44 | 45 | - [maintenance] update dependencies 46 | 47 | ## 6.3.0 48 | 49 | - [feature] add support for stylelint 13.x 50 | - [maintenance] update dependencies 51 | 52 | ## 6.2.0 53 | 54 | - [feature] add support for stylelint 12.x 55 | 56 | ## 6.1.0 57 | 58 | - [feature] add support for stylelint 11.x 59 | 60 | ## 6.0.0 61 | 62 | - [Breaking] drop node 6 support 63 | - [maintenance] update dependencies 64 | 65 | ## 5.1.1 66 | 67 | - [fix] allow scss placeholders 68 | - [maintenance] update dependencies 69 | 70 | ## 5.1.0 71 | 72 | - [feature] #10 add support for multiple namespaces 73 | - [maintenance] update dependencies 74 | 75 | ## 5.0.0 76 | 77 | - [maintenance] BREAKING: drop node 4 support 78 | - [maintenance] update dependencies 79 | 80 | ## 4.0.0 81 | 82 | - [feature] BREAKING: add rule to disallow hyphens at start and end of block parts 83 | - [feature] add possibility to disable prefixes by configuration 84 | - [maintenance] update dependencies 85 | 86 | ## 3.1.3 87 | 88 | - [maintenance] update dependencies 89 | 90 | ## 3.1.2 91 | 92 | - [maintenance] update dependencies 93 | - [maintanance] add node 8 ci tests 94 | 95 | ## 3.1.1 96 | 97 | - [docs] example fix 98 | 99 | ## 3.1.0 100 | 101 | - [feature] add possiblilty to overwrite default patternPrefixes and patternHelpers (#1) 102 | - [maintenance] update dependencies 103 | 104 | ## 3.0.2 105 | 106 | - update dependencies 107 | - add appveyor 108 | 109 | ## 3.0.1 110 | 111 | - Fix `Rule expected but "&" found` when using & inside a sass mixin 112 | 113 | ## 3.0.0 114 | 115 | - drop node 0.12 support 116 | - use namics code style 117 | - forbid placing modifiers in front of elements 118 | - allow to use multiple elements e.g. [prefix]-[block]\__[element]\__[element] 119 | 120 | ## 2.1.0 121 | 122 | - allow less mixins with props 123 | 124 | ## 2.0.0 125 | 126 | - Add Tests for missing blocks 127 | - More useful error messages 128 | 129 | ## 1.1.0 130 | 131 | - Remove `js` prefix 132 | - Support less-mixins 133 | 134 | ## 1.0.0 135 | 136 | - Initial Version 137 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2016 Merkle Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![License](https://img.shields.io/badge/license-MIT-green.svg)](http://opensource.org/licenses/MIT) 2 | [![NPM version](https://badge.fury.io/js/%40namics%2Fstylelint-bem.svg)](https://npmjs.org/package/@namics/stylelint-bem) 3 | [![Build Status](https://github.com/merkle-open/stylelint-bem/workflows/ci/badge.svg)](https://github.com/merkle-open/stylelint-bem/actions) 4 | 5 | # Stylelint BEM Namics 6 | 7 | Verifies that the given css/less/scss follows the following BEM code conventions. 8 | 9 | ![screenshot](https://raw.githubusercontent.com/merkle-open/stylelint-bem/master/example.png) 10 | 11 | ## Installation 12 | 13 | ``` 14 | npm install @namics/stylelint-bem --save-dev 15 | ``` 16 | 17 | ## Configuration 18 | 19 | ### Simple configuration 20 | ```js 21 | { 22 | "plugins": [ 23 | "@namics/stylelint-bem" 24 | ], 25 | "rules": { 26 | "plugin/stylelint-bem-namics": true 27 | } 28 | } 29 | ``` 30 | 31 | ### Advanced configuration 32 | 33 | You can define one or more namespaces which has to be prepended before every class name: 34 | 35 | ```js 36 | { 37 | "plugins": [ 38 | "@namics/stylelint-bem" 39 | ], 40 | "rules": { 41 | "plugin/stylelint-bem-namics": { 42 | "namespaces": ["ux-"] 43 | } 44 | } 45 | } 46 | ``` 47 | 48 | and in case of emergency you can overwrite the default prefixes 49 | 50 | ```js 51 | { 52 | "plugins": [ 53 | "@namics/stylelint-bem" 54 | ], 55 | "rules": { 56 | "plugin/stylelint-bem-namics": { 57 | "patternPrefixes": [ "a", "m", "o", "t", "p" ], 58 | "helperPrefixes": [ "is", "has" ] 59 | } 60 | } 61 | } 62 | ``` 63 | 64 | ... or you can pass empty prefixes to disable prefixes completely 65 | 66 | ```js 67 | { 68 | "plugins": [ 69 | "@namics/stylelint-bem" 70 | ], 71 | "rules": { 72 | "plugin/stylelint-bem-namics": { 73 | "patternPrefixes": [], 74 | "helperPrefixes": [] 75 | } 76 | } 77 | } 78 | ``` 79 | 80 | ## Valid examples 81 | 82 | ### Default Pattern Prefixes 83 | 84 | * `a` Atom 85 | * `m` Molecule 86 | * `o` Organism 87 | * `l` Layout 88 | * `g` Grid 89 | * `h` Helper 90 | 91 | ### Default Helper Prefixes 92 | 93 | * `state` State 94 | 95 | ```css 96 | .a-[block-name] {} 97 | .m-[block-name] {} 98 | .o-[block-name] {} 99 | .l-[block-name] {} 100 | .g-[block-name] {} 101 | .h-[block-name] {} 102 | 103 | .a-[block-name]--[modifier-name] {} 104 | .m-[block-name]--[modifier-name] {} 105 | .o-[block-name]--[modifier-name] {} 106 | .l-[block-name]--[modifier-name] {} 107 | .g-[block-name]--[modifier-name] {} 108 | .h-[block-name]--[modifier-name] {} 109 | 110 | .a-[block-name]__[element-name] {} 111 | .m-[block-name]__[element-name] {} 112 | .o-[block-name]__[element-name] {} 113 | .l-[block-name]__[element-name] {} 114 | .g-[block-name]__[element-name] {} 115 | .h-[block-name]__[element-name] {} 116 | 117 | .a-[block-name]__[element-name]__[element-name] {} 118 | .m-[block-name]__[element-name]__[element-name] {} 119 | .o-[block-name]__[element-name]__[element-name] {} 120 | .l-[block-name]__[element-name]__[element-name] {} 121 | .g-[block-name]__[element-name]__[element-name] {} 122 | .h-[block-name]__[element-name]__[element-name] {} 123 | 124 | .state-a-[block-name]--[state-name] {} 125 | .state-m-[block-name]--[state-name] {} 126 | .state-o-[block-name]--[state-name] {} 127 | .state-l-[block-name]--[state-name] {} 128 | .state-g-[block-name]--[state-name] {} 129 | .state-h-[block-name]--[state-name] {} 130 | ``` 131 | 132 | ## Exception 133 | 134 | Whenever you will apply rules you will run into edge cases like third-party code or wysiwyg content where those rules have to be bent a little bit. 135 | 136 | In this case you can get around the rules above but you should leave a comment why and enable 137 | the linting again: 138 | 139 | ```css 140 | /* wysiwyg does not follow bem */ 141 | /* stylelint-disable plugin/stylelint-bem-namics */ 142 | .wysiwyg .headline { 143 | font-size: 34px; 144 | } 145 | /* stylelint-enable plugin/stylelint-bem-namics */ 146 | ``` 147 | 148 | ## Changelog 149 | 150 | Please see the [CHANGELOG.md](https://github.com/merkle-open/stylelint-bem/blob/master/CHANGELOG.md) 151 | -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merkle-open/stylelint-bem/5ded0b4a714516e52fa8d2eecdf54a1946aba14b/example.png -------------------------------------------------------------------------------- /gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behaviour, in case users don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Explicitly declare text files we want to always be normalized and converted 5 | # to native line endings on checkout. 6 | *.sh text eol=lf 7 | *.pp text eol=lf 8 | *.rb text eol=lf 9 | *.yaml text eol=lf 10 | *.bash_aliases text eol=lf 11 | 12 | # Denote all files that are truly binary and should not be modified. 13 | *.eot binary 14 | *.ttf binary 15 | *.woff binary 16 | *.woff2 binary 17 | 18 | # Windows-specific files get windows endings 19 | *.bat eol=crlf 20 | *.cmd eol=crlf 21 | *-windows.tmpl eol=crlf 22 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* eslint complexity:off */ 2 | 'use strict'; 3 | // @see https://github.com/stylelint/stylelint/blob/master/docs/developer-guide/plugins.md 4 | const stylelint = require('stylelint'); 5 | const resolvedNestedSelector = require('postcss-resolve-nested-selector'); 6 | const extractCssClasses = require('./lib/css-selector-classes'); 7 | const util = require('util'); 8 | 9 | const ruleName = 'plugin/stylelint-bem-namics'; 10 | const messages = stylelint.utils.ruleMessages(ruleName, { 11 | expected: function expected(selector, expectedSelector) { 12 | return `Expected class name "${selector}" to ${expectedSelector}.`; 13 | }, 14 | }); 15 | 16 | // options 17 | const isString = (val) => typeof val === 'string' || val instanceof String; 18 | const optionsObjectSchema = { 19 | patternPrefixes: [isString], 20 | helperPrefixes: [isString], 21 | namespaces: [isString], 22 | namespace: isString, // deprecated 23 | }; 24 | 25 | // utils 26 | const addNamespace = util.deprecate((namespace, namespaces) => { 27 | if (!namespaces.includes(namespace)) { 28 | namespaces.push(namespace); 29 | } 30 | }, 'Using the "namespace" option of @namics/stylelint-bem is deprecated. ' + 31 | 'Please use the new namespaces option which allows using multiple namespaces'); 32 | 33 | module.exports = stylelint.createPlugin(ruleName, (options) => { 34 | options = options || ''; 35 | 36 | const validPatternPrefixes = Array.isArray(options.patternPrefixes) ? options.patternPrefixes : [ 37 | 'a', 38 | 'm', 39 | 'o', 40 | 'l', 41 | 'g', 42 | 'h', 43 | ]; 44 | 45 | const validHelperPrefixes = Array.isArray(options.helperPrefixes) ? options.helperPrefixes : [ 46 | 'state', 47 | ]; 48 | 49 | const validPrefixes = [] 50 | .concat(validPatternPrefixes) 51 | .concat(validHelperPrefixes); 52 | 53 | /** 54 | * Extracts the namespace, helper and prefix from the given className 55 | * 'ux-state-a-button' 56 | * @param {string} fullClassName the class name 57 | * @param {string[]} namespaces (optional) namespace 58 | * @returns {Object} namespace, helper, pattern, name 59 | * 60 | */ 61 | function parseClassName(fullClassName, namespaces) { 62 | const result = {}; 63 | let className = fullClassName; 64 | // Extract the namespace 65 | if (namespaces.length) { 66 | const namespaceIndex = namespaces.findIndex((namespace) => { 67 | return className.startsWith(namespace); 68 | }); 69 | if (namespaceIndex === -1) { 70 | return result; 71 | } 72 | const namespace = namespaces[namespaceIndex]; 73 | result.namespace = namespace; 74 | className = className.substr(namespace.length); 75 | } 76 | // Handle className with helper prefixes 77 | const helperPrefix = className.split('-')[0]; 78 | if (validHelperPrefixes.indexOf(helperPrefix) !== -1) { 79 | result.helper = helperPrefix; 80 | className = className.substr(helperPrefix.length + 1); 81 | } 82 | // Handle classNames with prefixes 83 | const patternPrefix = className.split('-')[0]; 84 | if (validPatternPrefixes.indexOf(patternPrefix) !== -1) { 85 | result.pattern = patternPrefix; 86 | className = className.substr(patternPrefix.length + 1); 87 | } 88 | result.name = className; 89 | result.parts = className.split(/__|--/); 90 | return result; 91 | } 92 | 93 | /** 94 | * Helper for error messages to tell the correct syntax 95 | * 96 | * @param {string} className the class name 97 | * @param {string[]} namespaces (optional) namespace 98 | * @returns {string} valid syntax 99 | */ 100 | function getValidSyntax(className, namespaces) { 101 | const parsedClassName = parseClassName(className, namespaces); 102 | // Try to guess the namespaces or use the first one 103 | let validSyntax = parsedClassName.namespace || namespaces[0] || ''; 104 | if (parsedClassName.helper) { 105 | validSyntax += `${parsedClassName.helper}-`; 106 | } 107 | if (parsedClassName.pattern) { 108 | validSyntax += `${parsedClassName.pattern}-`; 109 | } else if (validPatternPrefixes.length) { 110 | validSyntax += '[prefix]-'; 111 | } 112 | validSyntax += '[block]'; 113 | if (className.indexOf('__') !== -1) { 114 | validSyntax += '__[element]'; 115 | } 116 | if (validHelperPrefixes.indexOf(parsedClassName.helper) !== -1) { 117 | validSyntax += `--[${parsedClassName.helper}]`; 118 | } else if (className.indexOf('--') !== -1) { 119 | validSyntax += '--[modifier]'; 120 | } 121 | return validSyntax; 122 | } 123 | 124 | /** 125 | * Validates the given className and returns the error if it's not valid 126 | * @param {string} className - the name of the class e.g. 'a-button' 127 | * @param {string[]} namespaces - the namespace (optional) 128 | * @returns {string} error message 129 | */ 130 | function getClassNameErrors(className, namespaces) { 131 | 132 | if ((/[A-Z]/).test(className)) { 133 | return 'contain no uppercase letters'; 134 | } 135 | 136 | const parsedClassName = parseClassName(className, namespaces); 137 | const isAnyNamespaceUsed = namespaces.some((namespace) => parsedClassName.namespace === namespace); 138 | if (namespaces.length && !isAnyNamespaceUsed) { 139 | return namespaces.length > 1 140 | ? `use one of the valid namespaces "${namespaces.join('", "')}"` 141 | : `use the namespace "${namespaces[0]}"`; 142 | } 143 | 144 | // Valid helper but invalid pattern prefix 145 | // e.g. 'state-zz-button' 146 | if (validPatternPrefixes.length && parsedClassName.helper && !parsedClassName.pattern) { 147 | // Try to guess the namespace 148 | const namespace = parsedClassName.namespace || namespaces[0] || ''; 149 | const validPrefixExamples = validPatternPrefixes 150 | .map((prefix) => `"${namespace}${parsedClassName.helper}-${prefix}-"`) 151 | .join(', '); 152 | return `use the ${getValidSyntax(className, namespaces)} syntax. ` + 153 | `Valid ${parsedClassName.helper} prefixes: ${validPrefixExamples}`; 154 | } 155 | 156 | // Invalid pattern prefix 157 | if (validPatternPrefixes.length && !parsedClassName.pattern) { 158 | // Try to guess the namespace 159 | const namespace = parsedClassName.namespace || namespaces[0] || ''; 160 | const validPrefixExamples = validPrefixes 161 | .map((prefix) => `"${namespace}${prefix}-"`) 162 | .join(', '); 163 | return `start with a valid prefix: ${validPrefixExamples}`; 164 | } 165 | 166 | if (!((/^[a-z]/).test(parsedClassName.name))) { 167 | return `use the ${getValidSyntax(className, namespaces)} syntax`; 168 | } 169 | if ((/___/).test(parsedClassName.name)) { 170 | return 'use only two "_" as element separator'; 171 | } 172 | if ((/--.*__/).test(parsedClassName.name)) { 173 | return `use the ${getValidSyntax(className, namespaces)} syntax`; 174 | } 175 | if ((/--(-|.*--)/).test(parsedClassName.name)) { 176 | return 'use only one "--" modifier separator'; 177 | } 178 | if ((/(^|[^_])_([^_]|$)/).test(parsedClassName.name)) { 179 | return 'use "_" only as element separator'; 180 | } 181 | // disallow hyphens at start and end of block parts 182 | if (parsedClassName.parts.some((elem) => (/^(-.*|.*-)$/).test(elem))) { 183 | return 'use "-" only for composite names'; 184 | } 185 | if (parsedClassName.helper && parsedClassName.name.indexOf('--') === -1) { 186 | return `use the ${getValidSyntax(className, namespaces)} syntax`; 187 | } 188 | } 189 | 190 | return (root, result) => { 191 | const possible = (options === true || options === false) ? [true, false] : optionsObjectSchema; 192 | const validOptions = stylelint.utils.validateOptions( 193 | result, 194 | ruleName, 195 | { 196 | actual: options, 197 | optional: true, 198 | possible, 199 | }); 200 | 201 | if (!validOptions) { 202 | return; 203 | } 204 | 205 | const namespaces = options.namespaces || []; 206 | 207 | // As we now support options.namespaces 208 | // the following lines will be removed in future: 209 | if (options.namespace) { 210 | addNamespace(options.namespace, namespaces); 211 | } 212 | 213 | const classNameErrorCache = {}; 214 | root.walkRules((rule) => { 215 | // Skip keyframes 216 | if (rule.parent.name === 'keyframes') { 217 | return; 218 | } 219 | rule.selectors.forEach((selector) => { 220 | if (selector.startsWith('%')) { 221 | // Skip scss placeholders 222 | return; 223 | } 224 | if (selector.indexOf('(') !== -1 && (selector.indexOf(':') === -1 || selector.indexOf('@') !== -1)) { 225 | // Skip less mixins 226 | return; 227 | } 228 | resolvedNestedSelector(selector, rule).forEach((resolvedSelector) => { 229 | let classNames = []; 230 | try { 231 | // Remove ampersand from inner sass mixins and parse the class names 232 | classNames = extractCssClasses(resolvedSelector.replace(/&\s*/ig, '')); 233 | } catch (e) { 234 | stylelint.utils.report({ 235 | ruleName, 236 | result, 237 | node: rule, 238 | message: e.message, 239 | }); 240 | } 241 | classNames.forEach((className) => { 242 | if (classNameErrorCache[className] === undefined) { 243 | classNameErrorCache[className] = getClassNameErrors(className, namespaces, rule); 244 | } 245 | if (classNameErrorCache[className]) { 246 | stylelint.utils.report({ 247 | ruleName, 248 | result, 249 | node: rule, 250 | message: messages.expected(className, classNameErrorCache[className]), 251 | }); 252 | } 253 | }); 254 | }); 255 | }); 256 | }); 257 | }; 258 | }); 259 | 260 | module.exports.ruleName = ruleName; 261 | module.exports.messages = messages; 262 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | const getTestRule = require('jest-preset-stylelint/getTestRule'); 2 | 3 | global.testRule = getTestRule({ plugins: ['./'] }); 4 | -------------------------------------------------------------------------------- /lib/css-selector-classes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const createParser = require('css-selector-parser').createParser; 4 | const parse = createParser({ syntax: 'progressive' }); 5 | 6 | /* eslint-disable complexity */ 7 | 8 | /** 9 | * Recursively visits nodes in a CSS rule tree and applies a function to each rule node. 10 | * 11 | * @param {Object} node - The root node of the CSS rule tree. 12 | * @param {function} fn - The function to apply to each rule node. 13 | * @returns {void} 14 | */ 15 | function visitRules(node, fn) { 16 | 17 | if (!node) { return; } 18 | 19 | if (node.rules) { 20 | node.rules.forEach((rule) => visitRules(rule, fn)); 21 | } 22 | 23 | if (node.nestedRule?.pseudoClasses) { 24 | node.nestedRule.pseudoClasses.forEach((pseudo) => visitRules(pseudo.argument, fn)); 25 | } 26 | 27 | if (node.attributes) { 28 | const classAttribute = node.attributes.find((attribute) => attribute.name === 'class'); 29 | if (classAttribute?.value?.value) { 30 | fn({ classNames: [classAttribute.value.value] }); 31 | } 32 | } 33 | 34 | if (node.pseudoClasses) { 35 | node.pseudoClasses.forEach((pseudo) => { 36 | if (pseudo.argument?.rules) { 37 | pseudo.argument.rules.forEach((rule) => visitRules(rule, fn)); 38 | } 39 | }); 40 | } 41 | 42 | if (node.type === 'Rule') { 43 | fn(node); 44 | } 45 | } 46 | /* eslint-enable complexity */ 47 | 48 | /** 49 | * Return all the classes in a CSS selector. 50 | * 51 | * @param {string} selector A CSS selector 52 | * @returns {string[]} An array of every class present in the CSS selector 53 | */ 54 | function getCssSelectorClasses(selector) { 55 | let list = []; 56 | const ast = parse(selector); 57 | visitRules(ast, (ruleSet) => { 58 | if (ruleSet.classNames) { 59 | list = list.concat(ruleSet.classNames); 60 | } 61 | }); 62 | return Array.from(new Set(list)); 63 | } 64 | 65 | module.exports = getCssSelectorClasses; 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@namics/stylelint-bem", 3 | "version": "10.1.0", 4 | "description": "A stylelint plugin for the Namics BEM definitions", 5 | "main": "index.js", 6 | "engines": { 7 | "node": ">=20.19.0" 8 | }, 9 | "scripts": { 10 | "clean": "npx -y rimraf package-lock.json node_modules", 11 | "lint": "eslint .", 12 | "prepare": "husky", 13 | "prepublishOnly": "npm test && npx -y pkg-ok@2.3.1", 14 | "pretest": "npm run lint", 15 | "test": "jest", 16 | "update-dependencies": "npm-check-updates -u --deprecated" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/merkle-open/stylelint-bem.git" 21 | }, 22 | "files": [ 23 | "index.js", 24 | "lib" 25 | ], 26 | "keywords": [ 27 | "stylelint", 28 | "stylelint-plugin", 29 | "css", 30 | "nesting", 31 | "linter", 32 | "bem" 33 | ], 34 | "author": "Merkle Inc.", 35 | "license": "MIT", 36 | "dependencies": { 37 | "css-selector-parser": "2.3.2", 38 | "postcss-resolve-nested-selector": "0.1.6" 39 | }, 40 | "peerDependencies": { 41 | "stylelint": ">=14.0.0 <17.0.0" 42 | }, 43 | "devDependencies": { 44 | "@merkle-open/eslint-config": "4.0.0", 45 | "eslint": "8.57.1", 46 | "eslint-plugin-import": "2.32.0", 47 | "husky": "9.1.7", 48 | "jest": "30.1.3", 49 | "jest-light-runner": "0.7.10", 50 | "jest-preset-stylelint": "8.0.0", 51 | "lint-staged": "16.2.0", 52 | "npm-check-updates": "18.3.0", 53 | "stylelint": "16.24.0" 54 | }, 55 | "jest": { 56 | "runner": "jest-light-runner", 57 | "preset": "jest-preset-stylelint", 58 | "setupFiles": [ 59 | "./jest.setup.js" 60 | ] 61 | }, 62 | "lint-staged": { 63 | "*.js": [ 64 | "eslint" 65 | ] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /test/default.test.js: -------------------------------------------------------------------------------- 1 | /* eslint max-len:off */ 2 | const { ruleName } = require('../index'); 3 | 4 | testRule({ 5 | ruleName, 6 | config: true, 7 | accept: [ 8 | { 9 | code: '.a-block {}', 10 | }, 11 | { 12 | code: '.m-block {}', 13 | }, 14 | { 15 | code: '.o-block {}', 16 | }, 17 | { 18 | code: '.l-block {}', 19 | }, 20 | { 21 | code: '.g-block {}', 22 | }, 23 | { 24 | code: '.h-block {}', 25 | }, 26 | 27 | { 28 | code: '.state-a-block--state-name {}', 29 | }, 30 | { 31 | code: '.state-m-block--state-name {}', 32 | }, 33 | { 34 | code: '.state-o-block--state-name {}', 35 | }, 36 | { 37 | code: '.state-l-block--state-name {}', 38 | }, 39 | { 40 | code: '.state-g-block--state-name {}', 41 | }, 42 | { 43 | code: '.state-h-block--state-name {}', 44 | }, 45 | 46 | { 47 | code: '.a-block--modifier {}', 48 | }, 49 | { 50 | code: '.m-block--modifier {}', 51 | }, 52 | { 53 | code: '.o-block--modifier {}', 54 | }, 55 | { 56 | code: '.l-block--modifier {}', 57 | }, 58 | { 59 | code: '.g-block--modifier {}', 60 | }, 61 | { 62 | code: '.h-block--modifier {}', 63 | }, 64 | 65 | { 66 | code: '.a-block__element {}', 67 | }, 68 | { 69 | code: '.m-block__element {}', 70 | }, 71 | { 72 | code: '.o-block__element {}', 73 | }, 74 | { 75 | code: '.l-block__element {}', 76 | }, 77 | { 78 | code: '.g-block__element {}', 79 | }, 80 | { 81 | code: '.h-block__element {}', 82 | }, 83 | ], 84 | reject: [ 85 | { 86 | code: '.z-block {}', 87 | message: `Expected class name "z-block" to start with a valid prefix: "a-", "m-", "o-", "l-", "g-", "h-", "state-". (${ruleName})`, 88 | }, 89 | { 90 | code: '.0-block {}', 91 | message: `Expected class name "0-block" to start with a valid prefix: "a-", "m-", "o-", "l-", "g-", "h-", "state-". (${ruleName})`, 92 | }, 93 | { 94 | code: '.-block {}', 95 | message: `Expected class name "-block" to start with a valid prefix: "a-", "m-", "o-", "l-", "g-", "h-", "state-". (${ruleName})`, 96 | }, 97 | { 98 | code: '.--block {}', 99 | message: `Expected class name "--block" to start with a valid prefix: "a-", "m-", "o-", "l-", "g-", "h-", "state-". (${ruleName})`, 100 | }, 101 | { 102 | code: '.a-block___x {}', 103 | message: `Expected class name "a-block___x" to use only two "_" as element separator. (${ruleName})`, 104 | }, 105 | { 106 | code: '.a-block---x {}', 107 | message: `Expected class name "a-block---x" to use only one "--" modifier separator. (${ruleName})`, 108 | }, 109 | { 110 | code: '.a-block--y--x {}', 111 | message: `Expected class name "a-block--y--x" to use only one "--" modifier separator. (${ruleName})`, 112 | }, 113 | { 114 | code: '.a-Block {}', 115 | message: `Expected class name "a-Block" to contain no uppercase letters. (${ruleName})`, 116 | }, 117 | { 118 | code: '.m-__element {}', 119 | message: `Expected class name "m-__element" to use the m-[block]__[element] syntax. (${ruleName})`, 120 | }, 121 | { 122 | code: '.m--modifier {}', 123 | message: `Expected class name "m--modifier" to use the m-[block]--[modifier] syntax. (${ruleName})`, 124 | }, 125 | { 126 | code: '.m-block-__element {}', 127 | message: `Expected class name "m-block-__element" to use "-" only for composite names. (${ruleName})`, 128 | }, 129 | { 130 | code: '.m-block__-element {}', 131 | message: `Expected class name "m-block__-element" to use "-" only for composite names. (${ruleName})`, 132 | }, 133 | { 134 | code: '.m-block__element- {}', 135 | message: `Expected class name "m-block__element-" to use "-" only for composite names. (${ruleName})`, 136 | }, 137 | { 138 | code: '.m-block_--modifier {}', 139 | message: `Expected class name "m-block_--modifier" to use "_" only as element separator. (${ruleName})`, 140 | }, 141 | { 142 | code: '.m-block--_modifier {}', 143 | message: `Expected class name "m-block--_modifier" to use "_" only as element separator. (${ruleName})`, 144 | }, 145 | { 146 | code: '.m-block--modifier_ {}', 147 | message: `Expected class name "m-block--modifier_" to use "_" only as element separator. (${ruleName})`, 148 | }, 149 | { 150 | code: '.state-m--state {}', 151 | message: `Expected class name "state-m--state" to use the state-m-[block]--[state] syntax. (${ruleName})`, 152 | }, 153 | { 154 | code: '.state-block {}', 155 | message: `Expected class name "state-block" to use the state-[prefix]-[block]--[state] syntax. Valid state prefixes: "state-a-", "state-m-", "state-o-", "state-l-", "state-g-", "state-h-". (${ruleName})`, 156 | }, 157 | { 158 | code: '.state-a-block {}', 159 | message: `Expected class name "state-a-block" to use the state-a-[block]--[state] syntax. (${ruleName})`, 160 | }, 161 | { 162 | code: '.state-a-block_b {}', 163 | message: `Expected class name "state-a-block_b" to use "_" only as element separator. (${ruleName})`, 164 | }, 165 | { 166 | code: '.a--modifier__block {}', 167 | message: `Expected class name "a--modifier__block" to use the a-[block]__[element]--[modifier] syntax. (${ruleName})`, 168 | }, 169 | ], 170 | }); 171 | -------------------------------------------------------------------------------- /test/edge-cases.test.js: -------------------------------------------------------------------------------- 1 | /* eslint max-len:off */ 2 | const { ruleName } = require('../index'); 3 | 4 | // Sass and Less edgecases 5 | testRule({ 6 | ruleName, 7 | config: {}, 8 | accept: [ 9 | // Should not conflict with keyframes 10 | { 11 | code: '@keyframes blue-background-change { 0% { background: black } }', 12 | }, 13 | // Should not conflict with scss placeholders 14 | { 15 | code: '%placeholder { }', 16 | }, 17 | // Should not conflict with less mixins 18 | { 19 | code: '.mixin() { }', 20 | }, 21 | // Should not conflict with less mixins 22 | { 23 | code: '.mixin(@prop: black) { background: @prop }', 24 | }, 25 | // Should not conflict with less namespaces 26 | { 27 | code: '#namespace { }', 28 | }, 29 | // Should not conflict with ampersands inside mixins 30 | { 31 | code: '@mixin specialCase { &:hover { } }', 32 | }, 33 | // Should not conflict with unknown pseudo elements 34 | { 35 | code: '.m-search { &::-ms-clear { } &::-webkit-search-cancel-button { } }', 36 | }, 37 | // Should not conflict with scss nested outer selector 38 | { 39 | code: `.a-button { 40 | &:focus { 41 | [data-whatintent="keyboard"] & {} 42 | } 43 | }`, 44 | }, 45 | ], 46 | reject: [ 47 | { 48 | code: '.no-mixin:not(x) { }', 49 | message: `Expected class name "no-mixin" to start with a valid prefix: "a-", "m-", "o-", "l-", "g-", "h-", "state-". (${ruleName})`, 50 | }, 51 | // should conflict with scss variable interpolation, because we can not validate the definitive selector 52 | { 53 | code: '@mixin icon($name) { .a-icon-#{$name} {} }', 54 | message: 'Unknown word $name (CssSyntaxError)', 55 | }, 56 | // should conflict with less modifyVars interpolation, because we can not validate the definitive selector 57 | { 58 | code: '.@{css-prefix}-selector {}', 59 | message: 'Unknown word css-prefix (CssSyntaxError)', 60 | }, 61 | ], 62 | }); 63 | -------------------------------------------------------------------------------- /test/invalid-config.test.js: -------------------------------------------------------------------------------- 1 | const { ruleName } = require('../index'); 2 | 3 | testRule({ 4 | ruleName, 5 | config: { 6 | invalidKey: 'string', 7 | }, 8 | reject: [ 9 | { 10 | code: '.test {}', 11 | message: `Invalid option name "invalidKey" for rule "${ruleName}"`, 12 | }, 13 | ], 14 | }); 15 | 16 | testRule({ 17 | ruleName, 18 | config: { 19 | namespaces: true, 20 | }, 21 | reject: [ 22 | { 23 | code: '.test {}', 24 | message: `Invalid value "true" for option "namespaces" of rule "${ruleName}"`, 25 | }, 26 | ], 27 | }); 28 | 29 | testRule({ 30 | ruleName, 31 | config: { 32 | patternPrefixes: 42, 33 | }, 34 | reject: [ 35 | { 36 | code: '.test {}', 37 | message: `Invalid value "42" for option "patternPrefixes" of rule "${ruleName}"`, 38 | }, 39 | ], 40 | }); 41 | -------------------------------------------------------------------------------- /test/namespace.test.js: -------------------------------------------------------------------------------- 1 | /* eslint max-len:off */ 2 | const { ruleName } = require('../index'); 3 | 4 | testRule({ 5 | ruleName, 6 | config: { 7 | namespace: 'namespace-', 8 | }, 9 | accept: [ 10 | { 11 | code: '.namespace-a-block {}', 12 | }, 13 | { 14 | code: '.namespace-m-block {}', 15 | }, 16 | { 17 | code: '.namespace-o-block {}', 18 | }, 19 | { 20 | code: '.namespace-l-block {}', 21 | }, 22 | { 23 | code: '.namespace-g-block {}', 24 | }, 25 | { 26 | code: '.namespace-h-block {}', 27 | }, 28 | 29 | { 30 | code: '.namespace-state-a-block--state-name {}', 31 | }, 32 | { 33 | code: '.namespace-state-m-block--state-name {}', 34 | }, 35 | { 36 | code: '.namespace-state-o-block--state-name {}', 37 | }, 38 | { 39 | code: '.namespace-state-l-block--state-name {}', 40 | }, 41 | { 42 | code: '.namespace-state-g-block--state-name {}', 43 | }, 44 | { 45 | code: '.namespace-state-h-block--state-name {}', 46 | }, 47 | 48 | { 49 | code: '.namespace-a-block--modifier {}', 50 | }, 51 | { 52 | code: '.namespace-m-block--modifier {}', 53 | }, 54 | { 55 | code: '.namespace-o-block--modifier {}', 56 | }, 57 | { 58 | code: '.namespace-l-block--modifier {}', 59 | }, 60 | { 61 | code: '.namespace-g-block--modifier {}', 62 | }, 63 | { 64 | code: '.namespace-h-block--modifier {}', 65 | }, 66 | 67 | { 68 | code: '.namespace-a-block__block {}', 69 | }, 70 | { 71 | code: '.namespace-m-block__block {}', 72 | }, 73 | { 74 | code: '.namespace-o-block__block {}', 75 | }, 76 | { 77 | code: '.namespace-l-block__block {}', 78 | }, 79 | { 80 | code: '.namespace-g-block__block {}', 81 | }, 82 | { 83 | code: '.namespace-h-block__block {}', 84 | }, 85 | ], 86 | 87 | reject: [ 88 | { 89 | code: '.a-block {}', 90 | message: `Expected class name "a-block" to use the namespace "namespace-". (${ruleName})`, 91 | }, 92 | { 93 | code: '.namespace-z-block {}', 94 | message: `Expected class name "namespace-z-block" to start with a valid prefix: "namespace-a-", "namespace-m-", "namespace-o-", "namespace-l-", "namespace-g-", "namespace-h-", "namespace-state-". (${ruleName})`, 95 | }, 96 | { 97 | code: '.namespace-z-block__element {}', 98 | message: `Expected class name "namespace-z-block__element" to start with a valid prefix: "namespace-a-", "namespace-m-", "namespace-o-", "namespace-l-", "namespace-g-", "namespace-h-", "namespace-state-". (${ruleName})`, 99 | }, 100 | { 101 | code: '.namespace-m-__element {}', 102 | message: `Expected class name "namespace-m-__element" to use the namespace-m-[block]__[element] syntax. (${ruleName})`, 103 | }, 104 | { 105 | code: '.namespace-m--modifier {}', 106 | message: `Expected class name "namespace-m--modifier" to use the namespace-m-[block]--[modifier] syntax. (${ruleName})`, 107 | }, 108 | { 109 | code: '.namespace-state-m--state {}', 110 | message: `Expected class name "namespace-state-m--state" to use the namespace-state-m-[block]--[state] syntax. (${ruleName})`, 111 | }, 112 | { 113 | code: '.namespace-state-m__element {}', 114 | message: `Expected class name "namespace-state-m__element" to use the namespace-state-[prefix]-[block]__[element]--[state] syntax. Valid state prefixes: "namespace-state-a-", "namespace-state-m-", "namespace-state-o-", "namespace-state-l-", "namespace-state-g-", "namespace-state-h-". (${ruleName})`, 115 | }, 116 | { 117 | code: '.namespace-state-block {}', 118 | message: `Expected class name "namespace-state-block" to use the namespace-state-[prefix]-[block]--[state] syntax. Valid state prefixes: "namespace-state-a-", "namespace-state-m-", "namespace-state-o-", "namespace-state-l-", "namespace-state-g-", "namespace-state-h-". (${ruleName})`, 119 | }, 120 | { 121 | code: '.namespace-state-a-block {}', 122 | message: `Expected class name "namespace-state-a-block" to use the namespace-state-a-[block]--[state] syntax. (${ruleName})`, 123 | }, 124 | { 125 | code: '.namespace-a--modifier__block {}', 126 | message: `Expected class name "namespace-a--modifier__block" to use the namespace-a-[block]__[element]--[modifier] syntax. (${ruleName})`, 127 | }, 128 | ], 129 | }); 130 | 131 | // merge deprecated namespace with namespaces 132 | testRule({ 133 | ruleName, 134 | config: { 135 | namespace: 'namespace-', 136 | namespaces: ['namespace1-', 'namespace2-'], 137 | }, 138 | accept: [ 139 | { 140 | code: '.namespace-a-block {}', 141 | }, 142 | { 143 | code: '.namespace1-m-block {}', 144 | }, 145 | { 146 | code: '.namespace2-o-block {}', 147 | }, 148 | ], 149 | reject: [ 150 | { 151 | code: '.a-block {}', 152 | message: `Expected class name "a-block" to use one of the valid namespaces "namespace1-", "namespace2-", "namespace-". (${ruleName})`, 153 | }, 154 | ], 155 | }); 156 | -------------------------------------------------------------------------------- /test/namespaces.test.js: -------------------------------------------------------------------------------- 1 | /* eslint max-len:off */ 2 | const { ruleName } = require('../index'); 3 | 4 | testRule({ 5 | ruleName, 6 | config: { 7 | namespaces: ['namespace1-', 'namespace2-'], 8 | }, 9 | accept: [ 10 | { 11 | code: '.namespace1-a-block {}', 12 | }, 13 | { 14 | code: '.namespace1-m-block {}', 15 | }, 16 | { 17 | code: '.namespace1-o-block {}', 18 | }, 19 | { 20 | code: '.namespace1-l-block {}', 21 | }, 22 | { 23 | code: '.namespace1-g-block {}', 24 | }, 25 | { 26 | code: '.namespace1-h-block {}', 27 | }, 28 | 29 | { 30 | code: '.namespace1-state-a-block--state-name {}', 31 | }, 32 | { 33 | code: '.namespace1-state-m-block--state-name {}', 34 | }, 35 | { 36 | code: '.namespace1-state-o-block--state-name {}', 37 | }, 38 | { 39 | code: '.namespace1-state-l-block--state-name {}', 40 | }, 41 | { 42 | code: '.namespace1-state-g-block--state-name {}', 43 | }, 44 | { 45 | code: '.namespace1-state-h-block--state-name {}', 46 | }, 47 | 48 | { 49 | code: '.namespace1-a-block--modifier {}', 50 | }, 51 | { 52 | code: '.namespace1-m-block--modifier {}', 53 | }, 54 | { 55 | code: '.namespace1-o-block--modifier {}', 56 | }, 57 | { 58 | code: '.namespace1-l-block--modifier {}', 59 | }, 60 | { 61 | code: '.namespace1-g-block--modifier {}', 62 | }, 63 | { 64 | code: '.namespace1-h-block--modifier {}', 65 | }, 66 | 67 | { 68 | code: '.namespace1-a-block__block {}', 69 | }, 70 | { 71 | code: '.namespace1-m-block__block {}', 72 | }, 73 | { 74 | code: '.namespace1-o-block__block {}', 75 | }, 76 | { 77 | code: '.namespace1-l-block__block {}', 78 | }, 79 | { 80 | code: '.namespace1-g-block__block {}', 81 | }, 82 | { 83 | code: '.namespace1-h-block__block {}', 84 | }, 85 | 86 | { 87 | code: '.namespace2-a-block {}', 88 | }, 89 | { 90 | code: '.namespace2-m-block {}', 91 | }, 92 | { 93 | code: '.namespace2-o-block {}', 94 | }, 95 | { 96 | code: '.namespace2-l-block {}', 97 | }, 98 | { 99 | code: '.namespace2-g-block {}', 100 | }, 101 | { 102 | code: '.namespace2-h-block {}', 103 | }, 104 | 105 | { 106 | code: '.namespace2-state-a-block--state-name {}', 107 | }, 108 | { 109 | code: '.namespace2-state-m-block--state-name {}', 110 | }, 111 | { 112 | code: '.namespace2-state-o-block--state-name {}', 113 | }, 114 | { 115 | code: '.namespace2-state-l-block--state-name {}', 116 | }, 117 | { 118 | code: '.namespace2-state-g-block--state-name {}', 119 | }, 120 | { 121 | code: '.namespace2-state-h-block--state-name {}', 122 | }, 123 | 124 | { 125 | code: '.namespace2-a-block--modifier {}', 126 | }, 127 | { 128 | code: '.namespace2-m-block--modifier {}', 129 | }, 130 | { 131 | code: '.namespace2-o-block--modifier {}', 132 | }, 133 | { 134 | code: '.namespace2-l-block--modifier {}', 135 | }, 136 | { 137 | code: '.namespace2-g-block--modifier {}', 138 | }, 139 | { 140 | code: '.namespace2-h-block--modifier {}', 141 | }, 142 | 143 | { 144 | code: '.namespace2-a-block__block {}', 145 | }, 146 | { 147 | code: '.namespace2-m-block__block {}', 148 | }, 149 | { 150 | code: '.namespace2-o-block__block {}', 151 | }, 152 | { 153 | code: '.namespace2-l-block__block {}', 154 | }, 155 | { 156 | code: '.namespace2-g-block__block {}', 157 | }, 158 | { 159 | code: '.namespace2-h-block__block {}', 160 | }, 161 | ], 162 | reject: [ 163 | { 164 | code: '.a-block {}', 165 | message: `Expected class name "a-block" to use one of the valid namespaces "namespace1-", "namespace2-". (${ruleName})`, 166 | }, 167 | { 168 | code: '.namespace-a-block {}', 169 | message: `Expected class name "namespace-a-block" to use one of the valid namespaces "namespace1-", "namespace2-". (${ruleName})`, 170 | }, 171 | { 172 | code: '.namespace1-z-block {}', 173 | message: `Expected class name "namespace1-z-block" to start with a valid prefix: "namespace1-a-", "namespace1-m-", "namespace1-o-", "namespace1-l-", "namespace1-g-", "namespace1-h-", "namespace1-state-". (${ruleName})`, 174 | }, 175 | { 176 | code: '.namespace1-z-block__element {}', 177 | message: `Expected class name "namespace1-z-block__element" to start with a valid prefix: "namespace1-a-", "namespace1-m-", "namespace1-o-", "namespace1-l-", "namespace1-g-", "namespace1-h-", "namespace1-state-". (${ruleName})`, 178 | }, 179 | { 180 | code: '.namespace1-m-__element {}', 181 | message: `Expected class name "namespace1-m-__element" to use the namespace1-m-[block]__[element] syntax. (${ruleName})`, 182 | }, 183 | { 184 | code: '.namespace1-m--modifier {}', 185 | message: `Expected class name "namespace1-m--modifier" to use the namespace1-m-[block]--[modifier] syntax. (${ruleName})`, 186 | }, 187 | { 188 | code: '.namespace1-state-m--state {}', 189 | message: `Expected class name "namespace1-state-m--state" to use the namespace1-state-m-[block]--[state] syntax. (${ruleName})`, 190 | }, 191 | { 192 | code: '.namespace1-state-m__element {}', 193 | message: `Expected class name "namespace1-state-m__element" to use the namespace1-state-[prefix]-[block]__[element]--[state] syntax. Valid state prefixes: "namespace1-state-a-", "namespace1-state-m-", "namespace1-state-o-", "namespace1-state-l-", "namespace1-state-g-", "namespace1-state-h-". (${ruleName})`, 194 | }, 195 | { 196 | code: '.namespace1-state-block {}', 197 | message: `Expected class name "namespace1-state-block" to use the namespace1-state-[prefix]-[block]--[state] syntax. Valid state prefixes: "namespace1-state-a-", "namespace1-state-m-", "namespace1-state-o-", "namespace1-state-l-", "namespace1-state-g-", "namespace1-state-h-". (${ruleName})`, 198 | }, 199 | { 200 | code: '.namespace1-state-a-block {}', 201 | message: `Expected class name "namespace1-state-a-block" to use the namespace1-state-a-[block]--[state] syntax. (${ruleName})`, 202 | }, 203 | { 204 | code: '.namespace1-a--modifier__block {}', 205 | message: `Expected class name "namespace1-a--modifier__block" to use the namespace1-a-[block]__[element]--[modifier] syntax. (${ruleName})`, 206 | }, 207 | { 208 | code: '.namespace2-z-block {}', 209 | message: `Expected class name "namespace2-z-block" to start with a valid prefix: "namespace2-a-", "namespace2-m-", "namespace2-o-", "namespace2-l-", "namespace2-g-", "namespace2-h-", "namespace2-state-". (${ruleName})`, 210 | }, 211 | { 212 | code: '.namespace2-z-block__element {}', 213 | message: `Expected class name "namespace2-z-block__element" to start with a valid prefix: "namespace2-a-", "namespace2-m-", "namespace2-o-", "namespace2-l-", "namespace2-g-", "namespace2-h-", "namespace2-state-". (${ruleName})`, 214 | }, 215 | { 216 | code: '.namespace2-m-__element {}', 217 | message: `Expected class name "namespace2-m-__element" to use the namespace2-m-[block]__[element] syntax. (${ruleName})`, 218 | }, 219 | { 220 | code: '.namespace2-m--modifier {}', 221 | message: `Expected class name "namespace2-m--modifier" to use the namespace2-m-[block]--[modifier] syntax. (${ruleName})`, 222 | }, 223 | { 224 | code: '.namespace2-state-m--state {}', 225 | message: `Expected class name "namespace2-state-m--state" to use the namespace2-state-m-[block]--[state] syntax. (${ruleName})`, 226 | }, 227 | { 228 | code: '.namespace2-state-m__element {}', 229 | message: `Expected class name "namespace2-state-m__element" to use the namespace2-state-[prefix]-[block]__[element]--[state] syntax. Valid state prefixes: "namespace2-state-a-", "namespace2-state-m-", "namespace2-state-o-", "namespace2-state-l-", "namespace2-state-g-", "namespace2-state-h-". (${ruleName})`, 230 | }, 231 | { 232 | code: '.namespace2-state-block {}', 233 | message: `Expected class name "namespace2-state-block" to use the namespace2-state-[prefix]-[block]--[state] syntax. Valid state prefixes: "namespace2-state-a-", "namespace2-state-m-", "namespace2-state-o-", "namespace2-state-l-", "namespace2-state-g-", "namespace2-state-h-". (${ruleName})`, 234 | }, 235 | { 236 | code: '.namespace2-state-a-block {}', 237 | message: `Expected class name "namespace2-state-a-block" to use the namespace2-state-a-[block]--[state] syntax. (${ruleName})`, 238 | }, 239 | { 240 | code: '.namespace2-a--modifier__block {}', 241 | message: `Expected class name "namespace2-a--modifier__block" to use the namespace2-a-[block]__[element]--[modifier] syntax. (${ruleName})`, 242 | }, 243 | ], 244 | }); 245 | -------------------------------------------------------------------------------- /test/prefixes.test.js: -------------------------------------------------------------------------------- 1 | /* eslint max-len:off */ 2 | const { ruleName } = require('../index'); 3 | 4 | testRule({ 5 | ruleName, 6 | config: { 7 | patternPrefixes: ['a', 'b', 'cc'], 8 | helperPrefixes: ['state', 'is'], 9 | }, 10 | accept: [ 11 | { 12 | code: '.a-block {}', 13 | }, 14 | { 15 | code: '.b-block {}', 16 | }, 17 | { 18 | code: '.cc-block {}', 19 | }, 20 | 21 | { 22 | code: '.state-a-block--state-name {}', 23 | }, 24 | { 25 | code: '.state-b-block--state-name {}', 26 | }, 27 | { 28 | code: '.state-cc-block--state-name {}', 29 | }, 30 | 31 | { 32 | code: '.is-a-block--is-name {}', 33 | }, 34 | { 35 | code: '.is-b-block--is-name {}', 36 | }, 37 | { 38 | code: '.is-cc-block--is-name {}', 39 | }, 40 | 41 | { 42 | code: '.a-block--modifier {}', 43 | }, 44 | { 45 | code: '.b-block--modifier {}', 46 | }, 47 | { 48 | code: '.cc-block--modifier {}', 49 | }, 50 | 51 | { 52 | code: '.a-block__block {}', 53 | }, 54 | { 55 | code: '.b-block__block {}', 56 | }, 57 | { 58 | code: '.cc-block__block {}', 59 | }, 60 | ], 61 | reject: [ 62 | { 63 | code: '.m-block {}', 64 | message: `Expected class name "m-block" to start with a valid prefix: "a-", "b-", "cc-", "state-", "is-". (${ruleName})`, 65 | }, 66 | { 67 | code: '.m-block__element {}', 68 | message: `Expected class name "m-block__element" to start with a valid prefix: "a-", "b-", "cc-", "state-", "is-". (${ruleName})`, 69 | }, 70 | { 71 | code: '.a-__element {}', 72 | message: `Expected class name "a-__element" to use the a-[block]__[element] syntax. (${ruleName})`, 73 | }, 74 | { 75 | code: '.a--modifier {}', 76 | message: `Expected class name "a--modifier" to use the a-[block]--[modifier] syntax. (${ruleName})`, 77 | }, 78 | { 79 | code: '.state-a--state {}', 80 | message: `Expected class name "state-a--state" to use the state-a-[block]--[state] syntax. (${ruleName})`, 81 | }, 82 | { 83 | code: '.is-a--is {}', 84 | message: `Expected class name "is-a--is" to use the is-a-[block]--[is] syntax. (${ruleName})`, 85 | }, 86 | { 87 | code: '.state-a__element {}', 88 | message: `Expected class name "state-a__element" to use the state-[prefix]-[block]__[element]--[state] syntax. Valid state prefixes: "state-a-", "state-b-", "state-cc-". (${ruleName})`, 89 | }, 90 | { 91 | code: '.state-block {}', 92 | message: `Expected class name "state-block" to use the state-[prefix]-[block]--[state] syntax. Valid state prefixes: "state-a-", "state-b-", "state-cc-". (${ruleName})`, 93 | }, 94 | { 95 | code: '.state-a-block {}', 96 | message: `Expected class name "state-a-block" to use the state-a-[block]--[state] syntax. (${ruleName})`, 97 | }, 98 | { 99 | code: '.a--modifier__block {}', 100 | message: `Expected class name "a--modifier__block" to use the a-[block]__[element]--[modifier] syntax. (${ruleName})`, 101 | }, 102 | ], 103 | }); 104 | 105 | // invalid patternPrefixes configuration - uses default 106 | testRule({ 107 | ruleName, 108 | config: { 109 | patternPrefixes: 'string', 110 | }, 111 | // should use default patternPrefixes 112 | accept: [ 113 | { 114 | code: '.a-block {}', 115 | }, 116 | { 117 | code: '.state-a-block--state-name {}', 118 | }, 119 | { 120 | code: '.m-block--modifier {}', 121 | }, 122 | { 123 | code: '.o-block__element {}', 124 | }, 125 | ], 126 | reject: [ 127 | { 128 | code: '.z-block {}', 129 | message: `Expected class name "z-block" to start with a valid prefix: "a-", "m-", "o-", "l-", "g-", "h-", "state-". (${ruleName})`, 130 | }, 131 | { 132 | code: '.-block {}', 133 | message: `Expected class name "-block" to start with a valid prefix: "a-", "m-", "o-", "l-", "g-", "h-", "state-". (${ruleName})`, 134 | }, 135 | { 136 | code: '.a-block--y--x {}', 137 | message: `Expected class name "a-block--y--x" to use only one "--" modifier separator. (${ruleName})`, 138 | }, 139 | { 140 | code: '.state-m--state {}', 141 | message: `Expected class name "state-m--state" to use the state-m-[block]--[state] syntax. (${ruleName})`, 142 | }, 143 | { 144 | code: '.a--modifier__block {}', 145 | message: `Expected class name "a--modifier__block" to use the a-[block]__[element]--[modifier] syntax. (${ruleName})`, 146 | }, 147 | ], 148 | }); 149 | 150 | // empty prefixes 151 | testRule({ 152 | ruleName, 153 | config: { 154 | patternPrefixes: [], 155 | helperPrefixes: [], 156 | }, 157 | accept: [ 158 | { 159 | code: '.block {}', 160 | }, 161 | { 162 | code: '.block__element {}', 163 | }, 164 | { 165 | code: '.block--modifier {}', 166 | }, 167 | { 168 | code: '.block__element--modifier {}', 169 | }, 170 | { 171 | code: '.m--modifier {}', 172 | }, 173 | { 174 | code: '.a-block--state-name {}', 175 | }, 176 | { 177 | code: '.x-block--modifier {}', 178 | }, 179 | { 180 | code: '.f-block__element {}', 181 | }, 182 | { 183 | code: '.z-block__element--modifier {}', 184 | }, 185 | { 186 | code: '.state-m-block {}', 187 | }, 188 | { 189 | code: '.state-m--state {}', 190 | }, 191 | { 192 | code: '.state-a-block--state-name {}', 193 | }, 194 | { 195 | code: '.state-a-block__element--state-name {}', 196 | }, 197 | ], 198 | reject: [ 199 | { 200 | code: '.block___x {}', 201 | message: `Expected class name "block___x" to use only two "_" as element separator. (${ruleName})`, 202 | }, 203 | { 204 | code: '.block---x {}', 205 | message: `Expected class name "block---x" to use only one "--" modifier separator. (${ruleName})`, 206 | }, 207 | { 208 | code: '.block--y--x {}', 209 | message: `Expected class name "block--y--x" to use only one "--" modifier separator. (${ruleName})`, 210 | }, 211 | { 212 | code: '.Block {}', 213 | message: `Expected class name "Block" to contain no uppercase letters. (${ruleName})`, 214 | }, 215 | { 216 | code: '.a-__element {}', 217 | message: `Expected class name "a-__element" to use "-" only for composite names. (${ruleName})`, 218 | }, 219 | { 220 | code: '.-block {}', 221 | message: `Expected class name "-block" to use the [block] syntax. (${ruleName})`, 222 | }, 223 | { 224 | code: '.--block {}', 225 | message: `Expected class name "--block" to use the [block]--[modifier] syntax. (${ruleName})`, 226 | }, 227 | { 228 | code: '.m--modifier__block {}', 229 | message: `Expected class name "m--modifier__block" to use the [block]__[element]--[modifier] syntax. (${ruleName})`, 230 | }, 231 | ], 232 | }); 233 | 234 | // empty pattern prefixes 235 | testRule({ 236 | ruleName, 237 | config: { 238 | patternPrefixes: [], 239 | helperPrefixes: ['is', 'has'], 240 | }, 241 | accept: [ 242 | { 243 | code: '.block {}', 244 | }, 245 | { 246 | code: '.block__element {}', 247 | }, 248 | { 249 | code: '.block--modifier {}', 250 | }, 251 | { 252 | code: '.block__element--modifier {}', 253 | }, 254 | { 255 | code: '.m--modifier {}', 256 | }, 257 | { 258 | code: '.a-block--state-name {}', 259 | }, 260 | { 261 | code: '.x-block--modifier {}', 262 | }, 263 | { 264 | code: '.f-block__element {}', 265 | }, 266 | { 267 | code: '.z-block__element--modifier {}', 268 | }, 269 | { 270 | code: '.is-m--state {}', 271 | }, 272 | { 273 | code: '.is-a-block--state-name {}', 274 | }, 275 | { 276 | code: '.is-a-block__element--state-name {}', 277 | }, 278 | { 279 | code: '.has-a-block__element--state-name {}', 280 | }, 281 | ], 282 | reject: [ 283 | { 284 | code: '.block___x {}', 285 | message: `Expected class name "block___x" to use only two "_" as element separator. (${ruleName})`, 286 | }, 287 | { 288 | code: '.block---x {}', 289 | message: `Expected class name "block---x" to use only one "--" modifier separator. (${ruleName})`, 290 | }, 291 | { 292 | code: '.block--y--x {}', 293 | message: `Expected class name "block--y--x" to use only one "--" modifier separator. (${ruleName})`, 294 | }, 295 | { 296 | code: '.Block {}', 297 | message: `Expected class name "Block" to contain no uppercase letters. (${ruleName})`, 298 | }, 299 | { 300 | code: '.a-__element {}', 301 | message: `Expected class name "a-__element" to use "-" only for composite names. (${ruleName})`, 302 | }, 303 | { 304 | code: '.-block {}', 305 | message: `Expected class name "-block" to use the [block] syntax. (${ruleName})`, 306 | }, 307 | { 308 | code: '.--block {}', 309 | message: `Expected class name "--block" to use the [block]--[modifier] syntax. (${ruleName})`, 310 | }, 311 | { 312 | code: '.m--modifier__block {}', 313 | message: `Expected class name "m--modifier__block" to use the [block]__[element]--[modifier] syntax. (${ruleName})`, 314 | }, 315 | { 316 | code: '.is--state-name {}', 317 | message: `Expected class name "is--state-name" to use the is-[block]--[is] syntax. (${ruleName})`, 318 | }, 319 | { 320 | code: '.is-m-block {}', 321 | message: `Expected class name "is-m-block" to use the is-[block]--[is] syntax. (${ruleName})`, 322 | }, 323 | ], 324 | }); 325 | -------------------------------------------------------------------------------- /test/pseudo-classes.test.js: -------------------------------------------------------------------------------- 1 | /* eslint max-len:off */ 2 | const { ruleName } = require('../index'); 3 | 4 | // Sass and Less edgecases 5 | testRule({ 6 | ruleName, 7 | config: {}, 8 | accept: [ 9 | // Should not conflict with :not pseudo class 10 | { 11 | code: '.h-block__element:not(:first-child) {}', 12 | }, 13 | { 14 | code: '.a-block:not(.a-block--link) {}', 15 | }, 16 | { 17 | code: '.h-block__element:not(.h-block__element--not) {}', 18 | }, 19 | { 20 | code: 'p > :not(strong, b.h-important) {}', 21 | }, 22 | { 23 | code: 'ul li:not(:last-of-type) {}', 24 | }, 25 | // Should not conflict with :is pseudo class 26 | { 27 | code: ':is(ol, ul, menu, dir) :is(ol, ul, menu, dir) :is(ul, menu, dir) {}', 28 | }, 29 | { 30 | code: 'p > :is(strong, b.h-important, .h-strong) {}', 31 | }, 32 | // Should not conflict with :has pseudo class 33 | { 34 | code: '.m-select:has(> .a-icon) {};', 35 | }, 36 | { 37 | code: 'h1:has(+ p.h-lead) {}', 38 | }, 39 | // Should not conflict with combinations of pseudo classes 40 | { 41 | code: '.a-block:is(:not(a)) {}', 42 | }, 43 | { 44 | code: '.a-block:is(:not(.a-block--link)) {}', 45 | }, 46 | { 47 | code: '.a-block:is(:focus:not(.state-a-block--invisible-focus), :hover:not([disabled])) {}', 48 | }, 49 | { 50 | code: '.a-block:is(:not(.a-block--link)) {}', 51 | }, 52 | { 53 | code: '.a-block:is(:hover:not([disabled])) {}', 54 | }, 55 | { 56 | code: '.a-block:is(:focus:not(.a-block--invisible-focus), :hover:not([disabled])) {}', 57 | }, 58 | { 59 | code: '.m-select:has(> .a-icon:not(.a-icon--chevron)) {};', 60 | }, 61 | ], 62 | reject: [ 63 | { 64 | code: 'p > :not(strong, b.important, .h-strong) {}', 65 | message: `Expected class name "important" to start with a valid prefix: "a-", "m-", "o-", "l-", "g-", "h-", "state-". (${ruleName})`, 66 | }, 67 | { 68 | code: '.a-block:is(:focus:not(.has-invisible-focus), :hover:not([disabled])) {}', 69 | message: `Expected class name "has-invisible-focus" to start with a valid prefix: "a-", "m-", "o-", "l-", "g-", "h-", "state-". (${ruleName})`, 70 | }, 71 | { 72 | code: 'h1:has(+ p.lead) {}', 73 | message: `Expected class name "lead" to start with a valid prefix: "a-", "m-", "o-", "l-", "g-", "h-", "state-". (${ruleName})`, 74 | }, 75 | { 76 | code: '.a-block:is(:not(.is-link)) {}', 77 | message: `Expected class name "is-link" to start with a valid prefix: "a-", "m-", "o-", "l-", "g-", "h-", "state-". (${ruleName})`, 78 | }, 79 | ], 80 | }); 81 | --------------------------------------------------------------------------------