| | | 1 | | using System.Text; |
| | | 2 | | using System.Text.RegularExpressions; |
| | | 3 | | |
| | | 4 | | namespace AspxLint.Core.Rules; |
| | | 5 | | |
| | | 6 | | public sealed class Attr001MissingQuotes : IRule |
| | | 7 | | { |
| | 209 | 8 | | public string Id => "ATTR-001"; |
| | 30 | 9 | | public string Name => "Attribut sans guillemets"; |
| | 27 | 10 | | public Severity Severity => Severity.Warning; |
| | | 11 | | public string Description => |
| | 25 | 12 | | "Les valeurs d'attributs doivent toujours etre entre guillemets doubles pour respecter XHTML. Sinon les caracter |
| | 46 | 13 | | public bool HasFix => true; |
| | | 14 | | |
| | | 15 | | // attr=value — value est tout sauf espace, guillemet, < > = / (donc une vraie valeur "nue"). |
| | 5 | 16 | | private static readonly Regex AttrUnquoted = new( |
| | 5 | 17 | | @"\s([a-zA-Z][a-zA-Z0-9\-:_]*)=([^\s""'<>=/]+)", |
| | 5 | 18 | | RegexOptions.Compiled); |
| | | 19 | | |
| | 5 | 20 | | private static readonly Regex Comments = new(@"<!--|-->", RegexOptions.Compiled); |
| | 5 | 21 | | private static readonly Regex Directive = new(@"<%@", RegexOptions.Compiled); |
| | | 22 | | |
| | 5 | 23 | | private static readonly Regex AspBlock = new(@"<%[\s\S]*?%>", RegexOptions.Compiled); |
| | 5 | 24 | | private static readonly Regex TagWithAttrs = new( |
| | 5 | 25 | | @"(<[a-zA-Z][a-zA-Z0-9:_\-]*\b)([^>]*)>", |
| | 5 | 26 | | RegexOptions.Compiled); |
| | | 27 | | |
| | | 28 | | public IEnumerable<Issue> Detect(string content, string[] lines, RuleContext ctx) |
| | | 29 | | { |
| | | 30 | | for (int i = 0; i < lines.Length; i++) |
| | | 31 | | { |
| | | 32 | | var line = lines[i]; |
| | | 33 | | if (Comments.IsMatch(line)) continue; |
| | | 34 | | if (Directive.IsMatch(line)) continue; |
| | | 35 | | |
| | | 36 | | foreach (Match m in AttrUnquoted.Matches(line)) |
| | | 37 | | { |
| | | 38 | | // Verifie qu'on est bien a l'interieur d'une balise non encore fermee. |
| | | 39 | | var before = line[..m.Index]; |
| | | 40 | | var lastOpen = before.LastIndexOf('<'); |
| | | 41 | | var lastClose = before.LastIndexOf('>'); |
| | | 42 | | if (lastOpen <= lastClose) continue; |
| | | 43 | | |
| | | 44 | | yield return new Issue(Id, Name, Severity, |
| | | 45 | | i + 1, m.Index + 1, m.Value.Trim(), |
| | | 46 | | $"Entourer la valeur de guillemets : {m.Groups[1].Value}=\"{m.Groups[2].Value}\""); |
| | | 47 | | } |
| | | 48 | | } |
| | | 49 | | } |
| | | 50 | | |
| | | 51 | | public string? Fix(string content, RuleContext ctx) |
| | | 52 | | { |
| | | 53 | | // Decoupe par bloc <% %> pour ne JAMAIS toucher au code serveur. |
| | 23 | 54 | | var sb = new StringBuilder(content.Length + 64); |
| | 23 | 55 | | int last = 0; |
| | 72 | 56 | | foreach (Match m in AspBlock.Matches(content)) |
| | | 57 | | { |
| | 13 | 58 | | sb.Append(FixSegment(content[last..m.Index])); |
| | 13 | 59 | | sb.Append(m.Value); |
| | 13 | 60 | | last = m.Index + m.Length; |
| | | 61 | | } |
| | 23 | 62 | | sb.Append(FixSegment(content[last..])); |
| | 23 | 63 | | return sb.ToString(); |
| | | 64 | | } |
| | | 65 | | |
| | | 66 | | private static string FixSegment(string text) => |
| | 36 | 67 | | TagWithAttrs.Replace(text, m => |
| | 36 | 68 | | m.Groups[1].Value |
| | 36 | 69 | | + AttrUnquoted.Replace(m.Groups[2].Value, |
| | 36 | 70 | | mm => $" {mm.Groups[1].Value}=\"{mm.Groups[2].Value}\"") |
| | 36 | 71 | | + ">"); |
| | | 72 | | } |