< Summary

Information
Class: AspxLint.Core.Rules.Tag003UnbalancedTags
Assembly: AspxLint.Core
File(s): D:\a\claude-aspx-lint\claude-aspx-lint\src\AspxLint.Core\Rules\Tag003UnbalancedTags.cs
Line coverage
94%
Covered lines: 56
Uncovered lines: 3
Coverable lines: 59
Total lines: 113
Line coverage: 94.9%
Branch coverage
95%
Covered branches: 21
Total branches: 22
Branch coverage: 95.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Id()100%11100%
get_Name()100%11100%
get_Severity()100%11100%
get_Description()100%11100%
get_HasFix()100%11100%
.cctor()100%11100%
Detect(...)88.88%181891.17%
Fix(...)100%11100%
LineFromOffset(...)100%44100%

File(s)

D:\a\claude-aspx-lint\claude-aspx-lint\src\AspxLint.Core\Rules\Tag003UnbalancedTags.cs

#LineLine coverage
 1using System.Text.RegularExpressions;
 2
 3namespace AspxLint.Core.Rules;
 4
 5public sealed class Tag003UnbalancedTags : IRule
 6{
 2117    public string Id => "TAG-003";
 328    public string Name => "Balises non equilibrees";
 299    public Severity Severity => Severity.Error;
 10    public string Description =>
 2511        "Les balises ouvertes doivent etre fermees (sauf balises vides). Une pile non vide en fin de fichier signale une
 4812    public bool HasFix => false;
 13
 514    private static readonly HashSet<string> VoidTags = new()
 515    {
 516        "br","hr","img","input","meta","link","area","base","col","embed","source","track","wbr"
 517    };
 18
 519    private static readonly Regex TagRegex = new(
 520        @"<\/?([a-zA-Z][a-zA-Z0-9:_\-]*)\b[^>]*?(\/?)>",
 521        RegexOptions.Compiled);
 22
 23    // On masque code serveur, commentaires, scripts, styles avant tokenisation
 24    // (meme strategie que la version JS).
 525    private static readonly Regex AspBlock = new(@"<%[\s\S]*?%>", RegexOptions.Compiled);
 526    private static readonly Regex HtmlComment = new(@"<!--[\s\S]*?-->", RegexOptions.Compiled);
 527    private static readonly Regex ScriptBlock = new(
 528        @"<script\b[^>]*>[\s\S]*?<\/script>", RegexOptions.IgnoreCase | RegexOptions.Compiled);
 529    private static readonly Regex StyleBlock = new(
 530        @"<style\b[^>]*>[\s\S]*?<\/style>", RegexOptions.IgnoreCase | RegexOptions.Compiled);
 31
 32    public IEnumerable<Issue> Detect(string content, string[] lines, RuleContext ctx)
 33    {
 34        // Pre-calcul : offset de debut de chaque ligne, pour mapper match.Index -> ligne 1-indexee.
 10735        var lineStarts = new int[lines.Length];
 10736        int pos = 0;
 72837        for (int i = 0; i < lines.Length; i++)
 38        {
 25739            lineStarts[i] = pos;
 25740            pos += lines[i].Length + 1; // +1 pour le \n
 41        }
 42
 43        // Masque les zones a ignorer en gardant la longueur (on remplace par des espaces).
 10744        var cleaned = AspBlock.Replace(content, m => new string(' ', m.Length));
 10745        cleaned = HtmlComment.Replace(cleaned, m => new string(' ', m.Length));
 10746        cleaned = ScriptBlock.Replace(cleaned, m => new string(' ', m.Length));
 10747        cleaned = StyleBlock.Replace(cleaned, m => new string(' ', m.Length));
 48
 10749        var issues = new List<Issue>();
 10750        var stack = new Stack<(string tag, int line)>();
 51
 47852        foreach (Match m in TagRegex.Matches(cleaned))
 53        {
 13254            var tag = m.Groups[1].Value.ToLowerInvariant();
 13255            var isClose = m.Value.StartsWith("</");
 13256            var isSelfClose = m.Groups[2].Value == "/" || VoidTags.Contains(tag);
 13257            if (isSelfClose && !isClose) continue;
 58
 10459            if (isClose)
 60            {
 5161                if (stack.Count == 0)
 62                {
 063                    issues.Add(new Issue(Id, Name, Severity,
 064                        LineFromOffset(m.Index, lineStarts), 1, m.Value,
 065                        $"Balise fermante </{tag}> sans ouverture correspondante."));
 66                }
 67                else
 68                {
 5169                    var top = stack.Peek();
 5170                    if (top.tag == tag)
 71                    {
 4772                        stack.Pop();
 73                    }
 74                    else
 75                    {
 476                        issues.Add(new Issue(Id, Name, Severity,
 477                            LineFromOffset(m.Index, lineStarts), 1, m.Value,
 478                            $"Imbrication incorrecte : </{tag}> ferme alors que <{top.tag}> est encore ouvert (ligne {to
 479                        stack.Pop();
 80                    }
 81                }
 82            }
 83            else
 84            {
 5385                stack.Push((tag, LineFromOffset(m.Index, lineStarts)));
 86            }
 87        }
 88
 89        // Ce qui reste dans la pile = jamais ferme.
 21890        foreach (var s in stack)
 91        {
 292            issues.Add(new Issue(Id, Name, Severity,
 293                s.line, 1, $"<{s.tag}>",
 294                $"Balise <{s.tag}> jamais fermee."));
 95        }
 96
 10797        return issues;
 98    }
 99
 4100    public string? Fix(string content, RuleContext ctx) => null;
 101
 102    private static int LineFromOffset(int offset, int[] lineStarts)
 103    {
 114104        int lo = 0, hi = lineStarts.Length - 1;
 160105        while (lo < hi)
 106        {
 103107            int mid = (lo + hi + 1) / 2;
 153108            if (lineStarts[mid] <= offset) lo = mid;
 53109            else hi = mid - 1;
 110        }
 57111        return lo + 1;
 112    }
 113}