UNPKG

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