-
Notifications
You must be signed in to change notification settings - Fork 648
/
hint.ts
125 lines (98 loc) · 4.31 KB
/
hint.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
/**
* @fileoverview Ensure vendor-prefixed versions of a CSS property are listed
* before the unprefixed version.
*/
import { vendor, Declaration, Rule } from 'postcss';
import { HintContext } from 'hint/dist/src/lib/hint-context';
import { IHint } from 'hint/dist/src/lib/types';
import { debug as d } from '@hint/utils-debug';
import { getFullCSSCodeSnippet, getCSSLocationFromNode } from '@hint/utils-css';
import { StyleEvents, StyleParse } from '@hint/parser-css';
import { Severity } from '@hint/utils-types';
import meta from './meta';
import { getMessage } from './i18n.import';
const debug: debug.IDebugger = d(__filename);
type DeclarationPair = {
lastPrefixed: Declaration;
unprefixed: Declaration;
};
/** Determine if the order of a prefixed/unprefixed pair is valid. */
const validatePair = (pair: Partial<DeclarationPair>): boolean => {
// Valid if only prefixed or only unprefixed versions exist.
if (!pair.lastPrefixed || !pair.unprefixed) {
return false;
}
const prefixedLocation = getCSSLocationFromNode(pair.lastPrefixed) || { column: 0, line: 0 };
const unprefixedLocation = getCSSLocationFromNode(pair.unprefixed) || { column: 0, line: 0 };
// Valid if last prefixed line is before unprefixed line.
if (prefixedLocation.line < unprefixedLocation.line) {
return false;
}
// Invalid if last prefixed line is after unprefixed line.
if (prefixedLocation.line > unprefixedLocation.line) {
return true;
}
// Both are on the same line: valid only if last prefixed column is first.
return prefixedLocation.column > unprefixedLocation.column;
};
/** Determine if the order of all properties within a rule block are valid. */
const validateRule = (rule: Rule): DeclarationPair[] => {
const map = new Map<string, Partial<DeclarationPair>>();
rule.each((decl) => {
if (!('prop' in decl)) {
return;
}
const name = decl.prop;
const baseName = vendor.unprefixed(name);
const value = decl.value;
const baseValue = vendor.unprefixed(value);
if (!map.has(baseName)) {
map.set(baseName, {});
}
const pair = map.get(baseName)!;
if (name === baseName && value === baseValue) {
pair.unprefixed = decl;
} else {
pair.lastPrefixed = decl;
}
});
return [...map.values()].filter(validatePair) as DeclarationPair[];
};
/*
* ------------------------------------------------------------------------------
* Public
* ------------------------------------------------------------------------------
*/
export default class CssPrefixOrderHint implements IHint {
public static readonly meta = meta;
public constructor(context: HintContext<StyleEvents>) {
/** Generate a report message from an invalid pair. */
const formatMessage = (invalidPair: DeclarationPair): string => {
// Handle prefixed properties (e.g. `appearance` and `-webkit-appearance`).
let name = invalidPair.unprefixed.prop;
let prefixedName = invalidPair.lastPrefixed.prop;
// Handle prefixed values (e.g. `display: grid` and `display: -ms-grid`).
if (name === prefixedName) {
name = `${invalidPair.unprefixed}`;
prefixedName = `${invalidPair.lastPrefixed}`;
}
return getMessage('shouldBeListed', context.language, [name, prefixedName]);
};
context.on('parse::end::css', ({ ast, element, resource }: StyleParse) => {
debug('Validating hint css-prefix-order');
ast.walkRules((rule) => {
for (const invalidPair of validateRule(rule)) {
const message = formatMessage(invalidPair);
const isValue = invalidPair.lastPrefixed.prop === invalidPair.unprefixed.prop;
const location = getCSSLocationFromNode(invalidPair.unprefixed, { isValue });
const codeSnippet = getFullCSSCodeSnippet(invalidPair.unprefixed);
const severity = Severity.warning;
context.report(
resource,
message,
{ codeLanguage: 'css', codeSnippet, element, location, severity });
}
});
});
}
}