001/* 002 * SPDX-FileCopyrightText: none 003 * SPDX-License-Identifier: CC0-1.0 004 */ 005 006package dev.metaschema.modules.sarif; 007 008import org.schemastore.json.sarif.x210.Artifact; 009import org.schemastore.json.sarif.x210.ArtifactLocation; 010import org.schemastore.json.sarif.x210.Invocation; 011import org.schemastore.json.sarif.x210.LetTimingEntry; 012import org.schemastore.json.sarif.x210.Location; 013import org.schemastore.json.sarif.x210.LogicalLocation; 014import org.schemastore.json.sarif.x210.Message; 015import org.schemastore.json.sarif.x210.MultiformatMessageString; 016import org.schemastore.json.sarif.x210.Notification; 017import org.schemastore.json.sarif.x210.PhysicalLocation; 018import org.schemastore.json.sarif.x210.PropertyBag; 019import org.schemastore.json.sarif.x210.Region; 020import org.schemastore.json.sarif.x210.ReportingDescriptor; 021import org.schemastore.json.sarif.x210.Result; 022import org.schemastore.json.sarif.x210.Run; 023import org.schemastore.json.sarif.x210.Sarif; 024import org.schemastore.json.sarif.x210.SarifModule; 025import org.schemastore.json.sarif.x210.TimingData; 026import org.schemastore.json.sarif.x210.Tool; 027import org.schemastore.json.sarif.x210.ToolComponent; 028 029import java.io.IOException; 030import java.io.StringWriter; 031import java.math.BigDecimal; 032import java.math.BigInteger; 033import java.math.RoundingMode; 034import java.net.URI; 035import java.net.URISyntaxException; 036import java.nio.file.Path; 037import java.nio.file.StandardOpenOption; 038import java.time.Instant; 039import java.time.ZoneOffset; 040import java.time.ZonedDateTime; 041import java.util.ArrayList; 042import java.util.Collection; 043import java.util.LinkedHashMap; 044import java.util.LinkedList; 045import java.util.List; 046import java.util.Map; 047import java.util.Set; 048import java.util.UUID; 049import java.util.concurrent.ConcurrentHashMap; 050import java.util.concurrent.atomic.AtomicInteger; 051 052import dev.metaschema.core.datatype.markup.MarkupLine; 053import dev.metaschema.core.datatype.markup.MarkupMultiline; 054import dev.metaschema.core.metapath.item.node.INodeItem; 055import dev.metaschema.core.model.IAttributable; 056import dev.metaschema.core.model.IResourceLocation; 057import dev.metaschema.core.model.MetaschemaException; 058import dev.metaschema.core.model.constraint.ConstraintValidationFinding; 059import dev.metaschema.core.model.constraint.IConstraint; 060import dev.metaschema.core.model.constraint.IConstraint.Level; 061import dev.metaschema.core.model.constraint.ILet; 062import dev.metaschema.core.model.constraint.TimingCollector; 063import dev.metaschema.core.model.constraint.TimingRecord; 064import dev.metaschema.core.model.constraint.ValidationEventListener; 065import dev.metaschema.core.model.constraint.ValidationPhase; 066import dev.metaschema.core.model.validation.IValidationFinding; 067import dev.metaschema.core.model.validation.JsonSchemaContentValidator.JsonValidationFinding; 068import dev.metaschema.core.model.validation.XmlSchemaContentValidator.XmlValidationFinding; 069import dev.metaschema.core.util.CollectionUtil; 070import dev.metaschema.core.util.IVersionInfo; 071import dev.metaschema.core.util.ObjectUtils; 072import dev.metaschema.core.util.UriUtils; 073import dev.metaschema.databind.IBindingContext; 074import dev.metaschema.databind.io.Format; 075import dev.metaschema.databind.io.SerializationFeature; 076import edu.umd.cs.findbugs.annotations.NonNull; 077import edu.umd.cs.findbugs.annotations.Nullable; 078 079/** 080 * Supports building a Static Analysis Results Interchange Format (SARIF) 081 * document based on a set of validation findings. 082 */ 083@SuppressWarnings("PMD.CouplingBetweenObjects") 084public final class SarifValidationHandler implements ValidationEventListener { 085 private enum Kind { 086 NOT_APPLICABLE("notApplicable"), 087 PASS("pass"), 088 FAIL("fail"), 089 REVIEW("review"), 090 OPEN("open"), 091 INFORMATIONAL("informational"); 092 093 @NonNull 094 private final String label; 095 096 Kind(@NonNull String label) { 097 this.label = label; 098 } 099 100 @NonNull 101 public String getLabel() { 102 return label; 103 } 104 } 105 106 private enum SeverityLevel { 107 NONE("none"), 108 NOTE("note"), 109 WARNING("warning"), 110 ERROR("error"); 111 112 @NonNull 113 private final String label; 114 115 SeverityLevel(@NonNull String label) { 116 this.label = label; 117 } 118 119 @NonNull 120 public String getLabel() { 121 return label; 122 } 123 } 124 125 @NonNull 126 static final String SARIF_NS = "https://docs.oasis-open.org/sarif/sarif/v2.1.0"; 127 /** 128 * The property key for specifying a URL that provides help information for a 129 * constraint. 130 */ 131 @NonNull 132 public static final IAttributable.Key SARIF_HELP_URL_KEY 133 = IAttributable.key("help-url", SARIF_NS); 134 /** 135 * The property key for specifying plain text help content for a constraint. 136 */ 137 @NonNull 138 public static final IAttributable.Key SARIF_HELP_TEXT_KEY 139 = IAttributable.key("help-text", SARIF_NS); 140 /** 141 * The property key for specifying markdown-formatted help content for a 142 * constraint. 143 */ 144 @NonNull 145 public static final IAttributable.Key SARIF_HELP_MARKDOWN_KEY 146 = IAttributable.key("help-markdown", SARIF_NS); 147 148 @NonNull 149 private final URI source; 150 @Nullable 151 private final IVersionInfo toolVersion; 152 private final AtomicInteger artifactIndex = new AtomicInteger(-1); 153 private final AtomicInteger ruleIndex = new AtomicInteger(-1); 154 155 @SuppressWarnings("PMD.UseConcurrentHashMap") 156 @NonNull 157 private final Map<URI, ArtifactRecord> artifacts = new LinkedHashMap<>(); 158 @NonNull 159 private final List<AbstractRuleRecord> rules = new LinkedList<>(); 160 @SuppressWarnings("PMD.UseConcurrentHashMap") 161 @NonNull 162 private final Map<IConstraint, ConstraintRuleRecord> constraintRules = new LinkedHashMap<>(); 163 @NonNull 164 private final List<IResult> results = new LinkedList<>(); 165 @NonNull 166 private final SchemaRuleRecord schemaRule = new SchemaRuleRecord(); 167 private boolean schemaValid = true; 168 @Nullable 169 private TimingCollector timingCollector; 170 @NonNull 171 private final Instant constructionTimestamp = Instant.now(); 172 private final ThreadLocal<Long> currentEvaluationStartNanos = new ThreadLocal<>(); 173 private final ThreadLocal<List<ConstraintResult>> currentEvaluationResults = new ThreadLocal<>(); 174 private final ThreadLocal<Long> currentLetStartNanos = new ThreadLocal<>(); 175 @SuppressWarnings("PMD.UseConcurrentHashMap") 176 private final ThreadLocal<Map<ILet, Long>> currentLetDurations = new ThreadLocal<>(); 177 @NonNull 178 private final ConcurrentHashMap<IConstraint, EvaluationTimingSnapshot> evaluationTimings 179 = new ConcurrentHashMap<>(); 180 181 /** 182 * Construct a new validation handler. 183 * 184 * @param source 185 * the URI of the content that was validated 186 * @param toolVersion 187 * the version information for the tool producing the validation 188 * results 189 */ 190 public SarifValidationHandler( 191 @NonNull URI source, 192 @Nullable IVersionInfo toolVersion) { 193 if (!source.isAbsolute()) { 194 throw new IllegalArgumentException(String.format("The source URI '%s' is not absolute.", source.toASCIIString())); 195 } 196 197 this.source = source; 198 this.toolVersion = toolVersion; 199 } 200 201 @NonNull 202 private URI getSource() { 203 return source; 204 } 205 206 private IVersionInfo getToolVersion() { 207 return toolVersion; 208 } 209 210 /** 211 * Set the timing collector to enrich SARIF output with performance data. 212 * <p> 213 * When set, the generated SARIF document will include: 214 * <ul> 215 * <li>An invocation element with start/end timestamps</li> 216 * <li>Phase timing as tool execution notifications</li> 217 * <li>Per-constraint timing in rule properties</li> 218 * </ul> 219 * 220 * @param collector 221 * the timing collector containing measurement data, or {@code null} to 222 * disable timing output 223 */ 224 public void setTimingCollector(@Nullable TimingCollector collector) { 225 this.timingCollector = collector; 226 } 227 228 @Override 229 public void beforeValidation(@NonNull URI document) { 230 // No-op: always-on timing uses construction timestamp 231 } 232 233 @Override 234 public void afterValidation(@NonNull URI document) { 235 // No-op: always-on timing captures end time at SARIF generation 236 } 237 238 @Override 239 public void beforePhase(@NonNull ValidationPhase phase) { 240 // No-op: phase timing is handled by TimingCollector 241 } 242 243 @Override 244 public void afterPhase(@NonNull ValidationPhase phase) { 245 // No-op: phase timing is handled by TimingCollector 246 } 247 248 @Override 249 public void beforeConstraintEvaluation(@NonNull IConstraint constraint, @NonNull INodeItem target) { 250 currentEvaluationStartNanos.set(System.nanoTime()); 251 currentEvaluationResults.set(new ArrayList<>()); 252 currentLetDurations.set(new LinkedHashMap<>()); 253 } 254 255 @SuppressWarnings("PMD.NullAssignment") // ThreadLocal cleanup 256 @Override 257 public void afterConstraintEvaluation(@NonNull IConstraint constraint, @NonNull INodeItem target) { 258 Long startNanos = currentEvaluationStartNanos.get(); 259 List<ConstraintResult> evaluationResults = currentEvaluationResults.get(); 260 Map<ILet, Long> letDurations = currentLetDurations.get(); 261 262 if (startNanos != null) { 263 long durationNs = System.nanoTime() - startNanos; 264 Map<ILet, Long> snapshotLetDurations = letDurations != null && !letDurations.isEmpty() 265 ? new LinkedHashMap<>(letDurations) 266 : null; 267 268 // Set timing on inline results (added during this evaluation) 269 if (evaluationResults != null) { 270 for (ConstraintResult result : evaluationResults) { 271 result.setEvaluationDurationNs(durationNs); 272 if (snapshotLetDurations != null) { 273 result.setLetDurations(snapshotLetDurations); 274 } 275 } 276 } 277 278 // Store for deferred lookup (when findings are added after validation) 279 evaluationTimings.put(constraint, 280 new EvaluationTimingSnapshot(durationNs, snapshotLetDurations)); 281 } 282 283 currentEvaluationStartNanos.remove(); 284 currentEvaluationResults.remove(); 285 currentLetStartNanos.remove(); 286 currentLetDurations.remove(); 287 } 288 289 @Override 290 public void beforeLetEvaluation(@NonNull ILet let) { 291 currentLetStartNanos.set(System.nanoTime()); 292 } 293 294 @Override 295 public void afterLetEvaluation(@NonNull ILet let) { 296 Long startNanos = currentLetStartNanos.get(); 297 Map<ILet, Long> letDurations = currentLetDurations.get(); 298 if (startNanos != null && letDurations != null) { 299 long durationNs = System.nanoTime() - startNanos; 300 letDurations.merge(let, durationNs, Long::sum); 301 } 302 currentLetStartNanos.remove(); 303 } 304 305 @NonNull 306 private static final BigDecimal NS_PER_MS = BigDecimal.valueOf(1_000_000L); 307 308 /** 309 * Convert nanoseconds to milliseconds as a BigDecimal with 3 decimal places. 310 * 311 * @param nanoseconds 312 * the duration in nanoseconds 313 * @return the duration in milliseconds 314 */ 315 @NonNull 316 private static BigDecimal nsToMs(long nanoseconds) { 317 return ObjectUtils.notNull( 318 BigDecimal.valueOf(nanoseconds).divide(NS_PER_MS, 3, RoundingMode.HALF_UP)); 319 } 320 321 /** 322 * Convert a {@link TimingRecord} to a SARIF {@link TimingData} object. 323 * 324 * @param record 325 * the timing record to convert 326 * @return the SARIF timing data 327 */ 328 @NonNull 329 private static TimingData toTimingData(@NonNull TimingRecord record) { 330 TimingData data = new TimingData(); 331 data.setTotalMs(nsToMs(record.getTotalTimeNs())); 332 data.setCount(BigInteger.valueOf(record.getCount())); 333 if (record.getCount() > 0) { 334 data.setMinMs(nsToMs(record.getMinTimeNs())); 335 data.setMaxMs(nsToMs(record.getMaxTimeNs())); 336 } 337 return data; 338 } 339 340 /** 341 * Register a collection of validation finding. 342 * 343 * @param findings 344 * the findings to register 345 */ 346 public void addFindings(@NonNull Collection<? extends IValidationFinding> findings) { 347 for (IValidationFinding finding : findings) { 348 assert finding != null; 349 addFinding(finding); 350 } 351 } 352 353 /** 354 * Register a validation finding. 355 * 356 * @param finding 357 * the finding to register 358 */ 359 public void addFinding(@NonNull IValidationFinding finding) { 360 if (finding instanceof JsonValidationFinding) { 361 addJsonValidationFinding((JsonValidationFinding) finding); 362 } else if (finding instanceof XmlValidationFinding) { 363 addXmlValidationFinding((XmlValidationFinding) finding); 364 } else if (finding instanceof ConstraintValidationFinding) { 365 addConstraintValidationFinding((ConstraintValidationFinding) finding); 366 } else { 367 throw new IllegalStateException(); 368 } 369 } 370 371 private ConstraintRuleRecord getRuleRecord(@NonNull IConstraint constraint) { 372 ConstraintRuleRecord retval = constraintRules.get(constraint); 373 if (retval == null) { 374 retval = new ConstraintRuleRecord(constraint); 375 constraintRules.put(constraint, retval); 376 rules.add(retval); 377 } 378 return retval; 379 } 380 381 private ArtifactRecord getArtifactRecord(@NonNull URI artifactUri) { 382 ArtifactRecord retval = artifacts.get(artifactUri); 383 if (retval == null) { 384 retval = new ArtifactRecord(artifactUri); 385 artifacts.put(artifactUri, retval); 386 } 387 return retval; 388 } 389 390 private void addJsonValidationFinding(@NonNull JsonValidationFinding finding) { 391 results.add(new SchemaResult(finding)); 392 if (schemaValid && IValidationFinding.Kind.FAIL.equals(finding.getKind())) { 393 schemaValid = false; 394 } 395 } 396 397 private void addXmlValidationFinding(@NonNull XmlValidationFinding finding) { 398 results.add(new SchemaResult(finding)); 399 if (schemaValid && IValidationFinding.Kind.FAIL.equals(finding.getKind())) { 400 schemaValid = false; 401 } 402 } 403 404 private void addConstraintValidationFinding(@NonNull ConstraintValidationFinding finding) { 405 ConstraintResult constraintResult = new ConstraintResult(finding); 406 results.add(constraintResult); 407 408 // Track for per-evaluation timing if within a constraint evaluation (inline) 409 List<ConstraintResult> evaluationResults = currentEvaluationResults.get(); 410 if (evaluationResults != null) { 411 evaluationResults.add(constraintResult); 412 } else { 413 // Deferred pattern: look up timing from the most recent evaluation 414 for (IConstraint constraint : finding.getConstraints()) { 415 EvaluationTimingSnapshot snapshot = evaluationTimings.get(constraint); 416 if (snapshot != null) { 417 constraintResult.setEvaluationDurationNs(snapshot.durationNs); 418 if (snapshot.letDurations != null) { 419 constraintResult.setLetDurations(snapshot.letDurations); 420 } 421 break; 422 } 423 } 424 } 425 } 426 427 /** 428 * Generate a SARIF document based on the collected findings. 429 * 430 * @param outputUri 431 * the URI to use as the base for relative paths in the SARIF document 432 * @return the generated SARIF document 433 * @throws IOException 434 * if an error occurred while generating the SARIF document 435 */ 436 @NonNull 437 private Sarif generateSarif(@NonNull URI outputUri) throws IOException { 438 Sarif sarif = new Sarif(); 439 sarif.setVersion("2.1.0"); 440 441 Run run = new Run(); 442 sarif.addRun(run); 443 444 Artifact artifact = new Artifact(); 445 artifact.setLocation(getArtifactRecord(getSource()).generateArtifactLocation(outputUri)); 446 run.addArtifact(artifact); 447 448 for (IResult result : results) { 449 result.generateResults(outputUri).forEach(run::addResult); 450 } 451 452 IVersionInfo toolVersion = getToolVersion(); 453 if (!rules.isEmpty() || toolVersion != null) { 454 Tool tool = new Tool(); 455 ToolComponent driver = new ToolComponent(); 456 457 if (toolVersion != null) { 458 driver.setName(toolVersion.getName()); 459 driver.setVersion(toolVersion.getVersion()); 460 } 461 462 for (AbstractRuleRecord rule : rules) { 463 driver.addRule(rule.generate()); 464 } 465 466 tool.setDriver(driver); 467 run.setTool(tool); 468 } 469 470 enrichWithTiming(run); 471 472 return sarif; 473 } 474 475 /** 476 * Enrich the SARIF run with timing data. 477 * <p> 478 * Always creates an invocation with start/end timestamps (always-on timing). If 479 * a timing collector is set, overrides timestamps from the collector and adds 480 * phase/let-statement timing as tool execution notifications. 481 * 482 * @param run 483 * the SARIF run to enrich 484 */ 485 @SuppressWarnings("PMD.CognitiveComplexity") 486 private void enrichWithTiming(@NonNull Run run) { 487 // Always create invocation with timestamps (always-on timing) 488 Invocation invocation = new Invocation(); 489 invocation.setExecutionSuccessful(Boolean.TRUE); 490 invocation.setStartTimeUtc(ZonedDateTime.ofInstant(constructionTimestamp, ZoneOffset.UTC)); 491 invocation.setEndTimeUtc(ZonedDateTime.ofInstant(Instant.now(), ZoneOffset.UTC)); 492 493 TimingCollector collector = this.timingCollector; 494 if (collector != null) { 495 // Override with collector timestamps if available 496 TimingRecord validationTiming = collector.getValidationTiming(); 497 if (validationTiming != null) { 498 Instant start = validationTiming.getStartTimestampUtc(); 499 if (start != null) { 500 invocation.setStartTimeUtc(ZonedDateTime.ofInstant(start, ZoneOffset.UTC)); 501 } 502 Instant end = validationTiming.getEndTimestampUtc(); 503 if (end != null) { 504 invocation.setEndTimeUtc(ZonedDateTime.ofInstant(end, ZoneOffset.UTC)); 505 } 506 } 507 508 // Add phase timing as notifications 509 for (Map.Entry<ValidationPhase, TimingRecord> entry : collector.getPhaseTimings().entrySet()) { 510 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 511 Notification notification = new Notification(); 512 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 513 Message msg = new Message(); 514 msg.setText("Phase: " + entry.getKey().name()); 515 notification.setMessage(msg); 516 517 TimingRecord phaseRecord = entry.getValue(); 518 Instant phaseEnd = phaseRecord.getEndTimestampUtc(); 519 if (phaseEnd != null) { 520 notification.setTimeUtc(ZonedDateTime.ofInstant(phaseEnd, ZoneOffset.UTC)); 521 } 522 523 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 524 PropertyBag phaseProps = new PropertyBag(); 525 phaseProps.setTiming(toTimingData(phaseRecord)); 526 notification.setProperties(phaseProps); 527 528 invocation.addToolExecutionNotification(notification); 529 } 530 531 // Add let-statement timing as notifications 532 for (Map.Entry<ILet, TimingRecord> entry : collector.getLetTimings().entrySet()) { 533 ILet let = entry.getKey(); 534 535 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 536 Notification notification = new Notification(); 537 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 538 Message msg = new Message(); 539 msg.setText("$" + let.getName().getLocalName() + " := " + let.getValueExpression().getPath()); 540 notification.setMessage(msg); 541 542 TimingRecord letRecord = entry.getValue(); 543 Instant letEnd = letRecord.getEndTimestampUtc(); 544 if (letEnd != null) { 545 notification.setTimeUtc(ZonedDateTime.ofInstant(letEnd, ZoneOffset.UTC)); 546 } 547 548 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 549 PropertyBag letProps = new PropertyBag(); 550 letProps.setTiming(toTimingData(letRecord)); 551 notification.setProperties(letProps); 552 553 invocation.addToolExecutionNotification(notification); 554 } 555 } 556 557 run.addInvocation(invocation); 558 } 559 560 /** 561 * Write the collection of findings to a string in SARIF format. 562 * 563 * @param bindingContext 564 * the context used to access Metaschema module information based on 565 * Java class bindings 566 * @return the SARIF document as a string 567 * @throws IOException 568 * if an error occurred while generating the SARIF document 569 */ 570 @NonNull 571 public String writeToString(@NonNull IBindingContext bindingContext) throws IOException { 572 registerSarifMetaschemaModule(bindingContext); 573 try (StringWriter writer = new StringWriter()) { 574 bindingContext.newSerializer(Format.JSON, Sarif.class) 575 .disableFeature(SerializationFeature.SERIALIZE_ROOT) 576 .serialize(generateSarif(getSource()), writer); 577 return ObjectUtils.notNull(writer.toString()); 578 } 579 } 580 581 /** 582 * Write the collection of findings to the provided output file. 583 * 584 * @param outputFile 585 * the path to the output file to write to 586 * @param bindingContext 587 * the context used to access Metaschema module information based on 588 * Java class bindings 589 * @throws IOException 590 * if an error occurred while writing the SARIF file 591 */ 592 public void write( 593 @NonNull Path outputFile, 594 @NonNull IBindingContext bindingContext) throws IOException { 595 596 URI output = ObjectUtils.notNull(outputFile.toUri()); 597 Sarif sarif = generateSarif(output); 598 599 registerSarifMetaschemaModule(bindingContext); 600 bindingContext.newSerializer(Format.JSON, Sarif.class) 601 .disableFeature(SerializationFeature.SERIALIZE_ROOT) 602 .serialize( 603 sarif, 604 outputFile, 605 StandardOpenOption.CREATE, 606 StandardOpenOption.WRITE, 607 StandardOpenOption.TRUNCATE_EXISTING); 608 } 609 610 private static void registerSarifMetaschemaModule(@NonNull IBindingContext bindingContext) { 611 try { 612 bindingContext.registerModule(SarifModule.class); 613 } catch (MetaschemaException ex) { 614 throw new IllegalStateException("Unable to register the builtin SARIF module.", ex); 615 } 616 } 617 618 private interface IResult { 619 @NonNull 620 IValidationFinding getFinding(); 621 622 @NonNull 623 List<Result> generateResults(@NonNull URI output) throws IOException; 624 } 625 626 private abstract class AbstractResult<T extends IValidationFinding> implements IResult { 627 @NonNull 628 private final T finding; 629 630 protected AbstractResult(@NonNull T finding) { 631 this.finding = finding; 632 } 633 634 @Override 635 public T getFinding() { 636 return finding; 637 } 638 639 @NonNull 640 protected Kind kind(@NonNull IValidationFinding finding) { 641 IValidationFinding.Kind kind = finding.getKind(); 642 643 Kind retval; 644 switch (kind) { 645 case FAIL: 646 retval = Kind.FAIL; 647 break; 648 case INFORMATIONAL: 649 retval = Kind.INFORMATIONAL; 650 break; 651 case NOT_APPLICABLE: 652 retval = Kind.NOT_APPLICABLE; 653 break; 654 case PASS: 655 retval = Kind.PASS; 656 break; 657 default: 658 throw new IllegalArgumentException(String.format("Invalid finding kind '%s'.", kind)); 659 } 660 return retval; 661 } 662 663 @NonNull 664 protected SeverityLevel level(@NonNull Level severity) { 665 SeverityLevel retval; 666 switch (severity) { 667 case CRITICAL: 668 case ERROR: 669 retval = SeverityLevel.ERROR; 670 break; 671 case INFORMATIONAL: 672 case DEBUG: 673 retval = SeverityLevel.NOTE; 674 break; 675 case WARNING: 676 retval = SeverityLevel.WARNING; 677 break; 678 case NONE: 679 retval = SeverityLevel.NONE; 680 break; 681 default: 682 throw new IllegalArgumentException(String.format("Invalid severity '%s'.", severity)); 683 } 684 return retval; 685 } 686 687 protected void message(@NonNull IValidationFinding finding, @NonNull Result result) { 688 String message = finding.getMessage(); 689 if (message == null) { 690 message = ""; 691 } 692 693 Message msg = new Message(); 694 msg.setText(message); 695 result.setMessage(msg); 696 } 697 698 protected void location(@NonNull IValidationFinding finding, @NonNull Result result, @NonNull URI base) 699 throws IOException { 700 IResourceLocation location = finding.getLocation(); 701 if (location != null) { 702 // region 703 Region region = new Region(); 704 705 if (location.getLine() > -1) { 706 region.setStartLine(BigInteger.valueOf(location.getLine())); 707 region.setEndLine(BigInteger.valueOf(location.getLine())); 708 } 709 if (location.getColumn() > -1) { 710 region.setStartColumn(BigInteger.valueOf(location.getColumn() + 1)); 711 region.setEndColumn(BigInteger.valueOf(location.getColumn() + 1)); 712 } 713 if (location.getByteOffset() > -1) { 714 region.setByteOffset(BigInteger.valueOf(location.getByteOffset())); 715 region.setByteLength(BigInteger.ZERO); 716 } 717 if (location.getCharOffset() > -1) { 718 region.setCharOffset(BigInteger.valueOf(location.getCharOffset())); 719 region.setCharLength(BigInteger.ZERO); 720 } 721 722 PhysicalLocation physical = new PhysicalLocation(); 723 724 URI documentUri = finding.getDocumentUri(); 725 if (documentUri != null) { 726 physical.setArtifactLocation(getArtifactRecord(documentUri).generateArtifactLocation(base)); 727 } 728 physical.setRegion(region); 729 730 LogicalLocation logical = new LogicalLocation(); 731 732 logical.setDecoratedName(finding.getPath()); 733 734 Location loc = new Location(); 735 loc.setPhysicalLocation(physical); 736 loc.addLogicalLocation(logical); 737 result.addLocation(loc); 738 } 739 } 740 } 741 742 private final class SchemaResult 743 extends AbstractResult<IValidationFinding> { 744 745 protected SchemaResult(@NonNull IValidationFinding finding) { 746 super(finding); 747 } 748 749 @Override 750 public List<Result> generateResults(@NonNull URI output) throws IOException { 751 IValidationFinding finding = getFinding(); 752 753 Result result = new Result(); 754 755 result.setRuleId(schemaRule.getId()); 756 result.setRuleIndex(BigInteger.valueOf(schemaRule.getIndex())); 757 result.setGuid(schemaRule.getGuid()); 758 759 result.setKind(kind(finding).getLabel()); 760 result.setLevel(level(finding.getSeverity()).getLabel()); 761 message(finding, result); 762 location(finding, result, output); 763 764 return CollectionUtil.singletonList(result); 765 } 766 } 767 768 private final class ConstraintResult 769 extends AbstractResult<ConstraintValidationFinding> { 770 @Nullable 771 private Long evaluationDurationNs; 772 @Nullable 773 private Map<ILet, Long> letDurations; 774 775 protected ConstraintResult(@NonNull ConstraintValidationFinding finding) { 776 super(finding); 777 } 778 779 void setEvaluationDurationNs(long durationNs) { 780 this.evaluationDurationNs = durationNs; 781 } 782 783 void setLetDurations(@NonNull Map<ILet, Long> durations) { 784 this.letDurations = durations; 785 } 786 787 @Override 788 public List<Result> generateResults(@NonNull URI output) throws IOException { 789 ConstraintValidationFinding finding = getFinding(); 790 791 List<Result> retval = new LinkedList<>(); 792 793 Kind kind = kind(finding); 794 SeverityLevel level = level(finding.getSeverity()); 795 796 for (IConstraint constraint : finding.getConstraints()) { 797 assert constraint != null; 798 ConstraintRuleRecord rule = getRuleRecord(constraint); 799 800 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 801 Result result = new Result(); 802 803 String id = constraint.getId(); 804 if (id != null) { 805 result.setRuleId(id); 806 } 807 result.setRuleIndex(BigInteger.valueOf(rule.getIndex())); 808 result.setGuid(rule.getGuid()); 809 result.setKind(kind.getLabel()); 810 result.setLevel(level.getLabel()); 811 message(finding, result); 812 location(finding, result, output); 813 addPerResultTiming(result); 814 815 retval.add(result); 816 } 817 return retval; 818 } 819 820 @SuppressWarnings("PMD.CognitiveComplexity") 821 private void addPerResultTiming(@NonNull Result result) { 822 Long durationNs = this.evaluationDurationNs; 823 if (durationNs == null) { 824 return; 825 } 826 827 PropertyBag props = result.getProperties(); 828 if (props == null) { 829 props = new PropertyBag(); 830 result.setProperties(props); 831 } 832 833 TimingData timing = new TimingData(); 834 timing.setTotalMs(nsToMs(durationNs)); 835 timing.setCount(BigInteger.ONE); 836 props.setTiming(timing); 837 838 Map<ILet, Long> letDurs = this.letDurations; 839 if (letDurs != null) { 840 for (Map.Entry<ILet, Long> entry : letDurs.entrySet()) { 841 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 842 LetTimingEntry letEntry = new LetTimingEntry(); 843 letEntry.setName(entry.getKey().getName().getLocalName()); 844 845 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 846 TimingData letTiming = new TimingData(); 847 letTiming.setTotalMs(nsToMs(entry.getValue())); 848 letTiming.setCount(BigInteger.ONE); 849 letEntry.setTiming(letTiming); 850 851 props.addLetTimingEntry(letEntry); 852 } 853 } 854 } 855 } 856 857 private abstract class AbstractRuleRecord { 858 private final int index; 859 @NonNull 860 private final UUID guid; 861 862 private AbstractRuleRecord() { 863 this.index = ruleIndex.addAndGet(1); 864 this.guid = ObjectUtils.notNull(UUID.randomUUID()); 865 } 866 867 public int getIndex() { 868 return index; 869 } 870 871 @NonNull 872 public UUID getGuid() { 873 return guid; 874 } 875 876 @NonNull 877 protected abstract ReportingDescriptor generate(); 878 } 879 880 private final class SchemaRuleRecord 881 extends AbstractRuleRecord { 882 883 @Override 884 protected ReportingDescriptor generate() { 885 ReportingDescriptor retval = new ReportingDescriptor(); 886 retval.setId(getId()); 887 retval.setGuid(getGuid()); 888 return retval; 889 } 890 891 public String getId() { 892 return "schema-valid"; 893 } 894 } 895 896 private final class ConstraintRuleRecord 897 extends AbstractRuleRecord { 898 @NonNull 899 private final IConstraint constraint; 900 901 public ConstraintRuleRecord(@NonNull IConstraint constraint) { 902 this.constraint = constraint; 903 } 904 905 @NonNull 906 public IConstraint getConstraint() { 907 return constraint; 908 } 909 910 @Override 911 protected ReportingDescriptor generate() { 912 ReportingDescriptor retval = new ReportingDescriptor(); 913 IConstraint constraint = getConstraint(); 914 915 UUID guid = getGuid(); 916 917 String id = constraint.getId(); 918 if (id == null) { 919 retval.setId(guid.toString()); 920 } else { 921 retval.setId(id); 922 } 923 retval.setGuid(guid); 924 String formalName = constraint.getFormalName(); 925 if (formalName != null) { 926 MultiformatMessageString text = new MultiformatMessageString(); 927 text.setText(formalName); 928 retval.setShortDescription(text); 929 } 930 MarkupLine description = constraint.getDescription(); 931 if (description != null) { 932 MultiformatMessageString text = new MultiformatMessageString(); 933 text.setText(description.toText()); 934 text.setMarkdown(description.toMarkdown()); 935 retval.setFullDescription(text); 936 } 937 938 Set<String> helpUrls = constraint.getPropertyValues(SARIF_HELP_URL_KEY); 939 if (!helpUrls.isEmpty()) { 940 retval.setHelpUri(URI.create(helpUrls.stream().findFirst().get())); 941 } 942 943 Set<String> helpText = constraint.getPropertyValues(SARIF_HELP_TEXT_KEY); 944 Set<String> helpMarkdown = constraint.getPropertyValues(SARIF_HELP_MARKDOWN_KEY); 945 // if there is help text or markdown, produce a message 946 if (!helpText.isEmpty() || !helpMarkdown.isEmpty()) { 947 MultiformatMessageString help = new MultiformatMessageString(); 948 949 MarkupMultiline markdown = helpMarkdown.stream().map(MarkupMultiline::fromMarkdown).findFirst().orElse(null); 950 if (markdown != null) { 951 // markdown is provided 952 help.setMarkdown(markdown.toMarkdown()); 953 } 954 955 String text = helpText.isEmpty() 956 ? ObjectUtils.requireNonNull(markdown).toText() // if text is empty, markdown must be provided 957 : helpText.stream().findFirst().get(); // use the provided text 958 help.setText(text); 959 960 retval.setHelp(help); 961 } 962 963 // Add timing data if available 964 TimingCollector collector = timingCollector; 965 if (collector != null) { 966 TimingRecord record = collector.getConstraintTiming(constraint.getInternalIdentifier()); 967 if (record != null) { 968 PropertyBag props = retval.getProperties(); 969 if (props == null) { 970 props = new PropertyBag(); 971 retval.setProperties(props); 972 } 973 props.setTiming(toTimingData(record)); 974 } 975 } 976 977 return retval; 978 } 979 980 } 981 982 private final class ArtifactRecord { 983 @NonNull 984 private final URI uri; 985 private final int index; 986 987 public ArtifactRecord(@NonNull URI uri) { 988 this.uri = uri; 989 this.index = artifactIndex.addAndGet(1); 990 } 991 992 @NonNull 993 public URI getUri() { 994 return uri; 995 } 996 997 public int getIndex() { 998 return index; 999 } 1000 1001 public ArtifactLocation generateArtifactLocation(@NonNull URI baseUri) throws IOException { 1002 ArtifactLocation location = new ArtifactLocation(); 1003 1004 try { 1005 location.setUri(UriUtils.relativize(baseUri, getUri(), true)); 1006 } catch (URISyntaxException ex) { 1007 throw new IOException(ex); 1008 } 1009 1010 location.setIndex(BigInteger.valueOf(getIndex())); 1011 return location; 1012 } 1013 } 1014 1015 /** 1016 * Snapshot of per-evaluation timing data, stored for deferred lookup when 1017 * findings are added after validation completes. 1018 */ 1019 private static final class EvaluationTimingSnapshot { 1020 final long durationNs; 1021 @Nullable 1022 final Map<ILet, Long> letDurations; 1023 1024 EvaluationTimingSnapshot(long durationNs, @Nullable Map<ILet, Long> letDurations) { 1025 this.durationNs = durationNs; 1026 this.letDurations = letDurations; 1027 } 1028 } 1029}