stylelint
Version:
A mighty, modern CSS linter.
270 lines (221 loc) • 7.42 kB
JavaScript
const _ = require('lodash');
const assignDisabledRanges = require('./assignDisabledRanges');
const getOsEol = require('./utils/getOsEol');
const path = require('path');
const reportUnknownRuleNames = require('./reportUnknownRuleNames');
const rulesOrder = require('./rules');
/** @typedef {import('stylelint').StylelintInternalApi} StylelintInternalApi */
/** @typedef {import('stylelint').PostcssResult} PostcssResult */
/** @typedef {import('stylelint').StylelintPostcssResult} StylelintPostcssResult */
/** @typedef {import('stylelint').GetLintSourceOptions} Options */
/**
* Run stylelint on a PostCSS Result, either one that is provided
* or one that we create
* @param {StylelintInternalApi} stylelint
* @param {Options} options
* @returns {Promise<PostcssResult>}
*/
module.exports = function lintSource(stylelint, options = {}) {
if (!options.filePath && options.code === undefined && !options.existingPostcssResult) {
return Promise.reject(new Error('You must provide filePath, code, or existingPostcssResult'));
}
const isCodeNotFile = options.code !== undefined;
const inputFilePath = isCodeNotFile ? options.codeFilename : options.filePath;
if (inputFilePath !== undefined && !path.isAbsolute(inputFilePath)) {
if (isCodeNotFile) {
return Promise.reject(new Error('codeFilename must be an absolute path'));
}
return Promise.reject(new Error('filePath must be an absolute path'));
}
const getIsIgnored = stylelint.isPathIgnored(inputFilePath).catch((err) => {
if (isCodeNotFile && err.code === 'ENOENT') return false;
throw err;
});
return getIsIgnored.then((isIgnored) => {
if (isIgnored) {
/** @type {PostcssResult} */
let postcssResult;
if (options.existingPostcssResult) {
postcssResult = Object.assign(options.existingPostcssResult, {
stylelint: createEmptyStylelintPostcssResult(),
});
} else {
postcssResult = createEmptyPostcssResult(inputFilePath);
}
return postcssResult;
}
const configSearchPath = stylelint._options.configFile || inputFilePath;
const getConfig = stylelint.getConfigForFile(configSearchPath).catch((err) => {
if (isCodeNotFile && err.code === 'ENOENT') return stylelint.getConfigForFile(process.cwd());
throw err;
});
return getConfig.then((result) => {
if (!result) {
throw new Error('Config file not found');
}
const config = result.config;
const existingPostcssResult = options.existingPostcssResult;
const stylelintResult = {
ruleSeverities: {},
customMessages: {},
disabledRanges: {},
};
if (existingPostcssResult) {
const stylelintPostcssResult = Object.assign(existingPostcssResult, {
stylelint: stylelintResult,
});
return lintPostcssResult(stylelint, stylelintPostcssResult, config).then(
() => stylelintPostcssResult,
);
}
return stylelint
._getPostcssResult({
code: options.code,
codeFilename: options.codeFilename,
filePath: inputFilePath,
codeProcessors: config.codeProcessors,
})
.then((postcssResult) => {
const stylelintPostcssResult = Object.assign(postcssResult, {
stylelint: stylelintResult,
});
return lintPostcssResult(stylelint, stylelintPostcssResult, config).then(
() => stylelintPostcssResult,
);
});
});
});
};
/**
* @param {StylelintInternalApi} stylelint
* @param {PostcssResult} postcssResult
* @param {import('stylelint').StylelintConfig} config
* @returns {Promise<any>}
*/
function lintPostcssResult(stylelint, postcssResult, config) {
postcssResult.stylelint.ruleSeverities = {};
postcssResult.stylelint.customMessages = {};
postcssResult.stylelint.stylelintError = false;
postcssResult.stylelint.quiet = config.quiet;
/** @type {string} */
let newline;
const postcssDoc = postcssResult.root;
if (postcssDoc) {
if (!('type' in postcssDoc)) {
throw new Error('Unexpected Postcss root object!');
}
// @ts-ignore TODO TYPES property css does not exists
const newlineMatch = postcssDoc.source && postcssDoc.source.input.css.match(/\r?\n/);
newline = newlineMatch ? newlineMatch[0] : getOsEol();
assignDisabledRanges(postcssDoc, postcssResult);
}
if (stylelint._options.ignoreDisables) {
postcssResult.stylelint.ignoreDisables = true;
}
if (stylelint._options.reportNeedlessDisables) {
postcssResult.stylelint.reportNeedlessDisables = true;
}
const isFileFixCompatible = isFixCompatible(postcssResult);
if (!isFileFixCompatible) {
postcssResult.stylelint.disableWritingFix = true;
}
const postcssRoots = /** @type {import('postcss').Root[]} */ (postcssDoc &&
postcssDoc.constructor.name === 'Document'
? postcssDoc.nodes
: [postcssDoc]);
// Promises for the rules. Although the rule code runs synchronously now,
// the use of Promises makes it compatible with the possibility of async
// rules down the line.
/** @type {Array<Promise<any>>} */
const performRules = [];
const rules = config.rules
? Object.keys(config.rules).sort(
(a, b) => Object.keys(rulesOrder).indexOf(a) - Object.keys(rulesOrder).indexOf(b),
)
: [];
rules.forEach((ruleName) => {
const ruleFunction = rulesOrder[ruleName] || _.get(config, ['pluginFunctions', ruleName]);
if (ruleFunction === undefined) {
performRules.push(
Promise.all(
postcssRoots.map((postcssRoot) =>
reportUnknownRuleNames(ruleName, postcssRoot, postcssResult),
),
),
);
return;
}
const ruleSettings = _.get(config, ['rules', ruleName]);
if (ruleSettings === null || ruleSettings[0] === null) {
return;
}
const primaryOption = ruleSettings[0];
const secondaryOptions = ruleSettings[1];
// Log the rule's severity in the PostCSS result
const defaultSeverity = config.defaultSeverity || 'error';
postcssResult.stylelint.ruleSeverities[ruleName] = _.get(
secondaryOptions,
'severity',
defaultSeverity,
);
postcssResult.stylelint.customMessages[ruleName] = _.get(secondaryOptions, 'message');
performRules.push(
Promise.all(
postcssRoots.map((postcssRoot) =>
ruleFunction(primaryOption, secondaryOptions, {
fix:
stylelint._options.fix &&
// Next two conditionals are temporary measures until #2643 is resolved
isFileFixCompatible &&
!postcssResult.stylelint.disabledRanges[ruleName],
newline,
})(postcssRoot, postcssResult),
),
),
);
});
return Promise.all(performRules);
}
/**
* @returns {StylelintPostcssResult}
*/
function createEmptyStylelintPostcssResult() {
return {
ruleSeverities: {},
customMessages: {},
disabledRanges: {},
ignored: true,
stylelintError: false,
};
}
/**
* @param {string} [filePath]
* @returns {PostcssResult}
*/
function createEmptyPostcssResult(filePath) {
return {
root: {
source: {
input: { file: filePath },
},
},
messages: [],
opts: undefined,
stylelint: createEmptyStylelintPostcssResult(),
warn: () => {},
};
}
/**
* There are currently some bugs in the autofixer of Stylelint.
* The autofixer does not yet adhere to stylelint-disable comments, so if there are disabled
* ranges we can not autofix this document. More info in issue #2643.
*
* @param {PostcssResult} postcssResult
* @returns {boolean}
*/
function isFixCompatible({ stylelint }) {
// Check for issue #2643
if (stylelint.disabledRanges.all.length) return false;
return true;
}
;