Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 7 additions & 77 deletions Schema/Models/Schema.Validation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
/// <summary>
/// Gets every element a generator names an output file after, with that name.
/// </summary>
private IEnumerable<(string Name, ISchemaElement Element)> GeneratedFileNames() =>
internal IEnumerable<(string Name, ISchemaElement Element)> GeneratedFileNames() =>
ClassesInternal.Select(c => (c.Name.ToString(), (ISchemaElement)c))
.Concat(EnumsInternal.Select(e => (e.Name.ToString(), (ISchemaElement)e)))
.Concat(InterfacesInternal.Select(i => (i.Name.ToString(), (ISchemaElement)i)))
Expand Down Expand Up @@ -614,7 +614,7 @@
/// A numeric default has to fit the member's type, or each component of its vector, and lie
/// inside its range.
/// </summary>
private static void ValidateNumberDefault(Collection<SchemaValidationIssue> issues, ISchemaMetadataCarrier carrier, NumberDefault number, BaseType type, string path, ISchemaElement element, string kind)

Check failure on line 617 in Schema/Models/Schema.Validation.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 17 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Schema&issues=AaGVqFlTLh6Xm1Y7WWzv&open=AaGVqFlTLh6Xm1Y7WWzv&pullRequest=353
{
if (!type.IsNumeric && !IsVector(type))
{
Expand Down Expand Up @@ -1598,53 +1598,16 @@
Element = dataSource,
});
}
else
else if (CanResolvePaths)
{
ValidateDataSourceFileExists(issues, dataSource);
// Only possible once the schema knows its own location, since the path is relative to it.
// A schema built in memory has no anchor, and its data sources are left unchecked rather
// than resolved against whatever the working directory happens to be.
SchemaEnvironmentValidator.ReportMissingDataSourceFile(issues, dataSource);
}
}
}

/// <summary>
/// Reports a data source whose file is not there.
/// </summary>
/// <remarks>
/// Only possible once the schema knows its own location, since the path is relative to it.
/// A schema built in memory has no anchor, and its data sources are left unchecked rather
/// than resolved against whatever the working directory happens to be.
/// </remarks>
private void ValidateDataSourceFileExists(Collection<SchemaValidationIssue> issues, DataSource dataSource)
{
if (!CanResolvePaths || !dataSource.TryResolveFile(out Semantics.Paths.AbsoluteFilePath resolved))
{
return;
}

try
{
if (!File.Exists(resolved))
{
issues.Add(new()
{
Severity = SchemaValidationSeverity.Error,
Path = dataSource.Name,
Message = $"Data source file '{dataSource.File}' does not exist (resolved to '{resolved}').",
Element = dataSource,
});
}
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
issues.Add(new()
{
Severity = SchemaValidationSeverity.Warning,
Path = dataSource.Name,
Message = $"Data source file '{dataSource.File}' could not be checked: {ex.Message}",
Element = dataSource,
});
}
}

private void ValidateCodeGenerators(Collection<SchemaValidationIssue> issues)
{
foreach (SchemaCodeGenerator codeGenerator in CodeGeneratorsInternal)
Expand Down Expand Up @@ -1682,40 +1645,7 @@
Element = codeGenerator,
});
}
ReportReservedNames(issues, codeGenerator);
}
}

/// <summary>
/// Reports elements named after a file the configured generator writes for itself.
/// </summary>
/// <remarks>
/// The element's file and the generator's own would be the same entry in the output, so one
/// replaces the other on every file system. Matched case-insensitively for the reason
/// <see cref="ReportNamesDifferingOnlyInCase"/> gives.
/// </remarks>
private void ReportReservedNames(Collection<SchemaValidationIssue> issues, SchemaCodeGenerator codeGenerator)
{
if (!Generation.SchemaGenerator.TryGetGenerator(codeGenerator.Language, out Generation.ISchemaCodeGenerator? generator))
{
return;
}

HashSet<string> reserved = new(generator.ReservedNames(codeGenerator), StringComparer.OrdinalIgnoreCase);
if (reserved.Count == 0)
{
return;
}

foreach ((string name, ISchemaElement element) in GeneratedFileNames().Where(entry => reserved.Contains(entry.Name)))
{
issues.Add(new()
{
Severity = SchemaValidationSeverity.Error,
Path = name,
Message = $"'{name}' is the name of a file code generator '{codeGenerator.Name}' writes for itself, so the two would overwrite each other.",
Element = element,
});
SchemaEnvironmentValidator.ReportReservedNames(issues, codeGenerator, GeneratedFileNames());
}
}
}
82 changes: 82 additions & 0 deletions Schema/Models/SchemaEnvironmentValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.Schema.Models;

using System.Collections.ObjectModel;
using ktsu.Schema.Generation;
using ktsu.Semantics.Paths;

/// <summary>
/// The schema checks that consult something outside the schema: the file system and the generator registry.
/// </summary>
/// <remarks>
/// Kept out of <see cref="Schema"/> because every type the schema reaches counts against its
/// class-coupling budget (CA1506), and these checks reach types nothing else in it needs. It also keeps
/// the schema model from depending on the generators, which already depend on it.
/// </remarks>
internal static class SchemaEnvironmentValidator
{
/// <summary>
/// Reports a data source whose file is not there.
/// </summary>
/// <param name="issues">Where to add the issues.</param>
/// <param name="dataSource">A data source of a schema that knows its own location.</param>
internal static void ReportMissingDataSourceFile(Collection<SchemaValidationIssue> issues, DataSource dataSource)
{
if (!dataSource.TryResolveFile(out AbsoluteFilePath resolved))
{
return;
}

// File.Exists answers false rather than throwing for a path it can't read, so there is nothing to catch.
if (!File.Exists(resolved))
{
issues.Add(new()
{
Severity = SchemaValidationSeverity.Error,
Path = dataSource.Name,
Message = $"Data source file '{dataSource.File}' does not exist (resolved to '{resolved}').",
Element = dataSource,
});
}
}

/// <summary>
/// Reports each element whose generated file would collide with one the generator writes for itself.
/// </summary>
/// <remarks>
/// The element's file and the generator's own would be the same entry in the output, so one
/// replaces the other on every file system. Matched case-insensitively, because Windows and macOS
/// file systems ignore case.
/// </remarks>
/// <param name="issues">Where to add the issues.</param>
/// <param name="codeGenerator">The configured generator.</param>
/// <param name="generatedFileNames">Every element a generator names an output file after, with that name.</param>
internal static void ReportReservedNames(
Collection<SchemaValidationIssue> issues,
SchemaCodeGenerator codeGenerator,
IEnumerable<(string Name, ISchemaElement Element)> generatedFileNames)
{
if (!SchemaGenerator.TryGetGenerator(codeGenerator.Language, out ISchemaCodeGenerator? generator))
{
return;
}

HashSet<string> reserved = new(generator.ReservedNames(codeGenerator), StringComparer.OrdinalIgnoreCase);
if (reserved.Count == 0)
{
return;
}

foreach ((string name, ISchemaElement element) in generatedFileNames.Where(entry => reserved.Contains(entry.Name)))
{
issues.Add(new()
{
Severity = SchemaValidationSeverity.Error,
Path = name,
Message = $"'{name}' is the name of a file code generator '{codeGenerator.Name}' writes for itself, so the two would overwrite each other.",
Element = element,
});
}
}
}
Loading