diff --git a/src/Compilers/CSharp/Portable/Parser/Lexer.cs b/src/Compilers/CSharp/Portable/Parser/Lexer.cs index 719961f828e30..df472c8733279 100644 --- a/src/Compilers/CSharp/Portable/Parser/Lexer.cs +++ b/src/Compilers/CSharp/Portable/Parser/Lexer.cs @@ -475,12 +475,7 @@ private void ScanSyntaxToken(ref TokenInfo info) // dots followed by an integer (which will be treated as a range expression). // // Move back one space to see what's before this dot and adjust accordingly. - - this.TextWindow.Reset(atDotPosition - 1); - var priorCharacterIsDot = this.TextWindow.PeekChar() is '.'; - this.TextWindow.Reset(atDotPosition); - - if (priorCharacterIsDot) + if (this.TextWindow.PreviousChar() is '.') { // We have two dots in a row. Treat the second dot as a dot, not the start of a number literal. TextWindow.AdvanceChar(); diff --git a/src/Compilers/CSharp/Portable/Parser/SlidingTextWindow.cs b/src/Compilers/CSharp/Portable/Parser/SlidingTextWindow.cs index e06f6c8536842..6e39642976469 100644 --- a/src/Compilers/CSharp/Portable/Parser/SlidingTextWindow.cs +++ b/src/Compilers/CSharp/Portable/Parser/SlidingTextWindow.cs @@ -3,12 +3,8 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Concurrent; using System.Diagnostics; using System.Text; -using Microsoft.CodeAnalysis.Collections; -using Microsoft.CodeAnalysis.CSharp.Symbols; -using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; @@ -374,6 +370,25 @@ public char PeekChar(int delta) return ch; } + public char PreviousChar() + { + Debug.Assert(this.Position > 0); + if (_offset > 0) + { + // The allowed region of the window that can be read is from 0 to _characterWindowCount (which _offset + // is in between). So as long as _offset is greater than 0, we can read the previous character directly + // from the current chunk of characters in the window. + return this.CharacterWindow[_offset - 1]; + } + + // The prior character isn't in the window (trying to read the current character caused us to + // read in the next chunk of text into the window, throwing out the preceding characters). + // Just go back to the source text to find this character. While more expensive, this should + // be rare given that most of the time we won't be calling this right after loading a new text + // chunk. + return this.Text[this.Position - 1]; + } + /// /// If the next characters in the window match the given string, /// then advance past those characters. Otherwise, do nothing. @@ -480,5 +495,16 @@ internal static char GetCharsFromUtf32(uint codepoint, out char lowSurrogate) return (char)((codepoint - 0x00010000) / 0x0400 + 0xD800); } } + + internal TestAccessor GetTestAccessor() + => new TestAccessor(this); + + internal readonly struct TestAccessor(SlidingTextWindow window) + { + private readonly SlidingTextWindow _window = window; + + internal void SetDefaultCharacterWindow() + => _window._characterWindow = new char[DefaultWindowLength]; + } } } diff --git a/src/Compilers/CSharp/Test/Syntax/LexicalAndXml/LexicalTests.cs b/src/Compilers/CSharp/Test/Syntax/LexicalAndXml/LexicalTests.cs index b17f2475f3594..a2360d1bf4c0d 100644 --- a/src/Compilers/CSharp/Test/Syntax/LexicalAndXml/LexicalTests.cs +++ b/src/Compilers/CSharp/Test/Syntax/LexicalAndXml/LexicalTests.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Threading; using Microsoft.CodeAnalysis.CSharp.Syntax.InternalSyntax; using Microsoft.CodeAnalysis.CSharp.Test.Utilities; using Microsoft.CodeAnalysis.Text; @@ -4572,5 +4573,90 @@ disabled text 2 Assert.True(trivia.ContainsDiagnostics); Assert.Equal((int)ErrorCode.ERR_Merge_conflict_marker_encountered, trivia.Errors().Single().Code); } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/78593")] + public void TestDotPrefixedNumberStartingAtStartOfSlidingTextWindow() + { + // This test depends on the line endings for the file being \r\n to ensure the right contents lines up at + // the right locations. + // + // It specifically validates what happens when we see `.0` at the start of the + // sliding text window, where the lexer tries to peek back one char to see if this + // is actually `..0` (a range expr) or `.0` (a floating point number). + var code = Resources.DotPrefixedNumberStartingAtStartOfSlidingTextWindow; + if (!code.Contains("\r\n")) + code = code.Replace("\n", "\r\n"); + + var sourceText = SourceText.From(code); + + { + // Run a full parse, and validate the tree returned). + + using var lexer = new Lexer(sourceText, CSharpParseOptions.Default); + + // Ensure we have a normal window size, not some larger array that another test created and cached in + // the window pool + lexer.TextWindow.GetTestAccessor().SetDefaultCharacterWindow(); + + using var parser = new LanguageParser(lexer, oldTree: null, changes: null); + + Microsoft.CodeAnalysis.SyntaxTreeExtensions.VerifySource( + sourceText, parser.ParseCompilationUnit().CreateRed()); + } + + { + // Now, replicate the same conditions that hte parser runs through by driving the a new lexer here + // directly. That ensures that we are actually validating exactly the conditions that led to the bug + // (a dot token starting a number, right at the start of the character window). + var lexer = new Lexer(sourceText, CSharpParseOptions.Default); + + // Ensure we have a normal window size, not some larger array that another test created and cached in + // the window pool + lexer.TextWindow.GetTestAccessor().SetDefaultCharacterWindow(); + + var mode = LexerMode.Syntax; + for (var i = 0; i < 1326; i++) + lexer.Lex(ref mode); + + // Lexer will read from index 0 in the arrray. + Assert.Equal(0, lexer.TextWindow.Offset); + + // We have 205 real chars in the window + Assert.Equal(205, lexer.TextWindow.CharacterWindowCount); + + // The lexer is at position 10199 in the file. + Assert.Equal(10199, lexer.TextWindow.Position); + + /// The 205 characters represent the final part of the doc + Assert.Equal(lexer.TextWindow.Text.Length, lexer.TextWindow.Position + lexer.TextWindow.CharacterWindowCount); + + // We're at the start of a token. + Assert.Equal(lexer.TextWindow.LexemeStartPosition, lexer.TextWindow.Position); + + // Ensure that the lexer's window is starting with the next FP number (".03") right at + // the start of the window. + Assert.True(lexer.TextWindow.CharacterWindow is ['.', '0', '3', ',', ..], $"Start of window was '{new string(lexer.TextWindow.CharacterWindow, 0, 4)}'"); + + var token = lexer.Lex(ref mode); + Assert.Equal(SyntaxKind.NumericLiteralToken, token.Kind); + Assert.Equal(3, token.FullWidth); + Assert.Equal(".03", token.ToString()); + + // But we moved 3 characters forward. + Assert.Equal(3, lexer.TextWindow.Offset); + + // We still have 205 real chars in the window + Assert.Equal(205, lexer.TextWindow.CharacterWindowCount); + + // The lexer position has moved 3 characters forward as well. + Assert.Equal(10202, lexer.TextWindow.Position); + + // We're at the start of a token. + Assert.Equal(lexer.TextWindow.LexemeStartPosition, lexer.TextWindow.Position); + + // Character window didn't changee. + Assert.True(lexer.TextWindow.CharacterWindow is ['.', '0', '3', ',', ..], $"Start of window was '{new string(lexer.TextWindow.CharacterWindow, 0, 4)}'"); + } + } } } diff --git a/src/Compilers/CSharp/Test/Syntax/Resources.resx b/src/Compilers/CSharp/Test/Syntax/Resources.resx index 2004f7e760271..37ec16cc49bde 100644 --- a/src/Compilers/CSharp/Test/Syntax/Resources.resx +++ b/src/Compilers/CSharp/Test/Syntax/Resources.resx @@ -119,6 +119,253 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + // ------------------------------ +// THIS FILE IS AUTO GENERATED +// PLEASE DO NOT MODIFY +// ------------------------------ +// Hash: D5-B1-B5-D5-B6-27-C1-44-E0-20-77-14-52-A7-7A-E3-26-1C-6C-74-82-E1-2D-00-C3-73-22-7D-F5-CF-1B-76-95-45-88-4A-9A-0D-E3-D1-8F-69-F3-C3-76-41-92-67-F1-9D-64-EF-86-77-39-48-8C-0E-20-44-51-CA-5D-65 + +using Goobar.Combat; +using Goobar.Commons.Rules; +using Goobar.Localization; +using Goobar.Shared.Entities.Units; +using static System.Collections.Immutable.ImmutableArray; +using System; +using System.Collections.Immutable; +using Goobar.Shared.Utils; +using System.Collections.Generic; +using Goobar.GeneratedCode; + +namespace Goobar; + + +[GenerateAllValues(typeof(Goobar.Commons.Rules.UnitEquipmentAttributeDefinition))] +public static partial class EquipmentAttributes +{ + + public static class Minor_Power_Flat + { + public static readonly UnitEquipmentAttributeDefinition Instance = new( + Id: "Minor_Power_Flat", + Attribute: CombatAttribute.Power, + ModifierType: AttributeModifierType.Flat, + PrimaryStatsByLevel: null, + SecondaryStatByRarityFrom: Create(30.0, 60.0, 110.0, 180.0, 280.0), + SecondaryStatByRarityTo: Create(40.0, 80.0, 140.0, 220.0, 320.0), + CalibrationStatsByLevel: null, + UnitEquipmentSubType: UnitEquipmentSubType.Implant); + } + + public static class Minor_Power_Perc + { + public static readonly UnitEquipmentAttributeDefinition Instance = new( + Id: "Minor_Power_Perc", + Attribute: CombatAttribute.Power, + ModifierType: AttributeModifierType.Percentage, + PrimaryStatsByLevel: null, + SecondaryStatByRarityFrom: Create(.02, .04, .06, .09, .13), + SecondaryStatByRarityTo: Create(.03, .05, .08, .12, .16), + CalibrationStatsByLevel: null, + UnitEquipmentSubType: UnitEquipmentSubType.Implant); + } + + public static class Minor_CritChance_Flat + { + public static readonly UnitEquipmentAttributeDefinition Instance = new( + Id: "Minor_CritChance_Flat", + Attribute: CombatAttribute.CritChance, + ModifierType: AttributeModifierType.Flat, + PrimaryStatsByLevel: null, + SecondaryStatByRarityFrom: Create(.02, .04, .06, .09, .13), + SecondaryStatByRarityTo: Create(.03, .05, .08, .12, .16), + CalibrationStatsByLevel: null, + UnitEquipmentSubType: UnitEquipmentSubType.Implant); + } + + public static class Minor_CritBoost_Flat + { + public static readonly UnitEquipmentAttributeDefinition Instance = new( + Id: "Minor_CritBoost_Flat", + Attribute: CombatAttribute.CritBoost, + ModifierType: AttributeModifierType.Flat, + PrimaryStatsByLevel: null, + SecondaryStatByRarityFrom: Create(.02, .04, .06, .09, .13), + SecondaryStatByRarityTo: Create(.03, .05, .08, .12, .16), + CalibrationStatsByLevel: null, + UnitEquipmentSubType: UnitEquipmentSubType.Implant); + } + + public static class Minor_Defense_Flat + { + public static readonly UnitEquipmentAttributeDefinition Instance = new( + Id: "Minor_Defense_Flat", + Attribute: CombatAttribute.Defense, + ModifierType: AttributeModifierType.Flat, + PrimaryStatsByLevel: null, + SecondaryStatByRarityFrom: Create(40.0, 100.0, 170.0, 280.0, 380.0), + SecondaryStatByRarityTo: Create(60.0, 120.0, 210.0, 330.0, 480.0), + CalibrationStatsByLevel: null, + UnitEquipmentSubType: UnitEquipmentSubType.Implant); + } + + public static class Minor_Defense_Perc + { + public static readonly UnitEquipmentAttributeDefinition Instance = new( + Id: "Minor_Defense_Perc", + Attribute: CombatAttribute.Defense, + ModifierType: AttributeModifierType.Percentage, + PrimaryStatsByLevel: null, + SecondaryStatByRarityFrom: Create(.02, .04, .06, .09, .13), + SecondaryStatByRarityTo: Create(.03, .05, .08, .12, .16), + CalibrationStatsByLevel: null, + UnitEquipmentSubType: UnitEquipmentSubType.Implant); + } + + public static class Minor_HullPoints_Flat + { + public static readonly UnitEquipmentAttributeDefinition Instance = new( + Id: "Minor_HullPoints_Flat", + Attribute: CombatAttribute.HullPoints, + ModifierType: AttributeModifierType.Flat, + PrimaryStatsByLevel: null, + SecondaryStatByRarityFrom: Create(170.0, 390.0, 700.0, 1000.0, 1400.0), + SecondaryStatByRarityTo: Create(230.0, 450.0, 780.0, 1200.0, 1800.0), + CalibrationStatsByLevel: null, + UnitEquipmentSubType: UnitEquipmentSubType.Implant); + } + + public static class Minor_HullPoints_Perc + { + public static readonly UnitEquipmentAttributeDefinition Instance = new( + Id: "Minor_HullPoints_Perc", + Attribute: CombatAttribute.HullPoints, + ModifierType: AttributeModifierType.Percentage, + PrimaryStatsByLevel: null, + SecondaryStatByRarityFrom: Create(.02, .04, .06, .09, .13), + SecondaryStatByRarityTo: Create(.03, .05, .08, .12, .16), + CalibrationStatsByLevel: null, + UnitEquipmentSubType: UnitEquipmentSubType.Implant); + } + + public static class Minor_Manipulation_Flat + { + public static readonly UnitEquipmentAttributeDefinition Instance = new( + Id: "Minor_Manipulation_Flat", + Attribute: CombatAttribute.Manipulation, + ModifierType: AttributeModifierType.Flat, + PrimaryStatsByLevel: null, + SecondaryStatByRarityFrom: Create(2.0, 4.0, 6.0, 8.0, 10.0), + SecondaryStatByRarityTo: Create(3.0, 5.0, 7.0, 9.0, 12.0), + CalibrationStatsByLevel: null, + UnitEquipmentSubType: UnitEquipmentSubType.Implant); + } + + public static class Minor_Security_Flat + { + public static readonly UnitEquipmentAttributeDefinition Instance = new( + Id: "Minor_Security_Flat", + Attribute: CombatAttribute.Security, + ModifierType: AttributeModifierType.Flat, + PrimaryStatsByLevel: null, + SecondaryStatByRarityFrom: Create(2.0, 4.0, 6.0, 8.0, 10.0), + SecondaryStatByRarityTo: Create(3.0, 5.0, 7.0, 9.0, 12.0), + CalibrationStatsByLevel: null, + UnitEquipmentSubType: UnitEquipmentSubType.Implant); + } + + public static class Minor_Initiative_Flat + { + public static readonly UnitEquipmentAttributeDefinition Instance = new( + Id: "Minor_Initiative_Flat", + Attribute: CombatAttribute.Initiative, + ModifierType: AttributeModifierType.Flat, + PrimaryStatsByLevel: null, + SecondaryStatByRarityFrom: Create(2.0, 4.0, 6.0, 8.0, 10.0), + SecondaryStatByRarityTo: Create(3.0, 5.0, 7.0, 9.0, 12.0), + CalibrationStatsByLevel: null, + UnitEquipmentSubType: UnitEquipmentSubType.Implant); + } + + public static class Major_Power_Flat + { + public static readonly UnitEquipmentAttributeDefinition Instance = new( + Id: "Major_Power_Flat", + Attribute: CombatAttribute.Power, + ModifierType: AttributeModifierType.Flat, + PrimaryStatsByLevel: null, + SecondaryStatByRarityFrom: Create(40.0, 70.0, 130.0, 220.0, 340.0), + SecondaryStatByRarityTo: Create(50.0, 100.0, 170.0, 270.0, 400.0), + CalibrationStatsByLevel: null, + UnitEquipmentSubType: UnitEquipmentSubType.Implant); + } + + public static class Major_Power_Perc + { + public static readonly UnitEquipmentAttributeDefinition Instance = new( + Id: "Major_Power_Perc", + Attribute: CombatAttribute.Power, + ModifierType: AttributeModifierType.Percentage, + PrimaryStatsByLevel: null, + SecondaryStatByRarityFrom: Create(.03, .05, .07, .1, .16), + SecondaryStatByRarityTo: Create(.04, .06, .09, .15, .2), + CalibrationStatsByLevel: null, + UnitEquipmentSubType: UnitEquipmentSubType.Implant); + } + + public static class Major_CritChance_Flat + { + public static readonly UnitEquipmentAttributeDefinition Instance = new( + Id: "Major_CritChance_Flat", + Attribute: CombatAttribute.CritChance, + ModifierType: AttributeModifierType.Flat, + PrimaryStatsByLevel: null, + SecondaryStatByRarityFrom: Create(.03, .05, .07, .1, .16), + SecondaryStatByRarityTo: Create(.04, .06, .09, .15, .2), + CalibrationStatsByLevel: null, + UnitEquipmentSubType: UnitEquipmentSubType.Implant); + } + + public static class Major_CritBoost_Flat + { + public static readonly UnitEquipmentAttributeDefinition Instance = new( + Id: "Major_CritBoost_Flat", + Attribute: CombatAttribute.CritBoost, + ModifierType: AttributeModifierType.Flat, + PrimaryStatsByLevel: null, + SecondaryStatByRarityFrom: Create(.03, .05, .07, .1, .16), + SecondaryStatByRarityTo: Create(.04, .06, .09, .15, .2), + CalibrationStatsByLevel: null, + UnitEquipmentSubType: UnitEquipmentSubType.Implant); + } + + public static class Major_Defense_Flat + { + public static readonly UnitEquipmentAttributeDefinition Instance = new( + Id: "Major_Defense_Flat", + Attribute: CombatAttribute.Defense, + ModifierType: AttributeModifierType.Flat, + PrimaryStatsByLevel: null, + SecondaryStatByRarityFrom: Create(50.0, 120.0, 200.0, 340.0, 460.0), + SecondaryStatByRarityTo: Create(70.0, 140.0, 250.0, 400.0, 580.0), + CalibrationStatsByLevel: null, + UnitEquipmentSubType: UnitEquipmentSubType.Implant); + } + + public static class Major_Defense_Perc + { + public static readonly UnitEquipmentAttributeDefinition Instance = new( + Id: "Major_Defense_Perc", + Attribute: CombatAttribute.Defense, + ModifierType: AttributeModifierType.Percentage, + PrimaryStatsByLevel: null, + SecondaryStatByRarityFrom1: Create(.03, .05, .07, .1, .16), + SecondaryStatByRarityTo: Create(.04, .06, .09, .15, .2), + CalibrationStatsByLevel: null, + UnitEquipmentSubType: UnitEquipmentSubType.Implant); + } +} + extern alias libAlias=other_library.dll; class myClass @@ -18522,4 +18769,4 @@ static int Main() int x = 0; #if A == A\ - + \ No newline at end of file diff --git a/src/Compilers/Core/Portable/Syntax/SyntaxTreeExtensions.cs b/src/Compilers/Core/Portable/Syntax/SyntaxTreeExtensions.cs index 39dc59103b1ab..7422b9c1882ca 100644 --- a/src/Compilers/Core/Portable/Syntax/SyntaxTreeExtensions.cs +++ b/src/Compilers/Core/Portable/Syntax/SyntaxTreeExtensions.cs @@ -19,8 +19,13 @@ internal static class SyntaxTreeExtensions [Conditional("DEBUG")] internal static void VerifySource(this SyntaxTree tree, IEnumerable? changes = null) { - var root = tree.GetRoot(); - var text = tree.GetText(); + VerifySource(tree.GetText(), tree.GetRoot(), changes); + } + + /// + [Conditional("DEBUG")] + internal static void VerifySource(SourceText text, SyntaxNode root, IEnumerable? changes = null) + { var fullSpan = new TextSpan(0, text.Length); SyntaxNode? node = null;