diff --git a/Schema/Models/Schema.Validation.cs b/Schema/Models/Schema.Validation.cs index f922ae4..623eb7d 100644 --- a/Schema/Models/Schema.Validation.cs +++ b/Schema/Models/Schema.Validation.cs @@ -54,7 +54,7 @@ private void ValidateUniqueNames(Collection issues) /// /// Gets every element a generator names an output file after, with that name. /// - 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))) @@ -1598,53 +1598,16 @@ private void ValidateDataSources(Collection issues) 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); } } } - /// - /// Reports a data source whose file is not there. - /// - /// - /// 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. - /// - private void ValidateDataSourceFileExists(Collection 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 issues) { foreach (SchemaCodeGenerator codeGenerator in CodeGeneratorsInternal) @@ -1682,40 +1645,7 @@ private void ValidateCodeGenerators(Collection issues) Element = codeGenerator, }); } - ReportReservedNames(issues, codeGenerator); - } - } - - /// - /// Reports elements named after a file the configured generator writes for itself. - /// - /// - /// 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 - /// gives. - /// - private void ReportReservedNames(Collection issues, SchemaCodeGenerator codeGenerator) - { - if (!Generation.SchemaGenerator.TryGetGenerator(codeGenerator.Language, out Generation.ISchemaCodeGenerator? generator)) - { - return; - } - - HashSet 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()); } } } diff --git a/Schema/Models/SchemaEnvironmentValidator.cs b/Schema/Models/SchemaEnvironmentValidator.cs new file mode 100644 index 0000000..2a77f7e --- /dev/null +++ b/Schema/Models/SchemaEnvironmentValidator.cs @@ -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; + +/// +/// The schema checks that consult something outside the schema: the file system and the generator registry. +/// +/// +/// Kept out of 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. +/// +internal static class SchemaEnvironmentValidator +{ + /// + /// Reports a data source whose file is not there. + /// + /// Where to add the issues. + /// A data source of a schema that knows its own location. + internal static void ReportMissingDataSourceFile(Collection 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, + }); + } + } + + /// + /// Reports each element whose generated file would collide with one the generator writes for itself. + /// + /// + /// 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. + /// + /// Where to add the issues. + /// The configured generator. + /// Every element a generator names an output file after, with that name. + internal static void ReportReservedNames( + Collection issues, + SchemaCodeGenerator codeGenerator, + IEnumerable<(string Name, ISchemaElement Element)> generatedFileNames) + { + if (!SchemaGenerator.TryGetGenerator(codeGenerator.Language, out ISchemaCodeGenerator? generator)) + { + return; + } + + HashSet 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, + }); + } + } +}