blob: 12cc1393a645b9a6a30ea0d732185c107bceb3e8 [file] [log] [blame]
Mathias Bynens79e2cf02020-05-29 16:46:17 +02001'use strict';
2
Tim van der Lippe16b82282021-11-08 13:50:26 +00003const arrayEqual = require('./arrayEqual');
4const { isPlainObject } = require('is-plain-object');
Mathias Bynens79e2cf02020-05-29 16:46:17 +02005
Tim van der Lippe16b82282021-11-08 13:50:26 +00006const IGNORED_OPTIONS = new Set(['severity', 'message', 'reportDisables', 'disableFix']);
Mathias Bynens79e2cf02020-05-29 16:46:17 +02007
Tim van der Lippe16b82282021-11-08 13:50:26 +00008/** @typedef {import('stylelint').RuleOptions} RuleOptions */
9/** @typedef {import('stylelint').RuleOptionsPossible} Possible */
10/** @typedef {import('stylelint').RuleOptionsPossibleFunc} PossibleFunc */
Mathias Bynens79e2cf02020-05-29 16:46:17 +020011
12/**
13 * Validate a rule's options.
14 *
15 * See existing rules for examples.
16 *
17 * @param {import('stylelint').PostcssResult} result - postcss result
18 * @param {string} ruleName
Tim van der Lippe16b82282021-11-08 13:50:26 +000019 * @param {...RuleOptions} optionDescriptions - Each optionDescription can
Mathias Bynens79e2cf02020-05-29 16:46:17 +020020 * have the following properties:
Tim van der Lippe38208902021-05-11 16:37:59 +010021 * - `actual` (required): the actual passed option value or object.
22 * - `possible` (required): a schema representation of what values are
Mathias Bynens79e2cf02020-05-29 16:46:17 +020023 * valid for those options. `possible` should be an object if the
24 * options are an object, with corresponding keys; if the options are not an
25 * object, `possible` isn't, either. All `possible` value representations
26 * should be **arrays of either values or functions**. Values are === checked
27 * against `actual`. Functions are fed `actual` as an argument and their
28 * return value is interpreted: truthy = valid, falsy = invalid.
29 * - `optional` (optional): If this is `true`, `actual` can be undefined.
30 * @return {boolean} Whether or not the options are valid (true = valid)
31 */
Tim van der Lippe16b82282021-11-08 13:50:26 +000032function validateOptions(result, ruleName, ...optionDescriptions) {
Mathias Bynens79e2cf02020-05-29 16:46:17 +020033 let noErrors = true;
34
35 optionDescriptions.forEach((optionDescription) => {
36 validate(optionDescription, ruleName, complain);
37 });
38
39 /**
40 * @param {string} message
41 */
42 function complain(message) {
43 noErrors = false;
44 result.warn(message, {
45 stylelintType: 'invalidOption',
46 });
Tim van der Lippe16b82282021-11-08 13:50:26 +000047 result.stylelint = result.stylelint || {
48 disabledRanges: {},
49 ruleSeverities: {},
50 customMessages: {},
51 };
52 result.stylelint.stylelintError = true;
Mathias Bynens79e2cf02020-05-29 16:46:17 +020053 }
54
55 return noErrors;
Tim van der Lippe16b82282021-11-08 13:50:26 +000056}
Mathias Bynens79e2cf02020-05-29 16:46:17 +020057
58/**
Tim van der Lippe16b82282021-11-08 13:50:26 +000059 * @param {RuleOptions} opts
Mathias Bynens79e2cf02020-05-29 16:46:17 +020060 * @param {string} ruleName
Tim van der Lippe16b82282021-11-08 13:50:26 +000061 * @param {(message: string) => void} complain
Mathias Bynens79e2cf02020-05-29 16:46:17 +020062 */
63function validate(opts, ruleName, complain) {
64 const possible = opts.possible;
65 const actual = opts.actual;
66 const optional = opts.optional;
67
Tim van der Lippe16b82282021-11-08 13:50:26 +000068 if (actual === null || arrayEqual(actual, [null])) {
Mathias Bynens79e2cf02020-05-29 16:46:17 +020069 return;
70 }
71
72 const nothingPossible =
73 possible === undefined || (Array.isArray(possible) && possible.length === 0);
74
75 if (nothingPossible && actual === true) {
76 return;
77 }
78
79 if (actual === undefined) {
80 if (nothingPossible || optional) {
81 return;
82 }
83
84 complain(`Expected option value for rule "${ruleName}"`);
85
86 return;
87 }
88
89 if (nothingPossible) {
90 if (optional) {
91 complain(
92 `Incorrect configuration for rule "${ruleName}". Rule should have "possible" values for options validation`,
93 );
94
95 return;
96 }
97
98 complain(`Unexpected option value "${String(actual)}" for rule "${ruleName}"`);
99
100 return;
101 }
102
Tim van der Lippe16b82282021-11-08 13:50:26 +0000103 if (typeof possible === 'function') {
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200104 if (!possible(actual)) {
105 complain(`Invalid option "${JSON.stringify(actual)}" for rule ${ruleName}`);
106 }
107
108 return;
109 }
110
111 // If `possible` is an array instead of an object ...
Tim van der Lippe16b82282021-11-08 13:50:26 +0000112 if (Array.isArray(possible)) {
113 for (const a of [actual].flat()) {
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200114 if (isValid(possible, a)) {
Tim van der Lippe16b82282021-11-08 13:50:26 +0000115 continue;
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200116 }
117
118 complain(`Invalid option value "${String(a)}" for rule "${ruleName}"`);
Tim van der Lippe16b82282021-11-08 13:50:26 +0000119 }
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200120
121 return;
122 }
123
124 // If actual is NOT an object ...
Tim van der Lippe16b82282021-11-08 13:50:26 +0000125 if (!isPlainObject(actual) || typeof actual !== 'object' || actual == null) {
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200126 complain(
127 `Invalid option value ${JSON.stringify(actual)} for rule "${ruleName}": should be an object`,
128 );
129
130 return;
131 }
132
Tim van der Lippe16b82282021-11-08 13:50:26 +0000133 for (const [optionName, optionValue] of Object.entries(actual)) {
Tim van der Lippe38208902021-05-11 16:37:59 +0100134 if (IGNORED_OPTIONS.has(optionName)) {
Tim van der Lippe16b82282021-11-08 13:50:26 +0000135 continue;
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200136 }
137
Tim van der Lippe16b82282021-11-08 13:50:26 +0000138 const possibleValue = possible && possible[optionName];
139
140 if (!possibleValue) {
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200141 complain(`Invalid option name "${optionName}" for rule "${ruleName}"`);
142
Tim van der Lippe16b82282021-11-08 13:50:26 +0000143 continue;
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200144 }
145
Tim van der Lippe16b82282021-11-08 13:50:26 +0000146 for (const a of [optionValue].flat()) {
147 if (isValid(possibleValue, a)) {
148 continue;
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200149 }
150
151 complain(`Invalid value "${a}" for option "${optionName}" of rule "${ruleName}"`);
Tim van der Lippe16b82282021-11-08 13:50:26 +0000152 }
153 }
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200154}
155
156/**
Tim van der Lippe16b82282021-11-08 13:50:26 +0000157 * @param {Possible | Possible[]} possible
158 * @param {unknown} actual
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200159 * @returns {boolean}
160 */
161function isValid(possible, actual) {
Tim van der Lippe16b82282021-11-08 13:50:26 +0000162 for (const possibility of [possible].flat()) {
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200163 if (typeof possibility === 'function' && possibility(actual)) {
164 return true;
165 }
166
167 if (actual === possibility) {
168 return true;
169 }
170 }
171
172 return false;
173}
Tim van der Lippe16b82282021-11-08 13:50:26 +0000174
175module.exports = /** @type {typeof import('stylelint').utils.validateOptions} */ (validateOptions);