/
property-conflict-validator.ts
206 lines (180 loc) · 7.49 KB
/
property-conflict-validator.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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
import { MultiMap, TwoKeyMultiMap, objectValues } from "@opticss/util";
import * as propParser from "css-property-parser";
import { postcss } from "opticss";
import { Ruleset, Style } from "../../BlockTree";
import {
isAttrGroup,
isBooleanAttr,
isFalseCondition,
isTrueCondition,
} from "../ElementAnalysis";
import { Validator } from "./Validator";
// Convenience types to help our code read better.
type Pseudo = string;
type Property = string;
type ConflictMap = MultiMap<Property, Ruleset>;
type PropMap = TwoKeyMultiMap<Pseudo, Property, Ruleset>;
/**
* Add all rulesets from a Style to the supplied PropMap
* @param propToBlocks The PropMap to add properties to.
* @param obj The Style object to track.
*/
function add(propToBlocks: PropMap, obj: Style) {
for (let pseudo of obj.rulesets.getPseudos()) {
for (let prop of obj.rulesets.getProperties()) {
propToBlocks.set(pseudo, prop, ...obj.rulesets.getRulesets(prop, pseudo));
}
}
}
/**
* Test if a given Style object is in conflict with with a PropMap.
* If the they are in conflict, store the conflicting Styles list in
* the `conflicts` PropMap.
* @param obj The Style object to we're testing.
* @param propToBlocks The previously encountered properties mapped to the owner Styles.
* @param conflicts Where we store conflicting Style data.
*/
function evaluate(obj: Style, propToRules: PropMap, conflicts: ConflictMap) {
// Ew! Quadruple for loops! Can we come up with a better way to do this!?
// - For each pseudo this Style may effect
// - For each property concern of this Style
// - For each Ruleset we've already seen associated to this prop
// - For each Ruleset relevant to this Style / prop
for (let pseudo of obj.rulesets.getPseudos()) {
for (let prop of obj.rulesets.getProperties(pseudo)) {
for (let other of propToRules.get(pseudo, prop)) {
for (let self of obj.rulesets.getRulesets(prop, pseudo)) {
// If these styles are from the same block, abort!
if (other.style.block === self.style.block) { continue; }
// Get the declarations for this specific property.
let selfDecl = self.declarations.get(prop);
let otherDecl = other.declarations.get(prop);
if (!selfDecl || !otherDecl) { continue; }
// If these declarations have the exact same number of declarations,
// in the exact same order, or if there is an explicit resolution,
// ignore it and move on.
let valuesEqual = selfDecl.length === otherDecl.length;
if (valuesEqual) {
for (let i = 0; i < Math.min(selfDecl.length, otherDecl.length); i++) {
valuesEqual = valuesEqual && selfDecl[i].value === otherDecl[i].value;
}
}
if (valuesEqual ||
other.hasResolutionFor(prop, self.style) ||
self.hasResolutionFor(prop, other.style)
) { continue; }
// Otherwise, we found an unresolved conflict!
conflicts.set(prop, other);
conflicts.set(prop, self);
}
}
}
}
}
/**
* For every shorthand property in our conflicts map, remove all its possible longhand
* expressions that are set to the same value. Do this recursively to catch shorthands
* that expand to other shorthands.
* @param prop The property we're pruning.
* @param conflicts The ConflictMap we're modifying.
*/
function recursivelyPruneConflicts(prop: string, conflicts: ConflictMap): Ruleset[] {
if (propParser.isShorthandProperty(prop)) {
let longhands = propParser.expandShorthandProperty(prop, "inherit", false, true);
for (let longProp of Object.keys(longhands)) {
let rules = recursivelyPruneConflicts(longProp, conflicts);
for (let rule of rules) {
if (conflicts.hasValue(prop, rule)) {
conflicts.deleteValue(longProp, rule);
}
}
}
}
return conflicts.get(prop);
}
/**
* Simple print function for a ruleset conflict error message.
* @param prop The property we're printing on this Ruleset.
* @param rule The Ruleset we're printing.
*/
function printRulesetConflict(prop: string, rule: Ruleset) {
let decl = rule.declarations.get(prop);
let nodes: postcss.Rule[] | postcss.Declaration[] = decl ? decl.map((d) => d.node) : [rule.node];
let out = [];
for (let node of nodes) {
let line = node.source.start && `:${node.source.start.line}`;
let column = node.source.start && `:${node.source.start.column}`;
out.push(` ${rule.style.block.name}${rule.style.asSource()} (${rule.file}${line}${column})`);
}
return out.join("\n");
}
/**
* Prevent conflicting styles from being applied to the same element without an explicit resolution.
* @param correlations The correlations object for a given element.
* @param err Error callback.
*/
export const propertyConflictValidator: Validator = (elAnalysis, _templateAnalysis, err) => {
// Conflicting RuseSets stored here.
let conflicts: ConflictMap = new MultiMap(false);
// Storage for previously encountered Styles
let allConditions: PropMap = new TwoKeyMultiMap(false);
// For each static style, evaluate it and add it to the static store.
elAnalysis.static.forEach((obj) => {
evaluate(obj, allConditions, conflicts);
add(allConditions, obj);
});
// For each dynamic class, test it against the static classes,
// and independently compare the mutually exclusive truthy
// and falsy conditions. Once done, merge all concerns into the
// static store.
elAnalysis.dynamicClasses.forEach((condition) => {
let truthyConditions: PropMap = new TwoKeyMultiMap(false);
let falsyConditions: PropMap = new TwoKeyMultiMap(false);
if (isTrueCondition(condition)) {
condition.whenTrue.forEach((obj) => {
evaluate(obj, allConditions, conflicts);
evaluate(obj, truthyConditions, conflicts);
add(truthyConditions, obj);
});
}
if (isFalseCondition(condition)) {
condition.whenFalse.forEach((obj) => {
evaluate(obj, allConditions, conflicts);
evaluate(obj, falsyConditions, conflicts);
add(falsyConditions, obj);
});
}
allConditions.setAll(truthyConditions);
allConditions.setAll(falsyConditions);
});
// For each dynamic AttrValue, process those in their Attributes independently,
// as they are mutually exclusive. Boolean Attributes are evaluated directly.
elAnalysis.dynamicAttributes.forEach((condition) => {
if (isAttrGroup(condition)) {
let attrConditions: PropMap = new TwoKeyMultiMap(false);
for (let attr of objectValues(condition.group)) {
evaluate(attr, allConditions, conflicts);
add(attrConditions, attr);
}
allConditions.setAll(attrConditions);
}
else if (isBooleanAttr(condition)) {
evaluate(condition.value, allConditions, conflicts);
add(allConditions, condition.value);
}
});
// Prune longhand conflicts that are properly covered by shorthand conflict reports.
for (let prop of conflicts.keys()) {
if (propParser.isShorthandProperty(prop)) { recursivelyPruneConflicts(prop, conflicts); }
}
// For every set of conflicting properties, throw the error.
if (conflicts.size) {
let msg = "The following property conflicts must be resolved for these co-located Styles:";
let details = "\n";
for (let [prop, matches] of conflicts.entries()) {
if (!prop || !matches.length) { return; }
details += ` ${prop}:\n${matches.map((m) => printRulesetConflict(prop, m)).join("\n")}\n\n`;
}
err(msg, null, details);
}
};