csslint: add 'shorthand-overrides' rule

This commit is contained in:
tophf 2021-01-04 16:01:33 +03:00
parent cb85fe9392
commit 57233db546
2 changed files with 126 additions and 40 deletions

View File

@ -133,6 +133,7 @@ linterMan.DEFAULTS = {
'errors': 1, 'errors': 1,
'known-properties': 1, 'known-properties': 1,
'selector-newline': 1, 'selector-newline': 1,
'shorthand-overrides': 1,
'simple-not': 1, 'simple-not': 1,
'warnings': 1, 'warnings': 1,
// disabled // disabled

View File

@ -344,6 +344,102 @@ CSSLint.Util = {
} }
if (property) parser.addListener('property', property); if (property) parser.addListener('property', property);
}, },
registerShorthandEvents(parser, {property, endRule}) {
const {shorthands, shorthandsFor} = CSSLint.Util;
let props, inRule;
parser.addListener('startrule', onStartRule);
parser.addListener('startfontface', onStartRule);
parser.addListener('property', onProperty);
parser.addListener('endrule', onEndRule);
parser.addListener('endfontface', onEndRule);
function onStartRule() {
inRule = true;
props = null;
}
function onProperty(event) {
if (!inRule) return;
const name = event.property.text.toLowerCase();
const sh = shorthandsFor[name];
if (sh) {
if (!props) props = {};
(props[sh] || (props[sh] = {}))[name] = event;
} else if (property && props && name in shorthands) {
property(event, props, name);
}
}
function onEndRule(event) {
inRule = false;
if (endRule && props) {
endRule(event, props);
}
}
},
get shorthands() {
const WSC = 'width|style|color';
const TBLR = 'top|bottom|left|right';
const shorthands = Object.create(null);
const shorthandsFor = Object.create(null);
for (const [sh, pattern, ...args] of [
['animation', '%-1',
'name|duration|timing-function|delay|iteration-count|direction|fill-mode|play-state'],
['background', '%-1', 'image|size|position|repeat|origin|clip|attachment|color'],
['border', '%-1-2', TBLR, WSC],
['border-top', '%-1', WSC],
['border-left', '%-1', WSC],
['border-right', '%-1', WSC],
['border-bottom', '%-1', WSC],
['border-block-end', '%-1', WSC],
['border-block-start', '%-1', WSC],
['border-image', '%-1', 'source|slice|width|outset|repeat'],
['border-inline-end', '%-1', WSC],
['border-inline-start', '%-1', WSC],
['border-radius', 'border-1-2-radius', 'top|bottom', 'left|right'],
['border-color', 'border-1-color', TBLR],
['border-style', 'border-1-style', TBLR],
['border-width', 'border-1-width', TBLR],
['column-rule', '%-1', WSC],
['columns', 'column-1', 'width|count'],
['flex', '%-1', 'grow|shrink|basis'],
['flex-flow', 'flex-1', 'direction|wrap'],
['font', '%-style|%-variant|%-weight|%-stretch|%-size|%-family|line-height'],
['grid', '%-1',
'template-rows|template-columns|template-areas|' +
'auto-rows|auto-columns|auto-flow|column-gap|row-gap'],
['grid-area', 'grid-1-2', 'row|column', 'start|end'],
['grid-column', '%-1', 'start|end'],
['grid-gap', 'grid-1-gap', 'row|column'],
['grid-row', '%-1', 'start|end'],
['grid-template', '%-1', 'columns|rows|areas'],
['list-style', 'list-1', 'type|position|image'],
['margin', '%-1', TBLR],
['mask', '%-1', 'image|mode|position|size|repeat|origin|clip|composite'],
['outline', '%-1', WSC],
['padding', '%-1', TBLR],
['text-decoration', '%-1', 'color|style|line'],
['text-emphasis', '%-1', 'style|color'],
['transition', '%-1', 'delay|duration|property|timing-function'],
]) {
let res = pattern.replace(/%/g, sh);
args.forEach((arg, i) => {
res = arg.replace(/[^|]+/g, res.replace(new RegExp(`${i + 1}`, 'g'), '$$&'));
});
(shorthands[sh] = res.split('|')).forEach(r => {
shorthandsFor[r] = sh;
});
}
Object.defineProperties(CSSLint.Util, {
shorthands: {value: shorthands},
shorthandsFor: {value: shorthandsFor},
});
return shorthands;
},
get shorthandsFor() {
return CSSLint.Util.shorthandsFor ||
CSSLint.Util.shorthands && CSSLint.Util.shorthandsFor;
},
}; };
//endregion //endregion
@ -1428,50 +1524,39 @@ CSSLint.addRule({
browsers: 'All', browsers: 'All',
init(parser, reporter) { init(parser, reporter) {
const propertiesToCheck = {}; const {shorthands} = CSSLint.Util;
const mapping = { CSSLint.Util.registerShorthandEvents(parser, {
margin: ['margin-top', 'margin-bottom', 'margin-left', 'margin-right'], endRule: (event, props) => {
padding: ['padding-top', 'padding-bottom', 'padding-left', 'padding-right'], for (const [sh, events] of Object.entries(props)) {
}; const names = Object.keys(events);
let properties; if (names.length === shorthands[sh].length) {
let started = 0; const msg = `'${sh}' shorthand can replace '${names.join("' + '")}'`;
names.forEach(n => reporter.report(msg, events[n].line, events[n].col, this));
for (const short in mapping) {
for (const full of mapping[short]) {
propertiesToCheck[full] = short;
} }
} }
},
});
},
});
const startRule = () => { CSSLint.addRule({
started = 1; id: 'shorthand-overrides',
properties = {}; name: 'Avoid shorthands that override individual properties',
}; desc: 'Avoid shorthands like `background: foo` that follow individual properties ' +
'like `background-image: bar` thus overriding them',
browsers: 'All',
const property = event => { init(parser, reporter) {
if (!started) return; CSSLint.Util.registerShorthandEvents(parser, {
const name = event.property.toString().toLowerCase(); property: (event, props, name) => {
if (name in propertiesToCheck) { const ovr = props[name];
properties[name] = 1; if (ovr) {
} delete props[name];
}; reporter.report(`'${event.property}' overrides '${Object.keys(ovr).join("', '")}' above.`,
const endRule = event => {
started = 0;
for (const short in mapping) {
const fullList = mapping[short];
const total = fullList.reduce((sum = 0, name) => sum + (properties[name] ? 1 : 0));
if (total === fullList.length) {
reporter.report(`The properties ${fullList.join(', ')} can be replaced by ${short}.`,
event.line, event.col, this); event.line, event.col, this);
} }
} },
}; });
parser.addListener('startrule', startRule);
parser.addListener('startfontface', startRule);
parser.addListener('property', property);
parser.addListener('endrule', endRule);
parser.addListener('endfontface', endRule);
}, },
}); });