Добавлены mermaid диаграммы
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
using SQLLinter.Infrastructure.Configuration;
|
using SQLLinter.Infrastructure.Configuration;
|
||||||
|
using SQLLinter.Infrastructure.Diagram;
|
||||||
|
using SQLLinter.Infrastructure.Parser;
|
||||||
using SQLLinter.Infrastructure.Reporters;
|
using SQLLinter.Infrastructure.Reporters;
|
||||||
|
|
||||||
namespace SQLLinter.CLI
|
namespace SQLLinter.CLI
|
||||||
@@ -7,7 +9,7 @@ namespace SQLLinter.CLI
|
|||||||
{
|
{
|
||||||
static void Main(string[] args)
|
static void Main(string[] args)
|
||||||
{
|
{
|
||||||
var rep = new HTMLReporter();
|
var rep = new Reporter();
|
||||||
var con = new Config()
|
var con = new Config()
|
||||||
{
|
{
|
||||||
CompatibilityLevel = 170,
|
CompatibilityLevel = 170,
|
||||||
@@ -43,16 +45,27 @@ namespace SQLLinter.CLI
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var linter = new Linter(con, rep);
|
//var linter = new Linter(con, rep);
|
||||||
|
var fragmentBuilder = new FragmentBuilder(rep, con.CompatibilityLevel);
|
||||||
|
var sqlStreamReaderBuilder = new SqlStreamReaderBuilder();
|
||||||
|
var bpmn = new BpmnDiagram();
|
||||||
|
|
||||||
|
var linter = new Linter(con, rep, fragmentBuilder, sqlStreamReaderBuilder);
|
||||||
|
|
||||||
|
var diagramer = new Diagramer(bpmn, fragmentBuilder, sqlStreamReaderBuilder);
|
||||||
|
|
||||||
using (StreamReader reader = new StreamReader(@"C:\Users\frost\Downloads\Telegram Desktop\tdostdetail.sql"))
|
using (StreamReader reader = new StreamReader(@"C:\Users\frost\Downloads\Telegram Desktop\tdostdetail.sql"))
|
||||||
{
|
{
|
||||||
linter.Run("test.sql", reader.BaseStream);
|
linter.Run("test.sql", reader.BaseStream);
|
||||||
|
diagramer.Run("test.sql", reader.BaseStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
//linter.Run(@"C:\Users\frost\Desktop\DISTR-2599\test.sql");
|
//linter.Run(@"C:\Users\frost\Desktop\DISTR-2599\test.sql");
|
||||||
|
|
||||||
rep.SaveReport(@"C:\Users\frost\Downloads\Telegram Desktop\test.html");
|
var formatter = new HtmlReportFormatter();
|
||||||
|
var content = formatter.Format(rep.Violations, bpmn);
|
||||||
|
|
||||||
|
File.WriteAllText(@"C:\Users\frost\Downloads\Telegram Desktop\test.html", content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
9
SQLLinter/Core/Interfaces/ISqlDiagram.cs
Normal file
9
SQLLinter/Core/Interfaces/ISqlDiagram.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
namespace SQLLinter.Core.Interfaces;
|
||||||
|
|
||||||
|
public interface ISqlDiagramProcessor
|
||||||
|
{
|
||||||
|
void ProcessList(List<string> filePaths);
|
||||||
|
void ProcessList(Dictionary<string, Stream> files);
|
||||||
|
void ProcessPath(string path);
|
||||||
|
}
|
||||||
41
SQLLinter/Diagramer.cs
Normal file
41
SQLLinter/Diagramer.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using SQLLinter.Common.Helpers;
|
||||||
|
using SQLLinter.Core.Interfaces;
|
||||||
|
using SQLLinter.Infrastructure.Diagram;
|
||||||
|
using SQLLinter.Infrastructure.Interfaces;
|
||||||
|
|
||||||
|
namespace SQLLinter;
|
||||||
|
|
||||||
|
public class Diagramer
|
||||||
|
{
|
||||||
|
private ISqlDiagramProcessor _diagramProcessor;
|
||||||
|
|
||||||
|
public Diagramer(BpmnDiagram bpmnDiagram
|
||||||
|
, IFragmentBuilder fragmentBuilder
|
||||||
|
, ISqlStreamReaderBuilder sqlStreamReaderBuilder
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_diagramProcessor = new SqlDiagramProcessor(fragmentBuilder, bpmnDiagram, sqlStreamReaderBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Run(string filePath)
|
||||||
|
{
|
||||||
|
this.Run([filePath]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Run(List<string> filePaths)
|
||||||
|
{
|
||||||
|
List<string> files = FileHelpers.FindFilesWithMask(filePaths);
|
||||||
|
|
||||||
|
_diagramProcessor.ProcessList(filePaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Run(string fileName, Stream fileReader)
|
||||||
|
{
|
||||||
|
Run(new Dictionary<string, Stream> { [fileName] = fileReader });
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Run(Dictionary<string, Stream> files)
|
||||||
|
{
|
||||||
|
_diagramProcessor.ProcessList(files);
|
||||||
|
}
|
||||||
|
}
|
||||||
402
SQLLinter/Infrastructure/Diagram/BpmnBuilder.cs
Normal file
402
SQLLinter/Infrastructure/Diagram/BpmnBuilder.cs
Normal file
@@ -0,0 +1,402 @@
|
|||||||
|
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||||
|
|
||||||
|
namespace SQLLinter.Infrastructure.Diagram;
|
||||||
|
|
||||||
|
public enum BpmnNodeType
|
||||||
|
{
|
||||||
|
Start,
|
||||||
|
End,
|
||||||
|
Task,
|
||||||
|
Gateway,
|
||||||
|
Subprocess,
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BpmnNode
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
public string Label { get; set; } = string.Empty;
|
||||||
|
public BpmnNodeType Type { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BpmnEdge
|
||||||
|
{
|
||||||
|
public string From { get; set; } = string.Empty;
|
||||||
|
public string To { get; set; } = string.Empty;
|
||||||
|
public string Label { get; set; } = string.Empty;
|
||||||
|
public bool Dashed { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BpmnProcess
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public List<BpmnNode> Nodes { get; set; } = new();
|
||||||
|
public List<BpmnEdge> Edges { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BpmnDiagram
|
||||||
|
{
|
||||||
|
public BpmnProcess Main { get; set; } = new BpmnProcess { Id = "Main", Name = "Main" };
|
||||||
|
public List<BpmnProcess> Subprocesses { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class BpmnBuilder
|
||||||
|
{
|
||||||
|
public static BpmnDiagram Build(TSqlFragment fragment)
|
||||||
|
{
|
||||||
|
var visitor = new BpmnVisitor();
|
||||||
|
fragment.Accept(visitor);
|
||||||
|
return visitor.Diagram;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class BpmnVisitor : TSqlFragmentVisitor
|
||||||
|
{
|
||||||
|
public BpmnDiagram Diagram { get; } = new BpmnDiagram();
|
||||||
|
|
||||||
|
private Dictionary<string, string> _lastNodeByProcess = new();
|
||||||
|
private BpmnProcess _currentProcess;
|
||||||
|
private int _nodeCounter = 0;
|
||||||
|
|
||||||
|
public BpmnVisitor()
|
||||||
|
{
|
||||||
|
_currentProcess = Diagram.Main;
|
||||||
|
// create a Start node for main
|
||||||
|
var start = new BpmnNode { Id = NewId(), Label = "Start", Type = BpmnNodeType.Start };
|
||||||
|
Diagram.Main.Nodes.Add(start);
|
||||||
|
_lastNodeByProcess[Diagram.Main.Id] = start.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string NewId() => "n" + (_nodeCounter++);
|
||||||
|
|
||||||
|
private void AddNode(BpmnProcess proc, BpmnNode node)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(node.Id)) node.Id = NewId();
|
||||||
|
proc.Nodes.Add(node);
|
||||||
|
|
||||||
|
if (_lastNodeByProcess.TryGetValue(proc.Id, out var last) && !string.IsNullOrEmpty(last))
|
||||||
|
{
|
||||||
|
proc.Edges.Add(new BpmnEdge { From = last, To = node.Id });
|
||||||
|
}
|
||||||
|
_lastNodeByProcess[proc.Id] = node.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddNodeToCurrent(string label, BpmnNodeType type)
|
||||||
|
{
|
||||||
|
var node = new BpmnNode { Id = NewId(), Label = label, Type = type };
|
||||||
|
AddNode(_currentProcess, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Visit(TSqlScript node)
|
||||||
|
{
|
||||||
|
_currentProcess = Diagram.Main;
|
||||||
|
if (!_lastNodeByProcess.ContainsKey(_currentProcess.Id)) _lastNodeByProcess[_currentProcess.Id] = null;
|
||||||
|
base.Visit(node);
|
||||||
|
|
||||||
|
// add End node for main process
|
||||||
|
var end = new BpmnNode { Id = NewId(), Label = "End", Type = BpmnNodeType.End };
|
||||||
|
AddNode(Diagram.Main, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Visit(CreateProcedureStatement node)
|
||||||
|
{
|
||||||
|
// AST-based extraction of procedure name
|
||||||
|
var name = FindSchemaObjectName(node) ?? ("proc" + Diagram.Subprocesses.Count);
|
||||||
|
var pid = SanitizeId(name);
|
||||||
|
var proc = new BpmnProcess { Id = pid, Name = name };
|
||||||
|
|
||||||
|
// synthetic pool/label node
|
||||||
|
var poolNode = new BpmnNode { Id = pid + "_pool", Label = name, Type = BpmnNodeType.Subprocess };
|
||||||
|
proc.Nodes.Add(poolNode);
|
||||||
|
// create start node for subprocess
|
||||||
|
var start = new BpmnNode { Id = pid + "_start", Label = "Start", Type = BpmnNodeType.Start };
|
||||||
|
proc.Nodes.Add(start);
|
||||||
|
// set last node pointer to start
|
||||||
|
_lastNodeByProcess[proc.Id] = start.Id;
|
||||||
|
|
||||||
|
Diagram.Subprocesses.Add(proc);
|
||||||
|
|
||||||
|
var old = _currentProcess;
|
||||||
|
_currentProcess = proc;
|
||||||
|
if (!_lastNodeByProcess.ContainsKey(proc.Id)) _lastNodeByProcess[proc.Id] = start.Id;
|
||||||
|
|
||||||
|
base.Visit(node);
|
||||||
|
|
||||||
|
// after visiting procedure body add End node
|
||||||
|
var end = new BpmnNode { Id = pid + "_end", Label = "End", Type = BpmnNodeType.End };
|
||||||
|
AddNode(proc, end);
|
||||||
|
|
||||||
|
_currentProcess = old;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Visit(ExecuteStatement node)
|
||||||
|
{
|
||||||
|
// AST-based extraction for called proc
|
||||||
|
var called = FindProcedureNameFromExecute(node);
|
||||||
|
var label = called != null ? $"EXEC {called}" : node.ToString();
|
||||||
|
|
||||||
|
// task in current process
|
||||||
|
var task = new BpmnNode { Id = NewId(), Label = label, Type = BpmnNodeType.Task };
|
||||||
|
AddNode(_currentProcess, task);
|
||||||
|
|
||||||
|
// if calling known subprocess, add dashed edge from this node to subprocess start
|
||||||
|
if (called != null)
|
||||||
|
{
|
||||||
|
var proc = Diagram.Subprocesses.FirstOrDefault(p => string.Equals(p.Name, called, StringComparison.OrdinalIgnoreCase) || string.Equals(p.Id, SanitizeId(called), StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (proc != null)
|
||||||
|
{
|
||||||
|
// add dashed edge in main process (from this task to subprocess pool node)
|
||||||
|
Diagram.Main.Edges.Add(new BpmnEdge { From = task.Id, To = proc.Nodes.First().Id, Dashed = true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
base.Visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Visit(IfStatement node)
|
||||||
|
{
|
||||||
|
var gid = new BpmnNode { Id = NewId(), Label = "IF", Type = BpmnNodeType.Gateway };
|
||||||
|
AddNode(_currentProcess, gid);
|
||||||
|
|
||||||
|
// then / else placeholders
|
||||||
|
if (node.ThenStatement != null)
|
||||||
|
{
|
||||||
|
var thenNode = new BpmnNode { Id = NewId(), Label = "THEN", Type = BpmnNodeType.Task };
|
||||||
|
AddNode(_currentProcess, thenNode);
|
||||||
|
_currentProcess.Edges.Add(new BpmnEdge { From = gid.Id, To = thenNode.Id });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.ElseStatement != null)
|
||||||
|
{
|
||||||
|
var elseNode = new BpmnNode { Id = NewId(), Label = "ELSE", Type = BpmnNodeType.Task };
|
||||||
|
AddNode(_currentProcess, elseNode);
|
||||||
|
_currentProcess.Edges.Add(new BpmnEdge { From = gid.Id, To = elseNode.Id });
|
||||||
|
}
|
||||||
|
|
||||||
|
base.Visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Visit(SelectStatement node)
|
||||||
|
{
|
||||||
|
var label = "SELECT";
|
||||||
|
if (node.QueryExpression is QuerySpecification qs && qs.SelectElements != null)
|
||||||
|
{
|
||||||
|
label = $"SELECT ({qs.SelectElements.Count})";
|
||||||
|
}
|
||||||
|
AddNodeToCurrent(label, BpmnNodeType.Task);
|
||||||
|
base.Visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Visit(InsertStatement node)
|
||||||
|
{
|
||||||
|
var target = FindTargetNameForDml(node);
|
||||||
|
var label = "INSERT" + (string.IsNullOrEmpty(target) ? string.Empty : " -> " + target);
|
||||||
|
AddNodeToCurrent(label, BpmnNodeType.Task);
|
||||||
|
base.Visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Visit(UpdateStatement node)
|
||||||
|
{
|
||||||
|
var target = FindTargetNameForDml(node);
|
||||||
|
var label = "UPDATE" + (string.IsNullOrEmpty(target) ? string.Empty : " -> " + target);
|
||||||
|
AddNodeToCurrent(label, BpmnNodeType.Task);
|
||||||
|
base.Visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Visit(DeleteStatement node)
|
||||||
|
{
|
||||||
|
var target = FindTargetNameForDml(node);
|
||||||
|
var label = "DELETE" + (string.IsNullOrEmpty(target) ? string.Empty : " -> " + target);
|
||||||
|
AddNodeToCurrent(label, BpmnNodeType.Task);
|
||||||
|
base.Visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
// AST helpers
|
||||||
|
private static string? FindSchemaObjectName(TSqlFragment fragment)
|
||||||
|
{
|
||||||
|
var finder = new SchemaNameFinder();
|
||||||
|
fragment.Accept(finder);
|
||||||
|
return finder.FoundName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? FindProcedureNameFromExecute(ExecuteStatement node)
|
||||||
|
{
|
||||||
|
// Prefer ProcedureReference inside the statement
|
||||||
|
var finder = new ProcedureReferenceFinder();
|
||||||
|
node.Accept(finder);
|
||||||
|
return finder.FoundName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? FindTargetNameForDml(TSqlFragment fragment)
|
||||||
|
{
|
||||||
|
var finder = new TargetTableFinder();
|
||||||
|
fragment.Accept(finder);
|
||||||
|
return finder.FoundName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SchemaNameFinder : TSqlFragmentVisitor
|
||||||
|
{
|
||||||
|
public string? FoundName { get; private set; }
|
||||||
|
public override void Visit(SchemaObjectName node)
|
||||||
|
{
|
||||||
|
if (FoundName == null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (node.Identifiers != null && node.Identifiers.Count > 0)
|
||||||
|
{
|
||||||
|
FoundName = string.Join('.', node.Identifiers.Select(id => id.Value));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
FoundName = node.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
FoundName = node.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
base.Visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Visit(ProcedureReferenceName node)
|
||||||
|
{
|
||||||
|
if (FoundName == null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (node.ProcedureReference != null && node.ProcedureReference.Name != null)
|
||||||
|
{
|
||||||
|
// procedure reference name may contain identifiers
|
||||||
|
FoundName = node.ProcedureReference.Name.ToString();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
FoundName = node.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
FoundName = node.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
base.Visit(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ProcedureReferenceFinder : TSqlFragmentVisitor
|
||||||
|
{
|
||||||
|
public string? FoundName { get; private set; }
|
||||||
|
public override void Visit(ProcedureReference node)
|
||||||
|
{
|
||||||
|
if (FoundName == null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (node.Name != null)
|
||||||
|
{
|
||||||
|
if (node.Name.Identifiers != null && node.Name.Identifiers.Count > 0)
|
||||||
|
FoundName = string.Join('.', node.Name.Identifiers.Select(id => id.Value));
|
||||||
|
else
|
||||||
|
FoundName = node.Name.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
FoundName = node.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
base.Visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Visit(ExecuteSpecification node)
|
||||||
|
{
|
||||||
|
base.Visit(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TargetTableFinder : TSqlFragmentVisitor
|
||||||
|
{
|
||||||
|
public string? FoundName { get; private set; }
|
||||||
|
private int _bestDepth = int.MaxValue;
|
||||||
|
private int _depth = 0;
|
||||||
|
|
||||||
|
public override void Visit(TSqlFragment node)
|
||||||
|
{
|
||||||
|
_depth++;
|
||||||
|
base.Visit(node);
|
||||||
|
_depth--;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Visit(NamedTableReference node)
|
||||||
|
{
|
||||||
|
if (node == null) return;
|
||||||
|
int depth = _depth;
|
||||||
|
if (depth < _bestDepth)
|
||||||
|
{
|
||||||
|
_bestDepth = depth;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (node.SchemaObject != null && node.SchemaObject.Identifiers != null && node.SchemaObject.Identifiers.Count > 0)
|
||||||
|
FoundName = string.Join('.', node.SchemaObject.Identifiers.Select(id => id.Value));
|
||||||
|
else
|
||||||
|
FoundName = node.SchemaObject?.ToString() ?? node.ToString();
|
||||||
|
|
||||||
|
if (node.Alias != null && !string.IsNullOrWhiteSpace(node.Alias.Value))
|
||||||
|
FoundName += " AS " + node.Alias.Value;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
FoundName = node.SchemaObject?.ToString() ?? node.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
base.Visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Visit(VariableTableReference node)
|
||||||
|
{
|
||||||
|
if (node == null) return;
|
||||||
|
int depth = _depth;
|
||||||
|
if (depth < _bestDepth)
|
||||||
|
{
|
||||||
|
_bestDepth = depth;
|
||||||
|
FoundName = node.Variable?.ToString();
|
||||||
|
}
|
||||||
|
base.Visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Visit(QueryDerivedTable node)
|
||||||
|
{
|
||||||
|
if (node == null) return;
|
||||||
|
int depth = _depth;
|
||||||
|
if (depth < _bestDepth && node.Alias != null && !string.IsNullOrWhiteSpace(node.Alias.Value))
|
||||||
|
{
|
||||||
|
_bestDepth = depth;
|
||||||
|
FoundName = node.Alias.Value + " (derived)";
|
||||||
|
}
|
||||||
|
|
||||||
|
// still traverse inside
|
||||||
|
base.Visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Visit(CommonTableExpression node)
|
||||||
|
{
|
||||||
|
// CTE definitions: prefer not to pick CTE as target unless no other table found
|
||||||
|
base.Visit(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string SanitizeId(string s)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(s)) return "id";
|
||||||
|
var sb = new System.Text.StringBuilder();
|
||||||
|
foreach (var ch in s)
|
||||||
|
{
|
||||||
|
if (char.IsLetterOrDigit(ch)) sb.Append(ch);
|
||||||
|
else sb.Append('_');
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
51
SQLLinter/Infrastructure/Diagram/FragmentDiagramBuilder.cs
Normal file
51
SQLLinter/Infrastructure/Diagram/FragmentDiagramBuilder.cs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace SQLLinter.Infrastructure.Diagram;
|
||||||
|
|
||||||
|
public static class FragmentDiagramBuilder
|
||||||
|
{
|
||||||
|
public static string RenderMermaid(TSqlFragment fragment)
|
||||||
|
{
|
||||||
|
if (fragment == null) return string.Empty;
|
||||||
|
var diagram = BpmnBuilder.Build(fragment);
|
||||||
|
return MermaidRenderer.RenderMarkdown(diagram);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string RenderHtmlSvg(TSqlFragment fragment)
|
||||||
|
{
|
||||||
|
if (fragment == null) return string.Empty;
|
||||||
|
var diagram = BpmnBuilder.Build(fragment);
|
||||||
|
// Use mermaid HTML instead of SVG fallback
|
||||||
|
return MermaidRenderer.RenderHtml(diagram);
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep helpers used by earlier code if needed
|
||||||
|
private static string Escape(string s)
|
||||||
|
{
|
||||||
|
if (s == null) return string.Empty;
|
||||||
|
return System.Net.WebUtility.HtmlEncode(s).Replace("\n", " ").Replace("\r", " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Truncate(string s, int len)
|
||||||
|
{
|
||||||
|
if (s == null) return string.Empty;
|
||||||
|
if (s.Length <= len) return s;
|
||||||
|
return s.Substring(0, len - 3) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string SanitizeId(string s)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(s)) return "id";
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
foreach (var ch in s)
|
||||||
|
{
|
||||||
|
if (char.IsLetterOrDigit(ch)) sb.Append(ch);
|
||||||
|
else sb.Append('_');
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
147
SQLLinter/Infrastructure/Diagram/MermaidRenderer.cs
Normal file
147
SQLLinter/Infrastructure/Diagram/MermaidRenderer.cs
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace SQLLinter.Infrastructure.Diagram;
|
||||||
|
|
||||||
|
public static class MermaidRenderer
|
||||||
|
{
|
||||||
|
public static string ToMermaidContent(BpmnDiagram diagram)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("flowchart TB");
|
||||||
|
|
||||||
|
// main: print Start, then tasks/gateways, then End
|
||||||
|
var mainStart = diagram.Main.Nodes.Where(n => n.Type == BpmnNodeType.Start).ToList();
|
||||||
|
var mainTasks = diagram.Main.Nodes.Where(n => n.Type == BpmnNodeType.Task || n.Type == BpmnNodeType.Gateway).ToList();
|
||||||
|
var mainEnd = diagram.Main.Nodes.Where(n => n.Type == BpmnNodeType.End).ToList();
|
||||||
|
|
||||||
|
foreach (var node in mainStart)
|
||||||
|
{
|
||||||
|
var nid = SanitizeId(node.Id);
|
||||||
|
var label = Escape(node.Label);
|
||||||
|
sb.AppendLine($" {nid}((\"{label}\"))");
|
||||||
|
}
|
||||||
|
foreach (var node in mainTasks)
|
||||||
|
{
|
||||||
|
var nid = SanitizeId(node.Id);
|
||||||
|
var label = Escape(node.Label);
|
||||||
|
if (node.Type == BpmnNodeType.Gateway)
|
||||||
|
sb.AppendLine($" {nid}{{\"{label}\"}}");
|
||||||
|
else
|
||||||
|
sb.AppendLine($" {nid}[\"{label}\"]");
|
||||||
|
}
|
||||||
|
foreach (var node in mainEnd)
|
||||||
|
{
|
||||||
|
var nid = SanitizeId(node.Id);
|
||||||
|
var label = Escape(node.Label);
|
||||||
|
sb.AppendLine($" {nid}((\"{label}\"))");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
// subprocesses as subgraphs: pool label, start, tasks/gateways, end
|
||||||
|
foreach (var proc in diagram.Subprocesses)
|
||||||
|
{
|
||||||
|
var procId = SanitizeId(proc.Id);
|
||||||
|
sb.AppendLine($" subgraph {procId} [\"{Escape(proc.Name)}\"]");
|
||||||
|
|
||||||
|
var startNodes = proc.Nodes.Where(n => n.Type == BpmnNodeType.Start).ToList();
|
||||||
|
var taskNodes = proc.Nodes.Where(n => n.Type == BpmnNodeType.Task || n.Type == BpmnNodeType.Gateway).ToList();
|
||||||
|
var endNodes = proc.Nodes.Where(n => n.Type == BpmnNodeType.End).ToList();
|
||||||
|
var poolNodes = proc.Nodes.Where(n => n.Type == BpmnNodeType.Subprocess).ToList();
|
||||||
|
|
||||||
|
// pool label nodes (usually first)
|
||||||
|
foreach (var node in poolNodes)
|
||||||
|
{
|
||||||
|
var nid = SanitizeId(node.Id);
|
||||||
|
var label = Escape(node.Label);
|
||||||
|
sb.AppendLine($" {nid}[\"{label}\"]");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var node in startNodes)
|
||||||
|
{
|
||||||
|
var nid = SanitizeId(node.Id);
|
||||||
|
var label = Escape(node.Label);
|
||||||
|
sb.AppendLine($" {nid}((\"{label}\"))");
|
||||||
|
}
|
||||||
|
foreach (var node in taskNodes)
|
||||||
|
{
|
||||||
|
var nid = SanitizeId(node.Id);
|
||||||
|
var label = Escape(node.Label);
|
||||||
|
if (node.Type == BpmnNodeType.Gateway)
|
||||||
|
sb.AppendLine($" {nid}{{\"{label}\"}}");
|
||||||
|
else
|
||||||
|
sb.AppendLine($" {nid}[\"{label}\"]");
|
||||||
|
}
|
||||||
|
foreach (var node in endNodes)
|
||||||
|
{
|
||||||
|
var nid = SanitizeId(node.Id);
|
||||||
|
var label = Escape(node.Label);
|
||||||
|
sb.AppendLine($" {nid}((\"{label}\"))");
|
||||||
|
}
|
||||||
|
|
||||||
|
// edges inside subprocess
|
||||||
|
foreach (var e in proc.Edges)
|
||||||
|
{
|
||||||
|
var from = SanitizeId(e.From);
|
||||||
|
var to = SanitizeId(e.To);
|
||||||
|
var arrow = e.Dashed ? "-.->" : "-->";
|
||||||
|
var lbl = string.IsNullOrWhiteSpace(e.Label) ? string.Empty : $" |{Escape(e.Label)}|";
|
||||||
|
sb.AppendLine($" {from} {arrow} {to}{lbl}");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.AppendLine(" end");
|
||||||
|
sb.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
// main edges
|
||||||
|
foreach (var e in diagram.Main.Edges)
|
||||||
|
{
|
||||||
|
var from = SanitizeId(e.From);
|
||||||
|
var to = SanitizeId(e.To);
|
||||||
|
var arrow = e.Dashed ? "-.->" : "-->";
|
||||||
|
var lbl = string.IsNullOrWhiteSpace(e.Label) ? string.Empty : $" |{Escape(e.Label)}|";
|
||||||
|
sb.AppendLine($" {from} {arrow} {to}{lbl}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string RenderMarkdown(BpmnDiagram diagram)
|
||||||
|
{
|
||||||
|
var content = ToMermaidContent(diagram);
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("```mermaid");
|
||||||
|
sb.Append(content);
|
||||||
|
sb.AppendLine("```");
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string RenderHtml(BpmnDiagram diagram)
|
||||||
|
{
|
||||||
|
var content = ToMermaidContent(diagram);
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
|
// Put raw mermaid text inside .mermaid container so mermaid.js can render it
|
||||||
|
sb.AppendLine("<div class=\"mermaid\">\n" + content + "\n</div>");
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Escape(string s)
|
||||||
|
{
|
||||||
|
if (s == null) return string.Empty;
|
||||||
|
return s.Replace("\"", "\\\"").Replace("\n", " ").Replace("\r", " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string SanitizeId(string s)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(s)) return "id";
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
foreach (var ch in s)
|
||||||
|
{
|
||||||
|
if (char.IsLetterOrDigit(ch)) sb.Append(ch);
|
||||||
|
else sb.Append('_');
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
73
SQLLinter/Infrastructure/Diagram/SqlDiagramProcessor.cs
Normal file
73
SQLLinter/Infrastructure/Diagram/SqlDiagramProcessor.cs
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
using SQLLinter.Core.Interfaces;
|
||||||
|
using SQLLinter.Infrastructure.Interfaces;
|
||||||
|
using SQLLinter.Infrastructure.Parser;
|
||||||
|
|
||||||
|
namespace SQLLinter.Infrastructure.Diagram;
|
||||||
|
|
||||||
|
public class SqlDiagramProcessor : ISqlDiagramProcessor
|
||||||
|
{
|
||||||
|
private readonly BpmnDiagram _bpmnDiagram;
|
||||||
|
private readonly IFragmentBuilder _fragmentBuilder;
|
||||||
|
private readonly ISqlStreamReaderBuilder _sqlStreamReaderBuilder;
|
||||||
|
|
||||||
|
public SqlDiagramProcessor(IFragmentBuilder fragmentBuilder, BpmnDiagram bpmnDiagram)
|
||||||
|
: this(fragmentBuilder, bpmnDiagram, new SqlStreamReaderBuilder()) { }
|
||||||
|
|
||||||
|
public SqlDiagramProcessor(IFragmentBuilder fragmentBuilder, BpmnDiagram bpmnDiagram, ISqlStreamReaderBuilder sqlStreamReaderBuilder)
|
||||||
|
{
|
||||||
|
_fragmentBuilder = fragmentBuilder;
|
||||||
|
_bpmnDiagram = bpmnDiagram;
|
||||||
|
_sqlStreamReaderBuilder = sqlStreamReaderBuilder;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ProcessList(List<string> filePaths)
|
||||||
|
{
|
||||||
|
foreach (var path in filePaths)
|
||||||
|
{
|
||||||
|
ProcessFile(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ProcessPath(string path)
|
||||||
|
{
|
||||||
|
ProcessFile(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProcessFile(string filePath)
|
||||||
|
{
|
||||||
|
var fileStream = GetFileContents(filePath);
|
||||||
|
HandleProcessing(filePath, fileStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ProcessList(Dictionary<string, Stream> files)
|
||||||
|
{
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
HandleProcessing(file.Key, file.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleProcessing(string filePath, Stream fileStream)
|
||||||
|
{
|
||||||
|
var fragment = _fragmentBuilder.GetFragment(filePath, GetSqlTextReader(fileStream), out var errors);
|
||||||
|
|
||||||
|
if (fragment == null || errors.Count > 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var diagramm = BpmnBuilder.Build(fragment);
|
||||||
|
_bpmnDiagram.Subprocesses.Add(diagramm.Main);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Stream GetFileContents(string filePath)
|
||||||
|
{
|
||||||
|
return File.OpenRead(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private StreamReader GetSqlTextReader(Stream sqlFileStream)
|
||||||
|
{
|
||||||
|
return _sqlStreamReaderBuilder.CreateReader(sqlFileStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,20 +8,14 @@ public class SqlFileProcessor : ISqlFileProcessor
|
|||||||
{
|
{
|
||||||
private readonly IRuleVisitor ruleVisitor;
|
private readonly IRuleVisitor ruleVisitor;
|
||||||
|
|
||||||
private readonly IReporter reporter;
|
|
||||||
|
|
||||||
private readonly IPluginHandler pluginHandler;
|
|
||||||
|
|
||||||
private readonly IRuleExceptionFinder ruleExceptionFinder;
|
private readonly IRuleExceptionFinder ruleExceptionFinder;
|
||||||
|
|
||||||
public SqlFileProcessor(
|
public SqlFileProcessor(
|
||||||
IRuleVisitor ruleVisitor,
|
IRuleVisitor ruleVisitor,
|
||||||
IPluginHandler pluginHandler,
|
IPluginHandler pluginHandler
|
||||||
IReporter reporter)
|
)
|
||||||
{
|
{
|
||||||
this.ruleVisitor = ruleVisitor;
|
this.ruleVisitor = ruleVisitor;
|
||||||
this.pluginHandler = pluginHandler;
|
|
||||||
this.reporter = reporter;
|
|
||||||
ruleExceptionFinder = new RuleExceptionFinder(pluginHandler.RuleWithNames);
|
ruleExceptionFinder = new RuleExceptionFinder(pluginHandler.RuleWithNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
namespace SQLLinter.Infrastructure.Reporters;
|
|
||||||
|
|
||||||
public class FileReporter : Reporter
|
|
||||||
{
|
|
||||||
public virtual void SaveReport(string path)
|
|
||||||
{
|
|
||||||
File.WriteAllText(path, GetContent());
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual string GetContent()
|
|
||||||
{
|
|
||||||
return string.Join(Environment.NewLine, this.Violations);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
using SQLLinter.Common;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace SQLLinter.Infrastructure.Reporters;
|
|
||||||
|
|
||||||
public class HTMLReporter : FileReporter
|
|
||||||
{
|
|
||||||
public string GetContent()
|
|
||||||
{
|
|
||||||
var violations = Violations;
|
|
||||||
if (violations.Count == 0)
|
|
||||||
{
|
|
||||||
return "<p><em>Нет нарушений</em></p>";
|
|
||||||
}
|
|
||||||
|
|
||||||
var groupedByFile = violations
|
|
||||||
.GroupBy(v => v.FileName)
|
|
||||||
.OrderBy(g => g.Key);
|
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
sb.AppendLine("<!DOCTYPE html>");
|
|
||||||
sb.AppendLine("<html lang=\"ru\">");
|
|
||||||
sb.AppendLine("<head>");
|
|
||||||
sb.AppendLine("<meta charset=\"UTF-8\">");
|
|
||||||
sb.AppendLine("<title>Отчёт по SQL‑проверкам</title>");
|
|
||||||
sb.AppendLine("<style>");
|
|
||||||
sb.AppendLine("body { font-family: 'Segoe UI', Arial, sans-serif; background-color: #f5f5f5; margin: 0; padding: 0; color: #000; }");
|
|
||||||
sb.AppendLine("h1 { padding: 20px; margin: 0; background-color: #0078d4; color: white; }");
|
|
||||||
sb.AppendLine("h2 { margin-top: 20px; color: #333; }");
|
|
||||||
sb.AppendLine("h3 { margin-top: 15px; color: #555; }");
|
|
||||||
sb.AppendLine("table { border-collapse: collapse; width: 100%; margin: 20px 0; box-shadow: 0 2px 6px rgba(0,0,0,0.1); background-color: white; border-radius: 4px; overflow: hidden; }");
|
|
||||||
sb.AppendLine("th, td { border: 1px solid #e0e0e0; padding: 4px 8px; text-align: left; line-height: 1.2; }");
|
|
||||||
sb.AppendLine("th { background-color: #fafafa; font-weight: 600; }");
|
|
||||||
sb.AppendLine("td.line, td.column { width: 60px; text-align: center; }");
|
|
||||||
sb.AppendLine("td.rule { width: 300px; text-align: left; }");
|
|
||||||
sb.AppendLine("td.index { width: 40px; text-align: center; }");
|
|
||||||
sb.AppendLine(".critical { border-left: 4px solid #d13438; padding: 10px; margin-bottom: 20px; background-color: #fde7e9; }");
|
|
||||||
sb.AppendLine(".warning { border-left: 4px solid #ffaa44; padding: 10px; margin-bottom: 20px; background-color: #fff4ce; }");
|
|
||||||
sb.AppendLine(".info { border-left: 4px solid #0078d4; padding: 10px; margin-bottom: 20px; background-color: #deecf9; }");
|
|
||||||
sb.AppendLine(".tabs { position: fixed; bottom: 0; left: 0; right: 0; background-color: #ffffff; border-top: 1px solid #ccc; padding: 10px; display: flex; overflow-x: auto; scrollbar-width: thin; justify-content: flex-start; box-shadow: 0 -2px 6px rgba(0,0,0,0.1); }");
|
|
||||||
sb.AppendLine(".tab { margin-right: 10px; padding: 8px 16px; border-radius: 4px; background-color: #f3f2f1; cursor: pointer; transition: background-color 0.2s; }");
|
|
||||||
sb.AppendLine(".tab:hover { background-color: #e1dfdd; }");
|
|
||||||
sb.AppendLine(".tab.active { background-color: #0078d4; color: white; }");
|
|
||||||
sb.AppendLine(".file-report { display: none; padding: 20px 0; }");
|
|
||||||
sb.AppendLine(".file-report.active { display: block; }");
|
|
||||||
|
|
||||||
// Тёмная тема
|
|
||||||
sb.AppendLine("@media (prefers-color-scheme: dark) {");
|
|
||||||
sb.AppendLine(" body { background-color: #1e1e1e; color: #ddd; }");
|
|
||||||
sb.AppendLine(" h1 { background-color: #005a9e; }");
|
|
||||||
sb.AppendLine(" h2, h3 { color: #ddd; }");
|
|
||||||
sb.AppendLine(" table { background-color: #2d2d2d; box-shadow: none; }");
|
|
||||||
sb.AppendLine(" th { background-color: #3c3c3c; color: #fff; }");
|
|
||||||
sb.AppendLine(" td { border: 1px solid #444; }");
|
|
||||||
sb.AppendLine(" .tabs { background-color: #2d2d2d; border-top: 1px solid #444; }");
|
|
||||||
sb.AppendLine(" .tab { background-color: #3c3c3c; color: #ddd; }");
|
|
||||||
sb.AppendLine(" .tab:hover { background-color: #555; }");
|
|
||||||
sb.AppendLine(" .tab.active { background-color: #0078d4; color: white; }");
|
|
||||||
sb.AppendLine(" .critical { background-color: #4d1f1f; border-left-color: #d13438; }");
|
|
||||||
sb.AppendLine(" .warning { background-color: #4d3b1f; border-left-color: #ffaa44; }");
|
|
||||||
sb.AppendLine(" .info { background-color: #1f3b4d; border-left-color: #0078d4; }");
|
|
||||||
sb.AppendLine("}");
|
|
||||||
sb.AppendLine("</style>");
|
|
||||||
sb.AppendLine("</head>");
|
|
||||||
sb.AppendLine("<body>");
|
|
||||||
//sb.AppendLine("<h1>Отчёт по SQL‑проверкам</h1>");
|
|
||||||
|
|
||||||
int fileIndex = 0;
|
|
||||||
foreach (var fileGroup in groupedByFile)
|
|
||||||
{
|
|
||||||
string divId = $"file_{fileIndex}";
|
|
||||||
sb.AppendLine($"<div id=\"{divId}\" class=\"file-report{(fileIndex == 0 ? " active" : "")}\">");
|
|
||||||
sb.AppendLine($"<h1>Файл: {fileGroup.Key}</h1>");
|
|
||||||
|
|
||||||
var groupedBySeverity = fileGroup
|
|
||||||
.GroupBy(v => v.Severity)
|
|
||||||
.OrderByDescending(g => g.Key);
|
|
||||||
|
|
||||||
foreach (var severityGroup in groupedBySeverity)
|
|
||||||
{
|
|
||||||
string severityClass = severityGroup.Key switch
|
|
||||||
{
|
|
||||||
RuleViolationSeverity.Critical => "critical",
|
|
||||||
RuleViolationSeverity.Warning => "warning",
|
|
||||||
RuleViolationSeverity.Info => "info",
|
|
||||||
_ => ""
|
|
||||||
};
|
|
||||||
|
|
||||||
sb.AppendLine($"<div class=\"{severityClass}\">");
|
|
||||||
sb.AppendLine($"<h3>{severityGroup.Key}</h3>");
|
|
||||||
sb.AppendLine("<table>");
|
|
||||||
sb.AppendLine("<thead>");
|
|
||||||
sb.AppendLine("<tr><th>#</th><th>Строка</th><th>Колонка</th><th>Правило</th><th>Описание</th></tr>");
|
|
||||||
sb.AppendLine("</thead>");
|
|
||||||
sb.AppendLine("<tbody>");
|
|
||||||
|
|
||||||
int rowIndex = 1;
|
|
||||||
foreach (var v in severityGroup
|
|
||||||
.OrderBy(x => x.Line)
|
|
||||||
.ThenBy(x => x.Column))
|
|
||||||
{
|
|
||||||
sb.AppendLine($"<tr><td class=\"index\">{rowIndex}</td><td class=\"line\">{v.Line}</td><td class=\"column\">{v.Column}</td><td class=\"rule\">{v.RuleName}</td><td>{v.Text}</td></tr>");
|
|
||||||
rowIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.AppendLine("</tbody>");
|
|
||||||
sb.AppendLine("</table>");
|
|
||||||
sb.AppendLine("</div>");
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.AppendLine("</div>");
|
|
||||||
fileIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Табы снизу
|
|
||||||
sb.AppendLine("<div class=\"tabs\">");
|
|
||||||
fileIndex = 0;
|
|
||||||
foreach (var fileGroup in groupedByFile)
|
|
||||||
{
|
|
||||||
sb.AppendLine($"<div class=\"tab{(fileIndex == 0 ? " active" : "")}\" onclick=\"showReport('file_{fileIndex}', this)\">{fileGroup.Key}</div>");
|
|
||||||
fileIndex++;
|
|
||||||
}
|
|
||||||
sb.AppendLine("</div>");
|
|
||||||
|
|
||||||
// JS для переключения
|
|
||||||
sb.AppendLine("<script>");
|
|
||||||
sb.AppendLine("function showReport(id, tab) {");
|
|
||||||
sb.AppendLine(" document.querySelectorAll('.file-report').forEach(el => el.classList.remove('active'));");
|
|
||||||
sb.AppendLine(" document.getElementById(id).classList.add('active');");
|
|
||||||
sb.AppendLine(" document.querySelectorAll('.tab').forEach(el => el.classList.remove('active'));");
|
|
||||||
sb.AppendLine(" tab.classList.add('active');");
|
|
||||||
sb.AppendLine(" window.scrollTo({ top: 0, behavior: 'smooth' });");
|
|
||||||
sb.AppendLine("}");
|
|
||||||
sb.AppendLine("document.querySelector('.tabs').addEventListener('wheel', function(e) {");
|
|
||||||
sb.AppendLine(" e.preventDefault();");
|
|
||||||
sb.AppendLine(" this.scrollLeft += e.deltaY;");
|
|
||||||
sb.AppendLine("});");
|
|
||||||
sb.AppendLine("</script>");
|
|
||||||
|
|
||||||
sb.AppendLine("</body>");
|
|
||||||
sb.AppendLine("</html>");
|
|
||||||
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
242
SQLLinter/Infrastructure/Reporters/HtmlReportFormatter.cs
Normal file
242
SQLLinter/Infrastructure/Reporters/HtmlReportFormatter.cs
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
using SQLLinter.Common;
|
||||||
|
using SQLLinter.Infrastructure.Diagram;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace SQLLinter.Infrastructure.Reporters;
|
||||||
|
|
||||||
|
public class HtmlReportFormatter : IReportFormatter
|
||||||
|
{
|
||||||
|
public string Format(List<IRuleViolation> violations)
|
||||||
|
{
|
||||||
|
if (violations.Count == 0)
|
||||||
|
{
|
||||||
|
return "<p><em>Нет нарушений</em></p>";
|
||||||
|
}
|
||||||
|
|
||||||
|
var groupedByFile = violations
|
||||||
|
.GroupBy(v => v.FileName)
|
||||||
|
.OrderBy(g => g.Key);
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
GenerateBeginningHtml(sb);
|
||||||
|
|
||||||
|
int fileIndex = 0;
|
||||||
|
foreach (var fileGroup in groupedByFile)
|
||||||
|
{
|
||||||
|
string divId = $"file_{fileIndex}";
|
||||||
|
sb.AppendLine($"<div id=\"{divId}\" class=\"file-report{(fileIndex == 0 ? " active" : "")}\">");
|
||||||
|
sb.AppendLine($"<h1>Файл: {fileGroup.Key}</h1>");
|
||||||
|
|
||||||
|
var groupedBySeverity = fileGroup
|
||||||
|
.GroupBy(v => v.Severity)
|
||||||
|
.OrderByDescending(g => g.Key);
|
||||||
|
|
||||||
|
foreach (var severityGroup in groupedBySeverity)
|
||||||
|
{
|
||||||
|
string severityClass = severityGroup.Key switch
|
||||||
|
{
|
||||||
|
RuleViolationSeverity.Critical => "critical",
|
||||||
|
RuleViolationSeverity.Warning => "warning",
|
||||||
|
RuleViolationSeverity.Info => "info",
|
||||||
|
_ => ""
|
||||||
|
};
|
||||||
|
|
||||||
|
sb.AppendLine($"<div class=\"{severityClass}\">");
|
||||||
|
sb.AppendLine($"<h3>{severityGroup.Key}</h3>");
|
||||||
|
sb.AppendLine("<table>");
|
||||||
|
sb.AppendLine("<thead>");
|
||||||
|
sb.AppendLine("<tr><th>#</th><th>Строка</th><th>Колонка</th><th>Правило</th><th>Описание</th></tr>");
|
||||||
|
sb.AppendLine("</thead>");
|
||||||
|
sb.AppendLine("<tbody>");
|
||||||
|
|
||||||
|
int rowIndex = 1;
|
||||||
|
foreach (var v in severityGroup
|
||||||
|
.OrderBy(x => x.Line)
|
||||||
|
.ThenBy(x => x.Column))
|
||||||
|
{
|
||||||
|
sb.AppendLine($"<tr><td class=\"index\">{rowIndex}</td><td class=\"line\">{v.Line}</td><td class=\"column\">{v.Column}</td><td class=\"rule\">{v.RuleName}</td><td>{v.Text}</td></tr>");
|
||||||
|
rowIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.AppendLine("</tbody>");
|
||||||
|
sb.AppendLine("</table>");
|
||||||
|
sb.AppendLine("</div>");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.AppendLine("</div>");
|
||||||
|
fileIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Табы снизу
|
||||||
|
sb.AppendLine("<div class=\"tabs\">");
|
||||||
|
fileIndex = 0;
|
||||||
|
foreach (var fileGroup in groupedByFile)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"<div class=\"tab{(fileIndex == 0 ? " active" : "")}\" onclick=\"showReport('file_{fileIndex}', this)\">{fileGroup.Key}</div>");
|
||||||
|
fileIndex++;
|
||||||
|
}
|
||||||
|
sb.AppendLine("</div>");
|
||||||
|
|
||||||
|
GenerateEndingHtml(sb);
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Format(List<IRuleViolation> violations, BpmnDiagram diagram)
|
||||||
|
{
|
||||||
|
if (violations.Count == 0)
|
||||||
|
{
|
||||||
|
return "<p><em>Нет нарушений</em></p>";
|
||||||
|
}
|
||||||
|
|
||||||
|
var groupedByFile = violations
|
||||||
|
.GroupBy(v => v.FileName)
|
||||||
|
.OrderBy(g => g.Key);
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
GenerateBeginningHtml(sb);
|
||||||
|
|
||||||
|
int fileIndex = 0;
|
||||||
|
foreach (var fileGroup in groupedByFile)
|
||||||
|
{
|
||||||
|
string divId = $"file_{fileIndex}";
|
||||||
|
sb.AppendLine($"<div id=\"{divId}\" class=\"file-report{(fileIndex == 0 ? " active" : "")}\">");
|
||||||
|
sb.AppendLine($"<h1>Файл: {fileGroup.Key}</h1>");
|
||||||
|
|
||||||
|
var groupedBySeverity = fileGroup
|
||||||
|
.GroupBy(v => v.Severity)
|
||||||
|
.OrderByDescending(g => g.Key);
|
||||||
|
|
||||||
|
foreach (var severityGroup in groupedBySeverity)
|
||||||
|
{
|
||||||
|
string severityClass = severityGroup.Key switch
|
||||||
|
{
|
||||||
|
RuleViolationSeverity.Critical => "critical",
|
||||||
|
RuleViolationSeverity.Warning => "warning",
|
||||||
|
RuleViolationSeverity.Info => "info",
|
||||||
|
_ => ""
|
||||||
|
};
|
||||||
|
|
||||||
|
sb.AppendLine($"<div class=\"{severityClass}\">");
|
||||||
|
sb.AppendLine($"<h3>{severityGroup.Key}</h3>");
|
||||||
|
sb.AppendLine("<table>");
|
||||||
|
sb.AppendLine("<thead>");
|
||||||
|
sb.AppendLine("<tr><th>#</th><th>Строка</th><th>Колонка</th><th>Правило</th><th>Описание</th></tr>");
|
||||||
|
sb.AppendLine("</thead>");
|
||||||
|
sb.AppendLine("<tbody>");
|
||||||
|
|
||||||
|
int rowIndex = 1;
|
||||||
|
foreach (var v in severityGroup
|
||||||
|
.OrderBy(x => x.Line)
|
||||||
|
.ThenBy(x => x.Column))
|
||||||
|
{
|
||||||
|
sb.AppendLine($"<tr><td class=\"index\">{rowIndex}</td><td class=\"line\">{v.Line}</td><td class=\"column\">{v.Column}</td><td class=\"rule\">{v.RuleName}</td><td>{v.Text}</td></tr>");
|
||||||
|
rowIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.AppendLine("</tbody>");
|
||||||
|
sb.AppendLine("</table>");
|
||||||
|
sb.AppendLine("</div>");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.AppendLine("</div>");
|
||||||
|
fileIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
//mermaid диаграмма
|
||||||
|
sb.AppendLine($"<div id=\"mermaid\" class=\"file-report{(fileIndex == 0 ? " active" : "")}\">");
|
||||||
|
sb.AppendLine($"<h1>Диаграмма</h1>");
|
||||||
|
|
||||||
|
var mermaid = MermaidRenderer.RenderHtml(diagram);
|
||||||
|
sb.AppendLine(mermaid);
|
||||||
|
sb.AppendLine("</div>");
|
||||||
|
|
||||||
|
// Табы снизу
|
||||||
|
sb.AppendLine("<div class=\"tabs\">");
|
||||||
|
fileIndex = 0;
|
||||||
|
foreach (var fileGroup in groupedByFile)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"<div class=\"tab{(fileIndex == 0 ? " active" : "")}\" onclick=\"showReport('file_{fileIndex}', this)\">{fileGroup.Key}</div>");
|
||||||
|
fileIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.AppendLine($"<div class=\"tab{(fileIndex == 0 ? " active" : "")}\" onclick=\"showReport('mermaid', this)\">Диграмма</div>");
|
||||||
|
sb.AppendLine("</div>");
|
||||||
|
|
||||||
|
GenerateEndingHtml(sb);
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GenerateBeginningHtml(StringBuilder sb)
|
||||||
|
{
|
||||||
|
sb.AppendLine("<!DOCTYPE html>");
|
||||||
|
sb.AppendLine("<html lang=\"ru\">");
|
||||||
|
sb.AppendLine("<head>");
|
||||||
|
sb.AppendLine("<meta charset=\"UTF-8\">");
|
||||||
|
sb.AppendLine("<title>Отчёт по SQL‑проверкам</title>");
|
||||||
|
sb.AppendLine("<style>");
|
||||||
|
sb.AppendLine("body { font-family: 'Segoe UI', Arial, sans-serif; background-color: #f5f5f5; margin: 0; padding: 0; color: #000; }");
|
||||||
|
sb.AppendLine("h1 { padding: 20px; margin: 0; background-color: #0078d4; color: white; }");
|
||||||
|
sb.AppendLine("h2 { margin-top: 20px; color: #333; }");
|
||||||
|
sb.AppendLine("h3 { margin-top: 15px; color: #555; }");
|
||||||
|
sb.AppendLine("table { border-collapse: collapse; width: 100%; margin: 20px 0; box-shadow: 0 2px 6px rgba(0,0,0,0.1); background-color: white; border-radius: 4px; overflow: hidden; }");
|
||||||
|
sb.AppendLine("th, td { border: 1px solid #e0e0e0; padding: 4px 8px; text-align: left; line-height: 1.2; }");
|
||||||
|
sb.AppendLine("th { background-color: #fafafa; font-weight: 600; }");
|
||||||
|
sb.AppendLine("td.line, td.column { width: 60px; text-align: center; }");
|
||||||
|
sb.AppendLine("td.rule { width: 300px; text-align: left; }");
|
||||||
|
sb.AppendLine("td.index { width: 40px; text-align: center; }");
|
||||||
|
sb.AppendLine(".critical { border-left: 4px solid #d13438; padding: 10px; margin-bottom: 20px; background-color: #fde7e9; }");
|
||||||
|
sb.AppendLine(".warning { border-left: 4px solid #ffaa44; padding: 10px; margin-bottom: 20px; background-color: #fff4ce; }");
|
||||||
|
sb.AppendLine(".info { border-left: 4px solid #0078d4; padding: 10px; margin-bottom: 20px; background-color: #deecf9; }");
|
||||||
|
sb.AppendLine(".tabs { position: fixed; bottom: 0; left: 0; right: 0; background-color: #ffffff; border-top: 1px solid #ccc; padding: 10px; display: flex; overflow-x: auto; scrollbar-width: thin; justify-content: flex-start; box-shadow: 0 -2px 6px rgba(0,0,0,0.1); }");
|
||||||
|
sb.AppendLine(".tab { margin-right: 10px; padding: 8px 16px; border-radius: 4px; background-color: #f3f2f1; cursor: pointer; transition: background-color 0.2s; }");
|
||||||
|
sb.AppendLine(".tab:hover { background-color: #e1dfdd; }");
|
||||||
|
sb.AppendLine(".tab.active { background-color: #0078d4; color: white; }");
|
||||||
|
sb.AppendLine(".file-report { display: none; padding: 20px 0; }");
|
||||||
|
sb.AppendLine(".file-report.active { display: block; }");
|
||||||
|
|
||||||
|
// Тёмная тема
|
||||||
|
sb.AppendLine("@media (prefers-color-scheme: dark) {");
|
||||||
|
sb.AppendLine(" body { background-color: #1e1e1e; color: #ddd; }");
|
||||||
|
sb.AppendLine(" h1 { background-color: #005a9e; }");
|
||||||
|
sb.AppendLine(" h2, h3 { color: #ddd; }");
|
||||||
|
sb.AppendLine(" table { background-color: #2d2d2d; box-shadow: none; }");
|
||||||
|
sb.AppendLine(" th { background-color: #3c3c3c; color: #fff; }");
|
||||||
|
sb.AppendLine(" td { border: 1px solid #444; }");
|
||||||
|
sb.AppendLine(" .tabs { background-color: #2d2d2d; border-top: 1px solid #444; }");
|
||||||
|
sb.AppendLine(" .tab { background-color: #3c3c3c; color: #ddd; }");
|
||||||
|
sb.AppendLine(" .tab:hover { background-color: #555; }");
|
||||||
|
sb.AppendLine(" .tab.active { background-color: #0078d4; color: white; }");
|
||||||
|
sb.AppendLine(" .critical { background-color: #4d1f1f; border-left-color: #d13438; }");
|
||||||
|
sb.AppendLine(" .warning { background-color: #4d3b1f; border-left-color: #ffaa44; }");
|
||||||
|
sb.AppendLine(" .info { background-color: #1f3b4d; border-left-color: #0078d4; }");
|
||||||
|
sb.AppendLine("}");
|
||||||
|
sb.AppendLine("</style>");
|
||||||
|
sb.AppendLine("<script src=\"https://unpkg.com/mermaid@10/dist/mermaid.min.js\"></script>");
|
||||||
|
sb.AppendLine("<script>mermaid.initialize({ startOnLoad: true, securityLevel: 'loose' });</script>");
|
||||||
|
sb.AppendLine("</head>");
|
||||||
|
sb.AppendLine("<body>");
|
||||||
|
//sb.AppendLine("<h1>Отчёт по SQL‑проверкам</h1>");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GenerateEndingHtml(StringBuilder sb)
|
||||||
|
{
|
||||||
|
|
||||||
|
// JS для переключения
|
||||||
|
sb.AppendLine("<script>");
|
||||||
|
sb.AppendLine("function showReport(id, tab) {");
|
||||||
|
sb.AppendLine(" document.querySelectorAll('.file-report').forEach(el => el.classList.remove('active'));");
|
||||||
|
sb.AppendLine(" document.getElementById(id).classList.add('active');");
|
||||||
|
sb.AppendLine(" document.querySelectorAll('.tab').forEach(el => el.classList.remove('active'));");
|
||||||
|
sb.AppendLine(" tab.classList.add('active');");
|
||||||
|
sb.AppendLine(" window.scrollTo({ top: 0, behavior: 'smooth' });");
|
||||||
|
sb.AppendLine("}");
|
||||||
|
sb.AppendLine("document.querySelector('.tabs').addEventListener('wheel', function(e) {");
|
||||||
|
sb.AppendLine(" e.preventDefault();");
|
||||||
|
sb.AppendLine(" this.scrollLeft += e.deltaY;");
|
||||||
|
sb.AppendLine("});");
|
||||||
|
sb.AppendLine("</script>");
|
||||||
|
|
||||||
|
sb.AppendLine("</body>");
|
||||||
|
sb.AppendLine("</html>");
|
||||||
|
}
|
||||||
|
}
|
||||||
10
SQLLinter/Infrastructure/Reporters/IReportFormatter.cs
Normal file
10
SQLLinter/Infrastructure/Reporters/IReportFormatter.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using SQLLinter.Common;
|
||||||
|
using SQLLinter.Infrastructure.Diagram;
|
||||||
|
|
||||||
|
namespace SQLLinter.Infrastructure.Reporters;
|
||||||
|
|
||||||
|
public interface IReportFormatter
|
||||||
|
{
|
||||||
|
string Format(List<IRuleViolation> violations);
|
||||||
|
string Format(List<IRuleViolation> violations, BpmnDiagram diagram);
|
||||||
|
}
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace SQLLinter.Infrastructure.Reporters;
|
|
||||||
|
|
||||||
public class MarkdownFileReporter : FileReporter
|
|
||||||
{
|
|
||||||
public override string GetContent()
|
|
||||||
{
|
|
||||||
var violations = Violations;
|
|
||||||
if (violations.Count == 0)
|
|
||||||
{
|
|
||||||
return "_Нет нарушений_";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Группировка по файлу
|
|
||||||
var groupedByFile = violations
|
|
||||||
.GroupBy(v => v.FileName)
|
|
||||||
.OrderBy(g => g.Key); // сортировка файлов по имени
|
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
|
|
||||||
foreach (var fileGroup in groupedByFile)
|
|
||||||
{
|
|
||||||
sb.AppendLine($"## Файл: {fileGroup.Key}");
|
|
||||||
sb.AppendLine();
|
|
||||||
|
|
||||||
// Группировка по severity внутри файла
|
|
||||||
var groupedBySeverity = fileGroup
|
|
||||||
.GroupBy(v => v.Severity)
|
|
||||||
.OrderByDescending(g => g.Key); // сначала Error, потом Warning, потом Info
|
|
||||||
|
|
||||||
foreach (var severityGroup in groupedBySeverity)
|
|
||||||
{
|
|
||||||
sb.AppendLine($"### {severityGroup.Key}");
|
|
||||||
sb.AppendLine();
|
|
||||||
sb.AppendLine("| Строка | Колонка | Правило | Описание |");
|
|
||||||
sb.AppendLine("|--------|---------|---------|----------|");
|
|
||||||
|
|
||||||
foreach (var v in severityGroup
|
|
||||||
.OrderBy(x => x.Line)
|
|
||||||
.ThenBy(x => x.Column))
|
|
||||||
{
|
|
||||||
sb.AppendLine($"| {v.Line} | {v.Column} | {v.RuleName} | {v.Text} |");
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.AppendLine();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.ToString();
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
using SQLLinter.Common;
|
||||||
|
using SQLLinter.Infrastructure.Diagram;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace SQLLinter.Infrastructure.Reporters;
|
||||||
|
|
||||||
|
public class MarkdownReportFormatter : IReportFormatter
|
||||||
|
{
|
||||||
|
public string Format(List<IRuleViolation> violations)
|
||||||
|
{
|
||||||
|
if (violations.Count == 0)
|
||||||
|
{
|
||||||
|
return "_Нет нарушений_";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Группировка по файлу
|
||||||
|
var groupedByFile = violations
|
||||||
|
.GroupBy(v => v.FileName)
|
||||||
|
.OrderBy(g => g.Key); // сортировка файлов по имени
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
|
foreach (var fileGroup in groupedByFile)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"## Файл: {fileGroup.Key}");
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
// Группировка по severity внутри файла
|
||||||
|
var groupedBySeverity = fileGroup
|
||||||
|
.GroupBy(v => v.Severity)
|
||||||
|
.OrderByDescending(g => g.Key); // сначала Error, потом Warning, потом Info
|
||||||
|
|
||||||
|
foreach (var severityGroup in groupedBySeverity)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"### {severityGroup.Key}");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("| Строка | Колонка | Правило | Описание |");
|
||||||
|
sb.AppendLine("|--------|---------|---------|----------|");
|
||||||
|
|
||||||
|
foreach (var v in severityGroup
|
||||||
|
.OrderBy(x => x.Line)
|
||||||
|
.ThenBy(x => x.Column))
|
||||||
|
{
|
||||||
|
sb.AppendLine($"| {v.Line} | {v.Column} | {v.RuleName} | {v.Text} |");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.AppendLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Format(List<IRuleViolation> violations, BpmnDiagram diagram)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using SQLLinter.Common;
|
using SQLLinter.Common;
|
||||||
using SQLLinter.Common.Helpers;
|
using SQLLinter.Common.Helpers;
|
||||||
using SQLLinter.Core.Interfaces;
|
using SQLLinter.Core.Interfaces;
|
||||||
|
using SQLLinter.Infrastructure.Interfaces;
|
||||||
using SQLLinter.Infrastructure.Parser;
|
using SQLLinter.Infrastructure.Parser;
|
||||||
using SQLLinter.Infrastructure.Plugins;
|
using SQLLinter.Infrastructure.Plugins;
|
||||||
|
|
||||||
@@ -15,20 +16,25 @@ public class Linter
|
|||||||
private ISqlFileProcessor _fileProcessor;
|
private ISqlFileProcessor _fileProcessor;
|
||||||
|
|
||||||
public Linter(IConfig config, IReporter reporter)
|
public Linter(IConfig config, IReporter reporter)
|
||||||
|
: this(config
|
||||||
|
, reporter
|
||||||
|
, new FragmentBuilder(reporter, config.CompatibilityLevel)
|
||||||
|
, new SqlStreamReaderBuilder()
|
||||||
|
)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public Linter(IConfig config, IReporter reporter, IFragmentBuilder fragmentBuilder, ISqlStreamReaderBuilder sqlStreamReaderBuilder)
|
||||||
{
|
{
|
||||||
this._config = config;
|
this._config = config;
|
||||||
this._reporter = reporter;
|
this._reporter = reporter;
|
||||||
|
|
||||||
this._pluginHandler = new PluginHandler(reporter, config);
|
|
||||||
|
|
||||||
_reporter.Report($"Загрузка SQL Linter...");
|
_reporter.Report($"Загрузка SQL Linter...");
|
||||||
|
|
||||||
var fragmentBuilder = new FragmentBuilder(reporter, _config.CompatibilityLevel);
|
|
||||||
_pluginHandler = new PluginHandler(_reporter, _config);
|
_pluginHandler = new PluginHandler(_reporter, _config);
|
||||||
|
|
||||||
var ruleVisitor = new SqlRuleVisitor(_pluginHandler, fragmentBuilder, _reporter);
|
var ruleVisitor = new SqlRuleVisitor(_pluginHandler, fragmentBuilder, _reporter, sqlStreamReaderBuilder);
|
||||||
|
|
||||||
_fileProcessor = new SqlFileProcessor(ruleVisitor, _pluginHandler, reporter);
|
_fileProcessor = new SqlFileProcessor(ruleVisitor, _pluginHandler);
|
||||||
|
|
||||||
_reporter.Report($"SQL Linter загружен...");
|
_reporter.Report($"SQL Linter загружен...");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user