UNPKG

7.42 kBJavaScriptView Raw
1'use strict';
2
3const _ = require('lodash');
4const assignDisabledRanges = require('./assignDisabledRanges');
5const getOsEol = require('./utils/getOsEol');
6const path = require('path');
7const reportUnknownRuleNames = require('./reportUnknownRuleNames');
8const rulesOrder = require('./rules');
9
10/** @typedef {import('stylelint').StylelintInternalApi} StylelintInternalApi */
11/** @typedef {import('stylelint').PostcssResult} PostcssResult */
12/** @typedef {import('stylelint').StylelintPostcssResult} StylelintPostcssResult */
13/** @typedef {import('stylelint').GetLintSourceOptions} Options */
14
15/**
16 * Run stylelint on a PostCSS Result, either one that is provided
17 * or one that we create
18 * @param {StylelintInternalApi} stylelint
19 * @param {Options} options
20 * @returns {Promise<PostcssResult>}
21 */
22module.exports = function lintSource(stylelint, options = {}) {
23 if (!options.filePath && options.code === undefined && !options.existingPostcssResult) {
24 return Promise.reject(new Error('You must provide filePath, code, or existingPostcssResult'));
25 }
26
27 const isCodeNotFile = options.code !== undefined;
28
29 const inputFilePath = isCodeNotFile ? options.codeFilename : options.filePath;
30
31 if (inputFilePath !== undefined && !path.isAbsolute(inputFilePath)) {
32 if (isCodeNotFile) {
33 return Promise.reject(new Error('codeFilename must be an absolute path'));
34 }
35
36 return Promise.reject(new Error('filePath must be an absolute path'));
37 }
38
39 const getIsIgnored = stylelint.isPathIgnored(inputFilePath).catch((err) => {
40 if (isCodeNotFile && err.code === 'ENOENT') return false;
41
42 throw err;
43 });
44
45 return getIsIgnored.then((isIgnored) => {
46 if (isIgnored) {
47 /** @type {PostcssResult} */
48 let postcssResult;
49
50 if (options.existingPostcssResult) {
51 postcssResult = Object.assign(options.existingPostcssResult, {
52 stylelint: createEmptyStylelintPostcssResult(),
53 });
54 } else {
55 postcssResult = createEmptyPostcssResult(inputFilePath);
56 }
57
58 return postcssResult;
59 }
60
61 const configSearchPath = stylelint._options.configFile || inputFilePath;
62
63 const getConfig = stylelint.getConfigForFile(configSearchPath).catch((err) => {
64 if (isCodeNotFile && err.code === 'ENOENT') return stylelint.getConfigForFile(process.cwd());
65
66 throw err;
67 });
68
69 return getConfig.then((result) => {
70 if (!result) {
71 throw new Error('Config file not found');
72 }
73
74 const config = result.config;
75 const existingPostcssResult = options.existingPostcssResult;
76 const stylelintResult = {
77 ruleSeverities: {},
78 customMessages: {},
79 disabledRanges: {},
80 };
81
82 if (existingPostcssResult) {
83 const stylelintPostcssResult = Object.assign(existingPostcssResult, {
84 stylelint: stylelintResult,
85 });
86
87 return lintPostcssResult(stylelint, stylelintPostcssResult, config).then(
88 () => stylelintPostcssResult,
89 );
90 }
91
92 return stylelint
93 ._getPostcssResult({
94 code: options.code,
95 codeFilename: options.codeFilename,
96 filePath: inputFilePath,
97 codeProcessors: config.codeProcessors,
98 })
99 .then((postcssResult) => {
100 const stylelintPostcssResult = Object.assign(postcssResult, {
101 stylelint: stylelintResult,
102 });
103
104 return lintPostcssResult(stylelint, stylelintPostcssResult, config).then(
105 () => stylelintPostcssResult,
106 );
107 });
108 });
109 });
110};
111
112/**
113 * @param {StylelintInternalApi} stylelint
114 * @param {PostcssResult} postcssResult
115 * @param {import('stylelint').StylelintConfig} config
116 * @returns {Promise<any>}
117 */
118function lintPostcssResult(stylelint, postcssResult, config) {
119 postcssResult.stylelint.ruleSeverities = {};
120 postcssResult.stylelint.customMessages = {};
121 postcssResult.stylelint.stylelintError = false;
122 postcssResult.stylelint.quiet = config.quiet;
123
124 /** @type {string} */
125 let newline;
126 const postcssDoc = postcssResult.root;
127
128 if (postcssDoc) {
129 if (!('type' in postcssDoc)) {
130 throw new Error('Unexpected Postcss root object!');
131 }
132
133 // @ts-ignore TODO TYPES property css does not exists
134 const newlineMatch = postcssDoc.source && postcssDoc.source.input.css.match(/\r?\n/);
135
136 newline = newlineMatch ? newlineMatch[0] : getOsEol();
137
138 assignDisabledRanges(postcssDoc, postcssResult);
139 }
140
141 if (stylelint._options.ignoreDisables) {
142 postcssResult.stylelint.ignoreDisables = true;
143 }
144
145 if (stylelint._options.reportNeedlessDisables) {
146 postcssResult.stylelint.reportNeedlessDisables = true;
147 }
148
149 const isFileFixCompatible = isFixCompatible(postcssResult);
150
151 if (!isFileFixCompatible) {
152 postcssResult.stylelint.disableWritingFix = true;
153 }
154
155 const postcssRoots = /** @type {import('postcss').Root[]} */ (postcssDoc &&
156 postcssDoc.constructor.name === 'Document'
157 ? postcssDoc.nodes
158 : [postcssDoc]);
159
160 // Promises for the rules. Although the rule code runs synchronously now,
161 // the use of Promises makes it compatible with the possibility of async
162 // rules down the line.
163 /** @type {Array<Promise<any>>} */
164 const performRules = [];
165
166 const rules = config.rules
167 ? Object.keys(config.rules).sort(
168 (a, b) => Object.keys(rulesOrder).indexOf(a) - Object.keys(rulesOrder).indexOf(b),
169 )
170 : [];
171
172 rules.forEach((ruleName) => {
173 const ruleFunction = rulesOrder[ruleName] || _.get(config, ['pluginFunctions', ruleName]);
174
175 if (ruleFunction === undefined) {
176 performRules.push(
177 Promise.all(
178 postcssRoots.map((postcssRoot) =>
179 reportUnknownRuleNames(ruleName, postcssRoot, postcssResult),
180 ),
181 ),
182 );
183
184 return;
185 }
186
187 const ruleSettings = _.get(config, ['rules', ruleName]);
188
189 if (ruleSettings === null || ruleSettings[0] === null) {
190 return;
191 }
192
193 const primaryOption = ruleSettings[0];
194 const secondaryOptions = ruleSettings[1];
195
196 // Log the rule's severity in the PostCSS result
197 const defaultSeverity = config.defaultSeverity || 'error';
198
199 postcssResult.stylelint.ruleSeverities[ruleName] = _.get(
200 secondaryOptions,
201 'severity',
202 defaultSeverity,
203 );
204 postcssResult.stylelint.customMessages[ruleName] = _.get(secondaryOptions, 'message');
205
206 performRules.push(
207 Promise.all(
208 postcssRoots.map((postcssRoot) =>
209 ruleFunction(primaryOption, secondaryOptions, {
210 fix:
211 stylelint._options.fix &&
212 // Next two conditionals are temporary measures until #2643 is resolved
213 isFileFixCompatible &&
214 !postcssResult.stylelint.disabledRanges[ruleName],
215 newline,
216 })(postcssRoot, postcssResult),
217 ),
218 ),
219 );
220 });
221
222 return Promise.all(performRules);
223}
224
225/**
226 * @returns {StylelintPostcssResult}
227 */
228function createEmptyStylelintPostcssResult() {
229 return {
230 ruleSeverities: {},
231 customMessages: {},
232 disabledRanges: {},
233 ignored: true,
234 stylelintError: false,
235 };
236}
237
238/**
239 * @param {string} [filePath]
240 * @returns {PostcssResult}
241 */
242function createEmptyPostcssResult(filePath) {
243 return {
244 root: {
245 source: {
246 input: { file: filePath },
247 },
248 },
249 messages: [],
250 opts: undefined,
251 stylelint: createEmptyStylelintPostcssResult(),
252 warn: () => {},
253 };
254}
255
256/**
257 * There are currently some bugs in the autofixer of Stylelint.
258 * The autofixer does not yet adhere to stylelint-disable comments, so if there are disabled
259 * ranges we can not autofix this document. More info in issue #2643.
260 *
261 * @param {PostcssResult} postcssResult
262 * @returns {boolean}
263 */
264function isFixCompatible({ stylelint }) {
265 // Check for issue #2643
266 if (stylelint.disabledRanges.all.length) return false;
267
268 return true;
269}