From 99488e78b0b6ed52308ee6be68aa20a8a1fc18e8 Mon Sep 17 00:00:00 2001 From: Mads Kristensen Date: Mon, 20 Dec 2021 12:02:21 -0800 Subject: [PATCH] Improved validator [release] --- src/Commands/CommentCommand.cs | 4 +- src/Commands/UncommentCommand.cs | 4 +- src/Constants.cs | 5 +- src/Language/BraceCompletion.cs | 4 +- src/Language/ClassificationTagger.cs | 4 +- src/Language/CompletionCommitManager.cs | 4 +- src/Language/CompletionSource.cs | 13 +++-- src/Language/ErrorTagger.cs | 4 +- src/Language/Language.cs | 7 +-- src/Language/QuickInfoSource.cs | 4 +- src/Language/StructureTagger.cs | 5 +- src/Parser/DocumentParser.cs | 48 ++-------------- src/Parser/DocumentValidator.cs | 73 +++++++++++++++++++++++++ src/Parser/ItemType.cs | 1 + src/PkgdefLanguage.csproj | 1 + src/PkgdefPackage.cs | 10 ++-- test/ParseTest.cs | 14 +++++ 17 files changed, 130 insertions(+), 75 deletions(-) create mode 100644 src/Parser/DocumentValidator.cs diff --git a/src/Commands/CommentCommand.cs b/src/Commands/CommentCommand.cs index 7a9aa37..1f013d8 100644 --- a/src/Commands/CommentCommand.cs +++ b/src/Commands/CommentCommand.cs @@ -12,7 +12,7 @@ namespace PkgdefLanguage { [Export(typeof(ICommandHandler))] [Name(nameof(CommentCommand))] - [ContentType(Language.LanguageName)] + [ContentType(Constants.LanguageName)] [TextViewRole(PredefinedTextViewRoles.PrimaryDocument)] public class CommentCommand : ICommandHandler { @@ -25,7 +25,7 @@ public bool ExecuteCommand(CommentSelectionCommandArgs args, CommandExecutionCon foreach (ITextViewLine line in lines.Reverse()) { - args.TextView.TextBuffer.Insert(line.Start.Position, Constants.CommentChar.ToString()); + args.TextView.TextBuffer.Insert(line.Start.Position, Constants.CommentChars[0]); } return true; diff --git a/src/Commands/UncommentCommand.cs b/src/Commands/UncommentCommand.cs index afeaca4..6ac39f7 100644 --- a/src/Commands/UncommentCommand.cs +++ b/src/Commands/UncommentCommand.cs @@ -12,7 +12,7 @@ namespace PkgdefLanguage { [Export(typeof(ICommandHandler))] [Name(nameof(CommentCommand))] - [ContentType(Language.LanguageName)] + [ContentType(Constants.LanguageName)] [TextViewRole(PredefinedTextViewRoles.PrimaryDocument)] public class UncommentCommand : ICommandHandler { @@ -26,7 +26,7 @@ public bool ExecuteCommand(UncommentSelectionCommandArgs args, CommandExecutionC foreach (ITextViewLine line in lines.Reverse()) { var span = Span.FromBounds(line.Start, line.End); - var text = args.TextView.TextBuffer.CurrentSnapshot.GetText(span).TrimStart(Constants.CommentChar); + var text = args.TextView.TextBuffer.CurrentSnapshot.GetText(span).TrimStart(Constants.CommentChars[0][0]); args.TextView.TextBuffer.Replace(span, text); } diff --git a/src/Constants.cs b/src/Constants.cs index ba96c99..f8c4160 100644 --- a/src/Constants.cs +++ b/src/Constants.cs @@ -2,6 +2,9 @@ { public class Constants { - public const char CommentChar = ';'; + public const string LanguageName = "Pkgdef"; + public static string[] CommentChars = new[] { ";", "//" }; + public const string PkgDefExt = ".pkgdef"; + public const string PkgUndefExt = ".pkgundef"; } } diff --git a/src/Language/BraceCompletion.cs b/src/Language/BraceCompletion.cs index 912bd99..15938ca 100644 --- a/src/Language/BraceCompletion.cs +++ b/src/Language/BraceCompletion.cs @@ -10,8 +10,8 @@ namespace PkgdefLanguage [BracePair('{', '}')] [BracePair('"', '"')] [BracePair('$', '$')] - [ContentType(Language.LanguageName)] - [Name(Language.LanguageName)] + [ContentType(Constants.LanguageName)] + [Name(Constants.LanguageName)] internal sealed class BraceCompletion : BraceCompletionBase { diff --git a/src/Language/ClassificationTagger.cs b/src/Language/ClassificationTagger.cs index 2ca75dd..8fd1f2f 100644 --- a/src/Language/ClassificationTagger.cs +++ b/src/Language/ClassificationTagger.cs @@ -12,8 +12,8 @@ namespace PkgdefLanguage { [Export(typeof(ITaggerProvider))] [TagType(typeof(IClassificationTag))] - [ContentType(Language.LanguageName)] - [Name(Language.LanguageName)] + [ContentType(Constants.LanguageName)] + [Name(Constants.LanguageName)] internal class ClassificationTaggerProvider : ITaggerProvider { [Import] internal IClassificationTypeRegistryService _classificationRegistry = null; diff --git a/src/Language/CompletionCommitManager.cs b/src/Language/CompletionCommitManager.cs index 5395675..4b27571 100644 --- a/src/Language/CompletionCommitManager.cs +++ b/src/Language/CompletionCommitManager.cs @@ -6,8 +6,8 @@ namespace PkgdefLanguage { [Export(typeof(IAsyncCompletionCommitManagerProvider))] - [ContentType(Language.LanguageName)] - [Name(Language.LanguageName)] + [ContentType(Constants.LanguageName)] + [Name(Constants.LanguageName)] internal class CompletionCommitManager : CompletionCommitManagerBase { public override IEnumerable CommitChars => new char[] { ' ', '\'', '"', ',', '.', ';', ':' }; diff --git a/src/Language/CompletionSource.cs b/src/Language/CompletionSource.cs index 754a0fd..ec7d5f4 100644 --- a/src/Language/CompletionSource.cs +++ b/src/Language/CompletionSource.cs @@ -18,8 +18,8 @@ namespace PkgdefLanguage { [Export(typeof(IAsyncCompletionSourceProvider))] - [ContentType(Language.LanguageName)] - [Name(Language.LanguageName)] + [ContentType(Constants.LanguageName)] + [Name(Constants.LanguageName)] internal class CompletionSourceProvider : IAsyncCompletionSourceProvider { [Import] internal ITextStructureNavigatorSelectorService _structureNavigator = null; @@ -84,8 +84,8 @@ public CompletionStartData InitializeCompletion(CompletionTrigger trigger, Snaps // We don't trigger completion when user typed if (char.IsNumber(trigger.Character) // a number || char.IsPunctuation(trigger.Character) // punctuation + || char.IsSymbol(trigger.Character) // punctuation || trigger.Character == '\n' // new line - || trigger.Character == Constants.CommentChar || trigger.Reason == CompletionTriggerReason.Backspace || trigger.Reason == CompletionTriggerReason.Deletion) { @@ -107,9 +107,12 @@ public CompletionStartData InitializeCompletion(CompletionTrigger trigger, Snaps SnapshotSpan tokenSpan = FindTokenSpanAtPosition(triggerLocation); - if (triggerLocation.GetContainingLine().GetText().StartsWith(Constants.CommentChar.ToString(), StringComparison.Ordinal)) + foreach (var commentChar in Constants.CommentChars) { - return CompletionStartData.DoesNotParticipateInCompletion; + if (triggerLocation.GetContainingLine().GetText().StartsWith(commentChar, StringComparison.Ordinal)) + { + return CompletionStartData.DoesNotParticipateInCompletion; + } } return new CompletionStartData(CompletionParticipation.ProvidesItems, tokenSpan); diff --git a/src/Language/ErrorTagger.cs b/src/Language/ErrorTagger.cs index b82613c..2bdb591 100644 --- a/src/Language/ErrorTagger.cs +++ b/src/Language/ErrorTagger.cs @@ -11,8 +11,8 @@ namespace PkgdefLanguage { [Export(typeof(ITaggerProvider))] [TagType(typeof(IErrorTag))] - [ContentType(Language.LanguageName)] - [Name(Language.LanguageName)] + [ContentType(Constants.LanguageName)] + [Name(Constants.LanguageName)] public class RestErrorTaggerProvider : ITaggerProvider { public ITagger CreateTagger(ITextBuffer buffer) where T : ITag => diff --git a/src/Language/Language.cs b/src/Language/Language.cs index 49d42a8..34a9de4 100644 --- a/src/Language/Language.cs +++ b/src/Language/Language.cs @@ -8,17 +8,14 @@ namespace PkgdefLanguage [Guid(PackageGuids.EditorFactoryString)] public class Language : LanguageBase { - public const string LanguageName = "Pkgdef"; - public const string FileExtension = ".pkgdef"; - public Language(object site) : base(site) { ThreadHelper.ThrowIfNotOnUIThread(); } - public override string Name => LanguageName; + public override string Name => Constants.LanguageName; - public override string[] FileExtensions => new[] { FileExtension }; + public override string[] FileExtensions => new[] { Constants.PkgDefExt, Constants.PkgUndefExt }; public override void SetDefaultPreferences(LanguagePreferences preferences) { diff --git a/src/Language/QuickInfoSource.cs b/src/Language/QuickInfoSource.cs index 8d28ba0..2884f6b 100644 --- a/src/Language/QuickInfoSource.cs +++ b/src/Language/QuickInfoSource.cs @@ -8,8 +8,8 @@ namespace PkgdefLanguage { [Export(typeof(IAsyncQuickInfoSourceProvider))] - [ContentType(Language.LanguageName)] - [Name(Language.LanguageName)] + [ContentType(Constants.LanguageName)] + [Name(Constants.LanguageName)] internal sealed class QuickInfoSourceProvider : IAsyncQuickInfoSourceProvider { public IAsyncQuickInfoSource TryCreateQuickInfoSource(ITextBuffer buffer) => diff --git a/src/Language/StructureTagger.cs b/src/Language/StructureTagger.cs index cb60124..c68e7c9 100644 --- a/src/Language/StructureTagger.cs +++ b/src/Language/StructureTagger.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.ComponentModel.Composition; using System.Linq; -using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Adornments; @@ -13,8 +12,8 @@ namespace PkgdefLanguage { [Export(typeof(ITaggerProvider))] [TagType(typeof(IStructureTag))] - [ContentType(Language.LanguageName)] - [Name(Language.LanguageName)] + [ContentType(Constants.LanguageName)] + [Name(Constants.LanguageName)] public class StructureTaggerProvider : ITaggerProvider { public ITagger CreateTagger(ITextBuffer buffer) where T : ITag => diff --git a/src/Parser/DocumentParser.cs b/src/Parser/DocumentParser.cs index 3584d3c..4307769 100644 --- a/src/Parser/DocumentParser.cs +++ b/src/Parser/DocumentParser.cs @@ -1,14 +1,12 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Text.RegularExpressions; -using System.Threading.Tasks; namespace PkgdefLanguage { public partial class Document { - private static readonly Regex _regexProperty = new(@"^(?(@|"".+""))(\s)*(?=)\s*(?.+)", RegexOptions.Compiled); + private static readonly Regex _regexProperty = new(@"^(?.+)(\s)*(?=)\s*(?.+)", RegexOptions.Compiled); private static readonly Regex _regexRef = new(@"(?\$)(?[\w]+)(?\$)", RegexOptions.Compiled); public bool IsParsing { get; private set; } @@ -51,7 +49,7 @@ private IEnumerable ParseLine(int start, string line, List List items = new(); // Comment - if (trimmedLine.StartsWith(Constants.CommentChar.ToString())) + if (trimmedLine.StartsWith(Constants.CommentChars[0]) || trimmedLine.StartsWith(Constants.CommentChars[1])) { items.Add(ToParseItem(line, start, ItemType.Comment, false)); } @@ -65,8 +63,9 @@ private IEnumerable ParseLine(int start, string line, List // Property else if (tokens.Count > 0 && IsMatch(_regexProperty, trimmedLine, out Match matchHeader)) { - items.Add(ToParseItem(matchHeader, start, "name")); - items.Add(ToParseItem(matchHeader, start, "value", true)); + items.Add(ToParseItem(matchHeader, start, "name", false)); + items.Add(ToParseItem(matchHeader, start, "equals", ItemType.Operator, false)); + items.Add(ToParseItem(matchHeader, start, "value")); } // Unknown else if (trimmedLine.Length > 0) @@ -122,43 +121,6 @@ private void AddVariableReferences(ParseItem token) } } - private void ValidateDocument() - { - foreach (ParseItem item in Items) - { - // Unknown symbols - if (item.Type == ItemType.Unknown) - { - item.Errors.Add("Unknown token at this location."); - } - - // Registry key - if (item.Type == ItemType.RegistryKey) - { - var trimmedText = item.Text.Trim(); - - if (!trimmedText.EndsWith("]")) - { - item.Errors.Add("Unclosed registry key entry. Add the missing ] character"); - } - - if (trimmedText.Contains("/") && !trimmedText.Contains("\\/")) - { - item.Errors.Add("Use the backslash character as delimiter instead of forward slash."); - } - } - - // Unknown variables - if (item.Type == ItemType.ReferenceName) - { - if (!CompletionCatalog.Variables.Any(v => v.Key.Equals(item.Text, StringComparison.OrdinalIgnoreCase))) - { - item.Errors.Add($"The variable \"{item.Text}\" doens't exist."); - } - } - } - } - private void OrganizeItems() { List entries = new(); diff --git a/src/Parser/DocumentValidator.cs b/src/Parser/DocumentValidator.cs new file mode 100644 index 0000000..9ae4791 --- /dev/null +++ b/src/Parser/DocumentValidator.cs @@ -0,0 +1,73 @@ +using System; +using System.Linq; + +namespace PkgdefLanguage +{ + public partial class Document + { + private void ValidateDocument() + { + foreach (ParseItem item in Items) + { + // Unknown symbols + if (item.Type == ItemType.Unknown) + { + item.Errors.Add("Unknown token at this location."); + } + + // Registry key + if (item.Type == ItemType.RegistryKey) + { + var trimmedText = item.Text.Trim(); + + if (!trimmedText.EndsWith("]")) + { + item.Errors.Add("Unclosed registry key entry. Add the missing ] character"); + } + + if (trimmedText.Contains("/") && !trimmedText.Contains("\\/")) + { + item.Errors.Add("Use the backslash character as delimiter instead of forward slash."); + } + } + + // Properties + else if (item.Type == ItemType.Operator) + { + ParseItem name = item.Previous; + ParseItem value = item.Next; + + if (name?.Type == ItemType.String) + { + if (name.Text == "\"@\"") + { + name.Errors.Add("To set a registry key's default value, use '@' without quotation marks"); + } + } + else if (name?.Type == ItemType.Literal && name?.Text != "@") + { + name.Errors.Add("Value names must be enclosed in quotation marks."); + } + } + + // Make sure strings are correctly closed with quotation mark + if (item.Type == ItemType.String) + { + if (!item.Text.EndsWith("\"")) + { + item.Errors.Add("Value names must be enclosed in quotation marks."); + } + } + + // Unknown references + foreach (Reference reference in item.References) + { + if (!CompletionCatalog.Variables.Any(v => v.Key.Equals(reference.Value.Text, StringComparison.OrdinalIgnoreCase))) + { + reference.Value.Errors.Add($"The variable \"{item.Text}\" doens't exist."); + } + } + } + } + } +} diff --git a/src/Parser/ItemType.cs b/src/Parser/ItemType.cs index d3dc65c..10d2d2c 100644 --- a/src/Parser/ItemType.cs +++ b/src/Parser/ItemType.cs @@ -8,6 +8,7 @@ public enum ItemType RegistryKey, String, Literal, + Operator, Unknown, } } diff --git a/src/PkgdefLanguage.csproj b/src/PkgdefLanguage.csproj index 9ba79e9..cc14426 100644 --- a/src/PkgdefLanguage.csproj +++ b/src/PkgdefLanguage.csproj @@ -103,6 +103,7 @@ + diff --git a/src/PkgdefPackage.cs b/src/PkgdefPackage.cs index e7d7329..0f2ec9b 100644 --- a/src/PkgdefPackage.cs +++ b/src/PkgdefPackage.cs @@ -15,10 +15,12 @@ namespace PkgdefLanguage [Guid(PackageGuids.PkgdefLanguageString)] [ProvideMenuResource("Menus.ctmenu", 1)] - [ProvideLanguageService(typeof(Language), Language.LanguageName, 0, MatchBraces = true, MatchBracesAtCaret = true, EnableAsyncCompletion = true, EnableCommenting = true, ShowCompletion = true, ShowMatchingBrace = true)] - [ProvideLanguageExtension(typeof(Language), Language.FileExtension)] - [ProvideFileIcon(Language.FileExtension, "KnownMonikers.RegistrationScript")] - [ProvideBraceCompletion(Language.LanguageName)] + [ProvideLanguageService(typeof(Language), Constants.LanguageName, 0, MatchBraces = true, MatchBracesAtCaret = true, EnableAsyncCompletion = true, EnableCommenting = true, ShowCompletion = true, ShowMatchingBrace = true)] + [ProvideLanguageExtension(typeof(Language), Constants.PkgDefExt)] + [ProvideLanguageExtension(typeof(Language), Constants.PkgUndefExt)] + [ProvideFileIcon(Constants.PkgDefExt, "KnownMonikers.RegistrationScript")] + [ProvideFileIcon(Constants.PkgUndefExt, "KnownMonikers.RegistrationScript")] + [ProvideBraceCompletion(Constants.LanguageName)] [ProvideEditorFactory(typeof(Language), 0, CommonPhysicalViewAttributes = (int)__VSPHYSICALVIEWATTRIBUTES.PVA_SupportsPreview, TrustLevel = __VSEDITORTRUSTLEVEL.ETL_AlwaysTrusted)] [ProvideEditorLogicalView(typeof(Language), VSConstants.LOGVIEWID.TextView_string, IsTrusted = true)] public sealed class PkgdefPackage : ToolkitPackage diff --git a/test/ParseTest.cs b/test/ParseTest.cs index df746f5..5c58f12 100644 --- a/test/ParseTest.cs +++ b/test/ParseTest.cs @@ -65,5 +65,19 @@ public async Task Comment() Assert.AreEqual(2, doc.Items.Count); Assert.AreEqual(2, doc.Items.Where(i => i.Type == ItemType.Comment).Count()); } + + [TestMethod] + public async Task InvalidPropertyName() + { + var lines = new[] { "[test]\r\n", + "\"@\"=\"test\"", + }; + + var doc = Document.FromLines(lines); + await doc.WaitForParsingCompleteAsync(); + ParseItem prop = doc.Entries[0].Properties[0].Name; + + Assert.IsFalse(prop.IsValid); + } } }