source: josm/trunk/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerRule.java

Last change on this file was 18960, checked in by GerdP, 3 months ago

fix #23397: Improve the results of partial validations

  • pass also parent ways and relations of uploaded/selected objects to the testers, child objects are already added, this allows to find e.g. overlapping polygons problems with tags in members of relations
  • let CrossingWays find a problem if at least one of the crossing ways is in the partial selection.
  • let DuplicatWays find a problem if at least one of the duplicated ways is in the partial selection
  • add code to filter the detected issues so that those issues which are clearly not related to the original list of objects are removed. A few issues from mapcss tests may remain.
  • add new preference validator.partial.add.parents to disable the addition of parent objects, default is enabled
  • add new preference validator.partial.removeIrrelevant to disable the filtering of irrelevant errors, default is enabled
  • duplicated code to call AggregatePrimitivesVisitor was moved to ValidationTask
File size: 18.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.validation.tests;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.geom.Area;
7import java.io.Reader;
8import java.io.StringReader;
9import java.util.ArrayList;
10import java.util.Collection;
11import java.util.HashMap;
12import java.util.HashSet;
13import java.util.List;
14import java.util.Map;
15import java.util.Objects;
16import java.util.Optional;
17import java.util.Set;
18import java.util.function.Consumer;
19import java.util.function.Predicate;
20import java.util.stream.Collectors;
21
22import org.openstreetmap.josm.command.Command;
23import org.openstreetmap.josm.command.DeleteCommand;
24import org.openstreetmap.josm.command.SequenceCommand;
25import org.openstreetmap.josm.data.osm.IPrimitive;
26import org.openstreetmap.josm.data.osm.OsmPrimitive;
27import org.openstreetmap.josm.data.osm.Way;
28import org.openstreetmap.josm.data.osm.WaySegment;
29import org.openstreetmap.josm.data.validation.Severity;
30import org.openstreetmap.josm.data.validation.Test;
31import org.openstreetmap.josm.data.validation.TestError;
32import org.openstreetmap.josm.gui.mappaint.Environment;
33import org.openstreetmap.josm.gui.mappaint.Keyword;
34import org.openstreetmap.josm.gui.mappaint.MultiCascade;
35import org.openstreetmap.josm.gui.mappaint.mapcss.Condition;
36import org.openstreetmap.josm.gui.mappaint.mapcss.ConditionFactory.ClassCondition;
37import org.openstreetmap.josm.gui.mappaint.mapcss.Expression;
38import org.openstreetmap.josm.gui.mappaint.mapcss.Instruction;
39import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSRule;
40import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
41import org.openstreetmap.josm.gui.mappaint.mapcss.PlaceholderExpression;
42import org.openstreetmap.josm.gui.mappaint.mapcss.Selector;
43import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.MapCSSParser;
44import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.ParseException;
45import org.openstreetmap.josm.io.IllegalDataException;
46import org.openstreetmap.josm.tools.CheckParameterUtil;
47import org.openstreetmap.josm.tools.Logging;
48import org.openstreetmap.josm.tools.Utils;
49
50/**
51 * Tag check.
52 */
53final class MapCSSTagCheckerRule implements Predicate<OsmPrimitive> {
54 /**
55 * The selector of this {@code TagCheck}
56 */
57 final MapCSSRule rule;
58 /**
59 * Commands to apply in order to fix a matching primitive
60 */
61 final List<MapCSSTagCheckerFixCommand> fixCommands;
62 /**
63 * Tags (or arbitrary strings) of alternatives to be presented to the user
64 */
65 final List<String> alternatives;
66 /**
67 * An {@link org.openstreetmap.josm.gui.mappaint.mapcss.Instruction.AssignmentInstruction}-{@link Severity} pair.
68 * Is evaluated on the matching primitive to give the error message. Map is checked to contain exactly one element.
69 */
70 final Map<Instruction.AssignmentInstruction, Severity> errors;
71 /**
72 * MapCSS Classes to set on matching primitives
73 */
74 final Collection<String> setClassExpressions;
75 /**
76 * Denotes whether the object should be deleted for fixing it
77 */
78 boolean deletion;
79 /**
80 * A string used to group similar tests
81 */
82 String group;
83
84 MapCSSTagCheckerRule(MapCSSRule rule) {
85 this.rule = rule;
86 this.fixCommands = new ArrayList<>();
87 this.alternatives = new ArrayList<>();
88 this.errors = new HashMap<>();
89 this.setClassExpressions = new HashSet<>();
90 }
91
92 MapCSSTagCheckerRule(MapCSSTagCheckerRule check) {
93 this.rule = check.rule;
94 this.fixCommands = Utils.toUnmodifiableList(check.fixCommands);
95 this.alternatives = Utils.toUnmodifiableList(check.alternatives);
96 this.errors = Utils.toUnmodifiableMap(check.errors);
97 this.setClassExpressions = Utils.toUnmodifiableList(check.setClassExpressions);
98 this.deletion = check.deletion;
99 this.group = check.group;
100 }
101
102 MapCSSTagCheckerRule toImmutable() {
103 return new MapCSSTagCheckerRule(this);
104 }
105
106 private static final String POSSIBLE_THROWS = "throwError/throwWarning/throwOther";
107
108 static MapCSSTagCheckerRule ofMapCSSRule(final MapCSSRule rule, Consumer<String> assertionConsumer) throws IllegalDataException {
109 final MapCSSTagCheckerRule check = new MapCSSTagCheckerRule(rule);
110 final Map<String, Boolean> assertions = new HashMap<>();
111 for (Instruction i : rule.declaration.instructions) {
112 if (i instanceof Instruction.AssignmentInstruction) {
113 final Instruction.AssignmentInstruction ai = (Instruction.AssignmentInstruction) i;
114 if (ai.isSetInstruction) {
115 check.setClassExpressions.add(ai.key);
116 continue;
117 }
118 try {
119 final String val = ai.val instanceof Expression
120 ? Optional.ofNullable(((Expression) ai.val).evaluate(new Environment()))
121 .map(Object::toString).map(String::intern).orElse(null)
122 : ai.val instanceof String
123 ? (String) ai.val
124 : ai.val instanceof Keyword
125 ? ((Keyword) ai.val).val
126 : null;
127 if ("throwError".equals(ai.key)) {
128 check.errors.put(ai, Severity.ERROR);
129 } else if ("throwWarning".equals(ai.key)) {
130 check.errors.put(ai, Severity.WARNING);
131 } else if ("throwOther".equals(ai.key)) {
132 check.errors.put(ai, Severity.OTHER);
133 } else if (ai.key.startsWith("throw")) {
134 Logging.log(Logging.LEVEL_WARN,
135 "Unsupported " + ai.key + " instruction. Allowed instructions are " + POSSIBLE_THROWS + '.', null);
136 } else if ("fixAdd".equals(ai.key)) {
137 check.fixCommands.add(MapCSSTagCheckerFixCommand.fixAdd(ai.val));
138 } else if ("fixRemove".equals(ai.key)) {
139 CheckParameterUtil.ensureThat(!(ai.val instanceof String) || !(val != null && val.contains("=")),
140 "Unexpected '='. Please only specify the key to remove in: " + ai);
141 check.fixCommands.add(MapCSSTagCheckerFixCommand.fixRemove(ai.val));
142 } else if (val != null && "fixChangeKey".equals(ai.key)) {
143 CheckParameterUtil.ensureThat(val.contains("=>"), "Separate old from new key by '=>'!");
144 final String[] x = val.split("=>", 2);
145 final String oldKey = Utils.removeWhiteSpaces(x[0]);
146 final String newKey = Utils.removeWhiteSpaces(x[1]);
147 check.fixCommands.add(MapCSSTagCheckerFixCommand.fixChangeKey(oldKey, newKey));
148 } else if (val != null && "fixDeleteObject".equals(ai.key)) {
149 CheckParameterUtil.ensureThat("this".equals(val), "fixDeleteObject must be followed by 'this'");
150 check.deletion = true;
151 } else if (val != null && "suggestAlternative".equals(ai.key)) {
152 check.alternatives.add(val);
153 } else if (val != null && "assertMatch".equals(ai.key)) {
154 assertions.put(val, Boolean.TRUE);
155 } else if (val != null && "assertNoMatch".equals(ai.key)) {
156 assertions.put(val, Boolean.FALSE);
157 } else if (val != null && "group".equals(ai.key)) {
158 check.group = val;
159 } else if (ai.key.startsWith("-")) {
160 Logging.debug("Ignoring extension instruction: " + ai.key + ": " + ai.val);
161 } else {
162 throw new IllegalDataException("Cannot add instruction " + ai.key + ": " + ai.val + '!');
163 }
164 } catch (IllegalArgumentException e) {
165 throw new IllegalDataException(e);
166 }
167 }
168 }
169 if (check.errors.isEmpty() && check.setClassExpressions.isEmpty()) {
170 throw new IllegalDataException(
171 "No " + POSSIBLE_THROWS + " given! You should specify a validation error message for " + rule.selectors);
172 } else if (check.errors.size() > 1) {
173 throw new IllegalDataException(
174 "More than one " + POSSIBLE_THROWS + " given! You should specify a single validation error message for "
175 + rule.selectors);
176 }
177 if (assertionConsumer != null) {
178 MapCSSTagCheckerAsserts.checkAsserts(check, assertions, assertionConsumer);
179 }
180 return check.toImmutable();
181 }
182
183 static MapCSSTagChecker.ParseResult readMapCSS(Reader css) throws ParseException {
184 return readMapCSS(css, null);
185 }
186
187 static MapCSSTagChecker.ParseResult readMapCSS(Reader css, Consumer<String> assertionConsumer) throws ParseException {
188 CheckParameterUtil.ensureParameterNotNull(css, "css");
189
190 final MapCSSStyleSource source = new MapCSSStyleSource("");
191 final MapCSSParser preprocessor = new MapCSSParser(css, MapCSSParser.LexicalState.PREPROCESSOR);
192 try (StringReader mapcss = new StringReader(preprocessor.pp_root(source))) {
193 new MapCSSParser(mapcss, MapCSSParser.LexicalState.DEFAULT).sheet(source);
194 }
195 // Ignore "meta" rule(s) from external rules of JOSM wiki
196 source.removeMetaRules();
197 List<MapCSSTagCheckerRule> parseChecks = new ArrayList<>();
198 for (MapCSSRule rule : source.rules) {
199 try {
200 parseChecks.add(MapCSSTagCheckerRule.ofMapCSSRule(rule, assertionConsumer));
201 } catch (IllegalDataException e) {
202 Logging.error("Cannot add MapCSS rule: " + e.getMessage());
203 source.logError(e);
204 }
205 }
206 return new MapCSSTagChecker.ParseResult(parseChecks, source.getErrors());
207 }
208
209 @Override
210 public boolean test(OsmPrimitive primitive) {
211 // Tests whether the primitive contains a deprecated tag which is represented by this MapCSSTagChecker.
212 return whichSelectorMatchesPrimitive(primitive) != null;
213 }
214
215 Selector whichSelectorMatchesPrimitive(OsmPrimitive primitive) {
216 return whichSelectorMatchesEnvironment(new Environment(primitive, new MultiCascade(), Environment.DEFAULT_LAYER, null));
217 }
218
219 Selector whichSelectorMatchesEnvironment(Environment env) {
220 return rule.selectors.stream()
221 .filter(i -> i.matches(env.clearSelectorMatchingInformation()))
222 .findFirst()
223 .orElse(null);
224 }
225
226 /**
227 * Replaces occurrences of <code>{i.key}</code>, <code>{i.value}</code>, <code>{i.tag}</code> in {@code s} by the corresponding
228 * key/value/tag of the {@code index}-th {@link Condition} of {@code matchingSelector}.
229 *
230 * @param matchingSelector matching selector
231 * @param s any string
232 * @param p OSM primitive
233 * @return string with arguments inserted
234 */
235 static String insertArguments(Selector matchingSelector, String s, OsmPrimitive p) {
236 return PlaceholderExpression.insertArguments(matchingSelector, s, p);
237 }
238
239 /**
240 * Constructs a fix in terms of a {@link org.openstreetmap.josm.command.Command} for the {@link OsmPrimitive}
241 * if the error is fixable, or {@code null} otherwise.
242 *
243 * @param p the primitive to construct the fix for
244 * @return the fix or {@code null}
245 */
246 Command fixPrimitive(OsmPrimitive p) {
247 if (p.getDataSet() == null || (fixCommands.isEmpty() && !deletion)) {
248 return null;
249 }
250 try {
251 final Selector matchingSelector = whichSelectorMatchesPrimitive(p);
252 Collection<Command> cmds = fixCommands.stream()
253 .map(fixCommand -> fixCommand.createCommand(p, matchingSelector))
254 .filter(Objects::nonNull)
255 .collect(Collectors.toList());
256 if (deletion && !p.isDeleted()) {
257 cmds.add(new DeleteCommand(p));
258 }
259 return cmds.isEmpty() ? null
260 : new SequenceCommand(tr("Fix of {0}", getDescriptionForMatchingSelector(p, matchingSelector)), cmds);
261 } catch (IllegalArgumentException e) {
262 Logging.error(e);
263 return null;
264 }
265 }
266
267 /**
268 * Constructs a (localized) message for this deprecation check.
269 *
270 * @param selector The selector to use
271 * @param p OSM primitive
272 * @return a message
273 */
274 String getMessage(Selector selector, OsmPrimitive p) {
275 if (errors.isEmpty()) {
276 // Return something to avoid NPEs
277 return rule.declaration.toString();
278 } else {
279 final Object val = errors.keySet().iterator().next().val;
280 selector = selector == null && p != null ? whichSelectorMatchesPrimitive(p) : selector;
281 return String.valueOf(
282 val instanceof Expression
283 ? ((Expression) val).evaluate(new Environment(p).withSelector(selector))
284 : val
285 );
286 }
287 }
288
289 /**
290 * Constructs a (localized) description for this deprecation check.
291 *
292 * @param selector The selector to use
293 * @param p OSM primitive
294 * @return a description (possibly with alternative suggestions)
295 * @see #getDescriptionForMatchingSelector
296 */
297 String getDescription(Selector selector, OsmPrimitive p) {
298 if (alternatives.isEmpty()) {
299 return getMessage(selector, p);
300 } else {
301 /* I18N: {0} is the test error message and {1} is an alternative */
302 return tr("{0}, use {1} instead", getMessage(selector, p), String.join(tr(" or "), alternatives));
303 }
304 }
305
306 /**
307 * Constructs a (localized) description for this deprecation check
308 * where any placeholders are replaced by values of the matched selector.
309 *
310 * @param matchingSelector matching selector
311 * @param p OSM primitive
312 * @return a description (possibly with alternative suggestions)
313 */
314 String getDescriptionForMatchingSelector(OsmPrimitive p, Selector matchingSelector) {
315 return insertArguments(matchingSelector, getDescription(matchingSelector, p), p);
316 }
317
318 Severity getSeverity() {
319 return errors.isEmpty() ? null : errors.values().iterator().next();
320 }
321
322 @Override
323 public String toString() {
324 return getDescription(null, null);
325 }
326
327 /**
328 * Constructs a {@link TestError} for the given primitive, or returns null if the primitive does not give rise to an error.
329 *
330 * @param p the primitive to construct the error for
331 * @param matchingSelector the matching selector (e.g., obtained via {@link #whichSelectorMatchesPrimitive})
332 * @param env the environment
333 * @param tester the tester
334 * @return an instance of {@link TestError}, or returns null if the primitive does not give rise to an error.
335 */
336 List<TestError> getErrorsForPrimitive(OsmPrimitive p, Selector matchingSelector, Environment env, Test tester) {
337 List<TestError> res = new ArrayList<>();
338 if (matchingSelector != null && !errors.isEmpty()) {
339 final Command fix = fixPrimitive(p);
340 final String description = getDescriptionForMatchingSelector(p, matchingSelector);
341 final String description1 = group == null ? description : group;
342 final String description2 = group == null ? null : description;
343 final String selector = matchingSelector.toString();
344 TestError.Builder errorBuilder = TestError.builder(tester, getSeverity(), 3000)
345 .messageWithManuallyTranslatedDescription(description1, description2, selector);
346 if (fix != null) {
347 errorBuilder.fix(() -> fix);
348 }
349 if (env.child instanceof OsmPrimitive) {
350 res.add(errorBuilder.primitives(p, (OsmPrimitive) env.child).build());
351 } else if (env.children != null) {
352 for (IPrimitive c : env.children) {
353 if (c instanceof OsmPrimitive) {
354 errorBuilder = TestError.builder(tester, getSeverity(), 3000)
355 .messageWithManuallyTranslatedDescription(description1, description2, selector);
356 if (fix != null) {
357 errorBuilder.fix(() -> fix);
358 }
359 // check if we have special information about highlighted objects */
360 boolean hiliteFound = false;
361 if (env.intersections != null) {
362 Area is = env.intersections.get(c);
363 if (is != null) {
364 errorBuilder.highlight(is);
365 hiliteFound = true;
366 }
367 }
368 if (env.crossingWaysMap != null && !hiliteFound) {
369 Map<List<Way>, List<WaySegment>> is = env.crossingWaysMap.get(c);
370 if (is != null) {
371 Set<WaySegment> toHilite = new HashSet<>();
372 for (List<WaySegment> wsList : is.values()) {
373 toHilite.addAll(wsList);
374 }
375 errorBuilder.highlightWaySegments(toHilite);
376 }
377 }
378 res.add(errorBuilder.primitives(p, (OsmPrimitive) c).build());
379 }
380 }
381 } else if (env.parent != null) {
382 boolean imcompletePrimitives = false;
383 if (matchingSelector instanceof Selector.ChildOrParentSelector) {
384 Selector right = ((Selector.ChildOrParentSelector) matchingSelector).right;
385 if (right.getConditions().stream().anyMatch(ClassCondition.class::isInstance)) {
386 // see #23397
387 // TODO: find a way to collect all and only those parent objects which triggered this error
388 imcompletePrimitives = true;
389 }
390 }
391 if (imcompletePrimitives)
392 res.add(errorBuilder.primitives(p).highlight(p).imcompletePrimitives().build());
393 else
394 res.add(errorBuilder.primitives(p, (OsmPrimitive) env.parent).highlight(p).build());
395 } else {
396 res.add(errorBuilder.primitives(p).build());
397 }
398 }
399 return res;
400 }
401
402}
Note: See TracBrowser for help on using the repository browser.