Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate alpha-value-notation rule to ESM #7182

Merged
merged 1 commit into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions lib/rules/__tests__/index.test.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { execSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
Expand Down Expand Up @@ -61,13 +61,19 @@ describe('custom message option', () => {
test.each(ruleEntries)(
'"%s" should describe a custom message option in its doc',
async (ruleName) => {
const jsFile = path.join(__dirname, '..', ruleName, 'index.js');
let jsFile = path.join(__dirname, '..', ruleName, 'index.mjs');

// TODO: Remove after the migration to ESM.
if (!existsSync(jsFile)) {
jsFile = path.join(__dirname, '..', ruleName, 'index.js');
}

const jsCode = await readFile(jsFile, 'utf8');

// NOTE: If all rules support a custom message option, we should remove this `if` statement.
if (!jsCode.includes('\tmessageArgs: [')) return;

const doc = await readFile(jsFile.replace('index.js', 'README.md'), 'utf8');
const doc = await readFile(jsFile.replace(/index\.m?js$/, 'README.md'), 'utf8');

expect(doc).toContain('`message` secondary option');
},
Expand Down
2 changes: 1 addition & 1 deletion lib/rules/alpha-value-notation/__tests__/index.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { stripIndent } from 'common-tags';

import rule from '../index.js';
import rule from '../index.mjs';
const { messages, ruleName } = rule;

testRule({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';

const valueParser = require('postcss-value-parser');

const validateTypes = require('../../utils/validateTypes.cjs');
const declarationValueIndex = require('../../utils/declarationValueIndex.cjs');
const getDeclarationValue = require('../../utils/getDeclarationValue.cjs');
const isStandardSyntaxValue = require('../../utils/isStandardSyntaxValue.cjs');
Expand All @@ -10,7 +10,6 @@ const report = require('../../utils/report.cjs');
const ruleMessages = require('../../utils/ruleMessages.cjs');
const setDeclarationValue = require('../../utils/setDeclarationValue.cjs');
const validateOptions = require('../../utils/validateOptions.cjs');
const { isRegExp, isString, assert } = require('../../utils/validateTypes.cjs');

const ruleName = 'alpha-value-notation';

Expand Down Expand Up @@ -42,7 +41,7 @@ const rule = (primary, secondaryOptions, context) => {
{
actual: secondaryOptions,
possible: {
exceptProperties: [isString, isRegExp],
exceptProperties: [validateTypes.isString, validateTypes.isRegExp],
},
optional: true,
},
Expand Down Expand Up @@ -159,7 +158,7 @@ function asPercentage(value) {
function asNumber(value) {
const dimension = valueParser.unit(value);

assert(dimension);
validateTypes.assert(dimension);

const number = Number(dimension.number);

Expand Down Expand Up @@ -224,4 +223,5 @@ function isNumber(value) {
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;

module.exports = rule;
225 changes: 225 additions & 0 deletions lib/rules/alpha-value-notation/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import valueParser from 'postcss-value-parser';

import { assert, isRegExp, isString } from '../../utils/validateTypes.mjs';
import declarationValueIndex from '../../utils/declarationValueIndex.mjs';
import getDeclarationValue from '../../utils/getDeclarationValue.mjs';
import isStandardSyntaxValue from '../../utils/isStandardSyntaxValue.mjs';
import optionsMatches from '../../utils/optionsMatches.mjs';
import report from '../../utils/report.mjs';
import ruleMessages from '../../utils/ruleMessages.mjs';
import setDeclarationValue from '../../utils/setDeclarationValue.mjs';
import validateOptions from '../../utils/validateOptions.mjs';

const ruleName = 'alpha-value-notation';

const messages = ruleMessages(ruleName, {
expected: (unfixed, fixed) => `Expected "${unfixed}" to be "${fixed}"`,
});

const meta = {
url: 'https://stylelint.io/user-guide/rules/alpha-value-notation',
fixable: true,
};

const ALPHA_PROPS =
/^(?:opacity|shape-image-threshold|fill-opacity|flood-opacity|stop-opacity|stroke-opacity)$/i;
const ALPHA_FUNCTION = /(?:color|hsla?|rgba?|hwb|lab|lch|oklab|oklch)\(/i;
const ALPHA_FUNCTION_NAME = /^(?:color|hsla?|rgba?|hwb|lab|lch|oklab|oklch)$/i;
const DIGIT = /\d/;

/** @type {import('stylelint').Rule} */
const rule = (primary, secondaryOptions, context) => {
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{
actual: primary,
possible: ['number', 'percentage'],
},
{
actual: secondaryOptions,
possible: {
exceptProperties: [isString, isRegExp],
},
optional: true,
},
);

if (!validOptions) return;

const optionFuncs = Object.freeze({
number: {
expFunc: isNumber,
fixFunc: asNumber,
},
percentage: {
expFunc: isPercentage,
fixFunc: asPercentage,
},
});

root.walkDecls((decl) => {
const declarationValue = getDeclarationValue(decl);
const isAlphaProp = ALPHA_PROPS.test(decl.prop);

if (
// If the property is not an alpha property
(!isAlphaProp &&
// And the value is not an alpha function
!ALPHA_FUNCTION.test(declarationValue)) ||
// Or the value does not contain digits
!DIGIT.test(declarationValue)
) {
// Abort early
return;
}

let needsFix = false;
const parsedValue = valueParser(declarationValue);

parsedValue.walk((node) => {
/** @type {import('postcss-value-parser').Node | undefined} */
let alpha;

if (isAlphaProp && DIGIT.test(node.value)) {
alpha = findAlphaInValue(node);
} else if (node.type === 'function' && ALPHA_FUNCTION_NAME.test(node.value)) {
alpha = findAlphaInFunction(node);
}

if (!alpha) return;

const { value } = alpha;

if (!isStandardSyntaxValue(value)) return;

if (!isNumber(value) && !isPercentage(value)) return;

/** @type {'number' | 'percentage'} */
let expectation = primary;

if (optionsMatches(secondaryOptions, 'exceptProperties', decl.prop)) {
if (expectation === 'number') {
expectation = 'percentage';
} else if (expectation === 'percentage') {
expectation = 'number';
}
}

if (optionFuncs[expectation].expFunc(value)) return;

const fixed = optionFuncs[expectation].fixFunc(value);
const unfixed = value;

if (context.fix) {
alpha.value = String(fixed);
needsFix = true;

return;
}

const index = declarationValueIndex(decl) + alpha.sourceIndex;
const endIndex = index + alpha.value.length;

report({
message: messages.expected,
messageArgs: [unfixed, fixed],
node: decl,
index,
endIndex,
result,
ruleName,
});
});

if (needsFix) {
setDeclarationValue(decl, parsedValue.toString());
}
});
};

Check warning on line 140 in lib/rules/alpha-value-notation/index.mjs

View check run for this annotation

Codecov / codecov/patch

lib/rules/alpha-value-notation/index.mjs#L32-L140

Added lines #L32 - L140 were not covered by tests
};

/**
* @param {string} value
* @returns {string}
*/
function asPercentage(value) {
const number = Number(value);

return `${Number((number * 100).toPrecision(3))}%`;
}

Check warning on line 151 in lib/rules/alpha-value-notation/index.mjs

View check run for this annotation

Codecov / codecov/patch

lib/rules/alpha-value-notation/index.mjs#L147-L151

Added lines #L147 - L151 were not covered by tests

/**
* @param {string} value
* @returns {string}
*/
function asNumber(value) {
const dimension = valueParser.unit(value);

assert(dimension);

const number = Number(dimension.number);

return Number((number / 100).toPrecision(3)).toString();
}

Check warning on line 165 in lib/rules/alpha-value-notation/index.mjs

View check run for this annotation

Codecov / codecov/patch

lib/rules/alpha-value-notation/index.mjs#L157-L165

Added lines #L157 - L165 were not covered by tests

/**
* @template {import('postcss-value-parser').Node} T
* @param {T} node
* @returns {T | undefined}
*/
function findAlphaInValue(node) {
return node.type === 'word' || node.type === 'function' ? node : undefined;
}

Check warning on line 174 in lib/rules/alpha-value-notation/index.mjs

View check run for this annotation

Codecov / codecov/patch

lib/rules/alpha-value-notation/index.mjs#L172-L174

Added lines #L172 - L174 were not covered by tests

/**
* @param {import('postcss-value-parser').FunctionNode} node
* @returns {import('postcss-value-parser').Node | undefined}
*/
function findAlphaInFunction(node) {
const legacySyntax = node.nodes.some(({ type, value }) => type === 'div' && value === ',');

if (legacySyntax) {
const args = node.nodes.filter(({ type }) => type === 'word' || type === 'function');

if (args.length === 4) return args[3];

return undefined;
}

const slashNodeIndex = node.nodes.findIndex(({ type, value }) => type === 'div' && value === '/');

if (slashNodeIndex !== -1) {
const nodesAfterSlash = node.nodes.slice(slashNodeIndex + 1, node.nodes.length);

return nodesAfterSlash.find(({ type }) => type === 'word');
}

return undefined;
}

Check warning on line 200 in lib/rules/alpha-value-notation/index.mjs

View check run for this annotation

Codecov / codecov/patch

lib/rules/alpha-value-notation/index.mjs#L180-L200

Added lines #L180 - L200 were not covered by tests

/**
* @param {string} value
* @returns {boolean}
*/
function isPercentage(value) {
const dimension = valueParser.unit(value);

return dimension && dimension.unit === '%';
}

Check warning on line 210 in lib/rules/alpha-value-notation/index.mjs

View check run for this annotation

Codecov / codecov/patch

lib/rules/alpha-value-notation/index.mjs#L206-L210

Added lines #L206 - L210 were not covered by tests

/**
* @param {string} value
* @returns {boolean}
*/
function isNumber(value) {
const dimension = valueParser.unit(value);

return dimension && dimension.unit === '';
}

Check warning on line 220 in lib/rules/alpha-value-notation/index.mjs

View check run for this annotation

Codecov / codecov/patch

lib/rules/alpha-value-notation/index.mjs#L216-L220

Added lines #L216 - L220 were not covered by tests

rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
export default rule;
2 changes: 1 addition & 1 deletion lib/rules/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const require$1 = node_module.createRequire((typeof document === 'undefined' ? r

/** @type {import('stylelint')['rules']} */
const rules = {
'alpha-value-notation': importLazy(() => require$1('./alpha-value-notation/index.js'))(),
'alpha-value-notation': importLazy(() => require$1('./alpha-value-notation/index.cjs'))(),
'annotation-no-unknown': importLazy(() => require$1('./annotation-no-unknown/index.js'))(),
'at-rule-allowed-list': importLazy(() => require$1('./at-rule-allowed-list/index.js'))(),
'at-rule-disallowed-list': importLazy(() => require$1('./at-rule-disallowed-list/index.js'))(),
Expand Down
2 changes: 1 addition & 1 deletion lib/rules/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import importLazy from 'import-lazy';

/** @type {import('stylelint')['rules']} */
const rules = {
'alpha-value-notation': importLazy(() => require('./alpha-value-notation/index.js'))(),
'alpha-value-notation': importLazy(() => require('./alpha-value-notation/index.cjs'))(),
Mouvedia marked this conversation as resolved.
Show resolved Hide resolved
'annotation-no-unknown': importLazy(() => require('./annotation-no-unknown/index.js'))(),
'at-rule-allowed-list': importLazy(() => require('./at-rule-allowed-list/index.js'))(),
'at-rule-disallowed-list': importLazy(() => require('./at-rule-disallowed-list/index.js'))(),
Expand Down